The audit's third tier: everything between this installer and a fresh machine it has never met. The one path that could cost a person their display: the interview probes Secure Boot with mokutil, which install-packages had not installed yet, so on a minimal base the MOK question silently never fired -- and install-hardware still installed akmod-nvidia and blacklisted nouveau, arming a reboot into an unloadable driver with its fallback disabled. The probe tools (pciutils, mokutil, fwupd) now bootstrap beside gum, and install-hardware re-checks Secure Boot for itself and refuses the driver rather than the display. Secrets leave the checkout: the personal environment moves to ~/.config/panama/env at mode 600 by migration, and .bashrc sources it with a permission check that quietly re-tightens drift. change-settings no longer overwrites /etc/dnf/dnf.conf -- two performance keys are set additively, the defaultyes=True that made every `dnf remove` treat Enter as yes is gone, and a migration strips it from machines that already received it. Package installation survives the world changing: the initial and desktop lists run with --skip-unavailable and a report_missing pass that names what was skipped (resolved through --whatprovides, so capability names like awk do not cry wolf); the openh264, appstream and core-group extras go through soft; RustDesk resolves its RPM for the machine's own architecture; and the Claude Desktop repository script is fetched to a kept file and run, never piped from the network into root. The hardware predicates stop guessing: a wireless mouse's scope=Device battery no longer turns a tower into a laptop, USB-PD-only machines read their power state from the battery's own status instead of being permanently "on AC", the lid falls back to logind's LidClosed where ACPI is silent, and charge limits reach every pack of a two-battery machine in one authorization -- with the reported percentage summed across packs. And the parsers stop assuming this machine: snapper is read through --machine-readable csv with named columns instead of a localized box-drawing table, and reports whether snapshots are even possible so ext4 and unconfigured-btrfs stop looking identical; fprintd is parsed under LC_ALL=C; the hypridle drop-in resolves the binary it points at; the recorder's render node became an "auto" token resolved at record time; update-grub writes the config its firmware actually boots; the nvm prompt hook and the SSH tmux takeover are guarded; hipblas and rocm-opencl move to an opt-in gpu-compute category; and the two interactive python tools' libraries are declared. Claude-Session: https://claude.ai/code/session_01Epx9ZC1gwm81K3jm9x9CKh
203 lines
8.2 KiB
Bash
Executable File
203 lines
8.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'
|
|
|
|
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
|