The keyring is already unlocked at sign-in exactly as GNOME does it --
pam_gnome_keyring is in GDM's stack and the journal confirms it works
("gnome-keyring-daemon started properly and unlocked keyring"). So there
was no configuration bug to fix. What a bare Hyprland session lacks is
anywhere to see when that has stopped being true.
It stops being true rarely and expensively. gnome-keyring-daemon crashed
once on this machine -- an upstream abort in service_method_open_session,
with a core dump -- and D-Bus then activated a replacement. That
replacement never received the login password, so the keyring was locked
in the middle of a session that had unlocked it correctly at login.
Nothing announces this. What you see instead is a mail account that will
not authenticate, a git push that cannot find its key, or an integration
reporting "not configured", none of which mention keyrings. That is the
same root cause as the Home Assistant token failure earlier.
Privacy & Security now shows the state, offers an Unlock action that
raises the standard password dialog, and reports when the daemon holding
your secrets is a D-Bus replacement rather than PAM's -- because a
replacement that is currently unlocked was unlocked by hand and will not
survive a restart. The password never passes through Panama.
The contract stubs the secret service rather than touching the real one:
locking the login keyring breaks every saved password on the machine and
can only be undone by typing the password into a dialog, so it is not
something a test suite may do to a daily driver. Verified it catches a
helper that misreports locked as unlocked, and one that crashes instead
of reporting a missing service.
Worth recording: a locked keyring makes a NON-INTERACTIVE caller appear
to hang. It is not hung -- it is waiting on a dialog nobody is looking
at, which is exactly how the earlier secret-tool investigation lost an
hour.
Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
Reported symptom: Electron applications and a Chromium-based browser,
all set to follow the system, went light when the desktop went light and
never came back. Nothing reported an error, and the portal was serving
the correct value the whole time.
Cause: ColorScheme set gtk-theme to "Adwaita-dark" for dark and
"Adwaita" for light. Neither is installed on Fedora 44 -- only adw-gtk3
and adw-gtk3-dark are. GTK responds to an unknown theme name by falling
back to its built-in default, which is LIGHT. So asking for light worked
by accident, asking for dark silently produced light, and anything that
takes its cue from the GTK theme rather than the portal stayed light no
matter what org.freedesktop.appearance said. Verified: the portal emits
correctly in both directions, so this was never the portal's fault.
Second cause, the mirror of the first: gtk-3.0/settings.ini and
gtk-4.0/settings.ini were pinned to adw-gtk3-dark and prefer-dark=1 and
never regenerated. Under GNOME that file is ignored because
gnome-settings-daemon publishes over XSETTINGS; under Hyprland nothing
does, so for GTK3 it is authoritative -- and it contradicted the scheme
in light mode. They are now generated from a template on every switch,
gitignored as machine state, and seeded by link-dotfiles, the same shape
kitty's current-theme.conf already uses. These directories are symlinked
into the repository, so writing the live file directly would dirty the
working tree on every theme switch.
The failure was silent by construction, so it gets a test rather than a
comment: gtk-theme-contract asserts every theme name Panama sets is
actually installed, and that the generated GTK config agrees with the
scheme in both directions. Verified it catches both original bugs.
Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
About reported four component versions and, since this morning, a short
hardware summary. It now covers what someone actually wants from an
About page or a fastfetch run: operating system, model, hostname,
kernel, uptime, package counts, shell, locale, windowing system,
resolution, processor, graphics, memory, swap, and disk.
Rows are ordered the way fastfetch presents them -- what the system is,
then what is installed on it, then the hardware underneath -- and the
GPU rows are spliced in directly after Processor rather than appended,
because a graphics card listed after "Disk" reads as an afterthought.
Graphics is still joined from GraphicsDevices rather than read a second
time, so the two readouts cannot disagree.
Memory is total, not used. About is not a monitor: a "12.4 GiB used"
figure is stale before it finishes drawing, and the Home page's vitals
readout is where live numbers belong. Disk is the exception because free
space does not move while you look at it.
Package counting is why this whole helper runs on demand -- rpm -qa on a
full workstation is a few thousand lines and takes a moment. Swap and
package counts are omitted entirely when they are zero or the tools are
absent, rather than reported as "0" or "Unknown".
Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
It is the system settings app for this desktop, so it is named the way
one is. The window is "Settings", the titlebar is "Settings", the
wordmark above the search field is gone, the sidebar's last row is
"About", and the status line reads "Desktop is healthy".
Prose followed the same rule. Forty-odd strings explained what "Panama"
does -- "Panama never animates while idle", "Restore Panama defaults",
"Panama looks in ~/Pictures/Wallpapers" -- which is how a product
describes itself, not how a settings panel describes a setting. They now
say what happens. No user-visible "Panama" remains anywhere in the app.
Two consequences worth naming:
The SUPER+I shortcut's description is user-visible, because the
Shortcuts page is generated from it, so that is renamed too. Rebindings
are keyed by shipped chord rather than description, so no existing
override is orphaned by this.
Three contracts matched the window by title and one matched that
shortcut by description; all four are updated. There are no Hyprland
window rules keyed on the title, so nothing about the window's placement
changes.
The desktop entry is now Name=Settings, but the FILE keeps its
panama-settings name, as does the icon: the dock pins applications by
desktop id, and renaming the file would silently unpin it.
dock-pins-contract covers exactly that.
The dated design docs under docs/superpowers keep the old name. They are
a record of what was decided when, and editing them to agree with the
present would make them lie about the past.
Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
With I2C access granted, the helper still found nothing: every connected
monitor was probed on the wrong bus.
A connector has two. The `ddc` symlink points at the classic I2C line
that carries EDID on HDMI and DVI. DisplayPort carries DDC/CI over the
AUX channel instead, which appears as a child directory of the
connector. Both exist on a DP connector and both resolve, so the wrong
choice looks entirely reasonable and simply finds no monitor: this
machine's DP-2 has ddc -> i2c-5, where ddcutil reports "No monitor
detected", while its AUX child i2c-9 answers VCP 0x10 immediately.
This is the guess the previous commit said was unverified, and it was
wrong in the way that mattered. Enumerating from sysfs is still right --
it gives the connector name Hyprland uses and skips empty connectors --
but it has to prefer the AUX child and fall back to the symlink.
The fixture now mirrors sysfs properly: /sys/class/drm/<connector> is a
SYMLINK to the real device directory, and `find` does not follow the
path it is given. A fixture built from plain directories passes whether
or not the code resolves the symlink first, which is a test that agrees
with itself rather than with the kernel.
Verified on hardware: the Kuycon P20 reports 100%, accepts 70 and
returns to 100, and the media keys move it through codex's OSD path.
Both bus-selection mistakes are now caught by the contract.
Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
GNOME's Search panel configures which applications provide results in
gnome-shell's overview and which folders are indexed. gnome-shell does
not run here, so reimplementing those switches would store preferences
that change nothing. Under Panama searching is the launcher's job and
Vicinae carries its own preferences, so the Applications page says where
search lives and how to rebind the keys that open it, rather than
offering settings that would be inert or duplicated.
The nav contract is the more useful half. Adding a settings page means
editing four files, and missing any one of them fails quietly in a
different way: no sidebar row, a row that silently shows Home, a deep
link that redirects to Home, or -- worst -- a missing qmldir entry,
which makes the page "not a type" and takes the entire settings window
down with it. Nothing at runtime cross-checks the four. Having just
added four pages by hand, this checks them statically, and also fails on
a page file that exists but is unreachable.
Verified it catches a missing qmldir registration and a missing
allow-list entry.
Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
Two things, both closing gaps in work that was already reported done.
GNOME's Multitasking panel, in Hyprland's terms: tiling layout, split
behaviour, floating-window snapping, workspace wrap-around and
back-and-forth, whether applications may take focus, and whether the
pointer changes the active display. Hyprland creates and destroys
workspaces as you use them, so there is no fixed count to expose, and
the page says so rather than leaving a conspicuous absence.
The Desktop page described the first two of these as read-only facts --
"Layout: Tiling", "Workspace movement: Dynamic" -- which was never true.
Both are ordinary Hyprland options that happened to have no controls,
and TextRow's own documentation says a setting the user could reasonably
change does not belong in it. Schema defaults are Panama's shipped
values from looks.lua rather than Hyprland's own, so restoring defaults
returns the desktop to how it ships. 43 mapped options now, from 35.
The launcher had no light theme. vicinae.json already selected a theme
per system appearance, but both entries pointed at Moon, so choosing
light mode left the most frequently opened window on the desktop dark --
a hole in the light/dark work, not a missing feature. Day is authored
from the same palette as the kitty Day theme so the two cannot drift,
and link-dotfiles now installs every authored theme rather than only the
dark one, which is why the gap survived being noticed.
Its placeholder colour is not Tokyo Night Day's own: that measures
2.54:1 against the background, below the 3:1 floor for secondary text.
This is 3.25:1, the same value used for neovim's light comments.
Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
More GNOME Settings parity.
Region & Language is new. The locale is localectl's, and Panama stores no
copy of it -- there is exactly one system locale, so a preference here
would be a second source of truth that drifts the moment anything else
changes it. Codes are resolved against iso-codes into "Portuguese
(Brazil)" the way GNOME does, with the code kept visible because it is
what actually gets written and someone choosing between two Spanish
variants needs to see it. Changing it is privileged and only applies to
programs started afterwards, so the page says a sign-out is needed
rather than claiming the new language is in use.
The service is called SystemLocale, not Locale: QML has a built-in
Locale value type that silently shadows a singleton of that name, and
every binding then reads properties off the wrong thing. The page
rendered empty with nothing but "cannot read property of undefined" to
explain it.
About now answers what GNOME's About answers -- model, processor,
memory, disk, OS, kernel, windowing system -- where before it listed
only Panama's own component versions. Graphics is joined from
GraphicsDevices rather than read again, because two readouts of the same
hardware are two things that can disagree. Placeholder DMI strings
("To Be Filled By O.E.M.") are filtered out, and unreadable facts are
omitted rather than shown as "Unknown".
The Fedora hand-off card was one row listing five subjects that opened
the network panel regardless. Naming a panel and then not opening it
reads as a broken button rather than a deliberate hand-off. Each subject
now opens the panel that owns it, and openGnomePanel takes an optional
subpage so "Users" reaches System's users page the way GNOME's own
desktop entry does, instead of dropping the user on System's front page.
Printers and online accounts are not repeated here; they stay with the
network hardware on Network & Devices.
One bug this surfaced, caught by the hyprland write contract: the Lua
config key and the hyprctl option name genuinely differ for tap to
click. hl.config wants input.touchpad.tap_to_click; getoption answers to
input:touchpad:tap-to-click. Either spelling used for both fails -- a
hyphen is not a Lua identifier, and the underscored name is not a known
option -- which is what the schema's two separate fields are for.
Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
Continuing towards GNOME Settings parity. GNOME's Privacy panel covers
screen lock, camera and microphone access, file history, trash, and
device security; Panama had no equivalent page at all, despite already
tracking camera and microphone use for the bar indicator.
Device security is a new read-only readout: Secure Boot, TPM, disk
encryption, SELinux mode, and the firewall. None of these is a
preference -- they are set in firmware, at install time, or by system
policy, and a switch offering to change them would either fail or do
something far-reaching from a control that looks like every other
control. What it answers is "is this machine set up the way I think it
is", which otherwise takes five commands and root. Facts that cannot be
determined report Unknown rather than guessing, because a security
readout that quietly says "fine" when it failed to look is worse than
no readout.
File history and trash retention are deliberately NOT offered as
switches. They are GNOME preferences enforced by gsd-housekeeping, which
does not run in a Hyprland session -- verified, it is not running here.
Toggling them would store a preference, change nothing, and give no sign
of it. They are delegated to GNOME Settings by name instead.
Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
Working towards parity with GNOME Settings, which splits pointing
devices into their own panel. Panama had pointer speed and focus-follows
buried under a page called "Input & Shortcuts", and had nothing at all
for scroll direction, acceleration profile, scroll speed, left-handed
buttons, or any touchpad setting -- all of which could only be changed
by editing hypr/input.lua by hand, which is the thing this app exists to
stop.
Every new mapping was read back off the running compositor rather than
assumed, and two were not what they look like: touchpad drag lock is an
int with three states, not a switch, and scroll factors are floats even
at their default of exactly 1. Getting either wrong makes every write to
that setting look rejected. The shape contract now covers 35 mapped
options, up from 23.
The touchpad card renders only when a touchpad is attached, which is
what InputDevices is for. On a desktop it would be worse than useless:
every switch on it would appear to work, because the preference is
stored and Hyprland accepts an option for a device class it has no
member of, so the settings would silently affect nothing.
Keyboard layout was read-only text, justified by a note saying changes
needed a compositor reload. That is not true in 0.56.2 -- setting
input:kb_variant through hl.config re-keymaps attached keyboards
immediately, verified by watching active_keymap on a real keyboard
change to "English (US, intl., with dead keys)" and back. So layout,
variant, and options are now real controls, joined by a TextEntryRow
that commits on Enter or focus loss rather than per keystroke, since
half a layout name is a valid string meaning something else.
Rejected input is shown as rejected rather than sanitised: these strings
are serialised into an hl.config payload, where stripping an unexpected
character would turn a typo into a different working setting.
Verified each new pointer option applies and reverts against the live
compositor. Schema, search, commit/reset, and system contracts pass.
Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
brightnessctl drives the kernel backlight class, which laptop panels
have and this desktop does not -- it reports only keyboard and NIC LEDs.
So BrightnessControl removed itself and there was no way to dim the
screen from Panama at all. DDC/CI is the channel the buttons on a
monitor's bezel drive, and it is the only brightness an external display
has. Both sources now render a row each, so a machine gets whichever it
actually has, or none.
Displays are enumerated from sysfs rather than `ddcutil detect`. The
kernel publishes the connector-to-bus mapping as
/sys/class/drm/<card>-<connector>/ddc along with whether anything is
plugged in, which beats parsing detect's undocumented brief output,
yields the connector name spelled exactly as Hyprland spells it, and
probes only connectors with a monitor attached -- one bus on this
machine rather than fourteen, where each empty bus costs a timeout.
No model name is read: Hyprland already knows what every output is
called, so the UI joins on the connector instead of keeping a second
source of truth that could disagree with the Displays page.
Writes are debounced, serial, and read back. Serial because DDC/CI has
no arbitration and two ddcutil processes on one bus interleave their
exchanges and both return garbage. Read back because a write is not a
promise: panels clamp to their own range, ignore values while waking
from standby, and drop writes that arrive too fast. Without the read the
slider would show what Panama asked for rather than what the monitor
did, which is the same class of lie as trusting `hyprctl keyword`.
Brightness is deliberately not a stored preference. The monitor
remembers it and the bezel buttons change it behind Panama's back, so
persisting it would mean restoring a value the panel had moved past.
The contract runs against fixtures with ddcutil stubbed and both sysfs
roots redirected, so it never touches a real monitor. Its fixture
reports a maximum of 200 rather than 100 on purpose -- at 100 the
scaling arithmetic is the identity and a helper that ignored the
reported maximum would pass everything. Verified it catches that, plus a
dropped connection-status filter and an unstripped connector prefix.
Not yet confirmed against hardware: this machine cannot open any I2C bus
yet. ddcutil's udev rule grants that through uaccess but only to devices
created after it was installed, so it needs one udevadm trigger. The
helper detects exactly that case and returns the command as its error
rather than reporting "no displays".
Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
The scheme switch already reached everything that reads
org.freedesktop.appearance -- GTK4, Qt6, Chromium, Electron -- because
ColorScheme.qml writes the gsettings key those all watch. Applications
carrying their own palettes did not follow, so choosing light mode left
the two windows actually used all day, kitty and neovim, still dark.
kitty: the 32 colours move out of kitty.conf into themes/, and kitty.conf
ends with `include current-theme.conf`. The generated file is machine
state rather than configuration, so it is gitignored and link-dotfiles
seeds it on install -- otherwise a fresh checkout starts by complaining
about a missing include. Running terminals are re-coloured in place over
their control sockets; a restart is not needed.
neovim: reads settings.json directly, since it neither watches the portal
nor keeps a socket open. Tokyo Night ships Day in the same family as
Moon, so light mode keeps the editor's identity instead of turning it
into a different-looking application. The existing readability overrides
were written against Moon and are now dark-only -- applied to Day they
would have put light grey on a light background, the same problem they
exist to fix, inverted. Light mode gets one override of its own:
tokyonight's shipped comment colour measures 2.54:1 against Day's
background, under the 3:1 floor for secondary text, so it is replaced
with 3.25:1 -- readable, still dimmer than Normal's 4.52:1.
An editor already open when the scheme flips re-applies on FocusGained,
which is cheap and fires exactly when the mismatch would be noticed.
Verified both directions: kitty re-coloured 4 live terminals, and neovim
starts as tokyonight-day with background=light and tokyonight-moon with
background=dark.
Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
Light is Tokyo Night Day, the palette's own light variant, rather than
one invented to merely not be dark. Both share the same hues at
different lightness, which is what lets the Prism signature survive the
switch: blue still leads into orchid, it is simply a darker blue on a
lighter ground.
Every colour token became a binding on one boolean, so flipping it
repaints the whole shell without any component needing to know it
happened. That only worked because nothing outside Theme.qml defines a
colour; the two places that did are fixed here.
Surface alphas differ by scheme. The 0.34 that reads as glass over a
dark desktop reads as haze over a light one, and text stops being
legible on it.
Two things that draw on this desktop do not read Panama's store: GTK
applications, which read gsettings, and the compositor, which draws
window borders. A toolbar or a border still wearing the other scheme is
more jarring than either scheme on its own, so ColorScheme pushes the
choice to both. It also pushes at startup, since a scheme chosen in a
previous session would otherwise be in effect only for the shell.
The unfocused window border follows too. It is a flat neutral, and a
dark neutral is invisible against a light desktop. The focused border is
the prism gradient and needs no variant.
The live preview follows the scheme as well. A preview that stayed dark
while the shell around it went light did not read as "your desktop", it
read as a screenshot of someone else's.
Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
Theme.qml was the largest remaining thing in this desktop that could
only be changed by editing a file, and typeface is the first thing
someone changes when they want a desktop to feel like theirs. Interface
font, icon font, and the base text size are settings now.
The two font choices are deliberately separate lists. Theme.fontMono is
used only to draw glyphs -- workspace pills, the status cluster, search
icons -- so a plain monospace family there replaces every icon in the
shell with tofu. The picker offers only Nerd Fonts for that slot and
says why.
Candidates are rendered in the family they name. A list of font names
set in the current font tells you nothing about what you are choosing.
The four type sizes derive from the base rather than being stored
separately, so the relationship between body, caption, heading and title
survives a change instead of four numbers drifting apart.
hypr/looks.lua reads the same key. Following the preference only on the
QML side would leave the compositor and the shell disagreeing about the
interface font, which nobody notices until a tooltip renders in a
different typeface.
Only a family this machine reports is accepted: the value reaches
hl.config as a string, and a settings file moved between machines will
name fonts that are not installed. A missing family is reported rather
than silently substituted by fontconfig.
Also collapses the wallpaper grid to two rows. Sixty tiles is two
screens of pictures on a page that also holds typography, window
geometry and effects -- everything below it was unreachable without
scrolling past all of them.
Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
Panama Settings shipped with Icon=preferences-system-symbolic, a
monochrome glyph drawn for 16px toolbar use. Beside full-colour
application icons in a 48px dock it reads as a missing icon rather than
a quiet one. It now has its own: a gear, because a settings icon has to
be recognisable before it is clever, rendered in the Prism gradient on
the dark tile so it belongs to this desktop.
An earlier attempt drew the gear as a ring with radial strokes; at dock
size the strokes merged into the ring and it read as an X. The shipped
version is a real toothed outline, checked at 48px rather than only at
128.
The dock also pinned GNOME Settings first. Panama now covers what GNOME
Settings did for this desktop and delegates the remainder to it by name,
so pinning the thing it delegates TO put the fallback in front of the
real one. GNOME Settings stays installed and searchable.
link-dotfiles installs icons alongside desktop entries, so this survives
a fresh setup rather than being a file that happens to exist here.
The new contract asserts every pinned application resolves to an
installed desktop entry. DockBody drops an unresolvable pin rather than
drawing a broken icon, which is right at runtime and invisible to debug:
a typo or a renamed desktop id just removes an icon with nothing logged.
It also cost me a false negative while writing it -- DesktopEntries
populates asynchronously, and asking too early reports every pin as
missing.
Also makes tests/quickshell/osd-ui-contract.sh executable. It was
committed mode 644, the only test in the suite that was, so the runner
could not invoke it. It passes.
Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
The last two values that could only be changed by editing a file.
Weather was pinned to hardcoded coordinates, so the card could not be
pointed anywhere else. It is a location search now, not latitude and
longitude fields: nobody knows their own coordinates, and a control that
demands them is one nobody uses. Open-Meteo's geocoding endpoint needs
no key, the same reason the forecast already uses them. Only the search
term leaves the machine -- the stored place name is a label -- and
coordinates are rounded to four decimals, far finer than a weather
reading resolves and coarse enough to keep a precise home location out
of the settings file.
The graphics readout was hardcoded to card1. This machine has two amdgpu
cards, discrete and integrated, so that was right only by luck, and the
path is meaningless on any other machine. GPUs are enumerated with a
readable name from lspci, since sysfs exposes only numeric ids, and the
picker appears only when there is more than one to choose between. A
stored path the machine does not have is refused and reported rather
than silently measuring nothing.
Also merges the per-application notification rules UI. Its three commits
were believed integrated but the page half was not actually in the tree:
main had the service side in Notifs.qml and zero references to
setAppRule in NotificationsPage. Ancestry is not content.
Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
Audited bar, dock, quick settings, date menu and Settings for three
things: a setting reachable in one UI but not another, a setting that
exists but is unreachable anywhere, and UI that states something false.
Night Light was fully exposed in Quick Settings and had no control
anywhere in Settings. It now has a card on Displays, where GNOME also
puts it, with on/off, schedule, times and temperature.
Adding those controls would have shipped the exact defect this audit
exists to find. NightLight declared enabled, temperature and automatic
as bindings on the store, but toggle() assigns to them, and an
assignment destroys a QML binding permanently -- so the service wrote to
the store and never read from it again. The Settings controls would have
written values the service ignored, while Quick Settings kept working.
It now follows the store. Every other service was swept for the same
pattern; this was the only one.
The night light schedule was two hardcoded literals, so the hours could
not be changed. They are schema keys now, with a row that renders 17.5
as "5:30 PM" and honours the 24-hour preference rather than showing a
decimal nobody reads as a time.
keyboardLayout was in the schema and read by input.lua but had no
control anywhere: configurable in principle, unreachable in practice. It
is surfaced on Input & Shortcuts as read-only, with the reason, because
it needs a compositor reload and a control implying instant apply would
be a smaller lie but still a lie.
Caffeine was a Quick Settings toggle mentioned only in a subtitle in
Settings. It has a real control now.
Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
Registers the components and schema key the codex agent needs for the
Sound page and per-application notification rules, so its branches
compile against a manifest that already holds them rather than each
carrying a conflicting edit to the same file.
An absent notification rule is permissive rather than denying: a newly
installed application must be able to notify without an entry being
written for it first.
Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
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
Resolution, refresh rate, scale, and rotation, applied through
hl.monitor{} and stored per output.
This is the only setting in Panama where a wrong value can leave the
user unable to SEE the screen well enough to undo it: a mode the panel
cannot show, or a scale that makes everything unreadable, is not
recoverable through the UI that caused it. So a change is never applied
irreversibly. It is applied, then reverted automatically after fifteen
seconds unless confirmed, and confirming is what writes it to the
settings store -- letting the countdown run leaves nothing behind.
The contract tests that property specifically: it applies a scale, waits
out the countdown, and asserts the display came back and that nothing
was stored. A regression there is not a broken feature, it is a user
staring at a blank monitor.
Modes are grouped by resolution with refresh rates beside them. The
panel reports 35, many differing only in refresh-rate rounding -- 60.00
and 59.94 -- which as a flat list of buttons is noise rather than
choice; equal rounded pairs collapse, leaving 21.
Only mode, scale, and transform are configurable. Colour management and
bit depth stay in monitors.lua because they carry a documented screencopy
tradeoff that a settings page cannot explain at the moment you would be
changing it.
Also replaces the display policy rows with the schema-bound ones, so the
page no longer restates labels that PreferenceSchema already holds.
Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L