feat: violet-chaton v2 — palette originale, kitty, AGS, Maple Mono NF

Refonte complete du rice. Palette 100% originale (Mitsuri Kanroji inspired),
zero emprunt Dracula/Catppuccin. 50 fichiers, 3200+ lignes.

Palette v2:
- palette.sh source de verite unique (dark + light)
- 5 accents (magenta, lilac, mitsuri, lavande, champagne)
- 4 semantiques derivees, 4 niveaux texte, 6 fonds
- Gradient signature: magenta → lilac → lavande → mitsuri
- Variante Light: fonds lavande, accents assombris WCAG

Terminal:
- kitty (remplace COSMIC Term comme principal)
- Maple Mono NF (cursive italics, ligatures)
- Cursor trail magenta, splits/layouts tiling, undercurl
- Vi-mode zsh avec cursor shape adaptatif

Shell:
- starship 3 lignes (palette nommee, brain_name, battery, sudo)
- zshrc v2 (nouveaux outils, fzf pimp, shell functions, vi-mode)
- Commandes custom: proj, glog, fkill, colors, hotkeys, weather, y

Desktop:
- AGS config (bar 3-pills, OSD gradient, launcher, notifications)
- COSMIC Dark + Light v2 (7 fichiers RON chacun)
- COSMIC Term v2 (color schemes dark/light, Maple Mono NF)
- GTK3/GTK4 dark + light css
- Vivaldi theme v2

Outils:
- +kitty +dust +procs +tokei +sd +hyperfine +gping +Maple Mono NF
- Propagation palette sur: bat, btop, cava, yazi, lazygit, rofi,
  delta, fastfetch, atuin, ls-colors, vivaldi
- Claude Code statusline brain-aware

Docs:
- README v2 complet (palette, structure, raccourcis, commandes)
- help.md v2 (reference exhaustive)
This commit is contained in:
Tetardtek-Cortex
2026-03-26 03:57:18 +01:00
parent 7d4d54c5b8
commit 7e9d12e640
50 changed files with 3250 additions and 1633 deletions

View File

@@ -0,0 +1,34 @@
// ── violet-chaton v2 — AGS config ───────────────────────────────────────────
// Barre + OSD + Launcher + Notifications
// Modulaire : Hyprland = full, COSMIC = bar + OSD + launcher + notifs
import { Bar } from "./widgets/Bar.js";
import { OSD } from "./widgets/OSD.js";
import { Launcher } from "./widgets/Launcher.js";
import { Notifications } from "./widgets/Notifications.js";
// ── Compositor detection ────────────────────────────────────────────────────
const compositor = (() => {
const session = Utils.exec("echo $XDG_CURRENT_DESKTOP").trim();
if (session.includes("Hyprland")) return "hyprland";
if (session.includes("COSMIC")) return "cosmic";
return "unknown";
})();
print(`[violet-chaton] compositor: ${compositor}`);
// ── Windows ─────────────────────────────────────────────────────────────────
const windows = (monitor) => {
const wins = [
Bar(monitor, compositor),
OSD(monitor),
Launcher(monitor),
Notifications(monitor),
];
return wins;
};
export default {
style: `${App.configDir}/css/style.css`,
windows: windows(0).flat(),
};

View File

@@ -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;
}

View File

@@ -0,0 +1,228 @@
// ── violet-chaton v2 — Bar widget ───────────────────────────────────────────
// 3 pills glassmorphism — modulaire selon compositor
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");
// ── 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 = await 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(),
],
}),
}),
});

View File

@@ -0,0 +1,99 @@
// ── violet-chaton v2 — Launcher widget ──────────────────────────────────────
// App launcher avec recherche fuzzy
const applications = await 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,
],
}),
});
};

View File

@@ -0,0 +1,105 @@
// ── violet-chaton v2 — Notifications widget ─────────────────────────────────
// Popup notifications stylees — urgency-aware
const notifications = await 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)
),
}),
});

View File

@@ -0,0 +1,95 @@
// ── violet-chaton v2 — OSD widget ───────────────────────────────────────────
// Overlay volume / brightness avec gradient Mitsuri
const audio = await 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];
};