Signed-off-by: Hadi <hadi@example.com>
This commit is contained in:
Hadi
2026-07-31 13:00:40 +02:00
parent cd4971b30b
commit 00885e3213
10 changed files with 189 additions and 19 deletions
+1
View File
@@ -3,3 +3,4 @@
old/ old/
docs/superpowers/ docs/superpowers/
.superpowers/ .superpowers/
result/
+1
View File
@@ -17,6 +17,7 @@ in {
"result" "result"
"result-*" "result-*"
".superpowers/" ".superpowers/"
".claude/"
"docs/superpowers/" "docs/superpowers/"
]; ];
settings = { settings = {
+104
View File
@@ -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>:<newstate>".
# 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 "$@"
+19 -1
View File
@@ -20,7 +20,25 @@ in {
iw iw
hyprsunset 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 = [ wayland.windowManager.hyprland.settings.exec-once = [
"waybar" "waybar"
+29 -8
View File
@@ -1,10 +1,29 @@
{pkgs}: let {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 = '' updateOsd = ''
[ -f /tmp/waybar-osd-pid ] && kill "$(cat /tmp/waybar-osd-pid)" 2>/dev/null ${waybar-osd}/bin/waybar-osd "$OSD_TEXT"
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
''; '';
volGetText = '' volGetText = ''
@@ -46,10 +65,12 @@
# launcher, unusable for a list, so override the geometry here. # 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"; 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 { in {
inherit waybar-osd battery-monitor;
bluetoothScript = pkgs.writeShellScript "waybar-bluetooth" '' bluetoothScript = pkgs.writeShellScript "waybar-bluetooth" ''
jq=${pkgs.jq}/bin/jq jq=${pkgs.jq}/bin/jq
nl=$'\n' 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; } bt() { timeout 3 ${pkgs.bluez}/bin/bluetoothctl "$@" 2>/dev/null; }
powered=$(bt show | awk '/Powered:/ { print $2; exit }') powered=$(bt show | awk '/Powered:/ { print $2; exit }')
@@ -331,7 +352,7 @@ in {
record-toggle = pkgs.writeShellScriptBin "record-toggle" '' record-toggle = pkgs.writeShellScriptBin "record-toggle" ''
if pgrep -x wf-recorder >/dev/null; then 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 pkill -INT -x wf-recorder
OSD_TEXT="󰕧 Recording saved" OSD_TEXT="󰕧 Recording saved"
else else
@@ -385,7 +406,7 @@ in {
emoji-picker = pkgs.writeShellScriptBin "emoji-picker" '' emoji-picker = pkgs.writeShellScriptBin "emoji-picker" ''
export PATH="${pkgs.wtype}/bin:${pkgs.wl-clipboard}/bin:$PATH" export PATH="${pkgs.wtype}/bin:${pkgs.wl-clipboard}/bin:$PATH"
export BEMOJI_PICKER_CMD="${tofiMenu} --prompt-text 'emoji: '" 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 exec ${pkgs.bemoji}/bin/bemoji -t -c
''; '';
+1 -5
View File
@@ -8,7 +8,6 @@
}: let }: let
gaps-out = config.theme.gaps-out; gaps-out = config.theme.gaps-out;
c = config.lib.stylix.colors; c = config.lib.stylix.colors;
hasBattery = config.var.hasBattery or false;
in { in {
programs.waybar.settings = [ programs.waybar.settings = [
{ {
@@ -16,9 +15,7 @@ in {
position = "top"; position = "top";
height = config.theme.bar-height; height = config.theme.bar-height;
margin = "${toString gaps-out} ${toString gaps-out} 0"; margin = "${toString gaps-out} ${toString gaps-out} 0";
modules-center = modules-center = ["custom/osd" "custom/osd-sep" "clock" "tray" "hyprland/workspaces" "custom/network" "custom/bluetooth" "battery"];
["custom/osd" "custom/osd-sep" "clock" "tray" "hyprland/workspaces" "custom/network" "custom/bluetooth"]
++ lib.optional hasBattery "battery";
# ── Modules ───────────────────────────────────────────────────────── # ── Modules ─────────────────────────────────────────────────────────
@@ -34,7 +31,6 @@ in {
}; };
battery = { battery = {
bat = "BAT1";
interval = 20; interval = 20;
full-at = 100; full-at = 100;
tooltip = true; tooltip = true;
@@ -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
-2
View File
@@ -26,8 +26,6 @@
autoUpgrade = false; autoUpgrade = false;
autoGarbageCollector = true; autoGarbageCollector = true;
hasBattery = true;
}; };
# DON'T TOUCH THIS # DON'T TOUCH THIS
-2
View File
@@ -26,8 +26,6 @@
autoUpgrade = false; autoUpgrade = false;
autoGarbageCollector = true; autoGarbageCollector = true;
hasBattery = false;
}; };
# DON'T TOUCH THIS # DON'T TOUCH THIS
-1
View File
@@ -7,4 +7,3 @@
- hide bluetooth icon in tray - hide bluetooth icon in tray
- screenshots - screenshots
- enlever notif flake nvf - enlever notif flake nvf