9fbbdd902b91613672dc42faeff0b7f0ccb030f7
25
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9fbbdd902b |
Answer "what can I press" in one keypress
The Shortcuts settings page answers "how do I change this", which is worth opening a window for. This answers the other question, the one you have with your hands already on the keyboard, so it is an overlay on SUPER + / and the same key closes it. It reads Keybinds.grouped() rather than a written-down list, so a shortcut rebound in Settings shows its new chord here with nothing kept in sync. A cheatsheet that lies is worse than none: it gets consulted exactly when somebody does not already know. Three columns, balanced by how many shortcuts each category holds. The first attempt used a Flow, which wraps into as many columns as it likes and made 120 binds across six uneven categories unreadable; it also sized the card from a child that filled it, which is a circular binding and produced a card taller than the display with its contents running off the bottom. Both were found by looking at it rather than by a test, which is the argument for looking at it. Fixes a real bug on the way past: luaChord and formatChord appended the key unconditionally, so the window switcher's modifier-only release bind became "SUPER + " with a dangling separator. That matched neither the chord keybinds.lua binds nor the one an override is keyed by, so that bind could never be rebound and had no category -- it was sitting in a seventh group of its own, which is how it was noticed. |
||
|
|
bc6d63b70f |
Let the idle timings know whether you are plugged in
An idle screen costs a screen on wall power and the rest of your afternoon on battery, so they should not be the same number. hypridle has no concept of a power source -- one config, one set of timeouts -- so rather than maintaining two configs and swapping them, panama-idle builds the single config from whichever key set applies, and IdleLock rebuilds it when the charger comes or goes. That runs through the same 400ms debounce a settings change uses, so a loose charger cannot restart hypridle in a loop. The battery keys fall back to their AC counterparts rather than to the schema defaults. Without that, unplugging would silently override a deliberately long timing with a shipped short one, which is the kind of thing you would notice only by losing work. A machine with no battery reads none of it and generates exactly what it generated before. The contract pins that alongside the two obvious directions, and was checked by sabotaging the detection to confirm it fails rather than passing vacuously. |
||
|
|
3c359f3f7e |
Notice the battery, and the machine it is or is not in
Panama had no idea whether it was running on a laptop. No upower, no battery, no lid, no AC: hypridle.conf says "This is a desktop" in its own header, and that was true of the code as well as the machine. panama-hw answers hardware questions one at a time, exits 0 or 1, and prints nothing, so scripts, services and contracts all ask the same way. The definition the rest of the laptop work hangs on is one line: clamshell is lid-closed AND an external monitor. A machine with no mains supply at all reports as being on wall power, because a desktop cannot run out of it. The battery service follows Vitals: sysfs through FileView, an availability flag, and no subprocess on the timer. Globbing is the one thing QML cannot do -- a battery is BAT0 or BAT1 or CMB0, mains is AC or ADP1 or ACAD -- so panama-battery resolves the names once and the shell reads the files directly after. Nothing falls back to a plausible zero: a desktop shows no indicator, no card, and no charge limit control where the firmware has no ceiling. Also repairs two contracts that were already failing and had not been noticed, because only the full suite runs them. The dependency scanner treated line-initial variable assignments, case labels, comments and heredoc bodies as commands, and `count`, `host`, `cancel` and `import` are all real binaries on Fedora, so `command -v` could not filter them out. It now drops comments and heredoc bodies and requires a command to be followed by whitespace. Verified it still catches a genuinely undeclared dependency rather than passing quietly. The launcher command contract had not been told about the fourteen commands added earlier today. |
||
|
|
c02329ac3c |
Copy a password without leaving a trace of it
The launcher's Copy Password command, built alone and last as the plan required, because every line of it is the security design: the secret travels rbw to wl-copy through a pipe -- never argv, never a file -- and the copy carries wl-clipboard's --sensitive hint, which vicinae's clipboard history documents it ignores. That claim was not taken on faith: a plain probe landed in the live history database and a sensitive one did not, before any of this was written. A transient timer clears the clipboard after thirty seconds. An unconfigured rbw gets a setup message; a vault that locks between list and get gets an honest failure instead of an empty copy claiming success. rbw joins desktop-packages, and the contract pins the whole journey with a stub vault, including that the secret never appears on a command line. |
||
|
|
4e978bf3b7 |
Teach the launcher what an operating system knows
The OS-parity batch from the vicinae plan, tasks 1 through 7. The audit came back better than the plan guessed: the calculator already links libqalculate, the built-in file index answers in under 100ms across all of home, quicklinks and snippets ship as built-in stores -- so zero new packages, and `vicinae dmenu` replaces the planned compiled extension outright. What was missing gets built: a power menu (lock, suspend, log out through uwsm, restart, power off), reminders as transient systemd timers with a pick-to-cancel list, a color picker over hyprpicker, and dmenu pick-lists for window switching, force quit, kill process, SSH hosts, and recent files -- all through one panama-pick helper. The launcher commands contract exercises the reminder parsing and every pick-list against stubs, including killing its own sacrificial sleep. |
||
|
|
185d7edaa5 |
Open the windows you open together, together
Arrange the desktop, run "Save Layout as Project" from the launcher, name it. "Open Project" lays it out again on free workspaces, so it never lands on top of what you are already doing. Saved layouts are listed on the Desktop settings page, which is where they are removed. Recorded rather than written by hand, and a terminal's directory is most of why it is worth having: without it a project opens three terminals in your home folder and you change directory three times. This machine had two terminals in the same project when it was written, and reopening there is the difference between a layout and a working desktop. Four things had to be found by running it, none of which reading would have shown. A terminal's directory is not the terminal's working directory -- that is where it was launched from. The shell inside it is what has been cd'd. Reading the wrong one looked correct for exactly as long as the terminals under test had been started from the right place, which they had. gtk-launch cannot place a window. It activates over D-Bus, so the process Hyprland started exits and a [workspace N silent] rule has nothing left to apply to; Nautilus landed on whatever workspace was in front of you. The Exec line from the desktop entry is run directly instead. But DBusActivatable applications do the same thing regardless, so the window is found afterwards and moved by address -- which also means a window that never appeared is reported rather than assumed. /proc/PID/task/PID/children is a file of pids, not a directory. Listing it as one always raised, so the fast path was never once taken and everything went through pgrep. And kitty's --directory needs an equals sign or the short -d; the space-separated long form is accepted and silently ignored. The contract exercises a save and open round trip against a stubbed compositor, and reads a terminal's directory out of a real process tree rather than grepping the source for a shell name -- an earlier version passed against a helper that had been changed back, because the constant was still there. Claude-Session: https://claude.ai/code/session_01Q84axqUE5inJhf5Jz9CFy1 |
||
|
|
4c77bc2f61 |
Decide whether the other screens join in on workspaces
GNOME's Multitasking panel asked one workspace question worth reproducing, and it is not which workspace goes on which screen. It is whether the second screen participates at all: workspaces on the primary display only, or each screen with its own. Ten rows of per-workspace assignment would be more powerful and worse. Off is Hyprland's own behaviour and emits nothing. On pins workspaces 1 to 10 -- however many ALT+1..ALT+0 actually reach, read from keybinds.lua rather than written down twice -- to whichever output is recorded as primary. With no primary recorded, nothing is pinned: guessing one would move every workspace onto whichever output happened to sort first, and this machine is in exactly that state. Applying is a reload, which is the part that shaped the design. Hyprland reads workspace rules at config time and will not remove one afterwards -- a rule written with an empty monitor keeps its old binding, which was checked rather than assumed. Only a reload clears them, so the config is the only honest source and the page cannot pretend a change has landed before one happens. Hence a service that reads `hyprctl workspacerules` back rather than inferring success from having written the preference, and a Reload row that exists only while the two disagree. Verified end to end against the live compositor and put back: off emits nothing, on emits ten rules naming the primary, and turning it off clears them. The settings file came back byte-identical. Claude-Session: https://claude.ai/code/session_01Q84axqUE5inJhf5Jz9CFy1 |
||
|
|
9092a80f66 |
Search from the launcher, and give the touchpad something to do
Four things a Hyprland desktop can do that this one was not. Searching from the launcher needed no launcher work at all: Vicinae already models it, so this is a script command with one percent-encoded argument. Make it the fallback command and anything typed that matches nothing else offers to search it. Bangs come free -- they are a property of where the query is sent, not of the launcher -- so !yt reaches YouTube without a line of bang parsing. Suggestions could not be a script command. They need a view that reacts as you type, which is an extension: TypeScript, compiled, querying the same endpoint Firefox's address bar uses. It debounces, and aborts the request in flight on every keystroke -- typing is faster than the network, and an older answer landing after a newer one leaves the list describing a query that is no longer on screen. A bang skips suggestions entirely, because Google has no useful guesses about "!yt". The engine is now written down twice, once in each. The contract pins that they agree, since searching from the fallback and searching from the suggestions reaching different places is the kind of wrong that looks fine. Gestures mirror GNOME: three fingers sideways for workspaces, up for the overview, down to dismiss it. Open and close rather than toggle both ways -- toggling means swiping up from an open overview closes it, which is not what the fingers meant. Hyprland reads gesture registrations at startup so they cannot be a setting, but distance and direction can be, and are. Window swallowing is off by default and a preference like every other misc setting here. A terminal that vanishes when you did not ask for it is confusing rather than broken, which is worse. Claude-Session: https://claude.ai/code/session_01Q84axqUE5inJhf5Jz9CFy1 |
||
|
|
e1faaf7a76 |
Drop the extension, and give the test suite a front door
Phase 6, the last of the fresh-install spec. 159 scripts lose their .sh: 110 contracts, 47 Vicinae commands, 2 compositor contracts. A shebang and the executable bit already select the interpreter. The extension only ever added something that had to stay in sync, and the rename proved the point twice over in the space of an hour. The spec's stated risk was Vicinae's script discovery. One script was renamed and reloaded on its own before the other 46 followed; it came back as scripts:panama.capture and all 47 resolve. What the probe turned up instead is that the extension was never only a filename: Vicinae's command IDs embed it, so every ID changed. Nothing in this repository refers to them, so nothing breaks. The only trace is Vicinae's metadata.json, whose visited map had two Panama entries that are now orphaned -- two commands lost their usage ranking and will earn it back. Worth knowing before anyone renames these again on a machine that has a keybind pointing at one. Rewriting the references by exact filename missed two things it structurally could not see: a name built from a variable, settings-$page.sh, and a glob, -name '*.sh'. Both were in the contract that counts the generated commands, which promptly reported 47 expected and 0 found. The mechanical part of a rename is the part that looks finished. The three subcommands. panama doctor fronts a health check that already existed and already ran at the end of every install but could not be reached from a terminal. panama upgrade re-runs the installer from anywhere. panama test runs the suite, which had no entry point at all -- 121 files that were the main safety net in this repository and were invisible in it. Writing that runner found three tests nothing was running. calendar_agenda_bridge_test, home_assistant_bridge_test and kdeconnect_bridge_test are unittest suites without the executable bit, so no contract invoked them and the first draft of the runner skipped them silently. All three pass, and have passed unobserved for weeks. The runner collects *_test.py as well now, because a runner with a blind spot is worse than no runner for the same reason a dependency checker with one is: it reports PASS. Six worktrees pruned. Each was re-checked rather than trusted to the spec's list, and two needed it: panama-commands is not on feat/panama-commands but on feat/gnome-tweaks-parity, and fix/panama-displays-review reads [ahead 3] -- ahead of its remote, not of main, with every commit patch-equivalent to landed work. roadmap-completion stays; it has five commits that are genuinely unlanded. The branches are left alone: pruning a worktree costs nothing, deleting a branch is a decision. 121 contracts pass. Claude-Session: https://claude.ai/code/session_01NvgBuSWB5sE43yWmg21ozj |
||
|
|
3b01f1e020 |
Let the Dock choose an edge, choose its screens, and be dragged into order
Three things that were parked, and the reasons they were parked turned out to be the useful part of doing them. The Dock can sit on the left or the right as well as the bottom. Everything that assumed the bottom edge is now asked which edge it is on: the anchors, the axis that gets an implicit size, the sliver of input region that survives hiding, the direction the body slides away in, and which side a tooltip opens towards. The body was a Row and is a Grid, because one declaration then serves both orientations -- Row and Column would each need their own children, and the cross-axis anchors that centre items in a Row are the wrong axis in a Column. Bottom is unchanged in every particular, and the settings default to it, so a hot reload in the middle of this work left the running dock exactly where it was. One bug worth recording because static review would never have found it: a dock spans the edge it lives on, which means anchoring BOTH ends of that edge. The first side dock anchored top and left only, was free to collapse to its implicit height, and came out one pixel tall. It parsed, it loaded, and it rendered nothing. The contract measures the geometry rather than reading the source for that reason, and was verified by putting the single-ended anchor back. Per-screen is a list of names where empty means every screen, because a list is what goes stale when a display is unplugged and "all" should not be spelled as one. Turning off the last screen collapses to "all" rather than leaving no dock anywhere and no obvious way back. Pins can be dragged by a grip. The objection this file recorded for a long time was real -- dragging inside a Flickable inside a scrolling page fails in a way that reads as breakage -- and the answer is preventStealing on the grip, so the page cannot claim a gesture that started there. The arrow buttons stay: they are the keyboard-reachable path and a grip is not. The order is held locally during the drag and written once on release, rather than rewriting settings.json for every slot crossed. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L |
||
|
|
52e2a83a78 |
Add an SSH Keys page, and refuse the one control that would lie
The page shows which keys exist, what the agent is holding, and the hosts this machine has met, with a two-press forget for a host whose key has changed. Nothing here reads private key material. Fingerprints and comments come from the .pub file, and "does this key need a passphrase" is answered by asking ssh-keygen to derive the public half with an empty one -- it succeeds for an unencrypted key and fails for an encrypted one, and either way the only thing it can emit is public. The contract checks that against the payload that actually reaches the page rather than against the source, because what the code intends and what it ships are different claims. Unloading a key from the agent is refused, with its reason. On this desktop `ssh-add -d` prints "Identity removed" and the key is still offered a second later: gnome-keyring's agent lists every key it finds in ~/.ssh, so a removed one comes straight back off disk. That was measured rather than assumed -- a plain ssh-agent removes durably, this one does not -- and a button reporting success while changing nothing is worse than no button. The page says so and names the thing that does work: move the file out of ~/.ssh. SSH_AUTH_SOCK is not set in a normal shell here, so a naive check reports "no agent" while one is plainly running. The helper falls back to the keyring socket, and an agent started by hand still wins. That gap is the same one that made reaching these servers awkward in the first place. Generating a key is deliberately absent. A passphrase cannot reach ssh-keygen without going somewhere it should not -- -N puts it in argv, which every process on the machine can read -- and driving the prompt over a pty did not work. Offering to generate an unencrypted key instead would be a downgrade dressed as a feature, so the page does not offer to generate at all. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L |
||
|
|
6dc606b872 |
Give focus modes conditions rather than alarms, and let Gaming hand over
A mode is on because something is true right now: a game is running, a window is fullscreen on a given display, a workspace is focused, the clock is inside a window. That is asked again rather than fired once, and it is the whole reason schedules could be included here without the usual failure modes. A machine asleep at 23:30, rebooted at 02:00, or opened at 08:00 into a window that has already passed all reach the right answer by being asked again; an alarm gets all three wrong. The midnight-crossing rule is the part worth being careful about: a window belongs to the day it STARTS on, so a Friday-only 23:30-07:00 covers Saturday morning and must not cover Saturday night. That arithmetic was tested as pure logic before anything was built on it, including every malformed input failing closed -- silencing someone because a time string was wrong is the worst way this could fail. This does not take over the manual timed session. FocusSession already owns that, with its capsule, shortcut, Quick Settings entry and contracts, so modes defer entirely while one runs. Two writers of Do Not Disturb would each restore whatever the other happened to leave behind. Gaming hands over rather than being duplicated. The hook was silencing notifications itself, which would have made exactly those two owners -- and Gaming.active only polls while its settings page is open, so a mode could not have seen a game reliably in any case. The hook reports the game over IPC now and the mode decides what that means, the Gaming page points at it, and gamingSilenceNotifications is retired from the schema, since a setting nothing reads is the dead row this work keeps removing. Sleep ships disabled. A desktop that starts silencing someone on first boot has overstepped, whatever the default hour. Three contracts moved with it. gaming-contract asserted the hook uses setDnd, which was right before and wrong now; the shell-side assertions that setDnd and dndState exist stay, because a toggle would flip an already-silent machine back on. The new contract is proven to fail by breaking the midnight rule and by letting modes run alongside a manual session. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L |
||
|
|
32fab59d24 |
Let a sound device be heard, and the Dock's icons be sized
Nine outputs named after their chipsets cannot be told apart by reading, so each one gets a Test button that plays a short sample out of that device. Targeted by node name rather than by making it the default first, because finding out which is which should not move where everything else is playing. That belongs in its own service rather than in AudioDevices. sound-page-contract forbids Process, pactl and wpctl in the files that own device state, and it is right to: shelling out there races the PipeWire service that owns those same objects. Playback is a different thing -- pw-play opens its own stream and mutates no device, so there is nothing to race -- but the rule's letter covered it, and weakening a guard to fit a new case is how guards stop meaning anything. SoundTest exists so AudioDevices stays native bindings only. Worth recording next to the call: pw-play falls back to the default output for a target it cannot find, rather than failing. A stale node name would play from the wrong device and look exactly like a successful test, which is why the name is taken straight from the live node. The Dock's icon size was a constant in Theme. It goes through the preference schema like everything else, so validation, search, the generated docs and the write sweep all pick it up without being told about it separately -- and two contracts duly failed until docs/settings.md and the per-page commands were regenerated. Dock position is deliberately not here. It is not a setting but a rework: the dock is anchored bottom, and the reveal strip, tooltip placement, intellihide and the qs-dock rule in hypr/rules.lua all assume that. Doing it properly means changing compositor rules on a machine somebody uses daily, which is not something to start as a side effect of adding a slider. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L |
||
|
|
ac5e6e2130 |
Add a Containers page, grouped by project and led by what is exposed
Every container on this machine is created by rootless podman-compose and
labelled with the project it belongs to, so the grouping is read from the
labels rather than invented. State then decides prominence within that
grouping -- running containers get rows, stopped ones collapse to a line --
which is why neither axis had to be chosen over the other.
Acting on a stack uses plain podman over the labelled set, never
`podman-compose down`, which would remove containers this shell did not
create. The compose file is the source of truth for what exists and belongs
to the repository. Nothing here needs privilege.
The findings on top are the crossing the Firewall page reports, seen from the
side that can close it: the firewall knows only that something is listening,
while this page knows which container, which compose file, and which token is
missing from it. So `bind-local` prepends a loopback address and leaves the
line byte-for-byte -- variables, quoting and style intact -- then re-parses and
rolls back unless exactly those ports moved. It refuses anything ambiguous
rather than guessing. Rewriting the mapping to the port podman reports today
would have deleted the ${POSTGRES_PORT} indirection that makes it
configurable at all.
Unused volumes are read from podman's own dangling filter. The first version
used MountCount, which is a runtime lock counter and not a usage signal: it
reads zero for a volume a running container has mounted this second, so
"remove unused volumes" offered to delete the live Command Center database.
The cross-check against `podman system df` is what exposed it. The contract
reintroduces that bug deliberately and fails if the guard does not catch it,
because a guard nobody has seen fail proves nothing.
Every mutation in the contract runs against a stubbed podman. Nothing in the
suite starts, stops or removes a real container, image or volume.
Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
|
||
|
|
fd99569666 |
Add a Firewall page, led by what is actually reachable
Listing zones and services is what firewall-cmd already does. The question it does not answer needs both halves at once: a port is reachable only when something is LISTENING on a network address AND the firewall permits it. On this machine that crossing is the whole story. The rules look unremarkable -- one zone, three services, a port range -- and what they mean is that PostgreSQL and Redis, published by rootless containers on every interface, are reachable by anyone on the network. Neither half says that alone, which is exactly how a tidy rules list coexists with an open database. Nothing was misconfigured: Fedora's default zone met podman's default publish behaviour. Ephemeral client sockets are excluded. A browser's outbound UDP port is indistinguishable from a service in ss, and listing twenty of them buried the two rows that mattered. Closing the port range names what it would cut off, by service, before doing it, and removing ssh says so when someone is connected over it. Rich rules are shown and never edited: a syntax is not a setting, but hiding it would misrepresent the configuration. The contract needed a recorded firewall, and the reason is worth keeping. The rule this page exists for cannot be tested against this machine -- its zone permits everything above 1024, so "listening" and "listening and permitted" give identical answers, and a blocked listener needs a port below 1024, which needs root. With the crossing deleted, the contract passed. It now runs against a fixture where two listeners are blocked, and catches it. Also here: polkit response files are written 0600 rather than at the default mask, the agent sweeps requests left by an instance that did not exit cleanly, and the write sweep waits for its harness to be ready instead of reporting the startup race as settings that failed. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L |
||
|
|
4cbe3b882a |
Add a Gaming page, and let the desktop react to games
Live first, because unlike every other page here this one has a live dimension: card temperature, power draw, whether Game Mode actually engaged. It polls only while it is open, since a settings page nobody is looking at has no business waking the CPU. The part that makes it Panama's page rather than a gamemode config editor is the hook. gamemode runs a script when a game asks for it and another when the game exits, so the power profile switches to performance and notifications go quiet for exactly the duration of a game -- and afterwards both go back to what they WERE, not to a default. A Do Not Disturb someone set by hand survives a game; a power profile someone chose is restored rather than replaced. Verified against real gamemode activation, not merely by calling the hook. Two things the page reports rather than hides. Game Mode's headline trick is switching the CPU governor to performance, and this machine already runs performance, so it says so instead of implying it helps. And Proton builds are listed but never chosen: Steam picks the runtime per game, and a control here would claim an authority this page does not have. The hook first called a notifications function that did not exist, and the one that did was a TOGGLE -- the wrong primitive entirely, since toggling at game start would unsilence notifications that were already silent. The shell gained an explicit setter and reader. search-routing-contract kept its own hand-written list of every page, which made adding one fail as "not a known page" -- a sixth place to register a page and a sixth chance to forget. It now derives the mapping from the shell, which already knows it. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L |
||
|
|
8f0fe23377 |
Add Software Update, across packages, applications and firmware
Three sources that fail independently, so they are counted and applied separately: a flatpak mirror being down says nothing about whether a kernel security fix is waiting. Blending them into one number would hide exactly the case that matters. Checking costs about nine seconds, which is too long to spend every time a page opens, so the page opens on the last result and says when it was taken. A first visit with nothing cached goes and finds out rather than showing a confident "up to date" it has no basis for. Installing packages takes a snapshot first, named after what is about to happen, so Snapshots shows "before 32 package updates" rather than a timestamp. Best effort: a machine without snapper still updates, because an update that refuses to run when a nicety fails would be worse than one without a restore point. Automatic updates cover applications only, through a Panama-owned user timer running daily with a randomized delay. Packages still ask, and dnf-automatic is reported as absent rather than offered, because installing software is not a settings action. Health gained a check, and that is where the bug was: it first returned status "degraded", which is not in the doctor's vocabulary of ok, warning, error and unconfigured. It was counted as nothing at all while the summary still said healthy -- the same silent no-op this codebase keeps relearning. A contract now asserts every status a check can return is one the doctor counts, and the doctor's own contract knows about the new check rather than failing on its arrival. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L |
||
|
|
6997dd535f |
Add high contrast, and make remote desktop configurable
Two of the three panels still handed to GNOME, having actually checked each rather than repeating that they were not worth owning. Universal Access turned out to be mostly ours already: the magnifier, pointer size, text scale, motion and dimming were all present. High contrast was the real gap. It reaches GTK4 applications through the desktop portal, which republishes GNOME's accessibility setting as org.freedesktop.appearance contrast -- so no high-contrast theme is involved, and none is installed here. Verified end to end: committing the preference drove gsettings and the portal reported contrast 1. Sticky, slow and bounce keys stay absent. There is no Wayland or Hyprland implementation, and the compositor would store the XKB option while nothing ever acted on it. Remote desktop gained port, view-only, and clearing stored credentials. SETTING credentials opens a terminal running grdctl, which prompts for the password itself. That is not a hand-off for lack of effort: grdctl takes the password on a terminal and core-dumps without one, and the only alternative -- passing it as an argument -- would publish it through /proc to every process on this machine. Typed into grdctl directly it never passes through Panama, and a contract now fails if it ever appears on a command line. Color stays with GNOME, and not for lack of effort either. colord runs here with seven profiles and zero devices registered, because the daemons that register displays do not run under this session, and Hyprland exposes no ICC, gamma, or color-management option at all. A Color page could import a profile, attach it to nothing, and change nothing -- the same failure refused for rollback and printer drivers. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L |
||
|
|
1f40f8e136 |
Add a snapshot manager, and start covering home
The machine already had snapper running hourly on btrfs, so the tool was never missing. What was missing is that snapper's only configuration covered / -- and /home is a separate subvolume with no configuration at all. Six hundred and forty-three snapshots existed and not one of them contained a document. Anyone reaching for file history would have found their system and none of their files. /home now has a configuration on the same hourly timeline, with deliberately conservative retention: Steam's 1.2 TB lives on that subvolume and churns on every game update, so keeping five hourly and seven daily bounds what those updates can pin. Per volume, because on this machine "one is covered and the important one is not" was the news, and a timeline opening on system snapshots would have buried it. Inside a volume the timeline is the familiar view: points in time, newest first, each openable as a folder tree to take a file out of. Restoring sets the current version aside as .before-restore-N rather than overwriting it. A restore that destroys the thing you were about to compare against is how someone loses the work they were trying to save. Rollback is deliberately absent. snapper's rollback changes the btrfs default subvolume, and this system's fstab pins subvol= explicitly, which overrides it -- so a rollback would report success and change nothing after a reboot. A recovery feature that silently does nothing is worse than not having one, and making it work means editing fstab and the bootloader, whose failure cannot be repaired from inside the desktop. Per-snapshot size is reported as not measured, because measuring it needs btrfs quotas that cost performance on every write. Free space is shown instead, which is the number that decides anything. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L |
||
|
|
e9d567aa72 |
Own printers, driverless only
The fourth panel this desktop handed to GNOME Settings, and the last one worth owning. Driverless only, deliberately. Adding a printer that describes its own capabilities over IPP is supported; choosing a PPD or fetching a vendor driver is not, and the page says so rather than pretending. That restraint is the whole design: a wrong driver produces a printer that accepts jobs, reports success, and prints nothing, which is the worst failure this page could ship because it looks like it worked. A printer old enough to need a PPD stays a job for the system printer tool. Printers and the queue are separate cards because they answer separate questions. Which printers exist is one; where a document went is the other, and it is the one that actually brings someone here -- so the queue is a single list across every printer. Device URIs are validated by scheme before reaching CUPS, whose backends run as root. file: and pipe: do not lead to a printer and are refused here rather than further down. This machine has no printer, so the page was built against a temporary CUPS queue that was created, exercised through the service, and removed; the service was confirmed to observe the removal rather than merely perform it. Discovery and the driverless add path are verified by their refusals rather than against hardware. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L |
||
|
|
a23b42841a |
Own user accounts and sharing
Two of the panels this desktop still handed to GNOME Settings. Users manages the account through accountsservice -- the same daemon GNOME's panel drives, so a name or picture set here is what the login screen and lock screen read. Name, picture, account type, password, automatic login, and adding or removing other accounts. Every change is authorized by polkit through the agent this session already runs; a dismissed prompt is a normal outcome and says so. A new password is read from the helper's stdin, hashed by openssl reading its own stdin, and handed over D-Bus from inside that process. It is never an argument: argv is world-readable through /proc, so a password passed that way is published to every process on the machine. Removing an account takes two presses and says it destroys their files; the last administrator cannot be removed or demoted, because a machine nobody can administer is not a state to offer. Sharing reports what is actually true, including "the software for this is not installed" -- the honest answer for Samba here, and the case the panel it replaces shows as a switch that does nothing. Password sign-in is reported from sshd's configuration rather than assumed: claiming "keys only" when the file is silent would state a security property that cannot be backed up. The Control Center now draws the account's real picture and name. A generic glyph sat there while a real avatar was already set, which made the desktop look like it did not know whose it was. Also here: the KDE Connect contract no longer requires a phone to be awake. kdeconnectd drops its device objects for a phone it has not seen recently while the pairing survives in its config, so demanding one failed whenever the phone was off. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L |
||
|
|
99433c0e8e |
Add a Storage page
Nothing showed what was using the drive, and removable media was handled by a tray helper with no surface in Settings at all. One scroll rather than tabs: space above, the device below. Every other settings page is a scrolling card stack, and a tab would not be deep-linkable from the launcher command or from search. Three things the page has to get right, each now pinned by a contract, because each is a way it could quietly lie. / and /home are one btrfs filesystem sharing one pool of free space, and a page that copies df shows double the free space that exists. zram is a block device and is not storage; counting it as a drive overstates this machine by 8 GB. Unmount and eject refuse anything not on a removable drive, because the UI is what asks and a UI can be wrong. The cheap read -- layout, usage, health -- runs when the page opens, at around 90ms. Measuring what is filling the drive means walking every file, so it happens on request and says so rather than showing an empty list that reads as "nothing here". Partitioning and formatting are deliberately absent. A settings pane is the wrong place to put erasing a disk two clicks deep; the page opens GNOME Disks for that. Adding the page found a fourth hard-coded page list in ShellState. A page missing from it does not error -- openSettings() falls back to "home", so the launcher opens the wrong page and logs nothing. A registry contract now holds the three lists together. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L |
||
|
|
a68e4f6dcd |
Make every settings page reachable from the launcher
Settings had a search index and the launcher had script commands, and neither knew about the other: finding a setting meant opening Settings first and searching there. One command per page is now generated from the same sources the in-app search reads -- the sidebar page list, the group routing, and the schema labels -- so a setting searchable inside Settings is searchable from the launcher without a second list to maintain. Generating it surfaced a gap in the in-app search as well. Six pages had no vocabulary at all, because their contents come from the system rather than our schema, so searching "volume" found nothing in Settings either. They now carry entries, which improves both surfaces. The product prefix is dropped from every hand-written command title. The generated ones are qualified "Settings: <page>" instead: a bare page label collides with the feature of the same name, and two commands sharing a title are indistinguishable in a launcher. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L |
||
|
|
d855a26bd9 | Add quiet System Health entry points | ||
|
|
b28edcd01f | Add Panama commands to the launcher |