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
This commit is contained in:
@@ -1,38 +1,30 @@
|
||||
import AstalBattery from "gi://AstalBattery"
|
||||
import { createBinding, createDerivedBinding } from "ags"
|
||||
import { createBinding } from "ags"
|
||||
|
||||
export default function Battery() {
|
||||
const bat = AstalBattery.get_default()
|
||||
|
||||
const percentage = createBinding(bat, "percentage")
|
||||
const charging = createBinding(bat, "charging")
|
||||
const text = createBinding(bat, "percentage")((p: number) => {
|
||||
const pct = Math.round(p * 100)
|
||||
const isCharging = bat.charging
|
||||
let icon = ""
|
||||
if (isCharging) 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 text = createDerivedBinding(
|
||||
[percentage, charging],
|
||||
(p: number, isCharging: boolean) => {
|
||||
const pct = Math.round(p * 100)
|
||||
let icon = ""
|
||||
if (isCharging) 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 = createDerivedBinding(
|
||||
[percentage, charging],
|
||||
(p: number, isCharging: boolean) => {
|
||||
const pct = Math.round(p * 100)
|
||||
if (isCharging) return "battery charging"
|
||||
if (pct <= 10) return "battery low"
|
||||
if (pct <= 20) return "battery warning"
|
||||
return "battery"
|
||||
},
|
||||
)
|
||||
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} />
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createPoll } from "ags/time"
|
||||
import { exec } from "ags/process"
|
||||
|
||||
function formatTime(): string {
|
||||
const now = new Date()
|
||||
@@ -10,6 +11,16 @@ function formatTime(): 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()
|
||||
return now.toLocaleDateString("fr-FR", {
|
||||
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() {
|
||||
const time = createPoll("", 1000, () => formatTime())
|
||||
const date = createPoll("", 60000, () => formatDate())
|
||||
const date = createPoll("", 60000, () => formatDateShort())
|
||||
|
||||
return (
|
||||
<box>
|
||||
<label class="clock" label={time} />
|
||||
<label class="separator" label="│" />
|
||||
<label class="date" label={date} />
|
||||
<eventbox
|
||||
onHover={(self) => {
|
||||
self.tooltipText = buildTooltip()
|
||||
}}
|
||||
>
|
||||
<label class="date" label={date} hasTooltip />
|
||||
</eventbox>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,24 +5,27 @@ export default function Media() {
|
||||
const mpris = AstalMpris.get_default()
|
||||
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 (
|
||||
<box class="media-module">
|
||||
<For each={players}>
|
||||
<For each={filtered}>
|
||||
{(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 ? "" : ""
|
||||
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) + "..."
|
||||
return display.length > 45
|
||||
? display.substring(0, 42) + "..."
|
||||
: display
|
||||
})
|
||||
|
||||
@@ -32,23 +35,14 @@ export default function Media() {
|
||||
|
||||
return (
|
||||
<box class={cls}>
|
||||
<button
|
||||
class="media-prev"
|
||||
onClicked={() => player.previous()}
|
||||
>
|
||||
<label label="" />
|
||||
<button class="media-prev" onClicked={() => player.previous()}>
|
||||
<label label="⏮" />
|
||||
</button>
|
||||
<button
|
||||
class="media-play"
|
||||
onClicked={() => player.play_pause()}
|
||||
>
|
||||
<button class="media-play" onClicked={() => player.play_pause()}>
|
||||
<label label={icon} />
|
||||
</button>
|
||||
<button
|
||||
class="media-next"
|
||||
onClicked={() => player.next()}
|
||||
>
|
||||
<label label="" />
|
||||
<button class="media-next" onClicked={() => player.next()}>
|
||||
<label label="⏭" />
|
||||
</button>
|
||||
<label class="media-text" label={label} />
|
||||
</box>
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
import AstalNetwork from "gi://AstalNetwork"
|
||||
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() {
|
||||
const net = AstalNetwork.get_default()
|
||||
@@ -17,15 +28,23 @@ export default function Network() {
|
||||
else if (strength > 20) icon = ""
|
||||
return `${icon} ${ssid}`
|
||||
}
|
||||
if (type === AstalNetwork.Primary.WIRED) return " eth"
|
||||
if (type === AstalNetwork.Primary.WIRED) return " wired"
|
||||
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"
|
||||
if (type === AstalNetwork.Primary.WIFI) return "network module wifi"
|
||||
if (type === AstalNetwork.Primary.WIRED) return "network module wired"
|
||||
return "network module disconnected"
|
||||
})
|
||||
|
||||
return <label class={cls} label={text} />
|
||||
return (
|
||||
<eventbox
|
||||
onHover={(self) => {
|
||||
self.tooltipText = getIPs()
|
||||
}}
|
||||
>
|
||||
<label class={cls} label={text} hasTooltip />
|
||||
</eventbox>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import GLib from "gi://GLib"
|
||||
import { createPoll } from "ags/time"
|
||||
import { exec } from "ags/process"
|
||||
|
||||
function readProc(path: string): string | null {
|
||||
try {
|
||||
@@ -16,9 +17,9 @@ function getCpuUsage(): string {
|
||||
const content = readProc("/proc/stat")
|
||||
if (!content) return "?"
|
||||
|
||||
const line = content.split("\n")[0] // "cpu user nice system idle ..."
|
||||
const line = content.split("\n")[0]
|
||||
const parts = line.split(/\s+/).slice(1).map(Number)
|
||||
const idle = parts[3] + parts[4] // idle + iowait
|
||||
const idle = parts[3] + parts[4]
|
||||
const total = parts.reduce((a, b) => a + b, 0)
|
||||
|
||||
const dIdle = idle - prevIdle
|
||||
@@ -30,31 +31,59 @@ function getCpuUsage(): string {
|
||||
return `${Math.round(((dTotal - dIdle) / dTotal) * 100)}`
|
||||
}
|
||||
|
||||
function getRamUsage(): string {
|
||||
function getRamUsage(): { pct: string; used: string; total: string } {
|
||||
const content = readProc("/proc/meminfo")
|
||||
if (!content) return "?"
|
||||
if (!content) return { pct: "?", used: "?", total: "?" }
|
||||
|
||||
const lines = content.split("\n")
|
||||
let total = 0, available = 0
|
||||
let totalKb = 0, availableKb = 0
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("MemTotal:")) total = parseInt(line.split(/\s+/)[1])
|
||||
if (line.startsWith("MemAvailable:")) available = parseInt(line.split(/\s+/)[1])
|
||||
if (total && available) break
|
||||
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 (!total) return "?"
|
||||
return `${Math.round(((total - available) / total) * 100)}`
|
||||
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 {
|
||||
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 {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
export default function SystemStats() {
|
||||
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 (
|
||||
<box>
|
||||
<label class="cpu module" label={cpu((v: string) => ` ${v}%`)} />
|
||||
<label class="ram module" label={ram((v: string) => ` ${v}%`)} />
|
||||
<label class="cpu module" label={cpu((v: string) => `CPU ${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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ export default function Volume() {
|
||||
})
|
||||
|
||||
const cls = mute((m: boolean) =>
|
||||
m ? "volume muted" : "volume"
|
||||
m ? "volume module muted" : "volume module"
|
||||
)
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user