341 lines
19 KiB
Markdown
341 lines
19 KiB
Markdown
# Panama
|
||
|
||
Formerly Sunhat. A personal config for Fedora, with the intention of helping a
|
||
user set up their Fedora system with one command.
|
||
|
||
```sh
|
||
bash <(curl -fsSL https://git.gbrown.org/gib/Panama/raw/branch/main/boot)
|
||
```
|
||
|
||
`boot` installs git if the machine lacks it, clones this repository to
|
||
`~/.local/share/Panama` (or `$PANAMA_PATH`), and hands off to `install`. It is
|
||
deliberately small enough to read first, and the same two steps by hand work
|
||
identically:
|
||
|
||
```sh
|
||
git clone https://git.gbrown.org/gib/Panama.git ~/.local/share/Panama
|
||
~/.local/share/Panama/install
|
||
```
|
||
|
||
Both are safe to run again: an existing clone is fast-forwarded rather than
|
||
replaced. Once a machine exists, though, the command that keeps it current is
|
||
`panama update` — one command, and it never asks you anything:
|
||
|
||
```sh
|
||
panama update
|
||
```
|
||
|
||
It pulls, applies any repairs this machine has not had, and runs the stages
|
||
below that need no answers. `./install` remains what it is: how a machine is
|
||
built, and how you change an answer you gave.
|
||
|
||
The first question the interview asks is what the machine is: a **desktop**
|
||
gets everything below; a **server** — a VPS, a headless box — gets the same
|
||
shell environment minus everything that needs a screen, plus rootless podman
|
||
and the compose services in [`server/`](server/README.md). The answer is the
|
||
one thing Panama records durably (`~/.local/state/panama/role`), because
|
||
`panama update` asks nothing and still has to know. A fresh server skips the
|
||
question entirely:
|
||
|
||
```sh
|
||
bash <(curl -fsSL https://git.gbrown.org/gib/Panama/raw/branch/main/boot) --server
|
||
```
|
||
|
||
That command also works from a brand-new VPS's **root** login. It creates or
|
||
reuses your sudo-enabled user, then copies a safe root key when it can or
|
||
verifies the target key before offering SSH hardening. A verified target key
|
||
means the target user owns `.ssh` with mode `0700` and `authorized_keys` with
|
||
mode `0600`. SSH hardening is unavailable without a verified target key, and
|
||
the install continues without it.
|
||
|
||
When you accept hardening, Panama makes an atomic same-directory drop-in,
|
||
validates the complete SSH configuration with `sshd -t`, then reloads the
|
||
detected SSH unit. If validation or reload fails, it restores the previous
|
||
drop-in and validates and reloads that restored configuration; recovery that
|
||
cannot complete stops the handoff and prints the manual recovery command. The
|
||
fixture contracts exercise these branches. No real daemon reload runs under
|
||
`panama test --safe`, so that suite is not live-host proof.
|
||
|
||
After that, it hands off to a normal install as the new user.
|
||
|
||
`install` asks its questions first and then runs the stages in `setup/scripts/`
|
||
in order, without stopping again:
|
||
|
||
| Script | Does |
|
||
|---|---|
|
||
| `interview` | Every prompt, before anything is installed. Answers last one run and are never written to a durable path |
|
||
| `install-packages` | Repos (RPM Fusion, Terra, Hyprland COPR), the package lists in `setup/packages/`, then whichever optional categories were chosen |
|
||
| `link-dotfiles` | Symlinks `config/dot/<name>` → `~/.config/<name>`, and seeds the wallpaper, cursor theme and Firefox chrome |
|
||
| `link-skills` | Links the agent skills in `skills/` into `~/.claude/skills`, one per skill. Every machine gets these; personal ones link after and win a name clash |
|
||
| `link-user` | Links the personal content in `user/` — agent instructions, SSH host aliases — but only on a machine that answered yes. See [user/README.md](user/README.md) |
|
||
| `change-settings` | Copies `config/copy/` over `/`, applies gsettings, enables user services |
|
||
| `link-vicinae-scripts` | Publishes the Vicinae script commands |
|
||
| `setup-identity` | git config, `gh auth login`, an SSH key — whichever were asked for |
|
||
| `install-hardware` | NVIDIA, Secure Boot enrollment, Fedora's extras, firmware — each only if it was asked for. Last, because enrollment and firmware are consumed at the next boot |
|
||
| `setup-server` | Server role only: linger, unprivileged ports from 80, firewalld (80/443/81), the `nginx-bridge` network, and the nightly image-update timer |
|
||
| `link-server` | Server role only: creates `~/Server` and refreshes the links of whatever services this machine has enabled |
|
||
|
||
A server runs the shared stages plus its own two, and skips the rest —
|
||
`link-skills`, `change-settings`, `link-vicinae-scripts` and
|
||
`install-hardware` are desktop concerns. `link-dotfiles` links only the
|
||
universal dot dirs there (bash, nvim, tmux, btop and tmux theming, the hook
|
||
samples), and `install-packages` takes a short path: the core tools, nvm,
|
||
Bun, Claude Code and Codex — no third-party repos, no desktop, no flatpaks.
|
||
|
||
Then `panama migrate` applies any repairs this machine has not had yet. That is
|
||
the half of an upgrade installing cannot do: the stages above only ever add, so
|
||
nothing there can remove a file this repository stopped shipping or repair a
|
||
symlink that now points nowhere. A fresh machine is marked as already caught up
|
||
rather than having those repairs run against it, and a machine with work waiting
|
||
is told at the next login rather than left to find out.
|
||
|
||
The run ends with a health summary from `panama-doctor`, which reports what is
|
||
actually running rather than what was attempted. It never fails the install: on a
|
||
fresh machine it legitimately reports things as not yet configured.
|
||
|
||
### Optional applications
|
||
|
||
Every machine gets the lists in `setup/packages/`. The interview also offers the
|
||
categories in `setup/packages/extras/` as a checklist, so a work laptop need not
|
||
acquire emulators and a desktop need not skip Steam. Nothing is preselected.
|
||
|
||
A category is one file. A bare line is a dnf package and a `flatpak:` line is a
|
||
Flathub ID, because the applications in a category do not all come from one
|
||
place. Adding a category is adding a file — the menu is read from the directory,
|
||
not written down anywhere.
|
||
|
||
Existing configs are moved to `config/old/` rather than overwritten.
|
||
|
||
## The desktop
|
||
|
||
Hyprland, with a shell written from scratch. It began as a replacement for a
|
||
GNOME session — Forge for tiling, Dash-to-Dock, Openbar, Vitals — and was built
|
||
to reproduce it closely enough that muscle memory transferred: same keybinds,
|
||
same panel contents, same dock, same Tokyo Night Moon palette.
|
||
|
||
That is history now rather than a second option. Panama installs and configures
|
||
one desktop, and the GNOME session it grew out of is neither installed nor
|
||
configured here. What each piece replaced is recorded in
|
||
[`config/dot/hypr/DESKTOP-PARITY.md`](config/dot/hypr/DESKTOP-PARITY.md) and in
|
||
the comments of the components themselves, because knowing what a thing was
|
||
modelled on explains why it behaves the way it does.
|
||
|
||
GNOME is not gone from the machine: `gnome-control-center` is a declared
|
||
dependency, and two rows in Panama's own Settings still open it. Adding an
|
||
online account goes through the provider's dialog, because the OAuth sign-in
|
||
runs inside a library Fedora ships without a binding anything else can call.
|
||
And Digital wellbeing — screen time and break reminders — is the one panel of
|
||
GNOME's that still does something Panama does not. Everything else it used to
|
||
hand over is a page here now, and
|
||
[`tests/quickshell/gnome-handoff-contract`](tests/quickshell/gnome-handoff-contract)
|
||
fails the build if a door back opens onto a panel Panama owns, because those
|
||
rows kept working perfectly long after they stopped being true.
|
||
|
||
Displays is one of the panels it owns, and the only one where a wrong answer
|
||
can leave you unable to see well enough to undo it. So every change there is
|
||
applied as one complete layout, read back from the compositor, and reverted
|
||
after fifteen seconds unless you keep it — resolution, scale, rotation,
|
||
position and primary display, and now color profile, bit depth, SDR
|
||
brightness and saturation, and mirroring with them. Two of those opt out of
|
||
part of that, for reasons rather than convenience. A per-display variable
|
||
refresh rate override is applied but never verified, because `hyprctl` reports
|
||
whether adaptive sync is live this instant rather than what was asked for. And
|
||
a mirrored display's position is not asserted at all: the compositor stacks it
|
||
on the display it mirrors and ignores the coordinates the rule carried, so
|
||
holding it to them would make Keep permanently unavailable. Monitor brightness
|
||
sits outside the transaction entirely — it is the panel's own backlight over
|
||
DDC, and the buttons on the bezel change it behind our back.
|
||
|
||
| Piece | What it is |
|
||
|---|---|
|
||
| `config/dot/hypr/` | Compositor config. **Lua, not hyprlang** — see its README |
|
||
| `config/dot/quickshell/` | The shell: bar, dock, Continuum overview, Settings, Screen Intelligence, focus sessions, quick settings, notifications, screenshot UI |
|
||
| `config/containers/` | Container definitions systemd runs as units — currently the speech-to-text server behind dictation |
|
||
| `config/dot/vicinae/` | Raycast-style launcher, themed. Its commands live in `config/local/share/vicinae/` — script commands (settings deep links, power actions, projects, reminders, window switcher, kill process, SSH hosts, recent files, color picker), and one compiled extension that adds web search with live suggestions. File search, calculator, clipboard, and emoji are Vicinae's own |
|
||
| `config/dot/uwsm/` | Session environment (see the uwsm caveat in the hypr README) |
|
||
| `config/dot/wofi/` | Fallback launcher, in case the shell fails to start |
|
||
| `config/dot/xdg-desktop-portal/` | Portal backend routing |
|
||
|
||
New machines get a welcome card on first start, teaching the handful of keys
|
||
that matter and reachable afterwards from the launcher. `SUPER + /` shows every
|
||
shortcut the machine has, read from the live keymap so a rebind appears there
|
||
without anything being kept in sync. Rebinding is Settings' job, and so is
|
||
adding a shortcut of your own or assigning the four-finger touchpad swipes: an
|
||
entry names an application, a shell action or a window move rather than
|
||
carrying a command, so a hand-edited settings file can move a key but cannot
|
||
make one run something arbitrary. Settings carries a
|
||
[manual](config/dot/quickshell/manual/) written for the person using the
|
||
desktop rather than the person building it, opening with a chapter for people
|
||
arriving from GNOME, macOS or Windows.
|
||
|
||
Laptops work: battery, charge limits, idle timings that differ on battery, and
|
||
a closed lid that suspends when you are leaving and keeps working when you are
|
||
docked. A desktop sees none of it, because every one of those surfaces hides
|
||
when the hardware is absent.
|
||
|
||
**Start here: [`config/dot/hypr/README.md`](config/dot/hypr/README.md)** — it
|
||
covers the Lua migration, the uwsm environment gotcha, the HDR decision, the
|
||
full keymap, and troubleshooting.
|
||
|
||
Log in as **"Hyprland (uwsm-managed)"**, not plain "Hyprland".
|
||
|
||
## Layout
|
||
|
||
```
|
||
bin/ Small user-facing commands on PATH; `panama` is the entry point
|
||
migrations/ One repair per file, for machines installed before a change
|
||
config/
|
||
bash/ .bashrc, aliases, env (env is gitignored)
|
||
copy/ Files copied verbatim over / (needs sudo)
|
||
dot/ Symlinked into ~/.config; quickshell/manual/ is the manual
|
||
Settings renders
|
||
firefox/ Vendored Firefox chrome, linked into the browser profile
|
||
containers/ Quadlets, linked into ~/.config/containers/systemd
|
||
local/ Icons, the cursor theme, and the launcher's commands and
|
||
extensions, linked into ~/.local/share
|
||
old/ Backups of whatever was replaced (gitignored)
|
||
wallpapers/ Copied into ~/Pictures/Wallpapers when absent
|
||
server/ The server role: compose services (one directory per
|
||
service), the nightly image updater, and its units. See
|
||
server/README.md
|
||
skills/ Agent skills for operating this desktop, linked into
|
||
~/.claude/skills
|
||
setup/
|
||
apps/ Applications built from source, one file each
|
||
lib/ Shared by more than one stage; the extras catalog reader
|
||
packages/ One package per line; extras/ holds the optional categories
|
||
scripts/ Run in order by ./install
|
||
tests/ Contracts. See below
|
||
docs/ Settings reference, and the design specs behind the work
|
||
```
|
||
|
||
## Tests
|
||
|
||
187 of them, under `tests/`. `tests/contracts.manifest` classifies every
|
||
contract by the capabilities it needs. Run the hermetic set, or grant a
|
||
specific external capability when automation needs it:
|
||
|
||
```sh
|
||
panama test --safe
|
||
panama test --allow live-host updates
|
||
panama test --allow live-compositor keybinds
|
||
PANAMA_TEST_TIMEOUT_SECONDS=300 panama test --safe
|
||
tests/setup/interview-contract # or one directly; they are plain executables
|
||
```
|
||
|
||
`--safe` means hermetic contracts only. A plain full run prompts in a terminal
|
||
before it starts any non-hermetic contract. Automation cannot answer that
|
||
prompt, so it must grant every required capability with a repeatable `--allow`.
|
||
Each contract has a 180-second outer timeout by default. Set
|
||
`PANAMA_TEST_TIMEOUT_SECONDS` to a positive integer to override it. When a
|
||
contract fails, the runner prints its captured stdout and stderr. Successful
|
||
stdout stays quiet. Successful stderr is surfaced as a warning.
|
||
|
||
They are called contracts rather than unit tests because that is what they are:
|
||
each one pins a decision that was expensive to get right and is cheap to undo by
|
||
accident. Most read or measure the real thing — launching a shell to measure a
|
||
surface's geometry, standing stub commands on `PATH` to see what a stage would
|
||
have installed, running a script against a throwaway `HOME` — rather than
|
||
asserting things about source text, because the bugs worth catching here have all
|
||
been ones that source text looked fine for.
|
||
|
||
```
|
||
tests/setup/ The installer: the interview, package lists, hardware, extras
|
||
tests/quickshell/ The shell and its settings pages
|
||
tests/hypr/ The compositor config
|
||
tests/server/ The server role: the service catalog's shape, the secrets
|
||
rule that keeps a public repo safe, and panama-server
|
||
```
|
||
|
||
## Projects
|
||
|
||
A project is the set of windows you open together — which applications, which
|
||
workspace each was on, and for a terminal, which directory it was sitting in.
|
||
Arrange the desktop, then run **Save Layout as Project** from the launcher and
|
||
name it; **Open Project** lays it out again.
|
||
|
||
Workspaces are recorded as positions rather than numbers, and opening a project
|
||
claims free ones, so it never lands on top of what you are already doing. An
|
||
application that refuses to open twice — Slack, Thunderbird, the browser — is
|
||
moved into place rather than launched again. Saved layouts are listed on
|
||
Shell › Workspaces in Settings, which is also where they are removed.
|
||
|
||
## The `panama` command
|
||
|
||
```sh
|
||
panama update # bring this machine up to date; asks nothing
|
||
panama sync # review, commit and push your changes to this repo
|
||
panama edit # open it in Neovim
|
||
panama doctor # what is actually running, not what was installed
|
||
panama diagnose # hand the health summary and recent errors to your agent
|
||
panama test # every contract, prompting before non-hermetic work
|
||
panama test --safe # hermetic contracts only
|
||
panama test --allow live-host updates # grant one capability to automation
|
||
panama contracts <file> # which contracts mention a file, and can they be run
|
||
panama migrate # apply repairs this machine has not had yet
|
||
panama upgrade # re-run ./install from anywhere, interview and all
|
||
panama apps # choose applications to install, by category
|
||
panama app # applications no repository carries; build one by name
|
||
panama server # the services a server runs: list, enable, disable, status
|
||
```
|
||
|
||
`panama update` and `panama sync` are separate verbs on purpose. One acts on
|
||
the machine, the other on the repository. A single command that chose between
|
||
them by checking whether the working tree happened to be dirty would do a
|
||
different job depending on state nobody can see — and, worse, would never
|
||
update a machine belonging to somebody who had left a file edited.
|
||
|
||
`panama update` stashes uncommitted work across the pull and restores it
|
||
afterwards. If restoring conflicts it resets the checkout and leaves the work
|
||
in the stash, saying so at the end of the run: every dotfile here is a symlink
|
||
into this repository, so a conflict marker is not a thing to fix at leisure. It
|
||
is live in `~/.config` the moment it is written.
|
||
|
||
Only two things still need `./install`: a machine that does not exist yet, and
|
||
an answer you want to change. Adding a package to a list you already have is
|
||
`panama update`; adding an optional category is `panama apps`.
|
||
|
||
`panama apps` is the optional-application catalog, opened after the fact. The
|
||
interview offers the same categories during `./install`, whole; this picks a
|
||
category and then the applications inside it, so a machine can acquire Slack in
|
||
March without having wanted Discord in January. Both read
|
||
`setup/lib/extras-catalog`, so the two cannot describe different catalogues.
|
||
|
||
A category is one file under `setup/packages/extras/`. A bare line is a dnf
|
||
package, a `flatpak:` line is a Flathub id, `| Name` gives the menu something
|
||
readable, and an indented line belongs to the entry above it — which is how OBS
|
||
carries its sixteen plugin extensions as one thing to tick.
|
||
|
||
Hooks are the extension point: drop a script at `~/.config/panama/hooks/theme-set`
|
||
and it runs whenever the color scheme changes, with the scheme and accent as
|
||
arguments. Same for `post-upgrade` and `post-migrate`, and a `<name>.d/`
|
||
directory beside each so several things can react without fighting over one
|
||
file. A broken hook is reported and stepped over, never fatal. Samples are
|
||
copied into place on install.
|
||
|
||
`panama migrate` applies repairs an installed machine has not had yet. Safe to
|
||
re-run: nothing is applied twice, and a machine with nothing waiting says so.
|
||
|
||
`panama-sudo` is pkexec with a stated reason: `panama-sudo --reason "why" --
|
||
command` shows the reason on Panama's password prompt, clearly labeled as an
|
||
unverified claim beside polkitd's own action text — meant for agents and
|
||
scripts, so the person typing the password learns why before they do. Without
|
||
a reason, a running shell, or `qs` it behaves exactly like pkexec.
|
||
|
||
`panama app` is deliberately not part of `./install`. Everything else Panama
|
||
installs comes from dnf or Flathub; these are built from source because no
|
||
packaged form exists, and a source build is slow, wants the network throughout,
|
||
and depends on an upstream that moves. That is the failure the interview exists
|
||
to prevent, so asking for one is something you do on purpose — and it is also
|
||
how you rebuild when a new version ships. Nothing is pinned: each build takes
|
||
the current upstream and reports a failure rather than working around it.
|
||
|
||
Adding one is adding a file to `setup/apps/`, and the file has to say why the
|
||
exception exists.
|
||
|
||
No script in this repository carries a `.sh` extension, with one deliberate
|
||
exception: each repair in `migrations/` is named `<timestamp>.sh`, because
|
||
`panama-migrate` finds them by globbing exactly that. Everywhere else a shebang
|
||
and the executable bit already select the interpreter, and the extension only
|
||
becomes something to keep in sync — which it did not stay.
|