The audit's fourth tier -- what a Framework owner reaches for in the first week and found missing. The power button stops being an instant, unconfirmed poweroff: a shipped logind drop-in tells the daemon to stand down and the compositor binds the key to the power menu, the way GNOME turns it into a question. Holding it still hard-cuts through firmware. change-settings restarts logind so the change applies without waiting for a boot, and the Power page says what the button does now. The function row fills in: F10 (XF86RFKill) toggles airplane mode through a new panama-osd verb that blocks or unblocks every radio and says which way it went; F9 (XF86Display) opens the Displays page, the honest action until mirroring exists. And the lid becomes a switch bind: closing a docked lid turns the internal panel off so nothing renders inside a closed shell and no workspace strands on an invisible output, and opening it restores the panel with the mode and scale chosen in Settings. panama-lid owns both decisions; undocked machines suspend via logind before any of it matters. The battery gets an endgame. On battery the screen dims to 30% two-thirds of the way to blanking -- GNOME's single largest idle battery saver -- and restores exactly the level it saved. At the urgent threshold the machine suspends after a fifteen-second grace, cancelled by plugging in, because a suspend preserves the session for days and a hard cut at 0% preserves nothing; "Only warn" remains a choice on the Power page. Hibernate joins the power menu, but only where logind answers CanHibernate with yes -- an entry that fails silently is worse than none. And brightness stops being two code paths: the Displays page now embeds the same control the quick-settings panel uses, so the built-in backlight and DDC/CI monitors share one surface that withdraws itself where neither exists. The lid contract narrows to what its principle protects -- a HandleLidSwitch drop-in -- so deliberate policy for other keys can ship. Claude-Session: https://claude.ai/code/session_01Epx9ZC1gwm81K3jm9x9CKh
222 lines
9.2 KiB
Bash
Executable File
222 lines
9.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# Generates hypridle's configuration from Panama's shared settings.
|
|
#
|
|
# Why this exists rather than editing hypridle.conf directly: ~/.config/hypr is
|
|
# a symlink into the Panama repository, so writing hypridle.conf at runtime
|
|
# would dirty a tracked file with machine state. The generated config therefore
|
|
# lives under XDG_STATE_HOME, and a systemd drop-in points hypridle at it with
|
|
# `-c`. The repository's hypridle.conf remains the shipped default and is what
|
|
# runs if this has never been set up.
|
|
#
|
|
# panama-idle apply regenerate and restart hypridle
|
|
# panama-idle status report as JSON what is in effect
|
|
# panama-idle install write the systemd drop-in (idempotent)
|
|
# panama-idle remove remove the drop-in and fall back to the shipped config
|
|
#
|
|
# All values are read from the settings store and clamped here as well as in the
|
|
# schema, because this script is also reachable from a shell.
|
|
|
|
set -euo pipefail
|
|
|
|
settings="${XDG_CONFIG_HOME:-$HOME/.config}/panama/settings.json"
|
|
state_dir="${XDG_STATE_HOME:-$HOME/.local/state}/panama"
|
|
generated="$state_dir/hypridle.conf"
|
|
dropin_dir="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user/hypridle.service.d"
|
|
dropin="$dropin_dir/panama.conf"
|
|
|
|
# Set by generate() to the mktemp path it is currently writing, so concurrent
|
|
# invocations (e.g. rapid settings changes each spawning `apply`) never share
|
|
# a tmp file and interleave writes into a corrupt hypridle.conf. Cleared once
|
|
# the atomic mv below lands, so this is a no-op on a normal exit.
|
|
generated_tmp=""
|
|
cleanup() { rm -f "$generated_tmp" 2>/dev/null || true; }
|
|
trap cleanup EXIT
|
|
|
|
read_setting() {
|
|
local key="$1" fallback="$2"
|
|
[[ -r "$settings" ]] || { printf '%s' "$fallback"; return; }
|
|
jq -r --arg k "$key" --arg d "$fallback" \
|
|
'if has($k) and (.[$k] != null) then (.[$k] | tostring) else $d end' \
|
|
"$settings" 2>/dev/null || printf '%s' "$fallback"
|
|
}
|
|
|
|
clamp_int() {
|
|
local value="$1" low="$2" high="$3" fallback="$4"
|
|
[[ "$value" =~ ^-?[0-9]+$ ]] || { printf '%s' "$fallback"; return; }
|
|
(( value < low )) && value="$low"
|
|
(( value > high )) && value="$high"
|
|
printf '%s' "$value"
|
|
}
|
|
|
|
# hypridle has no concept of a power source: one config, one set of timeouts.
|
|
# So rather than maintaining two configs and swapping them, the single config
|
|
# is regenerated whenever the machine moves between wall power and battery, and
|
|
# this decides which set of keys it is built from. IdleLock watches
|
|
# Battery.acChanged and calls `apply` for exactly this reason.
|
|
#
|
|
# A machine with no battery never consults the battery keys at all, which is
|
|
# what keeps a desktop's generated config byte-for-byte what it was before any
|
|
# of this existed.
|
|
on_battery() {
|
|
local hw="${PANAMA_PATH:-$HOME/.local/share/Panama}/bin/panama-hw"
|
|
[[ -x "$hw" ]] || return 1
|
|
"$hw" battery || return 1
|
|
! "$hw" ac
|
|
}
|
|
|
|
load() {
|
|
local suffix=""
|
|
if on_battery; then
|
|
suffix="Battery"
|
|
fi
|
|
|
|
# An unwritten key falls back to ITS OWN schema default, never to the
|
|
# other power source's value. The battery keys once fell back to their AC
|
|
# counterparts, which sounded protective and produced a lie instead: the
|
|
# Power page shows the schema default (suspend at 20) for an unwritten
|
|
# battery key, while this generator quietly used the AC value (never), so
|
|
# a fresh laptop displayed one behavior and shipped another -- and
|
|
# discharged to zero in a bag. Whatever the sliders show is what must be
|
|
# generated; these fallbacks are pinned to the schema by
|
|
# tests/hypr/idle-defaults-contract.
|
|
if [[ "$suffix" == "Battery" ]]; then
|
|
blank_min="$(clamp_int "$(read_setting screenBlankMinutesBattery 2)" 0 120 2)"
|
|
lock_min="$(clamp_int "$(read_setting lockMinutesBattery 5)" 0 240 5)"
|
|
suspend_min="$(clamp_int "$(read_setting suspendMinutesBattery 20)" 0 480 20)"
|
|
else
|
|
blank_min="$(clamp_int "$(read_setting screenBlankMinutes 5)" 0 120 5)"
|
|
lock_min="$(clamp_int "$(read_setting lockMinutes 10)" 0 240 10)"
|
|
suspend_min="$(clamp_int "$(read_setting suspendMinutes 0)" 0 480 0)"
|
|
fi
|
|
lock_on_sleep="$(read_setting lockOnSleep true)"
|
|
[[ "$lock_on_sleep" == "true" || "$lock_on_sleep" == "false" ]] || lock_on_sleep=true
|
|
}
|
|
|
|
generate() {
|
|
load
|
|
mkdir -p "$state_dir"
|
|
|
|
# Unique per invocation, in the same directory as the destination so the
|
|
# final mv is an atomic same-filesystem rename rather than a copy.
|
|
generated_tmp="$(mktemp "$generated.XXXXXX")"
|
|
|
|
{
|
|
printf '# Generated by panama-idle from %s\n' "$settings"
|
|
printf '# Power source at generation: %s\n' \
|
|
"$(on_battery && echo battery || echo 'wall power')"
|
|
printf '# Do not edit: it is rewritten whenever the idle settings change.\n'
|
|
printf '# The shipped defaults live in the Panama repo at config/dot/hypr/hypridle.conf.\n\n'
|
|
|
|
printf 'general {\n'
|
|
printf ' lock_cmd = pidof hyprlock || ~/.config/quickshell/scripts/panama-lock run\n'
|
|
if [[ "$lock_on_sleep" == "true" ]]; then
|
|
printf ' before_sleep_cmd = loginctl lock-session\n'
|
|
fi
|
|
printf " after_sleep_cmd = hyprctl dispatch 'hl.dsp.dpms({ action = \"on\" })'\n"
|
|
printf ' inhibit_sleep = 2\n'
|
|
printf '}\n'
|
|
|
|
# On battery, dim before blanking -- GNOME's single largest idle
|
|
# battery saver, and the gentle warning that the screen is about to
|
|
# go. Two-thirds of the way to blank, floor of 30 seconds; -s saves
|
|
# the level and -r restores exactly it, so a person's brightness
|
|
# choice survives the round trip. Only where a real backlight exists:
|
|
# a desktop's DDC monitor is not worth waking over this.
|
|
if on_battery && (( blank_min > 0 )) \
|
|
&& command -v brightnessctl >/dev/null 2>&1 \
|
|
&& [[ -n "$(ls /sys/class/backlight 2>/dev/null)" ]]; then
|
|
dim_at=$(( blank_min * 60 * 2 / 3 ))
|
|
(( dim_at < 30 )) && dim_at=30
|
|
printf '\n# %s seconds -> dim, undone on activity.\n' "$dim_at"
|
|
printf 'listener {\n'
|
|
printf ' timeout = %s\n' "$dim_at"
|
|
printf ' on-timeout = brightnessctl -c backlight -s set 30%%\n'
|
|
printf ' on-resume = brightnessctl -c backlight -r\n'
|
|
printf '}\n'
|
|
fi
|
|
|
|
if (( blank_min > 0 )); then
|
|
printf '\n# %s minutes -> screen off.\n' "$blank_min"
|
|
printf 'listener {\n'
|
|
printf ' timeout = %s\n' "$(( blank_min * 60 ))"
|
|
printf " on-timeout = hyprctl dispatch 'hl.dsp.dpms({ action = \"off\" })'\n"
|
|
printf " on-resume = hyprctl dispatch 'hl.dsp.dpms({ action = \"on\" })'\n"
|
|
printf '}\n'
|
|
fi
|
|
|
|
if (( lock_min > 0 )); then
|
|
printf '\n# %s minutes -> lock.\n' "$lock_min"
|
|
printf 'listener {\n'
|
|
printf ' timeout = %s\n' "$(( lock_min * 60 ))"
|
|
printf ' on-timeout = loginctl lock-session\n'
|
|
printf '}\n'
|
|
fi
|
|
|
|
if (( suspend_min > 0 )); then
|
|
printf '\n# %s minutes -> suspend.\n' "$suspend_min"
|
|
printf 'listener {\n'
|
|
printf ' timeout = %s\n' "$(( suspend_min * 60 ))"
|
|
printf ' on-timeout = systemctl suspend\n'
|
|
printf '}\n'
|
|
fi
|
|
} >"$generated_tmp"
|
|
|
|
mv "$generated_tmp" "$generated"
|
|
generated_tmp=""
|
|
}
|
|
|
|
install_dropin() {
|
|
mkdir -p "$dropin_dir"
|
|
cat >"$dropin" <<EOF
|
|
# Installed by panama-idle. Points hypridle at the configuration Panama
|
|
# generates from its settings store, so idle timings are adjustable from
|
|
# Panama Settings rather than by editing a file in the Panama repository.
|
|
[Service]
|
|
ExecStart=
|
|
ExecStart=$(command -v hypridle || echo /usr/bin/hypridle) -c $generated
|
|
EOF
|
|
systemctl --user daemon-reload
|
|
}
|
|
|
|
case "${1:-apply}" in
|
|
apply)
|
|
generate
|
|
if [[ -f "$dropin" ]]; then
|
|
systemctl --user restart hypridle.service
|
|
fi
|
|
;;
|
|
install)
|
|
generate
|
|
install_dropin
|
|
# Restart only a daemon that is already running. `restart` on an
|
|
# inactive unit STARTS it, and this verb now also runs from
|
|
# change-settings during ./install -- possibly inside a GNOME
|
|
# session, where starting hypridle would fight GNOME's own idle
|
|
# handling. A session that has not started hypridle yet picks the
|
|
# drop-in up at its next launch.
|
|
if systemctl --user is-active -q hypridle.service; then
|
|
systemctl --user restart hypridle.service
|
|
fi
|
|
;;
|
|
remove)
|
|
rm -f "$dropin"
|
|
systemctl --user daemon-reload
|
|
systemctl --user restart hypridle.service
|
|
;;
|
|
status)
|
|
load
|
|
managed=false
|
|
[[ -f "$dropin" ]] && managed=true
|
|
printf '{"managed":%s,"active":"%s","powerSource":"%s","blankMinutes":%s,"lockMinutes":%s,"suspendMinutes":%s,"lockOnSleep":%s,"generated":"%s"}\n' \
|
|
"$managed" \
|
|
"$(systemctl --user is-active hypridle.service 2>/dev/null || printf unknown)" \
|
|
"$(on_battery && printf battery || printf ac)" \
|
|
"$blank_min" "$lock_min" "$suspend_min" "$lock_on_sleep" "$generated"
|
|
;;
|
|
*)
|
|
printf 'usage: panama-idle [apply|install|remove|status]\n' >&2
|
|
exit 2
|
|
;;
|
|
esac
|