mirror of
https://github.com/anotherhadi/nixy.git
synced 2026-08-21 10:45:49 +02:00
@@ -3,3 +3,4 @@
|
|||||||
old/
|
old/
|
||||||
docs/superpowers/
|
docs/superpowers/
|
||||||
.superpowers/
|
.superpowers/
|
||||||
|
result/
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ in {
|
|||||||
"result"
|
"result"
|
||||||
"result-*"
|
"result-*"
|
||||||
".superpowers/"
|
".superpowers/"
|
||||||
|
".claude/"
|
||||||
"docs/superpowers/"
|
"docs/superpowers/"
|
||||||
];
|
];
|
||||||
settings = {
|
settings = {
|
||||||
|
|||||||
@@ -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 "$@"
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -26,8 +26,6 @@
|
|||||||
|
|
||||||
autoUpgrade = false;
|
autoUpgrade = false;
|
||||||
autoGarbageCollector = true;
|
autoGarbageCollector = true;
|
||||||
|
|
||||||
hasBattery = true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
# DON'T TOUCH THIS
|
# DON'T TOUCH THIS
|
||||||
|
|||||||
@@ -26,8 +26,6 @@
|
|||||||
|
|
||||||
autoUpgrade = false;
|
autoUpgrade = false;
|
||||||
autoGarbageCollector = true;
|
autoGarbageCollector = true;
|
||||||
|
|
||||||
hasBattery = false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
# DON'T TOUCH THIS
|
# DON'T TOUCH THIS
|
||||||
|
|||||||
Reference in New Issue
Block a user