Fix ags battery icon + always select main monitor

This commit is contained in:
Thomas Avé 2026-08-11 10:20:19 +02:00
parent dbec898d65
commit c4ae794622
Signed by: thomasave
SSH Key Fingerprint: SHA256:bvIbWy6TO9+PdMTPzWy6dqkRlVQ3eSky+vQcc9aRIiE
3 changed files with 60 additions and 8 deletions

View File

@ -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 // Papirus encodes the battery level as a 35%-opacity overlay inside its
// *-symbolic icons. GTK4's symbolic recoloring flattens that opacity, so every // *-symbolic icons. GTK4's symbolic recoloring flattens that opacity, so every
// level renders as a solid, full battery. The Papirus *-symbolic icons are just // 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 // 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. // directly (as a non-symbolic file icon), which renders the correct level.
function batteryGicon(): Gio.Icon { function batteryGicon(): Gio.Icon {
const percentage = battery.percentage; const theme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default()!);
const thresholds = [...Array(11).keys()].map((i) => i * 10); // lookup_icon() never fails: for an unknown name it hands back the
const icon = thresholds.find((threshold) => threshold >= percentage * 100); // "image-missing" paintable, so check availability before resolving.
const name = battery.charging const names = batteryIconNames();
? `battery-level-${icon}-${percentage >= 0.99 ? "charged" : "charging"}-symbolic` const name = names.find((n) => theme.has_icon(n)) ?? names[names.length - 1];
: `battery-level-${icon}-symbolic`; const symbolic = theme
const symbolic = Gtk.IconTheme.get_for_display(Gdk.Display.get_default()!)
.lookup_icon(name, null, 16, 1, Gtk.TextDirection.NONE, 0) .lookup_icon(name, null, 16, 1, Gtk.TextDirection.NONE, 0)
.get_file(); .get_file();
if (!symbolic) return Gio.ThemedIcon.new(name); if (!symbolic) return Gio.ThemedIcon.new(name);
@ -217,15 +242,21 @@ function batteryGicon(): Gio.Icon {
return new Gio.FileIcon({ file: symbolic }); return new Gio.FileIcon({ file: symbolic });
} }
function batteryTooltip(): string {
return `${Math.round(battery.percentage * 100)}%`;
}
function BatteryIcon(): JSX.Element { function BatteryIcon(): JSX.Element {
if (battery.get_state() == 0) return <box />; if (battery.get_state() == 0) return <box />;
// AstalBattery does not emit notify::percentage/charging/state on this // AstalBattery does not emit notify::percentage/charging/state on this
// system, so a signal binding stays frozen at its initial value (e.g. // system, so a signal binding stays frozen at its initial value (e.g.
// showing "charged" forever). Poll the live values instead. // showing "charged" forever). Poll the live values instead.
const gicon = createPoll<Gio.Icon>(batteryGicon(), 10000, batteryGicon); const gicon = createPoll<Gio.Icon>(batteryGicon(), 10000, batteryGicon);
const tooltip = createPoll<string>(batteryTooltip(), 10000, batteryTooltip);
return ( return (
<button <button
class="battery-item" class="battery-item"
tooltipText={tooltip}
onClicked={() => execAsync(["gnome-power-statistics"])} onClicked={() => execAsync(["gnome-power-statistics"])}
> >
<box> <box>

View File

@ -4,6 +4,16 @@ import style from "./style.scss";
import Bar from "./Bar"; import Bar from "./Bar";
import NotificationPopups from "./notifications/NotificationPopups"; import NotificationPopups from "./notifications/NotificationPopups";
const PREFERRED_MONITOR = "eDP-1";
function pick_monitor(): Gdk.Monitor | undefined {
const monitors = app.get_monitors();
return (
monitors.find((m: Gdk.Monitor) => m.get_connector() === PREFERRED_MONITOR) ??
monitors[0]
);
}
function register_windows(monitor: Gdk.Monitor) { function register_windows(monitor: Gdk.Monitor) {
let scale = (monitor.get_geometry().width >= 3000) ? 1.2 : 1; let scale = (monitor.get_geometry().width >= 3000) ? 1.2 : 1;
Bar(monitor, scale); Bar(monitor, scale);
@ -14,6 +24,11 @@ app.start({
css: style, css: style,
iconTheme: "Papirus", iconTheme: "Papirus",
main() { main() {
register_windows(app.get_monitors()[0]); const monitor = pick_monitor();
if (!monitor) {
console.error("No monitors available, not creating the bar.");
return;
}
register_windows(monitor);
}, },
}); });

View File

@ -18,9 +18,15 @@ const app_icons = new Apps.Apps().list.reduce(
export function getIconName(app_id: string | null | undefined, title: string | null | undefined) { export function getIconName(app_id: string | null | undefined, title: string | null | undefined) {
if (!app_id && !title) return ""; if (!app_id && !title) return "";
// Our foot terminals carry a unique per-window app-id ("footclient-<pid>") so the
// "duplicate window" keybind can tell them apart; strip that suffix back to the
// base app-id ("footclient") so the icon still resolves.
const norm = app_id ? app_id.replace(/-\d+$/, "") : app_id;
// try fields matching Niri outputs // try fields matching Niri outputs
const possibleKeys = [ const possibleKeys = [
app_id, app_id,
norm,
title title
].filter(Boolean) as string[]; ].filter(Boolean) as string[];