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 (