From 910f357f08fc193e2fd83a5e8744d934c0f14ddc Mon Sep 17 00:00:00 2001 From: Bernardo Magri Date: Fri, 10 Jul 2026 13:29:44 +0100 Subject: [PATCH] =?UTF-8?q?fix(desktop):=20hardware=20findings=20=E2=80=94?= =?UTF-8?q?=20charge=20limit,=20polkit=20ws,=20bar=20deps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Latitude 5310 QA: - Clock tooltip: drop empty {calendar} popup; plain date+zone - Charge limit: restart oneshot via polkit + dual write/delay for Dell unplug race; toast verifies sysfs; looser menu match - pwvucontrol ships with waybar (right-click volume) - Print/region→clipboard toasts success - Polkit (and pinentry) open on current workspace + stayfocused V0: flake check --no-build green. V3: retest A3/A4/B1/tooltip/polkit. --- agent/HARDWARE-QUEUE.md | 9 +-- modules/home/hyprland.nix | 7 +++ modules/home/keybinds.nix | 2 +- modules/home/rofi.nix | 86 ++++++++++++++++++++++------- modules/home/waybar.nix | 20 ++++--- modules/nixos/power.nix | 41 ++++++++++++-- templates/downstream/home.nix | 3 +- themes/boreal/waybar.jsonc | 2 +- themes/executive-slate/waybar.jsonc | 2 +- themes/summer-day/waybar.jsonc | 2 +- themes/summer-night/waybar.jsonc | 2 +- 11 files changed, 134 insertions(+), 42 deletions(-) diff --git a/agent/HARDWARE-QUEUE.md b/agent/HARDWARE-QUEUE.md index b80be40..ac8c898 100644 --- a/agent/HARDWARE-QUEUE.md +++ b/agent/HARDWARE-QUEUE.md @@ -20,10 +20,11 @@ QA machine), the **T14s** (webcam case). Menu: type `sys` → **System** (dmenu path uses `rofi_menu` = `-i`; apps/drun already worked via config.rasi). Spot-check System and Tools filters the same way. Network picker inherits `rofi -i`. -- [ ] **Charge-limit live apply (2026-07-10)** — on a laptop with - `charge_control_end_threshold`: System › Battery limit → 80%; - confirm `cat …/charge_control_end_threshold` is 80 without rebuild; - unplug/replug AC, confirm still 80 (oneshot). User in `users` group. +- [ ] **Charge-limit live apply (2026-07-10, retest)** — Latitude first + pass: menu could not leave 80; unplug jumped to 100 until replug. + After fix: System/Power → limit 60 then 90; toast shows “sysfs now + N%”; `cat …/charge_control_end_threshold` matches without rebuild. + Unplug, wait ~3s, still N% (delayed oneshot); plug still N%. - [ ] **#41 float classes (polkit / pinentry / portal / blueman)** — after rebuild: (1) `pkexec true` or mount a disk → polkit dialog should float+center; if not, note `hyprctl clients` class while open. diff --git a/modules/home/hyprland.nix b/modules/home/hyprland.nix index 6a3c006..6418eff 100644 --- a/modules/home/hyprland.nix +++ b/modules/home/hyprland.nix @@ -493,10 +493,17 @@ in # Polkit auth (hyprpolkitagent — app id / binary name in the package) # and pinentry-qt (keys.nix default; org.gnupg.pinentry-qt desktop). + # workspace current: agent was started on ws1 at login; without this + # the dialog opens on that workspace and the current one looks dead + # (hardware: Snapshots menu from ws≠1). stayfocused so it grabs + # attention when it lands. "float 1, match:class (?i)^(hyprpolkitagent|\\.hyprpolkitagent-wrapped)$" "center 1, match:class (?i)^(hyprpolkitagent|\\.hyprpolkitagent-wrapped)$" + "workspace current, match:class (?i)^(hyprpolkitagent|\\.hyprpolkitagent-wrapped)$" + "stayfocused 1, match:class (?i)^(hyprpolkitagent|\\.hyprpolkitagent-wrapped)$" "float 1, match:class (?i)^(pinentry(-qt)?|org\\.gnupg\\.pinentry-qt)$" "center 1, match:class (?i)^(pinentry(-qt)?|org\\.gnupg\\.pinentry-qt)$" + "workspace current, match:class (?i)^(pinentry(-qt)?|org\\.gnupg\\.pinentry-qt)$" ]; # Rendered from ./keybinds.nix (the cheatsheet reads the same list), diff --git a/modules/home/keybinds.nix b/modules/home/keybinds.nix index eb43298..a1ce990 100644 --- a/modules/home/keybinds.nix +++ b/modules/home/keybinds.nix @@ -72,7 +72,7 @@ # Bare Print → region to clipboard; the two → file binds save a # timestamped PNG under ~/Pictures/Screenshots and toast the path, the # same plumbing the Capture menu's "→ file" rows use. - { mods = ""; key = "Print"; action = "exec, grim -g \"$(slurp)\" - | wl-copy"; desc = "Screenshot region → clipboard"; } + { mods = ""; key = "Print"; action = "exec, grim -g \"$(slurp)\" - | wl-copy && notify-send Screenshot \"Region copied to clipboard.\""; desc = "Screenshot region → clipboard"; } { mods = "SHIFT"; key = "Print"; action = "exec, f=$HOME/Pictures/Screenshots/$(date +%Y%m%d-%H%M%S).png; mkdir -p $HOME/Pictures/Screenshots && grim -g \"$(slurp)\" \"$f\" && notify-send \"Screenshot saved\" \"$f\""; desc = "Screenshot region → file"; } { mods = "CTRL"; key = "Print"; action = "exec, f=$HOME/Pictures/Screenshots/$(date +%Y%m%d-%H%M%S).png; mkdir -p $HOME/Pictures/Screenshots && grim \"$f\" && notify-send \"Screenshot saved\" \"$f\""; desc = "Screenshot screen → file"; } { mods = "$mod SHIFT"; key = "Print"; action = "exec, f=$HOME/Pictures/Screenshots/$(date +%Y%m%d-%H%M%S).png; mkdir -p $HOME/Pictures/Screenshots && grim -g \"$(slurp)\" - | satty --filename - --fullscreen --output-filename \"$f\""; desc = "Annotate region"; } diff --git a/modules/home/rofi.nix b/modules/home/rofi.nix index bdb9f5d..20f4e12 100644 --- a/modules/home/rofi.nix +++ b/modules/home/rofi.nix @@ -321,26 +321,48 @@ let fi back } | rofi_menu -show-icons -p "Power — profile: ''${curp:-n/a}, limit: ''${curc:-default}") || exit 0 - # Persist in-flake + apply live to sysfs (udev makes the node - # group-writable for users — Bernardo 2026-07-10). + # Persist in-flake, then re-apply via the root oneshot (polkit + # allows users to restart it) so a non-writable sysfs node still + # updates. Fallback: direct echo if the node is group-writable. set_limit() { nomarchy-theme-sync --quiet set settings.power.batteryChargeLimit "$1" --no-switch n="$1"; [ "$n" = null ] && n=100 + applied= + if systemctl restart nomarchy-battery-charge-limit.service 2>/dev/null; then + applied=1 + else + for d in /sys/class/power_supply/*/; do + [ "$(cat "$d/type" 2>/dev/null)" = Battery ] || continue + [ "$(cat "$d/scope" 2>/dev/null || echo System)" = Device ] && continue + thresh="$d/charge_control_end_threshold" + if [ -w "$thresh" ] && echo "$n" > "$thresh" 2>/dev/null; then + applied=1 + fi + done + fi + actual= for d in /sys/class/power_supply/*/; do [ "$(cat "$d/type" 2>/dev/null)" = Battery ] || continue [ "$(cat "$d/scope" 2>/dev/null || echo System)" = Device ] && continue - thresh="$d/charge_control_end_threshold" - [ -w "$thresh" ] && echo "$n" > "$thresh" 2>/dev/null || true + [ -r "$d/charge_control_end_threshold" ] \ + && actual=$(cat "$d/charge_control_end_threshold" 2>/dev/null) && break done - notify-send "Battery limit" "$2 — applied now." + if [ -n "$applied" ] && [ -n "$actual" ]; then + notify-send "Battery limit" "$2 — sysfs now $actual%." + elif [ -n "$applied" ]; then + notify-send "Battery limit" "$2 — applied (could not re-read sysfs)." + else + notify-send -u critical "Battery limit" \ + "Saved in theme-state but could not write sysfs (need rebuild or users-group on threshold)." + fi } case "$choice" in "$BACK") exec "$0" ;; "Profile: "*) powerprofilesctl set "''${choice#Profile: }" ;; - "Charge limit: 80%"*) set_limit 80 "Set to 80%" ;; - "Charge limit: 90%"*) set_limit 90 "Set to 90%" ;; - "Charge limit: 60%"*) set_limit 60 "Set to 60%" ;; - "Charge limit: Off"*) set_limit null "Off — charges to 100%" ;; + *"Charge limit: 80%"*) set_limit 80 "Set to 80%" ;; + *"Charge limit: 90%"*) set_limit 90 "Set to 90%" ;; + *"Charge limit: 60%"*) set_limit 60 "Set to 60%" ;; + *"Charge limit: Off"*) set_limit null "Off — charges to 100%" ;; esac ;; theme) @@ -521,7 +543,9 @@ ${themeRows} file="$dir/$(date +%Y%m%d-%H%M%S).png" case "$choice" in "$BACK") exec "$0" tools ;; - *"Region → clipboard"*) grim -g "$(slurp)" - | wl-copy ;; + *"Region → clipboard"*) + grim -g "$(slurp)" - | wl-copy \ + && notify-send "Screenshot" "Region copied to clipboard." ;; *"Region → file"*) mkdir -p "$dir"; grim -g "$(slurp)" "$file" \ && notify-send "Screenshot saved" "$file" ;; *"Annotate region"*) mkdir -p "$dir"; grim -g "$(slurp)" - | satty --filename - --fullscreen --output-filename "$file" ;; @@ -535,7 +559,9 @@ ${themeRows} || { notify-send "OCR" "No text recognized in that region."; exit 0; } printf '%s' "$txt" | wl-copy notify-send "OCR" "Copied $(printf '%s' "$txt" | wc -w) word(s) to the clipboard." ;; - *"Full screen → clipboard"*) grim - | wl-copy ;; + *"Full screen → clipboard"*) + grim - | wl-copy \ + && notify-send "Screenshot" "Screen copied to clipboard." ;; *"Full screen → file"*) mkdir -p "$dir"; grim "$file" \ && notify-send "Screenshot saved" "$file" ;; *"Stop recording"*) exec nomarchy-record stop ;; @@ -937,21 +963,43 @@ ${themeRows} set_limit() { nomarchy-theme-sync --quiet set settings.power.batteryChargeLimit "$1" --no-switch n="$1"; [ "$n" = null ] && n=100 + applied= + if systemctl restart nomarchy-battery-charge-limit.service 2>/dev/null; then + applied=1 + else + for d in /sys/class/power_supply/*/; do + [ "$(cat "$d/type" 2>/dev/null)" = Battery ] || continue + [ "$(cat "$d/scope" 2>/dev/null || echo System)" = Device ] && continue + thresh="$d/charge_control_end_threshold" + if [ -w "$thresh" ] && echo "$n" > "$thresh" 2>/dev/null; then + applied=1 + fi + done + fi + actual= for d in /sys/class/power_supply/*/; do [ "$(cat "$d/type" 2>/dev/null)" = Battery ] || continue [ "$(cat "$d/scope" 2>/dev/null || echo System)" = Device ] && continue - thresh="$d/charge_control_end_threshold" - [ -w "$thresh" ] && echo "$n" > "$thresh" 2>/dev/null || true + [ -r "$d/charge_control_end_threshold" ] \ + && actual=$(cat "$d/charge_control_end_threshold" 2>/dev/null) && break done - notify-send "Battery limit" "$2 — applied now." + if [ -n "$applied" ] && [ -n "$actual" ]; then + notify-send "Battery limit" "$2 — sysfs now $actual%." + elif [ -n "$applied" ]; then + notify-send "Battery limit" "$2 — applied (could not re-read sysfs)." + else + notify-send -u critical "Battery limit" \ + "Saved in theme-state but could not write sysfs (need rebuild or users-group on threshold)." + fi } case "$sel" in "$BACK") exec "$0" system ;; - "80%"*) set_limit 80 "Set to 80%" ;; - "90%"*) set_limit 90 "Set to 90%" ;; - "60%"*) set_limit 60 "Set to 60%" ;; - "Off"*) set_limit null "Off — charges to 100%" ;; - "Custom"*) + # Order: Off (100%) before 90/80/60 so "100%" does not hit *0%* patterns. + *Off*|*100%*) set_limit null "Off — charges to 100%" ;; + *90%*) set_limit 90 "Set to 90%" ;; + *80%*) set_limit 80 "Set to 80%" ;; + *60%*) set_limit 60 "Set to 60%" ;; + *Custom*) limit=$(rofi_menu -p "Charge limit % (50–100)" < /dev/null) || exit 0 [ -n "$limit" ] || exit 0 case "$limit" in diff --git a/modules/home/waybar.nix b/modules/home/waybar.nix index b8b1f4d..1aeb3b3 100644 --- a/modules/home/waybar.nix +++ b/modules/home/waybar.nix @@ -204,12 +204,14 @@ let # into clock + clock#date — one click surface for the calendar). format = "{:%H:%M · %a %d %b}"; # Left-click → the calendar (nomarchy-calendar → calcurse in a floating - # ghostty). Long weekday/month name + zone + month grid ride in the - # tooltip so the bar stays compact. + # ghostty). Tooltip is plain date/zone only — embedding {calendar} + # produced an empty popup on hardware (2026-07-10); month view is + # one click away. on-click = "nomarchy-calendar"; + tooltip = true; # The zone line stays live under auto-timezone (the tz-watch SIGUSR2 # reload keeps it showing the currently *detected* zone). - tooltip-format = "{:%A %d %B %Y}\n{:%Z (UTC%z)}\n{calendar}"; + tooltip-format = "{:%A, %d %B %Y}\n{:%Z (UTC%z)}"; }; # Active keyboard layout (per focused device) — only placed in @@ -503,10 +505,12 @@ in ); }; - # The power-profile helpers on PATH, so both the generated bar and the - # whole-swap themes' static waybar.jsonc can exec them by name — plus - # the supervisor hyprland.nix exec-onces. calcurse is the clock-click - # calendar (feature dependency of the bar, not an opt-in app). + # Bar helpers on PATH (whole-swap jsoncs exec them by bare name) + + # feature deps of bar clicks: calcurse (clock), pwvucontrol (volume + # right-click) — not opt-in template packages. home.packages = lib.optionals config.nomarchy.waybar.enable - [ waybarSupervisor powerProfileStatus powerProfileCycle vpnStatus doctorStatus calendarLauncher pkgs.calcurse ]; + [ waybarSupervisor powerProfileStatus powerProfileCycle vpnStatus doctorStatus calendarLauncher + pkgs.calcurse + pkgs.pwvucontrol + ]; } diff --git a/modules/nixos/power.nix b/modules/nixos/power.nix index 1164d40..c537735 100644 --- a/modules/nixos/power.nix +++ b/modules/nixos/power.nix @@ -74,7 +74,16 @@ in [ "$(cat "$d/type" 2>/dev/null)" = Battery ] || continue [ "$(cat "$d/scope" 2>/dev/null || echo System)" = Device ] && continue thresh="$d/charge_control_end_threshold" - [ -w "$thresh" ] && echo "$limit" > "$thresh" || true + [ -e "$thresh" ] || continue + # Keep the node group-writable for the menu's live path (udev + # sets this on add; re-assert after firmware recreates attrs). + chgrp users "$thresh" 2>/dev/null || true + chmod 0664 "$thresh" 2>/dev/null || true + # Some firmwares (Dell) reset the threshold on unplug *after* + # the udev event — write once, wait, write again. + echo "$limit" > "$thresh" 2>/dev/null || true + sleep 1 + echo "$limit" > "$thresh" 2>/dev/null || true done exit 0 ''; @@ -82,9 +91,33 @@ in # Writable threshold for the logged-in user (menu live apply) + AC # re-apply of the oneshot. Laptop only; no-op when the attr is absent. - services.udev.extraRules = lib.mkIf cfg.laptop '' - SUBSYSTEM=="power_supply", ATTR{type}=="Battery", TEST=="charge_control_end_threshold", GROUP="users", MODE="0664" - SUBSYSTEM=="power_supply", ATTR{type}=="Mains", RUN+="${config.systemd.package}/bin/systemctl --no-block restart nomarchy-battery-charge-limit.service" + # Immediate + delayed restart: Dell/Latitude firmware often stomps the + # threshold to 100 right after unplug; a single immediate oneshot + # loses the race (hardware: unplug→100, plug→80). systemd-run hands + # the delayed restart off so udev's short RUN budget is not held. + services.udev.extraRules = lib.mkIf cfg.laptop ( + let + systemctl = "${config.systemd.package}/bin/systemctl"; + systemdRun = "${config.systemd.package}/bin/systemd-run"; + in '' + SUBSYSTEM=="power_supply", ATTR{type}=="Battery", TEST=="charge_control_end_threshold", GROUP="users", MODE="0664" + SUBSYSTEM=="power_supply", ATTR{type}=="Mains", RUN+="${systemctl} --no-block restart nomarchy-battery-charge-limit.service" + SUBSYSTEM=="power_supply", ATTR{type}=="Mains", RUN+="${systemdRun} --no-block --collect --on-active=2s --timer-property=AccuracySec=200ms ${systemctl} --no-block restart nomarchy-battery-charge-limit.service" + '' + ); + + # Unprivileged restart of the oneshot so the menu can re-apply as + # root when the threshold node is not (yet) user-writable. + security.polkit.extraConfig = lib.mkIf cfg.laptop '' + polkit.addRule(function(action, subject) { + if (action.id == "org.freedesktop.systemd1.manage-units" && + subject.isInGroup("users")) { + var unit = action.lookup("unit"); + if (unit == "nomarchy-battery-charge-limit.service") { + return polkit.Result.YES; + } + } + }); ''; }; } diff --git a/templates/downstream/home.nix b/templates/downstream/home.nix index c722e49..7292cc3 100644 --- a/templates/downstream/home.nix +++ b/templates/downstream/home.nix @@ -85,8 +85,7 @@ inkscape # vector graphics mpv # media player amberol # music player (local library; streaming → spotify below) - pwvucontrol # PipeWire volume mixer (right-click the Waybar volume) - # calcurse ships with the Waybar clock (modules/home/waybar.nix) + # pwvucontrol + calcurse ship with the Waybar module (volume right-click / clock) # ── More apps — uncomment to add ───────────────────────────────── # Web browsers diff --git a/themes/boreal/waybar.jsonc b/themes/boreal/waybar.jsonc index 638362e..47706b0 100644 --- a/themes/boreal/waybar.jsonc +++ b/themes/boreal/waybar.jsonc @@ -40,7 +40,7 @@ "format": "{:%H:%M · %a %d %b}", "on-click": "nomarchy-calendar", "tooltip": true, - "tooltip-format": "{:%A %d %B %Y}\n{:%Z (UTC%z)}\n{calendar}" + "tooltip-format": "{:%A, %d %B %Y}\n{:%Z (UTC%z)}" }, "hyprland/workspaces": { "disable-scroll": true, diff --git a/themes/executive-slate/waybar.jsonc b/themes/executive-slate/waybar.jsonc index 602b672..11ac2cb 100644 --- a/themes/executive-slate/waybar.jsonc +++ b/themes/executive-slate/waybar.jsonc @@ -47,7 +47,7 @@ "format": "{:%H:%M · %a %d %b}", "on-click": "nomarchy-calendar", "tooltip": true, - "tooltip-format": "{:%A %d %B %Y}\n{:%Z (UTC%z)}\n{calendar}" + "tooltip-format": "{:%A, %d %B %Y}\n{:%Z (UTC%z)}" }, "custom/recording": { "return-type": "json", diff --git a/themes/summer-day/waybar.jsonc b/themes/summer-day/waybar.jsonc index 0c7a227..75a3a7a 100644 --- a/themes/summer-day/waybar.jsonc +++ b/themes/summer-day/waybar.jsonc @@ -97,7 +97,7 @@ "format": " {:%H:%M · %a %d %b}", "on-click": "nomarchy-calendar", "tooltip": true, - "tooltip-format": "{:%A %d %B %Y}\n{:%Z (UTC%z)}\n{calendar}" + "tooltip-format": "{:%A, %d %B %Y}\n{:%Z (UTC%z)}" }, "tray": { "spacing": 8 diff --git a/themes/summer-night/waybar.jsonc b/themes/summer-night/waybar.jsonc index 4e81bfe..cd955dc 100644 --- a/themes/summer-night/waybar.jsonc +++ b/themes/summer-night/waybar.jsonc @@ -96,7 +96,7 @@ "format": " {:%H:%M · %a %d %b}", "on-click": "nomarchy-calendar", "tooltip": true, - "tooltip-format": "{:%A %d %B %Y}\n{:%Z (UTC%z)}\n{calendar}" + "tooltip-format": "{:%A, %d %B %Y}\n{:%Z (UTC%z)}" }, "tray": { "spacing": 8