From 932b174c36ce154ad32c7a348e028a8108bee5dc Mon Sep 17 00:00:00 2001 From: Tetardtek-Cortex Date: Thu, 26 Mar 2026 06:54:17 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20Ghost=20Shell=20v2=20=E2=80=94=20AGS=20?= =?UTF-8?q?v3=20statusbar=20+=20violet-chaton=20v2=20palette?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AGS v3.1.0 (Astal/GTK3) Ghost Shell avec ghost mode (heartbeat + hover reveal) - Modules : clock, battery, volume (interactif), network, MPRIS, CPU/RAM, systray - Brain Power panel (Super + B) — lecture live focus/todos/session - tetardtek_ prompt avec curseur clignotant - Palette violet-chaton v2 documentée (Mitsuri Kanroji gradient magenta → green) - Autostart COSMIC via .desktop - Archive AGS v1 conservée pour référence --- ags-v1/config.js | 461 ++++++++++++++++++++++++++ ags-v1/css/style.css | 385 +++++++++++++++++++++ ags-v1/widgets/Bar.js | 228 +++++++++++++ ags-v1/widgets/Launcher.js | 99 ++++++ ags-v1/widgets/Notifications.js | 105 ++++++ ags-v1/widgets/OSD.js | 95 ++++++ ags-v3/.gitignore | 2 + ags-v3/app.ts | 30 ++ ags-v3/env.d.ts | 21 ++ ags-v3/lib/brain.ts | 73 ++++ ags-v3/package.json | 10 + ags-v3/style.scss | 300 +++++++++++++++++ ags-v3/styles/_bar.scss | 133 ++++++++ ags-v3/styles/_heartbeat.scss | 21 ++ ags-v3/styles/_palette.scss | 35 ++ ags-v3/toggle-brain.sh | 3 + ags-v3/tsconfig.json | 14 + ags-v3/widget/Bar.tsx | 74 +++++ ags-v3/widget/Heartbeat.tsx | 27 ++ ags-v3/widget/modules/Battery.tsx | 29 ++ ags-v3/widget/modules/Clock.tsx | 32 ++ ags-v3/widget/modules/Media.tsx | 60 ++++ ags-v3/widget/modules/Network.tsx | 31 ++ ags-v3/widget/modules/Prompt.tsx | 15 + ags-v3/widget/modules/SysTray.tsx | 24 ++ ags-v3/widget/modules/SystemStats.tsx | 31 ++ ags-v3/widget/modules/Volume.tsx | 41 +++ ags-v3/widget/panels/BrainPower.tsx | 104 ++++++ autostart/ghost-shell.desktop | 6 + palette/violet-chaton-v2.md | 68 ++++ 30 files changed, 2557 insertions(+) create mode 100644 ags-v1/config.js create mode 100644 ags-v1/css/style.css create mode 100644 ags-v1/widgets/Bar.js create mode 100644 ags-v1/widgets/Launcher.js create mode 100644 ags-v1/widgets/Notifications.js create mode 100644 ags-v1/widgets/OSD.js create mode 100644 ags-v3/.gitignore create mode 100644 ags-v3/app.ts create mode 100644 ags-v3/env.d.ts create mode 100644 ags-v3/lib/brain.ts create mode 100644 ags-v3/package.json create mode 100644 ags-v3/style.scss create mode 100644 ags-v3/styles/_bar.scss create mode 100644 ags-v3/styles/_heartbeat.scss create mode 100644 ags-v3/styles/_palette.scss create mode 100755 ags-v3/toggle-brain.sh create mode 100644 ags-v3/tsconfig.json create mode 100644 ags-v3/widget/Bar.tsx create mode 100644 ags-v3/widget/Heartbeat.tsx create mode 100644 ags-v3/widget/modules/Battery.tsx create mode 100644 ags-v3/widget/modules/Clock.tsx create mode 100644 ags-v3/widget/modules/Media.tsx create mode 100644 ags-v3/widget/modules/Network.tsx create mode 100644 ags-v3/widget/modules/Prompt.tsx create mode 100644 ags-v3/widget/modules/SysTray.tsx create mode 100644 ags-v3/widget/modules/SystemStats.tsx create mode 100644 ags-v3/widget/modules/Volume.tsx create mode 100644 ags-v3/widget/panels/BrainPower.tsx create mode 100644 autostart/ghost-shell.desktop create mode 100644 palette/violet-chaton-v2.md diff --git a/ags-v1/config.js b/ags-v1/config.js new file mode 100644 index 0000000..e6268ad --- /dev/null +++ b/ags-v1/config.js @@ -0,0 +1,461 @@ +// ── violet-chaton v2 — AGS config ─────────────────────────────────────────── +// Barre + OSD + Launcher + Notifications +// API: AGS v1.8.2 (.hook/.bind/.poll — no connections) + +const audio = await Service.import("audio"); +const battery = await Service.import("battery"); +const network = await Service.import("network"); +const systemtray = await Service.import("systemtray"); +const mpris = await Service.import("mpris"); +const notifications = await Service.import("notifications"); +const applications = await Service.import("applications"); + +// ── Compositor detection ──────────────────────────────────────────────────── +const compositor = (() => { + const session = Utils.exec("bash -c 'echo $XDG_CURRENT_DESKTOP'").trim(); + if (session.includes("Hyprland")) return "hyprland"; + if (session.includes("COSMIC")) return "cosmic"; + return "unknown"; +})(); + +print(`[violet-chaton] compositor: ${compositor}`); + +// ══════════════════════════════════════════════════════════════════════════════ +// BAR +// ══════════════════════════════════════════════════════════════════════════════ + +const Separator = () => Widget.Label({ className: "separator", label: "│" }); + +const Clock = () => Widget.Label({ className: "clock" }) + .poll(1000, (self) => { self.label = Utils.exec("date +%H:%M"); }); + +const DateWidget = () => Widget.Label({ className: "date" }) + .poll(60000, (self) => { self.label = Utils.exec("date '+%a %d %b'"); }); + +const CPU = () => Widget.Label({ className: "cpu" }) + .poll(2000, (self) => { + const usage = Math.round( + Number(Utils.exec(['bash', '-c', "top -bn1 | awk '/^%Cpu/ {print 100-$8}'"])) + ); + self.label = `󰻠 ${usage}%`; + self.toggleClassName("warning", usage > 70); + self.toggleClassName("critical", usage > 90); + }); + +const RAM = () => Widget.Label({ className: "ram" }) + .poll(2000, (self) => { + const used = Utils.exec(['bash', '-c', "free -m | awk '/^Mem:/ {printf \"%.1f\", $3/1024}'"]); + const total = Utils.exec(['bash', '-c', "free -m | awk '/^Mem:/ {printf \"%.1f\", $2/1024}'"]); + const pct = Math.round((parseFloat(used) / parseFloat(total)) * 100); + self.label = `󰑭 ${used}G`; + self.toggleClassName("warning", pct > 70); + self.toggleClassName("critical", pct > 90); + }); + +const Network = () => Widget.Label({ className: "network" }) + .hook(network, (self) => { + if (network.primary === "wifi") { + const wifi = network.wifi; + self.label = `󰤨 ${wifi?.ssid || ""}`; + self.toggleClassName("wifi", true); + self.toggleClassName("disconnected", false); + } else if (network.primary === "wired") { + self.label = "󰈀 Eth"; + self.toggleClassName("wifi", false); + self.toggleClassName("disconnected", false); + } else { + self.label = "󰤮 "; + self.toggleClassName("disconnected", true); + } + }); + +const Volume = () => Widget.Button({ + className: "volume", + onClicked: () => { audio.speaker.isMuted = !audio.speaker.isMuted; }, + child: Widget.Label() + .hook(audio, (self) => { + const vol = Math.round((audio.speaker?.volume || 0) * 100); + const muted = audio.speaker?.isMuted; + const icon = muted ? "󰝟" : vol > 66 ? "󰕾" : vol > 33 ? "󰖀" : "󰕿"; + self.label = `${icon} ${vol}%`; + self.parent?.toggleClassName("muted", muted); + }, "speaker-changed"), +}); + +const Battery = () => Widget.Label({ + className: "battery", + visible: battery.bind("available"), +}).hook(battery, (self) => { + const pct = battery.percent; + const charging = battery.charging; + const icon = charging ? "󰂄" : pct > 80 ? "󰁹" : pct > 60 ? "󰂀" : + pct > 40 ? "󰁾" : pct > 20 ? "󰁻" : "󰂎"; + self.label = `${icon} ${pct}%`; + self.toggleClassName("charging", charging); + self.toggleClassName("low", pct <= 20 && !charging); + self.toggleClassName("warning", pct <= 30 && !charging); +}); + +const Media = () => Widget.Label({ className: "media" }) + .hook(mpris, (self) => { + const player = mpris.players[0]; + if (!player) { + self.visible = false; + return; + } + self.visible = true; + const artist = player.trackArtists?.join(", ") || ""; + const title = player.trackTitle || ""; + const icon = player.playBackStatus === "Playing" ? " " : " "; + self.label = `${icon}${artist ? artist + " — " : ""}${title}`.slice(0, 50); + self.toggleClassName("paused", player.playBackStatus !== "Playing"); + }); + +const SysTray = () => Widget.Box({ + className: "systray", + children: systemtray.bind("items").as((items) => + items.map((item) => + Widget.Button({ + child: Widget.Icon({ icon: item.bind("icon"), size: 16 }), + tooltipMarkup: item.bind("tooltip-markup"), + onPrimaryClick: (_, event) => item.activate(event), + onSecondaryClick: (_, event) => item.openMenu(event), + }) + ) + ), +}); + +const LauncherBtn = () => Widget.Button({ + className: "launcher-btn", + label: "󱄅", + onClicked: () => App.toggleWindow("launcher"), +}); + +const PowerBtn = () => Widget.Button({ + className: "power-btn", + label: "⏻", + onClicked: () => Utils.exec("bash -c 'systemctl poweroff'"), +}); + +const Workspaces = () => { + if (compositor !== "hyprland") return Widget.Box({}); + // Hyprland not running — return empty + return Widget.Box({}); +}; + +const Bar = (monitor) => Widget.Window({ + name: `bar-${monitor}`, + monitor, + anchor: ["top", "left", "right"], + exclusivity: "exclusive", + className: "bar", + child: Widget.CenterBox({ + startWidget: Widget.Box({ + className: "modules-left", + children: [ + LauncherBtn(), + Separator(), + Workspaces(), + CPU(), + RAM(), + ], + }), + centerWidget: Widget.Box({ + className: "modules-center", + children: [ + Media(), + ], + }), + endWidget: Widget.Box({ + className: "modules-right", + hpack: "end", + children: [ + Network(), + Separator(), + Volume(), + Separator(), + Battery(), + Separator(), + DateWidget(), + Clock(), + Separator(), + SysTray(), + Separator(), + PowerBtn(), + ], + }), + }), +}); + +// ══════════════════════════════════════════════════════════════════════════════ +// OSD +// ══════════════════════════════════════════════════════════════════════════════ + +const VolumeOSD = () => { + const icon = Widget.Label({ className: "icon" }); + const progress = Widget.ProgressBar(); + const label = Widget.Label({ className: "label" }); + + return Widget.Box({ + className: "osd", + children: [icon, progress, label], + }).hook(audio, (self) => { + const vol = audio.speaker?.volume || 0; + const muted = audio.speaker?.isMuted; + icon.label = muted ? "󰝟" : vol > 0.66 ? "󰕾" : vol > 0.33 ? "󰖀" : "󰕿"; + progress.value = vol; + label.label = `${Math.round(vol * 100)}%`; + }, "speaker-changed"); +}; + +const BrightnessOSD = () => { + const icon = Widget.Label({ className: "icon", label: "󰃞" }); + const progress = Widget.ProgressBar(); + const label = Widget.Label({ className: "label" }); + + const getBrightness = () => { + try { + const max = Number(Utils.exec("brightnessctl max")); + const cur = Number(Utils.exec("brightnessctl get")); + return max > 0 ? cur / max : 0; + } catch { + return 0; + } + }; + + return Widget.Box({ + className: "osd", + children: [icon, progress, label], + }).poll(500, (self) => { + const val = getBrightness(); + progress.value = val; + label.label = `${Math.round(val * 100)}%`; + }); +}; + +const OSD = (monitor) => [ + Widget.Window({ + name: `osd-volume-${monitor}`, + monitor, + anchor: ["bottom"], + layer: "overlay", + visible: false, + child: VolumeOSD(), + }), + Widget.Window({ + name: `osd-brightness-${monitor}`, + monitor, + anchor: ["bottom"], + layer: "overlay", + visible: false, + child: BrightnessOSD(), + }), +]; + +// ══════════════════════════════════════════════════════════════════════════════ +// LAUNCHER +// ══════════════════════════════════════════════════════════════════════════════ + +const AppItem = (app) => Widget.Button({ + className: "app-item", + onClicked: () => { + app.launch(); + App.closeWindow("launcher"); + }, + child: Widget.Box({ + children: [ + Widget.Icon({ icon: app.iconName || "application-x-executable", size: 24 }), + Widget.Box({ + vertical: true, + children: [ + Widget.Label({ label: app.name, xalign: 0, truncate: "end" }), + Widget.Label({ + label: app.description || "", + xalign: 0, + truncate: "end", + css: "color: #716686; font-size: 11px; font-weight: normal;", + }), + ], + }), + ], + }), +}); + +const Launcher = (monitor) => { + let apps = applications.list; + + const list = Widget.Box({ vertical: true, spacing: 2 }); + + const entry = Widget.Entry({ + className: "search", + placeholderText: "Rechercher...", + onAccept: () => { + const first = apps[0]; + if (first) { + first.launch(); + App.closeWindow("launcher"); + } + }, + onChange: ({ text }) => { + apps = applications.query(text || ""); + list.children = apps.slice(0, 12).map(AppItem); + if (apps.length === 0) { + list.children = [Widget.Label({ + className: "no-results", + label: "Aucun resultat", + })]; + } + }, + }); + + return Widget.Window({ + name: "launcher", + monitor, + anchor: ["top"], + layer: "overlay", + visible: false, + keymode: "exclusive", + setup: (self) => { + self.keybind("Escape", () => App.closeWindow("launcher")); + self.hook(App, (_, name, visible) => { + if (name === "launcher" && visible) { + entry.text = ""; + apps = applications.list; + list.children = apps.slice(0, 12).map(AppItem); + entry.grab_focus(); + } + }); + }, + child: Widget.Box({ + className: "launcher", + vertical: true, + children: [ + Widget.Scrollable({ + hscroll: "never", + vscroll: "automatic", + css: "min-height: 400px;", + child: list, + }), + entry, + ], + }), + }); +}; + +// ══════════════════════════════════════════════════════════════════════════════ +// NOTIFICATIONS +// ══════════════════════════════════════════════════════════════════════════════ + +notifications.popupTimeout = 5000; +notifications.cacheActions = true; + +const NotificationIcon = (notif) => { + if (notif.image) { + return Widget.Box({ + css: ` + min-width: 48px; min-height: 48px; + background-image: url("${notif.image}"); + background-size: cover; + background-position: center; + border-radius: 8px; + margin-right: 10px; + `, + }); + } + return Widget.Icon({ + icon: notif.appIcon || notif.appEntry || "dialog-information", + size: 36, + css: "margin-right: 10px;", + }); +}; + +const Notification = (notif) => Widget.Box({ + className: `notification ${notif.urgency}`, + vertical: true, + children: [ + Widget.Box({ + children: [ + NotificationIcon(notif), + Widget.Box({ + vertical: true, + hexpand: true, + children: [ + Widget.Box({ + children: [ + Widget.Label({ + className: "title", + label: notif.summary, + xalign: 0, + hexpand: true, + truncate: "end", + }), + Widget.Label({ + className: "time", + label: new Date(notif.time * 1000) + .toLocaleTimeString("fr-FR", { + hour: "2-digit", + minute: "2-digit", + }), + }), + Widget.Button({ + className: "close-btn", + label: "✕", + onClicked: () => notif.close(), + }), + ], + }), + Widget.Label({ + className: "app-name", + label: notif.appName || "", + xalign: 0, + }), + ], + }), + ], + }), + ...(notif.body ? [Widget.Label({ + className: "body", + label: notif.body, + xalign: 0, + wrap: true, + useMarkup: true, + })] : []), + ...(notif.actions.length > 0 ? [Widget.Box({ + className: "actions", + children: notif.actions.map((action) => + Widget.Button({ + label: action.label, + onClicked: () => notif.invoke(action.id), + }) + ), + })] : []), + ], +}); + +const Notifications = (monitor) => Widget.Window({ + name: `notifications-${monitor}`, + monitor, + anchor: ["top", "right"], + layer: "overlay", + child: Widget.Box({ + vertical: true, + css: "min-width: 350px;", + children: notifications.bind("popups").as((popups) => + popups.map(Notification) + ), + }), +}); + +// ══════════════════════════════════════════════════════════════════════════════ +// EXPORT +// ══════════════════════════════════════════════════════════════════════════════ + +export default { + style: `${App.configDir}/css/style.css`, + windows: [ + Bar(0), + ...OSD(0), + Launcher(0), + Notifications(0), + ], +}; diff --git a/ags-v1/css/style.css b/ags-v1/css/style.css new file mode 100644 index 0000000..cd1dc8a --- /dev/null +++ b/ags-v1/css/style.css @@ -0,0 +1,385 @@ +/* ── violet-chaton v2 — AGS stylesheet ──────────────────────────────────────── + * + * Palette : + * crust #1a0e27 + * base #261537 + * mantle #341c4a + * surface0 #3d2454 + * surface1 #493161 + * surface2 #5a3875 + * magenta #ff4da6 accent primaire + * lilac #c9a0ff accent secondaire + * mitsuri #9adba8 vert pastel + * lavande #a4b4ff bleu-violet + * champagne #e8c87a or chaud + * danger #f25c7a + * text #f0eaf8 + * subtext1 #c4b8d4 + * subtext0 #9a8fad + * muted #716686 + * + * ─────────────────────────────────────────────────────────────────────────── */ + +/* ── Reset ───────────────────────────────────────────────────────────────── */ + +* { + font-family: "Maple Mono NF", "MapleMono Nerd Font", monospace; + font-size: 13px; + font-weight: bold; +} + +/* ══════════════════════════════════════════════════════════════════════════ + * BAR — 3 pills glassmorphism (island floating) + * ══════════════════════════════════════════════════════════════════════════ */ + +.bar { + background: transparent; +} + +.bar .modules-left, +.bar .modules-center, +.bar .modules-right { + background: alpha(#261537, 0.88); + border-radius: 14px; + border: 3px solid alpha(#ff4da6, 0.60); + margin: 8px 4px; + padding: 0 4px; +} + +.bar .modules-left:hover, +.bar .modules-center:hover, +.bar .modules-right:hover { + border-color: #ff4da6; + box-shadow: 0 4px 28px alpha(#c9a0ff, 0.18); +} + +/* ── Launcher button ─────────────────────────────────────────────────────── */ + +.bar .launcher-btn { + color: #ff4da6; + font-size: 19px; + padding: 0 14px 0 18px; + min-width: 0; + min-height: 0; +} + +.bar .launcher-btn:hover { + color: #c9a0ff; +} + +/* ── Separator ───────────────────────────────────────────────────────────── */ + +.bar .separator { + color: alpha(#f0eaf8, 0.12); + font-size: 11px; + padding: 0 4px; + font-weight: normal; +} + +/* ── Clock ───────────────────────────────────────────────────────────────── */ + +.bar .clock { + color: #ff4da6; + font-weight: 900; + font-size: 14px; + letter-spacing: 0.04em; + padding: 0 10px; +} + +.bar .clock:hover { + color: #c9a0ff; +} + +/* ── Date ────────────────────────────────────────────────────────────────── */ + +.bar .date { + color: #a4b4ff; + font-size: 12px; + font-weight: normal; + padding: 0 10px 0 2px; +} + +/* ── System modules ──────────────────────────────────────────────────────── */ + +.bar .cpu { color: #a4b4ff; } +.bar .cpu.warning { color: #e8c87a; } +.bar .cpu.critical { color: #f25c7a; } + +.bar .ram { color: #ff4da6; } +.bar .ram.warning { color: #e8c87a; } +.bar .ram.critical { color: #f25c7a; } + +.bar .temp { + color: alpha(#a4b4ff, 0.60); + font-size: 11px; + font-weight: normal; +} + +.bar .temp.warning { color: #e8c87a; } +.bar .temp.critical { color: #f25c7a; } + +/* ── Network ─────────────────────────────────────────────────────────────── */ + +.bar .network { + color: #a4b4ff; + font-size: 11px; + font-weight: normal; +} + +.bar .network.disconnected { color: #f25c7a; } +.bar .network.wifi { color: alpha(#a4b4ff, 0.80); } + +/* ── Volume ──────────────────────────────────────────────────────────────── */ + +.bar .volume { color: #ff4da6; } +.bar .volume.muted { color: alpha(#ff4da6, 0.30); } + +/* ── Battery ─────────────────────────────────────────────────────────────── */ + +.bar .battery { color: #ff4da6; } +.bar .battery.charging { color: #9adba8; } +.bar .battery.low { color: #f25c7a; } +.bar .battery.warning { color: #e8c87a; } + +/* ── Media (MPRIS) ───────────────────────────────────────────────────────── */ + +.bar .media { + color: #c9a0ff; + font-size: 12px; + font-weight: normal; + padding: 0 10px; +} + +.bar .media.paused { + color: alpha(#c9a0ff, 0.50); + font-style: italic; +} + +/* ── Systray ─────────────────────────────────────────────────────────────── */ + +.bar .systray { padding: 0 8px; } +.bar .systray .passive { opacity: 0.5; } + +/* ── Workspaces (Hyprland only) ──────────────────────────────────────────── */ + +.bar .workspaces button { + background: transparent; + color: #716686; + min-width: 24px; + min-height: 24px; + border-radius: 8px; + margin: 2px; + padding: 0; +} + +.bar .workspaces button.active { + background: alpha(#ff4da6, 0.20); + color: #ff4da6; + border: 1px solid alpha(#ff4da6, 0.40); +} + +.bar .workspaces button.occupied { + color: #c9a0ff; +} + +.bar .workspaces button:hover { + background: alpha(#c9a0ff, 0.12); + color: #c9a0ff; +} + +/* ── Power button ────────────────────────────────────────────────────────── */ + +.bar .power-btn { + color: #f25c7a; + font-size: 15px; + padding: 0 14px 0 8px; + min-width: 0; + min-height: 0; +} + +.bar .power-btn:hover { color: #ff4da6; } + +/* ── Hover global modules ────────────────────────────────────────────────── */ + +.bar .cpu:hover, +.bar .ram:hover, +.bar .temp:hover, +.bar .network:hover, +.bar .volume:hover, +.bar .battery:hover { + color: #c9a0ff; +} + +/* ══════════════════════════════════════════════════════════════════════════ + * OSD — volume / brightness overlay + * ══════════════════════════════════════════════════════════════════════════ */ + +.osd { + background: alpha(#261537, 0.92); + border-radius: 14px; + border: 2px solid alpha(#ff4da6, 0.40); + padding: 12px 20px; + margin: 0 0 40px 0; +} + +.osd .icon { + color: #ff4da6; + font-size: 24px; + margin-right: 12px; +} + +.osd progressbar trough { + background: #3d2454; + border-radius: 8px; + min-height: 8px; + min-width: 200px; +} + +.osd progressbar progress { + border-radius: 8px; + min-height: 8px; + background: linear-gradient(to right, #ff4da6, #c9a0ff, #a4b4ff, #9adba8); +} + +.osd .label { + color: #f0eaf8; + font-size: 12px; + margin-left: 8px; +} + +/* ══════════════════════════════════════════════════════════════════════════ + * LAUNCHER — app search + * ══════════════════════════════════════════════════════════════════════════ */ + +.launcher { + background: alpha(#1a0e27, 0.94); + border-radius: 14px; + border: 2px solid alpha(#ff4da6, 0.38); + padding: 10px; + min-width: 500px; +} + +.launcher .search { + background: alpha(#261537, 0.75); + border-radius: 12px; + border: 1px solid alpha(#5a3875, 0.50); + padding: 9px 14px; + color: #f0eaf8; + caret-color: #ff4da6; + font-size: 14px; +} + +.launcher .search:focus { + border-color: alpha(#ff4da6, 0.60); +} + +.launcher .app-item { + background: transparent; + border-radius: 8px; + padding: 7px 10px; + color: #f0eaf8; +} + +.launcher .app-item:hover, +.launcher .app-item:focus { + background: alpha(#ff4da6, 0.16); + border: 1px solid alpha(#ff4da6, 0.32); +} + +.launcher .app-item:hover label, +.launcher .app-item:focus label { + color: #ff4da6; +} + +.launcher .app-item image { + margin-right: 10px; +} + +.launcher .no-results { + color: #716686; + padding: 20px; +} + +/* ══════════════════════════════════════════════════════════════════════════ + * NOTIFICATIONS + * ══════════════════════════════════════════════════════════════════════════ */ + +.notification { + background: alpha(#261537, 0.94); + border-radius: 14px; + border: 2px solid alpha(#c9a0ff, 0.30); + padding: 12px; + margin: 8px; + min-width: 350px; +} + +.notification .title { + color: #ff4da6; + font-weight: bold; + font-size: 13px; +} + +.notification .body { + color: #c4b8d4; + font-weight: normal; + font-size: 12px; +} + +.notification .app-name { + color: #716686; + font-size: 11px; +} + +.notification .time { + color: #716686; + font-size: 10px; +} + +.notification .close-btn { + color: #716686; + font-size: 14px; + min-width: 0; + min-height: 0; + padding: 2px 6px; + border-radius: 6px; +} + +.notification .close-btn:hover { + color: #f25c7a; + background: alpha(#f25c7a, 0.12); +} + +.notification .actions button { + background: alpha(#5a3875, 0.50); + color: #c9a0ff; + border-radius: 8px; + padding: 4px 12px; + margin: 4px 4px 0 0; +} + +.notification .actions button:hover { + background: alpha(#ff4da6, 0.20); + color: #ff4da6; +} + +/* ── Urgency levels ──────────────────────────────────────────────────────── */ + +.notification.critical { + border-color: alpha(#f25c7a, 0.60); +} + +.notification.critical .title { + color: #f25c7a; +} + +/* ══════════════════════════════════════════════════════════════════════════ + * TOOLTIP — shared + * ══════════════════════════════════════════════════════════════════════════ */ + +tooltip { + background: alpha(#1a0e27, 0.96); + border: 1px solid alpha(#ff4da6, 0.30); + border-radius: 10px; + color: #f0eaf8; + padding: 6px 10px; +} diff --git a/ags-v1/widgets/Bar.js b/ags-v1/widgets/Bar.js new file mode 100644 index 0000000..6a1d6cc --- /dev/null +++ b/ags-v1/widgets/Bar.js @@ -0,0 +1,228 @@ +// ── violet-chaton v2 — Bar widget ─────────────────────────────────────────── +// 3 pills glassmorphism — modulaire selon compositor + +const audio = Service.import("audio"); +const battery = Service.import("battery"); +const network = Service.import("network"); +const systemtray = Service.import("systemtray"); +const mpris = Service.import("mpris"); + +// ── Helpers ───────────────────────────────────────────────────────────────── + +const Separator = () => Widget.Label({ className: "separator", label: "│" }); + +const Clock = () => Widget.Label({ + className: "clock", + connections: [[1000, (self) => { + self.label = Utils.exec("date +%H:%M"); + }]], +}); + +const DateWidget = () => Widget.Label({ + className: "date", + connections: [[60000, (self) => { + self.label = Utils.exec("date '+%a %d %b'"); + }]], +}); + +// ── System ────────────────────────────────────────────────────────────────── + +const CPU = () => Widget.Label({ + className: "cpu", + connections: [[2000, (self) => { + const usage = Math.round( + Number(Utils.exec(`bash -c "top -bn1 | awk '/^%Cpu/ {print 100-$8}'"`) + )); + self.label = `󰻠 ${usage}%`; + self.toggleClassName("warning", usage > 70); + self.toggleClassName("critical", usage > 90); + }]], +}); + +const RAM = () => Widget.Label({ + className: "ram", + connections: [[2000, (self) => { + const used = Utils.exec(`bash -c "free -m | awk '/^Mem:/ {printf \"%.1f\", $3/1024}'"`) + const total = Utils.exec(`bash -c "free -m | awk '/^Mem:/ {printf \"%.1f\", $2/1024}'"`) + const pct = Math.round((parseFloat(used) / parseFloat(total)) * 100); + self.label = `󰑭 ${used}G`; + self.toggleClassName("warning", pct > 70); + self.toggleClassName("critical", pct > 90); + }]], +}); + +// ── Network ───────────────────────────────────────────────────────────────── + +const Network = () => Widget.Label({ + className: "network", + connections: [[network, (self) => { + if (network.primary === "wifi") { + const wifi = network.wifi; + self.label = `󰤨 ${wifi?.ssid || ""}`; + self.toggleClassName("wifi", true); + self.toggleClassName("disconnected", false); + } else if (network.primary === "wired") { + self.label = "󰈀 Eth"; + self.toggleClassName("wifi", false); + self.toggleClassName("disconnected", false); + } else { + self.label = "󰤮 "; + self.toggleClassName("disconnected", true); + } + }]], +}); + +// ── Volume ────────────────────────────────────────────────────────────────── + +const Volume = () => Widget.Button({ + className: "volume", + onClicked: () => { audio.speaker.isMuted = !audio.speaker.isMuted; }, + child: Widget.Label({ + connections: [[audio, (self) => { + const vol = Math.round((audio.speaker?.volume || 0) * 100); + const muted = audio.speaker?.isMuted; + const icon = muted ? "󰝟" : vol > 66 ? "󰕾" : vol > 33 ? "󰖀" : "󰕿"; + self.label = `${icon} ${vol}%`; + self.parent?.toggleClassName("muted", muted); + }, "speaker-changed"]], + }), +}); + +// ── Battery ───────────────────────────────────────────────────────────────── + +const Battery = () => Widget.Label({ + className: "battery", + visible: battery.bind("available"), + connections: [[battery, (self) => { + const pct = battery.percent; + const charging = battery.charging; + const icon = charging ? "󰂄" : pct > 80 ? "󰁹" : pct > 60 ? "󰂀" : + pct > 40 ? "󰁾" : pct > 20 ? "󰁻" : "󰂎"; + self.label = `${icon} ${pct}%`; + self.toggleClassName("charging", charging); + self.toggleClassName("low", pct <= 20 && !charging); + self.toggleClassName("warning", pct <= 30 && !charging); + }]], +}); + +// ── Media ─────────────────────────────────────────────────────────────────── + +const Media = () => Widget.Label({ + className: "media", + connections: [[mpris, (self) => { + const player = mpris.players[0]; + if (!player) { + self.visible = false; + return; + } + self.visible = true; + const artist = player.trackArtists?.join(", ") || ""; + const title = player.trackTitle || ""; + const icon = player.playBackStatus === "Playing" ? " " : " "; + self.label = `${icon}${artist ? artist + " — " : ""}${title}`.slice(0, 50); + self.toggleClassName("paused", player.playBackStatus !== "Playing"); + }]], +}); + +// ── Systray ───────────────────────────────────────────────────────────────── + +const SysTray = () => Widget.Box({ + className: "systray", + children: systemtray.bind("items").transform((items) => + items.map((item) => + Widget.Button({ + child: Widget.Icon({ icon: item.bind("icon"), size: 16 }), + tooltipMarkup: item.bind("tooltip-markup"), + onPrimaryClick: (_, event) => item.activate(event), + onSecondaryClick: (_, event) => item.openMenu(event), + }) + ) + ), +}); + +// ── Launcher button ───────────────────────────────────────────────────────── + +const LauncherBtn = () => Widget.Button({ + className: "launcher-btn", + label: "󱄅", + onClicked: () => App.toggleWindow("launcher"), +}); + +// ── Power button ──────────────────────────────────────────────────────────── + +const PowerBtn = () => Widget.Button({ + className: "power-btn", + label: "⏻", + onClicked: () => Utils.exec("bash -c 'systemctl poweroff'"), +}); + +// ── Workspaces (Hyprland only) ────────────────────────────────────────────── + +const Workspaces = (compositor) => { + if (compositor !== "hyprland") return Widget.Box({}); + + const hyprland = Service.import("hyprland"); + return Widget.Box({ + className: "workspaces", + children: hyprland.bind("workspaces").transform((ws) => + ws.sort((a, b) => a.id - b.id) + .filter((w) => w.id > 0) + .map((w) => + Widget.Button({ + onClicked: () => hyprland.messageAsync(`dispatch workspace ${w.id}`), + child: Widget.Label({ label: `${w.id}` }), + className: hyprland.active.workspace.bind("id").transform( + (id) => id === w.id ? "active" : w.windows > 0 ? "occupied" : "" + ), + }) + ) + ), + }); +}; + +// ── Bar assembly ──────────────────────────────────────────────────────────── + +export const Bar = (monitor, compositor) => Widget.Window({ + name: `bar-${monitor}`, + monitor, + anchor: ["top", "left", "right"], + exclusivity: "exclusive", + className: "bar", + child: Widget.CenterBox({ + startWidget: Widget.Box({ + className: "modules-left", + children: [ + LauncherBtn(), + Separator(), + Workspaces(compositor), + Separator(), + CPU(), + RAM(), + ], + }), + centerWidget: Widget.Box({ + className: "modules-center", + children: [ + Media(), + ], + }), + endWidget: Widget.Box({ + className: "modules-right", + hpack: "end", + children: [ + Network(), + Separator(), + Volume(), + Separator(), + Battery(), + Separator(), + DateWidget(), + Clock(), + Separator(), + SysTray(), + Separator(), + PowerBtn(), + ], + }), + }), +}); diff --git a/ags-v1/widgets/Launcher.js b/ags-v1/widgets/Launcher.js new file mode 100644 index 0000000..b683149 --- /dev/null +++ b/ags-v1/widgets/Launcher.js @@ -0,0 +1,99 @@ +// ── violet-chaton v2 — Launcher widget ────────────────────────────────────── +// App launcher avec recherche fuzzy + +const applications = Service.import("applications"); + +const AppItem = (app) => Widget.Button({ + className: "app-item", + onClicked: () => { + app.launch(); + App.closeWindow("launcher"); + }, + child: Widget.Box({ + children: [ + Widget.Icon({ icon: app.iconName || "application-x-executable", size: 24 }), + Widget.Box({ + vertical: true, + children: [ + Widget.Label({ + label: app.name, + xalign: 0, + truncate: "end", + }), + Widget.Label({ + label: app.description || "", + xalign: 0, + truncate: "end", + className: "description", + css: "color: #716686; font-size: 11px; font-weight: normal;", + }), + ], + }), + ], + }), +}); + +export const Launcher = (monitor) => { + let apps = applications.list; + + const list = Widget.Box({ + vertical: true, + spacing: 2, + }); + + const entry = Widget.Entry({ + className: "search", + placeholderText: "Rechercher...", + onAccept: () => { + const first = apps[0]; + if (first) { + first.launch(); + App.closeWindow("launcher"); + } + }, + onChange: ({ text }) => { + apps = applications.query(text || ""); + list.children = apps.slice(0, 12).map(AppItem); + + if (apps.length === 0) { + list.children = [Widget.Label({ + className: "no-results", + label: "Aucun resultat", + })]; + } + }, + }); + + return Widget.Window({ + name: "launcher", + monitor, + anchor: ["top"], + layer: "overlay", + visible: false, + keymode: "exclusive", + setup: (self) => { + self.keybind("Escape", () => App.closeWindow("launcher")); + self.hook(App, (_, name, visible) => { + if (name === "launcher" && visible) { + entry.text = ""; + apps = applications.list; + list.children = apps.slice(0, 12).map(AppItem); + entry.grab_focus(); + } + }); + }, + child: Widget.Box({ + className: "launcher", + vertical: true, + children: [ + Widget.Scrollable({ + hscroll: "never", + vscroll: "automatic", + css: "min-height: 400px;", + child: list, + }), + entry, + ], + }), + }); +}; diff --git a/ags-v1/widgets/Notifications.js b/ags-v1/widgets/Notifications.js new file mode 100644 index 0000000..34670e1 --- /dev/null +++ b/ags-v1/widgets/Notifications.js @@ -0,0 +1,105 @@ +// ── violet-chaton v2 — Notifications widget ───────────────────────────────── +// Popup notifications stylees — urgency-aware + +const notifications = Service.import("notifications"); + +// Ne pas déranger +notifications.popupTimeout = 5000; +notifications.cacheActions = true; + +const NotificationIcon = (notif) => { + if (notif.image) { + return Widget.Box({ + css: ` + min-width: 48px; min-height: 48px; + background-image: url("${notif.image}"); + background-size: cover; + background-position: center; + border-radius: 8px; + margin-right: 10px; + `, + }); + } + return Widget.Icon({ + icon: notif.appIcon || notif.appEntry || "dialog-information", + size: 36, + css: "margin-right: 10px;", + }); +}; + +const Notification = (notif) => Widget.Box({ + className: `notification ${notif.urgency}`, + vertical: true, + children: [ + Widget.Box({ + children: [ + NotificationIcon(notif), + Widget.Box({ + vertical: true, + hexpand: true, + children: [ + Widget.Box({ + children: [ + Widget.Label({ + className: "title", + label: notif.summary, + xalign: 0, + hexpand: true, + truncate: "end", + }), + Widget.Label({ + className: "time", + label: new Date(notif.time * 1000) + .toLocaleTimeString("fr-FR", { + hour: "2-digit", + minute: "2-digit", + }), + }), + Widget.Button({ + className: "close-btn", + label: "✕", + onClicked: () => notif.close(), + }), + ], + }), + Widget.Label({ + className: "app-name", + label: notif.appName || "", + xalign: 0, + }), + ], + }), + ], + }), + ...(notif.body ? [Widget.Label({ + className: "body", + label: notif.body, + xalign: 0, + wrap: true, + useMarkup: true, + })] : []), + ...(notif.actions.length > 0 ? [Widget.Box({ + className: "actions", + children: notif.actions.map((action) => + Widget.Button({ + label: action.label, + onClicked: () => notif.invoke(action.id), + }) + ), + })] : []), + ], +}); + +export const Notifications = (monitor) => Widget.Window({ + name: `notifications-${monitor}`, + monitor, + anchor: ["top", "right"], + layer: "overlay", + child: Widget.Box({ + vertical: true, + css: "min-width: 350px;", + children: notifications.bind("popups").transform((popups) => + popups.map(Notification) + ), + }), +}); diff --git a/ags-v1/widgets/OSD.js b/ags-v1/widgets/OSD.js new file mode 100644 index 0000000..0ddc4cc --- /dev/null +++ b/ags-v1/widgets/OSD.js @@ -0,0 +1,95 @@ +// ── violet-chaton v2 — OSD widget ─────────────────────────────────────────── +// Overlay volume / brightness avec gradient Mitsuri + +const audio = Service.import("audio"); + +// ── OSD reveal timer ──────────────────────────────────────────────────────── + +let osdTimeout = null; + +const showOSD = (window) => { + window.visible = true; + if (osdTimeout) clearTimeout(osdTimeout); + osdTimeout = setTimeout(() => { + window.visible = false; + }, 1500); +}; + +// ── Volume OSD ────────────────────────────────────────────────────────────── + +const VolumeOSD = () => { + const icon = Widget.Label({ className: "icon" }); + const progress = Widget.ProgressBar(); + const label = Widget.Label({ className: "label" }); + + const box = Widget.Box({ + className: "osd", + children: [icon, progress, label], + connections: [[audio, (self) => { + const vol = audio.speaker?.volume || 0; + const muted = audio.speaker?.isMuted; + icon.label = muted ? "󰝟" : vol > 0.66 ? "󰕾" : vol > 0.33 ? "󰖀" : "󰕿"; + progress.value = vol; + label.label = `${Math.round(vol * 100)}%`; + }, "speaker-changed"]], + }); + + return box; +}; + +// ── Brightness OSD ────────────────────────────────────────────────────────── + +const BrightnessOSD = () => { + const icon = Widget.Label({ className: "icon", label: "󰃞" }); + const progress = Widget.ProgressBar(); + const label = Widget.Label({ className: "label" }); + + const getBrightness = () => { + try { + const max = Number(Utils.exec("brightnessctl max")); + const cur = Number(Utils.exec("brightnessctl get")); + return max > 0 ? cur / max : 0; + } catch { + return 0; + } + }; + + const box = Widget.Box({ + className: "osd", + children: [icon, progress, label], + connections: [[500, (self) => { + const val = getBrightness(); + progress.value = val; + label.label = `${Math.round(val * 100)}%`; + }]], + }); + + return box; +}; + +// ── OSD windows ───────────────────────────────────────────────────────────── + +export const OSD = (monitor) => { + const volumeWin = Widget.Window({ + name: `osd-volume-${monitor}`, + monitor, + anchor: ["bottom"], + layer: "overlay", + visible: false, + child: VolumeOSD(), + }); + + const brightnessWin = Widget.Window({ + name: `osd-brightness-${monitor}`, + monitor, + anchor: ["bottom"], + layer: "overlay", + visible: false, + child: BrightnessOSD(), + }); + + // Auto-show on volume change + Utils.merge([audio.speaker?.bind("volume")], () => showOSD(volumeWin)); + + return [volumeWin, brightnessWin]; +}; diff --git a/ags-v3/.gitignore b/ags-v3/.gitignore new file mode 100644 index 0000000..298eb4d --- /dev/null +++ b/ags-v3/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +@girs/ diff --git a/ags-v3/app.ts b/ags-v3/app.ts new file mode 100644 index 0000000..81bd64d --- /dev/null +++ b/ags-v3/app.ts @@ -0,0 +1,30 @@ +import app from "ags/gtk3/app" +import style from "./style.scss" +import Heartbeat from "./widget/Heartbeat" +import Bar from "./widget/Bar" +import BrainPower from "./widget/panels/BrainPower" + +app.start({ + css: style, + main() { + for (const monitor of app.get_monitors()) { + Heartbeat(monitor) + Bar(monitor) + BrainPower(monitor) + } + }, + requestHandler(request: any, res: (response: any) => void) { + const cmd = String(request) + if (cmd.includes("toggle-brain")) { + const win = app.get_window("brain-power") + if (win) { + win.visible = !win.visible + res("toggled") + } else { + res("window not found") + } + } else { + res(`unknown: '${cmd}'`) + } + }, +}) diff --git a/ags-v3/env.d.ts b/ags-v3/env.d.ts new file mode 100644 index 0000000..792ebfd --- /dev/null +++ b/ags-v3/env.d.ts @@ -0,0 +1,21 @@ +declare const SRC: string + +declare module "inline:*" { + const content: string + export default content +} + +declare module "*.scss" { + const content: string + export default content +} + +declare module "*.blp" { + const content: string + export default content +} + +declare module "*.css" { + const content: string + export default content +} diff --git a/ags-v3/lib/brain.ts b/ags-v3/lib/brain.ts new file mode 100644 index 0000000..54060aa --- /dev/null +++ b/ags-v3/lib/brain.ts @@ -0,0 +1,73 @@ +import GLib from "gi://GLib" + +const BRAIN_ROOT = "/home/tetardtek/Dev/Cortex-Template" + +export interface BrainState { + focus: string + todos: { open: number; done: number; top3: string[] } + session: string | null +} + +function readFile(path: string): string | null { + try { + const [ok, contents] = GLib.file_get_contents(path) + if (ok && contents) { + return new TextDecoder().decode(contents) + } + } catch {} + return null +} + +export function getFocus(): string { + const content = readFile(`${BRAIN_ROOT}/focus.md`) + if (!content) return "no focus" + // skip header lines, get the meat + const lines = content.split("\n").filter( + (l) => !l.startsWith("#") && !l.startsWith(">") && l.trim() !== "" && !l.startsWith("---") + ) + return lines[0]?.trim() || "no focus" +} + +export function getTodos(): { open: number; done: number; top3: string[] } { + const content = readFile(`${BRAIN_ROOT}/todo/README.md`) + if (!content) return { open: 0, done: 0, top3: [] } + + const lines = content.split("\n") + const open: string[] = [] + let done = 0 + + for (const line of lines) { + const trimmed = line.trim() + if (trimmed.startsWith("- [ ]") || trimmed.startsWith("⬜")) { + open.push(trimmed.replace(/^- \[ \] /, "").replace(/^⬜ ?/, "")) + } else if (trimmed.startsWith("- [x]") || trimmed.startsWith("✅")) { + done++ + } + } + + return { + open: open.length, + done, + top3: open.slice(0, 3), + } +} + +export function getSession(): string | null { + try { + const [ok, contents] = GLib.file_get_contents( + GLib.get_home_dir() + "/.claude/session-role" + ) + if (ok && contents) { + return new TextDecoder().decode(contents).trim() + } + } catch {} + return null +} + +export function getBrainState(): BrainState { + return { + focus: getFocus(), + todos: getTodos(), + session: getSession(), + } +} diff --git a/ags-v3/package.json b/ags-v3/package.json new file mode 100644 index 0000000..cbf736c --- /dev/null +++ b/ags-v3/package.json @@ -0,0 +1,10 @@ +{ + "dependencies": { + "ags": "*", + "gnim": "*" + }, + "prettier": { + "semi": false, + "tabWidth": 2 + } +} diff --git a/ags-v3/style.scss b/ags-v3/style.scss new file mode 100644 index 0000000..31f59ff --- /dev/null +++ b/ags-v3/style.scss @@ -0,0 +1,300 @@ +@use "styles/palette" as *; + +* { + font-family: $font; + font-size: 13px; + font-weight: bold; +} + +// ── Layer 0 — Heartbeat ── +window.Heartbeat { + background: transparent; + + .heartbeat-line { + background: linear-gradient( + to right, + rgba(255, 77, 166, 0.0), + rgba(255, 77, 166, 0.6), + rgba(201, 160, 255, 0.8), + rgba(255, 77, 166, 0.6), + rgba(255, 77, 166, 0.0) + ); + min-height: 4px; + } +} + +// ── Layer 1 — Ghost Bar ── +window.Bar { + background: transparent; + + > centerbox { + background: rgba(38, 21, 55, 0.88); + border-radius: $radius; + border: 3px solid rgba(255, 77, 166, 0.60); + margin: 6px 8px; + padding: 0 8px; + min-height: 38px; + } + + .modules-left, + .modules-center, + .modules-right { + padding: 0 4px; + } + + .module { + padding: 0 8px; + color: $text; + } + + .prompt-name { + color: $magenta; + font-size: 14px; + padding: 0 0 0 10px; + } + + .prompt-cursor { + color: $lilac; + font-size: 14px; + font-weight: 900; + } + + .separator { + color: rgba(240, 234, 248, 0.12); + font-size: 11px; + padding: 0 4px; + font-weight: normal; + } + + // ── clock ── + .clock { + color: $magenta; + font-weight: 900; + font-size: 14px; + letter-spacing: 0.04em; + padding: 0 10px; + } + + .date { + color: $lavande; + font-size: 12px; + font-weight: normal; + padding: 0 10px 0 2px; + } + + // ── system stats ── + .cpu { + color: $lavande; + &.warning { color: $champagne; } + &.critical { color: $danger; } + } + + .ram { + color: $magenta; + &.warning { color: $champagne; } + &.critical { color: $danger; } + } + + // ── network ── + .network { + color: $lavande; + font-size: 12px; + padding: 0 8px; + &.disconnected { color: $danger; } + } + + // ── volume ── + .volume { + color: $magenta; + padding: 0 8px; + &.muted { color: rgba(255, 77, 166, 0.30); } + } + + // ── battery ── + .battery { + color: $magenta; + padding: 0 8px; + &.charging { color: $mitsuri; } + &.low { color: $danger; } + &.warning { color: $champagne; } + } + + // ── hover effects ── + .module:hover, + .cpu:hover, + .ram:hover, + .network:hover, + .battery:hover { + color: $lilac; + } + + // ── systray ── + .systray { + padding: 0 4px; + } + + .systray-item { + background: transparent; + border: none; + padding: 0 3px; + min-width: 0; + min-height: 0; + + &:hover { + background: rgba(201, 160, 255, 0.12); + border-radius: $radius-sm; + } + } + + // ── volume interactive ── + button.volume, + button.muted { + background: transparent; + border: none; + padding: 0 8px; + min-width: 0; + min-height: 0; + + label { color: $magenta; } + &.muted label { color: rgba(255, 77, 166, 0.30); } + &:hover label { color: $lilac; } + } + + // ── media ── + .media-module { + padding: 0 4px; + } + + .media { + padding: 0 4px; + } + + .media.paused { + .media-text { + color: rgba(201, 160, 255, 0.50); + font-style: italic; + } + } + + .media-text { + color: $lilac; + font-size: 12px; + font-weight: normal; + padding: 0 6px; + } + + .media-prev, + .media-play, + .media-next { + background: transparent; + border: none; + padding: 0 3px; + min-width: 0; + min-height: 0; + + label { + color: $lilac; + font-size: 13px; + } + + &:hover label { + color: $magenta; + } + } +} + +// ── Layer 3 — Brain Power Panel ── +window.BrainPower { + background: transparent; + + .brain-panel { + background: rgba(26, 14, 39, 0.94); // $crust heavy glass + border-radius: 0 $radius $radius 0; + border: 2px solid rgba(201, 160, 255, 0.40); // $lilac border + border-left: none; + min-width: 380px; + padding: 16px; + } + + .brain-header { + padding: 0 0 12px 0; + } + + .brain-title { + color: $magenta; + font-size: 16px; + font-weight: 900; + letter-spacing: 0.08em; + } + + .brain-close { + background: transparent; + border: none; + min-width: 0; + min-height: 0; + padding: 2px 8px; + + label { + color: $muted; + font-size: 14px; + } + + &:hover label { + color: $danger; + } + } + + .brain-content { + padding: 0 4px; + } + + .brain-section { + padding: 8px 0 4px 0; + } + + .brain-section-title { + color: $lilac; + font-size: 11px; + font-weight: 900; + letter-spacing: 0.12em; + } + + .brain-divider { + background: rgba(90, 56, 117, 0.30); // $surface2 + min-height: 1px; + margin: 8px 0; + } + + .brain-focus { + color: $text; + font-size: 13px; + padding: 4px 0 4px 16px; + } + + .brain-session { + color: $champagne; + font-size: 12px; + font-weight: normal; + padding: 4px 0 4px 16px; + } + + .brain-todos-count { + color: $muted; + font-size: 11px; + font-weight: normal; + } + + .brain-todos-list { + color: $subtext1; + font-size: 12px; + font-weight: normal; + padding: 4px 0 4px 16px; + } + + .brain-terminal-placeholder { + color: $muted; + font-size: 11px; + font-style: italic; + padding: 8px 0 4px 16px; + } +} diff --git a/ags-v3/styles/_bar.scss b/ags-v3/styles/_bar.scss new file mode 100644 index 0000000..7fd969a --- /dev/null +++ b/ags-v3/styles/_bar.scss @@ -0,0 +1,133 @@ +@use "palette" as *; + +// Layer 1 — Ghost bar (hover reveal) +// GTK CSS alpha() must be unquoted strings to pass through SCSS compiler + +window.Bar { + background: transparent; + + > centerbox { + background: rgba(38, 21, 55, 0.88); // $base @ 0.88 + border-radius: $radius; + border: 3px solid rgba(255, 77, 166, 0.60); // $magenta @ 0.60 + margin: 6px 8px; + padding: 0 8px; + min-height: 38px; + } + + .modules-left, + .modules-center, + .modules-right { + padding: 0 4px; + } + + .module { + padding: 0 8px; + color: $text; + + &:hover { + color: $lilac; + } + } + + .separator { + color: rgba(240, 234, 248, 0.12); // $text @ 0.12 + font-size: 11px; + padding: 0 4px; + font-weight: normal; + } + + // ── clock ── + .clock { + color: $magenta; + font-weight: 900; + font-size: 14px; + letter-spacing: 0.04em; + padding: 0 10px; + + &:hover { color: $lilac; } + } + + .date { + color: $lavande; + font-size: 12px; + font-weight: normal; + padding: 0 10px 0 2px; + } + + // ── system ── + .cpu { color: $lavande; } + .ram { color: $magenta; } + .temp { + color: rgba(164, 180, 255, 0.60); // $lavande @ 0.60 + font-size: 11px; + font-weight: normal; + } + + .cpu, .ram, .temp { + &.warning { color: $champagne; } + &.critical { color: $danger; } + } + + // ── network ── + .network { + color: $lavande; + font-size: 12px; + &.disconnected { color: $danger; } + } + + // ── volume ── + .volume { + color: $magenta; + &.muted { color: rgba(255, 77, 166, 0.30); } + } + + // ── battery ── + .battery { + color: $magenta; + &.charging { color: $mitsuri; } + &.low { color: $danger; } + &.warning { color: $champagne; } + } + + // ── media ── + .media { + color: $lilac; + font-size: 12px; + font-weight: normal; + padding: 0 10px; + + &.paused { + color: rgba(201, 160, 255, 0.50); // $lilac @ 0.50 + font-style: italic; + } + } + + // ── systray ── + .systray { + padding: 0 6px; + } + + // ── workspaces ── + .workspaces button { + background: transparent; + color: $muted; + min-width: 22px; + min-height: 22px; + border-radius: $radius-sm; + margin: 2px; + padding: 0; + + &.active { + background: rgba(255, 77, 166, 0.20); + color: $magenta; + border: 1px solid rgba(255, 77, 166, 0.40); + } + + &.occupied { color: $lilac; } + &:hover { + background: rgba(201, 160, 255, 0.12); + color: $lilac; + } + } +} diff --git a/ags-v3/styles/_heartbeat.scss b/ags-v3/styles/_heartbeat.scss new file mode 100644 index 0000000..62dea0a --- /dev/null +++ b/ags-v3/styles/_heartbeat.scss @@ -0,0 +1,21 @@ +@use "palette" as *; + +// Layer 0 — heartbeat line +// Fin trait magenta en haut de l'écran — "le système est vivant" + +window.Heartbeat { + background: transparent; + + > box { + // gradient uses rgba() which SCSS understands natively + background: linear-gradient( + to right, + rgba(255, 77, 166, 0.0), + rgba(255, 77, 166, 0.6), + rgba(201, 160, 255, 0.8), + rgba(255, 77, 166, 0.6), + rgba(255, 77, 166, 0.0) + ); + min-height: 2px; + } +} diff --git a/ags-v3/styles/_palette.scss b/ags-v3/styles/_palette.scss new file mode 100644 index 0000000..db9dd8b --- /dev/null +++ b/ags-v3/styles/_palette.scss @@ -0,0 +1,35 @@ +// ── violet-chaton v2 ────────────────────────────────────────────────────── +// Mitsuri Kanroji inspired — gradient magenta → green +// Ghost Shell edition + +// backgrounds +$crust: #1a0e27; +$base: #261537; +$mantle: #341c4a; +$surface0: #3d2454; +$surface1: #493161; +$surface2: #5a3875; + +// accents +$magenta: #ff4da6; +$lilac: #c9a0ff; +$mitsuri: #9adba8; +$lavande: #a4b4ff; +$champagne: #e8c87a; +$danger: #f25c7a; + +// text +$text: #f0eaf8; +$subtext1: #c4b8d4; +$subtext0: #9a8fad; +$muted: #716686; + +// shared +$font: "Maple Mono NF", "MapleMono Nerd Font", monospace; +$radius: 14px; +$radius-sm: 8px; +$transition: 300ms ease-out; + +// Note: alpha() is a GTK CSS function, not SCSS. +// Use it raw in GTK CSS values, not in SCSS variables. +// For SCSS variables that need transparency, use rgba(). diff --git a/ags-v3/toggle-brain.sh b/ags-v3/toggle-brain.sh new file mode 100755 index 0000000..17bcd6e --- /dev/null +++ b/ags-v3/toggle-brain.sh @@ -0,0 +1,3 @@ +#!/bin/bash +# Toggle Brain Power panel via AGS IPC +ags request "toggle-brain" 2>/dev/null diff --git a/ags-v3/tsconfig.json b/ags-v3/tsconfig.json new file mode 100644 index 0000000..d20cbbc --- /dev/null +++ b/ags-v3/tsconfig.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "strict": true, + "module": "ES2022", + "target": "ES2020", + "lib": ["ES2023"], + "moduleResolution": "Bundler", + // "checkJs": true, + // "allowJs": true, + "jsx": "react-jsx", + "jsxImportSource": "ags/gtk3" + } +} diff --git a/ags-v3/widget/Bar.tsx b/ags-v3/widget/Bar.tsx new file mode 100644 index 0000000..acc38c8 --- /dev/null +++ b/ags-v3/widget/Bar.tsx @@ -0,0 +1,74 @@ +import app from "ags/gtk3/app" +import { Astal, Gtk, Gdk } from "ags/gtk3" +import Clock from "./modules/Clock" +import Battery from "./modules/Battery" +import Volume from "./modules/Volume" +import Network from "./modules/Network" +import SystemStats from "./modules/SystemStats" +import Media from "./modules/Media" +import SysTray from "./modules/SysTray" +import Prompt from "./modules/Prompt" + +export default function Bar(gdkmonitor: Gdk.Monitor) { + const { TOP, LEFT, RIGHT } = Astal.WindowAnchor + + let hideTimeout: number | null = null + + function scheduleHide(win: Astal.Window) { + if (hideTimeout) clearTimeout(hideTimeout) + hideTimeout = setTimeout(() => { + win.visible = false + hideTimeout = null + }, 800) + } + + function cancelHide() { + if (hideTimeout) { + clearTimeout(hideTimeout) + hideTimeout = null + } + } + + return ( + + cancelHide()} + onHoverLost={(self) => { + const win = self.get_toplevel() as Astal.Window + scheduleHide(win) + }} + > + + + + + + + + + + + + + + ) +} diff --git a/ags-v3/widget/Heartbeat.tsx b/ags-v3/widget/Heartbeat.tsx new file mode 100644 index 0000000..a156a80 --- /dev/null +++ b/ags-v3/widget/Heartbeat.tsx @@ -0,0 +1,27 @@ +import app from "ags/gtk3/app" +import { Astal, Gtk, Gdk } from "ags/gtk3" + +export default function Heartbeat(gdkmonitor: Gdk.Monitor) { + const { TOP, LEFT, RIGHT } = Astal.WindowAnchor + + return ( + + { + // reveal the bar + const bar = app.get_window("bar") + if (bar) bar.visible = true + }} + > + + + + ) +} diff --git a/ags-v3/widget/modules/Battery.tsx b/ags-v3/widget/modules/Battery.tsx new file mode 100644 index 0000000..c874541 --- /dev/null +++ b/ags-v3/widget/modules/Battery.tsx @@ -0,0 +1,29 @@ +import AstalBattery from "gi://AstalBattery" +import { createBinding } from "ags" + +export default function Battery() { + const bat = AstalBattery.get_default() + + const text = createBinding(bat, "percentage")((p: number) => { + const pct = Math.round(p * 100) + let icon = "" + if (bat.charging) icon = "󰂄" + else if (pct > 90) icon = "󰁹" + else if (pct > 70) icon = "󰂁" + else if (pct > 50) icon = "󰁿" + else if (pct > 30) icon = "󰁽" + else if (pct > 10) icon = "󰁻" + else icon = "󰂃" + return `${icon} ${pct}%` + }) + + const cls = createBinding(bat, "percentage")((p: number) => { + const pct = Math.round(p * 100) + if (bat.charging) return "battery charging" + if (pct <= 10) return "battery low" + if (pct <= 20) return "battery warning" + return "battery" + }) + + return