Files
Panama/config/dot/quickshell/modules/settings
Gabriel Brown 3c521cf5fa Handle Wi-Fi and Bluetooth in Settings
Network & Devices was 92 lines and two buttons that opened GNOME. It now
scans, joins, and pairs directly through Quickshell.Networking and
Quickshell.Bluetooth -- NetworkManager and BlueZ over DBus, no shelling
out to nmcli or bluetoothctl. That was the founding requirement for this
desktop: never having to drop to a terminal to join a network.

Scanning follows the page being visible. Wi-Fi scanning and especially
Bluetooth discovery hold the radio, and running either for a list nobody
is looking at spends airtime on nothing.

Joining a secured network gets a real password field, not the clipboard
popover's search box with different placeholder text: a Wi-Fi key typed
into a field that echoes it is readable by anyone behind you, and a
search glyph in front of a password prompt is simply wrong.

Two bugs found by looking at the rendered page, both silent:

The device lookups used enum names that do not exist --
NetworkDeviceType.Wifi rather than DeviceType.Wifi -- so both returned
null and the page reported "No Wi-Fi adapter" on a machine whose Wi-Fi
was connected. Nothing was logged; QML resolves an unknown enum member
to undefined and compares happily.

signalStrength is 0.0-1.0, not a percentage, so thresholds written for
0-100 put every network including the connected one in the bottom
bucket. The labels now use the same buckets as the icons in
quicksettings/WifiList.qml so the two cannot disagree.

The contract compares what the service resolves against what nmcli
reports, rather than only checking that nothing crashed.

Also makes the Home Assistant bridge hermetic: resolve_config read the
user's private env file even when a caller supplied an explicit
environment, so adding a real PANAMA_HOME_ASSISTANT_ENTITIES to that
file silently overrode a fixture asserting the legacy fallback. An
explicit environment is now the whole environment; production still
reads the file. Its live contract skips when no token is configured --
an absent credential is not a defect, and a suite expected to be red
stops being read -- while a configured-but-broken bridge still fails.

Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
2026-08-18 05:50:07 -04:00
..
2026-08-18 01:36:07 -04:00
2026-08-18 05:50:07 -04:00

Panama Settings

The control centre for everything Panama owns. Anything the system owns — hardware, accounts, printers — is delegated to GNOME Settings and labelled as such rather than half-reimplemented.

Adding a setting

One schema entry. That is the whole job.

// config/PreferenceSchema.qml
{
    key: "blurSize", type: "int", def: 8, min: 1, max: 20, step: 1,
    unit: "px", group: "effects",
    label: "Blur radius",
    detail: "Larger is softer and costs more frame time",
    hypr: { path: ["decoration", "blur", "size"], option: "decoration:blur:size", readAs: "int" }
}
// the page
SliderRow { setting: "blurSize" }

Persistence, validation, clamping, reset, search indexing, and — with a hypr block — live application to the compositor and the startup replay all derive from that entry. There is nothing else to register.

If it is compositor-backed, add the matching prefs.get("blurSize", 8) in hypr/looks.lua so the Hyprland config still stands alone with no settings file.

The rows

Component For
SettingsPage The page scaffold: title, lede, optional pinned header
ToggleRow { setting } A boolean
SliderRow { setting } A number; zeroLabel renders 0 as "Never"/"Instant"/"None"
ChoiceRow { setting } An enum, as a segmented control
ActionRow A button: opens a GNOME panel, runs a one-shot
TextRow A genuinely read-only fact

TextRow is for facts, not for settings that were merely expensive to wire. Before Stage 3 more than half of all rows were static text standing in for controls; that is the failure this vocabulary exists to prevent.

Rows write through SystemSettings.commitPreference(key, value), which routes compositor-backed keys through apply-and-verify and local keys straight to the store. A row never needs to know which kind it holds.

Things that will bite you

readAs describes the answer, not the setting. hyprctl getoption returns the value in a different JSON field per type — int, bool, float, str, and css for gaps (a four-value box). Declaring the wrong one does not fail loudly: it makes every write to that key look rejected, and the user sees an error for a change that worked. tests/quickshell/schema-hypr-shape-contract.sh asks the compositor for the real shape of every mapped option.

Never trust an exit code from hyprctl. keyword refuses to work on a Lua-configured Hyprland, prints the refusal to stdout, and exits 0. eval exits 0 on syntax and runtime errors too. The only trustworthy signal that a write landed is reading the value back.

The Settings window is tiled. implicitWidth is a hint; the layout decides, and it ranges from a half-screen split to the full display. SliderRow stacks its control under the label below 520px. Test narrow.

Binding an anchor to undefined does not reliably release it. Switching layouts that way left a slider anchored to both edges with the label squeezed into what was left. Position explicitly instead.

Inside a SettingsCard, parent is the card's internal Column. So parent.modelData in a nested Repeater is undefined and the rows silently never appear — you get a card with a heading and nothing under it. Address the outer model through an explicit id.

A TapHandler declared as a child of SettingRow lands in the trailing slot, because that is the row's default property, so only the right-hand edge becomes clickable. Use activatable: true with onActivated for a whole-row target.

A copy of the Quickshell config shares the live shell's ID. Quickshell derives the Shell ID from config content, not path, so cp -a config/dot/quickshell $tmp && qs -p $tmp kill kills the running desktop, and qs -p $tmp ipc call … can drive it. Harnesses that point at a single distinct .qml file are safe; copying the whole directory is not.

Where state lives

File Holds
~/.config/panama/settings.json Everything in the schema. Read by the shell and by hypr/prefs.lua
$XDG_STATE_HOME/panama/panama-home.json Home accessory favourites and aliases
$XDG_STATE_HOME/panama/backups/ Settings snapshots
$XDG_STATE_HOME/panama/hypridle.conf Generated idle config

SystemSettings.restoreDefaults() spans all of them. A reset that silently skipped one would be worse than having no reset, because nothing would say so.

Not stored by Panama

Timezone and network time are read from and written to timedatectl directly. They belong to the machine and are shared with sessions that never see Panama's file; storing a copy would create a second answer to a question the system already answers.