Compare commits

..

5 Commits

Author SHA1 Message Date
29b4c54370 feat(ags-v3): brain HUD terminal — kitty toggle + single instance + COSMIC window rule
- BrainPower panel: dashboard only (VTE embed pas compatible AGS JSX)
- toggle-brain.sh: ouvre/ferme panel AGS + terminal Kitty (single instance via pgrep)
- Kitty class brain-hud-terminal pour COSMIC window rule (floating)
- app.ts: cleanup focusBrainTerm removed
- style.scss: brain-terminal class + brain-commits-list
2026-03-26 15:54:49 +01:00
9eaaa01663 feat(ags-v3): desktop adaptation — ultrawide scaling, brain power panel, system stats
- Scaling 16px base pour ultrawide 3440x1440
- Bar: CPU/RAM/GPU visible, media single player (skip playerctld), network tooltip LAN/WAN IPv4
- Volume: class module pour sizing cohérent
- Battery: désactivé (PC fixe)
- Clock: tooltip calendrier + uptime
- BrainPower: panel enrichi (focus, session, intentions, todos, repos git, derniers commits)
- App: BrainPower sur moniteur principal uniquement
- Heartbeat: Layer.TOP pour compatibilité COSMIC
2026-03-26 15:25:03 +01:00
e94b841b2a Merge pull request 'feat: Giga Rice 2026 — violet-chaton v2 + Ghost Shell AGS v3' (#1) from features/giga-rice-2026 into dev
Reviewed-on: #1
2026-03-26 08:14:28 +00:00
51b725e1f7 fix(ags-v3): nice-to-have — portabilité, perf, réactivité
- Prompt.tsx: GLib.get_user_name() au lieu de hardcode "tetardtek"
- ghost-shell.desktop: $HOME au lieu de chemin absolu
- SystemStats.tsx: lecture /proc/stat + /proc/meminfo (zero fork, économie batterie)
- Battery.tsx: createDerivedBinding percentage+charging — réactif sur branchement
2026-03-26 09:13:36 +01:00
da22b6446d fix(ags-v3): audit fixes — portable brain path, reactive volume, SCSS dedup
- brain.ts: BRAIN_ROOT résolu via $BRAIN_ROOT env / ~/.config/brain-path / fallback ~/Dev/Brain
- Volume.tsx: bindings volume + mute séparés et réactifs
- style.scss: importe _bar.scss et _heartbeat.scss via @use, supprime 199 lignes dupliquées
2026-03-26 08:58:33 +01:00
16 changed files with 423 additions and 340 deletions

View File

@@ -7,11 +7,14 @@ import BrainPower from "./widget/panels/BrainPower"
app.start({ app.start({
css: style, css: style,
main() { main() {
for (const monitor of app.get_monitors()) { const monitors = app.get_monitors()
for (const monitor of monitors) {
Heartbeat(monitor) Heartbeat(monitor)
Bar(monitor) Bar(monitor)
BrainPower(monitor)
} }
// Brain Power on primary monitor only
const primary = monitors[0]
if (primary) BrainPower(primary)
}, },
requestHandler(request: any, res: (response: any) => void) { requestHandler(request: any, res: (response: any) => void) {
const cmd = String(request) const cmd = String(request)

View File

@@ -1,11 +1,18 @@
import GLib from "gi://GLib" import GLib from "gi://GLib"
import { exec } from "ags/process"
const BRAIN_ROOT = "/home/tetardtek/Dev/Cortex-Template" const BRAIN_ROOT = GLib.getenv("BRAIN_ROOT")
|| readFile(GLib.get_home_dir() + "/.config/brain-path")?.trim()
|| GLib.get_home_dir() + "/Dev/Brain"
export interface BrainState { export interface BrainState {
focus: string focus: string
todos: { open: number; done: number; top3: string[] } todos: { open: number; done: number; top3: string[] }
session: string | null session: string | null
intentions: { active: number; names: string[] }
repos: { name: string; status: string }[]
commits: string[]
kernelVersion: string
} }
function readFile(path: string): string | null { function readFile(path: string): string | null {
@@ -18,10 +25,13 @@ function readFile(path: string): string | null {
return null return null
} }
function sh(cmd: string): string {
try { return exec(`bash -c "${cmd}"`).trim() } catch { return "" }
}
export function getFocus(): string { export function getFocus(): string {
const content = readFile(`${BRAIN_ROOT}/focus.md`) const content = readFile(`${BRAIN_ROOT}/focus.md`)
if (!content) return "no focus" if (!content) return "no focus"
// skip header lines, get the meat
const lines = content.split("\n").filter( const lines = content.split("\n").filter(
(l) => !l.startsWith("#") && !l.startsWith(">") && l.trim() !== "" && !l.startsWith("---") (l) => !l.startsWith("#") && !l.startsWith(">") && l.trim() !== "" && !l.startsWith("---")
) )
@@ -45,11 +55,7 @@ export function getTodos(): { open: number; done: number; top3: string[] } {
} }
} }
return { return { open: open.length, done, top3: open.slice(0, 3) }
open: open.length,
done,
top3: open.slice(0, 3),
}
} }
export function getSession(): string | null { export function getSession(): string | null {
@@ -64,10 +70,53 @@ export function getSession(): string | null {
return null return null
} }
export function getIntentions(): { active: number; names: string[] } {
try {
const out = sh(`grep -rl 'status: active' ${BRAIN_ROOT}/intentions/*.yml 2>/dev/null | while read f; do grep '^name:' "$f" | head -1 | sed 's/name: //'; done`)
const names = out.split("\n").filter(Boolean)
return { active: names.length, names: names.slice(0, 5) }
} catch {
return { active: 0, names: [] }
}
}
export function getRepoStatus(): { name: string; status: string }[] {
const repos = [
{ name: "brain", path: BRAIN_ROOT },
{ name: "toolkit", path: `${BRAIN_ROOT}/toolkit` },
{ name: "progression", path: `${BRAIN_ROOT}/progression` },
]
return repos.map(({ name, path }) => {
const count = sh(`git -C ${path} status --short 2>/dev/null | wc -l`)
const n = parseInt(count) || 0
return {
name,
status: n === 0 ? "✅" : `⚠️ ${n} fichiers`,
}
})
}
export function getRecentCommits(): string[] {
const out = sh(`git -C ${BRAIN_ROOT} log --oneline -5 2>/dev/null`)
return out.split("\n").filter(Boolean)
}
export function getKernelVersion(): string {
const content = readFile(`${BRAIN_ROOT}/brain-compose.yml`)
if (!content) return "?"
const match = content.match(/^version:\s*"?([^"\n]+)"?/m)
return match ? match[1] : "?"
}
export function getBrainState(): BrainState { export function getBrainState(): BrainState {
return { return {
focus: getFocus(), focus: getFocus(),
todos: getTodos(), todos: getTodos(),
session: getSession(), session: getSession(),
intentions: getIntentions(),
repos: getRepoStatus(),
commits: getRecentCommits(),
kernelVersion: getKernelVersion(),
} }
} }

View File

@@ -1,208 +1,13 @@
@use "styles/palette" as *; @use "styles/palette" as *;
@use "styles/heartbeat";
@use "styles/bar";
* { * {
font-family: $font; font-family: $font;
font-size: 13px; font-size: 16px;
font-weight: bold; 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 ── // ── Layer 3 — Brain Power Panel ──
window.BrainPower { window.BrainPower {
background: transparent; background: transparent;
@@ -291,10 +96,39 @@ window.BrainPower {
padding: 4px 0 4px 16px; padding: 4px 0 4px 16px;
} }
.brain-terminal-placeholder { .brain-version {
color: $muted; color: $muted;
font-size: 11px; font-size: 12px;
font-style: italic; font-weight: normal;
padding: 8px 0 4px 16px; }
.brain-intentions-list {
color: $mitsuri;
font-size: 13px;
font-weight: normal;
padding: 4px 0 4px 16px;
}
.brain-repos-status {
color: $subtext1;
font-size: 13px;
font-weight: normal;
font-family: "Maple Mono NF", monospace;
padding: 4px 0 4px 16px;
}
.brain-commits-list {
color: $subtext0;
font-size: 12px;
font-weight: normal;
font-family: "Maple Mono NF", monospace;
padding: 4px 0 4px 16px;
}
.brain-terminal {
background: #1a0e27;
border-radius: 0 0 $radius 0;
padding: 4px;
min-height: 400px;
} }
} }

View File

@@ -1,7 +1,6 @@
@use "palette" as *; @use "palette" as *;
// Layer 1 — Ghost bar (hover reveal) // Layer 1 — Ghost bar (hover reveal)
// GTK CSS alpha() must be unquoted strings to pass through SCSS compiler
window.Bar { window.Bar {
background: transparent; background: transparent;
@@ -12,7 +11,7 @@ window.Bar {
border: 3px solid rgba(255, 77, 166, 0.60); // $magenta @ 0.60 border: 3px solid rgba(255, 77, 166, 0.60); // $magenta @ 0.60
margin: 6px 8px; margin: 6px 8px;
padding: 0 8px; padding: 0 8px;
min-height: 38px; min-height: 48px;
} }
.modules-left, .modules-left,
@@ -24,16 +23,24 @@ window.Bar {
.module { .module {
padding: 0 8px; padding: 0 8px;
color: $text; color: $text;
&:hover {
color: $lilac;
} }
.prompt-name {
color: $magenta;
font-size: 17px;
padding: 0 0 0 14px;
}
.prompt-cursor {
color: $lilac;
font-size: 17px;
font-weight: 900;
} }
.separator { .separator {
color: rgba(240, 234, 248, 0.12); // $text @ 0.12 color: rgba(240, 234, 248, 0.12); // $text @ 0.12
font-size: 11px; font-size: 14px;
padding: 0 4px; padding: 0 6px;
font-weight: normal; font-weight: normal;
} }
@@ -41,21 +48,19 @@ window.Bar {
.clock { .clock {
color: $magenta; color: $magenta;
font-weight: 900; font-weight: 900;
font-size: 14px; font-size: 18px;
letter-spacing: 0.04em; letter-spacing: 0.04em;
padding: 0 10px; padding: 0 10px;
&:hover { color: $lilac; }
} }
.date { .date {
color: $lavande; color: $lavande;
font-size: 12px; font-size: 15px;
font-weight: normal; font-weight: normal;
padding: 0 10px 0 2px; padding: 0 10px 0 2px;
} }
// ── system ── // ── system stats ──
.cpu { color: $lavande; } .cpu { color: $lavande; }
.ram { color: $magenta; } .ram { color: $magenta; }
.temp { .temp {
@@ -64,7 +69,9 @@ window.Bar {
font-weight: normal; font-weight: normal;
} }
.cpu, .ram, .temp { .gpu { color: $mitsuri; }
.cpu, .ram, .gpu, .temp {
&.warning { color: $champagne; } &.warning { color: $champagne; }
&.critical { color: $danger; } &.critical { color: $danger; }
} }
@@ -72,40 +79,94 @@ window.Bar {
// ── network ── // ── network ──
.network { .network {
color: $lavande; color: $lavande;
font-size: 12px; font-size: 15px;
padding: 0 10px;
&.disconnected { color: $danger; } &.disconnected { color: $danger; }
} }
// ── volume ── // ── volume ──
.volume { button.volume,
color: $magenta; button.muted {
&.muted { color: rgba(255, 77, 166, 0.30); } 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; }
} }
// ── battery ── // ── battery ──
.battery { .battery {
color: $magenta; color: $magenta;
padding: 0 8px;
&.charging { color: $mitsuri; } &.charging { color: $mitsuri; }
&.low { color: $danger; } &.low { color: $danger; }
&.warning { color: $champagne; } &.warning { color: $champagne; }
} }
// ── media ── // ── media ──
.media { .media-module {
color: $lilac; padding: 0 4px;
font-size: 12px; }
font-weight: normal;
padding: 0 10px;
&.paused { .media {
padding: 0 4px;
&.paused .media-text {
color: rgba(201, 160, 255, 0.50); // $lilac @ 0.50 color: rgba(201, 160, 255, 0.50); // $lilac @ 0.50
font-style: italic; font-style: italic;
} }
} }
.media-text {
color: $lilac;
font-size: 15px;
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: 16px; }
&:hover label { color: $magenta; }
}
// ── systray ── // ── systray ──
.systray { .systray {
padding: 0 6px; 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;
}
}
// ── hover effects ──
.module:hover,
.clock:hover,
.cpu:hover,
.ram:hover,
.network:hover,
.battery:hover {
color: $lilac;
} }
// ── workspaces ── // ── workspaces ──

View File

@@ -1,3 +1,32 @@
#!/bin/bash #!/bin/bash
# Toggle Brain Power panel via AGS IPC # Toggle Brain Power — dashboard AGS + terminal Kitty (single instance)
ags request "toggle-brain" 2>/dev/null BRAIN_ROOT="${BRAIN_ROOT:-$HOME/Dev/Brain}"
KITTY_CLASS="brain-hud-terminal"
# Check if brain kitty is already running (by window class)
kitty_pid=$(pgrep -f "class $KITTY_CLASS" | head -1)
if [ -n "$kitty_pid" ]; then
# Close everything
ags request "toggle-brain" 2>/dev/null
kill "$kitty_pid" 2>/dev/null
else
# Open everything
ags request "toggle-brain" 2>/dev/null
kitty \
--class "$KITTY_CLASS" \
--title "🧠 Brain HUD" \
--override remember_window_size=no \
--override initial_window_width=60c \
--override initial_window_height=30c \
--override background_opacity=0.94 \
--override background=#1a0e27 \
--override foreground=#f0eaf8 \
--override cursor=#ff4da6 \
--override font_size=13 \
--override confirm_os_window_close=0 \
--directory "$BRAIN_ROOT" \
-e zsh -c "echo '🧠 Brain HUD — navigate mode'; echo ''; exec zsh" \
&
disown
fi

View File

@@ -1,12 +1,12 @@
import app from "ags/gtk3/app" import app from "ags/gtk3/app"
import { Astal, Gtk, Gdk } from "ags/gtk3" import { Astal, Gtk, Gdk } from "ags/gtk3"
import Clock from "./modules/Clock" import Clock from "./modules/Clock"
import Battery from "./modules/Battery" // import Battery from "./modules/Battery" // desktop — no battery
import Volume from "./modules/Volume" import Volume from "./modules/Volume"
import Network from "./modules/Network" import Network from "./modules/Network"
import SystemStats from "./modules/SystemStats" import SystemStats from "./modules/SystemStats"
import Media from "./modules/Media" import Media from "./modules/Media"
import SysTray from "./modules/SysTray" // import SysTray from "./modules/SysTray" // TODO: needs astal-tray (appmenu-glib-translator)
import Prompt from "./modules/Prompt" import Prompt from "./modules/Prompt"
export default function Bar(gdkmonitor: Gdk.Monitor) { export default function Bar(gdkmonitor: Gdk.Monitor) {
@@ -33,7 +33,7 @@ export default function Bar(gdkmonitor: Gdk.Monitor) {
<window <window
class="Bar" class="Bar"
name="bar" name="bar"
visible={false} visible={true}
gdkmonitor={gdkmonitor} gdkmonitor={gdkmonitor}
exclusivity={Astal.Exclusivity.EXCLUSIVE} exclusivity={Astal.Exclusivity.EXCLUSIVE}
anchor={TOP | LEFT | RIGHT} anchor={TOP | LEFT | RIGHT}
@@ -42,9 +42,8 @@ export default function Bar(gdkmonitor: Gdk.Monitor) {
> >
<eventbox <eventbox
onHover={() => cancelHide()} onHover={() => cancelHide()}
onHoverLost={(self) => { onHoverLost={(_self) => {
const win = self.get_toplevel() as Astal.Window // disabled for debug — auto-hide off
scheduleHide(win)
}} }}
> >
<centerbox> <centerbox>
@@ -59,13 +58,10 @@ export default function Bar(gdkmonitor: Gdk.Monitor) {
<Clock /> <Clock />
</box> </box>
<box $type="end" class="modules-right" halign={Gtk.Align.END}> <box $type="end" class="modules-right" halign={Gtk.Align.END}>
<SysTray /> {/* <SysTray /> */}
<label class="separator" label="│" />
<Network /> <Network />
<label class="separator" label="│" /> <label class="separator" label="│" />
<Volume /> <Volume />
<label class="separator" label="│" />
<Battery />
</box> </box>
</centerbox> </centerbox>
</eventbox> </eventbox>

View File

@@ -11,7 +11,7 @@ export default function Heartbeat(gdkmonitor: Gdk.Monitor) {
exclusivity={Astal.Exclusivity.NONE} exclusivity={Astal.Exclusivity.NONE}
anchor={TOP | LEFT | RIGHT} anchor={TOP | LEFT | RIGHT}
application={app} application={app}
layer={Astal.Layer.OVERLAY} layer={Astal.Layer.TOP}
> >
<eventbox <eventbox
onHover={() => { onHover={() => {

View File

@@ -6,8 +6,9 @@ export default function Battery() {
const text = createBinding(bat, "percentage")((p: number) => { const text = createBinding(bat, "percentage")((p: number) => {
const pct = Math.round(p * 100) const pct = Math.round(p * 100)
const isCharging = bat.charging
let icon = "" let icon = ""
if (bat.charging) icon = "󰂄" if (isCharging) icon = "󰂄"
else if (pct > 90) icon = "󰁹" else if (pct > 90) icon = "󰁹"
else if (pct > 70) icon = "󰂁" else if (pct > 70) icon = "󰂁"
else if (pct > 50) icon = "󰁿" else if (pct > 50) icon = "󰁿"

View File

@@ -1,4 +1,5 @@
import { createPoll } from "ags/time" import { createPoll } from "ags/time"
import { exec } from "ags/process"
function formatTime(): string { function formatTime(): string {
const now = new Date() const now = new Date()
@@ -10,6 +11,16 @@ function formatTime(): string {
} }
function formatDate(): string { function formatDate(): string {
const now = new Date()
return now.toLocaleDateString("fr-FR", {
weekday: "long",
day: "numeric",
month: "long",
year: "numeric",
})
}
function formatDateShort(): string {
const now = new Date() const now = new Date()
return now.toLocaleDateString("fr-FR", { return now.toLocaleDateString("fr-FR", {
weekday: "short", weekday: "short",
@@ -18,15 +29,44 @@ function formatDate(): string {
}) })
} }
function getCalendar(): string {
try {
return exec("bash -c \"cal -h\"").trim()
} catch {
return ""
}
}
function getUptime(): string {
try {
return exec("bash -c \"uptime -p | sed 's/up //'\"").trim()
} catch {
return ""
}
}
function buildTooltip(): string {
const full = formatDate()
const cal = getCalendar()
const up = getUptime()
return `${full}\n\n${cal}\n\n⏱ uptime: ${up}`
}
export default function Clock() { export default function Clock() {
const time = createPoll("", 1000, () => formatTime()) const time = createPoll("", 1000, () => formatTime())
const date = createPoll("", 60000, () => formatDate()) const date = createPoll("", 60000, () => formatDateShort())
return ( return (
<box> <box>
<label class="clock" label={time} /> <label class="clock" label={time} />
<label class="separator" label="│" /> <label class="separator" label="│" />
<label class="date" label={date} /> <eventbox
onHover={(self) => {
self.tooltipText = buildTooltip()
}}
>
<label class="date" label={date} hasTooltip />
</eventbox>
</box> </box>
) )
} }

View File

@@ -5,24 +5,27 @@ export default function Media() {
const mpris = AstalMpris.get_default() const mpris = AstalMpris.get_default()
const players = createBinding(mpris, "players") const players = createBinding(mpris, "players")
// Filter to only show non-playerctld players
const filtered = players((list: AstalMpris.Player[]) =>
list.filter((p) => p.busName && !p.busName.includes("playerctld")).slice(0, 1)
)
return ( return (
<box class="media-module"> <box class="media-module">
<For each={players}> <For each={filtered}>
{(player) => { {(player) => {
const title = createBinding(player, "title") const title = createBinding(player, "title")
const artist = createBinding(player, "artist")
const status = createBinding(player, "playbackStatus") const status = createBinding(player, "playbackStatus")
const icon = status((s: AstalMpris.PlaybackStatus) => const icon = status((s: AstalMpris.PlaybackStatus) =>
s === AstalMpris.PlaybackStatus.PLAYING ? "󰐊" : "󰏤" s === AstalMpris.PlaybackStatus.PLAYING ? "" : ""
) )
const label = title((t: string) => { const label = title((t: string) => {
const a = player.artist const a = player.artist
const display = a ? `${a}${t}` : t const display = a ? `${a}${t}` : t
// truncate long titles return display.length > 45
return display.length > 40 ? display.substring(0, 42) + "..."
? display.substring(0, 37) + "..."
: display : display
}) })
@@ -32,23 +35,14 @@ export default function Media() {
return ( return (
<box class={cls}> <box class={cls}>
<button <button class="media-prev" onClicked={() => player.previous()}>
class="media-prev" <label label="⏮" />
onClicked={() => player.previous()}
>
<label label="󰒮" />
</button> </button>
<button <button class="media-play" onClicked={() => player.play_pause()}>
class="media-play"
onClicked={() => player.play_pause()}
>
<label label={icon} /> <label label={icon} />
</button> </button>
<button <button class="media-next" onClicked={() => player.next()}>
class="media-next" <label label="⏭" />
onClicked={() => player.next()}
>
<label label="󰒭" />
</button> </button>
<label class="media-text" label={label} /> <label class="media-text" label={label} />
</box> </box>

View File

@@ -1,5 +1,16 @@
import AstalNetwork from "gi://AstalNetwork" import AstalNetwork from "gi://AstalNetwork"
import { createBinding } from "ags" import { createBinding } from "ags"
import { exec } from "ags/process"
function getIPs(): string {
try {
const lan = exec("bash -c \"hostname -I | awk '{print $1}'\"").trim()
const wan = exec("bash -c \"curl -4 -s --max-time 2 ifconfig.me\"").trim()
return `LAN: ${lan || "?"}\nWAN: ${wan || "?"}`
} catch {
return "IPs unavailable"
}
}
export default function Network() { export default function Network() {
const net = AstalNetwork.get_default() const net = AstalNetwork.get_default()
@@ -17,15 +28,23 @@ export default function Network() {
else if (strength > 20) icon = "󰤟" else if (strength > 20) icon = "󰤟"
return `${icon} ${ssid}` return `${icon} ${ssid}`
} }
if (type === AstalNetwork.Primary.WIRED) return "󰈀 eth" if (type === AstalNetwork.Primary.WIRED) return "󰈀 wired"
return "󰤮 offline" return "󰤮 offline"
}) })
const cls = createBinding(net, "primary")((type: AstalNetwork.Primary) => { const cls = createBinding(net, "primary")((type: AstalNetwork.Primary) => {
if (type === AstalNetwork.Primary.WIFI) return "network wifi" if (type === AstalNetwork.Primary.WIFI) return "network module wifi"
if (type === AstalNetwork.Primary.WIRED) return "network wired" if (type === AstalNetwork.Primary.WIRED) return "network module wired"
return "network disconnected" return "network module disconnected"
}) })
return <label class={cls} label={text} /> return (
<eventbox
onHover={(self) => {
self.tooltipText = getIPs()
}}
>
<label class={cls} label={text} hasTooltip />
</eventbox>
)
} }

View File

@@ -1,6 +1,9 @@
import GLib from "gi://GLib"
import { createPoll } from "ags/time" import { createPoll } from "ags/time"
export default function Prompt() { export default function Prompt() {
const username = GLib.get_user_name() || "user"
const cursor = createPoll("_", 600, () => { const cursor = createPoll("_", 600, () => {
// alternate between _ and empty to create blink // alternate between _ and empty to create blink
return Date.now() % 1200 < 600 ? "_" : " " return Date.now() % 1200 < 600 ? "_" : " "
@@ -8,7 +11,7 @@ export default function Prompt() {
return ( return (
<box> <box>
<label class="prompt-name" label="tetardtek" /> <label class="prompt-name" label={username} />
<label class="prompt-cursor" label={cursor} /> <label class="prompt-cursor" label={cursor} />
</box> </box>
) )

View File

@@ -1,31 +1,89 @@
import GLib from "gi://GLib"
import { createPoll } from "ags/time" import { createPoll } from "ags/time"
import { exec } from "ags/process" import { exec } from "ags/process"
function getCpuUsage(): string { function readProc(path: string): string | null {
try { try {
const out = exec("bash -c \"top -bn1 | grep 'Cpu(s)' | awk '{print 100 - $8}'\"") const [ok, contents] = GLib.file_get_contents(path)
return `${Math.round(parseFloat(out))}` if (ok && contents) return new TextDecoder().decode(contents)
} catch { } catch {}
return "?" return null
}
} }
function getRamUsage(): string { let prevIdle = 0
let prevTotal = 0
function getCpuUsage(): string {
const content = readProc("/proc/stat")
if (!content) return "?"
const line = content.split("\n")[0]
const parts = line.split(/\s+/).slice(1).map(Number)
const idle = parts[3] + parts[4]
const total = parts.reduce((a, b) => a + b, 0)
const dIdle = idle - prevIdle
const dTotal = total - prevTotal
prevIdle = idle
prevTotal = total
if (dTotal === 0) return "0"
return `${Math.round(((dTotal - dIdle) / dTotal) * 100)}`
}
function getRamUsage(): { pct: string; used: string; total: string } {
const content = readProc("/proc/meminfo")
if (!content) return { pct: "?", used: "?", total: "?" }
const lines = content.split("\n")
let totalKb = 0, availableKb = 0
for (const line of lines) {
if (line.startsWith("MemTotal:")) totalKb = parseInt(line.split(/\s+/)[1])
if (line.startsWith("MemAvailable:")) availableKb = parseInt(line.split(/\s+/)[1])
if (totalKb && availableKb) break
}
if (!totalKb) return { pct: "?", used: "?", total: "?" }
const usedGb = ((totalKb - availableKb) / 1048576).toFixed(1)
const totalGb = (totalKb / 1048576).toFixed(0)
const pct = `${Math.round(((totalKb - availableKb) / totalKb) * 100)}`
return { pct, used: usedGb, total: totalGb }
}
function getGpuInfo(): string {
try { try {
return exec("bash -c \"free | awk '/Mem:/ {printf \\\"%.0f\\\", $3/$2 * 100}'\"") const out = exec("bash -c \"nvidia-smi --query-gpu=utilization.gpu,temperature.gpu --format=csv,noheader,nounits 2>/dev/null\"")
const [usage, temp] = out.trim().split(", ")
return `GPU ${usage}% · ${temp}°C`
} catch { } catch {
return "?" return ""
} }
} }
export default function SystemStats() { export default function SystemStats() {
const cpu = createPoll("0", 3000, () => getCpuUsage()) const cpu = createPoll("0", 3000, () => getCpuUsage())
const ram = createPoll("0", 5000, () => getRamUsage()) const ram = createPoll("", 5000, () => {
const r = getRamUsage()
return JSON.stringify(r)
})
const ramLabel = ram((v: string) => {
try {
const r = JSON.parse(v)
return `RAM ${r.used}/${r.total}G`
} catch { return "RAM ?" }
})
const gpu = createPoll("", 5000, () => getGpuInfo())
return ( return (
<box> <box>
<label class="cpu module" label={cpu((v: string) => ` ${v}%`)} /> <label class="cpu module" label={cpu((v: string) => `CPU ${v}%`)} />
<label class="ram module" label={ram((v: string) => `󰍛 ${v}%`)} /> <label class="separator" label="│" />
<label class="ram module" label={ramLabel} />
<label class="separator" label="│" />
<label class="gpu module" label={gpu((v: string) => v || "GPU ?")} />
</box> </box>
) )
} }

View File

@@ -4,7 +4,10 @@ import { createBinding } from "ags"
export default function Volume() { export default function Volume() {
const speaker = AstalWp.get_default()!.defaultSpeaker! const speaker = AstalWp.get_default()!.defaultSpeaker!
const text = createBinding(speaker, "volume")((v: number) => { const volume = createBinding(speaker, "volume")
const mute = createBinding(speaker, "mute")
const text = volume((v: number) => {
const pct = Math.round(v * 100) const pct = Math.round(v * 100)
const muted = speaker.mute const muted = speaker.mute
let icon = "󰕾" let icon = "󰕾"
@@ -16,8 +19,8 @@ export default function Volume() {
return `${icon} ${pct}%` return `${icon} ${pct}%`
}) })
const cls = createBinding(speaker, "mute")((m: boolean) => const cls = mute((m: boolean) =>
m ? "volume muted" : "volume" m ? "volume module muted" : "volume module"
) )
return ( return (

View File

@@ -2,32 +2,37 @@ import app from "ags/gtk3/app"
import { Astal, Gtk, Gdk } from "ags/gtk3" import { Astal, Gtk, Gdk } from "ags/gtk3"
import { createPoll } from "ags/time" import { createPoll } from "ags/time"
import { getBrainState } from "../../lib/brain" import { getBrainState } from "../../lib/brain"
import GLib from "gi://GLib"
function BrainContent() { function BrainContent() {
const state = createPoll("", 10000, () => JSON.stringify(getBrainState())) const state = createPoll("", 10000, () => JSON.stringify(getBrainState()))
const version = state((s: string) => {
try { return `kernel v${JSON.parse(s).kernelVersion}` } catch { return "" }
})
const focus = state((s: string) => { const focus = state((s: string) => {
try { return JSON.parse(s).focus } catch { return "..." } try { return JSON.parse(s).focus } catch { return "..." }
}) })
const session = state((s: string) => {
const todosLabel = state((s: string) => { try { return JSON.parse(s).session || "aucune session active" } catch { return "..." }
})
const intentionsTitle = state((s: string) => {
try { try {
const t = JSON.parse(s).todos const i = JSON.parse(s).intentions
return `${t.open} ouverts / ${t.done} faits` return ` INTENTIONS (${i.active} active${i.active > 1 ? "s" : ""})`
} catch { return " INTENTIONS" }
})
const intentionsList = state((s: string) => {
try {
return JSON.parse(s).intentions.names.map((n: string) => `${n}`).join("\n") || " aucune"
} catch { return "..." } } catch { return "..." }
}) })
const todosTitle = state((s: string) => {
try { const t = JSON.parse(s).todos; return `${t.open} ouverts / ${t.done} faits` } catch { return "" }
})
const todosList = state((s: string) => { const todosList = state((s: string) => {
try { try {
const t = JSON.parse(s).todos.top3 return JSON.parse(s).todos.top3.map((item: string) => `${item}`).join("\n") || " rien"
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 "..." } } catch { return "..." }
}) })
@@ -35,30 +40,25 @@ function BrainContent() {
<box orientation={Gtk.Orientation.VERTICAL} class="brain-content"> <box orientation={Gtk.Orientation.VERTICAL} class="brain-content">
<box class="brain-section"> <box class="brain-section">
<label class="brain-section-title" label=" FOCUS" xalign={0} /> <label class="brain-section-title" label=" FOCUS" xalign={0} />
<label class="brain-version" label={version} xalign={1} hexpand />
</box> </box>
<label class="brain-focus" label={focus} xalign={0} wrap /> <label class="brain-focus" label={focus} xalign={0} wrap />
<box class="brain-divider" /> <box class="brain-divider" />
<box class="brain-section"> <box class="brain-section">
<label class="brain-section-title" label=" SESSION" xalign={0} /> <label class="brain-section-title" label=" SESSION" xalign={0} />
</box> </box>
<label class="brain-session" label={session} xalign={0} /> <label class="brain-session" label={session} xalign={0} />
<box class="brain-divider" /> <box class="brain-divider" />
<box class="brain-section">
<label class="brain-section-title" label={intentionsTitle} xalign={0} />
</box>
<label class="brain-intentions-list" label={intentionsList} xalign={0} />
<box class="brain-divider" />
<box class="brain-section"> <box class="brain-section">
<label class="brain-section-title" label="󰄲 TODOS" xalign={0} /> <label class="brain-section-title" label="󰄲 TODOS" xalign={0} />
<label class="brain-todos-count" label={todosLabel} xalign={1} hexpand /> <label class="brain-todos-count" label={todosTitle} xalign={1} hexpand />
</box> </box>
<label class="brain-todos-list" label={todosList} xalign={0} /> <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> </box>
) )
} }
@@ -76,12 +76,6 @@ export default function BrainPower(gdkmonitor: Gdk.Monitor) {
anchor={TOP | LEFT | BOTTOM} anchor={TOP | LEFT | BOTTOM}
application={app} application={app}
layer={Astal.Layer.OVERLAY} 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 orientation={Gtk.Orientation.VERTICAL} class="brain-panel">
<box class="brain-header"> <box class="brain-header">
@@ -98,7 +92,6 @@ export default function BrainPower(gdkmonitor: Gdk.Monitor) {
</box> </box>
<BrainContent /> <BrainContent />
</box> </box>
</eventbox>
</window> </window>
) )
} }

View File

@@ -2,5 +2,5 @@
Type=Application Type=Application
Name=Ghost Shell Name=Ghost Shell
Comment=violet-chaton v2 AGS statusbar Comment=violet-chaton v2 AGS statusbar
Exec=ags run -d /home/tetardtek/.config/ags-v3 -g 3 Exec=sh -c "ags run -d $HOME/.config/ags-v3 -g 3"
X-GNOME-Autostart-enabled=true X-GNOME-Autostart-enabled=true