Sync current Hyprland widgets
This commit is contained in:
435
config/hypr/ags/widget-panel.tsx
Normal file
435
config/hypr/ags/widget-panel.tsx
Normal file
@@ -0,0 +1,435 @@
|
||||
import app from "ags/gtk4/app";
|
||||
import { Astal, Gtk } from "ags/gtk4";
|
||||
import { readFile } from "ags/file";
|
||||
import { execAsync } from "ags/process";
|
||||
import { createRoot } from "gnim";
|
||||
import css from "./widget-panel.css";
|
||||
import GLib from "gi://GLib";
|
||||
|
||||
const HYPR_DIR = GLib.getenv("HYPR_DIR") || `${GLib.get_home_dir()}/.config/hypr`;
|
||||
const THEME_DIR = `${HYPR_DIR}/Themes`;
|
||||
const CURRENT_WALLPAPER = `${HYPR_DIR}/current-wallpaper`;
|
||||
const REFRESH_SECONDS = 2;
|
||||
const WEATHER_SECONDS = 20 * 60;
|
||||
const HISTORY_LIMIT = 22;
|
||||
const ESC_KEYVAL = 65307;
|
||||
const START_HIDDEN = GLib.getenv("WIDGET_PANEL_START_HIDDEN") === "1";
|
||||
|
||||
type UiTheme = {
|
||||
accent: string;
|
||||
accent2: string;
|
||||
background: string;
|
||||
backgroundSoft: string;
|
||||
foreground: string;
|
||||
muted: string;
|
||||
panelHex: string;
|
||||
};
|
||||
|
||||
type SystemSnapshot = {
|
||||
cpu: number;
|
||||
memory: number;
|
||||
disk: number;
|
||||
temp: string;
|
||||
uptime: string;
|
||||
};
|
||||
|
||||
let lastCpuTotal = 0;
|
||||
let lastCpuIdle = 0;
|
||||
let system: SystemSnapshot = {
|
||||
cpu: 0,
|
||||
memory: 0,
|
||||
disk: 0,
|
||||
temp: "n/a",
|
||||
uptime: "n/a",
|
||||
};
|
||||
let cpuHistory: number[] = [];
|
||||
let weather = "Wetter wird geladen...";
|
||||
let weatherUpdated = 0;
|
||||
let timerStarted = false;
|
||||
let disposeRebuild: (() => void) | null = null;
|
||||
let panelWindow: Gtk.Window | null = null;
|
||||
|
||||
function readText(path: string) {
|
||||
try {
|
||||
return readFile(path);
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function listFiles(dir: string, predicate: (path: string, name: string) => boolean) {
|
||||
try {
|
||||
const directory = GLib.Dir.open(dir, 0);
|
||||
const files: string[] = [];
|
||||
let name = directory.read_name();
|
||||
|
||||
while (name !== null) {
|
||||
const path = `${dir}/${name}`;
|
||||
if (predicate(path, name)) {
|
||||
files.push(path);
|
||||
}
|
||||
name = directory.read_name();
|
||||
}
|
||||
|
||||
return files.sort((a, b) => a.localeCompare(b));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function shellValue(contents: string, key: string) {
|
||||
const regex = new RegExp(`^${key}=(["']?)(.*?)\\1$`, "m");
|
||||
return contents.match(regex)?.[2] || "";
|
||||
}
|
||||
|
||||
function currentWallpaper() {
|
||||
return readText(CURRENT_WALLPAPER).trim();
|
||||
}
|
||||
|
||||
function activeTheme(): UiTheme {
|
||||
const fallback = {
|
||||
accent: "#f38ba8",
|
||||
accent2: "#cba6f7",
|
||||
background: "rgba(24, 20, 31, 0.96)",
|
||||
backgroundSoft: "rgba(49, 50, 68, 0.82)",
|
||||
foreground: "#f5e0dc",
|
||||
muted: "#cdd6f4",
|
||||
panelHex: "#313244",
|
||||
};
|
||||
|
||||
const activeWallpaper = currentWallpaper();
|
||||
const themeFile = listFiles(THEME_DIR, (_path, name) => name.endsWith(".theme"))
|
||||
.find(path => shellValue(readText(path), "WALLPAPER") === activeWallpaper);
|
||||
|
||||
if (!themeFile) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const contents = readText(themeFile);
|
||||
return {
|
||||
accent: shellValue(contents, "ACCENT") || fallback.accent,
|
||||
accent2: shellValue(contents, "ACCENT_2") || fallback.accent2,
|
||||
background: shellValue(contents, "BACKGROUND") || fallback.background,
|
||||
backgroundSoft: shellValue(contents, "BACKGROUND_SOFT") || fallback.backgroundSoft,
|
||||
foreground: shellValue(contents, "FOREGROUND") || fallback.foreground,
|
||||
muted: shellValue(contents, "MUTED") || fallback.muted,
|
||||
panelHex: shellValue(contents, "PANEL_HEX") || fallback.panelHex,
|
||||
};
|
||||
}
|
||||
|
||||
function themeCss(theme: UiTheme) {
|
||||
return [
|
||||
`@define-color ags_accent ${theme.accent};`,
|
||||
`@define-color ags_accent_2 ${theme.accent2};`,
|
||||
`@define-color ags_bg ${theme.background};`,
|
||||
`@define-color ags_bg_soft ${theme.backgroundSoft};`,
|
||||
`@define-color ags_fg ${theme.foreground};`,
|
||||
`@define-color ags_muted ${theme.muted};`,
|
||||
`@define-color ags_panel ${theme.panelHex};`,
|
||||
css,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function clamp(value: number, min = 0, max = 100) {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
function percentLabel(value: number) {
|
||||
return `${Math.round(clamp(value))}%`;
|
||||
}
|
||||
|
||||
function updateSystem() {
|
||||
const cpuLine = readText("/proc/stat").split("\n")[0] || "";
|
||||
const cpuValues = cpuLine.trim().split(/\s+/).slice(1).map(value => Number(value) || 0);
|
||||
const idle = (cpuValues[3] || 0) + (cpuValues[4] || 0);
|
||||
const total = cpuValues.reduce((sum, value) => sum + value, 0);
|
||||
const totalDelta = total - lastCpuTotal;
|
||||
const idleDelta = idle - lastCpuIdle;
|
||||
|
||||
if (lastCpuTotal > 0 && totalDelta > 0) {
|
||||
system.cpu = clamp(((totalDelta - idleDelta) / totalDelta) * 100);
|
||||
}
|
||||
|
||||
lastCpuTotal = total;
|
||||
lastCpuIdle = idle;
|
||||
|
||||
const meminfo = readText("/proc/meminfo");
|
||||
const memTotal = Number(meminfo.match(/^MemTotal:\s+(\d+)/m)?.[1] || 0);
|
||||
const memAvailable = Number(meminfo.match(/^MemAvailable:\s+(\d+)/m)?.[1] || 0);
|
||||
if (memTotal > 0) {
|
||||
system.memory = clamp(((memTotal - memAvailable) / memTotal) * 100);
|
||||
}
|
||||
|
||||
const uptimeSeconds = Number(readText("/proc/uptime").split(" ")[0] || 0);
|
||||
const hours = Math.floor(uptimeSeconds / 3600);
|
||||
const minutes = Math.floor((uptimeSeconds % 3600) / 60);
|
||||
system.uptime = `${hours}h ${minutes}m`;
|
||||
|
||||
const temps = listFiles("/sys/class/thermal", (_path, name) => name.startsWith("thermal_zone"))
|
||||
.map(path => Number(readText(`${path}/temp`).trim()) / 1000)
|
||||
.filter(value => Number.isFinite(value) && value > 0);
|
||||
system.temp = temps.length ? `${Math.round(Math.max(...temps))} C` : "n/a";
|
||||
|
||||
execAsync(["bash", "-lc", "df -P / | awk 'NR==2 {gsub(/%/, \"\", $5); print $5}'"])
|
||||
.then(output => {
|
||||
system.disk = clamp(Number(output.trim()) || system.disk);
|
||||
})
|
||||
.catch(console.error);
|
||||
|
||||
cpuHistory = [...cpuHistory, system.cpu].slice(-HISTORY_LIMIT);
|
||||
}
|
||||
|
||||
function updateWeather(force = false) {
|
||||
if (!force && Date.now() - weatherUpdated < WEATHER_SECONDS * 1000) {
|
||||
return;
|
||||
}
|
||||
|
||||
weatherUpdated = Date.now();
|
||||
execAsync(["bash", "-lc", "curl -fsS --max-time 5 'https://wttr.in/?format=%l:+%c+%t+%w' 2>/dev/null || true"])
|
||||
.then(output => {
|
||||
weather = output.trim() || "Wetter nicht erreichbar";
|
||||
})
|
||||
.catch(() => {
|
||||
weather = "Wetter nicht erreichbar";
|
||||
});
|
||||
}
|
||||
|
||||
function sparkline(values: number[]) {
|
||||
const blocks = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
|
||||
if (!values.length) {
|
||||
return "·".repeat(HISTORY_LIMIT);
|
||||
}
|
||||
|
||||
return values
|
||||
.map(value => blocks[Math.round((clamp(value) / 100) * (blocks.length - 1))])
|
||||
.join("");
|
||||
}
|
||||
|
||||
function monthDays() {
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = now.getMonth();
|
||||
const first = new Date(year, month, 1);
|
||||
const last = new Date(year, month + 1, 0);
|
||||
const startOffset = (first.getDay() + 6) % 7;
|
||||
const cells: { day: string; today: boolean; muted: boolean }[] = [];
|
||||
|
||||
for (let i = 0; i < startOffset; i += 1) {
|
||||
cells.push({ day: "", today: false, muted: true });
|
||||
}
|
||||
|
||||
for (let day = 1; day <= last.getDate(); day += 1) {
|
||||
cells.push({ day: String(day), today: day === now.getDate(), muted: false });
|
||||
}
|
||||
|
||||
while (cells.length % 7 !== 0) {
|
||||
cells.push({ day: "", today: false, muted: true });
|
||||
}
|
||||
|
||||
return cells;
|
||||
}
|
||||
|
||||
function monthTitle() {
|
||||
return new Intl.DateTimeFormat("de-DE", { month: "long", year: "numeric" }).format(new Date());
|
||||
}
|
||||
|
||||
function Metric({ icon, label, value }: { icon: string; label: string; value: number }) {
|
||||
return (
|
||||
<box class="metric" orientation={Gtk.Orientation.VERTICAL} spacing={8}>
|
||||
<box>
|
||||
<label class="metric-name" hexpand xalign={0} label={`${icon} ${label}`} />
|
||||
<label class="metric-value" label={percentLabel(value)} />
|
||||
</box>
|
||||
<box class="progress">
|
||||
<box class="progress-fill" css={`min-width: ${Math.round(clamp(value) * 2.3)}px;`} />
|
||||
</box>
|
||||
</box>
|
||||
);
|
||||
}
|
||||
|
||||
function SystemMonitor() {
|
||||
return (
|
||||
<box class="card" orientation={Gtk.Orientation.VERTICAL} spacing={12}>
|
||||
<label class="card-title" xalign={0} label="System Monitoring" />
|
||||
<Metric icon="" label="CPU" value={system.cpu} />
|
||||
<Metric icon="" label="RAM" value={system.memory} />
|
||||
<Metric icon="" label="Disk /" value={system.disk} />
|
||||
<box class="meta-row">
|
||||
<label hexpand xalign={0} label={` ${system.temp}`} />
|
||||
<label xalign={1} label={` ${system.uptime}`} />
|
||||
</box>
|
||||
<label class="sparkline" xalign={0} label={sparkline(cpuHistory)} />
|
||||
</box>
|
||||
);
|
||||
}
|
||||
|
||||
function Calendar() {
|
||||
const cells = monthDays();
|
||||
const weekdays = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"];
|
||||
|
||||
return (
|
||||
<box class="card" orientation={Gtk.Orientation.VERTICAL} spacing={10}>
|
||||
<label class="card-title" xalign={0} label={monthTitle()} />
|
||||
<box class="calendar-grid" orientation={Gtk.Orientation.VERTICAL} spacing={5}>
|
||||
<box spacing={5}>
|
||||
{weekdays.map(day => <label class="weekday" label={day} />)}
|
||||
</box>
|
||||
{Array.from({ length: Math.ceil(cells.length / 7) }, (_unused, row) => (
|
||||
<box spacing={5}>
|
||||
{cells.slice(row * 7, row * 7 + 7).map(cell => (
|
||||
<label
|
||||
class={`day ${cell.today ? "today" : ""} ${cell.muted ? "muted" : ""}`}
|
||||
label={cell.day}
|
||||
/>
|
||||
))}
|
||||
</box>
|
||||
))}
|
||||
</box>
|
||||
</box>
|
||||
);
|
||||
}
|
||||
|
||||
function Weather() {
|
||||
return (
|
||||
<box class="card weather" orientation={Gtk.Orientation.VERTICAL} spacing={10}>
|
||||
<box>
|
||||
<label class="card-title" hexpand xalign={0} label="Wetter" />
|
||||
<button class="icon-button" tooltipText="Aktualisieren" onClicked={() => updateWeather(true)}>
|
||||
<label label="" />
|
||||
</button>
|
||||
</box>
|
||||
<label class="weather-main" xalign={0} wrap label={weather} />
|
||||
<label class="card-subtitle" xalign={0} label="Quelle: wttr.in" />
|
||||
</box>
|
||||
);
|
||||
}
|
||||
|
||||
function hidePanel() {
|
||||
const win = panelWindow || app.get_window("widget-panel");
|
||||
win?.set_visible(false);
|
||||
}
|
||||
|
||||
function PanelContent() {
|
||||
return (
|
||||
<box class="panel" orientation={Gtk.Orientation.VERTICAL} spacing={14}>
|
||||
<box class="header">
|
||||
<box orientation={Gtk.Orientation.VERTICAL} hexpand>
|
||||
<label class="title" xalign={0} label="Widgetbereich" />
|
||||
<label class="subtitle" xalign={0} label="System, Kalender und Wetter" />
|
||||
</box>
|
||||
<button class="icon-button close" tooltipText="Schliessen" onClicked={hidePanel}>
|
||||
<label label="" />
|
||||
</button>
|
||||
</box>
|
||||
<SystemMonitor />
|
||||
<Calendar />
|
||||
<Weather />
|
||||
</box>
|
||||
);
|
||||
}
|
||||
|
||||
function panelLayout() {
|
||||
const geometry = app.monitors[0]?.get_geometry();
|
||||
const screenWidth = geometry?.width || 1280;
|
||||
const screenHeight = geometry?.height || 720;
|
||||
|
||||
return {
|
||||
width: Math.round(clamp(screenWidth * 0.28, 360, 460)),
|
||||
height: Math.round(clamp(screenHeight - 96, 560, 900)),
|
||||
};
|
||||
}
|
||||
|
||||
function WidgetPanelWindow() {
|
||||
const layout = panelLayout();
|
||||
|
||||
return (
|
||||
<window
|
||||
name="widget-panel"
|
||||
namespace="widget-panel"
|
||||
class="widget-window"
|
||||
visible={!START_HIDDEN}
|
||||
keymode={Astal.Keymode.EXCLUSIVE}
|
||||
anchor={Astal.WindowAnchor.LEFT | Astal.WindowAnchor.TOP | Astal.WindowAnchor.BOTTOM}
|
||||
application={app}
|
||||
>
|
||||
<Gtk.EventControllerKey onKeyPressed={(_, keyval) => {
|
||||
if (keyval === ESC_KEYVAL) {
|
||||
hidePanel();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}} />
|
||||
<box marginStart={18} marginTop={48} marginBottom={48}>
|
||||
<scrolledwindow
|
||||
class="panel-scroll"
|
||||
widthRequest={layout.width}
|
||||
heightRequest={layout.height}
|
||||
hscrollbarPolicy={Gtk.PolicyType.NEVER}
|
||||
vscrollbarPolicy={Gtk.PolicyType.AUTOMATIC}
|
||||
>
|
||||
<PanelContent />
|
||||
</scrolledwindow>
|
||||
</box>
|
||||
</window>
|
||||
);
|
||||
}
|
||||
|
||||
function rebuild() {
|
||||
const win = app.get_window("widget-panel");
|
||||
if (!win) {
|
||||
return;
|
||||
}
|
||||
|
||||
disposeRebuild?.();
|
||||
const layout = panelLayout();
|
||||
createRoot(dispose => {
|
||||
disposeRebuild = dispose;
|
||||
win.set_child(
|
||||
<box marginStart={18} marginTop={48} marginBottom={48}>
|
||||
<scrolledwindow
|
||||
class="panel-scroll"
|
||||
widthRequest={layout.width}
|
||||
heightRequest={layout.height}
|
||||
hscrollbarPolicy={Gtk.PolicyType.NEVER}
|
||||
vscrollbarPolicy={Gtk.PolicyType.AUTOMATIC}
|
||||
>
|
||||
<PanelContent />
|
||||
</scrolledwindow>
|
||||
</box> as Gtk.Widget,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function startTimer() {
|
||||
if (timerStarted) {
|
||||
return;
|
||||
}
|
||||
|
||||
timerStarted = true;
|
||||
updateSystem();
|
||||
updateWeather(true);
|
||||
|
||||
GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, REFRESH_SECONDS, () => {
|
||||
updateSystem();
|
||||
updateWeather();
|
||||
rebuild();
|
||||
return GLib.SOURCE_CONTINUE;
|
||||
});
|
||||
}
|
||||
|
||||
app.start({
|
||||
instanceName: "widget-panel",
|
||||
css: themeCss(activeTheme()),
|
||||
main() {
|
||||
panelWindow = WidgetPanelWindow() as Gtk.Window;
|
||||
app.add_window(panelWindow);
|
||||
if (!START_HIDDEN) {
|
||||
panelWindow.present();
|
||||
}
|
||||
GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => {
|
||||
startTimer();
|
||||
rebuild();
|
||||
return GLib.SOURCE_REMOVE;
|
||||
});
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user