From c4ae79462290f1a8ec15affba3cc748ca6002dfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Av=C3=A9?= Date: Tue, 11 Aug 2026 10:20:19 +0200 Subject: [PATCH] Fix ags battery icon + always select main monitor --- home/ags/files/Bar.tsx | 45 ++++++++++++++++++++++++++++++++++------- home/ags/files/app.ts | 17 +++++++++++++++- home/ags/files/utils.ts | 6 ++++++ 3 files changed, 60 insertions(+), 8 deletions(-) diff --git a/home/ags/files/Bar.tsx b/home/ags/files/Bar.tsx index 2c97fcf..e853bf6 100644 --- a/home/ags/files/Bar.tsx +++ b/home/ags/files/Bar.tsx @@ -183,6 +183,32 @@ function Right() { ); } +// Icon names to try for the current battery state, most specific first. +// Papirus does not ship every combination, so each entry needs a fallback: +// "-charged-symbolic" only exists at level 100, and "-charging-symbolic" only +// up to level 90 (above that the panel icon has to be named directly). +function batteryIconNames(): string[] { + const percentage = battery.percentage; + const thresholds = [...Array(11).keys()].map((i) => i * 10); + const level = thresholds.find((threshold) => threshold >= percentage * 100); + const plain = `battery-level-${level}-symbolic`; + + // Being full is a state, not a 100% reading: with a charge limit set the + // battery stops well below 100 and sits there reporting FULLY_CHARGED. + if (battery.state === Battery.State.FULLY_CHARGED) { + return [`battery-level-${level}-charged-symbolic`, plain]; + } + if (battery.charging) { + const padded = String(level).padStart(3, "0"); + return [ + `battery-level-${level}-charging-symbolic`, + `battery-${padded}-charging`, + plain, + ]; + } + return [plain]; +} + // Papirus encodes the battery level as a 35%-opacity overlay inside its // *-symbolic icons. GTK4's symbolic recoloring flattens that opacity, so every // level renders as a solid, full battery. The Papirus *-symbolic icons are just @@ -190,13 +216,12 @@ function Right() { // normally — so we resolve each name to that underlying file and load it // directly (as a non-symbolic file icon), which renders the correct level. function batteryGicon(): Gio.Icon { - const percentage = battery.percentage; - const thresholds = [...Array(11).keys()].map((i) => i * 10); - const icon = thresholds.find((threshold) => threshold >= percentage * 100); - const name = battery.charging - ? `battery-level-${icon}-${percentage >= 0.99 ? "charged" : "charging"}-symbolic` - : `battery-level-${icon}-symbolic`; - const symbolic = Gtk.IconTheme.get_for_display(Gdk.Display.get_default()!) + const theme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default()!); + // lookup_icon() never fails: for an unknown name it hands back the + // "image-missing" paintable, so check availability before resolving. + const names = batteryIconNames(); + const name = names.find((n) => theme.has_icon(n)) ?? names[names.length - 1]; + const symbolic = theme .lookup_icon(name, null, 16, 1, Gtk.TextDirection.NONE, 0) .get_file(); if (!symbolic) return Gio.ThemedIcon.new(name); @@ -217,15 +242,21 @@ function batteryGicon(): Gio.Icon { return new Gio.FileIcon({ file: symbolic }); } +function batteryTooltip(): string { + return `${Math.round(battery.percentage * 100)}%`; +} + function BatteryIcon(): JSX.Element { if (battery.get_state() == 0) return ; // AstalBattery does not emit notify::percentage/charging/state on this // system, so a signal binding stays frozen at its initial value (e.g. // showing "charged" forever). Poll the live values instead. const gicon = createPoll(batteryGicon(), 10000, batteryGicon); + const tooltip = createPoll(batteryTooltip(), 10000, batteryTooltip); return (