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:
Tetardtek-Cortex
2026-03-26 06:54:17 +01:00
parent 7e9d12e640
commit 932b174c36
30 changed files with 2557 additions and 0 deletions

2
ags-v3/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules/
@girs/

30
ags-v3/app.ts Normal file
View 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
View 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
View 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
View File

@@ -0,0 +1,10 @@
{
"dependencies": {
"ags": "*",
"gnim": "*"
},
"prettier": {
"semi": false,
"tabWidth": 2
}
}

300
ags-v3/style.scss Normal file
View 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
View 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;
}
}
}

View 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;
}
}

View 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
View 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
View 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
View 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>
)
}

View 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>
)
}

View 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} />
}

View 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>
)
}

View 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>
)
}

View 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} />
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}