Commit Graph
295 Commits
Author SHA1 Message Date
Gabriel Brown 0fd59c59e3 Answer GNOME's Search panel, and pin the settings navigation wiring
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
2026-08-18 08:38:54 -04:00
Gabriel Brown b1f9891849 Add multitasking controls, and a light theme for the launcher
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
2026-08-18 08:34:05 -04:00
Gabriel Brown 6026bf308b Add Region & Language, and answer "what am I running on" in About
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
2026-08-18 08:30:21 -04:00
Gabriel Brown 24d8bfd641 Plan Panama health and recovery 2026-08-18 08:28:20 -04:00
Gabriel Brown 14529c011f Add a Privacy & Security page
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
2026-08-18 08:20:04 -04:00
Gabriel Brown 7378c18ab9 Merge remote-tracking branch 'origin/fix/osd-ddc-brightness' 2026-08-18 08:16:49 -04:00
Gabriel Brown 9ce7040b91 Add a Mouse & Touchpad page and make keyboard layout editable
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
2026-08-18 08:16:10 -04:00
Gabriel Brown af0ba13573 Design Panama Health and Recovery 2026-08-18 08:15:51 -04:00
Gabriel Brown 09e5f1ad6b Fix external monitor brightness OSD 2026-08-18 08:15:01 -04:00
Gabriel Brown 7541a45e77 Add external monitor brightness over DDC/CI
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
2026-08-18 08:01:49 -04:00
Gabriel Brown 1a91d00f2c Stop two contracts from latching a broken desktop state
Both tests run against the live session and capture "what it was before"
so they can put it back. Neither checked that what they found was sane,
so one interrupted run poisoned every run after it -- and because each
subsequent run faithfully restored the bad value, the desktop stayed
broken while the failure looked like an ordinary flake.

displays-contract left the monitor at scale 1.25 after a failed revert.
The next run recorded 1.25 as the original and restored the desktop to
it. It now reads the shipped scale out of monitors.lua and refuses to
run when the live display disagrees. A failure to parse that value is
fatal rather than skipped, because silently skipping the check is how
the laundering happened in the first place.

focus-session-expiry kills and restarts the shell mid-session, so an
interrupted run leaves caffeine on with nothing left to turn it off. The
next `focus start` recorded "previously on", handed it back on expiry,
and failed the assertion that caffeine ends off -- identically, forever,
with the desktop unable to idle or lock the entire time. It now refuses
to start unless caffeine is already off, which is the only state in
which the test can tell "restored correctly" from "never released".

Both guards name the exact command to recover with. Verified each fires
on a dirty state and passes on a clean one; caffeine was found latched
on this machine and has been released.

Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
2026-08-18 07:48:02 -04:00
Gabriel Brown 19063a3e02 Carry the colour scheme into terminals and the editor
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
2026-08-18 07:47:51 -04:00
Gabriel Brown 5e6dc9e533 Release stale Caffeine inhibitors 2026-08-18 07:43:15 -04:00
Gabriel Brown ca64d4b9b6 Harden Panama launcher actions 2026-08-18 07:31:11 -04:00
Gabriel Brown b28edcd01f Add Panama commands to the launcher 2026-08-18 07:31:11 -04:00
Gabriel Brown 9c574f9eb7 Add a light mode that keeps the same identity
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
2026-08-18 07:20:35 -04:00
Gabriel Brown d1b2cd2083 Make typography choosable instead of hardcoded
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
2026-08-18 07:06:53 -04:00
Gabriel Brown 8c4dee0ca8 Give Settings its own icon and the first dock slot
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
2026-08-18 06:54:43 -04:00
Gabriel Brown 71ee18048e Add a living Prism component gallery 2026-08-18 06:50:32 -04:00
Gabriel Brown 668983e16d Add polished system feedback overlays 2026-08-18 06:46:11 -04:00
Gabriel Brown 17e23a6cf4 Merge remaining settings work
Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
2026-08-18 06:38:46 -04:00
Gabriel Brown d18fe51553 Make the weather location and graphics device choosable
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
2026-08-18 06:38:46 -04:00
Gabriel Brown 9f9515ffe0 Merge per-application notification rules
Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
2026-08-18 06:29:47 -04:00
Gabriel Brown 94353aa0bd Close the gaps the cross-UI audit found
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
2026-08-18 06:25:15 -04:00
Gabriel Brown 3a2295ff53 Make credential setup keyboard-safe and verifiable 2026-08-18 06:14:07 -04:00
Gabriel Brown de1b8a7673 Add private Home Assistant credentials settings 2026-08-18 06:14:07 -04:00
Gabriel Brown 39a26adcc2 Make Home light ordering explicit and durable 2026-08-18 06:14:07 -04:00
Gabriel Brown eff0bc44ff fix: resolve persisted notification app labels 2026-08-18 06:14:07 -04:00
Gabriel Brown c038c57278 fix: harden notification application rule integration 2026-08-18 06:14:07 -04:00
Gabriel Brown 420b7aa1a6 feat: add notification application rules 2026-08-18 06:14:07 -04:00
Gabriel Brown f22e405b50 fix: resolve persisted notification app labels 2026-08-18 05:57:10 -04:00
Gabriel Brown 3cfa592db9 Build complete PipeWire sound settings 2026-08-18 05:54:14 -04:00
Gabriel Brown 81096e2d95 Declare the Sound and notification-rule manifest entries
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
2026-08-18 05:54:07 -04:00
Gabriel Brown c415c4f176 fix: harden notification application rule integration 2026-08-18 05:51:32 -04:00
Gabriel Brown 86742da63d Merge Wi-Fi and Bluetooth settings
Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
2026-08-18 05:50:07 -04:00
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
Gabriel Brown c087998710 feat: add notification application rules 2026-08-18 05:38:39 -04:00
Gabriel Brown b9800bfbab Scope compositor isolation to reset tests 2026-08-18 03:25:39 -04:00
Gabriel Brown 7b8270fdd6 Keep Settings recovery isolated and display-safe 2026-08-18 03:19:45 -04:00
Gabriel Brown 6d3f888784 Protect live state during restore and reset 2026-08-18 03:10:31 -04:00
Gabriel Brown c2cd79b547 Serialize display recovery checks 2026-08-18 02:55:43 -04:00
Gabriel Brown 0a18290137 Close display verification races 2026-08-18 02:55:43 -04:00
Gabriel Brown efc53435a2 Verify display restoration and expose outputs 2026-08-18 02:55:43 -04:00
Gabriel Brown 67674d297e Polish display confirmation state 2026-08-18 02:55:43 -04:00
Gabriel Brown 2d4b126f14 Harden display apply and recovery 2026-08-18 02:55:43 -04:00
Gabriel Brown 5671324eb2 Make displays configurable, with a revert countdown
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
2026-08-18 02:55:43 -04:00
Gabriel Brown 83391e0453 Make settings restore crash-safe 2026-08-18 02:22:23 -04:00
Gabriel Brown 172b099a04 Complete settings snapshot restoration 2026-08-18 02:22:23 -04:00
Gabriel Brown cae72b5179 Make Settings sidebar content scrollable 2026-08-18 01:54:00 -04:00
Gabriel Brown b3b8e0d66d Prevent shortcut reset collisions 2026-08-18 01:47:51 -04:00