Compare commits

..
35 Commits
Author SHA1 Message Date
Mehmet INCEandClaude Opus 5 13f18b2cb7 [Security] Stop the FIDO2 setup staging its authfile at a predictable /tmp path (backport of #7904)
Backport of the FIDO2 authfile fix (PR #7904 by @mdisec, merged to quattro as
23dab9ec) onto the v4-0-1 release branch.

pamu2fcfg wrote to /tmp/fido2 and the registration was then moved into place
with sudo mv. Any other local user can pre-create /tmp/fido2, and rename(2) does
not dereference the final component, so the privileged move installed the
attacker's symlink itself as pam_u2f's global authfile -- a file consulted by
sufficient lines in /etc/pam.d/sudo and /etc/pam.d/polkit-1.

The same move also carried the staged file's ownership into /etc, so on every
install to date /etc/fido2/fido2 is owned by the invoking user at mode 0644.
That needs no attacker, no race and no second account: anything running as that
uid can append its own credential and satisfy the machine's sudo prompt without
knowing the password.

The setup now creates a unique staging file as root beside the final authfile
and pipes pamu2fcfg into it, so root never reopens a caller-owned pathname,
rejects failed or empty enrollment output, publishes with an atomic mv -Tf,
cleans the exact staging file after every failure, refuses non-regular authfile
states, and installs root:root 0644. A migration repairs machines set up by
earlier versions by replacing the inode rather than chowning in place: a process
that already holds a writable descriptor on the legacy user-owned mapping keeps
it, so the repair has to leave that inode behind where PAM no longer reads it.

Clean cherry-pick: all six files are byte-identical to quattro, so merging
v4-0-1 into quattro resolves without a conflict. The migration's timestamp
(1787494718) is older than others already on this branch, which is harmless:
omarchy-migrate marks each migration by filename rather than tracking a
watermark, so this one runs on every machine that has not run this exact file.

test/shell passes: 195 files, including the three this adds -- the setup's
staging and failure paths, the migration's repair and unrepairable states, and
the removal's symlink handling. test/cli passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DEMYa9UWtroz93DhMTtcV
2026-08-25 12:12:38 +02:00
Adrian RangelandClaude Opus 5 2e989e35e5 [Security] Stop USB device names from being executed as Hyprland Lua (backport of #8129)
Backport of the input-device name fix (PR #8129 by @acrogenesis, merged to
quattro as 9285b19d) onto the v4-0-1 release branch.

Hyprland input-device and monitor names come from USB descriptors and hyprctl
output, so they are attacker-influenceable, yet the toggle and monitor commands
interpolated them straight into hyprctl eval and into generated Lua that
Hyprland re-executes on every reload. XF86TouchpadToggle is bound with
locked = true, so a malicious USB name reached Lua execution from the lock
screen as the logged-in user, and a persisted disable made it run on every
start. Publicly reported by Jorrit Jongma / Chainfire.

The disable is no longer executable Lua anywhere. The device name is stored as
plain-text data in a *-disabled-name sidecar and read back by a packaged module,
default/hypr/disabled-input-device.lua, on every reload; the live hyprctl eval
Lua-quotes the name and rejects control characters outright. The reload loader
excludes the two legacy filenames, so a leftover generated *-disabled.lua on a
not-yet-migrated install can never be sourced as code again, and a migration
recovers the device name from it and deletes it, sanitizing installs that ran
the vulnerable version. All four monitor scripts validate an output name against
a plain-connector-name pattern before writing it as Lua, closing the same latent
pattern in the siblings, and paths.lua treats a set-but-empty XDG_STATE_HOME as
unset to match the bash side.

Clean cherry-pick: all fourteen files are byte-identical to quattro, so merging
v4-0-1 into quattro resolves without a conflict. This branch ships no leftover
*-disabled.lua template of its own -- the tracked "disabled" files are the same
two quattro has -- so the migration is the only path that has to sanitize
anything here.

test/shell passes: 192 files, including the three this adds. The toggle suite's
public-PoC case passes here, as do the monitor scripts' accept/reject cases and
the XDG path cases. test/cli passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DEMYa9UWtroz93DhMTtcV
2026-08-25 11:07:35 +02:00
Mehmet INCEandClaude Opus 5 c6d676f23c Pin trusted PATH in privileged DNS helper (backport of #8172)
Backport of the DNS PATH pin (PR #8172 by @mdisec, merged to quattro as
4637735a) onto the v4-0-1 release branch.

omarchy dev link prepends a user-writable checkout's bin/ to sudo's secure_path
so privileged Omarchy commands resolve to the development versions, and that
reaches the subprocesses they launch too. This branch carries the same
passwordless grant -- etc/sudoers.d/omarchy-dns lets wheel run
/usr/bin/omarchy-dns Cloudflare, Google and DHCP without a password -- so the
packaged script ran as root while resolving bare helpers (dirname, install,
tee, rm, nmcli, systemctl, awk) through the caller's secure_path. Write access
to a dev checkout became arbitrary root execution, with no password prompt in
the way.

Pin PATH to trusted system directories once EUID is 0. The restriction lands
only after elevation, so the unprivileged wrapper phase keeps the caller's PATH
and can still find sudo or pkexec; every helper the privileged half uses is a
system utility, so it needs nothing from the checkout.

Clean cherry-pick: both files are byte-identical to quattro, so merging v4-0-1
into quattro resolves without a conflict. Verified by mutation: with the pin
removed, test/shell.d/dns-sudoers-test.sh fails at the poisoned-helper case;
restored, all four of its cases pass. test/shell (189 files) and test/cli pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DEMYa9UWtroz93DhMTtcV
2026-08-25 09:44:35 +02:00
Sameer PokharelandClaude Opus 5 0f836c0deb Renumber the README manual TOC after Extra themes was dropped (backport of #8089)
Backport of the TOC renumbering (PR #8089 by @samirpokharel, merged to quattro
as b86d4505) onto the v4-0-1 release branch.

Dropping manual/43-extra-themes.md shifted every chapter after it down by one,
on this branch as much as on quattro. The TOC rode along on the old numbers, so
its last ten links pointed at files that are not there: resolving every
manual/*.md link in this branch's README before the change found ten that do not
exist, from 43-extra-themes.md through 52-unattended-installs.md. After it, all
of them resolve.

Clean cherry-pick: the TOC now matches quattro's, so merging v4-0-1 into quattro
resolves without a conflict. test/shell passes: 189 files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DEMYa9UWtroz93DhMTtcV
2026-08-25 09:41:59 +02:00
OmarchybotandClaude Opus 5 07adef8a53 Let a received Taildrop file wait to be answered (backport of #7953)
Backport of the Taildrop toast fix (PR #7953, merged to quattro as 7e469f96)
onto the v4-0-1 release branch.

A delivery can land hours after it was sent, and the toast announcing it was
expiring after five seconds -- so a file that arrived while nobody was at the
machine was gone from the screen before anyone could click it open. Critical
urgency is what the shell reads as a popup that lives until it is clicked or
dismissed, the same thing omarchy-crash-watch uses to keep its click-to-diagnose
toast around.

The mechanism is already on this branch: omarchy-notification-send parses
options that follow the two positionals, and NotificationLogic.js gives a
critical popup duration 0, which never expires.

One conflict, in test/shell.d/notification-send-test.sh. #7953 added coverage
for the trailing-option path in the notify-send form it had then; #7926, which
merged after it on quattro and is already backported here, rewrote that file for
the Notify D-Bus form and carried the same coverage across. This branch
therefore already asserts what #7953 added -- an urgency and a glyph after the
description reach the call, and the urgency is set once -- so the branch's
version stands and the resolved file is unchanged.

Verified by mutation: with -u critical removed, test/shell.d/tailscale-receive-
test.sh fails at once; restored, it passes, including the new assertion that
every announcement waits to be answered.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DEMYa9UWtroz93DhMTtcV
2026-08-25 09:39:50 +02:00
OmarchybotandClaude Opus 5 e713ff3166 Share the git URL check, and refuse the transports Omarchy does not clone from (backport of #8174)
Backport of the shared URL check (PR #8174, merged to quattro as 68ab12f7) onto
the v4-0-1 release branch, on top of the #8067 backport it follows.

omarchy-theme-install and omarchy-plugin-add both clone a URL a stranger can
choose, and each carried its own copy of the rule that refuses a git option or a
transport helper before cloning. The rule now lives in omarchy-git-url-check and
both callers ask it: two copies of a security check drift, and the second copy
arrived four months after the first only because someone went looking for it.

That rule was also enforcing half of what it described. <helper>::<address> is
one of two shapes git resolves a remote helper from -- it also runs
git-remote-<scheme> for <scheme>://<address> whenever the scheme is not one it
connects itself, so ext::sh -c id and ext://sh -c id reach the same helper while
only the first was refused. Nothing exploitable follows on a stock system:
protocol.ext.allow defaults to never, and the :// spelling hands git-remote-ext
a command name it cannot exec. But a third-party helper installed on PATH is
reachable through the scheme form alone, and a guard is worth more when it
enforces the rule it states.

The :// shape cannot be refused the way :: is, because it is also how every
legitimate URL arrives, so the scheme is checked against the transports git
still connects itself: ssh git git+ssh ssh+git http https ftp ftps file.
git+ssh and ssh+git are on that list because they are spelled like a helper and
read as plain ssh; leaving them off would refuse a URL that clones today. ext
and fd are off it deliberately. A single colon is always scp-style ssh and a
bare path is always a path, so neither needs constraining.

The check fails closed: the callers read a non-zero status as a refusal, so a
missing omarchy-git-url-check refuses the URL rather than waving it through.

Clean cherry-pick on top of the #8067 backport: every file is byte-identical to
quattro, so merging v4-0-1 into quattro resolves without a conflict. test/shell
passes: 189 files, including the one this adds. test/cli passes, so the new
command's metadata is well-formed. Exercised the check here: https, scp-style,
ssh, git+ssh and scp-style IPv6 all pass, while ext:: ext:// fd:: fd:// and a
leading-dash form are refused, as is an uppercase EXT:// spelling.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DEMYa9UWtroz93DhMTtcV
2026-08-25 09:13:35 +02:00
OmarchybotandClaude Opus 5 c7af36d0aa Offer to reboot when toggling sudoless Docker; show only the relevant menu entry (backport of #8098)
Backport of the sudoless Docker follow-up (PR #8098, merged to quattro as
06a3dbca) onto the v4-0-1 release branch, on top of the #8056 and #8080
backports it follows.

Group membership only takes effect on a fresh session, and in practice a logout
or newgrp is not enough -- only a reboot reliably applies it. The setup and
remove commands now flag the reboot and offer to do it right away with a gum
confirm, the same shape as the GPU toggle, and their notices say "after a
reboot" instead of pointing at logout or newgrp. The existing-user migration
reuses the removal command inside omarchy update, so it passes
OMARCHY_DEFER_REBOOT to skip the prompt there and lets omarchy-update-restart
handle the reboot once the whole update has finished.

Setup > Security showed Sudoless Docker under both Setup and Remove, and the
guards tested the running session's groups, which do not change until the
reboot: after enabling sudoless Docker the menu still offered Setup, the one
action that could no longer do anything, while Remove stayed hidden. Add
omarchy-sudo-docker as the single answer to both questions that differ in that
window -- by default whether this session can reach the socket, which is what
decides if a command must elevate, and with --configured whether the account is
set up for it, which is what the menu and the toggles need. It succeeds when
sudo is needed, so the Setup entry appears while sudoless Docker is off and
Remove once it is on. lazydocker and the Windows VM keep prompting until the
reboot lands.

Clean cherry-pick on top of the earlier backports: every file is byte-identical
to quattro except default/omarchy/omarchy-menu.jsonc, which merged into this
branch's menu and whose two Sudoless Docker lines match quattro exactly.
test/shell passes: 188 files, including the two this adds. test/cli passes, so
the new command's metadata is well-formed. Exercised the helper here: with no
socket it reports sudo is needed, and --configured answers from the account's
groups.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DEMYa9UWtroz93DhMTtcV
2026-08-25 09:02:57 +02:00
BastiandClaude Opus 5 b3028f9bd9 Guard plugin-add against git transport-helper URLs (backport of #8067)
Backport of the plugin-add URL guard (PR #8067 by @bastidotnet, merged to
quattro as 30471bf3) onto the v4-0-1 release branch.

omarchy-plugin-add cloned a user-supplied git URL without the transport-helper
guard omarchy-theme-install already applies. That guard arrived with #7884,
which is on this branch, but it never touched plugin-add -- so the sibling
command still leaned entirely on git's own protocol.ext.allow=never to keep a
URL like ext::sh -c <cmd> from running a command at clone time.

Stock systems are unaffected: Omarchy sets no protocol.* override, so the git
default holds and there is no live exploit here. This is defense in depth --
it closes the gap #7884 left in the sibling path and drops a silent dependency
on a default the project does not control. Reject ext::/fd:: and leading-dash
forms; https, ssh, scp-style and token-auth URLs still clone, including an
scp-style IPv6 host, which carries :: of its own.

Clean cherry-pick: both files are byte-identical to quattro, so merging v4-0-1
into quattro resolves without a conflict. The guard reads the same as the one
already in bin/omarchy-theme-install on this branch. test/shell passes: 186
files, with the plugin-add suite's new cases all running here -- including the
pty-driven prompt case, which is the only path that reaches the guard's
leading-dash arm.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DEMYa9UWtroz93DhMTtcV
2026-08-25 08:32:58 +02:00
David Heinemeier HanssonandClaude Opus 5 0f4abeec8e Wait for the keypress ourselves instead of asking gum to (backport of #8082)
Backport of the keypress fix (PR #8082, merged to quattro as 5d3299fb) onto the
v4-0-1 release branch.

gum 2.0 runs a spun command without the terminal attached, so the gum spin --
read -n 1 that held the presentation terminal open returned at once. Every menu
command that ended in a failure took its window down with it before the error
could be read, which is how a failed update looked like a terminal that just
quit.

Read the key directly. gum's own terminal query replies are still sitting on the
tty when the spinner stops, so drain those first or they answer the prompt on
the user's behalf. The /dev/tty node is there whether or not a terminal is
behind it, so open it rather than test for it -- the existence check passed on a
headless run and left both reads failing with "No such device or address" -- and
prompt on the terminal rather than stdout, so a caller that redirects us does
not leave the user waiting on a prompt they were never shown.

The green dot reads better than the globe did, so the provisioning notice uses
it too and drops its spinner along the way.

Clean cherry-pick: both files are byte-identical to quattro, so merging v4-0-1
into quattro resolves without a conflict. Exercised against the installed gum
2.0.0: headless it exits 0 at once, and on a pty it prints the prompt and
returns on one keypress. test/shell (186 files) and test/cli pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DEMYa9UWtroz93DhMTtcV
2026-08-24 19:58:40 +02:00
OmarchybotandClaude Opus 5 7b89780810 Flag a reboot when the docker group changes (backport of #8080)
Backport of the reboot flag (PR #8080, merged to quattro as 1565919c) onto the
v4-0-1 release branch, on top of the #8056 backport it follows.

Group membership is fixed at login, so removing (or adding) the docker group
does not take effect in the running session. The existing-user migration and the
Setup > Security toggles now call omarchy-state set reboot-required, so
omarchy-update-restart prompts for the reboot that actually applies the change
and the bar shows it pending. A plain log out and back in still works.

The migration test now exercises the real removal command and omarchy-state
rather than a stub, asserting the reboot flag is set on removal and left alone
when the user is already out of the group.

Clean cherry-pick on top of the #8056 backport: all three files are
byte-identical to quattro, so merging v4-0-1 into quattro resolves without a
conflict. omarchy-state and omarchy-update-restart are unchanged on this branch
and read the same ~/.local/state/omarchy/reboot-required marker. test/shell
passes: 186 files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DEMYa9UWtroz93DhMTtcV
2026-08-24 19:56:19 +02:00
OmarchybotandClaude Opus 5 c0b593b349 Don't put the user in the docker group; make it opt-in (backport of #8056)
Backport of the docker group removal (PR #8056, merged to quattro as b5ded31e)
onto the v4-0-1 release branch.

The docker group is root-equivalent: anything in it can docker run -v /:/host
and rewrite the host as root with no password. On a single-user box that is not
an escalation -- the owner is already a wheel user -- but it hands any code
running as the user, a rogue plugin or a poisoned dependency, a silent,
headless, passwordless path to root that sudo's password prompt would otherwise
gate.

Stop granting the group by default. The daemon still runs, the Docker TUI and
the Windows VM reach it through a polkit prompt, and the plain docker CLI runs
under sudo. Sudoless Docker becomes a warned opt-in under Setup > Security, and
no automatic path re-grants it: install and first-boot provisioning never record
or apply the group, and the Quattro upgrade no longer adds it. A migration takes
existing installs out of the group, reusing omarchy-remove-security-sudoless-
docker so the change and its notice have one source of truth.

The Windows VM keeps needing the root daemon for a privileged container, so it
runs without the group without becoming a new way in: the compose moves to a
root-owned directory written only by an elevated, input-validated writer, volume
paths are rebuilt from $HOME on migration rather than trusted from the
user-writable legacy file, the privileged sub-action is checked against an
allowlist before dispatch, pkexec elevates a verified root-owned command path,
mount sources are refused when they are or resolve through a symlink, and the
guest password moves to a private 0600 per-user file instead of a
world-readable compose. Existing installs auto-migrate the VM without a
redownload.

Two files had diverged from quattro and were resolved by hand:

bin/omarchy-windows-vm -- v4-0-1 still carries the "Starting Windows VM" toast
that #7585 dropped on quattro, and the new start path has no user-side status
check to hang it on: after this change the user cannot inspect the container
without privilege, which is the whole point. Took quattro's version. #7585's
reason holds here too -- the shell shows its own "Launching Windows…" OSD until
the RDP window appears (shell/services/AppLibrary.qml) -- and the failure
notification stays. The file is now byte-identical to quattro.

manual/28-windows-vm.md -- took the new paragraph on the root-owned compose,
without the neighbouring OEM-key paragraph, which documents omarchy windows key,
a command quattro has and this branch does not.

test/shell passes here: 186 files, including the three this adds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DEMYa9UWtroz93DhMTtcV
2026-08-24 19:54:01 +02:00
OmarchybotandClaude Opus 5 7fa32bb98c Remove the sudo lockout reset command (backport of #8046)
Backport of the omarchy-sudo-reset removal (PR #8046, merged to quattro as
d99d4fc6) onto the v4-0-1 release branch, so 4.0.1 stops shipping the command.

Nothing in the repository called omarchy-sudo-reset, and its one line
interpolated an environment-supplied $USER into a string handed to a root
shell: su -c "faillock --reset --user $USER". $USER is an environment variable
rather than a kernel-supplied identity, so whatever set it before the command
ran chose the rest of what root's shell executed. That is not a way past PAM on
its own -- su still has to authenticate -- but the installer sets root's
password to the user's own, so the prompt this raises is one the user answers
by habit.

It bought little for that. Omarchy sets deny=10 unlock_time=120 in
/etc/pam.d/system-auth and in the lock screen's PAM stack, so a lockout takes
ten wrong passwords to reach and clears itself two minutes later, and
manual/45-troubleshooting.md documents the root-TTY reset for anyone who would
rather not wait. That reset is unaffected by this change.

The sudo group keeps keepalive and passwordless, so GROUP_DESCRIPTIONS[sudo] is
unchanged and omarchy sudo reset falls through to the router's unknown-command
path. No migration is needed: bin/omarchy-* ships as files in the omarchy
package, so an upgrade drops what the package no longer contains.

Clean cherry-pick: the file was byte-identical to quattro's pre-image and
nothing else on this branch referenced it, so merging v4-0-1 into quattro
resolves without a conflict. test/shell and test/cli pass here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DEMYa9UWtroz93DhMTtcV
2026-08-24 15:50:12 +02:00
OmarchybotandClaude Opus 5 2b1d3c4606 Stop a psmouse quirk from failing every install (backport of #7236)
Backport of the synaptic touchpad quirk fix (PR #7236, merged to quattro as
20400bad) onto the v4-0-1 release branch, so 4.0.1 installs stop dying on it.

install/hardware/fix-synaptic-touchpad.sh calls modprobe against the running
kernel. On 4.0 the only thing that runs it is the ISO finalizer, inside
arch-chroot, where uname -r still names the live ISO's kernel while
/lib/modules holds the target's. The live ISO always boots linux-t2 and the
configurator gives every machine that is not a T2 Mac stock linux, so the two
never match: modprobe exits 1 with "Module psmouse not found in directory
/lib/modules/<live kernel>". run_logged returns that status and
omarchy-apply-hardware runs under set -euo pipefail, so a fresh install stops
on the first machine with a device named "synaptics" and no psmouse loaded --
reported from a ThinkPad in #6985, but nothing about it is Lenovo-specific.

Ask modprobe itself, with -qn, whether it can resolve psmouse for the running
kernel before asking it to load one, and warn instead of failing when it
declines for any other reason. The chroot mismatch becomes an unresolvable
module, skipped in silence. An optional touchpad improvement must not be able
to halt an install.

This does not make InterTouch reach the installed system: a module loaded into
the live kernel is gone at reboot, so on 4.0 this script has never applied
anything to an installed machine -- it only ever aborted installs. Persisting
it would mean writing options psmouse synaptics_intertouch=1 to
/etc/modprobe.d, which forces the SMBus transport past the kernel's own
allowlist on any touchpad merely named "synaptics". That is a
hardware-behaviour change on a wide class of machines, left as a separate
decision on quattro.

Clean cherry-pick: both files are byte-identical to #7236, so merging v4-0-1
into quattro resolves without a conflict. test/shell.d/synaptic-touchpad-test.sh
passes here, as does the rest of test/shell (183 files).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DEMYa9UWtroz93DhMTtcV
2026-08-24 15:37:29 +02:00
Ryan Hughes 286b8c2b1c Run notification click actions as safe argv (backport of #7926)
Backport of the notification click-command hardening (PR #7926, merged to
quattro as 43bfe9b9) onto the v4-0-1 release branch. Click actions are argv
vectors run without a shell, omarchy-notification-send calls the Notify D-Bus
method directly via busctl instead of notify-send, and --exec takes the command
as rest-of-line words. Excludes docs/notifications.md, which does not exist on
v4-0-1.
2026-08-23 19:56:59 -04:00
Omarchybot f2a2973de8 Stop an installed theme from running code (#7884)
* Stop an installed theme from shipping code

`omarchy theme install <url>` clones a stranger's git repository into ~/.config/omarchy/themes, and omarchy-theme-set then copied that whole directory into the staged theme. Most of the files in a staged theme are code rather than colour: Hyprland requires hyprland.lua and gum_env.lua from it at login, Neovim loads neovim.lua at startup, and alacritty.toml, kitty.conf, foot.ini and ghostty.conf each name the program the terminal launches. Installing a theme was the same act as running its author's code, and nothing on disk distinguishes an installed theme from one the user wrote.

Stage only what a theme needs in order to be a theme: colors.toml, light.mode, the preview and unlock images, and image files under backgrounds/. Everything else is ignored, named on stderr, and generated from default/themed/*.tpl instead. Symlinks are never followed, because in an untrusted theme they point wherever the author chose. A theme older than colors.toml keeps its palette: its alacritty.toml is read for colours in a scratch directory and only the resulting colors.toml is staged, so the terminal config never lands.

The filter belongs in omarchy-theme-set rather than in omarchy-theme-install because staging is the choke point. It also covers themes installed before this change, themes copied in by hand, and files a theme gains later through `omarchy theme update`.

First-party themes under $OMARCHY_PATH/themes are unaffected. Per-theme overrides of a generated file are no longer available to user themes; the template at ~/.config/omarchy/themed/<file>.tpl replaces that, and icons.theme is the one setting with no replacement.

🤖 Generated by Opus 5 in Claude Code.

* Stop a theme URL or name being read as an option or a path

Three paths in the theme commands took an attacker-shaped string straight into git, into basename, or into rm.

`git clone "$REPO_URL"` passes the URL as the first positional argument, so a URL beginning with a dash is parsed as an option instead and the destination path becomes what git tries to clone. Pass `--` before the URL so a URL is always a URL. git also treats `<helper>::<address>` as a remote helper to run; git's own protocol.allow default already refuses `ext::`, so rejecting that shape here is a second line rather than the fix, and it keeps holding if that default ever moves. The helper name is a bare word at the very start of the URL, which is what the guard matches: an scp-style IPv6 host such as git@[2001:db8::1]:org/repo.git carries `::` of its own and still clones.

`basename "$REPO_PATH" .git` has the same problem one step later, after the scp-style prefix has been stripped: `host:-s/foo.git` leaves basename reading `-s` as an option and returning `.git` as the theme name. Take the name with `--`.

That name is then joined into a path that is about to be `rm -rf`'d, so a repo whose basename came out as `..` would take ~/.config/omarchy with it. omarchy-theme-remove had the same shape from its own argument, and omarchy-theme-set's sed/tr normalization does not stop a name containing a slash. Reject empty, anything starting with a dot, and anything containing `/` in all three, before the name reaches a path.

🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh.

Co-Authored-By: Codex XHigh <codex@openai.com>

* Re-stage the current theme for installs that already applied one

Dropping a theme's code at staging time only takes effect the next time a theme is staged. An install that already applied an extra theme keeps that theme's hyprland.lua, gum_env.lua, neovim.lua and terminal configs in ~/.local/state/omarchy/current/theme, which Hyprland requires at login and the terminals include at launch, and nothing forces a theme change — so for those installs the fix would arrive whenever the user next happened to switch themes, which may be never.

Re-stage once through omarchy-theme-refresh. First-party themes stage identically, so the cost for everyone else is a single retint during an update they are already running.

🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh.

Co-Authored-By: Codex XHigh <codex@openai.com>

* Stop a theme's unlock image republishing a file it points at

omarchy-plymouth-set-by-theme reads unlock.png straight out of ~/.config/omarchy/themes, which is an installed theme's own directory and outside the staging filter, and hands the path to omarchy-plymouth-set. That path was copied twice into world-readable /usr/share — once by the user into the Plymouth theme, and once by `sudo cp` into the SDDM theme. A symlink there was followed both times, so a theme could name a file it cannot read and have root publish it.

Refuse a symlinked logo, and copy the staged logo to SDDM instead of rereading the caller's path as root. The staged copy is made by the user, so nothing privileged opens a path the caller chose.

🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh.

Co-Authored-By: Codex XHigh <codex@openai.com>

* Limit only what an installed theme could run

Two corrections to the rule this branch introduced, both narrowing it to what it was actually for.

It applied to every theme under ~/.config/omarchy/themes, which swept up themes the user wrote themselves. Their machine, their file: a theme they wrote is theirs to fill however they like, and Omarchy's own themes were never in scope. Only a theme that came from someone else needs limiting, and the repo already knows which those are — omarchy-theme-extras calls a theme with a `.git` directory an extra and a symlink someone's working copy, because that is what `omarchy theme install` leaves behind when it clones. Use the same test.

It was also an allowlist, which dropped files that carry nothing but colour and left theme authors worse off for no gain. Drop only what can run: any `*.lua`, since Hyprland requires a theme's hyprland.lua and gum_env.lua at login and Neovim loads neovim.lua at startup; the four terminal configs, since each names the program the terminal launches; and vscode.json, whose extension field reaches `code --install-extension` and a VS Code extension is arbitrary JavaScript. Everything else an installed theme ships is kept, so btop.theme, chromium.theme, helix.toml, icons.theme, keyboard.rgb and shell.toml go back to being the theme's to set.

Symlinks are still dropped, now at any depth rather than only where an allowlist happened to look.

A denylist is wrong the moment someone adds a template and does not think about it, so the decision is forced rather than remembered: the test fails on any default/themed/*.tpl whose output is recorded as neither code nor colour, and a new terminal or a new Lua-loading editor cannot be added without classifying it.

What this does not cover, and is written down in docs/theming.md rather than implied: a theme shipped as an archive and unpacked by hand looks exactly like one the user wrote. `omarchy theme install` only takes git URLs, so the supported path is always filtered, but this marks where a theme came from and is not a sandbox.

🤖 Generated by Opus 5 in Claude Code.

* Fix what the review found

Four things, all confirmed against the source before changing anything.

The migration failed permanently when the active theme had been removed. `omarchy theme remove` deletes the directory without repointing theme.name, so the name survives, the staged copy survives, and omarchy-theme-refresh exits 1 because neither source directory exists — leaving the migration pending forever and the stale staged Lua exactly where it was, which is the one thing it existed to remove. Seed the default theme in that case: there is nothing to re-stage from, and the removal should have left a working theme behind anyway.

The staging test skipped the strict-mode header that docs/testing.md makes the contract for every shell test. Adding it means the patterns that fail on purpose have to stop being bare `cmd && fail` compounds, which errexit reads as the script itself failing; the mutations were re-run afterwards to confirm the assertions still fire rather than the run dying early and looking like something else.

The guards in omarchy-theme-install and omarchy-theme-remove had no coverage — they were checked by hand and left that way. theme-install-guards-test.sh stubs git and the themes directory and proves an option-shaped URL, a transport helper, and a name that would climb out all stop before git or rm runs, that a dash inside the path no longer becomes a basename option, and that an ordinary URL still clones and applies.

The new docs/theming.md prose was hard-wrapped, which AGENTS.md forbids for docs/. Unwrapped. The rest of that file is wrapped from before and is left alone rather than churned through this change.

🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh and Copilot.

---------

Co-authored-by: Codex XHigh <codex@openai.com>
(cherry picked from commit ef6d9e6605)
2026-08-23 19:12:29 +02:00
Adrian Rangel 64c5c02f55 [Security] Stop a video title from becoming the Download Video play command (#7847)
* Stop a video title from becoming the Download Video play command

The host parsed yt-dlp's after_move line as title plus path, so a newline in page metadata could forge the path. Clicking the toast then handed that value to mpv as options. Print only the real file, ignore anything that is not inside the download dir, and invoke mpv with --.

* Refuse downloads whose video title contains control characters

The hoodie page still offered a real hidden clip, so yt-dlp saved it even after the play-action fix. A title with newlines is not a legitimate name; abort before the download and tell the user it was refused.

* Test the forged record in the order yt-dlp emits it

The records ran forged-first and good-last, so the assertion measured recovery after
bad records rather than preservation of an already-captured path when a forged record
arrives afterwards. That is the shape a hostile title actually produces, because a
title ending in a newline closes its own record and leaves the genuine path on a line
the loop ignores. As written the assertion passed with resolve_download_file replaced
by a no-op.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Resolve the download path without dropping a trailing newline

Command substitution strips trailing newlines, so an in-directory symlink pointing at
a regular file whose name ends in one canonicalised to a different path -- which may
itself exist -- and that path then passed the containment check and reached ffmpeg and
the click command. Reading realpath's NUL-terminated output keeps the name intact, and
a resolved path carrying a control character is refused outright.

Not reachable through a yt-dlp download, since --restrict-filenames strips control
characters from the name it writes; it is the helper's contract that was wrong.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>

* Accept a download directory that resolves to /

realpath returns "/" for the root directory, which made the containment pattern "//*"
and rejected every file saved directly under it, so the host reported a failed download
after saving the file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>

* Stop refusing a download because its title has control characters

The gate cannot tell a hostile title from a legitimate one. --print emits one line per
extracted video and --no-playlist does not collapse a multi_video result, so a page
holding two clips arrives as two titles separated by a newline and is refused exactly
like a forged record would be.

It also guaranteed nothing it was read as guaranteeing. The simulate run and the
download run are separate fetches, so a site is free to answer them differently, and
the check never constrained the metadata the download actually used.

What stands between a record and the click command is resolve_download_file, which is
untouched here. Leaving a check that refuses valid pages while securing nothing invites
the path validation to be relaxed later on the strength of it. A gate that would work
is possible -- --print '%(title)j' encodes each title as JSON on its own line, which
separates a newline in the metadata from a newline between videos -- but it belongs
with a use for the title rather than as a bare refusal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>

* Disarm the legacy exec-before-download hook too

--no-exec clears the modern --exec map but leaves --exec-before-download stored
separately, and yt-dlp restores it as a before_dl postprocessor, so a hook configured
in the user's yt-dlp config still ran during the download this host drives.

The accompanying test runs download_url itself against stubbed tools. Everything else
in this file exercises the helpers in isolation, which left the invocation uncovered:
restoring the title to the record template, or dropping --no-exec or the trailing --,
passed every assertion here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>

* Toast the page title again instead of the saved filename

Deriving the toast text from the sanitised filename cost the title it was meant to
show: "My Great Clip" arrived as "My_Great_Clip [My_Great_Clip]". The title is safe as
notification text -- it is an argv element, never part of a command -- so the only
question was getting it out of yt-dlp without reopening the record forgery.

It now comes from the download run, so it describes the file that was actually saved,
and it is printed as %(title)j. JSON-encoding is what makes that safe: a newline or tab
in page metadata becomes an escape sequence inside one quoted string rather than a
record boundary, so a title can no longer split itself across lines. The decoder keeps
only what precedes the first control character, refuses anything notify-send would read
as an option, and leaves the filename-derived title as the fallback when a page offers
nothing usable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Name the saved file after the page title

The download landed as "My_Great_Clip [My_Great_Clip].mp4" when the page called it
"My Great Clip". --restrict-filenames was carrying more weight than it earns here: it
folds spaces to underscores and strips non-ASCII, which is what mangles the name, and
it is not what keeps the record stream safe. yt-dlp removes control characters from a
filename either way -- a newline becomes a space, tabs and DEL and NUL are dropped --
so a path printed after the move is still only ever one line, which is the property
resolve_download_file depends on.

Dropping the [%(id)s] suffix is the other half of matching the title, and it trades
away the uniqueness that suffix bought: two videos sharing a title now share a name,
and yt-dlp skips a download whose file already exists, so the second one toasts as a
failure. Restoring the suffix is a one-line change if that trade is the wrong way
round.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Omabot <omabot@omarchy.org>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex XHigh <noreply@openai.com>
(cherry picked from commit b71c60fe30)
2026-08-23 19:11:58 +02:00
Omarchybot d0aefc1f79 Only offer Update > Extra Themes when there is one (#7775)
* 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>
(cherry picked from commit 13a969e1ab)
2026-08-23 19:11:58 +02:00
Omarchybot 1626405638 Switch back to the packaged quickshell now that 0.3.1 kills synchronously (#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>
(cherry picked from commit 2c593dbbaa)
2026-08-23 19:11:58 +02:00
Matthias Nitsch 9b192256e7 Leave an auto-scaled internal panel alone in clamshell recovery (#7581)
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>
(cherry picked from commit b63616422f)
2026-08-23 19:11:58 +02:00
David Heinemeier Hansson 2487fbcce1 Fall back to polkit when the DNS sudoers grant is missing (#7492)
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>
(cherry picked from commit 1e70cca144)
2026-08-23 19:11:58 +02:00
Omarchybot 9434f220f0 Switch DNS providers without a password prompt (#7472)
* 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>
(cherry picked from commit 3765e8010b)
2026-08-23 19:11:58 +02:00
David Heinemeier Hansson 9e79b24342 Decode webp in the shell (#7477)
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. Any
third-party theme shipping a .webp background comes up blank without the
qt6-imageformats plugin, and extra themes are their own repositories.

Only the dependency is taken here. The background conversion that came with
it re-encodes all 79 bundled backgrounds, which 4.0.1 has no need of.

(cherry picked from commit a4219f8fbeb85ee7a2bc0e14eda9d38cc85d3d0d, dependency only)
2026-08-23 19:11:58 +02:00
David Heinemeier Hansson 6245965cb6 Decode mixed UTF-16 clipboard text (#7466)
* Decode mixed UTF-16 clipboard text

* Harden UTF-16 clipboard detection

* Finish UTF-16 decoder hardening

(cherry picked from commit 238021cd67)
2026-08-23 19:11:58 +02:00
Omarchybot f6b4183e4f Switch to the mise-bin package (#7244)
* Switch to the mise-bin package

mise-bin carries mise's own release artifacts from the Omarchy repo --
PGO+BOLT-optimized on x86_64, glibc-native on both arches -- instead of
Arch's mise, and tracks jdx/mise releases directly.

Existing installs need a migration because the two packages conflict, and
omarchy-pkg-add cannot make the swap: pacman answers its own conflict
question with No under --noconfirm and fails the transaction. --ask=4
answers that one question, so mise-bin replaces mise in a single
transaction -- which is also what keeps omarchy-zsh and omarchy-fish, both
of which depend on mise, satisfied through the swap by its provides.

* Guard the swap with a conditional instead of an early exit

Two-path control flow takes an if, per the style guide; the early exit only made the swap line unreachable from a distance.

(cherry picked from commit 262d6f0681)
2026-08-23 19:11:58 +02:00
David Heinemeier Hansson 39c2797d3b Decode UTF-16 clipboard text (#7249)
(cherry picked from commit 006460ad57)
2026-08-23 19:11:58 +02:00
David Heinemeier Hansson 8b979e0c4c Fix passwordless OWE Wi-Fi handling (#7238)
(cherry picked from commit 1c1116b626)
2026-08-23 19:11:58 +02:00
David Heinemeier Hansson 5c2fb68c0a Format the whole speed test dial for the locale (#6994)
The dial already grouped its digits for the locale above 10, but below 10 it
went through toFixed, which hardcodes a dot. A German desktop therefore read
9.5 and 1.235 off the same dial, switching decimal convention halfway up the
scale.

Send both branches through the locale. A reading is a measurement rather than
interface text, so its separators follow the system's number conventions even
though the interface itself stays English.

English output is unchanged.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 9f109a7f3e)
2026-08-23 19:11:58 +02:00
David Heinemeier Hansson 2d3826aff3 Keep the calendar's day names in English (#6988)
The calendar grid's header row and the week-start toggle label were the only
text in the shell that followed the system locale, so a German desktop drew
MO DI MI over an interface that is English everywhere else. Nothing chose that;
they were the only two places reading day names off Qt.locale().

Take them from en_US instead. Where the week starts still follows the locale:
that is a regional convention rather than a translation, and it stays
overridable through weekStartDay.

Dropping the trailing-period strip with it, since that existed only for the
locales this no longer renders.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 25bee1d2a3)
2026-08-23 19:11:58 +02:00
David Heinemeier Hansson eb201d0bd3 Fix Clone Plugin failing with "unknown clone option" (#6942)
omarchy-plugin-clone only takes the source id as the first argument, but the
menu passed --edit ahead of it, so the id fell through to the unknown-option
branch and every clone from Setup > Plugins failed.

Closes #6913

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit ff4e92e63b)
2026-08-23 19:11:58 +02:00
David Heinemeier Hansson ac1bdbfbf5 Stop the bar sticking in move mode after a press-and-hold on a widget (#6943)
Bar widgets propagate their composed press-and-hold down to the center gesture
area without handing over the grab, so the gesture area started a bar move and
then received neither a release nor a cancel to end it. The move ghost stayed on
screen for the rest of the session. Ignore the gesture unless we hold the press.

Closes #6881

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit c0a20e2e43)
2026-08-23 19:11:58 +02:00
David Heinemeier Hansson 9f7cdf6983 Keep mise wrappers from writing to stdout (#6940)
mise use -g announces the resolved tool on stdout, so every wrapped command
prepended a "tools:" line to its own output. That corrupts anything speaking a
protocol over stdout, such as codex app-server. Pass --quiet, which keeps errors
on stderr and preserves the exit status.

The obsolete-wrapper check in the agent migration matched the generated command
verbatim, so loosen it to match the package instead of the flags.

Closes #6908

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 1c3da94906)
2026-08-23 19:11:58 +02:00
David Heinemeier Hansson 1867dcaa50 Repair foot.ini configs holding a literal \n[text-bindings] line (#6938)
A 3.8.3 migration appended the section header with one backslash too many, so
sed wrote the literal characters instead of a newline plus the header. foot
rejects the line and stops reading the rest of the file. The later text-binding
migration matches the header with grep -qxF, misses the broken line, and appends
a second section, leaving the config broken across the Quattro upgrade.

Closes #6903

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit b5bb8dac05)
2026-08-23 19:11:58 +02:00
David Heinemeier Hansson 33d7363c33 Fix o.shell_succeeds() always returning false inside Hyprland
Hyprland reaps its own children, so os.execute() gets ECHILD from waitpid and
never sees an exit status. Every call reported failure, which meant the NVIDIA
env detection in hypr/nvidia.lua never set NVD_BACKEND, LIBVA_DRIVER_NAME or
__GLX_VENDOR_LIBRARY_NAME. Read a marker off stdout instead.

Closes #6914

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 0965ac2e4f)
2026-08-23 19:11:58 +02:00
Alonso David De León Rodarte 84336759cc Fix Windows VM launch never opening an RDP window (#6893)
* Wait for the current Windows boot before connecting RDP

docker logs retains output across stop/start, so grepping the whole log
matched "Windows started successfully" from an earlier boot and returned
immediately, firing xfreerdp3 while the guest was still booting. Anchor the
scan to the container's current StartedAt, and run it even when the container
was already running, since the image restarts the guest in place on reboot.

* Skip Kerberos when connecting to the Windows VM

FreeRDP 3 attempts Kerberos before NTLM for NLA, and Arch's stock
/etc/krb5.conf declares default_realm = ATHENA.MIT.EDU, so every launch tries
to reach MIT's KDC. Off the network each attempt blocks ~23s and xfreerdp3
sits in CLOSE-WAIT without drawing a window, which reads as the VM failing to
start. Point FreeRDP at a realm-less krb5 config so it falls through to NTLM,
which is what the local Windows account uses anyway.

* Re-read the container start time on every readiness poll

A failed docker inspect left STARTED_AT empty, and docker logs drops the
--since filter when it is, putting the scan back on the whole retained log
and its stale success line. Sampling per poll also keeps the window on the
current boot if the container restarts mid-wait.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 75b99f7fd4)
2026-08-23 19:11:58 +02:00
David Heinemeier Hansson 5f0704a0f8 Launch claude and codex agents with auto-review instead of full bypass (#7001)
* Launch claude and codex agents with auto-review instead of full bypass

Claude's auto permission mode and codex's --approve-for-me both run
unattended without prompting, but keep automatic review (and codex's
workspace-write sandbox) instead of skipping approval entirely. Grok
stays on bypassPermissions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Match the bash aliases to the agent launcher's auto-review modes

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit dd9dee417f)
2026-08-23 19:11:58 +02:00
430 changed files with 859 additions and 17386 deletions

No files matched your search

-2
View File
@@ -1,2 +0,0 @@
# Merges to protected branches need sign-off from an org owner.
* @dhh @ryanrhughes
-47
View File
@@ -1,47 +0,0 @@
# Security at Omarchy
## Report a vulnerability
If you believe you’ve found a security vulnerability in Omarchy, please tell the [Omarchy Security Team](https://omarchy.org/teams/#security) privately so we have an opportunity to investigate and fix it before it is made public.
[security@omarchy.org](mailto:security@omarchy.org?subject=Security%20report)
Please don’t report potential vulnerabilities publicly in GitHub Issues, Discord, or social media before they’ve been resolved.
## What is a vulnerability?
We consider a bug a security vulnerability when it can be exploited to cross a meaningful security boundary: an untrusted or lower-privileged party gains access, permissions, or control they didn’t already have.
Code that could be more robust but does not cross a security boundary is an improvement rather than a security vulnerability. We may still merge a proposed fix and credit the reporter in our release notes.
Eligibility for our [security credits](https://omarchy.org/security/credits/) page depends on whether a report identifies a confirmed security vulnerability, not on its severity.
## What to include
Give us enough information to understand and reproduce the issue:
- The affected component and Omarchy version.
- An explanation of what an attacker can do before and after exploitation.
- Steps to reproduce the issue and any proof of concept.
- Your preferred contact details for follow-up.
## Responsible disclosure
Please act in good faith while investigating and reporting vulnerabilities:
- Only test systems and accounts you own or have explicit permission to test.
- Avoid privacy violations, disruption, data destruction, and service degradation.
- Don’t exploit a vulnerability beyond what is needed to demonstrate it.
- Give us a reasonable opportunity to investigate and address the issue before publishing details.
We’ll review your report and keep you informed as we’re able while we work toward a resolution.
## Credits
Researchers who privately report a confirmed security vulnerability and give us the chance to ship a fix are thanked on the [security credits](https://omarchy.org/security/credits/) page. Accepted improvements that don’t cross a security boundary may still be credited in our release notes.
Credits link to each reporter’s X profile and show their avatar. For duplicate reports, only the first reporter is eligible for credit.
## Regular bugs and support
For anything that isn’t a security vulnerability, please use the [Omarchy issue tracker](https://github.com/omacom/omarchy/issues).
+3 -16
View File
@@ -9,24 +9,14 @@ matching guide before starting:
- [`agents/skills/icon-font.md`](agents/skills/icon-font.md) - adding branded glyphs to `default/fonts/omarchy/omarchy.ttf`
- [`agents/skills/acceptance-tests.md`](agents/skills/acceptance-tests.md) - writing or running graphical acceptance tests under `test/acceptance.d/`
- [`agents/skills/visual-verification.md`](agents/skills/visual-verification.md) - verifying any change with a visual effect in the running UI
- [`agents/skills/migrations.md`](agents/skills/migrations.md) - creating or changing migrations under `migrations/`
# Documentation Layout
Three documentation trees, split by genre and audience:
- `agents/skills/` - task procedure ("do this when doing X"), for anyone working on the codebase
- `docs/` - reference on how the system is shaped (file layout, update pipeline, theming, shell architecture), for anyone working on the codebase; skills link here for depth
- `manual/` - end-user documentation for using Omarchy, published; never codebase internals
- [`docs/migrations.md`](docs/migrations.md) - creating or changing migrations under `migrations/`
# Style
- In markdown documents (`plans/`, `docs/`, `manual/`), write full lines — no hard wrapping at 80 columns; break only at structural boundaries like headings and list items
- Two spaces for indentation, no tabs
- Use bash 5 conditionals: use `[[ ]]` for string/file tests and `(( ))` for numeric tests
- In `[[ ]]`, don't quote variables, but do quote string literals when comparing values (e.g., `[[ $branch == "dev" ]]`)
- Prefer `(( ))` over numeric operators inside `[[ ]]` (e.g., `(( count < 50 ))`, not `[[ $count -lt 50 ]]`)
- Prefer a full `if`/`else` conditional for simple two-path control flow; don't rely on `exec` or `exit` in one branch to make following statements unreachable
- For strings/paths with spaces, quote them instead of escaping spaces with `\ ` (e.g., `"$APP_DIR/Disk Usage.desktop"`, not `$APP_DIR/Disk\ Usage.desktop`)
- Shebangs must use `#!/bin/bash` consistently (never `#!/usr/bin/env bash`)
- Scripts under `install/` and `migrations/` may be sourced and intentionally omit shebangs
@@ -91,8 +81,7 @@ Exceptions are allowed for migration and package-helper scripts where the helper
# Menu
- The menu definition lives in `default/omarchy/omarchy-menu.jsonc`;
[`docs/menu.md`](docs/menu.md) covers the schema, guards, and providers.
- The menu definition lives in `default/omarchy/omarchy-menu.jsonc`.
- Do not add `aliases` to new menu entries. Aliases are reserved for
established alternate names users already type, kept for compatibility.
@@ -104,9 +93,7 @@ Exceptions are allowed for migration and package-helper scripts where the helper
# Tests
Run focused automated tests for the area you changed;
[`docs/testing.md`](docs/testing.md) covers how the suites are shaped. Current
test entry points:
Run focused automated tests for the area you changed. Current test entry points:
- `./test/all` - aggregate runner for CLI and shell tests; it intentionally does not run graphical acceptance tests
- `./test/cli` - CLI routing, command metadata, theme helpers, and safe dispatch coverage
-3
View File
@@ -27,7 +27,6 @@ declare -A BINARY_TO_KEY
declare -A GROUP_DESCRIPTIONS
GROUP_DESCRIPTIONS[agent]="AI coding agent usage data"
GROUP_DESCRIPTIONS[ascii]="Text drawn as ASCII art"
GROUP_DESCRIPTIONS[audio]="Audio input and output controls"
GROUP_DESCRIPTIONS[bar]="Omarchy shell bar layout and settings"
GROUP_DESCRIPTIONS[battery]="Battery status helpers"
@@ -40,7 +39,6 @@ GROUP_DESCRIPTIONS[channel]="Omarchy release channel management"
GROUP_DESCRIPTIONS[clipboard]="Clipboard helpers"
GROUP_DESCRIPTIONS[cmd]="Command and shortcut helpers"
GROUP_DESCRIPTIONS[config]="System configuration helpers"
GROUP_DESCRIPTIONS[crash]="Crash notification controls"
GROUP_DESCRIPTIONS[debug]="Diagnostics and support logs"
GROUP_DESCRIPTIONS[finalize]="Finalize user setup"
GROUP_DESCRIPTIONS[default]="Default application selection"
@@ -72,7 +70,6 @@ GROUP_DESCRIPTIONS[plymouth]="Plymouth boot theme management"
GROUP_DESCRIPTIONS[power]="Power supply detection"
GROUP_DESCRIPTIONS[powerprofiles]="Power profile management"
GROUP_DESCRIPTIONS[refresh]="Reset config to defaults"
GROUP_DESCRIPTIONS[region]="Region-specific sources and input method"
GROUP_DESCRIPTIONS[reinstall]="Reinstall and reset workflows"
GROUP_DESCRIPTIONS[reminder]="Desktop notification reminders"
GROUP_DESCRIPTIONS[remove]="Removal workflows"
+3 -10
View File
@@ -52,14 +52,14 @@ if omarchy-cmd-missing "$agent"; then
fi
# Agents launched from the keybinding or menu run unattended, so each one starts
# with its own spelling of "don't stop to ask". Pi and Ori have none to skip.
# with its own spelling of "don't stop to ask". Pi has no approval prompts.
case "$agent" in
opencode)
command=(opencode --auto)
[[ -n ${prompt:-} ]] && command+=(--prompt "$prompt")
;;
agy)
command=(agy --dangerously-skip-permissions)
gemini)
command=(gemini --yolo)
[[ -n ${prompt:-} ]] && command+=(--prompt-interactive "$prompt")
;;
copilot)
@@ -90,13 +90,6 @@ omp)
command=(omp --auto-approve)
[[ -n ${prompt:-} ]] && command+=(-- "$prompt")
;;
ori)
# Ori is a harness launcher, and `ori code` is the agent it runs itself.
# A prompt alone means one headless turn there, printed after the turn ends,
# so --interactive is what seeds the session with it and keeps the window.
command=(ori code)
[[ -n ${prompt:-} ]] && command+=(--interactive --prompt "$prompt")
;;
pi)
command=(pi)
[[ -n ${prompt:-} ]] && command+=("$prompt")
+1 -1
View File
@@ -528,7 +528,7 @@ def fetch_codex_rpc():
try:
proc = subprocess.Popen(
[codex, "-s", "read-only", "-a", "on-request", "app-server"],
[codex, "-s", "read-only", "-a", "untrusted", "app-server"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
+1 -1
View File
@@ -16,7 +16,7 @@ flags=()
only=()
declare -A excluded
while (( $# > 0 )); do
while [[ $# -gt 0 ]]; do
case "$1" in
--force | --limits-only) flags+=("$1") ;;
--except)
-1217
View File
File diff suppressed because it is too large. Load diff
-104
View File
@@ -1,104 +0,0 @@
#!/bin/bash
# omarchy:summary=Shared helpers for animating the About branding (source this, don't run it).
# omarchy:group=branding
# omarchy:name=about-animation
# omarchy:hidden=true
# The About branding's animation: a sheen, and the frames that make it. *When*
# those frames are written stays with the caller, because that is inseparable
# from how the caller's window closes and resizes; this only says what to write,
# and where — it is handed a logo and knows nothing about About beyond that.
# A band of light leans across the logo, rests, and leans across again. Two
# columns per row puts it at 45 degrees on screen, where a cell is twice as tall
# as it is wide.
ESC=$'\e'
# Bright white, so the band does not depend on how the terminal reads bold. Foot's
# bold-text-in-bright brightens a bold regular colour into its bright counterpart,
# which turns a bold green logo bright green — exactly the colour a bright green
# band would have used, leaving a glint nobody can see. No regular colour
# brightens into bright white, so this one shows either way.
SHEEN_BAND="${ESC}[1m${ESC}[97m"
SHEEN_SLANT=2
SHEEN_HALF=2
SHEEN_FRAME_SECONDS=0.025
SHEEN_REST_TICKS=8
# Re-measure the grid every half second, the cadence a still logo already cost,
# rather than spawning a process per frame.
SHEEN_POLL_FRAMES=20
# A frame is one string holding every row of the logo, positioned and coloured,
# so a tick writes the whole logo at once and never shows it half drawn.
compose_frame() {
local centre=$1
local row line length at from to frame=""
for (( row = 0; row < SHEEN_ROWS; row++ )); do
line=${SHEEN_LINES[row]}
length=${#line}
# Where the band crosses this row. Running off the right needs no clamp,
# because a slice that starts past the end of a line is already empty, but a
# negative offset would count from the end of it instead of off the left.
at=$(( centre - row * SHEEN_SLANT ))
from=$(( at - SHEEN_HALF ))
from=$(( from < 0 ? 0 : from ))
to=$(( at + SHEEN_HALF + 1 ))
to=$(( to < 0 ? 0 : to ))
frame+="${ESC}[$((SHEEN_TOP + row));${SHEEN_LEFT}H"
frame+="${SHEEN_BASE}${line:0:from}${SHEEN_BAND}${line:from:to - from}${SHEEN_BASE}${line:to}"
done
SHEEN_COMPOSED=$frame
}
# Builds every frame up front: a tick that recomputed a logo's worth of colour
# spans in bash would not hold the frame rate, and the sweep is the same every
# time. Leaves them in SHEEN_FRAMES, and answers whether this logo can be
# animated at all — one it cannot put back exactly as it found it is one to leave
# alone, because nothing on screen would say the difference.
#
# sheen_build <logo-file> <top-row> <left-column> <base-colour> <columns-available>
sheen_build() {
local file=$1 columns=$5
SHEEN_TOP=$2
SHEEN_LEFT=$3
SHEEN_BASE=$4
SHEEN_LINES=()
# A failing redirection reports itself before 2>/dev/null would apply, so order
# it first: the caller's window must not get a shell error painted across it.
mapfile -t SHEEN_LINES 2>/dev/null <"$file" || return 1
SHEEN_ROWS=${#SHEEN_LINES[@]}
(( SHEEN_ROWS > 0 )) || return 1
local row line width=0
for (( row = 0; row < SHEEN_ROWS; row++ )); do
line=${SHEEN_LINES[row]}
# A renderer substitutes $1 to $9 for colours, so a logo written with those is
# not the text that reached the screen.
[[ $line == *'$'* ]] && return 1
# These frames slice the row by character and a terminal draws it by column,
# so one character has to be one cell — and everything that breaks that breaks
# it here. A wide glyph, a combining mark or a joined emoji is not one cell; a
# tab or an escape is one the renderer expanded itself; and a shell whose
# locale is counting bytes is not counting characters at all.
(( ${#line} == $(printf '%s' "$line" | LC_ALL=C.UTF-8 wc -L) )) || return 1
if (( ${#line} > width )); then
width=${#line}
fi
done
(( width > 0 && width <= columns )) || return 1
SHEEN_FRAMES=()
local centre last=$(( width + SHEEN_ROWS * SHEEN_SLANT + SHEEN_HALF ))
for (( centre = -SHEEN_HALF; centre <= last; centre++ )); do
compose_frame "$centre"
SHEEN_FRAMES+=("$SHEEN_COMPOSED")
done
}
+4 -17
View File
@@ -4,13 +4,7 @@
# omarchy:args=[--no-osd] [+N%|N%-|N%]
# omarchy:examples=omarchy brightness display apple | omarchy brightness display apple +5% | omarchy brightness display apple --no-osd 50%
# Only cache under the user-private runtime dir. With no XDG_RUNTIME_DIR we skip
# caching (detect every run) rather than fall back to a predictable, world-writable
# /tmp path another user could pre-create.
device_cache=""
if [[ -n ${XDG_RUNTIME_DIR:-} ]]; then
device_cache="$XDG_RUNTIME_DIR/omarchy-brightness-display-apple.device"
fi
device_cache="${XDG_RUNTIME_DIR:-/tmp}/omarchy-brightness-display-apple.device"
no_osd=0
if [[ ${1:-} == "--no-osd" ]]; then
no_osd=1
@@ -34,14 +28,9 @@ find_apple_display_device() {
local cached=""
local device=""
if [[ -n $device_cache && -r $device_cache ]]; then
if [[ -r $device_cache ]]; then
read -r cached <"$device_cache" || true
# Trust a cached value only if it still names a hiddev character device. A
# stale or unexpected cache (a regular file, a non-hiddev node) is ignored and
# we re-detect instead of handing an arbitrary path to asdcontrol. The globs
# are left unquoted on purpose: [[ ]] pattern-matches an unquoted right side,
# and quoting them would turn the match into a literal string comparison.
if [[ ( $cached == /dev/hiddev* || $cached == /dev/usb/hiddev* ) && -c $cached ]]; then
if [[ -n $cached && -e $cached ]]; then
printf '%s\n' "$cached"
return 0
fi
@@ -50,9 +39,7 @@ find_apple_display_device() {
device="$(detect_apple_display_device)" || return 1
[[ -n $device ]] || return 1
if [[ -n $device_cache ]]; then
printf '%s\n' "$device" >"$device_cache"
fi
printf '%s\n' "$device" >"$device_cache"
printf '%s\n' "$device"
}
+1 -1
View File
@@ -170,7 +170,7 @@ main() {
local length payload url
# Detached worker: this is what actually runs yt-dlp and fires notifications.
if [[ ${1:-} == "--download" ]]; then
if [[ "${1:-}" == "--download" ]]; then
download_url "$2"
fi
-73
View File
@@ -1,73 +0,0 @@
#!/bin/bash
# omarchy:summary=Silence crash notifications for one program, or list what is silenced
# omarchy:args=[--] [<program>] [on|off|toggle]
# omarchy:examples=omarchy crash mute | omarchy crash mute hyprland | omarchy crash mute /usr/bin/hyprland | omarchy crash mute hyprland off
# The flag omarchy-crash-watch reads before announcing a crash. Muting is per
# program; Trigger > Toggle > Crash Capture is the switch for all of them.
set -uo pipefail
readonly MUTES="$HOME/.local/state/omarchy/toggles/crash-ignore"
usage() {
echo "Usage: omarchy crash mute [--] [<program>] [on|off|toggle]" >&2
}
# Only regular files, because that is all the watcher honours: anything else in
# there would be reported as muted while the crashes kept arriving. The dotted
# glob is for a program legitimately called .hidden, and `.` and `..` fail the
# same -f test that keeps them out.
list() {
local entry found=0
for entry in "$MUTES"/* "$MUTES"/.*; do
[[ -f $entry ]] || continue
printf '%s\n' "${entry##*/}"
found=1
done
((found)) || echo "No programs muted. Crashes all notify."
}
# A program may be named -h, and the router answers that with its own help
# before this ever runs. `omarchy crash mute -- -h` is the way through.
[[ ${1:-} == "--" ]] && shift
if (($# == 0)); then
list
exit 0
fi
program=$1
action=${2:-on}
# The watcher keys the mute on the executable's basename, so accept the path it
# reports as readily as the name, and reduce either the same way it does.
program=${program##*/}
if [[ -z $program || $program == "." || $program == ".." ]]; then
echo "Not a program name: $1" >&2
usage
exit 1
fi
case "$action" in
on|off|toggle) ;;
*)
echo "Not an action: $action" >&2
usage
exit 1
;;
esac
omarchy-toggle "crash-ignore/$program" "$action" || exit 1
# Report what is now true rather than what was asked for: the flag is what the
# watcher reads, and a toggle does not say which way it went.
if omarchy-toggle-enabled "crash-ignore/$program"; then
echo "Muted crash notifications for $program."
else
echo "Crash notifications for $program are back on."
fi
+5 -28
View File
@@ -48,17 +48,12 @@ announce() {
# -n 0 so a restart does not re-announce crashes already dealt with.
journalctl -f -n 0 -o json "MESSAGE_ID=$COREDUMP_MESSAGE_ID" 2>/dev/null |
while IFS= read -r entry; do
# A dash for a field that is empty as well as one that is missing: tab is
# IFS whitespace, so an empty field collapses into the next delimiter and
# every field after it shifts along one. A process can set its own comm to
# nothing, and that crash used to be read as somebody else's and dropped.
IFS=$'\t' read -r uid comm pid exe signal < <(
jq -r 'def field: if . == null or . == "" then "-" else . end;
[(._UID | field),
(.COREDUMP_COMM | field),
(.COREDUMP_PID | field),
(.COREDUMP_EXE | field),
(.COREDUMP_SIGNAL_NAME | field)] | @tsv' <<<"$entry" 2>/dev/null
jq -r '[(._UID // "-"),
(.COREDUMP_COMM // "-"),
(.COREDUMP_PID // "-"),
(.COREDUMP_EXE // "-"),
(.COREDUMP_SIGNAL_NAME // "-")] | @tsv' <<<"$entry" 2>/dev/null
)
[[ $pid =~ ^[0-9]+$ ]] || continue
@@ -76,29 +71,11 @@ journalctl -f -n 0 -o json "MESSAGE_ID=$COREDUMP_MESSAGE_ID" 2>/dev/null |
name=$comm
[[ $exe == /* ]] && name=${exe##*/}
# A process can set its own comm to anything prctl takes, slashes included,
# and a crash with no recorded executable falls back to it. The mute below
# turns this name into a path, so keep it one component: a crash must not
# reach a flag outside crash-ignore/, nor have a diagnosis write one there.
name=${name##*/}
# What that leaves is not always a name. "/" leaves nothing, which is no
# kind of array subscript and no kind of toast; a dot component names a
# directory rather than a flag, so a mute on it would touch that directory
# and then never match; and a dash is what the read above puts there when
# the crash recorded no name at all.
[[ -n $name && $name != "-" && $name != "." && $name != ".." ]] || name=unknown
[[ -n $ignore_pattern && $name =~ $ignore_pattern ]] && continue
# Never announce our own machinery, or it notifies about itself.
[[ $name == omarchy-crash-* || $name == omarchy-agent-* ]] && continue
# Muted at the end of a diagnosis, when the user was offered it and said
# yes. A flag per program rather than one list, so omarchy-crash-mute can
# lift one without reading, rewriting and re-parsing the rest.
omarchy-toggle-enabled "crash-ignore/$name" && continue
now=$EPOCHSECONDS
(((now - ${last_notified[$name]:-0}) < dedupe_seconds)) && continue
+3 -4
View File
@@ -1,7 +1,7 @@
#!/bin/bash
# omarchy:summary=Set and launch the default coding agent
# omarchy:args=[pi|omp|opencode|ori|claude|codex|grok|agy|copilot|crush]
# omarchy:args=[pi|omp|opencode|claude|codex|grok|gemini|copilot|crush]
# omarchy:examples=omarchy default agent | omarchy default agent codex | omarchy default agent claude
installing=false
@@ -27,15 +27,14 @@ case "$1" in
pi) agent="pi"; name="Pi" ;;
omp | oh-my-pi) agent="omp"; name="Oh My Pi"; agent_package="github:can1357/oh-my-pi" ;;
opencode | open-code) agent="opencode"; name="OpenCode" ;;
ori | openrouter) agent="ori"; name="Ori"; agent_package="github:OpenRouterLabs/ori-releases" ;;
claude | claude-code) agent="claude"; name="Claude Code" ;;
codex) agent="codex"; name="Codex" ;;
crush) agent="crush"; name="Crush" ;;
grok) agent="grok"; name="Grok"; agent_package="npm:@xai-official/grok" ;;
agy | antigravity | antigravity-cli | gemini | gemini-cli) agent="agy"; name="Antigravity"; agent_package="antigravity-cli" ;;
gemini | gemini-cli) agent="gemini"; name="Gemini" ;;
copilot | github-copilot) agent="copilot"; name="GitHub Copilot" ;;
*)
echo "Usage: omarchy-default-agent <pi|omp|opencode|ori|claude|codex|grok|agy|copilot|crush>"
echo "Usage: omarchy-default-agent <pi|omp|opencode|claude|codex|grok|gemini|copilot|crush>"
exit 1
;;
esac
+7 -21
View File
@@ -4,12 +4,6 @@
# omarchy:args=[chromium|chrome|brave|brave-origin|edge|firefox|zen]
# omarchy:examples=omarchy default browser firefox | omarchy default browser brave
installing=false
if [[ ${1:-} == "--install" ]]; then
installing=true
shift
fi
if (($# == 0)); then
case "$(env -u BROWSER xdg-settings get default-web-browser)" in
chromium.desktop) echo "chromium" ;;
@@ -25,27 +19,19 @@ if (($# == 0)); then
fi
case "$1" in
chromium) browser="chromium"; command="chromium"; desktop_id="chromium.desktop"; name="Chromium"; glyph= ;;
chrome) browser="chrome"; command="google-chrome-stable"; desktop_id="google-chrome.desktop"; name="Chrome"; glyph=󰊯 ;;
brave) browser="brave"; command="brave"; desktop_id="brave-browser.desktop"; name="Brave"; glyph=󰖟 ;;
brave-origin) browser="brave-origin"; command="brave-origin"; desktop_id="brave-origin.desktop"; name="Brave Origin"; glyph=󰖟 ;;
edge) browser="edge"; command="microsoft-edge-stable"; desktop_id="microsoft-edge.desktop"; name="Edge"; glyph=󰇩 ;;
firefox) browser="firefox"; command="firefox"; desktop_id="firefox.desktop"; name="Firefox"; glyph=󰈹 ;;
zen) browser="zen"; command="zen-browser"; desktop_id="zen.desktop"; name="Zen"; glyph=󰖟 ;;
chromium) desktop_id="chromium.desktop"; name="Chromium"; glyph= ;;
chrome) desktop_id="google-chrome.desktop"; name="Chrome"; glyph=󰊯 ;;
brave) desktop_id="brave-browser.desktop"; name="Brave"; glyph=󰖟 ;;
brave-origin) desktop_id="brave-origin.desktop"; name="Brave Origin"; glyph=󰖟 ;;
edge) desktop_id="microsoft-edge.desktop"; name="Edge"; glyph=󰇩 ;;
firefox) desktop_id="firefox.desktop"; name="Firefox"; glyph=󰈹 ;;
zen) desktop_id="zen.desktop"; name="Zen"; glyph=󰖟 ;;
*)
echo "Usage: omarchy-default-browser <chromium|chrome|brave|brave-origin|edge|firefox|zen>"
exit 1
;;
esac
if omarchy-cmd-missing "$command"; then
if [[ $installing == "false" ]]; then
exec omarchy-launch-floating-terminal-with-presentation omarchy-default-browser --install "$browser"
else
omarchy-install-browser "$browser" || exit 1
fi
fi
env -u BROWSER xdg-settings set default-web-browser "$desktop_id" || exit 1
omarchy-notification-send -g $glyph "$name is now the default browser"
+8 -31
View File
@@ -4,12 +4,6 @@
# omarchy:args=[code|cursor|zed|sublime_text|helix|vim|emacs|nvim]
# omarchy:examples=omarchy default editor | omarchy default editor code | omarchy default editor helix
installing=false
if [[ ${1:-} == "--install" ]]; then
installing=true
shift
fi
editor_file="$HOME/.local/state/omarchy/defaults/editor"
if (($# == 0)); then
@@ -22,37 +16,20 @@ if (($# == 0)); then
fi
case "$1" in
code) selection="code"; editor="code"; name="VSCode"; glyph= ;;
cursor) selection="cursor"; editor="cursor"; name="Cursor"; glyph= ;;
zed | zeditor) selection="zed"; editor="zeditor"; name="Zed"; glyph= ;;
sublime_text) selection="sublime_text"; editor="sublime_text"; name="Sublime Text"; glyph= ;;
helix) selection="helix"; editor="helix"; name="Helix"; glyph= ;;
vim) selection="vim"; editor="vim"; name="Vim"; glyph= ;;
emacs) selection="emacs"; editor="emacs"; name="Emacs"; glyph= ;;
nvim) selection="nvim"; editor="nvim"; name="Neovim"; glyph= ;;
code) editor="code"; name="VSCode"; glyph= ;;
cursor) editor="cursor"; name="Cursor"; glyph= ;;
zed | zeditor) editor="zeditor"; name="Zed"; glyph= ;;
sublime_text) editor="sublime_text"; name="Sublime Text"; glyph= ;;
helix) editor="helix"; name="Helix"; glyph= ;;
vim) editor="vim"; name="Vim"; glyph= ;;
emacs) editor="emacs"; name="Emacs"; glyph= ;;
nvim) editor="nvim"; name="Neovim"; glyph= ;;
*)
echo "Usage: omarchy-default-editor <code|cursor|zed|sublime_text|helix|vim|emacs|nvim>"
exit 1
;;
esac
if omarchy-cmd-missing "$editor"; then
if [[ $installing == "false" ]]; then
exec omarchy-launch-floating-terminal-with-presentation omarchy-default-editor --install "$selection"
else
case "$selection" in
code) omarchy-install-editor-vscode ;;
cursor) omarchy-pkg-add cursor-bin ;;
zed) omarchy-install-editor-zed ;;
sublime_text) omarchy-pkg-add sublime-text-4 ;;
helix) omarchy-install-editor-helix ;;
vim) omarchy-pkg-add vim ;;
emacs) omarchy-install-editor-emacs ;;
nvim) omarchy-pkg-add neovim ;;
esac || exit 1
fi
fi
mkdir -p "$(dirname "$editor_file")"
printf '%s\n' "$editor" >"$editor_file"
+4 -18
View File
@@ -4,12 +4,6 @@
# omarchy:args=[alacritty|foot|ghostty|kitty]
# omarchy:examples=omarchy default terminal ghostty | omarchy default terminal kitty
installing=false
if [[ ${1:-} == "--install" ]]; then
installing=true
shift
fi
if (($# == 0)); then
desktop_id=$(xdg-terminal-exec --print-id 2>/dev/null || true)
desktop_id=${desktop_id%%:*}
@@ -24,24 +18,16 @@ if (($# == 0)); then
fi
case "$1" in
alacritty) terminal="alacritty"; desktop_id="Alacritty.desktop"; name="Alacritty"; glyph= ;;
foot) terminal="foot"; desktop_id="foot.desktop"; name="Foot"; glyph= ;;
ghostty) terminal="ghostty"; desktop_id="com.mitchellh.ghostty.desktop"; name="Ghostty"; glyph= ;;
kitty) terminal="kitty"; desktop_id="kitty.desktop"; name="Kitty"; glyph= ;;
alacritty) desktop_id="Alacritty.desktop"; name="Alacritty"; glyph= ;;
foot) desktop_id="foot.desktop"; name="Foot"; glyph= ;;
ghostty) desktop_id="com.mitchellh.ghostty.desktop"; name="Ghostty"; glyph= ;;
kitty) desktop_id="kitty.desktop"; name="Kitty"; glyph= ;;
*)
echo "Usage: omarchy-default-terminal <alacritty|foot|ghostty|kitty>"
exit 1
;;
esac
if omarchy-cmd-missing "$terminal"; then
if [[ $installing == "false" ]]; then
exec omarchy-launch-floating-terminal-with-presentation omarchy-default-terminal --install "$terminal"
else
omarchy-install-terminal "$terminal" || exit 1
fi
fi
cat >~/.config/xdg-terminals.list <<EOF
# Terminal emulator preference order for xdg-terminal-exec
# The first found and valid terminal will be used
+2 -6
View File
@@ -34,12 +34,8 @@ systemd, shell, or app-launcher environment; reboot to make every layer agree.
Affects only \$OMARCHY_PATH-resolved trees: bin/, default/, shell/,
themes/, applications/, config/. Files installed at fixed system paths
(/etc/, /usr/lib/systemd/, udev rule bodies, /etc/skel after user
creation) are NOT covered — for those, use omarchy-dev-pkg-test to build
and install the package from the checkout.
The Plymouth and SDDM themes under /usr/share are the exception: omarchy
plymouth set and omarchy refresh plymouth republish them from the checkout,
reading this link's authorization out of the root-owned /etc/omarchy.conf.
creation, /usr/share/plymouth) are NOT covered — for those, use
omarchy-dev-pkg-test to build and install the package from the checkout.
Also writes $sudoers_file so sudo resolves omarchy-*
from the checkout instead of the packaged copies. That part takes effect
-6
View File
@@ -149,9 +149,6 @@ write_networkmanager_dns() {
local servers="$1"
install -d -m 0755 "$(dirname "$NM_DNS_CONF")"
# omarchy:heredoc-expands paths=none -- $servers is a normalized, single-line
# DNS server list written as data, not a path or command; nothing user-writable
# is resolved or executed from the root-owned drop-in.
cat >"$NM_DNS_CONF" <<EOF
# Managed by omarchy-dns. Remove this file or run omarchy dns DHCP to use DHCP DNS again.
[global-dns]
@@ -305,9 +302,6 @@ Custom)
split_dns_servers "$dns_servers"
write_networkmanager_dns "$dns_servers"
set_connection_dns "$ipv4_dns" "$ipv6_dns"
# omarchy:heredoc-expands paths=none -- $dns_servers is a normalized,
# single-line DNS server list; the //,/ turns its comma separators into the
# spaces resolved.conf wants. It is written as data, not a path or command.
tee /etc/systemd/resolved.conf >/dev/null <<EOF
[Resolve]
DNS=${dns_servers//,/ }
-8
View File
@@ -1,8 +0,0 @@
#!/bin/bash
# omarchy:summary=Match the Dell XPS 13 DX13260 that requires the sidecar amplifier workaround.
product_sku="${OMARCHY_DMI_PRODUCT_SKU:-/sys/class/dmi/id/product_sku}"
omarchy-hw-match "DX13260" &&
grep -qix "0E53" "$product_sku" 2>/dev/null
+1 -7
View File
@@ -16,12 +16,6 @@ fi
printf -v install_message '%q' "Installing ${name}..."
printf -v desktop_id_arg '%q' "$desktop_id"
# The list has to reach omarchy-pkg-add as several words, so each word is quoted
# rather than the whole string; -d '' reads past newlines and always ends at EOF.
read -r -d '' -a package_list <<<"$packages" || true
printf -v packages_arg '%q ' "${package_list[@]}"
packages_arg="${packages_arg% }"
# The subshell keeps & from backgrounding the package installation too.
exec omarchy-launch-floating-terminal-with-presentation \
"echo ${install_message}; omarchy-pkg-add ${packages_arg} && (setsid uwsm-app -- gtk-launch ${desktop_id_arg} >/dev/null 2>&1 &)"
"echo ${install_message}; omarchy-pkg-add ${packages} && (setsid uwsm-app -- gtk-launch ${desktop_id_arg} >/dev/null 2>&1 &)"
+1 -10
View File
@@ -12,13 +12,4 @@ if [[ -z $name || -z $packages ]]; then
exit 1
fi
printf -v install_message '%q' "Installing ${name}..."
# The list has to reach omarchy-pkg-add as several words, so each word is quoted
# rather than the whole string; -d '' reads past newlines and always ends at EOF.
read -r -d '' -a package_list <<<"$packages" || true
printf -v packages_arg '%q ' "${package_list[@]}"
packages_arg="${packages_arg% }"
exec omarchy-launch-floating-terminal-with-presentation \
"echo ${install_message}; omarchy-pkg-add ${packages_arg}"
exec omarchy-launch-floating-terminal-with-presentation "echo 'Installing ${name}...'; omarchy-pkg-add ${packages}"
+19 -24
View File
@@ -1,15 +1,12 @@
#!/bin/bash
# omarchy:summary=Install a supported browser
# omarchy:args=<chromium|chrome|brave|brave-origin|edge|firefox|zen>
# omarchy:examples=omarchy install browser chromium | omarchy install browser firefox
# omarchy:args=<chrome|brave|brave-origin|edge|firefox|zen>
# omarchy:examples=omarchy install browser firefox | omarchy install browser brave
set -e
source "$OMARCHY_PATH/install/helpers/browser-policy.sh"
setup_chromium_policy_directory() {
browser_policy_setup_dir "$1"
setup_policy_directory() {
sudo mkdir -p "$1"
sudo chmod a+rw "$1"
}
announce_browser_installed() {
@@ -24,26 +21,24 @@ copy_chromium_flags() {
omarchy-install-chromium-ytdlp
}
setup_firefox_preferences() {
local distribution_dir="$1"
setup_policy_directory "$distribution_dir"
sudo cp -f "$OMARCHY_PATH/default/firefox/policies.json" "$distribution_dir/policies.json"
}
setup_firefox_wayland() {
mkdir -p ~/.config/environment.d
echo "MOZ_ENABLE_WAYLAND=1" > ~/.config/environment.d/omarchy-firefox-wayland.conf
}
case $1 in
chromium)
echo "Installing Chromium..."
omarchy-pkg-add chromium
setup_chromium_policy_directory /etc/chromium/policies/managed
copy_chromium_flags ~/.config/chromium-flags.conf
omarchy-theme-set-browser
announce_browser_installed "Chromium"
;;
chrome)
echo "Installing Chrome..."
omarchy-pkg-aur-add google-chrome || exit 1
setup_chromium_policy_directory /etc/opt/chrome/policies/managed
setup_policy_directory /etc/opt/chrome/policies/managed
copy_chromium_flags ~/.config/chrome-flags.conf
omarchy-theme-set-browser
announce_browser_installed "Chrome"
@@ -52,7 +47,7 @@ edge)
echo "Installing Edge..."
omarchy-pkg-aur-add microsoft-edge-stable-bin || exit 1
setup_chromium_policy_directory /etc/opt/edge/policies/managed
setup_policy_directory /etc/opt/edge/policies/managed
copy_chromium_flags ~/.config/microsoft-edge-stable-flags.conf
omarchy-theme-set-browser
announce_browser_installed "Edge"
@@ -61,7 +56,7 @@ brave)
echo "Installing Brave..."
omarchy-pkg-aur-add brave-bin || exit 1
setup_chromium_policy_directory /etc/brave/policies/managed
setup_policy_directory /etc/brave/policies/managed
copy_chromium_flags ~/.config/brave-flags.conf
omarchy-theme-set-browser
announce_browser_installed "Brave"
@@ -70,7 +65,7 @@ brave-origin)
echo "Installing Brave Origin..."
omarchy-pkg-aur-add brave-origin-bin || exit 1
setup_chromium_policy_directory /etc/brave/policies/managed
setup_policy_directory /etc/brave/policies/managed
copy_chromium_flags ~/.config/brave-origin-flags.conf
omarchy-theme-set-browser
announce_browser_installed "Brave Origin"
@@ -79,7 +74,7 @@ firefox)
echo "Installing Firefox..."
omarchy-pkg-add firefox || exit 1
browser_policy_setup_firefox_distribution /usr/lib/firefox/distribution
setup_firefox_preferences /usr/lib/firefox/distribution
setup_firefox_wayland
announce_browser_installed "Firefox"
;;
@@ -87,12 +82,12 @@ zen)
echo "Installing Zen..."
omarchy-pkg-aur-add zen-browser-bin || exit 1
browser_policy_setup_firefox_distribution /opt/zen-browser/distribution
setup_firefox_preferences /opt/zen-browser/distribution
setup_firefox_wayland
announce_browser_installed "Zen"
;;
*)
echo "Usage: omarchy-install-browser <chromium|chrome|brave|brave-origin|edge|firefox|zen>"
echo "Usage: omarchy-install-browser <chrome|brave|brave-origin|edge|firefox|zen>"
exit 1
;;
esac
+1 -5
View File
@@ -13,9 +13,5 @@ if [[ -z $name || -z $package || -z $family ]]; then
exit 1
fi
printf -v install_message '%q' "Installing ${name}..."
printf -v package_arg '%q' "$package"
printf -v family_arg '%q' "$family"
exec omarchy-launch-floating-terminal-with-presentation \
"echo ${install_message}; omarchy-pkg-add ${package_arg} && sleep 2 && omarchy-font-set ${family_arg}"
"echo 'Installing ${name}...'; omarchy-pkg-add ${package} && sleep 2 && omarchy-font-set '${family}'"
+1 -1
View File
@@ -10,4 +10,4 @@ echo "Enabling ONCE background service..."
sudo systemctl enable --now once-background.service
echo -e "\nLaunching ONCE..."
sudo once
once
+13 -196
View File
@@ -10,50 +10,10 @@
LOGO_FILE="$HOME/.config/omarchy/branding/about.txt"
FIT_FILE="$HOME/.local/state/omarchy/windows/about.fit"
OMARCHY_FASTFETCH_DIR=/etc/fastfetch
# The logo block in the fastfetch config. The fit below reproduces this layout to
# size the window, and the sheen has to repaint the very cells fastfetch drew the
# logo on, so both read the padding from here.
LOGO_PAD_LEFT=2
LOGO_PAD_TOP=2
LOGO_PAD_RIGHT=6
# The content is measured once and then goes on living: an uptime that turns
# minutes into hours and hours into days, a version string that grows, a module
# that shows up on the next boot. A window fitted to exactly what was measured
# has nowhere to put any of it, and the layout clips or scrolls the moment it
# grows — which is also the moment the logo stops being where it was drawn. Keep
# a little in hand rather than measure again every time something ticks over.
FIT_SPARE_COLUMNS=2
FIT_SPARE_ROWS=1
POLL_SECONDS=0.5
# fastfetch has no animation of its own, so the sheen is ours. It knows about a
# logo and nothing about About, which is why it is a file of its own.
source omarchy-branding-about-animation
# A user's own fastfetch config can relocate or restyle the logo in ways this
# measurement cannot see, so leave sizing to the float rule in that case. It can
# sit in any of several directories fastfetch searches ahead of Omarchy's own, so
# ask fastfetch for that order rather than keep a copy here for its next release
# to outdate.
# A user-level fastfetch config can relocate or restyle the logo in ways this
# measurement cannot see, so leave sizing to the float rule in that case.
custom_fastfetch_config() {
local directory listed=false
# A whole line at a time, because a home directory may contain a space, and the
# marker fastfetch puts beside the config it settled on is not part of the path.
while IFS= read -r directory; do
listed=true
directory=${directory% (\*)}
[[ ${directory%/} == "$OMARCHY_FASTFETCH_DIR" ]] && return 1
[[ -f ${directory%/}/config.jsonc ]] && return 0
done < <(fastfetch --list-config-paths 2>/dev/null)
# Silence is not the same answer as "none of them", so fall back to the
# directory fastfetch has always looked in first rather than read it as one.
[[ $listed == true ]] && return 1
[[ -f $HOME/.config/fastfetch/config.jsonc ]]
}
@@ -140,22 +100,17 @@ fit_window() {
# The guard character keeps command substitution from eating the trailing
# break line, which provides the bottom padding row.
local modules module_w
local modules module_w module_h
modules=$(fastfetch --logo none | sed 's/\x1b\[[0-9;?]*[a-zA-Z]//g'; printf X)
modules=${modules%X}
module_w=$(printf '%s' "$modules" | display_columns)
# Ask fastfetch how tall its layout came out rather than predicting it from the
# logo and the module column: once the logo is the taller of the two, fastfetch
# writes a row more than that arithmetic expects, and a window sized by it
# scrolls the top padding away.
measure_layout || return 1
module_h=$(printf '%s' "$modules" | wc -l)
# Mirror the logo block in the fastfetch config: 2 columns of padding left of
# the logo, 6 between logo and modules. Then 2 columns of right padding to
# match, a row for the cursor so the trailing break shows, and the spare above.
local target_c=$(( LOGO_PAD_LEFT + logo_w + LOGO_PAD_RIGHT + module_w + LOGO_PAD_LEFT + FIT_SPARE_COLUMNS ))
local target_r=$(( LAYOUT_ROWS + 1 + FIT_SPARE_ROWS ))
# the logo, 6 between logo and modules, 2 rows above it. Then 2 columns of
# right padding to match, and a row for the cursor so the trailing break shows.
local target_c=$(( 2 + logo_w + 6 + module_w + 2 ))
local target_r=$(( (logo_h + 2 > module_h ? logo_h + 2 : module_h) + 1 ))
local nudges=0 rows cols address width height shift_w shift_h target_w target_h
while :; do
@@ -191,155 +146,17 @@ fit_window() {
return 1
}
# One run answers both questions the sheen has to ask first. How tall the layout
# is, because a window too small for it scrolls, which moves the logo off the rows
# the frames address. And what colour fastfetch drew the logo in, because the
# glint has to hand every cell back in the colour it arrived in — assume it, and
# a logo fastfetch colours differently comes out of the first glint a new one.
measure_layout() {
[[ -n ${LAYOUT_ROWS:-} ]] && return 0
local rendered plain needle offset found row
# --pipe false because fastfetch drops its colours when it is not writing to a
# terminal, and it is writing to this substitution.
rendered=$(fastfetch --pipe false 2>/dev/null; printf X)
rendered=${rendered%X}
LAYOUT_ROWS=$(printf '%s' "$rendered" | wc -l)
(( LAYOUT_ROWS > 0 )) || return 1
# Find the logo in what fastfetch drew rather than working it out from the
# padding this file was written against. The config that runs is the one in
# /etc, which a checkout does not replace, so the two can disagree — and a logo
# measured two rows above where it was drawn is a logo the sheen moves. Not
# finding it at all is the same answer as finding it somewhere unexpected:
# whatever is on screen is not the text in the file, so leave it alone.
IFS=$'\t' read -r offset needle < <(logo_landmark) || return 1
[[ -n $needle ]] || return 1
plain=$(printf '%s' "$rendered" | sed 's/\x1b\[[0-9;?]*[a-zA-Z]//g')
found=$(printf '%s' "$plain" | LC_ALL=C.UTF-8 awk -v needle="$needle" \
'index($0, needle) { print NR, index($0, needle); exit }')
[[ -n $found ]] || return 1
read -r LOGO_ROW LOGO_COLUMN <<<"$found"
LOGO_ROW=$(( LOGO_ROW - offset ))
(( LOGO_ROW >= 1 && LOGO_COLUMN >= 1 )) || return 1
# Whatever fastfetch set before that row is what the sheen has to give back.
row=$(printf '%s' "$rendered" | sed -n "$(( LOGO_ROW + offset ))p")
LOGO_COLOR=""
[[ $row =~ ^(($ESC\[[0-9;]*m)+) ]] && LOGO_COLOR=${BASH_REMATCH[1]}
return 0
}
# The longest line of the logo, and how far down the logo it sits — the most
# distinctive thing to look for in the render, and the offset that turns where it
# was found back into where the logo starts.
# Tab-separated, and the offset first, because the line may hold spaces of its
# own and splitting on them would cut the landmark short.
logo_landmark() {
LC_ALL=C.UTF-8 awk '
{ if (length($0) > best) { best = length($0); line = $0; at = NR - 1 } }
END { if (best > 0) printf "%d\t%s\n", at, line }
' "$LOGO_FILE" 2>/dev/null
}
# The About screen's own reasons the logo might not be where these frames would
# draw it. Whether the logo itself can be animated is the sheen's own question.
build_sheen() {
# Whatever the last build left is not this window's, and the loop below plays
# whatever is here — so a build that fails has to leave nothing to play.
SHEEN_FRAMES=()
custom_fastfetch_config && return 1
# fastfetch honours NO_COLOR when it writes to a terminal but not when it writes
# to the measurement below, so a logo drawn without colour would be measured as
# green and left green by the first glint. A glint is colour anyway, which is
# the thing NO_COLOR asks for none of.
[[ -n ${NO_COLOR:-} ]] && return 1
measure_layout || return 1
# The layout needs a row for the cursor past its last line. Without one it has
# scrolled, and the logo is no longer on the rows the frames address. Ask the
# terminal where the cursor actually is rather than trust the arithmetic, and
# keep the arithmetic for a terminal that will not say.
local rows cols
read -r rows cols <<<"$(stty size)"
(( rows > LAYOUT_ROWS )) || return 1
# The cell the logo's first row starts on, every attribute fastfetch left on
# those cells so a glint that has passed leaves them as it found them, and the
# room it has to work in left of the module column.
sheen_build "$LOGO_FILE" "$LOGO_ROW" "$LOGO_COLUMN" "${ESC}[0m${LOGO_COLOR}" "$(( cols - LOGO_COLUMN + 1 ))"
}
# What the frames were built against. A window that resized, or a logo that was
# rebranded, needs fastfetch run again before anything is drawn over it.
content_changed() {
[[ $resized == true ]] && return 0
[[ $(stty size) != "$grid" || $(stat -c %Y "$LOGO_FILE" 2>/dev/null) != "$logo_stamp" ]]
}
# A tick either times out, which is the delay, or a key arrives and About closes.
# Anything else on stdin is a terminal that went away, which closes it too.
tick() {
read -t "$1" -n 1 -s && exit
(( $? > 128 )) || exit
}
play_sheen() {
local index
for (( index = 0; index < ${#SHEEN_FRAMES[@]}; index++ )); do
# Stop before painting a frame rather than after: a resize has already moved
# the cells these address, and the rest of a sweep would land across them.
# The grid costs a process, so it stays on the poll interval. The trap costs
# nothing, so it is read last — a signal that arrived while the grid was being
# read would otherwise be seen only after another frame had gone out.
(( index % SHEEN_POLL_FRAMES == 0 )) && content_changed && return 1
[[ $resized == true ]] && return 1
printf '%s' "${SHEEN_FRAMES[index]}"
tick "$SHEEN_FRAME_SECONDS"
done
return 0
}
# The logo is still between glints, so About is a quiet window to leave open.
rest_sheen() {
local ticks
for (( ticks = 0; ticks < SHEEN_REST_TICKS; ticks++ )); do
tick "$POLL_SECONDS"
content_changed && return 1
done
return 0
}
if [[ ${1:-} == "--render" ]]; then
printf '\e[?25l'
# A sweep runs for seconds between polls, so it reads this instead. The polling
# stays as the backstop, for a signal that arrived while it could not be taken.
resized=false
trap 'resized=true' WINCH
# Give the compositor a moment to apply the window rules before measuring cells.
settle_grid
fitted=false
passes=0
while :; do
grid=$(stty size)
size=$(stty size)
logo_stamp=$(stat -c %Y "$LOGO_FILE" 2>/dev/null)
resized=false
LAYOUT_ROWS=""
clear
fastfetch
# A second pass picks up a fit that could not measure the window the first
@@ -348,10 +165,10 @@ if [[ ${1:-} == "--render" ]]; then
if [[ $fitted == false ]] && (( ++passes <= 2 )); then
fit_window && fitted=true
fi
# An empty frame list plays nothing, so a logo that cannot be animated waits
# here exactly as the still one did, and there is one loop rather than two.
build_sheen
while play_sheen && rest_sheen; do :; done
while [[ $(stty size) == $size && $(stat -c %Y "$LOGO_FILE" 2>/dev/null) == $logo_stamp ]]; do
read -t 0.5 -n 1 -s && exit
(( $? > 128 )) || exit
done
# A rebranded logo changes the content dimensions, so measure again.
if [[ $(stat -c %Y "$LOGO_FILE" 2>/dev/null) != $logo_stamp ]]; then
fitted=false
+4
View File
@@ -2,6 +2,10 @@
# omarchy:summary=Launch the Omarchy screensaver in the default terminal on the system with the correct font configuration.
if omarchy-cmd-missing ttfx; then
exit 1
fi
# Exit early if screensaver is already running
pgrep -f '[o]rg.omarchy.screensaver' && exit 0
+4 -76
View File
@@ -38,12 +38,6 @@ parse_keycodes() {
if (code != "" && symbol != "" && symbol != "NoSymbol") keycode_symbol[code] = toupper(symbol)
}
# Read off the keysym rather than the keycode, so the key left of 1 reads
# as the symbol printed on it whichever key the active layout puts it on.
for (code in keycode_symbol) {
if (keycode_symbol[code] == "GRAVE") keycode_symbol[code] = "~"
}
mouse_symbol["272"] = "LEFT MOUSE BUTTON"
mouse_symbol["273"] = "RIGHT MOUSE BUTTON"
mouse_symbol["274"] = "MIDDLE MOUSE BUTTON"
@@ -311,7 +305,6 @@ dynamic_bindings() {
case "$key" in
comma) key="COMMA" ;;
grave) key="~" ;;
period) key="PERIOD" ;;
minus) key="MINUS" ;;
equal) key="EQUAL" ;;
@@ -331,41 +324,15 @@ static_bindings() {
echo "SHIFT ALT,D,Download Video from Web App,sendshortcut,SHIFT ALT,D,"
}
# Actions Omarchy binds to a second chord meant as an alternative, named one at
# a time. 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. Only chords a user reaches
# for interchangeably belong on one row.
alternative_chord_actions() {
cat <<'ACTIONS'
Close window
Calculator
Toggle scratchpad
Move window to scratchpad
ACTIONS
}
# Parse and format keybindings
#
# `awk` does the heavy lifting:
# - Set the field separator to a comma ','.
# - Joins the key combination (e.g., "SUPER + Q").
# - Joins the command that the key executes.
# - Puts an action's alternative chord on the row with the first one.
# - Prints display text and dispatch metadata as tab-separated fields.
parse_binding_records() {
awk -F, -v alternatives="$(alternative_chord_actions)" '
BEGIN {
# The column every row pads its chords to. Nothing is allowed past it: the
# menu renders in monospace, and a row that overruns pushes its arrow out
# of a column the eye reads straight down.
column = 35;
split(alternatives, named, "\n");
for (i in named) {
if (named[i] != "") shares_a_row[named[i]] = 1;
}
}
awk -F, '
{
# Combine the modifier and key (first two fields)
key_combo = $1 " + " $2;
@@ -398,35 +365,8 @@ BEGIN {
gsub(/[ \t]+/, " ", key_combo); # Collapse multiple spaces to one
}
# An alternative chord joins the row the first one opened: "SUPER + W /
# SUPER + Q -> Close window". Both halves have to agree on what they run,
# since a label is only what a chord is called; an unresolved dispatcher
# says nothing at all, so it never counts as agreement.
if (action != "") {
action_key = action SUBSEP dispatcher SUBSEP arg;
together = "";
if (action in shares_a_row && dispatcher != "" && action_key in leads) {
together = chords[leads[action_key]] " / " key_combo;
}
if (together != "" && length(together) <= column) {
chords[leads[action_key]] = together;
} else {
# Also the path a pair too wide for the column takes: two rows in
# line beat one that juts out of it.
entries++;
chords[entries] = key_combo;
actions[entries] = action;
dispatchers[entries] = dispatcher;
args[entries] = arg;
leads[action_key] = entries;
}
}
}
END {
for (entry = 1; entry <= entries; entry++) {
printf "%-*s → %s\t%s\t%s\n", column, chords[entry], actions[entry], dispatchers[entry], args[entry];
printf "%-35s → %s\t%s\t%s\n", key_combo, action, dispatcher, arg;
}
}'
}
@@ -434,13 +374,7 @@ END {
prioritize_entries() {
awk -F '\t' '
{
# Alternative chords are display only. Classifying on them would read
# "SUPER SHIFT + RETURN / SUPER SHIFT + B" as the alternate browser chord
# and drop the browser 20 rows down its own list, so match the chord that
# leads the row and ignore the rest.
line = $1
sub(/ \/ [^→]*→/, " →", line)
prio = 50
if (match(line, /Keybindings/)) prio = 0
if (match(line, /Omarchy menu/)) prio = 1
@@ -502,12 +436,6 @@ prioritize_entries() {
cut -f2-
}
# Drop repeated records while keeping the order Hyprland reported them in, so
# the chord a user declared first is the one that leads a merged entry.
dedupe_binding_records() {
awk '!seen[$0]++'
}
output_binding_records_uncached() {
local dynamic
@@ -518,7 +446,7 @@ output_binding_records_uncached() {
[[ -n $dynamic ]] && printf '%s\n' "$dynamic"
static_bindings
} |
dedupe_binding_records |
sort -u |
parse_keycodes |
parse_binding_records |
prioritize_entries
@@ -530,7 +458,7 @@ output_binding_records_uncached() {
keybindings_cache_key() {
{
printf 'v13\n'
printf 'v11\n'
hyprctl devices 2>/dev/null | grep -F 'active keymap:'
hyprctl binds 2>/dev/null
} | sha256sum | awk '{ print $1 }'
+3 -3
View File
@@ -85,16 +85,16 @@ wait_for_pacman_transaction
mkdir -p "$STATE_DIR"
[[ -d $MIGRATIONS_DIR ]] || exit 0
while IFS=$'\t' read -r name file marker <&3; do
while IFS=$'\t' read -r name file marker; do
[[ -n $name ]] || continue
if [[ ! -f $marker ]]; then
echo -e "\e[32m\nRunning migration (${name%.sh})\e[0m"
OMARCHY_PATH="$OMARCHY_PATH" bash -euo pipefail "$file" 3<&-
OMARCHY_PATH="$OMARCHY_PATH" bash -euo pipefail "$file"
mkdir -p "$(dirname "$marker")"
touch "$marker"
fi
done 3< <(migration_entries)
done < <(migration_entries)
# Clear a login-time notification the user left sitting there and then resolved
# by running migrations some other way. The substring matches both the current
+13 -8
View File
@@ -34,18 +34,23 @@ print_status() {
return
fi
nm=$(nmcli -t -f GENERAL.STATE,GENERAL.CONNECTION dev show "$device" 2>/dev/null)
state=$(awk -F: '$1 == "GENERAL.STATE" { print $2; exit }' <<<"$nm")
ssid=$(awk -F: '$1 == "GENERAL.CONNECTION" { print $2; exit }' <<<"$nm")
signal=$(nmcli -t -f IN-USE,SIGNAL dev wifi list ifname "$device" --rescan no 2>/dev/null | awk -F: '$1 == "*" { print $2; exit }')
freq=$(iw dev "$device" link 2>/dev/null | awk '/freq:/ { print $2; exit }')
if omarchy-cmd-present nmcli; then
nm=$(nmcli -t -f GENERAL.STATE,GENERAL.CONNECTION dev show "$device" 2>/dev/null)
state=$(awk -F: '$1 == "GENERAL.STATE" { print $2; exit }' <<<"$nm")
ssid=$(awk -F: '$1 == "GENERAL.CONNECTION" { print $2; exit }' <<<"$nm")
signal=$(nmcli -t -f IN-USE,SIGNAL dev wifi list ifname "$device" --rescan no 2>/dev/null | awk -F: '$1 == "*" { print $2; exit }')
freq=$(iw dev "$device" link 2>/dev/null | awk '/freq:/ { print $2; exit }')
if [[ $state != 100* ]]; then
printf 'disconnected\t\t\t\n'
if [[ $state != 100* ]]; then
printf 'disconnected\t\t\t\n'
return
fi
printf 'wifi\t%s\t%s\t%s\n' "${ssid:-$device}" "$signal" "$freq"
return
fi
printf 'wifi\t%s\t%s\t%s\n' "${ssid:-$device}" "$signal" "$freq"
printf 'wifi\t%s\t\t\n' "$device"
}
ping_latency_ms() {
+2 -4
View File
@@ -3,7 +3,5 @@
# omarchy:summary=Restore the default Omarchy Plymouth boot theme and SDDM login screen
# omarchy:requires-sudo=true
set -euo pipefail
"$OMARCHY_PATH/bin/omarchy-refresh-plymouth"
"$OMARCHY_PATH/bin/omarchy-refresh-sddm"
omarchy-refresh-plymouth
omarchy-refresh-sddm
+72 -361
View File
@@ -5,377 +5,88 @@
# omarchy:examples=omarchy plymouth set '#1d2021' '#ebdbb2' ~/.local/state/omarchy/current/theme/plymouth/logo.png
# omarchy:requires-sudo=true
set -euo pipefail
# Configure the Plymouth boot theme with a custom background color, text color, and logo.
# Stages the change in a temp dir, then commits the staged files to /usr/share and
# rebuilds the initramfs. Also syncs the SDDM login screen (the post-logout
# screen) with the same colors and logo so boot/login stay visually unified.
# Build the authoritative theme in a root-owned directory, then publish each
# fixed destination atomically. The caller opens the selected logo before sudo,
# so the privileged process never resolves a user-controlled input path.
usage() {
if (( $# != 3 )); then
echo "Usage: omarchy-plymouth-set <background-hex> <text-hex> <path-to-logo.png>" >&2
exit 1
}
if (( $# == 3 )); then
mode=set
elif (( $# == 1 )); then
case "$1" in
--refresh-default)
mode=refresh-plymouth
;;
--refresh-sddm-default)
mode=refresh-sddm
;;
*)
usage
;;
esac
else
usage
fi
if (( EUID == 0 )); then
echo "Error: run omarchy-plymouth-set as your user, not under sudo." >&2
bg_hex="${1#\#}"
text_hex="${2#\#}"
logo_path="$3"
if ! [[ $bg_hex =~ ^[0-9a-fA-F]{6}$ ]]; then
echo "Invalid background color: $1 (expected #RRGGBB)" >&2
exit 1
fi
logo_fd=
if [[ $mode != "set" ]]; then
bg_hex=
text_hex=
if ! [[ $text_hex =~ ^[0-9a-fA-F]{6}$ ]]; then
echo "Invalid text color: $2 (expected #RRGGBB)" >&2
exit 1
fi
if [[ ! -f $logo_path ]]; then
echo "Logo file not found: $logo_path" >&2
exit 1
fi
# omarchy-plymouth-set-by-theme passes a theme's unlock.png straight from
# ~/.config/omarchy/themes, where an installed theme can make it a symlink to
# anything. The copies below land in world-readable /usr/share, so following one
# would republish whatever it points at.
if [[ -L $logo_path ]]; then
echo "Logo file is a symlink, which is not accepted: $logo_path" >&2
exit 1
fi
bg_r=$(awk -v n=$((16#${bg_hex:0:2})) 'BEGIN{printf "%.3f", n/255}')
bg_g=$(awk -v n=$((16#${bg_hex:2:2})) 'BEGIN{printf "%.3f", n/255}')
bg_b=$(awk -v n=$((16#${bg_hex:4:2})) 'BEGIN{printf "%.3f", n/255}')
theme_dir="/usr/share/plymouth/themes/omarchy"
staging_dir=$(mktemp -d)
trap 'rm -rf "$staging_dir"' EXIT
find "$OMARCHY_PATH/default/plymouth" -maxdepth 1 -type f -exec cp -t "$staging_dir/" {} +
cp "$logo_path" "$staging_dir/logo.png"
sed -i \
-e "s/^Window.SetBackgroundTopColor.*/Window.SetBackgroundTopColor($bg_r, $bg_g, $bg_b);/" \
-e "s/^Window.SetBackgroundBottomColor.*/Window.SetBackgroundBottomColor($bg_r, $bg_g, $bg_b);/" \
"$staging_dir/omarchy.script"
for asset in bullet.png entry.png lock.png progress_bar.png; do
magick "$staging_dir/$asset" -channel RGB +level-colors "#$text_hex","#$text_hex" "$staging_dir/$asset"
done
sudo cp -a --no-preserve=mode,ownership "$staging_dir/." "$theme_dir/"
sudo plymouth-set-default-theme omarchy
if omarchy-cmd-present limine-mkinitcpio; then
sudo limine-mkinitcpio
else
bg_hex="${1#\#}"
text_hex="${2#\#}"
logo_path="$3"
if ! [[ $bg_hex =~ ^[0-9a-fA-F]{6}$ ]]; then
echo "Invalid background color: $1 (expected #RRGGBB)" >&2
exit 1
fi
if ! [[ $text_hex =~ ^[0-9a-fA-F]{6}$ ]]; then
echo "Invalid text color: $2 (expected #RRGGBB)" >&2
exit 1
fi
if [[ ! -f $logo_path ]]; then
echo "Logo file not found: $logo_path" >&2
exit 1
fi
if [[ -L $logo_path ]]; then
echo "Logo file is a symlink, which is not accepted: $logo_path" >&2
exit 1
fi
# Open the logo while still unprivileged. A replacement symlink to a root-only
# file therefore fails here instead of being followed after sudo starts.
if ! exec {logo_fd}<"$logo_path"; then
echo "Unable to open logo file as the current user: $logo_path" >&2
exit 1
fi
if [[ ! -f /proc/$$/fd/$logo_fd ]]; then
echo "Logo input is no longer a regular file: $logo_path" >&2
exit 1
fi
sudo mkinitcpio -P
fi
run_root_transaction() {
sudo /bin/bash -c '
set -eEuo pipefail
PATH=/usr/bin:/bin
export PATH
umask 077
# Sync the SDDM login screen with the same colors and logo.
sddm_dir="/usr/share/sddm/themes/omarchy"
sddm_template="$OMARCHY_PATH/default/sddm/omarchy/Main.qml"
# Every check below is a bare assertion that aborts under set -e. Name the
# subject of each one so a refusal reaches the user instead of exiting mute.
failure_context="the privileged Plymouth transaction"
failure_reported=
report_failure() {
[[ -z $failure_reported ]] || return 0
failure_reported=1
printf "omarchy-plymouth-set: refusing to publish: %s failed validation\n" "$failure_context" >&2
if [[ -n ${failure_hint:-} ]]; then
printf "omarchy-plymouth-set: %s\n" "$failure_hint" >&2
fi
}
trap report_failure ERR
sed \
-e "s/#1a1b26/#$bg_hex/g" \
-e "s/#ffffff/#$text_hex/g" \
"$sddm_template" | sudo tee "$sddm_dir/Main.qml" >/dev/null
mode=$1
source_root=$2
bg_hex=$3
text_hex=$4
max_asset_size=$5
failure_context="the arguments of the privileged transaction"
[[ $mode == "set" || $mode == "refresh-plymouth" || $mode == "refresh-sddm" ]]
[[ $source_root == /* ]]
[[ $max_asset_size =~ ^[0-9]+$ ]]
(( max_asset_size > 0 ))
failure_context="the Omarchy source tree $source_root"
canonical_source_root=$(realpath -e -- "$source_root")
[[ $canonical_source_root == "$source_root" ]]
validate_trusted_directory() {
local directory=$1 canonical uid directory_mode
failure_context="directory $directory"
canonical=$(realpath -e -- "$directory")
[[ $canonical == "$directory" && -d $directory && ! -L $directory ]]
while :; do
failure_context="directory $directory (must be root-owned and not group- or world-writable)"
uid=$(stat -c %u -- "$directory")
directory_mode=$(stat -c %a -- "$directory")
(( uid == 0 ))
(( (8#$directory_mode & 0022) == 0 ))
[[ $directory == "/" ]] && break
directory=${directory%/*}
[[ -n $directory ]] || directory=/
done
}
validate_trusted_configuration_file() {
local configuration=$1 canonical uid configuration_mode size
failure_context="root configuration $configuration"
[[ -f $configuration && ! -L $configuration ]]
canonical=$(realpath -e -- "$configuration")
[[ $canonical == "$configuration" ]]
validate_trusted_directory "${configuration%/*}"
uid=$(stat -c %u -- "$configuration")
configuration_mode=$(stat -c %a -- "$configuration")
size=$(stat -c %s -- "$configuration")
(( uid == 0 ))
(( (8#$configuration_mode & 0022) == 0 ))
(( size > 0 && size <= 4096 ))
}
# A packaged tree must be root-owned. A development checkout is the one
# deliberate exception: omarchy dev link records its canonical path in a
# root-owned /etc/omarchy.conf. That is already an explicit decision to run
# privileged Omarchy commands from user-editable code in the checkout, so
# reading its packaged assets does not widen the development trust boundary.
development_source=false
source_root_uid=$(stat -c %u -- "$source_root")
if (( source_root_uid != 0 )); then
omarchy_conf=/etc/omarchy.conf
failure_context="$source_root is user-owned and $omarchy_conf must contain its trusted dev-link authorization; run omarchy dev link to authorize it"
failure_hint="$source_root is user-owned; run omarchy dev link to authorize this development checkout, or omarchy dev unlink to use the packaged tree"
validate_trusted_configuration_file "$omarchy_conf"
# validate_trusted_configuration_file walks /etc up to / and leaves its own
# subject behind in failure_context. Without restoring ours, a checkout
# that simply is not the authorized one refuses with "directory / must be
# root-owned and not group- or world-writable" -- naming a directory that
# passed, and sending the reader after a filesystem problem that is not
# there.
failure_context="the dev-link authorization in $omarchy_conf, which must name $source_root"
quoted_source_root=$source_root
quoted_source_root=${quoted_source_root//\\/\\\\}
quoted_source_root=${quoted_source_root//\"/\\\"}
quoted_source_root=${quoted_source_root//\$/\\\$}
quoted_source_root=${quoted_source_root//\`/\\\`}
expected_config_line="export OMARCHY_PATH=\"$quoted_source_root\""
mapfile -t omarchy_config_lines <"$omarchy_conf"
(( ${#omarchy_config_lines[@]} == 1 ))
[[ ${omarchy_config_lines[0]} == "$expected_config_line" ]]
development_source=true
failure_hint=
fi
if [[ $mode == "set" ]]; then
[[ $bg_hex =~ ^[0-9a-fA-F]{6}$ ]]
[[ $text_hex =~ ^[0-9a-fA-F]{6}$ ]]
fi
theme_dir=/usr/share/plymouth/themes/omarchy
sddm_dir=/usr/share/sddm/themes/omarchy
plymouth_theme_assets=(
bullet.png
entry.png
lock.png
logo.png
omarchy.plymouth
omarchy.script
preview-unlock.png
progress_bar.png
progress_box.png
)
plymouth_default_assets=("${plymouth_theme_assets[@]}" logos/oma.png)
sddm_theme_assets=(Main.qml bullet.png entry-failed.png entry.png lock-failed.png lock.png logo.png)
sddm_default_assets=("${sddm_theme_assets[@]}" metadata.desktop theme.conf)
plymouth_assets=()
sddm_assets=()
case "$mode" in
set)
plymouth_assets=("${plymouth_theme_assets[@]}")
sddm_assets=("${sddm_theme_assets[@]}")
;;
refresh-plymouth)
plymouth_assets=("${plymouth_default_assets[@]}")
;;
refresh-sddm)
sddm_assets=("${sddm_default_assets[@]}")
;;
esac
validate_trusted_file() {
local source=$1 canonical uid file_mode size
failure_context="packaged source file $source"
[[ -f $source && ! -L $source ]]
canonical=$(realpath -e -- "$source")
[[ $canonical == "$source" ]]
file_mode=$(stat -c %a -- "$source")
size=$(stat -c %s -- "$source")
(( size > 0 && size <= max_asset_size ))
if ! $development_source; then
validate_trusted_directory "${source%/*}"
uid=$(stat -c %u -- "$source")
(( uid == 0 ))
(( (8#$file_mode & 0022) == 0 ))
fi
}
copy_trusted_file() {
local source=$1 destination=$2
validate_trusted_file "$source"
install -o 0 -g 0 -m 0600 -- "$source" "$destination"
}
staging_dir=$(mktemp -d /tmp/omarchy-plymouth.XXXXXXXX)
temporary=
cleanup() {
[[ -z $temporary ]] || rm -f -- "$temporary"
rm -rf -- "$staging_dir"
}
trap cleanup EXIT HUP INT TERM
chown 0:0 -- "$staging_dir"
chmod 0700 -- "$staging_dir"
plymouth_stage=$staging_dir/plymouth
sddm_stage=$staging_dir/sddm
mkdir -m 0700 -p -- "$plymouth_stage/logos" "$sddm_stage"
for asset in "${plymouth_assets[@]}"; do
copy_trusted_file "$source_root/default/plymouth/$asset" "$plymouth_stage/$asset"
done
if [[ $mode == "set" ]]; then
# stdin was opened by the unprivileged caller. Read no more than the
# documented limit into the root-owned stage before doing other work.
failure_context="the selected logo (expected 1 to $max_asset_size bytes)"
head -c "$((max_asset_size + 1))" >"$plymouth_stage/logo.png"
logo_size=$(stat -c %s -- "$plymouth_stage/logo.png")
(( logo_size > 0 && logo_size <= max_asset_size ))
chown 0:0 -- "$plymouth_stage/logo.png"
chmod 0600 -- "$plymouth_stage/logo.png"
cp --reflink=never -- "$plymouth_stage/logo.png" "$sddm_stage/logo.png"
bg_r=$(awk -v n=$((16#${bg_hex:0:2})) "BEGIN{printf \"%.3f\", n/255}")
bg_g=$(awk -v n=$((16#${bg_hex:2:2})) "BEGIN{printf \"%.3f\", n/255}")
bg_b=$(awk -v n=$((16#${bg_hex:4:2})) "BEGIN{printf \"%.3f\", n/255}")
sed -i \
-e "s/^Window.SetBackgroundTopColor.*/Window.SetBackgroundTopColor($bg_r, $bg_g, $bg_b);/" \
-e "s/^Window.SetBackgroundBottomColor.*/Window.SetBackgroundBottomColor($bg_r, $bg_g, $bg_b);/" \
"$plymouth_stage/omarchy.script"
for asset in bullet.png entry.png lock.png progress_bar.png; do
magick "$plymouth_stage/$asset" -channel RGB +level-colors "#$text_hex","#$text_hex" "$plymouth_stage/$asset"
done
copy_trusted_file "$source_root/default/sddm/omarchy/Main.qml" "$sddm_stage/Main.qml"
sed -i \
-e "s/#1a1b26/#__OMARCHY_SDDM_BG__/g" \
-e "s/#ffffff/#__OMARCHY_SDDM_TEXT__/g" \
-e "s/#__OMARCHY_SDDM_BG__/#$bg_hex/g" \
-e "s/#__OMARCHY_SDDM_TEXT__/#$text_hex/g" \
"$sddm_stage/Main.qml"
for asset in bullet.png entry.png lock.png; do
cp --reflink=never -- "$plymouth_stage/$asset" "$sddm_stage/$asset"
done
for asset in entry lock; do
magick "$plymouth_stage/$asset.png" -channel RGB +level-colors "#f7768e","#f7768e" "$sddm_stage/$asset-failed.png"
done
chown -R 0:0 -- "$staging_dir"
find "$staging_dir" -type f -exec chmod 0600 -- {} +
elif (( ${#sddm_assets[@]} )); then
for asset in "${sddm_assets[@]}"; do
copy_trusted_file "$source_root/default/sddm/omarchy/$asset" "$sddm_stage/$asset"
done
fi
if (( ${#plymouth_assets[@]} )); then
validate_trusted_directory "$theme_dir"
if [[ $mode == "refresh-plymouth" ]]; then
validate_trusted_directory "$theme_dir/logos"
fi
fi
if (( ${#sddm_assets[@]} )); then
validate_trusted_directory "$sddm_dir"
fi
publish_asset() {
local source=$1 destination=$2 parent filename source_size copied_size
failure_context="destination $destination"
[[ -f $source && ! -L $source ]]
(( $(stat -c %u -- "$source") == 0 ))
source_size=$(stat -c %s -- "$source")
(( source_size > 0 && source_size <= max_asset_size ))
[[ $destination == /* && $destination != */ && $destination != *"/../"* ]]
parent=${destination%/*}
filename=${destination##*/}
[[ -n $parent && -n $filename && $filename != "." && $filename != ".." ]]
validate_trusted_directory "$parent"
temporary=$(mktemp --tmpdir="$parent" ".$filename.omarchy-new.XXXXXXXX")
install -o 0 -g 0 -m 0644 -- "$source" "$temporary"
copied_size=$(stat -c %s -- "$temporary")
(( copied_size == source_size ))
cmp -s -- "$source" "$temporary"
sync -f -- "$temporary"
mv --no-copy -fT -- "$temporary" "$destination"
temporary=
}
if (( ${#plymouth_assets[@]} )); then
for asset in "${plymouth_assets[@]}"; do
publish_asset "$plymouth_stage/$asset" "$theme_dir/$asset"
done
fi
if (( ${#sddm_assets[@]} )); then
for asset in "${sddm_assets[@]}"; do
publish_asset "$sddm_stage/$asset" "$sddm_dir/$asset"
done
validate_trusted_directory "$sddm_dir"
rm -f -- "$sddm_dir/logo.svg"
fi
' bash "$mode" "$OMARCHY_PATH" "$bg_hex" "$text_hex" "$((64 * 1024 * 1024))"
}
if [[ $mode == "set" ]]; then
run_root_transaction <&"$logo_fd"
else
run_root_transaction </dev/null
fi
if [[ $mode != "refresh-sddm" ]]; then
sudo plymouth-set-default-theme omarchy
if omarchy-cmd-present limine-mkinitcpio; then
sudo limine-mkinitcpio
else
sudo mkinitcpio -P
fi
fi
sudo cp "$staging_dir/logo.png" "$sddm_dir/logo.png"
for asset in bullet.png entry.png lock.png; do
sudo cp "$staging_dir/$asset" "$sddm_dir/$asset"
done
for asset in entry lock; do
magick "$staging_dir/$asset.png" -channel RGB +level-colors "#f7768e","#f7768e" "$staging_dir/$asset-failed.png"
sudo cp "$staging_dir/$asset-failed.png" "$sddm_dir/$asset-failed.png"
done
sudo rm -f "$sddm_dir/logo.svg"
+6 -16
View File
@@ -677,15 +677,11 @@ user_groups() {
if [[ -f $PROVISIONING_DIR/groups ]]; then
while IFS= read -r group; do
[[ -n $group ]] || continue
# Never replay old privileged group defaults. Docker is always opt-in.
# Input is only retained when the factory image has one of the features
# whose installer deliberately grants access to raw input devices.
# Never grant docker at first boot, even if an older install recorded it
# (or a factory snapshot predating the opt-in default carries it): the
# docker group is root-equivalent. It is opt-in via
# omarchy-setup-security-sudoless-docker.
[[ $group == "docker" ]] && continue
if [[ $group == "input" ]] &&
! pacman -Qq xpadneo-dkms &>/dev/null &&
! pacman -Qq ydotool &>/dev/null; then
continue
fi
getent group "$group" >/dev/null || continue
[[ ",$groups," == *",$group,"* ]] || groups+=",$group"
done <"$PROVISIONING_DIR/groups"
@@ -746,12 +742,6 @@ create_user() {
# for specific commands), and a duplicate grant is harmless.
echo "%wheel ALL=(ALL:ALL) ALL" >/etc/sudoers.d/00-omarchy-wheel
chmod 440 /etc/sudoers.d/00-omarchy-wheel
source "$OMARCHY_PATH/install/helpers/browser-policy.sh"
for dir in "${BROWSER_POLICY_MANAGED_DIRS[@]}"; do
[[ -d $dir || -L $dir ]] || continue
browser_policy_setup_dir "$dir"
done
}
install_authorized_keys() {
@@ -792,7 +782,7 @@ configure_login() {
# After=) is what makes it deterministic — no sleep/race against SDDM's startup.
install_autologin_once_cleanup() {
local unit=omarchy-provision-autologin-once.service
sed "s|@UNIT@|$unit|g" >"/etc/systemd/system/$unit" <<'UNIT'
cat >"/etc/systemd/system/$unit" <<UNIT
[Unit]
Description=Drop the first-boot autologin before the next login
Before=display-manager.service
@@ -801,7 +791,7 @@ ConditionPathExists=/etc/sddm.conf.d/autologin.conf
[Service]
Type=oneshot
ExecStart=/usr/bin/rm -f /etc/sddm.conf.d/autologin.conf
ExecStartPost=/usr/bin/rm -f /etc/systemd/system/graphical.target.wants/@UNIT@ /etc/systemd/system/@UNIT@
ExecStartPost=/usr/bin/rm -f /etc/systemd/system/graphical.target.wants/$unit /etc/systemd/system/$unit
[Install]
WantedBy=graphical.target
+1 -2
View File
@@ -84,7 +84,7 @@ fi
# Dev-aware skill symlinks. Cannot live in /etc/skel because OMARCHY_PATH may
# point at a dev checkout (omarchy dev link) where the target differs.
# Loops every skill directory, so shipping a new one needs no edit here.
mkdir -p ~/.agents/skills ~/.claude/skills ~/.codex/skills ~/.pi/agent/skills ~/.gemini/config/skills
mkdir -p ~/.agents/skills ~/.claude/skills ~/.codex/skills ~/.pi/agent/skills
for skill in "$OMARCHY_PATH"/default/agents/skills/*/; do
skill=${skill%/}
name=${skill##*/}
@@ -92,7 +92,6 @@ for skill in "$OMARCHY_PATH"/default/agents/skills/*/; do
ln -sfn "$skill" ~/.claude/skills/"$name"
ln -sfn "$skill" ~/.codex/skills/"$name"
ln -sfn "$skill" ~/.pi/agent/skills/"$name"
ln -sfn "$skill" ~/.gemini/config/skills/"$name"
done
mkdir -p ~/Downloads ~/Pictures ~/Videos ~/.config/gtk-3.0
+8 -3
View File
@@ -3,6 +3,11 @@
# omarchy:summary=Overwrite the user config for the Plymouth drive decryption and boot sequence with the Omarchy default and rebuild it.
# omarchy:requires-sudo=true
# Reuse the fixed-file publisher so root never resolves the source checkout or
# follows a destination symlink while restoring the packaged assets.
exec "$OMARCHY_PATH/bin/omarchy-plymouth-set" --refresh-default
sudo cp -r "$OMARCHY_PATH/default/plymouth/." /usr/share/plymouth/themes/omarchy/
sudo plymouth-set-default-theme omarchy
if omarchy-cmd-present limine-mkinitcpio; then
sudo limine-mkinitcpio
else
sudo mkinitcpio -P
fi
+2 -3
View File
@@ -3,6 +3,5 @@
# omarchy:summary=Refresh the SDDM theme from default
# omarchy:requires-sudo=true
# Reuse the fixed-file publisher so root never resolves an untrusted source or
# follows a destination symlink while restoring the packaged SDDM theme.
exec "$OMARCHY_PATH/bin/omarchy-plymouth-set" --refresh-sddm-default
sudo rm -rf /usr/share/sddm/themes/omarchy
sudo cp -r "$OMARCHY_PATH/default/sddm/omarchy" /usr/share/sddm/themes/omarchy
-120
View File
@@ -1,120 +0,0 @@
#!/bin/bash
# omarchy:summary=Show or set the region the machine is set up for
# omarchy:args=[world|china]
# omarchy:examples=omarchy region | omarchy region china
set -euo pipefail
region_file="$HOME/.config/omarchy/region"
if (($# == 0)); then
region="World"
if [[ -f $region_file ]] && read -r stored <"$region_file"; then
region=$stored
fi
echo "$region"
exit 0
fi
if (($# > 1)); then
echo "Usage: omarchy-region <world|china>"
exit 1
fi
case "$1" in
world | World) region="World" ;;
china | China) region="China" ;;
*)
echo "Usage: omarchy-region <world|china>"
exit 1
;;
esac
menu_overlay="$HOME/.config/omarchy/extensions/omarchy-menu.jsonc"
menu_sample="$OMARCHY_PATH/config/omarchy/extensions/omarchy-menu.jsonc"
overlay_marker="// Omarchy region managed"
if [[ $region == "China" ]]; then
# The stock machine carries the commented sample extension; a menu extension
# someone wrote by hand refuses the switch instead of being overwritten.
if [[ -s $menu_overlay ]] && ! grep -qFx "$overlay_marker" "$menu_overlay" &&
! cmp -s "$menu_overlay" "$menu_sample"; then
echo "menu: $menu_overlay has content Omarchy does not manage" >&2
echo "Resolve it by hand, then rerun: omarchy region china" >&2
exit 1
fi
mkdir -p "${menu_overlay%/*}"
cp "$OMARCHY_PATH/default/omarchy/omarchy-menu.zh-cn.jsonc" "$menu_overlay"
echo "menu -> Chinese labels"
# localectl writes /etc/locale.conf; the session picks it up at next login.
if ! grep -qsFx 'LANG=zh_CN.UTF-8' /etc/locale.conf; then
sudo sed -i 's/^#\(zh_CN\.UTF-8 UTF-8\)/\1/' /etc/locale.gen
sudo locale-gen
sudo localectl set-locale LANG=zh_CN.UTF-8
fi
echo "language -> zh_CN.UTF-8 (from the next login)"
else
if [[ -f $menu_overlay ]] && grep -qFx "$overlay_marker" "$menu_overlay"; then
cp "$menu_sample" "$menu_overlay"
echo "menu -> stock labels"
fi
if grep -qsFx 'LANG=zh_CN.UTF-8' /etc/locale.conf; then
sudo localectl set-locale LANG=en_US.UTF-8
echo "language -> en_US.UTF-8 (from the next login)"
fi
fi
# Rime Ice's TUI drives its own sudo and fcitx5 restart; the schema file is
# what a finished install leaves behind. A fresh install has no repo databases
# until its first sync, and both pkg-add and the installer's pacman need them.
if [[ $region == "China" && ! -f $HOME/.local/share/fcitx5/rime/rime_ice.schema.yaml ]]; then
sudo pacman -Sy
omarchy-pkg-add rime-ice-installer
rime-ice-installer
fi
# Rime first makes Chinese the active default; keyboard-us stays one
# Ctrl+Space away, pinned explicitly rather than left to fcitx5's default.
if [[ $region == "China" ]]; then
fcitx_dir="$HOME/.config/fcitx5"
mkdir -p "$fcitx_dir"
cat >"$fcitx_dir/profile" <<EOF
[Groups/0]
Name=Default
Default Layout=us
DefaultIM=rime
[Groups/0/Items/0]
Name=rime
Layout=
[Groups/0/Items/1]
Name=keyboard-us
Layout=
[GroupOrder]
0=Default
EOF
touch "$fcitx_dir/config"
awk '
/^\[Hotkey\/TriggerKeys\]$/ { skip = 1; next }
/^\[/ { skip = 0 }
!skip { print }
' "$fcitx_dir/config" >"$fcitx_dir/config.tmp"
printf '[Hotkey/TriggerKeys]\n0=Control+space\n' >>"$fcitx_dir/config.tmp"
mv "$fcitx_dir/config.tmp" "$fcitx_dir/config"
echo "input method -> Rime Ice by default, Ctrl+Space toggles"
if systemctl --user is-active omarchy-fcitx5.service >/dev/null 2>&1; then
systemctl --user restart omarchy-fcitx5.service
fi
fi
mkdir -p "${region_file%/*}"
printf '%s\n' "$region" >"$region_file"
echo "Region set to $region"
-17
View File
@@ -1,17 +0,0 @@
#!/bin/bash
# omarchy:summary=Remove the ChatGPT desktop app along with its configuration and caches.
# omarchy:requires-sudo=true
set -e
omarchy-pkg-drop openai-codex-desktop
# Not ~/.cache/codex-runtimes: the Codex CLI resolves its own runtime under
# there, and it ships in a different package that survives this one.
rm -rf \
"$HOME/.config/Codex" \
"$HOME/.cache/Codex"
echo ""
echo "ChatGPT has been removed."
-16
View File
@@ -1,16 +0,0 @@
#!/bin/bash
# omarchy:summary=Remove Grok Bot along with its settings and data.
# omarchy:requires-sudo=true
set -e
omarchy-pkg-drop grok-bot
# Not ~/.grok: that is the Grok CLI, which is installed and used on its own.
rm -rf \
"$HOME/.config/Grok Bot" \
"$HOME/.grokbot"
echo ""
echo "Grok Bot has been removed."
-29
View File
@@ -1,29 +0,0 @@
#!/bin/bash
# omarchy:summary=Remove LM Studio along with its configuration and every model it downloaded.
# omarchy:requires-sudo=true
set -e
omarchy-pkg-drop lmstudio-bin
# LM Studio's home is relocatable and the pointer is the only record of where
# the models went, so it has to be read before it is deleted with everything else.
lmstudio_home=""
if [[ -f $HOME/.lmstudio-home-pointer ]]; then
read -r lmstudio_home <"$HOME/.lmstudio-home-pointer" || true
fi
rm -rf \
"$HOME/.config/LM Studio" \
"$HOME/.config/LM-Studio" \
"$HOME/.lmstudio" \
"$HOME/.lmstudio-home-pointer"
if [[ -n $lmstudio_home && $lmstudio_home == /* && -d $lmstudio_home ]] &&
[[ $lmstudio_home != "/" && $lmstudio_home != "$HOME" ]]; then
rm -rf "$lmstudio_home"
fi
echo ""
echo "LM Studio and its models have been removed."
-18
View File
@@ -1,18 +0,0 @@
#!/bin/bash
# omarchy:summary=Remove Ollama along with every model it pulled.
# omarchy:requires-sudo=true
set -e
# The service is what holds the models open, so it goes before the package.
sudo systemctl disable --now ollama.service 2>/dev/null || true
# Whichever acceleration variant is installed provides the same command.
omarchy-pkg-drop ollama ollama-cuda ollama-rocm ollama-vulkan
sudo rm -rf /var/lib/ollama
rm -rf "$HOME/.ollama"
echo ""
echo "Ollama and its models have been removed."
-18
View File
@@ -1,18 +0,0 @@
#!/bin/bash
# omarchy:summary=Remove T3 Code along with its configuration and workspaces.
# omarchy:requires-sudo=true
set -e
omarchy-pkg-drop t3code-bin
# T3 Code drives agents that keep their own state -- ~/.claude.json, ~/.grok,
# ~/.local/share/opencode -- and those outlive it, so only its own goes.
rm -rf \
"$HOME/.config/t3code" \
"$HOME/.t3"
rm -f "$HOME/.config/t3code-flags.conf"
echo ""
echo "T3 Code has been removed."
+2 -2
View File
@@ -10,7 +10,7 @@ if [[ -z $1 ]]; then
fi
remove_php() {
omarchy-pkg-drop php composer php-sqlite xdebug
sudo pacman -Rns --noconfirm php composer php-sqlite xdebug 2>/dev/null || true
}
case "$1" in
@@ -50,7 +50,7 @@ laravel)
;;
symfony)
echo -e "Removing Symfony CLI...\n"
omarchy-pkg-drop symfony-cli
sudo pacman -Rns --noconfirm symfony-cli 2>/dev/null || true
;;
python)
echo -e "Removing Python...\n"
+2 -2
View File
@@ -13,9 +13,9 @@ if gum confirm "Are you sure you want to remove all preinstalled web apps, TUI w
hyprctl reload
# Remove mise stubs
rm -f ~/.local/bin/codex ~/.local/bin/claude ~/.local/bin/agy ~/.local/bin/copilot \
rm -f ~/.local/bin/codex ~/.local/bin/claude ~/.local/bin/gemini ~/.local/bin/copilot \
~/.local/bin/gh ~/.local/bin/opencode ~/.local/bin/playwright ~/.local/bin/playwright-cli ~/.local/bin/pi \
~/.local/bin/omp ~/.local/bin/ori ~/.local/bin/grok ~/.local/bin/crush ~/.local/bin/ghui ~/.local/bin/hunk
~/.local/bin/omp ~/.local/bin/grok ~/.local/bin/crush ~/.local/bin/ghui ~/.local/bin/hunk
omarchy-pkg-drop \
aether \
-3
View File
@@ -41,9 +41,6 @@ setup_pam_config() {
fi
else
echo "Creating polkit configuration with fingerprint authentication..."
# omarchy:heredoc-expands paths=none -- $fprintd_gate is the literal PAM
# line defined above, shared with the two sed insertions so the gate cannot
# drift between files. The only path in it is the fixed /usr/bin one.
sudo tee /etc/pam.d/polkit-1 >/dev/null <<EOF
$fprintd_gate
auth sufficient pam_fprintd.so
+15 -100
View File
@@ -1,62 +1,32 @@
#!/bin/bash
# omarchy:summary=Set up the OpenSSH server, open the firewall, and authorize an SSH key
# omarchy:args=[--key=<public-key>] [--gh-keys <github-username>]
# omarchy:examples=omarchy-setup-security-sshd | omarchy-setup-security-sshd --gh-keys dhh | omarchy-setup-security-sshd --key="ssh-ed25519 AAAA... user@host"
# omarchy:args=[--key=<public-key>]
# omarchy:examples=omarchy-setup-security-sshd | omarchy-setup-security-sshd --key="ssh-ed25519 AAAA... user@host"
# omarchy:requires-sudo=true
set -e
AUTHORIZED_KEYS="$HOME/.ssh/authorized_keys"
KEY=""
GITHUB_USER=""
# Checked while parsing, before anything is installed or opened: an empty or
# option-shaped username otherwise falls through to the interactive menu having
# already changed the machine, and `--gh-keys --help` would take --help as the
# username and set the server up on its way to failing.
require_github_user() {
if [[ -z $1 || $1 == -* ]]; then
echo "omarchy-setup-security-sshd: --gh-keys needs a GitHub username." >&2
exit 2
fi
}
while (( $# > 0 )); do
case "$1" in
--key=*) KEY="${1#--key=}" ;;
--gh-keys=*)
GITHUB_USER="${1#--gh-keys=}"
require_github_user "$GITHUB_USER"
;;
--gh-keys)
shift
GITHUB_USER="${1:-}"
require_github_user "$GITHUB_USER"
;;
for arg in "$@"; do
case "$arg" in
--key=*) KEY="${arg#--key=}" ;;
-h | --help)
echo "Usage: omarchy-setup-security-sshd [--key=<public-key>] [--gh-keys <github-username>]"
echo "Usage: omarchy-setup-security-sshd [--key=<public-key>]"
echo
echo "Sets up the OpenSSH server, opens the SSH port in the UFW firewall,"
echo "and authorizes an SSH key (from GitHub, pasted, or passed via --key)."
echo
echo "Passing --key or --gh-keys skips the prompts, so the command can run"
echo "unattended from a script or a fresh machine's first login."
exit 0
;;
*)
echo "omarchy-setup-security-sshd: unknown option '$1'. Try --help." >&2
echo "omarchy-setup-security-sshd: unknown option '$arg'. Try --help." >&2
exit 2
;;
esac
shift
done
if [[ -n $KEY && -n $GITHUB_USER ]]; then
echo "omarchy-setup-security-sshd: pass either --key or --gh-keys, not both." >&2
exit 2
fi
setup_sshd() {
echo "Installing and starting the OpenSSH server..."
omarchy-pkg-add openssh
@@ -100,7 +70,13 @@ authorize_key() {
}
authorize_keys_from_github() {
local username="$1" keys added=0
local username keys added=0
username=$(gum input --prompt "GitHub username> " --placeholder "dhh") || exit 1
if [[ -z $username ]]; then
echo -e "\e[31mNo GitHub username given.\e[0m" >&2
exit 1
fi
echo "Fetching keys from https://github.com/$username.keys..."
if ! keys=$(curl -fsSL "https://github.com/$username.keys") || [[ -z $keys ]]; then
@@ -119,18 +95,6 @@ authorize_keys_from_github() {
fi
}
prompt_for_github_user() {
local username
username=$(gum input --prompt "GitHub username> " --placeholder "dhh") || exit 1
if [[ -z $username ]]; then
echo -e "\e[31mNo GitHub username given.\e[0m" >&2
exit 1
fi
authorize_keys_from_github "$username"
}
authorize_pasted_key() {
local key
@@ -143,50 +107,6 @@ authorize_pasted_key() {
authorize_key "$key" || exit 1
}
# Only called after a key is authorized. Disabling password authentication
# before then could lock the owner out of the machine.
disable_password_auth() {
local config=/etc/ssh/sshd_config.d/10-omarchy-hardening.conf
local effective_config
if [[ ! -s $AUTHORIZED_KEYS ]]; then
echo -e "\e[31mCannot disable SSH password authentication without an authorized key.\e[0m" >&2
return 1
fi
echo "Disabling SSH password authentication, now that a key is authorized..."
sudo install -Dm644 /dev/stdin "$config" <<'CONF'
# Written by omarchy-setup-security-sshd once an SSH key was authorized.
# Delete this file and reload sshd to allow password logins again.
PasswordAuthentication no
KbdInteractiveAuthentication no
CONF
# Validate before reloading: a config sshd rejects would otherwise take the
# service down on its next restart, potentially stranding a remote owner.
if ! sudo sshd -t; then
echo -e "\e[31msshd rejected the hardening config; removing it and leaving passwords on.\e[0m" >&2
sudo rm -f "$config"
return 1
fi
# Syntax alone is insufficient because sshd uses the first value it reads for
# these settings. An earlier administrator rule could leave passwords enabled.
# Match keywords case-insensitively: OpenSSH 9.x dumps them lowercase, 10.x
# in CamelCase.
if ! effective_config=$(sudo sshd -T) ||
! grep -qixF "passwordauthentication no" <<<"$effective_config" ||
! grep -qixF "kbdinteractiveauthentication no" <<<"$effective_config"; then
echo -e "\e[31msshd did not apply the password-authentication restrictions; removing the ineffective config.\e[0m" >&2
sudo rm -f "$config"
return 1
fi
# Reload rather than restart so an administrator already connected keeps
# their session.
sudo systemctl reload sshd.service
}
echo -e "\e[32mSetting up SSH server access with key-based authentication.\n\e[0m"
setup_sshd
@@ -195,18 +115,13 @@ open_firewall
echo
if [[ -n $KEY ]]; then
authorize_key "$KEY" || exit 1
elif [[ -n $GITHUB_USER ]]; then
authorize_keys_from_github "$GITHUB_USER"
else
case $(gum choose "Grab key from GitHub" "Paste key manually" --header "How would you like to add your SSH key?") in
"Grab key from GitHub") prompt_for_github_user ;;
"Grab key from GitHub") authorize_keys_from_github ;;
"Paste key manually") authorize_pasted_key ;;
*) exit 1 ;;
esac
fi
disable_password_auth
echo -e "\e[32m\nPerfect! The SSH server is running and your key is authorized.\e[0m"
echo "Password logins are off; this machine now accepts authorized keys only."
echo "You can now connect with: ssh $USER@$(hostname)"
+6 -16
View File
@@ -23,26 +23,16 @@ omarchy-git-url-check "$REPO_URL" || exit 1
THEMES_DIR="$HOME/.config/omarchy/themes"
# Strip user@host: prefix from scp-style SSH URLs so basename sees just the path.
# git reads a URL as scp-style when a colon appears before any slash, so the path
# after it need not hold one: `git@host:omarchy-blue-theme.git` is a repo in that
# user's home, and leaving its prefix on names the theme after the whole URL.
# Strip user@host: prefix from scp-style SSH URLs so basename sees just the path
REPO_PATH="$REPO_URL"
[[ $REPO_PATH != *"://"* && $REPO_PATH == *:* && ${REPO_PATH%%:*} != */* ]] && REPO_PATH="${REPO_PATH#*:}"
[[ $REPO_PATH != *"://"* && $REPO_PATH == *:*/* ]] && REPO_PATH="${REPO_PATH#*:}"
THEME_NAME=$(basename -- "$REPO_PATH" .git | sed -E 's/^omarchy-//; s/-theme$//' | tr '[:upper:]' '[:lower:]')
THEME_PATH="$THEMES_DIR/$THEME_NAME"
# The name comes from the URL, is joined into a path that is about to be
# removed, and then names a directory the rest of Omarchy passes around by
# name: Style > Unlock builds a command line out of the one the picker
# returned. So it is held to the characters a theme name needs rather than
# screened for the harm of the day -- a repo called `..` would take
# ~/.config/omarchy with it, and one called `a';'id` would carry its own
# command into that picker. The leading character is kept out of `.` and `-`,
# which also covers `host:-s/foo.git` leaving basename with `.git`.
# A bracket range follows the locale's collation, not ASCII: `[a-z]` takes in
# `é` under en_US.UTF-8. Pin the locale so the set is the one written here.
if ! (LC_ALL=C; [[ $THEME_NAME =~ ^[a-z0-9_][a-z0-9._+-]*$ ]]); then
# The name comes from the URL and is joined into a path that is about to be
# removed, so a repo called `..` would take ~/.config/omarchy with it. A leading
# dot is refused with it: `host:-s/foo.git` leaves basename with `.git`.
if [[ -z $THEME_NAME || $THEME_NAME == .* || $THEME_NAME == */* ]]; then
echo "Error: '$REPO_URL' does not give a usable theme name."
exit 1
fi
+19 -9
View File
@@ -3,15 +3,23 @@
# omarchy:summary=Apply the current theme color to Chromium, Chrome, Edge, and Brave
# omarchy:hidden=true
source "$OMARCHY_PATH/install/helpers/browser-policy.sh"
CHROMIUM_THEME=$HOME/.local/state/omarchy/current/theme/chromium.theme
THEME_HEX_COLOR=$BROWSER_POLICY_DEFAULT_COLOR
if [[ -f $CHROMIUM_THEME ]]; then
THEME_HEX_COLOR=$(browser_policy_theme_hex "$(<$CHROMIUM_THEME)")
THEME_RGB_COLOR=$(<$CHROMIUM_THEME)
THEME_HEX_COLOR=$(printf '#%02x%02x%02x' ${THEME_RGB_COLOR//,/ })
else
# Use a default, neutral grey if theme doesn't have a color
THEME_HEX_COLOR="#1c2027"
fi
set_browser_policy() {
local policy_dir="$1"
[[ -d $policy_dir ]] || return
echo "{\"BrowserThemeColor\": \"$THEME_HEX_COLOR\", \"BrowserColorScheme\": \"device\"}" | tee "$policy_dir/color.json" >/dev/null
}
refresh_running_browser() {
local process="$1"
local command="$2"
@@ -22,15 +30,17 @@ refresh_running_browser() {
fi
}
failed=0
omarchy-theme-set-browser-policy "${THEME_HEX_COLOR#\#}" || failed=1
set_browser_policy /etc/chromium/policies/managed
refresh_running_browser chromium chromium
set_browser_policy /etc/opt/chrome/policies/managed
refresh_running_browser chrome google-chrome-stable || refresh_running_browser chrome google-chrome
set_browser_policy /etc/opt/edge/policies/managed
refresh_running_browser msedge microsoft-edge-stable
set_browser_policy /etc/brave/policies/managed
refresh_running_browser brave brave
# Match on the binary path: the running process is named plain "brave", and a
# bare -f brave-origin pattern would also match the installer's own terminal.
refresh_running_browser /opt/brave-origin-bin/ brave-origin -f
exit "$failed"
-123
View File
@@ -1,123 +0,0 @@
#!/bin/bash
# omarchy:summary=Write the current theme color into the browser policy directories
# omarchy:args=<rrggbb>
# omarchy:hidden=true
set -euo pipefail
# Whenever this runs as root — invoked directly through the passwordless
# sudoers rule, or re-execed by require_root below — sudo's secure_path decides
# where a bare helper resolves, and a dev link (etc/sudoers.d/omarchy-dev-path)
# prepends a user-writable checkout bin/ to it. Every helper this script calls
# by bare name (printf's builtin aside: install, mktemp, rm) is a system tool,
# never an omarchy-* command, so pin PATH to trusted system directories and keep
# root from resolving one out of that checkout. The unprivileged wrapper phase
# keeps the caller's PATH so it can still find sudo/pkexec.
if (( EUID == 0 )); then
export PATH=/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin:/bin:/sbin
fi
# Enterprise policy trust roots. The list is fixed here rather than taken from
# the caller: the caller chooses a color, never a path.
POLICY_DIRS=(
/etc/chromium/policies/managed
/etc/opt/chrome/policies/managed
/etc/opt/edge/policies/managed
/etc/brave/policies/managed
)
# The path etc/sudoers.d/omarchy-theme-browser names. The privileged half always
# runs from there rather than from whichever copy was invoked, so the rule
# matches even where $OMARCHY_PATH points at a checkout.
PACKAGED_PATH=/usr/bin/omarchy-theme-set-browser-policy
usage() {
echo "Usage: omarchy-theme-set-browser-policy <rrggbb>" >&2
}
if (( $# != 1 )); then
usage
exit 1
fi
color="$1"
# Six lowercase hex digits is the whole of what this accepts. The leading "#"
# is added when the JSON is written rather than passed in: "#" opens a comment
# in sudoers, and keeping it out of argv lets the sudoers rule spell the
# argument as a plain six-character glob.
if [[ ! $color =~ ^[0-9a-f]{6}$ ]]; then
echo "omarchy-theme-set-browser-policy: expected six lowercase hex digits, got '$color'" >&2
exit 1
fi
# True when sudo would run this exact command without stopping for a password.
# `sudo -l` on its own reports whether a command is permitted, which the blanket
# %wheel rule answers yes to for everything; the long listing prints the matched
# entry's tags, so !authenticate is the grant in
# etc/sudoers.d/omarchy-theme-browser and nothing else. Listing runs nothing
# and, under -n, prompts for nothing.
sudo_grants_passwordless() {
sudo -n -l -l "$PACKAGED_PATH" "$@" 2>/dev/null | grep -q '!authenticate'
}
require_root() {
if (( EUID == 0 )); then
return
elif [[ -t 0 ]] || sudo_grants_passwordless "$@"; then
exec sudo "$PACKAGED_PATH" "$@"
else
exec pkexec "$PACKAGED_PATH" "$@"
fi
}
require_root "$color"
failed=0
staged=""
# Bash 5.3 makes the EXIT trap's last command decide the script's exit status,
# so this handler must not end on a false test. Every successful run clears
# staged, and a trailing `[[ -n $staged ]] && ...` would report that as failure.
cleanup() {
if [[ -n $staged ]]; then
rm -f "$staged"
fi
}
trap cleanup EXIT
for policy_dir in "${POLICY_DIRS[@]}"; do
# Only browsers Omarchy has installed have a policy directory. Creating one
# here would hand a browser a managed-policy root it does not otherwise have.
[[ -d $policy_dir && ! -L $policy_dir ]] || continue
dest=$policy_dir/color.json
staged=$(mktemp) || {
failed=1
continue
}
printf '{"BrowserThemeColor": "#%s", "BrowserColorScheme": "device"}\n' "$color" >"$staged"
if [[ -L $dest || -d $dest ]]; then
if ! rm -rf -- "$dest"; then
rm -f "$staged"
staged=""
echo "omarchy-theme-set-browser-policy: cannot replace $dest" >&2
failed=1
continue
fi
fi
if ! install -m 0644 -o root -g root -T "$staged" "$dest"; then
rm -f "$staged"
staged=""
echo "omarchy-theme-set-browser-policy: cannot write $dest" >&2
failed=1
continue
fi
rm -f "$staged"
staged=""
done
exit "$failed"
-6
View File
@@ -5,9 +5,3 @@
# omarchy:examples=omarchy toggle bar | omarchy toggle bar off | omarchy toggle bar on
omarchy-toggle bar-off "${1:-toggle}"
# The shell's watch on the toggles directory can miss flag changes that land in
# quick succession, stranding the bar off screen until the shell restarts.
# Nudge the bar to re-read the flag; quiet best-effort so the toggle still
# works when the shell is not up.
omarchy-shell -q omarchy.bar syncHidden
+5
View File
@@ -101,6 +101,11 @@ if [[ ! -f $image_path ]]; then
exit 1
fi
if omarchy-cmd-missing magick; then
echo "ImageMagick is required to convert logo images" >&2
exit 1
fi
if [[ $mode == "braille" ]]; then
pixel_width=$((width * 2))
pixel_height=$((height * 4))
+6 -40
View File
@@ -597,9 +597,6 @@ preserve_kernel_cmdline_root() {
fi
log "Preserving the kernel cmdline root parameters in $default_conf"
# omarchy:heredoc-expands paths=none -- ${boot_params[*]} is the kernel
# cmdline fragment assembled above from findmnt output (root=, rootflags=),
# not a filesystem path this script controls.
as_root tee -a "$default_conf" >/dev/null <<EOF
# Written by omarchy-upgrade-to-quattro. The += drop-ins in
# /etc/limine-entry-tool.d/ stop limine-entry-tool from reading
@@ -1190,19 +1187,11 @@ ensure_sleep_lock_service() {
}
run_post_upgrade_migrations() {
local pending_status
PATH="$package_path" command -v omarchy-migrate >/dev/null 2>&1 || return 0
log "Running Omarchy migrations"
if ! run_as_user_omarchy OMARCHY_UPGRADE_TO_QUATTRO_LIVE=1 omarchy-migrate; then
fail "Omarchy migrations did not complete. Fix the error above and rerun the upgrade before rebooting."
fi
if run_as_user_omarchy omarchy-migrate --pending >/dev/null; then
fail "Omarchy migrations are still pending. Rerun the upgrade before rebooting."
else
pending_status=$?
if (( pending_status != 1 )); then
fail "Could not verify that Omarchy migrations completed. Rerun the upgrade before rebooting."
fi
warn "Could not run Omarchy migrations; the user may be prompted to run them after login."
fi
}
@@ -1323,23 +1312,9 @@ apply_system_transition() {
/usr/share/icons/Yaru/scalable/actions/go-next-symbolic.svg
as_root gtk-update-icon-cache /usr/share/icons/Yaru >/dev/null 2>&1 || true
local browser_policy_helper=/usr/share/omarchy/install/helpers/browser-policy.sh
if ! as_root test -f "$browser_policy_helper"; then
warn "$browser_policy_helper is unavailable; Chromium policy directories were not hardened."
else
as_root env OMARCHY_PATH=/usr/share/omarchy \
bash -euo pipefail -c '
source "$OMARCHY_PATH/install/helpers/browser-policy.sh"
browser_policy_setup_dir /etc/chromium/policies/managed
for dir in "${BROWSER_POLICY_MANAGED_DIRS[@]}"; do
[[ $dir == "/etc/chromium/policies/managed" ]] && continue
[[ -d $dir || -L $dir ]] || continue
browser_policy_setup_dir "$dir"
done
'
fi
as_root install -d -m 0777 /etc/chromium/policies/managed
as_root install -d -m 0755 /usr/lib/chromium
printf '%s\n' '{"distribution":{"require_eula":false},"browser":{"theme":{"color_scheme":0,"color_scheme2":0}}}' | \
printf '%s\n' '{"browser":{"theme":{"color_scheme":0,"color_scheme2":0}}}' | \
as_root tee /usr/lib/chromium/initial_preferences >/dev/null
# Deliberately do NOT add the user to the docker group. That group is
@@ -1375,6 +1350,7 @@ EOF
as_root systemctl disable docker.service >/dev/null 2>&1 || true
enable_system_service cups.service
enable_system_service cups-browsed.service
enable_system_service avahi-daemon.service
enable_system_service linux-modules-cleanup.service
enable_system_service docker.socket
@@ -1406,9 +1382,6 @@ EOF
autologin_user=$(as_root awk -F= '/^User=/ { print $2; exit }' /etc/sddm.conf.d/autologin.conf 2>/dev/null || true)
fi
[[ -n ${autologin_user:-} ]] || autologin_user="$target_user"
# omarchy:heredoc-expands paths=none -- $autologin_user is a username, read
# back from the root-owned drop-in or falling back to $target_user. Same
# mechanism as the old getty override: a name expands, no path does.
cat <<EOF | as_root tee /etc/sddm.conf.d/autologin.conf >/dev/null
[Autologin]
User=$autologin_user
@@ -1419,8 +1392,6 @@ EOF
fi
as_root install -d -m 0755 -o sddm -g sddm /var/lib/sddm 2>/dev/null || as_root install -d -m 0755 /var/lib/sddm
# omarchy:heredoc-expands paths=none -- $target_user is a username, not a
# path; SDDM's state file records who logged in last.
cat <<EOF | as_root tee /var/lib/sddm/state.conf >/dev/null
[Last]
Session=omarchy.desktop
@@ -2335,11 +2306,6 @@ refresh_current_theme_after_upgrade() {
# hooks because one of them runs `hyprctl reload`. Still poke terminal
# emulators so the active upgrade terminal picks up generated theme files.
run_as_user_omarchy omarchy-restart-terminal >/dev/null 2>&1 || true
# apply_system_transition purged user-owned color.json. Headless theme-set
# skipped omarchy-theme-set-browser, so rewrite the colour here.
run_as_user_omarchy omarchy-theme-set-browser >/dev/null 2>&1 ||
warn "Could not apply browser theme colour. Run 'omarchy theme set \"$theme_name\"' after reboot if Chromium's theme looks stale."
}
# Everything below mutates the system, so a non-zero exit from here on leaves a
@@ -2374,8 +2340,8 @@ run_as_user_omarchy omarchy-bar defaults ||
cleanup_retired_services
ensure_sleep_lock_service
remove_retired_default_packages
run_final_system_package_upgrade
run_post_upgrade_migrations
run_final_system_package_upgrade
run_post_upgrade_update_steps
refresh_current_theme_after_upgrade
# Do not force-reload Hyprland in the live upgraded session. The legacy
-2
View File
@@ -14,8 +14,6 @@ fi
if grep -q "https://pkgs.omarchy.org/stable/" /etc/pacman.conf; then
pkgs="stable"
elif grep -q "https://pkgs.omarchy.org/rc/" /etc/pacman.conf; then
pkgs="rc"
elif grep -q "https://pkgs.omarchy.org/edge/" /etc/pacman.conf; then
pkgs="edge"
else
+16 -98
View File
@@ -13,18 +13,6 @@ safe_icon_name() {
| sed 's/[^[:alnum:]]\+/-/g; s/^-//; s/-$//'
}
require_plain_name() {
# The name becomes a filename. A slash would turn it into directory levels, so
# the launcher lands somewhere omarchy-webapp-remove cannot address and the app
# is stuck in the launcher; a leading ../ leaves the applications directory
# altogether. Refuse rather than silently renaming what the user typed -- most
# often it is a URL entered in the name field.
if [[ $1 == */* ]]; then
echo "App name cannot contain '/': $1"
exit 1
fi
}
icon_name_from_ref() {
local ref="$1"
local name
@@ -54,34 +42,6 @@ download_icon() {
[[ -s $2 && $(file -b --mime-type "$2") == image/* ]]
}
# Chromium --app= treats javascript:, file:, and data: as a document to
# run. Prefix schemeless input with https as before, then refuse anything
# that is not http(s).
normalize_webapp_url() {
local url=$1
if [[ ! $url =~ ^[a-zA-Z][a-zA-Z0-9+.-]*: ]]; then
url="https://$url"
fi
printf '%s' "$url"
}
# Raw whitespace must be percent-encoded in a URL. Refuse it before serializing
# the desktop entry; before Exec argument quoting, it also split browser flags
# and additional URLs into separate arguments. Schemes are case-insensitive.
require_http_url() {
local url=$1
if [[ $url =~ [[:space:]] ]]; then
echo "Error: web app URL must not contain whitespace." >&2
exit 1
fi
if [[ ! ${url,,} =~ ^https?:// ]]; then
echo "Error: web app URL must be http or https." >&2
exit 1
fi
}
fetch_site_icon() {
local site_url="$1" dest="$2"
local origin page icon_url
@@ -105,44 +65,13 @@ fetch_site_icon() {
download_icon "https://www.google.com/s2/favicons?domain=${site_url}&sz=256" "$dest"
}
desktop_string_escape() {
# Desktop Entry "string" value (freedesktop Desktop Entry Spec, "Value types"):
# a raw newline would start a new key line and let a value inject a second
# Exec=. Escape backslash first, then tab/CR/LF and a leading space. Every value
# written into the .desktop file passes through here.
#
# Parameter expansion rather than sed: GNU sed's N auto-prints the pattern space
# and exits at end of input, so a `:a;N;$!ba` slurp skips every following s///
# for a value with no newline in it - which is every value except the injection
# attempt this exists to stop.
local value="$1"
value=${value//\\/\\\\}
value=${value//$'\t'/\\t}
value=${value//$'\r'/\\r}
value=${value//$'\n'/\\n}
[[ $value == " "* ]] && value="\\s${value# }"
printf '%s' "$value"
}
desktop_exec_arg() {
# One Exec argument, double-quoted per the freedesktop Exec spec: inside quotes
# " ` $ \ take a backslash and a literal % becomes %%. Only the default Exec's
# URL needs this; $CUSTOM_EXEC stays a whole command line (file-syntax only).
local escaped
escaped=$(printf '%s' "$1" \
| sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/`/\\`/g' -e 's/\$/\\$/g' -e 's/%/%%/g')
printf '"%s"' "$escaped"
}
if (( $# < 3 )); then
echo -e "\e[32mLet's create a new web app you can start with the app launcher.\n\e[0m"
APP_NAME=$(gum input --prompt "Name> " --placeholder "My favorite web app")
require_plain_name "$APP_NAME"
APP_URL=$(gum input --prompt "URL> " --placeholder "https://example.com")
APP_URL=$(normalize_webapp_url "$APP_URL")
require_http_url "$APP_URL"
if [[ ! $APP_URL =~ ^[a-zA-Z][a-zA-Z0-9+.-]*: ]]; then
APP_URL="https://$APP_URL"
fi
# Try to fetch the site's icon automatically first.
mkdir -p "$ICON_DIR"
@@ -159,8 +88,10 @@ if (( $# < 3 )); then
INTERACTIVE_MODE=true
else
APP_NAME="$1"
APP_URL=$(normalize_webapp_url "$2")
require_http_url "$APP_URL"
APP_URL="$2"
if [[ ! $APP_URL =~ ^[a-zA-Z][a-zA-Z0-9+.-]*: ]]; then
APP_URL="https://$APP_URL"
fi
ICON_REF="$3"
CUSTOM_EXEC="$4" # Optional custom exec command
MIME_TYPES="$5" # Optional mime types
@@ -173,8 +104,6 @@ if [[ -z $APP_NAME || -z $APP_URL ]]; then
exit 1
fi
require_plain_name "$APP_NAME"
if [[ -z $ICON_REF ]]; then
ICON_VALUE=$(safe_icon_name "$APP_NAME")
mkdir -p "$ICON_DIR"
@@ -199,39 +128,28 @@ else
ICON_VALUE=$(icon_name_from_ref "$ICON_REF")
fi
# Default Exec quotes the URL as one Exec-spec argument; the whole line then gets
# the file-syntax escaping below (unescaped first at read time per spec, so the
# layers compose). $CUSTOM_EXEC is a full command line, so it gets file-syntax only.
if [[ -n $CUSTOM_EXEC ]]; then
EXEC_COMMAND=$CUSTOM_EXEC
else
EXEC_COMMAND="omarchy-launch-webapp $(desktop_exec_arg "$APP_URL")"
fi
# Use custom exec if provided, otherwise default behavior
EXEC_COMMAND="${CUSTOM_EXEC:-omarchy-launch-webapp $APP_URL}"
# Create application .desktop file
DESKTOP_DIR="$HOME/.local/share/applications"
DESKTOP_FILE="$DESKTOP_DIR/$APP_NAME.desktop"
mkdir -p "$DESKTOP_DIR"
name_field=$(desktop_string_escape "$APP_NAME")
exec_field=$(desktop_string_escape "$EXEC_COMMAND")
icon_field=$(desktop_string_escape "$ICON_VALUE")
DESKTOP_FILE="$HOME/.local/share/applications/$APP_NAME.desktop"
mkdir -p "$(dirname "$DESKTOP_FILE")"
cat >"$DESKTOP_FILE" <<EOF
[Desktop Entry]
Version=1.0
Name=$name_field
Comment=$name_field
Exec=$exec_field
Name=$APP_NAME
Comment=$APP_NAME
Exec=$EXEC_COMMAND
Terminal=false
Type=Application
Icon=$icon_field
Icon=$ICON_VALUE
StartupNotify=true
EOF
# Add mime types if provided
if [[ -n $MIME_TYPES ]]; then
printf 'MimeType=%s\n' "$(desktop_string_escape "$MIME_TYPES")" >>"$DESKTOP_FILE"
echo "MimeType=$MIME_TYPES" >>"$DESKTOP_FILE"
fi
chmod +x "$DESKTOP_FILE"
+8 -26
View File
@@ -9,31 +9,14 @@ ICON_DIR="$HOME/.local/share/icons/hicolor/256x256/apps"
OLD_ICON_DIR="$HOME/.local/share/applications/icons"
DESKTOP_DIR="$HOME/.local/share/applications/"
# Always index the launchers, so removal deletes the file that was found rather
# than a path rebuilt from the displayed name. Installs predating the name
# validation could nest the launcher inside directories, and those are exactly
# the ones a reconstructed path cannot reach.
WEB_APP_PATHS=()
while IFS= read -r -d '' file; do
if grep -q '^Exec=.*\(omarchy-launch-webapp\|omarchy-webapp-handler\).*' "$file"; then
WEB_APPS+=("$(basename "${file%.desktop}")")
WEB_APP_PATHS+=("$file")
fi
done < <(find "$DESKTOP_DIR" -name '*.desktop' -print0 2>/dev/null)
# The launcher matching a chosen name, or empty when nothing was indexed under
# it (an app removed between the scan and the pick, say).
path_for_web_app() {
local wanted="$1" i
for i in "${!WEB_APPS[@]}"; do
if [[ ${WEB_APPS[$i]} == "$wanted" ]]; then
printf '%s\n' "${WEB_APP_PATHS[$i]}"
return 0
fi
done
}
if (( $# == 0 )); then
# Find all web apps
while IFS= read -r -d '' file; do
if grep -q '^Exec=.*\(omarchy-launch-webapp\|omarchy-webapp-handler\).*' "$file"; then
WEB_APPS+=("$(basename "${file%.desktop}")")
fi
done < <(find "$DESKTOP_DIR" -name '*.desktop' -print0)
if ((${#WEB_APPS[@]})); then
mapfile -t SORTED_WEB_APPS < <(printf '%s\n' "${WEB_APPS[@]}" | sort)
APP_NAME=$(omarchy-menu-select "Select web app to remove" "${SORTED_WEB_APPS[@]}" -- --width 520 --maxheight 520)
@@ -51,8 +34,7 @@ if [[ -z $APP_NAME ]]; then
fi
icon_name=$(printf '%s\n' "$APP_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^[:alnum:]]\+/-/g; s/^-//; s/-$//')
desktop_file=$(path_for_web_app "$APP_NAME")
rm -f "${desktop_file:-$DESKTOP_DIR/$APP_NAME.desktop}"
rm -f "$DESKTOP_DIR/$APP_NAME.desktop"
rm -f "$ICON_DIR/$icon_name.png" "$ICON_DIR/$APP_NAME.png" "$OLD_ICON_DIR/$APP_NAME.png"
if [[ ${OMARCHY_REMOVE_NOTIFY:-true} != "false" ]]; then
-41
View File
@@ -1,41 +0,0 @@
#!/bin/bash
# omarchy:summary=Print the OEM Windows product key stored in firmware
# omarchy:requires-sudo=true
set -euo pipefail
MSDM=${OMARCHY_MSDM_PATH:-/sys/firmware/acpi/tables/MSDM}
if [[ ! -e $MSDM ]]; then
echo "No Windows license key found in firmware." >&2
exit 1
fi
read_msdm() {
if [[ -r $MSDM ]]; then
"$@" "$MSDM"
else
sudo "$@" "$MSDM"
fi
}
extract_key() {
grep -aoEm1 '[A-Z0-9]{5}(-[A-Z0-9]{5}){4}'
}
key=""
if omarchy-cmd-present strings; then
key=$(read_msdm strings | extract_key) || true
fi
if [[ -z $key ]]; then
key=$(read_msdm cat | extract_key) || true
fi
if [[ -z $key ]]; then
echo "Firmware license table found, but no Windows product key could be extracted." >&2
exit 1
fi
printf '%s\n' "$key"
+124 -897
View File
File diff suppressed because it is too large. Load diff
+3 -11
View File
@@ -1,10 +1,10 @@
-- See https://wiki.hypr.land/Configuring/Basics/Monitors/
-- List current monitors and supported resolutions with: hyprctl monitors all
-- Monitor scale is Hyprland's scale for the output. It sizes everything
-- Wayland-native, accepts fractions (1.6, 1.75), and applies immediately.
-- "auto" lets Hyprland pick per display.
local omarchy_gdk_scale = 2
local omarchy_monitor_scale = "auto"
hl.env("GDK_SCALE", tostring(omarchy_gdk_scale))
hl.monitor({ output = "", mode = "preferred", position = "auto", scale = omarchy_monitor_scale })
-- Configure a specific monitor.
@@ -12,11 +12,3 @@ hl.monitor({ output = "", mode = "preferred", position = "auto", scale = omarchy
-- Portrait/rotated secondary monitor (transform: 1 = 90°, 3 = 270°).
-- hl.monitor({ output = "DP-2", mode = "preferred", position = "auto", scale = 1, transform = 1 })
-- GDK scale is GDK_SCALE, the factor GTK draws its own UI at. It's what
-- sizes X11/XWayland windows, which Omarchy leaves unscaled so they stay
-- crisp instead of being stretched by the compositor. GTK only honors whole
-- numbers, so use the nearest integer to the monitor scale, and restart an
-- app for a change to reach it.
local omarchy_gdk_scale = 2
hl.env("GDK_SCALE", tostring(omarchy_gdk_scale))
+3 -3
View File
@@ -10,7 +10,7 @@
// label Visible row title.
// action Shell command to run. If omitted, the row is a submenu.
// target Existing submenu id to open. Use for links/aliases.
// provider Name of a shell-defined runtime row source (e.g. "fonts").
// provider Runtime provider function/command returning JSON rows.
// aliases alternate `omarchy menu summon <name>` routes; also searchable.
// description Optional subtitle and extra search text.
// when Shell condition; hide row when it fails.
@@ -21,8 +21,8 @@
// "personal.notes": {"icon":"󰎞","label":"Notes","action":"omarchy-launch-editor ~/notes"},
// "personal.files": {"icon":"","label":"Files","action":"uwsm-app -- nautilus ~/Documents"},
//
// provider can only reference row sources the shell already defines (the
// providers map in Menu.qml). Static submenus only need dotted ids.
// Only use provider when a provider_name function or command named "name"
// returns JSON rows. Static submenus only need dotted ids.
//
// Example: replace the default About action by reusing the same id. Existing
// fields are kept unless overridden.
+1 -32
View File
@@ -87,38 +87,7 @@ ambiguous, say so rather than assembling confidence out of guesswork.
**Leave the system as you found it.** Diagnosis reads; it does not fix, tidy, or
reconfigure. The one thing to clean up is your own: delete the core you extracted
above, which is a copy of the crashed process's memory. The single change a
diagnosis may make is the mute below, and only when the user asks for it.
## Offer to stop the notifications for this program
A crash you have explained often keeps happening anyway. Finish by offering to
silence notifications for **that one program**, and never run it unprompted. Say
how to lift it in the same breath, so it is not a one-way door.
```bash
omarchy-crash-mute '<program>' # silence it
omarchy-crash-mute '<program>' off # let it speak again
omarchy-crash-mute # list what is muted
```
Pass the `binary:` path from the crash facts, or the `process:` name where no
binary was recorded; the command reduces either to the name the watcher keys on.
A diagnosis run by hand from `omarchy agent crash <pid>` has neither, so take
them from `coredumpctl info`. Prefer the binary: a process name is truncated to
15 characters and a basename is not, so muting the truncated form matches
nothing, forever, while looking like it worked.
Quote it. The name is whatever the crashed program's author called a file, and a
single quote inside one closes yours and runs the rest as your shell.
The key is a bare name, so anything run through an interpreter is keyed as the
interpreter: muting `python3.13` silences every Python program on the machine.
Say so rather than quietly doing it.
None of this fixes anything, and a mute offered in place of a fix that was within
reach is the wrong answer. For every program rather than one, the switch is
_Trigger > Toggle > Crash Capture_.
above, which is a copy of the crashed process's memory.
## If it is an Omarchy bug
@@ -32,7 +32,7 @@ validated_on="2026-07-24"
validated_hardware="XPS 14 DA14260 (0DB9)"
## Measured against that reference under a dense pink-weighted multitone of 104
## bin-aligned tones. See docs/audio-tuning.md for how to reproduce these.
## bin-aligned tones. See docs/AUDIO-TUNING.md for how to reproduce these.
magnitude_rms_db="1.24"
bass_group_delay_swing_ms="13.2"
limiter_headroom_db="1.6" ## worst-case peak on a hot master vs -1 dBFS
-2
View File
@@ -10,8 +10,6 @@ The private-use glyphs in `omarchy.ttf` are:
- `U+E905` — Codex, from the OpenAI mark on <https://developers.openai.com/codex/>
- `U+E906` — LM Studio, from <https://simpleicons.org/icons/lmstudio.svg>
- `U+E907` — Ollama, from <https://simpleicons.org/icons/ollama.svg>
- `U+E908` — T3 Code, traced from the app icon in <https://aur.archlinux.org/cgit/aur.git/plain/t3code-icon.png?h=t3code-bin>, since upstream publishes no monochrome SVG
- `U+E909` — Ori, from <https://openrouter.ai/brand/v2/openrouter-glyph-dark.svg>, OpenRouter's own mark: Ori ships no separate logo and its product page uses this one
The agent marks are monochrome so the menu can render them using the active
theme's foreground and selection colors.
Binary file not shown.
+1 -4
View File
@@ -3,12 +3,9 @@
o.window(".*[Rr]esolve.*", {
float = true,
stay_focused = true,
-- Prevent modal dialog pointer warps when focus follows the mouse.
no_follow_mouse = true,
tag = "-default-opacity",
opacity = "1 1",
})
o.window({ class = ".*[Rr]esolve.*", title = "^DaVinci Resolve( Studio)? - .+$" }, { fullscreen = true })
-- Resolve exposes the Voiceover panel under the generic "Dialog" title.
o.window({ class = ".*[Rr]esolve.*", title = "^(DaVinci Resolve( Studio)? - .+|Project Manager|Preferences|Find Directory|Dialog)$" }, { stay_focused = false })
o.window({ class = ".*[Rr]esolve.*", title = "^(DaVinci Resolve( Studio)? - .+|Project Manager)$" }, { stay_focused = false })
-5
View File
@@ -1,5 +0,0 @@
-- Keep the Windows VM display opaque instead of applying the default window opacity.
o.window({ class = "^xfreerdp$", title = "^Windows VM - Omarchy$" }, {
tag = "-default-opacity",
opacity = "1 1",
})
-3
View File
@@ -1,5 +1,4 @@
o.bind("SUPER + W", "Close window", hl.dsp.window.close())
o.bind("SUPER + Q", "Close window", hl.dsp.window.close())
o.bind("CTRL + ALT + DELETE", "Close all windows", "omarchy-hyprland-window-close-all")
o.bind("SUPER + J", "Toggle window split", hl.dsp.layout("togglesplit"))
@@ -27,8 +26,6 @@ end
o.bind("SUPER + S", "Toggle scratchpad", hl.dsp.workspace.toggle_special("scratchpad"))
o.bind("SUPER + ALT + S", "Move window to scratchpad", hl.dsp.window.move({ workspace = "special:scratchpad", follow = false }))
o.bind("SUPER + grave", "Toggle scratchpad", hl.dsp.workspace.toggle_special("scratchpad"))
o.bind("SUPER + SHIFT + grave", "Move window to scratchpad", hl.dsp.window.move({ workspace = "special:scratchpad", follow = false }))
o.bind("SUPER + TAB", "Next workspace", hl.dsp.focus({ workspace = "e+1" }))
o.bind("SUPER + SHIFT + TAB", "Previous workspace", hl.dsp.focus({ workspace = "e-1" }))
+1
View File
@@ -86,6 +86,7 @@ hl.animation({ leaf = "layersOut", enabled = true, speed = 1.5, bezier = "linear
hl.animation({ leaf = "fadeLayersIn", enabled = true, speed = 1.79, bezier = "almostLinear" })
hl.animation({ leaf = "fadeLayersOut", enabled = true, speed = 1.39, bezier = "almostLinear" })
hl.animation({ leaf = "workspaces", enabled = false })
hl.animation({ leaf = "specialWorkspace", enabled = true, speed = 3, bezier = "easeOutQuint", style = "slidevert" })
hl.config({
dwindle = {
-1
View File
@@ -15,7 +15,6 @@ if _G.omarchy_default_bindings ~= false then
end
require("default.hypr.envs")
require("default.hypr.looknfeel")
require("default.hypr.qconsole")
require("default.hypr.input")
require("default.hypr.windows")
-88
View File
@@ -1,88 +0,0 @@
-- The scratchpad, presented as a Quake console: a dimmed overlay that drops
-- down over whatever workspace you are on. Its bindings live in
-- bindings/tiling.lua, and its slide is animated below.
-- How much of the usable screen the console covers, measured from the top.
local share = 0.5
-- Seed the console with the default agent the first time it opens, rather than
-- at boot, so nothing is running until it is wanted. The exec rule has to pin
-- the workspace itself: Hyprland only tags a spawn with the workspace it came
-- from while misc.initial_workspace_tracking is on, and looknfeel turns it off.
-- Omarchy ships without a default agent, and omarchy-agent exits without
-- opening anything when none is set, so until one is picked this just opens an
-- empty console.
local seed = "[workspace special:scratchpad silent] omarchy-agent"
-- Dimming only applies while a special workspace is open, so the console gets
-- its separation from the workspace underneath without costing anything the
-- rest of the time.
hl.config({
decoration = {
dim_special = 0.6,
},
})
-- Refitting replaces the rule in place rather than stacking a new one, but it
-- still schedules a monitor and window state refresh, and monitor.focused fires
-- on every hop between screens. Most of those hops do not change the number, so
-- only write the rule when it actually moves.
local covering = nil
local function cover(bottom)
if covering == bottom then
return
end
covering = bottom
hl.workspace_rule({
workspace = "special:scratchpad",
gaps_in = 0,
gaps_out = { top = 0, right = 0, bottom = bottom, left = 0 },
-- Nothing to highlight in a console that is only ever focused when it is
-- open, and the active border reads as a stray frame around a panel that
-- is already set apart by the dimming behind it.
no_border = true,
on_created_empty = seed,
})
end
-- Sizing the console with a window rule would freeze it at whatever the screen
-- measured when it first opened, because Hyprland resolves those expressions
-- once, as the window maps. Rescaling the monitor afterwards would leave a
-- console that is no longer half of anything. Gaps are re-applied by the layout
-- instead, so the console is sized by the gap left underneath it and that gap
-- is recomputed whenever the monitor layout changes.
local function fit()
local monitor = hl.get_active_monitor()
-- A monitor handle whose output has gone away answers nil to every field, and
-- layout changes are exactly when that happens, so this also covers reading
-- height and reserved below.
if not monitor or not monitor.scale or monitor.scale <= 0 then
return
end
-- Monitor dimensions are in physical pixels; gaps are logical, so the scale
-- has to come out before the reserved area (already logical) comes off.
local reserved = monitor.reserved
local usable = monitor.height / monitor.scale - reserved.top - reserved.bottom
cover(math.max(0, math.floor(usable * (1 - share))))
end
-- Until a monitor can be read, cover the whole work area rather than leaving
-- the console unruled, so it is never seeded without its placement.
cover(0)
fit()
hl.on("monitor.layout_changed", fit)
hl.on("monitor.focused", fit)
-- The direction names the edge the offset is measured from, not where the
-- workspace goes: "slide top" drops it down into view, and "slide bottom"
-- retracts it back up the way a Quake console does.
hl.animation({ leaf = "specialWorkspaceIn", enabled = true, speed = 3, bezier = "easeOutQuint", style = "slide top" })
hl.animation({ leaf = "specialWorkspaceOut", enabled = true, speed = 2, bezier = "easeInOutCubic", style = "slide bottom" })
+97 -113
View File
@@ -4,9 +4,8 @@
// IDs are object keys. Dotted IDs imply hierarchy: trigger.share.file belongs
// under trigger.share.
// Kind is inferred: action -> action, target -> link, otherwise submenu.
// Dotted IDs define the tree. provider:"name" points a submenu at one of
// the shell's providers (the providers map in shell/plugins/menu/Menu.qml);
// new provider names cannot be declared here. See docs/menu.md.
// Dotted IDs define the tree. Use provider:"name" only when the submenu
// calls provider_name() or a command named "name" to return JSON rows.
// Optional fields:
// aliases alternate `omarchy menu summon <name>` routes; also searchable.
// Reserved for established names users already type; new entries
@@ -15,10 +14,6 @@
// title header text shown when the submenu is open; defaults to label
// when shell condition; hide row when it fails
// checked shell condition; append ✓ when it succeeds
// disabled shell condition; when it succeeds the row stays listed but
// goes dim, takes a ✓ and can no longer be selected. Install
// rows use it so software already on the machine reads as
// installed instead of vanishing from the list.
// Root Menu
"apps": {"icon":"󰀻","label":"Apps","aliases":["app","applications"],"provider":"apps"},
@@ -103,7 +98,7 @@
// Style
"style.theme": {"icon":"󰸌","label":"Theme","aliases":["theme","themes"],"action":"theme=$(omarchy-theme-switcher); [[ -n $theme ]] && omarchy-theme-set \"$theme\""},
"style.background": {"icon":"","label":"Background","aliases":["background","wallpaper"],"action":"background=$(omarchy-theme-bg-switcher); [[ -n $background ]] && omarchy-theme-bg-set \"$background\""},
"style.unlock": {"icon":"󰟵","label":"Unlock","aliases":["unlock"],"action":"unlock=$(omarchy-plymouth-switcher); if [[ $unlock == default ]]; then omarchy-launch-floating-terminal-with-presentation omarchy-plymouth-reset; elif [[ -n $unlock ]]; then omarchy-launch-floating-terminal-with-presentation \"omarchy-plymouth-set-by-theme $(printf %q \"$unlock\")\"; fi"},
"style.unlock": {"icon":"󰟵","label":"Unlock","aliases":["unlock"],"action":"unlock=$(omarchy-plymouth-switcher); if [[ $unlock == default ]]; then omarchy-launch-floating-terminal-with-presentation omarchy-plymouth-reset; elif [[ -n $unlock ]]; then omarchy-launch-floating-terminal-with-presentation \"omarchy-plymouth-set-by-theme '$unlock'\"; fi"},
"style.font": {"icon":"","label":"Font","provider":"fonts"},
"style.bar": {"icon":"󰍜","label":"Menu Bar"},
"style.bar.position": {"icon":"","label":"Position"},
@@ -135,41 +130,37 @@
"setup.network.qr": {"icon":"󰐲","label":"QR Code","aliases":["wifi-qr"],"when":"[[ $(omarchy-network-status) == wifi* ]]","action":"omarchy-shell shell summon omarchy.wifiqr"},
"setup.default": {"icon":"","label":"Defaults","aliases":["default","defaults"]},
"setup.default.agent": {"icon":"󰚩","label":"Agent","title":"Default Agent"},
"setup.default.agent.agy": {"icon":"󰫢","label":"Antigravity","checked":"[[ \"$(omarchy-default-agent)\" == \"agy\" ]]","action":"omarchy-default-agent agy"},
"setup.default.agent.claude": {"icon":"󰛄","label":"Claude","checked":"[[ \"$(omarchy-default-agent)\" == \"claude\" ]]","action":"omarchy-default-agent claude"},
"setup.default.agent.codex": {"icon":"","iconFont":"omarchy","label":"Codex","checked":"[[ \"$(omarchy-default-agent)\" == \"codex\" ]]","action":"omarchy-default-agent codex"},
"setup.default.agent.copilot": {"icon":"","label":"Copilot","checked":"[[ \"$(omarchy-default-agent)\" == \"copilot\" ]]","action":"omarchy-default-agent copilot"},
"setup.default.agent.crush": {"icon":"󰋑","label":"Crush","checked":"[[ \"$(omarchy-default-agent)\" == \"crush\" ]]","action":"omarchy-default-agent crush"},
"setup.default.agent.gemini": {"icon":"󰫢","label":"Gemini","checked":"[[ \"$(omarchy-default-agent)\" == \"gemini\" ]]","action":"omarchy-default-agent gemini"},
"setup.default.agent.grok": {"icon":"","iconFont":"omarchy","label":"Grok","checked":"[[ \"$(omarchy-default-agent)\" == \"grok\" ]]","action":"omarchy-default-agent grok"},
"setup.default.agent.omp": {"icon":"","iconFont":"omarchy","label":"omp","checked":"[[ \"$(omarchy-default-agent)\" == \"omp\" ]]","action":"omarchy-default-agent omp"},
"setup.default.agent.opencode": {"icon":"","iconFont":"omarchy","label":"OpenCode","checked":"[[ \"$(omarchy-default-agent)\" == \"opencode\" ]]","action":"omarchy-default-agent opencode"},
"setup.default.agent.ori": {"icon":"","iconFont":"omarchy","label":"Ori","checked":"[[ \"$(omarchy-default-agent)\" == \"ori\" ]]","action":"omarchy-default-agent ori"},
"setup.default.agent.pi": {"icon":"","iconFont":"omarchy","label":"Pi","checked":"[[ \"$(omarchy-default-agent)\" == \"pi\" ]]","action":"omarchy-default-agent pi"},
"setup.default.browser": {"icon":"","label":"Browser","title":"Default Browser"},
"setup.default.browser.chromium": {"icon":"","label":"Chromium","checked":"[[ \"$(omarchy-default-browser)\" == \"chromium\" ]]","action":"omarchy-default-browser chromium"},
"setup.default.browser.chrome": {"icon":"󰊯","label":"Chrome","checked":"[[ \"$(omarchy-default-browser)\" == \"chrome\" ]]","action":"omarchy-default-browser chrome"},
"setup.default.browser.brave": {"icon":"󰖟","label":"Brave","checked":"[[ \"$(omarchy-default-browser)\" == \"brave\" ]]","action":"omarchy-default-browser brave"},
"setup.default.browser.brave-origin": {"icon":"󰖟","label":"Brave Origin","checked":"[[ \"$(omarchy-default-browser)\" == \"brave-origin\" ]]","action":"omarchy-default-browser brave-origin"},
"setup.default.browser.edge": {"icon":"󰇩","label":"Edge","checked":"[[ \"$(omarchy-default-browser)\" == \"edge\" ]]","action":"omarchy-default-browser edge"},
"setup.default.browser.firefox": {"icon":"","label":"Firefox","checked":"[[ \"$(omarchy-default-browser)\" == \"firefox\" ]]","action":"omarchy-default-browser firefox"},
"setup.default.browser.zen": {"icon":"󰖟","label":"Zen","checked":"[[ \"$(omarchy-default-browser)\" == \"zen\" ]]","action":"omarchy-default-browser zen"},
"setup.default.browser.chromium": {"icon":"","label":"Chromium","when":"omarchy-cmd-present chromium","checked":"[[ \"$(omarchy-default-browser)\" == \"chromium\" ]]","action":"omarchy-default-browser chromium"},
"setup.default.browser.chrome": {"icon":"󰊯","label":"Chrome","when":"omarchy-cmd-present google-chrome-stable","checked":"[[ \"$(omarchy-default-browser)\" == \"chrome\" ]]","action":"omarchy-default-browser chrome"},
"setup.default.browser.brave": {"icon":"󰖟","label":"Brave","when":"omarchy-cmd-present brave","checked":"[[ \"$(omarchy-default-browser)\" == \"brave\" ]]","action":"omarchy-default-browser brave"},
"setup.default.browser.brave-origin": {"icon":"󰖟","label":"Brave Origin","when":"omarchy-cmd-present brave-origin","checked":"[[ \"$(omarchy-default-browser)\" == \"brave-origin\" ]]","action":"omarchy-default-browser brave-origin"},
"setup.default.browser.edge": {"icon":"󰇩","label":"Edge","when":"omarchy-cmd-present microsoft-edge-stable","checked":"[[ \"$(omarchy-default-browser)\" == \"edge\" ]]","action":"omarchy-default-browser edge"},
"setup.default.browser.firefox": {"icon":"","label":"Firefox","when":"omarchy-cmd-present firefox","checked":"[[ \"$(omarchy-default-browser)\" == \"firefox\" ]]","action":"omarchy-default-browser firefox"},
"setup.default.browser.zen": {"icon":"󰖟","label":"Zen","when":"omarchy-cmd-present zen-browser","checked":"[[ \"$(omarchy-default-browser)\" == \"zen\" ]]","action":"omarchy-default-browser zen"},
"setup.default.terminal": {"icon":"","label":"Terminal","title":"Default Terminal"},
"setup.default.terminal.alacritty": {"icon":"","label":"Alacritty","checked":"[[ \"$(omarchy-default-terminal)\" == \"alacritty\" ]]","action":"omarchy-default-terminal alacritty"},
"setup.default.terminal.foot": {"icon":"","label":"Foot","checked":"[[ \"$(omarchy-default-terminal)\" == \"foot\" ]]","action":"omarchy-default-terminal foot"},
"setup.default.terminal.ghostty": {"icon":"","label":"Ghostty","checked":"[[ \"$(omarchy-default-terminal)\" == \"ghostty\" ]]","action":"omarchy-default-terminal ghostty"},
"setup.default.terminal.kitty": {"icon":"","label":"Kitty","checked":"[[ \"$(omarchy-default-terminal)\" == \"kitty\" ]]","action":"omarchy-default-terminal kitty"},
"setup.default.terminal.alacritty": {"icon":"","label":"Alacritty","when":"omarchy-cmd-present alacritty","checked":"[[ \"$(omarchy-default-terminal)\" == \"alacritty\" ]]","action":"omarchy-default-terminal alacritty"},
"setup.default.terminal.foot": {"icon":"","label":"Foot","when":"omarchy-cmd-present foot","checked":"[[ \"$(omarchy-default-terminal)\" == \"foot\" ]]","action":"omarchy-default-terminal foot"},
"setup.default.terminal.ghostty": {"icon":"","label":"Ghostty","when":"omarchy-cmd-present ghostty","checked":"[[ \"$(omarchy-default-terminal)\" == \"ghostty\" ]]","action":"omarchy-default-terminal ghostty"},
"setup.default.terminal.kitty": {"icon":"","label":"Kitty","when":"omarchy-cmd-present kitty","checked":"[[ \"$(omarchy-default-terminal)\" == \"kitty\" ]]","action":"omarchy-default-terminal kitty"},
"setup.default.editor": {"icon":"","label":"Editor","title":"Default Editor"},
"setup.default.editor.neovim": {"icon":"","label":"Neovim","checked":"[[ \"$(omarchy-default-editor)\" == \"nvim\" ]]","action":"omarchy-default-editor nvim"},
"setup.default.editor.vscode": {"icon":"","label":"VSCode","checked":"[[ \"$(omarchy-default-editor)\" == \"code\" ]]","action":"omarchy-default-editor code"},
"setup.default.editor.cursor": {"icon":"","label":"Cursor","checked":"[[ \"$(omarchy-default-editor)\" == \"cursor\" ]]","action":"omarchy-default-editor cursor"},
"setup.default.editor.zed": {"icon":"","label":"Zed","checked":"[[ \"$(omarchy-default-editor)\" == \"zeditor\" ]]","action":"omarchy-default-editor zed"},
"setup.default.editor.sublime": {"icon":"","label":"Sublime Text","checked":"[[ \"$(omarchy-default-editor)\" == \"sublime_text\" ]]","action":"omarchy-default-editor sublime_text"},
"setup.default.editor.helix": {"icon":"","label":"Helix","checked":"[[ \"$(omarchy-default-editor)\" == \"helix\" ]]","action":"omarchy-default-editor helix"},
"setup.default.editor.vim": {"icon":"","label":"Vim","checked":"[[ \"$(omarchy-default-editor)\" == \"vim\" ]]","action":"omarchy-default-editor vim"},
"setup.default.editor.emacs": {"icon":"","label":"Emacs","checked":"[[ \"$(omarchy-default-editor)\" == \"emacs\" ]]","action":"omarchy-default-editor emacs"},
"setup.region": {"icon":"󰇧","label":"Region"},
"setup.region.world": {"icon":"󰖟","label":"World","checked":"[[ \"$(omarchy-region)\" == \"World\" ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-region world'"},
"setup.region.china": {"icon":"󰗊","label":"China","checked":"[[ \"$(omarchy-region)\" == \"China\" ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-region china'"},
"setup.default.editor.neovim": {"icon":"","label":"Neovim","when":"omarchy-cmd-present nvim","checked":"[[ \"$(omarchy-default-editor)\" == \"nvim\" ]]","action":"omarchy-default-editor nvim"},
"setup.default.editor.vscode": {"icon":"","label":"VSCode","when":"omarchy-cmd-present code","checked":"[[ \"$(omarchy-default-editor)\" == \"code\" ]]","action":"omarchy-default-editor code"},
"setup.default.editor.cursor": {"icon":"","label":"Cursor","when":"omarchy-cmd-present cursor","checked":"[[ \"$(omarchy-default-editor)\" == \"cursor\" ]]","action":"omarchy-default-editor cursor"},
"setup.default.editor.zed": {"icon":"","label":"Zed","when":"omarchy-cmd-present zeditor","checked":"[[ \"$(omarchy-default-editor)\" == \"zeditor\" ]]","action":"omarchy-default-editor zed"},
"setup.default.editor.sublime": {"icon":"","label":"Sublime Text","when":"omarchy-cmd-present sublime_text","checked":"[[ \"$(omarchy-default-editor)\" == \"sublime_text\" ]]","action":"omarchy-default-editor sublime_text"},
"setup.default.editor.helix": {"icon":"","label":"Helix","when":"omarchy-cmd-present helix","checked":"[[ \"$(omarchy-default-editor)\" == \"helix\" ]]","action":"omarchy-default-editor helix"},
"setup.default.editor.vim": {"icon":"","label":"Vim","when":"omarchy-cmd-present vim","checked":"[[ \"$(omarchy-default-editor)\" == \"vim\" ]]","action":"omarchy-default-editor vim"},
"setup.default.editor.emacs": {"icon":"","label":"Emacs","when":"omarchy-cmd-present emacs","checked":"[[ \"$(omarchy-default-editor)\" == \"emacs\" ]]","action":"omarchy-default-editor emacs"},
"setup.plugin": {"icon":"󰐱","label":"Plugins","aliases":["plugin","plugins"]},
"setup.plugin.enable": {"icon":"󰄬","label":"Enable Plugin","action":"omarchy-menu-plugin enable"},
"setup.plugin.disable": {"icon":"󰅖","label":"Disable Plugin","action":"omarchy-menu-plugin disable"},
@@ -192,10 +183,8 @@
// Install
"install.package": {"icon":"󰣇","label":"Package","action":"xdg-terminal-exec --app-id=org.omarchy.terminal omarchy-pkg-install"},
"install.aur": {"icon":"󰣇","label":"AUR","action":"xdg-terminal-exec --app-id=org.omarchy.terminal omarchy-pkg-aur-install"},
"install.ai": {"icon":"󱚤","label":"AI"},
"install.service": {"icon":"","label":"Service"},
"install.development": {"icon":"󰵮","label":"Development"},
"install.editor": {"icon":"","label":"Editor"},
"install.webapp": {"icon":"","label":"Web App","action":"omarchy-launch-floating-terminal-with-presentation omarchy-webapp-install"},
"install.tui": {"icon":"","label":"TUI","action":"omarchy-launch-floating-terminal-with-presentation omarchy-tui-install"},
"install.style": {"icon":"","label":"Style"},
"install.style.theme": {"icon":"󰸌","label":"Theme","action":"omarchy-launch-floating-terminal-with-presentation omarchy-theme-install"},
"install.style.background": {"icon":"","label":"Background","action":"omarchy-theme-bg-install"},
@@ -206,88 +195,89 @@
"install.style.font.victor": {"icon":"","label":"Victor Code","action":"omarchy-install-font 'Victor Code' ttf-victor-mono-nerd 'VictorMono Nerd Font'"},
"install.style.font.bitstream": {"icon":"","label":"Bitstream Vera Mono","action":"omarchy-install-font 'Bitstream Vera Code' ttf-bitstream-vera-mono-nerd 'BitstromWera Nerd Font'"},
"install.style.font.iosevka": {"icon":"","label":"Iosevka","action":"omarchy-install-font Iosevka ttf-iosevka-nerd 'Iosevka Nerd Font Mono'"},
"install.gaming": {"icon":"","label":"Gaming"},
"install.browser": {"icon":"","label":"Browser"},
"install.webapp": {"icon":"","label":"Web App","action":"omarchy-launch-floating-terminal-with-presentation omarchy-webapp-install"},
"install.service": {"icon":"","label":"Service"},
"install.development": {"icon":"󰵮","label":"Development"},
"install.editor": {"icon":"","label":"Editor"},
"install.terminal": {"icon":"","label":"Terminal"},
"install.tui": {"icon":"","label":"TUI","action":"omarchy-launch-floating-terminal-with-presentation omarchy-tui-install"},
"install.windows": {"icon":"󰍲","label":"Windows","disabled":"[[ -f $HOME/.local/share/applications/windows-vm.desktop ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-windows-vm install'"},
"install.preinstalls": {"icon":"󰏓","label":"Preinstalls","disabled":"[[ ! -f $HOME/.local/state/omarchy/preinstalls-removed ]]","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-preinstalls"},
"install.browser.chrome": {"icon":"","label":"Chrome","disabled":"omarchy-pkg-present google-chrome","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-browser chrome'"},
"install.browser.edge": {"icon":"󰇩","label":"Edge","disabled":"omarchy-pkg-present microsoft-edge-stable-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-browser edge'"},
"install.browser.brave": {"icon":"","label":"Brave","disabled":"omarchy-pkg-present brave-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-browser brave'"},
"install.browser.brave-origin": {"icon":"","label":"Brave Origin","disabled":"omarchy-pkg-present brave-origin-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-browser brave-origin'"},
"install.browser.firefox": {"icon":"","label":"Firefox","disabled":"omarchy-pkg-present firefox","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-browser firefox'"},
"install.browser.zen": {"icon":"󰖟","label":"Zen","disabled":"omarchy-pkg-present zen-browser-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-browser zen'"},
"install.service.1password": {"icon":"󰢁","label":"1Password","disabled":"omarchy-pkg-present 1password","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-1password"},
"install.service.dropbox": {"icon":"","label":"Dropbox","disabled":"omarchy-pkg-present dropbox","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-dropbox"},
"install.service.spotify": {"icon":"󰓇","label":"Spotify","disabled":"omarchy-pkg-present spotify","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-spotify"},
"install.service.signal": {"icon":"󰭹","label":"Signal","disabled":"omarchy-pkg-present signal-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-signal"},
"install.service.tailscale": {"icon":"","label":"Tailscale","disabled":"omarchy-pkg-present tailscale","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-tailscale"},
"install.service.nordvpn": {"icon":"󱇱","label":"NordVPN","disabled":"omarchy-pkg-present nordvpn-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-nordvpn"},
"install.service.once": {"icon":"󰏖","label":"ONCE","disabled":"omarchy-pkg-present once-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-once"},
"install.service.bitwarden": {"icon":"󰟵","label":"Bitwarden","disabled":"omarchy-pkg-present bitwarden","action":"omarchy-install-and-launch Bitwarden 'bitwarden bitwarden-cli' bitwarden"},
"install.service.chromium-account": {"icon":"","label":"Chromium Account","when":"[[ -f ~/.config/chromium-flags.conf ]]","disabled":"grep -q oauth2-client-id ~/.config/chromium-flags.conf","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-chromium-google-account"},
"install.editor.vscode": {"icon":"","label":"VSCode","disabled":"omarchy-pkg-present visual-studio-code-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-editor-vscode"},
"install.editor.cursor": {"icon":"","label":"Cursor","disabled":"omarchy-pkg-present cursor-bin","action":"omarchy-install-and-launch Cursor cursor-bin cursor"},
"install.editor.zed": {"icon":"","label":"Zed","disabled":"omarchy-pkg-present zed","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-editor-zed"},
"install.editor.sublime": {"icon":"","label":"Sublime Text","disabled":"omarchy-pkg-present sublime-text-4","action":"omarchy-install-and-launch 'Sublime Text' sublime-text-4 sublime_text"},
"install.editor.helix": {"icon":"","label":"Helix","disabled":"omarchy-pkg-present helix","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-editor-helix"},
"install.editor.vim": {"icon":"","label":"Vim","disabled":"omarchy-pkg-present vim","action":"omarchy-install-app Vim vim"},
"install.editor.emacs": {"icon":"","label":"Emacs","disabled":"omarchy-pkg-present omarchy-emacs","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-editor-emacs"},
"install.terminal.alacritty": {"icon":"","label":"Alacritty","disabled":"omarchy-pkg-present alacritty","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-terminal alacritty'"},
"install.terminal.foot": {"icon":"","label":"Foot","disabled":"omarchy-pkg-present foot","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-terminal foot'"},
"install.terminal.ghostty": {"icon":"","label":"Ghostty","disabled":"omarchy-pkg-present ghostty","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-terminal ghostty'"},
"install.terminal.kitty": {"icon":"","label":"Kitty","disabled":"omarchy-pkg-present kitty","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-terminal kitty'"},
"install.ai.chatgpt": {"icon":"","iconFont":"omarchy","label":"ChatGPT Desktop","disabled":"omarchy-pkg-present openai-codex-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-ai-chatgpt"},
"install.ai.dictation": {"icon":"","label":"Dictation","disabled":"omarchy-pkg-present voxtype-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-voxtype-install"},
"install.ai.grok-bot": {"icon":"","iconFont":"omarchy","label":"Grok Bot","disabled":"omarchy-pkg-present grok-bot","action":"omarchy-install-and-launch 'Grok Bot' grok-bot grok-bot"},
"install.ai.lm-studio": {"icon":"","iconFont":"omarchy","label":"LM Studio","disabled":"omarchy-pkg-present lmstudio-bin","action":"omarchy-install-app 'LM Studio' lmstudio-bin"},
"install.ai.ollama": {"icon":"","iconFont":"omarchy","label":"Ollama","disabled":"omarchy-cmd-present ollama","action":"if omarchy-cmd-present nvidia-smi; then ollama_pkg=ollama-cuda; elif omarchy-cmd-present rocminfo; then ollama_pkg=ollama-rocm; else ollama_pkg=ollama; fi; omarchy-install-app Ollama \"$ollama_pkg\""},
"install.ai.t3-code": {"icon":"","iconFont":"omarchy","label":"T3 Code","disabled":"omarchy-pkg-present t3code-bin","action":"omarchy-install-and-launch 'T3 Code' t3code-bin t3code"},
"install.gaming.steam": {"icon":"","label":"Steam","disabled":"omarchy-pkg-present steam","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-gaming-steam"},
"install.gaming.retroarch": {"icon":"󰯉","label":"RetroArch","disabled":"omarchy-pkg-present retroarch","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-gaming-retroarch"},
"install.gaming.minecraft": {"icon":"󰍳","label":"Minecraft","disabled":"omarchy-pkg-present minecraft-launcher","action":"omarchy-install-and-launch Minecraft minecraft-launcher minecraft-launcher"},
"install.gaming.geforce-now": {"icon":"󰢹","label":"NVIDIA GeForce NOW","disabled":"flatpak info com.nvidia.geforcenow","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-gaming-geforce-now"},
"install.gaming.xbox-cloud": {"icon":"","label":"Xbox Cloud Gaming","disabled":"[[ -f \"$HOME/.local/share/applications/Xbox Cloud Gaming.desktop\" ]]","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-gaming-xbox-cloud"},
"install.gaming.xbox-controllers": {"icon":"󰂯","label":"Xbox Controllers","disabled":"omarchy-pkg-present xpadneo-dkms","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-gaming-xbox-controllers"},
"install.gaming.battlenet": {"icon":"","label":"Battle.net","disabled":"[[ -d $HOME/Games/battlenet ]]","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-gaming-battlenet"},
"install.gaming.lutris": {"icon":"","label":"Lutris","disabled":"omarchy-pkg-present lutris","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-gaming-lutris"},
"install.gaming.heroic": {"icon":"󱓟","label":"Heroic (Epic Games)","disabled":"omarchy-pkg-present heroic-games-launcher-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-gaming-heroic"},
"install.browser": {"icon":"","label":"Browser"},
"install.ai": {"icon":"󱚤","label":"AI"},
"install.gaming": {"icon":"","label":"Gaming"},
"install.windows": {"icon":"󰍲","label":"Windows","when":"[[ ! -f $HOME/.local/share/applications/windows-vm.desktop ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-windows-vm install'"},
"install.preinstalls": {"icon":"󰏓","label":"Preinstalls","when":"[[ -f $HOME/.local/state/omarchy/preinstalls-removed ]]","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-preinstalls"},
"install.browser.chrome": {"icon":"","label":"Chrome","when":"! omarchy-pkg-present google-chrome","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-browser chrome'"},
"install.browser.edge": {"icon":"󰇩","label":"Edge","when":"! omarchy-pkg-present microsoft-edge-stable-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-browser edge'"},
"install.browser.brave": {"icon":"","label":"Brave","when":"! omarchy-pkg-present brave-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-browser brave'"},
"install.browser.brave-origin": {"icon":"","label":"Brave Origin","when":"! omarchy-pkg-present brave-origin-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-browser brave-origin'"},
"install.browser.firefox": {"icon":"","label":"Firefox","when":"! omarchy-pkg-present firefox","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-browser firefox'"},
"install.browser.zen": {"icon":"󰖟","label":"Zen","when":"! omarchy-pkg-present zen-browser-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-browser zen'"},
"install.service.1password": {"icon":"󰢁","label":"1Password","when":"! omarchy-pkg-present 1password","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-1password"},
"install.service.dropbox": {"icon":"","label":"Dropbox","when":"! omarchy-pkg-present dropbox","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-dropbox"},
"install.service.spotify": {"icon":"󰓇","label":"Spotify","when":"! omarchy-pkg-present spotify","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-spotify"},
"install.service.signal": {"icon":"󰭹","label":"Signal","when":"! omarchy-pkg-present signal-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-signal"},
"install.service.tailscale": {"icon":"","label":"Tailscale","when":"! omarchy-pkg-present tailscale","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-tailscale"},
"install.service.nordvpn": {"icon":"󱇱","label":"NordVPN","when":"! omarchy-pkg-present nordvpn-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-nordvpn"},
"install.service.once": {"icon":"󰏖","label":"ONCE","when":"! omarchy-pkg-present once-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-once"},
"install.service.bitwarden": {"icon":"󰟵","label":"Bitwarden","when":"! omarchy-pkg-present bitwarden","action":"omarchy-install-and-launch Bitwarden 'bitwarden bitwarden-cli' bitwarden"},
"install.service.chromium-account": {"icon":"","label":"Chromium Account","when":"[[ -f ~/.config/chromium-flags.conf ]] && ! grep -q oauth2-client-id ~/.config/chromium-flags.conf","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-chromium-google-account"},
"install.editor.vscode": {"icon":"","label":"VSCode","when":"! omarchy-pkg-present visual-studio-code-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-editor-vscode"},
"install.editor.cursor": {"icon":"","label":"Cursor","when":"! omarchy-pkg-present cursor-bin","action":"omarchy-install-and-launch Cursor cursor-bin cursor"},
"install.editor.zed": {"icon":"","label":"Zed","when":"! omarchy-pkg-present zed","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-editor-zed"},
"install.editor.sublime": {"icon":"","label":"Sublime Text","when":"! omarchy-pkg-present sublime-text-4","action":"omarchy-install-and-launch 'Sublime Text' sublime-text-4 sublime_text"},
"install.editor.helix": {"icon":"","label":"Helix","when":"! omarchy-pkg-present helix","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-editor-helix"},
"install.editor.vim": {"icon":"","label":"Vim","when":"! omarchy-pkg-present vim","action":"omarchy-install-app Vim vim"},
"install.editor.emacs": {"icon":"","label":"Emacs","when":"! omarchy-pkg-present omarchy-emacs","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-editor-emacs"},
"install.terminal.alacritty": {"icon":"","label":"Alacritty","when":"! omarchy-pkg-present alacritty","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-terminal alacritty'"},
"install.terminal.foot": {"icon":"","label":"Foot","when":"! omarchy-pkg-present foot","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-terminal foot'"},
"install.terminal.ghostty": {"icon":"","label":"Ghostty","when":"! omarchy-pkg-present ghostty","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-terminal ghostty'"},
"install.terminal.kitty": {"icon":"","label":"Kitty","when":"! omarchy-pkg-present kitty","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-terminal kitty'"},
"install.ai.chatgpt": {"icon":"","iconFont":"omarchy","label":"ChatGPT Desktop","when":"! omarchy-pkg-present openai-codex-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-ai-chatgpt"},
"install.ai.dictation": {"icon":"","label":"Dictation","when":"! omarchy-pkg-present voxtype-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-voxtype-install"},
"install.ai.grok-bot": {"icon":"","iconFont":"omarchy","label":"Grok Bot","when":"! omarchy-pkg-present grok-bot","action":"omarchy-install-and-launch 'Grok Bot' grok-bot grok-bot"},
"install.ai.lm-studio": {"icon":"","iconFont":"omarchy","label":"LM Studio","when":"! omarchy-pkg-present lmstudio-bin","action":"omarchy-install-app 'LM Studio' lmstudio-bin"},
"install.ai.ollama": {"icon":"","iconFont":"omarchy","label":"Ollama","when":"! omarchy-cmd-present ollama","action":"if omarchy-cmd-present nvidia-smi; then ollama_pkg=ollama-cuda; elif omarchy-cmd-present rocminfo; then ollama_pkg=ollama-rocm; else ollama_pkg=ollama; fi; omarchy-install-app Ollama \"$ollama_pkg\""},
"install.gaming.steam": {"icon":"","label":"Steam","when":"! omarchy-pkg-present steam","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-gaming-steam"},
"install.gaming.retroarch": {"icon":"󰯉","label":"RetroArch","when":"! omarchy-pkg-present retroarch","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-gaming-retroarch"},
"install.gaming.minecraft": {"icon":"󰍳","label":"Minecraft","when":"! omarchy-pkg-present minecraft-launcher","action":"omarchy-install-and-launch Minecraft minecraft-launcher minecraft-launcher"},
"install.gaming.geforce-now": {"icon":"󰢹","label":"NVIDIA GeForce NOW","when":"! flatpak info com.nvidia.geforcenow","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-gaming-geforce-now"},
"install.gaming.xbox-cloud": {"icon":"","label":"Xbox Cloud Gaming","when":"[[ ! -f \"$HOME/.local/share/applications/Xbox Cloud Gaming.desktop\" ]]","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-gaming-xbox-cloud"},
"install.gaming.xbox-controllers": {"icon":"󰂯","label":"Xbox Controllers","when":"! omarchy-pkg-present xpadneo-dkms","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-gaming-xbox-controllers"},
"install.gaming.battlenet": {"icon":"","label":"Battle.net","when":"[[ ! -d $HOME/Games/battlenet ]]","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-gaming-battlenet"},
"install.gaming.lutris": {"icon":"","label":"Lutris","when":"! omarchy-pkg-present lutris","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-gaming-lutris"},
"install.gaming.heroic": {"icon":"󱓟","label":"Heroic (Epic Games)","when":"! omarchy-pkg-present heroic-games-launcher-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-gaming-heroic"},
"install.gaming.retro-launcher": {"icon":"󰯉","label":"RetroArch Game Launcher","action":"omarchy-games-retro-install"},
"install.development.rails": {"icon":"󰫏","label":"Ruby on Rails","disabled":"[[ -d $HOME/.local/share/mise/installs/ruby ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env ruby'"},
"install.development.rails": {"icon":"󰫏","label":"Ruby on Rails","when":"[[ ! -d $HOME/.local/share/mise/installs/ruby ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env ruby'"},
"install.development.docker-dbs": {"icon":"","label":"Docker DB","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-docker-dbs"},
"install.development.javascript": {"icon":"","label":"JavaScript"},
"install.development.go": {"icon":"","label":"Go","disabled":"[[ -d $HOME/.local/share/mise/installs/go ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env go'"},
"install.development.go": {"icon":"","label":"Go","when":"[[ ! -d $HOME/.local/share/mise/installs/go ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env go'"},
"install.development.php": {"icon":"","label":"PHP"},
"install.development.python": {"icon":"","label":"Python","disabled":"[[ -d $HOME/.local/share/mise/installs/python ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env python'"},
"install.development.python": {"icon":"","label":"Python","when":"[[ ! -d $HOME/.local/share/mise/installs/python ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env python'"},
"install.development.elixir": {"icon":"","label":"Elixir"},
"install.development.zig": {"icon":"","label":"Zig","disabled":"[[ -d $HOME/.local/share/mise/installs/zig ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env zig'"},
"install.development.rust": {"icon":"","label":"Rust","disabled":"[[ -d $HOME/.rustup ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env rust'"},
"install.development.java": {"icon":"","label":"Java","disabled":"[[ -d $HOME/.local/share/mise/installs/java ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env java'"},
"install.development.dotnet": {"icon":"","label":".NET","disabled":"[[ -d $HOME/.local/share/mise/installs/dotnet ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env dotnet'"},
"install.development.ocaml": {"icon":"","label":"OCaml","disabled":"[[ -d $HOME/.opam ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env ocaml'"},
"install.development.clojure": {"icon":"","label":"Clojure","disabled":"[[ -d $HOME/.local/share/mise/installs/clojure ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env clojure'"},
"install.development.scala": {"icon":"","label":"Scala","disabled":"[[ -d $HOME/.local/share/mise/installs/scala ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env scala'"},
"install.development.javascript.node": {"icon":"","label":"Node.js","disabled":"[[ -d $HOME/.local/share/mise/installs/node ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env node'"},
"install.development.javascript.bun": {"icon":"","label":"Bun","disabled":"[[ -d $HOME/.local/share/mise/installs/bun ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env bun'"},
"install.development.javascript.deno": {"icon":"","label":"Deno","disabled":"[[ -d $HOME/.local/share/mise/installs/deno ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env deno'"},
"install.development.php.php": {"icon":"","label":"PHP","disabled":"omarchy-pkg-present php","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env php'"},
"install.development.php.laravel": {"icon":"","label":"Laravel","disabled":"[[ -x $HOME/.config/composer/vendor/bin/laravel ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env laravel'"},
"install.development.php.symfony": {"icon":"","label":"Symfony","disabled":"omarchy-pkg-present symfony-cli","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env symfony'"},
"install.development.elixir.elixir": {"icon":"","label":"Elixir","disabled":"[[ -d $HOME/.local/share/mise/installs/elixir ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env elixir'"},
"install.development.elixir.phoenix": {"icon":"","label":"Phoenix","disabled":"compgen -G \"$HOME/.mix/archives/phx_new*\"","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env phoenix'"},
"install.development.zig": {"icon":"","label":"Zig","when":"[[ ! -d $HOME/.local/share/mise/installs/zig ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env zig'"},
"install.development.rust": {"icon":"","label":"Rust","when":"[[ ! -d $HOME/.rustup ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env rust'"},
"install.development.java": {"icon":"","label":"Java","when":"[[ ! -d $HOME/.local/share/mise/installs/java ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env java'"},
"install.development.dotnet": {"icon":"","label":".NET","when":"[[ ! -d $HOME/.local/share/mise/installs/dotnet ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env dotnet'"},
"install.development.ocaml": {"icon":"","label":"OCaml","when":"[[ ! -d $HOME/.opam ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env ocaml'"},
"install.development.clojure": {"icon":"","label":"Clojure","when":"[[ ! -d $HOME/.local/share/mise/installs/clojure ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env clojure'"},
"install.development.scala": {"icon":"","label":"Scala","when":"[[ ! -d $HOME/.local/share/mise/installs/scala ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env scala'"},
"install.development.javascript.node": {"icon":"","label":"Node.js","when":"[[ ! -d $HOME/.local/share/mise/installs/node ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env node'"},
"install.development.javascript.bun": {"icon":"","label":"Bun","when":"[[ ! -d $HOME/.local/share/mise/installs/bun ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env bun'"},
"install.development.javascript.deno": {"icon":"","label":"Deno","when":"[[ ! -d $HOME/.local/share/mise/installs/deno ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env deno'"},
"install.development.php.php": {"icon":"","label":"PHP","when":"! omarchy-pkg-present php","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env php'"},
"install.development.php.laravel": {"icon":"","label":"Laravel","when":"[[ ! -x $HOME/.config/composer/vendor/bin/laravel ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env laravel'"},
"install.development.php.symfony": {"icon":"","label":"Symfony","when":"! omarchy-pkg-present symfony-cli","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env symfony'"},
"install.development.elixir.elixir": {"icon":"","label":"Elixir","when":"[[ ! -d $HOME/.local/share/mise/installs/elixir ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env elixir'"},
"install.development.elixir.phoenix": {"icon":"","label":"Phoenix","when":"! compgen -G \"$HOME/.mix/archives/phx_new*\"","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-dev-env phoenix'"},
// Remove
"remove.package": {"icon":"󰣇","label":"Package","action":"xdg-terminal-exec --app-id=org.omarchy.terminal omarchy-pkg-remove"},
"remove.ai": {"icon":"󱚤","label":"AI","title":"Remove"},
"remove.service": {"icon":"","label":"Services","title":"Remove"},
"remove.development": {"icon":"󰵮","label":"Development","title":"Remove"},
"remove.theme": {"icon":"󰸌","label":"Theme","action":"omarchy-theme-remove"},
"remove.gaming": {"icon":"","label":"Gaming","title":"Remove"},
"remove.browser": {"icon":"","label":"Browser","title":"Remove"},
"remove.webapp": {"icon":"","label":"Web App","when":"grep -qE '^Exec=.*(omarchy-launch-webapp|omarchy-webapp-handler)' $HOME/.local/share/applications/*.desktop","action":"omarchy-webapp-remove"},
"remove.tui": {"icon":"","label":"TUI","when":"grep -qE '^Exec=.*(\\$TERMINAL|xdg-terminal-exec).*-e' $HOME/.local/share/applications/*.desktop","action":"omarchy-tui-remove"},
"remove.development": {"icon":"󰵮","label":"Development","title":"Remove"},
"remove.theme": {"icon":"󰸌","label":"Theme","action":"omarchy-theme-remove"},
"remove.browser": {"icon":"","label":"Browser","title":"Remove"},
"remove.dictation": {"icon":"","label":"Dictation","when":"omarchy-pkg-present voxtype-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-voxtype-remove"},
"remove.gaming": {"icon":"","label":"Gaming","title":"Remove"},
"remove.service": {"icon":"","label":"Services","title":"Remove"},
"remove.windows": {"icon":"󰍲","label":"Windows","when":"[[ -f $HOME/.local/share/applications/windows-vm.desktop ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-windows-vm remove'"},
"remove.preinstalls": {"icon":"󰏓","label":"Preinstalls","when":"[[ ! -f $HOME/.local/state/omarchy/preinstalls-removed ]]","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-preinstalls"},
"remove.security": {"icon":"","label":"Security","title":"Remove"},
@@ -303,12 +293,6 @@
"remove.browser.zen": {"icon":"","label":"Zen","when":"omarchy-pkg-present zen-browser-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-remove-browser zen'"},
"remove.service.dropbox": {"icon":"","label":"Dropbox","when":"omarchy-pkg-present dropbox","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-service-dropbox"},
"remove.service.tailscale": {"icon":"","label":"Tailscale","when":"omarchy-pkg-present tailscale","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-service-tailscale"},
"remove.ai.chatgpt": {"icon":"","iconFont":"omarchy","label":"ChatGPT Desktop","when":"omarchy-pkg-present openai-codex-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-ai-chatgpt"},
"remove.ai.dictation": {"icon":"","label":"Dictation","when":"omarchy-pkg-present voxtype-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-voxtype-remove"},
"remove.ai.grok-bot": {"icon":"","iconFont":"omarchy","label":"Grok Bot","when":"omarchy-pkg-present grok-bot","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-ai-grok-bot"},
"remove.ai.lm-studio": {"icon":"","iconFont":"omarchy","label":"LM Studio","when":"omarchy-pkg-present lmstudio-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-ai-lm-studio"},
"remove.ai.ollama": {"icon":"","iconFont":"omarchy","label":"Ollama","when":"omarchy-pkg-present ollama","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-ai-ollama"},
"remove.ai.t3-code": {"icon":"","iconFont":"omarchy","label":"T3 Code","when":"omarchy-pkg-present t3code-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-ai-t3-code"},
"remove.gaming.steam": {"icon":"","label":"Steam","when":"omarchy-pkg-present steam","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-gaming-steam"},
"remove.gaming.retroarch": {"icon":"","label":"RetroArch","when":"omarchy-pkg-present retroarch","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-gaming-retroarch"},
"remove.gaming.minecraft": {"icon":"󰍳","label":"Minecraft","when":"omarchy-pkg-present minecraft-launcher","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-gaming-minecraft"},
-168
View File
@@ -1,168 +0,0 @@
// Omarchy region managed
// Chinese labels for the shipped menu. `omarchy region china` installs this
// as the user menu extension; ids keep their actions and icons.
{
"apps": {"label":"应用"},
"learn": {"label":"学习"},
"trigger": {"label":"触发"},
"style": {"label":"风格"},
"setup": {"label":"设置"},
"install": {"label":"安装"},
"remove": {"label":"移除"},
"update": {"label":"更新"},
"about": {"label":"关于"},
"system": {"label":"系统"},
"system.screensaver": {"label":"屏保"},
"system.lock": {"label":"锁屏"},
"system.suspend": {"label":"睡眠"},
"system.hibernate": {"label":"休眠"},
"system.logout": {"label":"注销"},
"system.reboot": {"label":"重启"},
"system.shutdown": {"label":"关机"},
"learn.keybindings": {"label":"快捷键"},
"learn.omarchy": {"label":"Omarchy 手册"},
"learn.community": {"label":"社区"},
"trigger.emoji": {"label":"表情"},
"trigger.reminder": {"label":"提醒"},
"trigger.capture": {"label":"截取"},
"trigger.capture.screenshot": {"label":"截图"},
"trigger.capture.screenrecord.stop": {"label":"停止录屏"},
"trigger.capture.screenrecord": {"label":"录屏"},
"trigger.capture.text": {"label":"OCR 取字"},
"trigger.capture.qr": {"label":"二维码"},
"trigger.capture.color": {"label":"取色"},
"trigger.capture.screenrecord.no-audio": {"label":"无音频"},
"trigger.capture.screenrecord.desktop-audio": {"label":"含桌面音频"},
"trigger.capture.screenrecord.microphone": {"label":"含桌面与麦克风音频"},
"trigger.capture.screenrecord.webcam": {"label":"含桌面、麦克风与摄像头"},
"trigger.transcode": {"label":"转码"},
"trigger.share": {"label":"分享"},
"trigger.toggle": {"label":"开关"},
"trigger.hardware": {"label":"硬件"},
"trigger.tests": {"label":"测速"},
"trigger.hardware.laptop-display": {"label":"笔记本屏幕"},
"trigger.hardware.mirror-display": {"label":"镜像显示"},
"trigger.hardware.hybrid-gpu": {"label":"混合显卡"},
"trigger.hardware.touchpad": {"label":"触控板"},
"trigger.hardware.touchpad-haptics": {"label":"触控板振动"},
"trigger.hardware.touchpad-haptics.low": {"label":"低"},
"trigger.hardware.touchpad-haptics.mid": {"label":"中"},
"trigger.hardware.touchpad-haptics.high": {"label":"高"},
"trigger.hardware.touchscreen": {"label":"触摸屏"},
"trigger.reminder.set": {"label":"设一个提醒"},
"trigger.reminder.show": {"label":"查看全部"},
"trigger.reminder.clear": {"label":"清除全部"},
"trigger.share.clipboard": {"label":"剪贴板"},
"trigger.share.file": {"label":"文件"},
"trigger.share.folder": {"label":"文件夹"},
"trigger.share.receive": {"label":"接收"},
"trigger.toggle.idle-lock": {"label":"保持唤醒"},
"trigger.toggle.notifications": {"label":"通知"},
"trigger.toggle.crash-capture": {"label":"崩溃捕获"},
"trigger.toggle.screensaver": {"label":"屏保"},
"trigger.toggle.nightlight": {"label":"夜间模式"},
"trigger.toggle.top-bar": {"label":"菜单栏"},
"trigger.toggle.battery-percentage": {"label":"电量百分比"},
"trigger.toggle.workspace-layout": {"label":"工作区布局"},
"trigger.toggle.window-gaps": {"label":"窗口间距"},
"trigger.toggle.one-window-ratio": {"label":"单窗口比例"},
"trigger.tests.network-speedtest": {"label":"网络测速"},
"trigger.tests.disk-speedtest": {"label":"磁盘测速"},
"style.theme": {"label":"主题"},
"style.background": {"label":"壁纸"},
"style.unlock": {"label":"解锁画面"},
"style.font": {"label":"字体"},
"style.bar": {"label":"菜单栏"},
"style.bar.position": {"label":"位置"},
"style.bar.transparency": {"label":"透明"},
"style.screensaver": {"label":"屏保"},
"style.about": {"label":"关于"},
"style.bar.position.top": {"label":"顶部"},
"style.bar.position.bottom": {"label":"底部"},
"style.bar.position.left": {"label":"左侧"},
"style.bar.position.right": {"label":"右侧"},
"style.about.text": {"label":"编辑文字"},
"style.about.image": {"label":"取自图片"},
"style.about.default": {"label":"恢复默认"},
"style.screensaver.text": {"label":"编辑文字"},
"style.screensaver.image": {"label":"取自图片"},
"style.screensaver.default": {"label":"恢复默认"},
"setup.monitors": {"label":"显示器"},
"setup.keybindings": {"label":"快捷键"},
"setup.input": {"label":"输入设备"},
"setup.network": {"label":"网络"},
"setup.network.dns.custom": {"label":"自定义"},
"setup.network.qr": {"label":"二维码"},
"setup.default": {"label":"默认程序"},
"setup.default.agent": {"title":"默认 Agent"},
"setup.default.browser": {"label":"浏览器","title":"默认浏览器"},
"setup.default.terminal": {"label":"终端","title":"默认终端"},
"setup.default.editor": {"label":"编辑器","title":"默认编辑器"},
"setup.region": {"label":"区域"},
"setup.region.world": {"label":"世界"},
"setup.region.china": {"label":"中国"},
"setup.plugin": {"label":"插件"},
"setup.plugin.enable": {"label":"启用插件"},
"setup.plugin.disable": {"label":"停用插件"},
"setup.plugin.add": {"label":"添加插件"},
"setup.plugin.clone": {"label":"克隆插件"},
"setup.plugin.remove": {"label":"移除插件"},
"setup.security": {"label":"安全"},
"setup.config": {"label":"配置"},
"setup.security.fingerprint": {"label":"指纹"},
"setup.security.passwordless-sudo": {"label":"免密 sudo"},
"setup.security.sudoless-docker": {"label":"免 sudo Docker"},
"setup.direct-boot": {"label":"直接启动"},
"setup.reset": {"label":"恢复出厂设置"},
"install.package": {"label":"软件包"},
"install.service": {"label":"服务"},
"install.development": {"label":"开发环境"},
"install.editor": {"label":"编辑器"},
"install.style": {"label":"风格"},
"install.style.theme": {"label":"主题"},
"install.style.background": {"label":"壁纸"},
"install.style.font": {"label":"字体"},
"install.gaming": {"label":"游戏"},
"install.browser": {"label":"浏览器"},
"install.webapp": {"label":"网页应用"},
"install.terminal": {"label":"终端"},
"install.preinstalls": {"label":"预装应用"},
"install.service.chromium-account": {"label":"Chromium 账号"},
"install.ai.dictation": {"label":"语音听写"},
"install.gaming.xbox-cloud": {"label":"Xbox 云游戏"},
"install.gaming.xbox-controllers": {"label":"Xbox 手柄"},
"install.gaming.retro-launcher": {"label":"RetroArch 启动器"},
"install.development.docker-dbs": {"label":"Docker 数据库"},
"remove.package": {"label":"软件包"},
"remove.ai": {"title":"移除"},
"remove.service": {"label":"服务","title":"移除"},
"remove.development": {"label":"开发环境","title":"移除"},
"remove.theme": {"label":"主题"},
"remove.gaming": {"label":"游戏","title":"移除"},
"remove.browser": {"label":"浏览器","title":"移除"},
"remove.webapp": {"label":"网页应用"},
"remove.preinstalls": {"label":"预装应用"},
"remove.security": {"label":"安全","title":"移除"},
"remove.security.fingerprint": {"label":"指纹"},
"remove.security.sudoless-docker": {"label":"免 sudo Docker"},
"remove.ai.dictation": {"label":"语音听写"},
"remove.gaming.xbox-cloud": {"label":"Xbox 云游戏"},
"remove.gaming.xbox-controllers": {"label":"Xbox 手柄 (󰂯)"},
"remove.development.javascript": {"title":"移除"},
"remove.development.php": {"title":"移除"},
"remove.development.elixir": {"title":"移除"},
"update.channel": {"label":"通道"},
"update.config": {"label":"配置","title":"重置为默认"},
"update.themes": {"label":"附加主题"},
"update.process": {"label":"进程","title":"重启"},
"update.hardware": {"label":"硬件","title":"重启"},
"update.firmware": {"label":"固件"},
"update.password": {"label":"密码"},
"update.timezone": {"label":"时区"},
"update.time": {"label":"时间"},
"update.hardware.audio": {"label":"音频"},
"update.hardware.bluetooth": {"label":"蓝牙"},
"update.hardware.trackpad": {"label":"触控板"},
"update.password.drive": {"label":"磁盘加密"},
"update.password.user": {"label":"用户"},
}
+1
View File
@@ -26,6 +26,7 @@ Include = /etc/pacman.d/mirrorlist
Include = /etc/pacman.d/mirrorlist
[omarchy]
SigLevel = Optional TrustAll
Server = https://pkgs.omarchy.org/edge/$arch
# Repositories for debug symbol packages.
+2 -1
View File
@@ -26,4 +26,5 @@ Include = /etc/pacman.d/mirrorlist
Include = /etc/pacman.d/mirrorlist
[omarchy]
Server = https://pkgs.omarchy.org/rc/$arch
SigLevel = Optional TrustAll
Server = https://pkgs.omarchy.org/edge/$arch
+1
View File
@@ -26,4 +26,5 @@ Include = /etc/pacman.d/mirrorlist
Include = /etc/pacman.d/mirrorlist
[omarchy]
SigLevel = Optional TrustAll
Server = https://pkgs.omarchy.org/stable/$arch
@@ -1,6 +1,6 @@
[Unit]
Description=Omarchy speaker tuning filter-chain
Documentation=https://github.com/basecamp/omarchy/blob/master/docs/audio-tuning.md
Documentation=https://github.com/basecamp/omarchy/blob/master/docs/AUDIO-TUNING.md
# WirePlumber does the linking, so starting before it is up risks the output being
# linked before the speaker device has been discovered.
After=pipewire.service wireplumber.service
+2 -19
View File
@@ -12,9 +12,7 @@ default/audio/tunings/<vendor>-<model>/
```
`on` renders the graph into `~/.config/pipewire/omarchy-speaker-tuning.conf.d/`
(plus the shared host config `omarchy-speaker-tuning.conf`, from
`default/audio/filter-chain-host.conf`) and runs it as its own PipeWire client via
`omarchy-speaker-tuning.service`, rather than
and runs it as its own PipeWire client via `omarchy-speaker-tuning.service`, rather than
loading it into the audio daemon. The daemon only reads its own config at startup,
so a daemon-loaded tuning could only be switched by restarting PipeWire — which
drops every PulseAudio client's connection, and applications that do not reconnect
@@ -42,13 +40,6 @@ omarchy audio tuning status # installed? in use? what matches?
`match` and `fronted-sink` are also accepted; they exist for the install hooks
and the sink-listing scripts rather than for daily use.
`on` verifies its own work: it waits for the tuning sink to appear and confirms
its output linked to the expected physical sink, and uninstalls everything again
on either failure — so a broken tuning cannot be left half-applied. It also
switches the default sink to the tuning and moves existing app streams onto it
(`off` reverses both). When everything is already installed and linked, `on` is
a no-op; pass `--force` when iterating on a tuning in place.
## Adding a tuning
Add a directory with a `tuning.conf` and a `filter-chain.conf`. No new command is
@@ -68,16 +59,8 @@ been validated on:
match_sku=("0DB9" "0DBA") # XPS 14 and XPS 16
```
Only the first *defined* key is consulted, in the order `match_command`,
`match_sku`, `match_dmi` — keys below a defined one are ignored, and a tuning
defining none never matches.
`sink_pattern` is required whichever method you use, since the graph's target sink
is substituted from it. The graph must also keep two fixed properties: the sink
node is named `omarchy_speaker_tuning` (the install checks for that exact name,
so renaming it silently fails verification), and its output stream sets
`node.dont-fallback` alongside `node.dont-move`, so WirePlumber cannot link it
somewhere else when the target sink is absent at host start.
is substituted from it.
Gate narrowly and widen as models are validated; a tuning aimed at the wrong
drivers can sound worse than none and can stress them. When a tuning covers a model
-130
View File
@@ -1,130 +0,0 @@
# The Omarchy CLI router
`bin/omarchy` maps spaced commands onto the flat `bin/omarchy-*` namespace:
`omarchy theme set foo` becomes `exec bin/omarchy-theme-set foo`. There is no
registry to maintain — every executable `bin/omarchy-*` file is a command, and
its filename is its default route. Metadata comments in the file header refine
how it presents and routes; the keys are documented in
[`agents/skills/command-metadata.md`](../agents/skills/command-metadata.md).
This document covers what that guide does not: how resolution and dispatch
actually work.
## How a binary becomes routes
The stem after `omarchy-` splits at the first hyphen: `omarchy-theme-set` gets
group `theme` and name `set`, with remaining hyphens becoming spaces
(`omarchy-hw-asus-rog` → group `hw`, name `asus rog`). A single-segment stem
(`omarchy-update`) is the root command of its own group, with an empty name.
Every command registers two routes: the canonical route `omarchy <group>
<name>` after metadata overrides, and the filename route with *all* hyphens
turned to spaces. When metadata moves nothing, they are the same route. When it
does, both keep working — `# omarchy:name=gaming xbox-cloud` on
`omarchy-install-gaming-xbox-cloud` keeps the hyphen inside the name, so
`omarchy install gaming xbox-cloud` is canonical while the filename route
`omarchy install gaming xbox cloud` still resolves. An explicitly *empty* `#
omarchy:name=` makes a command the root of its group: `omarchy-menu-share` sets
`group=share` and an empty name, so its canonical route is `omarchy share`
while `omarchy menu share` remains as the filename route. Alias routes register
the same way but are flagged, so listings show them as aliases rather than
commands.
Two routes claiming different binaries is a collision: the first registration
wins, dispatch is unaffected, and the conflict is recorded for `omarchy
commands --check` to report. Hidden commands (`# omarchy:hidden=true`) still
register and dispatch normally — hiding only removes them from listings, which
is how install-time plumbing like `omarchy apply hardware` stays callable
without being browsable.
Metadata is read from the comment header only: the first 80 lines, stopping at
the first non-comment line, so metadata-shaped comments after code never take
effect. Malformed `omarchy:` lines and unknown keys are ignored rather than
fatal — a typo degrades a command to its filename route instead of breaking the
router. The first plain comment line doubles as a fallback summary, and a
command with no comments at all still gets a generated one, though `--check`
demands the explicit form (below).
## Dispatch
Resolution is longest-prefix: the router tries the full argument list as a
route, then drops trailing words until something matches. Whatever it drops is
passed to the binary as arguments. This runs in two passes.
The fast path joins argument prefixes with hyphens and checks for an
executable file: `omarchy theme set foo` probes `omarchy-theme-set-foo`, then
`omarchy-theme-set`, which exists — resolved without reading a single metadata
header. This exists because plain dispatch is the hot path: parsing the
headers of several hundred binaries on every invocation is measurable
latency (`omarchy dev benchmark cli` tracks it), and a filename probe is a few
stat calls. Metadata loads lazily, only for the resolved command when help is
needed.
When no filename matches — metadata-moved routes like `omarchy share`, and
aliases like `omarchy screenshot` — the router falls back to loading all
metadata and resolving against the registered route table, with the same
longest-prefix rule.
Both paths intercept `--help`/`-h` *anywhere* in the leftover arguments, not
just the first one. Resolution can succeed with unresolved words still ahead of
the flag — `omarchy update aur --help` resolves `update` with leftovers `aur
--help` — and checking only the first leftover once let that invocation start a
real update. A `--` ends the scan: everything after it belongs to the command,
so `omarchy foo run -- --help` forwards the flag. `--json` alongside `--help`
switches the help output to the command's JSON record; `--json` alone is just
an argument for the command.
Bare invocations are also guarded. If a command declares required arguments
(its `args` metadata, minus `[bracketed]` optional parts, is non-empty) and
none were given, the router shows help instead of executing — `omarchy theme
set` prints usage rather than running an interactive setter. A bare group name
with child commands shows the group help.
Dispatch is `exec`: the router process is replaced, the binary sees only the
leftover arguments, and the exit code is the binary's own. The router itself
exits 127 for unknown routes or missing binaries.
When nothing resolves, the router tries a prefix listing — `omarchy hw asus`
prints every command whose usage starts with that prefix — and otherwise
errors with a "did you mean" suggestion (a known route extending the first
word) and a pointer to `omarchy commands --all`.
## Groups and the top-level listing
Group help is synthesized from metadata, not written anywhere. A command
belongs to a group when either its metadata group or its filename group
matches, listed under the route that fits the group being viewed: `omarchy
menu --help` shows `omarchy-menu-share` as `omarchy menu share`, while its
canonical `omarchy share` stands alone. On the fast path, group help loads
only that group's filename-prefixed binaries rather than everything.
The top-level `omarchy` listing is driven entirely by the hand-curated
`GROUP_DESCRIPTIONS` table in `bin/omarchy`, which also titles each group's
help. An entry there advertises the group even when every command in it is
hidden — which is exactly why `apply` and `provision` have none: they route,
but a listing entry would put install-time plumbing back in front of users
(see the Command Naming section of `AGENTS.md`). Adding a browsable command
group means adding its `GROUP_DESCRIPTIONS` entry; adding hidden plumbing
means deliberately not doing so.
## Introspection
`omarchy commands` prints every non-hidden command with its summary, plus an
alias table. `--all` includes hidden commands, `--markdown` emits a table, and
`--json` emits full records: route, binary, group, name, summary, flags, args,
examples, aliases, `filename_route`, and `routes` (the union of everything
that resolves to the binary). Per-command JSON comes from `omarchy <route>
--help --json`.
`omarchy commands --check` is the metadata lint, run by `test/cli`. It fails
on:
- route collisions between binaries
- a missing explicit `# omarchy:summary=` — a plain-comment fallback renders
in help but does not satisfy the check
- invalid boolean metadata: `hidden` and `requires-sudo` must be `true` or
omitted, never `false`
- a registered command whose binary is missing or not executable
When debugging a routing surprise, `omarchy <route> --help` shows the resolved
binary and, when it differs, the filename route; `omarchy commands --all
--json` shows every route the router knows.
+47 -81
View File
@@ -5,8 +5,8 @@ system.
## Mental model
Two Arch packages are built from this one repo (PKGBUILDs live in the
separate `omarchy-pkgs` repository, under `pkgbuilds/`):
Two Arch packages are built from this one repo (PKGBUILDs live in
`omarchy-pkgs/pkgbuilds/`):
- **`omarchy`** — runtime binaries (`bin/`, including `bin/omarchy-dev-*`),
install/finalize scripts (`install/`), migrations, themes, and the
@@ -22,24 +22,19 @@ separate `omarchy-pkgs` repository, under `pkgbuilds/`):
(`omarchy-debug`, `omarchy-debug-idle`, `omarchy-upload-log`) needed by
the live ISO env.
Two other packages live in `omarchy-pkgs` but stand alone:
Two other packages live in `omarchy-pkgs/` but stand alone:
`omarchy-keyring` (GPG keys for pacman) and `omarchy-nvim` (the Neovim
setup; independently seeds `/etc/skel`).
Some trees ship in neither package and exist only in the repo: `manual/`
(user manual chapters), `agents/skills/` (contributor task guides), `docs/`,
`test/`, and `plans/`.
Three layers populate `$HOME`:
1. **Seed** — `omarchy-settings` ships static defaults to `/etc/skel/`.
Arch's `useradd -m` copies that tree into a new user's `$HOME` at user
creation. This is the only mechanism that touches a brand-new user's home
for these files.
2. **Finalize** — `omarchy-provision-user` (routed as `omarchy finalize
user`) runs once per user and handles the things `/etc/skel` can't do
because they need `$HOME` expansion, the live `$OMARCHY_PATH`, or runtime
detection of system state.
2. **Finalize** — `omarchy-finalize-user` runs once per user and handles the
things `/etc/skel` can't do because they need `$HOME` expansion, the live
`$OMARCHY_PATH`, or runtime detection of system state.
3. **Resync** — `omarchy-reinstall-configs` is the explicit, destructive
command for an existing user to clobber their configs back to shipped
defaults.
@@ -47,13 +42,6 @@ Three layers populate `$HOME`:
`/etc/skel` only fires at user creation. Existing users picking up new
defaults must use the resync command.
Deferred-provisioning installs (`omarchy-apply-system --defer-provisioning`)
create no user at all: the ISO leaves `/var/lib/omarchy/provisioning/pending`
behind, which arms `omarchy-provision-owner.service` (shipped from
`install/provisioning/`, alongside the factory-reset finish unit and
`setup-form.sh`). On first boot `bin/omarchy-provision-owner` creates the
user on tty1 and runs the finalize step itself.
Current generated theme state lives under
`~/.local/state/omarchy/current/`. Keep `~/.config/omarchy/` for files a user
may intentionally version in a dotfile manager, such as user themes, hooks,
@@ -94,12 +82,7 @@ applications/icons/* ──► omarchy-settings /usr/share/icons/h
etc/** ──► omarchy-settings /etc/** (drop-ins we own outright)
├─ mkinitcpio.conf.d/{omarchy_hooks,thunderbolt_module}.conf
├─ limine-entry-tool.d/{omarchy-defaults,omarchy-uki}.conf
├─ NetworkManager/, sudoers.d/, sysctl.d/, tmpfiles.d/,
│ profile.d/omarchy.sh, … (a summary — `ls etc/` for the full ~17-entry tree)
└─ security/faillock.conf, nsswitch.conf,
cups/cups-browsed.conf, plymouth/plymouthd.conf /usr/share/omarchy/etc-overrides/
→ /etc/* (post_install cp -f, see below)
└─ limine-entry-tool.d/{omarchy-defaults,omarchy-uki}.conf
default/limine/limine.conf ──► omarchy-settings /usr/share/omarchy/default/limine/limine.conf
default/limine/default.conf ──► omarchy-settings /usr/share/omarchy/default/limine/default.conf
@@ -112,8 +95,7 @@ default/** ──► omarchy-settings /usr/share/omarchy
│ (sourced by every shell/session entry point; see "Env bootstrap")
├─ bashrc /usr/share/omarchy/etc-overrides/dot.bashrc
│ → /etc/skel/.bashrc (post_install cp -f)
├─ hypr/toggles/*.lua (flags,
│ single-window-aspect-ratio, window-no-gaps) /etc/skel/.local/state/omarchy/toggles/hypr/
├─ hypr/toggles/flags.lua /etc/skel/.local/state/omarchy/toggles/hypr/
├─ nautilus-python/extensions/*.py /etc/skel/.local/share/nautilus-python/extensions/
├─ tensaku/state.toml /etc/skel/.local/state/tensaku/state.toml
├─ uwsm/env.d/10-omarchy /usr/share/uwsm/env.d/
@@ -122,16 +104,18 @@ default/** ──► omarchy-settings /usr/share/omarchy
│ + symlink /etc/fonts/conf.d/50-omarchy.conf
├─ xdg-terminal-exec/*.list /usr/share/xdg-terminal-exec/
├─ applications/mimeapps.list /usr/share/applications/mimeapps.list
├─ systemd/user/*.service /usr/lib/systemd/user/
├─ systemd/user/*.{service,path} /usr/lib/systemd/user/
├─ systemd/user/app.slice.d/10-oomd.conf /usr/lib/systemd/user/app.slice.d/
├─ systemd/system-sleep/{force-igpu,
│ keyboard-backlight,unmount-fuse} /usr/lib/systemd/system-sleep/
├─ systemd/system-sleep/unmount-fuse /usr/lib/systemd/system-sleep/
├─ systemd/zram-generator.conf.d/90-omarchy.conf /usr/lib/systemd/zram-generator.conf.d/
├─ fonts/omarchy/omarchy.ttf /usr/share/fonts/omarchy/
├─ sddm/omarchy/ /usr/share/sddm/themes/omarchy/
├─ sddm/hyprland.lua /usr/share/sddm/hyprland.lua
├─ wayland-sessions/omarchy.desktop /usr/local/share/wayland-sessions/
└─ plymouth/ /usr/share/plymouth/themes/omarchy/
├─ plymouth/ /usr/share/plymouth/themes/omarchy/
└─ security/faillock, nsswitch, cups-browsed,
plymouthd.conf, os-release /usr/share/omarchy/etc-overrides/
→ /etc/* (post_install cp -f, see below)
logo.{txt,svg}, icon.{txt,png} ──► omarchy-settings /usr/share/omarchy/ (resync source)
/usr/share/pixmaps/omarchy.png
@@ -142,10 +126,9 @@ logo.{txt,svg}, icon.{txt,png} ──► omarchy-settings /usr/share/omarchy
### Why `etc-overrides/` exists
Some files under `/etc/` (`.bashrc` in `/etc/skel`, `nsswitch.conf`,
`security/faillock.conf`, `cups/cups-browsed.conf`, `plymouth/plymouthd.conf`)
are owned by upstream Arch packages, so we can't install over them via pacman
without a file conflict. Instead their sources (under `etc/` in the repo;
`.bashrc` from `default/bashrc`) ship at
`security/faillock.conf`, `cups/cups-browsed.conf`, `plymouth/plymouthd.conf`,
`os-release`) are owned by upstream Arch packages, so we can't install over
them via pacman without a file conflict. Instead they ship at
`/usr/share/omarchy/etc-overrides/` and the `omarchy-settings` `post_install`
/ `post_upgrade` scriptlet `cp -f`'s them into place.
@@ -163,10 +146,6 @@ Single source of truth for `OMARCHY_PATH` and dev-link-aware `PATH`. It:
- Prepends `$OMARCHY_PATH/bin` to `PATH` **only when** `OMARCHY_PATH` is
not `/usr/share/omarchy`. On a production install the binaries are
already on `PATH` as `/usr/bin/omarchy-*` via the `omarchy` package.
- Appends `~/.local/share/mise/shims` and `~/.local/bin` so login shells and
the uwsm session find mise-managed tools — kept in sync with the PAM `PATH`
line written by `install/config/ssh-command-path.sh`, which covers SSH
commands that run no shell setup at all.
Sourced by every entry point that needs the env set:
@@ -192,17 +171,15 @@ yet and silently runs the packaged copy of one it has. The drop-in is validated
with `visudo -c` before install and removed by `omarchy-dev-unlink`; unlike
`/etc/omarchy.conf`, it takes effect without a reboot.
## Runtime finalization (`omarchy-provision-user`)
## Runtime finalization (`omarchy-finalize-user`)
Runs once per user. It does **not** copy `~/.config/**`, `~/.bashrc`,
`flags.lua`, or the nautilus extensions — `/etc/skel` already seeded those.
It only does the things `/etc/skel` can't:
- Skill symlinks `~/.{agents,claude,codex,pi/agent}/skills/<name>` →
`$OMARCHY_PATH/default/agents/skills/<name>`, looping over every skill
directory there (currently `omarchy` and `diagnose-crash`) so new skills
need no edit. Symlinks (not copies) so `omarchy dev link` against a dev
checkout repoints them correctly.
- Skill symlinks `~/.{agents,claude,codex,pi/agent}/skills/omarchy` →
`$OMARCHY_PATH/default/agents/skills/omarchy`. Symlinks (not copies) so
`omarchy dev link` against a dev checkout repoints them correctly.
- `xdg-user-dirs-update` (Templates/Public/Desktop folded back into `$HOME`)
and `~/.config/gtk-3.0/bookmarks` (needs `$HOME` expansion).
- Hyprland's package-owned default input reads `XKBLAYOUT` / `XKBVARIANT`
@@ -210,23 +187,21 @@ It only does the things `/etc/skel` can't:
- `xdg-settings set default-web-browser chromium.desktop` and
`xdg-mime default HEY.desktop x-scheme-handler/mailto` (XDG-aware paths).
- `omarchy-refresh-applications` (composes generated `.desktop` launchers).
- Sources `install/user/all.sh` — theme, chromium, git, xcompose, mise,
keyring, per-user hardware quirks (asus mic/mixer, framework f13 audio, …).
- Sources `install/user/all.sh` — theme, git, mise, keyring, per-user
hardware quirks (asus mic/mixer, framework f13 audio, …).
- On `--first-install`, marks every shipped user migration as already applied
for the freshly-created user.
Idempotency marker: `~/.local/state/omarchy/done/finalize-user`, managed
by `omarchy-done`.
The ISO calls it as `omarchy-provision-user --force --first-install` in the
The ISO calls it as `omarchy-finalize-user --force --first-install` in the
target chroot as the install user, after `omarchy-apply-system` has finished
the root-side work. `omarchy-provision-owner` makes the same call (with
`OMARCHY_SETUP_CONTEXT=provision-owner`) when it creates the user during
deferred first-boot provisioning.
the root-side work.
## Migrations (`omarchy-migrate`)
See [`migrations.md`](../agents/skills/migrations.md) for the full migration model, authoring
See [`migrations.md`](migrations.md) for the full migration model, authoring
guidelines, and troubleshooting notes.
Omarchy migrations live in `migrations/*.sh` and run per-user through
@@ -238,10 +213,9 @@ machine-wide repairs should no-op when another user already applied them.
Each graphical user has `omarchy-migrate-notify.service`, started once per login
through `WantedBy=graphical-session.target` and ordered after that target so
notification actions can safely launch through UWSM. The `omarchy-pkgs`
PKGBUILD has shipped `omarchy-update-user-notify.service` as a symlink onto
it, so users enabled under the old unit name keep working before they reach
migration `1785095882`.
notification actions can safely launch through UWSM. The package also ships
`omarchy-update-user-notify.service` as a symlink onto it, so users enabled
under the old unit name keep working before they reach migration `1785095882`.
It runs `omarchy-migrate-notify` as
that user, which checks `omarchy-migrate --pending`. If this user has missing
migration state, it shows a notification that opens a terminal for
@@ -260,31 +234,27 @@ transaction in the already-visible update terminal, then runs
## First-run (`omarchy-provision-first-run`)
Runs once on first interactive login, after the user manager is live. It
first runs `omarchy-provision-user || true` so finalize catches up if it
never ran, then handles the steps that need a running graphical session
and/or a working user systemd instance:
Runs once on first interactive login, after the user manager is live. Used
for steps that need a running graphical session and/or a working user
systemd instance:
- `omarchy-hook-install post-update` for the three shipped hooks
(`install-voxtype.hook`, `setup-fingerprint.hook`, `setup-agent.hook`).
- `install/user/first-run/enable-user-units.sh` — daemon-reload, then
`systemctl --user enable --now` the shipped user units (`bt-agent`,
`omarchy-sleep-lock`, `omarchy-recover-internal-monitor`,
`omarchy-migrate-notify.service`, `omarchy-fcitx5.service`,
`omarchy-crash-watch.service`) so they run in the first session too.
- `omarchy-hook-install post-update install-voxtype.hook` — register the
Voxtype post-update hook.
- `install/user/first-run/enable-user-units.sh` — `systemctl --user enable`
the shipped user units (`bt-agent`, `omarchy-sleep-lock`,
`omarchy-recover-internal-monitor`, `omarchy-migrate-notify.service`,
`omarchy-fcitx5.service`).
Done here, not at finalize, because
the user manager isn't reachable from the ISO chroot; `ConditionPath*`
in the unit files keeps services inert when they don't apply.
- `install/user/first-run/gnome-theme.sh`,
`install/user/first-run/gtk-primary-paste.sh` — GNOME/GTK settings that
need the dconf daemon.
- `install/user/first-run/audio-tuning.sh` — apply speaker tuning.
- `install/user/first-run/welcome.sh` — keybindings toast that greets the
first login and opens the cheatsheet when clicked. The caller runs
`omarchy-notification-wait` once before this and the Wi-Fi step, so both
toasts land on a live notification server.
- `install/user/first-run/wifi.sh` — Wi-Fi/update toasts (waits detached on
`nm-online` so the update prompt only lands once there is a connection).
first login and opens the cheatsheet when clicked.
- `install/user/first-run/wifi.sh` — Wi-Fi/update toasts (waits for a live
notification server before firing, then waits detached on `nm-online` so the
update prompt only lands once there is a connection).
The entire sequence has one idempotency marker:
`~/.local/state/omarchy/done/first-run-user`, managed by `omarchy-done`.
@@ -304,8 +274,8 @@ the legacy finalization marker from `~/.local/state/omarchy/` into `done/`.
finalization. It sources:
- `install/config/all.sh` — theme links, lockout limits, lockscreen PAM,
powerprofilesctl shebang fix, SSH command path and keepalive, docker setup,
Snapper retention, locate index tuning, service enablement, firewall.
powerprofilesctl shebang fix, docker setup, Snapper retention, locate
index tuning, service enablement, firewall.
- `install/hardware/all.sh` via `omarchy-apply-hardware` — vendor- and
device-specific kernel modules, udev rules, microcode, wireless regdom,
ASUS / Framework / Intel / Apple / Lenovo quirks.
@@ -315,10 +285,6 @@ finalization. It sources:
Logging goes to `/var/log/omarchy-install.log` via
`install/helpers/logging.sh`.
The package lists the ISO pacstraps live at `install/omarchy-base.packages`
and `install/omarchy-other.packages`; the ISO builder also reads them when
constructing its offline mirror.
## Explicit resync (`omarchy-reinstall-configs`)
When an existing user wants to reset to shipped defaults:
@@ -344,10 +310,10 @@ return to the packaged default.
| --- | --- |
| Default file at `~/.config/foo/` | `config/foo/` |
| `/etc/` drop-in we own outright | `etc/` |
| `/etc/` file owned by an upstream package | `etc/` (see `etc/security/faillock.conf`), then add to `etc-overrides` in `omarchy-settings` PKGBUILD + scriptlet |
| Package-owned system file (e.g. systemd user service in `/usr/lib`) | `default/`, then add the `install -Dm644` line in `omarchy-settings` PKGBUILD |
| `/etc/` file owned by an upstream package | `default/`, then add to `etc-overrides` in `omarchy-settings` PKGBUILD + scriptlet |
| Package-owned system file (e.g. systemd user service/path in `/usr/lib`) | `default/`, document the mapping in `default/package-defaults.tsv`, then add the `install -Dm644` line in `omarchy-settings` PKGBUILD |
| Per-user file that's static but lives outside `~/.config` | `default/`, then add `install -Dm644 ... $pkgdir/etc/skel/...` in `omarchy-settings` PKGBUILD |
| Runtime tweak that needs `$HOME` or live system state | extend `omarchy-provision-user`, or add a per-user leaf under `install/user/` and wire into `install/user/all.sh` |
| Runtime tweak that needs `$HOME` or live system state | extend `omarchy-finalize-user`, or add a per-user leaf under `install/user/` and wire into `install/user/all.sh` |
| One-time root-side setup step | `install/config/*.sh` or `install/hardware/*.sh`, wire into `install/config/all.sh` or `install/hardware/all.sh` |
| One-time fix for existing installs | `migrations/<unix-timestamp>.sh` |
| Package-owned path something else may already write | Prefer a path nothing else writes, such as a vendor drop-in under `/usr/lib`. Otherwise the `--overwrite` entry in `bin/omarchy-update-system-pkgs` has to ship a release before the file |
-167
View File
@@ -1,167 +0,0 @@
# The Omarchy menu
The menu is the `omarchy.menu` plugin of the Quickshell desktop, with its
content defined as data in `default/omarchy/omarchy-menu.jsonc` (read at
runtime from `$OMARCHY_PATH`) and overlaid by the user's
`~/.config/omarchy/extensions/omarchy-menu.jsonc`. The shell parses both files
at startup and watches them for changes, so the keybind → IPC → visible path
never shells out to parse anything, and edits to either file take effect
without restarting the shell. Rendering and behavior live in
`shell/plugins/menu/Menu.qml`; the pure logic lives in
`shell/plugins/menu/MenuModel.js`, which is plain JavaScript that Node can
also load — the shell tests in `test/shell.d/menu-test.sh` and
`menu-guards-test.sh` exercise it directly.
JSONC here means JSON plus comments and trailing commas, stripped by the
parser rather than a real JSONC grammar: only whole-line `//` comments are
removed, so an inline trailing comment breaks the parse. A file that fails to
parse contributes no entries — a broken user extension silently drops every
user entry while the shipped menu keeps working.
## Entry schema
Entries are object keys. The dotted id is the tree: `trigger.share.file` is a
child of `trigger.share`, and an id with no dot sits on the root menu. There
is no separate parent field to keep in sync — where an entry appears follows
from what it is called (an explicit `parent` is accepted but nothing shipped
uses one).
Kind is inferred rather than declared: an entry with `action` is an action,
one with `target` is a link to another submenu, and anything else is a
submenu. The fields:
| Field | Meaning |
|---|---|
| `icon` | Glyph in the icon column (usually Nerd Font) |
| `iconFont` | Font family for the glyph when it differs from the menu font — how the private `omarchy` font's brand glyphs render |
| `label` | Visible row title; defaults to the id |
| `title` | Header text when the submenu is open; defaults to `label`. Lets a row read "Browser" under Defaults while the open menu says "Default Browser" |
| `action` | Shell command to run, detached, when selected |
| `target` | Existing submenu id to open; makes the row a link |
| `provider` | Runtime row source for this submenu (see Providers) |
| `aliases` | Alternate `omarchy menu summon <name>` routes; also searchable |
| `description` | Subtitle shown while searching, and extra search text matched by whole word |
| `when` / `checked` / `disabled` | Shell conditions (see Guards) |
Do not add `aliases` to new entries. They are reserved for established
alternate names users already type (`power-menu`, `settings`), kept for
compatibility — see `AGENTS.md`. Search does not need them: labels, the last
id segment, and descriptions are all searchable.
## Load and merge
`mergeMenuSources` overlays user entries on the defaults per key: reusing a
shipped id replaces only the fields you declare, so an extension can retitle
or re-icon a row without re-declaring its action, and an overridden entry
keeps its original position in the list. New ids append. A `root` entry is
injected if neither file declares one.
The sample extension at `config/omarchy/extensions/omarchy-menu.jsonc`
(refreshed into `~/.config/`) documents the format in its header and ships
only comments, so the default state adds nothing.
## Guards
`when`, `checked`, and `disabled` are bash conditions. The shell never
evaluates them on the open path: all guards in the menu are batched into a
single bash process per (re)load and per open, reporting `<id>:<w|c|d>:<0|1>`
lines. The menu opens immediately on the previous evaluation's answers, so
the batch's runtime is exactly how long a row can contradict the state it
describes — which is why the batch works hard to be fast:
- Package and command presence (`omarchy-pkg-present` and friends) are
answered in-process from one `pacman -Q` snapshot instead of a fork per
row. The snapshot resolves provides too, so gvim answers for vim.
- Commands that several rows read a value from — every Defaults > Browser row
compares against `$(omarchy-default-browser)` — run once, with the captured
answer substituted into each expression. The reader list is
`GUARD_READERS` in `MenuModel.js`; a new `$(omarchy-...)` reader used by
more than one row must be added there, and `menu-guards-test.sh` fails the
build if it is not.
The three guards differ in what failure means:
- `when` hides the row when it fails. A submenu whose visible descendants
are all hidden disappears with them (provider-backed submenus stay, since
their rows load on demand).
- `checked` appends ✓ when it succeeds — the "this is the current choice"
marker on defaults, DNS, channel rows.
- `disabled` keeps the row listed but dims it, marks it ✓, and makes it
unselectable: cursor, pointer, and Enter all step over it, and search omits
it. The Install submenus use it so software already on the machine reads as
installed rather than vanishing from the list it was installed from — the
list stays a catalog of what Omarchy can install. Since a dimmed row means
"you already have this", it earns the same ✓ as `checked` does elsewhere.
Install rows should therefore carry `disabled:` with the presence check, not
`when:`; Remove rows are the opposite, hiding via `when:` what is not there
to remove. `menu-test.sh` enforces the Install side of this convention.
## Providers
A submenu with `provider: "name"` gets its rows at runtime instead of from
JSONC. The names are defined by the shell, not the menu file — an extension
can point a submenu at an existing provider but cannot declare a new one:
- `apps` is QML-native: rows come from the shared AppLibrary (desktop
entries), carrying image icons, launch feedback, and uninstall support like
the launcher. App rows are searchable by their desktop Keywords but never
routable, so an installed app cannot capture a menu route (htop ships
`Keywords=system;...`, and SUPER+ESCAPE must still open the system menu).
- `fonts` and `power-profiles` are bash one-liners in the `providers` map in
`Menu.qml`. The contract is one tab-delimited line per row:
`label\tvalue\tcurrent`. The row whose value equals `current` gets the ✓
icon, and selection runs the spec's `actionFor(value)`. Row ids are
`<menuId>.<slugify(value)>`, with a `-` appended on collision so two values
that slugify alike cannot silently drop a row.
A provider marked `volatile` re-runs every time its submenu is entered — a
font installed since the shell started shows up without a restart — but not
on search keystrokes, which would restart the same enumeration per key.
`swapProviderRows` in `MenuModel.js` merges the results: rows carry the id of
the submenu that produced them, so a provider that runs again drops its
previous batch without disturbing static children declared in JSONC. Both it
and the app merge return fresh item maps for the caller to assign in one go —
writing into a map held by a QML `var` property occasionally loses the write,
which used to duplicate launcher rows. Never mutate `root.items` in place.
Adding a provider means adding an entry to the `providers` map in `Menu.qml`
(script, icon, `actionFor`, optionally `volatile`) and pointing a submenu at
it with `provider:`.
## Driving the menu from the CLI
`bin/omarchy-menu` is a thin wrapper over the standard plugin IPC surface:
```bash
omarchy menu # toggle the root menu
omarchy menu toggle system # open at a route, or close if already open
omarchy menu summon style.theme # always open (no close-if-visible)
omarchy menu close
omarchy menu refresh # re-parse the JSONC files
omarchy menu ping
```
A route is an item id or a declared alias, case-insensitive, with
underscores normalized to dashes. An exact id beats any alias; empty input,
`go`, and `menu` mean root; an unknown string falls through as a literal id
so a misspelling still attempts to open that id. Summoning a route that
resolves to an action — an alias for a leaf, like `screenrecord-stop` — runs
the action directly instead of opening an action with no children, and a
link is followed to its target. The default Hyprland bindings in
`default/hypr/bindings/utilities.lua` all go through this surface
(SUPER+SPACE toggles root, SUPER+ESCAPE the system menu, and so on).
## Select and input modes
The same plugin doubles as the system's dmenu. `omarchy-menu-select` and
`omarchy-menu-input` summon it with a `mode: select` or `mode: input`
payload, then block on a tempfile handshake: the shell writes the selection
to `selectionFile` and touches `doneFile`, and cancellation (empty
selection) exits 1. A select option is `label`, `glyph\tlabel`, or
`glyph\tlabel\tsubtext` — the glyph shows but never returns, the subtext
renders under the label, filters with it, and comes back as
`label\tsubtext` so callers with same-named rows get a stable key. This is
how the pickers behind menu actions (`omarchy-menu-plugin`,
`omarchy-menu-timezone`, ...) present lists without owning any UI.
@@ -1,7 +1,5 @@
# Omarchy migrations
Read this before creating or changing migrations under `migrations/`.
Omarchy migrations are one-time repair scripts for existing installs. They are
used when a package update needs to change state that pacman cannot safely own by
itself.
@@ -126,7 +124,6 @@ New migration format:
- Start with an `echo` describing what the migration does.
- Use `$OMARCHY_PATH` to reference the Omarchy directory.
- Be idempotent. Check existing state before changing it.
- Migrations are strictly ordered and synchronous. A migration that cannot finish must exit non-zero, remain pending, and stop the queue; never mark later migrations complete against state an earlier migration has not established.
- Use helper commands such as `omarchy-cmd-present`, `omarchy-cmd-missing`,
`omarchy-pkg-add`, `omarchy-pkg-drop`, `omarchy-pkg-present`, and
`omarchy-pkg-missing` when appropriate.
@@ -166,5 +163,3 @@ omarchy-migrate
Omarchy 4.0 is upgraded through `bin/omarchy-upgrade-to-quattro`, not through the
normal migration runner. Do not add compatibility migrations for old installer
layouts; put pre-4 package-layout transition work in the upgrade command instead.
Clearing a privileged file that a retired installer left on disk is the exception, and belongs in a migration whether or not that installer was part of a package layout transition. The upgrade command only runs on a machine still making the 3 to 4 crossing, so anything put there never reaches an install that crossed already, and it never runs at all for an installer that was retired on its own — while the file the installer wrote is still sitting on those machines. The upgrade command finishes by running `omarchy-migrate` (`run_post_upgrade_migrations`), so one migration reaches every population; a copy in the upgrade command would only be a second copy of the same predicate to keep correct. Such a migration must name the defect it clears and match what the old installer actually produced before deleting it. Leave safe administrator-authored files alone; if one still contains the vulnerable privileged action, preserve it under an inactive name rather than discarding custom content or leaving the action executable. A user config that depends on the same retired compatibility path may be repaired in that migration when doing so eliminates an overlapping migration, but only by matching and replacing the exact legacy path while preserving the rest of the file.
-199
View File
@@ -1,199 +0,0 @@
# Notifications
The shell is the notification daemon: `shell/plugins/notifications/Service.qml`
hosts a Quickshell `NotificationServer` that claims `org.freedesktop.Notifications`
on the session bus. There is no dunst or mako — anything that speaks the
freedesktop notification protocol (notify-send, libnotify apps, Chromium web
apps) lands in the shell, which renders it as a toast card stacked in the
top-right corner. The pure decision logic lives in `NotificationLogic.js`,
which is also loadable from Node so `test/shell.d/` can exercise it without
a compositor.
The end-user view (hotkey notices for time, battery, weather) is in
`manual/10-notices.md`; this document is the system shape behind it.
## Toast lifecycle
A toast lives on screen for at least 5s (low), 8s (normal), or forever
(critical), stretched up to 30s if the sender asked for a longer
`expire_timeout`. Hovering pauses the countdown, and a content update restarts
it — new text deserves a full look. Left-click invokes the default action,
right-click or the hover-revealed close button dismisses.
Every on-screen popup is mirrored to its own file under
`~/.local/state/omarchy/notifications/` (one JSON line per file, named
`<timestamp>-<id>.json`), so live toasts survive the shell restart that
`omarchy-update` performs. When a toast leaves the screen — expiry, dismissal,
or click — its file moves into `notifications/history/`, trimmed to the newest
ten. That directory *is* the history: `showHistory` replays exactly what has
been moved in there. Referenced avatars/images are copied into
`notifications/images/`, because senders delete their originals on close.
`replaces_id` updates never produce a second notification signal: the server
writes new content onto the object the service already holds, so the service
watches the object's property-change signals and rewrites the row and its file
in place, under the popup's original file identity. Restored rows carry ids
from a dead server generation (ids restart from 1 each shell process), so
they are keyed by timestamp+id and never matched against live objects — a
fresh notification reusing an old id must not dismiss or replace them.
## Silencing
Do-not-disturb is a single boolean, persisted as the `dnd` key in
`~/.local/state/omarchy/notifications.json` and toggled via shell IPC
(`omarchy-shell notifications toggleDnd` / `setDnd` / `dndState`).
`omarchy-toggle-notification-silencing` wraps the toggle and refreshes the
bar's `omarchy.indicators` widget, whose Dnd indicator binds directly to the
service's `doNotDisturb` property.
Two kinds of notification punch through DND, chosen to be intentional and
rare:
- `app_name` = `omarchy-action` — Omarchy's own user-action confirmation
toasts ("Theme changed"). The user just did something; their feedback shows.
- urgency critical *and* `app_name` = `notify-send` — bare-CLI emergency
alerts. Critical alone is not enough, because chat apps abuse it to force
visibility; they set `app_name` to their brand, which fails this rule.
A silenced notification that anyone might look back at is written straight
into history — "what did I miss while silenced" is what history is for.
Ephemeral ones (the freedesktop `transient` hint, or an `app_name` of
`notify-send`/`omarchy-action`) are dropped entirely.
## The sender contract
`bin/omarchy-notification-send` is the one way Omarchy code sends
notifications — never raw `notify-send`. It calls
`org.freedesktop.Notifications.Notify` directly over the session bus (via
`busctl --user`), so each value is one typed D-Bus parameter and there is no
argv layer that could reinterpret a relayed headline as an option or a hint. Its
flags map onto that call:
| Flag | Becomes | Meaning |
|---|---|---|
| `-g` / `--glyph` | hint `omarchy-glyph` | Nerd Font glyph for the icon slot when no image icon resolves |
| `--exec <program> [args…]` | hint `omarchy-exec-argv` | the click command; consumes the rest of the line, so it comes last. Each word is a discrete argument the shell runs without re-parsing (see below) |
| `--image` | hint `image-path` | the standard freedesktop image hint |
| `-i` / `--icon` | `app_icon` | themed icon name for the toast |
| `--app-name` | `app_name` | defaults to `omarchy-action` |
| `-u` / `--urgency` | hint `urgency` (byte) | `low`/`normal`/`critical`; defaults to `low` |
| `-t` / `--expire-time` | `expire_timeout` | milliseconds on screen; server default otherwise |
Unknown flags are a hard error, not a silent pass-through: `--exec` is the only
door to a click command, and there is no generic option pass-through to smuggle
one through.
The defaults are the point: an unadorned `omarchy-notification-send "Done"`
is a low-urgency user-action toast that pops through DND and is treated as
ephemeral noise when silenced.
The click command is deliberately not a libnotify action. An action keeps the
sender blocked waiting for `ActionInvoked`, and dies unanswered whenever the
shell restarts underneath it — the installer toasts restart the shell as their
first act. Carrying the command as a hint means the shell executes the click
itself (detached, so the command outlives the shell process) from the copy it
keeps with the popup, which the persistence files preserve: a restored toast
clicks through exactly like a live one, and oneshot senders can exit
immediately. For third-party clients the click falls back to the libnotify
`default` action while the sender is alive, then to focusing the sender's
window by class via `omarchy-hyprland-focus-app` — chat apps rarely register
an action and just expect click-to-jump.
### Click commands are argv, never shell strings
`--exec` consumes the rest of the line as the click command:
```bash
omarchy-notification-send "Download complete" "$title" --exec mpv -- "$file"
```
The caller's shell has already split those words into discrete arguments, and a
quoted argument (`"$file"`) stays one argument even with spaces. On the shell
side they are run through `Util.execArgv`, which invokes `bash -lc 'exec "$@"'`
with the arguments as **positional parameters** — never interpolated into the
script text. bash expands `"$@"` without re-tokenizing or re-evaluating it, so a
value carrying data an attacker controls — a downloaded video's title, a
received filename, a crashed process's name — is only ever a single argument and
can never be reparsed as a command. The login shell keeps the PATH and session
environment that GUI click targets (the screenshot editor, mpv, xdg-open) expect.
The critical rule: **the splitting must happen at the call site, not inside the
tool.** Passing a single quoted string (`--exec "mpv $title"`) and letting the
tool whitespace-split it would hand argument boundaries to whoever controls the
string's content — a title with a space could inject an extra option or program.
So `--exec` refuses a lone quoted-string argument and points at the unquoted
form. There is no "take a command string and sanitize it" path; that is the
escaping trap (string-concatenated SQL) the yt-dlp title RCE exploited.
The shell fails closed on a malformed argv hint (it must be a JSON array of
strings whose program is present and not a leading-dash option) rather than
running anything it can't validate. A caller can still deliberately name a shell
as the program (`--exec sh -c …`), but that runs code because the *developer*
wrote it, not because attacker data became a command — a reviewable red flag
(greppable as `--exec sh`/`--exec bash`), not an injection. Insulating against a
native same-user process is out of scope: it already runs with your privileges
and needs no notification to execute code. What is fully closed is untrusted
*content* — web notifications can't set the exec hint at all, and any relayed
title/filename is confined to inert argument data.
The sender keeps that last part true structurally rather than leaving it to each
caller. Because it calls `Notify` directly, the headline and description are
typed string parameters — a relayed value like `--hint=…` or `-rf` is the
summary or body, never an option or a hint, and there is no argv/option layer
(no `notify-send`) left to reinterpret it. `--exec` is the only thing that can
build the `omarchy-exec-argv` hint. (The leading `--` on the `busctl` call is a
belt for `busctl`'s own getopt, which would otherwise read a dash-leading value
as a `busctl` option; the summary/body themselves are never parsed as options.)
## Helper commands
- `omarchy-notification-wait [timeout]` — polls until the shell answers IPC
*and* has claimed the bus name. Anything sending near session start or a
shell restart uses it, or the toast is sent into the void.
- `omarchy-notification-dismiss <summary>` — dismiss by summary substring,
used by the first-run toasts once their action has been clicked.
- `omarchy-notification-time` / `-battery` — the hotkey notices: one-line
low-urgency glyph toasts wrapping `date` and `omarchy-battery-status`.
- `omarchy-notification-weather` — despite the name, not a sender: it toggles
the `omarchy.weather` shell panel.
Keybindings live in `default/hypr/bindings/utilities.lua`: `Super+comma`
variants map to the IPC methods `dismissOne`, `dismissAll`, `invokeLast`,
`showHistory`, and the silencing toggle.
## How subsystems plug in
Everything goes through the same sender contract, so the pieces are small:
- **Low battery** — `omarchy-battery-low` sends a critical toast and runs the
`battery-low` hook.
- **Crash capture** — `omarchy-crash-watch` follows the systemd-coredump
journal stream and announces each crashed program (deduped per minute) as a
critical toast whose click runs `omarchy-agent-crash` (via `--exec`, so a
hostile process name stays a discrete argument). It waits for the
server first: a shell crash takes the notification server down with it, and
that crash is the one most worth reporting.
- **Pending migrations** — `omarchy-migrate-notify` (from its user service
after `graphical-session.target`) waits for the server, then sends a
critical toast whose click opens a terminal running `omarchy-migrate`,
falling back to printing in the terminal if the hand-off fails.
## Reminders
Reminders ride on notifications rather than being their own daemon.
`bin/omarchy-reminder <minutes> [message]` creates a transient systemd user
timer via `systemd-run --user --collect --on-active=<minutes>m` under the
unit name `omarchy-reminder-<minutes>m-<epoch>`; the timer's payload sends the
reminder toast, deletes its message file, and refreshes the bar indicator.
Custom messages are stashed in `$XDG_RUNTIME_DIR/omarchy-reminders/<unit>.message`
since a unit name cannot carry arbitrary text. `--collect` means fired timers
leave nothing behind.
The state therefore lives entirely in systemd: `show` and `clear` enumerate
`systemctl --user list-timers "omarchy-reminder-*.timer"` — `show` as a
summary toast, `show --json` as the JSON the bar's Reminder indicator polls
(refreshed by the same `omarchy-shell -q omarchy.indicators refresh` call the
timers and mutations make). `omarchy-reminder -i` summons the
`omarchy.reminders` overlay (`shell/plugins/reminders/ReminderFlow.qml`), a
two-step minutes/message prompt that shells back out to `omarchy-reminder` to
do the setting.
+23 -51
View File
@@ -2,12 +2,9 @@
A single long-running [Quickshell](https://quickshell.org/) instance
that hosts the Omarchy desktop. The bar, panels, overlays, menus, and
services all run inside as plugins. Hyprland autostart launches the
shell via `omarchy-launch-shell`; restart it with `omarchy-restart-shell`.
IPC is the canonical way for CLIs to talk to a running shell —
`omarchy-shell` forwards a call and fails when the shell is not running
(`-q` makes it quiet best-effort; `OMARCHY_SHELL_IPC_TIMEOUT` bounds the
wait).
services all run inside as plugins. IPC is the canonical way for CLIs
to talk to a running shell — `omarchy-shell-ipc` auto-starts it on
first call.
## Plugin manifest
@@ -37,14 +34,9 @@ wait).
Only one full bar option is active at a time. The built-in `omarchy.bar` is
used when `bar.id` is omitted or when a selected third-party bar cannot load.
Panels, overlays, and menus are loaded when summoned. Plugins can set the
top-level manifest key `keepLoaded: true` to survive between summons.
First-party services are loaded at startup.
Entry points are QML `Item`s. Panel, overlay, and menu entry points expose
`open(payloadJson)` and `close()` for summon/hide; on load the host injects
`omarchyPath`, `shell`, `manifest`, and the registries (`pluginRegistry` /
`barWidgetRegistry`) as properties.
Panels, overlays, and menus are loaded when summoned. Plugins can set
`keepLoaded: true` to survive between summons. First-party services are
loaded at startup.
Full schema: [`shell/services/PluginRegistry.qml`](../shell/services/PluginRegistry.qml).
@@ -84,25 +76,20 @@ widgets that omit it default to `center`.
Plugins run as **unsandboxed code** inside `omarchy-shell`. Adding warns you
before cloning, plugins land disabled so you can review the code before
`omarchy plugin enable`, and updates show a diff before touching anything.
Commands confirm in a terminal even when given arguments; without one they
refuse rather than guess. Add `--yes` to skip every prompt (the path for
scripts and agents).
Commands prompt when run bare in a terminal and run unattended when given
arguments — add `--yes` to skip every prompt (the path for scripts and agents).
You can still install by hand: drop a plugin into
`~/.config/omarchy/plugins/<id>/`, run `omarchy-shell shell rescanPlugins`, then
`omarchy plugin enable <id>`. A bar widget starts in its declared default
section; enabling a full bar replaces the one in use. `omarchy bar` drives the
bar from the CLI — `use | reset | defaults | position | transparent | put |
move | set`, with placement flags such as `--section` and `--index`.
section and can be moved with `omarchy bar move`; enabling a full bar
replaces the one in use.
The lower-level IPC methods remain available through `omarchy-shell shell ...`.
## IPC
The shell exposes a `shell` target (the host also registers
`image-selector`) plus targets registered by individual plugins, named
for the plugin rather than for where it appears: `background`, `osd`,
`media`, `notifications`, and per-widget targets such as `omarchy.clock`
or `omarchy.power`. There is no `bar` target.
The shell exposes a `shell` target plus extra targets registered by
individual plugins (`bar`, `image-selector`, …).
| Method | Effect |
|---------------------------------------|---------------------------------|
@@ -110,24 +97,17 @@ or `omarchy.power`. There is no `bar` target.
| `summon <id> <payloadJson>` | load + open a plugin |
| `hide <id>` | close a previously-summoned |
| `toggle <id> <payloadJson>` | summon if closed, hide if open |
| `togglePanelAt <section> <index>` | toggle the panel at a bar position |
| `call <id> <method> <arg>` | call an already-loaded plugin |
| `rescanPlugins` | re-walk plugin dirs and hot-reload plugin code |
| `reloadConfig` | reload shell.json |
| `applyTheme <colorsB64> <shellB64>` | push theme colors + shell.toml |
| `toggleBarTransparency` | flip the bar background between solid and transparent |
| `setPluginEnabled <id> <"true"\|…>` | flip enabled bit (`ok` / `unknown`) |
| `enablePlugin <id> <placementJson>` | enable and place in one mutation |
| `putBarWidget <id> <placementJson>` | place a widget only where absent (`omarchy bar put`) |
| `moveBarWidget <id> <placementJson>` | move a configured widget |
| `setBarWidget <id> <key> <valueJson> <selectorJson>` | set an inline widget option |
| `listPlugins` | JSON of every discovered plugin |
| `listShellConfig` | effective shell.json as JSON |
| `debugBarGeometry` | bar geometry dump for debugging |
`setPluginEnabled` takes a string; only literal `"true"` enables. Methods
answer on stdout with exit 0 — `ok` on success, `unknown` or an error
string on a miss.
`setPluginEnabled` takes a string; only literal `"true"` enables.
## shell.json
@@ -142,7 +122,8 @@ string on a miss.
"id": "omarchy.bar",
"position": "top",
"transparent": false,
"centerAnchor": "omarchy.clock",
"centerAnchor": "calendar",
"fontFamily": "JetBrainsMono Nerd Font",
"layout": {
"left": [ { "id": "omarchy.menu" } ],
"center": [ { "id": "omarchy.clock", "format": "HH:mm" } ],
@@ -165,9 +146,10 @@ Rules:
3. Settings are inline on the entry. No `config:` sub-object, no
merge layers.
4. Built-in bar widget ids are namespaced (`omarchy.clock`, `omarchy.audio`, …).
The migration rewrites older ids such as `Clock` and `AudioPanel` forward.
5. Third-party enabled ⇔ present; for full bar options that means `bar.id`.
First-party non-bar plugins are enabled unless listed in `disabledPlugins[]`.
6. `barWidget.allowMultiple: true` in the manifest permits multiple instances.
6. `allowMultiple: true` in the manifest permits multiple instances.
7. `idle.screensaver` and `idle.lock` are seconds since user idle began.
8. `version: 1` is required.
@@ -175,12 +157,6 @@ Rules:
user `shell.json` exists, defaults are used verbatim. Once the user
customizes, `shell.json` is canonical — there is no deep-merge.
`shell.json` is shell configuration; theme tokens live in `shell.toml`
(next section). Both are current — they answer different questions. A
machine-level `~/.config/omarchy/shell.toml` is watched live by the
shell and its keys win over the active theme's `shell.toml`, so
overrides like `omarchy display text size` survive theme switches.
## Theme tokens
See [`theming.md`](theming.md) for the full theme/template workflow,
@@ -189,23 +165,20 @@ including generated `*.tpl` files, gradient helpers, and shell border syntax.
Themes ship colors in `themes/<name>/colors.toml` and surface roles +
sizing in `themes/<name>/shell.toml`. Defaults are generated from
`default/themed/shell.toml.tpl`; a theme may also drop a hand-written
`shell.toml` next to its `colors.toml` to replace the generated file,
or override a single section with `shell.<section>.toml`, merged in by
`omarchy-theme-set-templates` (see [`theming.md`](theming.md)).
`shell.toml` next to its `colors.toml` to replace the generated file.
`colors.toml` uses `foreground` and `background` for the foundational
text/background palette, exposed to QML as `Color.foreground` and
`Color.background`.
The shell exposes these tokens to QML via three singletons in
The shell exposes these tokens to QML via two singletons in
`qs.Commons`:
- `Color` — palette (`foreground`, `background`, `accent`, `urgent`)
and per-surface roles (`Color.bar.*`, `Color.popups.*`,
`Color.tooltip.*`, `Color.notifications.*`, `Color.menu.*`,
`Color.polkit.*`, `Color.lock.*`, `Color.imagePicker.*`). Clipboard
and emojis share `Color.menu.*`; the `[launcher]` section is consumed
by the launcher outside shell QML.
`Color.launcher.*`, `Color.imagePicker.*`, `Color.polkit.*`,
`Color.lock.*`). Clipboard and emojis share `Color.menu.*`.
- `Style` — structural tokens (`cornerRadius`), shared interactive
state tokens/helpers, spacing (`Style.spacing.*` / `Style.space(px)`),
the type scale (`Style.font.*`), and bar dimensions
@@ -398,6 +371,5 @@ Then `~/.config/omarchy/bar/modules/gpu.qml` (or set `source` to point
elsewhere). The module is an `Item` and receives `bar`, `moduleName`,
`settings` properties. `bar` exposes `foreground` / `background` /
`urgent` / `fontFamily` / `position` / `vertical` / `barSize`, plus
`run(cmd)`, `showTooltip(t, s)` / `hideTooltip(t)`,
`requestPopout(o)` / `releasePopout(o)`. To shell-quote arguments for
`run`, use `Util.shellQuote(v)` from `qs.Commons`.
`run(cmd)`, `shellQuote(v)`, `showTooltip(t, s)` / `hideTooltip(t)`,
`requestPopout(o)` / `releasePopout(o)`.
-138
View File
@@ -1,138 +0,0 @@
# Testing
How the non-graphical test suites are organized: what each runner owns, the
protocol test files speak, and the conventions that keep them runnable on any
machine — including headless CI sandboxes with no compositor. The graphical
acceptance suite is a separate thing that drives a live session in a disposable
VM; see [`agents/skills/acceptance-tests.md`](../agents/skills/acceptance-tests.md).
## Suite map
`./test/all` runs both suites below and keeps going when one fails, so a single
failure cannot hide the other suite behind it. It reports the failed suites at
the end and exits non-zero.
- **`./test/cli`** — one big script, one suite. It owns the CLI router: help
and group rendering, route resolution, aliases, hidden commands, and the
guarantee that a trailing `--help` never executes the target. It also owns
the metadata lint — every `omarchy-*` executable under `bin/` is checked for a
`# omarchy:summary=` header and against removed or redundant fields — plus
the theme pipeline: template rendering (`omarchy-theme-set-templates`,
`omarchy-theme-color`, `omarchy-theme-osc`), the theme sync commands
(tmux, GNOME, VS Code, Pi, Claude) run against stub binaries and a fake
`$HOME`, and the theme-state migrations.
- **`./test/shell`** — runs every `test/shell.d/*-test.sh` (except
`base-test.sh` itself). Each file is an independent suite covering one area:
a shell plugin, a `bin/` command, a config invariant, a migration. This is
where new tests go.
- **Acceptance** — everything that needs a real desktop doing real things.
Deliberately excluded from `./test/all`; it runs in a VM, not the
development session.
A new shell test only needs the right name: drop `<area>-test.sh` into
`test/shell.d/` and `./test/shell` picks it up automatically. Shared fixtures
live under `test/shell.d/fixtures/`.
## The base-test.sh contract
Every shell test starts the same way:
```bash
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
```
`base-test.sh` refuses to be executed directly — it is a library. It discovers
the repo root from its own location and exports it as `ROOT`, so tests
reference files as `$ROOT/bin/...` and never depend on the caller's working
directory or an installed Omarchy.
Assertions are TAP-flavored and blunt:
- `pass "description"` prints `ok - description`.
- `fail "description" [detail]` prints the optional detail and
`not ok - description` to stderr, then **exits the file**. There is no
counting or continuing within a file: the first failed assertion ends it,
which keeps later assertions from reporting against state the failure
already invalidated.
- `require_command <cmd>` fails the file when a needed tool is absent.
The runner compensates for that early exit: `./test/shell` continues past a
failing file and summarizes the failures at the end. Aborting the whole run at
the first bad file once let a single packaging failure mask 114 of 134 files.
Failure granularity is therefore per file inside a run, per assertion inside a
file.
## Compositor-dependent tests
Some tests launch Quickshell or query Hyprland, but the suite must stay green
on headless machines. `require_compositor "description"` handles this: when no
compositor answers it prints `ok - no Wayland compositor; skipping ...` and
exits 0 — a skip is a passing test — and otherwise returns so the file
proceeds.
The probe is more than an environment check, because `WAYLAND_DISPLAY` only
proves the variable was inherited. Sandboxes pass the environment through
while blocking `$XDG_RUNTIME_DIR`, so Quickshell clears a bare variable check
and then aborts inside QGuiApplication — a core dump per launch where a skip
belonged. So `compositor_reachable` checks the socket actually exists, then
asks Hyprland itself (`hyprctl -j monitors`, retried, and only when
`HYPRLAND_INSTANCE_SIGNATURE` makes it askable), since a compositor that died
mid-session leaves its socket behind. When the compositor is reachable,
`require_compositor` also sets `ulimit -c 0`: Quickshell leaves through
`qFatal()` if its connection drops mid-run, and the test should fail without
writing a core dump as debris.
Gate only what needs gating — put `require_compositor` in files whose runtime
half needs a live session, and keep static analysis of the same area in code
that runs unconditionally before or beside it.
## Unit-testing shell JavaScript from bash
The Quickshell plugins keep their logic in plain `.js` modules
(`shell/plugins/menu/MenuModel.js`, `bar/BarModel.js`, ...) that end in a
guarded `if (typeof module !== "undefined") module.exports = {...}` block. QML
imports them directly and ignores the guard; Node loads them as CommonJS. That
dual citizenship is what makes the shell's model logic unit-testable without a
compositor.
`run_node_test` is the bridge: it prepends a JS prelude to a heredoc and pipes
the result into `node`. The prelude mirrors the bash assertion protocol
(`pass`, `fail`, `assert`, `assertEqual`, `assertDeepEqual` — same
`ok`/`not ok` lines, same exit-on-first-failure) and provides `root` (from the
exported `ROOT`), `path`, and `requireFromRoot(relativePath)`:
```bash
run_node_test <<'JS'
const menu = requireFromRoot('shell/plugins/menu/MenuModel.js')
const parsed = menu.parseMenuJsonc('{ "items": { "root": { "label": "Go" }, }, }')
assertEqual(parsed.length, 1, 'menu parses JSONC with trailing commas')
JS
```
Roughly a quarter of the shell test files use this to test parsing, merging,
and layout logic as pure functions, reserving compositor-gated tests for what
only a live session can prove.
## Conventions worth copying
- **Stub the world, run the real code.** Tests build a scratch `bin/` of stub
executables (`sudo`, `tmux`, `gsettings`, helper commands) that log their
arguments to a file, prepend it to `PATH`, and then run the real script
under test. Assertions grep the call log and the files the script wrote.
- **Fake `$HOME`, real `$OMARCHY_PATH`.** Anything touching user state runs
with `HOME` pointed at a `mktemp -d` directory (cleaned up via
`trap ... EXIT`) and `OMARCHY_PATH="$ROOT"`, so tests exercise the checkout
without touching the developer's machine.
- **Migrations run directly.** A migration test builds the legacy state in a
fake `$HOME`, runs `bash -euo pipefail "$ROOT/migrations/<ts>.sh"`, and
asserts the resulting state — including running it twice to prove
idempotence, and once against non-legacy state to prove it leaves user
customization alone.
- **Assert the invariant, not the snapshot.** Config tests pin the property a
test is named for (this widget stays adjacent to that one) rather than whole
structures, so unrelated churn does not fail them.
+5 -7
View File
@@ -101,8 +101,7 @@ shell palette is loaded from:
`color4`
- `muted` — de-emphasized elements (comments, placeholders, dividers); also
serves as ANSI `color8`
- `red` / `color1` — populate the shell's urgent role; there is no `urgent`
palette key (one defined in `colors.toml` is ignored)
- `urgent` / `red` / `color1`
Themes and user templates using the legacy short names remain supported.
Canonical names take precedence when both forms are defined, and resolved
@@ -342,11 +341,10 @@ BorderSurface {
}
```
Use `Border.surfaceSpec(section, token, fallbackColor, fallbackWidth, alphaKey)`
for shell theme tokens (the optional `alphaKey` names the alpha token, e.g.
`"border-alpha"`), `Border.controlSpec(state, foreground, accent, urgent)` for
shared controls, and `Border.flat(color, width)` for a deliberate local border
that should not be overridden by the active theme. `Color.<section>.border` is the
Use `Border.surfaceSpec(section, token, fallbackColor, fallbackWidth)` for
shell theme tokens, `Border.controlSpec(state, foreground, accent)` for shared
controls, and `Border.flat(color, width)` for a deliberate local border that
should not be overridden by the active theme. `Color.<section>.border` is the
flat first-stop color for consumers that cannot render full border specs.
## Hyprland templates
+6 -31
View File
@@ -30,7 +30,7 @@ The design goal is:
## Migration layout
See [`migrations.md`](../agents/skills/migrations.md) for the full migration model, authoring
See [`migrations.md`](migrations.md) for the full migration model, authoring
guidelines, and troubleshooting notes.
Migrations live in:
@@ -82,7 +82,7 @@ Omarchy update command, the hook exits non-zero with `AbortOnFail`, which stops
the transaction before packages are changed.
`omarchy-update-system-pkgs`, `omarchy-refresh-pacman`, `omarchy-reinstall-pkgs`,
`omarchy-channel-set`, and the v4 upgrader run pacman through:
and the v4 upgrader run pacman through:
```bash
env OMARCHY_UPDATE_PACMAN=1 pacman ...
@@ -116,14 +116,9 @@ omarchy-update
├─ omarchy-update-lock
│ └─ acquire the update lock and run omarchy-update inside it
├─ omarchy-update-requires-free-space
│ └─ abort below the configured free-space threshold on /
│ └─ check free space on / and warn below the configured threshold
├─ confirm unless -y
├─ omarchy-update-pkg-prune
│ └─ trim the pacman cache to two versions per package, deliberately
│ before the snapshot since the cache lives on the snapshotted subvolume
├─ create snapper snapshot (skipped silently without snapper; snapper
│ installed but unconfigured fails the snapshot loudly, pointing at
│ install/config/snapper.sh, and the update continues without one)
├─ create snapper snapshot, if snapper is installed
├─ omarchy-update-stay-awake start
├─ run package updates, migrations, hooks, and log analysis
├─ omarchy-update-status
@@ -137,9 +132,6 @@ Important behavior:
- In dev-link mode, `omarchy update` fast-forwards the active checkout from its
configured upstream before changing system packages or running migrations.
- `-y` exports `OMARCHY_UPDATE_UNATTENDED=1` — a promise not to ask anything.
Steps that would prompt (orphan removal, conflict handoff) report and skip
instead of blocking.
- The free-space requirement uses a 10 GiB threshold and stops the update before
confirmation when it is not met. If free space cannot be determined, the
check is silently skipped. Set `OMARCHY_UPDATE_FORCE=1` to bypass the check.
@@ -243,20 +235,6 @@ Exit codes:
The widget runs this check on shell startup and every six hours. Clicking the
update icon launches `omarchy-update` in a floating terminal.
## Channels and versions
Updates install whatever the active channel points at. `omarchy-channel-set
<stable|rc|edge|dev>` switches channels: the three package channels select
which pacman repo the mirrorlist points at (and swap between the `omarchy` and
`omarchy-dev` packages through a guard-allowed pacman run), while `dev` links
the runtime to a git checkout via the dev-link mechanism, after which
`omarchy update` fast-forwards that checkout instead of upgrading a package.
There is no version file at runtime. `omarchy-version` derives the version from
`pacman -Q` on whichever package is installed, or reports `dev (<hash>)` for a
linked checkout, and `omarchy-version-channel` sniffs the mirrorlist and
pacman.conf to answer which channel is active.
## Update-related binaries
This inventory is intentionally opinionated. Some commands are useful as stable
@@ -272,17 +250,14 @@ scripts.
| `omarchy-update-confirm` | Gum confirmation copy for `omarchy update`. | **Question.** Could be inlined into `omarchy-update`; separate file only helps keep copy isolated. |
| `omarchy-update-dev` | Fast-forwards the active dev-linked checkout from its configured upstream; no-ops for package-backed installs. | **Keep.** Runs before package updates so a checkout conflict stops the update before system mutation. |
| `omarchy-update-keyring` | Ensures Omarchy keyring and Arch keyring are current before the main transaction. | **Keep, but review.** It uses targeted `pacman -Sy` for keyring bootstrapping; acceptable for this special case but should remain tightly scoped. |
| `omarchy-update-system-pkgs` | Runs `sudo env OMARCHY_UPDATE_PACMAN=1 pacman -Syu --noconfirm` with `--overwrite '/usr/share/omarchy/*'`, capturing stderr to a report file; on failure it execs `omarchy-update-system-pkgs-when-conflicted`. | **Keep for now.** Small leaf command, clear/testable. |
| `omarchy-update-system-pkgs-when-conflicted` | Hidden conflict handler: quarantines unowned conflicting files under `/var/lib/omarchy/replaced`, retries the upgrade once, restores files the upgrade didn't claim, and hands package-vs-package conflicts to an interactive pacman run (never under `-y`). | **Keep internal/hidden.** Keeps conflict recovery out of the happy path. |
| `omarchy-update-pkg-prune` | Trims the pacman cache to two versions per package (`paccache -rk2`) before the snapshot, keeping the offline downgrade path while capping snapshot growth. | **Keep internal/hidden.** |
| `omarchy-update-requires-free-space` | Aborts the update below a 10 GiB free-space threshold on `/`; silently skipped when free space cannot be determined; `OMARCHY_UPDATE_FORCE=1` bypasses. | **Keep internal/hidden.** |
| `omarchy-update-system-pkgs` | Runs `sudo env OMARCHY_UPDATE_PACMAN=1 pacman -Syu --noconfirm` with targeted transition `--overwrite` entries so the ALPM guard allows the transaction and early package-layout conflicts are handled. | **Keep for now.** Small leaf command, clear/testable. |
| `omarchy-migrate` | Public migration command. Waits for pacman, then runs all pending migrations for the current user. Supports `--pending`. | **Keep.** This replaces the discarded `omarchy-update-user-finalize` name and no longer needs `--force`. |
| `omarchy-update-pacman-guard` | ALPM pre-transaction guard that aborts direct `pacman -Syu` style upgrades unless Omarchy set `OMARCHY_UPDATE_PACMAN=1` or the user explicitly set `OMARCHY_ALLOW_DIRECT_PACMAN=1`. | **Keep internal/hidden.** This is what nudges users back to `omarchy update`. |
| `omarchy-migrate-notify` | Internal login-time notification helper. Uses `omarchy-migrate --pending` and shows a notification only when this user has pending migrations. | **Keep internal/hidden.** Clear name now that the public command is `omarchy-migrate`. |
| `omarchy-update-user-notify` | Hidden compatibility wrapper for `omarchy-migrate-notify`. | **Temporary.** Keep only for old callers. |
| `omarchy-update-available` | Update checker for shell widget and post-update refresh. | **Keep.** Could eventually be renamed `omarchy-update-check`, but current name matches widget semantics. |
| `omarchy-update-aur-pkgs` | Updates AUR packages with `yay -Sua` if foreign packages exist and AUR is reachable. | **Question.** Omarchy is package-backed now, but users may still install AUR packages. Keep for now. |
| `omarchy-update-mise` | Runs `MISE_MINIMUM_RELEASE_AGE=0 mise up` for mise-managed tools — the override of mise's release-age cooldown is the point. | **Keep.** Mise-managed tools are intentionally part of the blessed update path. |
| `omarchy-update-mise` | Runs `mise up` for mise-managed tools. | **Keep.** Mise-managed tools are intentionally part of the blessed update path. |
| `omarchy-update-orphan-pkgs` | Lists orphans and prompts before removal; noninteractive mode never removes. | **Keep for now.** Safe because it is prompt-only. |
| `omarchy-update-analyze-logs` | Scans `/tmp/omarchy-update.log` for known failure patterns, currently initramfs generation. | **Keep/expand.** Useful safety net; should grow only for high-signal checks. |
| `omarchy-update-restart` | Prompts for reboot after kernel/Hyprland updates, restarts components with `restart-*-required` markers, and always restarts the shell. | **Keep.** Important final step; may eventually include service-restart checks. |
+4 -8
View File
@@ -1,8 +1,4 @@
# Keep state away from /var/cache/cups, which is writable by the account CUPS
# uses for print filters. cups-browsed is the only writer to this directory.
CacheDir /var/cache/cups-browsed
# Auto-create queues only for modern driverless IPP printers. Remote queues
# exported by another CUPS server can still be added manually when needed.
CreateIPPPrinterQueues Driverless
CreateRemoteCUPSPrinterQueues No
# Omarchy override of cups-browsed's shipped config. The only behavioural
# change vs the upstream default (all-commented) is enabling auto-registration
# of remote IPP printers discovered via Avahi/mDNS.
CreateRemotePrinters Yes
-90
View File
@@ -1,90 +0,0 @@
#
# File/directory/user/group configuration file for the CUPS scheduler.
# See "man cups-files.conf" for a complete description of this file.
#
# List of events that are considered fatal errors for the scheduler...
#FatalErrors config
# Strip domain in local username?
#StripUserDomain No
# Do we call fsync() after writing configuration or status files?
#SyncOnClose No
# Default user and group for filters/backends/helper programs; this cannot be
# any user or group that resolves to ID 0 for security reasons...
User 209
Group 209
# Administrator user group, used to match @SYSTEM in cupsd.conf policy rules...
# This cannot contain the Group value for security reasons...
SystemGroup cups-browsed sys root
# Are Unix domain socket peer credentials used for authorization?
PeerCred on
# User that is substituted for unauthenticated (remote) root accesses...
#RemoteRoot remroot
# Do we allow file: device URIs other than to /dev/null?
#FileDevice No
# Permissions for configuration and log files...
#ConfigFilePerm 0640
#LogFilePerm 0644
# Location of the file logging all access to the scheduler; may be the name
# "syslog". If not an absolute path, the value of ServerRoot is used as the
# root directory. Also see the "AccessLogLevel" directive in cupsd.conf.
AccessLog /var/log/cups/access_log
# Location of cache files used by the scheduler...
#CacheDir /var/cache/cups
# Location of data files used by the scheduler...
#DataDir /usr/share/cups
# Location of the static web content served by the scheduler...
#DocumentRoot /usr/share/cups/doc
# Location of the file logging all messages produced by the scheduler and any
# helper programs; may be the name "syslog". If not an absolute path, the value
# of ServerRoot is used as the root directory. Also see the "LogLevel"
# directive in cupsd.conf.
ErrorLog /var/log/cups/error_log
# Location of the file logging all pages printed by the scheduler and any
# helper programs; may be the name "syslog". If not an absolute path, the value
# of ServerRoot is used as the root directory. Also see the "PageLogFormat"
# directive in cupsd.conf.
PageLog /var/log/cups/page_log
# Location of the file listing all of the local printers...
#Printcap /etc/printcap
# Format of the Printcap file...
#PrintcapFormat bsd
#PrintcapFormat plist
#PrintcapFormat solaris
# Location of all spool files...
#RequestRoot /var/spool/cups
# Location of helper programs...
#ServerBin /usr/lib/cups
# SSL/TLS keychain for the scheduler...
#ServerKeychain ssl
# Location of other configuration files...
#ServerRoot /etc/cups
# Location of scheduler state files...
#StateDir /run/cups
# Location of scheduler/helper temporary files. This directory is emptied on
# scheduler startup and cannot be one of the standard (public) temporary
# directory locations for security reasons...
#TempDir /var/spool/cups/tmp
+1
View File
@@ -0,0 +1 @@
%wheel ALL=(ALL) NOPASSWD: /usr/bin/asdcontrol
-8
View File
@@ -1,8 +0,0 @@
# Theme switching is a menu action with no terminal to carry a password prompt,
# and it repaints the browser accent on every switch, so this one write must not
# stop for a password. The argument is spelled out as six hex digits rather than
# a wildcard: the grant covers a color and nothing else, and sudoers matches a
# command's arguments exactly, so it cannot be stretched into extra ones. The
# helper revalidates the same shape, since the terminal path does not come
# through this rule.
%wheel ALL=(root) NOPASSWD: /usr/bin/omarchy-theme-set-browser-policy [0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]
+1 -1
View File
@@ -1 +1 @@
%wheel ALL=(root) NOPASSWD: /usr/bin/timedatectl ^set-timezone [A-Za-z0-9_+][A-Za-z0-9_+.-]*(/[A-Za-z0-9_+][A-Za-z0-9_+.-]*)*$
%wheel ALL=(root) NOPASSWD: /usr/bin/timedatectl set-timezone *
@@ -1,11 +0,0 @@
[Service]
User=cups-browsed
Group=cups-browsed
CacheDirectory=cups-browsed
CacheDirectoryMode=0750
UMask=0027
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
RestrictSUIDSGID=yes
-1
View File
@@ -1 +0,0 @@
u cups-browsed - "CUPS printer discovery" / -
-1
View File
@@ -1,5 +1,4 @@
run_logged "$OMARCHY_INSTALL/config/theme-system.sh"
run_logged "$OMARCHY_INSTALL/config/browser-policy.sh"
run_logged "$OMARCHY_INSTALL/config/increase-lockout-limit.sh"
run_logged "$OMARCHY_INSTALL/config/lockscreen-pam.sh"
run_logged "$OMARCHY_INSTALL/config/fix-powerprofilesctl-shebang.sh"
-2
View File
@@ -1,2 +0,0 @@
source "$OMARCHY_PATH/install/helpers/browser-policy.sh"
browser_policy_setup_dir /etc/chromium/policies/managed
+5 -3
View File
@@ -1,14 +1,16 @@
# Enable services only. Installs are followed by reboot, so don't start/reload
# daemons mid-install. UFW and hardware-gated services stay in their own scripts.
systemctl enable cups.service
systemctl enable cups-browsed.service
systemctl enable avahi-daemon.service
systemctl enable linux-modules-cleanup.service
systemctl enable docker.socket
systemctl enable systemd-resolved.service
systemctl enable NetworkManager.service
# Don't let network-online.target hold up graphical.target waiting for
# DHCP/Wi-Fi association. Nothing in the session needs to block on the network.
# Mirrors the systemd-networkd-wait-online mask in install/hardware/network.sh.
# Don't let network-online.target (pulled in by cups-browsed) hold up
# graphical.target waiting for DHCP/Wi-Fi association. Nothing in the session
# needs to block on the network. Mirrors the systemd-networkd-wait-online mask
# in install/hardware/network.sh.
systemctl mask NetworkManager-wait-online.service
systemctl enable power-profiles-daemon.service
systemctl enable sddm.service
+6 -3
View File
@@ -6,8 +6,11 @@ ln -snf /usr/share/icons/Adwaita/symbolic/actions/go-next-symbolic.svg \
/usr/share/icons/Yaru/scalable/actions/go-next-symbolic.svg
gtk-update-icon-cache /usr/share/icons/Yaru &>/dev/null || true
# Seed Chromium's first run: follow system appearance ("device") instead of dark,
# and skip the terms-of-service dialog Chromium 151 turned on by default.
# Chromium policy directory for theme
mkdir -p /etc/chromium/policies/managed
chmod a+rw /etc/chromium/policies/managed
# Default Chromium to follow system appearance ("device") instead of dark
mkdir -p /usr/lib/chromium
echo '{"distribution":{"require_eula":false},"browser":{"theme":{"color_scheme":0,"color_scheme2":0}}}' > \
echo '{"browser":{"theme":{"color_scheme":0,"color_scheme2":0}}}' > \
/usr/lib/chromium/initial_preferences
+1 -4
View File
@@ -4,6 +4,7 @@ run_logged "$OMARCHY_INSTALL/hardware/dell-xps-touchpad-haptics.sh"
run_logged "$OMARCHY_INSTALL/hardware/surface.sh"
run_logged "$OMARCHY_INSTALL/hardware/network.sh"
run_logged "$OMARCHY_INSTALL/hardware/input-group.sh"
run_logged "$OMARCHY_INSTALL/hardware/set-wireless-regdom.sh"
run_logged "$OMARCHY_INSTALL/hardware/fix-fkeys.sh"
run_logged "$OMARCHY_INSTALL/hardware/fix-synaptic-touchpad.sh"
@@ -24,10 +25,6 @@ run_logged "$OMARCHY_INSTALL/hardware/intel/fred.sh"
run_logged "$OMARCHY_INSTALL/hardware/intel/fix-wifi7-eht.sh"
run_logged "$OMARCHY_INSTALL/hardware/intel/sof-firmware.sh"
# Rebuilds the boot image, so it has to follow the Panther Lake kernel swap
# above rather than sit with the other Dell leaf at the top of this file.
run_logged "$OMARCHY_INSTALL/hardware/dell-xps13-sidecar-amps.sh"
run_logged "$OMARCHY_INSTALL/hardware/asus/fix-asus-ptl-display-backlight.sh"
run_logged "$OMARCHY_INSTALL/hardware/asus/fix-asus-ptl-b9406-display.sh"
run_logged "$OMARCHY_INSTALL/hardware/asus/fix-asus-ptl-b9406-touchpad.sh"
@@ -1,10 +0,0 @@
# Enable the temporary sidecar amplifier workaround on the exact Dell XPS 13 model that needs it.
#
# Pacman registers a package even when its post_install scriptlet fails, so the
# apply command runs explicitly here: a failed cleanup or boot-image rebuild has
# to reach the caller rather than hide behind a successfully registered package.
if omarchy-hw-dell-xps13-sidecar-amps; then
omarchy-pkg-add dell-xps13-sidecar-amps &&
sudo dell-xps13-sidecar-amps-apply
fi
+11
View File
@@ -0,0 +1,11 @@
# Give this user privileged input access for dictation tools + xbox controllers to work.
# Recorded for provisioning first-boot user creation and factory reset, granted directly
# when the install user already exists (deferred-provisioning installs create the user at
# first boot instead).
provisioning_dir="${OMARCHY_PROVISIONING_DIR:-/var/lib/omarchy/provisioning}"
mkdir -p "$provisioning_dir"
grep -qxF input "$provisioning_dir/groups" 2>/dev/null || echo input >>"$provisioning_dir/groups"
if [[ -n ${OMARCHY_INSTALL_USER:-} ]] && getent passwd "$OMARCHY_INSTALL_USER" >/dev/null; then
usermod -aG input "$OMARCHY_INSTALL_USER"
fi
-7
View File
@@ -1,7 +0,0 @@
as_root() {
if (( EUID == 0 )); then
"$@"
else
sudo "$@"
fi
}
Loaded 100 of 430 files, more files were not shown because too many files have changed in this diff. Show more