feat: Ghost Shell v2 — AGS v3 statusbar + violet-chaton v2 palette
- 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
This commit is contained in:
461
ags-v1/config.js
Normal file
461
ags-v1/config.js
Normal file
@@ -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),
|
||||
],
|
||||
};
|
||||
385
ags-v1/css/style.css
Normal file
385
ags-v1/css/style.css
Normal 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;
|
||||
}
|
||||
228
ags-v1/widgets/Bar.js
Normal file
228
ags-v1/widgets/Bar.js
Normal file
@@ -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(),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
99
ags-v1/widgets/Launcher.js
Normal file
99
ags-v1/widgets/Launcher.js
Normal file
@@ -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,
|
||||
],
|
||||
}),
|
||||
});
|
||||
};
|
||||
105
ags-v1/widgets/Notifications.js
Normal file
105
ags-v1/widgets/Notifications.js
Normal file
@@ -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)
|
||||
),
|
||||
}),
|
||||
});
|
||||
95
ags-v1/widgets/OSD.js
Normal file
95
ags-v1/widgets/OSD.js
Normal file
@@ -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];
|
||||
};
|
||||
Reference in New Issue
Block a user