- 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
462 lines
17 KiB
JavaScript
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),
|
|
],
|
|
};
|