Files
dotfiles-violet-chaton/ags-v1/config.js
Tetardtek-Cortex 932b174c36 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
2026-03-26 06:54:17 +01:00

462 lines
17 KiB
JavaScript

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