diff --git a/modules/home/rofi.nix b/modules/home/rofi.nix index 8f01568..4f9dd00 100644 --- a/modules/home/rofi.nix +++ b/modules/home/rofi.nix @@ -157,7 +157,7 @@ let " ${prettyKeys (lib.head m)}"; menuHint = module: hintForAction "exec, nomarchy-menu ${module}"; # Apps opens rofi drun directly, not a nomarchy-menu module. - appsHint = " SUPER + D"; + appsHint = " SUPER + Space"; # A menu row that runs the same op as a global bind whose action isn't a # `nomarchy-menu` module (the Capture rows ↔ the Print screenshot keys): # match on mods+key so the hint still reads from keybinds.nix (single @@ -327,6 +327,7 @@ let set_limit() { nomarchy-theme-sync --quiet set settings.power.batteryChargeLimit "$1" --no-switch n="$1"; [ "$n" = null ] && n=100 + # Root oneshot sets Custom charge type (Dell) + thresholds. applied= if systemctl restart nomarchy-battery-charge-limit.service 2>/dev/null; then applied=1 @@ -334,21 +335,34 @@ let 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 + if [ "$n" -lt 100 ] 2>/dev/null; then + for tnode in charge_types charge_type; do + [ -w "$d$tnode" ] || continue + listed=$(cat "$d$tnode" 2>/dev/null || true) + case " $listed " in *" Custom "*|*"Custom"*) echo Custom > "$d$tnode" 2>/dev/null || true ;; esac + done + fi thresh="$d/charge_control_end_threshold" if [ -w "$thresh" ] && echo "$n" > "$thresh" 2>/dev/null; then applied=1 fi done fi - actual= + actual=; ctype= 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 [ -r "$d/charge_control_end_threshold" ] \ - && actual=$(cat "$d/charge_control_end_threshold" 2>/dev/null) && break + && actual=$(cat "$d/charge_control_end_threshold" 2>/dev/null) + ctype=$(cat "$d/charge_types" 2>/dev/null || cat "$d/charge_type" 2>/dev/null || true) + [ -n "$actual" ] && break done + # Highlight active type: "… [Custom] …" + active=$(printf '%s' "$ctype" | sed -n 's/.*\[\([^]]*\)\].*/\1/p') + extra= + [ -n "$active" ] && extra=" (type $active)" if [ -n "$applied" ] && [ -n "$actual" ]; then - notify-send "Battery limit" "$2 — sysfs now $actual%." + notify-send "Battery limit" "$2 — stop at $actual%$extra." elif [ -n "$applied" ]; then notify-send "Battery limit" "$2 — applied (could not re-read sysfs)." else @@ -970,21 +984,32 @@ ${themeRows} 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 + if [ "$n" -lt 100 ] 2>/dev/null; then + for tnode in charge_types charge_type; do + [ -w "$d$tnode" ] || continue + listed=$(cat "$d$tnode" 2>/dev/null || true) + case " $listed " in *" Custom "*|*"Custom"*) echo Custom > "$d$tnode" 2>/dev/null || true ;; esac + done + fi thresh="$d/charge_control_end_threshold" if [ -w "$thresh" ] && echo "$n" > "$thresh" 2>/dev/null; then applied=1 fi done fi - actual= + actual=; ctype= 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 [ -r "$d/charge_control_end_threshold" ] \ - && actual=$(cat "$d/charge_control_end_threshold" 2>/dev/null) && break + && actual=$(cat "$d/charge_control_end_threshold" 2>/dev/null) + ctype=$(cat "$d/charge_types" 2>/dev/null || cat "$d/charge_type" 2>/dev/null || true) + [ -n "$actual" ] && break done + active=$(printf '%s' "$ctype" | sed -n 's/.*\[\([^]]*\)\].*/\1/p') + extra=; [ -n "$active" ] && extra=" (type $active)" if [ -n "$applied" ] && [ -n "$actual" ]; then - notify-send "Battery limit" "$2 — sysfs now $actual%." + notify-send "Battery limit" "$2 — stop at $actual%$extra." elif [ -n "$applied" ]; then notify-send "Battery limit" "$2 — applied (could not re-read sysfs)." else diff --git a/modules/nixos/options.nix b/modules/nixos/options.nix index 28d5810..1bc84ca 100644 --- a/modules/nixos/options.nix +++ b/modules/nixos/options.nix @@ -103,6 +103,8 @@ batteryChargeLimit = lib.mkOption { type = lib.types.nullOr (lib.types.ints.between 50 100); default = config.nomarchy.settings.power.batteryChargeLimit or null; + # Dell Adaptive charge mode ignores the end threshold unless we + # also select Custom (power.nix oneshot); see Latitude 5310 QA. example = 80; description = '' Stop charging at this percentage to extend battery lifespan, @@ -112,8 +114,10 @@ null leaves charging at the firmware default (menu writes 100). Backend-independent: the menu applies live via sysfs (udev GROUP=users on the threshold node) and persists settings in - theme-state; a oneshot re-applies on boot and AC replug. Needs - nomarchy.system.power.laptop. + theme-state; a oneshot re-applies on boot and AC replug. On + Dell (and similar) the oneshot also selects charge type Custom + — Adaptive ignores the threshold while still reporting it. + Needs nomarchy.system.power.laptop. ''; }; }; diff --git a/modules/nixos/power.nix b/modules/nixos/power.nix index c537735..fb61a58 100644 --- a/modules/nixos/power.nix +++ b/modules/nixos/power.nix @@ -75,14 +75,62 @@ in [ "$(cat "$d/scope" 2>/dev/null || echo System)" = Device ] && continue thresh="$d/charge_control_end_threshold" [ -e "$thresh" ] || continue - # Keep the node group-writable for the menu's live path (udev + # Keep nodes 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 + for node in charge_control_end_threshold charge_control_start_threshold charge_types charge_type; do + [ -e "$d$node" ] || continue + chgrp users "$d$node" 2>/dev/null || true + chmod 0664 "$d$node" 2>/dev/null || true + done + # Dell (and some others): charge_control_* thresholds are only + # honoured in Custom mode. Adaptive/Standard ignore end_threshold + # while still reporting the written value — battery keeps charging + # past the cap (Latitude 5310: end=80, type=[Adaptive], capacity 96%+). + # Off (100): restore Adaptive if listed, else leave type alone. + ctypes="$d/charge_types" + ctype="$d/charge_type" + if [ -e "$ctypes" ] || [ -e "$ctype" ]; then + listed=$(cat "$ctypes" 2>/dev/null || cat "$ctype" 2>/dev/null || true) + write_type() { + local t="$1" + [ -n "$t" ] || return 0 + if [ -w "$ctypes" ]; then echo "$t" > "$ctypes" 2>/dev/null || true + elif [ -w "$ctype" ]; then echo "$t" > "$ctype" 2>/dev/null || true + fi + } + if [ "$limit" -lt 100 ] 2>/dev/null; then + # listed looks like: "Trickle Fast Standard [Adaptive] Custom" + case "$listed" in *Custom*) write_type Custom ;; esac + else + case "$listed" in + *Adaptive*) write_type Adaptive ;; + *Standard*) write_type Standard ;; + esac + fi + fi + # Start threshold: must be < end. Keep a ~10% hysteresis band when + # the node exists (firmware may reject start >= end). + start_node="$d/charge_control_start_threshold" + if [ -w "$start_node" ] && [ "$limit" -lt 100 ] 2>/dev/null; then + start=$(( limit > 15 ? limit - 10 : 0 )) + echo "$start" > "$start_node" 2>/dev/null || true + fi # Some firmwares (Dell) reset the threshold on unplug *after* - # the udev event — write once, wait, write again. + # the udev event — write once, wait, write again (type first). echo "$limit" > "$thresh" 2>/dev/null || true sleep 1 + if [ -e "$ctypes" ] || [ -e "$ctype" ]; then + listed=$(cat "$ctypes" 2>/dev/null || cat "$ctype" 2>/dev/null || true) + if [ "$limit" -lt 100 ] 2>/dev/null; then + case "$listed" in + *Custom*) + if [ -w "$ctypes" ]; then echo Custom > "$ctypes" 2>/dev/null || true + elif [ -w "$ctype" ]; then echo Custom > "$ctype" 2>/dev/null || true + fi + ;; + esac + fi + fi echo "$limit" > "$thresh" 2>/dev/null || true done exit 0 @@ -101,6 +149,9 @@ in 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}=="Battery", TEST=="charge_types", GROUP="users", MODE="0664" + SUBSYSTEM=="power_supply", ATTR{type}=="Battery", TEST=="charge_type", GROUP="users", MODE="0664" + SUBSYSTEM=="power_supply", ATTR{type}=="Battery", TEST=="charge_control_start_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" '' diff --git a/pkgs/nomarchy-doctor/nomarchy-doctor.sh b/pkgs/nomarchy-doctor/nomarchy-doctor.sh index 4232f23..fd02e83 100644 --- a/pkgs/nomarchy-doctor/nomarchy-doctor.sh +++ b/pkgs/nomarchy-doctor/nomarchy-doctor.sh @@ -233,6 +233,8 @@ fi # ── laptop battery charge threshold (only when a limit is set) ─────── # Name-agnostic (BAT0, CMB0, …) — same type/scope filter as notify (#60). +# Dell: Adaptive mode ignores end_threshold while still reporting the +# written value — warn if type is not Custom when a limit is active. bat_seen=0; bat_limited=0 for bat in /sys/class/power_supply/*/; do [ "$(cat "$bat/type" 2>/dev/null)" = Battery ] || continue @@ -246,7 +248,15 @@ for bat in /sys/class/power_supply/*/; do esac if [ "$lim" -lt 100 ]; then bat_limited=1 - ok "$(basename "$bat") charge limit active at ${lim}%" + ctype=$(cat "$bat/charge_types" 2>/dev/null || cat "$bat/charge_type" 2>/dev/null || true) + active=$(printf '%s' "$ctype" | sed -n 's/.*\[\([^]]*\)\].*/\1/p') + name=$(basename "$bat") + if [ -n "$active" ] && [ "$active" != Custom ]; then + warn "$name charge limit ${lim}% but type is $active (not Custom)" \ + "thresholds only apply in Custom — run: systemctl restart nomarchy-battery-charge-limit" + else + ok "$name charge limit active at ${lim}%${active:+ (type $active)}" + fi fi done if [ "$bat_seen" -eq 1 ] && [ "$bat_limited" -eq 0 ]; then