Commit Graph
8 Commits
Author SHA1 Message Date
Gabriel Brown e1ff25fc66 Draw idle as one timeline, and let the power button answer to its owner
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
2026-08-24 20:33:44 -04:00
Gabriel Brown 9530e32326 Give a laptop its first week: the keys, the lid, the battery's endgame
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
2026-08-23 12:21:08 -04:00
Gabriel Brown 153554b5df Make ./install something you could hand a stranger
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
2026-08-23 12:10:03 -04:00
Gabriel Brown 3d21e20041 Make every control tell the truth
The audit's first tier, in one change: every case found where the interface
asserted something the system did not do.

Twenty-one compositor-owned preferences -- the whole Mouse & Touchpad page,
plus window layout, snapping, dim-inactive and the magnifier -- had the live
half (hyprctl eval) and not the config-time half, so they quietly reverted on
every hyprctl reload. All 70 hypr-backed keys now have a prefs.get() in the
Lua, and hypr-prefs-contract pins both the presence and that the Lua fallback
equals the schema default, which is how the touchpad page misreported natural
scrolling on first boot.

The idle generator fell back from unwritten battery keys to the AC values
while the Power page displayed the schema defaults: a fresh laptop showed
"suspend at 20 minutes" and generated no suspend listener, then discharged to
zero in a bag. Unwritten keys now use the defaults the page shows
(idle-defaults-contract pins generator to schema; idle-config-contract
re-pinned to the new rule with the tradeoff recorded), and change-settings
enables managed idle on any machine with a battery -- without starting
hypridle in whatever session the installer runs under.

The per-app lock-screen notification switches wrote fields nothing read:
hyprlock cannot render notifications. Removed, with the rule model shrunk to
{enabled}, stale stored fields dropped at normalization, and the contract now
forbidding the page from growing lock-screen switches it cannot honor.

The battery warning thresholds were searchable, documented as "Found on
Power & Lock", and rendered nowhere -- and crossing the low threshold changed
only a glyph's color. Both sliders now exist where search was already sending
people, and low battery publishes a real notification at important priority.

Three handoffs opened GNOME panels that are inert in a Hyprland session. The
keyboard handoff is gone (that panel writes gsettings nothing here reads, and
the working controls sat on the same page); Connectivity gains a Wi-Fi row
that opens GNOME's actual Wi-Fi panel -- hidden SSIDs and 802.1X finally have
a road -- beside the network row that legitimately drives NetworkManager; the
universal-access handoff is gone, its few working toggles being controls this
app already owns. And the accessibility page now gives the true reason sticky
keys are missing: each Wayland compositor implements its own and Hyprland
does not yet -- not "an X11 feature with no Wayland equivalent," which sent
people to the wrong conclusion about the platform.

Claude-Session: https://claude.ai/code/session_01Epx9ZC1gwm81K3jm9x9CKh
2026-08-23 11:43:39 -04:00
Gabriel Brown bc6d63b70f Let the idle timings know whether you are plugged in
An idle screen costs a screen on wall power and the rest of your
afternoon on battery, so they should not be the same number. hypridle
has no concept of a power source -- one config, one set of timeouts --
so rather than maintaining two configs and swapping them, panama-idle
builds the single config from whichever key set applies, and IdleLock
rebuilds it when the charger comes or goes. That runs through the same
400ms debounce a settings change uses, so a loose charger cannot
restart hypridle in a loop.

The battery keys fall back to their AC counterparts rather than to the
schema defaults. Without that, unplugging would silently override a
deliberately long timing with a shipped short one, which is the kind
of thing you would notice only by losing work.

A machine with no battery reads none of it and generates exactly what
it generated before. The contract pins that alongside the two obvious
directions, and was checked by sabotaging the detection to confirm it
fails rather than passing vacuously.
2026-08-21 22:18:29 -04:00
Gabriel Brown 70d8d32ee2 Fix the install pipeline and an idle-lock startup race
The initial package list was quoted into a single bogus dnf argument
and every dnf error was discarded, so a fresh install silently skipped
most of it. Two package lists were never wired into the pipeline at
all, and change-settings ran before install-packages, so the vicinae
theme step was permanently skipped. Fixed the ordering, the quoting,
and stopped swallowing errors.

Separately, hypridle could start with its WAYLAND_DISPLAY condition
unmet if it raced the env-publish call, silently never starting --
and it's the only listener for the logind Lock signal. Made the start
wait on the environment synchronously. panama-idle also wrote its
generated config to a fixed temp path with no locking, so concurrent
applies could interleave into a corrupt file; switched to mktemp plus
an atomic mv.

Claude-Session: https://claude.ai/code/session_01E6TJUAh41HaP25MVHWkhRZ
2026-08-18 21:22:52 -04:00
Gabriel Brown 3520700983 Route session locking through Panama 2026-08-18 15:00:18 -04:00
Gabriel Brown 2bc12e6022 Add wallpaper, power, date, accessibility, and real search
Continues the settings expansion toward replacing GNOME Settings for
everything Panama actually owns.

Wallpaper. A thumbnail grid rather than a path field: the value of this
setting is the picture, so typing a path to something you cannot see is
the worst version of it. Two things about hyprpaper 0.8 shaped this. Its
IPC is much smaller than older documentation suggests -- preload,
listloaded, unload, and reload all answer "invalid hyprpaper request",
so setting is a single call with no preload. And the "<empty>,<path>"
form that used to mean every output is silently ignored, so a wallpaper
set that way appears to succeed and never changes; outputs are walked
explicitly instead. hyprpaper.conf lives in the repo through the
~/.config/hypr symlink and so cannot hold machine state, which is why
the choice lives in the shared settings store and is re-applied at
startup.

Power & Lock. hypridle has no IPC for reconfiguration and its config is
hyprlang rather than the shared JSON, so scripts/panama-idle generates a
config from the settings store and restarts the daemon. The generated
file lives under XDG_STATE_HOME for the same symlink reason, with a
systemd drop-in pointing hypridle at it. Management is a real state and
the page says which one you are in rather than showing sliders that
quietly do nothing. Zero means never for all three timers, which a naive
template would render as "immediately".

Date & Time. Deliberately not stored in Panama's settings: the timezone
and network time belong to the machine and are shared with sessions that
never see this file. Storing a copy would create a second answer to a
question the system already answers. Reads and writes timedatectl
directly; a cancelled polkit prompt surfaces as an error rather than as
a value that appears to have been accepted.

Accessibility. Pointer size and text scale have to agree across three
consumers with no shared configuration -- the compositor, GTK, and the
shell -- so the store is the source of truth and the values are pushed
outward to gsettings and hyprctl setcursor.

Search now indexes the schema instead of the twelve page labels. "gaps",
"wallpaper", and "screenshot" previously found nothing on an app that
has all three, which is the clearest way a settings app feels smaller
than it is. Shortcuts are indexed by what they do. A contract asserts
every non-internal schema label is reachable, so a new setting cannot be
added in an undiscoverable state.

The GNOME delegation allow-list was widened to the panel names
gnome-control-center actually reports; the previous list contained
"users", which is not one of them and so opened nothing.

Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
2026-08-18 00:42:13 -04:00