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];
|
||||||
|
};
|
||||||
2
ags-v3/.gitignore
vendored
Normal file
2
ags-v3/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules/
|
||||||
|
@girs/
|
||||||
30
ags-v3/app.ts
Normal file
30
ags-v3/app.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import app from "ags/gtk3/app"
|
||||||
|
import style from "./style.scss"
|
||||||
|
import Heartbeat from "./widget/Heartbeat"
|
||||||
|
import Bar from "./widget/Bar"
|
||||||
|
import BrainPower from "./widget/panels/BrainPower"
|
||||||
|
|
||||||
|
app.start({
|
||||||
|
css: style,
|
||||||
|
main() {
|
||||||
|
for (const monitor of app.get_monitors()) {
|
||||||
|
Heartbeat(monitor)
|
||||||
|
Bar(monitor)
|
||||||
|
BrainPower(monitor)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
requestHandler(request: any, res: (response: any) => void) {
|
||||||
|
const cmd = String(request)
|
||||||
|
if (cmd.includes("toggle-brain")) {
|
||||||
|
const win = app.get_window("brain-power")
|
||||||
|
if (win) {
|
||||||
|
win.visible = !win.visible
|
||||||
|
res("toggled")
|
||||||
|
} else {
|
||||||
|
res("window not found")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
res(`unknown: '${cmd}'`)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
21
ags-v3/env.d.ts
vendored
Normal file
21
ags-v3/env.d.ts
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
declare const SRC: string
|
||||||
|
|
||||||
|
declare module "inline:*" {
|
||||||
|
const content: string
|
||||||
|
export default content
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "*.scss" {
|
||||||
|
const content: string
|
||||||
|
export default content
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "*.blp" {
|
||||||
|
const content: string
|
||||||
|
export default content
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "*.css" {
|
||||||
|
const content: string
|
||||||
|
export default content
|
||||||
|
}
|
||||||
73
ags-v3/lib/brain.ts
Normal file
73
ags-v3/lib/brain.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import GLib from "gi://GLib"
|
||||||
|
|
||||||
|
const BRAIN_ROOT = "/home/tetardtek/Dev/Cortex-Template"
|
||||||
|
|
||||||
|
export interface BrainState {
|
||||||
|
focus: string
|
||||||
|
todos: { open: number; done: number; top3: string[] }
|
||||||
|
session: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
function readFile(path: string): string | null {
|
||||||
|
try {
|
||||||
|
const [ok, contents] = GLib.file_get_contents(path)
|
||||||
|
if (ok && contents) {
|
||||||
|
return new TextDecoder().decode(contents)
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFocus(): string {
|
||||||
|
const content = readFile(`${BRAIN_ROOT}/focus.md`)
|
||||||
|
if (!content) return "no focus"
|
||||||
|
// skip header lines, get the meat
|
||||||
|
const lines = content.split("\n").filter(
|
||||||
|
(l) => !l.startsWith("#") && !l.startsWith(">") && l.trim() !== "" && !l.startsWith("---")
|
||||||
|
)
|
||||||
|
return lines[0]?.trim() || "no focus"
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTodos(): { open: number; done: number; top3: string[] } {
|
||||||
|
const content = readFile(`${BRAIN_ROOT}/todo/README.md`)
|
||||||
|
if (!content) return { open: 0, done: 0, top3: [] }
|
||||||
|
|
||||||
|
const lines = content.split("\n")
|
||||||
|
const open: string[] = []
|
||||||
|
let done = 0
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim()
|
||||||
|
if (trimmed.startsWith("- [ ]") || trimmed.startsWith("⬜")) {
|
||||||
|
open.push(trimmed.replace(/^- \[ \] /, "").replace(/^⬜ ?/, ""))
|
||||||
|
} else if (trimmed.startsWith("- [x]") || trimmed.startsWith("✅")) {
|
||||||
|
done++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
open: open.length,
|
||||||
|
done,
|
||||||
|
top3: open.slice(0, 3),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSession(): string | null {
|
||||||
|
try {
|
||||||
|
const [ok, contents] = GLib.file_get_contents(
|
||||||
|
GLib.get_home_dir() + "/.claude/session-role"
|
||||||
|
)
|
||||||
|
if (ok && contents) {
|
||||||
|
return new TextDecoder().decode(contents).trim()
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBrainState(): BrainState {
|
||||||
|
return {
|
||||||
|
focus: getFocus(),
|
||||||
|
todos: getTodos(),
|
||||||
|
session: getSession(),
|
||||||
|
}
|
||||||
|
}
|
||||||
10
ags-v3/package.json
Normal file
10
ags-v3/package.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"ags": "*",
|
||||||
|
"gnim": "*"
|
||||||
|
},
|
||||||
|
"prettier": {
|
||||||
|
"semi": false,
|
||||||
|
"tabWidth": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
300
ags-v3/style.scss
Normal file
300
ags-v3/style.scss
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
@use "styles/palette" as *;
|
||||||
|
|
||||||
|
* {
|
||||||
|
font-family: $font;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Layer 0 — Heartbeat ──
|
||||||
|
window.Heartbeat {
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
.heartbeat-line {
|
||||||
|
background: linear-gradient(
|
||||||
|
to right,
|
||||||
|
rgba(255, 77, 166, 0.0),
|
||||||
|
rgba(255, 77, 166, 0.6),
|
||||||
|
rgba(201, 160, 255, 0.8),
|
||||||
|
rgba(255, 77, 166, 0.6),
|
||||||
|
rgba(255, 77, 166, 0.0)
|
||||||
|
);
|
||||||
|
min-height: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Layer 1 — Ghost Bar ──
|
||||||
|
window.Bar {
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
> centerbox {
|
||||||
|
background: rgba(38, 21, 55, 0.88);
|
||||||
|
border-radius: $radius;
|
||||||
|
border: 3px solid rgba(255, 77, 166, 0.60);
|
||||||
|
margin: 6px 8px;
|
||||||
|
padding: 0 8px;
|
||||||
|
min-height: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modules-left,
|
||||||
|
.modules-center,
|
||||||
|
.modules-right {
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module {
|
||||||
|
padding: 0 8px;
|
||||||
|
color: $text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-name {
|
||||||
|
color: $magenta;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 0 0 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-cursor {
|
||||||
|
color: $lilac;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
color: rgba(240, 234, 248, 0.12);
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 0 4px;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── clock ──
|
||||||
|
.clock {
|
||||||
|
color: $magenta;
|
||||||
|
font-weight: 900;
|
||||||
|
font-size: 14px;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
color: $lavande;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: normal;
|
||||||
|
padding: 0 10px 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── system stats ──
|
||||||
|
.cpu {
|
||||||
|
color: $lavande;
|
||||||
|
&.warning { color: $champagne; }
|
||||||
|
&.critical { color: $danger; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.ram {
|
||||||
|
color: $magenta;
|
||||||
|
&.warning { color: $champagne; }
|
||||||
|
&.critical { color: $danger; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── network ──
|
||||||
|
.network {
|
||||||
|
color: $lavande;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 0 8px;
|
||||||
|
&.disconnected { color: $danger; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── volume ──
|
||||||
|
.volume {
|
||||||
|
color: $magenta;
|
||||||
|
padding: 0 8px;
|
||||||
|
&.muted { color: rgba(255, 77, 166, 0.30); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── battery ──
|
||||||
|
.battery {
|
||||||
|
color: $magenta;
|
||||||
|
padding: 0 8px;
|
||||||
|
&.charging { color: $mitsuri; }
|
||||||
|
&.low { color: $danger; }
|
||||||
|
&.warning { color: $champagne; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── hover effects ──
|
||||||
|
.module:hover,
|
||||||
|
.cpu:hover,
|
||||||
|
.ram:hover,
|
||||||
|
.network:hover,
|
||||||
|
.battery:hover {
|
||||||
|
color: $lilac;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── systray ──
|
||||||
|
.systray {
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.systray-item {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
padding: 0 3px;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(201, 160, 255, 0.12);
|
||||||
|
border-radius: $radius-sm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── volume interactive ──
|
||||||
|
button.volume,
|
||||||
|
button.muted {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
padding: 0 8px;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
|
||||||
|
label { color: $magenta; }
|
||||||
|
&.muted label { color: rgba(255, 77, 166, 0.30); }
|
||||||
|
&:hover label { color: $lilac; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── media ──
|
||||||
|
.media-module {
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media {
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media.paused {
|
||||||
|
.media-text {
|
||||||
|
color: rgba(201, 160, 255, 0.50);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-text {
|
||||||
|
color: $lilac;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: normal;
|
||||||
|
padding: 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-prev,
|
||||||
|
.media-play,
|
||||||
|
.media-next {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
padding: 0 3px;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
|
||||||
|
label {
|
||||||
|
color: $lilac;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover label {
|
||||||
|
color: $magenta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Layer 3 — Brain Power Panel ──
|
||||||
|
window.BrainPower {
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
.brain-panel {
|
||||||
|
background: rgba(26, 14, 39, 0.94); // $crust heavy glass
|
||||||
|
border-radius: 0 $radius $radius 0;
|
||||||
|
border: 2px solid rgba(201, 160, 255, 0.40); // $lilac border
|
||||||
|
border-left: none;
|
||||||
|
min-width: 380px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brain-header {
|
||||||
|
padding: 0 0 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brain-title {
|
||||||
|
color: $magenta;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brain-close {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 2px 8px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
color: $muted;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover label {
|
||||||
|
color: $danger;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.brain-content {
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brain-section {
|
||||||
|
padding: 8px 0 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brain-section-title {
|
||||||
|
color: $lilac;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brain-divider {
|
||||||
|
background: rgba(90, 56, 117, 0.30); // $surface2
|
||||||
|
min-height: 1px;
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brain-focus {
|
||||||
|
color: $text;
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 4px 0 4px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brain-session {
|
||||||
|
color: $champagne;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: normal;
|
||||||
|
padding: 4px 0 4px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brain-todos-count {
|
||||||
|
color: $muted;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brain-todos-list {
|
||||||
|
color: $subtext1;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: normal;
|
||||||
|
padding: 4px 0 4px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brain-terminal-placeholder {
|
||||||
|
color: $muted;
|
||||||
|
font-size: 11px;
|
||||||
|
font-style: italic;
|
||||||
|
padding: 8px 0 4px 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
133
ags-v3/styles/_bar.scss
Normal file
133
ags-v3/styles/_bar.scss
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
@use "palette" as *;
|
||||||
|
|
||||||
|
// Layer 1 — Ghost bar (hover reveal)
|
||||||
|
// GTK CSS alpha() must be unquoted strings to pass through SCSS compiler
|
||||||
|
|
||||||
|
window.Bar {
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
> centerbox {
|
||||||
|
background: rgba(38, 21, 55, 0.88); // $base @ 0.88
|
||||||
|
border-radius: $radius;
|
||||||
|
border: 3px solid rgba(255, 77, 166, 0.60); // $magenta @ 0.60
|
||||||
|
margin: 6px 8px;
|
||||||
|
padding: 0 8px;
|
||||||
|
min-height: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modules-left,
|
||||||
|
.modules-center,
|
||||||
|
.modules-right {
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module {
|
||||||
|
padding: 0 8px;
|
||||||
|
color: $text;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $lilac;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
color: rgba(240, 234, 248, 0.12); // $text @ 0.12
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 0 4px;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── clock ──
|
||||||
|
.clock {
|
||||||
|
color: $magenta;
|
||||||
|
font-weight: 900;
|
||||||
|
font-size: 14px;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
padding: 0 10px;
|
||||||
|
|
||||||
|
&:hover { color: $lilac; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
color: $lavande;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: normal;
|
||||||
|
padding: 0 10px 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── system ──
|
||||||
|
.cpu { color: $lavande; }
|
||||||
|
.ram { color: $magenta; }
|
||||||
|
.temp {
|
||||||
|
color: rgba(164, 180, 255, 0.60); // $lavande @ 0.60
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cpu, .ram, .temp {
|
||||||
|
&.warning { color: $champagne; }
|
||||||
|
&.critical { color: $danger; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── network ──
|
||||||
|
.network {
|
||||||
|
color: $lavande;
|
||||||
|
font-size: 12px;
|
||||||
|
&.disconnected { color: $danger; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── volume ──
|
||||||
|
.volume {
|
||||||
|
color: $magenta;
|
||||||
|
&.muted { color: rgba(255, 77, 166, 0.30); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── battery ──
|
||||||
|
.battery {
|
||||||
|
color: $magenta;
|
||||||
|
&.charging { color: $mitsuri; }
|
||||||
|
&.low { color: $danger; }
|
||||||
|
&.warning { color: $champagne; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── media ──
|
||||||
|
.media {
|
||||||
|
color: $lilac;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: normal;
|
||||||
|
padding: 0 10px;
|
||||||
|
|
||||||
|
&.paused {
|
||||||
|
color: rgba(201, 160, 255, 0.50); // $lilac @ 0.50
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── systray ──
|
||||||
|
.systray {
|
||||||
|
padding: 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── workspaces ──
|
||||||
|
.workspaces button {
|
||||||
|
background: transparent;
|
||||||
|
color: $muted;
|
||||||
|
min-width: 22px;
|
||||||
|
min-height: 22px;
|
||||||
|
border-radius: $radius-sm;
|
||||||
|
margin: 2px;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: rgba(255, 77, 166, 0.20);
|
||||||
|
color: $magenta;
|
||||||
|
border: 1px solid rgba(255, 77, 166, 0.40);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.occupied { color: $lilac; }
|
||||||
|
&:hover {
|
||||||
|
background: rgba(201, 160, 255, 0.12);
|
||||||
|
color: $lilac;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
ags-v3/styles/_heartbeat.scss
Normal file
21
ags-v3/styles/_heartbeat.scss
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
@use "palette" as *;
|
||||||
|
|
||||||
|
// Layer 0 — heartbeat line
|
||||||
|
// Fin trait magenta en haut de l'écran — "le système est vivant"
|
||||||
|
|
||||||
|
window.Heartbeat {
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
> box {
|
||||||
|
// gradient uses rgba() which SCSS understands natively
|
||||||
|
background: linear-gradient(
|
||||||
|
to right,
|
||||||
|
rgba(255, 77, 166, 0.0),
|
||||||
|
rgba(255, 77, 166, 0.6),
|
||||||
|
rgba(201, 160, 255, 0.8),
|
||||||
|
rgba(255, 77, 166, 0.6),
|
||||||
|
rgba(255, 77, 166, 0.0)
|
||||||
|
);
|
||||||
|
min-height: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
35
ags-v3/styles/_palette.scss
Normal file
35
ags-v3/styles/_palette.scss
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
// ── violet-chaton v2 ──────────────────────────────────────────────────────
|
||||||
|
// Mitsuri Kanroji inspired — gradient magenta → green
|
||||||
|
// Ghost Shell edition
|
||||||
|
|
||||||
|
// backgrounds
|
||||||
|
$crust: #1a0e27;
|
||||||
|
$base: #261537;
|
||||||
|
$mantle: #341c4a;
|
||||||
|
$surface0: #3d2454;
|
||||||
|
$surface1: #493161;
|
||||||
|
$surface2: #5a3875;
|
||||||
|
|
||||||
|
// accents
|
||||||
|
$magenta: #ff4da6;
|
||||||
|
$lilac: #c9a0ff;
|
||||||
|
$mitsuri: #9adba8;
|
||||||
|
$lavande: #a4b4ff;
|
||||||
|
$champagne: #e8c87a;
|
||||||
|
$danger: #f25c7a;
|
||||||
|
|
||||||
|
// text
|
||||||
|
$text: #f0eaf8;
|
||||||
|
$subtext1: #c4b8d4;
|
||||||
|
$subtext0: #9a8fad;
|
||||||
|
$muted: #716686;
|
||||||
|
|
||||||
|
// shared
|
||||||
|
$font: "Maple Mono NF", "MapleMono Nerd Font", monospace;
|
||||||
|
$radius: 14px;
|
||||||
|
$radius-sm: 8px;
|
||||||
|
$transition: 300ms ease-out;
|
||||||
|
|
||||||
|
// Note: alpha() is a GTK CSS function, not SCSS.
|
||||||
|
// Use it raw in GTK CSS values, not in SCSS variables.
|
||||||
|
// For SCSS variables that need transparency, use rgba().
|
||||||
3
ags-v3/toggle-brain.sh
Executable file
3
ags-v3/toggle-brain.sh
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Toggle Brain Power panel via AGS IPC
|
||||||
|
ags request "toggle-brain" 2>/dev/null
|
||||||
14
ags-v3/tsconfig.json
Normal file
14
ags-v3/tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true,
|
||||||
|
"module": "ES2022",
|
||||||
|
"target": "ES2020",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
// "checkJs": true,
|
||||||
|
// "allowJs": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"jsxImportSource": "ags/gtk3"
|
||||||
|
}
|
||||||
|
}
|
||||||
74
ags-v3/widget/Bar.tsx
Normal file
74
ags-v3/widget/Bar.tsx
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import app from "ags/gtk3/app"
|
||||||
|
import { Astal, Gtk, Gdk } from "ags/gtk3"
|
||||||
|
import Clock from "./modules/Clock"
|
||||||
|
import Battery from "./modules/Battery"
|
||||||
|
import Volume from "./modules/Volume"
|
||||||
|
import Network from "./modules/Network"
|
||||||
|
import SystemStats from "./modules/SystemStats"
|
||||||
|
import Media from "./modules/Media"
|
||||||
|
import SysTray from "./modules/SysTray"
|
||||||
|
import Prompt from "./modules/Prompt"
|
||||||
|
|
||||||
|
export default function Bar(gdkmonitor: Gdk.Monitor) {
|
||||||
|
const { TOP, LEFT, RIGHT } = Astal.WindowAnchor
|
||||||
|
|
||||||
|
let hideTimeout: number | null = null
|
||||||
|
|
||||||
|
function scheduleHide(win: Astal.Window) {
|
||||||
|
if (hideTimeout) clearTimeout(hideTimeout)
|
||||||
|
hideTimeout = setTimeout(() => {
|
||||||
|
win.visible = false
|
||||||
|
hideTimeout = null
|
||||||
|
}, 800)
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelHide() {
|
||||||
|
if (hideTimeout) {
|
||||||
|
clearTimeout(hideTimeout)
|
||||||
|
hideTimeout = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<window
|
||||||
|
class="Bar"
|
||||||
|
name="bar"
|
||||||
|
visible={false}
|
||||||
|
gdkmonitor={gdkmonitor}
|
||||||
|
exclusivity={Astal.Exclusivity.EXCLUSIVE}
|
||||||
|
anchor={TOP | LEFT | RIGHT}
|
||||||
|
application={app}
|
||||||
|
layer={Astal.Layer.TOP}
|
||||||
|
>
|
||||||
|
<eventbox
|
||||||
|
onHover={() => cancelHide()}
|
||||||
|
onHoverLost={(self) => {
|
||||||
|
const win = self.get_toplevel() as Astal.Window
|
||||||
|
scheduleHide(win)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<centerbox>
|
||||||
|
<box $type="start" class="modules-left" halign={Gtk.Align.START}>
|
||||||
|
<Prompt />
|
||||||
|
<label class="separator" label="│" />
|
||||||
|
<SystemStats />
|
||||||
|
<label class="separator" label="│" />
|
||||||
|
<Media />
|
||||||
|
</box>
|
||||||
|
<box $type="center" class="modules-center">
|
||||||
|
<Clock />
|
||||||
|
</box>
|
||||||
|
<box $type="end" class="modules-right" halign={Gtk.Align.END}>
|
||||||
|
<SysTray />
|
||||||
|
<label class="separator" label="│" />
|
||||||
|
<Network />
|
||||||
|
<label class="separator" label="│" />
|
||||||
|
<Volume />
|
||||||
|
<label class="separator" label="│" />
|
||||||
|
<Battery />
|
||||||
|
</box>
|
||||||
|
</centerbox>
|
||||||
|
</eventbox>
|
||||||
|
</window>
|
||||||
|
)
|
||||||
|
}
|
||||||
27
ags-v3/widget/Heartbeat.tsx
Normal file
27
ags-v3/widget/Heartbeat.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import app from "ags/gtk3/app"
|
||||||
|
import { Astal, Gtk, Gdk } from "ags/gtk3"
|
||||||
|
|
||||||
|
export default function Heartbeat(gdkmonitor: Gdk.Monitor) {
|
||||||
|
const { TOP, LEFT, RIGHT } = Astal.WindowAnchor
|
||||||
|
|
||||||
|
return (
|
||||||
|
<window
|
||||||
|
class="Heartbeat"
|
||||||
|
gdkmonitor={gdkmonitor}
|
||||||
|
exclusivity={Astal.Exclusivity.NONE}
|
||||||
|
anchor={TOP | LEFT | RIGHT}
|
||||||
|
application={app}
|
||||||
|
layer={Astal.Layer.OVERLAY}
|
||||||
|
>
|
||||||
|
<eventbox
|
||||||
|
onHover={() => {
|
||||||
|
// reveal the bar
|
||||||
|
const bar = app.get_window("bar")
|
||||||
|
if (bar) bar.visible = true
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<box class="heartbeat-line" />
|
||||||
|
</eventbox>
|
||||||
|
</window>
|
||||||
|
)
|
||||||
|
}
|
||||||
29
ags-v3/widget/modules/Battery.tsx
Normal file
29
ags-v3/widget/modules/Battery.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import AstalBattery from "gi://AstalBattery"
|
||||||
|
import { createBinding } from "ags"
|
||||||
|
|
||||||
|
export default function Battery() {
|
||||||
|
const bat = AstalBattery.get_default()
|
||||||
|
|
||||||
|
const text = createBinding(bat, "percentage")((p: number) => {
|
||||||
|
const pct = Math.round(p * 100)
|
||||||
|
let icon = ""
|
||||||
|
if (bat.charging) icon = ""
|
||||||
|
else if (pct > 90) icon = ""
|
||||||
|
else if (pct > 70) icon = ""
|
||||||
|
else if (pct > 50) icon = ""
|
||||||
|
else if (pct > 30) icon = ""
|
||||||
|
else if (pct > 10) icon = ""
|
||||||
|
else icon = ""
|
||||||
|
return `${icon} ${pct}%`
|
||||||
|
})
|
||||||
|
|
||||||
|
const cls = createBinding(bat, "percentage")((p: number) => {
|
||||||
|
const pct = Math.round(p * 100)
|
||||||
|
if (bat.charging) return "battery charging"
|
||||||
|
if (pct <= 10) return "battery low"
|
||||||
|
if (pct <= 20) return "battery warning"
|
||||||
|
return "battery"
|
||||||
|
})
|
||||||
|
|
||||||
|
return <label class={cls} label={text} />
|
||||||
|
}
|
||||||
32
ags-v3/widget/modules/Clock.tsx
Normal file
32
ags-v3/widget/modules/Clock.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { createPoll } from "ags/time"
|
||||||
|
|
||||||
|
function formatTime(): string {
|
||||||
|
const now = new Date()
|
||||||
|
return now.toLocaleTimeString("fr-FR", {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(): string {
|
||||||
|
const now = new Date()
|
||||||
|
return now.toLocaleDateString("fr-FR", {
|
||||||
|
weekday: "short",
|
||||||
|
day: "numeric",
|
||||||
|
month: "short",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Clock() {
|
||||||
|
const time = createPoll("", 1000, () => formatTime())
|
||||||
|
const date = createPoll("", 60000, () => formatDate())
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box>
|
||||||
|
<label class="clock" label={time} />
|
||||||
|
<label class="separator" label="│" />
|
||||||
|
<label class="date" label={date} />
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
}
|
||||||
60
ags-v3/widget/modules/Media.tsx
Normal file
60
ags-v3/widget/modules/Media.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import AstalMpris from "gi://AstalMpris"
|
||||||
|
import { createBinding, For } from "ags"
|
||||||
|
|
||||||
|
export default function Media() {
|
||||||
|
const mpris = AstalMpris.get_default()
|
||||||
|
const players = createBinding(mpris, "players")
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box class="media-module">
|
||||||
|
<For each={players}>
|
||||||
|
{(player) => {
|
||||||
|
const title = createBinding(player, "title")
|
||||||
|
const artist = createBinding(player, "artist")
|
||||||
|
const status = createBinding(player, "playbackStatus")
|
||||||
|
|
||||||
|
const icon = status((s: AstalMpris.PlaybackStatus) =>
|
||||||
|
s === AstalMpris.PlaybackStatus.PLAYING ? "" : ""
|
||||||
|
)
|
||||||
|
|
||||||
|
const label = title((t: string) => {
|
||||||
|
const a = player.artist
|
||||||
|
const display = a ? `${a} — ${t}` : t
|
||||||
|
// truncate long titles
|
||||||
|
return display.length > 40
|
||||||
|
? display.substring(0, 37) + "..."
|
||||||
|
: display
|
||||||
|
})
|
||||||
|
|
||||||
|
const cls = status((s: AstalMpris.PlaybackStatus) =>
|
||||||
|
s === AstalMpris.PlaybackStatus.PLAYING ? "media" : "media paused"
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box class={cls}>
|
||||||
|
<button
|
||||||
|
class="media-prev"
|
||||||
|
onClicked={() => player.previous()}
|
||||||
|
>
|
||||||
|
<label label="" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="media-play"
|
||||||
|
onClicked={() => player.play_pause()}
|
||||||
|
>
|
||||||
|
<label label={icon} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="media-next"
|
||||||
|
onClicked={() => player.next()}
|
||||||
|
>
|
||||||
|
<label label="" />
|
||||||
|
</button>
|
||||||
|
<label class="media-text" label={label} />
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
}
|
||||||
31
ags-v3/widget/modules/Network.tsx
Normal file
31
ags-v3/widget/modules/Network.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import AstalNetwork from "gi://AstalNetwork"
|
||||||
|
import { createBinding } from "ags"
|
||||||
|
|
||||||
|
export default function Network() {
|
||||||
|
const net = AstalNetwork.get_default()
|
||||||
|
|
||||||
|
const text = createBinding(net, "primary")((type: AstalNetwork.Primary) => {
|
||||||
|
if (type === AstalNetwork.Primary.WIFI) {
|
||||||
|
const wifi = net.wifi
|
||||||
|
if (!wifi) return " offline"
|
||||||
|
const ssid = wifi.ssid || "wifi"
|
||||||
|
const strength = wifi.strength
|
||||||
|
let icon = ""
|
||||||
|
if (strength > 80) icon = ""
|
||||||
|
else if (strength > 60) icon = ""
|
||||||
|
else if (strength > 40) icon = ""
|
||||||
|
else if (strength > 20) icon = ""
|
||||||
|
return `${icon} ${ssid}`
|
||||||
|
}
|
||||||
|
if (type === AstalNetwork.Primary.WIRED) return " eth"
|
||||||
|
return " offline"
|
||||||
|
})
|
||||||
|
|
||||||
|
const cls = createBinding(net, "primary")((type: AstalNetwork.Primary) => {
|
||||||
|
if (type === AstalNetwork.Primary.WIFI) return "network wifi"
|
||||||
|
if (type === AstalNetwork.Primary.WIRED) return "network wired"
|
||||||
|
return "network disconnected"
|
||||||
|
})
|
||||||
|
|
||||||
|
return <label class={cls} label={text} />
|
||||||
|
}
|
||||||
15
ags-v3/widget/modules/Prompt.tsx
Normal file
15
ags-v3/widget/modules/Prompt.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { createPoll } from "ags/time"
|
||||||
|
|
||||||
|
export default function Prompt() {
|
||||||
|
const cursor = createPoll("_", 600, () => {
|
||||||
|
// alternate between _ and empty to create blink
|
||||||
|
return Date.now() % 1200 < 600 ? "_" : " "
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box>
|
||||||
|
<label class="prompt-name" label="tetardtek" />
|
||||||
|
<label class="prompt-cursor" label={cursor} />
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
}
|
||||||
24
ags-v3/widget/modules/SysTray.tsx
Normal file
24
ags-v3/widget/modules/SysTray.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import AstalTray from "gi://AstalTray"
|
||||||
|
import Gtk from "gi://Gtk?version=3.0"
|
||||||
|
import { createBinding, For } from "ags"
|
||||||
|
|
||||||
|
export default function SysTray() {
|
||||||
|
const tray = AstalTray.get_default()
|
||||||
|
const items = createBinding(tray, "items")
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box class="systray">
|
||||||
|
<For each={items}>
|
||||||
|
{(item) => (
|
||||||
|
<button
|
||||||
|
class="systray-item"
|
||||||
|
tooltipText={createBinding(item, "tooltipMarkup")}
|
||||||
|
onClicked={() => item.activate(0, 0)}
|
||||||
|
>
|
||||||
|
<icon pixelSize={16} gicon={createBinding(item, "gicon")} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
}
|
||||||
31
ags-v3/widget/modules/SystemStats.tsx
Normal file
31
ags-v3/widget/modules/SystemStats.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { createPoll } from "ags/time"
|
||||||
|
import { exec } from "ags/process"
|
||||||
|
|
||||||
|
function getCpuUsage(): string {
|
||||||
|
try {
|
||||||
|
const out = exec("bash -c \"top -bn1 | grep 'Cpu(s)' | awk '{print 100 - $8}'\"")
|
||||||
|
return `${Math.round(parseFloat(out))}`
|
||||||
|
} catch {
|
||||||
|
return "?"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRamUsage(): string {
|
||||||
|
try {
|
||||||
|
return exec("bash -c \"free | awk '/Mem:/ {printf \\\"%.0f\\\", $3/$2 * 100}'\"")
|
||||||
|
} catch {
|
||||||
|
return "?"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SystemStats() {
|
||||||
|
const cpu = createPoll("0", 3000, () => getCpuUsage())
|
||||||
|
const ram = createPoll("0", 5000, () => getRamUsage())
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box>
|
||||||
|
<label class="cpu module" label={cpu((v: string) => ` ${v}%`)} />
|
||||||
|
<label class="ram module" label={ram((v: string) => ` ${v}%`)} />
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
}
|
||||||
41
ags-v3/widget/modules/Volume.tsx
Normal file
41
ags-v3/widget/modules/Volume.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import AstalWp from "gi://AstalWp"
|
||||||
|
import { createBinding } from "ags"
|
||||||
|
|
||||||
|
export default function Volume() {
|
||||||
|
const speaker = AstalWp.get_default()!.defaultSpeaker!
|
||||||
|
|
||||||
|
const text = createBinding(speaker, "volume")((v: number) => {
|
||||||
|
const pct = Math.round(v * 100)
|
||||||
|
const muted = speaker.mute
|
||||||
|
let icon = ""
|
||||||
|
if (muted) icon = ""
|
||||||
|
else if (v > 0.66) icon = ""
|
||||||
|
else if (v > 0.33) icon = ""
|
||||||
|
else if (v > 0) icon = ""
|
||||||
|
else icon = ""
|
||||||
|
return `${icon} ${pct}%`
|
||||||
|
})
|
||||||
|
|
||||||
|
const cls = createBinding(speaker, "mute")((m: boolean) =>
|
||||||
|
m ? "volume muted" : "volume"
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
class={cls}
|
||||||
|
onClicked={() => {
|
||||||
|
speaker.mute = !speaker.mute
|
||||||
|
}}
|
||||||
|
onScroll={(_self: any, event: any) => {
|
||||||
|
const delta = event.delta_y
|
||||||
|
if (delta < 0) {
|
||||||
|
speaker.volume = Math.min(speaker.volume + 0.05, 1.0)
|
||||||
|
} else {
|
||||||
|
speaker.volume = Math.max(speaker.volume - 0.05, 0.0)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<label label={text} />
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
104
ags-v3/widget/panels/BrainPower.tsx
Normal file
104
ags-v3/widget/panels/BrainPower.tsx
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import app from "ags/gtk3/app"
|
||||||
|
import { Astal, Gtk, Gdk } from "ags/gtk3"
|
||||||
|
import { createPoll } from "ags/time"
|
||||||
|
import { getBrainState } from "../../lib/brain"
|
||||||
|
|
||||||
|
function BrainContent() {
|
||||||
|
const state = createPoll("", 10000, () => JSON.stringify(getBrainState()))
|
||||||
|
|
||||||
|
const focus = state((s: string) => {
|
||||||
|
try { return JSON.parse(s).focus } catch { return "..." }
|
||||||
|
})
|
||||||
|
|
||||||
|
const todosLabel = state((s: string) => {
|
||||||
|
try {
|
||||||
|
const t = JSON.parse(s).todos
|
||||||
|
return `${t.open} ouverts / ${t.done} faits`
|
||||||
|
} catch { return "..." }
|
||||||
|
})
|
||||||
|
|
||||||
|
const todosList = state((s: string) => {
|
||||||
|
try {
|
||||||
|
const t = JSON.parse(s).todos.top3
|
||||||
|
return t.map((item: string) => ` ⬜ ${item}`).join("\n") || " rien"
|
||||||
|
} catch { return "..." }
|
||||||
|
})
|
||||||
|
|
||||||
|
const session = state((s: string) => {
|
||||||
|
try {
|
||||||
|
const sess = JSON.parse(s).session
|
||||||
|
return sess || "aucune"
|
||||||
|
} catch { return "..." }
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box orientation={Gtk.Orientation.VERTICAL} class="brain-content">
|
||||||
|
<box class="brain-section">
|
||||||
|
<label class="brain-section-title" label=" FOCUS" xalign={0} />
|
||||||
|
</box>
|
||||||
|
<label class="brain-focus" label={focus} xalign={0} wrap />
|
||||||
|
|
||||||
|
<box class="brain-divider" />
|
||||||
|
|
||||||
|
<box class="brain-section">
|
||||||
|
<label class="brain-section-title" label=" SESSION" xalign={0} />
|
||||||
|
</box>
|
||||||
|
<label class="brain-session" label={session} xalign={0} />
|
||||||
|
|
||||||
|
<box class="brain-divider" />
|
||||||
|
|
||||||
|
<box class="brain-section">
|
||||||
|
<label class="brain-section-title" label=" TODOS" xalign={0} />
|
||||||
|
<label class="brain-todos-count" label={todosLabel} xalign={1} hexpand />
|
||||||
|
</box>
|
||||||
|
<label class="brain-todos-list" label={todosList} xalign={0} />
|
||||||
|
|
||||||
|
<box class="brain-divider" />
|
||||||
|
|
||||||
|
<box class="brain-section">
|
||||||
|
<label class="brain-section-title" label=" TERMINAL" xalign={0} />
|
||||||
|
</box>
|
||||||
|
<label class="brain-terminal-placeholder" label=" bientôt — agent brain-hud" xalign={0} />
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BrainPower(gdkmonitor: Gdk.Monitor) {
|
||||||
|
const { TOP, LEFT, BOTTOM } = Astal.WindowAnchor
|
||||||
|
|
||||||
|
return (
|
||||||
|
<window
|
||||||
|
class="BrainPower"
|
||||||
|
name="brain-power"
|
||||||
|
visible={false}
|
||||||
|
gdkmonitor={gdkmonitor}
|
||||||
|
exclusivity={Astal.Exclusivity.NONE}
|
||||||
|
anchor={TOP | LEFT | BOTTOM}
|
||||||
|
application={app}
|
||||||
|
layer={Astal.Layer.OVERLAY}
|
||||||
|
>
|
||||||
|
<eventbox
|
||||||
|
onHoverLost={() => {
|
||||||
|
const win = app.get_window("brain-power")
|
||||||
|
if (win) win.visible = false
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<box orientation={Gtk.Orientation.VERTICAL} class="brain-panel">
|
||||||
|
<box class="brain-header">
|
||||||
|
<label class="brain-title" label=" BRAIN POWER" hexpand xalign={0} />
|
||||||
|
<button
|
||||||
|
class="brain-close"
|
||||||
|
onClicked={() => {
|
||||||
|
const win = app.get_window("brain-power")
|
||||||
|
if (win) win.visible = false
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<label label="✕" />
|
||||||
|
</button>
|
||||||
|
</box>
|
||||||
|
<BrainContent />
|
||||||
|
</box>
|
||||||
|
</eventbox>
|
||||||
|
</window>
|
||||||
|
)
|
||||||
|
}
|
||||||
6
autostart/ghost-shell.desktop
Normal file
6
autostart/ghost-shell.desktop
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=Ghost Shell
|
||||||
|
Comment=violet-chaton v2 AGS statusbar
|
||||||
|
Exec=ags run -d /home/tetardtek/.config/ags-v3 -g 3
|
||||||
|
X-GNOME-Autostart-enabled=true
|
||||||
68
palette/violet-chaton-v2.md
Normal file
68
palette/violet-chaton-v2.md
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
# violet-chaton v2 — Palette Reference
|
||||||
|
|
||||||
|
> Mitsuri Kanroji inspired gradient magenta → green
|
||||||
|
> Created by Tetardtek — 2026
|
||||||
|
|
||||||
|
## Colors
|
||||||
|
|
||||||
|
### Backgrounds
|
||||||
|
| Name | Hex | Usage |
|
||||||
|
|------|-----|-------|
|
||||||
|
| crust | `#1a0e27` | Deepest background, panels heavy glass |
|
||||||
|
| base | `#261537` | Primary background, bar glass |
|
||||||
|
| mantle | `#341c4a` | Elevated surfaces |
|
||||||
|
| surface0 | `#3d2454` | Input backgrounds, troughs |
|
||||||
|
| surface1 | `#493161` | Hover states, subtle borders |
|
||||||
|
| surface2 | `#5a3875` | Active states, dividers |
|
||||||
|
|
||||||
|
### Accents
|
||||||
|
| Name | Hex | Usage |
|
||||||
|
|------|-----|-------|
|
||||||
|
| magenta | `#ff4da6` | Primary accent — clock, battery, volume, borders, heartbeat |
|
||||||
|
| lilac | `#c9a0ff` | Secondary accent — hover states, media, brain panel |
|
||||||
|
| mitsuri | `#9adba8` | Green pastel — charging state, success, health good |
|
||||||
|
| lavande | `#a4b4ff` | Blue-violet — date, CPU, network, info states |
|
||||||
|
| champagne | `#e8c87a` | Warm gold — warnings, session info |
|
||||||
|
| danger | `#f25c7a` | Red-pink — errors, low battery, disconnected |
|
||||||
|
|
||||||
|
### Text
|
||||||
|
| Name | Hex | Usage |
|
||||||
|
|------|-----|-------|
|
||||||
|
| text | `#f0eaf8` | Primary text |
|
||||||
|
| subtext1 | `#c4b8d4` | Secondary text, todo items |
|
||||||
|
| subtext0 | `#9a8fad` | Tertiary text |
|
||||||
|
| muted | `#716686` | Disabled, placeholders, empty states |
|
||||||
|
|
||||||
|
## Font
|
||||||
|
- **Primary**: Maple Mono NF (cursive italics)
|
||||||
|
- **Size**: 13px bold (bar), 14px 900 (clock, prompt)
|
||||||
|
- **Icons**: Nerd Font glyphs from Maple Mono NF
|
||||||
|
|
||||||
|
## Signature Visual Elements
|
||||||
|
- Glassmorphism: `rgba(38, 21, 55, 0.88)` (base @ 88%)
|
||||||
|
- Heavy glass: `rgba(26, 14, 39, 0.94)` (crust @ 94%)
|
||||||
|
- Border accent: `3px solid rgba(255, 77, 166, 0.60)` (magenta @ 60%)
|
||||||
|
- Hover glow: `box-shadow: 0 4px 28px rgba(201, 160, 255, 0.18)` (lilac @ 18%)
|
||||||
|
- Gradient progress: `linear-gradient(to right, magenta, lilac, lavande, mitsuri)`
|
||||||
|
- Heartbeat: gradient `magenta 0% → magenta 60% → lilac 80% → magenta 60% → magenta 0%`
|
||||||
|
- Border radius: 14px (pills), 8px (buttons)
|
||||||
|
|
||||||
|
## SCSS Variables
|
||||||
|
```scss
|
||||||
|
$crust: #1a0e27;
|
||||||
|
$base: #261537;
|
||||||
|
$mantle: #341c4a;
|
||||||
|
$surface0: #3d2454;
|
||||||
|
$surface1: #493161;
|
||||||
|
$surface2: #5a3875;
|
||||||
|
$magenta: #ff4da6;
|
||||||
|
$lilac: #c9a0ff;
|
||||||
|
$mitsuri: #9adba8;
|
||||||
|
$lavande: #a4b4ff;
|
||||||
|
$champagne: #e8c87a;
|
||||||
|
$danger: #f25c7a;
|
||||||
|
$text: #f0eaf8;
|
||||||
|
$subtext1: #c4b8d4;
|
||||||
|
$subtext0: #9a8fad;
|
||||||
|
$muted: #716686;
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user