Conversation
| function isMullvadPeer(peer) { | ||
| var hostName = String((peer && peer.HostName) || "").toLowerCase() | ||
| var dnsName = cleanDnsName((peer && peer.DNSName) || "").toLowerCase() | ||
| return dnsName.indexOf(".mullvad.ts.net") !== -1 || hostName.indexOf(".mullvad.ts.net") !== -1 |
| function isMullvadPeer(peer) { | ||
| var hostName = String((peer && peer.HostName) || "").toLowerCase() | ||
| var dnsName = cleanDnsName((peer && peer.DNSName) || "").toLowerCase() | ||
| return dnsName.indexOf(".mullvad.ts.net") !== -1 || hostName.indexOf(".mullvad.ts.net") !== -1 |
| var country = sliceTableColumn(line, countryStart, cityStart) | ||
| var city = sliceTableColumn(line, cityStart, statusStart) | ||
| var status = sliceTableColumn(line, statusStart, -1) | ||
| if (host.indexOf(".mullvad.ts.net") === -1) continue |
|
Great work!!! SUPER + ALT + [ / ] Can we have a different key to / as in german / swiss keyboards, this is available via shift. So it would be SUPER+ALT+SHIFT+7. Usually having / and \ are not working keyboard shortcuts and need manual fixing. |
|
BTW: I can suggest alternatives and provide an MR if we're open to this change incl. past shortcuts... |
|
I just YOLO'd my 3.8... Workin like a boss. Nothing but what was expecting and a few pleasant surprises. Much appreciation to all of ya'll who made this happen! |
|
Running Quattro as my daily driver (upgraded an existing install, large 4K display (LG 48GQ900-B 48” OLED) at scale 1). The unified text-size knob is a great idea, but it hard-links three surfaces that don't always want to move together, and there's no way to express "just the shell" for example. Concrete case: I wanted the shell components (bar, menus, notifications) larger. Moving the display-panel slider did that, but it also:
There's also a small legibility trap in the mapping: because of the 12px→9pt anchor, the number you pick is never the number that lands in your terminal — e.g. the slider stop labeled "16" writes 12pt into terminal configs. The three legs are already independently settable underneath ([font] base-size in ~/.config/omarchy/shell.toml, gsettings, terminal configs), so I got what I wanted manually. But that means the knob is unusable for anyone tuning per-surface, one touch of the slider re-links everything. Proposal: scope flags on omarchy-display-text-size, something like --shell, --gtk, --terminals, defaulting to all three so the one-knob behavior is unchanged. The panel slider keeps calling it with no flags. Happy to submit this as a follow-up PR if there's interest. |
Quattro (Omarchy 4) breaks every hook this repo has into Omarchy: the git checkout at ~/.local/share/omarchy becomes a pacman-owned symlink, Hyprland config converts from .conf to Lua, Waybar is replaced by a Quickshell shell configured through shell.json, and generated theme state moves from ~/.config/omarchy/current to ~/.local/state/omarchy/current. This patch rewrites the overlays quattro-native. The theme-set and update-perform script overrides shrink to two hooks (zellij retint on theme-set, rustup/cargo on post-update) because user themed templates, helix restarts, cwd-aware terminal launches, mise updates, and persistent workspaces 1-5 are now stock. The hypr .conf files become Lua unbind/rebind overrides, waybar becomes a shell.json carrying only the clock format and Berkeley Mono, and the theme symlinks plus alacritty/ghostty imports re-point at the new state dir. My themed/*.tpl files survive untouched since upstream kept legacy colorN aliases. Do NOT merge or apply this on a machine still running Omarchy 3.x; it deletes the .conf and waybar files the live session reads. Upgrade day: run omarchy-upgrade-to-quattro, reboot, merge this bookmark, then chezmoi apply (accepting the .chezmoiremove deletions), re-run omarchy-theme-set, and reload Hyprland. Quattro is still alpha, so re-verify the hook names and shell.json schema against upstream first. Tested: chezmoi execute-template renders input/monitors for every dpi/mousedpi branch and luac -p passes on all of them plus bindings.lua and looknfeel.lua; shell.json validates; chezmoi status shows the expected adds/deletes. NOT TESTED on a real Quattro install. The monitor-relative floating window size in looknfeel.lua needs checking against hl.window_rule serialization on a live system. Ref: basecamp/omarchy#6231 Assisted-by: Claude:claude-fable-5
Quattro (Omarchy 4) breaks every hook this repo has into Omarchy: the git checkout at ~/.local/share/omarchy becomes a pacman-owned symlink, Hyprland config converts from .conf to Lua, Waybar is replaced by a Quickshell shell, and generated theme state moves from ~/.config/omarchy/current to ~/.local/state/omarchy/current. This patch rewrites the overlays quattro-native. The theme-set and update-perform script overrides shrink to two hooks (zellij retint on theme-set, rustup/cargo on post-update) because user themed templates, helix restarts, cwd-aware terminal launches, mise updates, and persistent workspaces 1-5 are now stock. The hypr .conf files become Lua unbind/rebind overrides, and the theme symlinks plus alacritty/ghostty imports re-point at the new state dir. My themed/*.tpl files survive untouched since upstream kept legacy colorN aliases. The Quickshell bar carries no user config. shell.json's clock keys are gone (upstream's stock layout and ISO-week clock are fine), and its fontFamily key never fed the bar anyway: the bar binds font.family to the "monospace" alias, so Berkeley Mono now lives in ~/.config/fontconfig/fonts.conf, the file `omarchy font set` owns and the shell, Qt apps, and everything resolving "monospace" read. Berkeley Mono has no Nerd glyphs, so fonts.conf adds a Nerd Font to the monospace fallback to keep bar and TUI icons rendering; upstream needs no such entry because its monospace alias is itself a Nerd Font. Do NOT merge or apply this on a machine still running Omarchy 3.x; it deletes the .conf and waybar files the live session reads. Upgrade day: run the quattro upgrade, reboot, merge this bookmark, then chezmoi apply (accepting the .chezmoiremove deletions), re-run omarchy-theme-set, and reload Hyprland. Quattro is still alpha, so re-verify the hook names against upstream first. Tested: chezmoi execute-template plus luac -p pass on bindings.lua, looknfeel.lua, and input.lua/monitors.lua rendered across every mousedpi (high/mid/low) and dpi (retina/mid/low) branch. xmllint validates fonts.conf, and fc-match against the rendered fonts.conf resolves monospace to Berkeley Mono Variable and falls back to an installed Nerd Font (JetBrainsMono/Caskaydia) for glyphs Berkeley lacks. chezmoi status shows the expected fontconfig modify plus the hypr .conf->.lua swaps. NOT TESTED on a real Quattro install. The monitor-relative floating window size in looknfeel.lua still needs checking against hl.window_rule serialization on a live system. Ref: basecamp/omarchy#6231 Assisted-by: Claude:claude-opus-4.8
Quattro (Omarchy 4) breaks every hook this repo has into Omarchy: the git checkout at ~/.local/share/omarchy becomes a pacman-owned symlink, Hyprland config converts from .conf to Lua, Waybar is replaced by a Quickshell shell, and generated theme state moves from ~/.config/omarchy/current to ~/.local/state/omarchy/current. This patch rewrites the overlays quattro-native. The theme-set and update-perform script overrides shrink to two hooks (zellij retint on theme-set, rustup/cargo on post-update) because user themed templates, helix restarts, cwd-aware terminal launches, mise updates, and persistent workspaces 1-5 are now stock. The hypr .conf files become Lua unbind/rebind overrides, and the theme symlinks plus alacritty/ghostty imports re-point at the new state dir. My themed/*.tpl files survive untouched since upstream kept legacy colorN aliases. The Quickshell bar carries no user config. shell.json's clock keys are gone (upstream's stock layout and ISO-week clock are fine), and its fontFamily key never fed the bar anyway: the bar binds font.family to the "monospace" fontconfig alias. Berkeley Mono is installed out of band (it's paid, so unlike my other fonts it isn't committed here), and the alias is set with `omarchy font set "Berkeley Mono Variable"`, which rewrites ~/.config/fontconfig/fonts.conf from the current package default. I don't manage that file in chezmoi for the same reason I dropped shell.json: omarchy owns and rewrites it, so a committed copy would both fork an upstream file that still gains fixes (the recent Arabic Naskh fallback) and fight the next `omarchy font set`. Do NOT merge or apply this on a machine still running Omarchy 3.x; it deletes the .conf and waybar files the live session reads. Upgrade day: run the quattro upgrade, reboot, merge this bookmark, then chezmoi apply (accepting the .chezmoiremove deletions), re-run omarchy-theme-set, run `omarchy font set "Berkeley Mono Variable"` to point the monospace alias (and thus the bar) at Berkeley Mono, and reload Hyprland. Quattro is still alpha, so re-verify the hook names against upstream first. Tested: chezmoi execute-template plus luac -p pass on bindings.lua, looknfeel.lua, and input.lua/monitors.lua rendered across every mousedpi (high/mid/low) and dpi (retina/mid/low) branch. Confirmed against upstream Style.qml that the bar's font.family binds to the "monospace" alias (so shell.json's fontFamily was dead) and that `omarchy font set` is what writes fonts.conf. chezmoi status shows only the expected hypr .conf->.lua swaps. NOT TESTED on a real Quattro install. The monitor-relative floating window size in looknfeel.lua still needs checking against hl.window_rule serialization on a live system. Ref: basecamp/omarchy#6231 Assisted-by: Claude:claude-opus-4.8
Quattro (Omarchy 4) breaks every hook this repo has into Omarchy: the git checkout at ~/.local/share/omarchy becomes a pacman-owned symlink, Hyprland config converts from .conf to Lua, Waybar is replaced by a Quickshell shell, and generated theme state moves from ~/.config/omarchy/current to ~/.local/state/omarchy/current. This patch rewrites the overlays quattro-native. The theme-set and update-perform script overrides shrink to two hooks (zellij retint on theme-set, rustup/cargo on post-update) because user themed templates, helix restarts, cwd-aware terminal launches, mise updates, and persistent workspaces 1-5 are now stock. The hypr .conf files become Lua unbind/rebind overrides, and the theme symlinks plus alacritty/ghostty imports re-point at the new state dir. My themed/*.tpl files survive untouched since upstream kept legacy colorN aliases. The Quickshell bar carries no user config. shell.json's clock keys are gone (upstream's stock layout and ISO-week clock are fine), and its fontFamily key never fed the bar anyway: the bar binds font.family to the "monospace" fontconfig alias. Berkeley Mono is installed out of band (it's paid, so unlike my other fonts it isn't committed here), and the alias is set with `omarchy font set "Berkeley Mono Variable"`, which rewrites ~/.config/fontconfig/fonts.conf from the current package default. I don't manage that file in chezmoi for the same reason I dropped shell.json: omarchy owns and rewrites it, so a committed copy would both fork an upstream file that still gains fixes (the recent Arabic Naskh fallback) and fight the next `omarchy font set`. Do NOT merge or apply this on a machine still running Omarchy 3.x; it deletes the .conf and waybar files the live session reads. Upgrade day: run the quattro upgrade, reboot, merge this bookmark, then chezmoi apply (accepting the .chezmoiremove deletions), re-run omarchy-theme-set, run `omarchy font set "Berkeley Mono Variable"` to point the monospace alias (and thus the bar) at Berkeley Mono, and reload Hyprland. Quattro is still alpha, so re-verify the hook names against upstream first. Tested: chezmoi execute-template plus luac -p pass on bindings.lua, looknfeel.lua, and input.lua/monitors.lua rendered across every mousedpi (high/mid/low) and dpi (retina/mid/low) branch. Confirmed against upstream Style.qml that the bar's font.family binds to the "monospace" alias (so shell.json's fontFamily was dead) and that `omarchy font set` is what writes fonts.conf. chezmoi status shows only the expected hypr .conf->.lua swaps. NOT TESTED on a real Quattro install. The monitor-relative floating window size in looknfeel.lua still needs checking against hl.window_rule serialization on a live system. Ref: basecamp/omarchy#6231 Assisted-by: Claude:claude-opus-4.8
Quattro (Omarchy 4) breaks every hook this repo has into Omarchy: the git checkout at ~/.local/share/omarchy becomes a pacman-owned symlink, Hyprland config converts from .conf to Lua, Waybar is replaced by a Quickshell shell, and generated theme state moves from ~/.config/omarchy/current to ~/.local/state/omarchy/current. This patch rewrites the overlays quattro-native. The theme-set and update-perform script overrides shrink to two hooks (zellij retint on theme-set, rustup/cargo on post-update) because user themed templates, helix restarts, cwd-aware terminal launches, mise updates, and persistent workspaces 1-5 are now stock. The hypr .conf files become Lua unbind/rebind overrides, and the theme symlinks plus alacritty/ghostty imports re-point at the new state dir. My themed/*.tpl files survive untouched since upstream kept legacy colorN aliases. The Quickshell bar carries no user config. shell.json's clock keys are gone (upstream's stock layout and ISO-week clock are fine), and its fontFamily key never fed the bar anyway: the bar binds font.family to the "monospace" fontconfig alias. Berkeley Mono is installed out of band (it's paid, so unlike my other fonts it isn't committed here), and the alias is set with `omarchy font set "Berkeley Mono Variable"`, which rewrites ~/.config/fontconfig/fonts.conf from the current package default. I don't manage that file in chezmoi for the same reason I dropped shell.json: omarchy owns and rewrites it, so a committed copy would both fork an upstream file that still gains fixes (the recent Arabic Naskh fallback) and fight the next `omarchy font set`. Do NOT merge or apply this on a machine still running Omarchy 3.x; it deletes the .conf and waybar files the live session reads. Upgrade day: run the quattro upgrade, reboot, merge this bookmark, then chezmoi apply (accepting the .chezmoiremove deletions), re-run omarchy-theme-set, run `omarchy font set "Berkeley Mono Variable"` to point the monospace alias (and thus the bar) at Berkeley Mono, and reload Hyprland. Quattro is still alpha, so re-verify the hook names against upstream first. Tested: chezmoi execute-template plus luac -p pass on bindings.lua, looknfeel.lua, and input.lua/monitors.lua rendered across every mousedpi (high/mid/low) and dpi (retina/mid/low) branch. Confirmed against upstream Style.qml that the bar's font.family binds to the "monospace" alias (so shell.json's fontFamily was dead) and that `omarchy font set` is what writes fonts.conf. chezmoi status shows only the expected hypr .conf->.lua swaps. NOT TESTED on a real Quattro install. The monitor-relative floating window size in looknfeel.lua still needs checking against hl.window_rule serialization on a live system. Re-checked the overlay against upstream quattro at f4e8470c3a1b: the theme-set and post-update hook dirs, the themed template renderer's legacy colorN aliases, the `o.bind` dispatcher tables, `o.window`, and the floating-window tag all still behave as this patch assumes, and the state dir the symlinks point at is unchanged. The uwsm/default override survives too: quattro moved the file into the package, but /usr/share/uwsm/env.d/10-omarchy still sources ~/.config/uwsm/default, and the upgrade keeps hash-mismatched user copies active, so EDITOR stays helix. Worth knowing for upgrade day: the upgrade's always-copy list rewrites all four hypr .lua files with quattro skeletons, which is why chezmoi apply has to run after the upgrade rather than before. Ref: basecamp/omarchy#6231 Assisted-by: Claude:claude-opus-4.8 Assisted-by: Claude:claude-opus-5
Quattro (Omarchy 4) breaks every hook this repo has into Omarchy: the git checkout at ~/.local/share/omarchy becomes a pacman-owned symlink, Hyprland config converts from .conf to Lua, Waybar is replaced by a Quickshell shell, and generated theme state moves from ~/.config/omarchy/current to ~/.local/state/omarchy/current. This patch rewrites the overlays quattro-native. The theme-set and update-perform script overrides shrink to two hooks (zellij retint on theme-set, rustup/cargo on post-update) because user themed templates, helix restarts, cwd-aware terminal launches, mise updates, and persistent workspaces 1-5 are now stock. The hypr .conf files become Lua unbind/rebind overrides, and the theme symlinks plus alacritty/ghostty imports re-point at the new state dir. My themed/*.tpl files survive untouched since upstream kept legacy colorN aliases. The Quickshell bar carries no user config. shell.json's clock keys are gone (upstream's stock layout and ISO-week clock are fine), and its fontFamily key never fed the bar anyway: the bar binds font.family to the "monospace" fontconfig alias. Berkeley Mono is installed out of band (it's paid, so unlike my other fonts it isn't committed here), and the alias is set with `omarchy font set "Berkeley Mono Variable"`, which rewrites ~/.config/fontconfig/fonts.conf from the current package default. I don't manage that file in chezmoi for the same reason I dropped shell.json: omarchy owns and rewrites it, so a committed copy would both fork an upstream file that still gains fixes (the recent Arabic Naskh fallback) and fight the next `omarchy font set`. Do NOT merge or apply this on a machine still running Omarchy 3.x; it deletes the .conf and waybar files the live session reads. Upgrade day: run the quattro upgrade, reboot, merge this bookmark, then chezmoi apply (accepting the .chezmoiremove deletions), re-run omarchy-theme-set, run `omarchy font set "Berkeley Mono Variable"` to point the monospace alias (and thus the bar) at Berkeley Mono, and reload Hyprland. Quattro is still alpha, so re-verify the hook names against upstream first. Tested: chezmoi execute-template plus luac -p pass on bindings.lua, looknfeel.lua, and input.lua/monitors.lua rendered across every mousedpi (high/mid/low) and dpi (retina/mid/low) branch. Confirmed against upstream Style.qml that the bar's font.family binds to the "monospace" alias (so shell.json's fontFamily was dead) and that `omarchy font set` is what writes fonts.conf. chezmoi status shows only the expected hypr .conf->.lua swaps. NOT TESTED on a real Quattro install. The monitor-relative floating size in looknfeel.lua is now verified on Hyprland 0.56.0: a probe window took 1654x1016 on a 3008x1692 logical monitor, so monitor_w/monitor_h resolve against logical and not physical pixels, which is what makes 55%/60% right on a retina display. The percentage form (size 55% 60%) is not a substitute: hyprctl accepts it and then silently ignores it, leaving the app's own default. What remains unproven is only the Lua table to rule-string serialization, and upstream ships that same shape in webcam-overlay.lua. Re-checked the overlay against upstream quattro at f4e8470c3a1b: the theme-set and post-update hook dirs, the themed template renderer's legacy colorN aliases, the `o.bind` dispatcher tables, `o.window`, and the floating-window tag all still behave as this patch assumes, and the state dir the symlinks point at is unchanged. The uwsm/default override survives too: quattro moved the file into the package, but /usr/share/uwsm/env.d/10-omarchy still sources ~/.config/uwsm/default, and the upgrade keeps hash-mismatched user copies active, so EDITOR stays helix. Worth knowing for upgrade day: the upgrade's always-copy list rewrites all four hypr .lua files with quattro skeletons, which is why chezmoi apply has to run after the upgrade rather than before. Ref: basecamp/omarchy#6231 Assisted-by: Claude:claude-opus-4.8 Assisted-by: Claude:claude-opus-5
Quattro (Omarchy 4) breaks every hook this repo has into Omarchy: the git checkout at ~/.local/share/omarchy becomes a pacman-owned symlink, Hyprland config converts from .conf to Lua, Waybar is replaced by a Quickshell shell, and generated theme state moves from ~/.config/omarchy/current to ~/.local/state/omarchy/current. This patch rewrites the overlays quattro-native. The theme-set and update-perform script overrides shrink to two hooks (zellij retint on theme-set, rustup/cargo on post-update) because user themed templates, helix restarts, cwd-aware terminal launches, mise updates, and persistent workspaces 1-5 are now stock. The hypr .conf files become Lua unbind/rebind overrides, and the theme symlinks plus alacritty/ghostty imports re-point at the new state dir. My themed/*.tpl files survive untouched since upstream kept legacy colorN aliases. The Quickshell bar carries no user config. shell.json's clock keys are gone (upstream's stock layout and ISO-week clock are fine), and its fontFamily key never fed the bar anyway: the bar binds font.family to the "monospace" fontconfig alias. Berkeley Mono is installed out of band (it's paid, so unlike my other fonts it isn't committed here), and the alias is set with `omarchy font set "Berkeley Mono Variable"`, which rewrites ~/.config/fontconfig/fonts.conf from the current package default. I don't manage that file in chezmoi for the same reason I dropped shell.json: omarchy owns and rewrites it, so a committed copy would both fork an upstream file that still gains fixes (the recent Arabic Naskh fallback) and fight the next `omarchy font set`. Do NOT merge or apply this on a machine still running Omarchy 3.x; it deletes the .conf and waybar files the live session reads. Upgrade day: run the quattro upgrade, reboot, merge this bookmark, then chezmoi apply (accepting the .chezmoiremove deletions), re-run omarchy-theme-set, run `omarchy font set "Berkeley Mono Variable"` to point the monospace alias (and thus the bar) at Berkeley Mono, and reload Hyprland. Quattro is still alpha, so re-verify the hook names against upstream first. Tested: chezmoi execute-template plus luac -p pass on bindings.lua, looknfeel.lua, and input.lua/monitors.lua rendered across every mousedpi (high/mid/low) and dpi (retina/mid/low) branch. Confirmed against upstream Style.qml that the bar's font.family binds to the "monospace" alias (so shell.json's fontFamily was dead) and that `omarchy font set` is what writes fonts.conf. chezmoi status shows only the expected hypr .conf->.lua swaps. NOT TESTED on a real Quattro install. The monitor-relative floating size in looknfeel.lua is now verified on Hyprland 0.56.0: a probe window took 1654x1016 on a 3008x1692 logical monitor, so monitor_w/monitor_h resolve against logical and not physical pixels, which is what makes 55%/60% right on a retina display. The percentage form (size 55% 60%) is not a substitute: hyprctl accepts it and then silently ignores it, leaving the app's own default. What remains unproven is only the Lua table to rule-string serialization, and upstream ships that same shape in webcam-overlay.lua. Re-checked the overlay against upstream quattro at f4e8470c3a1b: the theme-set and post-update hook dirs, the themed template renderer's legacy colorN aliases, the `o.bind` dispatcher tables, `o.window`, and the floating-window tag all still behave as this patch assumes, and the state dir the symlinks point at is unchanged. The uwsm/default override survives too: quattro moved the file into the package, but /usr/share/uwsm/env.d/10-omarchy still sources ~/.config/uwsm/default, and the upgrade keeps hash-mismatched user copies active, so EDITOR stays helix. Worth knowing for upgrade day: the upgrade's always-copy list rewrites all four hypr .lua files with quattro skeletons, which is why chezmoi apply has to run after the upgrade rather than before. Ref: basecamp/omarchy#6231 Assisted-by: Claude:claude-opus-5
…gin (#6598) * Extract the Wi-Fi QR share card into its own omarchy.wifiqr panel plugin omarchy-network-qr now leads with an iface/security/ssid meta line, so a bare summon self-detects the connection and the plugin owns the whole share flow. The network panel loses its overlay lifecycle: with no centered card left inside it, the shadowed open/close collapses back to the stock panel behavior, and the QR button just summons the plugin -- which a clone or third-party plugin can replace, like the speed test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Keep canceled QR and password runs from leaking into their replacements Copilot review: the cancellation guards dropped in onExited while the canceled run's collectors were still allowed to fire, so a stale stderr could shadow a successful regeneration and a stale password could be revealed under a new network's card. The guards now stay up until the next run launches, good output settles any earlier error, and a bare re-summon no longer inherits the previous card's SSID. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Split the end-user omarchy skill into topic guides Move default/omarchy-skill to default/agents/skills/omarchy and break the monolithic SKILL.md into on-demand topic files for Hyprland config, shell plugins, theming, and hooks. Update the skill symlink wiring, relink existing installs through a migration, and correct claims that had drifted from the implementation: plugin hot-reload, terminal reload, menu customization, refresh scopes, theme overlays, background locations, hook timing, and the packaged (not git-managed) system directory. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Add capture and contributing guides to the omarchy skill Cover screenshots, screen recording, OCR text capture, and LocalSend or Taildrop sharing, plus how to route bug reports, suggestions, and support questions upstream with diagnostics and captures of the problem attached. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Scope Hyprland reload guidance to the Lua config files hyprsunset.conf and xdph.conf are read by separate processes, so hyprctl neither applies nor validates them. Document restarting hyprsunset after editing its config, including in the night light example. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Evaluate menu guards one run at a time A second evaluation starting while one was in flight could not replace it. Process ignores a command change until the next run and `running = true` is a no-op while running, so setting them did nothing -- but clearing `collected` first threw away the lines the running script had already emitted. Its tail then landed as the entire result, and every id missing from it went back to showing, since `when:` only hides a row on an explicit false. That is how Setup > Defaults > Browser ends up listing browsers that are not installed. Queue the evaluation instead and run it once the one in flight lands, the way provider enumeration already waits its turn. * Answer repeated menu guard questions once per evaluation The menu opens on the last evaluation's answers, so however long the guard batch takes is how long a row can contradict the state it describes: stop a recording and Screenrecord still offers to stop it, because the `pgrep` that would hide it is queued behind fifty package lookups. Almost none of that time is the questions, it is asking them one process at a time. The shipped menu runs `omarchy-pkg-present` 54 times and `omarchy-cmd-present` 23, and reads `omarchy-default-browser` once per row in Defaults > Browser. Prepend a prelude that answers all of it inside the one guard process, off a single package listing, bash's own PATH lookup, and one capture per reader command. The captures are eager because `checked:` reads them inside `$()`, where a lazy memo would not outlive the subshell. Takes the shipped batch from 1.49s to 0.25s with identical answers for all 175 guards. * Make the guard prelude answer exactly as the commands it stands in for The prelude only helps if it is indistinguishable from the commands it shadows, and it was not: - `pacman -Q` resolves a name through what installed packages provide, so with gvim installed it reports `vim` as present. A set built from `pacman -Qq` sees only names, so `install.editor.vim` came back and offered to install what was already there. Build the set from provides too, and send version constraints, which no set can answer, to pacman. - `omarchy-cmd-present` uses `command -v`, which finds builtins; `type -P` searches PATH alone and disagreed on every one of them. - Shadowing a reader with a function caught far more than the plain `$(reader)` the rows use: `command -v omarchy-dns` got the function name, and `VAR=x omarchy-channel-current` got an answer captured without the variable. Substitute the captured value into the expression instead and leave every other form to run the real command. - A reader that exits nonzero could take the batch down under a login shell with errexit set. Also keep the results of a batch that was killed rather than finished, since a row whose `when:` went unanswered shows, which is the failure this set of changes exists to remove. Costs 0.25s -> 0.33s against 1.49s before any of this, still with answers identical to evaluating each guard on its own. * Read every provide pacman reports, wrapped or not `pacman -Qi` wraps a long list onto indented continuation lines whenever COLUMNS is set in the environment, which the login shell the batch runs under may well have done. Reading only the line that starts with `Provides` dropped the rest: at COLUMNS=80 that is 537 of 856 provides on this machine, which puts back exactly the "offers to install what is already there" failure the provides lookup was added to prevent. Follow the continuation lines instead. The version-constraint case was also not testing what it claimed. Interpolating the argument into the shadow's script text let `bash>=1` parse as a redirection, so the shadow was handed `bash` and quietly agreed for the wrong reason -- and left an `=1` file behind, which got committed. Pass arguments as argv to both sides, drop the file, and wrap gvim's provides in the stub so the parser is held to the format pacman actually emits.
* Persist on-screen notification popups across shell restarts Mirror every popup to its own file under ~/.local/state/omarchy/notifications/ for exactly as long as it is on screen: written when the toast appears, deleted when it expires, is dismissed, is acted upon, or is replaced via freedesktop replaces_id. On startup the directory is read back and still-valid popups re-shown, so toasts survive the restart omarchy-update performs — critical alerts, which never expire, always make it across. Restored popups keep ids from the previous server generation, so the replaces_id cleanup tracks them separately instead of mistaking a fresh notification's reused id for a replacement, and the startup restore only discards a persisted file when a live row with a different timestamp has superseded it. Files are read back with awk so a torn write can't glue itself onto the next file and take a valid popup down with it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Close the remaining cross-generation id collisions in popup persistence Notification ids restart from 1 with every server process, so an id alone never identifies a notification across a shell restart. The first round of fixes guarded row removal, but review (and a live repro) showed the same collision biting everywhere else an id was used on its own: - Dismissing or clicking a restored toast resolved liveRefs by id and could dismiss, or fire the action of, an unrelated fresh notification, and archive its pending row. Restored rows now never resolve to a live object, and pending rows are matched by id plus timestamp. - parsePopupFiles deduped files by id, so a fresh notification reusing a restored critical alert's id got that alert's file deleted as a "stale duplicate" on the next restore. Files are never deduped now: each one is a popup that was on screen, and the rare genuine leftover from a crash re-shows once and cleans itself up. - The restore only skips an entry when a live row matches both id and timestamp (it is that entry); an id-only match shows both toasts rather than guessing which one to drop. - A same-millisecond replaces_id update shares its predecessor's filename; the replacement's file is no longer deleted alongside the replaced row. - A restored popup's reset lifetime is persisted as an absolute deadline, so a second restart judges it by the clock that actually governs its display instead of dropping it while still on screen. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…6539) * Fix unbound XKBLAYOUT under set -u in the keyboard-layout migration /etc/vconsole.conf only guarantees KEYMAP -- XKBLAYOUT is written by some installers but not required, and vconsole.conf on a stock install may not define it at all. omarchy-migrate runs every migration with set -euo pipefail, so referencing the unset variable directly aborted the migration run instead of just skipping a layout this migration doesn't care about. Read it with a default-empty expansion first, then apply the existing comma-strip separately -- keeps both failure modes (unset, and set with a trailing keymap variant) handled explicitly instead of folding them into one expansion that only covers one of the two. * Survive a missing vconsole.conf in the keyboard-layout migration Defaulting XKBLAYOUT fixed the unset variable but not the other way this line takes the migration chain down. `.` fails when /etc/vconsole.conf is not there at all, `&&` short-circuits, and the non-zero status leaves the command substitution and kills the assignment under omarchy-migrate's `bash -euo pipefail` -- the same abort, one branch over. The file is optional enough that both other readers of it, omarchy_hooks.conf and 1781485962.sh, guard with `-f` first. Run the echo unconditionally so the substitution reports its status instead of the source's. Read both paths from the environment, the way the zram migration already does, and cover the layout cases plus both crashes with a test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Read the keyboard layout from vconsole.conf alone Two ways the layout could come from somewhere other than the file, both found reviewing the fix before it: Sourcing does not clear an exported XKBLAYOUT, so on a machine that exports one, a vconsole.conf that sets no layout -- or none at all -- left the caller's environment deciding what the initramfs bundles. Unset it in the subshell so only the file can answer. Skipping the source when the file is missing, rather than letting the substitution swallow the failure, also stops depending on errexit being discarded inside command substitution. inherit_errexit takes that back and the chain aborts again; nothing in Omarchy sets it today, but the other two readers of vconsole.conf already check `-f` first and this now matches. Test the layout list past its one Cyrillic entry, both orders of a comma-separated pair, an exported XKBLAYOUT, and inherit_errexit. Each of those catches a mutation that survived before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Drop the test scaffolding from the keyboard-layout migration A migration runs once and then gets deleted, so a suite entry for this one is upkeep with a short shelf life. The path overrides existed only so that test could aim the migration at fixtures; with the test gone they are indirection nothing exercises, so both go back to literal paths. The fixes stay: check the file before sourcing it, unset XKBLAYOUT so an exported one cannot answer for a file that sets none, and strip the keymap variant before matching. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Toni Nowak <t.nowak@ai-flow.no> Co-authored-by: David Heinemeier Hansson <david@hey.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ents (#6603) * Add agent usage collectors that write display-ready data files One omarchy-agent-usage-scan-<agent> collector per AI coding agent prints a complete display-ready usage record — identity, tier, status, rate limits, and today/week/all-time stats. omarchy-agent-usage-update runs every collector it finds and writes the records atomically to ~/.local/state/omarchy/agents/usage/, so anything that displays usage only ever reads JSON from there. The Claude collector absorbs what the shell previously did in-process: transcript scanning, the stats-cache/history fallback, credentials parsing, and the OAuth limits probe, now with a probe throttle and last-good limits kept across network failures. The Codex collector is the existing scanner reshaped to the shared record contract. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Redo the model-usage plugin as omarchy.agents watching usage data files The panel is now strictly a display. It discovers the JSON records that omarchy-agent-usage-update maintains under ~/.local/state/omarchy/agents/usage/, watches them for changes, and draws whatever appears — so adding an agent means shipping a collector, never touching the panel. Marks resolve by convention (assets/<id>.svg with an optional -light twin), the limits meters read a generic limits array, and the per-provider QML adapters and in-plugin scanner scripts are gone. Cross-device sync aggregation stays in the shell and keeps the snapshot field names older versions wrote, so mixed-version fleets still merge in both directions. With the provider fan-out gone, the widget takes its real name: the plugin id becomes omarchy.agents. A migration renames it wherever a user's config mentions it — layout entries keep their settings and position, a disabled widget stays disabled — then primes the data files once and drops the old scanner cache. The migration test also drops a stale assertion that expected migrations to restart the shell themselves, which c992cdf moved to omarchy update. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Address Codex review: synced-only tabs, limits retry, history fallback Three data-availability gaps from review. An agent whose records only exist in synced snapshots — a collector installed on just one machine — now gets its tab by unioning the synced aggregate into the provider list, with rate limits blank since those never travel. A Claude limits probe that reaches no server at all writes retryAdvised into its record, and the shell honors it with one 30-second retry instead of waiting out the full refresh interval, restoring the old boot-before-DHCP behavior. And a machine with only history.jsonl — no transcripts, no stats-cache — still reports today's prompt and session counts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Address second Codex pass: history-only visibility, targeted retries Today's prompt and session counts now count toward an agent's presence in the bar, so a machine whose only Claude source is history.jsonl shows up without waiting for limits. And the 30-second limits retry passes the advising agent ids to the updater, so an outage at one provider no longer puts every other collector on a retry treadmill. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Drop omarchy-cmd-present jq guards from the agents migrations jq ships in the default package set, which makes it a runtime invariant per AGENTS.md — call it directly. The migration tests lose their now-unused omarchy-cmd-present stubs with it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Drop the scan infix from the collector command names Collectors are omarchy-agent-usage-<agent>; the updater skips its own name when globbing them, and the update test proves it with a decoy. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Keep the credential store out of the printed usage record The Claude collector now reads .credentials.json once into three scalars — the access token, its expiry, and the plan label — instead of passing the parsed store around. The token reaches nothing but the Authorization header of the limits probe, and only the plan label may travel into the record, which is what CodeQL's clear-text-logging alert on the record print was unable to see when the whole dict flowed through. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The panel resolves marks by walking assets/<id>-light.svg then assets/<id>.svg, advancing on Image.Error. But the candidates binding is re-evaluated whenever the provider objects are rebuilt, and a fresh array identity with identical content reset the walk to the -light candidate. Re-pointing source at a URL whose load already failed emits no statusChanged, so the walker never advanced again and agents that ship a single mark — Claude — fell back to the generic bar glyph on light surfaces. Key the reset on the candidate URLs instead of the array identity, and defer the error advance one tick so stepping source from inside its own status change doesn't trip the binding-loop detector. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Faking HOME alone was never enough: the shell QML and the agent usage updater read XDG_STATE_HOME and XDG_CACHE_HOME directly, so a test quickshell inherited the session's real paths. The bar widget contract test instantiated the agents widget, whose refresh ran the real collectors against the empty fake HOME and wrote hollow "Waiting for auth" records into the developer's real usage data files — hiding the agents widget from their bar — while littering the real cache with per-tmpdir scan files. Point XDG_CONFIG_HOME, XDG_CACHE_HOME, and XDG_STATE_HOME under the fake home in every test that boots quickshell with one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: reopen wifi passphrase prompt after a wrong saved password A failed first connection attempt leaves the network profile saved, so the network shows up as known. Clicking it again reconnects with the stored wrong PSK and fails with WifiAuthTimeout, but the inline passphrase prompt only reopened on NoSecrets, leaving no way to re-enter the password short of forgetting the network. Treat an auth timeout on a protected network as a wrong saved passphrase and reopen the prompt; connectWithPsk overwrites the stored PSK on submit. Fixes #6582 * Scope the wifi passphrase reprompt to panel-initiated connects Background auto-connect retries also fire connectionFailed; without a gate they would pop the passphrase prompt open unbidden, stealing focus and wiping a passphrase mid-entry when another network fails. For the gate to see the failure, the action safety-net timer must outlast NetworkManager's 25s supplicant timeout -- at 15s it cleared the action state before WifiAuthTimeout arrived, so a wrong saved password showed "Timed out connecting" instead of "Wrong password". Bump it to 30s. Also share the one ConnectionFailReason map between the Model.js helpers instead of building a second partial copy inline. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: David Heinemeier Hansson <david@hey.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(network): drop the redundant rescan on the bar click Opening from the bar ran open() and then a bare refresh(). open() already triggers onOpenedChanged -> refresh(true), which defers the PHY scan by disabling the scanner and re-enabling it from scanRestart. The bare refresh() that followed defaults scanWifi to false, so it took the other branch and set wifiDevice.scannerEnabled synchronously on the click frame, undoing the deferral and stalling the open on NetworkManager's access-point flood. It also double-started the DNS and band probes. Co-Authored-By: shrijit <shrijitsrivastav@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(network): keep wifi rows QObject-free to prevent a delegate crash wifiRow() embedded the WifiNetwork QObject in the row it returns, and those rows are list-model data, so every delegate held a live QObject wrapper in a var property. When NetworkManager churns the list -- a scan's access-point flood, an AP disappearing -- the object can be destroyed while a delegate is still incubating, and quickshell segfaults in QObjectWrapper::wrap_slowPath on the dangling wrapper. Project primitives only and resolve the backend object at action time via the existing networkForSsid(). Both failNetworkAction() and checkActionCompletion() already no-op on a null network, so a row whose network has since vanished is handled the same way it was before. Co-Authored-By: shrijit <shrijitsrivastav@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(bluetooth): keep device rows QObject-free to prevent a delegate crash Same crash class as the wifi rows: scrollRows embedded the BlueZ Device QObject in list-model data, so every delegate held a live wrapper in a var property. Discovery churn -- a scan timeout dropping a device, an unpair -- can destroy the object while a delegate is still incubating, and quickshell segfaults on the dangling wrapper. Project primitives for both the scroll rows and the connected rows, and resolve the backend object by address in deviceFor() for the click actions. The keyboard flow already went through deviceAt(), which reads the live device arrays directly rather than model data, so it is untouched. Co-Authored-By: shrijit <shrijitsrivastav@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(network): guard row disconnects against a vanished network Row activation resolved the WifiNetwork with networkForSsid() and passed the result straight to disconnect(), which falls back to connectedWifiNetwork when handed null. A row is a primitive snapshot, so scan churn can remove its backing object while the row is still on screen -- activating it then tore down whatever happened to be connected at that moment rather than doing nothing. Route both row paths through disconnectRow(), which resolves first and only acts when the row still maps to a live network. disconnect() keeps its fallback for callers that mean "drop the current connection". Also covers the bar-click open path, which had no regression: the suite already asserts against Panel.qml source, so assert the closed branch calls open() alone and never a second refresh(). Co-Authored-By: shrijit <shrijitsrivastav@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: shrijit <shrijitsrivastav@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Keep root= in the kernel cmdline when upgrading to Quattro The packaged drop-in /etc/limine-entry-tool.d/omarchy-defaults.conf sets KERNEL_CMDLINE[default] with the += operator. limine-entry-tool.conf documents what that costs: "+= appends parameters to an existing cmdline ... Ignores /etc/kernel/cmdline and /proc/cmdline". As soon as the drop-in lands, the tool stops auto-detecting the cmdline. Fresh installs are unaffected, because the ISO writes /etc/default/limine from default/limine/default.conf with @@cmdline@@ substituted. The upgrade path never created that file. A pre-quattro install that relied on the auto-detected root= therefore ends up with a cmdline that has no root= at all, in both limine.conf entries and in the cmdline embedded in the UKIs. The next boot fails with "ERROR: Failed to mount '' on real root" and drops to an emergency shell, where the error gives no hint that the cmdline is the cause. Capture the boot-critical parameters from /proc/cmdline before the reboot, while the still-correct cmdline of the running kernel is readable, and write them to /etc/default/limine, which is loaded last so += keeps the drop-in parameters instead of replacing them. Copy root=, rootflags, rootfstype, resume, resume_offset, the cryptdevice and rd.luks keys and rw/ro verbatim rather than reconstructing them, so LUKS and hibernation setups survive too. Re-running the upgrade on an already-broken system has no root= left to copy, so fall back to deriving it from the mounted root, including the subvolume on btrfs. Any config layer that already pins root= is treated as authoritative and left untouched. * Anchor the cmdline guard and harden the repair path The early-return guard searched for the bare string root=, which matches the commented example limine-entry-tool.conf ships at line 53: #KERNEL_CMDLINE[default]+=rw root=UUID=... That file is present on every stock machine, so the guard always fired and the function never wrote anything. Match assignments instead, and check /etc/kernel/cmdline separately since it holds bare parameters rather than shell assignments. Three fixes on the repair path: Assigning boot_params discarded every parameter the collection loop had just captured, so cryptdevice, cryptkey, resume and ro were dropped, and rw was forced over a captured ro. Prepend the derived root= instead, and only add rw when the booted cmdline stated no mount mode. On an encrypted root, findmnt reports the unlocked mapper device, whose UUID says nothing about which container to unlock. Emitting it produced a cmdline that still could not boot while satisfying the final check, so the user rebooted into the same emergency shell believing it was repaired. Warn and write nothing in that case. The allowlist gained rd.luks.key, rd.luks.crypttab, rd.md.uuid, rd.dm.uuid, rootwait, rootdelay and dm-mod.create. Verification now also reads the .cmdline section of each UKI. With omarchy-uki.conf among the drop-ins that embedded copy is what actually boots, so a green limine.conf alone did not prove the machine would come up. * Filter guard paths and narrow the dm-crypt and UKI checks The guard passed /etc/default/limine to grep unconditionally, and that file is absent on exactly the machines this targets. A missing operand makes grep exit 2 without -q, so a drop-in pinning a real root= went undetected and the function appended a second one, overriding the explicit setup it promises to leave alone. Rather than relying on -q returning 0 despite the error, which is a GNU grep special case and not true of every implementation, filter the paths first and only grep the ones that exist. The exit status is then unambiguous. The dm-crypt check gated on the /dev/mapper/* prefix, which also matches plain LVM, dm-raid and multipath. Those roots need no unlock parameters and were repairable before, so the prefix test denied them a working root=UUID= and told them they were encrypted. Gate on the device mapper target type instead. root_filesystem_encrypted() is not reused here on purpose: it treats every /dev/mapper/* path and any non-empty /etc/crypttab as an encrypted root, which suits its own call site but would reintroduce the same false positive. UKI verification now runs through as_root, since a restrictive ESP fmask would otherwise make find return nothing and the check pass in silence, and is scoped to the omarchy_linux*.efi images limine-entry-tool generates so a shared ESP or a stub without a .cmdline section cannot raise a false "do not reboot" warning. The allowlist gained rd.lvm.lv and rd.lvm.vg. * Strip the subvolume before resolving the root device type findmnt appends the subvolume for btrfs mounts, so the source read back for an encrypted btrfs root is /dev/mapper/cryptroot[/@]. lsblk cannot resolve that path, the device type came back empty, and the crypt gate never fired. The function then wrote root=UUID= with no unlock parameters, limine.conf ended up carrying a root= so the final check stayed quiet, and the machine still booted to an emergency shell. That is the layout Omarchy installs when encryption is picked, so the gate missed exactly the roots it exists for. The previous /dev/mapper/* prefix test matched the bracketed form by accident. Moving to the device mapper target type is still the right call, it just needs the unbracketed source, which findmnt --nofsroot provides. Also give root_type an explicit empty default. It is assigned inside a branch and read outside it, and set -u treats a declared-but-unassigned local as unbound, so a findmnt that cannot answer would abort the upgrade with the quattro packages already installed and everything from configure_snapper_policy onward skipped. * Look for the crypt layer across the whole device stack lsblk -no TYPE reports only the target's own type. On the standard full-disk encryption layout, LUKS container -> LVM PV -> root LV, that type is lvm and the crypt layer sits in the parents, so the gate never fired: the function wrote root=UUID= with no unlock parameters, the final check found a root= and stayed quiet, and the machine still booted to an emergency shell. Walk the parents with lsblk -s and look for a crypt layer anywhere in the chain. That keeps LVM, dm-raid and multipath roots on the repair path, since they carry no crypt layer and root=UUID= is enough once mkinitcpio assembles them. root_type is replaced by root_stacks_crypt, which says what is actually being tested and drops the LVM-versus-crypt caveat the old target-type check needed. Also drop a vacuous test assertion: piping a bracketed literal through grep -qv '\[' selects nothing, so the branch was unreachable and the case passed whatever the script did. The --nofsroot assertion above it is what holds that fix. * Keep the crypt gate off a pipeline exit status Capture the device stack and match it from a here-string rather than piping lsblk into grep -q. Under pipefail a short-circuiting grep can leave the producer with SIGPIPE and turn the pipeline into 141, which reads as "no crypt layer" and disarms the gate silently. lsblk writes its whole table in one go, so this is out of reach in practice, but nothing about the gate should depend on how much output a helper happens to buffer. Also correct a stale test comment that described the target-type check the previous revision used, four lines above the comment explaining why that check was insufficient. * Harden the kernel cmdline preservation against false root= pins The /etc/kernel/cmdline early return trusted a file limine-entry-tool ignores once a += drop-in sets KERNEL_CMDLINE[default], leaving exactly the targeted machines unbootable. The pin guard now reads only the *.conf layers the tool loads, only the default key, and tokenizes the assignment value so quoted decoys and volatile-root= cannot pin. /proc/cmdline is tokenized quote-aware so dm-mod.create="..." survives verbatim, the root= verification is token-anchored, and an unverified cmdline now blocks the reboot instead of only warning. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Ask limine-entry-tool for the effective cmdline instead of parsing its configs --get-cmdline default answers whether root= survives the tool's own config merge, replacing the glob, grep and quote-aware tokenizer walk over the config layers, and the quote-aware /proc/cmdline parsing reverts to plain word splitting. The verification and the reboot gate stay: they are what catches anything the simpler paths miss. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: David Heinemeier Hansson <david@hey.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Fail the snapshot when Snapper is installed but has no configs omarchy-snapshot create loops over the configs snapper reports. With none, the loop body never runs, so it prints "Create system snapshot" and exits 0 without capturing anything. Every update then reports a snapshot it never took, and the absence only surfaces when a rollback is needed and the snapshot list turns out to be empty. * Say so when the update proceeds without a snapshot The update ignores exit 127 so a system without snapper updates quietly. Any other snapshot failure was being swallowed by the same expression, which let the update continue with no indication that it was now unprotected. Keep continuing, but say it out loud. * Point the snapshot repair hint at how the installer runs it Also hold the green header until a snapshot will actually be attempted, so the no-config failure doesn't open with a success banner. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Continue the quattro upgrade when the pre-upgrade snapshot fails The upgrade runs under set -e, so the new non-zero exit from an unconfigured Snapper would have aborted a re-run at the snapshot step instead of proceeding like omarchy-update does. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: David Heinemeier Hansson <david@hey.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…#6610) The pacman configuration says nothing about debug symbol packages. Someone who wants `glibc-debug` so that gdb or Valgrind has symbols to work with has to discover on their own that Arch publishes `-debug` repositories, find a mirror that serves them, and work out that the versions only line up while the system tracks Arch current. Appending the three repository sections to `pacman-edge.conf`, commented out, will put them within reach without turning them on. The comment records that the packages match the edge channel only, and that debuginfod already covers most debugging on edge, so enabling them is often unnecessary. Only the edge template gets them, because on stable and rc the upstream debug packages are newer than the pinned packages they would need to match. ref: #6185
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Re-encode over-encoded theme backgrounds Several backgrounds shipped at quality 95-100, which buys nothing visible on a wallpaper but costs 20 MB. Re-encode those at quality 85, leaving resolution untouched -- every file keeps its original pixel dimensions. Only files stored above quality 90 are touched, and only when the result saves at least 15% and stays above 38 dB PSNR against the original. The 34 backgrounds already stored at quality 85 or below are left alone rather than pushed through another lossy generation for a few hundred KB. osaka-jade/2-shaded-entrance is skipped for that reason: nothing clears both bars. ristretto/2-coffee-beans is re-encoded at 90 instead of 85, where 85 fell below the PSNR floor. Theme backgrounds drop from 107 MB to 87 MB. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Store nord's night-hawks background as a palette PNG The file uses only 8 distinct colors but was saved as truecolor RGB, costing 1.8 MB for an image an indexed palette stores in half the space. Converting to a palette PNG is lossless: same dimensions, and zero pixels differ from the original. 1782 KB -> 909 KB. The other palette-eligible PNGs are already indexed, and re-compressing them with ImageMagick only makes them bigger, so they are left alone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Recompress theme PNGs losslessly with oxipng Every background PNG was left at its authoring tool's default deflate settings. Running oxipng over them re-packs the same pixels: all 31 files decode bit-for-bit identically (AE=0) at unchanged resolution, for 981 KB. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Store photographic backgrounds as JPEG instead of PNG Four backgrounds were shipped as PNG despite being photographs and painterly illustrations, where lossless coding buys nothing the eye can see. Re-encoding them at the same q85 used by the other backgrounds keeps every pixel dimension and stays above 40 dB PSNR, for 3.3 MB. The remaining PNGs stay PNG: JPEG is larger for the dot patterns and the flat-shaded pieces, and 0-launch.png genuinely uses its alpha channel. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: David Heinemeier Hansson <david@hey.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* Give scratchpad a Quake-style presentation * Keep the Quake scratchpad from restyling every window The presentation was bought with global decoration defaults: rounding went 0 -> 5 and both shadow and blur were switched back on for every window on the system, undoing 935283c. Blur in particular is close to invisible here, since every window is already tagged 0.985/0.96 opacity and no layer rule asks for it, so it was GPU load on every frame for almost nothing. Put the globals back and scope the rounding to the scratchpad with a window rule, the same way popped windows already get theirs. The inset, dim_special and the slide carry the effect on their own. dim_special only applies while a special workspace is open, so it stays. Also drop the four workspace-rule booleans that only restated Hyprland's defaults, fold the stale scratchpad rows in the hotkey table into the new ones, and give the binding assertions their own fixture instead of borrowing the bar-panel one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Omabot <david@hey.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Open the scratchpad with the default agent already in it on_created_empty fires when the special workspace is created empty, so the agent starts the first time the console drops down instead of at boot, and comes back on the next open if you close it. The exec rule pins the workspace rather than trusting the spawn to inherit it: Hyprland only tags a process with its origin workspace while misc.initial_workspace_tracking is on, and we turn that off. Nothing to guard for a missing default agent. Omarchy picks none for you, and omarchy-agent exits without opening a window when none is set, so the scratchpad just opens empty until one is chosen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Move the console into its own file and size it to half the screen The scratchpad's presentation was scattered through looknfeel: a dim in the decoration block, a workspace rule below it, two animation leaves further down again. Gathered into qconsole.lua, where the whole console is one readable thing. Sized to half the screen while it moved. A window rule cannot do that: its size expressions resolve once, when the window maps, so rescaling the monitor afterwards leaves a console that is no longer half of anything. Gaps are re-applied by the layout, so the console is sized by the gap left underneath it, recomputed from the monitor whenever the layout changes. Monitor dimensions come back in physical pixels while gaps are logical, so the scale comes out before the reserved area comes off. That arithmetic is the whole trick, and the test pins it at 1x, 2x and 1.5x. The test runs lua with an explicit "-". Bare `lua <<EOF` reads stdin as a REPL and exits 0 even after an error, which would leave its assertions unable to fail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Only rewrite the console rule when its size actually changes Refitting replaces the rule in place rather than stacking a new one, so there was no leak, but each write still schedules a monitor and window state refresh and monitor.focused fires on every hop between screens. Remember what was last written and skip the write when the number has not moved. Also say out loud that the scale guard is what keeps the arithmetic below it safe: a monitor handle that has outlived its output answers nil to every field, and a layout change is exactly when that happens. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Drop the active window border inside the console The gradient border marks which window has focus, which the console does not need: it is only ever focused while it is open, and the dimmed workspace behind it already sets it apart. On a single agent terminal the highlight just reads as a frame around the panel. no_border on the workspace rule pins the border to 0 at workspace-rule priority, so it applies to whatever ends up in there without touching the global border. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Omabot <david@hey.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Decode mixed UTF-16 clipboard text * Harden UTF-16 clipboard detection * Finish UTF-16 decoder hardening
* Print the OEM Windows product key from firmware Machines that shipped with Windows keep the OEM key in the ACPI MSDM table. `omarchy windows license key` reads it with strings, then cat. * Rename the firmware key command to omarchy-windows-key
* Decode webp in the shell The background and the lock screen are drawn by Quickshell, so they decode through Qt, which ships handlers for png, jpeg and gif but not webp. QImageReader answers "Unsupported image format" and the layer comes up blank. Any third-party theme shipping a .webp background hits this today, even though every path that goes looking for a background already globs the extension. qt6-imageformats supplies the missing plugin for 71 KB downloaded. Its one new dependency of substance, libwebp, is already on every machine by way of libvips. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Store theme backgrounds as webp WebP codes both of the things these backgrounds are made of better than the formats they were in: the photographs, where its lossy mode is worth a third or more over JPEG at matched quality, and the flat art and dot patterns, where its lossless mode undercuts an oxipng-packed PNG. 28 of them become lossless webp and decode bit-for-bit identically (AE=0), so the dot patterns and flat-shaded pieces carry no quality question at all. That includes 0-launch, whose alpha channel comes through intact. The other 51 are photographs held to the same 38 dB PSNR floor as the JPEG pass, landing between 38.0 and 54.6 dB. Every image keeps its exact pixel dimensions, for 29.8 MB. Each one is encoded from the original as it stands in quattro rather than from the file the earlier commits produced, so nothing picks up a second generation of loss on the way here. 13 stay JPEG. WebP is plainly larger for most of them, and three are grainy enough that its filter smooths the grain instead of coding it: PSNR plateaus near 34 dB however high the quality goes, well under the floor. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* Switch DNS providers without a password prompt The network panel and the menu run omarchy-dns from a process with no terminal, so require_root reached for pkexec and put a polkit password prompt in front of what is meant to be a one-click toggle. Grant %wheel passwordless sudo for the three stock providers and take that path whenever the grant covers the invocation. Custom stays out of the grant: it points the machine at servers the caller supplies, and it already runs in a terminal that can ask. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Pick the elevation path without asking sudo The `sudo -n -l` probe answered the wrong question. It reports whether a command is permitted, not whether it is passwordless, and the %wheel rule every Omarchy install ships permits everything -- `sudo -n -l /usr/bin/rm -rf /tmp/x` exits 0. So the probe passed for Custom too, and the exec below it ran `sudo -n`, which fails outright with no terminal and no way back to pkexec. Decide from what the sudoers rule actually says instead: sudo when there is a terminal to type into, or when the resolved path and the provider are both ones the rule names. Everything else keeps going through polkit. Pin a root-owned PATH once elevated, too. `omarchy dev link` puts a user-writable checkout ahead of sudo's secure_path for every command, so a passwordless grant on a script that resolves nmcli, tee, and install through PATH would otherwise hand root to whoever can write there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Keep users outside %wheel on the polkit path The rule grants %wheel, so path and provider alone do not mean sudo will take it. A user outside the group was sent to sudo anyway, and with no terminal to answer the prompt that is a dead end -- polkit at least offers to authenticate as somebody else. Two holes in the test alongside it: it accepted any file containing the expected rule, so a second, argument-free line would have widened the grant unnoticed, and run as root it would have sailed past the stubs and rewritten the host's own DNS config. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Elevate the system install, whatever copy was invoked The rule names /usr/bin/omarchy-dns, so a dev-linked checkout handed sudo a path nothing could match and fell back to a polkit prompt. Re-exec the packaged path instead: the privileged half is the system install everywhere, the grant matches everywhere, and the path comparison and the PATH pinning that existed to work around the checkout both go away. Dev-linked checkouts run their own unprivileged half and the installed one as root, which is the trade for not carrying a second code path. --------- Co-authored-by: Omabot <david@hey.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
grant_covers re-implemented etc/sudoers.d/omarchy-dns in bash -- one of the three providers, and %wheel -- but never asked whether the rule was installed. It ships in the etc/ tree that omarchy-settings copies, so every machine still on an older settings package answers yes to a grant it does not have. require_root then execs into sudo with no way back, and the panel's one-click toggle dies on a password prompt it has no terminal to show. Ask sudo instead. `sudo -l` alone reports whether a command is permitted, which the blanket %wheel rule answers yes to for everything, but the long listing prints the matched entry's tags -- !authenticate is the grant and nothing else. It runs nothing, and under -n it prompts for nothing, so a machine without the rule falls through to polkit and gets a prompt on screen. The provider list and the wheel check go away with it; sudo owns that policy now, and it stays right if the rule is ever edited or removed. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
t3code-bin is in the Omarchy repo now, so the menu can offer it the way it offers Cursor and Grok Bot: install the package, then launch the desktop entry it ships. The mark is a trace rather than a download. T3 publishes no monochrome SVG — the app icon is a black rounded tile with the letters knocked out of it, and a tile flattens to a solid square once the menu recolors every path with the theme foreground. Tracing the lettermark out of that icon keeps the silhouette that actually reads. The font is package-owned, so the glyph reaches a desktop through an omarchy-settings release rather than omarchy update. Until that release lands, a pulled checkout draws the entry with no icon. 🤖 Generated by Opus 5 in Claude Code.
* Add Remove > AI for the apps Install > AI offers Install > AI grew five entries and Remove grew one: Dictation, sitting on its own at the top level. Everything else installed from that menu had to come back out through Remove > Package by name. Mirror the install tree instead. Dictation moves under the new AI submenu where its installer already lives, and ChatGPT Desktop, Grok Bot, LM Studio, Ollama and T3 Code get removers beside it. Each entry is conditional on the thing actually being installed, so the submenu only ever lists what is there. What each remover deletes was read off a machine that had all five installed and launched, not guessed. That matters most for T3 Code, which bootstraps the agents it drives: ~/.claude.json, ~/.grok, ~/.npm and ~/.local/share/opencode all appear the first time it runs, and all of them outlive it, so it takes only ~/.config/t3code and ~/.t3. Grok Bot is the same trap in miniature -- ~/.grokbot is its own, ~/.grok belongs to the Grok CLI. Ollama drops every acceleration variant rather than the one the installer happened to pick, and disables the service before the package, since that is what holds the models open. 🤖 Generated by Opus 5 in Claude Code. * Remove only what these apps own, and only where removal works Three defects from an independent review of the previous commit. ChatGPT Desktop was deleting ~/.cache/codex-runtimes, which belongs to the Codex CLI rather than the desktop app: the `codex` binary resolves its runtime and plugins out of that directory, and it ships in a package this remover does not touch. Removing the desktop app took the CLI's prepared runtime with it, leaving a separate, still-installed tool to rebuild it -- and unable to, offline. Ollama's row appeared whenever the `ollama` command existed, but omarchy-pkg-drop removes exact package names. With ollama-bin, ollama-git or a hand-built binary the entry offered a removal it could not perform: the service went down, /var/lib/ollama and ~/.ollama were deleted, and it reported success with the program still installed. Every acceleration variant depends on the base package, so testing for that package covers each one the installer can produce and nothing it cannot remove. LM Studio keeps its models under a relocatable home, and ~/.lmstudio-home-pointer is the only record of where they went. The remover deleted the pointer and the default path, so a user who had moved their models kept every one of them while being told they were gone. Read the pointer before deleting it, and refuse one aimed at / or at the home directory itself, since following it there would take everything. Co-Authored-By: Codex XHigh <noreply@openai.com> --------- Co-authored-by: Codex XHigh <noreply@openai.com>
* Use (( )) for the numeric argument test Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Drop the quotes on a variable inside [[ ]] Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Use omarchy-pkg-drop instead of raw pacman -Rns omarchy-pkg-drop already filters to installed packages, so the 2>/dev/null || true suppression is no longer needed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Drop defensive checks around default-set commands ttfx, imagemagick, and networkmanager are all in the default package set, so their commands are runtime invariants and should be invoked directly. Removing the nmcli guard also removes the degraded wifi fallthrough that only ran when nmcli was missing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The shell already shows a "Launching Windows…" OSD from the moment the desktop entry is activated until the RDP window appears, so the notification duplicated feedback the user is already looking at. The failure notification stays: nothing else reports a VM that never came up.
* Add a clock format with live seconds Right-clicking the clock now reaches "Thursday 09:39:23" and its AM/PM twin, and the widget's SystemClock ticks once a second only while a format that prints seconds is showing — every other format keeps the minute precision it had, so nobody pays for a repaint a second to read a label that changes once a minute. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Read an unterminated literal in a clock format as text Qt reads an opening quote with no closing one as a literal running to the end of the format, so "HH:mm 'sec" prints "09:39 sec" and never a second count — but the seconds test stripped only balanced quotes, saw the s, and put the widget on a per-second tick for a label that changes once a minute. The wiring assertions went the other way: each passed while the feature was broken, so hard-coding showsSeconds to false, dropping the label's onDateChanged, or commenting the precision line out and leaving the text behind all shipped green. Comments now come out of the source before it is matched, and both halves of the tick are asserted. Co-Authored-By: Codex XHigh <noreply@openai.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Codex XHigh <noreply@openai.com>
* Replace Gemini coding agent with Antigravity * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Remove the dead Gemini mise wrapper in the Antigravity migration Remove Preinstalls no longer lists gemini, so the wrapper Omarchy created would have stayed in ~/.local/bin with nothing left to clean it up. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Install Antigravity when it is the default a Gemini user is migrated onto The opt-out check skipped the install but the rewrite ran anyway, so anyone who had removed the preinstalls was left with a default agent naming a command that is not there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Fix Antigravity skill provisioning and Gemini wrapper migration - Wires Omarchy's default skills into Antigravity by linking them to ~/.gemini/config/skills/ in bin/omarchy-provision-user and migrations/1786719479.sh. - Fixes the Gemini wrapper migration in migrations/1786719479.sh to recognize and remove wrappers containing either `mise use -g "gemini"` or `mise use -g --quiet "gemini"`, while leaving hand-written wrappers intact. - Adds regression tests for both skill provisioning and wrapper removal in test/shell.d/default-agent-test.sh and test/shell.d/provision-user-test.sh. * Stop the provisioning test from retheming the session it runs in The test ran the real omarchy-provision-user, which sources install/user/all.sh and so reached omarchy-theme-set: hyprctl reload against the live compositor, gsettings against the live desktop, and a global Node install, none of which the skill symlinks it asserts need. Its mocks for omarchy-done and omarchy-refresh-applications were shadowed anyway, because provisioning prepends $OMARCHY_PATH/bin ahead of them, so stubbing the install suite at its own path is what a mock cannot do here. The exit status is checked rather than discarded: the assertion held even when provisioning died outright, because the symlinks are made twenty lines before the suite runs. * Match the Gemini default and wrapper the way Omarchy writes them The migration decided both questions differently from the code that owns them. It read the default agent with grep -qxF, while omarchy-default-agent takes the first line through read, so a padded " gemini " that the launcher still resolves was left naming an agent the launcher no longer supports. The wrapper it deletes was matched anywhere in the file, so a hand-written one that only mentions the installer's line in a comment went with Omarchy's own. Reading it the launcher's way and anchoring the match settles both against whoever wrote the file. The skills loop guards its glob the way migrations/1786539345.sh does, so an empty source cannot leave a symlink named "*" behind a migration already marked complete. Co-Authored-By: Codex XHigh <noreply@anthropic.com> * List Antigravity among the skill directories The manual named Claude Code, Codex, Pi and the generic location; provisioning now links ~/.gemini/config/skills too. Co-Authored-By: Codex XHigh <noreply@anthropic.com> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: David Heinemeier Hansson <david@hey.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Omabot <omabot@omarchy.org>
With the default scale = "auto", sync_internal_scale read the config, rejected "auto" as non-numeric, fell back to the hardcoded default 2, and force-applied it whenever the compositor's auto resolution differed. Since the script runs from omarchy-system-wake after every idle cycle, the panel flapped between 2 and auto's own value (1.5666667 on a 198 DPI panel) on every wake/reload pair. A config without a usable number -- "auto", or an expression only Hyprland's Lua can evaluate -- delegates the scale to the compositor: whatever it resolved for the enabled panel is the configured scale, so there is nothing to correct. Recovery of a disabled panel is unchanged and still re-enables it with the remembered scale, falling back to the historical default 2. Fixes #7265. Also the scale-revert half of #7301. Claude-Session: https://claude.ai/code/session_01L4Z6GimYhR1Kpsir24VAPF Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Given Finley's confusion
…usly (#7769) * Switch back to the packaged quickshell now that 0.3.1 kills synchronously Omarchy shipped the quickshell-git build for a single fix: 0.3.0's `kill` returned before the instance had exited, so the kill loop in omarchy-restart-shell could race a dying shell. Upstream 0.3.1 ships that fix, which makes extra/quickshell the better package to be on again — signed, versioned, and not rebuilt from a moving branch on every update. The migration swaps unconditionally instead of first checking which version the mirror offers. A machine left holding quickshell-git while the shipped package list names quickshell has no way to reconcile the two: omarchy-reinstall-pkgs installs that list with --needed, which does not skip a name that is not installed, and the conflict it then walks into has no answer under --noconfirm. A mirror that is briefly behind installs 0.3.0 instead and the next upgrade carries it to 0.3.1, which is much the cheaper way to be wrong. 🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh. Co-Authored-By: Codex XHigh <codex@openai.com> * Drop the quickshell version note from the shell restart loop The comment qualified the kill loop as needing 0.3.1 or newer, but omarchy-restart-shell ships in the same package upgrade that brings quickshell along, so a machine running this code already has the version the loop depends on. The caveat could never be false where it was read, which left it as version archaeology rather than something the code could not say for itself. 🤖 Generated by Opus 5 in Claude Code. --------- Co-authored-by: Codex XHigh <codex@openai.com>
* Only offer Update > Extra Themes when there is one omarchy-theme-update pulls the themes under ~/.config/omarchy/themes that came from a git clone, so on a machine that has never installed one by hand the row opens a terminal that prints nothing and closes. Guard it with the same predicates the command itself applies, since a row that shows over a symlinked theme or a worktree's `.git` file is the same dead end in a narrower shape, and pin the two to each other in the guard test. Co-Authored-By: Codex XHigh <noreply@openai.com> * Extract the Extra Themes guard into omarchy-theme-extras The row's `when:` and omarchy-theme-update each carried their own idea of which themes came from a git clone, and the two only matched because a test held them together. Name it once instead: omarchy-theme-extras lists those directories and exits nonzero when there are none, so the row asks exactly the command its action runs. Living in a script also puts the glob out of reach of whatever shopt a login shell left set for the guard batch. Co-Authored-By: Codex XHigh <noreply@openai.com> --------- Co-authored-by: Codex XHigh <noreply@openai.com>
* Add Super + Q as a second chord for closing a window Super + W stays the documented default. Super + Q is the chord people arrive with from macOS, where Command + Q quits the app, and typing it into Omarchy did nothing at all until now. 🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh. * Put an action's alternative chord on one keybindings row Super + W and Super + Q both read "Close window" in the menu, two rows apart, with nothing to say they were the same thing -- and the alternative sorted above the default. The scratchpad and the calculator had the same trouble, each bound to a chord and to a second key. Four actions are named as having an alternative, one at a time, and the second chord joins the first one's row. A rule would be wrong here: Alt + Tab and Shift + Alt + Tab both say "Reveal active window on top" while cycling opposite ways, and a media key is nobody's idea of an alternative to a Super chord. Both halves still have to agree on what they dispatch, since a label is only what a chord is called, and an unresolved dispatcher never counts as agreement. Nothing is allowed past the 35-character column: a pair that would overrun it stays as two rows rather than pushing its arrow out of line. The menu elides a row that outgrows its card -- 754px of label, 78 monospace characters at the heading size -- and the longest entry already sits at 74, so widening the column to fit the widest pair would have cost two dozen rows the end of their description. Priority ordering reads the rendered row, so the chord sharing it would otherwise reclassify the entry: XF86Calculator alone belongs in the tail kept for media keys, and it took the calculator down there with it. Ranking now reads the chord that leads the row. The key left of 1 reads as ~ rather than Hyprland's name for it, whether a bind names it or reports the keycode for the keymap to resolve. Cached records predate all of this, so the cache version moves with it. 🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh. Co-authored-by: Codex XHigh <noreply@openai.com> --------- Co-authored-by: Codex XHigh <noreply@openai.com>
* Animate the About logo with a passing glint fastfetch has no animation of its own, so the sweep is ours. Every frame is composed once up front and a tick writes one of them, repainting only the cells fastfetch drew the logo on, which holds 40fps for under 1% of a core and never reaches the module column six columns to its right. The logo is handed back exactly as it arrived: frames carry the colour fastfetch drew it in rather than a plain reset, so between glints the window is byte-for-byte the one it was before this change. Where the logo on screen might not be the text in the file, it stays still rather than guess — a fastfetch config in any directory searched ahead of Omarchy's own, a logo built from $1 colour placeholders or from a tab fastfetch expands itself, a window too small for the layout to fit without scrolling, and a shell whose locale is counting bytes instead of characters. A resize moves those cells, so a WINCH trap ends the sweep on the frame it happens rather than up to a poll later, which measured 10ms against 830ms. The polling stays as the backstop for a signal that arrived while it could not be taken. 🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Codex XHigh <noreply@openai.com> * Give the sheen a file of its own The launcher was carrying two things: what a glint looks like, and what it is drawn over. Only the second is about fastfetch. bin/omarchy-logo-sheen now knows how to lean a band of light across an ASCII logo and nothing about About, and it is handed where the logo sits, what colour to give its cells back, and how much room it has. When a frame may be written stays with the launcher, because that is inseparable from how the window closes and resizes. The tests split along the same seam, and the sheen's half no longer strips the launcher's tail to reach the code it tests. 🤖 Generated by Opus 5 in Claude Code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Leave a logo still when one character is not one cell The frames slice the logo by character and the terminal draws it by column, so the sheen only puts a row back where it found it while those two agree. A double-width glyph, a combining mark and a joined emoji each break that, and a cut through one of them renders as something else — pushing the rest of the row right, into the module column, with nothing on screen to say the logo moved. A user can put any of the three in the logo by editing the text. Comparing the row's character count against its width in columns is the assumption itself, so it is what gets checked, rather than a list of the glyph classes that happen to break it today. 🤖 Generated by Opus 5 in Claude Code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Size the About window from the layout fastfetch drew The fit predicted the content height from the logo and the module column, taking the taller of the two. Once the logo is the taller one fastfetch writes a row more than that arithmetic expects, so every logo of 27 rows or more got a window one row short and scrolled its top padding away. The shipped logo is 26 rows, which is why it never showed. Ask fastfetch how tall the layout came out instead of predicting it. That measurement was already being taken for the sheen, which refuses to animate a layout that scrolled — so a tall logo used to lose the glint as well as the padding row. 🤖 Generated by Opus 5 in Claude Code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Leave the logo still when NO_COLOR asks for none fastfetch drops the logo's colour when it writes to a terminal that set NO_COLOR, but not when it writes to the substitution that measures it. The colour to hand those cells back in was therefore measured as green while the logo on screen had none, and the first glint would have left the whole logo green. It stops suppressing autowrap there too, so a narrow window soft-wraps and scrolls where the measurement cannot see it and the frames go on addressing rows that moved. Both follow from animating a screen drawn in a mode the measurement did not reproduce. A glint is colour besides, which is the thing NO_COLOR asks for none of. 🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Codex XHigh <noreply@openai.com> * Read fastfetch's config paths whole, and do not read silence as an answer Splitting each listed path on whitespace dropped everything after the first space, so a config under a home directory containing one was missed and the fit and the sheen went ahead against a layout neither had measured. The marker fastfetch prints beside the config it settled on is not part of the path either. A listing that failed was also indistinguishable from one that found nothing, because the status of a process substitution is discarded, so a fastfetch that could not enumerate read as "no custom config". Fall back to the directory fastfetch has always looked in first rather than take silence for an answer. 🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Codex XHigh <noreply@openai.com> * Notice a resize that lands while the grid is being read The sweep read the WINCH flag before the grid rather than after it. A signal arriving during the two command substitutions that read the grid is delivered only once they finish, so the flag was still false when it was tested, the grid still compared equal, and one more frame went out at coordinates the resize had already moved. Read the flag last, after the check it could have landed during. 🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Codex XHigh <noreply@openai.com> * Check that the render loop is what plays the sheen Every builder the tests drive can be exercised while nothing on screen ever animates. Replacing the render loop's animated branch with the still one it replaced left all of them passing, so nothing was holding the animation onto the screen it belongs on. 🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Codex XHigh <noreply@openai.com> * Name the animation after the branding it animates omarchy-logo-sheen sat in a group of its own, and a group whose only command is hidden still gets a header printed for it with nothing underneath. It belongs beside omarchy-branding-about, whose art it animates: the group already exists, the two halves of About branding are next to each other, and `omarchy branding about` still routes to the command rather than the helper now sharing its prefix. The tests take the names of what they cover, the way the rest of the suite does. 🤖 Generated by Opus 5 in Claude Code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Fewer moving parts in the sheen Four questions about a logo turned out to be one. Whether the shell is counting characters or bytes, whether a tab or an escape is in the line, whether a glyph is double-width or a combining mark or a joined emoji — each was asking whether one character is one cell, so that is the only thing asked now. It is also less strict in the one case that deserves it: plain ASCII art animates in a C locale, where the locale probe used to refuse everything. One band of light instead of a white core inside a green halo, which is three colour spans a row rather than five and no helper to clamp four cuts with. Only the left cut needs clamping at all: a slice starting past the end of a line is already empty, while a negative offset would count from the end of it. One loop instead of two, because an empty frame list plays nothing. A logo that cannot be animated now waits in the loop the animated one rests in rather than in a second copy of it, and the build that failed leaves no frames behind, since that loop plays whatever it finds. The logo's colour comes out of one match rather than a loop eating one escape at a time. 🤖 Generated by Opus 5 in Claude Code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Say in the manual which art the glint leaves alone The line promised the glint to any art in the file, and the code does not: art whose characters are not one column wide is left still on purpose, as is the whole screen for anyone keeping a fastfetch config of their own. Somebody reading the old line and seeing a still logo would take deliberate behaviour for a broken feature. Name the condition rather than the list of glyph classes behind it, and say that both modes of Set From Image meet it, because that is the path almost everyone is on. 🤖 Generated by Opus 5 in Claude Code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Draw the band in a colour bold cannot collapse A terminal can be told to render bold text in a brighter colour — foot's bold-text-in-bright, whose palette-based form brightens a bold regular colour into its bright counterpart. fastfetch draws the logo bold green, so under that setting the logo is already bright green, which is the colour the band was using: the glint came out the same green as the art and nothing appeared to happen at all. None of the four terminal configs set it, so it was waiting on whoever turned it on. Bright white instead, because no regular colour brightens into it, and the band shows whatever the terminal does with bold. Narrower with it, since white against green needs less width to read than a second green did. 🤖 Generated by Opus 5 in Claude Code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Codex XHigh <noreply@openai.com>
* Ask the terminal whether the layout scrolled Whether the logo is on the rows the frames address was decided by arithmetic: count the lines of a second fastfetch run, and animate if the window has one more. That holds only while the second run and the real one agree, and they need not — a module that appears between them, a wrap the measurement never sees, anything that makes the painted layout a row taller than the counted one. The screen then scrolls, the logo sits a row above where these frames draw it, and the animation paints a copy of the logo one row below the real one. fastfetch has just painted when the question is asked, so the terminal already knows the answer: an unscrolled layout leaves the cursor one row past its last line. Ask for it, and keep the arithmetic for a terminal that will not say. The reply has to be read without a command substitution around it, or the query goes into the substitution's own pipe instead of to the terminal, and what comes back is the escape rather than a row. 🤖 Generated by Opus 5 in Claude Code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Find the logo in the render instead of assuming where it is The row and column the frames draw on came from the padding in this repo's fastfetch config. The config that runs is the one in /etc, and `omarchy dev link` says plainly that it does not replace files at fixed system paths — so the two can disagree, and nothing in the code would notice. A logo fastfetch drew on row 1 or row 5, redrawn on row 3, is a logo that jumps the moment the animation starts and stays where the animation put it. fastfetch's own output is already being captured to measure the layout's height, so the logo can be found in it: take the longest line of the file as a landmark, look for it in the render, and turn where it was found back into where the logo starts. Padding of any size lands correctly, and a render that does not contain the file's own text is not this logo at all — a config that restyled it, a placeholder that was substituted — so there is nothing to animate and it is left alone. That last part subsumes what the config-path check was guessing at. It is kept, because it also decides whether the window may be fitted, which happens before there is a terminal to measure anything in. 🤖 Generated by Opus 5 in Claude Code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Take the cursor query back out Asking the terminal where the cursor came to rest was meant to see a scrolled layout rather than infer one. It cannot: a linefeed at the bottom margin scrolls the screen and leaves the cursor on the bottom row, so a render that fitted exactly and one that scrolled both answer with the same number. Measured on a real terminal — a 29-line layout in windows of 30, 29 and 28 rows answers 30, 29, 28. The guard therefore only ever agreed with the arithmetic it replaced, and only in the case the arithmetic already had right. What it did add was a read on the same stdin the keypress that closes About arrives on. A key pressed while the query is outstanding is swallowed; a reply that arrives after the timeout is read by the next tick as the keypress, and the window closes on its own; typed text of the right shape is accepted as an answer, because nothing checked for the introducer; and the query is written before the read turns echo off, so a fast reply can be echoed onto the screen. None of that buys anything the line count did not already give, so it goes. 🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Codex XHigh <noreply@openai.com> * Fit the About window with a little room to spare The window was sized to exactly what was measured: the columns the widest module line needed, and one row past the layout's last line for the cursor. That measurement is taken once, and then the content goes on living. An uptime turns minutes into hours and hours into days, a version string grows, a module shows up after the next boot — and a window with nothing in hand clips at the right edge or scrolls the top padding away the moment any of it happens. A scrolled layout also moves the logo off the row it was drawn on, which is where a shifting logo comes from. Two columns and a row, which is invisible on screen and enough for anything that ticks over. The remembered size in about.fit is keyed on the logo, so the first launch after this still opens at the old size and the fit corrects it there and then. 🤖 Generated by Opus 5 in Claude Code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Codex XHigh <noreply@openai.com>
Omarchy Quattro: Release Candidate
Omarchy 4 aka Quattro is the biggest release since the project started. The entire desktop shell has been reimagined in Quickshell: the bar, launcher, menus, notifications, on-screen displays, control panels, lock screen, and polkit agent now all live inside a single long-running shell process with a plugin architecture. That means Waybar, Walker, Mako, SwayOSD, hyprlock, hypridle, swaybg, and polkit-gnome are all gone, replaced by one coherent, fully-themed, IPC-scriptable shell.
You can upgrade an existing Omarchy installation to Quattro by first
Update > Omarchy, thenUpdate > Channel > RC, thenUpdate > Omarchy to Quattro. Just remember toUpdate > Channel > Stableafter the final release!Or install it fresh on a secondary machine using the ISO: https://iso.omarchy.org/omarchy-4.0.0.rc4-2.iso
Always take a backup of important data!
Headline Features
Setup > Reset Computer) from new Quattro installs for safe resale/gifting of your machine.Super + Space).The Shell
Setup > Menu Barin the Omarchy menu)omarchy plugin add <git-url>, and managed fromSetup > Plugins(add, clone, enable, disable, remove) by @ryanrhughes and @dhhSUPER + SPACEsearches apps and commands from one surfaceSUPER + ALT + SPACESUPER + SHIFT + ALT + ,brings back the last ten notifications exactly as they were shown, including the ones do-not-disturb silenced — and popups on screen survive shell restarts (like the one every update performs), so critical alerts always make it across by @ryanrhughes and @dhhSUPER + CTRL + V)SUPER + CTRL + E) replacing Walker pickerSUPER + CTRL + A), Bluetooth (SUPER + CTRL + B), Network (SUPER + CTRL + W), Display (SUPER + CTRL + D), and Power (SUPER + CTRL + P) replacing bluetui, impala by @dhh and @ryanrhughes~/.config/omarchy/extensions/omarchy-menu.jsonc) by @ryanrhughes and @dhhSUPER + CTRL + 1through9, counted left to right — rearranging the section or adding a widget renumbers them with no binding to rewrite by @dhhTheming & Aesthetics
SUPER + SHIFT + CTRL + SPACE) by @dhhSUPER + CTRL + SPACE) by @dhh~/.config/omarchy/shell.tomlas a machine-level override merged over the active theme, so a personal font, spacing, or bar tweak survives theme switching — and it's watched, so edits re-flow the shell liveomarchy display text size: one knob, 9–20px, that moves the shell font, GTK's text-scaling-factor, and the terminal point size in lockstep, with a notched slider in the display panelApps & Defaults
SUPER + SHIFT + W) by @dhhSUPER + CTRL + Q) by @dhhSetup > Defaults > Agent, then launch it withSUPER + SHIFT + CTRL + Aor theaterminal alias — lazy-installed on first use, opened as its ownorg.omarchy.agentapp, and started in~/Workwhen summoned from home so trust actually sticks by @dhhAlt + Shift + D) by @dhhInstall > AI, dropping the web app from the defaults so the launcher can't show two identical entries by @dhhfindof the whole home directory in a terminal by @dhhCapture
SUPER + ALT + [ / ]) by @dhh (on request from @jasonfried)RETURNcaptures the highlighted window,CTRL + RETURNthe whole display, andTABand the arrow keys move the selection through the workspace's windows by @jzimdars and @dhhotpauth://setup code on screen no longer needs a phone. The decoded value never touches disk and is marked sensitive, so clipboard history skips it by @dhh and @WondertanControls
o.bind("SUPER + SHIFT + W", "Omawrite", { launch = "omawrite" })) by @ryanrhughesSUPER + ALT + HOME/SUPER + HOMEby @dhhhdl/hds/hdlm/hsldevelopment-layout helpers by @dhhSUPER + ALT + K) and new-pane/split bindings for tmux, kitty, ghostty, and alacritty by @ryanrhughes, @vtmx, @heymaikol, and @dhhSUPER + CTRL + RETURNand its keybindings viewer withSUPER + CTRL + K, mirroring the tmux pair — plus tmux's tab moves, zoom flag, hostname in the window title, and hidden outer pane frame carried over by @dhhNetworking/Connections
System & Upgrade
/etc, so updates flow through pacman by @ryanrhughesomarchy update(bypassable viaOMARCHY_ALLOW_DIRECT_PACMAN=1) by @ryanrhughesomarchy updateon current releases instead of waiting out the release cooldown by @dhhpre-refresh-pacman.dhooks for custom repository support by @tyvsmithTrigger > Speed Test, sharing the live dial interface with the network test by @dhhSetup > Reset Computer: swap the running root for a fresh clone of the@factorysnapshot the ISO takes at install time, scrub machine identity, accounts, and fingerprint enrollments, and hand the machine back in that untouched first-boot state. A machine with no factory snapshot is turned away with an explanation rather than given a partial wipe by @dhhdiagnose-crashskill that covers symbolizing the backtrace and — only with your agreement, and after a duplicate search — reporting a confirmed Omarchy bug upstream by @dhhpaccache -rk2as the first step of an update, before the snapshot so the space is actually reclaimed, and keeping one spare version so the offline downgrade path survives by @dhhFixes
fipand friends misparsing under zsh by @pkwagnerpipefail, rerun the T2 defaults migration they skipped, and choose the gmux display backlight instead of the Touch Bar on T2 Macs/booton encrypted installs, fixing direct-boot setup and stale-UKI cleanupeflag, an unescaped VS Code theme name, unvalidated keyboard RGB), and drop the unconstrained tzupdate sudoers grant that let any wheel user write a root-owned symlink anywhere by @KazeTachinuu\uescapes instead of characters by @dhh~, which cost btop, Helix, and VS Code their theme while the migration reported success by @shrijit37--helpbeing ignored for commands that resolve with arguments left over, likeomarchy update aur --helpby @EFrMG and @dhhThanks also to @AksharP5, @artfwo, @dalmasluca, @DataDave-Dev, @DiegoMirner, @flavorjones, @franciscoaccabral, @GebaRoanoke, @glafeara, @grantwiley, @heyssh, @JustMrMendez, @kevinmcconnell, @lukehsiao, @lukewalker2010, @markbus-ai, @meirdick, @monorkin, @richardlences, @RushiChaganti, @s-gato, @setiapam, @Vtorrealba, and @vivek7405 for fixes and polish to the new Quattro features during the beta itself.
Feature presentations
The theme switcher
Switching themes is now a more visual affair: a filterable carousel of live previews for every installed theme, opened with
SUPER + SHIFT + CTRL + SPACEor via Style > Theme. You can see the new bar riding on top too.The background switcher
Backgrounds get the same treatment on
SUPER + CTRL + SPACE: flip through the current theme's backgrounds (plus any you've added) and see exactly what you're picking.The launcher and the menu, together
Walker is gone, and
SUPER + SPACEnow opens the Omarchy menu itself: a native, filterable command palette living inside the shell. Once the menu could search its own nested entries, there was no reason to keep two palettes with two shortcuts around — so one box now finds your apps and every Omarchy command, with instant response, and it's extensible through a JSONC file if you want to add your own entries.The app-launching side lost nothing in the merge: fuzzy and acronym matching, live icon indexing (freshly-installed apps show up with their icons immediately), and the ability to hide entries you never use. And when you want a launcher scoped to just your apps,
SUPER + ALT + SPACEopens exactly that.Control panels
The bar widgets now open real control panels. Audio (
SUPER + CTRL + A) has output/input device switching and volume control:Network (
SUPER + CTRL + W) shows live throughput, ping, packet loss, a one-click speed test, DNS provider selection, Wi-Fi QR sharing, and nearby networks:And weather now has a proper forecast panel, which can be pinned to a chosen location instead of following your IP around:
A bar you can grab
There's no settings panel for the bar — you just grab it. Click and hold any empty stretch of bar and drag it toward a screen edge, and a ghost slab previews where it will dock. Drop it on the left or right and it turns into a vertical bar. Double-click empty bar space to toggle transparency. Widgets are added, removed, and rearranged with
omarchy bar putandomarchy bar move, and third-party widgets install straight from git withomarchy plugin add.Privilege escalation with context
Omarchy now uses pkexec/polkit for privileged operations, with a themed prompt that tells you exactly what command you're authorizing before you type your password.
Pick your coding agent
Omarchy no longer picks a coding agent for you — it asks. Choose Claude Code, Codex, OpenCode, Pi, Oh My Pi, Gemini, Grok, Copilot, or Crush under Setup > Defaults > Agent, and it's lazy-installed the first time you use it. From then on,
SUPER + SHIFT + CTRL + A(or justain any terminal) summons your agent in its own window, started in~/Workso trust actually sticks. The model-usage bar widget keeps an eye on your Claude and Codex spend, and when a process crashes, one click on the toast briefs your agent on the core dump for a diagnosis.The new default apps
Typora is out, and Omawrite is in. A dead-simple Markdown writing app that opens in a blink, autosaves as you type, and stays out of the way. Bound to
SUPER + SHIFT + W. Free software.It's joined by Omacut, a dead-simple video trimmer. Drop in a clip — like the screen recordings Omarchy takes — drag the handles on the filmstrip, and export the cut with ffmpeg doing the heavy lifting.
Omacalc completes the trio: a dead-simple calculator replacing GNOME Calculator, on
SUPER + CTRL + Q.And the defaults have been refreshed elsewhere too: Foot is the new default terminal, Tensaku replaces Satty for image annotation, Moonlight ships for Sunshine game/desktop streaming, and dua handles disk usage.
Install it for someone else
An install can now finish with no user at all. Deferred first-boot provisioning means you can set up a machine — for a family member, a colleague, a whole classroom — and hand it over untouched. On the first boot, the owner picks their keyboard, account, hostname, and timezone behind the same logo, progress bar, and rotating tips the ISO installer uses. On an encrypted disk, the LUKS volume is re-keyed from the throwaway install passphrase to the owner's password, all-or-nothing.
And when the machine comes back,
Setup > Reset Computerhands it over again: the running root is swapped for a fresh clone of the@factorysnapshot the ISO takes at install time, with machine identity, accounts, and fingerprint enrollments scrubbed — back to that untouched first-boot state.This PR description is up to date as of
144f4d1e; next time, review commits after this one.