Commit Graph
25 Commits
Author SHA1 Message Date
Gabriel Brown 0ca83f74c7 Harden Panama doctor probes 2026-08-18 09:00:35 -04:00
Gabriel Brown d58c431199 Add Panama system health diagnostics 2026-08-18 08:52:49 -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 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 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 71ee18048e Add a living Prism component gallery 2026-08-18 06:50:32 -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 de1b8a7673 Add private Home Assistant credentials settings 2026-08-18 06:14: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 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 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 375ecfcd95 Add application and autostart settings 2026-08-18 01:28:29 -04:00
Gabriel Brown 150f3cdb09 Make the Dock editable and add settings snapshots
The Dock's pinned applications were a sixteen-entry literal in
Settings.qml, so changing what sits in the Dock meant editing QML. They
are now an ordered list in the shared store, with move up, move down,
unpin, and a filtered picker for adding installed applications. Keeping
them in the shared store rather than a file of their own means they are
covered by Restore defaults like everything else.

This needed a "json" schema type for values the schema stores and resets
but does not validate field by field. It exists so structured settings
can live in the one file rather than growing a fourth preference store;
the owning service validates the contents.

Snapshots make the settings app safe to experiment with. The whole
configuration is one file, so a backup is a copy and a restore is an
overwrite, and restoring snapshots what it replaces so it is itself
undoable. A snapshot is validated as JSON before it can be restored over
a working configuration, and a name that is not a plain snapshot
filename from the backup directory is refused.

Snapshot names carry milliseconds. At one-second resolution a save
followed promptly by a restore produced the same filename twice, and the
restore's own safety snapshot overwrote the file it was about to read --
found by the contract, which restores immediately after saving.

Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
2026-08-18 00:59:08 -04:00
Gabriel Brown 2bc12e6022 Add wallpaper, power, date, accessibility, and real search
Continues the settings expansion toward replacing GNOME Settings for
everything Panama actually owns.

Wallpaper. A thumbnail grid rather than a path field: the value of this
setting is the picture, so typing a path to something you cannot see is
the worst version of it. Two things about hyprpaper 0.8 shaped this. Its
IPC is much smaller than older documentation suggests -- preload,
listloaded, unload, and reload all answer "invalid hyprpaper request",
so setting is a single call with no preload. And the "<empty>,<path>"
form that used to mean every output is silently ignored, so a wallpaper
set that way appears to succeed and never changes; outputs are walked
explicitly instead. hyprpaper.conf lives in the repo through the
~/.config/hypr symlink and so cannot hold machine state, which is why
the choice lives in the shared settings store and is re-applied at
startup.

Power & Lock. hypridle has no IPC for reconfiguration and its config is
hyprlang rather than the shared JSON, so scripts/panama-idle generates a
config from the settings store and restarts the daemon. The generated
file lives under XDG_STATE_HOME for the same symlink reason, with a
systemd drop-in pointing hypridle at it. Management is a real state and
the page says which one you are in rather than showing sliders that
quietly do nothing. Zero means never for all three timers, which a naive
template would render as "immediately".

Date & Time. Deliberately not stored in Panama's settings: the timezone
and network time belong to the machine and are shared with sessions that
never see this file. Storing a copy would create a second answer to a
question the system already answers. Reads and writes timedatectl
directly; a cancelled polkit prompt surfaces as an error rather than as
a value that appears to have been accepted.

Accessibility. Pointer size and text scale have to agree across three
consumers with no shared configuration -- the compositor, GTK, and the
shell -- so the store is the source of truth and the values are pushed
outward to gsettings and hyprctl setcursor.

Search now indexes the schema instead of the twelve page labels. "gaps",
"wallpaper", and "screenshot" previously found nothing on an app that
has all three, which is the clearest way a settings app feels smaller
than it is. Shortcuts are indexed by what they do. A contract asserts
every non-internal schema label is reachable, so a new setting cannot be
added in an undiscoverable state.

The GNOME delegation allow-list was widened to the panel names
gnome-control-center actually reports; the previous list contained
"users", which is not one of them and so opened nothing.

Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
2026-08-18 00:42:13 -04:00
Gabriel Brown ff67387c42 Add Home Assistant light catalog and dimming 2026-08-17 16:01:50 -04:00
Gabriel Brown 190e3200e0 Keep offline phone capabilities visible 2026-08-17 13:42:31 -04:00
Gabriel Brown 173c00fca7 Add the Panama Home Assistant bridge 2026-08-17 13:27:11 -04:00
Gabriel Brown 3a903934c0 Add the Panama KDE Connect bridge 2026-08-17 13:22:31 -04:00
Gabriel Brown d37d4a87de Add the EDS calendar bridge 2026-08-17 11:28:12 -04:00
Gabriel Brown 5248883e4b Build the Panama Hyprland desktop 2026-08-17 10:32:55 -04:00