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