// ── 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), ], };