diff --git a/.gitignore b/.gitignore index 885944c2..20bfa5b2 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ old/ docs/superpowers/ .superpowers/ +result/ diff --git a/home/programs/tui/git/default.nix b/home/programs/tui/git/default.nix index e82f566c..e86cf5df 100644 --- a/home/programs/tui/git/default.nix +++ b/home/programs/tui/git/default.nix @@ -17,6 +17,7 @@ in { "result" "result-*" ".superpowers/" + ".claude/" "docs/superpowers/" ]; settings = { diff --git a/home/system/waybar/battery-monitor.sh b/home/system/waybar/battery-monitor.sh new file mode 100644 index 00000000..7a707772 --- /dev/null +++ b/home/system/waybar/battery-monitor.sh @@ -0,0 +1,104 @@ +# battery-monitor — low-battery alerts for waybar/swaync. +# Sourced by the tests with BATTERY_MONITOR_TEST=1 (defines the functions, skips main). +# Otherwise runs main once (triggered by the battery-monitor systemd timer). + +POWER_SUPPLY_DIR="${POWER_SUPPLY_DIR:-/sys/class/power_supply}" +STATE_FILE="${BATTERY_STATE_FILE:-/tmp/battery-alert-state}" +CRIT_ID_FILE="${BATTERY_CRIT_ID_FILE:-/tmp/battery-crit-id}" + +WARN_THRESHOLD=10 +CRIT_THRESHOLD=5 + +# Pure decision. Prints ":". +# action ∈ noop | osd_warn | crit_show | crit_clear ; state ∈ none | warn10 | crit5 +battery_decide() { + status=$1 + cap=$2 + prev=$3 + + if [ "$status" = "Discharging" ]; then + if [ "$cap" -le "$CRIT_THRESHOLD" ]; then + echo "crit_show:crit5" + elif [ "$cap" -le "$WARN_THRESHOLD" ]; then + if [ "$prev" = "crit5" ]; then + echo "crit_clear:warn10" + elif [ "$prev" = "warn10" ]; then + echo "noop:warn10" + else + echo "osd_warn:warn10" + fi + else + if [ "$prev" = "crit5" ]; then + echo "crit_clear:none" + else + echo "noop:none" + fi + fi + else + if [ "$prev" = "crit5" ]; then + echo "crit_clear:none" + else + echo "noop:none" + fi + fi +} + +# First real battery (type == Battery, with status + capacity). +resolve_battery() { + for d in "$POWER_SUPPLY_DIR"/*; do + [ -r "$d/type" ] || continue + [ "$(cat "$d/type")" = "Battery" ] || continue + [ -r "$d/status" ] && [ -r "$d/capacity" ] || continue + printf '%s' "$d" + return 0 + done + return 1 +} + +show_warn_osd() { + waybar-osd "󰂃 Low battery · $1%" +} + +show_crit_notification() { + cap=$1 + id=$(cat "$CRIT_ID_FILE" 2>/dev/null || echo 0) + newid=$(notify-send --print-id --replace-id="$id" \ + --urgency=critical --expire-time=0 \ + "󰂃 Critical battery" "Battery at $cap% — plug in the charger") + printf '%s' "$newid" > "$CRIT_ID_FILE" +} + +clear_crit_notification() { + id=$(cat "$CRIT_ID_FILE" 2>/dev/null || echo 0) + if [ "$id" != "0" ]; then + gdbus call --session \ + --dest org.freedesktop.Notifications \ + --object-path /org/freedesktop/Notifications \ + --method org.freedesktop.Notifications.CloseNotification "$id" \ + >/dev/null 2>&1 || true + fi + rm -f "$CRIT_ID_FILE" +} + +main() { + dir=$(resolve_battery) || exit 0 + + status=$(cat "$dir/status") + cap=$(cat "$dir/capacity") + prev=$(cat "$STATE_FILE" 2>/dev/null || echo none) + + result=$(battery_decide "$status" "$cap" "$prev") + action=${result%%:*} + newstate=${result##*:} + + case "$action" in + osd_warn) show_warn_osd "$cap" ;; + crit_show) show_crit_notification "$cap" ;; + crit_clear) clear_crit_notification ;; + noop) : ;; + esac + + printf '%s' "$newstate" > "$STATE_FILE" +} + +[ "${BATTERY_MONITOR_TEST:-}" = "1" ] || main "$@" diff --git a/home/system/waybar/default.nix b/home/system/waybar/default.nix index 27bb342e..22053edf 100644 --- a/home/system/waybar/default.nix +++ b/home/system/waybar/default.nix @@ -20,7 +20,25 @@ in { iw hyprsunset ] - ++ (with scripts; [vol-up vol-down vol-mute mic-mute bright-up bright-down nightshift-toggle focus-toggle waybar-toggle wifi-toggle bluetooth-toggle dnd-toggle output-cycle input-cycle color-pick screenshot-edit record-toggle power-cycle airplane-toggle clipboard-menu emoji-picker caffeine-toggle mic-status]); + ++ (with scripts; [waybar-osd battery-monitor vol-up vol-down vol-mute mic-mute bright-up bright-down nightshift-toggle focus-toggle waybar-toggle wifi-toggle bluetooth-toggle dnd-toggle output-cycle input-cycle color-pick screenshot-edit record-toggle power-cycle airplane-toggle clipboard-menu emoji-picker caffeine-toggle mic-status]); + + # Poll battery level every 30s; battery-monitor no-ops when no battery exists. + systemd.user.services.battery-monitor = { + Unit.Description = "Low-battery OSD and critical notification monitor"; + Service = { + Type = "oneshot"; + ExecStart = "${scripts.battery-monitor}/bin/battery-monitor"; + }; + }; + + systemd.user.timers.battery-monitor = { + Unit.Description = "Poll battery level for low-battery alerts"; + Timer = { + OnBootSec = "1min"; + OnUnitActiveSec = "30s"; + }; + Install.WantedBy = ["timers.target"]; + }; wayland.windowManager.hyprland.settings.exec-once = [ "waybar" diff --git a/home/system/waybar/scripts.nix b/home/system/waybar/scripts.nix index da71fa76..62dc1a48 100644 --- a/home/system/waybar/scripts.nix +++ b/home/system/waybar/scripts.nix @@ -1,10 +1,29 @@ {pkgs}: let + # Canonical OSD popup: write the shared slot, signal waybar, expire after 2.5s. + # Packaged (runtimeInputs) so it works outside a graphical session (systemd). + waybar-osd = pkgs.writeShellApplication { + name = "waybar-osd"; + runtimeInputs = with pkgs; [procps coreutils]; + text = '' + if [ -f /tmp/waybar-osd-pid ]; then + kill "$(cat /tmp/waybar-osd-pid)" 2>/dev/null || true + fi + printf '%s' "$1" > /tmp/waybar-osd + pkill -RTMIN+8 waybar 2>/dev/null || true + ( sleep 2.5; rm -f /tmp/waybar-osd /tmp/waybar-osd-pid; pkill -RTMIN+8 waybar 2>/dev/null || true ) & + printf '%s' "$!" > /tmp/waybar-osd-pid + ''; + }; + + # battery-monitor: low-battery alerts, run by the systemd user timer. + battery-monitor = pkgs.writeShellApplication { + name = "battery-monitor"; + runtimeInputs = with pkgs; [waybar-osd libnotify glib coreutils]; + text = builtins.readFile ./battery-monitor.sh; + }; + updateOsd = '' - [ -f /tmp/waybar-osd-pid ] && kill "$(cat /tmp/waybar-osd-pid)" 2>/dev/null - printf '%s' "$OSD_TEXT" > /tmp/waybar-osd - pkill -RTMIN+8 waybar 2>/dev/null - ( sleep 2.5; rm -f /tmp/waybar-osd /tmp/waybar-osd-pid; pkill -RTMIN+8 waybar 2>/dev/null ) & - printf '%s' "$!" > /tmp/waybar-osd-pid + ${waybar-osd}/bin/waybar-osd "$OSD_TEXT" ''; volGetText = '' @@ -46,10 +65,12 @@ # launcher, unusable for a list, so override the geometry here. tofiMenu = "${pkgs.tofi}/bin/tofi --horizontal false --anchor center --width 700 --height 500 --margin-top 0 --margin-left 0 --margin-right 0 --num-results 10"; in { + inherit waybar-osd battery-monitor; + bluetoothScript = pkgs.writeShellScript "waybar-bluetooth" '' jq=${pkgs.jq}/bin/jq nl=$'\n' - # bluetoothctl bloque sans contrôleur par défaut → toujours borné par timeout. + # bluetoothctl hangs without a controller by default → always bounded by timeout. bt() { timeout 3 ${pkgs.bluez}/bin/bluetoothctl "$@" 2>/dev/null; } powered=$(bt show | awk '/Powered:/ { print $2; exit }') @@ -331,7 +352,7 @@ in { record-toggle = pkgs.writeShellScriptBin "record-toggle" '' if pgrep -x wf-recorder >/dev/null; then - # -INT laisse wf-recorder finaliser le fichier proprement. + # -INT lets wf-recorder finalize the file cleanly. pkill -INT -x wf-recorder OSD_TEXT="󰕧 Recording saved" else @@ -385,7 +406,7 @@ in { emoji-picker = pkgs.writeShellScriptBin "emoji-picker" '' export PATH="${pkgs.wtype}/bin:${pkgs.wl-clipboard}/bin:$PATH" export BEMOJI_PICKER_CMD="${tofiMenu} --prompt-text 'emoji: '" - # -t tape l'emoji (wtype), -c le copie aussi. + # -t types the emoji (wtype), -c also copies it. exec ${pkgs.bemoji}/bin/bemoji -t -c ''; diff --git a/home/system/waybar/settings.nix b/home/system/waybar/settings.nix index 3bb3790e..cd40b219 100644 --- a/home/system/waybar/settings.nix +++ b/home/system/waybar/settings.nix @@ -8,7 +8,6 @@ }: let gaps-out = config.theme.gaps-out; c = config.lib.stylix.colors; - hasBattery = config.var.hasBattery or false; in { programs.waybar.settings = [ { @@ -16,9 +15,7 @@ in { position = "top"; height = config.theme.bar-height; margin = "${toString gaps-out} ${toString gaps-out} 0"; - modules-center = - ["custom/osd" "custom/osd-sep" "clock" "tray" "hyprland/workspaces" "custom/network" "custom/bluetooth"] - ++ lib.optional hasBattery "battery"; + modules-center = ["custom/osd" "custom/osd-sep" "clock" "tray" "hyprland/workspaces" "custom/network" "custom/bluetooth" "battery"]; # ── Modules ───────────────────────────────────────────────────────── @@ -34,7 +31,6 @@ in { }; battery = { - bat = "BAT1"; interval = 20; full-at = 100; tooltip = true; diff --git a/home/system/waybar/tests/battery-decide.test.sh b/home/system/waybar/tests/battery-decide.test.sh new file mode 100644 index 00000000..32127fbe --- /dev/null +++ b/home/system/waybar/tests/battery-decide.test.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# Unit tests for battery_decide. Run: bash home/system/waybar/tests/battery-decide.test.sh +set -u + +DIR="$(cd "$(dirname "$0")/.." && pwd)" +BATTERY_MONITOR_TEST=1 . "$DIR/battery-monitor.sh" + +fail=0 +check() { + desc=$1 status=$2 cap=$3 prev=$4 expected=$5 + got=$(battery_decide "$status" "$cap" "$prev") + if [ "$got" = "$expected" ]; then + printf 'ok - %s\n' "$desc" + else + printf 'FAIL - %s: expected %s, got %s\n' "$desc" "$expected" "$got" + fail=1 + fi +} + +check "discharging 4% fresh -> crit" Discharging 4 none crit_show:crit5 +check "discharging 4% already crit -> refresh" Discharging 4 crit5 crit_show:crit5 +check "discharging 5% boundary -> crit" Discharging 5 none crit_show:crit5 +check "discharging 8% fresh -> warn osd" Discharging 8 none osd_warn:warn10 +check "discharging 8% already warned -> noop" Discharging 8 warn10 noop:warn10 +check "discharging 8% recovering from crit -> clear+warn" Discharging 8 crit5 crit_clear:warn10 +check "discharging 10% boundary -> warn" Discharging 10 none osd_warn:warn10 +check "discharging 11% -> noop none" Discharging 11 none noop:none +check "discharging 12% rearm from warn -> none" Discharging 12 warn10 noop:none +check "discharging 40% recover from crit -> clear" Discharging 40 crit5 crit_clear:none +check "charging 4% from crit -> clear" Charging 4 crit5 crit_clear:none +check "charging 8% from warn -> none" Charging 8 warn10 noop:none +check "full 100% -> noop none" Full 100 none noop:none + +exit $fail diff --git a/hosts/laptop/variables.nix b/hosts/laptop/variables.nix index 48128572..c703f538 100644 --- a/hosts/laptop/variables.nix +++ b/hosts/laptop/variables.nix @@ -26,8 +26,6 @@ autoUpgrade = false; autoGarbageCollector = true; - - hasBattery = true; }; # DON'T TOUCH THIS diff --git a/hosts/work/variables.nix b/hosts/work/variables.nix index f8569301..9247e0c5 100644 --- a/hosts/work/variables.nix +++ b/hosts/work/variables.nix @@ -26,8 +26,6 @@ autoUpgrade = false; autoGarbageCollector = true; - - hasBattery = false; }; # DON'T TOUCH THIS diff --git a/todo b/todo index 66c13fa4..37770b89 100644 --- a/todo +++ b/todo @@ -7,4 +7,3 @@ - hide bluetooth icon in tray - screenshots - enlever notif flake nvf -