11 Commits
Author SHA1 Message Date
Gabriel Brown 06c53d6c21 Shortcuts you invent, rules you write, gestures you own - all still just data
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
2026-08-25 01:51:59 -04:00
Gabriel Brown e723fabed3 Stop the session you started, so the next one can begin
Logging out of plain Hyprland left the desktop running. autostart.lua starts
about ten user units, none of them children of the compositor, and the shutdown
handler that was meant to stop them stopped hyprland-session.target -- a unit
Fedora does not ship. systemctl says 'Unit not found', the handler reports
nothing, and hyprpaper, hypridle, vicinae and the polkit agent keep running
after logout. Each of them Requires=graphical-session.target, so that target
stayed active with no session behind it.

The symptom was not a broken Hyprland. Both uwsm and gnome-session refuse to
start into a session that is already running, so 'Hyprland (uwsm-managed)' and
GNOME both bounced back to the login screen, and plain Hyprland -- which makes
no such check -- was the only one that worked. The session that looked healthy
was the one leaving the mess, and it was the one Panama tells people not to use.

Now stops graphical-session.target, which every one of those units is PartOf,
so a unit added to the start handler later cannot be forgotten here.

panama-crash-watch had the same leak and no PartOf at all: a journal follower
per login, and a 'once per program per session' dedup that had quietly become
once per boot.
2026-08-23 13:55:47 -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 6ae8265730 Say what a keybind is for, rather than guessing from its name
The Shortcuts page grouped shortcuts by matching substrings in their
descriptions, which put "Close window" and "Close the notification
list" in the same group and left anything phrased unusually in
whichever bucket matched first. The cheatsheet that comes next would
have inherited the same guesswork.

keybinds.lua says it outright now. Its sections already were the
categories, so a section sets one and the binds below inherit it: one
line per section instead of one per bind, and a new bind lands in the
category of the section somebody wrote it in without having to
remember anything.

Hyprland reports a Lua bind's dispatcher as __lua with a bytecode
offset, so nothing can be attached to a bind that survives into
`hyprctl binds`. The config writes a manifest at load instead, keyed
by the chord actually bound so the shell can join on what it sees.
Writing never raises: a read-only state directory costs the grouping,
never the keymap, and the shell keeps the old derivation as its
fallback so a machine that has not reloaded its compositor still works.

The one failure mode is a section that forgets to set a category and
silently inherits the one above. That is not hypothetical -- it
happened while writing this, because the dictation section sits in the
middle of the media binds and its category leaked onto the volume,
media and brightness keys below it. The contract walks the file for
sections with binds and no category, and spot-checks the boundaries
where inheritance is doing the work.
2026-08-21 23:30:36 -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 4c77bc2f61 Decide whether the other screens join in on workspaces
GNOME's Multitasking panel asked one workspace question worth reproducing, and
it is not which workspace goes on which screen. It is whether the second screen
participates at all: workspaces on the primary display only, or each screen with
its own. Ten rows of per-workspace assignment would be more powerful and worse.

Off is Hyprland's own behaviour and emits nothing. On pins workspaces 1 to 10 --
however many ALT+1..ALT+0 actually reach, read from keybinds.lua rather than
written down twice -- to whichever output is recorded as primary. With no
primary recorded, nothing is pinned: guessing one would move every workspace
onto whichever output happened to sort first, and this machine is in exactly
that state.

Applying is a reload, which is the part that shaped the design. Hyprland reads
workspace rules at config time and will not remove one afterwards -- a rule
written with an empty monitor keeps its old binding, which was checked rather
than assumed. Only a reload clears them, so the config is the only honest source
and the page cannot pretend a change has landed before one happens. Hence a
service that reads `hyprctl workspacerules` back rather than inferring success
from having written the preference, and a Reload row that exists only while the
two disagree.

Verified end to end against the live compositor and put back: off emits nothing,
on emits ten rules naming the primary, and turning it off clears them. The
settings file came back byte-identical.

Claude-Session: https://claude.ai/code/session_01Q84axqUE5inJhf5Jz9CFy1
2026-08-21 02:16:29 -04:00
Gabriel Brown 9092a80f66 Search from the launcher, and give the touchpad something to do
Four things a Hyprland desktop can do that this one was not.

Searching from the launcher needed no launcher work at all: Vicinae already
models it, so this is a script command with one percent-encoded argument. Make
it the fallback command and anything typed that matches nothing else offers to
search it. Bangs come free -- they are a property of where the query is sent,
not of the launcher -- so !yt reaches YouTube without a line of bang parsing.

Suggestions could not be a script command. They need a view that reacts as you
type, which is an extension: TypeScript, compiled, querying the same endpoint
Firefox's address bar uses. It debounces, and aborts the request in flight on
every keystroke -- typing is faster than the network, and an older answer
landing after a newer one leaves the list describing a query that is no longer
on screen. A bang skips suggestions entirely, because Google has no useful
guesses about "!yt".

The engine is now written down twice, once in each. The contract pins that they
agree, since searching from the fallback and searching from the suggestions
reaching different places is the kind of wrong that looks fine.

Gestures mirror GNOME: three fingers sideways for workspaces, up for the
overview, down to dismiss it. Open and close rather than toggle both ways --
toggling means swiping up from an open overview closes it, which is not what the
fingers meant. Hyprland reads gesture registrations at startup so they cannot be
a setting, but distance and direction can be, and are.

Window swallowing is off by default and a preference like every other misc
setting here. A terminal that vanishes when you did not ask for it is confusing
rather than broken, which is worse.

Claude-Session: https://claude.ai/code/session_01Q84axqUE5inJhf5Jz9CFy1
2026-08-21 01:49:29 -04:00
Gabriel Brown e1faaf7a76 Drop the extension, and give the test suite a front door
Phase 6, the last of the fresh-install spec.

159 scripts lose their .sh: 110 contracts, 47 Vicinae commands, 2 compositor
contracts. A shebang and the executable bit already select the interpreter. The
extension only ever added something that had to stay in sync, and the rename
proved the point twice over in the space of an hour.

The spec's stated risk was Vicinae's script discovery. One script was renamed and
reloaded on its own before the other 46 followed; it came back as
scripts:panama.capture and all 47 resolve. What the probe turned up instead is
that the extension was never only a filename: Vicinae's command IDs embed it, so
every ID changed. Nothing in this repository refers to them, so nothing breaks.
The only trace is Vicinae's metadata.json, whose visited map had two Panama
entries that are now orphaned -- two commands lost their usage ranking and will
earn it back. Worth knowing before anyone renames these again on a machine that
has a keybind pointing at one.

Rewriting the references by exact filename missed two things it structurally
could not see: a name built from a variable, settings-$page.sh, and a glob,
-name '*.sh'. Both were in the contract that counts the generated commands, which
promptly reported 47 expected and 0 found. The mechanical part of a rename is the
part that looks finished.

The three subcommands. panama doctor fronts a health check that already existed
and already ran at the end of every install but could not be reached from a
terminal. panama upgrade re-runs the installer from anywhere. panama test runs
the suite, which had no entry point at all -- 121 files that were the main safety
net in this repository and were invisible in it.

Writing that runner found three tests nothing was running.
calendar_agenda_bridge_test, home_assistant_bridge_test and kdeconnect_bridge_test
are unittest suites without the executable bit, so no contract invoked them and
the first draft of the runner skipped them silently. All three pass, and have
passed unobserved for weeks. The runner collects *_test.py as well now, because a
runner with a blind spot is worse than no runner for the same reason a dependency
checker with one is: it reports PASS.

Six worktrees pruned. Each was re-checked rather than trusted to the spec's list,
and two needed it: panama-commands is not on feat/panama-commands but on
feat/gnome-tweaks-parity, and fix/panama-displays-review reads [ahead 3] -- ahead
of its remote, not of main, with every commit patch-equivalent to landed work.
roadmap-completion stays; it has five commits that are genuinely unlanded. The
branches are left alone: pruning a worktree costs nothing, deleting a branch is a
decision.

121 contracts pass.

Claude-Session: https://claude.ai/code/session_01NvgBuSWB5sE43yWmg21ozj
2026-08-20 21:55:55 -04:00
Gabriel Brown d96863b687 Convert British spellings to American across the repo
colour -> color, behaviour -> behavior, centre -> center, favourite ->
favorite, and about twenty other pairs, applied consistently across
comments, docs, error/UI copy, and a handful of QML identifiers that
used the British spelling as their actual name: SystemSettings'
serialiseValue/serialiseTable/normaliseGradient, Displays'
normaliseModes, Wallpaper's normalisePolicy, SettingsBackup's
serialiseHomeState, DateTime's ntpSynchronised property, Clipboard's
_normalise helper, and ShortcutCapture's cancelled signal (with its
onCancelled handler in ShortcutsPage.qml). Every call site and the two
tests that assert on the literal source text (settings-ownership and
settings-backup-live contracts) were updated in lockstep.

Left untouched: config/dot/espanso/match/packages/misspell-en/ is a
vendored third-party autocorrect dictionary -- its entries are typo
corrections, not our prose, and rewriting them would fight the
package's own purpose (and any future re-sync from upstream).

The already-American `favorites` property (Home page pinned
accessories) was never actually misspelled -- only nearby comments and
error strings said "favourites" -- so no data migration was needed
there.

Claude-Session: https://claude.ai/code/session_01E6TJUAh41HaP25MVHWkhRZ
2026-08-19 08:07:55 -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
Gabriel Brown 00a81edadd Make Panama settings one shared source of truth
Panama had grown into three configuration surfaces that only agreed because
they had been typed to agree: looks.lua hardcoded values, DesktopPreferences
independently defaulted the same values, and SystemSettings replayed them at
startup. Nothing kept them in sync, and the Lua side read no shared state at
all. This lands the first three stages of docs/superpowers/plans/2026-08-17-panama-cohesion.md.

Fix silently failing Hyprland writes. On a Lua-configured Hyprland, hyprctl
keyword refuses the write, prints the refusal to stdout, and still exits 0, so
the HDR, VRR, and direct-scanout toggles persisted their value and reported
success while the compositor never changed. Writes now go through hyprctl eval,
which has the same hazard on syntax and runtime errors, so success is defined
as reading the value back and finding it equal. The existing contract passed
throughout the outage because it re-applied the values already in place; the
new one flips each value to something it does not hold.

Derive preferences from a schema. Every setting used to be restated four times
-- a property alias, a JSON adapter property, a change handler, and a line in
reset -- where omitting any one failed silently. PreferenceSchema.qml is now
the single source, and persistence, validation, reset, and the Hyprland mapping
all derive from it. Unknown keys on disk survive a write so a rollback does not
discard a newer build's settings, and a corrupt file falls back to shipped
defaults. The store moved to ~/.config/panama/settings.json, migrating from the
old state directory without deleting it.

Share that file with Hyprland. prefs.lua reads it at config time with every
shipped literal kept as the fallback, so the config still stands alone. The Lua
is the default, the JSON is the truth, and Settings is the editor. The
compositor-adjustable surface goes from 3 keys to 23.

Also fixes two test-hygiene bugs found by running the suite end to end for the
first time: settings-pages-contract could see the window settings-window-contract
leaves behind, and the new write contract was persisting its deliberately-wrong
values into the user's real store.

Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
2026-08-17 23:26:56 -04:00