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 ( ); } function SystemMonitor() { return ( ); } function Calendar() { const cells = monthDays(); const weekdays = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"]; return ( ); } function Weather() { return ( ); } function hidePanel() { const win = panelWindow || app.get_window("widget-panel"); win?.set_visible(false); } function PanelContent() { return ( ); } 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 ( { if (keyval === ESC_KEYVAL) { hidePanel(); return true; } return false; }} /> ); } function rebuild() { const win = app.get_window("widget-panel"); if (!win) { return; } disposeRebuild?.(); const layout = panelLayout(); createRoot(dispose => { disposeRebuild = dispose; win.set_child( 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; }); }, });