Compare commits

...
77 Commits
Author SHA1 Message Date
David Heinemeier Hansson f0020448ca More manual tweaks 2026-08-14 16:57:59 +02:00
David Heinemeier HanssonandClaude Fable 5 ebdc0263e0 Point the theme installer at the extra themes page on omarchy.org
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 16:41:06 +02:00
David Heinemeier HanssonandClaude Fable 5 e6d7c620de Extra themes are added via PRs to omarchy-site now
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 16:38:54 +02:00
David Heinemeier HanssonandClaude Fable 5 1fe471dc53 Link the manual to the extra themes page on omarchy.org
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 16:34:31 +02:00
Luis Fernando Jiménez b15ec6c6b3 Fix typo(Primeagen) in Neovim documentation (#6867) 2026-08-14 16:19:47 +02:00
David Heinemeier HanssonandClaude Fable 5 8b70f01583 Point Learn > Omarchy at the manual on omarchy.org
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 12:56:39 +02:00
David Heinemeier HanssonandClaude Fable 5 e7ea031f98 Host the manual's images in the repo under manual/images (#6861)
* Host the manual's images in the repo under manual/images

Replaces all learn.omacom.io/manual.omakub.org hotlinks with local webp
files capped at 1600px wide (~20MB total), so the manual is fully
self-contained. Theme and unlock previews are converted from the
canonical themes/*/preview.png files. A handful of illustration shots
are interim conversions of the current images, pending retakes.

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

* Retake the illustration screenshots fresh at 3x scaling

Navigation, clipboard history, notices, tmux layouts, prompt, and About
reshot on a clean workspace at 3.13x (1920x1080 logical on 6K) across
ten themes: Tokyo Night, Catppuccin, Gruvbox, Kanagawa, Everforest,
Nord, Osaka Jade, Rose Pine, Matte Black, and Ristretto.

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

* Drop the extra themes chapter from the manual

The community theme gallery will live elsewhere. Removes the chapter
and its 114 gallery images, renumbers the following chapters, and
repoints the two references (theme installs still work via
Install > Style > Theme in the menu).

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

* Reference the shipped theme previews instead of copying them

The themes chapter now points straight at themes/*/preview.png and
preview-unlock.png, so previews can never drift from the source and
manual/images drops from 6.3MB to 3.5MB.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 12:47:38 +02:00
David Heinemeier HanssonandClaude Opus 5 4559f2d5fc Add Moonlight to the preinstall remove/install lists
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019DPqQk3igpstNXSTkfSbJa
2026-08-14 12:30:32 +02:00
David Heinemeier HanssonandClaude Opus 5 28dcbae376 Restore preinstalls from the menu, and match the lists to what quattro ships (#6854)
* Restore preinstalls from the menu, and drop the Omacom apps with them

Remove Preinstalls missed omacut, omacalc, and omawrite, so the three Omacom
apps survived an opt-out that was supposed to clear the desk.

Opting out was also one-way. Install > Preinstalls now puts everything back:
the shipped .desktop launchers and mise stubs via omarchy-refresh-applications,
the dropped packages via pacman, and the opt-out marker deleted so the
preinstalled keybindings return on reload. The two menu entries guard on the
marker, so exactly one of them is ever visible.

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

* Trim the preinstall lists to what quattro actually ships

Remove Preinstalls was still dropping typora, spotify, 1password, 1password-cli,
signal-desktop, opencode, claude-code, and github-cli. None of those are in
omarchy-base.packages anymore: typora gave way to omawrite, the services moved
to on-demand menu installs, and the agent CLIs are mise-managed. Removing them
took out apps the user had deliberately installed, and restoring them would have
put back what we no longer ship.

Both lists are now the same twelve packages, all of them in omarchy-base.packages.

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

* Keep the opt-out marker when a restore fails

omarchy-pkg-add exits non-zero when pacman cannot install a package, but the
restore ran straight past it, cleared the marker, and reloaded Hyprland. That
reported success and brought back keybindings for apps that never arrived. The
marker now falls last, behind a check on the transaction.

The new test also pins the two lists to each other and to omarchy-base.packages,
which is the drift that let retired packages linger in the removal list.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 12:24:47 +02:00
Fayi FB fa8359359b Fix Pi prompt launch (#6857) 2026-08-14 12:23:07 +02:00
David Heinemeier Hansson ca5add0463 Words 2026-08-14 12:21:59 +02:00
David Heinemeier Hansson 3bd6ed02fe Words 2026-08-14 09:51:17 +02:00
David Heinemeier Hansson 864b0d050a Recognize colored package conflict errors 2026-08-14 00:15:07 -07:00
David Heinemeier HanssonandClaude Opus 5 5ca3030c5a Put a blocked package upgrade back to whoever is updating (#6830)
Pacman answers its own conflict question with No under --noconfirm, so one
retired package can stop every update after it. Which package to drop is a
decision rather than a cleanup, so run the upgrade again with pacman asking
when there is a terminal to answer on, and report instead when -y promised
not to ask.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 09:08:34 +02:00
David Heinemeier HanssonandClaude Fable 5 625c66301d Point the headless theme test at the renumbered Tokyo Night background (#6851)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 08:59:38 +02:00
Shrijit Srivastav d35f4b6d59 Wait on the profile being repaired, not on every browser (#6837)
The migration asked a user to close every running browser before repairing
the Copy URL shortcut, but a browser only ever rewrites its own Preferences
on exit. Waiting on all browsers deadlocks `omarchy update` for anyone whose
main browser is effectively never closed: the pending ghosts commonly sit in
a stale profile nobody has open, yet the migration blocks on the always-open
browser until the prompt is declined, failing the whole update.

A running Chromium-family browser holds a SingletonLock (and socket) inside
its user-data-dir, so whether the profile being repaired is open is
mechanical. Gate on that instead of on the sheer presence of a browser
process — the repair proceeds where the affected profile is closed, and
browsers attached to other profiles no longer hold the update hostage.

The gate stays conservative while an affected profile actually is open, and
the existing post-repair verification still catches a browser that starts
mid-repair and restores stale Preferences on exit.

The migration test now simulates an open profile with its SingletonLock
instead of a pgrep stub; every prior scenario still passes.
2026-08-14 08:59:14 +02:00
David Heinemeier HanssonandClaude Opus 5 dc698e5df0 Suppress LocalSend's redundant tray item (#6852)
LocalSend registers an Ayatana item with no ItemIsMenu and no Activate
handler, so its primary click is a silent no-op and the menu offers only
Open and Quit. Share > Receive already opens it, so drop the item the way
Dropbox's is dropped when its dedicated widget owns the surface.

Closes #6838

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 08:23:45 +02:00
Adrian RangelandClaude Opus 5 df708831b6 Keep the Copy URL migration's browser prompt visible (#6842)
gum draws its confirm UI on stderr, so the migration's `2>/dev/null` threw
away the whole prompt while gum still held the terminal in raw mode reading
keys. With a browser open, an update stopped after "Running migration
(1786643346)" on an unpainted screen with no way to tell it was waiting for
an answer.

Nothing else in the repo suppresses gum's stderr; the redirect only ever hid
gum's own error in the no-terminal case, where the migration already explains
itself on stderr before deferring.

Fixes #6841


Claude-Session: https://claude.ai/code/session_01So4yKzTDdJJp7orjnF8i1J

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 08:19:25 +02:00
David Heinemeier HanssonandClaude Fable 5 5e5197a081 Point the README manual TOC at the renumbered chapters
The switchers chapter shifted everything from chapter 3 up by one, and
the TOC was still on the old numbers. Add its missing entry too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 22:53:23 +02:00
David Heinemeier HanssonandClaude Fable 5 3a4eda7f08 Drop retired nmtui and bluetoothctl interactions from the bar README
The network and bluetooth panels stopped launching them; the module
catalogue still said otherwise.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 22:52:13 +02:00
David Heinemeier HanssonandClaude Fable 5 1ae83f1606 Add a switchers chapter and fix drift found on a second manual pass
Add "Coming From Mac or Windows" as chapter 3: a translation layer for
Cmd/Win instincts rather than a feature tour.

Fill the remaining coverage gaps: fcitx5 input methods, laptop speaker
tuning, touchpad haptics, crash capture in the toggle table, the
installable font list, and the ChatGPT Desktop and Grok Bot entries.

Correct claims that drifted from the source: the network and bluetooth
bar icons no longer launch nmtui/bluetoothctl, monitor scaling steps
rather than cycles, and the display widget's scroll steps brightness.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 22:52:13 +02:00
David Heinemeier HanssonandClaude Fable 5 de854d3f0c Free the Copy URL shortcut from ghost extension registrations (#6821)
* Rebind ghost Copy URL shortcut registrations to the pinned id

Chromium never hands a suggested shortcut to one extension while
another — even a long-gone one — still holds the registration. Profiles
that first loaded Copy URL before its id was pinned registered
Alt+Shift+L under an id derived from the extension's load path at the
time, so the pinned extension never receives the shortcut and the
keypress does nothing (#6816).

The quattro upgrade tried to repair this against one hardcoded
path-derived id, which only ever matched a single home directory. The
historical ids are unknowable in general — they hash long-gone absolute
paths through whatever symlinks existed then — but the registration
itself names the command, so a migration now rebinds any copy-url
command that points away from the pinned id, unless that id belongs to
an extension that is actually installed or the pinned extension already
holds a binding of its own.

Browsers rewrite Preferences on exit, which reverts any repair made
while one runs, so the migration asks for this user's browser windows to
be closed first — failing and staying pending when there is no terminal
to ask in or the prompt is declined. The backup a repair leaves behind
marks it as attempted but unverified: until a browser-free run confirms
the registration stayed repaired, the migration keeps itself pending
rather than trusting a disk state an open browser may still overwrite.

The upgrade-time repair is dropped: the upgrade already runs migrations,
so the migration is the single implementation.

Fixes #6816

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

* Pin the WhatsApp Slim extension id

Keyless unpacked extensions get path-derived ids, which go stale if the
load path or packaging ever changes — the same class of bug that broke
the Copy URL shortcut for pre-package installs. Pin the id with a
manifest key like the other bundled extensions.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 22:37:29 +02:00
David Heinemeier HanssonandClaude Opus 5 30f8f191c0 Add a toggle for crash capture (#6824)
Crash capture stays on by default, but Trigger > Toggle > Crash Capture (or
`omarchy toggle crash-capture`) now turns the watcher off. The toggle writes the
usual flag file and stops the unit for this session; the unit checks the same
flag with ConditionPathExists, so the choice survives a logout without the unit
having to be disabled.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 21:26:08 +02:00
Eduardo EscobarandClaude Opus 5 f73740ff07 Blank the lock screen while the fingerprint reader waits (#6817)
The blank timer was gated on `authenticating`, which is
`authenticatingPassword || fingerprintAuthenticating`. The fingerprint
PAM sits armed for the entire lock waiting for a finger, so on any
machine with a reader enrolled the gate is true from lock until unlock:
the timer is stopped when the lock begins and never re-armed, and the
display stays lit indefinitely.

Gate on `authenticatingPassword` instead. A password check in flight
still holds the display up, and the passive fingerprint wait no longer
does.


Claude-Session: https://claude.ai/code/session_01EDpyC9793TKZBS2jXUNECG

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 21:04:47 +02:00
Jitendra dara 5c51ad6d54 Fix broken manual links in README (#6809)
* Fix README having broken links

Reordered and updated the list of manual sections in the README.

* Update README with new section ordering and links
2026-08-13 21:02:47 +02:00
82ae514609 Fix notification focus for agent terminals (#6801)
* Fix notification focus for agent terminals

* Restrict notification title fallback to agents

* Simplify the focus fallback to a lazy two-tier query

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

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 20:28:43 +02:00
David Heinemeier Hansson 14dc3a31d1 Add omarchy-dev-font for adding marks to the icon font (#6819) 2026-08-13 19:46:46 +02:00
Rob Zolkos ed795d8a13 Select packaged Cursor for theme sync (#6808) 2026-08-13 19:33:22 +02:00
David Heinemeier HanssonandClaude Fable 5 b08598086c Keep AppLibrary scans out of login shells (#6815)
* Keep AppLibrary scans out of login shells

A login shell sources the user's profile, and tools like mise touch
~/.local/share on activation. Quickshell's desktop-entry watcher monitors
that tree, so every scan retriggered another scan: a self-sustaining loop
that pinned ~20% of a core at idle. The scans only use bash builtins,
find, and sort, so they don't need the login environment.

Fixes #6806

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

* Pin the AppLibrary scans to non-login shells in tests

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 19:30:56 +02:00
David Heinemeier HanssonandClaude Fable 5 5b8fef406f Flesh out the Windows VM chapter
Cover the KVM and disk requirements, the resources and credentials the
installer asks for, the launch/stop/status commands and --keep-alive,
what the RDP session carries, and what removal deletes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 18:18:46 +02:00
David Heinemeier Hansson b508541c88 Use official marks for the AI installers (#6814)
* Add LM Studio and Ollama marks to the Omarchy icon font

* Use the official marks for the AI installers in the menu
2026-08-13 18:18:17 +02:00
David Heinemeier Hansson e2cffe1771 Add Grok Bot to the Install > AI menu (#6811) 2026-08-13 18:02:15 +02:00
David Heinemeier HanssonandClaude Fable 5 b2207c3357 Persist notification images so history keeps avatars (#6804)
* Persist notification images so history keeps avatars

Persisted popup and history entries stored image/appIcon as URLs into
resources that die with the live notification: Chromium-family senders
(every Omarchy web app, WhatsApp included) pass avatars as files in a
scoped /tmp dir deleted when the notification closes, and raw image-data
hints surface as in-process image:// URLs that die with the server
object. Replaying history then found dead references and hid the icon.

Copy file-backed images into the notification state dir when persisting,
keyed by the entry's file stem, and reference the copies from the JSON.
Blank dead image:// URLs so the card falls back to the app icon. The
copies die with their JSON: superseded-popup deletes, history trims and
clears remove them, and a startup sweep collects copies orphaned by a
restart killing a queued job mid-write.

Hold DND-silenced notifications open until their history write has run,
since untracking tells the sender to delete its avatar file, and carry
replayed on-screen rows over via their persisted copies, since the
replay dismisses their live notifications first.

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

* Coalesce silenced updates and bound image copies through temp files

A replaces_id update lands on a held DND notification without a second
onNotification, so releasing after the first write could persist a stale
snapshot. Re-snapshot when the write completes and write again until the
content is stable, reusing the original file identity.

The image copy reopened the sender-controlled path after checking it, so
a file growing or becoming a FIFO mid-copy defeated the size bound. Read
through head -c under a timeout into a temp file, validate its size, and
rename it into place; the startup sweep clears temp files a killed job
leaves behind.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 18:01:48 +02:00
David Heinemeier HanssonandClaude Fable 5 edcbcb4691 Point the hyprsunset autostart hint at autostart.lua
The comment still described the old exec-once conf syntax.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 17:04:28 +02:00
David Heinemeier HanssonandClaude Fable 5 93372d5fad Drop the tz and style command group descriptions
Neither group has any commands behind it, so both only ever printed
"Unknown Omarchy command" when browsed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 17:04:28 +02:00
David Heinemeier HanssonandClaude Fable 5 55434792d1 Cover the shell, capture, browsers, and networking in the manual
Add chapters for the top bar and its panels, screenshots and recording,
toggles/idle/screensaver, browsers, shell plugins, and networking, which
collects the Wi-Fi, DNS, firewall, and Tailscale material that was spread
across the FAQ, TUIs, and services chapters.

Fill smaller gaps in the existing chapters: user hooks, autostart, menu
extensions, printing, power profiles, factory reset, drive and user
passwords, passwordless sudo, firmware updates, display text size, and
the LocalSend share menu.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 17:04:09 +02:00
David Heinemeier Hansson 144f4d1e31 First pass on updates for Quattro 2026-08-13 15:18:33 +02:00
David Heinemeier Hansson 9c9e082954 New launch backgrounds for Omarchy 2026-08-13 15:18:33 +02:00
David Heinemeier Hansson fa78633596 Not needed 2026-08-13 05:42:02 -07:00
25c2b3aed2 perf(agents): cut codex usage collector memory with SQL filter and cache (#6780)
* perf(agents): cut codex usage collector memory with SQL filter and cache

The codex collector scanned every row of opencode.db (1.7 GB, 55k+ rows)
with Python-side json.loads, peaking around 716 MB of RSS on every run
-- including the panel's refreshLimits() call, which passed --limits-only
that the collector silently ignored.

Filter rows in SQL (LIKE gates + json_valid + json_extract authority,
mirroring the old Python filter semantics) so giant blobs are never
parsed, and cache the local stats scan in XDG_CACHE_HOME following the
claude collector's pattern (atomic writes, flock, schemaVersion).
--force rescans, --limits-only and normal mode reuse a fresh cache and
fall back to a full scan when it is missing, stale, or corrupt.

Measured: cold scan 716 MB -> 158 MB peak; warm --limits-only ~85 MB
and ~1.4 s. Output record schema and values are unchanged for the same
data (parity verified against the old filter, including malformed rows).

* Scope the codex scan cache's 15-minute reuse to --limits-only

A no-flag run is the widget's periodic refresh, and refreshIntervalSec is
configurable down to 30 seconds; holding every mode to a 15-minute cache
meant stats could lag far behind the interval the user asked for. Mirror
the claude collector: normal runs reuse a scan for ~20 seconds purely to
dedup concurrent collectors, and only --limits-only, which promises just
fresh limits, may reuse a scan for up to 15 minutes.

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

* Invalidate the codex scan cache across day boundaries

The cached stats embed date-dependent fields (todayPrompts,
todayTotalTokens, recentDays), but only the file's age was checked, so a
cache written at 23:58 served yesterday's numbers as "today" for up to
15 minutes past midnight. Stamp the envelope with the scan's local date
and treat any other date as a miss.

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

* Reject codex scan caches with a future mtime

A cache whose mtime is ahead of the clock has a negative age, which the
freshness check accepted forever: setting the clock backwards froze the
stats until real time caught up with the file. Require a non-negative
age before trusting the cache.

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

* Never cache an interrupted opencode scan

A transient lock, schema migration, or corrupted database aborts the
opencode scan mid-flight; the partial numbers still serve the current
run, but persisting them let a single bad read suppress opencode usage
for every cache reader until expiry. The claude collector already skips
its opencode cache write on a database error; do the same here.

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

* Make the json_valid guard order explicit in the opencode query

The query relied on json_valid(data) evaluating before json_extract(),
but SQLite does not promise that AND terms run left to right; a
reordered plan would let json_extract raise on a malformed row and
silently truncate the scan. Wrap each json_extract in a CASE so the
guard is structural rather than positional.

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

* Drop a claude-collector comment that is false for codex

"These caches were world-readable before" was copied from the claude
collector; codex had no caches before this one existed. Explain the
chmod on its own terms.

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

---------

Co-authored-by: markbusking <marcosbustos.dev@gmail.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 13:47:44 +02:00
David Heinemeier Hansson 929849c7b9 Add the manual to the source code for better agent evolution 2026-08-13 13:37:57 +02:00
David Heinemeier HanssonandClaude Fable 5 78d3224846 Bound the hybrid GPU gate's supergfxctl query (#6799)
omarchy-hw-hybrid-gpu gates the Hybrid GPU menu entry, and it queried
supergfxctl unbounded — a wedged supergfxd stalled menu rendering
forever. Bound the query with the same TERM-then-KILL escalation the
toggle uses, and treat a daemon that cannot answer like a machine
without supergfxctl: fall back to counting GPUs rather than hiding
hardware that is really there. An ordinary supergfxctl failure still
hides the entry.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 12:37:43 +02:00
Aditya Garud 090574e3a1 Treat LVDS panels as internal displays (#6752)
* Treat LVDS panels as internal displays

* Test empty DRM monitor discovery
2026-08-13 12:25:25 +02:00
8b9e43d3e5 Only offer video capture devices as webcams (#6732)
* Only offer video capture devices as webcams

V4L2 exposes raw processing and output-only nodes beside usable cameras. The webcam picker and automatic recorder selection treated the first /dev/video node as a camera, so IPU6 laptops opened a black overlay instead of their loopback capture device.\n\nShare one device lister across detection, selection, and recording, and keep only groups whose first video node advertises Video Capture in Device Caps. Cover raw IPU nodes, ordinary capture devices, and capture-less systems.

* Fall through to a later capture-capable node in a webcam group

A group whose first video node is not capture-capable vanished entirely,
even when a later node in the same group could capture. Probe each node
until one qualifies, still emitting at most one device per group. Also
exit zero explicitly: a trailing filtered device used to leak the failed
capability check as the script's exit status.

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

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 12:23:48 +02:00
860503f56a Stop hybrid GPU mode queries from hanging (#6738)
* Bound hybrid GPU mode queries

* Test blocked hybrid GPU queries

* Give the blocked-client test headroom over its 12s of kill cycles

The third case spends ~12s of real TERM/KILL escalation against its own
15s watchdog, which can tip to a spurious 124 on a loaded machine. Also
drop the TEST_LOG plumbing no stub ever wrote.

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

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 12:22:58 +02:00
David Heinemeier HanssonandClaude Opus 5 ce21845407 Let --force actually re-probe the Claude limits endpoint (#6798)
The interval floor was applied with max(), so the zero that --force
picked could never win: max(0, 15) is 15. Forcing a refresh within
fifteen seconds of the last probe silently served the cache instead,
though --force documents itself as ignoring them.

The window exists to absorb a panel opened and shut repeatedly, which
arrives as --limits-only. --force is a person pressing refresh, and it
should outrank a window meant for flicks.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 12:13:41 +02:00
Grant Wiley f6fd2e705a Wait for ttfx before closing its terminal (#6773) 2026-08-13 11:46:45 +02:00
Francisco Cabral 5f74a996c7 Stop closed network panels from leaving Wi-Fi scanning enabled (#6772)
* Stop closed network panels from leaving Wi-Fi scanning enabled

refresh() defaults scanWifi to false and its no-scan branch enabled the
scanner unconditionally. Five paths reach it with no panel on screen —
Component.onCompleted, clearNetworkAction(), failNetworkAction(), the
band-change actionProc exit, and the 30s actionTimeout — so the scanner
stayed on and Quickshell kept re-arming RequestScan behind a closed panel.
scanRestart had the mirror gap: it enabled the scanner 100ms after
refresh(true) without re-checking that the panel was still open.

Every sweep takes the radio off the operating channel, so this degraded
the link it was scanning from: one sweep every 17s, gateway RTT rising
from ~2ms to repeated 150ms+ spikes on an otherwise idle connection.

Gate the scanner block on the panel being open, cancel a pending restart
on close and re-check the panel when it fires, and track the WifiDevice
this instance enabled so close, device replacement and destruction
release the right object. Destruction matters on its own: a bar reload
with the panel open would otherwise die with opened still true and never
write scannerEnabled = false.

* Cover the scanner ownership helper's own invariants

The previous assertion only pinned that no write bypasses
setScannerEnabled(); it said nothing about what the helper does. Dropping
either the opened gate or the release-before-adopt from the helper still
passed, while a closed instance could reclaim scanning and a device swap
could leave the previous interface scanning.

Run the helper's actual JavaScript against stand-in devices instead,
following the extract-and-eval pattern the agents panel tests already use.
Removing either invariant now fails its own assertion.
2026-08-13 11:46:16 +02:00
David Heinemeier HanssonandClaude Opus 5 8ca61d6b8b Open the About window at the size it last settled on (#6770)
* Measure the About layout in UTF-8 so its window is not fitted too narrow

wc -L only counts display columns in a UTF-8 locale. A session that never
set one leaves it counting the box-drawing and Nerd Font glyphs the About
layout is built from as nothing, which measured the content 21 columns
narrower than it renders and fitted the window to clip it.

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

* Open the About window at the size it last settled on

The window used to map at the float rule's starting size, paint, and only
then measure itself and resize, so every open flashed one window size and
reflowed into another. The size that hugs the content can only be measured
from inside the terminal, so remember it and apply it as a window rule
before the terminal is spawned: the window now maps at its final size and
never moves. A rebranded logo or a new font falls back to the float rule
for one launch, refits, and is remembered from then on.

The fit itself now moves the window by the cells it is off by, rather than
scaling it to the grid, which multiplied up the terminal's padding along
with them and left the fit a column or two short. It accepts a cell of
slack instead of chasing an exact grid, since a window lands where the
terminal's cell boundaries put it, not where it was asked to.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 11:32:20 +02:00
David Heinemeier HanssonandClaude Opus 5 c8fb5be42e Explain an expired Claude sign-in instead of hiding the limits (#6795)
Only the Claude Code CLI can refresh the OAuth token it saves; the
collector just reads it. A machine left alone long enough finds the
token lapsed, and that branch returned an empty limits list with no
status text at all, so the panel hid its whole limits section and
explained nothing.

Say what is wrong, and fall back to the cached limits already on disk
rather than discarding them. Cached windows are kept only until they
reset: a percentage from a window that has rolled over describes a
period that is over, and pinning a stale 78% on an allowance that is
now untouched would be worse than showing nothing. The probe-failure
path gets the same filtering for the same reason.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 11:13:07 +02:00
ecd57bceee Drop the kms hook when the proprietary NVIDIA driver handles early KMS (#6791)
* Drop the kms hook when the proprietary NVIDIA driver handles early KMS

install/hardware/nvidia.sh early-loads nvidia_drm (modeset=1) for early KMS,
but HOOKS still carried the kms hook, so autodetect pulled nouveau and
~100 MB of its GSP firmware into every initramfs for a driver that never
runs. On a Limine UKI setup that meant a 256 MB image where ~144 MB is
normal, doubled again by the fallback history on /boot.

Filter kms out of HOOKS when nvidia_drm is in MODULES (nvidia.conf sorts
before this drop-in) and every PCI display controller is NVIDIA. Hybrid
systems keep kms so the iGPU retains early KMS at the LUKS prompt.

Verified on an RTX 4090 (nvidia-open-dkms 610.57.04): UKI shrinks
256,183,296 -> 144,066,048 bytes, nouveau and its firmware gone, the
nvidia-utils GSP blobs and all four nvidia modules retained, Plymouth
still owns the LUKS prompt via nvidia_drm.

Fixes #6790

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

* Address review: quote literals, harden PCI detection, add shell tests

Quote fixed string literals in the [[ ]] comparisons per AGENTS.md, and
read the PCI tree through OMARCHY_PCI_DEVICES_PATH, the same seam
bin/omarchy-hw-nvidia already uses.

Require a positively identified NVIDIA display controller before dropping
kms: an empty or unreadable PCI tree previously counted as "no non-NVIDIA
GPU" and would have dropped the hook. Unexpected trees now keep kms.

Cover the conditional in test/shell.d/nvidia-kms-hook-test.sh: nvidia-only,
hybrid, no nvidia_drm, MODULES unset under set -u, audio-function-only,
empty tree, and a device directory missing its sysfs attributes.

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

* Treat unreadable PCI devices as inconclusive and make the test hermetic

A device whose class/vendor attributes cannot be read could be another
GPU, so skipping it let a readable NVIDIA GPU beside it drop kms without
having verified the whole tree. Count it as a non-NVIDIA sighting so kms
stays, and cover the mixed case in the test.

The test also sourced the host's /etc/vconsole.conf under set -u, where a
valid KEYMAP-only file makes the XKBLAYOUT expansion fail in the subshell
and ties the result to the machine running it. Predefine XKBLAYOUT and
FILES before sourcing the config.

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

* Rebuild existing initramfses once the kms hook no longer applies

The settings package deploys the new omarchy_hooks.conf conditional, but
nothing rebuilds the initramfs when only a mkinitcpio drop-in changes, so
existing NVIDIA-only installs would carry nouveau's ~100 MB of GSP
firmware until their next kernel update. Following the precedent of
1784476564, add a migration that rebuilds via limine-mkinitcpio — once
per machine, and only where evaluating the installed drop-ins shows the
conditional actually dropped kms, so hybrid machines, non-NVIDIA
machines, and user-edited configs are left alone.

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

* Document the mid-sourcing MODULES caveat in the kms conditional

A later-sorting drop-in that resets MODULES outright (as
surface_device_modules.conf does) would strip nvidia_drm after kms was
already dropped. Every machine Omarchy writes such a file for is hybrid
Intel and keeps kms through the PCI scan, but that is worth stating so
the invariant is not broken by accident.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-08-13 10:48:18 +02:00
David Heinemeier HanssonandClaude Fable 5 6fad76184f Unmask wpa_supplicant left masked from the iwd era (#6793)
Quattro hands wifi to NetworkManager, which starts wpa_supplicant through
D-Bus activation. Installs carrying a wpa_supplicant.service mask from the
iwd days break that activation: NetworkManager retries five times, gives
up, and every wifi device sits at "unavailable" with no network to
research the fix on. Remove the mask (including a runtime one) and restart
an active NetworkManager when a wifi device is stuck, so wifi comes back
without a reboot.

Fixes #6783

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 10:36:01 +02:00
David Heinemeier HanssonandClaude Fable 5 457aec6a8c Stop Bluetooth discovery when the panel closes (#6794)
The discovery retry timer turned adapter.discovering on every second
while the panel was open, and nothing ever turned it off. The BlueZ
discovery session behind it is held by quickshell's D-Bus connection,
so one visit to the panel left the radio in inquiry until the next
shell restart — continuously starving A2DP audio on the same controller
into stuttering, and 'bluetoothctl show' kept reporting
'Discovering: yes' long after the panel was gone.

The panel now tracks the StopDiscovery it owes BlueZ and settles it
once closed. A timer bound to the confirmed discovery state does the
stopping, rather than a write in the close handler: quickshell only
forwards a discovering write that differs from the last state BlueZ
reported, so a stop issued while a just-fired StartDiscovery is still
awaiting confirmation would be swallowed and leak the session. Binding
to adapter.discovering re-arms the stop whenever the confirmation
lands, a reopen inside the first interval keeps the scan running
uninterrupted, and attempts are bounded so a session another BlueZ
client holds up cannot draw StopDiscovery calls forever.

One widget instance exists per monitor and they all share the default
adapter — the same shared-backend shape the network panel's wifi
scanner fix (#6772) dealt with — so the debt follows the session: an
instance opening onto a running scan adopts it, a closing instance
hands it to a panel still open on another monitor (the popout handoff
closes one instance as it opens the next), and a destroyed instance
passes it to a surviving sibling.

Fixes #6789

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 10:35:22 +02:00
f075a789f4 Recover the wallpaper picker after interrupted thumbnails (#6775)
* Recover image picker after interrupted thumbnails

* Use arithmetic assertions in image cache tests

* Bound thumbnail lock waits and reap partial thumbnails

A hung generator (vips stuck on a corrupt file or slow mount) held its
flock forever, wedging every later picker open; the directory-lock era
capped that wait at 30 seconds, so keep the same bound. A generator
killed mid-write also stranded its partial .jpg.<pid>.jpg forever, since
nothing prunes the cache directory; only the lock holder writes those,
so reap them right after taking the lock.

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

* Harden thumbnail locks and cache publication against races

Adversarial review caught three holes. The lock fd leaked into
vipsthumbnail, so an orphaned or hung vips kept holding the lock after
its shell died; close it for the child. Reaping legacy lock directories
unconditionally raced a still-running legacy generator through an
upgrade; only reap ones older than the longest plausible generation.
And cache publication was neither atomic nor exclusive, so a picker
killed mid-write, or two interleaving, could leave truncated or
mismatched rows behind signatures that still validated - the same
permanent hiding this branch set out to fix; publish via renames under
a per-key lock, rows first.

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

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 09:58:15 +02:00
9672d852f0 fix --help being skipped for bin routes that only partially resolve to a file (#6788)
* fix `--help` being skipped for bin routes that only partially resolve to a file

* Add regression tests for --help on partially resolved routes

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

* Stop help-flag scanning at -- and match --json as an exact token

A `--` marks the rest of the args as belonging to the command itself, so
neither --help nor --json past it should be treated as router flags. The
--json check also matched substrings of flattened args, so a --json inside
one quoted argument switched --help output to JSON.

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

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 09:53:31 +02:00
David Heinemeier Hansson 38542a1f51 Open the agent in a normal tiled window (#6769)
The floating rule pinned every agent terminal to 1200x800, which
overflows small and scaled displays: window rules see logical pixels, so
a 2560x1440 monitor at scale 1.6 is only 1600x900 and the window covered
89% of its height. Tiling drops the fixed size along with the rule, and
the shared app-id still earns the terminal tag from terminals.lua.
2026-08-12 22:37:16 +02:00
David Heinemeier Hansson 438f7b3340 Stop ttfx before closing the screensaver terminal (#6764) 2026-08-12 20:05:10 +02:00
David Heinemeier Hansson f5893ddd9e Launch the default agent from the agents widget right-click (#6759) 2026-08-12 18:46:17 +02:00
David Heinemeier Hansson 2cc3510d2a Offer an AI diagnosis when a process crashes (#6746)
* Offer an AI diagnosis when a process crashes

systemd-coredump journals every core dump under a known MESSAGE_ID with the
crashing program, pid, and signal as structured fields. omarchy-crash-watch
follows that stream and raises a "Process crashed: <program>" toast; clicking it
opens omarchy-agent-crash, which briefs the default agent on the crash.

The toast goes through omarchy-notification-send --exec rather than a libnotify
action, because the shell runs clicks from its own omarchy-exec hint and never
emits ActionInvoked. It keeps the default "omarchy-action" app name too, the
only one shouldBypassDnd() lets through -- a crash being the last notification
worth swallowing. It stays quiet until an agent is configured, since a
diagnosis is all it offers.

The method lives in a diagnose-crash skill rather than the prompt, so it is
edited in one place and works with whichever agent is default. It covers
investigating the core, and reporting a confirmed Omarchy bug upstream: scoped
to bugs Omarchy controls, searched for duplicates first, only with the user's
agreement, and signed with the model and harness that produced it.

A migration reaches existing installs, whose skill symlinks and unit enablement
would otherwise sit behind one-time setup paths.

* Let the diagnosis clean up the core it extracted

"Do not modify or delete anything" contradicted the symbolization step right
above it, which writes a core to a temp file and deletes it on exit. Read
literally, the core survives -- and the same section warns it holds passwords
and tokens. The prohibition is about the system, not about your own scratch.

* Do not spend a crash toast on a dead notification server

The shell owns org.freedesktop.Notifications, so its own crash takes the
notification server down with it -- and a shell crash is exactly what you want
told about. The toast was sent once into that gap and the dedupe window was
recorded regardless, so the rest of the crash loop went quiet for a minute and
`journalctl -n 0` never replays what was missed.

It now waits for the restarted shell to reclaim the bus name, as
omarchy-migrate-notify already does, and only a delivered toast starts the
dedupe window.
2026-08-12 18:37:40 +02:00
David Heinemeier Hansson 9502b81f3b Reshape the agent launcher into omarchy agent (#6757)
* Reshape the agent launcher into omarchy agent

omarchy-launch-agent becomes omarchy-agent, with prompts on omarchy-agent-prompt
rather than the bare route: `omarchy agent` is both a command and a group, so a
positional prompt there would shadow any subcommand under it. The launcher takes
flags only and points at `omarchy agent prompt` when handed one.

Every agent window now launches under a fixed org.omarchy.agent app-id instead of
omarchy-launch-tui's default of org.omarchy.<binary>, so one rule floats them all
whichever agent is default.

Omarchy also stops picking an agent for you. omarchy-default-agent prints nothing
until one is chosen, leaving every entry under Setup > Defaults > Agent unchecked,
and a first-run invitation offers to take you there.

* Wordsmith

* Cover the agent routes and the invitation

The route split is the point of the change, so exercise `omarchy agent`,
`omarchy agent prompt`, and a rejected positional prompt through the router
rather than only the binaries behind them.

The invitation gets the same treatment as the Voxtype and fingerprint ones: it
notifies once, opens the agent defaults menu, and leaves both the notification
and the marker alone for anyone who already chose an agent.

* Offer the agent choice from the keybinding

Super + Shift + Ctrl + A now runs `omarchy-agent --pick`, which opens Setup >
Defaults > Agent when nothing is chosen yet. A keypress that writes to stderr
and opens nothing just looks broken.

* Reach existing installs with the agent invitation

first-run installs the invitation hook, and existing accounts marked it complete
long ago, so they would never see it -- while being the accounts most likely to
need it, since the old getter returned opencode implicitly and most have no
agent recorded at all. Post-update hooks run later in the same update, so the
invitation arrives without waiting for another one.

* Say what the Defaults submenus set

Setup > Defaults lists Agent, Browser, Terminal, Editor, but the header inside
each repeated the same bare word, which reads as a category rather than a
setting -- and says nothing at all when the menu is summoned straight into it.
The list keeps its short labels; the headers now name the setting.
2026-08-12 17:56:19 +02:00
David Heinemeier HanssonandClaude Opus 5 b97a1480dc Treat the guard test's socket fixture as optional (#6755)
Standing in for an abandoned compositor means binding a Unix socket, and the
sandboxes this guard exists for are the ones that deny it: the fixture raised
PermissionError and took the whole file down with set -e, adding a failure in
the environment the guard was written to keep clean. Run the cases that need no
socket first and skip the rest when one cannot be bound.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 16:58:46 +02:00
66e3f479a6 Follow the keyboard being typed on in the layout widget (#6740)
* Read the keyboard being typed on rather than the one holding main

The main flag names no keyboard for long. fcitx5 takes it with the
virtual keyboard it binds to inject, and those are filtered out, so on a
seat running an input method the pick lands on nothing at all: no label,
and the widget hides itself off the bar. #6727 keeps polling in that
state rather than settling it, and the poll has nothing new to read.
Once fcitx5 unbinds, the flag lands on whichever device libinput listed
last, as easily a lid switch as a keyboard, and a device that never
receives the toggle reports the layout it started on forever, which is
the reading #6574 opened.

Every device carries the seat's layout list, but only the keyboard being
typed on advances through it, so read the furthest-advanced one.
activelayout names the keyboard it moved ahead of the layout, so take
that name and let it settle the pick, and the click that switches it.

* Leave the buttons out of the seat the widget reads

Reading the keyboard being typed on left keyboardName standing for two
things at once: the device a click switches, and the device activelayout
last named. Only the second was still being set, so the first went empty
until a switch happened -- which left the click doing nothing on a seat
whose only switch is the click, and left the poll running forever on the
one-keyboard install it was written to leave alone. Give each its own
property, and set the switch target from the reading that confirmed the
keyboard is there.

Layout progress only points at the keyboard being typed on while the
other devices stay where they started, and the ACPI power button, lid
switch and sleep key never do move on their own -- but they answer to
switchxkblayout and can hold the main flag, so anything that reads or
switches whatever the seat hands back can end up describing a button, and
unplugging the keyboard beside one leaves it standing in for the seat.
Drop them where the virtual keyboards are already dropped.

A reading that reaches hyprctl and finds no keyboard now clears the label
rather than leaving a device that is gone described on the bar, told
apart from the empty output a killed query leaves by the device list
itself, and the watchdog asks again rather than waiting for a poll that a
settled seat has already stopped.

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

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 16:57:57 +02:00
David Heinemeier HanssonandClaude Opus 5 0fa3170504 Skip shell tests when the compositor can't be reached (#6749)
* Skip shell tests when the compositor can't be reached, not just when WAYLAND_DISPLAY is unset

A set variable only proves the environment was inherited. Sandboxes pass it
through while blocking $XDG_RUNTIME_DIR, so Quickshell cleared the guard and
aborted inside QGuiApplication, leaving two core dumps per launch instead of a
clean skip. Probe the socket and, when there's a signature to ask with,
Hyprland itself. Disable core dumps on the way through for the compositor that
dies mid-run, which no probe can catch.

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

* Retry the compositor query before calling it dead

Hyprland can miss a query while it reconfigures outputs, and one miss was enough
to skip a whole file's runtime coverage. Retry the way omarchy-launch-shell
does. Only a leftover socket reaches the query at all, so the ordinary skip
still returns immediately.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 16:49:15 +02:00
David Heinemeier HanssonandClaude Opus 5 0900855a28 Stop hot-reloading the shell when a package upgrade rewrites it (#6751)
Quickshell watches the QML it loaded and reloads on change, so pacman
replacing /usr/share/omarchy/shell mid-transaction makes the running
shell reload against a half-written tree. That reload fails, and a
failure that reaches the config load is not harmless: it raises the
reload popup, which is a second engine generation.
EngineGeneration::currentGeneration() returns null unless exactly one
exists, so the IPC kill that omarchy-update sends moments later takes
the QCoreApplication::exit(0) branch instead of the generation's own
quit, and Quickshell tears the QML graph down after deleting the
QGuiApplication. The first GUI resource touched on the way out aborts:

  FATAL: QPixmap: Must construct a QGuiApplication before a QPixmap

The user gets the crash dialog after an update and a coredump per
occurrence. Reported in #6748 with 3 crashes across 10 updates, always
following a failed reload.

Fixing this in omarchy-update — stopping the shell around the pacman
step — would cover one caller and cost the polkit agent and the
notification server for the length of the transaction, which the
migrations that run next still notify through. It would also have to
carry omarchy-restart-shell's refusal to restart a locked session, or
reintroduce the hazard that refusal exists for.

And omarchy-update is not the only thing that rewrites the tree. The
pacman guard turns away a bare pacman -Syu, but nothing turns away a
targeted pacman -S omarchy, a pacman -U of a locally built package, the
documented OMARCHY_ALLOW_DIRECT_PACMAN bypass, omarchy-dev-pkg-test, or
a checkout in a dev-linked tree.

Turn the watcher off instead. Omarchy has never reloaded through it:
omarchy-restart-shell is what picks up QML changes, and config and
plugin changes go through the shell's own IPC. Third-party plugin hot
reload is PluginRegistry's own inotifywait and FileView watches its own
files, neither of which this touches — QuickshellSettings::watchFiles()
gates the config scanner and nothing else. The popup goes off with it,
because QML can still ask for a reload directly and leave the same
extra generation behind.

Environment reaches Quickshell only at launch, so the update that
delivers this still runs under a watching shell. It takes effect from
the next one.

Verified against an isolated instance: breaking a config in place and
then sending the IPC kill reproduces the FATAL, and it stops with either
variable set. QS_DISABLE_FILE_WATCHER also keeps the failed reload from
happening at all.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 16:42:37 +02:00
David Heinemeier HanssonandClaude Opus 5 106320ab11 Install ChatGPT as a desktop app under Install > AI (#6747)
Drops the ChatGPT web app from the default set at the same time, so installing
the openai-codex-desktop package can't leave two identical-looking ChatGPT
entries in the launcher. Super + Shift + A still opens the web version, which
is the only place it was really used.

The bundled ChatGPT icon stays: the package's own chatgpt.desktop asks for
Icon=chatgpt and ships no hicolor icon of its own.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 15:56:40 +02:00
David Heinemeier HanssonandClaude Opus 5 970ec26bb0 Don't let a usbfs claim count as a kernel driver (#6744)
The vendor-ID guess rejects any device with a driver bound, on the
reasoning that libfprint drives readers from userspace so a real one sits
there unbound. But libusb claims interfaces through a synthetic usbfs
driver, so the reader binds one for as long as fprintd holds the claim —
which is exactly while it is being enrolled or verified against.

Readers that name themselves take the product-string branch and never
reach this, so the exposure is the ones that don't: Goodix 27c6:6594
reports "Goodix USB2.0 MISC", matches on vendor ID alone, and drops out
of detection mid-authentication. The menu entry disappears and the
first-run invitation stops firing while the reader is in use.

Ignore a driver link that resolves to usbfs, and keep rejecting the real
ones — usbio-bridge, usbhid, uvcvideo — including on a device that has
both.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 14:01:58 +02:00
a9c159a1f4 Restore FPC fingerprint detection in Quattro (#6737)
* Restore FPC fingerprint detection

* Anchor the FPC product match to a prefix

*fpc* is an unanchored three-letter token on the one branch that is
trusted outright, with none of the kernel-driver checking the vendor
guess gets. Every FPC reader on record leads with it — "FPC Sensor
Controller", "FPC Sensor Controller L:0002 FW:25.26.23.14", and this
branch's "FPC L:0000 FW:1425046" — so requiring the prefix costs no
coverage while keeping three letters from matching mid-string, where
FPC abbreviates unrelated things like flexible printed circuit.

Also restore the note about why Elan is kept out of the vendor list, so
the two signatures read as the same deliberate exception.

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

* Write no product descriptor for a two-field device spec

${remainder#*:} returns the string unchanged when there is no second
colon, so a spec meant to describe a device with no product descriptor
wrote the product id out as its product string instead. Every call site
happened to pass a trailing colon, so the suite was right by accident.

Guard the split and drop the trailing colons, so the two vendor-match
cases exercise the path they were written for.

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

* Bind a named driver in the kernel-driver fixture

Touching a bare `driver` file asserts that any driver at all disqualifies
a vendor guess, which is more than the detector should promise: libusb
claims an interface through a synthetic `usbfs` driver, so a reader in
active use looks bound by that rule.

Link the interface at a named driver directory instead, the way sysfs
does. The case still covers what it was written for — a Synaptics bridge
or a camera on a fingerprint vendor ID — without fixing the shape of the
answer for drivers it was never about.

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>
2026-08-12 13:59:01 +02:00
markbus-aiandmarkbus-ai 5c74f82300 Ask for the keyboard layout only when the answer can change (#6727)
The widget polled hyprctl every 10 seconds per monitor, including on the single-layout install where it never shows. Keep the poll only where its answer can change - a seat with more than one keyboard, where Hyprland moves the main flag with no event to announce it - and stop it entirely once a one-keyboard seat has been read.

Also coalesce a refresh that arrives mid-query instead of dropping it, time out a query that never returns rather than letting it hold the guard shut for good, and re-read the layout on configreloaded.

Co-Authored-By: markbus-ai <markbus-ai@users.noreply.github.com>
2026-08-12 13:28:44 +02:00
David Heinemeier Hansson 0fcdd716a1 Drop backgrounds no longer worthy 2026-08-12 12:18:57 +02:00
David Heinemeier Hansson 3da9eaf61c Add quattro background by vulturetone 2026-08-12 12:18:47 +02:00
David Heinemeier Hansson 21b610a7fa Need an extra CR for presentation 2026-08-12 11:58:25 +02:00
David Heinemeier HanssonandClaude Opus 5 9b8bf1da71 Fix two races in the notification popup and history handling (#6735)
* Replay the history a dismissal or a clear was still being written into

The popup files a replay reads are written by a serialized queue of shell
jobs, and the read ran as its own process alongside it. A dismissal issued a
moment earlier could still be queued when the directory was read, leaving the
notification out of the replay it was the newest entry of, and a clear issued
a moment earlier could still be queued too, replaying entries it was about to
remove.

The read now waits for the queue to go idle, so the replay shows the history
as of the moment it was asked for rather than whichever jobs happened to have
landed.

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

* Catch up on an update that arrived before its popup had a row

Watching a notification for in-place updates starts the moment it is handed
over, but the row those updates write to is inserted a tick later, deferred to
keep a mid-incubation Repeater from being mutated underneath. A client fast
enough to update inside that window found no row to write to, and a property
that has already changed does not change again — so the toast and its file sat
on the superseded content until something else moved.

The row is now refreshed from the live notification once it exists. That reads
the same object the signals would have, so an update that beat the insert is
picked up and one that did not costs nothing: a refresh whose content matches
the row it would write is dropped, which also collapses the several signals a
single multi-property update emits into one rewrite.

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

* Hold queued file work behind the replay's read, not just ahead of it

The read waited for everything queued before it, but nothing stopped the queue
from running on while it worked. A clear or an archive issued during the read
could delete or move files out from under awk mid-glob, so a replay could still
show a partial history — some of what a clear was in the middle of emptying.

The read is a barrier in both directions now: the queue holds until it exits,
and it releases on exit rather than on output, so a read that comes back empty
or fails cannot park the queue behind it.

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

* Queue the replay's read instead of waiting for the queue to empty

Waiting for the queue to go idle before starting the read still let work
overtake it. A clear or an archive enqueued after the replay was asked for,
while the current job was running, was dequeued the moment that job exited —
the read only starts once nothing is left — so the replay showed the state
after those jobs, which is the race this was meant to close. Unbroken file
traffic could postpone the read indefinitely for the same reason.

The read is now an entry in that queue rather than a process running beside
it. It takes its place in line behind the work queued before the request and
ahead of everything queued after, so no later job can overtake it and no
amount of traffic can push it back.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 11:57:05 +02:00
David Heinemeier HanssonandClaude Opus 5 fd23ca023a Prune the package cache before updating (#6734)
The pacman cache grows without bound across updates, and nothing in the
update flow ever reclaimed it. On a machine that has been updating for a
while it reaches several gigabytes of superseded versions that nothing
will ever install again.

Prune it with paccache -rk2 as the first step of an update. Both halves
of that placement are load-bearing.

Keeping two versions rather than one preserves the rollback path. The
cache is Arch's only offline downgrade: when an update breaks a single
package, reinstalling its predecessor from here is the surgical fix,
where a snapshot rollback would revert every other package too. Pruning
before the packages update means the installed version is still the
newest cached, so it survives along with a spare. Retention is by
version order and never consults what is installed, so that holds while
the installed version is among the two newest cached; a deliberate
downgrade or repeated failed transactions can stack newer archives on
top of it.

Running before the snapshot is what actually frees the space. The cache
sits on the snapshotted root subvolume, so a prune taken afterwards
leaves the fresh snapshot holding those extents and reclaims nothing
until it ages out of the number cleanup.

A failed prune warns and continues. Cache housekeeping should not trip
the update's ERR trap and tell the user their update went wrong.

This runs after omarchy-update-requires-free-space, so it reclaims space
during healthy updates but does not rescue a machine already under the
10 GiB gate.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 11:56:02 +02:00
David Heinemeier HanssonandClaude Opus 5 cd84583b56 Show a notification the sender updated, instead of the version it replaced
A client that updates a notification through replaces_id does not produce a
second onNotification: Quickshell writes the new content onto the Notification
object the shell is already holding. The card draws a snapshot copied out of
that object — deliberately, since a live QObject in a ListModel role becomes a
dangling pointer the moment the server destroys it — so the toast kept showing
the superseded text, and archived it to history when it left the screen. A
Slack thread that updates in place read as stuck.

Every property the card draws is now watched on the notification we hold, and
a change rewrites both the model row and the file the popup was persisted
under. The file name is that popup's identity, so the rewrite lands in place:
a shell restart restores the version last shown, and so does the copy that
reaches history.

The countdown starts over when the content changes. New text arriving a second
before the toast was due to expire deserves a full look, not the remainder of
the clock the text it replaced had nearly run through.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 00:58:14 -07:00
David Heinemeier HanssonandClaude Opus 5 ab57ad65fd Make notification history the last ten notifications on disk
History was a pair of in-memory lists mirrored into notifications.json, split
into "pending" and "past" by a seen/unseen distinction no surface exposed,
capped at 100, deduped by an id that repeats across server generations, and
pruned by a 15-minute TTL. Replaying it showed five rows drawn from whichever
list happened to hold them.

Every toast already writes a file under ~/.local/state/omarchy/notifications
so it can survive a shell restart. That file is now the history record: when
the popup leaves the screen it moves into notifications/history instead of
being deleted, the newest ten are kept, and showHistory replays exactly what
is in there, including the toasts still on screen when it is asked for. A
notification DND silenced is written straight into the same directory, since
a toast that never showed is the one worth looking back at.

That leaves the models, notifications.json history payload, past pruning, and
the /tmp image cache that existed to keep century-old history thumbnails alive
with nothing to do, so they go.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 00:58:14 -07:00
David Heinemeier HanssonandClaude Opus 5 d6b21f8075 Launch the default coding agent without permission prompts (#6729)
Every supported agent spells it differently, so map each one to its own
bypass flag instead of leaving the launcher at each agent's default.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 08:56:03 +02:00
David Heinemeier HanssonandClaude Opus 5 2fa490dc96 Rebuild boot images the Plymouth migration left stale
1784917531 gated its UKI rebuild on initramfs_async=0 being present in
the Limine config, but omarchy-settings ships omarchy-defaults.conf with
that parameter already in it. Any machine that installed the package and
ran the migration in the same update matched the config the package had
just written, skipped the rebuild, and kept booting an image baked
before the config existed — without initramfs_async=0, so encrypted
boots still fell back to an unthemed text LUKS prompt. Compare the
booted command line against the configured one and rebuild when they
disagree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 23:35:40 -07:00
231 changed files with 9394 additions and 1211 deletions

No files matched your search

+1
View File
@@ -6,6 +6,7 @@ matching guide before starting:
- [`agents/skills/command-metadata.md`](agents/skills/command-metadata.md) - adding or changing commands in `bin/`
- [`agents/skills/install-scripts.md`](agents/skills/install-scripts.md) - working under `install/` or on system/user setup commands
- [`agents/skills/shell-dev.md`](agents/skills/shell-dev.md) - editing the Quickshell desktop under `shell/`
- [`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
- [`docs/migrations.md`](docs/migrations.md) - creating or changing migrations under `migrations/`
+71
View File
@@ -4,6 +4,77 @@ Omarchy is a beautiful, modern & opinionated Linux distribution by DHH.
Read more at [omarchy.org](https://omarchy.org).
## The Omarchy Manual
The manual lives in [`manual/`](manual/), which is its authoritative source. It's
mirrored to [learn.omacom.io](https://learn.omacom.io/2/the-omarchy-manual), where
its screenshots are also hosted.
- [Welcome to Omarchy!](manual/01-welcome-to-omarchy.md)
**The Basics**
- [Getting Started](manual/02-getting-started.md)
- [Coming From Mac or Windows](manual/03-coming-from-mac-or-windows.md)
- [Navigation](manual/04-navigation.md)
- [The top bar](manual/05-the-top-bar.md)
- [Themes](manual/06-themes.md)
- [Hotkeys](manual/07-hotkeys.md)
- [Unified Clipboard & History](manual/08-unified-clipboard-history.md)
- [Reminders](manual/09-reminders.md)
- [Notices](manual/10-notices.md)
- [Text Extraction & Dictation](manual/11-text-extraction-dictation.md)
- [Screenshots & Recording](manual/12-screenshots-recording.md)
- [Toggles, idle & screensaver](manual/13-toggles-idle-screensaver.md)
- [Omarchy CLI](manual/14-omarchy-cli.md)
**The Applications**
- [Terminal](manual/15-terminal.md)
- [Neovim](manual/16-neovim.md)
- [AI](manual/17-ai.md)
- [Development Tools](manual/18-development-tools.md)
- [Shell Tools](manual/19-shell-tools.md)
- [Shell Functions](manual/20-shell-functions.md)
- [TUIs](manual/21-tuis.md)
- [GUIs](manual/22-guis.md)
- [Browsers](manual/23-browsers.md)
- [Commercial apps/services](manual/24-commercial-apps-services.md)
- [Web Apps](manual/25-web-apps.md)
- [Gaming](manual/26-gaming.md)
- [Filling out PDFs](manual/27-filling-out-pdfs.md)
- [Windows VM](manual/28-windows-vm.md)
- [Other Packages](manual/29-other-packages.md)
**Configuration**
- [Updates](manual/30-updates.md)
- [Dotfiles](manual/31-dotfiles.md)
- [Shell plugins](manual/32-shell-plugins.md)
- [Monitors](manual/33-monitors.md)
- [Keyboard, Mouse, Trackpad](manual/34-keyboard-mouse-trackpad.md)
- [Networking](manual/35-networking.md)
- [System sleep](manual/36-system-sleep.md)
- [Hardware authentication](manual/37-hardware-authentication.md)
- [Fonts](manual/38-fonts.md)
- [Backgrounds](manual/39-backgrounds.md)
- [Prompt](manual/40-prompt.md)
- [Branding](manual/41-branding.md)
- [Common tweaks](manual/42-common-tweaks.md)
- [Extra themes](manual/43-extra-themes.md)
- [Making your own theme](manual/44-making-your-own-theme.md)
**The Rest**
- [Mac support](manual/45-mac-support.md)
- [Troubleshooting](manual/46-troubleshooting.md)
- [FAQ](manual/47-faq.md)
- [System snapshots](manual/48-system-snapshots.md)
- [Security](manual/49-security.md)
- [Omarchy on...](manual/50-omarchy-on.md)
- [Dual Boot Install](manual/51-dual-boot-install.md)
- [Unattended Installs](manual/52-unattended-installs.md)
## License
Omarchy is released under the [MIT License](https://opensource.org/licenses/MIT).
+79
View File
@@ -0,0 +1,79 @@
# Omarchy Icon Font
Read this before adding a branded glyph to `default/fonts/omarchy/omarchy.ttf`.
The Omarchy icon font is a small private-use font carrying the marks Nerd
Fonts does not have: the Omarchy logo and the agent and app brand marks. The
menu draws one by naming the font on an entry:
```jsonc
"setup.default.agent.grok": {"icon":"","iconFont":"omarchy","label":"Grok", ...}
```
Without `iconFont`, an entry's `icon` is drawn in the menu font, so reach for
this font only when a Nerd Font glyph would misrepresent the thing. A generic
robot for four different AI apps is the case that justifies a real mark; a
folder or a microphone is not.
`default/fonts/omarchy/README.md` lists every glyph with the URL its artwork
came from. Keep that list accurate — it is the only record of provenance.
## Adding a glyph
`omarchy dev font` does the work:
```bash
omarchy dev font list
omarchy dev font add ollama https://simpleicons.org/icons/ollama.svg
```
`add` fetches the SVG, scales it into the same 64..960 box the existing marks
use so it lands at their optical size, appends it at the next free private-use
codepoint, and adds a line to the font README. It prints the codepoint and the
glyph itself.
The source must be a **monochrome SVG with a single `<path>`**, because the
menu recolors the glyph with the active theme's foreground and selection
colors. Brand icon sets such as <https://simpleicons.org> publish exactly that
shape. App favicons usually do not: they are multi-color, carry a container
tile, or split across several paths. Prefer the official mark when it is
published as flat monochrome art, and fall back to an icon set's redraw when it
is not.
Two-tone marks are a trap. Every path becomes solid foreground, so a logo whose
meaning depends on lighter and darker halves turns into an unreadable blob.
Pick a source whose silhouette alone reads.
## After adding
The command prints these, and all of them matter:
- Point the menu entry at the new codepoint with `"iconFont":"omarchy"`.
- Bump the charset range asserted in `test/shell.d/menu-test.sh`; that test
pins the font's coverage and fails until it matches.
- Check the README line the command added, and give it a proper display name
with `--label` if the glyph name is not the brand's name.
The font is package-owned: `omarchy-settings` installs it to
`/usr/share/fonts/omarchy/omarchy.ttf`, so a new glyph reaches the desktop
through a settings release, not through `omarchy update`. Between the merge and
that release, a pulled checkout renders the new entry with no icon.
## Verifying
Render the whole font and look at it, which catches inverted contours and
filled counters that a glyph list cannot show:
```bash
python3 -c "open('/tmp/row.txt','w').write(' '.join(chr(c) for c in range(0xE900, 0xE910)))"
magick -background white -fill black -font default/fonts/omarchy/omarchy.ttf \
-pointsize 110 label:@/tmp/row.txt /tmp/font-row.png
```
Then confirm it in the running menu per
[`visual-verification.md`](visual-verification.md). Fontconfig prefers the
packaged font over a copy in `~/.local/share/fonts` for the same family, so a
preview needs either the real file replaced or a `<rejectfont>` rule in
`~/.config/fontconfig/conf.d/` pointing fontconfig away from the packaged one.
Restart the shell afterwards — Qt reads the font database at startup, so
`omarchy menu refresh` alone will not pick up a changed font.
-8
View File
@@ -1,8 +0,0 @@
[Desktop Entry]
Version=1.0
Name=ChatGPT
Exec=omarchy-launch-webapp https://chatgpt.com/
Terminal=false
Type=Application
Icon=chatgpt
StartupNotify=true
+29 -6
View File
@@ -78,7 +78,6 @@ GROUP_DESCRIPTIONS[setup]="Interactive setup wizards"
GROUP_DESCRIPTIONS[shell]="Omarchy shell IPC helpers"
GROUP_DESCRIPTIONS[screensaver]="Screensaver branding and animation"
GROUP_DESCRIPTIONS[snapshot]="System snapshots"
GROUP_DESCRIPTIONS[style]="Global UI style controls"
GROUP_DESCRIPTIONS[sudo]="Sudo configuration helpers"
GROUP_DESCRIPTIONS[system]="System status, reboot, shutdown, logout, and lock"
GROUP_DESCRIPTIONS[tailscale]="Tailscale helpers"
@@ -86,7 +85,6 @@ GROUP_DESCRIPTIONS[theme]="Theme management"
GROUP_DESCRIPTIONS[toggle]="Toggle Omarchy features"
GROUP_DESCRIPTIONS[transcode]="Image and video transcoding"
GROUP_DESCRIPTIONS[tui]="Terminal UI launchers"
GROUP_DESCRIPTIONS[tz]="Timezone selection"
GROUP_DESCRIPTIONS[update]="Omarchy and system updates"
GROUP_DESCRIPTIONS[version]="Version and channel information"
GROUP_DESCRIPTIONS[voxtype]="Voxtype dictation"
@@ -122,6 +120,31 @@ append_pipe_value() {
fi
}
# A route can resolve to a binary before all args are consumed (`update aur` only matches the `update` binary, leaving `aur` and `--help` as leftovers)
# --help must never be lost among those leftovers and get forwarded into the real command, so this checks the whole remainder rather than just the first token.
# A `--` marks everything after it as belonging to the command itself, so scanning stops there.
remaining_has_help_flag() {
local token=""
for token in "$@"; do
[[ $token == "--" ]] && break
[[ $token == "--help" || $token == "-h" ]] && return 0
done
return 1
}
remaining_has_json_flag() {
local token=""
for token in "$@"; do
[[ $token == "--" ]] && break
[[ $token == "--json" ]] && return 0
done
return 1
}
register_route() {
local route="$1"
local key="$2"
@@ -930,7 +953,7 @@ dispatch_fast_or_help() {
remaining=("${args[@]:DIRECT_RESOLVED_COUNT}")
binary_path="$OMARCHY_BIN_DIR/$DIRECT_RESOLVED_BINARY"
if (( ${#remaining[@]} > 0 )) && [[ ${remaining[0]} == "--help" || ${remaining[0]} == "-h" ]]; then
if remaining_has_help_flag "${remaining[@]}"; then
if ! load_command_by_binary "$DIRECT_RESOLVED_BINARY"; then
echo "Binary is missing or not executable: $DIRECT_RESOLVED_BINARY" >&2
return 127
@@ -941,7 +964,7 @@ dispatch_fast_or_help() {
fi
key="${BINARY_TO_KEY[$DIRECT_RESOLVED_BINARY]}"
if [[ " ${remaining[*]} " == *" --json "* ]]; then
if remaining_has_json_flag "${remaining[@]}"; then
show_command_json "$key"
else
show_command_help "$key"
@@ -994,8 +1017,8 @@ dispatch_or_help() {
key="$RESOLVED_KEY"
remaining=("${args[@]:RESOLVED_COUNT}")
if (( ${#remaining[@]} > 0 )) && [[ ${remaining[0]} == "--help" || ${remaining[0]} == "-h" ]]; then
if [[ " ${remaining[*]} " == *" --json "* ]]; then
if remaining_has_help_flag "${remaining[@]}"; then
if remaining_has_json_flag "${remaining[@]}"; then
show_command_json "$key"
else
show_command_help "$key"
+105
View File
@@ -0,0 +1,105 @@
#!/bin/bash
# omarchy:summary=Launch the default coding agent in a terminal
# omarchy:args=[--inline] [--pick]
# omarchy:examples=omarchy agent | omarchy agent --inline
inline=false
pick=false
# Flags only. A bare prompt would shadow the subcommands under this group, so
# prompts go through omarchy-agent-prompt, which passes one here as --prompt.
while (($#)); do
case "$1" in
--inline)
inline=true
shift
;;
--pick)
pick=true
shift
;;
--prompt)
prompt=${2:?--prompt needs a value}
shift 2
;;
*)
echo "Unexpected argument: $1" >&2
echo "To pass a prompt: omarchy agent prompt \"$*\"" >&2
exit 1
;;
esac
done
# Agents refuse to remember trust for $HOME, so launches from the keybinding or
# menu start in the work directory instead of re-asking on every session.
[[ $PWD == "$HOME" && -d $HOME/Work ]] && cd "$HOME/Work"
agent=$(omarchy-default-agent)
# Omarchy ships without a default, so there is nothing to launch until one is
# picked. --pick offers the choice instead of the error, which is what the
# keybinding wants: a keypress that opens nothing explains nothing.
if [[ -z $agent ]]; then
[[ $pick == "true" ]] && exec omarchy-menu summon setup.default.agent
echo "Choose default agent with: omarchy default agent <name>" >&2
exit 1
fi
if omarchy-cmd-missing "$agent"; then
echo "$agent is not installed. Choose an installed agent with: omarchy default agent <name>" >&2
exit 1
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 has no approval prompts.
case "$agent" in
opencode)
command=(opencode --auto)
[[ -n ${prompt:-} ]] && command+=(--prompt "$prompt")
;;
gemini)
command=(gemini --yolo)
[[ -n ${prompt:-} ]] && command+=(--prompt-interactive "$prompt")
;;
copilot)
command=(copilot --allow-all)
[[ -n ${prompt:-} ]] && command+=(--interactive "$prompt")
;;
crush)
# --yolo belongs to the interactive command only; `crush run` never prompts.
if [[ -n ${prompt:-} ]]; then
command=(crush run "$prompt")
else
command=(crush --yolo)
fi
;;
claude | grok)
command=("$agent" --permission-mode bypassPermissions)
[[ -n ${prompt:-} ]] && command+=(-- "$prompt")
;;
codex)
command=(codex --dangerously-bypass-approvals-and-sandbox)
[[ -n ${prompt:-} ]] && command+=(-- "$prompt")
;;
omp)
command=(omp --auto-approve)
[[ -n ${prompt:-} ]] && command+=(-- "$prompt")
;;
pi)
command=(pi)
[[ -n ${prompt:-} ]] && command+=("$prompt")
;;
*)
echo "Unsupported default agent: $agent" >&2
exit 1
;;
esac
if [[ $inline == "true" ]]; then
exec "${command[@]}"
else
# A fixed app-id rather than the default org.omarchy.<binary>, so every agent
# window shares one class for window rules and themes to single out.
exec omarchy-launch-tui --app-id=org.omarchy.agent "${command[@]}"
fi
+52
View File
@@ -0,0 +1,52 @@
#!/bin/bash
# omarchy:summary=Diagnose a crashed process with the default coding agent
# omarchy:args=<pid> [comm] [exe] [signal]
# omarchy:examples=omarchy agent crash 1516893
# Clicked from a "Process crashed:" notification, or run by hand against any PID
# in `coredumpctl list`. The method lives in the diagnose-crash skill so it is
# edited in one place and works with whichever agent is default; this only
# gathers the facts and points at it.
set -euo pipefail
pid=${1:?usage: omarchy-agent-crash <pid> [comm] [exe] [signal]}
if [[ ! $pid =~ ^[0-9]+$ ]]; then
echo "Not a PID: $pid" >&2
echo "Usage: omarchy agent crash <pid> (see: coredumpctl list)" >&2
exit 1
fi
comm=${2:-unknown}
exe=${3:-unknown}
signal=${4:-unknown}
skill="$OMARCHY_PATH/default/agents/skills/diagnose-crash/SKILL.md"
# Looked up live so a hand-run PID still gets a timestamp. A rotated-away core
# only costs the timestamp, so failure is tolerated.
when=$(coredumpctl list "$pid" --no-pager --no-legend 2>/dev/null | tail -1 | cut -d' ' -f1-4) || true
when=${when:-unknown}
prompt=$(
cat <<PROMPT
A process crashed on this Omarchy machine and I want to know why.
What systemd-coredump recorded:
process: $comm
PID: $pid
binary: $exe
signal: $signal
time: $when
Use the diagnose-crash skill: it covers how to investigate, what to report, and
when a crash is worth reporting upstream to Omarchy. If your harness has no skill
mechanism, read the skill files directly and follow them instead:
$skill
PROMPT
)
exec omarchy-agent --prompt "$prompt"
+23
View File
@@ -0,0 +1,23 @@
#!/bin/bash
# omarchy:summary=Launch the default coding agent with a prompt
# omarchy:args=[--inline] <prompt...>
# omarchy:examples=omarchy agent prompt "Review this project"
# Prompts live here rather than on `omarchy agent`, where a bare prompt would
# shadow the subcommands under that group.
set -euo pipefail
inline=()
if [[ ${1:-} == "--inline" ]]; then
inline=(--inline)
shift
fi
if (($# == 0)); then
echo "Usage: omarchy agent prompt [--inline] <prompt...>" >&2
exit 1
fi
exec omarchy-agent "${inline[@]}" --prompt "$*"
+54 -11
View File
@@ -765,24 +765,67 @@ def probe_limits(access_token: str) -> dict[str, Any]:
return {"ok": True, "limits": limits}
# A cached percentage outlives the probe that measured it, but only until its
# window rolls over: once a window has reset, the figure describes a period
# that is over, and a stale 78% would misreport an allowance that is now
# untouched. A window with no reset time, or one that will not parse, is kept
# — an unreadable timestamp is no reason to throw away a real number.
def limit_window_open(entry: dict[str, Any], now: dt.datetime) -> bool:
raw = str(entry.get("resetsAt") or "")
if raw == "":
return True
try:
resets_at = dt.datetime.fromisoformat(raw.replace("Z", "+00:00"))
except Exception:
return True
if resets_at.tzinfo is None:
resets_at = resets_at.replace(tzinfo=dt.timezone.utc)
return resets_at > now
def usable_cached_limits(cached: dict[str, Any]) -> list[dict[str, Any]]:
entries = cached.get("limits")
if not isinstance(entries, list):
return []
now = dt.datetime.now(dt.timezone.utc)
return [entry for entry in entries if isinstance(entry, dict) and limit_window_open(entry, now)]
def collect_limits(access_token: str, expires_at_ms: int, force: bool) -> dict[str, Any]:
result = {"limits": [], "usageStatusText": "", "authHelpText": AUTH_HELP}
if access_token == "":
result["usageStatusText"] = "Waiting for auth"
return result
if expires_at_ms > 0 and expires_at_ms <= time.time() * 1000:
return result
# A panel that is opened and shut repeatedly must not turn into a request
# per flick, so recent probe results are reused for a short window — and
# kept as the answer of record when a later probe fails.
probe_cache = cache_root() / "claude-limits.json"
cached = read_fresh_json(probe_cache, float("inf")) or {}
fallback = usable_cached_limits(cached)
# Probing needs a live token and only the Claude Code CLI can mint one: it
# refreshes the credential file when it runs, so a machine left alone long
# enough finds the saved token lapsed. Say so — an empty limits list with
# nothing else set hides the whole section and explains nothing — and keep
# showing the last numbers whose window has not since reset.
if access_token == "":
result["limits"] = fallback
result["usageStatusText"] = "Waiting for auth"
return result
if expires_at_ms > 0 and expires_at_ms <= time.time() * 1000:
result["limits"] = fallback
result["usageStatusText"] = "Sign-in expired"
result["authHelpText"] = (
"Claude Code's saved sign-in expired"
+ (" — showing the last known limits." if fallback else ".")
+ " Start Claude Code, or run `claude auth login`, to refresh it."
)
return result
# --force is a person asking for fresh numbers, so it skips the reuse window
# entirely; the interval is there to absorb repeated panel opens, not to
# overrule someone who pressed refresh.
fetched_at = number(cached.get("fetchedAtMs")) / 1000
min_interval = 0 if force else PROBE_MIN_INTERVAL_SECONDS
if cached.get("limits") and time.time() - fetched_at < max(min_interval, PROBE_MIN_INTERVAL_SECONDS):
result["limits"] = cached["limits"]
if fallback and not force and time.time() - fetched_at < PROBE_MIN_INTERVAL_SECONDS:
result["limits"] = fallback
return result
probe = probe_limits(access_token)
@@ -795,8 +838,8 @@ def collect_limits(access_token: str, expires_at_ms: int, force: bool) -> dict[s
# route. Ask the shell to try again sooner than its regular interval.
if probe.get("transport"):
result["retryAdvised"] = True
if cached.get("limits"):
result["limits"] = cached["limits"]
if fallback:
result["limits"] = fallback
else:
result["usageStatusText"] = "Claude limits unavailable"
result["authHelpText"] = probe["helpText"]
+196 -24
View File
@@ -11,12 +11,16 @@ panel only ever reads the JSON this prints.
"""
import argparse
import fcntl
import hashlib
import json
import os
import select
import shutil
import sqlite3
import subprocess
import sys
import tempfile
import time
from datetime import datetime, timedelta, timezone
from pathlib import Path
@@ -25,6 +29,14 @@ AGENT_ID = "codex"
AGENT_NAME = "Codex"
AUTH_HELP = "Run `codex login` to authenticate."
# A scan this recent is only reused to dedup concurrent collector runs (the
# update command backgrounds one per agent while the panel refreshes on its
# own); every periodic widget refresh lands a real rescan, however low
# refreshIntervalSec is set. --limits-only promises only fresh limits, so it
# may reuse a scan for up to 15 minutes.
SCAN_REUSE_SECONDS = 20
LIMITS_ONLY_REUSE_SECONDS = 900
def local_day(value):
if value is None:
@@ -197,16 +209,47 @@ def scan_opencode_sessions():
# A subscription burned entirely through opencode leaves no native session
# files, but opencode records per-message provider, model, and token usage
# in its own database. Read-only: opencode may be writing right now.
# Returns whether the scan ran to completion: a scan cut short by a
# database error still contributes what it read, but must not be cached
# as if it were the whole story.
db = Path(os.environ.get("XDG_DATA_HOME") or (Path.home() / ".local" / "share")) / "opencode" / "opencode.db"
if not db.is_file():
return
return True
try:
conn = sqlite3.connect(db.resolve().as_uri() + "?mode=ro", uri=True, timeout=2)
except sqlite3.Error:
return
return False
try:
conn.execute("PRAGMA query_only = ON")
for session_id, raw in conn.execute("SELECT session_id, data FROM message"):
# OpenCode DBs grow huge (every historical message, JSON included), and
# Python-side json.loads of every row wasted ~600 MB of RSS on machines
# whose subscription never ran on OpenAI. The json_extract conditions are
# the authority for the rows that reach them: role == "assistant" and
# providerID == "openai", the same exact-match values the Python filter
# below checks. The guards in front are pure acceleration, not a perfect
# proxy for the Python filter:
# - The LIKE gates skip rows whose JSON cannot contain the two
# key/value pairs, avoiding the JSON parse of tens-of-MB blobs. They
# can differ from json.loads on duplicate keys (Python keeps the
# last, SQLite json_extract keeps the first) and ASCII-escaped
# values (\"ass\\u0069stant\" decodes for Python but not for LIKE),
# so a row the authority would accept can be gated out. Both cases
# are vanishingly rare in real opencode data.
# - json_valid guards the parse itself: json_extract() RAISES on
# malformed JSON instead of returning NULL, and one such row would
# otherwise abort the whole scan. SQLite does not promise that AND
# terms evaluate left to right, so the guard is a CASE around each
# json_extract rather than a separate AND term. Rows that are not
# well-formed JSON are skipped here; the per-row try/except below
# stays as the final safety net for rows that pass the SQL filter
# but fail json.loads.
for session_id, raw in conn.execute(
"SELECT session_id, data FROM message"
" WHERE data LIKE '%\"role\"%:%\"assistant\"%'"
" AND data LIKE '%\"providerID\"%:%\"openai\"%'"
" AND CASE WHEN json_valid(data) THEN json_extract(data, '$.role') END = 'assistant'"
" AND CASE WHEN json_valid(data) THEN json_extract(data, '$.providerID') END = 'openai'"
):
# One malformed row must not abort the scan, so every shape assumption
# lives inside the try.
try:
@@ -232,9 +275,12 @@ def scan_opencode_sessions():
continue
add_usage(day, "opencode:" + str(session_id), model, input_tokens, output_tokens, cache_read, cache_write)
except sqlite3.Error:
pass
# Transient lock, schema migration, corruption: the numbers stop here,
# incomplete.
return False
finally:
conn.close()
return True
def scan_native_codex_sessions():
@@ -291,6 +337,143 @@ def scan_native_codex_sessions():
continue
def cache_root():
root = Path(os.environ.get("XDG_CACHE_HOME") or (Path.home() / ".cache")) / "omarchy" / "agent-usage"
root.mkdir(parents=True, exist_ok=True)
return root
def scan_cache_paths():
codex_home = Path(os.environ.get("CODEX_HOME") or (Path.home() / ".codex"))
db = Path(os.environ.get("XDG_DATA_HOME") or (Path.home() / ".local" / "share")) / "opencode" / "opencode.db"
# The digest covers every data path the scan reads: the codex session
# roots, the opencode DB, and (via Path.home()) the pi/omp session roots.
digest = hashlib.sha1((str(Path.home()) + "\n" + str(codex_home) + "\n" + str(db)).encode("utf-8")).hexdigest()[:16]
root = cache_root()
return root / f"codex-scan-{digest}.json", root / f"codex-scan-{digest}.lock"
def read_fresh_json(path, max_age_seconds):
if max_age_seconds <= 0 or not path.exists():
return None
try:
# A negative age means the mtime is in the future: the clock moved
# backwards since the write, so the cache's freshness cannot be trusted.
age = time.time() - path.stat().st_mtime
if 0 <= age <= max_age_seconds:
return json.loads(path.read_text(encoding="utf-8"))
except Exception:
return None
return None
def write_json(path, payload):
# A temp name unique to this writer, not derived from the target: several
# collectors can run at once (the update command backgrounds one per agent,
# the panel refreshes on its own), and a shared temp path means the second
# replace finds the first one's file already moved away.
handle_fd, tmp_name = tempfile.mkstemp(dir=path.parent, prefix=path.name + ".", suffix=".tmp")
tmp = Path(tmp_name)
try:
with os.fdopen(handle_fd, "w", encoding="utf-8") as handle:
handle.write(json.dumps(payload, separators=(",", ":")) + "\n")
# mkstemp opens at 0600; nothing in the cache is sensitive, so open it
# up to the usual 0644.
tmp.chmod(0o644)
tmp.replace(path)
except BaseException:
tmp.unlink(missing_ok=True)
raise
# The cache payload is a versioned envelope around the local-stats dict, so a
# corrupted or foreign-shaped file is a cache miss (rescan + rewrite) instead
# of a crash or a garbage record.
def read_cached_stats(cache_file, max_age_seconds):
cached = read_fresh_json(cache_file, max_age_seconds)
if not isinstance(cached, dict) or cached.get("schemaVersion") != 1:
return None
# today* fields only mean "today" on the day they were scanned. A cache
# from another local date (midnight passed, or the clock moved) is a miss,
# not merely old, whatever its mtime says.
if cached.get("scanDate") != today:
return None
stats = cached.get("stats")
if not isinstance(stats, dict):
return None
if not all(key in stats for key in ("todayPrompts", "todayTotalTokens", "recentDays", "activeDates", "modelUsage")):
return None
return stats
def write_cached_stats(cache_file, stats):
try:
write_json(cache_file, {"schemaVersion": 1, "scanDate": today, "stats": stats})
except Exception as exc:
print(f"omarchy-agent-usage-codex: could not write usage cache ({exc})", file=sys.stderr)
def local_stats():
"""Snapshot the aggregated local usage into the record's stats dict."""
return {
"todayPrompts": today_prompts,
"todaySessions": len(today_sessions),
"todayTotalTokens": today_total_tokens,
"todayTokensByModel": today_tokens_by_model,
"recentDays": [recent[day] for day in recent_dates],
"totalPrompts": total_prompts,
"totalSessions": len(total_sessions),
# Days with any recorded usage, for the all-time "N days" summary. The
# dates travel too: merging snapshots from several machines needs their
# union, which a count alone cannot give.
"activeDays": len(active_days),
"activeDates": sorted(active_days),
"modelUsage": model_usage,
}
def run_local_scans():
scan_pi_sessions()
scan_native_codex_sessions()
complete = scan_opencode_sessions()
return local_stats(), complete
def cached_local_stats(max_age):
"""Local stats, with the cache as a pure optimization.
The cache must never take the collector down: any cache-layer failure
(unwritable cache root, lock errors, disk full) degrades to a direct scan
and a warning on stderr. The JSON record is the contract; the cache is not.
"""
try:
return _cached_local_stats(max_age)
except Exception as exc:
print(f"omarchy-agent-usage-codex: cache unavailable ({exc}); scanning directly", file=sys.stderr)
stats, _ = run_local_scans()
return stats
def _cached_local_stats(max_age):
cache_file, lock_file = scan_cache_paths()
cached = read_cached_stats(cache_file, max_age)
if cached is not None:
return cached
with lock_file.open("w") as lock:
fcntl.flock(lock, fcntl.LOCK_EX)
cached = read_cached_stats(cache_file, max_age)
if cached is not None:
return cached
stats, complete = run_local_scans()
# An interrupted scan still serves this run, but caching it would
# suppress the missing usage for every reader until the cache expires.
if complete:
write_cached_stats(cache_file, stats)
return stats
def rpc_request(proc, request_id, method, params=None, timeout=8):
payload = {"id": request_id, "method": method, "params": params or {}}
proc.stdin.write(json.dumps(payload) + "\n")
@@ -390,16 +573,17 @@ def fetch_codex_rpc():
def main():
parser = argparse.ArgumentParser()
# Local stats and limits come from the same cheap scan, so there is no
# cache to force past and no faster limits-only path. The flags exist so
# every collector accepts the same invocation.
# --force rescans everything and rewrites the cache. --limits-only is kept
# for CLI compatibility with the panel's refreshLimits() call: only the
# limits probe must be fresh, so it may reuse a scan for far longer than a
# normal run, whose short window exists purely to dedup concurrent
# collector runs.
parser.add_argument("--force", action="store_true")
parser.add_argument("--limits-only", action="store_true")
parser.parse_args()
args = parser.parse_args()
scan_pi_sessions()
scan_native_codex_sessions()
scan_opencode_sessions()
max_age = 0 if args.force else (LIMITS_ONLY_REUSE_SECONDS if args.limits_only else SCAN_REUSE_SECONDS)
stats = cached_local_stats(max_age)
rpc = fetch_codex_rpc()
record = {
@@ -409,20 +593,8 @@ def main():
"updatedAt": datetime.now(timezone.utc).isoformat(),
"ready": True,
"hasLocalStats": True,
"todayPrompts": today_prompts,
"todaySessions": len(today_sessions),
"todayTotalTokens": today_total_tokens,
"todayTokensByModel": today_tokens_by_model,
"recentDays": [recent[day] for day in recent_dates],
"totalPrompts": total_prompts,
"totalSessions": len(total_sessions),
# Days with any recorded usage, for the all-time "N days" summary. The
# dates travel too: merging snapshots from several machines needs their
# union, which a count alone cannot give.
"activeDays": len(active_days),
"activeDates": sorted(active_days),
"modelUsage": model_usage,
}
record.update(stats)
record.update(rpc)
print(json.dumps(record, separators=(",", ":")))
+1 -1
View File
@@ -64,7 +64,7 @@ start_webcam_overlay() {
# Auto-detect first available webcam if none specified
if [[ -z $WEBCAM_DEVICE ]]; then
WEBCAM_DEVICE=$(v4l2-ctl --list-devices 2>/dev/null | grep -m1 "^[[:space:]]*/dev/video" | tr -d '\t')
WEBCAM_DEVICE=$(omarchy-capture-webcam-list | sed -n '1s/[[:space:]].*//p')
if [[ -z $WEBCAM_DEVICE ]]; then
omarchy-notification-send -u critical -t 3000 "No webcam devices found"
return 1
@@ -3,21 +3,7 @@
# omarchy:summary=Pick a webcam and start a screen recording with it
# omarchy:examples=omarchy capture screenrecording-with-webcam
set -o pipefail
webcam_list() {
v4l2-ctl --list-devices 2>/dev/null | while IFS= read -r line; do
if [[ $line != $'\t'* && -n $line ]]; then
local name="$line"
local device
IFS= read -r device || break
device=$(tr -d '\t' <<<"$device" | head -1)
[[ -n $device ]] && echo "$device $name"
fi
done
}
mapfile -t devices < <(webcam_list)
mapfile -t devices < <(omarchy-capture-webcam-list)
if (( ${#devices[@]} == 0 )); then
omarchy-notification-send "No webcam devices found" -u critical -t 3000
exit 1
+33
View File
@@ -0,0 +1,33 @@
#!/bin/bash
# omarchy:summary=List webcam devices that support video capture
# omarchy:hidden=true
capture_capable() {
local device="$1"
v4l2-ctl --device "$device" --info 2>/dev/null | awk '
/^[[:space:]]*Device Caps[[:space:]]*:/ { inspect = 1; next }
inspect && /^[[:space:]]*Video Capture/ { found = 1 }
END { exit !found }
'
}
name=""
emitted=0
while IFS= read -r line; do
if [[ -n $line && $line != [[:space:]]* ]]; then
name="$line"
emitted=0
elif (( ! emitted )); then
device="${line#"${line%%[![:space:]]*}"}"
if [[ $device == /dev/video* ]] && capture_capable "$device"; then
emitted=1
printf '%s %s\n' "$device" "$name"
fi
fi
done < <(v4l2-ctl --list-devices 2>/dev/null)
exit 0
+86
View File
@@ -0,0 +1,86 @@
#!/bin/bash
# omarchy:summary=Watch for process crashes and offer an AI diagnosis
# omarchy:hidden=true
# systemd-coredump journals every core dump under a known MESSAGE_ID with
# structured COREDUMP_* fields, which carry more than the core filenames do.
set -uo pipefail
# See systemd.journal-fields(7).
readonly COREDUMP_MESSAGE_ID=fc2e22bc6ee647b6b90729ab34a250b1
# nf-md-robot_dead, escaped so this file reads without a Nerd Font.
readonly CRASH_GLYPH=$'\U000f16a1'
# Crash loops dump core repeatedly, so announce each program at most once a
# window.
readonly dedupe_seconds=${OMARCHY_CRASH_DEDUPE_SECONDS:-60}
# Extended regex of process names never worth announcing.
readonly ignore_pattern=${OMARCHY_CRASH_IGNORE:-}
declare -A last_notified
announce() {
local comm=$1 pid=$2 exe=$3 signal=$4 exec_command
exec_command=$(printf 'omarchy-agent-crash %q %q %q %q' "$pid" "$comm" "$exe" "$signal")
# The shell owns org.freedesktop.Notifications, so a shell crash takes the
# notification server down with it and a toast sent into that gap is lost.
# Wait for the restarted shell to claim the name again: the crash least
# likely to be delivered is the one most worth reporting.
omarchy-notification-wait || return 1
# --exec rather than a libnotify action: the shell runs clicks from its own
# omarchy-exec hint and never emits ActionInvoked. Keeps the default
# "omarchy-action" app name too, the only one shouldBypassDnd() lets through.
omarchy-notification-send \
--urgency critical \
--glyph "$CRASH_GLYPH" \
--exec "$exec_command" \
"Process crashed: $comm" \
"Click to diagnose with AI"
}
# -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
IFS=$'\t' read -r uid comm pid exe signal < <(
jq -r '[(._UID // "-"),
(.COREDUMP_COMM // "-"),
(.COREDUMP_PID // "-"),
(.COREDUMP_EXE // "-"),
(.COREDUMP_SIGNAL_NAME // "-")] | @tsv' <<<"$entry" 2>/dev/null
)
[[ $pid =~ ^[0-9]+$ ]] || continue
# The toast only offers a diagnosis, so it has nothing to offer until an
# agent is chosen. Checked per crash, not at startup, so picking one takes
# effect without restarting this service.
[[ -n $(omarchy-default-agent) ]] || continue
# Only this user's crashes; a daemon dumping core is a sysadmin's problem.
[[ $uid =~ ^[0-9]+$ ]] || continue
((uid == UID)) || continue
# comm is truncated to 15 characters, so prefer the executable's basename.
name=$comm
[[ $exe == /* ]] && name=${exe##*/}
[[ -n $ignore_pattern && $name =~ $ignore_pattern ]] && continue
# Never announce our own machinery, or it notifies about itself.
[[ $name == omarchy-crash-* || $name == omarchy-agent-* ]] && continue
now=$EPOCHSECONDS
(((now - ${last_notified[$name]:-0}) < dedupe_seconds)) && continue
# Only a delivered toast starts the dedupe window. A failed send that
# counted would suppress the rest of a crash loop for a minute, and
# `journalctl -n 0` never replays what was missed.
announce "$name" "$pid" "$exe" "$signal" && last_notified[$name]=$now
done
+5 -3
View File
@@ -17,7 +17,9 @@ if (($# == 0)); then
read -r agent <"$agent_file"
fi
[[ -n $agent ]] && echo "$agent" || echo "opencode"
# Silent when unset rather than defaulting: Omarchy picks no agent for you, so
# the menu leaves every entry unchecked until one is chosen.
[[ -n ${agent:-} ]] && echo "$agent"
exit 0
fi
@@ -57,7 +59,7 @@ printf '%s\n' "$agent" >"$agent_file"
if [[ $installing == "true" ]]; then
printf '\033[2J\033[3J\033[H'
exec omarchy-launch-agent --inline
exec omarchy-agent --inline
else
exec omarchy-launch-agent
exec omarchy-agent
fi
+652
View File
@@ -0,0 +1,652 @@
#!/usr/bin/python3
# omarchy:summary=Add branded glyphs to the Omarchy icon font
# omarchy:args=[list|add] <name> <svg-file-or-url> [--codepoint U+E9xx] [--font PATH]
# omarchy:examples=omarchy dev font list | omarchy dev font add ollama https://simpleicons.org/icons/ollama.svg
"""Append monochrome SVG marks to default/fonts/omarchy/omarchy.ttf.
The font is a private-use icon font: the menu renders a mark by setting
"iconFont":"omarchy" on an entry and using the glyph's codepoint as its
icon. Adding a mark means appending a glyph here, then pointing a menu
entry at the codepoint this prints.
Marks must be monochrome single-path SVGs so the menu can draw them in the
active theme's foreground and selection colors. Brand icon sets like
simpleicons.org publish exactly that shape; app favicons often do not.
The glyph is scaled into the same 64..960 box the existing marks use, so a
new mark lands at the same optical size as the ones already in the font.
"""
import argparse
import math
import os
import re
import struct
import sys
import urllib.request
UPEM = 1024
ART_BOX = (64, 64, 960, 960) # the box every existing mark is drawn in
TOL = 0.6 # cubic -> quadratic error tolerance, in font units
PUA_FIRST = 0xE900
# --- SVG path parsing ------------------------------------------------------
NUM = re.compile(r'[-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?')
CMD = re.compile(r'[MmZzLlHhVvCcSsQqTtAa]')
def tokenize(d):
out, i = [], 0
while i < len(d):
c = d[i]
if CMD.match(c):
out.append(c)
i += 1
elif c in ' ,\t\r\n':
i += 1
else:
m = NUM.match(d, i)
if not m:
raise ValueError('bad path data at %d: %r' % (i, d[i:i + 20]))
out.append(float(m.group()))
i = m.end()
return out
def arc_to_cubics(p0, rx, ry, phi, large, sweep, p1):
"""Endpoint-parameterized SVG arc -> cubic segments."""
if p0 == p1:
return []
rx, ry = abs(rx), abs(ry)
if rx == 0 or ry == 0:
return [('L', p1)]
phi = math.radians(phi % 360)
cosp, sinp = math.cos(phi), math.sin(phi)
dx2, dy2 = (p0[0] - p1[0]) / 2.0, (p0[1] - p1[1]) / 2.0
x1 = cosp * dx2 + sinp * dy2
y1 = -sinp * dx2 + cosp * dy2
lam = x1 * x1 / (rx * rx) + y1 * y1 / (ry * ry)
if lam > 1:
s = math.sqrt(lam)
rx, ry = rx * s, ry * s
num = rx * rx * ry * ry - rx * rx * y1 * y1 - ry * ry * x1 * x1
den = rx * rx * y1 * y1 + ry * ry * x1 * x1
co = math.sqrt(max(0.0, num / den)) if den else 0.0
if large == sweep:
co = -co
cx1 = co * rx * y1 / ry
cy1 = -co * ry * x1 / rx
cx = cosp * cx1 - sinp * cy1 + (p0[0] + p1[0]) / 2.0
cy = sinp * cx1 + cosp * cy1 + (p0[1] + p1[1]) / 2.0
def angle(ux, uy, vx, vy):
dot = ux * vx + uy * vy
n = math.hypot(ux, uy) * math.hypot(vx, vy)
a = math.acos(max(-1.0, min(1.0, dot / n))) if n else 0.0
return -a if ux * vy - uy * vx < 0 else a
theta = angle(1, 0, (x1 - cx1) / rx, (y1 - cy1) / ry)
delta = angle((x1 - cx1) / rx, (y1 - cy1) / ry, (-x1 - cx1) / rx, (-y1 - cy1) / ry)
if not sweep and delta > 0:
delta -= 2 * math.pi
elif sweep and delta < 0:
delta += 2 * math.pi
segs = []
n = max(1, int(math.ceil(abs(delta) / (math.pi / 2) - 1e-9)))
step = delta / n
k = 4.0 / 3.0 * math.tan(step / 4.0)
def at(t):
x, y = rx * math.cos(t), ry * math.sin(t)
return (cosp * x - sinp * y + cx, sinp * x + cosp * y + cy)
def deriv(t):
x, y = -rx * math.sin(t), ry * math.cos(t)
return (cosp * x - sinp * y, sinp * x + cosp * y)
for i in range(n):
t0 = theta + i * step
t1 = t0 + step
a, b = at(t0), at(t1)
da, db = deriv(t0), deriv(t1)
segs.append(('C',
(a[0] + k * da[0], a[1] + k * da[1]),
(b[0] - k * db[0], b[1] - k * db[1]),
b))
return segs
def parse_path(d):
"""Return one list of absolute segments per subpath."""
t = tokenize(d)
subs, cur = [], None
pos = start = (0.0, 0.0)
prev_c = prev_q = None
i, cmd = 0, None
while i < len(t):
if isinstance(t[i], str):
cmd = t[i]
i += 1
rel = cmd.islower()
c = cmd.upper()
def take(n):
nonlocal i
v = t[i:i + n]
i += n
return v
def abspt(x, y):
return (pos[0] + x, pos[1] + y) if rel else (x, y)
if c == 'M':
x, y = take(2)
pos = start = abspt(x, y)
if cur:
subs.append(cur)
cur = [('M', pos)]
cmd = 'l' if rel else 'L' # implicit lineto for extra pairs
prev_c = prev_q = None
elif c == 'Z':
if cur:
subs.append(cur)
cur = None
pos = start
prev_c = prev_q = None
elif c in 'LHV':
if c == 'L':
x, y = take(2)
p = abspt(x, y)
elif c == 'H':
x = take(1)[0]
p = (pos[0] + x, pos[1]) if rel else (x, pos[1])
else:
y = take(1)[0]
p = (pos[0], pos[1] + y) if rel else (pos[0], y)
cur.append(('L', p))
pos = p
prev_c = prev_q = None
elif c in 'CS':
if c == 'C':
x1, y1, x2, y2, x, y = take(6)
c1, c2, p = abspt(x1, y1), abspt(x2, y2), abspt(x, y)
else:
x2, y2, x, y = take(4)
c2, p = abspt(x2, y2), abspt(x, y)
c1 = (2 * pos[0] - prev_c[0], 2 * pos[1] - prev_c[1]) if prev_c else pos
cur.append(('C', c1, c2, p))
prev_c, prev_q = c2, None
pos = p
elif c in 'QT':
if c == 'Q':
x1, y1, x, y = take(4)
q, p = abspt(x1, y1), abspt(x, y)
else:
x, y = take(2)
p = abspt(x, y)
q = (2 * pos[0] - prev_q[0], 2 * pos[1] - prev_q[1]) if prev_q else pos
cur.append(('C',
(pos[0] + 2.0 / 3 * (q[0] - pos[0]), pos[1] + 2.0 / 3 * (q[1] - pos[1])),
(p[0] + 2.0 / 3 * (q[0] - p[0]), p[1] + 2.0 / 3 * (q[1] - p[1])),
p))
prev_q, prev_c = q, None
pos = p
elif c == 'A':
rx, ry, rot, large, sweep, x, y = take(7)
p = abspt(x, y)
cur.extend(arc_to_cubics(pos, rx, ry, rot, int(large), int(sweep), p))
pos = p
prev_c = prev_q = None
else:
raise ValueError('unsupported path command %r' % cmd)
if cur:
subs.append(cur)
return subs
# --- outline conversion ----------------------------------------------------
def cubic_to_quads(p0, p1, p2, p3, tol, depth=0):
"""Approximate one cubic with quadratics: [(control, end), ...]."""
ex = p0[0] - 3 * p1[0] + 3 * p2[0] - p3[0]
ey = p0[1] - 3 * p1[1] + 3 * p2[1] - p3[1]
if math.sqrt(3) / 36 * math.hypot(ex, ey) <= tol or depth >= 8:
return [(((3 * p1[0] - p0[0] + 3 * p2[0] - p3[0]) / 4.0,
(3 * p1[1] - p0[1] + 3 * p2[1] - p3[1]) / 4.0), p3)]
def mid(a, b):
return ((a[0] + b[0]) / 2.0, (a[1] + b[1]) / 2.0)
p01, p12, p23 = mid(p0, p1), mid(p1, p2), mid(p2, p3)
p012, p123 = mid(p01, p12), mid(p12, p23)
m = mid(p012, p123)
return (cubic_to_quads(p0, p01, p012, m, tol, depth + 1) +
cubic_to_quads(m, p123, p23, p3, tol, depth + 1))
def contours_from_svg(path_d, view):
"""SVG path -> TrueType contours [[(x, y, on_curve), ...], ...]."""
vbx, vby, vbw, vbh = view
ax0, ay0, ax1, ay1 = ART_BOX
scale = min((ax1 - ax0) / vbw, (ay1 - ay0) / vbh)
ox = ax0 + ((ax1 - ax0) - vbw * scale) / 2.0
oy = ay0 + ((ay1 - ay0) - vbh * scale) / 2.0
def tf(p):
# SVG y grows down, font y grows up.
return (ox + (p[0] - vbx) * scale, oy + (vbh - (p[1] - vby)) * scale)
contours = []
for sub in parse_path(path_d):
pts, cursor = [], None
for seg in sub:
if seg[0] in ('M', 'L'):
cursor = tf(seg[1])
pts.append((cursor[0], cursor[1], True))
elif seg[0] == 'C':
c1, c2, p = tf(seg[1]), tf(seg[2]), tf(seg[3])
for q, end in cubic_to_quads(cursor, c1, c2, p, TOL):
pts.append((q[0], q[1], False))
pts.append((end[0], end[1], True))
cursor = p
if len(pts) > 2:
contours.append(clean(pts))
return orient(contours)
def clean(pts):
"""Round to integers, drop duplicates and the redundant closing point."""
out = []
for x, y, on in pts:
p = (int(round(x)), int(round(y)), on)
if not out or out[-1] != p:
out.append(p)
while len(out) > 1 and out[-1][:2] == out[0][:2] and out[-1][2] and out[0][2]:
out.pop()
return out
def area(pts):
a = 0.0
for i in range(len(pts)):
x0, y0 = pts[i][0], pts[i][1]
x1, y1 = pts[(i + 1) % len(pts)][0], pts[(i + 1) % len(pts)][1]
a += x0 * y1 - x1 * y0
return a / 2.0
def orient(contours):
"""TrueType draws outer contours clockwise (negative shoelace area).
Flipping the whole set preserves each hole's direction relative to its
outer contour, which is what keeps counters (eyes, cutouts) unfilled.
"""
if contours and area(max(contours, key=lambda c: abs(area(c)))) > 0:
return [list(reversed(c)) for c in contours]
return contours
# --- glyph and table encoding ----------------------------------------------
def encode_glyph(contours):
if not contours:
return b''
xs = [p[0] for c in contours for p in c]
ys = [p[1] for c in contours for p in c]
out = [struct.pack('>hhhhh', len(contours), min(xs), min(ys), max(xs), max(ys))]
ends, n = [], 0
for c in contours:
n += len(c)
ends.append(n - 1)
out.append(struct.pack('>%dH' % len(ends), *ends))
out.append(struct.pack('>H', 0)) # no instructions
flags, xdel, ydel = [], [], []
px = py = 0
for c in contours:
for x, y, on in c:
dx, dy = x - px, y - py
px, py = x, y
f = 1 if on else 0
if dx == 0:
f |= 0x10
elif -255 <= dx <= 255:
f |= 0x02 | (0x10 if dx > 0 else 0)
xdel.append(struct.pack('>B', abs(dx)))
else:
xdel.append(struct.pack('>h', dx))
if dy == 0:
f |= 0x20
elif -255 <= dy <= 255:
f |= 0x04 | (0x20 if dy > 0 else 0)
ydel.append(struct.pack('>B', abs(dy)))
else:
ydel.append(struct.pack('>h', dy))
flags.append(struct.pack('>B', f))
data = b''.join(out + flags + xdel + ydel)
return data + b'\0' * (-len(data) % 4)
def read_tables(d):
t = {}
for i in range(struct.unpack('>H', d[4:6])[0]):
off = 12 + i * 16
tag = d[off:off + 4].decode('latin-1')
s, l = struct.unpack('>II', d[off + 8:off + 16])
t[tag] = (s, l)
return t
def checksum(data):
data += b'\0' * (-len(data) % 4)
return sum(struct.unpack('>%dI' % (len(data) // 4), data)) & 0xFFFFFFFF
def read_loca(d, t, ng):
ls = t['loca'][0]
if struct.unpack('>h', d[t['head'][0] + 50:t['head'][0] + 52])[0] == 0:
return [x * 2 for x in struct.unpack('>%dH' % (ng + 1), d[ls:ls + 2 * (ng + 1)])]
return list(struct.unpack('>%dI' % (ng + 1), d[ls:ls + 4 * (ng + 1)]))
def read_cmap(d, cs):
"""Read the format 12 subtable; it carries every mapping the font has."""
m = {}
for i in range(struct.unpack('>H', d[cs + 2:cs + 4])[0]):
pid, eid, off = struct.unpack('>HHI', d[cs + 4 + i * 8:cs + 12 + i * 8])
sub = cs + off
if struct.unpack('>H', d[sub:sub + 2])[0] != 12:
continue
for j in range(struct.unpack('>I', d[sub + 12:sub + 16])[0]):
s, e, g = struct.unpack('>III', d[sub + 16 + j * 12:sub + 28 + j * 12])
for c in range(s, e + 1):
m[c] = g + (c - s)
return m
def read_names(d, ps, ng):
n = struct.unpack('>H', d[ps + 32:ps + 34])[0]
idx = list(struct.unpack('>%dH' % n, d[ps + 34:ps + 34 + n * 2]))
p = ps + 34 + n * 2
pool = []
need = max([i - 258 + 1 for i in idx if i >= 258] or [0])
while len(pool) < need:
ln = d[p]
pool.append(d[p + 1:p + 1 + ln].decode('latin-1'))
p += 1 + ln
out = [pool[i - 258] if i >= 258 else '#mac%d' % i for i in idx]
return out + ['#mac0'] * (ng - len(out))
def build_post(d, ps, names):
idx, pool = [], []
for nm in names:
if nm.startswith('#mac'):
idx.append(int(nm[4:]))
else:
idx.append(258 + len(pool))
pool.append(nm)
body = struct.pack('>H', len(idx)) + struct.pack('>%dH' % len(idx), *idx)
for nm in pool:
body += struct.pack('>B', len(nm)) + nm.encode('latin-1')
return d[ps:ps + 32] + body
def build_cmap(m):
"""Emit format 4 and 12 subtables under the usual encoding records."""
groups = []
for c in sorted(m):
if groups and c == groups[-1][1] + 1 and m[c] == groups[-1][2] + (c - groups[-1][0]):
groups[-1][1] = c
else:
groups.append([c, c, m[c]])
segs = [(a, b, g) for a, b, g in groups if b <= 0xFFFF] + [(0xFFFF, 0xFFFF, 0)]
count = len(segs)
pow2 = 2 ** (count.bit_length() - 1) * 2
f4 = struct.pack('>HHHHHHH', 4, 16 + count * 8, 0, count * 2,
pow2, count.bit_length() - 1, count * 2 - pow2)
f4 += struct.pack('>%dH' % count, *[s[1] for s in segs])
f4 += struct.pack('>H', 0)
f4 += struct.pack('>%dH' % count, *[s[0] for s in segs])
deltas = [((s[2] - s[0]) & 0xFFFF) if s[0] != 0xFFFF else 1 for s in segs]
f4 += struct.pack('>%dh' % count, *[x - 65536 if x > 32767 else x for x in deltas])
f4 += struct.pack('>%dH' % count, *([0] * count))
f12 = struct.pack('>HHIII', 12, 0, 16 + len(groups) * 12, 0, len(groups))
for a, b, g in groups:
f12 += struct.pack('>III', a, b, g)
f0 = struct.pack('>HHH', 0, 262, 0) + bytes(256)
body, offsets = b'', {}
for data in (f4, f12, f0):
offsets[id(data)] = len(body)
body += data
records = [(0, 3, f4), (0, 4, f12), (1, 0, f0), (3, 1, f4), (3, 10, f12)]
head = struct.pack('>HH', 0, len(records))
base = 4 + len(records) * 8
for pid, eid, data in records:
head += struct.pack('>HHI', pid, eid, base + offsets[id(data)])
return head + body
def assemble(tables):
tags = sorted(tables)
n = len(tags)
sr = 2 ** (n.bit_length() - 1) * 16
font = struct.pack('>IHHHH', 0x00010000, n, sr, n.bit_length() - 1, n * 16 - sr)
offset = 12 + n * 16
body, records = b'', []
for tag in tags:
data = tables[tag]
records.append((tag, checksum(data), offset + len(body), len(data)))
body += data + b'\0' * (-len(data) % 4)
font += b''.join(struct.pack('>4sIII', t.encode('latin-1'), c, o, l)
for t, c, o, l in records)
font += body
adj = (0xB1B0AFBA - checksum(font)) & 0xFFFFFFFF
hs = [o for t, c, o, l in records if t == 'head'][0]
return font[:hs + 8] + struct.pack('>I', adj) + font[hs + 12:]
# --- commands --------------------------------------------------------------
def load_font(path):
d = open(path, 'rb').read()
t = read_tables(d)
ng = struct.unpack('>H', d[t['maxp'][0] + 4:t['maxp'][0] + 6])[0]
return d, t, ng
def glyph_list(path):
d, t, ng = load_font(path)
loca = read_loca(d, t, ng)
names = read_names(d, t['post'][0], ng)
gs = t['glyf'][0]
rows = []
for cp, gid in sorted(read_cmap(d, t['cmap'][0]).items()):
if cp < PUA_FIRST:
continue
a, b = loca[gid], loca[gid + 1]
box = ''
if b > a:
_, x0, y0, x1, y1 = struct.unpack('>hhhhh', d[gs + a:gs + a + 10])
box = '%d x %d' % (x1 - x0, y1 - y0)
name = names[gid]
rows.append((cp, '' if name.startswith('#mac') else name, box))
return rows
def add_glyph(font_path, codepoint, name, contours):
d, t, ng = load_font(font_path)
loca = read_loca(d, t, ng)
gs = t['glyf'][0]
glyphs = [d[gs + loca[i]:gs + loca[i + 1]] for i in range(ng)]
nhm = struct.unpack('>H', d[t['hhea'][0] + 34:t['hhea'][0] + 36])[0]
hm = t['hmtx'][0]
adv, lsb = [], []
for i in range(ng):
if i < nhm:
a, b = struct.unpack('>Hh', d[hm + i * 4:hm + i * 4 + 4])
else:
a = adv[-1]
b = struct.unpack('>h', d[hm + nhm * 4 + (i - nhm) * 2:][:2])[0]
adv.append(a)
lsb.append(b)
names = read_names(d, t['post'][0], ng)
cmap = read_cmap(d, t['cmap'][0])
gid = len(glyphs)
glyphs.append(encode_glyph(contours))
xs = [p[0] for c in contours for p in c]
adv.append(UPEM)
lsb.append(min(xs) if xs else 0)
names.append(name)
cmap[codepoint] = gid
ng = len(glyphs)
offs, o = [0], 0
for g in glyphs:
o += len(g)
offs.append(o)
short = offs[-1] <= 0x1FFFE and all(x % 2 == 0 for x in offs)
hhea = bytearray(d[t['hhea'][0]:t['hhea'][0] + t['hhea'][1]])
struct.pack_into('>H', hhea, 34, ng)
maxp = bytearray(d[t['maxp'][0]:t['maxp'][0] + t['maxp'][1]])
struct.pack_into('>H', maxp, 4, ng)
struct.pack_into('>H', maxp, 6, max(struct.unpack('>H', bytes(maxp[6:8]))[0],
sum(len(c) for c in contours)))
struct.pack_into('>H', maxp, 8, max(struct.unpack('>H', bytes(maxp[8:10]))[0],
len(contours)))
boxes = [struct.unpack('>hhhhh', g[:10])[1:] for g in glyphs if len(g) >= 10]
head = bytearray(d[t['head'][0]:t['head'][0] + t['head'][1]])
struct.pack_into('>hhhh', head, 36,
min(b[0] for b in boxes), min(b[1] for b in boxes),
max(b[2] for b in boxes), max(b[3] for b in boxes))
struct.pack_into('>h', head, 50, 0 if short else 1)
struct.pack_into('>I', head, 8, 0) # checkSumAdjustment, recomputed below
tables = {tag: d[s:s + l] for tag, (s, l) in t.items()}
tables['glyf'] = b''.join(glyphs)
tables['loca'] = (struct.pack('>%dH' % len(offs), *[x // 2 for x in offs]) if short
else struct.pack('>%dI' % len(offs), *offs))
tables['hmtx'] = b''.join(struct.pack('>Hh', adv[i], lsb[i]) for i in range(ng))
tables['hhea'] = bytes(hhea)
tables['maxp'] = bytes(maxp)
tables['cmap'] = build_cmap(cmap)
tables['post'] = build_post(d, t['post'][0], names)
tables['head'] = bytes(head)
open(font_path, 'wb').write(assemble(tables))
def read_svg(location):
if not location.startswith(('http://', 'https://')):
return open(location, encoding='utf-8').read()
# Icon sites reject the stock urllib agent, so ask like a browser would.
req = urllib.request.Request(location, headers={'User-Agent': 'omarchy-dev-font'})
try:
with urllib.request.urlopen(req, timeout=30) as r:
return r.read().decode('utf-8')
except OSError as e:
sys.exit('could not fetch %s: %s' % (location, e))
def svg_contours(svg, location):
vb = re.search(r'viewBox="([^"]+)"', svg)
if not vb:
sys.exit('%s: no viewBox; need an SVG with a viewBox' % location)
view = tuple(float(x) for x in vb.group(1).replace(',', ' ').split())
paths = re.findall(r'<path[^>]*\bd="([^"]+)"', svg)
if len(paths) != 1:
sys.exit('%s: expected a single <path>, found %d — flatten the mark to '
'one monochrome path first' % (location, len(paths)))
return contours_from_svg(paths[0], view)
def default_font():
root = os.environ.get('OMARCHY_PATH')
if not root:
sys.exit('OMARCHY_PATH is not set')
return os.path.join(root, 'default/fonts/omarchy/omarchy.ttf')
def readme_for(font_path):
return os.path.join(os.path.dirname(font_path), 'README.md')
def record_source(font_path, codepoint, label, location):
"""Append the new mark to the font's README so sources stay tracked."""
readme = readme_for(font_path)
if not os.path.exists(readme):
return None
lines = open(readme, encoding='utf-8').read().split('\n')
last = max(i for i, l in enumerate(lines) if l.startswith('- `U+'))
entry = '- `U+%04X` — %s' % (codepoint, label)
if location.startswith(('http://', 'https://')):
entry += ', from <%s>' % location
lines.insert(last + 1, entry)
open(readme, 'w', encoding='utf-8').write('\n'.join(lines))
return readme
def main():
common = argparse.ArgumentParser(add_help=False)
common.add_argument('--font', help='font to edit '
'(default: $OMARCHY_PATH/default/fonts/omarchy/omarchy.ttf)')
p = argparse.ArgumentParser(prog='omarchy dev font', parents=[common],
description=__doc__.split('\n')[0])
sub = p.add_subparsers(dest='command')
sub.add_parser('list', parents=[common], help='show the marks the font carries')
add = sub.add_parser('add', parents=[common],
help='append a mark from a monochrome SVG')
add.add_argument('name', help='glyph name, e.g. ollama')
add.add_argument('svg', help='SVG file or URL')
add.add_argument('--codepoint', help='private-use codepoint (default: next free)')
add.add_argument('--label', help='README label (default: the glyph name)')
args = p.parse_args()
font = args.font or default_font()
if not os.path.exists(font):
sys.exit('no font at %s' % font)
if args.command in (None, 'list'):
for cp, name, box in glyph_list(font):
print('U+%04X %s %-12s %s' % (cp, chr(cp), name, box))
return
taken = {cp for cp, _, _ in glyph_list(font)}
if args.codepoint:
cp = int(args.codepoint.upper().replace('U+', ''), 16)
if cp in taken:
sys.exit('U+%04X is already used' % cp)
else:
cp = max(taken) + 1 if taken else PUA_FIRST
contours = svg_contours(read_svg(args.svg), args.svg)
add_glyph(font, cp, args.name, contours)
readme = record_source(font, cp, args.label or args.name, args.svg)
print('Added %s as U+%04X (%s)' % (args.name, cp, chr(cp)))
print()
print('Next:')
if readme:
print(' - check the new line in %s' % os.path.relpath(readme))
print(' - point a menu entry at it: "icon":"%s","iconFont":"omarchy"' % chr(cp))
print(' - bump the charset range in test/shell.d/menu-test.sh to e900-%x' % cp)
print(' - the font is package-owned, so it reaches the desktop through an')
print(' omarchy-settings release, not through omarchy update')
if __name__ == '__main__':
main()
+5 -2
View File
@@ -2,8 +2,11 @@
# omarchy:summary=Returns true when an external monitor is physically connected.
for status in /sys/class/drm/card*-*/status; do
[[ $status == *-eDP-*/status ]] && continue
drm_path="${OMARCHY_DRM_PATH:-/sys/class/drm}"
for status in "$drm_path"/card*-*/status; do
[[ -e $status ]] || continue
[[ $status =~ -(eDP|LVDS|DSI)-[^/]+/status$ ]] && continue
[[ $(< $status) == "connected" ]] && exit 0
done
exit 1
+17 -7
View File
@@ -9,6 +9,7 @@
# also make USB touchscreens) are left out to avoid nagging laptops with no
# reader — those still match on the product string below when present.
fingerprint_vendors=" 27c6 138a 06cb 08ff 1c7a 147e "
usb_devices_path="${OMARCHY_USB_DEVICES_PATH:-/sys/bus/usb/devices}"
# libfprint drives every reader it supports from userspace over libusb, so a
# real reader sits there with no kernel driver bound to any of its interfaces.
@@ -17,23 +18,32 @@ fingerprint_vendors=" 27c6 138a 06cb 08ff 1c7a 147e "
# — all bind one. Only the vendor-ID guess needs this; a device that names
# itself a fingerprint reader is trusted outright.
has_kernel_driver() {
local intf
local intf driver
for intf in "$1"/*:*; do
[[ -e $intf/driver ]] && return 0
[[ -e $intf/driver ]] || continue
# usbfs is the exception: libusb claims an interface through it, so a reader
# fprintd is enrolling or verifying against binds a driver for as long as it
# holds the claim. That is userspace driving the device — what a reader is
# supposed to look like — so it must not read as a kernel driver here.
driver=$(readlink -f "$intf/driver")
[[ ${driver##*/} == "usbfs" ]] || return 0
done
return 1
}
for dev in /sys/bus/usb/devices/*; do
for dev in "$usb_devices_path"/*; do
# The device's own product descriptor usually names it, e.g. "Goodix
# Fingerprint USB Device" — driver-independent and vendor-agnostic.
if [[ -r $dev/product ]]; then
product=$(<"$dev/product")
product=${product,,}
# Elan's match-on-chip readers report "ELAN:ARM-M4", the family name
# rather than the function. Elan is left out of the vendor list above
# on purpose, so without this they match nothing.
[[ $product == *fingerprint* || $product == *biometric* || $product == *elan:arm-m4* ]] && exit 0
# Elan's match-on-chip readers report "ELAN:ARM-M4" and Fingerprint Cards'
# report "FPC Sensor Controller" or "FPC L:0000 FW:1425046" — the family or
# the manufacturer rather than the function. Both vendors are left out of
# the list above on purpose (Elan also makes touchscreens), so without these
# they match nothing. FPC leads the string on every reader on record, and
# three letters are little to match on, so require the prefix.
[[ $product == *fingerprint* || $product == *biometric* || $product == *elan:arm-m4* || $product == "fpc "* ]] && exit 0
fi
if [[ -r $dev/idVendor ]]; then
+18 -3
View File
@@ -2,8 +2,23 @@
# omarchy:summary=Detect whether the system has an active hybrid GPU configuration
if omarchy-cmd-present supergfxctl; then
supergfxctl -s 2>/dev/null | grep -qw Hybrid
else
multiple_gpus() {
(($(lspci | grep -cE 'VGA|3D|Display') >= 2))
}
if omarchy-cmd-present supergfxctl; then
# A wedged supergfxd blocks its clients forever, and this gate runs while
# the menu renders. Bound the query, and treat a daemon that cannot answer
# like a machine without supergfxctl: count GPUs instead of hiding hardware
# that is really there.
modes=$(timeout --kill-after=1s 1s supergfxctl -s 2>/dev/null)
status=$?
if ((status == 124 || status == 137)); then
multiple_gpus
else
grep -qw Hybrid <<<"$modes"
fi
else
multiple_gpus
fi
+1 -1
View File
@@ -2,4 +2,4 @@
# omarchy:summary=Check whether a webcam is available
v4l2-ctl --list-devices 2>/dev/null | grep -q '^[[:space:]]*/dev/video'
[[ -n $(omarchy-capture-webcam-list) ]]
+9 -2
View File
@@ -1,6 +1,6 @@
#!/bin/bash
# omarchy:summary=Focus a Hyprland window by application class
# omarchy:summary=Focus a Hyprland window by application identity
# omarchy:args=<app-name>
# omarchy:examples=omarchy hyprland focus app Slack
@@ -12,10 +12,17 @@ usage() {
app=${1:-}
[[ -n $app ]] || usage
# Agent terminals notify as kitty/foot/etc. while their shared window class is
# org.omarchy.agent, leaving the terminal name only in initialTitle. So match
# by class first, then fall back to the launch-time title of agent windows.
address=$(
hyprctl clients -j 2>/dev/null |
jq -r --arg pattern "$app" \
'[.[] | select((.class // "") | test($pattern; "i"))] | first.address // empty'
'def matches($value): ($value // "") | test($pattern; "i");
first(
(.[] | select(matches(.class))),
(.[] | select(.initialClass == "org.omarchy.agent" and matches(.initialTitle)))
).address // empty'
)
[[ -n $address ]] || exit 1
+15
View File
@@ -0,0 +1,15 @@
#!/bin/bash
# omarchy:summary=Install the ChatGPT desktop app
# omarchy:requires-sudo=true
set -e
echo "Installing ChatGPT..."
omarchy-pkg-add openai-codex-desktop
echo "Opening ChatGPT..."
setsid uwsm-app -- /usr/bin/chatgpt >/dev/null 2>&1 &
echo ""
echo "ChatGPT has been installed."
+36
View File
@@ -0,0 +1,36 @@
#!/bin/bash
# omarchy:summary=Restore the preinstalled Omarchy applications (web apps, TUIs, and selected packages).
# omarchy:requires-sudo=true
if gum confirm "Are you sure you want to restore all preinstalled web apps, TUI wrappers, and desktop applications?"; then
echo -e "Restoring preinstalled Omarchy applications...\n"
# Recreates the shipped .desktop launchers (web apps and TUIs) and the mise stubs
# that back claude, gh, opencode, and the rest of the agents
omarchy-refresh-applications
# Mirrors the list in omarchy-remove-preinstalls; both track omarchy-base.packages
if ! omarchy-pkg-add \
aether \
cliamp \
libreoffice-fresh \
xournalpp \
pinta \
obsidian \
obs-studio \
kdenlive \
moonlight-qt \
lazydocker \
omacut \
omacalc \
omawrite; then
echo -e "\nPreinstalls are still marked as removed. Fix the errors above and try again."
exit 1
fi
# Last, so a failure above leaves the opt-out intact rather than restoring
# keybindings for apps that never came back
rm -f ~/.local/state/omarchy/preinstalls-removed
hyprctl reload
fi
+132 -40
View File
@@ -2,13 +2,32 @@
# omarchy:summary=Launch the fastfetch TUI that gives information about the current system.
# The float rule only sets a starting size: the exact fit depends on the terminal
# font and the user's About logo, so once the terminal grid has settled we measure
# the rendered content and resize the window to hug it with even padding all around.
# Bash defers WINCH traps while read blocks, so poll for size changes and re-render
# fastfetch at the new width whenever the window is resized.
# The size that hugs the About content depends on the terminal font and the user's
# logo, so it can only be measured from inside the terminal. We remember the size
# that fit and apply it as a window rule before launching, so the window opens at
# it instead of resizing after the first paint. Bash defers WINCH traps while read
# blocks, so poll for size changes and re-render fastfetch whenever it is resized.
LOGO_FILE="$HOME/.config/omarchy/branding/about.txt"
FIT_FILE="$HOME/.local/state/omarchy/windows/about.fit"
# 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() {
[[ -f $HOME/.config/fastfetch/config.jsonc ]]
}
# wc -L counts display columns only in a UTF-8 locale. A session that never set
# one counts every box-drawing and Nerd Font glyph in the About layout as
# nothing, which measures the content narrower than it renders.
display_columns() {
LC_ALL=C.UTF-8 wc -L
}
logo_dimensions() {
[[ -f $LOGO_FILE ]] || return 1
printf '%s %s' "$(display_columns <"$LOGO_FILE")" "$(wc -l <"$LOGO_FILE")"
}
hypr_dispatch() {
local lua="$1"
@@ -16,74 +35,147 @@ hypr_dispatch() {
hyprctl dispatch "$lua" >/dev/null 2>&1 || hyprctl dispatch "$@" >/dev/null
}
remember_fit() {
local directory tmp
directory=$(dirname "$FIT_FILE")
mkdir -p "$directory"
tmp=$(mktemp "$directory/.about.fit.XXXXXX")
printf '%s %s %s %s\n' "$1" "$2" "$3" "$4" >"$tmp"
mv "$tmp" "$FIT_FILE"
}
# Replaces the rule from the last launch so a remembered size never outlives the
# fit it came from. Called without one it only clears, leaving the float rule's
# starting size — where a Hyprland without the Lua API stays too.
apply_size_rule() {
local rule=""
(( $# == 2 )) && rule="omarchy_about_size_rule = hl.window_rule({ match = { class = \"org.omarchy.about\" }, size = { $1, $2 } })"
hyprctl eval "if omarchy_about_size_rule then omarchy_about_size_rule:set_enabled(false) end; omarchy_about_size_rule = nil; $rule" >/dev/null 2>&1
}
# Sized before the terminal is spawned, so the window maps at its final size.
presize_window() {
local logo_w logo_h fit_logo_w fit_logo_h fit_w fit_h
if ! custom_fastfetch_config && [[ -r $FIT_FILE ]]; then
read -r logo_w logo_h <<<"$(logo_dimensions)"
read -r fit_logo_w fit_logo_h fit_w fit_h <"$FIT_FILE"
# A different logo needs a different window, so leave that launch to the
# float rule and let the fit measure the new size.
if [[ -n ${logo_w:-} && $logo_w == "$fit_logo_w" && $logo_h == "$fit_logo_h" ]] &&
[[ $fit_w =~ ^[0-9]+$ && $fit_h =~ ^[0-9]+$ ]]; then
apply_size_rule "$fit_w" "$fit_h"
return
fi
fi
apply_size_rule
}
# Hyprland animates a resize and the terminal reflows to every step of it, so
# wait for the grid to hold still before measuring it.
settle_grid() {
local current previous="" held=0
for _ in {1..20}; do
current=$(stty size)
if [[ $current == $previous ]]; then
(( ++held == 3 )) && break
else
held=0
previous=$current
fi
sleep 0.05
done
}
fit_window() {
# 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.
[[ -f $HOME/.config/fastfetch/config.jsonc ]] && return 0
custom_fastfetch_config && return 0
local rows cols address width height
read -r rows cols <<<"$(stty size)"
read -r address width height <<<"$(hyprctl clients -j | jq -r '.[] | select(.class == "org.omarchy.about") | "\(.address) \(.size[0]) \(.size[1])"')"
[[ -f $LOGO_FILE && -n ${address:-} ]] || return 1
(( cols > 0 && rows > 0 && width > 0 && height > 0 )) || return 1
# Mirror the logo block in the fastfetch config: 2 columns of padding on the
# left of the logo, 6 between the logo and the modules, 2 rows above the logo.
# wc -L measures display columns, so double-width glyphs count correctly.
local logo_w logo_h
logo_w=$(wc -L <"$LOGO_FILE")
logo_h=$(( $(wc -l <"$LOGO_FILE") + 2 ))
read -r logo_w logo_h <<<"$(logo_dimensions)"
[[ -n ${logo_w:-} ]] || return 1
# The guard character keeps command substitution from eating the trailing
# break line, which provides the bottom padding row.
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" | wc -L)
module_w=$(printf '%s' "$modules" | display_columns)
module_h=$(printf '%s' "$modules" | wc -l)
# Two columns of right padding to match the logo's left padding, and one row
# for the cursor line so the trailing break stays visible as bottom padding.
# Mirror the logo block in the fastfetch config: 2 columns of padding left of
# 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 > module_h ? logo_h : module_h) + 1 ))
local target_w=$(( (target_c * width + cols / 2) / cols ))
local target_h=$(( (target_r * height + rows / 2) / rows ))
local target_r=$(( (logo_h + 2 > module_h ? logo_h + 2 : module_h) + 1 ))
# Skip resizes within one terminal cell so the fit doesn't churn.
(( (target_w - width) ** 2 <= (width / cols) ** 2 && (target_h - height) ** 2 <= (height / rows) ** 2 )) && return
local nudges=0 rows cols address width height shift_w shift_h target_w target_h
while :; do
read -r rows cols <<<"$(stty size)"
read -r address width height <<<"$(hyprctl clients -j | jq -r '.[] | select(.class == "org.omarchy.about") | "\(.address) \(.size[0]) \(.size[1])"')"
[[ -n ${address:-} ]] || return 1
(( cols > 0 && rows > 0 && width > 0 && height > 0 )) || return 1
hypr_dispatch "hl.dsp.window.resize({ window = \"address:$address\", x = $target_w, y = $target_h })" resizewindowpixel "exact $target_w $target_h,address:$address"
hypr_dispatch "hl.dsp.window.center({ window = \"address:$address\" })" centerwindow
# A window has to land on the terminal's cell boundaries, so take a cell of
# slack over chasing an exact grid, and remember where it came to rest.
if (( cols >= target_c && cols <= target_c + 1 && rows >= target_r && rows <= target_r + 1 )); then
remember_fit "$logo_w" "$logo_h" "$width" "$height"
return 0
fi
# Two nudges is the budget, and each one is measured before the next is spent.
(( ++nudges <= 2 )) || return 1
# Move by the cells the window is off by, rather than scaling it to the grid,
# which would multiply up the terminal's padding along with them. Dividing a
# window that carries that padding still leaves the cell a touch generous, so
# round the move away from the grid that would clip.
shift_w=$(( (target_c - cols) * width ))
shift_h=$(( (target_r - rows) * height ))
target_w=$(( width + (shift_w >= 0 ? (shift_w + cols - 1) / cols : shift_w / cols) ))
target_h=$(( height + (shift_h >= 0 ? (shift_h + rows - 1) / rows : shift_h / rows) ))
hypr_dispatch "hl.dsp.window.resize({ window = \"address:$address\", x = $target_w, y = $target_h })" resizewindowpixel "exact $target_w $target_h,address:$address"
hypr_dispatch "hl.dsp.window.center({ window = \"address:$address\" })" centerwindow
settle_grid
done
return 1
}
if [[ ${1:-} == "--render" ]]; then
printf '\e[?25l'
# Give the compositor a moment to apply the float rule before measuring cells.
previous=""
for _ in {1..20}; do
current=$(stty size)
[[ $current == $previous ]] && break
previous=$current
sleep 0.05
done
# Give the compositor a moment to apply the window rules before measuring cells.
settle_grid
fitted=false
passes=0
while :; do
size=$(stty size)
logo_stamp=$(stat -c %Y "$LOGO_FILE" 2>/dev/null)
clear
fastfetch
if [[ $fitted == false ]] && fit_window; then
fitted=true
# A second pass picks up a fit that could not measure the window the first
# time. Beyond that, a window that will not settle would be fitted again on
# every repaint.
if [[ $fitted == false ]] && (( ++passes <= 2 )); then
fit_window && fitted=true
fi
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.
[[ $(stat -c %Y "$LOGO_FILE" 2>/dev/null) == $logo_stamp ]] || fitted=false
if [[ $(stat -c %Y "$LOGO_FILE" 2>/dev/null) != $logo_stamp ]]; then
fitted=false
passes=0
fi
done
fi
presize_window
exec omarchy-launch-or-focus-tui --app-id=org.omarchy.about omarchy-launch-about --render
-72
View File
@@ -1,72 +0,0 @@
#!/bin/bash
# omarchy:summary=Launch the default coding agent in a terminal
# omarchy:args=[--inline] [prompt...]
# omarchy:examples=omarchy launch agent | omarchy launch agent "Review this project"
if [[ ${1:-} == "--inline" ]]; then
inline=true
shift
else
inline=false
fi
# Agents refuse to remember trust for $HOME, so launches from the keybinding or
# menu start in the work directory instead of re-asking on every session.
[[ $PWD == "$HOME" && -d $HOME/Work ]] && cd "$HOME/Work"
agent=$(omarchy-default-agent)
if omarchy-cmd-missing "$agent"; then
echo "$agent is not installed. Choose an installed agent with: omarchy default agent <name>" >&2
exit 1
fi
if (($# > 0)); then
prompt="$*"
fi
case "$agent" in
opencode)
if [[ -n ${prompt:-} ]]; then
command=(opencode --prompt "$prompt")
else
command=(opencode)
fi
;;
gemini)
if [[ -n ${prompt:-} ]]; then
command=(gemini --prompt-interactive "$prompt")
else
command=(gemini)
fi
;;
copilot)
if [[ -n ${prompt:-} ]]; then
command=(copilot --interactive "$prompt")
else
command=(copilot)
fi
;;
crush)
if [[ -n ${prompt:-} ]]; then
command=(crush run "$prompt")
else
command=(crush)
fi
;;
pi | omp | claude | codex | grok)
command=("$agent")
[[ -n ${prompt:-} ]] && command+=(-- "$prompt")
;;
*)
echo "Unsupported default agent: $agent" >&2
exit 1
;;
esac
if [[ $inline == "true" ]]; then
exec "${command[@]}"
else
exec omarchy-launch-tui "${command[@]}"
fi
+7 -1
View File
@@ -9,8 +9,14 @@
#
# Backgrounded because bash defers a trap until a foreground command returns but
# interrupts wait. systemd-cat execs, so the job is Quickshell itself.
#
# Quickshell's own reloading is off; Omarchy restarts the shell deliberately.
# A package upgrade rewriting $OMARCHY_PATH/shell would otherwise reload it
# against a half-written tree, and that failed reload leaves a second engine
# generation behind that turns the next restart's IPC kill into a crash.
run_shell() {
systemd-cat -t omarchy-shell -- quickshell -n -p "$OMARCHY_PATH/shell" &
QS_DISABLE_FILE_WATCHER=1 QS_NO_RELOAD_POPUP=1 \
systemd-cat -t omarchy-shell -- quickshell -n -p "$OMARCHY_PATH/shell" &
shell_pid=$!
local status
+43 -22
View File
@@ -111,8 +111,8 @@ cache_key=$(printf '%s' "$image_dirs_env" | md5sum | cut -d ' ' -f 1)
rows_cache_file="$cache_dir/$cache_key.rows"
rows_signature_file="$cache_dir/$cache_key.signature"
rows_fast_signature_file="$cache_dir/$cache_key.fast-signature"
rows_signature="v2"$'\n'
rows_fast_signature="v1"$'\n'
rows_signature="v3"$'\n'
rows_fast_signature="v2"$'\n'
rows_cacheable=true
rows_cache_hit=false
image_files=()
@@ -142,23 +142,32 @@ generate_thumbnail() {
local image="$1"
local thumbnail="$2"
local lock="$thumbnail.lock"
local lock_fd
local tmp="$thumbnail.$$.jpg"
if mkdir "$lock" 2>/dev/null; then
# Callers fan out one generator per image, so keep each vips single-threaded.
if VIPS_CONCURRENCY=1 vipsthumbnail "$image" --size 1536x864 --smartcrop=centre --path "$tmp[Q=82,strip]"; then
mv -f "$tmp" "$thumbnail"
else
rm -f "$tmp" "$thumbnail"
fi
# Older releases used directories as locks, which could survive a killed
# generator and block this thumbnail forever. Only reap aged ones, so a
# legacy generator still running through an upgrade keeps its lock.
if [[ -d $lock ]] && (( $(date +%s) - $(stat -c '%Y' "$lock" 2>/dev/null || date +%s) > 120 )); then
rmdir "$lock" 2>/dev/null
fi
rmdir "$lock" 2>/dev/null || true
exec {lock_fd}>"$lock" || return
flock -w 30 "$lock_fd" || return
# A generator killed mid-write leaves its partial $thumbnail.<pid>.jpg
# behind. Only the lock holder writes these, so any found now are stale.
rm -f "$thumbnail".*.jpg
[[ -f $thumbnail ]] && return
# Callers fan out one generator per image, so keep each vips single-threaded.
# Close the lock fd for vips: an orphaned or hung vips must not keep holding
# the lock after this shell is killed.
if VIPS_CONCURRENCY=1 vipsthumbnail "$image" --size 1536x864 --smartcrop=centre --path "$tmp[Q=82,strip]" {lock_fd}>&-; then
mv -f "$tmp" "$thumbnail"
else
for ((i = 0; i < 3000; i++)); do
[[ -f $thumbnail ]] && return
[[ -d $lock ]] || break
sleep 0.01
done
rm -f "$tmp" "$thumbnail"
fi
}
@@ -226,7 +235,10 @@ elif [[ $rows_cache_hit != true ]]; then
if [[ -s $pending_file ]]; then
pruned=""
while IFS=$'\t' read -r row_image row_thumbnail; do
[[ -e $row_thumbnail ]] || continue
if [[ ! -e $row_thumbnail ]]; then
rows_cacheable=false
continue
fi
if [[ -z $pruned ]]; then
pruned="$row_image"$'\t'"$row_thumbnail"
@@ -237,12 +249,21 @@ elif [[ $rows_cache_hit != true ]]; then
rows="$pruned"
fi
if [[ $rows_cacheable == true ]]; then
printf '%s' "$rows" >"$rows_cache_file"
printf '%s' "$rows_signature" >"$rows_signature_file"
printf '%s' "$rows_fast_signature" >"$rows_fast_signature_file"
else
rm -f "$rows_cache_file" "$rows_signature_file" "$rows_fast_signature_file"
# Publish the cache under a lock and via renames: a picker killed mid-write,
# or two pickers interleaving, must never leave truncated or mismatched rows
# behind signatures that still validate. Rows go first so a kill between
# renames leaves signatures that are either older (a harmless miss) or
# describe the same directory state.
if exec {rows_lock_fd}>"$rows_cache_file.lock" && flock -w 30 "$rows_lock_fd"; then
if [[ $rows_cacheable == true ]]; then
rm -f "$cache_dir/$cache_key".*.tmp
printf '%s' "$rows" >"$rows_cache_file.$$.tmp" && mv -f "$rows_cache_file.$$.tmp" "$rows_cache_file"
printf '%s' "$rows_signature" >"$rows_signature_file.$$.tmp" && mv -f "$rows_signature_file.$$.tmp" "$rows_signature_file"
printf '%s' "$rows_fast_signature" >"$rows_fast_signature_file.$$.tmp" && mv -f "$rows_fast_signature_file.$$.tmp" "$rows_fast_signature_file"
else
rm -f "$rows_cache_file" "$rows_signature_file" "$rows_fast_signature_file"
fi
exec {rows_lock_fd}>&-
fi
fi
+2
View File
@@ -72,6 +72,8 @@ run_first_run_step "install Voxtype post-update hook" \
omarchy-hook-install post-update "$OMARCHY_PATH/install/user/first-run/install-voxtype.hook"
run_first_run_step "install fingerprint setup post-update hook" \
omarchy-hook-install post-update "$OMARCHY_PATH/install/user/first-run/setup-fingerprint.hook"
run_first_run_step "install agent setup post-update hook" \
omarchy-hook-install post-update "$OMARCHY_PATH/install/user/first-run/setup-agent.hook"
run_first_run_step "enable user systemd units" \
bash "$OMARCHY_PATH/install/user/first-run/enable-user-units.sh"
+9 -4
View File
@@ -83,11 +83,16 @@ 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
ln -sfn "$OMARCHY_PATH/default/agents/skills/omarchy" ~/.agents/skills/omarchy
ln -sfn "$OMARCHY_PATH/default/agents/skills/omarchy" ~/.claude/skills/omarchy
ln -sfn "$OMARCHY_PATH/default/agents/skills/omarchy" ~/.codex/skills/omarchy
ln -sfn "$OMARCHY_PATH/default/agents/skills/omarchy" ~/.pi/agent/skills/omarchy
for skill in "$OMARCHY_PATH"/default/agents/skills/*/; do
skill=${skill%/}
name=${skill##*/}
ln -sfn "$skill" ~/.agents/skills/"$name"
ln -sfn "$skill" ~/.claude/skills/"$name"
ln -sfn "$skill" ~/.codex/skills/"$name"
ln -sfn "$skill" ~/.pi/agent/skills/"$name"
done
mkdir -p ~/Downloads ~/Pictures ~/Videos ~/.config/gtk-3.0
xdg-user-dirs-update --set TEMPLATES "$HOME"
+4 -8
View File
@@ -20,19 +20,15 @@ if gum confirm "Are you sure you want to remove all preinstalled web apps, TUI w
omarchy-pkg-drop \
aether \
cliamp \
typora \
spotify \
libreoffice-fresh \
1password \
1password-cli \
xournalpp \
signal-desktop \
pinta \
obsidian \
obs-studio \
kdenlive \
moonlight-qt \
lazydocker \
opencode \
claude-code \
github-cli
omacut \
omacalc \
omawrite
fi
+3
View File
@@ -20,4 +20,7 @@ if pgrep -x "1password" >/dev/null && omarchy-cmd-present 1password; then
fi
# Avoid running screensaver when locked
pkill -x ttfx 2>/dev/null || true
# ttfx handles SIGTERM asynchronously, so keep its terminal alive until it exits.
timeout 1s pidwait -x ttfx 2>/dev/null || true
pkill -f '[o]rg.omarchy.screensaver' 2>/dev/null || true
+1 -1
View File
@@ -6,7 +6,7 @@
# omarchy:examples=omarchy theme install git@github.com:example/omarchy-example-theme.git
if [[ -z $1 ]]; then
echo -e "\e[32mSee https://manuals.omamix.org/2/the-omarchy-manual/90/extra-themes\n\e[0m"
echo -e "\e[32mSee https://omarchy.org/themes/\n\e[0m"
REPO_URL=$(gum input --placeholder="Git repo URL (https or git@host:org/repo.git)" --header="")
else
REPO_URL="$1"
+1 -1
View File
@@ -150,4 +150,4 @@ set_theme() {
! omarchy-toggle-enabled skip-vscode-theme-changes && set_theme "code" "$HOME/.config/Code/User/settings.json" "$HOME/.vscode/extensions"
! omarchy-toggle-enabled skip-vscode-insiders-theme-changes && set_theme "code-insiders" "$HOME/.config/Code - Insiders/User/settings.json" "$HOME/.vscode-insiders/extensions"
! omarchy-toggle-enabled skip-codium-theme-changes && set_theme "codium" "$HOME/.config/VSCodium/User/settings.json" "$HOME/.vscode-oss/extensions"
! omarchy-toggle-enabled skip-cursor-theme-changes && set_theme "cursor" "$HOME/.config/Cursor/User/settings.json" "$HOME/.cursor/extensions"
! omarchy-toggle-enabled skip-cursor-theme-changes && set_theme "/usr/bin/cursor" "$HOME/.config/Cursor/User/settings.json" "$HOME/.cursor/extensions"
+18
View File
@@ -0,0 +1,18 @@
#!/bin/bash
# omarchy:summary=Toggle crash capture notifications
# nf-md-robot_dead, escaped so this file reads without a Nerd Font.
readonly CRASH_GLYPH=$'\U000f16a1'
omarchy-toggle crash-capture-off
# The unit checks the same flag with ConditionPathExists, so stopping and
# starting only settles this session; the next login reads the flag itself.
if omarchy-toggle-enabled crash-capture-off; then
systemctl --user stop omarchy-crash-watch.service
omarchy-notification-send -g "$CRASH_GLYPH" "Crash capture disabled"
else
systemctl --user start omarchy-crash-watch.service
omarchy-notification-send -g "$CRASH_GLYPH" "Crash capture enabled"
fi
+14 -1
View File
@@ -22,7 +22,20 @@ CONF
sudo systemctl enable --now supergfxd
fi
gpu_mode=$(supergfxctl -g)
gpu_mode=""
for attempt in {1..3}; do
if gpu_mode=$(timeout --kill-after=1s 3s supergfxctl -g 2>/dev/null) && [[ -n $gpu_mode ]]; then
break
fi
gpu_mode=""
(( attempt < 3 )) && sleep 1
done
if [[ -z $gpu_mode ]]; then
echo "supergfxd is not responding. Try again, or check: systemctl status supergfxd" >&2
exit 1
fi
case "$gpu_mode" in
"Integrated")
+12
View File
@@ -21,7 +21,15 @@ trap 'omarchy-update-stay-awake stop' EXIT
omarchy-update-requires-free-space
# -y is a promise not to ask anything. Steps that would need an answer report
# and move on instead of waiting on a prompt nobody is here to give.
[[ ${1:-} != "-y" ]] || export OMARCHY_UPDATE_UNATTENDED=1
if [[ ${1:-} == "-y" ]] || omarchy-update-confirm; then
# Before the snapshot: the cache is on the snapshotted subvolume, so pruning
# after it frees nothing until that snapshot ages out.
omarchy-update-pkg-prune
# 127 means Snapper is deliberately absent. Any other failure already said
# what went wrong, and a missing snapshot is not worth blocking an update
# over, but it must not pass for one either.
@@ -32,6 +40,10 @@ if [[ ${1:-} == "-y" ]] || omarchy-update-confirm; then
omarchy-update-dev
omarchy-update-keyring
# Migrations ship with the packages installed here and are written against
# them, so everything below waits on this finishing. An upgrade that stopped
# takes the update with it rather than migrating against what is still on disk.
omarchy-update-system-pkgs
omarchy-migrate
omarchy-hook post-update
+13
View File
@@ -0,0 +1,13 @@
#!/bin/bash
# omarchy:summary=Prune superseded versions from the pacman package cache
# omarchy:requires-sudo=true
set -e
# The cache is the only offline downgrade path, so keep two. Run before the
# packages update and the installed version survives with a spare. paccache
# retains by version order, never by what is installed.
echo -e "\e[32m\nPrune package cache\e[0m"
sudo paccache -rk2 || echo -e "\e[33mCould not prune the package cache.\e[0m" >&2
echo
+11 -3
View File
@@ -5,6 +5,14 @@
set -e
# The conflict handler's last resort: pacman puts its questions to the person
# running the update instead of answering them itself. Nothing here may capture
# a stream, because an upgrade without --noconfirm prompts on stderr. The
# handler has already said why, so no heading here either.
if [[ ${OMARCHY_UPDATE_CONFLICT:-} == 1 && ${OMARCHY_UPDATE_INTERACTIVE:-} == 1 ]]; then
exec sudo env OMARCHY_UPDATE_PACMAN=1 pacman -Syu --overwrite '/usr/share/omarchy/*'
fi
echo -e "\e[32m\nUpdate system packages\e[0m"
errors=$(mktemp)
@@ -22,9 +30,9 @@ if sudo env LC_ALL=C OMARCHY_UPDATE_PACMAN=1 pacman -Syu --noconfirm \
fi
cat "$errors" >&2
# An upgrade blocked only by files pacman doesn't own yet is the one failure
# worth retrying: the handler clears them and runs this again. Anything else,
# including a second failure, is for a human.
# The handler takes it from here: it clears files pacman doesn't own yet and
# runs this again, and puts a package conflict to whoever started the update.
# Anything else, including a second failure, is for a human.
[[ ${OMARCHY_UPDATE_RETRY:-} != 1 ]] || exit 1
# exec, so the EXIT trap above does not fire and the handler can still read the
# report. It takes over deleting it.
+25 -1
View File
@@ -1,6 +1,6 @@
#!/bin/bash
# omarchy:summary=Retry a system package update after resolving file conflicts
# omarchy:summary=Retry a system package update that hit a conflict
# omarchy:hidden=true
# Internal to omarchy-update-system-pkgs. Not a command to run by hand: it acts
@@ -11,6 +11,10 @@
# release's installer, an in-place edit -- belong to nobody, and pacman refuses
# to install over a file it doesn't own. This clears them and runs the upgrade
# again.
#
# Packages that conflict with each other are the other blocked upgrade, and that
# one is a decision rather than a cleanup, so it goes back to the person
# watching the update.
set -e
@@ -57,6 +61,26 @@ restore_moved() {
trap 'restore_moved; rm -f "$errors"' EXIT
trap 'exit 1' INT TERM
# Pacman answers its own conflict question with No under --noconfirm, so one
# retired package can stop every upgrade after it. Which package to drop is a
# decision -- an upstream split retires one, but so does a package the user
# picked on purpose -- so put the question to whoever started the update rather
# than guess at it. Pacman printed what conflicts just above.
if grep -q 'unresolvable package conflicts detected' "$errors"; then
# -y promised not to ask anything, and without a terminal there is nowhere to
# ask; either way pacman would sit on a prompt nobody answers. An upgrade that
# isn't running --noconfirm puts its questions on stderr and reads the answers
# from stdin, so those are the two that have to be a terminal.
if [[ ${OMARCHY_UPDATE_UNATTENDED:-} == 1 || ! -t 0 || ! -t 2 ]]; then
echo -e "\e[33m\nThis upgrade needs an answer. Run omarchy update interactively to give it.\e[0m" >&2
exit 1
fi
echo -e "\e[33m\nA package conflict stopped this upgrade. Running it again so you can answer:\e[0m"
OMARCHY_UPDATE_INTERACTIVE=1 omarchy-update-system-pkgs
exit 0
fi
# Paths one of these packages installs that pacman doesn't own. Moving them out
# of the way is what lets the upgrade through, and works on a leftover directory
# as well as a file.
-77
View File
@@ -1792,82 +1792,6 @@ CHROMIUM_FLAGS_PATCH_PY
done
}
# True if any Chromium-family browser that stores the Copy URL shortcut is running.
browser_is_running() {
pgrep -x 'chromium|chrome|brave|msedge|vivaldi-bin|vivaldi|opera|helium' >/dev/null 2>&1
}
repair_chromium_copy_url_shortcuts() {
local profile_root preferences
local -a profile_roots=(
"$HOME/.config/chromium"
"$HOME/.config/google-chrome"
"$HOME/.config/google-chrome-beta"
"$HOME/.config/BraveSoftware/Brave-Browser"
"$HOME/.config/BraveSoftware/Brave-Browser-Beta"
"$HOME/.config/microsoft-edge"
"$HOME/.config/microsoft-edge-beta"
"$HOME/.config/vivaldi"
"$HOME/.config/opera"
"$HOME/.config/helium"
)
local -a pending=()
for profile_root in "${profile_roots[@]}"; do
[[ -d $profile_root ]] || continue
for preferences in "$profile_root"/*/Preferences; do
[[ -f $preferences ]] || continue
grep -qF 'bocglpkldciamkbmlphanhkfnhpmnbma' "$preferences" || continue
pending+=("$preferences")
done
done
(( ${#pending[@]} )) || return 0
# A running browser holds Preferences in memory and rewrites it on exit,
# which would revert this repair. Ask the user to close it first.
if browser_is_running && (( ! OMARCHY_UPGRADE_YES )) && [[ -r /dev/tty ]]; then
if ! gum confirm "Close all browser windows before the Copy URL shortcut is repaired, then continue" </dev/tty; then
echo "Skipped Copy URL shortcut repair; re-run after closing your browser." >&2
return 0
fi
fi
for preferences in "${pending[@]}"; do
python3 - "$preferences" "$preferences.omarchy-upgrade-to-quattro.$BACKUP_SUFFIX.bak" <<'CHROMIUM_SHORTCUTS_PATCH_PY'
import json
import shutil
import sys
from pathlib import Path
path = Path(sys.argv[1])
backup_path = Path(sys.argv[2])
old_id = "bocglpkldciamkbmlphanhkfnhpmnbma"
new_id = "bgpiichlckmfanooecilcjemknkcpngb"
preferences = json.loads(path.read_text())
extensions = preferences.get("extensions", {})
commands = extensions.get("commands", {})
changed = False
for command in commands.values():
if command.get("extension") == old_id and command.get("command_name") == "copy-url":
command["extension"] = new_id
changed = True
if changed:
settings = extensions.get("settings", {})
old_command = settings.get(old_id, {}).get("commands", {}).get("copy-url", {})
new_command = settings.get(new_id, {}).get("commands", {}).get("copy-url", {})
old_command.pop("was_assigned", None)
if new_command:
new_command["was_assigned"] = True
shutil.copy2(path, backup_path)
path.write_text(json.dumps(preferences, separators=(",", ":")))
CHROMIUM_SHORTCUTS_PATCH_PY
done
}
repair_sleep_lock_unit_override() {
local unit="$HOME/.config/systemd/user/omarchy-sleep-lock.service"
local legacy_exec='ExecStart=%h/.local/share/omarchy/bin/omarchy-system-sleep-monitor'
@@ -1964,7 +1888,6 @@ copy_missing_config_defaults "$root/config" "$HOME/.config"
mark_removed_preinstalls_from_legacy_bindings
copy_always_config_defaults
repair_chromium_copy_url_extension_flags
repair_chromium_copy_url_shortcuts
# Carry over user-added backgrounds from the legacy git checkout backup, then
# remove old symlinks that pointed ~/.config/omarchy/themes/* back into the
+2 -2
View File
@@ -5,8 +5,8 @@ profile {
identity = true
}
# To enable auto switch to nightlight, set in your .config/hypr/autostart:
# exec-once = uwsm app -- hyprsunset
# To enable auto switch to nightlight, add to your .config/hypr/autostart.lua:
# o.launch_on_start("hyprsunset")
# and use the following:
# profile {
# time = 20:00
@@ -0,0 +1,97 @@
---
name: diagnose-crash
description: >
Diagnose why a program crashed on this machine, from a systemd-coredump core dump.
Use when a process has segfaulted, aborted, or otherwise dumped core, when asked
why an application crashed or disappeared, or when a "Process crashed:" desktop
notification is acted on. Triggers: crash, segfault, SIGSEGV, SIGABRT, core dump,
coredumpctl, "why did X crash", "X keeps crashing", backtrace symbolization.
Covers reporting a confirmed Omarchy bug upstream — see reporting.md.
---
# Diagnosing a Crash
Work from evidence. The goal is an honest account of what happened, not a
plausible-sounding story.
## Establish the facts
`coredumpctl info <pid>` is the starting point. Beyond the backtrace, note the
**command line** the process was started with — it usually reveals what the
program was working on when it died, which is often the whole answer.
`coredumpctl list` shows whether this crash is a one-off or a pattern. Repeated
crashes of the same program, or several programs dying together, point somewhere
different than a single failure does.
## Rule out the boring causes first
Check resource exhaustion before blaming the program: `free -h`, and the journal
for OOM kills. A process killed by the OOM killer is not a bug in that process.
## Correlate against the timeline
The crash timestamp is the most underused piece of evidence. Compare it against:
- **Filesystem mtimes.** A directory or file whose mtime lands on the same second
as the crash strongly suggests what triggered it.
- **The journal** around that moment, for related warnings from the same or
neighbouring processes.
- **Recent package updates.** A crash that starts right after an update points at
the update.
## Read the whole core, not just frame 0
Thread stacks other than the crashing one show what work was **in flight** —
thumbnailers, image loaders, IPC readers, GPU queues. That context often explains
the trigger even when the crashing frame itself cannot be symbolized.
Note any third-party code in the address space: file-manager or browser
extensions, plugins, out-of-tree drivers. In-process third-party code is a common
crash source and worth flagging — but do not pin blame on it without evidence
that it is actually implicated.
## Symbolize when you can
This is Arch, which runs a public debuginfod server:
```bash
core=$(mktemp -t crash-XXXXXX.core)
trap 'rm -f "$core"' EXIT
coredumpctl dump <pid> --output="$core"
DEBUGINFOD_URLS="https://debuginfod.archlinux.org" \
gdb -q <executable> "$core" \
-batch -ex 'set debuginfod enabled on' -ex 'bt'
```
A core is a verbatim copy of the process's memory and can hold passwords, tokens,
and private documents. Write it to a fresh `mktemp` path rather than a predictable
shared one, and delete it when you are done — never leave it lying in `/tmp`.
Many packages publish no debug symbols. When frames stay unresolved, say so —
never invent function names to fill the gap. An unsymbolized stack still has
shape: which library each frame belongs to, and whether the crash came from a
signal handler, a main loop, or a worker thread.
## Report
1. What crashed, and what it was doing at the time.
2. The most likely mechanism — separating clearly what the evidence **proves**
from what you are **inferring**.
3. Whether any user data was lost, and where it can be recovered from. Check the
trash before concluding anything is gone.
4. Whether it is likely to recur, and what would avoid or fix it.
Be straight about the limits of the evidence. If the cause is genuinely
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.
## If it is an Omarchy bug
Most application crashes are upstream bugs in those applications, not Omarchy's
doing. In the minority of cases where the cause really does sit within Omarchy's
sphere of control, read [`reporting.md`](reporting.md) before offering to file
anything.
@@ -0,0 +1,104 @@
# Reporting a Crash Upstream to Omarchy
Read this only after concluding that a crash is genuinely Omarchy's to fix.
## Is it even Omarchy's bug?
Be strict here. Omarchy is a configuration layer over Arch Linux, so a crash
inside a third-party application — a file manager, a browser, a GNOME or Qt
library — is almost always an upstream bug in **that** project, not in Omarchy.
Omarchy's sphere of control is roughly:
- the `omarchy-*` commands
- the Quickshell shell and its plugins
- the Hyprland and terminal configuration it ships
- its themes
- its install and migration scripts
- how it packages and configures what it installs
A crash in a program Omarchy merely installs is **not** an Omarchy bug unless
Omarchy's own packaging or configuration is implicated.
If it is not Omarchy's, say so and stop. Suggesting the right upstream project is
useful; filing there yourself is not part of this.
## Three conditions, all required
1. **It is a verified bug in Omarchy's sphere**, established on evidence. Issues
are for verified bugs only. An "is this even a bug?" belongs on the Discord at
<https://omarchy.org/discord>; a feature idea belongs in GitHub Discussions
under Suggestions.
2. **The user has explicitly agreed.** Show them the exact title and body you
propose, and wait for a yes. Never file unprompted.
3. **The machine can file it** — `gh auth status` must succeed. If `gh` is missing
or unauthenticated, do not install or authenticate it. Say so, and hand the
user the finished text to submit themselves.
## Search before filing
A duplicate issue costs a maintainer more time than no report at all.
```bash
gh search issues --repo basecamp/omarchy "<program> crash"
gh issue list --repo basecamp/omarchy --state all --search "<signal> <program>"
```
Search on the crashing program, the signal, and distinctive symbols from the
backtrace — not on the wording of the title you were about to write.
`gh search issues` accepts only `open` or `closed` for `--state`, and errors on
anything else. Leaving it off searches both, which is what you want here.
Include **closed** issues. A matching issue closed as fixed, when the crash still
reproduces on a current system, is a regression — and reporting that is worth far
more than another duplicate.
## Adding to an existing report
If a plausible match comes back, read it properly first:
```bash
gh issue view <number> --repo basecamp/omarchy --comments
```
Confirm it is genuinely the same failure. The same program crashing is not the
same bug if the trigger or the stack differs.
If it is the same, add to that issue rather than opening a new one — but only
when you have something the thread does not already contain: a different
reproduction, a symbolized stack where it has none, a narrower trigger, a version
where it regressed.
A comment that only says the bug happens to you too is noise. If that is all you
have, tell the user so and file nothing.
```bash
gh issue comment <number> --repo basecamp/omarchy --body "..."
```
## Filing a new issue
Only when the search turns up nothing that matches:
```bash
gh issue create --repo basecamp/omarchy --title "..." --body "..."
```
Include what happened, what was expected, steps to reproduce, system details from
`omarchy version`, and diagnostics from `omarchy debug --no-sudo --print` (which
also writes `/tmp/omarchy-debug.log`; the interactive `omarchy debug` can upload
it and print a shareable URL worth including).
`gh` cannot attach media. If a screenshot would help, save one and give the user
the path to drag into the web form.
## Signing
End the issue or comment with a line naming the model and agent harness that
produced it, so a human reader knows it was machine-authored:
> Filed by \<model name\> via \<agent harness\>.
Use your actual model and harness names. If you are not certain of them, say so
plainly rather than inventing a version string.
+1 -1
View File
@@ -43,7 +43,7 @@ alias ...='cd ../..'
alias ....='cd ../../..'
# Tools
alias a='omarchy-launch-agent --inline'
alias a='omarchy-agent --inline'
alias c='opencode --auto'
alias cx='printf "\033[2J\033[3J\033[H" && claude --permission-mode bypassPermissions'
alias cy='codex -s danger-full-access -a never'
@@ -2,12 +2,19 @@
"manifest_version": 3,
"name": "WhatsApp Slim",
"version": "1.1",
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAszLhFYxjvKqx1VAORt3M6b0H1Kz03cmQ1MrUjJZ8gj3nhRcvufk6PS17yUIjEdKKmuj+1HuFVwOaHJQsACGJXwn2TeKQqZmZx/83yd7ydgabHXTsJbhy5O+Yunv+VA1B2IX/phcXpkH9bPv2PuC/bQ13nmmAvT2b+EMyq8EPQHOoSWhE4M4kIhLDczTmxfJX5X3tTxH66952DYuQWvGpIXLAUXkvIgap0qemW68/LymPaWzcg6g3oE/nZ7C0j7JE5bnqLFxqXNlA9Jt+Ss/1Y33QL3OSD9EEsrpgueS0bbqtkmFQ9/jGlruIL+qj0xe+CAtqbJ5ty9H/sqi01kjfyQIDAQAB",
"description": "Make WhatsApp Web follow the system theme and collapse its chat list in narrow windows, this extension is installed by Omarchy",
"content_scripts": [
{
"matches": ["https://web.whatsapp.com/*"],
"css": ["whatsapp.css"],
"js": ["system-theme.js"],
"matches": [
"https://web.whatsapp.com/*"
],
"css": [
"whatsapp.css"
],
"js": [
"system-theme.js"
],
"run_at": "document_start"
}
]
+2
View File
@@ -8,6 +8,8 @@ The private-use glyphs in `omarchy.ttf` are:
- `U+E903` — omp, from <https://omp.sh/favicon.svg>
- `U+E904` — Grok, from <https://grok.com/images/favicon.svg>
- `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>
The agent marks are monochrome so the menu can render them using the active
theme's foreground and selection colors.
Binary file not shown.
+3 -2
View File
@@ -20,8 +20,9 @@ o.window({
}, { tag = "+floating-window" })
-- The About fastfetch layout needs more columns than the standard float provides.
-- This size only positions the first paint: omarchy-launch-about then measures
-- the rendered content and fits the window to it exactly.
-- This size only covers the first launch: omarchy-launch-about measures the
-- rendered content, remembers the size that hugs it, and applies that as its own
-- rule before every later launch.
o.window("org.omarchy.about", { float = true })
o.window("org.omarchy.about", { center = true })
o.window("org.omarchy.about", { size = { 920, 480 } })
+1 -1
View File
@@ -93,7 +93,7 @@ o.bind("SUPER + CTRL + ALT + T", "Show time", "omarchy-notification-time")
o.bind("SUPER + CTRL + ALT + B", "Show battery remaining", "omarchy-notification-battery")
o.bind("SUPER + CTRL + ALT + W", "Toggle weather", "omarchy-notification-weather")
o.bind("SUPER + SHIFT + CTRL + A", "Agent", "omarchy-launch-agent")
o.bind("SUPER + SHIFT + CTRL + A", "Agent", "omarchy-agent --pick")
o.bind("SUPER + CTRL + A", "Audio", "omarchy-shell shell toggle omarchy.audio")
o.bind("SUPER + CTRL + B", "Bluetooth", "omarchy-shell shell toggle omarchy.bluetooth")
o.bind("SUPER + CTRL + D", "Display", "omarchy-shell shell toggle omarchy.monitor")
+11 -7
View File
@@ -38,7 +38,7 @@
// Learn
"learn.keybindings": {"icon":"","label":"Keybindings","action":"omarchy-menu-keybindings"},
"learn.omarchy": {"icon":"","label":"Omarchy","action":"omarchy-launch-webapp 'https://learn.omacom.io/2/the-omarchy-manual'"},
"learn.omarchy": {"icon":"","label":"Omarchy","action":"omarchy-launch-webapp 'https://omarchy.org/manual/'"},
"learn.hyprland": {"icon":"","label":"Hyprland","action":"omarchy-launch-webapp 'https://wiki.hypr.land/'"},
"learn.arch": {"icon":"󰣇","label":"Arch","action":"omarchy-launch-webapp 'https://wiki.archlinux.org/title/Main_page'"},
"learn.neovim": {"icon":"","label":"Neovim","action":"omarchy-launch-webapp 'https://www.lazyvim.org/keymaps'"},
@@ -84,6 +84,7 @@
"trigger.share.receive": {"icon":"󰥦","label":"Receive","action":"uwsm-app -- localsend"},
"trigger.toggle.idle-lock": {"icon":"󰅶","label":"Stay Awake","action":"omarchy-toggle-idle"},
"trigger.toggle.notifications": {"icon":"󰂛","label":"Notifications","action":"omarchy-toggle-notification-silencing"},
"trigger.toggle.crash-capture": {"icon":"󱚡","label":"Crash Capture","action":"omarchy-toggle-crash-capture"},
"trigger.toggle.screensaver": {"icon":"󱄄","label":"Screensaver","action":"omarchy-toggle-screensaver"},
"trigger.toggle.nightlight": {"icon":"󰔎","label":"Nightlight","action":"omarchy-toggle-nightlight"},
"trigger.toggle.top-bar": {"icon":"󰍜","label":"Menu Bar","action":"omarchy-toggle-bar"},
@@ -128,7 +129,7 @@
"setup.network.dns.custom": {"icon":"","label":"Custom","checked":"[[ \"$(omarchy-dns)\" == \"Custom\" ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-dns Custom'"},
"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"},
"setup.default.agent": {"icon":"󰚩","label":"Agent","title":"Default Agent"},
"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"},
@@ -138,7 +139,7 @@
"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.pi": {"icon":"","iconFont":"omarchy","label":"Pi","checked":"[[ \"$(omarchy-default-agent)\" == \"pi\" ]]","action":"omarchy-default-agent pi"},
"setup.default.browser": {"icon":"","label":"Browser"},
"setup.default.browser": {"icon":"","label":"Browser","title":"Default Browser"},
"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"},
@@ -146,12 +147,12 @@
"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"},
"setup.default.terminal": {"icon":"","label":"Terminal","title":"Default Terminal"},
"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"},
"setup.default.editor": {"icon":"","label":"Editor","title":"Default Editor"},
"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"},
@@ -201,6 +202,7 @@
"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'"},
@@ -227,9 +229,11 @@
"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.lm-studio": {"icon":"󱚤","label":"LM Studio","when":"! omarchy-pkg-present lmstudio-bin","action":"omarchy-install-app 'LM Studio' lmstudio-bin"},
"install.ai.ollama": {"icon":"󱚤","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.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"},
@@ -0,0 +1,19 @@
[Unit]
Description=Announce process crashes and offer an AI diagnosis
# Needs the session bus to notify, and uwsm-app to open the diagnosis terminal.
# Both are up only after graphical-session.target.
After=graphical-session.target
PartOf=graphical-session.target
ConditionEnvironment=WAYLAND_DISPLAY
# Set by omarchy-toggle-crash-capture. Checked here so a disabled watcher stays
# disabled across logins without the unit having to be disabled.
ConditionPathExists=!%h/.local/state/omarchy/toggles/crash-capture-off
[Service]
Type=simple
ExecStart=/usr/bin/omarchy-crash-watch
Restart=always
RestartSec=5
[Install]
WantedBy=graphical-session.target
+40
View File
@@ -1,5 +1,45 @@
HOOKS=(base udev plymouth keyboard autodetect microcode modconf kms keymap consolefont block encrypt filesystems fsck btrfs-overlayfs)
# The proprietary NVIDIA driver does early KMS itself: nvidia.conf (written by
# install/hardware/nvidia.sh, sourced before this file) early-loads nvidia_drm
# with modeset=1. Keeping the kms hook on such a system makes autodetect pull
# in nouveau — and ~100 MB of its GSP firmware — for a driver that never runs.
# Drop kms only when nvidia_drm is early-loaded and NVIDIA owns every display
# controller. Hybrid systems keep kms: their iGPU still needs it for early
# KMS at the LUKS prompt. So does anything unexpected, like a PCI tree that
# cannot be read.
#
# This reads MODULES midway through mkinitcpio's drop-in sourcing, so a
# later-sorting drop-in that resets MODULES outright — surface_device_modules.conf
# does — would strip nvidia_drm after kms was already dropped. Every machine
# Omarchy writes such a file for carries an Intel iGPU, which keeps kms here
# through the scan below; keep it that way.
if [[ " ${MODULES[*]:-} " == *" nvidia_drm "* ]]; then
_omarchy_nvidia_gpu=0
_omarchy_other_gpu=0
for _omarchy_pci in "${OMARCHY_PCI_DEVICES_PATH:-/sys/bus/pci/devices}"/*; do
if [[ ! -r $_omarchy_pci/class || ! -r $_omarchy_pci/vendor ]]; then
# An unreadable device could be another GPU. Inconclusive keeps kms.
_omarchy_other_gpu=1
continue
fi
[[ $(<"$_omarchy_pci/class") == "0x03"* ]] || continue
if [[ $(<"$_omarchy_pci/vendor") == "0x10de" ]]; then
_omarchy_nvidia_gpu=1
else
_omarchy_other_gpu=1
fi
done
if ((_omarchy_nvidia_gpu && !_omarchy_other_gpu)); then
_omarchy_hooks=()
for _omarchy_hook in "${HOOKS[@]}"; do
[[ $_omarchy_hook == "kms" ]] || _omarchy_hooks+=("$_omarchy_hook")
done
HOOKS=("${_omarchy_hooks[@]}")
fi
unset _omarchy_nvidia_gpu _omarchy_other_gpu _omarchy_pci _omarchy_hooks _omarchy_hook
fi
# Bundle vconsole.conf so Plymouth uses the configured keyboard layout at the
# LUKS prompt, but only when that layout types Latin letters. Passphrases are
# Latin characters, so bundling a Hebrew/Greek/Cyrillic/Arabic layout would
+2 -1
View File
@@ -17,4 +17,5 @@ systemctl --user enable --now \
omarchy-recover-internal-monitor.service \
omarchy-sleep-lock.service \
omarchy-migrate-notify.service \
omarchy-fcitx5.service
omarchy-fcitx5.service \
omarchy-crash-watch.service
+11
View File
@@ -0,0 +1,11 @@
#!/bin/bash
set -e
# Omarchy ships no default agent, so invite once rather than picking one. An
# agent already chosen means the invitation has nothing to offer.
if [[ -z $(omarchy-default-agent) ]] && omarchy-done ensure agent-setup-invitation; then
omarchy-notification-send -u critical -g 󰚩 "Set your default agent" \
"Let your favorite agent help with Omarchy." \
--exec "omarchy menu summon setup.default.agent"
fi
+11
View File
@@ -0,0 +1,11 @@
# Welcome to Omarchy!
Omarchy is an [omakase](https://manuals.omamix.org/3/omacom/76/omakase-computing) Linux distribution based on [Arch](https://archlinux.org/), the tiling window manager [Hyprland](https://hypr.land/), and the desktop construction-kit [Quickshell](https://quickshell.org/). It ships with everything a modern, savvy computer user needs to be productive immediately. From [Neovim](https://neovim.io/) (btw) to Chromium, [Obsidian](https://obsidian.md/) to LibreOffice, and Kdenlive to OBS Studio. Hell, even a retro Winamp-style music player is there!
This isn't just a grab bag of preinstalled packages, though. It's a complete system designed with both aesthetics and productivity in mind. Because a _beautiful_ system is a _motivating_ system, and productivity has always been [downstream from motivation](https://world.hey.com/dhh/beautiful-motivations-6fef7c73). There's zero bloat here: Just everything I use.
It's true that developing an eye for the beauty of a TUI-heavy, theme-delighted, tiling-window-managed system like Omarchy can be an acquired taste. But that's why you're here, isn't it? To experience something a little outside of your comfort zone? To embark on a little bit of an adventure into a new way of working with computers? I hope so.
Omarchy isn't like Windows and it's not like macOS either. It's not trying to be as familiar as possible. It's trying to be beautiful and _better_. Embrace the Linux-ness of it all. Manually editing some config files, sure. Heavy on the terminal, definitely.
Let's get started with the basics.
+39
View File
@@ -0,0 +1,39 @@
# Getting Started
Omarchy is installed using an ISO. You can choose between a full-disk install, which takes over the entire drive, or a free-space install, which puts Omarchy in the unallocated space on a drive — that's how you dual boot alongside Windows or another OS (see [dual-boot install](50-dual-boot-install.md) — note that you'll need to turn off BitLocker in Windows first). Either way, the installation defaults to full encryption, and the full-disk option will wipe the selected drive, so be sure to take a backup before using an existing one!
[Download the Omarchy ISO](https://omarchy.org/) first, put it on a USB stick (use [balenaEtcher](https://etcher.balena.io/) on Mac/Windows or [caligula](https://github.com/ifd3f/caligula) on Linux), and boot off the stick.
_You must turn off Secure Boot and/or TPM in the BIOS. You have to turn these off to be able to install Omarchy. They're Microsoft security schemes meant for Windows and Microsoft-affiliated Linux distributions._
Then answer the configuration questions, and confirm them like this:
![install-config](images/install-config.webp)
Then select a drive for your installation, and sit back and watch the installation show go. It can be done in under a minute on the fastest modern machines, but it shouldn't take more than 5 minutes even on an older computer.
![install-done](images/install-done.webp)
Now you're ready to Omarchy!
### Use a wired or 2.4ghz keyboard!
The full-disk encryption won't allow you to enter the password from a Bluetooth keyboard at startup. Just like you can't use a Bluetooth keyboard to enter the BIOS on a PC. You'll need a keyboard that either uses a 2.4ghz dongle or a cable (which is much nicer for latency anyway!). I personally love the [Lofree Flow84](https://www.lofree.co/products/lofree-flow-the-smoothest-mechanical-keyboard)!
### Installing for another owner
If you're setting up a machine for someone else — a family member, a new employee, a buyer — you shouldn't be answering the personal questions on their behalf. Hit `Ctrl + C` on the very first screen of the installer (the keyboard selection), and Omarchy will offer to prepare the machine for another owner instead. The system installs right away, but all the personal setup — keyboard layout, username, password — is deferred until the machine boots for the first time. The drive is still encrypted by default, and the password the new owner picks on that first boot becomes the encryption password too. (A machine you've already been using can be handed over without a reinstall too — see [resetting the computer](48-security.md).)
### Unattended installs
The ISO can also install completely on its own — no keyboard, no wizard — when it's handed its configuration on a second drive. That's the way to treat Omarchy as a base image for VMs and fleet machines. See [unattended installs](51-unattended-installs.md).
### No-encryption installations
Omarchy is installed with encryption by default. It's the safe, responsible choice for any computer that can possibly be lost or stolen. You don't want anyone with access to your hardware to be able to get your data!
But in special circumstances, like remote Omarchy installs on protected computers or for throw-away installations without sensitive data, you may want to install without encryption. You can hit `Ctrl + C` on the disk formatting confirmation to switch to an encryption-less installation.
### Help if you're stuck
If you get stuck, you can usually find someone willing to help in the _#omarchy-help_ channel on [the community Discord](https://omarchy.org/discord).
+57
View File
@@ -0,0 +1,57 @@
# Coming From Mac or Windows
If you've spent years on macOS or Windows, your fingers know a hundred things your brain has forgotten it ever learned. This chapter is the translation layer: where those instincts go in Omarchy. The features themselves are covered in depth elsewhere — this is just the map.
### Super is the center of everything
All the muscle memory you've built around Cmd or the Windows key transfers to one key: Super. That's the Windows key on a PC keyboard, and it's the anchor for nearly every hotkey in Omarchy.
Your Spotlight or Raycast or Start-menu reflex becomes `Super + Space`. That opens the Omarchy menu, which launches apps, changes settings, installs software, captures the screen — just about everything. Start typing to filter. There's also a dedicated apps-only menu on `Super + Alt + Space`. See [navigation](04-navigation.md).
### There's no dock and no desktop icons
Nothing to click to launch things, no icons to arrange on the desktop. Apps start from a hotkey (`Super + Return` for the terminal, `Super + Shift + Return` for the browser, and `Super + K` for a list of everything that's mapped) or from the menu. The one persistent piece of UI is [the top bar](05-the-top-bar.md), which covers what the menu bar, system tray, and Notification Center did for you before — and nearly every widget on it does something on left, right, and middle click.
### Windows place themselves
The biggest mindset shift: you don't drag windows around or snap them to screen halves. Open a window and it takes the whole screen. Open a second and they split it. You never fish a window out from under another one, because windows don't overlap.
When you genuinely need a floating window, `Super + T` toggles the active one out of the tiling (and back). But give the tiling a real chance first — it's the heart of the whole thing. [Navigation](04-navigation.md) walks you through it.
Workspaces will feel familiar: they're macOS Spaces or Windows virtual desktops, except you'll actually use them, because `Super + 1/2/3/4` jumps straight to one and `Super + Shift + 1/2/3/4` sends the active window there. No animation delay, just instant jumps. This means that you might not even need multiple monitors, if you were used to that before.
### Copy and paste just work
On the Mac you had Cmd + C everywhere. On Windows you had Ctrl + C everywhere — except the terminal, where it kills your program. Omarchy gives you `Super + C`, `Super + X`, and `Super + V`, and they work everywhere, including the terminal. No separate reflex to learn for the shell.
Windows folks: your Win + V clipboard history lives on `Super + Ctrl + V`, and it holds images as well as text. See [unified clipboard & history](08-unified-clipboard-history.md).
### The translation table
| You reach for | In Omarchy |
| ------------- | ---------- |
| Spotlight / Raycast / Start menu | `Super + Space` — the Omarchy menu |
| AirDrop | LocalSend, via `Super + Ctrl + S` — see [GUIs](22-guis.md) |
| Cmd + Shift + 4 / Win + Shift + S | `Print Screen` — see [screenshots & recording](12-screenshots-recording.md) |
| Notification Center | Notification history on `Super + Shift + Alt + ,` |
| Time Machine (for the system) | Automatic [system snapshots](47-system-snapshots.md) on every update |
| App Store / downloading an installer | _Install_ in the menu, or `omarchy pkg add` — see [other packages](29-other-packages.md) |
| System Settings / Control Panel | _Setup_ in the menu, which edits plain config files — see [dotfiles](31-dotfiles.md) |
### Some things really are different
A lot of settings live in text files you edit, not panels you click through. That sounds primitive until you realize it means every tweak can be seen, copied to your next machine, and put in version control. The _Setup_ menu drops you straight into the right file and restarts whatever needs restarting when you're done.
Updates come through one command — _Update > Omarchy_ — that updates Omarchy itself and every package on the system, taking a snapshot first. No per-app updaters nagging you at random. See [updates](30-updates.md).
Software comes from a package manager, not from downloaded installers.
And when you close a window, the app actually quits. There's no macOS limbo where the program keeps running with no windows. `Super + W` means gone.
### On Mac hardware
Omarchy runs well on Intel Macs — see [Mac support](44-mac-support.md). And the keyboard is kind to you: Omarchy doesn't remap anything, and Linux treats the Command key as Super, so Super sits right where Cmd always was. Your thumb won't notice the move.
### Give it two weeks
The instincts transfer faster than you'd think. Skim the [hotkeys](07-hotkeys.md) chapter once, and whenever you blank on a binding, hit `Super + K` — it shows you all of them. That's the only hotkey you actually have to memorize.
+71
View File
@@ -0,0 +1,71 @@
# Navigation
Everything in Omarchy happens via the keyboard — _EVERYTHING!_ When the system first starts, you literally can't do a thing with the mouse alone. But you can hit `Super + Space` to reveal the Omarchy Menu and from here you to do just about everything.
But the Omarchy menu is not even intended to be the main way to operate the system most of the time. We can get faster than that! All the most important applications are bound directly to individual hotkeys. You start the terminal with `Super + Return` and a browser with `Super + Shift + Return`. Try doing one after the other, and you'll see the magic of Hyprland's tiling in action:
![navigation-browser-terminal](images/navigation-browser-terminal.webp)
You can then hit `Super + J` to stack them on top of each other instead of side by side:
![navigation-stacked](images/navigation-stacked.webp)
Hit `Super + J` again to return them to their side-by-side positions. Then try `Super + Shift + Arrow Right` while on the browser to swap the windows.
Now try `Super + Ctrl + T` to start the Activity monitor. That'll appears as a floating window. You can tile it using `Super + T` (and hit that again to make it floating again). Now press `Super + Shift + F` to open the files manager. You'll have a neat four-way setup:
![navigation-fourway-tiling](images/navigation-fourway-tiling.webp)
You navigate between the window you want to be active with `Super + Arrow`. This will switch focus and move the cursor to the center of the new application.
If you hit `Super + Shift + 2`, you'll move the current focused application onto the second workspace. `Super + Shift + 1` moves it back. (And `Super + Shift + Alt + 2` will move the current focused application onto the second workspace without switching to it).
If you hold down `Super` and use the mouse to click on a window, you'll be able to rearrange where it sits. If you hold `Super` and use the right button on the mouse, you can freely resize the window.
You close a window on `Super + W` (and close all windows on `Ctrl + Alt + Delete`).
You can also go full screen with `Super + F` or even just full-width (keeping the top bar) with `Super + Alt + F` or full-screen within a window with `Super + Ctrl + F` (good for YouTube!).
### Dwindle vs scrolling layout
Omarchy's default layout is called dwindle. It keeps all the windows you open on a single workspace visible at all time, even if it has to shrink them down.
![navigation-dwindle-layout](images/navigation-dwindle-layout.webp)
But you can also choose to turn a workspace into the scrolling layout where windows are lined up side-by-side, beyond the visible edge of the display. You turn a single workspace into this layout via `Super + L`.
![navigation-scrolling-layout](images/navigation-scrolling-layout.webp)
The choice is per workspace, and it sticks. So you can keep workspace 1 on dwindle for browsing and workspace 2 on scrolling for code, and they'll come back that way after a restart. (The same toggle is under _Trigger > Toggle > Workspace Layout_ in the Omarchy menu).
If you wish to use the scrolling layout as the default, you can set that in `~/.config/hypr/looknfeel.lua`:
```lua
hl.config({
general = {
layout = "scrolling",
},
})
```
### Grouping windows
Windows can be grouped using `Super + G`. Once you're in a group, every window you start while that's active will belong to the group. You can move between these grouped windows using `Super + Ctrl + Arrow Left/Right` or `Super + Alt + 1/2/3/4` to go directly to grouped window in order.
You can move a window out of the grouping with `Super + Alt + G` or disassemble the entire group by hitting `Super + G` again. Finally, you can move windows outside the group into it with `Super + Alt + Arrows`.
### Popping windows
You can pop a window out of its workspace allocation with `Super + O`. That'll pin it as a floating window that follows you on whatever workspace you go to. Great for video players and the like.
![navigation-popped-window](images/navigation-popped-window.webp)
### Scratchpad workspace
Finally, there's a special scratchpad workspace that overlays on whatever workspace you're currently on. You access what's on that using `Super + S` and you place windows there using `Super + Alt + S`.
It works well for controls or perhaps a terminal that you quickly want to interact with in an overlay without leaving the current workspace. If you want to move a window off the scratchpad, you just move it directly to another workspace with something like `Super + Shift + 1` to move it to workspace 1.
### It takes some getting used to!
It takes a little while to get used to navigating your desktop like this, but once you do, it'll be hard to go back to a traditional mouse-driven desktop experience!
+134
View File
@@ -0,0 +1,134 @@
# The Top Bar
The strip along the top of your screen is the Omarchy bar. It's not a bolted-on status bar but part of the Omarchy shell, the single long-running Quickshell process that also draws the menu, the notifications, the OSD popups, and the lock screen. That's why it themes perfectly with everything else and why a panel opens instantly instead of spawning a new app.
It's also the one piece of the desktop that's always on screen, so it's worth knowing what all those little glyphs do.
## What's on it by default
The bar has three sections. On the left sits the Omarchy logo (the menu launcher) and the workspace indicators. In the center you get the status indicators, the clock, the keyboard layout, the weather, and an Omarchy update badge. On the right: the system tray, agents, bluetooth, network, audio, display, and power.
A few of those only show up when they have something to say. The keyboard layout appears only if you've configured more than one layout. The update badge appears only when there's an Omarchy update waiting. And the agents icon appears the first time Omarchy finds AI coding usage on the machine (see [AI](17-ai.md)).
## Clicking around
Nearly every widget does something on left, right, and middle click, and several respond to scrolling. This is the part people miss — the right and middle buttons are where a lot of the good stuff hides.
| Widget | Left | Right | Middle / scroll |
| --- | --- | --- | --- |
| Menu | Omarchy menu | New terminal | — |
| Workspaces | Focus that workspace | — | — |
| Clock | Calendar popup | Cycle the label format | Middle: timezone picker |
| Weather | Forecast popup | Full weather as a notification | Middle: refresh |
| Audio | Audio panel | Mute | Middle: panel · scroll: volume |
| Microphone | Mute the mic | — | Middle: audio panel · scroll: input volume |
| Network | Network panel | — | — |
| Bluetooth | Bluetooth panel | Toggle the radio | — |
| Display | Display panel | — | Scroll: brightness |
| Power | Power panel | Toggle the battery percentage | — |
| Media | Play/pause | Cover-art popup | Middle: next · scroll: prev/next |
| Agents | Agents panel | Launch your agent | Middle: next subscription |
| Tray | Hover to reveal the drawer | Right on the chevron to manage | — |
| Omarchy update | Run the update | — | — |
Not everything in that table is on your bar out of the box. The media widget (MPRIS now-playing, with a scrolling track and artist) and the microphone widget are both built in but off by default — add them if you want them, as described below.
## The panels
Clicking a bar icon opens a panel, which is a proper popup with sliders, lists, and keyboard navigation rather than a tooltip. Each one also has a hotkey, so you never have to aim at a 16-pixel glyph:
| Hotkey | Panel |
| --- | --- |
| `Super + Ctrl + A` | Audio |
| `Super + Ctrl + W` | Network |
| `Super + Ctrl + B` | Bluetooth |
| `Super + Ctrl + D` | Display |
| `Super + Ctrl + P` | Power |
| `Super + Ctrl + Alt + D` | Calendar |
| `Super + Ctrl + 1-9` | Toggle the nth panel in the right section |
The panels aren't read-outs. They're where you actually do the thing:
- **Audio** has a master volume slider, an output-device picker, and a per-app mixer, so you can turn down that one browser tab without touching everything else.
- **Network** scans for Wi-Fi, shows signal strength, connects, and lets you pick a DNS provider.
- **Bluetooth** lists your devices with connect/disconnect and battery levels.
- **Power** shows battery stats, switches power profiles (it remembers a separate choice for battery and AC), and prints some system info.
- **Display** carries a brightness slider, text size, monitor scaling presets, and — when you have more than one screen — per-monitor controls. See [monitors](33-monitors.md) for the deeper story.
- **Clock** opens a month grid with ISO week numbers and month stepping.
Every panel takes the keyboard as well as the mouse: arrows move, Return activates, Tab steps to the neighbouring panel, and Escape closes.
`Super + Ctrl + 1-9` counts panels left to right in the right section, skipping the tray since it has no panel of its own. So the number matches the icon you'd point at.
### Tailscale and Dropbox
Two more widgets appear on the bar only once you install the matching service from **Install → Service**, and both are worth knowing about because they do more than report status.
The **Tailscale** panel connects and disconnects the tailnet, switches between accounts, and picks an exit node (your own machines and Mullvad regions both show up in that list). It also browses your machines — and with one selected, `s` sends files to it over Taildrop, which is the fastest way to move a file to your phone or another laptop. `c` copies the machine's IP, `n` its name, and `d` its full DNS name. There's a send button on each machine row too, if you'd rather click. The same thing from the terminal is `omarchy tailscale send <machine> [file...]`.
The **Dropbox** panel handles login, shows how much storage you've used, and lists recently synced files.
Removing either service takes its widget back off the bar.
## Indicators
The little cluster in the center is the indicators widget. These are status glyphs for modes you've turned on: do not disturb, night light, a queued [reminder](09-reminders.md), an active screen recording, stay awake, and [dictation](11-text-extraction-dictation.md). They light up when the mode is active and otherwise stay out of the way — hover the center of the bar to peek at the inactive ones. Clicking an indicator toggles that mode.
If you'd rather they were always visible, set `alwaysShow` to `true` on the widget. And if you only care about some of them, list the ones you want in `items`: `["Dnd", "Reminder", "NightLight"]`. You can have more than one indicators widget, so different sections can show different subsets.
## Rearranging the bar
The bar configures itself. You don't have to open a config file to move things.
Grab an empty patch of the bar around the center and drag it toward another screen edge, and the bar moves there — left, right, top, or bottom all work, and every widget adapts (vertical bars fall back to compact icon-only forms). A click-and-hold starts the same drag. Double-left-click that same empty space to toggle transparency. And drag any widget to reorder it or throw it into another section.
If you'd rather pick from a menu, **Style → Menu Bar** has both position and transparency.
The same things have commands, which is what you want for a [dotfiles](31-dotfiles.md) setup:
```bash
omarchy bar position bottom
omarchy bar transparent toggle
omarchy bar move omarchy.clock --section center --index 0
omarchy bar set omarchy.clock format "HH:mm"
omarchy bar defaults # back to the shipped layout
```
To add or remove a widget entirely, use the plugin commands. `omarchy plugin list` prints every widget the shell knows about with its id, and then:
```bash
omarchy plugin enable omarchy.media --section center
omarchy plugin disable omarchy.weather
```
## Hiding the bar
`Super + Shift + Space` toggles the bar off and back on without killing the shell — panels and hotkeys keep working, you just get the pixels back. It's also in the menu under **Trigger → Toggle → Menu Bar**.
## The config file
All of it is stored in `~/.config/omarchy/shell.json`, under the `bar` key. Here's a trimmed version:
```json
{
"version": 1,
"bar": {
"position": "top",
"transparent": false,
"centerAnchor": "omarchy.clock",
"layout": {
"left": [{ "id": "omarchy.menu" }, { "id": "omarchy.workspaces" }],
"center": [{ "id": "omarchy.clock", "format": "HH:mm" }],
"right": [{ "id": "omarchy.audio" }, { "id": "omarchy.power" }]
}
}
}
```
Every widget is one entry in one of the three layout arrays, and its settings sit inline on that entry — there's no separate settings file and no `config` sub-object. The clock's `format`, `formatAlt` (what right-click cycles to), and `verticalFormat` all live right there on `{ "id": "omarchy.clock" }`.
`centerAnchor` names the one center widget that gets pinned to the exact center of the screen, with the others flanking it. That's how the clock stays dead center even as the weather and update badge come and go. Set it to an empty string and the center list is just centered as a group instead.
One rule worth internalizing: **once you have your own `shell.json`, it's canonical**. Until you customize anything, the shell reads Omarchy's default file. The moment you drag a widget, run `omarchy bar`, or edit the file yourself, you own it — there's no deep merge, so new default widgets in future Omarchy releases won't appear on your bar automatically. `omarchy bar defaults` puts the shipped layout back whenever you want a clean slate.
The same file also holds your idle timings at the top level, outside the `bar` key: `idle.screensaver` and `idle.lock`, both in seconds since you went idle. So the default screensaver kicks in at 150 seconds and the lock at 300.
+127
View File
@@ -0,0 +1,127 @@
# Themes
Omarchy comes with twenty-two beautiful themes. You can select between them via _Style > Theme_ in the Omarchy Menu (`Super + Space`) or hop directly to the theme selector using `Super + Ctrl + Shift + Space`.
Each theme styles the desktop, terminal, neovim, activity screen (btop), Chromium, and the entire Omarchy shell: top bar, menu, notifications, OSD, and the lock screen. (For Obsidian, you must manually select the Omarchy theme via _Appearance > Themes_ inside the app).
Themes have a set of background images that you can pick between using `Super + Ctrl + Space`.
You can find even more themes on [the extra themes page](https://omarchy.org/themes/) or even [make your own theme](43-making-your-own-theme.md).
![tokyo-night](../themes/tokyo-night/preview.png)
_Tokyo Night_
![catppuccin](../themes/catppuccin/preview.png)
_Catppuccin_
![lumon](../themes/lumon/preview.png)
_Lumon_
![ethereal](../themes/ethereal/preview.png)
_Ethereal_
![everforest](../themes/everforest/preview.png)
_Everforest_
![gruvbox](../themes/gruvbox/preview.png)
_Gruvbox_
![miasma](../themes/miasma/preview.png)
_Miasma_
![hackerman](../themes/hackerman/preview.png)
_Hackerman_
![osaka-jade](../themes/osaka-jade/preview.png)
_Osaka Jade_
![kanagawa](../themes/kanagawa/preview.png)
_Kanagawa_
![nord](../themes/nord/preview.png)
_Nord_
![matte-black](../themes/matte-black/preview.png)
_Matte Black_
![vantablack](../themes/vantablack/preview.png)
_Vantablack_
![ristretto](../themes/ristretto/preview.png)
_Ristretto_
![retro-82](../themes/retro-82/preview.png)
_Retro 82_
![flexoki-light](../themes/flexoki-light/preview.png)
_Flexoki Light_
![rose-pine](../themes/rose-pine/preview.png)
_Rose Pine_
![catppuccin-latte](../themes/catppuccin-latte/preview.png)
_Catppuccin Latte_
![white](../themes/white/preview.png)
_White_
### Unlocks
Themes can also have a custom unlock design, which is used for the boot decryption process. You can select one of these under _Style > Unlock_. They look like this:
![catppuccin](../themes/catppuccin/preview-unlock.png)
_Catppuccin_
![catppuccin-latte](../themes/catppuccin-latte/preview-unlock.png)
_Catppuccin Latte_
![ethereal](../themes/ethereal/preview-unlock.png)
_Ethereal_
![everforest](../themes/everforest/preview-unlock.png)
_Everforest_
![flexoki-light](../themes/flexoki-light/preview-unlock.png)
_Flexoki Light_
![gruvbox](../themes/gruvbox/preview-unlock.png)
_Gruvbox_
![hackerman](../themes/hackerman/preview-unlock.png)
_Hackerman_
![kanagawa](../themes/kanagawa/preview-unlock.png)
_Kanagawa_
![lumon](../themes/lumon/preview-unlock.png)
_Lumon_
![matte-black](../themes/matte-black/preview-unlock.png)
_Matte Black_
![miasma](../themes/miasma/preview-unlock.png)
_Miasma_
![nord](../themes/nord/preview-unlock.png)
_Nord_
![osaka-jade](../themes/osaka-jade/preview-unlock.png)
_Osaka Jade_
![retro-82](../themes/retro-82/preview-unlock.png)
_Retro 82_
![ristretto](../themes/ristretto/preview-unlock.png)
_Ristretto_
![rose-pine](../themes/rose-pine/preview-unlock.png)
_Rose Pine_
![tokyo-night](../themes/tokyo-night/preview-unlock.png)
_Tokyo Night_
![vantablack](../themes/vantablack/preview-unlock.png)
_Vantablack_
![white](../themes/white/preview-unlock.png)
_White_
+373
View File
@@ -0,0 +1,373 @@
# Hotkeys
You can see all the main keyboard bindings with `Super + K` (Tmux bindings with `Super + Alt + K` and Herdr bindings with `Super + Ctrl + K`).
## Navigating
| Hotkey | Function |
| ----------------------- | --------------------- |
| `Super + Space` | Omarchy menu (apps and everything else) |
| `Super + Alt + Space` | Apps menu |
| `Super + Escape` | System menu (suspend, restart, etc) |
| `Super + Ctrl + L` | Lock computer |
| `Super + W` | Close window |
| `Ctrl + Alt + Del` | Close all windows |
| `Super + T` | Toggle window between tiling/floating |
| `Super + J` | Toggle window position (horizontal/vertical) |
| `Super + O` | Toggle popping window into sticky'n'floating |
| `Super + L` | Toggle between dwindle and scrolling layout |
| `Super + P` | Toggle pseudo window style (natural v stretch) |
| `Super + F` | Go full screen |
| `Super + Alt + F` | Go full width |
| `Super + Ctrl + F` | Go full screen inside window |
| `Super + 1/2/3/4` | Jump to specific workspace |
| `Super + Tab` | Jump to next workspace |
| `Super + Shift + Tab` | Jump to previous workspace |
| `Super + Ctrl + Tab` | Jump to former workspace |
| `Super + Shift + 1/2/3/4` | Move window to workspace |
| `Super + Shift + Alt + 1/2/3/4` | Move window to workspace without following |
| `Super + Shift + Alt + Arrows` | Move workspaces to directional monitor |
| `Super + Arrow` | Move focus to window in direction of arrow |
| `Super + Shift + Arrow` | Swap window with another in direction of arrow |
| `Super + Minus` | Expand window left |
| `Super + Equal` | Shrink window left |
| `Super + Shift + Minus` | Shrink window up |
| `Super + Shift + Equal` | Expand window down |
| `Super + Alt + Minus/Equal` | Same resizing in smaller steps |
| `Super + Ctrl + Minus/Equal` | Same resizing in bigger steps |
| `Super + Alt + Home` | Save window width |
| `Super + Home` | Restore saved window width |
| `Super + Left Mouse` | Drag window around |
| `Super + Right Mouse` | Resize window |
| `Super + Scroll Wheel` | Scroll through workspaces |
| `Super + G` | Toggle window grouping |
| `Super + Alt + G` | Move window out of grouping |
| `Super + Alt + Tab` | Cycle between windows in grouping |
| `Super + Alt + Shift + Tab` | Cycle backward between windows in grouping |
| `Super + Alt + 1/2/3/4/5` | Jump to specific window in grouping |
| `Super + Alt + Arrow` | Move window into grouping in direction of arrow |
| `Super + Ctrl + Left/Right` | Move between windows inside a tiling group |
| `Super + S` | Show scratchpad workspace overlay |
| `Super + Alt + S` | Move window to scratchpad workspace |
| `Super + Ctrl + Z` | Zoom in on screen (repeat for more zoom) |
| `Super + Ctrl + Alt + Z` | Zoom fully out from screen |
| `Super + /` | Step forward through monitor scaling options |
| `Super + Alt + /` | Step backward through monitor scaling options |
| `Alt + Tab` | Cycle forward through windows on the active workspace |
| `Alt + Shift + Tab` | Cycle backward through windows on the active workspace |
| `Ctrl + Alt + Tab`| Cycle focus forward through monitors |
| `Ctrl + Alt + Shift + Tab`| Cycle focus backwards through monitors |
## System controls
| Hotkey | Function |
| ----------------------- | --------------------- |
| `Super + Ctrl + A` | Audio panel |
| `Super + Ctrl + B` | Bluetooth panel |
| `Super + Ctrl + W` | Wifi/network panel |
| `Super + Ctrl + D` | Display panel |
| `Super + Ctrl + P` | Power panel |
| `Super + Ctrl + Alt + D` | Calendar panel |
| `Super + Ctrl + 1-9` | Toggle bar panel by position |
| `Super + Ctrl + S` | Share menu (via LocalSend) |
| `Super + Ctrl + T` | Activity (btop) |
| `Super + Ctrl + C` | Capture controls (screenshot/-recording/picker) |
| `Super + Ctrl + O` | Toggle menu |
| `Super + Ctrl + H` | Hardware menu |
| `Super + Ctrl + Q` | Calculator |
| `Super + Ctrl + E` | Emoji picker |
| `Super + Ctrl + .` | Transcode media |
| `Super + Shift + Ctrl + A` | Pick an AI agent |
## Adjustments
| Hotkey | Function |
| ----------------------- | --------------------- |
| `Shift + Brightness Up` | Maximum screen brightness |
| `Shift + Brightness Down` | Minimum screen brightness |
| `Alt + Brightness Up/Down` | Precise 1% brightness changes |
| `Alt + Volume Up/Down` | Precise 1% volume changes |
| `Keyboard Brightness Up/Down` | Keyboard backlight brighter/dimmer |
| `Keyboard Backlight` | Cycle the keyboard backlight levels |
| `Alt + Play` | Next track |
| `Alt + Shift + Play` | Previous track |
## Launching apps
| Hotkey | Function |
| ----------------------- | --------------------- |
| `Super + Return` | Terminal |
| `Super + Alt + Return` | Tmux terminal |
| `Super + Ctrl + Return` | Herdr (agent manager) |
| `Super + Shift + Return` | Browser |
| `Super + Shift + Alt + B` | Browser (private/incognito) |
| `Super + Shift + F` | File manager |
| `Super + Shift + Alt + F` | File manager in cwd of terminal |
| `Super + Shift + M` | Music (Spotify) |
| `Super + Shift + Alt + M` | Music (cliamp) |
| `Super + Shift + /` | Password manager (1password) |
| `Super + Shift + N` | Editor (Neovim) |
| `Super + Shift + C` | Calendar ([HEY](https://hey.com/)) |
| `Super + Shift + E` | Email ([HEY](https://hey.com/)) |
| `Super + Shift + Alt + E` | New email ([HEY](https://hey.com/)) |
| `Super + Shift + A` | AI (ChatGPT) |
| `Super + Shift + Alt + A` | AI (Grok) |
| `Super + Shift + G` | Messenger (Signal) |
| `Super + Shift + P` | Google Photos |
| `Super + Shift + S` | Google Maps |
| `Super + Shift + Alt + G` | Messenger (WhatsApp) |
| `Super + Shift + Ctrl + G` | Messenger (Google) |
| `Super + Shift + D` | Docker (LazyDocker) |
| `Super + Shift + O` | Obsidian |
| `Super + Shift + W` | Writing (Omawrite) |
| `Super + Shift + X` | X |
| `Super + Shift + Alt + X` | X Compose |
| `Super + Shift + Y` | YouTube |
Change/add bindings in `~/.config/hypr/bindings.lua`.
## Universal clipboard
| Hotkey | Function |
| ----------------------- | --------------------- |
| `Super + C` | Copy |
| `Super + X` | Cut (not in terminal) |
| `Super + V` | Paste |
| `Super + Ctrl + V` | Clipboard manager |
Usually on Linux, you need `Ctrl + Shift + C/V` to copy'n'paste in the terminal and `Ctrl + C/V` to do it everywhere else. These Omarchy unified clipboard hotkeys work everywhere.
## Capture
| Hotkey | Function |
| ------------------- | ------------------------------ |
| `Super + Ctrl + C` | Capture menu (for keyboards w/o PrintScr button) |
| `Print Screen` | Screenshot |
| `Alt + Print Screen` | Screenrecord |
| `Super + Print Screen` | Color picker |
| `Super + Ctrl + Print Screen` | Text extraction to clipboard |
| `Super + Alt + [` | Make webcam overlay smaller while recording |
| `Super + Alt + ]` | Make webcam overlay larger while recording |
| `Alt + Shift + L` | Copy current URL from webapp or Chromium |
| `Alt + Shift + D` | Download the video on the current page to `~/Videos` |
| `Super + Ctrl + X` | Start/stop dictation (requires _Install > AI > Dictation_) |
| `F9` | Push-to-talk dictation (requires _Install > AI > Dictation_) |
With screenrecordings, the hotkey first asks which audio you want, then starts recording. Hit it again to stop. See [screenshots and recording](12-screenshots-recording.md) for the details.
All capture options are also accessible under _Trigger > Capture_ in the Omarchy menu (`Super + Space`).
## Notifications
| Hotkey | Function |
| ----------------------- | --------------------- |
| `Super + ,` | Dismiss latest notification |
| `Super + Shift + ,` | Dismiss all notifications |
| `Super + Ctrl + ,` | Toggle silencing notifications |
| `Super + Alt + ,` | Invoke most recent notification |
| `Super + Shift + Alt + ,` | Open notification history |
## Style
| Hotkey | Function |
| ----------------------- | --------------------- |
| `Super + Ctrl + Shift + Space` | Pick a new theme |
| `Super + Ctrl + Space` | Pick theme background |
| `Super + Backspace` | Toggle transparency on a window |
| `Super + Ctrl + Backspace` | Toggle single-window square aspect |
Extra background images live in `~/.config/omarchy/backgrounds/<theme name>`. Also available via _Install > Style > Background_ in the Omarchy menu.
All style options are also accessible under _Style_ in the Omarchy menu (`Super + Space`).
## Toggles
| Hotkey | Function |
| ----------------------- | --------------------- |
| `Super + Ctrl + I` | Toggle locking on idle |
| `Super + Ctrl + N` | Toggle nightlight display temperature |
| `Super + Ctrl + Delete` | Toggle laptop display on/off |
| `Super + Ctrl + Alt + Delete` | Toggle laptop display mirroring |
| `Super + Shift + Space` | Toggle the top bar |
| `Shift + Mute` | Switch to next audio output |
| `Shift + Play` | Switch to next media source |
| `Super + Shift + Backspace` | Toggle window gaps |
## Reminders
| Hotkey | Function |
| ----------------------- | --------------------- |
| `Super + Ctrl + R` | Set a reminder |
| `Super + Ctrl + Alt + R` | See all reminders |
| `Super + Ctrl + Shift + R` | Clear all reminders |
## Notices
| Hotkey | Function |
| ----------------------- | --------------------- |
| `Super + Ctrl + Alt + T` | Show time as notification |
| `Super + Ctrl + Alt + B` | Show battery as notification |
| `Super + Ctrl + Alt + W` | Toggle weather as notification |
## Tmux
The prefix key is `Ctrl + Space` (`Ctrl + B` also works). You can change these bindings in `~/.config/tmux/tmux.conf`.
### Panes
| Hotkey | Function |
| ----------------------- | --------------------- |
| `Prefix + v` | Split pane beside (vertical) |
| `Prefix + h` | Split pane below (horizontal) |
| `Prefix + x` | Kill pane |
| `Prefix + z` | Toggle pane zoom (fullscreen) |
| `Alt + Enter` | Split pane below (no prefix) |
| `Alt + Shift + Enter` | Split pane beside (no prefix) |
| `Alt + Escape` | Kill pane (no prefix) |
| `Ctrl + Alt + Arrows` | Move between panes |
| `Ctrl + Alt + Shift + Arrows` | Resize panes |
### Windows
| Hotkey | Function |
| ----------------------- | --------------------- |
| `Prefix + c` | New window |
| `Prefix + k` | Kill window |
| `Prefix + r` | Rename window |
| `Alt + 1-9` | Go to specific window |
| `Alt + Arrow Left/Right` | Move between windows |
| `Alt + Shift + Arrow Left/Right` | Move window left/right |
### Sessions
| Hotkey | Function |
| ----------------------- | --------------------- |
| `Prefix + C` | New session |
| `Prefix + K` | Kill session |
| `Prefix + R` | Rename session |
| `Prefix + N` | Next session |
| `Prefix + P` | Previous session |
| `Alt + Arrow Up/Down` | Move between sessions |
| `Prefix + s` | List sessions |
| `Prefix + d` | Detach from session |
### Copy mode (vi-style)
| Hotkey | Function |
| ----------------------- | --------------------- |
| `Prefix + [` | Enter copy mode |
| `v` | Begin selection (in copy mode) |
| `y` | Copy selection (in copy mode) |
### General
| Hotkey | Function |
| ----------------------- | --------------------- |
| `Prefix + q` | Reload config |
| `Prefix + ?` | Show Tmux keybindings |
| `Prefix + :` | Command prompt |
### Tmux layout functions
These functions must be run inside a Tmux session.
| Command | Function |
| ------- | -------- |
| `tdl <ai> [<second_ai>]` | Create dev layout with editor, AI, and terminal |
| `tdlm <ai> [<second_ai>]` | Create dev layout per subdirectory |
| `tsl <count> <command>` | Create multi-pane swarm layout |
## Ghostty Terminal
Ghostty terminal is installed using _Install > Terminal_ via the Omarchy menu.
| Hotkey | Function |
| ----------------------- | --------------------- |
| `Ctrl + Shift + E` | New split below |
| `Ctrl + Shift + O` | New split besides |
| `Ctrl + Alt + Arrows` | Move between splits |
| `Super + Ctrl + Shift + Arrows` | Resize split by 10 lines |
| `Super + Ctrl + Shift + Alt + Arrows` | Resize split by 100 lines |
| `Ctrl + Shift + T` | New tab |
| `Ctrl + Shift + Arrows` | Move between tabs |
| `Alt + Numbers` | Go to specific tab |
| `Shift + Pg Up/Down` | Scroll the history |
| `Ctrl + Left mouse` | Open link in browser |
## File Manager
| Hotkey | Function |
| ------------------- | ------------------------------ |
| `Ctrl + L` | Go to path |
| `Space` | Preview file (arrows navigate) |
| `Backspace` | Go back one folder |
## Neovim (w/ lazyvim)
### Navigation
| Hotkey | Function |
| ------------------------ | ------------------------------- |
| `Space` | Show command options |
| `Space Space` | Open file via fuzzy search |
| `Space E` | Toggle sidebar |
| `Space G G` | Show git controls |
| `Space S G` | Search file content |
| `Ctrl + W W` | Jump between sidebar and editor |
| `Ctrl + Left/right arrow` | Change size of sidebar |
| `Shift + H` | Go to left file tab |
| `Shift + L` | Go to right file tab |
| `Space B D` | Close file tab |
### While in sidebar
| Hotkey | Function |
| ------------------------ | ------------------------------- |
| `A` | Add new file in parent dir |
| `Shift + A` | Add new subdir in parent dir |
| `D` | Delete highlighted file/dir |
| `M` | Move highlighted file/dir |
| `R` | Rename highlighted file/dir |
| `?` | Show help for all commands |
[See all the Neovim hotkeys configured by LazyVim](https://www.lazyvim.org/keymaps).
## Quick Emojis
You can use `Super + Ctrl + E` to show a complete emoji picker that'll put the selection on the clipboard or you can use these quick access options.
| Hotkey | EM | Clue |
| ------------ | -- | ---------- |
| `CapsLock M S` | 😄 | smile |
| `CapsLock M C` | 😂 | cry |
| `CapsLock M L` | 😍 | love |
| `CapsLock M V` | ✌️ | victory |
| `CapsLock M H` | ❤️ | heart |
| `CapsLock M Y` | 👍 | yes |
| `CapsLock M N` | 👎 | no |
| `CapsLock M F` | 🖕 | fuck |
| `CapsLock M W` | 🤞 | wish |
| `CapsLock M R` | 🤘 | rock |
| `CapsLock M K` | 😘 | kiss |
| `CapsLock M E` | 🙄 | eyeroll |
| `CapsLock M I` | 😉 | wink |
| `CapsLock M P` | 🙏 | pray |
| `CapsLock M D` | 🤤 | drool |
| `CapsLock M M` | 💰 | money |
| `CapsLock M X` | 🎉 | xellebrate |
| `CapsLock M 1` | 💯 | 100% |
| `CapsLock M T` | 🥂 | toast |
| `CapsLock M O` |👌 | ok |
| `CapsLock M G` |👋 | greeting |
| `CapsLock M A` |💪 | arm |
| `CapsLock M B` |🤯 | blowing |
## Quick Completions
| Hotkey | Completion |
| ------------ | ---------- |
| `CapsLock Space Space` | — (mdash) |
| `CapsLock Space N` | Your name (as entered on setup) |
| `CapsLock Space E` | Your email (as entered on setup) |
You can add more of your own by editing `~/.XCompose`, then running `omarchy-restart-xcompose` in the terminal to get the changes picked up.
+24
View File
@@ -0,0 +1,24 @@
# Unified Clipboard & History
Usually on Linux, you need `Ctrl + Shift + C/V` to copy'n'paste in the terminal and `Ctrl + C/V` to do it everywhere else. That's hard to get used to for anyone who hasn't been born and bred on Linux! So too is the switch from super to ctrl, if you're coming from the Mac.
Omarchy tackles both problems with unified clipboard hotkeys that work (almost) everywhere. They are:
| Hotkey | Command |
| ------- | ----------- |
| Super + C | Copy |
| Super + X | Cut |
| Super + V | Paste |
| Super + Ctrl + V | Clipboard history |
_Note that most agent harnesses will use `Ctrl + V` for pasting images, but `Super + V` for pasting text._
### Clipboard history
The clipboard history is provided by the Omarchy shell and works for both text and images. You trigger it by `Super + Ctrl + V`, select your entry with return, and then that'll be placed on the clipboard ready to paste on `Super + V`.
![clipboard-history](images/clipboard-history.webp)
You can also search the history just by starting to type:
![clipboard-history-search](images/clipboard-history-search.webp)
+5
View File
@@ -0,0 +1,5 @@
# Reminders
Omarchy has a built-in way to set simple reminders based on a countdown timer and with a message. You can do this via `Super + Ctrl + R`, seeing all the ones set via `Super + Ctrl + Alt + R`, and clearing all via `Super + Ctrl + Shift + R`. Everything also accessible via _Trigger > Reminder_. You can also use the cli with `omarchy reminder 7 'Tea ready'`.
![reminders](images/reminders.webp)
+23
View File
@@ -0,0 +1,23 @@
# Notices
You can quickly access the date and time, battery status, and current weather using the hotkey notices.
### Date & Time
`Super + Ctrl + Alt + T`
![notice-datetime](images/notice-datetime.webp)
### Weather
`Super + Ctrl + Alt + W`
![notice-weather](images/notice-weather.webp)
The location is detected from your IP address, which is usually close enough, but not always. You can pin it down with `omarchy weather location --set Malibu`, or be exact about it by adding coordinates: `omarchy weather location --set Malibu 34.0259,-118.7798`. Run `omarchy weather location` on its own to see where it thinks you are, and `--clear` to go back to auto-detection.
### Battery
`Super + Ctrl + Alt + B`
![notice-battery](images/notice-battery.webp)
+15
View File
@@ -0,0 +1,15 @@
# Text Extraction & Dictation
### Text Extraction
Hit `Super + Ctrl + PrtScr` to select a region on the screen for text extraction. The tesseract open source OCR model will then quickly convert that selection into text and place it on the clipboard. Then you just hit `Super + V` to paste.
This is very helpful for grabbing addresses out of image footers or phone numbers embedded in website headlines.
![text-extraction](images/text-extraction.webp)
### Dictation
Omarchy offers AI dictation via [Voxtype](https://voxtype.io/). You install it via _Install > AI > Dictation_ through the Omarchy menu. By default, it'll load a base English model that takes up 150MB. But you can tweak which model you'd like to use by running `voxtype setup model` in the terminal. And you can tweak all the settings via `~/.config/voxtype/config.toml`.
Once installed, you dictate by holding down `F9` or by toggling with `Super + Ctrl + X`, and the dictated text will appear in the focused input area.
+78
View File
@@ -0,0 +1,78 @@
# Screenshots & Recording
Everything you can grab off the screen hangs off the Print Screen key. One key on its own takes a picture, and the modifiers take a recording, a colour, or the text inside a region. If your keyboard doesn't have a Print Screen key at all, `Super + Ctrl + C` opens the same set as a menu.
| Hotkey | Function |
| ------ | -------- |
| `Print Screen` | Screenshot |
| `Alt + Print Screen` | Screenrecord (or stop the one that's running) |
| `Super + Print Screen` | Colour picker |
| `Super + Ctrl + Print Screen` | Extract text from a region |
| `Super + Ctrl + C` | Capture menu |
| `Super + Ctrl + .` | Transcode a picture or video |
## Screenshots
Hit `Print Screen` and the screen freezes so nothing shifts under you while you aim. Drag a box for a freeform region, or just click once and the shot snaps to whatever rectangle you clicked in — a window if you landed on one, the whole monitor if you landed on the bar or in a gap. Changed your mind? Hit `Print Screen` again to dismiss the picker.
The result goes two places at once: a PNG in your pictures directory, and the clipboard, so you can paste it straight into a chat window with `Super + V`. A notification pops up with a thumbnail. Click it (or hit `Super + Alt + ,` to invoke the last notification) and the shot opens in Tensaku, the annotation editor, where you can draw arrows and boxes on it before you send it.
Files land in `~/Pictures` by default, named `screenshot-2026-08-13_14-22-05.png`. If you'd rather keep them in their own folder, set `OMARCHY_SCREENSHOT_DIR` — see [the FAQ](46-faq.md) for where to put session environment variables. Omarchy creates the directory for you if it isn't there. You can swap the editor too with `OMARCHY_SCREENSHOT_EDITOR`.
From the terminal, `omarchy screenshot` takes the same shot, and you can be explicit about it: `omarchy capture screenshot region` for freeform only, `windows` to snap to window and monitor rectangles, or `fullscreen` to skip the picker entirely and grab the focused monitor. A second argument of `copy` puts the shot only on the clipboard, and `save` only on disk.
### Driving the picker from the keyboard
While the selection is up, you don't have to use the mouse at all:
| Key | Function |
| --- | -------- |
| `Return` | Capture the highlighted window |
| `Ctrl + Return` | Capture the whole screen |
| `Tab` / `Ctrl + Tab` | Highlight the next / previous window |
| Arrow keys | Highlight the window in that direction |
The arrows and Tab move the cursor to the window they pick, so the highlight follows along and you can see what you're about to capture. These bindings only exist while a selection is on screen, so they can't collide with anything in your own config.
## Screen recording
`Alt + Print Screen` opens _Trigger > Capture > Screenrecord_, which asks what you want on the soundtrack: no audio, desktop audio, desktop plus microphone, or desktop plus microphone plus webcam. That last one only shows up if you actually have a camera plugged in. Pick one and you get the same picker as a screenshot: drag a region, or click a window or monitor.
Recording runs on gpu-screen-recorder, which encodes on the GPU at 60fps and falls back to the CPU if it has to. The result is an MP4 in `~/Videos`, named `screenrecording-2026-08-13_14-22-05.mp4`. Set `OMARCHY_SCREENRECORD_DIR` to change that — but note that unlike the screenshot directory, this one has to exist already, or the recording refuses to start.
While you're recording, a little indicator shows up in the bar. Click it to stop. You can also stop with `Alt + Print Screen` again, or with the _Stop Screenrecording_ entry under _Trigger > Capture > Screenrecord_, which only appears while something is actually recording.
Stopping does a bit of tidying before it hands you the file: the first frame gets trimmed, and if there's audio it's normalized to -14 LUFS with the PipeWire capture pop at the very start muted out. Then a notification appears with a thumbnail from the recording. Click it to play the file in mpv.
### The webcam overlay
When you record with a webcam, the camera appears as a pinned, cropped portrait window in the bottom-right corner of whatever you're recording. If it's sitting on top of something you need, resize it on the fly:
| Hotkey | Function |
| ------ | -------- |
| `Super + Alt + [` | Make the webcam overlay smaller |
| `Super + Alt + ]` | Make the webcam overlay larger |
There are three sizes — small, medium, and large — and the hotkeys step between them. Medium is the default. They're proportional to the recording, so the camera takes up the same share of the frame whether you're recording a 1080p monitor or a 6K one. And if you recorded a region rather than a whole display, the overlay anchors to that region's corner rather than the monitor's, so it stays inside the shot.
You can also call it directly with `omarchy-capture-webcam-resize small`, or `reset` to go back to medium.
## Text, QR codes, and colours
`Super + Ctrl + Print Screen` selects a region and OCRs it to the clipboard. That's covered properly in [Text Extraction & Dictation](11-text-extraction-dictation.md).
_Trigger > Capture > QR Code_ does the same trick for QR codes. Select the region with the code in it, and the decoded value goes to the clipboard. It only looks for QR codes — dense screen content has a habit of false-positiving as a barcode otherwise. Worth knowing: the decoded value goes to the clipboard and nowhere else. It isn't printed, it isn't in the notification, and it's marked sensitive so it doesn't stick around in [clipboard history](08-unified-clipboard-history.md). QR codes routinely carry secrets — the `otpauth://` URI behind a 2FA setup code, for one — and you don't want that in a log. Pasting still works fine.
`Super + Print Screen` (or _Trigger > Capture > Color_) turns the cursor into an eyedropper. Click anything on screen and the colour lands on the clipboard. Press the hotkey again to back out without picking.
## Transcoding before you share
A 4K screen recording or a raw HEIC off your phone is often too big to just send. `Super + Ctrl + .` (or _Trigger > Transcode_) fixes that. It offers you a fuzzy file picker over `~/Pictures` and `~/Videos`, then asks for a format and a size.
Pictures go to jpg or png at high, medium, or low, which cap the width at 3160, 2160, and 1080 pixels. Videos go to mp4 or an animated gif at 4k, 1080p, or 720p. The converted file is written next to the original with the resolution in the name — `demo-1080p.mp4` — and the path is copied to the clipboard as a file URI, so you can paste it directly into an app that takes file drops.
It works from the terminal too, if you already know what you want: `omarchy transcode ~/Videos/demo.mov mp4 1080p`. There's also `omarchy transcode ascii`, which turns an image into ASCII art — that one's mostly for [branding](41-branding.md).
## Sending it somewhere
Once you've got the file, `Super + Ctrl + S` opens the Share menu and sends it to another device on your network via LocalSend. See [GUIs](22-guis.md) for that.
+101
View File
@@ -0,0 +1,101 @@
# Toggles, Idle & the Screensaver
A lot of what you change day to day isn't really a setting. It's a mode you flip on for an hour and off again: night light while you're working late, do not disturb while you're presenting, stay awake while you're watching something. Omarchy calls those toggles, and they all work the same way — a hotkey, a menu entry, and a command, all hitting the same switch.
### The toggle menu
`Super + Ctrl + O` opens _Trigger > Toggle_ directly, or you can walk there from the Omarchy menu (`Super + Space`). Everything in that list is a switch you can flip without thinking about where the state lives.
From the terminal, the same switches are `omarchy toggle <thing>`. Run `omarchy toggle` on its own to see the whole group.
| Toggle | Hotkey | Command |
| ------ | ------ | ------- |
| Night light | `Super + Ctrl + N` | `omarchy toggle nightlight` |
| Silence notifications | `Super + Ctrl + ,` | `omarchy toggle notification silencing` |
| Stay awake (no idle lock) | `Super + Ctrl + I` | `omarchy toggle idle` |
| Crash capture | — | `omarchy toggle crash-capture` |
| Screensaver | — | `omarchy toggle screensaver` |
| Menu bar | `Super + Shift + Space` | `omarchy toggle bar` |
| Touchpad | `XF86TouchpadToggle` | `omarchy toggle touchpad` |
| Touchscreen | — | `omarchy toggle touchscreen` |
| Suspend | — | `omarchy toggle suspend` |
| Hybrid GPU | — | `omarchy toggle hybrid gpu` |
The touchpad, touchscreen, and hybrid GPU switches live under _Trigger > Hardware_ (`Super + Ctrl + H`) rather than under Toggle, since they only show up when you actually have that hardware. The touchpad and touchscreen ones survive a Hyprland reload — the disabled state is written back out as a small Lua file that Hyprland sources on startup.
The Toggle menu also carries a few things that aren't `omarchy toggle` commands but behave the same: battery percentage in the bar, workspace layout (`Super + L`), window gaps (`Super + Shift + Backspace`), and the 1-window square aspect (`Super + Ctrl + Backspace`).
Most of these are just a flag file under `~/.local/state/omarchy/toggles/`. If you want to branch on one in a script, `omarchy-toggle-enabled` gives you an exit code instead of making you go looking:
```bash
omarchy-toggle-enabled screensaver-off && echo "screensaver is off"
```
The flags are named for the off state — `screensaver-off`, `suspend-off`, `bar-off` — so their presence means the feature is disabled.
### Indicators in the bar
When a mode is on, you get a small glyph in the middle of the top bar next to the clock. That's the indicators widget, and it carries dictation, screen recording, pending reminders, night light, do not disturb, and stay awake.
Inactive indicators are hidden. Hover the area around them and they fade in dimmed, so you can click one to turn it on without knowing its hotkey. Clicking an active one turns it back off. If you'd rather see all of them all the time, set `alwaysShow` to `true` on the `omarchy.indicators` entry in `~/.config/omarchy/shell.json` — see [the top bar](05-the-top-bar.md) for how bar widgets are configured.
### Night light
`Super + Ctrl + N` warms the screen to 4000K, and hitting it again puts it back to 6500K. It's driven by hyprsunset, which the toggle starts for you if it isn't already running.
By default hyprsunset does nothing to your screen at all. `~/.config/hypr/hyprsunset.conf` ships with an identity profile precisely so the display stays untouched until you ask for warmth. If you'd rather have it switch by the clock, replace that with a time profile:
```
profile {
time = 20:00
temperature = 4000
}
```
Then start hyprsunset at login by adding `o.launch_on_start("hyprsunset")` to `~/.config/hypr/autostart.lua`. The 4000K/6500K pair used by the toggle is fixed, so the config file is where you go if you want a different temperature.
### Do not disturb
`Super + Ctrl + ,` silences notifications. No toasts pop up while it's on, and the crossed-out bell indicator sits in the bar to remind you why the desktop has gone quiet.
Nothing is lost, though. A silenced notification is written straight into your notification history, which is exactly the record you want when you come back and wonder what you missed. Open it with `Super + Shift + Alt + ,`. See [notices](10-notices.md) for the rest of the notification story.
Two kinds of message still get through: Omarchy's own confirmation toasts for something you just did ("Theme changed", "Screenshot saved"), and critical alerts sent from the command line. Chat apps that mark everything critical to force their way in front of you don't qualify.
### Idle
The Omarchy shell owns idle behavior, and the timings are a top-level `idle` block in `~/.config/omarchy/shell.json`:
```json
{
"version": 1,
"idle": {
"screensaver": 150,
"lock": 300
}
}
```
Both numbers are seconds counted from the moment you went idle — not from each other. So with the defaults, the screensaver comes up after two and a half minutes and the lock screen takes over at five minutes, whether or not the screensaver ran. Save the file and the shell picks up the new timings right away.
If you dismiss the screensaver before the lock deadline, that counts as activity and the pending lock is cancelled. You don't get locked out for glancing at your machine.
To stop locking on idle entirely, `Super + Ctrl + I` — or `omarchy toggle idle` — flips stay awake on, and the coffee cup indicator appears in the bar. That's the one to hit before a long presentation or a build you want to watch. Hit it again to go back to normal. `omarchy toggle idle status` prints the current state as JSON if you need it from a script.
This is about locking and the screensaver, not power. Suspend and hibernation have their own setup in [system sleep](36-system-sleep.md).
### The screensaver
Omarchy's screensaver is ASCII art running through random text effects, one instance per monitor. Any key or mouse movement exits it.
You can start it on demand from _System > Screensaver_ (`Super + Esc`), which forces it up even if you've turned the idle screensaver off. There's no hotkey bound to it by default.
`omarchy toggle screensaver` is what turns the idle one off, if you'd rather go straight from working to locked. It needs a terminal it knows how to configure — Alacritty, Foot, Ghostty, or Kitty — and will tell you so if your default terminal is something else.
The logo it draws is yours to change, under _Style > Screensaver_. Upload a png or svg and Omarchy converts it to ASCII. See [branding](41-branding.md).
### The lock screen
`Super + Ctrl + L` locks the machine. That runs the lock screen from the Omarchy shell, blanks the display, resets your keyboard layout to the first one so you're not typing your password in the wrong alphabet, and — if you have it running — locks 1Password on the way out.
The lock screen takes a password, and it'll take a fingerprint too once you've set one up. That, and the other ways to authenticate, are covered in [hardware authentication](37-hardware-authentication.md).
+62
View File
@@ -0,0 +1,62 @@
# Omarchy CLI
Omarchy is usually controlled through the hotkeys and the Omarchy menu (`Super + Space`). But you can also control it through the `omarchy` CLI. This is particularly helpful when you're having an AI agent work with you on customization or configuration.
The CLI has access to all the internal tooling that is used both via the menu and otherwise. You can see everything that's available by running `omarchy` in the terminal.
It looks something like this:
```
~ ❯ omarchy
Omarchy command center
Usage:
omarchy <command> [args...]
omarchy commands [--all] [--json] [--check]
omarchy <group> --help
omarchy <group> <command> --help
Common commands:
omarchy update Update Omarchy and system packages
omarchy theme list List available themes
omarchy theme set <name> Apply a theme
omarchy font list List available fonts
omarchy screenshot Take a screenshot
omarchy debug Print debugging information
Groups:
agent AI coding agent usage data
audio Audio input and output controls
bar Omarchy shell bar layout and settings
battery Battery status helpers
bluetooth Bluetooth device controls
branch Omarchy git branch management
branding About and screensaver branding
brightness Display and keyboard brightness
capture Screenshots and screen recording
channel Omarchy release channel management
clipboard Clipboard helpers
cmd Command and shortcut helpers
config System configuration helpers
debug Diagnostics and support logs
...
```
And you can dive deeper on every group:
```
~ ❯ omarchy capture
Capture commands — Screenshots and screen recording:
omarchy capture qr Decode a QR code from a screenshot region
omarchy capture screenrecording [--fullscreen] [--with-desktop-audio] [--with-microphone-audio] [--with-webcam] [--webcam-device=<device>] [--webcam-size=<small|medium|large>] [--resolution=<size>] [--stop-recording] Start or stop screen recording
omarchy capture screenrecording with webcam Pick a webcam and start a screen recording with it
omarchy capture screenshot [smart|region|windows|fullscreen] [slurp|copy|save] [--editor=<name>] Take a screenshot
omarchy capture text Extract text from a screenshot region with OCR
omarchy capture webcam resize <smaller|larger|reset|small|medium|large> Resize the active webcam recording overlay
```
Every command takes `--help` too, whether you ask a whole group (`omarchy capture --help`) or a single command (`omarchy capture screenshot --help`).
### Opening the menu from the terminal
The Omarchy menu is scriptable as well, which is handy for your own keybindings. `omarchy menu` opens it at the root, and you can jump straight to any point in the tree by naming it: `omarchy menu summon style.theme` goes right to the theme picker, `omarchy menu toggle system` opens the system menu and closes it again if it's already up, and `omarchy menu close` puts it away.
+39
View File
@@ -0,0 +1,39 @@
# Terminal
[Foot](https://codeberg.org/dnkl/foot) is the default terminal for Omarchy. It's fast, lightweight, and compatible with even old computers. It does not, however, support native tabs or splits.
If you use Tmux, you may not mind, but if not, we fully support _Alacritty_, _Ghostty_, and _Kitty_ as options as well. Pick your preference under _Install > Terminal_ in the Omarchy menu.
You start a new terminal using `Super + Return`. (This binding will automatically point to whichever Terminal you've installed via _Install > Terminal_, and you can switch between installed terminals under _Setup > Defaults > Terminal_.)
## Tmux
Tmux provides a consistent, programmable interface for panes, windows (aka tabs), and resumable sessions regardless of your terminal. It even works on remote hosts, so when you're SSH'ing into a server, you can use the same approach.
You start a new Tmux session in a fresh terminal using `Super + Alt + Return`, and because Tmux is a persistent process, you can resume your session even if you close that terminal. Just hit `Ctrl + Space` (called the prefix key) then `s` to see all your active sessions.
Omarchy ships with an ergonomically-optimized Tmux configuration, which has a lot of keybindings to learn, so keep [the cheatsheet handy](07-hotkeys.md#tmux).
## Tmux layout functions
Because Tmux is programmable, we can use functions to create layouts. Omarchy ships with four different functions for common developer layouts.
`tdl [agent]` starts a three-way split IDE-like interface with the `$EDITOR` on the left, your chosen AI agent on the right (like `c` for opencode or `cx` for Claude or `codex` for OpenAI), and then a terminal at the bottom.
So `tdl c` would start this (or just `ic`):
![tmux-tdl](images/tmux-tdl.webp)
You can also start a second agent with `tdl c cx` (opencode + claude) (or just `icx`):
![tmux-tdl2](images/tmux-tdl2.webp)
There's also `tds`, which starts a four-way square with the editor top left, a live diff watcher top right, a terminal bottom left, and opencode bottom right.
You can also start this layout configuration for every subdirectory in the current directory using `tdlm [agent]`, then navigate using `alt + 1/2/3/4/5/...`:
![tmux-tdlm](images/tmux-tdlm.webp)
Finally, you can start a swarm of agents using `tsl [panes] [command]`. So `tsl 4 c` will give you a four-way grid of opencode agents:
![tmux-tsl](images/tmux-tsl.webp)
+42
View File
@@ -0,0 +1,42 @@
# Neovim
[Neovim](https://neovim.io/) is a modern implementation of [the vi editor](<https://en.wikipedia.org/wiki/Vi_(text_editor)>) created by Bill Joy all the way back in 1976. It's a modal editor where insert mode and command mode are separated, and it's a bit of a superpower once you learn even just a subset of the incredibly deep key command set. But it's also quite the learning curve!
If you're totally new to vim-style editing, I recommend you checkout [ThePrimeagen's Vim As Your Editor series](https://www.youtube.com/watch?v=X6AR2RMB5tE&list=PLm323Lc7iSW_wuxqmKx_xxNtJC_hJbQ7R) on YouTube. That'll teach you the basics. Just know that unlike more similar mainstream editors, it's going to take you longer to get basic proficiency with vim. But once you do, the payoff is also larger.
Now Neovim is basically infinitely configurable. If you really want to go nuts, you can create your own Neovim configuration from scratch. There's a great course from [Typecraft on setting up Neovim from scratch](https://www.youtube.com/watch?v=zHTeCSVAFNY). And [ThePrimeagen has one as well](https://www.youtube.com/watch?v=w7i4amO_zaE).
But Omarchy ships with a complete Neovim setup — the `omarchy-nvim` package — that's been lovingly tuned to showcase the best of what's possible out of the box. Without you having to write a single line of configuration! It's built on [LazyVim](https://www.lazyvim.org/), a distribution of Neovim plugins and configurations. It's awesome.
## LazyVim Basics
As mentioned, I'm not going to teach you vim in this short introduction, but I can show you a few basics of LazyVim, and how to get around.
First, Neovim has the idea of the leader key. That's basically the gateway to all the commands. LazyVim has set that to `Space`. So just press that, wait a second, and you'll see a bunch of options explained inline like this:
Here are some basic commands I use all the time:
- `Space Space` - Fuzzy-find any file in the current directory.
- `Space S G` - Search all files using grep with a preview.
- `Space E` - Toggle the file tree on/off.
- `Ctrl + W W` - Hop from the file tree to the editor and back.
- `Shift + H` - Move left between the open tabs (vim calls them buffers).
- `Shift + L` - Move right between the open tabs.
- `Space B D` - Close a tab.
- `Space B O` - Close all other tabs but the current.
- `Space G G` - Launch LazyGit in a floating pane from the current directory.
- `Space U W` - Toggle soft wrap.
While you're in the file tree (`Space E` to reveal, `Ctrl + W W` to hop over there), you can add a new file with `a` or a new directory with `A`. Press `?` while in the tree to see all commands.
If you want to pickup the basic vim language, I've written about [the three-part syntax](https://world.hey.com/dhh/wonderful-vi-a1d034d3) and how to pull off those sick combo moves!
You can see all the possible commands on [the LazyVim Keymaps page](https://www.lazyvim.org/keymaps).
## Starting Neovim
You can start Neovim using `Super + Shift + N` (the binding launches your default editor, which is Neovim out of the box), but it's usually easier to drive it from the terminal by navigating to the directory you wish to work in and typing `n`. The `n` is the alias for `nvim`, which will use the the present directory to open by default. You can open a single file with `n myfile.txt`.
## Using Neovim for sudo edits
If you need to edit files that you can only change as a super user, you can use neovim with all your plugins setup by running `sudoedit /etc/sudoers.d/00-sudo-only-file`.
+51
View File
@@ -0,0 +1,51 @@
# AI
Omarchy treats AI coding agents as first-class citizens, but it doesn't pick a favorite for you. Instead, every major coding-agent CLI comes pre-wired as a lazy-loaded launcher. The launchers are tiny [mise](https://mise.jdx.dev/)-managed stubs in `~/.local/bin/`, so nothing is downloaded until the first time you actually run one. Invoke any of these and authenticate when prompted:
| Command | Agent |
|------------|------------------------------------------------------------------|
| `claude` | [Claude Code](https://code.claude.com/docs/en/overview) |
| `codex` | [OpenAI Codex](https://github.com/openai/codex) |
| `opencode` | [OpenCode](https://opencode.ai/) |
| `gemini` | [Google Gemini CLI](https://github.com/google-gemini/gemini-cli) |
| `copilot` | [GitHub Copilot CLI](https://github.com/github/copilot-cli) |
| `crush` | [Crush](https://github.com/charmbracelet/crush) |
| `grok` | Grok CLI from xAI |
| `pi` | [Mario Zechner's Pi](https://github.com/badlogic/pi-mono) |
| `omp` | [Oh My Pi](https://github.com/can1357/oh-my-pi) |
To wrap an additional CLI the same way, run `omarchy-mise-install <package> [command-name]`. The stubs are kept current along with everything else mise manages when you run `omarchy update` (or the `mup` alias).
### The default agent
Pick your default agent with `omarchy default agent <name>` or under _Setup > Defaults > Agent_ in the Omarchy Menu (`Super + Space`). If the agent isn't installed yet, picking it installs it first. A fresh Omarchy will invite you to make this choice with a one-time notification.
Once you've chosen, `Super + Shift + Ctrl + A` launches the default agent in a dedicated terminal window (or brings up the picker if you haven't chosen yet). You can also launch it straight into a task with `omarchy agent prompt "Review this project"`. Agents launched this way run unattended in their respective don't-stop-to-ask modes, so be ready for them to actually do things! And since agents refuse to remember trust for your home directory, launches from `$HOME` start in `~/Work` instead.
There are terminal shortcuts too: `a` runs the default agent inline in the current terminal, while `c`, `cx`, and `cy` start OpenCode, Claude Code, and Codex directly (again in their auto-approving modes). Theme changes sync to the agents as well: Claude Code, Pi, and OpenCode all follow along when you switch the Omarchy theme.
### The agents panel
The top bar grows an agents icon the first time Omarchy finds AI coding usage on the machine (and stays out of the way until then). The panel behind it tracks every subscription in one place: your plan, the percentage used of the 5-hour session and weekly limits (or the remaining prepaid balance), and token usage by day and by model. Claude Code, Codex, and Fireworks are covered out of the box.
Left-click the bar icon for the panel, right-click to launch your default agent. The usage records behind it are regenerated every 15 minutes by `omarchy agent usage-update`, and the panel can even merge usage from your other machines via a synced folder. See the README under `$OMARCHY_PATH/shell/plugins/agents/` for the full settings.
### Crash diagnosis
Omarchy watches systemd-coredump for process crashes. When something segfaults, you'll get a "Process crashed" notification — click it, and the crash is handed to your default agent along with Omarchy's diagnose-crash skill, which walks the agent through establishing the facts from the core dump and deciding whether the crash is worth reporting upstream. You can also run it by hand against any PID from `coredumpctl list` with `omarchy agent crash <pid>`.
The watching is on by default. Turn it off under _Trigger > Toggle > Crash Capture_ (or with `omarchy toggle crash-capture`) and the notifications stop; `omarchy agent crash <pid>` still works by hand.
### Desktop apps
The _Install > AI_ menu also carries a couple of graphical AI apps: the ChatGPT desktop app, and Grok Bot for chatting with xAI's models.
### Local LLMs
Omarchy recommends two ways of running local LLM models: LM Studio and Ollama. LM Studio provides a GUI interface for finding open-weight models, installing them, and running them. It's a great way to get going easily. Ollama offers a CLI for doing so similarly. But if you're new to local models, I'd start with LM Studio. You can install either under _Install > AI_ in the Omarchy Menu.
### The Omarchy Skill
Agent skills help AI use specific tools in a specific way, and Omarchy ships with a default skill for tailoring the system. Like tweaking your Hyprland config, adjusting the bar, or even creating a new theme from scratch. It's symlinked into the skill directories for Claude Code (`~/.claude/skills`), Codex (`~/.codex/skills`), Pi (`~/.pi/agent/skills`), and the generic `~/.agents/skills` location, so most harnesses pick it up automatically.
But you should treat this skill as experimental. Different models will use it to different effect. It's best to run in plan mode first, so you have an idea of what the agent would like to change. And then be ready to rollback changes or even invoking `omarchy reinstall configs`, if the agent makes a mess of everything.
+33
View File
@@ -0,0 +1,33 @@
# Development Tools
## Alternative Editors
Omarchy ships with [Neovim](https://neovim.io/) by default, but if you'd like something a bit more mainstream and familiar, you can run the Omarchy Menu (`Super + Space`) and see the options under _Install > Editor_. We have VSCode, Cursor, Zed, Sublime Text, Helix, Vim, and Emacs listed there. If you don't find what you're looking for, checkout _Install > Package_, and see if it isn't in an Arch package (and if not, try _Install > AUR_ to check the AUR).
Theme matching is offered for `VSCode`, `Cursor`, `VSCodium`, and `Helix`.
You can set the system-wide default editor under `Setup > Defaults > Editor`.
## Environment
Omarchy supports setting up a whole host of development environments through the _Install > Development_ section of the Omarchy Menu (`Super + Space`). You'll of course find _Ruby on Rails_, but also all three major runtimes for JavaScript (Node.js, Bun, Deno), as well as popular PHP frameworks like Laravel and Symfony. Oh, and there's Go, Rust, Python, Java, Elixir (with Phoenix), .NET, OCaml, Zig, Clojure, and Scala too. It's a very broad selection!
The majority of these environments are managed by [Mise](https://mise.jdx.dev/). It's a tool that lets you install and run multiple versions of a programming language on the same machine. It's like rbenv or rvm for Ruby or virtualenv for Python, but it works for a bunch of different environments.
To install, say, Ruby, you'd run `mise use -g ruby`, which will both install Ruby and set it as the global default. Or, if your project has a .ruby-version file, you can just run `mise i` in the root of that project.
## Docker
[Docker](https://www.docker.com/) hardly needs any introduction. It allows you to run isolated containers, and Omarchy installs everything needed to run it well. This includes Docker itself, [Docker Compose](https://docs.docker.com/compose/), and the user group changes needed for you to run Docker as the normal user and not as root.
Remember to checkout the Lazydocker command to manage your containers in a cool TUI using `Super + Shift + D`.
You can setup the common databases for local development in Docker using _Install > Development > Docker DB_ in the Omarchy menu.
## GitHub CLI
[The GitHub CLI](https://cli.github.com/) let's you authenticate with your GitHub account and clone private repositories using it. It's wired up as one of the lazy-loading mise stubs, so the first time you run `gh`, it installs itself. To authenticate, run `gh auth login`. Then you can checkout private repositories using `gh repo clone org/repo`.
You can also perform a bunch of other GitHub operations using this command. Just run `gh` to see everything that's possible.
There's a lazy-installing stub for `ghui` for managing your pull requests in a TUI too. And [lazygit](https://github.com/jesseduffield/lazygit) is preinstalled, if you'd like to drive git itself from a TUI as well.
+59
View File
@@ -0,0 +1,59 @@
# Shell Tools
In addition to the standard Linux tools, Omarchy also ships with a bunch of enhanced shell tools. Here are the key ones.
## fzf
[fzf](https://junegunn.github.io/fzf/) gives you fuzzy finding of files via the `ff` alias. Go to any directory, type `ff`, and you'll be able to fuzzy find your way to any file in that tree, while seeing a preview of the files you're narrowing down on the right-hand side.
You can use `Ctrl + R` to use fzf to fuzzy find through your command history.
This tool is also used by Neovim when you type `Space Space`.
The full manual can be found via `man fzf`.
## Zoxide
[Zoxide](https://github.com/ajeetdsouza/zoxide) is a replacement for cd. It remembers the directories you've been in, so you can more easily jump to them next time. Say you do `cd ~/.config/omarchy` once. Next time, you can just do `cd omarchy` (or even just `cd oma`), and Zoxide will take you directly there.
The full manual can be found via `man zoxide`.
## ripgrep
[ripgrep](https://github.com/BurntSushi/ripgrep) searches the contents of files by using `rg <pattern> <path>`, like `rg Controller app/` to find all mentions of `Controller` in the directory app.
This tool is also used by Neovim when you type `Space S G`.
The full manual can be found via `man rg`.
## eza
[eza](https://eza.rocks/) is a replacement for ls. It gives you directory listings with more information, color, and icons. By default, eza has been aliased as ls. You can also use `lt` to get a listing of two-deep levels of nesting. `lsa` gives you a listing including hidden files. And `lta` a nested listing with hidden files.
The full manual can be found via `man eza`.
## fd
[fd](https://github.com/sharkdp/fd) is an easier to use replacement for `find`. Use `fd person.rb` to find a file called `person.rb` within the current tree. `fd person.rb /` will search the entire file system. `fd person.rb / -H` searches the entire file system, including hidden directories.
The full manual can be found via `man fd`.
## bat
[bat](https://github.com/sharkdp/bat) is `cat` with syntax highlighting, line numbers, and paging. Run `bat somefile.rb` and you'll see why it's hard to go back. It's also quietly doing work for you elsewhere: it's what colors your man pages, and what renders the previews in `ff`.
The full manual can be found via `man bat`.
## tldr
[tldr](https://tldr.sh/) is the antidote to man pages that open with three screens of history before showing you a single example. `tldr tar` gives you the handful of invocations you actually wanted.
## yt-dlp
[yt-dlp](https://github.com/yt-dlp/yt-dlp) downloads video from YouTube and hundreds of other sites. `yt-dlp <url>` grabs the best quality available into the current directory.
The full manual can be found via `man yt-dlp`.
## try
[try](https://github.com/tobi/try) makes it easy to manage programming experiments with date-stamped directories. All experiments live in `~/Work/tries` and you can access them via `try`.
+49
View File
@@ -0,0 +1,49 @@
# Shell Functions
Omarchy comes with a set of shell functions to simplify common tasks and encapsulate convoluted parameter calls.
## Compression
- `compress [file/dir]`: Create a tar.gz archive from the file/dir.
- `decompress [file.tar.gz]`: Expand a tar.gz file.
## Drives
- `iso2sd [image.iso]`: Create a bootable drive on an SD card using the referenced iso file and picking the drive interactively.
- `format-drive [device] [name]`: Format an entire disk with a single exFAT partition (which works on Windows and macOS too). Run it without arguments to see the available drives. Be careful!
## Dev layouts
Instant multi-pane development layouts for tmux:
- `tdl [ai]`: Create a Tmux Dev Layout with your editor, an AI agent, and a terminal. Use the agent aliases, like `tdl c` for opencode or `tdl cx` for Claude Code, or pass a second agent to run both, like `tdl c cx`.
- `tds`: Create a Tmux Dev Square with editor, diff watching (via `hunk diff --watch`), terminal, and opencode.
- `tdlm [ai]`: Create a `tdl` window for every subdirectory in the current directory.
- `tsl [count] [command]`: Create a swarm of panes tiled in a grid, all running the same command (great for AI agents).
The same layouts are available for Herdr as `hdl`, `hds`, `hdlm`, and `hsl`.
## Git worktrees
- `ga [branch]`: Create a new worktree and branch next to the current repository and jump into it.
- `gd`: Remove the current worktree and its branch (asks for confirmation first).
## Rsync watchers
- `rsw [source] [destination]`: Start a background watcher that rsyncs source to destination whenever anything changes. The destination can be a remote host, like `rsw ~/Work/app nyc-dev:Work/app`.
- `lsw`: List all active watchers.
- `dsw`: Stop all active watchers.
## SSH Portforwarding
Ideal for doing web development with localhost secure-context privileges against a remote box.
- `fip`: Forward one or more ports from a remote host to localhost via SSH.
- `dip`: Disconnect one or more forwarded ports.
- `lip`: List all active SSH port forwards.
Say you start a dev server on port `3000` on a machine accessible as `nyc-dev`, then you can run `fip nyc-dev 3000` to forward that port, so `localhost:3000` actually reaches `nyc-dev:3000`, but without the need for SSL certificates to establish the secure context needed for testing web sockets or the like.
## SSH reconnection
`ssh` itself is wrapped in a function that cleans up the terminal if a connection dies while a remote tmux, Herdr, or editor has claimed it, and then automatically reconnects when an interactive session drops (Ctrl-C stops the retry loop).
+51
View File
@@ -0,0 +1,51 @@
# TUIs
## Lazygit
[Lazygit](https://github.com/jesseduffield/lazygit) is a delightful alternative to something like the GitHub Desktop application, and it runs inside the terminal.
You can run it directly, by going to any directory managed by git and running `lazygit`. Or you can run it inside Neovim where it can be started with `Space G G`.
You hop between the different panes using `Tab`. In the Files pane, you select files for staging using `Space`, and then you can create a new commit using `c`. You can see all the commands available using `?`.
## Lazydocker
[Lazydocker](https://github.com/jesseduffield/lazydocker) is made in the same spirit like Lazygit, and also gives you a terminal interface for managing your containers and images.
You can start it with `Super + Shift + D`.
You stop a container using `s` or start/restart it using `r`. See all commands using `?`.
## Btop
[Btop](https://github.com/aristocratos/btop) is a beautiful resource manager that shows memory, CPU, disk, and network usage. It also lists all active processes, and allows you to manage them.
Omarchy calls it Activity, and you start it by hitting `Super + Ctrl + T`. It opens as a floating window, which you can tile with `Super + T`.
## Herdr
[Herdr](https://github.com/omacom-io/herdr) is a terminal workspace manager that gives you workspaces, tabs, and panes, and keeps them all running in a persistent session you can detach from and come back to later.
You start it (or reattach to your existing session) with `Super + Ctrl + Return`. Omarchy ships a Herdr configuration that mirrors its Tmux config, so the prefix key is `Ctrl + Space` here too. You can browse all the keybindings with `Super + Ctrl + K`.
## Fastfetch
[Fastfetch](https://github.com/fastfetch-cli/fastfetch) shows system information, like kernel version, uptime, theme, CPU, memory, and more. It's a successor to the popular neofetch tool.
Omarchy has packaged this as _About_ in the Omarchy menu (`Super + Space`).
## Disk Usage
When the drive fills up and you have no idea what's eating it, launch _Disk Usage_ from the app launcher (`Super + Space`). It's [dua](https://github.com/Byron/dua-cli) in interactive mode pointed at the whole file system, so you can walk down into whatever directory is the culprit, sorted biggest first, and delete from right inside it.
## Cliamp
[Cliamp](https://www.cliamp.stream/) is a retro terminal music player inspired by Winamp 2.x, complete with built-in radio stations for lo-fi beats. Launch it with `Super + Shift + Alt + M`, or from the Omarchy menu under _Apps_. Press `?` for the full keybinding list.
## What about Wi-Fi and Bluetooth?
You won't find TUIs for Wi-Fi and Bluetooth — those jobs belong to the Omarchy shell. Click the Wi-Fi icon in the top bar (or hit `Super + Ctrl + W`) to see networks and connect, and click the Bluetooth icon (or hit `Super + Ctrl + B`) to pair and connect devices. See [networking](35-networking.md) for the full story.
## Adding your own
Any terminal program can get the full app treatment. Go to _Install > TUI_ in the Omarchy menu (`Super + Space`), give it a name, a launch command, a window style, and an icon, and it'll show up in the app launcher like any other application. You can remove it again under _Remove > TUI_.
+96
View File
@@ -0,0 +1,96 @@
# GUIs
## Files
Files (Nautilus) is the graphical file manager. `Super + Shift + F` opens it, and `Super + Shift + Alt + F` opens it in the directory your terminal is sitting in, which saves a lot of clicking. `Ctrl + L` lets you type a path, and hitting `Space` on any file gives you a quick preview without opening anything.
Plug in a USB stick or an SD card and it's mounted automatically, so it just shows up in the sidebar. For anything more involved — formatting a drive, checking SMART health, creating partitions — launch _Disks_ from the app launcher (`Super + Space`).
Double-clicking follows sensible defaults: images open in imv, video in mpv, PDFs in Document Viewer, and plain text in Neovim.
## Obsidian
[Obsidian](https://obsidian.md/) is a free and highly extensible note taking application that uses simple Markdown files for storage.
Obsidian is free for all purposes, including personal, commercial, and non-profit use.
Obsidian also offers a [commercial add-on for syncing](https://obsidian.md/sync) with mobile apps on iOS and Android. (https://obsidian.md/pricing).
You start Obsidian with `Super + Shift + O`. To use theme syncing, you must select the `Omarchy` theme under settings.
## Omawrite
[Omawrite](https://github.com/omacom-io/omawrite) is Omarchy's own dead-simple Markdown writing app. No vaults, no plugins, just you and the words.
You start Omawrite with `Super + Shift + W`.
## Pinta
[Pinta](https://www.pinta-project.com/) is a basic image editing tool that's great for cropping, resizing, and other basic manipulations. Just don't expect a Photoshop alternative. But it's still got a Magic Wand and layers!
You start Pinta via the application launcher (`Super + Space`).
## Aether
[Aether](https://github.com/bjarneo/aether) is a theming application that can extract colors from a background image and turn them into a complete, cohesive theme. It's the easiest way to [make your own theme](43-making-your-own-theme.md).
You start Aether via the application launcher (`Super + Space`).
## LocalSend
[LocalSend](https://localsend.org/) lets you send files to other devices on the same network running the app, like Apple's AirDrop. It's cross-platform, though, so you can send files to and from Windows, macOS, Android, iOS, and of course Linux.
You can open the Share menu on `Super + Ctrl + S` or under _Trigger > Share_ in the Omarchy menu. It gives you four options:
- **Clipboard** sends whatever you've copied as a text file. Great for getting a link or a snippet onto your phone without emailing yourself.
- **File** opens a file picker where you can select several at once.
- **Folder** sends an entire directory.
- **Receive** opens LocalSend proper so another device can send something to you.
The same thing works from the terminal with `omarchy share clipboard`, `omarchy share file [path]`, and `omarchy share folder [path]`. Leave the path off and you get the picker.
You can also send straight from the file manager: right-click any selection in Nautilus and pick _Send via LocalSend_.
Omarchy's firewall is closed by default except for LocalSend's port, so this works out of the box on a fresh install. See [security](48-security.md).
## LibreOffice
[LibreOffice](https://www.libreoffice.org/) is a complete office package with word processor, spreadsheet, presentations, drawing application, and more. It's compatible with files from Microsoft Office, so this is a great way to be able to open those Word documents.
You start LibreOffice via the application launcher (`Super + Space`).
## Omacalc
[Omacalc](https://github.com/omacom-io/omacalc) is Omarchy's own dead-simple calculator, which opens in a floating window.
You start Omacalc with `Super + Ctrl + Q` (or the calculator key, if your keyboard has one).
## Signal
[Signal](https://signal.org/) is the pioneer of E2E encrypted messaging, and a great communication option for anyone who'd prefer not to go through one of the big tech conglomerates.
You start Signal with `Super + Shift + G`. It's not part of the base install, so the first time you hit that, Omarchy will offer to install it for you (it's also under _Install > Service_ in the Omarchy menu).
## mpv
[mpv](https://mpv.io/) is a simple, fast media player that'll play almost anything from any source. Great for watching videos.
You start mpv via the application launcher (`Super + Space`) or just double-click on a video in the file manager.
## OBS Studio
[OBS Studio](https://obsproject.com/) lets you record or stream video from multiple inputs. You can mix a screencast with a webcam with a microphone input. It's what was used to record the Omarchy screencasts.
You start OBS Studio via the application launcher (`Super + Space`).
## Kdenlive
[Kdenlive](https://kdenlive.org/) is an excellent video editor. Perfect for working on video that comes out of OBS Studio before sharing it.
You start Kdenlive via the application launcher (`Super + Space`).
## Omacut
[Omacut](https://github.com/omacom-io/omacut) is Omarchy's own dead-simple video trimmer. When all you need is to cut the start and end off a clip, it beats firing up a full video editor.
You start Omacut via the application launcher (`Super + Space`).
+39
View File
@@ -0,0 +1,39 @@
# Browsers
Omarchy ships with [Chromium](https://www.chromium.org/) as the default browser. It's the plain open-source build, themed to match the rest of your system, and it's what `Super + Shift + Return` opens and what every [web app](25-web-apps.md) runs inside.
If Chromium isn't your taste, you're not stuck with it. Under _Install > Browser_ in the Omarchy menu you'll find Chrome, Edge, Brave, Brave Origin, Firefox, and [Zen](https://zen-browser.app/). Pick one and Omarchy installs it, sets up its policy directory, and applies your current theme to it.
## Making one the default
Installing a browser doesn't promote it. Once it's on the machine, go to _Setup > Defaults > Browser_ and pick it — the menu only lists browsers you actually have installed, and marks the current default with a check.
From the terminal it's:
```bash
omarchy default browser firefox
```
Run it with no argument and it tells you the current default. This sets the XDG handler, so it's not just Omarchy's hotkeys that follow along — anything that opens a link, from a chat app to a terminal command, goes to the browser you picked.
## Copy URL and Download Video
The Chromium-family browsers (Chromium itself, Chrome, Edge, and Brave) come with two Omarchy extensions that reach out of the browser and into the rest of your system.
**Copy URL** puts the current tab's address on your clipboard with `Alt + Shift + L`. That's faster than clicking into the address bar and copying, and because it goes through the system clipboard rather than the browser's, you get an Omarchy notification confirming it and the URL is immediately available in [clipboard history](08-unified-clipboard-history.md) and every other app. There's a toolbar button too, if you prefer clicking.
**Download Video** grabs the video playing on the page you're looking at with `Alt + Shift + D`. It hands the URL to [yt-dlp](https://github.com/yt-dlp/yt-dlp), so it works on far more than YouTube, and the download lands in `~/Videos`. Progress shows up in the same on-screen display that volume and brightness use, updating in place rather than stacking up notifications. Set `OMARCHY_YTDLP_DIR` if you'd rather the files went somewhere else — see the [FAQ](46-faq.md) for where to put environment variables.
Both extensions talk to Omarchy through a small native messaging host, which gets installed for you along with the browser. That's the piece that lets a web page's video end up in your home directory and a URL end up in your clipboard manager, which a normal extension can't do on its own.
These are Chromium-family only. Firefox and Zen don't get them.
## Firefox and Zen
Firefox and Zen are a different family, so they get different treatment: Omarchy installs a policies file for sensible defaults and switches them into native Wayland mode, which you want for fractional scaling and smooth trackpad scrolling.
They don't get the Chromium extensions above, and they're not themed by Omarchy, so those parts of the experience are yours to set up.
## Removing one again
Anything you installed here can be taken back off under _Remove > Browser_. Chromium isn't in that list — it's part of the base system.
+37
View File
@@ -0,0 +1,37 @@
# Commercial apps/services
Omarchy is mostly focused on providing free, open source software, but it's not religious about it. Sometimes the best solution is a commercial offering, and that's just fine. Here are some of the options we provide an easy installation for.
## 1Password
Keeping your passwords in a password manager is a best practice. Doubly so if you're working with a team. And [1password](https://1password.com/) is a great solution, which also comes with a command line tool for integrating key lookups in scripts.
You start 1Password with `Super + Shift + /`. If it isn't installed yet, that hotkey kicks off the installation first (you can also use _Install > Service > 1Password_ from the Omarchy menu). The installer sets up the 1Password extension for Chromium as well.
## Bitwarden
[Bitwarden](https://bitwarden.com/) is the open source alternative in the password manager space, with a free tier that covers most personal use. Install it with _Install > Service > Bitwarden_ from the Omarchy menu, which brings along the Bitwarden command line tool as well.
## Spotify
[Spotify](https://spotify.com/) is the world's most popular streaming music service. And the Linux application provides everything you'd expect, including offline playing.
You start Spotify using `Super + Shift + M`. Like 1Password, the hotkey kicks off the installation first if Spotify isn't installed yet (or use _Install > Service > Spotify_ from the Omarchy menu).
## Dropbox
[Dropbox](https://www.dropbox.com/) is a great way to sync files between machines while keeping a backup in the cloud. To set it up, select _Install > Service > Dropbox_ from the Omarchy menu. Once it's running, hover the tray in the top right of the bar and right-click the Dropbox icon to finish the setup.
## Tailscale
[Tailscale](https://tailscale.com/) is a mesh VPN that makes getting access to all your computers and servers over the internet securely super simple. To set it up, select _Install > Service > Tailscale_ from the Omarchy menu.
It gets a panel in the bar, a web app for the admin console, and Taildrop for sending files between your machines — see [networking](35-networking.md).
## ONCE
[ONCE](https://once.com/) is 37signals' line of software you buy once and run on your own server, like the Campfire chat system. Select _Install > Service > ONCE_ from the Omarchy menu to install it, which enables its background service and drops you into the ONCE terminal interface to take it from there.
## NordVPN
[NordVPN](https://nordvpn.com/) is a standard VPN service that lets you exit your traffic from most regions around the world. To set it up, select _Install > Service > NordVPN_ from the Omarchy menu. After the reboot it asks for, run `nordvpn login` to authenticate.
+75
View File
@@ -0,0 +1,75 @@
# Web Apps
You can add your own web apps using _Install > Web App_ in the Omarchy menu (`Super + Space`). It'll ask you for the app name, app URL, and the icon URL, if it can't retrieve it via favicon. You can get great PNG icons for many popular web apps on [Dashboard Icons](https://dashboardicons.com).
They'll then be accessible through the app launcher (`Super + Space`), and use the beautiful frameless web-app window.
If you wish to remove a web app, just go to _Remove > Web App_ in the Omarchy menu.
It's best if you log into all your accounts using a regular browser before using the web app shortcuts. The thin wrapper frame doesn't work well with 1password, so just easier to be logged in directly first.
All the keyboard hotkeys for these web apps can be changed in `~/.config/hypr/bindings.lua`.
When you're in a web app, you can copy the current URL to the clipboard using `Shift + Alt + L`.
By default, Omarchy already ships with an assortment of default apps:
## HEY
[HEY](https://www.hey.com/) is an email and calendar service that serves as a great alternative to people tired of Gmail, Outlook, or Apple Mail. It's made by [37signals](https://37signals.com/) where Omarchy originated.
You can start HEY Email using `Super + Shift + E`, jump straight to composing a new email using `Super + Shift + Alt + E`, and start HEY Calendar using `Super + Shift + C`.
## Basecamp
[Basecamp](https://basecamp.com/) is a project management service that helps small teams move faster and make more progress. Instead of patching together a mishmash of Trello, Slack, Asana, Notion, or whatever, you can have it all in one place with Basecamp. It's made by [37signals](https://37signals.com/) where Omarchy originated.
You can start Basecamp using the application launcher (`Super + Space`)
## ChatGPT
[ChatGPT](https://chatgpt.com) is the most popular AI chat bot in the world.
You can start ChatGPT using `Super + Shift + A`.
## Grok
[Grok](https://grok.com) is xAI's chat bot.
You can start Grok using `Super + Shift + Alt + A`.
## WhatsApp
[WhatsApp](https://www.whatsapp.com/) is one of the most popular messaging services in the world, and the web version is a great option for Linux.
You can start WhatsApp using `Super + Shift + Alt + G`.
## Google apps
Google Messages, Google Photos, Google Maps, and Google Contacts are all included as web apps too.
You can start Google Messages using `Super + Shift + Ctrl + G`, Google Photos using `Super + Shift + P`, and Google Maps using `Super + Shift + S`. Google Contacts is available through the app launcher (`Super + Space`).
## X
X is where news break.
You can start X using `Super + Shift + X` and go straight to writing a new post with `Super + Shift + Alt + X`.
## YouTube
[YouTube](https://youtube.com/) is the most popular video platform in the world.
You can start YouTube using `Super + Shift + Y`.
## Zoom
[Zoom](https://zoom.us/) is the most popular video chat system used in the US. Great connections across the world. And 40-minute meetings can be held without a paying account. Omarchy wraps Zoom's web client, and zoom meeting links will open straight into it.
You start Zoom using the application launcher (`Super + Space`).
## Discord
[Discord](https://discord.com/) is where most gaming and open source communities hang out, including [Omarchy's own](https://discord.gg/tXFUdasqhY).
You start Discord using the application launcher (`Super + Space`).
+85
View File
@@ -0,0 +1,85 @@
# Gaming
Omarchy isn't just for _pRoDUcTiVItY_, it's also for having fun, and what's more fun than gaming? Omarchy ships with a whole suite of gaming options — Steam and RetroArch for native and retro play, Battle.net, Lutris, and Heroic for non-Steam stores, Moonlight for PC streaming, Xbox Cloud Gaming + NVIDIA GeForce NOW for cloud, plus the evergreen Minecraft.
Thanks to Valve's incredible work on [the proton compatibility layer](https://en.wikipedia.org/wiki/Proton_(software)), there are now tens of thousands of playable modern games on Linux. Oh, and did you know that the [Steam Deck](https://store.steampowered.com/steamdeck/) actually runs Arch!
All gaming installers live under _Install > Gaming_ in the Omarchy menu (`Super + Space`). If you ever want to undo one, use _Remove > Gaming_.
## Steam
Install [Steam](https://store.steampowered.com/) by selecting _Install > Gaming > Steam_ from the Omarchy menu (`Super + Space`).
After you've installed it, you'll be able to launch Steam with `Super + Space`.
Note that Steam can take 10-20 seconds to start up, and it's not going to provide any visual feedback that it's loading.
![gaming-steam](images/gaming-steam.webp)
## RetroArch
Install [RetroArch](https://www.retroarch.com/) by selecting _Install > Gaming > RetroArch_ from the Omarchy menu (`Super + Space`). It comes with the full set of libretro cores, so all the classic systems are covered.
RetroArch is fully preconfigured with the beautiful CRT Royale shader for that perfect retro look.
To get going:
1. Drop your BIOS files into `~/Games/bios` and your ROMs into `~/Games/roms`.
2. Launch RetroArch with `Super + Space` and typing `retro`.
3. Scan the `~/Games/roms` directory and you're ready to play.
You can also give a favorite game its own entry in the app launcher with _Install > Gaming > RetroArch Game Launcher_, which lets you pick a core and a ROM, and jump straight into the game from `Super + Space`.
![gaming-retroarch](images/gaming-retroarch.webp)
## Xbox Cloud Gaming
Install the Xbox Cloud Gaming web app by selecting _Install > Gaming > Xbox Cloud Gaming_ from the Omarchy menu (`Super + Space`). It's "just" a web app for the service, but it's quick to start and runs great at 1080p.
If you already have Xbox Game Pass, this is a solid way to play Fortnite and other titles you can't run natively on Linux.
![gaming-xbox-cloud](images/gaming-xbox-cloud.webp)
## NVIDIA GeForce Now
Install the cloud-gaming service [NVIDIA GeForce NOW](https://www.nvidia.com/en-us/geforce-now/) by selecting _Install > Gaming > NVIDIA GeForce NOW_ from the Omarchy menu (`Super + Space`). Another great way to play titles that aren't available natively on Linux.
![gaming-geforce-now](images/gaming-geforce-now.webp)
## Minecraft
Install Minecraft by selecting _Install > Gaming > Minecraft_ from the Omarchy menu (`Super + Space`).
Like Steam, note that it can take a while after logging in or starting up for the next screen to appear, and you're not going to get any feedback while you're waiting.
![gaming-minecraft](images/gaming-minecraft.webp)
## Xbox Controllers
Install support for Bluetooth Xbox controllers by selecting _Install > Gaming > Xbox Controllers_ from the Omarchy menu (`Super + Space`). Pair the controllers via Bluetooth (`Super + Ctrl + B`) and they'll work in all your games. You don't need this if you're just hardwiring your controller with a USB-C cable.
## Moonlight (Game streaming from a PC)
The [Moonlight client](https://github.com/moonlight-stream/moonlight-qt) comes preinstalled with Omarchy, so you can stream games from a Windows PC running [Sunshine](https://app.lizardbyte.dev/Sunshine/) right away. Launch Moonlight via `Super + Space`.
If both your Omarchy machine and the remote gaming PC are hardwired, the experience is indistinguishable from playing locally. Crank the resolution up to native, set the refresh to 120Hz, and max the bitrate — this is the best way to play competitive shooters like Fortnite on Linux.
You can also turn an Omarchy machine into the host by running `omarchy install service sunshine`, which installs Sunshine and opens the Moonlight streaming ports for your LAN and Tailscale.
## Battle.net
Install [Battle.net](https://eu.shop.battle.net/en-us) by selecting _Install > Gaming > Battle.net_ from the Omarchy menu (`Super + Space`). This gives you titles like Diablo, Starcraft, and World of Warcraft as a standalone install running under GE-Proton — no Steam, Lutris, or Heroic needed.
![gaming-starcraft](images/gaming-starcraft.webp)
## Lutris (Windows games)
Install [Lutris](https://lutris.net/) by selecting _Install > Gaming > Lutris_ from the Omarchy menu (`Super + Space`). Lutris is the way to play Windows games from stores like EA and Ubisoft Connect that don't have their own installer above.
Installation is a little janky and looks like nothing is happening at times — just be patient, it's working in the background.
## Heroic Launcher (Epic Games)
Install the [Heroic Launcher](https://heroicgameslauncher.com/) by selecting _Install > Gaming > Heroic (Epic Games)_ from the Omarchy menu (`Super + Space`). Heroic lets you run Epic Games titles, like OddSparks, that don't rely on anti-cheat — plus games from GOG and Amazon Prime Gaming. Sadly, that means no Fortnite and no Rocket League — until Tim Sweeney comes to Linux, this is as close as it gets.
Like Lutris, it can feel slow and janky while installing games. Give it time.
+9
View File
@@ -0,0 +1,9 @@
# Filling out PDFs
Omarchy ships with a nice, basic PDF viewer called Document Viewer. This is the program that'll open any PDFs you just double click on.
But you can only use Document Viewer to fill out PDFs that have been setup as forms. If you need to fill out PDFs that haven't, or you need to sign a PDF, you'll have to right-click the file, select _Open With..._, and pick Xournal++.
Xournal++ will let you write anywhere on a PDF using the T tool. If you need to sign a document, you'll need an image of your signature, and you can use the Image tool to insert this signature and resize it.
When you're done filling out the PDF, use _File > Export as PDF_ to save the final version.
+41
View File
@@ -0,0 +1,41 @@
# Windows VM
Omarchy offers an easy way to run Windows through a Docker VM. You can install it using _Install > Windows_ from the Omarchy menu (`Super + Space`).
Your machine needs KVM virtualization for this, which most do — but it's sometimes switched off in the BIOS, and the installer will tell you if that's the case. You'll also want the disk space: whatever you give Windows, plus about 10GB for the image itself.
The installer asks how much RAM, how many CPU cores, and how much disk to hand over (64GB or more is the sensible floor), then for a Windows username and password. Leave those blank and you get `docker` / `admin`. The download takes a while — 10-15 minutes is normal — and you can follow the progress in the browser at `http://127.0.0.1:8006`.
![windows-vm](images/windows-vm.webp)
## Using it
Once it's installed, launch _Windows_ from the app launcher. That starts the VM if it isn't already running and connects you over RDP, full screen. Give it 15-30 seconds on a cold start.
The RDP session carries sound, your microphone, and a shared clipboard, so copying text between Linux and Windows just works. The resolution follows your window, and Omarchy passes your display scaling through, so it's not a blurry mess on a HiDPI screen.
When you close the RDP window, the VM shuts down automatically. If you'd rather leave it running — say you've got something working away in there — launch it with `omarchy windows vm launch --keep-alive` instead.
The rest of the controls are on the same command:
```bash
omarchy windows vm status # is it running?
omarchy windows vm stop # shut it down
omarchy windows vm launch # start and connect
```
## Sharing files
The directory `~/Windows` in your home directory is automatically shared with the VM. Put files there if you want them accessible to Windows. The VM has no access to any other part of your file system, so you're safe from anything nasty on the Windows side. Its own virtual disk lives in `~/.windows`.
The VM's ports are bound to localhost only, so nothing on your network can reach the Windows machine.
## Limits and licensing
There's no GPU passthrough with this setup, so it's not suitable for gaming or video editing. It's a great way to run Microsoft Office or whatever else you absolutely must have.
The version installed is Windows 11 Pro, unactivated. You'll need your own license key to use the gated features.
You can change the resource allocation later by editing `~/.config/windows/docker-compose.yml`, which is also where you'd mount a USB device. See all the options on [the Dockur Windows project](https://github.com/dockur/windows).
To get rid of the whole thing, use _Remove > Windows_ from the Omarchy menu. That deletes the VM's disk and all its data, so make sure anything you care about is out of `~/Windows` first.
+9
View File
@@ -0,0 +1,9 @@
# Other Packages
Arch has an amazing wealth of packages available for almost any type of software between the official repository and the Arch User Repository (AUR).
It couldn't be easier to use either. You install a new Arch package by going to _Install > Package_ in the Omarchy menu (`Super + Space`) and typing the package you want. It'll automatically fuzzy filter the list of all packages. (You can also do it manually using `omarchy pkg add [package]` in the terminal).
You can do the same with AUR, just use _Install > AUR_. Just remember that the AUR isn't vetted by the Arch team. It's like RubyGems or npm. Anyone can upload.
If you want to remove a package, you can use _Remove > Package_ from the Omarchy menu. It'll remove package, config files, and dependencies. (You can also do it manually using `omarchy pkg drop [package]`).
+37
View File
@@ -0,0 +1,37 @@
# Updates
Omarchy and your packages are kept up to date via _Update > Omarchy_ in the Omarchy menu (`Super + Space`).
Omarchy itself is installed as regular pacman packages from the [Omarchy Package Repository](https://github.com/omacom-io/omarchy-pkgs), so an update installs [the latest Omarchy release](https://github.com/basecamp/omarchy/releases), runs any pending migrations to get your system in sync with the latest, and updates all system packages from the [Omarchy Arch Mirror](https://github.com/omacom-io/omarchy-mirror) and [AUR](https://aur.archlinux.org/) (if you have installed any AUR packages).
When new releases are made, a circle arrow icon will appear to the right of your clock. Click it and the update process will start.
![update-available](images/update-available.webp)
### Four channels
Omarchy is updated along four channels: stable, RC, edge, and dev. New installations start on the stable channel, which tracks the [official releases](https://github.com/basecamp/omarchy/releases/), as well as the [stable Omarchy Arch mirror](https://github.com/omacom-io/omarchy-mirror) that's running one month behind the latest, so we can catch any new incompatibilities that require config changes before they cause problems for people.
But if you'd like to help spot those potential issues, you can run on the edge channel. That'll keep your Omarchy packages tracking the latest development builds, and lets you update to the latest Arch packages as soon as they're available. You should only do this if you're experienced with Linux, and know how to recover a system that has problems.
Before any new major release, we'll be doing final validation using the RC channel. If you're interested in helping with final polishing, come hang out in #omarchy-release-candidates on the Discord.
Finally, there's the dev channel, which links Omarchy directly to a git checkout of the source code in `~/omarchy`, combined with the edge packages. You should only use this channel if you're an experienced Linux user, working directly on Omarchy, and willing to tolerate breakage.
You can switch between channels using _Update > Channel_ from the Omarchy menu (or `omarchy-channel-set` in the terminal).
### Firmware updates
Your packages aren't the only thing that goes stale. Many laptops and peripherals ship BIOS, SSD, and dock firmware through the Linux Vendor Firmware Service, and _Update > Firmware_ in the Omarchy menu will fetch and install whatever your hardware has waiting. It installs `fwupd` the first time you run it. Plenty of firmware can only be written during a reboot, so don't be surprised to be asked for one.
### Warning about direct pacman/yay updates
If you're already familiar with Arch, you might be tempted to just run `pacman -Syu` or `yay -Syu` yourself, but if you do that, you'll miss the snapshot, migrations, and configuration updates that Omarchy runs together with new packages. That's why Omarchy will actually stop a direct system upgrade and point you to `omarchy update` instead. (If you really know what you're doing, the guard will tell you how to bypass it for a single transaction.)
### Rolling back bad updates
If you ever have a problem after doing an update, you can rollback your system to the snapshot taken before the update. Just restart and pick the snapshot in the boot loading menu from before you started the update.
![bootloader](images/bootloader.webp)
If somehow your configuration files have been corrupted, you can also perform an Omarchy reinstall using `omarchy reinstall` in the terminal. This will reinstall all the default Omarchy packages, put you on stable and downgrade any packages that are too new, and reset all the configuration files. Note that all your user config changes to the Omarchy defaults will be overwritten doing this!
+78
View File
@@ -0,0 +1,78 @@
# Dotfiles
Omarchy is primarily configured through the so-called dotfiles that live in `~/.config`. Those are considered your files for your changes. The files that live in `/usr/share/omarchy` belong to Omarchy itself, and you shouldn't be messing with those. If you need to change anything in `/usr/share/omarchy`, you should be overwriting the value in `~/.config` instead.
The key configs can be edited straight from the Omarchy menu (`Super + Space`), like _Setup > Monitors_, _Setup > Keybindings_, _Setup > Input_, and _Setup > Config > [file]_. When you do it this way, any process that needs restarting after config edits automatically will be after you quit the editor (Neovim by default — `:wq`, remember! — but you can change that via _Setup > Defaults > Editor_).
Here's a list of the key files in `~/.config` and what they control:
| File | Purpose |
| ----------------------- | --------------------- |
| `~/.config/hypr/hyprland.lua` | The main Hyprland config. Loads the Omarchy defaults plus your override files below. [Learn more about Hyprland configs](https://wiki.hypr.land/Configuring/). |
| `~/.config/hypr/bindings.lua` | Your own keybindings and overrides of the defaults. |
| `~/.config/hypr/monitors.lua` | Controls your monitors, resolution, and position. |
| `~/.config/hypr/input.lua` | Controls your keyboard layout, mouse, and trackpad settings. |
| `~/.config/hypr/looknfeel.lua` | Controls gaps, borders, animations, and the rest of the look. |
| `~/.config/hypr/autostart.lua` | Controls extra processes started with the session. |
| `~/.config/omarchy/shell.json` | Controls the Omarchy shell: bar position, layout, and widgets, plus screensaver, lock, and idle timings. |
| `~/.config/foot/foot.ini` | Controls your terminal (foot is the default). |
| `~/.XCompose` | Defines your quick-access emoji and name/email autocomplete. Make sure to run `omarchy-restart-xcompose` after making changes. |
If you end up making a lot of changes to tweak your own setup, it's a good idea to backup all these dotfiles. [Stow is a great way to do that](https://www.youtube.com/watch?v=NoFiYOqnC4o).
### Starting your own apps with the session
If you want something to run every time you log in — a sync daemon, a chat app, your own script — put it in `~/.config/hypr/autostart.lua`:
```lua
o.launch_on_start("my-service")
```
That starts the command as part of the session, so it's properly cleaned up when you log out again.
### Running scripts on system events
Omarchy fires hooks at a handful of moments, and you can hang your own scripts off them. They live in `~/.config/omarchy/hooks/<event>.d/`, one directory per event, and every executable file in there runs when the event happens:
| Event | When it runs |
| ----- | ------------ |
| `post-boot` | Right after the desktop has started |
| `post-update` | During `omarchy update`, after packages and migrations |
| `pre-refresh-pacman` | Before `omarchy refresh pacman` re-syncs the package config |
| `theme-set` | After a theme change (theme name in `$1`) |
| `font-set` | After a font change (font name in `$1`) |
| `battery-low` | When the battery gets low (percentage in `$1`) |
Each of those directories already holds a `.sample` file showing the shape of a hook — drop the `.sample` from the name to put it to work. To install a script you've written elsewhere, use `omarchy hook install post-boot ~/my-hook`, which copies it in and makes it executable.
### Adding your own menu entries
The Omarchy menu (`Super + Space`) can be extended with your own rows by editing `~/.config/omarchy/extensions/omarchy-menu.jsonc`. Entries are keyed by a dotted id, and the id is what places them in the tree, so `personal` shows up on the root menu and `personal.notes` shows up inside it:
```jsonc
"personal": {"icon":"","label":"Personal"},
"personal.notes": {"icon":"󰎞","label":"Notes","action":"omarchy-launch-editor ~/notes"},
```
Reuse an existing id and you override that row instead of adding a new one. The file ships with all the available fields documented as comments.
### Adding your own shell exports, functions, and aliases
Omarchy ships with a bunch of ergonomic aliases and helpful functions, but it's very common to want to add your own. You should add both aliases, functions, and exports in `~/.bashrc`. This file will not be overwritten on updates. If you want to change any of the Omarchy defaults, you can also safely add them here.
### Changing internal Omarchy files
Look, this is your computer. You can do whatever you want with it, but I would advise against making changes to the files in `/usr/share/omarchy` directly. They belong to the Omarchy pacman package, so your changes will simply be overwritten on the next update. You're better off just overwriting any default values you don't like in the `~/.config/*` folder instead.
You can change just about everything that way, like the default keybindings. Just edit `~/.config/hypr/bindings.lua` to, say, replace [Obsidian](https://obsidian.md/) with [Joplin](https://joplinapp.org/) (install with `omarchy-pkg-add joplin-bin`):
```
hl.unbind("SUPER + SHIFT + O")
o.bind("SUPER + SHIFT + O", "Joplin", "joplin-desktop")
```
If you insist on hacking on the internal Omarchy files, switch to the dev channel via _Update > Channel > Dev_. That links Omarchy to a git checkout of the source code in `~/omarchy`, which you're free to change to your heart's content. Ain't nobody here to tell you what to do!
### Resetting any changes
If you end up making a mess of the configurations, you can always revert them to the defaults via _Update > Config_ in the Omarchy menu. Or by running `omarchy reinstall configs` to reset everything.
+102
View File
@@ -0,0 +1,102 @@
# Shell Plugins
The Omarchy desktop runs as a single long-lived Quickshell process called `omarchy-shell`, and almost everything you see on screen is a plugin inside it. The bar is a plugin. So are the panels that drop down from it, the fullscreen overlays like the emoji picker and the clipboard manager, the Omarchy menu itself, the lock screen, the polkit dialog, and the headless services that watch your battery and warm your screen up at night.
That's not just an implementation detail. It means you can turn pieces of the desktop off, swap them out, or write your own without touching a line of Omarchy's source.
The first-party plugins ship with Omarchy and live in `$OMARCHY_PATH/shell/plugins/`. Anything you add yourself — your own experiments, or something you found on GitHub — lives in `~/.config/omarchy/plugins/`. Both are discovered the same way at startup; the only difference is where they sit on disk.
## Seeing what you have
```
omarchy plugin list
```
That prints every discovered plugin with its id, whether it's enabled, whether it's first-party or third-party, its kinds, and its display name. Add `--json` if you're feeding it to something else.
Plugin ids are namespaced. The built-ins all start with `omarchy.` — `omarchy.clock`, `omarchy.network`, `omarchy.notifications` — and that namespace is reserved, so a third-party plugin can never claim it.
## Turning them on and off
```
omarchy plugin enable omarchy.tailscale
omarchy plugin disable omarchy.weather
```
Or use the menu: _Setup > Plugins_ has Enable, Disable, Add, Clone, and Remove, each with a picker that only offers the plugins that make sense for that action.
Enabled state is stored in `~/.config/omarchy/shell.json`, and the rule differs slightly for the two kinds of plugin. A third-party plugin is enabled exactly when its id appears somewhere in that file — as a bar layout entry, as an entry in `plugins[]`, or as `bar.id`. First-party plugins that aren't bar widgets are the other way around: they're on by default and only turn off by being listed in `disabledPlugins[]`.
A full bar plugin has no off state at all. There's always exactly one bar, so you replace it by enabling another one. Bar widget placement is covered in [the top bar](05-the-top-bar.md).
## Adding a plugin from git
A third-party plugin is just a git repo with a `manifest.json` at its root.
```
omarchy plugin add https://github.com/acme/omarchy-weather.git --enable
```
Before it does anything, it tells you plainly that plugins run as arbitrary, unsandboxed code inside your long-lived shell process, shows you the URL, and asks you to confirm. Take that seriously. A plugin isn't a config file — it's code that runs for as long as your session does, with everything your user account can reach. Only add repos you're willing to run, and read them before you enable them.
Then it clones the repo into a staging directory, validates the manifest, refuses the install if another plugin already claims that id, and moves it into `~/.config/omarchy/plugins/<id>/`. Without `--enable` it asks whether you want it on now, and you can say no and go read the code first. It never runs anything from the plugin, never executes an install hook, and never asks for sudo — it clones files, checks the manifest, and flips a bit over IPC.
Updating is a fast-forward pull of that same checkout:
```
omarchy plugin update acme.weather
omarchy plugin update
```
With no id it updates every git-managed plugin you have. It shows you the diff before applying it, refuses to update if you've got local changes it can't fast-forward past, and rolls back if the new revision fails validation.
```
omarchy plugin remove acme.weather
```
Removal disables the plugin first, then deletes it if it's a git checkout (the repo is still upstream) or unlinks it if it's a symlink. A hand-made plugin folder with no git repo gets moved to a timestamped backup inside the plugins directory instead of being deleted outright.
## Cloning a built-in to modify it
This is my favorite part. If you want to change how a built-in widget behaves, don't edit the files under `$OMARCHY_PATH` — those belong to the package and the next update will overwrite them. Clone it instead:
```
omarchy plugin clone omarchy.clock
```
That copies the whole plugin into `~/.config/omarchy/plugins/dhh.clock` (your username, not mine), renames it to "My Clock", enables it, and switches the shell over from the built-in to your copy — keeping an existing bar widget's position and settings. Add `--edit` to open the new directory in your `$EDITOR` right away, which is what the menu's _Setup > Plugins > Clone Plugin_ does for you.
The username prefix keeps your clone's id yours, so sharing it doesn't collide with anyone else's. Calls made to the original built-in id get routed to your clone, so nothing that referred to `omarchy.clock` needs updating. And if you make a mess of it, `omarchy plugin remove dhh.clock` puts the built-in back.
Saving a file anywhere under `~/.config/omarchy/plugins/` reloads the plugin code automatically, so you can leave the editor open and watch your changes land.
## Writing your own
A plugin is a directory with a `manifest.json` and some QML. The manifest declares `schemaVersion: 1`, an `id`, `name`, `version`, one or more `kinds`, and an `entryPoints` object pointing at the QML file for each kind:
| Kind | What it is |
|------|------------|
| `bar-widget` | A component the active bar can drop into a section |
| `panel` | A persistent or summoned floating window |
| `overlay` | A fullscreen overlay |
| `menu` | A summoned menu surface |
| `service` | A headless singleton with no UI |
| `bar` | A full bar that replaces the built-in one |
A plugin can declare several kinds at once — the media plugin is both a `service` and a `bar-widget`. Bar widgets get an extra `barWidget` block with a display name, a category, an optional `defaultSection`, and `allowMultiple`, which says whether it makes sense to have more than one on the bar. Most widgets set it to `false`; spacers and indicators set it to `true`.
Before you publish anything, check it:
```
omarchy plugin validate ./my-plugin
```
That runs the same checks the shell does at load time: the schema version, the required fields, an id that isn't reserved, entry points that are safe relative paths and actually exist, an entry point for every kind you claimed, and no symlinks anywhere inside the folder.
For the full picture, the source is the documentation: `shell/README.md` in the Omarchy repo covers the manifest schema, the shell's IPC contract, and the exact shape of `shell.json`, and `shell/plugins/README.md` lists every first-party plugin with its id, kinds, and entry points.
## Sharing yours with the world
Once you've made something you like, put it in a public git repo. That's the whole distribution mechanism — anyone can then run `omarchy plugin add` against your URL and have it running in seconds.
To help people actually find it, list it at [omarchyplugins.com](https://omarchyplugins.com). That's the community directory of Omarchy shell plugins, and it's the first place to look when you're wondering whether someone has already built the widget you're about to write. Browse it before you start!
+55
View File
@@ -0,0 +1,55 @@
# Monitors
Omarchy assumes you're running on a 2x-capable retina-class display by default. This is what you need to get those nice, crisp programmer fonts. It's what almost all new premium laptops with high-resolution screens are optimized for. It's what you'd want to run on a 27" 5K [Apple Studio Display](https://www.apple.com/studio-display/)/[ProArt PA27JCV](https://www.asus.com/us/displays-desktops/monitors/proart/proart-display-5k-pa27jcv/)/[Samsung S9](https://www.samsung.com/us/computing/monitors/5k/27-viewfinity-s9-5k-monitor-with-thunderbolt-4-matte-display-and-smart-features-ls27c900panxza/)/[Kuycon G27P](https://kuycon.us/monitors/G27P/) or 32" 6K [Apple XDR](https://www.apple.com/pro-display-xdr/)/[ProArt PA32QCV](https://www.asus.com/displays-desktops/monitors/proart/proart-display-6k-pa32qcv/)/[Kuycon G32P](https://kuycon.us/monitors/G32P/).
But if you're not running a display with a PPI of 218 or above, you'll want to change the monitor settings. For example, if you have a 27" or 32" 4K, you can use fractional scaling by opening `~/.config/hypr/monitors.lua` (via _Setup > Monitors_ in the Omarchy menu) and switching to the recommendation for that combo:
```lua
local omarchy_gdk_scale = 2
local omarchy_monitor_scale = 1.6
```
If you're using a 1080p or 1440p display, you'll probably just want to use 1x scaling, so you can use:
```lua
local omarchy_gdk_scale = 1
local omarchy_monitor_scale = 1
```
Changes to `GDK_SCALE` apply to applications started after the change (and GTK only honors whole numbers, so keep it at the nearest integer of your monitor scale). So make sure you quit the windows that you have that are oversized after the change (or close all windows with `Ctrl + Alt + Del`!).
You can also quickly step through the major monitor scaling ratios (1x, 1.25x, 1.6x, 2x, 3x, 4x) using `Super + /` to go higher and `Super + Alt + /` to go lower. If you have the default configuration, these changes will also persist past reboot.
### Making text bigger or smaller
Monitor scaling changes the size of everything. If all you want is bigger or smaller _text_, there's a single knob for that:
```
omarchy display text size 14
```
That takes a pixel size between 9 and 20, and moves the Omarchy shell, GTK applications, and your terminal together, so the whole desktop stays in proportion. Run it without an argument to see where you're at, and `omarchy display text size reset` to go back to the default. Foot is the one straggler: it has no way to reload its config, so running terminals keep their old size until you open a new one.
### Extending and mirroring laptop displays
When you connect an external screen to your laptop, the display is automatically extended. But you can change that to mirroring instead using _Trigger > Hardware_ in the Omarchy menu or `Super + Ctrl + Alt + Delete`. This is especially helpful if that external screen is a projector, and you want to show something while working.
When you're extending, closing the lid on the laptop will automatically turn off the internal screen. Opening the lid will turn it back on. You can also control this manually using _Trigger > Hardware_ in the Omarchy menu or `Super + Ctrl + Delete`.
### Arranging multiple screens
Hyprland works great with multiple screens. Read more about how to lay them out in [the Hyprland monitor documentation](https://wiki.hypr.land/Configuring/Basics/Monitors/). You can [bind specific workspaces to specific monitors](https://wiki.hypr.land/Configuring/Basics/Workspace-Rules/) as well. In Omarchy, these rules go in `~/.config/hypr/monitors.lua` as `hl.monitor` entries — the file ships with commented examples for pinning a specific monitor to a resolution, position, and rotation.
You can also checkout [Hyprmon](https://github.com/erans/hyprmon/), if you'd like a TUI to help you with the positioning of multiple screens.
### Controlling brightness
Monitor brightness is controlled by the dedicated function keys for brightness up/down. If you hold down shift while pressing these, you'll go to maximum or minimum brightness. The keys control the display you're focused on, so external monitors that speak DDC/CI are adjusted the same way as the laptop screen.
### Apple Displays
If you're using an Apple display, the regular keyboard brightness keys will also automatically work, if you're focused on the Apple display. This is done through the `asdcontrol` command.
Note that if you're using an Apple 6K XDR display, you may see a phantom screen in your `hyprctl monitors` listing. You can turn this off with something like `hl.monitor({ output = "DP-2", disabled = true })` via _Setup > Monitors_.
On Intel machines, you should be connecting to Apple displays using a regular Thunderbolt cable. On other machines without Thunderbolt, you'll typically have to use a [DP + USB-A -> USB-C cable](https://www.amazon.com/dp/B0BNX7MS6N) to make it work.
+64
View File
@@ -0,0 +1,64 @@
# Keyboard, Mouse, Trackpad
Hyprland lets you configure all your inputs in great detail. You can change the keyboard repeat to be supersonically fast or make the trackpad use natural scrolling. You change all of it in `~/.config/hypr/input.lua`, which you can also reach via _Setup > Input_ in the Omarchy menu (`Super + Space`). Anything you set there replaces Omarchy's defaults.
Here's an example:
```lua
hl.config({
input = {
-- Use multiple keyboard layouts and switch between them with Left Alt + Right Alt
kb_layout = "us,dk",
kb_options = "compose:caps,shift:both_capslock_cancel,grp:alts_toggle",
-- Change speed of keyboard repeat
repeat_rate = 40,
repeat_delay = 600,
-- Increase sensitivity for mouse/trackpad (default: 0)
sensitivity = 0.35,
touchpad = {
-- Use natural (inverse) scrolling
natural_scroll = true,
-- Use two-finger clicks for right-click instead of lower-right corner
clickfinger_behavior = true,
-- Control the speed of your scrolling
scroll_factor = 0.3,
},
},
})
-- Scroll faster in the terminal
o.window("(Alacritty|kitty|foot)", { scroll_touchpad = 1.5 })
```
You can [see all the input options](https://wiki.hypr.land/Configuring/Basics/Variables/#input) on the Hyprland wiki for inputs.
### Trackpad gestures
You can also turn on [touchpad gestures](https://wiki.hypr.land/Configuring/Advanced-and-Cool/Gestures/), like swiping with three fingers to change workspaces:
```lua
hl.gesture({ fingers = 3, direction = "horizontal", action = "workspace" })
```
On Dell XPS laptops with a haptic touchpad, you can also set the click strength to low, mid, or high under _Trigger > Hardware > Touchpad Haptics_.
### Typing in Chinese, Japanese, and other languages
Omarchy runs the [fcitx5](https://fcitx-im.org/) input method framework as part of every session — it's what powers the CapsLock compose sequences. That means the plumbing for non-Latin input is already in place: install an input engine like `fcitx5-mozc` (Japanese) or `fcitx5-chinese-addons` (Chinese) with `omarchy pkg add`, plus `fcitx5-configtool` to add the engine to your input methods and set the key that switches between them.
### Use ALT as SUPER
On some keyboards, it's not convenient to use the primary meta key (Windows/cmd key) as SUPER. You can change this to be ALT instead using this change:
```lua
hl.config({
input = {
kb_options = "compose:caps,shift:both_capslock_cancel,altwin:swap_alt_win",
},
})
```
+45
View File
@@ -0,0 +1,45 @@
# Networking
Networking in Omarchy is handled by NetworkManager, and you drive it from the network icon in the [top bar](05-the-top-bar.md) or with `Super + Ctrl + W`.
That panel scans for Wi-Fi networks, shows signal strength, and connects. Ethernet needs nothing at all — plug it in and it works. If you'd rather stay in the terminal, `nmtui` gives you the same controls, and there's an `omarchy network` command group too.
## Sharing your Wi-Fi
Rather than reading a long password out loud, run _Setup > Network > QR Code_ while you're on Wi-Fi. That puts a QR code on screen that any phone camera can scan to join. It's one of those things you'll use more than you'd expect once you know it's there.
If you actually need the password itself, `omarchy network password <interface>` prints it.
## DNS
Omarchy uses whatever DNS your network hands out over DHCP. You can override that for the whole machine under _Setup > Network > DNS_, where Cloudflare and Google are one click away. Pick _Custom_ to type in your own servers.
From the terminal, `omarchy dns` prints the current provider and `omarchy dns Cloudflare` sets one.
## Pinning the Wi-Fi band
If your router puts 2.4GHz, 5GHz, and 6GHz on the same network name, your laptop will sometimes cling to the slow one. `omarchy network band` shows which band you're on, and `omarchy network band 5` pins it. Use `auto` to let it choose again.
This pins the band rather than a specific access point, so you keep roaming between APs normally.
## How fast is it?
_Trigger > Speed Test > Network Speed Test_ measures your actual up and down speed with a pair of dials. From the terminal it's `omarchy network speedtest down` or `up`. (There's a disk speed test sitting next to it in that menu, if you're benchmarking the other bottleneck.)
## The firewall
The firewall is on by default and blocks all incoming traffic, with one exception: port 53317, so [LocalSend](22-guis.md) works out of the box.
SSH is off until you turn it on with _Setup > Security > SSHD_, which starts the daemon, opens port 22 rate-limited against brute force, and authorizes a key. Docker is locked down too, so containers can't accidentally expose themselves to the world. See [security](48-security.md) for the whole story.
## Tailscale
[Tailscale](https://tailscale.com/) is a mesh VPN that makes reaching all your computers and servers over the internet simple and secure. Install it with _Install > Service > Tailscale_.
That gives you a Tailscale panel in the bar, which connects and disconnects the tailnet, switches accounts, and picks an exit node — your own machines and Mullvad regions both show up in the list. It also browses your machines, and that's where Taildrop lives: select a machine and press `s` to send it files, or `c`, `n`, and `d` to copy its IP, name, or full DNS name. The terminal equivalent is `omarchy tailscale send <machine> [file...]`, and files sent to you land in `~/Downloads` automatically.
Installing it also adds a web app for the Tailscale admin console.
## When it stops working
Before rebooting, try restarting the offending piece on its own. _Update > Hardware_ has Wi-Fi, Bluetooth, Audio, and Trackpad, and reloading one of those clears up most "it worked five minutes ago" situations. See [troubleshooting](45-troubleshooting.md).
+19
View File
@@ -0,0 +1,19 @@
# System sleep
Omarchy enables suspend and hibernation by default, but if you're having issues with either on your machine, you can toggle them off.
### Power profiles
On a laptop, Omarchy remembers your power profile separately for plugged in and running on battery, and switches between the two as you plug and unplug. Out of the box that means performance on AC and balanced on battery.
You can see what your machine offers with `omarchy powerprofiles list`, and set the one you want for the state you're currently in with `omarchy powerprofiles set autodetect power-saver`. To set the other state without unplugging anything, name it directly: `omarchy powerprofiles set battery power-saver`. Whatever you pick is what you'll get back the next time you're in that state.
### Toggle suspend
You toggle suspend by running `omarchy toggle suspend` from the terminal. That just reveals/hides the option under _System_ (or `Super + Esc`), and then you can see if it works consistently on your system. If not, you can hide it again with the same command.
### Toggle hibernation
You set up hibernation by running `omarchy hibernation setup` from the terminal. Hibernation creates a /swap subvolume on your boot drive the size of your physical RAM allocation, so make sure you have plenty of room to spare. On a 32GB machine, you'll always need 32GB+ free for this volume. Hibernation also requires the default Limine bootloader.
When set up, you'll see the hibernate option under _System_ (or `Super + Esc`), and then you can see if it works consistently on your system. If not, you can remove it again by running `omarchy hibernation remove`.
+17
View File
@@ -0,0 +1,17 @@
# Hardware authentication
### Fingerprint authentication
A lot of laptops come with a fingerprint sensor to do authentication. You can use this with Omarchy by running _Setup > Security > Fingerprint_ in the Omarchy menu (`Super + Space`).
That'll install the fingerprint package, collect your print, verify it, and you'll be set to go using your fingerprint to unlock from the lock screen (which you can trigger with `Super + Ctrl + L`), enter sudo mode, and authorize system prompts.
When your laptop lid is closed, the fingerprint prompt is automatically skipped, so you go straight to the password prompt instead of waiting on a sensor you can't reach. If you otherwise need to work on an external keyboard that doesn't have a sensor, just hit `CTRL + C`, when you're prompted for your fingerprint during `sudo`.
You can remove the fingerprint authentication under _Remove > Security > Fingerprint_ in the Omarchy menu.
### Fido2 authentication
If you're using a Fido2 device, you can set it up for `sudo` authentication using _Setup > Security > Fido2_ in the Omarchy menu (`Super + Space`). It covers `sudo` and system authorization prompts, though, not unlocking your computer.
You can remove the fido2 authentication under _Remove > Security > Fido2_ in the Omarchy menu.
+9
View File
@@ -0,0 +1,9 @@
# Fonts
Omarchy uses JetBrainsMono Nerd Font as both the terminal and system font by default.
![fonts-jetbrainsmono](images/fonts-jetbrainsmono.webp)
You can change this through the _Style > Font_ menu in the Omarchy menu (`Super + Space`). That sets the monospace font everywhere: the terminal, the bar, and anything else that asks for it.
You can install other popular programming fonts via _Install > Style > Font_ in the Omarchy menu: Cascadia Mono, Meslo LG Mono, Fira Code, Victor Code, Bitstream Vera Mono, and Iosevka are all one click away, in their Nerd Font versions so the glyphs in the bar and terminal keep working. After installing one, pick it under _Style > Font_.
+7
View File
@@ -0,0 +1,7 @@
# Backgrounds
Every theme ships with its own set of backgrounds, and you can add extras of your own in `~/.config/omarchy/backgrounds/[theme]`. If you want to add an extra background image to, say, the nord theme, you just put the file in `~/.config/omarchy/backgrounds/nord`.
You can do this most easily by going to _Install > Style > Background_ in the Omarchy Menu. That'll bring up the folder where the backgrounds for that theme is stored. Hit `Super + Shift + F` to start another file manager, find your background, copy it over. Now it'll be included in the choices of backgrounds you can select between using `Super + Ctrl + Space`.
You can find a huge collection of cool curated backgrounds on https://github.com/dharmx/walls.
+7
View File
@@ -0,0 +1,7 @@
# Prompt
Omarchy ships with a minimal [Starship](https://starship.rs/) prompt by default. That's how I like to keep my prompt. I don't need to know the user, because it's always me, and I don't need to know the time, because it's always at the top.
![prompt](images/prompt.webp)
If you want more information or style, you can change the [Starship.rs](https://starship.rs/) configuration in `~/.config/starship.toml`. There's a lot you can do. Just don't go overboard (or do go overboard, do whatever you want, it's your computer!)
+43
View File
@@ -0,0 +1,43 @@
# Branding
Omarchy allows you to set your company logo or personal image for both the boot unlock, the screensaver, and the about screen.
### Boot unlock
You can use `omarchy plymouth preview` to see what your custom logo and colors would look like. It takes a background color, a text color, a logo png, and a path for the preview image:
```
omarchy plymouth preview '#1d2021' '#ebdbb2' logo.png preview.png
```
Then apply the setup with `omarchy plymouth set '#1d2021' '#ebdbb2' logo.png`, which will also give the SDDM login screen the same colors and logo. If you want to revert, you can use `omarchy plymouth reset`.
![branding-plymouth-shopify](images/branding-plymouth-shopify.webp)
### Screensaver
You can change the logo used for the screensaver under _Style > Screensaver_. It's an ASCII logo, so you can edit the text directly, but you can also hand it a png or svg image, and we'll convert that to ASCII. It looks pretty cool.
![branding-screensaver](images/branding-screensaver.webp)
There are three entries in that menu:
- **Edit Text** opens `~/.config/omarchy/branding/screensaver.txt` in your editor. Type or paste whatever you like — ASCII art, your name, a rude word. Save and quit, and the screensaver fires up immediately so you can see it.
- **Set From Image** opens a file picker for a png or svg, converts it to ASCII, and shows you the result. Logos with a clear silhouette work far better than photos.
- **Restore Default** puts the Omarchy logo back.
### About screen
The same three options are under _Style > About_ for the _About_ screen you get from the Omarchy menu, and they work identically — the file is `~/.config/omarchy/branding/about.txt`, and the About window pops up after each change. The About art is converted to a smaller size than the screensaver's, since it has to fit in a window rather than fill your display.
![branding-about](images/branding-about.webp)
### Converting images yourself
Both of the _Set From Image_ options are just calling `omarchy transcode ascii`, which you can run directly if you want control over the conversion:
```
omarchy transcode ascii ~/logo.svg ~/.config/omarchy/branding/screensaver.txt --width 100
```
It takes `--width` and `--height` in terminal columns and rows, a `--mode` of either `braille` (the default, and much finer) or `block`, a `--threshold` percentage for deciding which pixels count as part of the logo, and `--invert` for when your logo is light on a dark background. If a conversion comes out as a blob, the threshold is usually the knob to turn.
+37
View File
@@ -0,0 +1,37 @@
# Common tweaks
This is a collection of common tailorings to the Omarchy setup. Know that it might occasionally be necessary for system updates to restore certain configs to their original condition. If this happens, your changes won't be lost, but put in a `.bak` file in the same directory.
If you screw something up, you can restore individual configs to their original setup via _Update > Config_ in the Omarchy menu. If you _really_ screw everything up, you can reset all configs via `omarchy-reinstall`.
### Reveal all tray icons all the time
By default, tray icons, like Dropbox, 1password, or Steam, are hidden behind the tray expander arrow, which reveals them when you hover it. If you'd like to have them exposed all the time, right-click the expander arrow to open the tray icon manager, then pin the icons you want to keep visible (you can also hide the ones you never want to see).
### Rounded window corners
Omarchy's default design is one of square corners, but if you like to soften that up a bit, you can change `~/.config/hypr/looknfeel.lua` so rounding is no longer commented out:
```
hl.config({
decoration = {
-- Use round window corners.
rounding = 8,
},
})
```
### Remove window gaps
On laptop displays, some people prefer not to waste any pixels on window gaps (or even a top bar, which you can toggle off with `Super + Shift + Space`). You can toggle all gaps and borders off with `Super + Shift + Backspace`, or remove them permanently by removing the comments in this section of `~/.config/hypr/looknfeel.lua`:
```
hl.config({
general = {
-- No gaps between windows or borders.
gaps_in = 0,
gaps_out = 0,
border_size = 0,
},
})
```
+31
View File
@@ -0,0 +1,31 @@
# Making your own theme
You can add your own themes to `~/.config/omarchy/themes`. Just copy one of the existing ones as a base (look in `/usr/share/omarchy/themes`), then tweak to your delight. As long as your theme is inside that folder, it'll be included in the theme selection menu.
The main file you have to tweak is `colors.toml`. That defines the color set that's then used to generate configurations for the terminal (Foot/Alacritty/Ghostty/Kitty), btop, Chromium, Hyprland, Neovim, Helix, VSCode, Obsidian, and the entire Omarchy shell (top bar, menu, notifications, OSD, and lock screen).
You can also use the included Aether application to create a new theme using a lovely GUI interface to play with colors and search for backgrounds. Just start it via the apps menu on `Super + Alt + Space`.
### Light mode
If you're making a light mode theme, set `mode = "light"` at the top of your `colors.toml`. Then it'll automatically be paired with light mode for all the apps. (The old way of dropping an empty file called `light.mode` in the root of your theme still works too.)
### Icon colors
If you'd like to color-match the file manager icons to your theme, add a file called `icons.theme` with the name of the icon set you want to use. By default, the options are: `Yaru Yaru-blue Yaru-dark Yaru-magenta Yaru-olive Yaru-prussiangreen Yaru-purple Yaru-red Yaru-sage Yaru-wartybrown Yaru-yellow`.
### Unlock image
Themes supplied with `unlock.png` and `preview-unlock.png` images will be listed under _Style > Unlock_. Your `unlock.png` should preferably be a transparent png. And you can create the preview image using `omarchy plymouth preview`.
### Theming apps Omarchy doesn't cover
If you use an app that isn't in that list, you can teach Omarchy to theme it yourself with a template. Drop a file in `~/.config/omarchy/themed/` named after the config it generates plus a `.tpl` extension, and write the config with `{{ background }}`, `{{ foreground }}`, `{{ accent }}`, `{{ red }}`, `{{ color0 }}` through `{{ color15 }}`, and the rest of the palette as placeholders. Every time you switch themes, the file is regenerated with that theme's colors.
There's a fully commented `alacritty.toml.tpl.sample` in that folder to copy from — it lists every variable you can use, plus the `_strip` and `_rgb` modifiers for apps that want their colors without the `#` or as decimal RGB. Your templates take priority over Omarchy's own, so you can also use this to override how a built-in app gets themed.
### Distributing your theme
If you want to distribute your theme so others can use it, you need to put it on a public git server, like GitHub. Then people can install it using _Install > Style > Theme_ in the Omarchy menu using that URL. It's recommended that you follow the naming convention of `omarchy-[themename]-theme`, as the theme will show correctly as just `[themename]` in the theme selection menu after installation.
You can have your theme added to [the extra themes page](https://omarchy.org/themes/) by sending a pull request to [the omarchy-site repo](https://github.com/omacom-io/omarchy-site).
+71
View File
@@ -0,0 +1,71 @@
# Mac support
Omarchy has built-in support for **Intel Macs**. There are a couple of known limitations at the moment, but as long as you're aware and OK with those; you can breathe some new life into your old Macs by loading Omarchy.
Please note that installing on an M-series Mac is not directly supported at this time. You can find out more about the state of this in #omarchy-on-other in our [Discord](https://discord.gg/tXFUdasqhY).
In a simple test, we were able to achieve 36% performance gains on a 2019 MacBook Pro just by installing Omarchy.
![macbook-omarchy](images/macbook-omarchy.webp)
### Installing Omarchy on Mac
Omarchy only supports being the **only** OS installed at the moment. During the installation, the drive will be wiped and MacOS will no longer be bootable.
You can still restore it later via Internet Recovery if you'd like.
For the sake of this part, we'll assume you've already reviewed [Getting Started](02-getting-started.md) and have your USB drive ready. If you don't go ahead and do that now.
#### Disable Secure Boot
It is necessary to disable Apple's Secure Boot in order to boot the bootable USB, as well as the OS. To disable it, perform the following:
1. Turn off your Mac
2. Turn it on and _immediately_ press and hold Command-R until you see the loading screen appear
3. Select your user and enter your password if prompted
4. Once in the recovery screen, choose **Utilities > Startup Security Utility** from the menubar
5. Enter your password when prompted to authenticate
6. Choose "No Security" from the Secure Boot options
7. Choose "Allow booting from external or removable media" from the External Boot options
#### Start the Installation
1. Insert the USB drive
2. Restart your Mac and _immediately_ press and hold Option until you see a screen of boot devices
3. Select the orange EFI Boot device
4. Proceed with the [install as normal](02-getting-started.md)
The installer detects Mac hardware and applies the needed fixes automatically: Broadcom Wi-Fi drivers and firmware, the SPI keyboard driver on the MacBook models that need it, and an NVMe suspend fix for those same models.
### Known Limitations
Members of the community are constantly working on solutions to these challenges so if these are problematic for you, join #omarchy-on-other in our [Discord](https://discord.gg/tXFUdasqhY) and see if there's any up-to-date methods for resolving these.
#### Devices with T1 Chip
The Apple T1 chip was introduced in late 2016 and used exclusively in the first-generation MacBook Pro models with Touch Bar.
- MacBook Pro 13-inch (2016, two Thunderbolt 3 ports) – Model: A1706
- MacBook Pro 13-inch (2016, four Thunderbolt 3 ports) – Model: A1708
- MacBook Pro 15-inch (2016) – Model: A1707
#### Known Issues
- Touch Bar is non-functional
- Sound is not functioning
#### Devices with T2 Chip
The Apple T2 Security Chip was introduced in 2017. The T2 chip was discontinued with the transition to Apple silicon (M-series chips) starting in 2020.
- iMac Pro (2017) – Model: A1862
- MacBook Pro 13-inch (2018, four Thunderbolt 3 ports) – Model: A1989
- MacBook Pro 15-inch (2018) – Model: A1990
- MacBook Air (Retina, 13-inch, 2018) – Model: A1932
- Mac mini (2018) – Model: A1998
- MacBook Pro 13-inch (2019, two Thunderbolt 3 ports) – Model: A2159
- MacBook Pro 13-inch (2019, four Thunderbolt 3 ports) – Model: A2178
- MacBook Pro 15-inch (2019) – Model: A1990
- MacBook Pro 13-inch (2020, two Thunderbolt 3 ports) – Model: A2265
- MacBook Pro 15-inch (2020) – Model: A1990
On these models, the installer automatically sets up the patched `linux-t2` kernel, the T2 audio configuration, Apple's Broadcom Wi-Fi/Bluetooth firmware, and fan control via `t2fanrd`. The Touch Bar runs on the kernel's built-in Boot Camp-style support.
+49
View File
@@ -0,0 +1,49 @@
# Troubleshooting
### I broke my system with an update!
First try to [rollback your system](47-system-snapshots.md) the version before your recent update. If that doesn't work, use `omarchy-debug` to share with your problem on #omarchy-help in the Discord. And if all that fails, you can reinstall the defaults configs and packages using `omarchy-reinstall`.
### Why are some apps so large on my display?
Omarchy assumes a 2x high-resolution display, which requires setting `GDK_SCALE` to 2 in `~/.config/hypr/monitors.lua`. But if you're on a 1x display, you can change `local omarchy_gdk_scale = 2` to 1 (and then restart any app that's oversized). See [the manual on monitors](33-monitors.md).
For Spotify, you can use `Ctrl + Minus` to shrink the UI (and `Ctrl + Plus` to make it bigger).
### Why isn't Caps Lock working?
In Omarchy, Caps Lock has been designated to be the xcompose key. That's how you get [quick emojis](07-hotkeys.md#quick-emojis) and [other autocompletions](07-hotkeys.md#quick-completions) done. If you really miss using Caps Lock, you can remap the xcompose key to something else by editing `~/.config/hypr/input.lua`, like setting it to the right alt key:
```
hl.config({
input = {
kb_options = "compose:ralt",
},
})
```
### My Wi-Fi, Bluetooth, audio, or trackpad just stopped working
Before you reboot, try restarting the offending subsystem on its own. _Update > Hardware_ in the Omarchy menu has Wi-Fi, Bluetooth, Audio, and Trackpad, and reloading one of those clears up the majority of "it worked five minutes ago" situations — a Bluetooth headset that won't reconnect, a trackpad that went dead after a suspend, sound that vanished when you unplugged a monitor.
### Why are my external speakers not playing?
Probably because they're not set as the primary output. Click on the speaker icon on the right side of the bar, and it'll open the volume popup where you can pick the output device (and mix per-app volumes too).
### My laptop speakers sound off
On some laptops, Omarchy automatically applies a speaker tuning that corrects the built-in speakers' frequency response. `omarchy audio tuning status` tells you whether one is active on your machine, and `omarchy audio tuning off` turns it off if you'd rather hear the speakers raw.
### Why can't I login or sudo with my password?
You probably typed it wrong too many times and got locked out. If this is happening on the lock screen, you can hit `CTRL + ALT + F2` to start a new TTY where you can login as root, then run `faillock --reset --user [your-username]`. That'll reset the lockout, and you're good to go.
### Why isn't my 1Password authorization prompts for 1Password SSH Agent / CLI appearing?
This can happen for 2 reasons:
In order for the rich approval prompt to appear, Settings > Advanced > Use Hardware Acceleration must be turned on. _Note: This requires a reboot to begin working._
![troubleshooting-1password](images/troubleshooting-1password.webp)
Or if you haven't launched 1Password since booting up, the prompt will not appear.
+79
View File
@@ -0,0 +1,79 @@
# FAQ
### How do I switch between keyboard layouts?
Edit your `~/.config/hypr/input.lua` file and add this to switch between layouts on `Left Alt + Right Alt`:
```
hl.config({
input = {
-- Use multiple keyboard layouts and switch between them with Left Alt + Right Alt
kb_layout = "us,fr",
kb_options = "compose:caps,shift:both_capslock_cancel,grp:alts_toggle",
},
})
```
The bar will automatically show your current keyboard layout once you have multiple layouts configured (and you can click it to switch too).
### How do I change the clock format to 12-hour?
Right-click the clock in the bar to cycle through the common formats, including the 12-hour ones. You can also set the format directly:
```
omarchy bar set omarchy.clock format "dddd h:mm AP"
```
This will display Sunday 10:55 AM.
### How do I change my timezone?
Run _Update > Timezone_ in the Omarchy menu and pick from the list. If the timezone is right but the clock itself has drifted, _Update > Time_ restarts the time synchronization for you.
### How do I change my DNS, share my Wi-Fi, or check my connection speed?
Those all live in [networking](35-networking.md).
### How do I check how fast my disk is?
_Trigger > Speed Test > Disk Speed Test_ measures live read and write speed on your drive, or `omarchy disk speedtest` from the terminal.
### Why can't I sign into my Google account in Chromium?
The plain open source Chromium build doesn't ship with the OAuth credentials that Google requires for account sign-in. Run _Install > Service > Chromium Account_ in the Omarchy menu to add them, restart the browser, and the sign-in will go through.
### How do I add a printer?
Printing is set up and running out of the box, so a printer on your network is usually already discovered. Launch _Print Settings_ from the app launcher (`Super + Space`) to see what's there, add one by hand, or set the default. Printing to a PDF file works without any printer at all.
### How do I change where screenshots or screenrecordings are saved?
If you want screenshots to be saved to `~/Pictures/Screenshots` instead of just `~/Pictures`, you can add this to a file under `~/.config/uwsm/env.d/` (like `~/.config/uwsm/env.d/capture`):
```
export OMARCHY_SCREENSHOT_DIR="$HOME/Pictures/Screenshots"
```
You can do the same for screenrecordings using `OMARCHY_SCREENRECORD_DIR`.
Just remember to create the directoy you want to save to and restart Omarchy for this to take effect.
### How do I get the speakers + webcam working on my Apple Studio Display?
You'd think that it should all work just plugging in USB C, but unfortunately that isn't the case. The solution I've found to make it work reliably is using [the WJESOG DisplayPort + USB-A => USB-C cable](https://www.amazon.com/WJESOG-DisplayPort-Adapter-Converter-Thunderbolt/dp/B0BNX7MS6N/). Then speakers and webcam work like a charm.
Remember that you have built-in brightness control in Omarchy for the Apple Displays (both Studio and XDR) using the regular keyboard brightness buttons.
### How do I get rid of all the extra software?
If you don't want programs like Obsidian or LibreOffice or any of the other preinstalled stuff, you can very easily remove it.
Run _Remove > Package_ to see every package that's installed. Then you can select any package you'd like to remove with tab, and start removing everything you've selected with return.
And you can use _Remove > Web App_ from the Omarchy menu to remove any of the preinstalled web apps you don't want.
Or run _Remove > Preinstalls_ to sweep out all the preinstalled extras — web apps, TUIs, and optional applications — in one go. The hotkeys that launched them go away with them, so you're left with a clean slate of bindings to fill with your own in `~/.config/hypr/bindings.lua`.
---
For errors and broken bits, see [the Troubleshooting section](45-troubleshooting.md).
Loaded 100 of 231 files, more files were not shown because too many files have changed in this diff. Show more