Files
Panama/docs/superpowers/plans/2026-08-21-panama-os-parity.md
Gabriel Brown 41dd91eb75 Write down where all of this came from
Section G, the closeout.

UPSTREAM-INSPIRATION gains the second and largest borrowing round: what
was taken from omarchy, end-4, caelestia, ML4W and HyDE, what was
rebuilt rather than transplanted, and -- the part that matters more --
what was declined and why. Wallpaper-derived colour is declined on
identity, because dynamic colour is how rices end up looking like each
other. Audio visualisers are declined because a continuously repainting
equaliser is the GPU-pegging animation this project avoids. Patching
application internals to theme them is declined because the maintenance
is unbounded and lands on whoever is holding the desktop that day.

DESKTOP-PARITY gains a Portable machines section, which it has never
had, because until yesterday Panama was a desktop configuration in the
code as well as on the machine. Nine rows, one of them honest about not
having run on hardware with a lid. The remaining-gaps list gains the
four things now known to be missing rather than the one.

The README stops describing a desktop from before yesterday: migrations
in the install flow, the welcome and cheatsheet and manual, and laptops
working with every surface hiding itself where the hardware is absent.

The plan records its own outcome per section, including the two places
the work departed from it: D2's template renderer deferred once D1
removed the duplication it existed to fix, and E1 inverted after using
it for an afternoon.
2026-08-22 07:48:24 -04:00

34 KiB

Panama OS-Parity Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Close the gaps between Panama and what a GNOME, macOS, or Windows user expects, and give Panama a way to keep converging after it is installed. Seven sections, ordered so the foundation lands first. Sections C onward can be reordered freely; Section A cannot.

Architecture: Six mechanisms, each chosen because the repository already has the shape of it.

  • Migrations are timestamp-named shell scripts with marker files, mirroring config/dot/quickshell/config/Migrations.qml, which already does exactly this for the settings JSON and documents the rules that make it safe.
  • Hardware predicates are one panama-hw command with subcommands that exit 0 or 1, so scripts, services, and contracts all ask the same question.
  • New shell state follows services/Vitals.qml: read /sys and /proc through FileView, carry an availability flag, spawn no subprocess. Writes that need root go through a helper script, as panama-idle and panama-power-profile already do.
  • Transient feedback extends services/StatusEvents.qml, which is already a curated system-toast layer distinct from the OSD and the notification center. No fourth layer is built.
  • Theme values move to one JSON palette that QML, Lua, and bash all read, replacing five hand-synced copies. Per-app output becomes templates.
  • New surfaces are PanelWindow overlays following modules/overview/Overview.qml, registered in ShellState, instantiated bare in shell.qml, with a matching layer rule in hypr/rules.lua.

Status (2026-08-22): Sections A, B, C, E and G are done. Section D landed its half that mattered (one palette, six copies removed) and deferred the template renderer. Section F is not started. What changed along the way is recorded per section below and in docs/UPSTREAM-INSPIRATION.md.

Tech Stack: Bash, jq, Python 3, Lua 5.4 (Hyprland 0.56), QML / Qt 6.11.1, Quickshell 0.3.0, systemd user units, udev, freedesktop notifications, PipeWire, hyprctl.

Spec: none. Shaped in conversation on 2026-08-21 from a survey of omarchy, end-4/dots-hyprland, caelestia, DankMaterialShell, noctalia, HyDE, ML4W, JaKooLit, and hyprdots. Provenance belongs in docs/UPSTREAM-INSPIRATION.md (Task G1).

Corrections to the survey, found while planning

Four things turned out differently from the initial reading. They are recorded here because each one changed a task.

  1. There is no existing welcome-screen design. docs/superpowers/specs/2026-08-20-panama-fresh-install-design.md is about installer correctness: undeclared fonts, the missing cursor theme, the wallpaper nothing ships. It does not specify onboarding. Section C is greenfield.
  2. StatusEvents is already the system-toast layer. Its header calls itself "curated shell feedback ... not a notification store," it has priority tiers, same-key coalescing, a four-deep queue, and a silent-startup window. Headset switching already publishes through it as device-output. Task E3 shrank from "build a third category" to "add two producers."
  3. hypridle has no concept of AC versus battery. Per-power-source idle timings cannot be expressed in one config. Task B4 therefore regenerates the single config on power-source transitions rather than maintaining two.
  4. Display hotplug does not re-apply stored layout. services/Displays.qml reacts to screenSignature changing by refreshing and aborting any in-flight confirmation, but persisted per-output layout is applied by hypr/monitors.lua at config time only. That is the actual clamshell gap.

Global Constraints

  • Nothing new polls what it can observe. Timers gate on visibility or on a state that makes them relevant, following Capture.qml and HomeAssistant.qml. No continuously repainting animation.
  • A widget whose data is absent hides itself. VitalsWidget.qml:44 is the precedent: visible: Settings.showGpu && Vitals.gpuAvailable. A desktop must see no laptop chrome at all.
  • IPC functions annotate every parameter and the return type. Quickshell silently declines to register a function that does not, and will not warn. shell.qml:114 says so.
  • One IpcHandler per target name, enforced by tests/quickshell/ipc-targets-contract.
  • A new Settings page touches five files, and tests/quickshell/settings-nav-contract is the checklist: SettingsSidebar.qml (the row), SettingsShell.qml (the switch arm and the one-line Component declaration), modules/settings/qmldir, the allowed array in ShellState.qml:119, then SettingsSearch.qml for search terms. Launcher commands are regenerated with scripts/panama-settings-commands, never hand-written.
  • A new module directory ships an explicit qmldir, and tests/quickshell/qmldir-registration-contract will hold every sibling to it.
  • A new layer-shell namespace needs a rule in hypr/rules.lua or it gets no blur and no dim. ^qs-popover already matches by prefix; anything else must be added to the alternation.
  • A schema change means regenerating docs/settings.md with scripts/panama-settings-docs, pinned by tests/quickshell/settings-docs-contract.
  • Every keybind carries a description. tests/quickshell/keybinds-contract fails the build otherwise, and Keybinds.parse() drops undescribed binds entirely.
  • Migrations are idempotent, tolerate absent or wrong-typed input, and never assume a previous migration ran. Root-level work goes through panama-sudo --reason, never bare sudo.
  • Secrets never reach argv or a file. Established by panama-pick password.
  • No .sh extensions, and every new test file bumps the count in README.md that tests/setup/readme-contract pins.

Section A: Migrations

The foundation. Panama gained a curl installer, so it now has users whose machines must converge on later versions. Today change-settings only adds: sudo cp -r config/copy/. /. Nothing can remove an /etc drop-in, disable a unit, repair a stale symlink, or correct a gsettings default on a machine that installed in March.

Task A1: The runner and its state

Files:

  • Create: bin/panama-migrate
  • Create: migrations/.keep
  • Modify: bin/panama (dispatcher entry)

Steps:

  • migrations/<unix-timestamp>.sh, ordered by lexicographic glob over fixed-width epoch seconds, which is chronological order without a sequence number to collide on across branches
  • Markers at ${XDG_STATE_HOME:-$HOME/.local/state}/panama/migrations/<filename>; presence means applied; no database, no version integer
  • Run each unmarked migration with bash -euo pipefail, exporting PANAMA_PATH; touch the marker only on success, so a failure stays pending and retries
  • --pending exits 0 when work is waiting and 1 otherwise, for the notifier to poll cheaply
  • --list prints pending and applied; --force <file> re-runs one; a failing migration reports which file and stops rather than continuing into ones written to follow it
  • panama migrate in bin/panama as a cmd_migrate following the existing cmd_* convention, plus its usage() line

Task A2: Authoring, and the fresh-install baseline

Files:

  • Create: bin/panama-dev-migration
  • Modify: install

Steps:

  • panama-dev-migration "<what it does>" stamps migrations/$(git log -1 --format=%cd --date=unix).sh with a header comment template and the executable bit
  • The template states the two rules in the file itself: the script must be safe to run twice, and it must tolerate the state it is repairing already being correct
  • install pre-marks every shipped migration on a fresh machine, so a new install never runs repairs for versions it never had. This mirrors Migrations.qml's baseline stamping, and the comment should say so
  • Pre-marking happens only when the marker directory does not yet exist, so re-running install on an existing machine does not mask genuinely pending work

Task A3: Knowing there is work

Files:

  • Create: config/local/share/systemd/user/panama-migrate-notify.service
  • Modify: config/dot/hypr/autostart.lua
  • Modify: bin/panama-migrate

Steps:

  • A oneshot user service that runs panama-migrate --pending and, when work is waiting, sends a notification whose action opens a terminal running panama migrate
  • Started from autostart.lua, not enabled globally: change-settings deliberately enables nothing because graphical-session.target is active under GNOME too, and that reasoning applies here
  • Wait for the notification server before sending, because the shell owns org.freedesktop.Notifications and a shell that failed to start is exactly when a migration might be pending
  • The notification is critical urgency so it does not expire before it is read, and carries an action rather than instructions

Task A4: Root repairs, and the first real migrations

Files:

  • Modify: bin/panama-migrate
  • Create: migrations/<timestamp>.sh (two or three real ones)
  • Create: tests/setup/migrations-contract

Steps:

  • Document the root convention in panama-migrate's header: a migration needing root calls panama-sudo --reason "<what this repairs and why>", so the password prompt names the repair. Never bare sudo
  • Write the first migrations against real drift this repository has already created: the settings-ssh-keys.sh launcher-command orphan left when the generator dropped .sh extensions, and the startsunshine alias removed from config/bash/aliases
  • Contract: a fresh state directory plus a fixture migration runs once and not twice; a failing migration leaves no marker and is retried; markers are per-file so an added migration runs alone; --pending exit codes; pre-marking suppresses everything
  • Contract asserts no shipped migration calls bare sudo

Section B: Laptop support

Panama has no upower reference anywhere, no battery, no lid, no AC awareness. hypridle.conf says in its own header "This is a desktop." This section is the difference between a desktop configuration and one anybody can install. It is testable: the hardware exists.

Task B1: Hardware predicates

Files:

  • Create: bin/panama-hw
  • Create: tests/setup/hardware-predicates-contract

Steps:

  • One command, subcommands exit 0 or 1 and print nothing: laptop (DMI chassis type 8, 9, 10, 14), battery, ac, lid-closed, external-monitor, clamshell, touchpad, nvidia
  • clamshell is defined as lid closed and at least one external output active. That one line is the whole feature; everything else composes from it
  • nvidia reads cached sysfs vendor and class IDs rather than lspci, which touches PCI config space and wakes a runtime-suspended GPU
  • Every predicate answers on a machine missing the hardware entirely, without error output
  • panama-hw --json prints all predicates at once, for the health page and for contracts
  • Contract drives each predicate against fixture sysfs trees; the real machine is only asked whether the command runs

Task B2: The battery service and its indicator

Files:

  • Create: config/dot/quickshell/services/Battery.qml
  • Modify: config/dot/quickshell/modules/bar/StatusCluster.qml
  • Modify: config/dot/quickshell/config/PreferenceSchema.qml
  • Create: tests/quickshell/battery-contract
  • Create: config/dot/quickshell/battery-harness.qml

Steps:

  • Follow Vitals.qml exactly: FileView on /sys/class/power_supply/BAT*/{capacity,status,charge_control_end_threshold} and AC*/online, printErrors: false, parsed in onLoaded, no subprocess
  • available is false when no battery path loads, driven by onLoadFailed, exactly as gpuAvailable is
  • Expose percent, charging, acOnline, timeRemaining when the kernel offers it, and lowBattery / criticalBattery derived from thresholds
  • Poll interval is slow (60s is generous for a battery) and the file set is tiny; do not add a subprocess to make it faster
  • Indicator is a StatusGlyph in StatusCluster, visible: Battery.available && Settings.showBattery, following the showGpu && gpuAvailable precedent. A desktop shows nothing
  • Schema: showBattery (bool, def true, group vitals), batteryLowPercent (int, def 20), batteryCriticalPercent (int, def 5)
  • Harness plus contract: fixture sysfs paths drive percent, charging, and availability; assert the indicator hides when unavailable
  • Regenerate docs/settings.md

Task B3: The Power page grows a battery section

Files:

  • Modify: config/dot/quickshell/modules/settings/PowerPage.qml
  • Create: config/dot/quickshell/scripts/panama-battery
  • Modify: config/dot/quickshell/config/PreferenceSchema.qml
  • Modify: tests/quickshell/power-profile-contract or a new sibling

Steps:

  • A "Battery" card above "Power profile", visible: Battery.available, showing charge, state, and time remaining as TextRows
  • Charge threshold control where the kernel exposes charge_control_end_threshold; hidden entirely where it does not, rather than shown disabled
  • panama-battery status|set-threshold is the root boundary; the write goes through panama-sudo --reason "Setting the battery charge limit to N%"
  • Threshold is re-read after writing rather than assumed, matching how PowerProfiles.set() always re-queries
  • Schema: batteryChargeLimit (int, def 100, min 50, max 100, group power)
  • Add the power group to groupPages in SettingsSearch.qml if it is not already routed

Task B4: Idle timings that know about the wall

Files:

  • Modify: config/dot/quickshell/config/PreferenceSchema.qml
  • Modify: config/dot/quickshell/scripts/panama-idle
  • Modify: config/dot/quickshell/services/IdleLock.qml
  • Modify: config/dot/quickshell/modules/settings/PowerPage.qml
  • Modify: tests/hypr/idle-config-contract

Steps:

  • hypridle has no AC concept, so there is one generated config and it is regenerated when the power source changes. Do not maintain two configs
  • Schema adds battery variants: screenBlankMinutesBattery, lockMinutesBattery, suspendMinutesBattery, each defaulting shorter than its AC counterpart, group idle
  • panama-idle reads panama-hw ac and selects the key set; when no battery exists the battery keys are never consulted
  • IdleLock regenerates on power-source transition. Battery.acOnline changing is the trigger, joined to the existing 400ms debounce so a flapping charger cannot restart hypridle in a loop
  • The Power page shows the battery sliders only when Battery.available, in a second column or a second card, labeled so it is obvious which set is active right now
  • Extend tests/hypr/idle-config-contract: generation under both power sources against a fixture, and that a machine without a battery produces exactly today's output

Task B5: The lid

Files:

  • Create: bin/panama-lid
  • Create: config/copy/etc/systemd/logind.conf.d/10-panama-lid.conf
  • Create: migrations/<timestamp>.sh
  • Create: tests/setup/lid-contract

Steps:

  • Determine the lid event source on the real hardware before writing the handler. Candidates in order of preference: a logind inhibitor, a udev rule on the lid switch, /proc/acpi/button/lid/*/state. Record which one and why in the script header
  • logind.conf.d sets HandleLidSwitch=ignore so Panama decides, and the file says why in a comment
  • panama-lid close locks immediately rather than waiting for PrepareForSleep, because logind's delay inhibitor is a timer that expires whether or not the session is secure
  • Then it branches: clamshell means turn the internal output off and stay awake; otherwise suspend
  • panama-lid open restores the internal output, and recovers when the output was left disabled by an interrupted close
  • A migration installs the logind drop-in on machines that already ran install before this existed. This is the first migration that genuinely needs root, and it is the reason Section A comes first
  • Contract: the clamshell branch against stubbed predicates; assert lock happens before suspend in the non-docked path

Task B6: Docking, and getting the layout back

Files:

  • Modify: config/dot/quickshell/services/Displays.qml
  • Modify: config/dot/quickshell/modules/settings/DisplaysPage.qml
  • Modify: tests/quickshell/displays-contract

Steps:

  • reconcileTopology() currently refreshes and aborts a pending confirmation. Extend it: when an output reappears and a stored entry for it exists in the displays preference, re-apply that entry
  • Re-application is not a user-confirmed transaction. It restores something the user already confirmed, so it must not start the 15-second countdown. Say so in the comment, because the next reader will wonder
  • Never re-apply onto a topology where the stored geometry would strand an output offscreen; fall back to automatic placement and publish a StatusEvents toast saying the layout could not be restored
  • Undocking must leave a usable session: if the primary output disappears, re-seat primary on a connected one, which performRevert() already knows how to do
  • Extend the displays contract with a hotplug fixture through screenOverride, which already exists for exactly this

Section C: Onboarding and discovery

Thirty settings pages is the opposite of the usual problem. A new user cannot tell which four matter.

Task C1: Keybind categories at the source

Files:

  • Modify: config/dot/hypr/keybinds.lua
  • Modify: config/dot/quickshell/services/Keybinds.qml
  • Modify: tests/quickshell/keybinds-contract

Steps:

  • Keybinds.qml derives groups today by substring-matching descriptions in groupFor(). That is guesswork and it drifts. Replace the source, not the mechanism
  • The bind() wrapper in keybinds.lua already intercepts every bind for chord overrides. Extend it to accumulate { chord, description, category } and write the manifest to ${XDG_STATE_HOME}/panama/keybind-categories.json at config load
  • Writing must never raise: a read-only state directory costs the categories, not the keymap. prefs.lua establishes the never-raise convention
  • Keybinds.qml joins the manifest on luaChord (the shipped chord, which is what overrides are keyed by) and keeps today's substring derivation as the fallback when the manifest is absent
  • Categories are chosen to answer "what can I press," not to mirror the config's internal structure: Windows, Workspaces, Applications, Shell, Media and hardware, Session
  • Contract: every bind carries a category; the manifest round-trips; a missing manifest still produces grouped output

Task C2: The cheatsheet overlay

Files:

  • Create: config/dot/quickshell/modules/cheatsheet/{Cheatsheet.qml,CheatsheetGroup.qml,qmldir}
  • Modify: config/dot/quickshell/shell.qml
  • Modify: config/dot/quickshell/services/ShellState.qml
  • Modify: config/dot/hypr/keybinds.lua
  • Modify: config/dot/hypr/rules.lua
  • Create: tests/quickshell/cheatsheet-contract

Steps:

  • A PanelWindow on WlrLayer.Overlay following Overview.qml: the mapped and unmapTimer pattern so the close animation is seen, Keys.onEscapePressed, WlrKeyboardFocus.Exclusive while open
  • Namespace qs-popover-cheatsheet, which inherits the existing ^qs-popover blur rule rather than needing a new alternation entry. Confirm against rules.lua and add it explicitly if the prefix rule does not cover it
  • Registered in ShellState.activeOverlay as "cheatsheet", so opening it closes whatever else was open, and the line-20 comment lists it
  • Consumes Keybinds.grouped() and calls Keybinds.refresh() on open, so it reflects user rebinds
  • Columns, not one scroll: roughly 118 binds across six categories does not read as a list
  • Bound to SUPER + slash with a description, plus an IPC target and a launcher command
  • Contract: the surface maps (poll hyprctl layers as signal-glass-contract does), every category renders, and the bind exists

Task C3: The welcome surface

Files:

  • Create: config/dot/quickshell/modules/welcome/{Welcome.qml,WelcomeStep.qml,qmldir}
  • Modify: config/dot/quickshell/shell.qml
  • Modify: config/dot/quickshell/services/ShellState.qml
  • Modify: config/dot/quickshell/config/PreferenceSchema.qml
  • Modify: config/dot/hypr/rules.lua
  • Create: tests/quickshell/welcome-contract

Steps:

  • Shown once, on the first shell start after install, then never again unless asked for
  • The sentinel is a schema key welcomeSeen (bool, def false, internal: true), stored in ~/.config/panama/settings.json rather than Quickshell's state dir, because Quickshell derives that directory from config content and it moves when the config changes materially
  • Decide deliberately whether "Restore defaults" re-shows the welcome. Recommendation: yes, and say so in the schema detail, because a reset user is a user who wants the tour again
  • Content is four things and no more: the keys that open the launcher, the terminal, Settings, and the cheatsheet. It ends on a button that opens the manual
  • Relaunchable from a launcher command and from the About page, so it is a resource and not a one-shot
  • Contract: the sentinel gates it, dismissing sets it, the IPC surface opens it again

Task C4: The manual

Files:

  • Create: manual/01-welcome.mdmanual/NN-*.md
  • Create: config/dot/quickshell/modules/settings/ManualPage.qml
  • Modify: SettingsSidebar.qml, SettingsShell.qml, modules/settings/qmldir, ShellState.qml, SettingsSearch.qml
  • Modify: setup/scripts/link-dotfiles (expose manual/ at a runtime path)
  • Create: tests/quickshell/manual-contract

Steps:

  • Numbered markdown chapters. Write these first, in this order: coming from GNOME, macOS, or Windows; the keymap; what to do when something breaks. The rest can accrete
  • Rendered with Text { textFormat: Text.MarkdownText }, which Qt 6.11.1 supports fully. Nothing in the repo uses it yet, so this is the first
  • Render one chapter per Text, never the whole manual in one, because Text has an implicit texture size limit on very long documents
  • Load through FileView; resolve the manual directory from a runtime path rather than the repository, since ~/.config/quickshell is a symlink and the manual is not currently exposed anywhere
  • onLinkActivated goes to Qt.openUrlExternally; internal chapter links navigate within the page
  • Chapter navigation uses the takeSettingsSection() consume-once idiom so a deep link opens a chapter without pinning it
  • Full five-file page registration, then regenerate launcher commands with panama-settings-commands
  • Contract: every chapter file renders, the page is registered in all five places, and no chapter links to a file that does not exist

Section D: One palette, many outputs

Outcome: D1 done. D2 deferred, deliberately: the concrete pain the template renderer was meant to solve was the hand-synced accent maps, and D1 removed those. Porting six working generators to a template engine is real risk with nobody currently waiting on it. Revisit when a seventh application wants theming.

scripts/panama-theme-apps:215 admits it: "there is no shared source between QML and a shell script." The eight-accent map exists in five places. Adding a themed application is a code change in three languages.

Task D1: The palette becomes data

Files:

  • Create: config/dot/quickshell/config/palette.json
  • Modify: config/dot/quickshell/config/Theme.qml
  • Modify: config/dot/quickshell/services/ThemeProfileModel.js
  • Modify: config/dot/hypr/looks.lua
  • Modify: config/dot/quickshell/scripts/panama-theme-apps
  • Modify: config/dot/quickshell/scripts/panama-lock
  • Create: tests/quickshell/palette-contract

Steps:

  • JSON is the format because all three consumers can already read it: QML natively, bash through jq, and Lua through the hand-rolled parser in prefs.lua
  • One file holds the eight named accents per scheme and the scheme base colors. Nothing else moves yet
  • Each of the five current copies is replaced by a read, and the accent_hex() and hex_to_rgb() duplication between panama-theme-apps and panama-lock collapses into one sourced helper
  • looks.lua must keep working when the palette file is unreadable, following prefs.lua's never-raise rule: fall back to the values it carries today
  • Contract: all five consumers resolve the same hex for the same accent and scheme. This is the test that makes the duplication impossible to reintroduce

Task D2: Per-app output becomes templates

Files:

  • Create: config/dot/quickshell/themes/templates/*.tpl
  • Modify: config/dot/quickshell/scripts/panama-theme-apps
  • Create: tests/quickshell/theme-template-contract

Steps:

  • A template is a file with a target path on line one and placeholders in the body, following HyDE's .dcol shape, which is leaner than mustache and needs no dependency
  • Placeholders resolve from the palette: the color, the color without #, and the color as R, G, B decimal, which is what hyprlock wants
  • Port today's targets one at a time, asserting byte-identical output against the current generator before deleting it: kitty, GTK3, GTK4, hyprlock, btop, tmux
  • kitty keeps both halves, the generated include and the live set-colors over the control socket, because a scheme change that only affects new windows reads as broken
  • User templates in ~/.config/panama/themes/templates/ render first and never overwrite an existing hand-written output, so a user's own file always wins
  • Contract: adding a template file adds an output with no code change, which is the entire point

Section E: The small wins

Outcome: E1, E2 and E4 done. E3 done for the charger and low battery; the keyboard-layout notice was not built, because Hyprland exposes the active keymap but not a change event Quickshell already consumes, and a single-layout machine cannot test one.

E1 changed shape after use. Focusing on the plain application keys made "give me another terminal" the awkward case, which on a tiling desktop is the normal one. The plain keys open a new window as they always did, and SUPER+ALT is the go-to chord.

Task E1: Launch or focus

Files:

  • Create: bin/panama-launch
  • Modify: config/dot/hypr/keybinds.lua
  • Create: tests/setup/launch-or-focus-contract

Steps:

  • Match on class and title with word boundaries over hyprctl clients -j, dispatch focuswindow on the first match, else launch detached
  • Wrap the seven application binds: terminal, editor, browser, files, calculator, mail, settings
  • Settings already toggles through IPC and must keep doing so; do not route it through this
  • A second press while the window is focused does nothing, rather than cycling. Cycling is a different feature and it needs a decision this task should not make
  • Contract: a running window focuses, an absent one launches, and a match is never a substring accident (mail must not match gmail-notifier)

Task E2: Web applications

Files:

  • Create: bin/panama-webapp
  • Create: config/local/share/vicinae/scripts/install-web-app
  • Create: tests/setup/webapp-contract

Steps:

  • panama-webapp install <url> [name] writes a desktop entry to ~/.local/share/applications/ whose Exec launches the browser in app mode
  • Icon resolution in four steps: the page's apple-touch-icon, then origin/apple-touch-icon.png, then a favicon service, then a generic fallback. Never fail the install over an icon
  • Sanitize the name into the filename; refuse a name that escapes the applications directory
  • remove and list reverse and enumerate by matching the launcher line, so nothing else is ever deleted
  • The browser is whatever panama-default-apps reports, falling back only when that browser cannot do app mode
  • Contract: install writes a valid entry with the right Exec; remove deletes only its own; a hostile name cannot write outside the directory

Task E3: Two more toasts

Files:

  • Modify: config/dot/quickshell/services/DeviceEvents.qml
  • Modify: config/dot/quickshell/services/InputDevices.qml
  • Modify: config/dot/quickshell/shell.qml (fixtures)
  • Modify: tests/quickshell/curated-events-policy-contract

Steps:

  • Charger connect and disconnect publish through StatusEvents at ambientPriority, so DND quiets them, which is correct for a charger
  • Battery critical publishes at criticalPriority, so DND never hides it. The DND cutoff sits below importantPriority, and that threshold is the actual policy decision here
  • Both observe Battery from Task B2. Charger detection is the only genuinely new plumbing of the three examples; headset switching already publishes device-output today
  • Keyboard layout changes publish from a new observer sourced from Hyprland, since InputDevices.qml has no layout handling at all
  • Every new producer joins the discoverySettle silent-startup window, or a cold boot fires a burst of toasts. curated-events-policy-contract already pins that window and will fail an eager initializer
  • Add fixtures to the status-events IPC target for each new class, because that IPC is what the contracts drive

Task E4: The file manager learns two verbs

Files:

  • Create: config/local/share/nautilus-python/extensions/{panama-share.py,panama-transcode.py}
  • Create: bin/panama-transcode
  • Modify: setup/scripts/link-dotfiles
  • Modify: setup/packages/desktop-packages (add nautilus-python)
  • Create: tests/setup/nautilus-extensions-contract

Steps:

  • Right-click Share routes to the existing sharing path rather than inventing a second one
  • Right-click Transcode offers picture formats and video resolutions, wrapping ffmpeg, which desktop-packages already declares
  • Transcode never overwrites its input and never writes outside the input's directory without asking
  • Extensions are linked per-file, following the quadlet and desktop-entry precedent in link-dotfiles, not by symlinking a directory Nautilus also writes to
  • Contract: both extensions are valid Python and declare the right Nautilus interface; the transcode wrapper refuses an unknown format rather than passing it to ffmpeg

Section F: Extension points and awareness

Task F1: Hooks

Files:

  • Create: bin/panama-hook
  • Modify: config/dot/quickshell/scripts/panama-theme-apps, install, config/dot/quickshell/services/FocusSession.qml call sites
  • Create: config/dot/panama/hooks/*.sample
  • Create: tests/setup/hooks-contract

Steps:

  • panama-hook <name> [args...] runs ~/.config/panama/hooks/<name> and every executable in ~/.config/panama/hooks/<name>.d/, non-fatally and in sorted order
  • A failing hook is reported and stepped over. A user's broken script must never break a theme change
  • Ship call sites for theme-set, post-upgrade, post-migrate, and focus-start / focus-end, each with a .sample explaining what it receives
  • This is deliberately the thirty-line version of the plugin system that docs/UPSTREAM-INSPIRATION.md defers. Say that in the header so the deferral stays a decision rather than an oversight
  • Contract: hooks run in order, a failure does not propagate, and a missing hook directory is silent

Task F2: Crash awareness

Files:

  • Create: bin/panama-crash-watch
  • Create: config/local/share/systemd/user/panama-crash-watch.service
  • Modify: config/dot/hypr/autostart.lua
  • Create: tests/setup/crash-watch-contract

Steps:

  • Follow journalctl -f filtered on systemd-coredump's message id, read the structured COREDUMP_* fields, and filter to the current UID
  • Wait for the notification server before sending, because a shell crash takes the notification server with it, and that is exactly the crash worth reporting
  • Dedupe per program on a window, and only start the window once a notification was actually delivered
  • The notification action opens the crash detail, not a raw core file
  • Fedora ships systemd-coredump by default, so this needs no new package. Verify that before writing the unit
  • Contract: a synthetic journal line produces exactly one notification, a repeat within the window produces none, and another user's dump produces none

Task F3: Agent usage in the bar

Files:

  • Create: config/dot/quickshell/scripts/panama-agent-usage
  • Create: config/dot/quickshell/services/AgentUsage.qml
  • Create: config/dot/quickshell/modules/bar/AgentUsageWidget.qml
  • Modify: config/dot/quickshell/modules/bar/Bar.qml, PreferenceSchema.qml
  • Create: tests/quickshell/agent-usage-contract

Steps:

  • The collector writes one display-ready JSON record to ${XDG_STATE_HOME}/panama/agent-usage.json; the QML only ever reads it. Adding a second agent later is a collector, not a QML change
  • Rate-limit pace and reset countdown are the useful numbers. Token totals are secondary and can be a tooltip
  • The widget hides itself when the record is absent or stale, following the availability convention
  • The collector runs on a timer measured in minutes, never on a bar repaint
  • Schema: showAgentUsage (bool, def false), because this is not a default-on feature for a general desktop
  • Contract: a fixture record renders; a missing record hides the widget; a stale record hides it too

Section G: Closeout

Task G1: Provenance and documentation

Files:

  • Modify: docs/UPSTREAM-INSPIRATION.md
  • Modify: README.md
  • Modify: config/dot/hypr/DESKTOP-PARITY.md
  • Modify: config/dot/hypr/README.md

Steps:

  • Record every borrowed idea in the ledger with its upstream project, the reviewed revision, and what Panama rebuilt rather than transplanted. This is the largest borrowing round the project has had
  • Record what was deliberately declined and why: the audio visualizer for GPU cost, app-internal theming for maintenance cost, wallpaper-derived palettes for identity, the plugin system as still premature
  • README gains the new capabilities and the corrected test count
  • DESKTOP-PARITY gains the laptop rows, which it has never had
  • The hypr README keymap gains the cheatsheet bind

Task G2: The suite is honest

Steps:

  • Full panama test green
  • README.md test count matches, pinned by tests/setup/readme-contract
  • docs/settings.md regenerated for every schema addition in Sections B, C, and F
  • Launcher commands regenerated with panama-settings-commands --check clean
  • Every new script carries the why-comment the repository expects, and no file carries a .sh extension