Notice the battery, and the machine it is or is not in

Panama had no idea whether it was running on a laptop. No upower, no
battery, no lid, no AC: hypridle.conf says "This is a desktop" in its
own header, and that was true of the code as well as the machine.

panama-hw answers hardware questions one at a time, exits 0 or 1, and
prints nothing, so scripts, services and contracts all ask the same
way. The definition the rest of the laptop work hangs on is one line:
clamshell is lid-closed AND an external monitor. A machine with no
mains supply at all reports as being on wall power, because a desktop
cannot run out of it.

The battery service follows Vitals: sysfs through FileView, an
availability flag, and no subprocess on the timer. Globbing is the one
thing QML cannot do -- a battery is BAT0 or BAT1 or CMB0, mains is AC
or ADP1 or ACAD -- so panama-battery resolves the names once and the
shell reads the files directly after. Nothing falls back to a
plausible zero: a desktop shows no indicator, no card, and no charge
limit control where the firmware has no ceiling.

Also repairs two contracts that were already failing and had not been
noticed, because only the full suite runs them. The dependency
scanner treated line-initial variable assignments, case labels,
comments and heredoc bodies as commands, and `count`, `host`, `cancel`
and `import` are all real binaries on Fedora, so `command -v` could
not filter them out. It now drops comments and heredoc bodies and
requires a command to be followed by whitespace. Verified it still
catches a genuinely undeclared dependency rather than passing quietly.
The launcher command contract had not been told about the fourteen
commands added earlier today.
This commit is contained in:
Gabriel Brown
2026-08-21 21:43:14 -04:00
parent e446a1072c
commit 3c359f3f7e
17 changed files with 1032 additions and 9 deletions
+127
View File
@@ -0,0 +1,127 @@
#!/usr/bin/env bash
# The battery, for the shell and the Power page.
#
# panama-battery paths resolve which sysfs files to watch
# panama-battery status one JSON reading, for scripts and contracts
# panama-battery set-threshold N cap charging at N% (needs root)
#
# `paths` exists so the shell does not have to poll a subprocess. Globbing is
# the one thing QML cannot do -- a battery is BAT0 on most machines, BAT1 on
# some, CMB0 on a few, and the mains supply is AC, AC0, ADP1 or ACAD depending
# on the firmware -- so this resolves the names once and the shell reads the
# files directly from then on, the way Vitals.qml reads procfs.
#
# Which machine has what is panama-hw's question, so the search lives there and
# this asks it rather than keeping a second copy of the answer.
#
# Charge thresholds are a root write to a sysfs attribute, and the only part of
# this that needs privilege. It goes through panama-sudo so the prompt names
# what is being changed, rather than asking for a password with polkit's
# generic "run a program as another user".
set -uo pipefail
PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}"
HW="$PANAMA_PATH/bin/panama-hw"
SYS="${PANAMA_HW_SYS:-/sys}"
battery_dir() {
[[ -x "$HW" ]] || return 1
"$HW" battery-path 2>/dev/null
}
# The mains supply, if the machine has one. A desktop has none, and that is
# not an error: panama-hw's `ac` predicate treats "no mains at all" as being on
# wall power, and the shell falls back to the same assumption.
mains_dir() {
local supply
for supply in "$SYS"/class/power_supply/*; do
[[ -r "$supply/type" ]] || continue
[[ "$(cat "$supply/type" 2>/dev/null)" == "Mains" ]] || continue
printf '%s\n' "$supply"
return 0
done
return 1
}
read_int() {
local file="$1" value
[[ -r "$file" ]] || return 1
value="$(cat "$file" 2>/dev/null)" || return 1
[[ "$value" =~ ^[0-9]+$ ]] || return 1
printf '%s\n' "$value"
}
cmd_paths() {
local battery mains threshold=""
battery="$(battery_dir)" || battery=""
mains="$(mains_dir)" || mains=""
# Only report the threshold file when it exists AND is writable through
# root -- a machine whose kernel exposes a read-only stub would otherwise
# get a control that silently does nothing.
if [[ -n "$battery" && -r "$battery/charge_control_end_threshold" ]]; then
threshold="$battery/charge_control_end_threshold"
fi
printf '{"battery":"%s","mains":"%s","threshold":"%s"}\n' \
"$battery" "$mains" "$threshold"
}
cmd_status() {
local battery mains capacity="" state="Unknown" online=1 threshold=0
battery="$(battery_dir)" || battery=""
mains="$(mains_dir)" || mains=""
if [[ -n "$battery" ]]; then
capacity="$(read_int "$battery/capacity")" || capacity=""
[[ -r "$battery/status" ]] && state="$(cat "$battery/status" 2>/dev/null)"
threshold="$(read_int "$battery/charge_control_end_threshold")" || threshold=0
fi
if [[ -n "$mains" ]]; then
online="$(read_int "$mains/online")" || online=0
fi
printf '{"available":%s,"percent":%s,"status":"%s","acOnline":%s,"chargeLimit":%s}\n' \
"$([[ -n "$capacity" ]] && echo true || echo false)" \
"${capacity:-0}" "$state" \
"$([[ "$online" == "1" ]] && echo true || echo false)" \
"$threshold"
}
cmd_set_threshold() {
local value="${1:-}" battery file
[[ "$value" =~ ^[0-9]+$ ]] || { echo 'set-threshold needs a percentage' >&2; return 2; }
(( value >= 50 && value <= 100 )) || { echo 'threshold must be between 50 and 100' >&2; return 2; }
battery="$(battery_dir)" || { echo 'no battery on this machine' >&2; return 1; }
file="$battery/charge_control_end_threshold"
[[ -e "$file" ]] || { echo 'this machine cannot set a charge threshold' >&2; return 1; }
# tee rather than a redirect: the redirect is performed by the calling
# shell, which is not the one holding root.
"$PANAMA_PATH/bin/panama-sudo" \
--reason "Capping battery charging at ${value}% to reduce wear" \
-- sh -c "printf '%s\n' '$value' | tee '$file' >/dev/null" || return 1
# Read it back rather than reporting success from the write's exit code:
# some firmware silently clamps or ignores the value.
read_int "$file"
}
case "${1:-status}" in
paths) cmd_paths ;;
status) cmd_status ;;
set-threshold) shift; cmd_set_threshold "$@" ;;
-h|--help)
cat <<'USAGE'
usage: panama-battery [paths|status|set-threshold <50-100>]
paths JSON: which sysfs files hold the battery, mains and threshold
status JSON: one reading of charge, state, power source and limit
set-threshold cap charging at N percent (asks for a password)
USAGE
;;
*) echo "panama-battery: unknown command: $1" >&2; exit 2 ;;
esac
+2 -1
View File
@@ -17,7 +17,8 @@
set -euo pipefail
usage() {
echo 'usage: panama-remind add "<20m|1h30m|9:30>" "<text>" | list | pick | cancel <unit>' >&2
echo 'usage: panama-remind add "<20m, 1h30m, or 9:30>" "<text>"' >&2
echo ' panama-remind list, pick, or cancel <unit>' >&2
exit 2
}