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:
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user