Compare commits

..
Author SHA1 Message Date
ZacharyZhang-NY a9b39710a2 Add Simplified Chinese as a system language
The installer asks for the language before the keyboard, and
Setup > Language (omarchy language en|zh) switches it later. Under
zh_CN the menu reads its labels from omarchy-menu.zh-cn.jsonc, the
shell from I18nZh.js through I18n.tr, and notifications are worded
by their sender through omarchy-text, which reads the same table.
User text is never translated.
2026-10-12 02:56:40 -04:00
Ryan Hughes defbf90e89 Fully revert USB and Thunderbolt device approval (#15091)
* Revert "Merge pull request #11874 from acrogenesis/feature/usb-device-authorization"

This reverts commit 5cb31317b4, reversing
changes made to 9d48fc402e.

* Undo USB and Thunderbolt device approval on machines that ran it

* Verify boot rebuilds, recover Thunderbolt journals and prove USBGuard ownership in the removal migration

* Read the boot partition from ESP_PATH and stop on failed Bolt reads in the removal migration

* Recover Bolt's original mode from the setup journal in the removal migration
2026-10-12 01:20:01 -04:00
bjarneo 9480d1b6a9 Merge pull request #15065 from bjarneo/fix/bar-icon-consistency
Normalize bar icon size and alignment
2026-10-11 22:07:57 +02:00
Bjarne Oeverli 6575f06d53 Correct custom bar icon bounds 2026-10-11 22:02:37 +02:00
Bjarne Oeverli 201de1c0d6 Normalize bar icon size and alignment 2026-10-11 21:39:17 +02:00
David Heinemeier HanssonandClaude Opus 5.5 e71a816e90 Cache workspace layouts instead of running a script on every switch (#15028)
* Cache workspace layouts instead of running a script on every switch

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

* Test the workspace layout cache by running its logic, not matching its source

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

* Keep the fake layout read running while its exit handler runs

Addresses Codex review: clearing running first let an undeferred, broken retry pass the cache tests.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 23:03:30 +09:00
David Heinemeier HanssonandClaude Opus 5.5 1b1cc5ecb5 Track the power profile through Quickshell instead of polling busctl (#15027)
* Track the power profile through Quickshell instead of polling busctl

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

* Recover the power profile when the daemon was down at shell start

Addresses Codex review: Quickshell never attaches its PowerProfiles listener if the daemon failed at startup. One-off reads on startup, power source changes and an IPC refresh from omarchy-powerprofiles-set cover that, still with no timer.

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

* Don't let a stale power profile read win over a change signal or drop a refresh

Addresses Codex re-review: a busctl read in flight could overwrite a newer PowerProfiles signal, and refreshes requested while a read ran were dropped.

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

* Test power profile transitions and stale reads as behavior, not source text

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 23:01:49 +09:00
David Heinemeier HanssonandClaude Opus 5.5 3c1f7b780f Refresh the passwordless sudo indicator on change instead of polling (#15026)
* Refresh the passwordless sudo indicator on change instead of polling

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

* Refresh the sudo indicator after failed changes and on a wall-clock deadline

Addresses Codex review: a rolled-back renewal skipped the refresh, the monotonic user timer paused through suspend, and default timer accuracy allowed a minute's delay.

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

* Test the sudo indicator refreshes by running the grant flows

Replace source-string checks with runs of timed, renewed, failed, permanent and disable flows against the fixture, asserting the recorded omarchy-shell refresh and wall-clock systemd-run --user timer.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 23:01:45 +09:00
David Heinemeier HanssonandClaude Opus 5.5 8bd1b7c045 Tell agents to react to events instead of polling (#15029)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 22:42:26 +09:00
David Heinemeier Hansson c4803d7d74 Update comment 2026-10-11 22:33:14 +09:00
David Heinemeier HanssonandClaude Opus 5.5 5f25ef1da8 Let Neovim's current line and status line show the terminal background on existing installs (#15024)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 22:30:32 +09:00
David Heinemeier HanssonandClaude Opus 5.5 88de9ecc6c Let herdr's tab bar show the terminal background, so it turns glassy too (#15022)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 22:25:16 +09:00
David Heinemeier HanssonandClaude Opus 5.5 1ac31e55ae Never leave the desktop without a wallpaper after a theme intro (#15000)
A theme switch that plays an intro gave the shell the new still only when
the intro failed. The shell kept the old theme's path, which the theme swap
deletes, and loaded it again when OWE handed the desktop back, leaving it
empty. The new still is now handed over either way.

The shell also re-reads the background link whenever OWE hands the desktop
back or a wallpaper fails to load, falling back to the theme's first
background when the link itself is broken.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 21:42:00 +09:00
David Heinemeier HanssonandClaude Opus 5.5 754d2d04b3 Only recover a session lock no other locker may hold (#15020)
Hyprland lets any client take over a held lock, so a second shell started
beside the desktop judged the real shell's live lock stranded, took it, and
then exited, leaving the session behind the failsafe while the real shell
still reported itself secure. Recovery now waits while any other Quickshell
or hyprlock runs, and gives up if one never exits.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 21:26:49 +09:00
David Heinemeier HanssonandClaude Opus 5.5 a98e55eb91 Pick window transparency from Appearance > Window Transparency, and a Glassy bar background (#14991)
* Pick window transparency from Appearance > Window Transparency

Transparent, the default, keeps today's windows. Opaque turns the windows
Omarchy makes transparent solid. Glassy is the frosted glass Starship
(#14726): its blur behind every see-through window and shell surface, and a
see-through, blurred background in the theme's terminal files, since foot
turns compositor blur down and a Hyprland opacity rule fades text.

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

* Leave window transparency to Appearance when animations are off

Turning animations off no longer makes windows opaque. VM installs start
with opaque windows instead, and a migration keeps them opaque for anyone
who already has animations off.

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

* Add a Glassy bar background, under Menu Bar > Bar Background

Menu Bar > Bar Background picks Opaque Bar, Glassy Bar or Clear Bar, and
Indicators becomes Bar Indicators to match. Glassy keeps the theme
background faintly and Hyprland blurs what is behind it, with the same blur
as Glassy windows.

omarchy bar background opaque|glassy|none|cycle sets it and prints it when
given nothing. omarchy bar transparent true|false|toggle stays for
compatibility, as none and opaque. Double-clicking the bar cycles through
the three backgrounds the same way, from clear to glassy to opaque.

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

* Stop when a saved corner choice can't be removed

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

* Make btop follow glassy windows

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 21:21:00 +09:00
David Heinemeier HanssonandClaude Opus 5.5 be7c7bf378 Replace Xournal++ with Signed for signing PDFs (#14999)
Signed, Omacom's PDF signing app, takes Xournal++'s place in the base
install and the preinstall set, so Remove and Install Preinstalls cover
it too. A migration installs it on existing machines, unless their owner
has removed the preinstalls; Xournal++ stays on machines that already
have it. The Filling out PDFs chapter now walks through Signed, the GUIs
chapter introduces it, the setup tip points to it, and the unused
Xournal++ config is gone.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 10:57:16 +02:00
David Heinemeier HanssonandClaude Opus 5.5 4df545905d Pick window corners from Appearance > Window Corners (#14990)
* Expect Appearance before Toggle under Trigger

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

* Read the default workspace layout once for all the menu rows that ask

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

* Pick window corners from Appearance > Window Corners

Theme Corners leaves them to each theme. Sharp and Rounded override every
theme through a Hyprland toggle, with Rounded using Solitude's corners.

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

* Keep the corner choice when saving a new one fails, and test the load order

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 17:06:54 +09:00
David Heinemeier Hansson 3882c27cfd Better ordering 2026-10-11 16:11:18 +09:00
David Heinemeier HanssonandClaude Opus 5.5 5ec8bd300b Pick the bar clock format from Appearance > Menu Bar > Indicators > Clock
Every preset the clock cycles through gets its own row, labeled with how
it reads, and the current one is checked. omarchy-bar gains get <id> <key>
and a bare position to read the settings the rows compare against.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 16:10:33 +09:00
David Heinemeier HanssonandClaude Opus 5.5 eb1a34ecd0 Move Battery Percentage under Appearance > Menu Bar > Indicators
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 16:03:50 +09:00
David Heinemeier HanssonandClaude Opus 5.5 b6ceb8f4d6 Use the menu's animations icon in the toggle notifications
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 16:00:47 +09:00
David Heinemeier HanssonandClaude Opus 5.5 bef30c1c50 Move the Menu Bar from Style to Appearance, with Hide and Show (#14982)
* Match the menu test to the rearranged Trigger menu

Trigger now leads with Toggle and Appearance, and Appearance's entries,
now including Herdr Theme Sync, are in their new order.

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

* Move the Menu Bar from Style to Appearance, with Hide and Show

Trigger > Appearance > Menu Bar now holds the bar's Position and
Transparency from Style > Menu Bar, and offers Hide while the bar shows
and Show while it is hidden, instead of a bare toggle.

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

* Name the Menu Bar entries after the bar, and split transparency

Hide Bar, Show Bar, Position Bar, Top Bar and the rest read on their own
when searched. Transparency becomes Transparent Bar while the bar is
opaque and Opaque Bar while it is transparent.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 15:59:25 +09:00
David Heinemeier HanssonandClaude Opus 5.5 18e059d9a4 Show the remote session indicator only during a remote session (#14981)
Hovering the indicators revealed it dimmed alongside the ones that can be
switched on, though there is nothing to do with it there. An indicator can
now opt out of that reveal, and remote session does.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 15:58:19 +09:00
David Heinemeier Hansson cf473b5c5d Rearrange menu items a bit 2026-10-11 15:50:04 +09:00
David Heinemeier HanssonandClaude Opus 5.5 d1a614ce66 Decide lock requests from the current lock state (#14978)
The locked binding once read true after an unlock, though the request,
the session lock, and its secure state were all false. A lock before
suspend then took the session for locked and did nothing, and the
machine slept unlocked. Lock requests, the status reply, and stranded
lock recovery now read those three values directly.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 08:37:45 +02:00
1498718969 Add a floating mode with titlebars to the workspace layout cycle (#14482)
* Let the workspace layout test's Lua assertions fail the suite

Lua 5.5's standalone interpreter exits 0 when the script that raised the error came in on stdin, and exits 1 for the same error in a script file. The block here was piped in through a heredoc, so every assertion in it reported as passing however wrong the value was: changing the expected layout to one the toggle never writes still left the file green.

* Add a floating mode to the workspace layout cycle

Super + L cycled a workspace between dwindle and scrolling. It now carries on into floating, where every window on the workspace floats.

Floating cannot be a third `layout` on the workspace rule. Hyprland has no layout by that name and accepts one anyway: `hl.workspace_rule({ layout = "floating" })` returns ok and silently leaves the workspace on dwindle, exactly as a misspelt name does. So the mode floats the windows themselves, through the same `float` dispatcher SUPER + T uses.

Ending the mode has to tile the windows the mode floated and only those, so each one is tagged with `omarchy-mode-floated`. A window already floating when the mode reaches it -- an app with a float rule of its own, a dialog, one popped out with SUPER + T -- is left unclaimed and is still floating when the mode ends. The tag lives on the window rather than in a table here: it dies with the window, follows it between workspaces, and survives the config reload that plugging in a monitor performs, none of which a table on this side would do. A pinned window is left alone entirely, because tiling one unpins it as well.

Windows arrive on a workspace three ways and a window rule only covers one of them, so the mode watches events: window.open for what opens there, window.move_to_workspace for what is carried in, and the same event for what is carried back out, which gives up the floating the mode gave it rather than staying floating on a workspace that tiles. Every tiled window is claimed before any of them is floated, because floating one member of a group floats the rest, and claiming as it went would find those already floating and leave them stranded.

The mode is read back from the saved file rather than from the compositor. Hyprland reports a workspace's tiled layout and floating is not one, so a floating workspace still answers dwindle or scrolling; trusting that answer would skip a step of the cycle. A file saved before floating existed names a layout rather than a mode and falls through to what Hyprland reports, so the cycle keeps working and the next press rewrites it.

Two faults in the old script had to go first, because saving the mode makes both persistent. Named workspaces report a negative id, which passed the `^-?[0-9]+$` guard, and Hyprland reads a leading "-" as a selector relative to the current workspace: pressing Super + L on a named workspace reconfigured workspace 1 instead, and would now save a file that did it again at every login. And the saved state was written under $HOME/.local/state while the Hyprland side reads XDG_STATE_HOME, which also had to reach package.path: the loader lists that directory but loads the files by module name, so a state home anywhere else listed the saved layouts and then loaded same-named ones from the home directory.

Saving is checked too. The mode reaching the compositor and the mode reaching disk are separate things that can fail separately, and a save that failed silently would leave the next press reading a file that never got written, cycling between two modes and never reaching the third.

The guard against named workspaces stops new files being written, but an earlier version already wrote them, and they go on being loaded at every login. A migration removes the saved layouts named for a negative id.

The keyword fallback is gone. `hyprctl keyword` refuses a Lua configuration outright -- "keyword can't work with non-legacy parsers. Use eval." -- and exits 0 while doing so, so as a fallback it neither worked nor reported that it had not. A failed apply now says so and saves nothing, rather than announcing a layout the screen does not show and restoring it at the next login.

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

* Add titlebars, edge snapping, and a window Shelf to floating workspaces

Floating workspaces become a traditional desktop that works with every
theme. Windows get themed titlebars from the omarchy-hyprland-titlebars
plugin, which is loaded only while something floats. Dragging a titlebar
to the left or right edge snaps the window to that half, and dragging it
to the top or double-clicking maximizes it. The window floating mode
places is sized, centered, and cascaded, and floating workspaces focus
on click.

Super + M, or the minus button, sets a window aside on the Shelf, which
works from any workspace and restores the window onto the current one
in that workspace's layout. Float All Workspaces makes floating the
default while Super + L still sets a single workspace's layout.

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

* Bring resize on border to tiling too

* Fix floating mode cleanup, fullscreen windows, and failed Shelf notes

Turning Float All Workspaces off with no workspace layouts left behind
started nothing to give the floated windows back. A window fullscreen
when its workspace began floating now floats once it leaves fullscreen.
A window whose Shelf note cannot be saved stays where it is, and the
titlebars read the theme from where omarchy-theme-set writes it.

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

* Place the Shelf on the bar once, instead of on every minimize

Putting it back on each minimize kept a Shelf the user moved where they
put it, but re-added one they had removed. A migration places it once for
existing bars; Super + Alt + M opens the Shelf either way.

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

* Raise titlebars off the window background

A titlebar in the theme's background color ran into the window under it.
It takes the theme's lighter background, the raised surface every theme
defines, and the selection color on themes that make the two the same.

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

* Drop minimizing and edge snapping from floating workspaces

A floating workspace is now titlebars with maximize and close, and nothing
else. The Shelf (its bar widget, panel, commands, notes, Super + M and
Super + Alt + M) goes with the minimize button, and the titlebar plugin's
edge snapping is left off, along with the shell's snap preview.

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

* Stop describing the titlebar package as snapping

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

* Name web app windows in their launchers

Chromium names a web app window after its URL, and titles it with the page
title or just the domain. StartupWMClass ties the window to its launcher,
so the titlebar plugin can show the launcher's Name instead: WhatsApp
rather than web.whatsapp.com. New web apps, the shipped ones, and existing
launchers (through a migration) all carry it.

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

* Drop the titlebar package comment from the base list

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

* Show the workspace layout in the bar and cycle it on click

The focused workspace's mark is a square for dwindle, a triangle for
scrolling, and a circle for floating, and clicking it cycles the layout
as Super + L does. Both layout toggles ask the bar to read the new mode.

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

* Show scrolling as a circle and floating as a triangle

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

* Move the look of the desktop to Trigger > Appearance

Menu Bar, Battery Percentage, Workspace Layout, Window Gaps, Animations,
and 1-Window Ratio change how the desktop looks rather than switching a
behavior, so they leave Trigger > Toggle for their own menu.

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

* Choose a default workspace layout instead of floating everything

Trigger > Appearance > Workspace Layout sets the layout of every workspace
without one of its own: Tiling, Scrolling, or Floating. A tiled default
is Hyprland's own layout, and a floating one is the floating mode. It
replaces Float All Workspaces, which also overrode and set aside the
layouts workspaces had been given with Super + L.

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

* Title the workspace layout menu Default Workspace Layout

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

* Use the animation icon for Appearance > Animations

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

* Open clipboard history from Trigger

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

* Leave edge resizing to looknfeel on floating workspaces

looknfeel now resizes from borders everywhere, so floating workspaces
only need to switch pointer focus to click.

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

* Convert saved workspace layout rules to modes

Released versions saved a workspace's layout as a plain workspace rule,
which names no mode, so a floating default would override a workspace
set to scrolling, and the bar and Super + L would misread it. A migration
rewrites those files as modes.

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

* Cycle workspace layouts backwards with Super + Alt + L

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

* Test the Trigger > Appearance split and Clipboard History placement

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

* Decode escaped web app URLs before naming their windows

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

* Describe the workspace widget's layout mark and click

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

* Let a workspace cycled onto the default follow it again

Once Super + L gave a workspace a layout of its own, it kept it for good,
so a later change of the default never reached it. Cycling onto the
default layout now forgets the workspace's own instead of saving it.
Without a saved default, the default is Hyprland's own layout, which
the user's looknfeel may have set.

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

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Codex XHigh <noreply@openai.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 08:20:45 +02:00
David Heinemeier HanssonandClaude Opus 5.5 c09ddcff90 Move the look of the desktop to Trigger > Appearance (#14973)
* Move the look of the desktop to Trigger > Appearance

Menu Bar, Battery Percentage, Workspace Layout, Window Gaps, Animations,
and 1-Window Ratio change how the desktop looks rather than switching a
behavior, so they leave Trigger > Toggle for their own menu.

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

* Use the animation icon for Appearance > Animations

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

* Open clipboard history from Trigger

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

* Test the Trigger > Appearance split and Clipboard History placement

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 08:17:33 +02:00
David Heinemeier HanssonandClaude Opus 5.5 dc50bbe574 Switch out of an input method and back with a lone Shift (#14975)
* Switch out of an input method and back with a lone Shift

Fcitx's alternate trigger only acts while an engine like Pinyin is or was
active, so Shift + letter and plain Latin typing are unaffected. It needs
Shift releases intact, which the both-Shifts Caps Lock option prevented.

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

* Follow XDG_CONFIG_HOME in the Shift migration and test it

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 15:15:03 +09:00
35a55fd236 Remove redundant agent guidance in user customization skill (#5925)
* Remove skill body trigger conditions; bolster frontmatter content.

* Keep window behavior and screen recording in the skill's triggers

The removed "When This Skill MUST Be Used" section listed both, and the description that replaces it had dropped them, so a request to record the screen or change how windows behave no longer matched anything the agent reads before choosing the skill.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Codex Medium <noreply@openai.com>

---------

Co-authored-by: omarchybot <omarchybot@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Codex Medium <noreply@openai.com>
Co-authored-by: Omarchybot <317366263+omarchybot@users.noreply.github.com>
2026-10-11 07:45:22 +02:00
David Heinemeier HanssonandClaude Opus 5.5 10f1cbf7cf Install clang with the Rust dev environment instead of for everyone (#14967)
Nothing in the base install builds with clang: Qt apps and Ruby's native
gems compile with gcc, and Ruby comes prebuilt from mise. Rust crates that
use bindgen, including rb-sys gems, need libclang, so the Rust dev
environment installs it. llvm was unused even there.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 13:37:23 +09:00
d74d6ea249 Build the calendar month grid in UTC (#10476)
* Build the calendar month grid in UTC

The grid walked a cursor at local midnight and stepped it with setDate(getDate() + 1). Chile starts DST at 24:00 on the first Saturday of September, so local midnight of 6 September 2026 does not exist, and Qt's V4 engine resolves a missing local time backward -- onto 5 September 23:00, the same calendar date, which the next iteration reads again. September 2026 draws 30 31 1 2 3 4 5 and then 5 6 7 8 9 10 11, and every later date sits one weekday early. Cuba, Lebanon, the Azores and Egypt change over at 00:00 too and break the same way in their own transition month; where the gap lands on a leading padding cell instead, the whole six-week grid shifts by a day with no duplicate in it.

Node resolves the identical gap forward, so the suite has never seen this: running the shipped monthGrid under Node across all 598 tzdata zones for 2020-2035 reports nothing, while the same code under the QML engine breaks 402 grids. A sequence of civil dates never needed a wall clock, so it walks UTC, where every day is 24 hours long, and the test pins that by building the grid with a Date whose local constructor and accessors throw.

stepMonth reads a month back off the same kind of date. No zone puts a midnight gap on a first of the month, so it is not broken today, but it moves with the grid rather than waiting for a tzdata release to make it a second bug.

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

* Name the calendar heading's month from local noon

The heading built a local midnight on the first of the month, and that midnight does not always exist: Cuba started DST at 00:00 on 1 April 2012, so in Havana the QML engine resolves it back onto 31 March and the heading reads March 2012 above an April grid. Noon exists on every first of the month there has been, and keeping it local is what makes Qt.formatDate print the right month in every zone.

The Apia case comes from #13186: Samoa skipped 30 December 2011 entirely, so unlike a one-hour midnight gap, Node's V8 cannot resolve its way past it, and the test fails on the old grid under Node instead of only in the shell.

Co-Authored-By: Codex Medium <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Codex XHigh <noreply@openai.com>
Co-authored-by: Omarchybot <317366263+omarchybot@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 06:14:23 +02:00
David Heinemeier HanssonandClaude Opus 5.5 3609968a42 Stop shipping OBS Studio and Kdenlive to new installs (#14962)
Monologue records and Omacut trims, which covers what most people used
them for. Existing installs keep both; there is no migration, and the
preinstall removal no longer treats them as Omarchy's to uninstall.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 12:57:28 +09:00
David Heinemeier Hansson c1096aa36f Refresh all default theme previews (#14944) 2026-10-11 12:50:06 +09:00
ccf263611d Correct omarchy-pkg-add AUR claim in AGENTS.md (#13895)
* Correct omarchy-pkg-add AUR claim in AGENTS.md

* Say omarchy-pkg-add installs from the pacman repos, not the official ones

It runs plain pacman -S, which also reaches the [omarchy] repo every shipped pacman.conf enables, so "official repos" would send an agent to the AUR helper for Omarchy's own packages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Codex Medium <noreply@openai.com>

---------

Co-authored-by: Omarchybot <317366263+omarchybot@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Codex Medium <noreply@openai.com>
2026-10-11 05:33:32 +02:00
stephenlentonandClaude Sonnet 5 4d3a85969a Clarify that omarchy-toggle-bar's on/off tracks the bar-off flag (#11803)
on hides the bar and off shows it, matching the literal flag name
rather than an inferred "bar visibility on" reading. The behavior is
correct and depended on by omarchy-toggle-fullscreen-desktop; this
just makes the --help summary say so.


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

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-11 05:32:58 +02:00
a3d4bea9d0 docs(agents): add plugin UI safety baseline (#7792)
* docs(agents): add plugin UI safety baseline

Document safe rendering and validation of dynamic plugin data after a marketplace review surfaced a QML AutoText boundary.

Assisted-by: Codex/GPT-5

* docs(agents): allow consented background refreshes

Clarify that plugin networking may refresh periodically when disclosed and user-controlled.

Assisted-by: Codex/GPT-5

* docs(agents): keep plugin guide label concise

Leave the safety baseline discoverable within the plugin guide without expanding its topic index label.

Assisted-by: Codex/GPT-5

* docs(agents): point plugin authors at their own markup helper

Omarchy ships no markup-neutralizing helper, and this skill is for installed systems, where the parent SKILL.md forbids editing the packaged shell components the old line suggested extending.

Co-Authored-By: Codex Medium <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Omarchybot <317366263+omarchybot@users.noreply.github.com>
Co-authored-by: Codex Medium <noreply@openai.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 05:17:02 +02:00
kevin koontz 9d47dd1497 Point the plugin manual at plugins.omarchy.org (#10499)
The marketplace moved to the official host on 30 Aug 2026. omarchyplugins.com redirects, but the published manual should name the current URL.
2026-10-11 05:00:53 +02:00
Vtorrealba 77b13fd026 Fix typo on 04_navigation.md (#7463) 2026-10-11 04:59:07 +02:00
Ed Schofield 9476e47a10 docs: add 2020 iMac models to the T2 Mac list (#10783) 2026-10-11 04:57:41 +02:00
Spencer Bull 9830142834 Merge pull request #14935 from omacom/startup-cursor-recovery
Recover the startup cursor across Hyprland reloads
2026-10-10 20:59:21 -05:00
Spencer BullandCodex XHigh 0bcb6a2559 Keep the default cursor alias on ordinary boots
Omarchy never sets XCURSOR_THEME, so asking GSettings on every cold boot replaced the "default" alias with whatever GSettings held, which is the schema default Adwaita when nothing was ever set. That overrode a theme chosen through ~/.icons/default and exported the substitute to every application. The saved theme and GSettings are now consulted only when recovering from the blank startup theme or an interrupted reveal.

Co-Authored-By: Codex XHigh <codex@openai.com>
2026-10-10 20:55:29 -05:00
David Heinemeier Hansson 04b374a989 Add Starship theme preview (#14942) 2026-10-11 09:15:47 +09:00
David Heinemeier HanssonandClaude Opus 5.5 1b5222be74 Return Caps Lock to normal and make Shift + Caps Lock the compose key (#14939)
* Return Caps Lock to normal and make Shift + Caps Lock the compose key

Caps Lock as compose, with both Shifts as Caps Lock, confused too many
people. Stock XKB has no option for compose on the shifted Caps key, so
Omarchy defines one in /etc/xkb, which libxkbcommon reads before the
system rules. The explicit actions keep the Shift level from toggling
Caps Lock. Lone Shift is no longer part of Caps Lock, which leaves it
free for input methods.

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

* Require xkbcli so the keymap check cannot silently skip

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 09:06:59 +09:00
David Heinemeier HanssonandClaude Opus 5.5 0b9dace018 Unify input methods with installer keyboard selection (#14433)
* Unify input methods with the installer keyboard selection

* Preserve inherited input keys and later keyboard changes

* Exercise input engines across desktop toolkits

* Show input modes in the keyboard indicator

* Keep Pinyin setup prompts visible

* Remember input selections before an app gains focus

* Remove the Japanese input welcome notification

* Show explicit choices on notification cards

* Suppress the Cloudpinyin first-use prompt

* Show the input switching shortcut in keyboard hints

* Verify input setup and composition stay quiet

* Use Super+I for input switching independently of Fcitx keys

* Restore linked input settings and attribute test notifications to Fcitx

* Correct the input setup shortcut hint

* Keep input tooltips to the name and switching shortcut

* Place the input language selector with the right bar widgets

* Show input switching only for multiple layouts or methods

* Unify input method and keyboard layout switching

* Select keyboard layouts and input methods in the Typing menu

* Apply typing selections immediately from the menu

* Use installer keyboard choices and keep typing selections accurate

* Queue typing changes and wait for saved selections

* Keep queued typing selections when reopening the menu

* Cycle keyboard layouts alongside composition engines

* Avoid shadowing Python typing and retain pending mixed input selections

* Keep input clicks working through controller and compositor failures

* Move the keyboard layout widget to the right side of existing bars

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

* Map keyboard choices to the layouts the installer sets

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

* Keep the switching order when changing input methods

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

* Set input method fonts only after the selection saves

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

* Restore confirmed picker checks when an immediate change fails

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

* Route Super+I through the shell's input reader so unfocused choices stick

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

* Restore the input group when the font preference cannot be written

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

* Drop the azerty conversion now that the installer no longer offers it

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

* Apply the owner's keyboard layout once before persisting the input choice

A quattro merge kept a second apply_keyboard after the retry loop, which
re-ran loadkeys and firstboot, ignored their failure, and overwrote the
XKB layout setup supplies for keymaps kbd-model-map lacks.

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

* Give the Lao keyboard choice a Lao desktop layout

The row used la-latin1, kbd's Latin American Spanish keymap, so Lao
installs typed Spanish. There is no Lao console keymap; like Korean, the
console stays US and the desktop gets la, which Omarchy keeps behind US
as a non-Latin layout.

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

* Load engines installed after Fcitx started when they are selected

The Typing menu refused an engine whose package an update had installed,
since Fcitx discovers engines only at startup. Restart input once and wait
for the engine; name the package when it really is not installed.

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

* Cycle input methods back with Super+Shift+I

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

* Drop notification action buttons now that the Pinyin prompt is suppressed

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

* Cycle input only through the shell's reader

Without the shell there is no bar or menu either, so the one-shot cycling
path only duplicated the reader's logic.

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

* Add engines from the command line through the Typing menu's path

omarchy setup input <engine> had its own restart, append, select and
live-rollback code in configure.py, parallel to the menu's. It now
installs the packages and appends through typing_setup, leaving
configure.py with install-time seeding and update defaults.

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

* Keep Ctrl+Space out of Fcitx's toggles instead of probing for it

The terminal prefix bind read the Fcitx config and profile on every press
to avoid fighting a Ctrl+Space toggle. configure --defaults now removes
that key from every trigger list, so the bind only checks for a terminal.

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

* Run the input-method scripts with the system Python

A bare python resolves through PATH, so a mise, pyenv or venv interpreter
could run them without the system's PyGObject.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 08:16:30 +09:00
Spencer BullandGPT-6.1-Sol XHigh f5170dd070 Recover the startup cursor across Hyprland reloads
Hyprland discards Lua globals and timers during reload, so the pending startup reveal must survive outside Lua state. Preserve the normal cursor theme for this compositor session, recover orphaned blank settings, and repair activation environments before future applications inherit them.

🤖 Generated by GPT-6.1-Sol High in Codex through T3 Code. Reviewed by GPT-6.1-Sol XHigh in Codex through T3 Code.

Co-Authored-By: GPT-6.1-Sol XHigh <codex@openai.com>
2026-10-10 17:17:28 -05:00
David Heinemeier HanssonandClaude Opus 5.5 f2a39c4dca Drop the Azerbaijani keyboard choice, which installed French AZERTY (#14877)
kbd's azerty keymap is French AZERTY and there is no Azerbaijani console
keymap. XKB's az moves letter keys, so pairing it with a US console would
type passwords differently at disk unlock than on the desktop.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 07:06:54 +09:00
Marcelo Alcantara 6fee7105af Re-check Bolt less often while there is no Thunderbolt controller (#14931)
Both Thunderbolt loops reconciled or rescanned every 2 s even on machines with
no controller: on an idle M2 Max that was about 1,590 process launches a
minute. While nothing is connected or boot-protected and no controller is
present, the daemon now reconciles every 10 s, inside a snapshot's 20 s
lifetime, and returns to every tick as soon as a controller, a policy change
or a boot recovery checkpoint appears. The watcher skips an unchanged, fresh,
device-free snapshot, and both loops wait without forking sleep.
2026-10-10 18:03:22 -04:00
David Heinemeier Hansson 77bfaca408 Place Elsewhen after the clock by default (#14870) 2026-10-10 15:50:15 +02:00
outfoxxed 077ac1da93 Merge pull request #14845 from omacom/fix-qt612-color
Patch all QtQuick imports to 6.11 to work around plugin Color bug
2026-10-10 03:33:00 -07:00
outfoxxed 51717f10ba Patch all QtQuick imports to 6.11 to work around plugin Color bug 2026-10-10 03:08:55 -07:00
Emir Beganović a15636b59e Merge pull request #14799 from omacom/fix/bash-startup-check-in-chroot
Find the caller of the Bash startup check the way /proc numbers it
2026-10-10 07:51:58 +02:00
Emir Beganovic 0801356155 Test the startup check from a subshell, with a decoy -p, and rejecting in a PID namespace
The rejection inside the new PID namespace ran without privileged mode, so
it never reached the caller lookup, and nothing would have caught a check
that accepted -p anywhere in the arguments.
2026-10-10 02:52:51 +02:00
Emir Beganovic dd05114e95 Find the caller of the Bash startup check the way /proc numbers it
The check read /proc/$$/cmdline. arch-chroot runs its command in a new PID
namespace beneath the outer /proc, where $$ names no process or someone
else's, so every bash -p entrypoint was rejected there with status 126.

The installer met this as soon as one was called at install time:
install/config/thunderbolt-authorization.sh runs
omarchy-thunderbolt-authorization-admin, and the edge ISO stopped in
"Configuring system" with "/proc/219/cmdline: No such file or directory".

The check runs in a child of the caller, so read the parent from that
child's /proc/self/stat, which is given in the numbering of the /proc it is
read through. Outside a PID namespace that is $$, as before.
2026-10-10 02:48:26 +02:00
Emir Beganović 7cc6a55f24 Merge pull request #14796 from omacom/fix/user-units-not-shipped
Enable first-run units one at a time, and test that units are shipped
2026-10-10 02:43:32 +02:00
Emir Beganovic 5489418c9d Require the unit copy to come before the enable, and compare first-run to a clean run
Naming a unit's source in default/ counted as installing it, so a command
that never copied the unit, or copied it after enabling it, passed. Look
for the install or cp of that source ahead of the enable instead.

The first-run test accepted any script that tried three of the units. It
now requires one enable --now per unit and, with each unit unknown in
turn, exactly the calls of a clean run minus that unit.
2026-10-10 02:27:56 +02:00
Emir Beganovic 55fc353e47 Enable first-run units one at a time, and test that units are shipped
The Thunderbolt approval watcher is enabled by its setup command, a
migration and first-run, but no package put it in a user unit folder and
nothing installed it. On edge the migration failed with "Unit
omarchy-thunderbolt-authorization.service does not exist", so every later
migration stayed pending. First-run failed too, and enabled none of its
units: given a list, systemctl enables nothing when one unit is unknown.

The units are now shipped by the settings packages (omarchy-pkgs#922).
Here, first-run enables its units one at a time, so a unit missing from a
build costs only that unit, and config-test fails when a unit under
default/systemd/user is enabled without being shipped or installed first.
2026-10-10 02:20:08 +02:00
+12 e1b0e5e9bb Converge omarchy-mac and omarchy-mx-mac into upstream Omarchy (#14431)
* Add omarchy-hw-platform to tell Apple Silicon and Qualcomm machines apart

aarch64 covers Apple Silicon Macs, Snapdragon laptops, Raspberry Pis and
VMs, so the CPU architecture can't pick platform setup. omarchy-hw-platform
prints apple-silicon, qualcomm, generic-aarch64 or generic from the vendor
prefixes in the device tree's root compatible (/proc/device-tree, then
/sys/firmware/devicetree/base) and the CPU, and fails when they contradict
each other. Its qualcomm answer uses the same evidence as the dragon
branch's omarchy-hw-qualcomm-soc, and the fixtures check the two agree.
omarchy-hw-apple-silicon is the Apple predicate on top of it.

An image built away from its hardware names its target in a root-owned
manifest, /var/lib/omarchy/image/target (format=1, platform=). While the
root is being built rather than booted, the detector answers from it and
never reads the build host's device tree; a booted system always answers
from its hardware. As root it restarts in an empty environment, so no
variable picks the platform for privileged setup.

Co-authored-by: Birk Skyum <74932975+birkskyum@users.noreply.github.com>
Co-authored-by: Jimmy Van Veen <8206925+JimmayVV@users.noreply.github.com>

* Reject platform gates keyed only on the CPU architecture

An architecture check is right for an ABI, a binary or a repository's
$arch, not for picking a platform. The lint lists each shipped check after
review and fails on a new one, so platform setup goes through
omarchy-hw-platform.

* Compose default packages from base, architecture and platform lists

omarchy-reinstall-pkgs installed the base list on every machine. A
platform's default set is now the base list, then install/omarchy-aarch64.packages
on aarch64, then the platform's own list when it has one
(install/omarchy-qualcomm.packages adds linux-firmware-qcom).
omarchy-pkg-defaults prints the set for the detected platform, or for the
one named, and reinstall installs it. x86_64 still gets exactly the base
list, and an image build gets its target's set.

* Early-load thunderbolt only where the kernel builds the module

linux-aarch64 has no thunderbolt module, and mkinitcpio fails the whole
image over a MODULES entry it cannot find. The optional form, thunderbolt?,
is not enough: a systemd initramfs copies MODULES as written into
modules-load.d, where "thunderbolt?" names no module, and a kernel without
the module would still list it there. thunderbolt_module.conf now adds plain
thunderbolt only when modinfo finds it as a module for the kernel being
built.

Co-authored-by: Scott Jones <444693+scottjones@users.noreply.github.com>

* Compose mkinitcpio HOOKS from an early baseline drop-in

omarchy_hooks.conf assigned HOOKS outright, and mkinitcpio sources drop-ins in
name order, so it ran after any numbered drop-in (90-*.conf) and threw its
hooks away. HOOKS now comes from 00-omarchy-hooks.conf, which sorts first, and
later drop-ins add to it. The NVIDIA kms filter and the vconsole handling stay
in omarchy_hooks.conf. With the drop-ins Omarchy writes, machines keep the
same HOOKS, MODULES and FILES; a user drop-in that sets HOOKS and sorts before
omarchy_hooks.conf now takes effect instead of being overwritten.

Migration 1786605598 now sources the baseline first, since without it the
migration would read empty HOOKS and also rebuild hybrid systems. It skips a
machine that lacks the baseline.

* Resolve the LUKS parent from raw lsblk output

lsblk -s prints the ancestor tree with box-drawing prefixes ("└─/dev/nvme0n1p6"), so the crypto_LUKS parent came back as a path that does not exist and first-boot setup stopped with "Could not locate the LUKS device to re-key". -r prints raw names. Factory reset uses the same lookup.

* Check the factory-reset passphrase against keyslots only

cryptsetup open tries enrolled tokens before the key file, so a TPM2 or
FIDO2 token could authorize the re-key with a wrong passphrase.

* Test LUKS parent lookup during owner setup and factory reset

Exercise raw ancestry output without invoking provisioning or reset. Cover btrfs subvolume roots, absent encryption, failed probes, and explicit cryptdevice specifications.

* Cover LUKS parent lookup through an LVM volume group

* Journal the first-boot LUKS re-key so an interrupted attempt resumes

A first boot killed after the staged install key's slot was retired left the
key file behind, and every retry then stopped at "the staged LUKS key no
longer unlocks". A lost key file with the boot-time auto-unlock still
configured let setup finish with the disk unlocking unattended.

The re-key now runs from install/provisioning/luks-rekey.sh and records its
phase and slot numbers (never key material) in
/var/lib/omarchy/provisioning/luks-rekey.state. It reuses the owner's slot
instead of adding a duplicate, retires LUKS1 slots too, and destroys the
staged key only after the volume proves it opens nothing. Setup refuses to
finish while any part of the provisioning unlock remains. The journal ends at
phase=done and leaves together with pending, so a retry after the staged key
is retired must use the password the disk already holds.

Key checks exclude LUKS2 tokens: cryptsetup open tries an enrolled token
(say a TPM2 slot a previous owner left behind) before the key and reports its
slot whatever key it was given. Slot listings read only the keyslot section,
since a luks2-keyring token looks like a keyslot in luksDump.

* Change the root volume's LUKS key before the login and root passwords

Installation gives the login user, root and the disk holding / one password,
but omarchy-drive-password changed only the LUKS key, so the three drifted
apart. For the disk holding / it now checks the current password opens the
disk, changes the LUKS key, confirms the new key opens the disk and the old
one no longer does, and only then sets the login and root passwords. Any other
encrypted drive changes its LUKS key alone. Every drive now asks for the
current password with gum instead of cryptsetup's prompt.

A journal in ~/.local/state/omarchy/drive-password.state (disk UUID, key slots
and phase, never a password, written with the re-key journal helpers) lets the
next run finish an interrupted change. The rerun accepts only the old key
(removing the slot the interrupted change added) or the new one (retiring the
old slot), so another key such as a recovery passphrase never becomes the login
password. luksChangeKey is judged by whether the new key opens the disk, not
by its exit status, and key checks exclude LUKS2 tokens, which cryptsetup
open would otherwise accept in place of any password.

Runs are serialised, cryptsetup calls hold a disk lock, a / whose LUKS
partition cannot be resolved fails closed, and passwords are never traced. A
new password equal to the current one is refused, and --pbkdf argon2id is
passed only to LUKS2, so LUKS1 drives can change their password again.

* Add omarchy-lifecycle-dispatch for platform boot operations

Provisioning, factory reset and update call a fixed set of operations
through it. A platform whose boot chain the generic Limine path can't drive
registers a directory of root-owned entrypoints in the dispatcher; every
other platform gets a no-op and keeps its generic path. Apple Silicon is
registered to omarchy-mac-boot's /usr/lib/omarchy/mac-boot, with its
required operations failing clearly when the package or an entrypoint is
missing. docs/lifecycle-dispatch.md is the contract, including how a
Qualcomm boot package would plug in.

* Let the platform's boot package own first-boot setup's boot chain and key slots

Owner provisioning asks provision-prepare before the owner form, hands the
staged unlock to provision-commit and provision-verify where the platform
implements both, and records the kept LUKS slot through luks-slots before
the staged key is destroyed; the disk password change records the owner's
new slot the same way. On x86, Snapdragon and generic aarch64 every call is
a no-op and first boot is unchanged.

* Reset through the platform's boot package where it owns the boot chain

Where a platform implements the reset operations, reset-prepare and
reset-verify stage and prove the factory root's boot files, the throwaway
slot is added only after that, and reset-commit gets the key on standard
input once the factory root is active. A failure before the switch rolls the
boot state back and revokes the slot. Elsewhere the Limine UKI path is
unchanged.

* Check the platform's boot chain after an update

omarchy-update-boot runs update-verify after AUR packages, the last
sudo-capable step, so it also covers the initramfs rebuilds they trigger.
It follows third-party build code, so like AUR it authenticates through the
no-update wrapper. A failed verification finishes the update's remaining
steps, then exits non-zero without offering the reboot. Where the platform
does not implement it nothing runs and nothing asks for root.

* Hand a machine to its platform's migration through lifecycle dispatch

migrations/1790347292.sh resolves the new optional migrate operation and,
where the platform's boot package implements it, runs it as root. The
entrypoint moves the machine onto the platform's official package set,
idempotently and resumably; its failure leaves the migration pending. Once
it succeeds, a root-owned marker spares other accounts the migration and its
root prompt. Everywhere else, Macs without the boot package included, the
migration is a no-op, and an undetermined platform keeps it pending.

* Refuse packages tagged for another platform in a pacman hook

A platform package names its platform with groups=('omarchy-platform-<name>'),
<name> being what omarchy-hw-platform prints. omarchy-settings ships a
pre-transaction hook, the guard it runs and a copy of the detector, so the
guard is resident before the runtime and any platform package. The guard
reads tags from the synced databases and refuses a transaction that installs
a package tagged for another platform; a transaction without a tagged
package passes without asking which machine this is. Image builds answer
from the image's root-owned target manifest, as the detector does.

The guard ships on aarch64 only: no platform package is built for x86_64,
so x86 transactions run no hook. Hardware setup starts by proving the guard
is resident and checking the packages an installer placed without it. On
generic aarch64, where no platform packages exist, a missing guard is
logged, not fatal, and on x86 it is expected.
docs/platform-guard.md is the contract.

Co-authored-by: Birk Skyum <74932975+birkskyum@users.noreply.github.com>
Co-authored-by: Jimmy Van Veen <8206925+JimmayVV@users.noreply.github.com>

* Enable Voxtype's Vulkan backend only when it is installed

omarchy-hw-vulkan reports a Vulkan runtime, not whether this voxtype build
has a Vulkan backend. The aarch64 voxtype-bin package ships only the CPU and
ONNX binaries, so on any aarch64 machine with a Vulkan driver setup ran
`voxtype setup gpu --enable` and printed "Vulkan backend not installed" in
the middle of an otherwise working install. Require the backend binary too.

x86_64 voxtype-bin always ships /usr/lib/voxtype/voxtype-vulkan, so x86
installs still switch to the GPU backend as before.

Ported from omacom/omarchy-mac#483.

Co-authored-by: Oliver Lukschander <33756270+oliverlukschander@users.noreply.github.com>

* Link the system Widevine CDM into Brave

The widevine package registers its CDM for Chromium and Firefox only.
Brave looks for the CDM in its own install directory, finds nothing, and
DRM sites (Netflix, Apple TV+, Spotify) fail with a blank player and no
useful error. This hits machines where Widevine comes from that package,
such as aarch64 ones, since no Widevine component is published for them.

Link /opt/WidevineCdm/chromium into /opt/brave-bin and /opt/brave-origin-bin
through as_root when Brave or Brave Origin is installed, and add a migration
that repairs existing installs. Both do nothing when the widevine package is
absent or the browser already has a CDM. A standard x86 install has no
widevine package, so nothing changes there.

Ported from omacom/omarchy-mac#474, extended to Brave Origin.

Co-authored-by: Caleb Marshall <68087341+cajomar@users.noreply.github.com>

* Report failed setup steps on the console

With file logging, run_logged sends a failing leaf's output and its
"Failed:" marker to the install log only, so the caller sees setup stop
with an exit status and nothing else. A missing command (exit 127) was the
case that made this hard to diagnose.

Print the failed script, its exit status and the log path on stderr. Stdout
logging mode already shows the failure marker and is unchanged, and
successful steps stay quiet.

Ported from omacom/omarchy-mac#389 (the logging part).

Co-authored-by: Scott Jones <444693+scottjones@users.noreply.github.com>

* Let a migration defer itself without stopping the update

A migration that exits 75 changed nothing and cannot finish yet: omarchy-migrate
leaves it pending with its login notification, runs the migrations after it and
exits 0, so omarchy update carries on. Any other failure still stops the queue.
The platform migration passes a deferred refusal of its entrypoint through, and
defers itself where the dispatcher can't resolve its entrypoint (the platform
can't be told, or the entrypoint isn't trusted): nothing has run then, so the
update goes on to its boot verification, which fails there, as documented.

* Keep Apple Silicon's systemd initramfs on the HOOKS baseline

00-omarchy-hooks.conf set the busybox line on every machine. On a Mac,
omarchy-mac-boot's drop-ins then add the Asahi firmware hooks, but its
encryption drop-in reads busybox encrypt as a Mac unlocked through
cryptdevice= and leaves the line alone. The image came out as
"... block encrypt asahi omarchy-vendorfw filesystems ...", with no
systemd, sd-encrypt or omarchy-mac-encrypt, so an encrypted Mac could not
unlock its disk. Reproduced against an M1 Pro's installed drop-ins.

The baseline now asks omarchy-hw-platform. Apple Silicon starts from the
systemd line its boot package builds on. A Mac whose own mkinitcpio.conf
carries the asahi hook, or busybox encrypt, keeps that line, as a legacy
install needs. x86, Snapdragon and generic aarch64 keep today's busybox
line, as does a machine with no detector. The detector is the runtime's,
or else the copy omarchy-settings ships for its platform guard, so a
settings package newer than the runtime still places a Mac. A detector
that cannot place the machine stops the build.

The HOOKS test now composes the baseline on each platform and on legacy
Mac lines. A new test composes it with omarchy-mac-boot 20260926-1.45's
drop-ins, kept as fixtures, and checks the exact HOOKS the M1 Pro builds.
The NVIDIA test pins its platform.

* Hide optional installs that have no package on this machine

Not every optional install exists on every architecture: some vendors ship no
aarch64 build, and some repositories don't carry a package at all. Each
Install row now carries a `when:` guard naming every package its install
command adds, checked with omarchy-pkg-available, so a row with nothing to
install disappears instead of failing halfway through a pacman transaction.
The rows that build from the AUR (Chrome, Edge, Brave, Brave Origin, Zen,
NordVPN) are guarded on the CPU architectures their vendors publish for.

omarchy-pkg-available asks the sync databases: one `pacman -Slq` per batch,
and `pacman -Sp` for a name that list doesn't hold, so a provided name
(libappindicator-gtk3) and a version constraint resolve the way `pacman -S`
will. The menu's guard prelude sources the same function, so the whole batch
still costs one fork. omarchy-reinstall-pkgs reports and skips a default
that no configured repository offers instead of failing the transaction.

On x86_64 every guarded row resolves against the stable and edge
repositories, so the menu is unchanged there.

Tests derive each row's packages from its install command on x86_64 and
aarch64 and fail when the guard names different ones.

Co-authored-by: Ryan Hughes <1630358+ryanrhughes@users.noreply.github.com>

* Run 1Password with software GL where there is no render GPU

Electron draws a blank window when the machine has no DRM render node: a VM,
a headless or passthrough GPU, or a GPU whose driver isn't up yet (Apple M3
and later on Linux today). omarchy-hw-render-gpu tells whether a renderD node
exists, and omarchy-cmd-electron-gl-args prints --ozone-platform=wayland
--disable-gpu only when it doesn't.

omarchy-cmd-electron-gl-wrap installs a marked PATH wrapper in /usr/local/bin
that execs the real binary with those flags, so it follows the machine rather
than the moment it was installed. It refuses to replace a launcher it didn't
write, or to wrap a binary over itself, and stages the write so a failure
leaves the old launcher in place. omarchy-cmd-desktop-exec-repair points the
user's desktop entry at the wrapper by replacing only the executable token,
keeping arguments, other actions and customizations, with a backup.

The 1Password installer uses them when the machine has no render node. With
one, which is every x86 machine with a working GPU, nothing changes.

Co-authored-by: Scott Jones <444693+scottjones@users.noreply.github.com>

* Record with wf-recorder when gpu-screen-recorder can't start

gpu-screen-recorder knows NVIDIA, AMD, Intel and Broadcom GPUs and aborts
during GL init on anything else, such as Apple Silicon's, before a recording
starts. Screen recording now falls back to wf-recorder, when it is installed,
if gpu-screen-recorder is missing or exits before the file appears. The
picker, file naming, notifications and post-processing are shared. wf-recorder
records one audio device, so desktop plus microphone go through a temporary
null sink that stop unloads, and its track is pinned to 48 kHz. Portal
capture and --resolution stay gpu-screen-recorder only.

The finalize pass keeps 48 kHz too: loudnorm declares 192 kHz output, so
without -ar the AAC encoder wrote 96 kHz from a 48 kHz recording, which many
editors decode as silence, and which on Apple Silicon makes the speaker
safety daemon lock the amps. #11092 makes the same one-line change; whichever
lands second drops it.

Stop and status matched `pgrep -f ^gpu-screen-recorder`, which misses
wf-recorder and matches any command line that starts with that path. The
script now records the recorder's pid, and the new
omarchy-capture-screenrecording-process checks or signals it by its
executable, through a pidfd opened before the check so a reused pid can't be
signalled. When that pid is no longer a recorder (a recording started before
this change, or one that died), every recorder the user runs counts, as it
does for the bar indicator and the Stop row, which use the same helper.

Where gpu-screen-recorder works, which is every x86 GPU it supports today, it
records as before.

Co-authored-by: Scott Jones <444693+scottjones@users.noreply.github.com>
Co-authored-by: Issac Trotts <15530+ijt@users.noreply.github.com>

* Hold logind's lid switch while a display it doesn't count is in use

systemd-logind treats a closed lid as docked only when an external display
is on a connector type it lists (HDMI, DisplayPort, DVI, VGA...). Displays on
"USB" connectors, which Apple Silicon uses for USB-C displays, aren't on that
list, and a missing comma fuses DVI-A and Composite into one entry. With such
a display in use, closing the lid suspended a laptop that was still being
used on it.

omarchy-system-lid-inhibit holds a handle-lid-switch inhibitor while one of
those connectors is connected and enabled in Hyprland, on a laptop where
HandleLidSwitchDocked is ignore, which is exactly the case logind would have
ignored had it counted the display. It releases the inhibitor when the display
goes. A compositor or logind that doesn't answer changes nothing, since
releasing then would suspend a closed lid. The inhibitor runs as a transient
user unit bound to the monitor watcher's unit, so a watcher that dies can't
leave the lid inhibited until logout.

The monitor watcher runs it alongside the clamshell sync. On a machine whose
external displays are all on connectors logind counts, it does nothing.

* Keep the focused workspace in front when a monitor goes away

When a monitor is unplugged, or the lid turns off the laptop panel, Hyprland
first warps focus to whichever monitor it lists first and only then moves the
removed monitor's workspaces over. The workspace you were on lands hidden
behind that monitor's own (Hyprland 0.56.2, CMonitor::onDisconnect).

monitor.focused fires before Hyprland records the new focus, so the monitor
still reported as active is the one being left: its workspace is remembered
until the event loop next turns, and a monitor.removed in that window focuses
it again once the removal has finished. An open special workspace already
comes along, and any focus or workspace change made before the restore runs
wins.

* Drive the keyboard backlight from the ambient light sensor

Laptops with an ambient light sensor and a keyboard backlight LED now light
the keys in the dark and dim them as the room brightens, inverse to display
auto-brightness. omarchy-brightness-keyboard-auto polls the sensor every five
seconds (preferring an IIO device that names itself ALS, and converting a raw
channel with its offset and scale) and maps lux onto the LED, with a deadband
so it doesn't chase noise. It pauses while the screen is locked or the lid is
closed.

A level set with the keyboard-brightness keys wins until the light changes
enough that it no longer fits. omarchy-brightness-keyboard records each level
it sets in the session's runtime directory (a private state directory without
one), so the loop can tell keys turned off on purpose from keys left off by
the lock screen or a restored zero, which it lights again.

The user unit runs it in the graphical session. Its ExecCondition asks
--available, so it is a no-op on machines without both a sensor and a
keyboard LED. First run enables it, and a migration enables it for existing
installs, writing the enable symlink itself when there is no live user
manager.

Co-authored-by: Scott Jones <444693+scottjones@users.noreply.github.com>
Co-authored-by: Gabe Ohlson <3355909+6abe@users.noreply.github.com>

* Defer an image build's hardware setup to the machine's first boot

An image is built away from the machine it will run on, so its hardware
leaves would probe the builder. While the image manifest the detector
already reads (/var/lib/omarchy/image/target) exists, omarchy-apply-hardware
queues every leaf of install/hardware/all.sh instead of running it and arms
omarchy-provision-hardware.service. On the first boot, before owner setup
and the login screen, omarchy-provision-hardware retires the manifest and
runs the queue on the real hardware: a failed step stays queued for the
next boot (exit 75), and the initramfs is rebuilt once after the last step
when a step changed its inputs or asked for it. Bluetooth starts its
service on that boot, whose target is already up. Root ignores any
environment override of these paths. Without a manifest, as on every
ISO install, hardware setup runs exactly as before. Nothing here fetches
package databases: an image carries its sync databases and every package
its steps install.

* Give an image-built root a UTF-8 locale when LANG is still C

A root built from a distribution tarball instead of the ISO (Arch Linux
ARM's ships LANG=C) never went through the ISO's locale step, so it ran
non-UTF-8. The config phase now selects en_US.UTF-8, generating it if
needed, but only when LANG is unset, C or POSIX: any named locale is
somebody's choice and stays. The ISO writes the locale before this runs,
so an ISO install is unchanged.

Co-authored-by: Scott Jones <444693+scottjones@users.noreply.github.com>

* Let a platform's packages run their own boot, system and user setup

Three dispatch operations give a platform one place for the setup it
needs beyond Omarchy's, instead of inline platform branches in the setup
leaves. The last hardware leaf runs setup-boot, then setup-system, as
root, so an image build queues them with the others and its first boot
runs them, with image-first-boot (offline-safe, asking for the one boot
rebuild instead of building). setup-boot belongs to the boot package and
is required on Apple Silicon, where hardware setup has no other way to
run the Mac's boot setup; a failed setup-boot stops the leaf before
setup-system. setup-user runs as the user being set up, refuses root,
and gets only the user's home and session variables: from the last user
leaf at finalization (skipped inside an image build, where the detector
would read the build host) and again from first run once the session is
up. On Apple Silicon setup-boot resolves in omarchy-mac-boot's directory
and the other two, both optional, in omarchy-mac's, /usr/lib/omarchy/mac;
x86, generic aarch64 and Qualcomm register none, so all three are no-ops
there.

A failed finalization now keeps first run pending, like any failed
first-run step, instead of letting the other steps mark it complete.

Co-authored-by: Scott Jones <444693+scottjones@users.noreply.github.com>

* Find batteries, lids and panels that aren't named like a PC's

Laptop detection assumed PC names: a battery called BAT*, a lid in /proc/acpi, a
touchpad or trackpad in the device name, a GPU backlight. Apple Silicon has
none of them (macsmc-battery, the SMC lid switch, apple-mtp-multi-touch,
apple-panel-bl), and some Framework and Chromebook firmware calls its battery
CMB0, so those machines lost the battery panel, laptop-only services, lid
handling, the touchpad toggle and display brightness.

- Battery presence goes by type instead of name, and skips a battery scoped
  Device (a wireless mouse or keyboard), so a desktop with one isn't a laptop.
- Battery status takes the first UPower battery that powers the machine, reads
  thresholds and cycles from it (BAT* stays the fallback), shows the magnitude
  of a signed power_now, and rounds the percentage half-up for display like the
  bar, while the charge-hold check keeps the unrounded value.
- A laptop is also one with any input device that reports a lid switch
  (SW_LID), whatever its name: Apple's SMC input device has the same name on a
  Mac mini, where it reports none. Whether the lid is closed comes from ACPI
  where there is an ACPI lid, as before; otherwise logind is asked, with a
  short timeout, only when a device reports a lid switch.
- A device named touchpad or trackpad still wins; only when there is none does
  the touchpad match take a mouse udev marks as a touchpad (Apple's
  multi-touch trackpad), never a touchscreen.
- The platform package can describe its displays in
  /usr/share/omarchy-platform/displays.conf: backlights that never drive the
  panel (a Touch Bar's), the panel's own where there is no GPU backlight, and
  a display driver without DDC channels, where a probe walks unrelated I2C
  buses for about 3 s: there only a connector with a ddc node is probed, and
  a monitor without one reports no brightness instead of dimming the built-in
  panel. Omarchy ships no such file and names no device in it; without it the
  backlight choice and DDC probing are unchanged.

Co-authored-by: Scott Jones <444693+scottjones@users.noreply.github.com>
Co-authored-by: Ryan Hughes <1630358+ryanrhughes@users.noreply.github.com>
Co-authored-by: Sparsh Kumar <22640187+skkumarsparsh@users.noreply.github.com>
Co-authored-by: dl-alexandre <166029845+dl-alexandre@users.noreply.github.com>

* Keep the user's XDG config and state directories in platform user setup

setup-user runs with an allowlisted environment, which dropped
XDG_CONFIG_HOME and XDG_STATE_HOME. A platform's user setup reads the user's
configuration and keeps its setup markers there, so for a user who sets
either it looked in, and wrote to, the default directories instead. Both now
pass through with the rest of the session; XDG_DATA_HOME and everything else
still don't.

* Let a platform package add Hyprland defaults of its own

A platform's runtime package may need Hyprland defaults of its own (a Mac's
capture keys and three-finger workspace swipe, say), and Omarchy's config
should carry no branch for them. Three directories under the platform root,
/usr/share/omarchy-platform, now take them. The one installed platform package
owns that root, Omarchy reads it by literal path and ships nothing there, so
without a platform package nothing changes. Each file is loaded by its path,
so it can't shadow a module, and one that fails is logged and shown as a
notification while the rest of the config loads.

- hypr/defaults/*.lua loads before Omarchy's defaults. A
  chord bound there replaces Omarchy's default for it, and a decorator it adds
  runs before every later bind, so it can bind what has to run first. It sees
  the command a bind runs, including the command a shell shortcut such as
  { menu = "root" } stands for when it binds the shell's global shortcut.
- hypr/settings/*.lua loads after Omarchy's defaults and
  before the theme, for settings that replace Omarchy's.
- hypr/gestures/*.lua loads after the user's files, only for what must
  see them: the gesture registry, which now records on every platform, lets a
  default gesture step aside for the user's own.

The user's files load after the first two and can unbind or rebind any of it.

* Let a platform package name keys in the keybindings menu

The keybindings menu shows a key by its keysym, which on some keyboards is not
what the key says: on a MacBook, XF86MonBrightnessUp and Down are the F2 and
F1 keys, so the menu listed rows like "SHIFT + XF86MonBrightnessUp". A
platform package can now list names for such keys in /usr/share/omarchy-
platform/key-names, one "<keysym> <name>" per line, and the menu shows those
instead. The file is part of the menu cache's key, so installing or changing
it refreshes the menu. Without it the menu is as before.

Co-authored-by: Scott Jones <444693+scottjones@users.noreply.github.com>
Co-authored-by: kx0101 <90087463+kx0101@users.noreply.github.com>

* Keep the top bar out of a panel's camera cutout

A laptop panel with a camera notch at its top (Apple Silicon MacBooks, for
one) covered the middle of the top bar, and a bar shorter than the notch left
a sliver of every window beside the camera. Omarchy knows no panel's cutout
itself: the platform's own package describes them in
/usr/share/omarchy-platform/display-cutouts.json, by
connector, physical mode and the rows the cutout covers. On a described panel
a top bar is never shorter than the cutout, its center section sits beside
the right one, and hiding it parks all of it; [bar] notch-height sets that
floor by hand, on that panel only. Without a description, which is every
machine without such a package, the bar is as before.

Co-authored-by: Scott Jones <444693+scottjones@users.noreply.github.com>

* Keep PC and Intel Mac quirks off Apple Silicon

A few hardware steps and commands written for PCs and Intel Macs do the wrong
thing on Apple Silicon:

- The Intel Mac Wi-Fi quirk turns off the Broadcom firmware supplicant for
  every Mac whose DMI vendor is Apple and whose chip is on its list. Apple
  Silicon reports Apple Inc. through U-Boot's DMI tables, and the M1 Pro and
  Max's BCM4387 is on the list, but there the firmware supplicant is the one
  that works: turning it off makes firmware commands time out and breaks
  scanning. The install step and its migration now skip Apple Silicon.
- fnmode=2 for Apple-like keyboards is left to the Mac's own platform package
  on Apple Silicon, where the built-in keyboard's top row depends on it.
- The SPI keyboard check reads the DMI product name without failing the
  hardware pass where there is none.
- The Windows VM, an x86_64 guest under KVM, runs on x86_64 only, and direct
  boot (U-Boot finds the bootloader and makes its boot entries itself)
  refuses Apple Silicon; the menu hides both there. Hibernation setup skips
  it; its kernel has no hibernation today, and resume there is not wired.

Everywhere else each step runs as before.

Co-authored-by: Scott Jones <444693+scottjones@users.noreply.github.com>

* Let a platform's runtime package adjust the apps it needs to

Some apps need a platform's own touch once they are installed. On Apple
Silicon, Chromium-family browsers must decode video in software (the hardware
decoder renders green frames in them), and Steam only runs through a
FEX launcher, which then depends on Steam and must go before Steam can be
removed. Until now the Mac runtime did this with inline branches in the
installers.

Two optional dispatch operations take their place. post-install <app> runs
as the user once an installer has installed the app and finished its own
setup of it: every browser install (after its flags file, before it says the
browser is installed) and Steam's (after the package, before the 32-bit
drivers and the first launch). pre-remove <app> runs before Steam's removal
drops its packages. Where a platform registers them they refuse root (a
platform with nothing registered is a no-op, so sudo installs still work
there) and get the same home and session variables as setup-user, and a
failure fails the install or stops the removal. On Apple Silicon they
resolve in omarchy-mac's directory; every other platform registers none, so
installs and removal run as before there.

Steam's 32-bit driver step also stops failing the install on a machine with no
Intel, AMD or NVIDIA GPU (Apple Silicon, but also Snapdragon or a VM): it has
nothing to add there and now says so.

Co-authored-by: Scott Jones <444693+scottjones@users.noreply.github.com>

* Ask the platform's boot package before an update takes over its files

When an Omarchy package's upgrade stops only on files no package owns, the
update moves them aside and retries. On a Mac some of those files belong to its
boot chain or were written by its image (initramfs and pacman configuration,
say), and moving them aside can leave it unable to boot or update. The Mac
runtime refused those paths with an inline list.

update-takeover <path>... asks the platform's boot package instead, before
anything moves: read-only, exit 0 lets the takeover go ahead, anything else
keeps every file and the update fails as it did before the retry existed. The
update resolves it as the user and runs it with sudo only where it resolves,
inside the authorization the move needs anyway. On Apple Silicon it is
required: a Mac without its boot package, or with one too old to answer, keeps
its files. Every other platform registers none, so the takeover goes ahead as
before, asking no one.

* Install the Mac's package set on Apple Silicon

omarchy-pkg-defaults already composes install/omarchy-<platform>.packages
after the base and aarch64 lists, but Apple Silicon had no list, so a Mac got
the generic aarch64 set: no omarchy-mac or omarchy-mac-boot, whose entrypoints
the dispatcher requires there.

install/omarchy-apple-silicon.packages names the Mac's runtime and boot
packages, plus the defaults an owner may remove: the startup-volume tool,
hardware video decode, Widevine, and wf-recorder, since gpu-screen-recorder
can't capture on Apple Silicon and screen recording falls back to it. What a
Mac can't work without (its speaker stack, PipeWire's PulseAudio server and
realtime scheduling, the GPU's Vulkan driver) comes as omarchy-mac's
dependencies, so a new Mac driver needs no Omarchy change. Other platforms
compose exactly what they did.

Co-authored-by: Scott Jones <444693+scottjones@users.noreply.github.com>

* Give existing aarch64 machines the zram swap Omarchy configures

omarchy-settings configures swap on zram and tunes memory reclaim for it.
x86_64 gets zram-generator from its installer and new aarch64 installs from
their default packages, but an aarch64 machine installed before
omarchy-aarch64.packages named it has no generator, so the configuration asks
for a device nothing creates.

Migration 1790328426 installs zram-generator where the platform's default
packages name it and it is missing, and starts the zram swap wherever it is
down, unless no device is configured (then the generator makes no swap unit)
or the administrator masked it. A swap that won't start, or whose unit can't
be read, only warns: the generator's unit starts with every boot, so a reboot
retries it, and failing the migration would stop every update. A failed
install waits (75) for the next migrate, as it does where the platform can't
be told, changing nothing. It keeps the name it has in omarchy-mac, so Macs
that already ran it skip it. It also retires an /etc copy of the drop-in
identical to the packaged one, which earlier Mac setups made and which would
hide the packaged file's updates; an edited copy stays. On x86_64 the default
packages don't name the generator and there is no copy, so it does nothing.

* Keep an ARM machine's repositories when it changes channel

omarchy-refresh-pacman and install finalization copied x86_64 templates
everywhere, so on an aarch64 machine a channel change or a finished install
wrote Arch Linux's x86 [core]/[extra]/[multilib] and dropped Arch Linux ARM
and the platform's own repositories.

Channel changes now tell the platform apart. x86_64 is unchanged. On aarch64
the machine's configuration is kept, with its repositories, mirrors and
signature policies, and only the [omarchy] server moves to the new channel;
a configuration whose Omarchy server can't be identified is refused
unchanged, and one with no Omarchy repository gets it at the end (not on
Apple Silicon, whose own repository must not come first). Only pacman.conf is
backed up and rewritten, straight from this process through sudo, never
through a file another process could change first.

Before that, a channel must be qualified for the platform: a signed upgrade
and reboot tested there, not only a published repository. x86_64 keeps
stable, rc and edge; Snapdragon and other aarch64 machines take edge, as
dragon does; Apple Silicon takes none until the Mac packages qualify on edge.
The refresh then checks, as the user, that the channel's omarchy.db
publishes the runtime pair, and on Apple Silicon omarchy-mac and
omarchy-mac-boot. omarchy-channel-set refuses an unqualified channel before
anything else, the dev confirmation included.

Install finalization writes a whole template, as before on x86_64. Other
aarch64 platforms get Arch Linux ARM (dragon's pacman-aarch64.conf and
mirrorlist-aarch64, byte for byte) with its keyring, plus Omarchy on a
qualified channel. Apple Silicon gets omarchy-mac's template on a qualified
channel and keeps its image's configuration otherwise. In an image build,
finalization leaves the keyring to the machine's first boot, so machines
flashed from one image don't share a pacman master key. On an unqualified
channel, a machine that gets no Omarchy repository is told why.

Co-authored-by: Birk Skyum <74932975+birkskyum@users.noreply.github.com>
Co-authored-by: Jim Martin <1334144+jdvmi00@users.noreply.github.com>

* Keep Arch Linux ARM's signing keys current on ARM machines

omarchy-update-keyring reinstalls archlinux-keyring before every system
upgrade, since keys change without a version bump. An aarch64 machine's Arch
Linux ARM repositories are signed with keys from archlinuxarm-keyring, which
nothing refreshed, so a rotated ALARM key failed the upgrade that followed.

On an aarch64 platform where archlinuxarm-keyring is installed, it is
reinstalled in the same transaction. On x86_64 the keyring step runs exactly
as before, even where that package happens to be installed.

* Mark the runtime's platform files as deciding at runtime

The omarchy-settings recipe leaves the mkinitcpio, Limine, memory and USB
drop-ins out of its aarch64 package, because they used to assume x86_64. That
leaves an aarch64 install without the HOOKS baseline its initramfs needs, and
without the zram and reclaim settings every other Omarchy machine gets.

Each of those files now decides at runtime whether it applies: the HOOKS
baseline asks the platform, thunderbolt is added only where the kernel has the
module, the Limine files are read only where Limine is installed, the oomd
drop-ins are inert without systemd-oomd, aarch64 installs the zram generator
the zram drop-in needs, and the USB autosuspend option is ignored by a kernel
that builds usbcore in. default/settings-runtime-profile says so, file by
file; a recipe that finds it ships the same files on every architecture
(omacom/omarchy-pkgs#638), and older sources keep today's aarch64 package. A
test checks that every file it names exists and that each runtime decision is
in place.

* Set the wireless regulatory domain from the owner's timezone at first-boot setup

A deferred install (an Apple Silicon image, an OEM ISO install) runs its
hardware steps before the owner picks a timezone, on the UTC placeholder,
which names no country, so install/hardware/set-wireless-regdom.sh leaves the
regulatory domain unset and the radios stay on the world domain. Owner setup
now runs that step again once the owner's timezone is set, and applies it to
the radios through wireless-regdb's helper, since no reboot follows setup. An
up-front install, which picks the timezone before its hardware steps, is
unchanged.

* Ask again for a keyboard layout first-boot setup could not apply

First-boot setup kept going when the chosen layout did not take: loadkeys
errors were ignored, a keymap localectl did not list kept the default, and a
failed or partial write of /etc/vconsole.conf was only logged. The owner then
typed the disk password in one layout while the boot files asked for it in
another, and could only unlock the disk by typing US-style.

The keyboard step now fails closed. On a console loadkeys must succeed, the
keymap must be in localectl's list exactly (a list localectl cannot produce
counts as a failure too, logged apart), and after systemd-firstboot or the
localectl fallback, /etc/vconsole.conf is read back in a clean shell and must
name the chosen keymap with an XKB layout. Any failure is logged, the owner is
told the layout could not be set, and the layout is asked for again, so the
password form is never reached with a layout that did not take.

* Refresh a platform's own signing keyrings before an update

omarchy-update-keyring reinstalls archlinux-keyring (and Arch Linux ARM's on
aarch64) before the system upgrade, so a rotated key never fails that
upgrade's signature checks. A platform's own repository has a keyring of its
own that nothing refreshed: on Apple Silicon, asahi-alarm-keyring for
[asahi-alarm].

A platform package now lists those keyrings in
/usr/share/omarchy-platform/keyrings, one package name per line, and
omarchy-update-keyring reinstalls each one that is installed in the same
transaction. It adds no repository the machine didn't already trust. A line
that isn't one keyring package name is skipped with a warning, so a mistake in
the list can't stop every update. Without the file, as on x86 today, the step
runs as before.

* Drive an untyped default input from the audio panel

Quickshell types only exact media classes, so a virtual source (an
Audio/Source/Virtual: EasyEffects' source, or the microphone mapping a
platform package adds) has no PwNode.audio. With one as the default input the
audio panel showed the input at 0% and its slider changed nothing, the peak
meter stayed flat, the microphone widget read it as muted, and the panel could
not list it unless it was already the default.

Such an input now goes through wpctl for its volume and mute (UntypedInput in
shell/Commons, one reader and one pactl subscribe for the whole shell), the
panel meters it with the new omarchy-audio-source-level, and the input list
takes an untyped node PulseAudio lists as a source.

A platform whose audio runs through its own processing can also name nodes
that are neither devices nor apps in /usr/share/omarchy-platform/audio.json,
which Omarchy ships none of: the panel and the microphone widget leave them
out, and a "replaced" node only while its replacement exists. Without the
file nothing is hidden.

On every machine: the input list leaves out a source whose ports are all
unavailable (an empty headset jack) unless it is the default, as the output
list does for sinks; choosing an input no longer moves a filter's own capture
(a source output with node.link-group), which would feed the filter from the
new input or from itself; the shell's own level meters no longer light the
microphone widget; and omarchy-audio-sink-availability reads pactl in the C
locale for sinks too, since "not available" is translated.

* Give first-boot setup an XKB layout for keymaps systemd cannot map

systemd-firstboot writes an XKB layout only for keymaps listed in
kbd-model-map. Nine the setup form offers are not (Colemak, Azerbaijani,
Bulgarian, Czech, Swiss German, Kyrgyz, Norwegian, Polish, Ukrainian), so it
wrote KEYMAP= alone, the readback refused it, and owner setup could never get
past the keyboard step for them.

Setup now adds the XKB settings itself when firstboot left none, matching the
console map so the shared password types the same on the desktop: kbd's cz is
QWERTY, bg-cp1251 phonetic and azerty French AZERTY, and non-Latin layouts keep
a US layout to switch to, as kbd-model-map's own rows do. A test checks every
layout the form offers ends with an XKB layout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>

* Leave keys a firmware key turned off dark

The ambient keyboard backlight treated any 0 the keyboard-brightness keys had
not recorded as an accident and relit it within five seconds. Firmware Fn keys
(Fn+Space on ThinkPads and Framework laptops, Fn+F10 on Dells) write the LED
directly, so on those laptops turning the keys off never stuck.

The lock screen's off now records a blank, and only that blank or a 1%
leftover is taken back; any other off is a choice that pauses auto like the
keys do. A wake clears the blank only once the restore succeeded, the command
runs one at a time so a blank racing a wake can't leave the LED and record
disagreeing, and the loop drops a blank once it has taken the keys back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>

* Drop the optional-install availability guards

Each menu open checked every Install row against the sync databases, which
costs most of a second of pacman work on every machine to hide rows only an
ARM machine is missing. The fix for those is publishing the ARM packages, not
hiding the rows, so this removes omarchy-pkg-available, the Install rows'
availability and architecture guards, and reinstall's skipping of defaults no
repository offers.

The Windows VM row keeps its x86_64 guard, since no package fixes a guest that
needs an x86_64 host, and reinstall keeps its aarch64 channel handling.

This reverts commit aea001cbde4d0852b311ef42b5e8c380c719f833.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>

* Put the current root back when the factory root cannot take its place

Factory reset moved the running root aside, marked the reset committed, then
renamed the staged factory root to @. If that second rename failed, cleanup
saw a committed reset: it skipped the boot package's rollback and kept the
throwaway LUKS slot, and since the swap had not happened it deleted the staged
root as well, leaving nothing at @.

The reset now commits only once the factory root is at @. A failed activation
renames the old root back first, then cleanup rolls the platform's boot state
back and revokes the throwaway slot. If that rename fails too, both roots are
kept, cleanup still rolls back and revokes, and the error names the rename
that recovers the machine. The generic Limine path has already rebuilt its
boot files from the factory root by then, as it had before this change.

* Leave other recorders alone when the saved recorder pid is stale

A saved pid that no longer named a recorder made status and stop select every
gpu-screen-recorder and wf-recorder the user ran, so stopping could interrupt,
and after five seconds kill, a recording started somewhere else.

A stale pid now means no recording of ours is running: the toggle starts a new
one, and --stop-recording finds nothing to stop. Stop acts on the recorder the
status check selected rather than selecting again. Without a saved pid, as for
a recording started before the pid was saved, every recorder the user runs
still counts.

* Name the audio a wf-recorder recording could not capture

With desktop and microphone audio both requested, wf-recorder records a null
sink fed by two loopbacks, and a loopback that failed to load was ignored: the
recording went ahead without that source, or silent when both failed. A
missing null sink or default device dropped audio just as quietly.

A source that cannot be captured is now left out with a notification naming
it, the loopbacks that did load are unloaded, and the recording carries the
source that remains, or no audio track at all.

* Show the Edge, Spotify and Dropbox installers on x86_64 only

These vendors currently publish Linux builds for x86_64 alone (the AUR's
microsoft-edge-stable-bin and spotify are arch=('x86_64'), and Dropbox lists
no Linux ARM client), so no native aarch64 build is available and choosing
one on an ARM machine only fails. Like the Windows VM row they keep a uname
check, which the menu answers once per open without asking pacman. The other
Install rows are unchanged.

* Revoke the generic reset's throwaway slot when the reset fails

The generic Limine path added its throwaway LUKS slot without recording it,
so a reset that failed before activation never revoked it. Once the factory
root's UKI was on the ESP, a failed activation that put the old root back left
a machine that booted the factory kernel and unlocked unattended with a key
readable on the ESP, while the screen said the current root was back.

The generic slot is now recorded like the platform's and revoked by cleanup
whenever the reset fails before the factory root is active. A failed
activation on that path says the boot files were already rebuilt from the
factory system and to run limine-update before rebooting.

* Clear a stale recorder pid when a recording starts

The recorder's pid is saved only once its file exists. Until then a pid left
by a recording that ended without a stop still counted as stale, so pressing
the toggle again while the new recorder started found nothing of ours and
could start a second recording. Starting a recording now clears that pid,
and during startup the toggle falls back to the user's recorders as before.

* Word the dropped-audio notice for every reason a source is left out

The notice said the dropped source "could not be captured", which was wrong
when the null sink itself failed to load: the microphone was never tried, and
desktop audio recorded alone as it always has. It now says the source could
not be added to the recording, true for a missing device, a failed loopback
and a failed null sink alike.

* Show the Heroic and Minecraft installers on x86_64 only

Heroic publishes Linux builds for x86_64 alone (its arm64 builds are for
macOS and Windows), and Mojang's Linux launcher is a single x86_64 tarball.
They join Edge, Spotify and Dropbox under the same uname check, so every row
whose vendor ships no native Linux aarch64 build is treated alike.

* Ask the recorder script whether its stop has a recording to end

The bar indicator and the menu's Stop Screenrecording row asked the process
helper whether any recorder the user runs was alive, while the toggle's stop
acts only on the recording it saved. With a stale saved pid alongside an
unrelated recorder, both offered a stop that did nothing.

omarchy-capture-screenrecording --status answers with the selection the stop
uses, and both now ask it. Status needs no recordings directory and never
notifies, so the recordings-directory check moved below argument parsing and
skips it.

* Copy every platform's pacman templates into place, as x86_64 does

On aarch64 a channel change edited the machine's pacman.conf in place: an awk
pass found the [omarchy] server and rewrote its channel, appended a section
where there was none, checked the channel's omarchy.db first, and gated it all
on a per-platform table of qualified channels. Install finalization appended
Omarchy to a template, or on a Mac copied one bundled inside omarchy-mac. What
a machine ended up with was never in the repository.

Each platform now has a pacman.conf and mirrorlist per channel, copied whole:
x86_64's stay in default/pacman, Snapdragon and other aarch64 machines use
default/pacman/aarch64, and Apple Silicon default/pacman/apple-silicon, with
Omarchy and Asahi ALARM ahead of Arch Linux ARM. Omarchy publishes aarch64
packages on edge alone so far, so every ARM channel points at edge for now and
channel changes work there; moving a channel is a template edit. A channel
without both files is refused before anything changes, including a dev
checkout's own templates. Reinstall goes back to resetting to stable, and the
update confirmation loses the missing-repository notice it no longer needs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>

* Revert "Copy every platform's pacman templates into place, as x86_64 does"

This reverts commit 9a07e138cc6c3a19b58d2f3d7eb536dd3042ccc5.

* Copy every platform's pacman templates into place, as x86_64 does

On aarch64 a channel change edited the machine's pacman.conf in place: an awk
pass found the [omarchy] server and rewrote its channel, appended a section
where there was none, checked the channel's omarchy.db first, and gated it all
on a per-platform table of qualified channels. Install finalization appended
Omarchy to a template, or on a Mac copied one bundled inside omarchy-mac. What
a machine ended up with was never in the repository.

Each platform now has a pacman.conf and mirrorlist per channel, copied whole:
x86_64's stay in default/pacman, Snapdragon and other aarch64 machines use
default/pacman/aarch64, and Apple Silicon default/pacman/apple-silicon, with
Omarchy and Asahi ALARM ahead of Arch Linux ARM. Omarchy publishes aarch64
packages on edge alone so far, so every ARM channel points at edge for now and
channel changes work there; moving a channel is a template edit. A channel
without both files is refused before anything changes, including a dev
checkout's own templates. Reinstall goes back to resetting to stable, and the
update confirmation loses the missing-repository notice it no longer needs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>

* Drop the migrations no installed machine needs

Fresh installs mark every migration done, and every Apple Silicon machine is a
fresh install, so the Mac handoff to omarchy-mac-boot (and the migrate
operation it called), zram for aarch64 installs that predate the package list,
the Brave Widevine link and the Broadcom migration's Apple Silicon guard never
run anywhere. The exit-75 deferral in omarchy-migrate existed only for two of
them. Keyboard backlight auto stays on new installs through first run rather
than switching on for existing ones, and migration 1786605598 goes back to
quattro: the mkinitcpio drop-ins arrive by package.

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

* Drop the generic changes Mac support doesn't need

The pacman platform guard (and the detector copy omarchy-settings shipped for
it, which the HOOKS baseline fell back to), the lint against uname gates, first
run staying pending after a failed finalization, run_logged echoing failures to
stderr, half-up battery rounding, hiding empty-jack inputs and leaving a
filter's capture alone when choosing an input all rode along with the Mac work
without serving it. Each goes back to quattro's behaviour and can come back on
its own merits.

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

* Drop the platform Hyprland slots

A platform package could load its own Hyprland binds, settings and gestures:
binds that replaced Omarchy's chords and decorated every later bind, settings
ahead of the theme, and gestures after the user's files, which needed every
hl.gesture call wrapped so a default could step aside. On a Mac those carried
macOS habits (a three-finger workspace swipe, tap-to-click off, menus opening
on the built-in screen, other screenshot and keyboard-backlight chords), not
anything the hardware needs, and Omarchy should behave the same on every
machine. The one bind a Mac does need, its lid switch under the SMC's name,
joins Omarchy's own lid binds.

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

* Keep aarch64 machines off the release channels until a release supports them

Every aarch64 channel's template pointed at the edge repository, but a switch
to stable or rc installs the release-named omarchy and omarchy-settings. On
edge/aarch64 those are the 4.0.4 release line, which has no omarchy-hw-platform,
lifecycle dispatch or omarchy-pkg-defaults, so `omarchy-channel-set stable` on
a Mac replaced the quattro runtime it runs with one that cannot run it, and
succeeded.

aarch64 platforms now have edge templates alone, so a switch to stable or rc is
refused before anything changes; adding those templates once a release
supports aarch64 opens them. Refresh with no channel named, as reinstall runs
it, and install finalization for a channel the platform has no templates for
take the platform's default channel: stable on x86_64, edge on aarch64.

* Leave a filter's own capture alone when choosing an input again

d0b94d98c returned omarchy-audio-input-set-default to moving every source
output, as not serving Mac support. It does: asahi-audio's microphone DSP on
Apple Silicon records the raw array through a filter-chain capture stream
(audio_effect.jNNN-mic, node.link-group "filter-chain-..."). On an M1 Pro,
choosing the Mac's microphone in the audio panel moved that stream onto the
DSP's own output, and the microphone recorded digital silence (-91 dB, from
-55 dB of room noise) until PipeWire restarted. With the skip it keeps
recording (-54 dB).

This restores the skip of source outputs carrying node.link-group, and its
test, as they were before that commit.

* Say aarch64 offers edge alone where the templates are described

The aarch64 edge templates, the template helper and the update-process docs
still said every platform has a template per channel and every ARM channel
installs from edge, which the previous commit made untrue.

* Point the Mac packages at omacom/omarchy-mac-pkgs

omarchy-mac and omarchy-mac-boot now live in omacom/omarchy-mac-pkgs, with
each package at the top level. The dispatcher, its documentation and the
Apple Silicon package list name that repository, and the drop-in test's
fixtures, byte for byte the same as omarchy-mac-boot/files/etc/mkinitcpio.conf.d
at 4399105, cite it there.

* Stop naming a platform migration among the dispatcher's callers

The migrate operation went with the Mac migrations, and no caller hands a
machine to a platform migration any more, so the header lists only the
flows that still dispatch. The optional-operations comment is rewrapped.

* Keep the Intel Mac Broadcom migration off Apple Silicon again

A fresh install marks every migration done only for the user it creates, so
an account added later runs them all on its first update. On an Apple
Silicon Mac this one matches: an M1 Pro reports sys_vendor "Apple Inc." and
a BCM4387 (14e4:4433), so it would append feature_disable=0x82000, which
stops that chip scanning once brcmfmac reloads. Restore the guard and its
test case.

* Say when Steam's post-install hook runs now that the drivers go first

Steam's installer now adds the 32-bit drivers before Steam itself, so the hook runs after both and before the first launch.

* Release fallback audio after the recorder exits

When wf-recorder ends outside the normal Stop path, status rejects the saved PID before unloading the combined-audio loopbacks. Release those owned modules when the saved recorder is gone, so the next status or stop call ends microphone capture without signaling another recorder. Leave a mix without a saved PID alone: its recorder can still be starting.

Co-Authored-By: GPT-6 in Codex <noreply@openai.com>
Co-Authored-By: Codex Medium <noreply@openai.com>

* Serialize recording state changes and stale audio cleanup

Status can finish checking an exited recorder after another command has replaced its audio modules, and stop finalization can outlive the recorder while a new recording starts. Share a lock across start, stop and cleanup so each command finishes updating its resources before another changes them. Status uses a read-only check when the lock is busy, keeping the bar responsive during the picker. Close the lock descriptor in persistent children so recording and preview cleanup cannot retain it.

Co-Authored-By: GPT-6 in Codex <noreply@openai.com>
Co-Authored-By: Greptile <165735046+greptile-apps[bot]@users.noreply.github.com>

* Keep automatic and manual keyboard brightness updates together

A brightness key can update the LED and its chosen-level record after the automatic loop checks for a manual change but before it applies a new target. The automatic write then overwrites the user's choice and deletes the record. Take the manual command's lock for the whole tick, skipping a busy tick and releasing the descriptor on every return. Create the private fallback directory before opening the lock outside a session runtime directory.

The normal lock screen waits five seconds before blanking, so an ordinary tick should already see the session locked. The regression exercises a brightness key arriving within a tick instead of relying on an implausibly delayed lock-state check.

Co-Authored-By: GPT-6 in Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 High via Pi <noreply@anthropic.com>

* Read the split hook baseline in the NVIDIA migration

The NVIDIA firmware migration decides whether to rebuild by sourcing the hardware and filter drop-ins. With HOOKS now defined in the earlier baseline, that partial read treats every hybrid machine as if kms had been removed. Source the new baseline first when it exists, keeping the legacy two-file layout working and skipping rebuilds when the effective hooks still include kms.

Co-Authored-By: GPT-6 in Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 High via Pi <noreply@anthropic.com>

* Use a hardware helper for x86-only menu entries

Keep the x86_64 availability checks for Windows and vendor-specific installers in a hardware predicate, so menu conditions use the same command style as other hardware checks.

Co-Authored-By: GPT-6 in Codex <noreply@openai.com>

* Keep restored keyboard levels from pausing automatic brightness

Idle wake, quick unlock and hibernate can restore a saved level older than the ambient loop current level without the lock blanking first. Treat that restoration as a one-tick event, preserving existing manual pauses and the normal blank-and-restore path. Consume unmatched and unchanged restore records too, so a later firmware key cannot look like the earlier restore.

Co-Authored-By: GPT-6 in Codex <noreply@openai.com>

Co-Authored-By: Claude Opus 5.5 High via Pi <noreply@anthropic.com>

* Preserve restored keyboard levels across a later lock blank

The ambient loop skips a locked session, so a restored level may still be unconsumed when the next blank saves it. Carry that origin through blanking and restore failures so fingerprint unlock cannot make the old saved level look like a manual choice. A brightness key replaces the marker with its chosen level and retains the ordinary blank behavior.

Co-Authored-By: GPT-6 in Codex <noreply@openai.com>

Co-Authored-By: Claude Opus 5.5 High via Pi <noreply@anthropic.com>

* Preserve firmware brightness choices before a lock blank

Firmware keys change the LED without updating the record. Carry a pending restoration through the next blank only if the observed LED still matches that restoration, or is still zero from its previous blank. A firmware change otherwise follows the ordinary blank-and-restore path so automatic lighting pauses on the chosen level.

Co-Authored-By: GPT-6 in Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 High via Pi <noreply@anthropic.com>

* Change only the disk password in omarchy-drive-password

Changing the system disk's password also set the login and root passwords
to match. That changes account-password policy on every platform, x86
included, and overwrites passwords someone chose to keep different, so it
moves to its own change with an explicit confirm.

The disk change itself stays as it was: the new key must open the disk and
the old one must not before the change counts, the journal finishes an
interrupted change, and where the platform's boot checks prove the key
slots, the owner's new slot is recorded before the run finishes. The
journal's confirmed phase is now "record" and no longer needs a login user.

* Cancel a pending recording start instead of queueing behind it

Starting a recording holds the recorder lock through the picker and until the
recorder creates its file. A second toggle or a stop then waited on that lock,
with no end while a portal pick or a stuck recorder never finished, and once
the start gave up the queued toggle began another recording.

While a start is pending, a toggle or stop that cannot take the lock within a
second now leaves a cancel marker and returns. The start checks it after the
picker, after the webcam overlay and while waiting on its recorder, then stops
that recorder and releases its audio. A recorder other than the portal's that
has not created its file after 15 seconds is stopped and the next backend
tried.

* Move automatic keyboard lighting to its own change

Ambient-light keyboard backlight is a new feature on every laptop with the
sensor, not something Apple hardware needs to run Omarchy, so it leaves this
change for a separate review. The keyboard-brightness command returns to
upstream's version, and first run no longer enables the user service.

* Move workspace refocus on monitor removal to its own change

Keeping the viewed workspace in front after a monitor is unplugged changes
desktop behaviour on every machine, not just Apple hardware, so it leaves
this change for a separate review.

* Move prebuilt-image hardware deferral to its own change

Building an image away from its machine needs a manifest the detector reads,
a queue of hardware steps for the first boot, a first-boot service and a
per-machine pacman keyring. An install on the machine itself needs none of
it, so it leaves this change for a separate review with an installer and
package boundary of its own. Hardware and user setup now run where they are
installed, and the platform setup operations take no image argument.

* Move the Qualcomm platform to its own change

Snapdragon support is separable from Apple compatibility, so the detector no
longer names a qualcomm platform: a "qcom," device tree is generic aarch64,
which Snapdragon already behaves as everywhere else here, and a tree naming
both Apple and Qualcomm is still refused. linux-firmware-qcom's platform list
leaves with it.

* Keep a cancel that arrives while a recording start finishes

A stop could land after the start's last cancel check, while it saved its
recording as started, and be dropped with the marker. The start now removes
its starting marker before a final check, a request reads that marker only
after leaving its own, and one that finds the start gone waits for the lock
and acts unless the start already used its request. A recorder ended by a
cancel or a readiness timeout gets two seconds after TERM before KILL, so a
recorder ignoring TERM cannot hold the start and its lock.

* Defer an image build's hardware setup to the machine's first boot

Brings back prebuilt-image support split out of #14431 so it gets its own review. An image built away from its machine names its target in a root-owned manifest the detector reads; hardware setup is queued for the machine's first boot, run there by omarchy-provision-hardware, which also makes the machine's own pacman keyring; and the platform setup operations learn they run on an image's first boot. Apple Silicon images built by the omarchy-mac installer need this; an install on the machine itself does not.

* Rebuild the initramfs when deferred setup changes module options

mkinitcpio's modconf hook copies /etc/modprobe.d into the initramfs, but the
deferred first-boot setup only compared mkinitcpio's configuration and the
kernel command line when deciding whether to rebuild. A step that only wrote
module options, as nvidia.sh does, left the old ones in the boot image.

* Put the Omarchy repository first on aarch64, as on x86_64

quattro now searches [omarchy] before the Arch repositories (34016cfb2), and
its migration 1791403252 moves the section first in /etc/pacman.conf on every
machine. The aarch64 edge template still listed it last, so refreshing pacman
on a generic aarch64 machine would undo that migration. Apple Silicon's
template already lists it first.

* Refuse disk password changes that recovery could not settle

An interrupted change to the system disk is finished from the one key slot
the password opened. If the old password also sat in another slot, a LUKS2
change made in place and stopped before the old key was retired let the rerun
accept the new password as the old one and finish while the other copy still
opened the disk. A system disk whose current password opens more than one slot
is now refused before anything changes, naming the extra slot to remove and,
where the platform records key slots, how to record the ones kept.

The journal now notes that check (checked=1). A change journaled without it,
by an earlier version, on a disk that has other key slots may still hold a
copy of the old password, and no password entered now can show which slot is
whose, so it is left for the owner to settle by hand, in every phase, with the
commands to do it. On a disk with one slot it settles as before.

A new password that already opens a key slot is refused on every drive too:
the new key's slot is how the change is followed, and an existing copy made
the run take the wrong slot as the owner's. A settled interrupted change now
says the disk opens with the password just entered, which is true whether it
finished the change or rolled it back.

* Ignore the start marker of a recording start that was killed

A start killed before it removed its marker left it behind, and a later toggle that had to wait for the lock took that marker for a start still in progress and exited without doing anything. The marker now holds the starting process's pid and counts only while that process runs.

* Say why generic aarch64 searches Omarchy's repository first

Putting [omarchy] ahead of Arch Linux ARM follows quattro's x86_64 order and its migration 1791403252, which already reorders existing aarch64 machines. It means a package Omarchy also publishes now comes from Omarchy, and a refresh can move it there, downgrading it if Omarchy's build is older. The update-process notes and the template now say so.

* Keep a working key in the manual settlement steps

When an earlier version's interrupted change has to be settled by hand, the steps now start by finding and keeping the slot of the password to keep, test the other slots with passphrases only, never remove the kept slot, and record a Mac's slots before the journal goes. Before, removing every slot the old password opened could remove the only key when the change had not landed.

* Keep Superwhisper's x86_64-only default off aarch64

quattro now ships Superwhisper as the default dictation backend and puts
superwhisper-bin in the base list, but it has no aarch64 build. On an aarch64
machine omarchy-reinstall-pkgs and omarchy-upgrade-to-quattro asked for a
package no repository carries, Setup > Defaults offered an installer that
cannot work, new users were preset to that backend, and the dictation keys, F9
and Right Alt (AltGr on many layouts) included, were bound to it.

install/omarchy-x86_64-only.packages names the base packages only x86_64
builds. omarchy-pkg-defaults leaves them out of every aarch64 platform's set,
and omarchy-upgrade-to-quattro out of the base list it installs there. The
Superwhisper default row shows on x86_64 only, like the other x86_64-only
rows. User setup on aarch64 removes the shipped Superwhisper preset, so a new
user starts with no backend and picks one; any other selection stays.
The dictation keys are bound only once the selected backend's own program is
installed, which changes nothing where it is.

* Install the stub dictation backend where binding tests expect its keys

The dictation keys are now bound only once the selected backend's program is installed. The binding-conflict check selected Voxtype without providing it, so it no longer saw the dictation keys and failed its F9 check; the default-config check now also covers a selected backend that is not installed.

* Make the stale start marker test's dead pid reliably dead

The test killed a backgrounded sleep to get a pid that no longer runs. When the signal landed before sleep started, wait never returned and the suite timed out, about one run in three. A process that exits on its own gives the same dead pid without the race.

* Drop the virtual input level meter the panel ran through a helper

Volume and mute for an untyped input still go through wpctl; typed inputs
keep Quickshell's peak monitor. Quickshell's monitor takes typed nodes only,
so the panel hides the level bar for a virtual input rather than showing an
empty bar that reads as a silent mic.

* Find recording processes with pgrep instead of a Python /proc scan

pgrep matches the kernel's 15-character process name, so gpu-screen-recorder
is matched as gpu-screen-reco. That prefix also lets any gpu-screen-reco*
executable, or a process that renames itself, pass for a recorder, so each
match is confirmed against its /proc exe link, ignoring the " (deleted)"
suffix an upgrade leaves.

--signal re-checks that link just before signalling each recorder, which
narrows the pid-reuse window the old pidfd-based scan closed, and exits 1 when
no signal was delivered. --pid rejects values above the Linux pid limit
instead of letting bash arithmetic wrap them.

* Leave 1Password's software GL wrapper to its own change

This reverts f200105c2; the render-GPU check, Electron GL wrapper and desktop entry repair return in a separate branch.

* Keep Gliff's x86_64-only package off aarch64

quattro added gliff to the base list and a migration that installs it, but its recipe builds for x86_64 alone. It joins Superwhisper in install/omarchy-x86_64-only.packages, so aarch64 package sets leave it out, and the migration installs it on x86_64 only.

* Stub accessory enrollment in the first-boot re-key test

Owner setup now enrolls the owner's USB devices and Thunderbolt accessories after the staged LUKS key is gone, and the re-key test's provisioning run reached that step with nothing to answer it. It now stubs both and checks that enrollment runs once, after the staged unlock is retired.

* Keep USB authorization from boot off Apple Silicon

Blocking USB from the start of kernel boot writes Limine's entry-tool drop-in
and checks the Limine menu and UKI directly, but a Mac's boot chain belongs to
its boot package. omarchy-setup-security-usb-authorization --boot is refused
there before anything changes, and Setup and Remove > Security hide USB at
Boot. The boot helper itself refuses enable on Apple Silicon and treats
disable as done when nothing is configured, so turning USB device
authorization off still works on a Mac; a boot-time setting configured some
other way is left with steps to remove it by hand rather than reported
removed. USB device authorization after login is unchanged.

* Skip fixture-only checks when the shell suite runs as root

Root's locale step always writes the real /etc/locale.conf and locale.gen, and root's platform check always reads this machine, so their fixtures do not apply there. The locale test now stops after its static checks as root instead of changing the runner's locale, and the Apple Silicon refusal in the USB authorization test runs unprivileged only.

* Fetch aarch64 packages from Omarchy's HTTPS Arch Linux ARM mirror

* Name platforms x86, aarch64 and aarch64-apple

omarchy-hw-platform now prints the CPU architecture, then the hardware
family when it needs handling of its own: x86, aarch64 (every ARM machine
without a family) and aarch64-apple (Apple Silicon). A later family such
as aarch64-qualcomm or aarch64-n1x follows the same form, so aarch64*
always means ARM.

The Apple predicate is now omarchy-hw-aarch64-apple; omarchy-hw-apple-silicon
stays as a hidden alias while omarchy-mac's packages and units call it.
The Apple package list and pacman templates move to
install/omarchy-aarch64-apple.packages and default/pacman/aarch64-apple/.

Image manifests and omarchy-pkg-defaults still accept apple-silicon,
generic-aarch64 and generic from builders written before the names
settled, and the mkinitcpio baseline accepts a detector that still says
apple-silicon while omarchy-settings and omarchy update apart.

* Add omarchy-hw-aarch64 and gate architecture checks on the helpers

omarchy-hw-aarch64 is the counterpart of omarchy-hw-x86: true on every ARM
machine, Apple Silicon included. The Windows VM asks omarchy-hw-x86 instead
of reading uname itself, and the keyring refresh asks omarchy-hw-aarch64, so
an x86 update no longer runs the platform detector for it.

* Document platform names, helpers and the consistency rule for agents

AGENTS.md gains a Platforms section: the x86 / aarch64 / aarch64-<family>
naming standard, which helper answers architecture and which answers
hardware family, where platform-specific packages, templates, setup and
boot code live, and that every platform should get the same experience,
with missing packages fixed upstream before any workaround and alternative
packages kept as a last resort.

---------

Co-authored-by: Marcelo Alcantara <maralc@gmail.com>
Co-authored-by: Birk Skyum <74932975+birkskyum@users.noreply.github.com>
Co-authored-by: Jimmy Van Veen <8206925+JimmayVV@users.noreply.github.com>
Co-authored-by: Scott Jones <444693+scottjones@users.noreply.github.com>
Co-authored-by: Scott Jones <scottajones@gmail.com>
Co-authored-by: Marcelo Alcantara <marcelo.alcantara@bp.com>
Co-authored-by: Oliver Lukschander <33756270+oliverlukschander@users.noreply.github.com>
Co-authored-by: Caleb Marshall <68087341+cajomar@users.noreply.github.com>
Co-authored-by: Ryan Hughes <1630358+ryanrhughes@users.noreply.github.com>
Co-authored-by: Issac Trotts <15530+ijt@users.noreply.github.com>
Co-authored-by: Gabe Ohlson <3355909+6abe@users.noreply.github.com>
Co-authored-by: Sparsh Kumar <22640187+skkumarsparsh@users.noreply.github.com>
Co-authored-by: dl-alexandre <166029845+dl-alexandre@users.noreply.github.com>
Co-authored-by: kx0101 <90087463+kx0101@users.noreply.github.com>
Co-authored-by: Jim Martin <1334144+jdvmi00@users.noreply.github.com>
Co-authored-by: Ryan Hughes <ryan@heyoodle.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Codex XHigh <noreply@openai.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Omarchybot <317366263+omarchybot@users.noreply.github.com>
Co-authored-by: Greptile <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-10-09 20:15:56 -04:00
Emir Beganović 61263219b5 Merge pull request #14771 from omacom/boot/console-no-ansi-query
Fix the 1-in-10 black screen at boot, where systemd waits on the console
2026-10-10 00:19:04 +02:00
Emir Beganović 187a145ad5 Merge pull request #14603 from manuaudio/fix/repair-webp-background-links
Repair wallpaper links after bundled WebP conversion
2026-10-10 00:18:32 +02:00
Emir Beganovic 452835da8b Read the boot loader's config only where the migration needs it
The migration read ESP_PATH from /etc/default/limine at its top. On a
machine without that file sed fails, and under bash -euo pipefail the
migration stopped there with an error, before the check that lets a
machine without Limine skip it.

The read moves to after the rebuild, the one place the path is used; a
path given for the tests is taken first, and a missing file means /boot.
2026-10-10 00:06:46 +02:00
Emir Beganovic 3824577c12 Mark the boot image migration done only when a boot entry has the parameter
limine-mkinitcpio carries on past a kernel it could not build, with a
full boot partition for one, and still exits 0. The migration then wrote
its marker over an unchanged boot image, and no later update would try
again.

After the rebuild it now looks at the boot menu: if the menu has command
lines and none carries the parameter, the migration fails and stays
pending. A menu with no command lines in it, where they are inside the
boot images, cannot be checked this way and is accepted as before.
2026-10-09 23:54:17 +02:00
Emir Beganovic 295a926ffd Test the boot image migration as omarchy-migrate runs it, and a failed rebuild
The test ran the migration with plain bash. omarchy-migrate runs it with
bash -euo pipefail, and the migration depends on that: it is what stops
a failed limine-mkinitcpio before the marker is written, so the next
update tries again. The stand-in rebuild could not fail, so nothing held
that promise.

The test now uses the same options and adds the case: a rebuild that
fails ends the migration with an error and leaves no marker, and the
next run rebuilds and marks it.
2026-10-09 23:52:07 +02:00
Emir Beganovic f03bf06742 Stop the boot from waiting on a black screen for the console to answer
About 1 boot in 10 of an installed system stops on a black screen at
"Starting Switch Root" until a key is pressed. systemd 257 and later
asks the console for its size with ANSI escape sequences when PID 1
starts and waits for the reply; with Plymouth holding the VT the reply
sometimes never comes.

Measured in QEMU on repeated boots of one install: 3 hangs in 30 and 1
in 40 on the prebuilt UKI, 7 in 40 on the stock initramfs, every one of
them ended by pressing Enter; none in 40 with a serial console, none in
40 with plymouth.enable=0, and none in 80 with
systemd.tty.term.console=dumb, which makes systemd skip the query.
systemd fixed two variants of this in v261; Arch's 261.2 still shows it.

The parameter goes where the last Plymouth boot fix went (3f725531,
initramfs_async=0): into the packaged omarchy-defaults.conf for fresh
installs, and a migration rebuilds the boot image on existing systems,
once per machine, only where the booted command line lacks it. Nothing
visible changes under "quiet splash".

Refs #2931
2026-10-09 23:26:20 +02:00
Erik Melton 2519fd9a97 Merge pull request #13047 from AFOliveira/fix/factory-reset-packaged-path-20260923
Pin factory-reset elevation to the packaged command
2026-10-09 21:17:17 +02:00
Erik Melton 5cb31317b4 Merge pull request #11874 from acrogenesis/feature/usb-device-authorization
Require approval for new USB and Thunderbolt devices by default.

Reported-by: Jurre van Bergen me@jurrevanbergen.nl
2026-10-09 21:16:12 +02:00
David Heinemeier HanssonandClaude Fable 5.1 9d48fc402e Install gliff by default and show when a remote session is active (#14712)
* Install gliff by default

Gliff is the Hyprland remote desktop over SSH. Ship it in the base
package set for new installs, with a migration for existing ones.

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

* Show a bar indicator while a gliff remote session is active

The machine being controlled should make it obvious that someone is on
it. A first-party remote-session service watches Hyprland's screencast
events, which gliff-server triggers through ext-image-copy-capture, and
probes for the server process, picking up the ssh client address from
its environment. The RemoteSession indicator in the default tray lights
up in the urgent colour while a session runs, and a notification
announces each session start and end.

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

* Address review of the remote session indicator

Probe only the current user's gliff-server processes, forward refresh
through the third-party service proxy so cloned indicator widgets can
re-probe, exercise the Hyprland screencast event path in the service
test with a grim capture instead of calling refresh by hand, and
describe the indicator as status only in the manual.

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

* Drop the remote session notifications

The bar indicator is the signal; a toast on every connect and
disconnect is noise on top of it.

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

* Tighten remote session detection after review

Hyprland reports a screencast stopped as soon as frames pause for half a
second and says nothing more when the server later exits, so poll every
two seconds while a session is active instead of trusting a stop event.
Match only the screencast event, not its screencastv2 twin, and debounce
the burst around one transition so it costs one probe. Move the probe
into its own script that the service and the test both run, poll for
state in the service test instead of fixed delays, and drop the unused
session count.

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

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-09 17:02:42 +02:00
54fc12db76 Stop push-to-talk even when other keys are pressed during the hold (#14645)
Hyprland skips a release bind once any other key or mouse button is
released while its key is held (KeybindManager::shadowKeybinds), and
when the modifiers changed since the press. That is what makes a bind
like "bindr = SUPER, SUPER_L" tap-only, but it means typing or clicking
while holding F9 or Right Alt never runs omarchy-dictation stop, so the
recording keeps going.

o.bind_hold binds a press and a release action together. The release
half is transparent (it can't be shadowed), ignores modifiers, and is
non-consuming, so it doesn't take the key from apps when pressed with
other modifiers (Shift+F9 still reaches the app). It runs only after its
own press ran, so releasing the key without the rest of a chord does
nothing. Dictation's push-to-talk keys use it.

Co-authored-by: superduck00 <294879536+superduck00@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 10:04:59 -04:00
David Heinemeier Hansson 988f44ea1a Delete extra themes from the theme picker (#14713)
* Delete extra themes from the theme picker

Delete on an extra (user-installed) theme opens the shared faded confirmation dialog; confirming runs omarchy-theme-remove and selection lands on the theme before the deleted one. Stock themes and user overrides of them are not offered for deletion, apply/re-ask are blocked while a removal runs, and row refreshes queue so a deletion never leaves the list stale.

* Fail closed on theme deletion until stock themes are known

Keep Delete disabled until the stock listing finishes successfully, so a stock override cannot look deletable in the startup window. Also hold theme apply (and further delete asks) from removal exit until the refreshed rows load, so Enter cannot apply the just-deleted theme in the gap.
2026-10-09 13:18:28 +02:00
David Heinemeier Hansson fcf9eeb5c4 Show the image picker instantly, with no backdrop (#14709)
Remove the carousel's fade and geometry transitions, hold the reveal until every visible preview has decoded and two frames have pre-rendered (falling back to revealing anyway after 400ms), and drop the fullscreen scrim and its theme tokens so the picker sits directly on the desktop. Only Escape is accepted before the picker is visible.
2026-10-09 12:21:45 +02:00
David Heinemeier Hansson d38b70c3c2 Add Starship as a default theme (#14705)
* Add Starship as a default theme

Adapted from Starship To Orbit by Erik Johansson (@erikrjohansson).
https://github.com/erikrjohansson/omarchy-starship-to-orbit-theme
Copyright (c) 2026 Erik Johansson.

Retain the basic palette and three SpaceX launch photographs, with
standard Omarchy styling. Compress the backgrounds to WebP while
preserving their full 6016 x 3384 resolution.

The photographs remain copyright SpaceX and are not covered by MIT.

* Add engine cluster background to Starship

* Use Flight 14 dawn and reorder Starship backgrounds
2026-10-09 12:19:04 +02:00
David Heinemeier Hansson 7f95505dec Slow down the reveal effect a little on background changes 2026-10-09 11:21:23 +02:00
be052339d8 Fix lopsided cursor highlight on network panel header actions (#8787)
* Fix lopsided cursor highlight on network panel header actions

The QR and speed-test header buttons used raw Button components with
asymmetric horizontal/vertical padding, producing a highlight box wider
than tall around their near-square icons. Swap them for
PanelActionButton, the canonical square inline-action component already
used elsewhere in the kit (e.g. connectPwBtn), which sizes itself
symmetrically around the icon.

Fixes #8782

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

* Center icons in PanelActionButton on whole pixels

A fractional size and anchors.centerIn put the glyph on half-pixel
coordinates, so it rounded a pixel off the center of the fill: the
speed-test icon sat low in the network header. Round the size, make it
odd so odd-width glyphs get even margins, and place the icon on whole
pixels.

Co-authored-by: Fabian Behrendt <7931874+fabianbehrendt@users.noreply.github.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Fabian Behrendt <7931874+fabianbehrendt@users.noreply.github.com>
2026-10-09 10:08:26 +02:00
manuaudio b6844dcc33 Fix WebP background migration boundaries 2026-10-08 08:52:21 -07:00
manuaudio a54bf5fd95 [verified] Repair background links after WebP conversion 2026-10-08 08:43:00 -07:00
acrogenesis c3cdcb1e82 Keep USB approvals portable and preserve approval prompts 2026-09-29 10:16:56 -06:00
acrogenesis 60d6b255da Require approval for new Thunderbolt accessories 2026-09-28 09:16:57 -06:00
Afonso Oliveira 99e3c8ca2f Test factory reset version mismatches and packaged invocation 2026-09-24 22:42:46 +01:00
Afonso Oliveira 45a6433139 Refuse factory reset handoff to a different installed version 2026-09-24 22:42:46 +01:00
Afonso Oliveira 4dd5414bfc Test factory-reset elevation paths and arguments 2026-09-23 09:45:24 +01:00
Afonso Oliveira 7d4773311e Pin factory-reset elevation to the packaged command
Use the installed executable for non-root self-elevation, including calls from a development checkout or symlink.

Reported-by: Aleksej <aleksejhairov@yandex.ru>
2026-09-23 09:45:24 +01:00
acrogenesis 27bbbb2fe9 Merge quattro into USB authorization branch 2026-09-21 08:23:51 -06:00
acrogenesis e12c8ed452 Restore signed boot files when USB boot policy changes fail 2026-09-18 13:30:03 -06:00
acrogenesis de72b64872 Re-sign historical UKIs before changing USB boot policy 2026-09-18 12:43:29 -06:00
acrogenesis db6ab5a8bf Resume USB approval verification after transient failures 2026-09-17 15:55:32 -06:00
acrogenesis 90d604f41d Expire USB approval deduplication across daemon and watcher restarts 2026-09-17 12:04:02 -06:00
acrogenesis afad853062 Disable restored USB policy until factory owner enrollment 2026-09-17 11:02:10 -06:00
acrogenesis 58a9c95bf6 Verify USB approval before reporting success 2026-09-16 16:06:13 -06:00
acrogenesis 67d173c9f9 Defer USB enrollment until owner provisioning completes 2026-09-16 16:05:59 -06:00
acrogenesis 54bacefbf4 Explain USB availability in older recovery snapshots 2026-09-15 14:11:46 -06:00
acrogenesis 9e20c2a185 Retry USB approval delivery when notifications become available 2026-09-15 14:06:32 -06:00
acrogenesis 79db1a1826 Allow USB authorization setup with an empty device inventory 2026-09-15 14:06:32 -06:00
acrogenesis 666800eb0c Persist USB boot policy in Limine snapshot manifests 2026-09-15 13:35:57 -06:00
acrogenesis 4ea410d6a1 Retry USB approval notifications after delivery failure 2026-09-15 13:35:57 -06:00
acrogenesis 7309746ac8 Fix USB approval alerts after policy changes and reconnects 2026-09-15 13:05:25 -06:00
acrogenesisandOutfoxxed 44f68f4329 Require approval for new USB devices by default
Co-authored-by: Outfoxxed <outfoxxed@outfoxxed.me>
2026-09-14 22:13:36 -06:00
434 changed files with 21417 additions and 3079 deletions

No files matched your search

+31 -7
View File
@@ -63,6 +63,30 @@ guidance does not drift from the router.
- `$OMARCHY_PATH` is set at the top level by the uwsm session environment and is always available to Omarchy runtime code.
- Commands in `bin/` and Quickshell QML should rely on `$OMARCHY_PATH` / `Quickshell.env("OMARCHY_PATH")`; do not derive fallback paths from `HOME`, `Quickshell.shellDir`, or re-export/default `OMARCHY_PATH` manually.
# Platforms
Omarchy runs on x86 and aarch64 (ARM). A platform name is the CPU architecture, then a hardware family only when that family needs handling of its own:
- `x86` - every x86_64 machine
- `aarch64` - every ARM machine without a family of its own (Snapdragon laptops, Raspberry Pis and ARM VMs today)
- `aarch64-apple` - Apple Silicon Macs
A new family is named `aarch64-<family>` (e.g. `aarch64-qualcomm`, `aarch64-n1x`) and is added only once it needs code of its own, so `aarch64*` always means ARM. `omarchy-hw-platform` prints the most specific name. Use these exact names wherever a platform is named: package lists, pacman template directories, image manifests, dispatch registration, tests and docs. Never introduce `generic`, `arm64`, `apple-silicon` or other spellings; the old `apple-silicon`, `generic-aarch64` and `generic` are read only for compatibility with older image builders.
Ask a helper; don't read `uname -m` or the device tree in feature code:
- `omarchy-hw-x86` / `omarchy-hw-aarch64` - the CPU architecture. Use them for binary and ABI availability: a package with no aarch64 build, an x86 guest VM, multilib.
- `omarchy-hw-aarch64-apple` (and later `omarchy-hw-aarch64-<family>`) - built on `omarchy-hw-platform`, image-build aware. Use them for hardware behaviour. `omarchy-hw-apple-silicon` is a hidden alias kept for the omarchy-mac packages; don't use it in new code.
Where platform code lives:
- Packages: `install/omarchy-base.packages` for everyone, `install/omarchy-aarch64.packages` for every ARM machine, `install/omarchy-<platform>.packages` for one family, and base packages with no aarch64 build in `install/omarchy-x86_64-only.packages`. `omarchy-pkg-defaults` composes them.
- Pacman templates: `default/pacman/` for x86, `default/pacman/<platform>/` for each ARM platform.
- Hardware setup: `install/hardware/<vendor-or-family>/`, gated by a predicate. `install/hardware/apple/` holds Intel Mac quirks; Apple Silicon's own setup ships in the omarchy-mac packages.
- Boot chains Omarchy can't drive generically: a platform package behind `omarchy-lifecycle-dispatch` ([`docs/lifecycle-dispatch.md`](docs/lifecycle-dispatch.md)). Platforms that boot Limine with UKIs like x86 keep the generic path.
The experience should be the same on every platform. When a package or feature is missing or broken on one, the default is to get it fixed upstream (an aarch64 build, a bug report, a patch to the maintainer) and leave the gap visible meanwhile, not to paper over it. When asked for a platform workaround, say so and propose the upstream fix first. Hiding a menu entry or skipping a package is fine only for things that can't exist there (an x86 guest VM, an x86-only proprietary binary). Alternative packages and platform-only replacements are a last resort, used only when upstream can't or won't fix it, with the reason written next to the code.
# Privileged Commands
- Follow the "Privilege Escalation" section of `default/agents/skills/omarchy/SKILL.md`. It draws the
@@ -80,7 +104,7 @@ Use these instead of raw shell commands:
- `omarchy-cmd-missing` / `omarchy-cmd-present` - check for commands
- `omarchy-pkg-missing` / `omarchy-pkg-present` - check for packages (don't use these if you can just use `omarchy-pkg-add`/`omarchy-pkg-drop`)
- `omarchy-pkg-add` - install packages (handles both pacman and AUR)
- `omarchy-pkg-add` - install packages from the configured pacman repos, Omarchy's included; use `omarchy-pkg-aur-add` for AUR packages
- `omarchy-pkg-drop` - remove packages; use this instead of raw `pacman -R*`
- `omarchy-notification-send` - send desktop notifications; do not call `notify-send` directly
- `omarchy-hw-asus-rog` - detect ASUS ROG hardware (and similar `hw-*` commands)
@@ -89,6 +113,10 @@ Commands installed by Omarchy's default package set are runtime invariants. Invo
Exceptions are allowed for migration and package-helper scripts where the helper may not be available yet, where the helper itself is being implemented, or where direct package-manager behavior is required.
# Background Work
- Long-running code (the shell, daemons, watchers) reacts to events instead of polling with a timer. Polling is the last resort, for state with no change signal; [`agents/skills/shell-dev.md`](agents/skills/shell-dev.md) lists the event sources and the push-refresh pattern.
# Menu
- The menu definition lives in `default/omarchy/omarchy-menu.jsonc`;
@@ -96,13 +124,9 @@ Exceptions are allowed for migration and package-helper scripts where the helper
- Do not add `aliases` to new menu entries. Aliases are reserved for
established alternate names users already type, kept for compatibility.
# Region
# Language
- `/etc/omarchy/region` holds `cn`; no file means Global. `omarchy-region` prints the region and switches it (`omarchy region cn|global`); the profile lives in `default/regions/cn/`, its contract in `docs/regions.md`, user-facing docs in `manual/52-china.md`.
- `install/config/region.sh` is the one system leaf for both the ISO install (the `/etc/omarchy/region` marker the installer derives from the timezone) and the runtime switch (`OMARCHY_REGION`): the region file and the profile drop-ins it symlinks. Input methods are not part of a region: the installer's keyboard choice and `omarchy-setup-input` own them (`docs/input-methods.md`).
- `omarchy-apply-pacman` renders pacman for every caller: the channel templates, the profile's `pacman/mirrorlist-<channel>.prepend` ahead of the mirrorlist, and its `pacman.conf.append`. Nothing in `omarchy update` or migrations may change the region or `/etc/locale.conf`.
- Language is separate from region: `omarchy-language en|zh` (Setup > Language) owns `LANG` in `/etc/locale.conf`, the installer sets it from its first screen, and nothing keyed by region touches it.
- China-only menu rows carry `"when":"[[ \"$(omarchy-region)\" == \"China\" ]]"` on every row, not only the submenu, because search checks each row's own guard.
- `omarchy-language en|zh` (Setup > Language) owns `LANG` in `/etc/locale.conf`; the installer sets it from its first screen. Nothing in `omarchy update` or migrations may change `/etc/locale.conf`.
- Chinese menu labels live in `default/omarchy/omarchy-menu.zh-cn.jsonc`, merged when `LANG` starts with `zh_CN`; give a new menu row with translatable words a label there.
- Shell text goes through `I18n.tr("English %1", arg)` from `qs.Commons`, with its Chinese in `shell/Commons/I18nZh.js` keyed by the English. Notifications and OSD messages are worded by their sender with `omarchy-text "English %1" "$arg"` (Python: `text()`, QML: `I18n.tr`), which reads the same table; the shell shows them as given, so user text is never translated. `test/shell.d/shell-i18n-test.sh` fails on a missing entry.
-1
View File
@@ -71,7 +71,6 @@ The manual lives in [`manual/`](manual/), which is its authoritative source.
- [Omarchy on...](manual/49-omarchy-on.md)
- [Dual Boot Install](manual/50-dual-boot-install.md)
- [Unattended Installs](manual/51-unattended-installs.md)
- [China](manual/52-china.md)
## License
+2
View File
@@ -12,6 +12,8 @@ commands and reusable setup leaves:
- avoid `exit` in sourced setup scripts unless intentionally aborting setup.
- use `$OMARCHY_INSTALL` and `$OMARCHY_PATH` instead of hard-coded Omarchy paths.
- keep root-scoped hardware setup under `install/hardware/` and orchestrate it through `install/hardware/all.sh`.
- in an image build every `run_logged` leaf in `install/hardware/all.sh` is queued and runs on the machine's first boot instead (`bin/omarchy-provision-hardware`, see `docs/file-layout.md`), possibly offline, using packages already in the image, and before any user exists; keep those leaves idempotent and put nothing but `run_logged` lines in `all.sh`.
- gate platform-specific setup on a predicate built on `omarchy-hw-platform` (`omarchy-hw-aarch64-apple`), not on `uname -m`; architecture checks (`omarchy-hw-x86`, `omarchy-hw-aarch64`) are only for ABI, binary availability or a repository's `$arch`. Platform names and where platform code lives are in the Platforms section of `AGENTS.md`.
- keep every per-user setup leaf under `install/user/` (including `install/user/hardware/` and `install/user/first-run/`) so it is clear what must run for each user.
- The base install supplies matching kernel headers before hardware setup. DKMS installers should install their driver packages and assume the headers exist.
- prefer helper commands for package and command checks where available.
+11
View File
@@ -42,6 +42,17 @@ Wrap every user-visible string in `I18n.tr()` (`import qs.Commons`) and add its
bar widgets register one target each — `omarchy.indicators`,
`omarchy.system-update`, `omarchy.clock`. There is no `bar` target.
## Keeping state current without polling
The shell runs all day on battery, and a widget is built once per monitor, so a `Timer` that starts a `Process` costs that process times the number of monitors, every tick, forever. Don't poll for state that only changes through known events. Pick the first of these that fits:
- A native Quickshell service that signals on change: `UPower` and `PowerProfiles` (`Quickshell.Services.UPower`), `Hyprland` and each workspace's `lastIpcObject`, `Pipewire`, `Mpris`, `SystemTray`, `Notifications`.
- An event stream the shell already runs, such as the Hyprland IPC events or the long-running `pactl subscribe` in `Commons/UntypedInput.qml`. Extend it rather than starting a second one.
- A file watch: `FileView` with `watchChanges`, or the directory watch in `plugins/bar/Bar.qml`, when the state lives in a file the user can read.
- A push from the command that changes the state. If only Omarchy commands change it, have them call `omarchy-shell -q <target> refresh` after the change, the way `omarchy-reminder` and `omarchy-sudo-passwordless` do. When the change happens later, without a command running (an expiry, say), schedule the refresh with a transient `systemd-run --user --on-active=…` timer. A root-owned timer cannot reach the user's shell.
Polling is the last resort, kept for state with no change signal at all (a remote API, say). Then gate the timer on the panel being open where you can, pick the longest interval the UI can tolerate, and put the poll in one shared service rather than in a per-monitor widget. Never poll a privileged probe: every `sudo -n` can write to the auth log.
## Editing widget files with glyphs
Widget files in `shell/plugins/bar/widgets/` contain Nerd Font glyphs as raw
+1
View File
@@ -2,6 +2,7 @@
Version=1.0
Name=Basecamp
Exec=omarchy-launch-webapp https://launchpad.37signals.com
StartupWMClass=chrome-launchpad.37signals.com__-Default
Terminal=false
Type=Application
Icon=basecamp
+1
View File
@@ -2,6 +2,7 @@
Version=1.0
Name=Discord
Exec=omarchy-launch-webapp https://discord.com/channels/@me
StartupWMClass=chrome-discord.com__channels_@me-Default
Terminal=false
Type=Application
Icon=omarchy-discord
+1
View File
@@ -2,6 +2,7 @@
Version=1.0
Name=Google Contacts
Exec=omarchy-launch-webapp https://contacts.google.com/
StartupWMClass=chrome-contacts.google.com__-Default
Terminal=false
Type=Application
Icon=google-contacts
+1
View File
@@ -2,6 +2,7 @@
Version=1.0
Name=Google Maps
Exec=omarchy-launch-webapp https://maps.google.com
StartupWMClass=chrome-maps.google.com__-Default
Terminal=false
Type=Application
Icon=google-maps
+1
View File
@@ -2,6 +2,7 @@
Version=1.0
Name=Google Messages
Exec=omarchy-launch-webapp https://messages.google.com/web/conversations
StartupWMClass=chrome-messages.google.com__web_conversations-Default
Terminal=false
Type=Application
Icon=google-messages
+1
View File
@@ -2,6 +2,7 @@
Version=1.0
Name=Google Photos
Exec=omarchy-launch-webapp https://photos.google.com/
StartupWMClass=chrome-photos.google.com__-Default
Terminal=false
Type=Application
Icon=google-photos
+1
View File
@@ -2,6 +2,7 @@
Version=1.0
Name=WhatsApp
Exec=omarchy-launch-webapp https://web.whatsapp.com/
StartupWMClass=chrome-web.whatsapp.com__-Default
Terminal=false
Type=Application
Icon=whatsapp
+1
View File
@@ -2,6 +2,7 @@
Version=1.0
Name=X
Exec=omarchy-launch-webapp https://x.com/
StartupWMClass=chrome-x.com__-Default
Terminal=false
Type=Application
Icon=x
+1
View File
@@ -2,6 +2,7 @@
Version=1.0
Name=YouTube
Exec=omarchy-launch-webapp https://youtube.com/
StartupWMClass=chrome-youtube.com__-Default
Terminal=false
Type=Application
Icon=youtube
-1
View File
@@ -75,7 +75,6 @@ GROUP_DESCRIPTIONS[plymouth]="Plymouth boot theme management"
GROUP_DESCRIPTIONS[power]="Power supply detection"
GROUP_DESCRIPTIONS[powerprofiles]="Power profile management"
GROUP_DESCRIPTIONS[refresh]="Reset config to defaults"
GROUP_DESCRIPTIONS[region]="Region-specific package sources and setup"
GROUP_DESCRIPTIONS[reinstall]="Reinstall and reset workflows"
GROUP_DESCRIPTIONS[reminder]="Desktop notification reminders"
GROUP_DESCRIPTIONS[remove]="Removal workflows"
+15 -1
View File
@@ -19,6 +19,9 @@ for diagnostics or after hardware/package updates.
--defer-provisioning runs without an install user (deferred-provisioning installs create the user at
first boot); group grants are recorded in /var/lib/omarchy/provisioning/groups.
In an image build (/var/lib/omarchy/image/target exists) nothing runs here: every step is queued
for omarchy-provision-hardware on the machine's first boot.
USAGE
}
@@ -66,10 +69,21 @@ else
fi
export OMARCHY_INSTALL_USER="$install_user"
# Only omarchy-provision-hardware runs steps as an image's first boot.
unset OMARCHY_IMAGE_DEFERRED_HARDWARE
export OMARCHY_PATH="${OMARCHY_PATH:-/usr/share/omarchy}"
export OMARCHY_INSTALL="${OMARCHY_INSTALL:-$OMARCHY_PATH/install}"
export OMARCHY_INSTALL_LOG_FILE="${OMARCHY_INSTALL_LOG_FILE:-/var/log/omarchy-install.log}"
export PATH="$OMARCHY_PATH/bin:$PATH"
source "$OMARCHY_INSTALL/helpers/logging.sh"
source "$OMARCHY_INSTALL/hardware/all.sh"
source "$OMARCHY_INSTALL/helpers/image-target.sh"
omarchy_image_init
omarchy_image_lock
if omarchy_image_manifest_present; then
omarchy_image_read_manifest
omarchy_image_defer_hardware
else
source "$OMARCHY_INSTALL/hardware/all.sh"
fi
-77
View File
@@ -1,77 +0,0 @@
#!/bin/bash
# omarchy:summary=Restore channel and regional pacman defaults in an installed target
# omarchy:args=<stable|rc|edge> [target-root] | --render <stable|rc|edge> <output-dir> [target-root]
# omarchy:requires-sudo=true
# omarchy:hidden=true
set -euo pipefail
usage() {
echo "Usage: omarchy-apply-pacman <stable|rc|edge> [target-root]" >&2
echo " omarchy-apply-pacman --render <stable|rc|edge> <output-dir> [target-root]" >&2
exit 1
}
# --render only builds the two files, so an unprivileged caller can hand root
# nothing but plain copies of the finished result.
render_dir=""
if [[ ${1:-} == "--render" ]]; then
(( $# == 3 || $# == 4 )) || usage
channel="$2"
render_dir="$3"
[[ -d $render_dir ]] || { echo "Render directory not found: $render_dir" >&2; exit 1; }
etc="${4:-/}/etc"
else
(( $# >= 1 && $# <= 2 )) || usage
channel="$1"
etc="${2:-/}/etc"
fi
case "$channel" in
stable|rc|edge) ;;
*) echo "Invalid pacman channel: $channel" >&2; exit 1 ;;
esac
# The ISO writes this plain-text marker before target finalization. Missing
# markers keep existing installations global; never source it as shell code.
region=global
if [[ -e $etc/omarchy/region ]]; then
region=$(<"$etc/omarchy/region")
fi
case "$region" in
global|cn) ;;
*) echo "Unsupported region: $region (expected global or cn)" >&2; exit 1 ;;
esac
# Build both files before replacing either. Explicit refreshes retain backups;
# ordinary updates do not call this command or enforce regional defaults.
if [[ -n $render_dir ]]; then
staging="$render_dir"
else
staging=$(mktemp -d)
trap 'rm -rf "$staging"' EXIT
fi
cp "$OMARCHY_PATH/default/pacman/pacman-$channel.conf" "$staging/pacman.conf"
if [[ $region == "global" ]]; then
cp "$OMARCHY_PATH/default/pacman/mirrorlist-$channel" "$staging/mirrorlist"
else
profile="$OMARCHY_PATH/default/regions/$region/pacman"
printf '\n' >> "$staging/pacman.conf"
cat "$profile/pacman.conf.append" >> "$staging/pacman.conf"
# pacman tries servers in order, so the regional mirror leads and the
# channel's own mirror stays as the fallback.
cat "$profile/mirrorlist-$channel.prepend" "$OMARCHY_PATH/default/pacman/mirrorlist-$channel" > "$staging/mirrorlist"
fi
if [[ -z $render_dir ]]; then
mkdir -p "$etc/pacman.d"
for file in pacman.conf pacman.d/mirrorlist; do
if [[ -e $etc/$file ]]; then
cp -f "$etc/$file" "$etc/$file.bak"
fi
done
# Copying over an existing file keeps its owner and mode, as refresh always has.
cp -f "$staging/pacman.conf" "$etc/pacman.conf"
cp -f "$staging/mirrorlist" "$etc/pacman.d/mirrorlist"
fi
+9 -1
View File
@@ -15,6 +15,14 @@ fi
wpctl set-default "$node_id" 2>/dev/null || true
pactl set-default-source "$source_name" 2>/dev/null || true
pactl list short source-outputs 2>/dev/null | awk '{ print $1 }' | while read -r output; do
# A filter's own capture (the input half of a filter chain, loopback or echo
# canceller, which PipeWire joins to its output by node.link-group) is the
# processing, not an app: moving it would feed the filter from the new input, or
# from its own output. pactl translates the listing, so it is read in the C
# locale.
LC_ALL=C pactl list source-outputs 2>/dev/null | awk '
/^Source Output #/ { if (id != "" && !filter) print id; id = substr($3, 2); filter = 0 }
/^[[:space:]]*node\.link-group = / { filter = 1 }
END { if (id != "" && !filter) print id }' | while read -r output; do
[[ -n $output ]] && pactl move-source-output "$output" "$source_name" 2>/dev/null || true
done
+17 -4
View File
@@ -1,14 +1,27 @@
#!/bin/bash
# omarchy:summary=Print PulseAudio sink availability for the shell
# omarchy:summary=Print PulseAudio sink or source availability for the shell
# omarchy:group=audio
# omarchy:args=[sources]
# With "sources", print source availability instead: a jack input such as a
# headset microphone reports its port unavailable while nothing is plugged in.
# pactl translates the listing ("not available" too), so it is read in the C
# locale.
kind=sinks
header="Sink #"
if [[ ${1:-} == "sources" ]]; then
kind=sources
header="Source #"
fi
# A speaker tuning is a virtual sink in front of the real speakers. Both exist
# in the graph, but selecting the physical one would only bypass the tuning, so
# report it unavailable and keep it out of the output list.
fronted="$(omarchy-audio-tuning fronted-sink 2>/dev/null || true)"
fronted=""
[[ $kind == sinks ]] && fronted="$(omarchy-audio-tuning fronted-sink 2>/dev/null || true)"
pactl list sinks 2>/dev/null | awk -v fronted="$fronted" '
LC_ALL=C pactl list "$kind" 2>/dev/null | awk -v fronted="$fronted" -v header="$header" '
function emit_sink() {
if (name == "") return
if (fronted != "" && name == fronted) {
@@ -18,7 +31,7 @@ pactl list sinks 2>/dev/null | awk -v fronted="$fronted" '
print name "\t" ((port_count == 0 || available) ? 1 : 0)
}
/^Sink #/ {
index($0, header) == 1 {
emit_sink()
name = ""
in_ports = 0
+104 -10
View File
@@ -2,7 +2,7 @@
# omarchy:summary=Configure the bar and its widget layout
# omarchy:group=bar
# omarchy:args=use <id> | reset | defaults | position <top|bottom|left|right> | transparent <true|false|toggle> | put <id> [placement] | move <id> [placement] | set <id> <key> <value> [--json] [placement]
# omarchy:args=use <id> | reset | defaults | position [top|bottom|left|right] | background [opaque|glassy|none|cycle] | put <id> [placement] | move <id> [placement] | set <id> <key> <value> [--json] [placement] | get <id> <key>
# omarchy:examples=omarchy bar use local.neon-bar | omarchy bar put omarchy.keyboard-layout --after omarchy.clock | omarchy bar move omarchy.clock --section center --index 0 | omarchy bar set omarchy.clock format HH:mm
set -euo pipefail
@@ -16,13 +16,19 @@ Usage: omarchy bar <command> [args...]
use <id> Use a bar option as the active bar
reset Return to the built-in Omarchy bar
defaults Restore the default bar and service widgets
position <top|bottom|left|right> Bar position
transparent <true|false|toggle> Bar transparency
position [top|bottom|left|right] Bar position; prints the current one
when given none
background [opaque|glassy|none|cycle]
Bar background: glassy shows what is
behind the bar blurred, cycle steps
none, glassy, opaque; prints the
current one when given none
put <id> [placement] Put a widget on the bar, leaving one
that is already there where it is
move <id> [placement] Move a widget within or between sections
set <id> <key> <value> [--json] [placement]
Set a per-widget option
get <id> <key> Print a per-widget option
Placement:
--section <left|center|right> Target section
@@ -192,30 +198,92 @@ cmd_defaults() {
" \
--slurpfile defaults "$DEFAULTS_FILE" \
--argjson widgets "$optional_widgets"
sync_glass || fail "could not change the blur behind the bar"
echo "Restored the default Omarchy bar"
}
cmd_position() {
local position="${1:-}"
[[ -n $position ]] || fail "position is required"
if (( $# == 0 )); then
jq -r '.bar.position // "top"' "$(source_file)"
return
fi
local position="$1"
(( $# == 1 )) || fail "position takes a single value"
[[ $position =~ ^(top|bottom|left|right)$ ]] || fail "position must be top, bottom, left, or right"
commit "$NORMALIZE | .bar.position = \$position" --arg position "$position"
echo "Bar position set to $position"
}
# The bar background is opaque, glassy, or none. A shell.json from before it
# had one says transparent instead, which is none, and setting a background
# drops it.
current_background() {
jq -r '.bar.background // (if .bar.transparent == true then "none" else "opaque" end)' "$(source_file)"
}
cmd_background() {
if (( $# == 0 )); then
current_background
return
fi
local background="$1"
(( $# == 1 )) || fail "background takes a single value"
[[ $background =~ ^(opaque|glassy|none|cycle)$ ]] || fail "background must be opaque, glassy, none, or cycle"
if [[ $background == "cycle" ]]; then
# Each step changes the bar by one degree: clear, then glassy, then opaque.
case $(current_background) in
none) background="glassy" ;;
glassy) background="opaque" ;;
*) background="none" ;;
esac
fi
local previous
previous=$(current_background)
commit "$NORMALIZE | .bar.background = \$background | del(.bar.transparent)" --arg background "$background"
if ! sync_glass; then
commit "$NORMALIZE | .bar.background = \$background | del(.bar.transparent)" --arg background "$previous"
fail "could not change the blur behind the bar"
fi
echo "Bar background set to $background"
}
# The older on/off form of the background, kept for scripts and dotfiles:
# transparent is none, and not transparent is opaque.
cmd_transparent() {
local transparent="${1:-}"
[[ -n $transparent ]] || fail "transparent is required"
(( $# == 1 )) || fail "transparent takes a single value"
[[ $transparent =~ ^(true|false|toggle)$ ]] || fail "transparent must be true, false, or toggle"
if [[ $transparent == "toggle" ]]; then
commit "$NORMALIZE | .bar.transparent = (.bar.transparent != true)"
echo "Bar transparency toggled"
else
commit "$NORMALIZE | .bar.transparent = \$transparent" --argjson transparent "$transparent"
echo "Bar transparency set to $transparent"
[[ $(current_background) == "none" ]] && transparent="false" || transparent="true"
fi
if [[ $transparent == "true" ]]; then
cmd_background none
else
cmd_background opaque
fi
}
# Hyprland blurs behind the bar only while the bar-glassy toggle is on, so it
# follows the bar background, reloading Hyprland only when it changes. The
# toggle sits where Hyprland reads it, under XDG_STATE_HOME.
sync_glass() {
local dir="${XDG_STATE_HOME:-$HOME/.local/state}/omarchy/toggles/hypr"
local flag="$dir/bar-glassy.lua"
if [[ $(current_background) == "glassy" ]]; then
[[ ! -f $flag ]] || return 0
mkdir -p "$dir" && cp "$OMARCHY_PATH/default/hypr/toggles/bar-glassy.lua" "$flag" || return 1
else
[[ -f $flag ]] || return 0
rm -f "$flag" || return 1
fi
hyprctl reload >/dev/null 2>&1 || true
}
bar_widget_default_section() {
@@ -363,6 +431,21 @@ cmd_set() {
echo "Set $key on $id"
}
# Prints the option from the first instance of the widget on the bar, raw for a
# string and as JSON otherwise, and fails when no instance sets it.
cmd_get() {
local id="${1:-}"
local key="${2:-}"
[[ -n $id ]] || fail "get requires a widget id"
[[ -n $key ]] || fail "get requires a setting key"
(( $# == 2 )) || fail "get takes a widget id and a setting key"
jq -e -r --arg id "$id" --arg key "$key" '
first(.bar.layout[]?[]? | select(.id == $id) | .[$key] | select(. != null))
| if type == "string" then . else tojson end
' "$(source_file)" 2>/dev/null || exit 1
}
# --------------------------------------------------------------------- dispatch
command="${1:-}"
@@ -385,6 +468,14 @@ case "$command" in
transparent)
cmd_transparent "$@"
;;
background)
cmd_background "$@"
;;
# Internal: the shell runs this when it loads a bar background, so one set
# by editing shell.json gets its blur too.
sync-glass)
sync_glass || fail "could not change the blur behind the bar"
;;
put)
cmd_put "$@"
;;
@@ -394,6 +485,9 @@ case "$command" in
set)
cmd_set "$@"
;;
get)
cmd_get "$@"
;;
-h | --help | help | "")
usage
;;
+10 -4
View File
@@ -2,10 +2,16 @@
# omarchy:summary=Returns true if a battery is present on the system.
for bat in /sys/class/power_supply/BAT*; do
[[ -r $bat/present ]] &&
[[ $(cat $bat/present) == "1" ]] &&
[[ $(cat $bat/type) == "Battery" ]] &&
# Not every battery is called BAT*: Apple Silicon reports macsmc-battery and
# some Framework and Chromebook firmware CMB0, so decide by type, not by name.
power_supply_path="${OMARCHY_POWER_SUPPLY_PATH:-/sys/class/power_supply}"
for bat in "$power_supply_path"/*; do
[[ -r $bat/present && $(<"$bat/present") == "1" ]] || continue
[[ -r $bat/type && $(<"$bat/type") == "Battery" ]] || continue
# A wireless mouse or keyboard registers a Battery too, scoped Device. The
# one that powers the machine is scoped System, or unscoped on older drivers.
[[ -r $bat/scope && $(<"$bat/scope") == "Device" ]] && continue
exit 0
done
+27 -6
View File
@@ -18,11 +18,23 @@ case "${1:-}" in
;;
esac
battery=$(upower -e 2>/dev/null | grep BAT | head -n 1)
# UPower names the device after the sysfs entry, and only some kernels call
# that BAT*: Apple Silicon reports battery_macsmc_battery. Match on the class,
# then take the first one that powers the machine: a wireless mouse or keyboard
# is a battery_ device too, and UPower marks it "power supply: no".
battery=""
battery_info=""
while read -r candidate; do
[[ -n $candidate ]] || continue
candidate_info=$(upower -i "$candidate" 2>/dev/null) || continue
if grep -qE '^\s*power supply:\s+yes' <<<"$candidate_info"; then
battery=$candidate
battery_info=$candidate_info
break
fi
done < <(upower -e 2>/dev/null | grep '/battery_')
[[ -z $battery ]] && exit 0
battery_info=$(upower -i "$battery")
percentage=$(awk '/percentage/ { print int($2); exit }' <<<"$battery_info")
capacity=$(awk '/energy-full:/ { printf "%d", $2; exit }' <<<"$battery_info")
time_remaining=$(awk '/time to (empty|full)/ {
@@ -56,7 +68,10 @@ elif [[ -r $battery_path/current_now && -r $battery_path/voltage_now ]]; then
'BEGIN { print microamps * microvolts / 1000000000000 }')
fi
# Some kernels sign power_now by direction (negative while discharging on
# Apple Silicon); the state field already says which way it flows.
power_rate=$(awk -v rate="${power_rate_raw:-0}" 'BEGIN {
if (rate < 0) rate = -rate
rounded = sprintf("%.1f", rate)
sub(/\.0$/, "", rounded)
print rounded
@@ -65,8 +80,12 @@ state=$(awk '/state/ { print $2; exit }' <<<"$battery_info")
threshold_start=$(awk '/charge-start-threshold:/ { gsub(/%/, "", $2); print int($2); exit }' <<<"$battery_info")
threshold_end=$(awk '/charge-end-threshold:/ { gsub(/%/, "", $2); print int($2); exit }' <<<"$battery_info")
[[ -z $threshold_end ]] && threshold_end=$(cat "$power_supply_path"/BAT*/charge_control_end_threshold 2>/dev/null | head -1)
[[ -z $threshold_start ]] && threshold_start=$(cat "$power_supply_path"/BAT*/charge_control_start_threshold 2>/dev/null | head -1)
# Read the thresholds from the battery UPower resolved rather than a BAT* glob,
# which misses every battery with another name.
[[ -n $threshold_end || ! -r $battery_path/charge_control_end_threshold ]] || threshold_end=$(<"$battery_path/charge_control_end_threshold")
[[ -n $threshold_start || ! -r $battery_path/charge_control_start_threshold ]] || threshold_start=$(<"$battery_path/charge_control_start_threshold")
[[ -n $threshold_end ]] || threshold_end=$(cat "$power_supply_path"/BAT*/charge_control_end_threshold 2>/dev/null | head -1)
[[ -n $threshold_start ]] || threshold_start=$(cat "$power_supply_path"/BAT*/charge_control_start_threshold 2>/dev/null | head -1)
ac_online=false
for supply in "$power_supply_path"/*; do
@@ -107,7 +126,9 @@ if [[ $shell_output == "true" ]]; then
printf 'size\t%s\n' "${capacity}Wh"
printf 'time\t%s\n' "$time_remaining"
cycles=$(cat "$power_supply_path"/BAT*/cycle_count 2>/dev/null | head -1)
cycles=""
[[ ! -r $battery_path/cycle_count ]] || cycles=$(<"$battery_path/cycle_count")
[[ -n $cycles ]] || cycles=$(cat "$power_supply_path"/BAT*/cycle_count 2>/dev/null | head -1)
[[ -n $cycles ]] && printf 'cycles\t%s\n' "$cycles"
+28 -1
View File
@@ -40,7 +40,30 @@ use_apple_display() {
}
use_ddc_display() {
[[ -n $monitor ]] && ! monitor_is_internal
[[ -n $monitor ]] && ! monitor_is_internal && monitor_may_have_ddc
}
# A platform whose display driver registers no DDC channel says so in its
# displays.conf (see docs/file-layout.md): there a probe finds only unrelated
# I2C buses, slowly, so only a connector with a ddc node is probed. Everywhere
# else every external monitor is.
monitor_may_have_ddc() {
local displays_conf=/usr/share/omarchy-platform/displays.conf directive argument
[[ -r $displays_conf ]] || return 0
while IFS=$' \t\r' read -r directive argument || [[ -n $directive ]]; do
if [[ $directive == "ddc-require-connector-ddc" && -z $argument ]]; then
compgen -G "/sys/class/drm/card*-$monitor/ddc" >/dev/null
return
fi
done <"$displays_conf"
return 0
}
# The kernel backlight is the built-in panel's, so an external monitor without
# DDC fails rather than dimming the panel.
use_backlight_display() {
[[ -z $monitor ]] || monitor_is_internal
}
if (( $# == 0 )); then
@@ -50,6 +73,8 @@ if (( $# == 0 )); then
elif use_ddc_display; then
omarchy-brightness-display-ddc "$monitor"
exit
elif ! use_backlight_display; then
exit 1
fi
device="$(omarchy-hw-display)" || exit 1
@@ -85,6 +110,8 @@ if use_apple_display; then
elif use_ddc_display; then
brightness="$(omarchy-brightness-display-ddc "$monitor" "$step")" || exit 1
(( no_osd )) || omarchy-osd -i brightness -p "$brightness"
elif ! use_backlight_display; then
exit 1
else
# Current device highlighted
device="$(omarchy-hw-display)" || exit 1
+309 -48
View File
@@ -2,7 +2,7 @@
# omarchy:summary=Start or stop screen recording
# omarchy:group=capture
# omarchy:args=[--fullscreen] [--with-desktop-audio] [--with-microphone-audio] [--with-webcam] [--webcam-device=<device>] [--webcam-size=<small|medium|large>] [--resolution=<size>] [--stop-recording]
# omarchy:args=[--fullscreen] [--with-desktop-audio] [--with-microphone-audio] [--with-webcam] [--webcam-device=<device>] [--webcam-size=<small|medium|large>] [--resolution=<size>] [--stop-recording] [--status]
# omarchy:examples=omarchy screenrecord | omarchy capture screenrecording --with-desktop-audio
# omarchy:aliases=omarchy screenrecord
#
@@ -14,19 +14,20 @@
# DMA-BUF modifier import on some configurations, leaving recording unable to
# start.
#
# Env: OMARCHY_SCREENRECORD_DEBUG=true appends gpu-screen-recorder's stderr (and
# the picker target it was launched with) to
# $XDG_RUNTIME_DIR/omarchy-screenrecord.log so users can attach a log when
# reporting capture failures.
# Env: OMARCHY_SCREENRECORD_DEBUG=true appends the recorder's stderr (and the
# picker target it was launched with) to $XDG_RUNTIME_DIR/omarchy-screenrecord.log
# so users can attach a log when reporting capture failures.
#
# Backend: gpu-screen-recorder. Where it is not installed, or it exits before
# the recording starts (it knows only NVIDIA, AMD, Intel and Broadcom GPUs, and
# aborts on others such as Apple Silicon's), wf-recorder records instead when
# it is installed. wf-recorder encodes on the CPU and captures one audio
# device, so desktop and microphone audio together go through a temporary null
# sink. Portal capture and --resolution are gpu-screen-recorder only.
[[ -f ~/.config/user-dirs.dirs ]] && source ~/.config/user-dirs.dirs
OUTPUT_DIR="${OMARCHY_SCREENRECORD_DIR:-${XDG_VIDEOS_DIR:-$HOME/Videos}}"
if [[ ! -d $OUTPUT_DIR ]]; then
omarchy-notification-send -u critical -t 3000 "$(omarchy-text "Screen recording directory does not exist: %1" "$OUTPUT_DIR")"
exit 1
fi
DESKTOP_AUDIO="false"
MICROPHONE_AUDIO="false"
WEBCAM="false"
@@ -35,6 +36,7 @@ WEBCAM_SIZE="medium"
RESOLUTION=""
FULLSCREEN="false"
STOP_RECORDING="false"
STATUS="false"
# Both of these live in the per-user runtime directory, which is 0700. What
# RECORDING_FILE holds is read back on stop and used as a path -- ffmpeg writes
# beside it, `mv` replaces it, and `rm -f` deletes its preview -- so a name in
@@ -46,6 +48,17 @@ STOP_RECORDING="false"
RUNTIME_DIR="${XDG_RUNTIME_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/omarchy}"
RECORDING_FILE="$RUNTIME_DIR/omarchy-screenrecord-filename"
REGION_FILE="$RUNTIME_DIR/omarchy-screenrecord-region"
# The recorder's pid, so stop and status act on that process alone.
REC_PID_FILE="$RUNTIME_DIR/omarchy-screenrecord-pid"
# The PulseAudio module ids of a combined-audio null sink, for cleanup.
PA_MODULES_FILE="$RUNTIME_DIR/omarchy-screenrecord-pa-modules"
# Present while a start holds the lock; another toggle or stop then leaves
# CANCEL_FILE for that start instead of queueing behind its picker or recorder.
STARTING_FILE="$RUNTIME_DIR/omarchy-screenrecord-starting"
CANCEL_FILE="$RUNTIME_DIR/omarchy-screenrecord-cancel"
# How long a recorder may take to create its file, except through the portal,
# whose picker waits on the user.
READY_TIMEOUT_TICKS=75
LOG_FILE=$([[ ${OMARCHY_SCREENRECORD_DEBUG:-false} == "true" ]] && echo "$RUNTIME_DIR/omarchy-screenrecord.log" || echo "/dev/null")
for arg in "$@"; do
@@ -58,6 +71,7 @@ for arg in "$@"; do
--resolution=*) RESOLUTION="${arg#*=}" ;;
--fullscreen) FULLSCREEN="true" ;;
--stop-recording) STOP_RECORDING="true" ;;
--status) STATUS="true" ;;
esac
done
@@ -69,6 +83,13 @@ small | medium | large) ;;
;;
esac
# --status only answers whether our recording runs, for the bar and the menu, so
# it needs no recordings directory and never notifies.
if [[ $STATUS != "true" && ! -d $OUTPUT_DIR ]]; then
omarchy-notification-send -u critical -t 3000 "$(omarchy-text "Screen recording directory does not exist: %1" "$OUTPUT_DIR")"
exit 1
fi
start_webcam_overlay() {
cleanup_webcam
@@ -99,7 +120,7 @@ start_webcam_overlay() {
'--vf=lavfi=[crop=ih*8/9:ih]' \
--title="WebcamOverlay" --wayland-app-id="WebcamOverlay-$WEBCAM_SIZE" \
--no-border --no-audio --no-osc --osd-level=0 \
--really-quiet &>/dev/null &
--really-quiet 9>&- &>/dev/null &
# The move has to settle before gpu-screen-recorder starts, or the camera is
# recorded sliding into its corner. Waiting for the map is what the blind
@@ -153,12 +174,134 @@ select_capture_target() {
echo "region:${BASH_REMATCH[3]}x${BASH_REMATCH[4]}+${BASH_REMATCH[1]}+${BASH_REMATCH[2]}"
}
start_screenrecording() {
mkdir -p "$RUNTIME_DIR" || return 1
# XDG_STATE_HOME may be outside a private home; protect existing fallbacks too.
if [[ -z ${XDG_RUNTIME_DIR:-} ]]; then
chmod 700 "$RUNTIME_DIR" || return 1
# gpu-screen-recorder first, then wf-recorder, of those installed. The base
# packages carry gpu-screen-recorder, but an architecture may build none.
recorder_backends() {
local backend found=false
for backend in gpu-screen-recorder wf-recorder; do
if omarchy-cmd-present "$backend"; then
echo "$backend"
found=true
fi
done
# Neither installed: fail the way a missing gpu-screen-recorder always has.
$found || echo gpu-screen-recorder
}
# wf-recorder records a single audio device. Map the desktop/mic flags onto one:
# desktop -> default sink monitor, mic -> default source. When both are requested,
# route them through a throwaway null sink so a single track carries both; the
# module ids are saved to PA_MODULES_FILE so stop can unload them. A requested
# source that can't be recorded is left out with a notification naming it, and
# the recording carries whichever source remains.
wf_audio_device() {
local sink="" source="" m
[[ $DESKTOP_AUDIO == "true" ]] && sink=$(pactl get-default-sink 2>/dev/null)
[[ $MICROPHONE_AUDIO == "true" ]] && source=$(pactl get-default-source 2>/dev/null)
if [[ -n $sink && -n $source ]]; then
if m=$(pactl load-module module-null-sink sink_name=omarchy_screenrec sink_properties=device.description=omarchy_screenrec 2>/dev/null); then
echo "$m" >"$PA_MODULES_FILE"
if m=$(pactl load-module module-loopback source="${sink}.monitor" sink=omarchy_screenrec latency_msec=20 2>/dev/null); then
echo "$m" >>"$PA_MODULES_FILE"
else
sink=""
fi
if m=$(pactl load-module module-loopback source="$source" sink=omarchy_screenrec latency_msec=20 2>/dev/null); then
echo "$m" >>"$PA_MODULES_FILE"
else
source=""
fi
if [[ -n $sink && -n $source ]]; then
echo "omarchy_screenrec.monitor"
return
fi
wf_audio_cleanup
else
# No device to mix into: desktop audio records alone, as it always has.
source=""
fi
fi
[[ $DESKTOP_AUDIO == "true" && -z $sink ]] && wf_audio_unavailable desktop
[[ $MICROPHONE_AUDIO == "true" && -z $source ]] && wf_audio_unavailable microphone
if [[ -n $sink ]]; then
echo "${sink}.monitor"
elif [[ -n $source ]]; then
echo "$source"
fi
}
wf_audio_unavailable() {
local audio
audio=$(omarchy-text "$1")
omarchy-notification-send -u critical -t 5000 "$(omarchy-text "Recording without %1 audio" "$audio")" "$(omarchy-text "The %1 audio could not be added to this recording." "$audio")" >/dev/null 2>&1
}
wf_audio_cleanup() {
[[ -f $PA_MODULES_FILE ]] || return
# Unload in reverse so the loopbacks drop before the null sink they feed.
tac "$PA_MODULES_FILE" 2>/dev/null | while read -r m; do
[[ -n $m ]] && pactl unload-module "$m" 2>/dev/null
done
rm -f "$PA_MODULES_FILE"
}
# Launches wf-recorder for the given picker target, backgrounded, with REC_PID set.
launch_wf_recorder() {
local target=$1 filename=$2
local geom_args=()
case $target in
monitor:*)
geom_args=(-o "${target#monitor:}")
;;
region:*)
# region:WxH+X+Y (logical coords) -> wf-recorder -g "X,Y WxH" (same space).
[[ ${target#region:} =~ ^([0-9]+)x([0-9]+)\+(-?[0-9]+)\+(-?[0-9]+)$ ]] || return 1
geom_args=(-g "${BASH_REMATCH[3]},${BASH_REMATCH[4]} ${BASH_REMATCH[1]}x${BASH_REMATCH[2]}")
;;
*)
return 1
;;
esac
local audio_args=() dev
dev=$(wf_audio_device)
[[ -n $dev ]] && audio_args=(--audio="$dev")
# -r 60 caps the framerate: capturing a 120 Hz panel at its native rate makes
# needlessly large files. -R 48000 keeps the AAC track at 48 kHz rather than
# following a sink that advertises 96 kHz into a file many players, and some
# speaker DSPs, play as silence.
wf-recorder "${geom_args[@]}" -r 60 -R 48000 "${audio_args[@]}" -f "$filename" 9>&- 2>>"$LOG_FILE" &
REC_PID=$!
}
start_cancelled() {
[[ -e $CANCEL_FILE ]]
}
# Stops a recorder this start launched: TERM, two seconds to exit, then KILL.
end_launched_recorder() {
local pid=$1 waited=0 state
kill "$pid" 2>/dev/null
while (( waited++ < 20 )); do
state=$(<"/proc/$pid/stat") 2>/dev/null || break
state=${state##*) }
[[ ${state:0:1} != "Z" ]] || break
sleep 0.1
done
kill -KILL "$pid" 2>/dev/null
wait "$pid" 2>/dev/null
}
start_screenrecording() {
# A pid left by a recording that ended without a stop would otherwise hide
# this one from the toggle until it has started and saved its own.
rm -f "$REC_PID_FILE"
local capture_args=()
local target
@@ -174,6 +317,7 @@ start_screenrecording() {
capture_args=(-w portal -s "${RESOLUTION:-$(default_resolution)}")
else
target=$(select_capture_target) || return 1
start_cancelled && return 1
case $target in
monitor:*)
@@ -187,50 +331,101 @@ start_screenrecording() {
fi
[[ $WEBCAM == "true" ]] && start_webcam_overlay "$target"
start_cancelled && return 1
local filename="$OUTPUT_DIR/screenrecording-$(date +'%Y-%m-%d_%H-%M-%S').mp4"
local audio_devices=""
local audio_args=()
local backend pid=""
[[ $DESKTOP_AUDIO == "true" ]] && audio_devices+="default_output"
for backend in $(recorder_backends); do
[[ $backend == "wf-recorder" && $target == "portal" ]] && continue
echo "===== $(date '+%F %T') args: $* backend: $backend target: $target =====" >>"$LOG_FILE"
if [[ $MICROPHONE_AUDIO == "true" ]]; then
# Merge audio tracks into one - separate tracks only play one at a time in most players
[[ -n $audio_devices ]] && audio_devices+="|"
audio_devices+="default_input"
fi
if [[ $backend == "wf-recorder" ]]; then
launch_wf_recorder "$target" "$filename" || continue
pid=$REC_PID
else
local audio_devices=""
local audio_args=()
[[ -n $audio_devices ]] && audio_args+=(-a "$audio_devices" -ac aac)
[[ $DESKTOP_AUDIO == "true" ]] && audio_devices+="default_output"
echo "===== $(date '+%F %T') args: $* target: $target =====" >>"$LOG_FILE"
gpu-screen-recorder "${capture_args[@]}" -k auto -f 60 -fm cfr -fallback-cpu-encoding yes -o "$filename" "${audio_args[@]}" 2>>"$LOG_FILE" &
local pid=$!
if [[ $MICROPHONE_AUDIO == "true" ]]; then
# Merge audio tracks into one - separate tracks only play one at a time in most players
[[ -n $audio_devices ]] && audio_devices+="|"
audio_devices+="default_input"
fi
while kill -0 $pid 2>/dev/null && [[ ! -f $filename ]]; do
sleep 0.2
[[ -n $audio_devices ]] && audio_args+=(-a "$audio_devices" -ac aac)
gpu-screen-recorder "${capture_args[@]}" -k auto -f 60 -fm cfr -fallback-cpu-encoding yes -o "$filename" "${audio_args[@]}" 9>&- 2>>"$LOG_FILE" &
pid=$!
fi
local ticks=0
while kill -0 $pid 2>/dev/null && [[ ! -f $filename ]]; do
start_cancelled && break
if [[ $target != "portal" ]] && (( ++ticks > READY_TIMEOUT_TICKS )); then
end_launched_recorder "$pid"
wf_audio_cleanup
rm -f "$filename"
continue 2
fi
sleep 0.2
done
if start_cancelled; then
end_launched_recorder "$pid"
wf_audio_cleanup
rm -f "$filename"
return 1
fi
if kill -0 $pid 2>/dev/null; then
echo "$pid" >"$REC_PID_FILE"
echo "$filename" >"$RECORDING_FILE"
toggle_screenrecording_indicator
return 0
fi
wf_audio_cleanup
rm -f "$filename"
done
if kill -0 $pid 2>/dev/null; then
echo "$filename" >"$RECORDING_FILE"
toggle_screenrecording_indicator
fi
return 1
}
# Selects the recorder this script started, by the pid it saved. A saved pid
# that is no longer a recorder is a recording that ended without a stop, so
# nothing of ours is recording and no other recorder is touched. Without a
# saved pid (a recording started before the pid was saved), every recorder the
# user runs counts, as for the bar indicator and the menu.
select_recorder() {
RECORDER=()
[[ -f $REC_PID_FILE ]] || return 0
local pid
pid=$(<"$REC_PID_FILE")
[[ $pid =~ ^[0-9]+$ ]] && omarchy-capture-screenrecording-process --pid "$pid" || return 1
RECORDER=(--pid "$pid")
}
# Stops the recorder screenrecording_active selected.
stop_screenrecording() {
pkill -SIGINT -f "^gpu-screen-recorder" # SIGINT required to save video properly
omarchy-capture-screenrecording-process "${RECORDER[@]}" --signal INT # SIGINT required to save video properly
# Wait a maximum of 5 seconds to finish before hard killing
local count=0
while pgrep -f "^gpu-screen-recorder" >/dev/null && ((count < 50)); do
while omarchy-capture-screenrecording-process "${RECORDER[@]}" && ((count < 50)); do
sleep 0.1
count=$((count + 1))
done
toggle_screenrecording_indicator
cleanup_webcam
wf_audio_cleanup
if pgrep -f "^gpu-screen-recorder" >/dev/null; then
pkill -9 -f "^gpu-screen-recorder"
if omarchy-capture-screenrecording-process "${RECORDER[@]}"; then
omarchy-capture-screenrecording-process "${RECORDER[@]}" --signal KILL
omarchy-notification-send -u critical -t 5000 "$(omarchy-text "Screen recording error")" "$(omarchy-text "Recording process had to be force-killed. Video may be corrupted.")"
else
finalize_recording
@@ -251,10 +446,10 @@ stop_screenrecording() {
(
sleep 2
rm -f "$preview"
) &
) 9>&- &
fi
rm -f "$RECORDING_FILE"
rm -f "$RECORDING_FILE" "$REC_PID_FILE"
}
toggle_screenrecording_indicator() {
@@ -262,7 +457,14 @@ toggle_screenrecording_indicator() {
}
screenrecording_active() {
pgrep -f "^gpu-screen-recorder" >/dev/null
if select_recorder && omarchy-capture-screenrecording-process "${RECORDER[@]}"; then
return 0
else
if [[ -f $REC_PID_FILE ]]; then
wf_audio_cleanup
fi
return 1
fi
}
finalize_recording() {
@@ -283,7 +485,10 @@ finalize_recording() {
# Hard-mute the first 400ms to drop the PipeWire capture-open pop (a near-clipping
# transient around 130-200ms that a gentle fade-in can't attenuate enough), then a
# 50ms fade avoids a click at the boundary before loudnorm normalizes the rest.
args+=(-af "volume=enable='lt(t,0.4)':volume=0,afade=t=in:st=0.4:d=0.05,loudnorm=I=-14:TP=-1.5:LRA=11")
#
# loudnorm declares 192 kHz output, so without -ar the AAC encoder writes
# 96 kHz from a 48 kHz recording.
args+=(-ar 48000 -af "volume=enable='lt(t,0.4)':volume=0,afade=t=in:st=0.4:d=0.05,loudnorm=I=-14:TP=-1.5:LRA=11")
fi
local processed="${latest%.mp4}-processed.mp4"
@@ -294,10 +499,66 @@ finalize_recording() {
fi
}
if screenrecording_active; then
stop_screenrecording
elif [[ $STOP_RECORDING == "true" ]]; then
exit 1
else
start_screenrecording || cleanup_webcam
mkdir -p "$RUNTIME_DIR" || exit 1
# XDG_STATE_HOME may be outside a private home; protect existing fallbacks too.
if [[ -z ${XDG_RUNTIME_DIR:-} ]]; then
chmod 700 "$RUNTIME_DIR" || exit 1
fi
# Serialize changes to the recorder and its files; background children close 9.
# A busy status query stays read-only so a picker cannot stall the bar.
{
if [[ $STATUS == "true" ]]; then
if flock -n 9; then
screenrecording_active
else
select_recorder && omarchy-capture-screenrecording-process "${RECORDER[@]}"
fi
else
# A start still in its picker or waiting on its recorder is cancelled, not
# queued behind: that toggle would otherwise begin another recording.
# The start reads CANCEL_FILE only after it removes STARTING_FILE, so a
# request left while STARTING_FILE stands is always seen. One that finds it
# gone waits its turn, and a request the start used up by then is done.
if ! flock -w 1 9; then
touch "$CANCEL_FILE"
# A start that was killed leaves its marker: it counts only while that
# start still runs.
starter=""
[[ -r $STARTING_FILE ]] && starter=$(<"$STARTING_FILE")
if [[ $starter =~ ^[0-9]+$ ]] && kill -0 "$starter" 2>/dev/null; then
exit 0
fi
flock 9 || exit 1
[[ -e $CANCEL_FILE ]] || exit 0
rm -f "$CANCEL_FILE"
fi
if screenrecording_active; then
stop_screenrecording
elif [[ $STOP_RECORDING == "true" ]]; then
exit 1
else
rm -f "$CANCEL_FILE"
echo $$ >"$STARTING_FILE"
if start_screenrecording; then
rm -f "$STARTING_FILE"
# Cancelled while the recording was being saved as started.
if start_cancelled; then
end_launched_recorder "$(<"$REC_PID_FILE")"
wf_audio_cleanup
cleanup_webcam
rm -f "$(<"$RECORDING_FILE")" "$REC_PID_FILE" "$RECORDING_FILE"
toggle_screenrecording_indicator
rm -f "$CANCEL_FILE"
fi
# A request after that check is left for its requester, who stops this
# recording once it has the lock.
else
rm -f "$STARTING_FILE"
wf_audio_cleanup
cleanup_webcam
rm -f "$CANCEL_FILE"
fi
fi
fi
} 9>>"$RUNTIME_DIR/omarchy-screenrecord.lock"
+72
View File
@@ -0,0 +1,72 @@
#!/bin/bash
# omarchy:summary=Check or signal the current user's recording processes
# omarchy:args=[--pid <pid>] [--signal INT|KILL]
# omarchy:hidden=true
usage() {
echo "Usage: omarchy-capture-screenrecording-process [--pid <pid>]... [--signal INT|KILL]" >&2
exit 2
}
proc_root=${OMARCHY_PROC_ROOT:-/proc}
wanted=" "
signal=""
while (($#)); do
case $1 in
--pid)
# Compare textually first: bash arithmetic wraps values past 64 bits.
[[ ${2-} =~ ^0*([0-9]+)$ ]] || usage
pid=${BASH_REMATCH[1]}
((${#pid} <= 10 && 10#$pid <= 4194304)) || usage
wanted+="$((10#$pid)) "
shift 2
;;
--signal)
[[ ${2-} == "INT" || ${2-} == "KILL" ]] || usage
signal=$2
shift 2
;;
*)
usage
;;
esac
done
is_recorder() {
local exe
exe=$(readlink "$proc_root/$1/exe" 2>/dev/null) || return 1
# Linux appends this when an upgrade unlinks a still-running executable.
exe=${exe% (deleted)}
exe=${exe##*/}
[[ $exe == "gpu-screen-recorder" || $exe == "wf-recorder" ]]
}
# Linux truncates a process name to 15 characters: gpu-screen-recorder runs as
# gpu-screen-reco, which any executable sharing that prefix also matches, and a
# process can rename itself, so the executable path confirms each candidate.
matched=()
while read -r pid; do
if [[ $wanted == " " || $wanted == *" $pid "* ]] && is_recorder "$pid"; then
matched+=("$pid")
fi
done < <(pgrep -u "$UID" -x 'gpu-screen-reco(rder)?|wf-recorder')
if ((${#matched[@]} == 0)); then
exit 1
fi
if [[ -n $signal ]]; then
delivered=0
for pid in "${matched[@]}"; do
# The pid can be reused between discovery and kill; re-checking exe here narrows that window.
if is_recorder "$pid" && kill "-$signal" "$pid" 2>/dev/null; then
delivered=1
fi
done
# A recorder that exits before its signal has stopped anyway; fail only if none was signalled.
((delivered)) || exit 1
fi
exit 0
+31 -25
View File
@@ -47,31 +47,9 @@ validate_dev_checkout() {
fi
}
# The dev channel refreshes pacman with the checkout's own tools. A checkout
# without this machine's region profile would silently drop its repositories.
validate_dev_region() {
local checkout="$1" region_file="${OMARCHY_REGION_FILE:-/etc/omarchy/region}" region=global
[[ -f $region_file ]] && region=$(<"$region_file")
[[ $region == "global" ]] && return 0
# The refresh after linking needs a runnable helper and every fragment, not
# just a directory with the region's name: dev refreshes edge, and leaving
# dev later refreshes stable or rc with this checkout's tools.
local profile="$checkout/default/regions/$region/pacman" fragment supported=true
[[ -x $checkout/bin/omarchy-apply-pacman ]] || supported=false
for fragment in pacman.conf.append mirrorlist-stable.prepend mirrorlist-rc.prepend mirrorlist-edge.prepend; do
[[ -r $profile/$fragment ]] || supported=false
done
if ! $supported; then
fail "$checkout does not support the '$region' region, so switching would drop its package repositories. Check out a branch with default/regions/$region, or remove $region_file to use global defaults."
fi
}
link_dev_checkout() {
local checkout="$1" required
[[ -d $checkout/.git ]] || git clone https://github.com/omacom/omarchy.git "$checkout"
validate_dev_region "$checkout"
# Check the destination before changing /etc/omarchy.conf or sudo's path.
# An existing checkout is not pulled automatically and may predate this policy.
@@ -82,6 +60,23 @@ link_dev_checkout() {
fi
done
# The checkout's own channel refresh runs next, from its own templates.
if [[ ! -f $checkout/${templates#"$OMARCHY_PATH"/}/pacman-$pacman_channel.conf ||
! -f $checkout/${templates#"$OMARCHY_PATH"/}/mirrorlist-$pacman_channel ]]; then
fail "Update the checkout before switching to dev; it has no $pacman_channel templates for $platform."
fi
# On aarch64 one from before it had platform templates would replace this
# machine's repositories with x86's.
if [[ $platform != "x86" ]]; then
for required in bin/omarchy-hw-platform install/helpers/pacman.sh; do
if [[ ! -f $checkout/$required || ! -r $checkout/$required ||
( $required == "bin/omarchy-hw-platform" && ! -x $checkout/$required ) ]]; then
fail "Update the checkout before switching to dev; on $platform it needs $required for this machine's repositories."
fi
done
fi
omarchy-dev-link "$checkout" --no-reboot
}
@@ -124,9 +119,6 @@ case "$channel" in
packages=(omarchy-dev omarchy-settings-dev)
;;
dev)
confirm_dev || { echo "Cancelled."; exit 0; }
dev_checkout="$HOME/omarchy"
validate_dev_checkout "$dev_checkout"
pacman_channel=edge
packages=(omarchy-dev omarchy-settings-dev)
;;
@@ -137,6 +129,20 @@ case "$channel" in
;;
esac
# A channel this machine's platform has no templates for is refused before
# anything changes (install/helpers/pacman.sh).
source "$OMARCHY_PATH/install/helpers/pacman.sh"
platform=$(omarchy-hw-platform) || fail "Cannot tell this machine's platform; nothing changed."
templates=$(omarchy_pacman_templates "$platform") || exit 1
[[ -f $templates/pacman-$pacman_channel.conf && -f $templates/mirrorlist-$pacman_channel ]] ||
fail "Omarchy has no $pacman_channel channel for $platform; nothing changed."
if [[ $channel == "dev" ]]; then
confirm_dev || { echo "Cancelled."; exit 0; }
dev_checkout="$HOME/omarchy"
validate_dev_checkout "$dev_checkout"
fi
# A failure past this point leaves the channel switch half-applied, so say how
# to pick it back up rather than dying silently under set -e.
trap 'echo -e "\nThe channel switch did not complete. Review the error above, then rerun: omarchy-channel-set '"$channel"'" >&2' ERR
+419 -19
View File
@@ -1,32 +1,432 @@
#!/bin/bash
# omarchy:summary=Set a new encryption password for a drive selected.
# omarchy:summary=Set a new password for an encrypted drive
# omarchy:requires-sudo=true
encrypted_drives=$(blkid -t TYPE=crypto_LUKS -o device)
# Changes a drive's LUKS key and nothing else. For the disk holding /, the new
# key must open the disk and the old one must not before the change counts. The
# journal records only the disk's UUID, key slots and phase, so a change that
# stops part way is finished by the next run. A platform whose boot checks prove
# the disk's key slots records the owner's new one before the run finishes.
# Passwords live in variables; never trace them.
set +x
REKEY_STATE=${XDG_STATE_HOME:-$HOME/.local/state}/omarchy/drive-password.state
source "$OMARCHY_PATH/install/provisioning/luks-rekey.sh"
# The LUKS partition beneath /, or nothing when / is not encrypted. Fails when
# it cannot tell, including a crypt layer whose LUKS partition lsblk cannot name.
root_luks_device() {
local root_source ancestry device
root_source=$(findmnt -no SOURCE /) && [[ -n $root_source ]] || return 1
ancestry=$(lsblk -nsrpo NAME,TYPE,FSTYPE "${root_source%%[*}") || return 1
device=$(awk '$3 == "crypto_LUKS" { print $1; exit }' <<<"$ancestry")
if [[ -n $device ]]; then
readlink -f "$device"
elif awk '$2 == "crypt" { found = 1 } END { exit !found }' <<<"$ancestry"; then
return 1
fi
}
# Every LUKS device, from udev's probe as root_luks_device reads it. An
# unprivileged blkid only reads /run/blkid, empty until root runs blkid. lsblk
# repeats a device under each parent (RAID members), so each is listed once.
luks_drives() {
lsblk -nrpo NAME,FSTYPE | awk '$2 == "crypto_LUKS" && !seen[$1]++ { print $1 }'
}
# Every cryptsetup call holds the disk lock, so one left running by a run that
# was killed finishes before the next run looks at the drive.
# Print the key slot the password opens. Returns 2 when it opens none, and
# another failure when the drive cannot be checked. cryptsetup open tries
# enrolled tokens (TPM2, FIDO2, keyring) before the key and reports a token's
# slot whatever key it was given, so allow only a token type no token has.
drive_key_slot() {
local drive=$1 password=$2 out status=0
out=$(printf '%s' "$password" | sudo flock "$disk_lock" env LC_ALL=C cryptsetup open --test-passphrase --verbose --token-type passphrase-only --key-file - "$drive" 2>&1) || status=$?
(( status == 0 )) || return "$status"
grep -o 'Key slot [0-9]* unlocked' <<<"$out" | grep -o '[0-9]*' | head -1
}
# The enabled key slots, comma-separated. A luks2-keyring token under "Tokens:"
# looks like a keyslot, so read only the keyslot section.
drive_slots() {
local dump
dump=$(sudo flock "$disk_lock" cryptsetup luksDump "$1") || return 1
awk '
/^[^ \t]/ { keyslots = ($0 == "Keyslots:") }
keyslots && /^ +[0-9]+: luks2/ { sub(":", "", $1); print $1 }
/^Key Slot [0-9]+: ENABLED/ { sub(":", "", $3); print $3 }' <<<"$dump" | paste -sd, -
}
# The key slots other than FOUND that the password also opens, comma-separated,
# or nothing. Fails when a slot cannot be checked.
other_key_slots() {
local drive=$1 password=$2 found=$3 slots slot status
local -a others=()
slots=$(drive_slots "$drive") || return 1
for slot in ${slots//,/ }; do
[[ $slot != "$found" ]] || continue
status=0
printf '%s' "$password" | sudo flock "$disk_lock" cryptsetup open --test-passphrase --token-type passphrase-only --key-slot "$slot" --key-file - "$drive" >/dev/null 2>&1 || status=$?
if (( status == 0 )); then
others+=("$slot")
elif (( status != 2 )); then
return 1
fi
done
local IFS=,
printf '%s' "${others[*]}"
}
kill_slot() {
local drive=$1 password=$2 slot=$3
printf '%s' "$password" | sudo flock "$disk_lock" cryptsetup luksKillSlot -q --key-file - "$drive" "$slot"
}
# luksChangeKey writes the new key before it retires the old one (LUKS2 swaps
# them in one header update), so a failure that leaves the new key unable to
# open the drive changed nothing: that returns 2. One that stops in between
# leaves both keys, so the old key is retired here until it opens nothing.
# Sets new_slot.
change_luks_key() {
local drive=$1 current=$2 new=$3 old_slot=$4 remains attempt status
local -a pbkdf=(--iter-time 2000)
echo "Changing full-disk encryption password for $drive"
if sudo flock "$disk_lock" cryptsetup isLuks --type luks2 "$drive"; then
pbkdf+=(--pbkdf argon2id)
fi
# Both keys travel over stdin, the current one on the first line.
printf '%s\n%s' "$current" "$new" |
sudo flock "$disk_lock" bash -c 'IFS= read -r current; drive=$1; shift; exec cryptsetup luksChangeKey "$@" --key-file <(printf "%s" "$current") "$drive" <(cat)' bash "$drive" "${pbkdf[@]}"
new_slot=$(drive_key_slot "$drive" "$new") || return
for (( attempt = 0; attempt < 8; attempt++ )); do
status=0
remains=$(drive_key_slot "$drive" "$current") || status=$?
(( status == 2 )) && return 0
(( status == 0 )) && [[ $remains != "$new_slot" ]] || return 1
kill_slot "$drive" "$new" "$remains" || return 1
done
return 1
}
# The entrypoint that records the system disk's key slots for the platform's
# boot checks, or nothing where none does. A machine without its platform's
# boot package at all (dispatch exit 3) predates it, and nothing checks its
# slots. Fails with the dispatcher's message otherwise, such as a boot package
# too old to ship luks-slots.
slot_recorder() {
local out status=0
out=$("$OMARCHY_PATH/bin/omarchy-lifecycle-dispatch" --resolve luks-slots 2>&1) || status=$?
if (( status == 0 )); then
printf '%s' "$out"
elif (( status != 3 )); then
printf '%s\n' "$out" >&2
return 1
fi
}
# Refuse a current password that opens any slot but the owner's where the
# platform records the slots: on a disk an earlier setup gave a second key (a
# recovery key), changing that one would leave the owner's password as it was.
# luks-slots --owner prints the recorded slot (0), or exits 4 when no owner slot
# is recorded, as on a Mac set up before its boot package recorded slots; any
# other status is a failure the entrypoint explains, and stops the change.
# Where nothing is recorded, a password that opens the disk's only key slot is
# the owner's, and the change records its new slot (unrecorded_owner).
owner_slot_matches() {
local slot=$1 recorder owner errors reason status=0
recorder=$(slot_recorder) || return 1
[[ -n $recorder ]] || return 0
errors=$(mktemp) || return 1
owner=$(sudo "$OMARCHY_PATH/bin/omarchy-lifecycle-dispatch" luks-slots --owner 2>"$errors") || status=$?
reason=$(<"$errors")
rm -f "$errors"
if (( status == 4 )); then
only_key_slot "$slot" || return 1
unrecorded_owner=1
return 0
elif (( status != 0 )); then
echo "Could not read which key slot holds the system disk password: ${reason:-luks-slots --owner exited $status}"
return 1
elif [[ ! $owner =~ ^[0-9]+$ ]]; then
echo "Could not read which key slot holds the system disk password: the platform's boot package named '$owner', which is not a key slot."
return 1
fi
if [[ $owner != "$slot" ]]; then
echo "That password opens another key slot on the system disk, such as a recovery key from an earlier setup. Enter the system disk password."
return 1
fi
}
# With no owner slot recorded, the password must open the system disk's one key
# slot: with more keys there is no telling which is the owner's, and recording
# only the owner's would fail the platform's boot check, which wants every kept
# slot recorded. Says how to settle it.
only_key_slot() {
local slot=$1 slots others
slots=$(drive_slots "$root_drive") || { echo "Could not read the key slots of $root_drive."; return 1; }
[[ $slots != "$slot" ]] || return 0
others=$(tr ',' '\n' <<<"$slots" | grep -vx -- "$slot" | paste -sd, -)
echo "No key slot is recorded as the system disk password's, and $root_drive has keys in slots $slots, so which one is yours can't be told. The password you entered opens slot $slot."
if [[ $others =~ ^[0-9]+$ ]]; then
echo "If that password is yours and slot $others holds a recovery key you keep, record both: sudo omarchy-lifecycle-dispatch luks-slots owner=$slot recovery=$others"
echo "Otherwise remove the key you don't use: sudo cryptsetup luksKillSlot $root_drive <slot>"
else
echo "Remove the keys you don't use: sudo cryptsetup luksKillSlot $root_drive <slot>"
fi
echo "Then run omarchy-drive-password again."
return 1
}
# Record the slot the owner's password opens now; a no-op where nothing does.
record_owner_slot() {
local recorder
recorder=$(slot_recorder) || return 1
[[ -z $recorder ]] || sudo "$OMARCHY_PATH/bin/omarchy-lifecycle-dispatch" luks-slots "owner=$1"
}
# Phase luks: the key change may or may not have landed. Phase record: the new
# key alone opens the recorded slot, which the boot checks still need.
change_system_password() {
local current=$1 new=$2 old_slot=$3 slots status=0
slots=$(drive_slots "$root_drive")
if [[ ,$slots, != *",$old_slot,"* ]]; then
echo "Could not read the key slots of $root_drive."
return 1
fi
# A disk taken as the owner's for holding one key must still hold only it.
if (( unrecorded_owner )) && [[ $slots != "$old_slot" ]]; then
echo "The key slots of $root_drive changed while you entered the new password. Run omarchy-drive-password again."
return 1
fi
mkdir -p "${REKEY_STATE%/*}" &&
rekey_state_put uuid "$root_uuid" old_slot "$old_slot" slots "$slots" checked 1 phase luks || return 1
change_luks_key "$root_drive" "$current" "$new" "$old_slot" || status=$?
if (( status == 2 )); then
rm -f "$REKEY_STATE"
echo "The disk password did not change."
return 1
elif (( status != 0 )); then
echo "Could not confirm the disk password change. Run omarchy-drive-password again to finish."
return 1
fi
rekey_state_put slot "$new_slot" phase record || return 1
if ! record_owner_slot "$new_slot"; then
echo "Could not record the new key slot for the boot checks. Run omarchy-drive-password again to finish."
return 1
fi
rm -f "$REKEY_STATE"
}
# A journal left on this system's disk by a change that stopped part way.
change_pending() {
local phase uuid
phase=$(rekey_state_get phase) || return 1
uuid=$(rekey_state_get uuid)
if [[ $phase == "luks" || $phase == "record" || $phase == "accounts" ]] && [[ -n $uuid && $uuid == "$root_uuid" ]]; then
return 0
fi
rm -f "$REKEY_STATE"
return 1
}
# Settle the disk on the password it opens with now, then record its slot.
# Before the change was confirmed that is the old key (removing the slot the
# change added, if any) or the new key in the one slot the change added
# (retiring the old slot); after, only the confirmed slot. Any other key on the
# disk, such as a recovery passphrase, is refused, and so is a disk that gained
# more than one slot since.
finish_interrupted_change() {
local phase old_slot slots slot header password opened now added extra count=0 status=0
phase=$(rekey_state_get phase)
# Earlier versions named the confirmed phase "accounts".
[[ $phase != "accounts" ]] || phase=record
old_slot=$(rekey_state_get old_slot)
slots=$(rekey_state_get slots)
slot=$(rekey_state_get slot)
if [[ $phase == "luks" && ! ( $old_slot =~ ^[0-9]+$ && ,$slots, == *",$old_slot,"* ) ]] ||
[[ $phase == "record" && ! $slot =~ ^[0-9]+$ ]]; then
echo "$REKEY_STATE is unreadable. Remove it once the disk opens with only the password you want."
return 1
fi
# Earlier versions journaled the change without checking that the old
# password opened one slot alone (checked=1), so with other slots on the disk
# a copy of it may have outlived the change. No password entered now can show
# which slot was whose, so the owner settles that by hand.
if [[ -z $(rekey_state_get checked) ]]; then
now=$(drive_slots "$root_drive") || { echo "Could not read the key slots of $root_drive."; return 1; }
if [[ $now == *,* ]]; then
echo "The last password change for $root_drive was started by an earlier version of omarchy-drive-password, and the disk has key slots $now, so it may still hold a copy of the old password."
echo "Settle it by hand, in this order:"
echo "1. Find the slot the password you want to keep opens, and keep it: sudo cryptsetup open --test-passphrase --token-type passphrase-only --verbose $root_drive names it. If only the old password opens the disk, that is the one you keep."
echo "2. If the password you keep is not the old one, check each other slot with the old password (sudo cryptsetup open --test-passphrase --token-type passphrase-only --key-slot <slot> $root_drive) and remove each one it opens (sudo cryptsetup luksKillSlot $root_drive <slot>). Never remove the slot from step 1."
if [[ -n $(slot_recorder 2>/dev/null) ]]; then
echo "3. Record the slots you kept for the boot checks: sudo omarchy-lifecycle-dispatch luks-slots owner=<slot> recovery=<slot>, with an empty recovery= when no recovery key is left."
fi
echo "Then remove $REKEY_STATE, and run omarchy-drive-password again if the disk should still take a new password."
return 1
fi
fi
echo "The last password change for $root_drive stopped part way."
if [[ $phase == "record" ]]; then
header="New encryption password"
else
header="Encryption password that opens $root_drive now (the new one if the change went through)"
fi
password=$(gum input --password --header "$header") || return 1
opened=$(drive_key_slot "$root_drive" "$password") || status=$?
if (( status != 0 && status != 2 )); then
echo "Could not check the password against $root_drive."
return 1
fi
if [[ $phase == "luks" && -n $opened ]]; then
now=$(drive_slots "$root_drive") || { echo "Could not read the key slots of $root_drive."; return 1; }
for extra in ${now//,/ }; do
if [[ ,$slots, != *",$extra,"* ]]; then
added=$extra
count=$(( count + 1 ))
fi
done
if (( count > 1 )); then
echo "The key slots of $root_drive changed since the interrupted change. Finish it by hand."
return 1
fi
if [[ $opened == "$old_slot" ]]; then
[[ -z $added ]] || kill_slot "$root_drive" "$password" "$added" || return 1
elif [[ -n $added && $opened == "$added" ]]; then
if [[ ,$now, == *",$old_slot,"* ]]; then
kill_slot "$root_drive" "$password" "$old_slot" || return 1
fi
else
opened=""
fi
[[ -z $opened ]] || rekey_state_put slot "$opened" phase record || return 1
elif [[ $phase == "record" && $opened != "$slot" ]]; then
opened=""
fi
if [[ -z $opened ]]; then
echo "That password does not finish the change on $root_drive. Run omarchy-drive-password again to finish."
return 1
fi
if ! record_owner_slot "$opened"; then
echo "Could not record the key slot for the boot checks. Run omarchy-drive-password again to finish."
return 1
fi
rm -f "$REKEY_STATE"
echo "$root_drive now opens with the password you entered, and the change is settled."
}
change_selected_drive() {
local encrypted_drives drive current old_slot others new_password confirmation status=0 system_drive=false
encrypted_drives=$(luks_drives)
if [[ -z $encrypted_drives ]]; then
echo "No encrypted drives available."
return 1
fi
if [[ -n $encrypted_drives ]]; then
if (( $(wc -l <<<"$encrypted_drives") == 1 )); then
drive_to_change="$encrypted_drives"
drive="$encrypted_drives"
else
drive_to_change="$(omarchy-drive-select "$encrypted_drives")"
drive="$(omarchy-drive-select "$encrypted_drives")"
fi
if [[ -n $drive_to_change ]]; then
new_password=$(gum input --password --header "New encryption password") || exit 1
[[ -n $new_password ]] || { echo "Password cannot be empty."; exit 1; }
confirmation=$(gum input --password --header "Confirm new encryption password") || exit 1
[[ $new_password == "$confirmation" ]] || { echo "Passwords do not match."; exit 1; }
echo "Changing full-disk encryption password for $drive_to_change"
# The new key travels over stdin and reaches cryptsetup as a keyfile via
# <(cat), leaving the tty free for the current-passphrase prompt.
printf "%s" "$new_password" | sudo bash -c 'exec cryptsetup luksChangeKey --pbkdf argon2id --iter-time 2000 "$1" <(cat) </dev/tty' bash "$drive_to_change"
else
if [[ -z $drive ]]; then
echo "No drive selected."
return 0
fi
if [[ -n $root_drive && $(readlink -f "$drive") == "$root_drive" ]]; then
system_drive=true
drive=$root_drive
fi
current=$(gum input --password --header "Current encryption password") || return 1
old_slot=$(drive_key_slot "$drive" "$current") || status=$?
if (( status == 2 )); then
echo "That password does not open $drive."
return 1
elif (( status != 0 )); then
echo "Could not check the password against $drive."
return 1
fi
if [[ $system_drive == "true" ]]; then
owner_slot_matches "$old_slot" || { echo "The system disk password did not change."; return 1; }
# An interrupted change is finished from the one slot the password opened;
# another copy of it would outlive the change.
others=$(other_key_slots "$drive" "$current" "$old_slot") || { echo "Could not check the key slots of $drive."; return 1; }
if [[ -n $others ]]; then
echo "That password also opens key slot $others on $drive. Remove the extra copy first (sudo cryptsetup luksKillSlot $drive <slot>), then run omarchy-drive-password again."
if [[ -n $(slot_recorder 2>/dev/null) ]]; then
echo "Then record the slots you keep for the boot checks: sudo omarchy-lifecycle-dispatch luks-slots owner=<slot> recovery=<slot>, with an empty recovery= when no recovery key is left."
fi
echo "The system disk password did not change."
return 1
fi
fi
new_password=$(gum input --password --header "New encryption password") || return 1
[[ -n $new_password ]] || { echo "Password cannot be empty."; return 1; }
confirmation=$(gum input --password --header "Confirm new encryption password") || return 1
[[ $new_password == "$confirmation" ]] || { echo "Passwords do not match."; return 1; }
[[ $new_password != "$current" ]] || { echo "The new password is the current one."; return 1; }
# Its slot is how the change is followed, so it must not open one already.
status=0
drive_key_slot "$drive" "$new_password" >/dev/null || status=$?
if (( status == 0 )); then
echo "That password already opens a key slot on $drive. Choose another."
return 1
elif (( status != 2 )); then
echo "Could not check the password against $drive."
return 1
fi
status=0
if [[ $system_drive == "true" ]]; then
change_system_password "$current" "$new_password" "$old_slot"
else
change_luks_key "$drive" "$current" "$new_password" "$old_slot" || status=$?
if (( status == 2 )); then
echo "The password of $drive did not change."
return 1
elif (( status != 0 )); then
echo "Could not confirm the password change on $drive."
return 1
fi
fi
}
disk_lock=${REKEY_STATE%/*}/drive-password.disk.lock
mkdir -p "${REKEY_STATE%/*}" && touch "$disk_lock" && exec {lock_fd}>"${REKEY_STATE%/*}/drive-password.lock" && flock -n "$lock_fd" ||
{ echo "Another drive password change is running."; exit 1; }
unrecorded_owner=0
root_drive=$(root_luks_device) || { echo "Could not tell which drive holds the system."; exit 1; }
root_uuid=""
if [[ -n $root_drive ]]; then
root_uuid=$(lsblk -dno UUID "$root_drive") && [[ -n $root_uuid ]] || { echo "Could not read the UUID of $root_drive."; exit 1; }
fi
if change_pending; then
finish_interrupted_change
else
echo "No encrypted drives available."
exit 1
change_selected_drive
fi
+6
View File
@@ -13,6 +13,12 @@ for arg in "$@"; do
esac
done
# Resume is Limine-oriented and hibernate is unreliable on Asahi.
if omarchy-hw-aarch64-apple; then
echo "Skipping hibernation setup (not supported on Apple Silicon)"
exit 0
fi
if [[ ! -f /sys/power/image_size ]]; then
echo -e "Hibernation is not supported on your system" >&2
exit 0
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
# omarchy:summary=Detect whether the CPU is aarch64 (every ARM machine, Apple Silicon included).
[[ $(uname -m) == "aarch64" ]]
+17
View File
@@ -0,0 +1,17 @@
#!/bin/bash -p
# omarchy:summary=Detect whether the computer is an Apple Silicon Mac.
# Callers run this by absolute path, from systemd and from the shell, so use the
# detector shipped beside it rather than whichever one PATH finds. -p keeps a
# root caller's exported functions out, so a launch without it is refused, and
# nothing here searches PATH.
if [[ $- != *p* ]]; then
echo "Refusing an unsafe Bash startup." >&2
exit 126
fi
dir=${BASH_SOURCE[0]%/*}
[[ $dir != "${BASH_SOURCE[0]}" ]] || dir=.
platform=$("$dir/omarchy-hw-platform") || exit 1
[[ $platform == "aarch64-apple" ]]
+15
View File
@@ -0,0 +1,15 @@
#!/bin/bash -p
# omarchy:summary=Detect whether the computer is an Apple Silicon Mac (the old name of omarchy-hw-aarch64-apple).
# omarchy:hidden=true
# Kept while omarchy-mac's packages and units still call it by this name. Like
# the predicate it runs, it refuses a launch without -p and searches no PATH.
if [[ $- != *p* ]]; then
echo "Refusing an unsafe Bash startup." >&2
exit 126
fi
dir=${BASH_SOURCE[0]%/*}
[[ $dir != "${BASH_SOURCE[0]}" ]] || dir=.
exec "$dir/omarchy-hw-aarch64-apple"
+40 -6
View File
@@ -5,14 +5,48 @@
backlight_path="${OMARCHY_BACKLIGHT_PATH:-/sys/class/backlight}"
# The platform package may name backlights to skip and to prefer (see
# docs/file-layout.md).
displays_conf=/usr/share/omarchy-platform/displays.conf
skips=()
prefers=()
if [[ -r $displays_conf ]]; then
# \r in IFS: a file saved with CRLF line ends reads the same.
while IFS=$' \t\r' read -r directive argument extra || [[ -n $directive ]]; do
[[ -n $argument && -z $extra && $argument != */* && $argument != . && $argument != .. ]] || continue
case $directive in
backlight-skip) skips+=("$argument") ;;
backlight-prefer) prefers+=("$argument") ;;
esac
done <"$displays_conf"
fi
usable() {
local skip
# The Touch Bar on T2 Macs registers a backlight that never drives the display panel.
[[ -n $1 && $1 != appletb_backlight ]] || return 1
for skip in "${skips[@]}"; do
# shellcheck disable=SC2053 # a skip is a glob
[[ $1 == $skip ]] && return 1
done
return 0
}
# Start with the first possible output, then refine to the most likely given an order heuristic.
# Glob the candidates in the loop list: [[ ]] does not do pathname expansion.
# The Touch Bar on T2 Macs registers a backlight that never drives the display panel.
device="$(ls -1 "$backlight_path" 2>/dev/null | grep -vx appletb_backlight | head -n1)"
device=""
while IFS= read -r name; do
if usable "$name"; then
device="$name"
break
fi
done < <(ls -1 "$backlight_path" 2>/dev/null)
# gmux comes first: apple-gmux only registers when the kernel has already picked it, and on
# dual-GPU Macs the GPU's own PWM stops driving the panel once that GPU suspends.
for candidate in "$backlight_path"/gmux_backlight "$backlight_path"/amdgpu_bl* "$backlight_path"/intel_backlight "$backlight_path"/acpi_video*; do
if [[ -e $candidate ]]; then
# dual-GPU Macs the GPU's own PWM stops driving the panel once that GPU suspends. The
# platform's preferred backlights follow, then the GPU and ACPI ones.
# Glob the candidates in the loop list: [[ ]] does not do pathname expansion.
for candidate in "$backlight_path"/gmux_backlight "${prefers[@]/#/$backlight_path/}" "$backlight_path"/amdgpu_bl* "$backlight_path"/intel_backlight "$backlight_path"/acpi_video*; do
if [[ -e $candidate ]] && usable "${candidate##*/}"; then
device="${candidate##*/}"
break
fi
+26 -2
View File
@@ -2,15 +2,39 @@
# omarchy:summary=Returns true when running on a laptop (has a lid or laptop chassis).
acpi_lid="${OMARCHY_ACPI_LID_PATH:-/proc/acpi/button/lid}"
input_class="${OMARCHY_INPUT_CLASS_PATH:-/sys/class/input}"
dmi_chassis="${OMARCHY_DMI_CHASSIS_TYPE_PATH:-/sys/class/dmi/id/chassis_type}"
shopt -s nullglob
acpi_states=("$acpi_lid"/*/state)
input_switches=("$input_class"/*/capabilities/sw)
shopt -u nullglob
# A lid switch is the definitive signal for clamshell-capable hardware.
for state in /proc/acpi/button/lid/*/state; do
for state in "${acpi_states[@]}"; do
[[ -e $state ]] && exit 0
done
# Without an ACPI lid (Apple Silicon, for one), any input device that reports a
# lid switch (SW_LID, bit 0 of its switch capabilities, the last word of the
# bitmap) counts. The switch's name varies: Apple's SMC input device carries
# the same name on a Mac mini, where it reports no lid.
for switches in "${input_switches[@]}"; do
[[ -r $switches ]] || continue
read -r -a words <"$switches" || true
(( ${#words[@]} )) || continue
word=${words[-1]}
[[ $word =~ ^[0-9a-fA-F]+$ ]] || continue
(( 16#$word & 1 )) && exit 0
done
# Fall back to the DMI chassis type for laptops that don't expose an ACPI lid
# button. 8=Portable 9=Laptop 10=Notebook 14=Sub Notebook 30=Tablet
# 31=Convertible 32=Detachable.
case $(< /sys/class/dmi/id/chassis_type 2>/dev/null) in
chassis_type=""
read -r chassis_type <"$dmi_chassis" 2>/dev/null || true
case $chassis_type in
8 | 9 | 10 | 14 | 30 | 31 | 32) exit 0 ;;
esac
+40 -4
View File
@@ -3,9 +3,45 @@
# omarchy:summary=Returns true when the laptop lid is closed
# omarchy:hidden=true
for state in /proc/acpi/button/lid/*/state; do
[[ -r $state ]] || continue
[[ $(< "$state") == *"closed"* ]] && exit 0
done
acpi_lid="${OMARCHY_ACPI_LID_PATH:-/proc/acpi/button/lid}"
input_class="${OMARCHY_INPUT_CLASS_PATH:-/sys/class/input}"
shopt -s nullglob
acpi_states=("$acpi_lid"/*/state)
input_switches=("$input_class"/*/capabilities/sw)
shopt -u nullglob
# An input device that reports a lid switch: SW_LID, bit 0 of the last word
# of its switch bitmap.
has_lid_switch() {
local switches word
local -a words
for switches in "${input_switches[@]}"; do
[[ -r $switches ]] || continue
read -r -a words <"$switches" || true
(( ${#words[@]} )) || continue
word=${words[-1]}
[[ $word =~ ^[0-9a-fA-F]+$ ]] || continue
(( 16#$word & 1 )) && return 0
done
return 1
}
if (( ${#acpi_states[@]} )); then
# An ACPI lid reports its own state.
for state in "${acpi_states[@]}"; do
[[ -r $state ]] || continue
[[ $(<"$state") == *"closed"* ]] && exit 0
done
elif has_lid_switch; then
# Elsewhere (Apple Silicon's SMC, for one) only logind reads the switch. Ask
# it only then: it answers "open" when it tracks no lid. This runs in sudo's
# and polkit's PAM stack where fingerprint login is set up, so a stalled
# logind must not hold up every sudo.
lid_state=$(busctl --timeout=2 get-property org.freedesktop.login1 \
/org/freedesktop/login1 org.freedesktop.login1.Manager LidClosed 2>/dev/null)
[[ $lid_state == "b true" ]] && exit 0
fi
exit 1
+202
View File
@@ -0,0 +1,202 @@
#!/bin/bash -p
# omarchy:summary=Print the hardware platform: x86, aarch64 or aarch64-apple.
# Platform names are the CPU architecture, then the hardware family when it
# needs handling of its own: x86, aarch64 (every ARM machine without one) and
# aarch64-apple (Apple Silicon). A later family is aarch64-<family>, so
# "aarch64*" always means ARM (AGENTS.md, Platforms).
#
# ARM laptops have no DMI, so the board identity is the root node of the device
# tree the firmware hands the kernel: "apple,<model>" from Asahi's m1n1 on every
# Apple Silicon Mac, "qcom,<soc>" on Snapdragon laptops, which are plain aarch64
# here and must never pass for a Mac. Each compatible string is a NUL-separated
# list, and only a token's vendor prefix counts.
#
# An image is built away from the machine it will run on, so while this root is
# being built rather than booted, the image-target manifest the builder wrote
# names the platform and the build host's device tree is never read.
#
# Fixture roots let unprivileged tests fake the device tree, PID 1 and the image
# root. Naming any of them makes the whole view a fixture: nothing live is read,
# not even the device tree when only the image root is named. A privileged
# caller must not let its environment pick the platform at all.
# Privileged mode (-p) keeps exported functions and BASH_ENV out of this shell,
# so a launch without it is refused. Root then starts over in an empty
# environment and reads only the live paths with a fixed PATH.
if [[ $- != *p* ]]; then
echo "Refusing an unsafe Bash startup." >&2
exit 126
fi
if (( EUID == 0 )) && [[ ${1:-} != --clean-environment ]]; then
exec /usr/bin/env -i PATH=/usr/bin /bin/bash -p -- "${BASH_SOURCE[0]}" --clean-environment "$@"
fi
if (( EUID == 0 )); then
export PATH=/usr/bin
fixtures=""
else
fixtures=${OMARCHY_PROC_ROOT:-}${OMARCHY_SYS_ROOT:-}${OMARCHY_IMAGE_ROOT:-}
fi
if [[ -z $fixtures ]]; then
proc=/proc
sources=(/proc/device-tree/compatible /sys/firmware/devicetree/base/compatible)
image_root=""
image_dir=/var/lib/omarchy/image
image_owner=0
else
proc=${OMARCHY_PROC_ROOT:-}
sources=()
[[ -z ${OMARCHY_PROC_ROOT:-} ]] || sources+=("$OMARCHY_PROC_ROOT/device-tree/compatible")
[[ -z ${OMARCHY_SYS_ROOT:-} ]] || sources+=("$OMARCHY_SYS_ROOT/firmware/devicetree/base/compatible")
image_root=${OMARCHY_IMAGE_ROOT:-}
image_dir=${image_root:+$image_root/var/lib/omarchy/image}
image_owner=$EUID
fi
manifest=$image_dir/target
contradiction() {
echo "Error: contradictory platform identity: $1" >&2
exit 1
}
if ! machine=$(uname -m) || [[ -z $machine ]]; then
echo "Error: cannot read the CPU architecture" >&2
exit 1
fi
# A booted system is one systemd runs as PID 1 from this root. An image build
# is told apart by what it shows: no /run/systemd/system (a root with a /run of
# its own), PID 1's root being another (a chroot on the build host, even with
# the host's /run bound in, as arch-chroot does), or PID 1 being something other
# than systemd (a PID namespace whose PID 1 is the build). What cannot be seen,
# such as PID 1's root to a normal user or /proc/1 behind hidepid, never makes a
# build: a booted system always answers from its hardware, so a build runs as
# root. A caller in a private PID namespace looks like a build too, so nothing
# that asks the detector runs with PrivatePIDs=.
booted() {
local comm
[[ -d $image_root/run/systemd/system ]] || return 1
if [[ -n $proc && -e $proc/1/root ]]; then
[[ $proc/1/root -ef $image_root/ ]] || return 1
fi
if [[ -n $proc && -r $proc/1/comm ]]; then
read -r comm <"$proc/1/comm" && [[ $comm == "systemd" ]] || return 1
fi
}
# Owned by root (the test's own user for a fixture), not a symlink, and not
# writable by anyone else.
trusted() {
local owner mode
[[ -e $1 && ! -L $1 ]] || return 1
read -r owner mode < <(stat -c '%u %a' -- "$1") || return 1
[[ $owner =~ ^[0-9]+$ && $mode =~ ^[0-7]+$ ]] && (( owner == image_owner && (8#$mode & 8#022) == 0 ))
}
# The manifest: format=1, platform= one of the platform names, comments and
# unknown keys ignored, and any other line refused. Image builders written before
# the names settled say apple-silicon, generic-aarch64 or generic, which still
# read as aarch64-apple, aarch64 and x86. The detector reads it itself, so it
# stands alone wherever a package ships a copy of it.
image_platform() {
local line key value format="" platform=""
if [[ ! -d $image_dir || ! -f $manifest ]] || ! trusted "$image_dir" || ! trusted "$manifest"; then
echo "Error: the image manifest $manifest is not a root-owned regular file" >&2
return 1
fi
while IFS= read -r line || [[ -n $line ]]; do
[[ -n $line && $line != \#* ]] || continue
if [[ $line != *=* ]]; then
echo "Error: the image manifest $manifest is malformed: $line" >&2
return 1
fi
key=${line%%=*}
value=${line#*=}
case $key in
format) format=$value ;;
platform) platform=$value ;;
esac
done <"$manifest"
if [[ $format != "1" ]]; then
echo "Error: the image manifest $manifest is not format=1" >&2
return 1
fi
case $platform in
apple-silicon) platform=aarch64-apple ;;
generic-aarch64) platform=aarch64 ;;
generic) platform=x86 ;;
esac
if [[ ! $platform =~ ^(x86|aarch64|aarch64-apple)$ ]]; then
echo "Error: the image manifest $manifest names no known platform: ${platform:-none}" >&2
return 1
fi
echo "$platform"
}
if [[ -n $image_dir ]] && [[ -e $manifest || -L $manifest ]] && ! booted; then
platform=$(image_platform) || exit 1
if [[ $platform == "x86" ]]; then
[[ $machine != "aarch64" ]] || contradiction "$manifest names $platform hardware but the CPU is $machine"
else
[[ $machine == "aarch64" ]] || contradiction "$manifest names $platform hardware but the CPU is $machine"
fi
echo "$platform"
exit 0
fi
vendor=""
vendor_source=""
for source in "${sources[@]}"; do
[[ -r $source ]] || continue
apple=0
qcom=0
mapfile -d '' -t tokens <"$source"
for token in "${tokens[@]}"; do
[[ $token == "apple,"* ]] && apple=1
[[ $token == "qcom,"* ]] && qcom=1
done
if (( apple && qcom )); then
contradiction "$source names both Apple and Qualcomm hardware"
elif (( apple )); then
found=apple
elif (( qcom )); then
found=qcom
else
found=none
fi
if [[ -n $vendor && $vendor != "$found" ]]; then
contradiction "$vendor_source says $vendor but $source says $found"
fi
vendor=$found
vendor_source=$source
done
case $vendor in
apple | qcom)
[[ $machine == "aarch64" ]] || contradiction "$vendor_source names $vendor hardware but the CPU is $machine"
if [[ $vendor == "apple" ]]; then
echo aarch64-apple
else
echo aarch64
fi
;;
*)
if [[ $machine == "aarch64" ]]; then
echo aarch64
else
echo x86
fi
;;
esac
+26 -1
View File
@@ -2,5 +2,30 @@
# omarchy:summary=Print the detected Hyprland touchpad or trackpad device name
device=$(hyprctl devices -j | jq -r '[.mice[] | .name | select(test("touchpad|trackpad"; "i"))] | first // empty')
mice=$(hyprctl devices -j | jq -r '.mice[].name')
device=$(grep -iE -m1 'touchpad|trackpad' <<<"$mice")
# Some touchpads carry neither word (Apple Silicon's is "apple-mtp-multi-touch"),
# so ask udev which mouse is one. The name alone can't say: some touchscreens'
# mouse interfaces are called multi-touch too.
if [[ -z $device ]]; then
input_class="${OMARCHY_INPUT_CLASS_PATH:-/sys/class/input}"
for event in "$input_class"/event*; do
[[ -r $event/device/name ]] || continue
# Hyprland's name for a device: the kernel's, lowercased, with spaces,
# commas and newlines as dashes, and -1, -2 and so on after a name another
# device already took.
name=$(<"$event/device/name")
name=${name,,}
name=${name//[ ,$'\n']/-}
udevadm info --query=property "$event" 2>/dev/null | grep -Fxq 'ID_INPUT_TOUCHPAD=1' || continue
while IFS= read -r mouse; do
if [[ $mouse == "$name" || ( $mouse == "$name"-* && ${mouse#"$name"-} =~ ^[0-9]+$ ) ]]; then
device=$mouse
break 2
fi
done <<<"$mice"
done
fi
[[ -n $device ]] && echo "$device"
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
# omarchy:summary=Detect whether the CPU is x86_64.
[[ $(uname -m) == "x86_64" ]]
+41
View File
@@ -0,0 +1,41 @@
#!/bin/bash
# omarchy:summary=Show or set window corners: theme, sharp, or rounded
# omarchy:args=[theme|sharp|rounded]
# omarchy:examples=omarchy hyprland corners | omarchy hyprland corners rounded
TOGGLES_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/omarchy/toggles/hypr"
# Sharp and rounded are toggle files loaded after every theme, so they override
# the corners of whichever theme is current. Theme removes both.
if (( $# == 0 )); then
if [[ -f $TOGGLES_DIR/corners-sharp.lua ]]; then
echo "sharp"
elif [[ -f $TOGGLES_DIR/corners-rounded.lua ]]; then
echo "rounded"
else
echo "theme"
fi
exit 0
fi
case $1 in
theme) LABEL=$(omarchy-text "Corners follow the theme") ;;
sharp) LABEL=$(omarchy-text "Sharp corners for all themes") ;;
rounded) LABEL=$(omarchy-text "Rounded corners for all themes") ;;
*)
echo "Usage: omarchy-hyprland-corners [theme|sharp|rounded]" >&2
exit 1
;;
esac
# Save the new choice before dropping the old one, so a failed save keeps it.
if [[ $1 != "theme" ]]; then
mkdir -p "$TOGGLES_DIR" && cp "$OMARCHY_PATH/default/hypr/toggles/corners-$1.lua" "$TOGGLES_DIR/corners-$1.lua" || exit 1
fi
for mode in sharp rounded; do
[[ $mode == "$1" ]] || rm -f "$TOGGLES_DIR/corners-$mode.lua" || exit 1
done
hyprctl reload >/dev/null || exit 1
omarchy-notification-send -g 󰘇 "$LABEL"
+1
View File
@@ -10,6 +10,7 @@ sync_clamshell() {
(
flock -n 9 || exit 0
omarchy-hyprland-monitor-clamshell
omarchy-system-lid-inhibit
) 9>"$LOCK"
}
+19
View File
@@ -0,0 +1,19 @@
#!/bin/bash
# omarchy:summary=Returns true when the compositor holds a session lock no other locker is alive to own
# omarchy:args=<own-pid>
# omarchy:hidden=true
# Hyprland lets any client take over a held lock (misc:allow_session_lock_restore),
# so a shell that recovers a lock it merely sees can take the live lock screen of
# another shell, and strand it when it exits. A lock is stranded only while no
# other Quickshell or hyprlock is running; until then the answer is undetermined,
# so the caller keeps asking until that locker exits or it gives up.
#
# Exits 0 stranded, 1 unlocked, 2 undetermined. The caller passes its own pid,
# as a crash re-execs the shell in place.
omarchy-hyprland-session-locked || exit
if pgrep -u "$UID" -x 'quickshell|qs|hyprlock' | grep -qvxF "$1"; then
exit 2
fi
+70
View File
@@ -0,0 +1,70 @@
#!/bin/bash
# omarchy:summary=Show or set how see-through windows are: opaque, transparent, or glassy
# omarchy:args=[opaque|transparent|glassy]
# omarchy:examples=omarchy hyprland window transparency | omarchy hyprland window transparency glassy
TOGGLES_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/omarchy/toggles/hypr"
# Opaque and glassy are toggle files loaded after every theme: opaque makes the
# windows Omarchy makes transparent solid, and glassy blurs what is behind any
# see-through window and gives terminals a see-through background. Transparent,
# the default, removes both.
current_mode() {
if [[ -f $TOGGLES_DIR/window-opaque.lua ]]; then
echo "opaque"
elif [[ -f $TOGGLES_DIR/window-glassy.lua ]]; then
echo "glassy"
else
echo "transparent"
fi
}
# Save the new choice before dropping the old one, so a failed save keeps it.
save_mode() {
if [[ $1 != "transparent" ]]; then
mkdir -p "$TOGGLES_DIR" && cp "$OMARCHY_PATH/default/hypr/toggles/window-$1.lua" "$TOGGLES_DIR/window-$1.lua" || return 1
fi
local mode
for mode in opaque glassy; do
[[ $mode == "$1" ]] || rm -f "$TOGGLES_DIR/window-$mode.lua" || return 1
done
}
if (( $# == 0 )); then
current_mode
exit 0
fi
case $1 in
opaque) LABEL=$(omarchy-text "Opaque windows") ;;
transparent) LABEL=$(omarchy-text "Transparent windows") ;;
glassy) LABEL=$(omarchy-text "Glassy windows") ;;
*)
echo "Usage: omarchy-hyprland-window-transparency [opaque|transparent|glassy]" >&2
exit 1
;;
esac
# A theme switch renders the next theme's terminal files from the saved choice
# and then replaces the current theme, so the choice and the glass written into
# the current theme are changed under its lock, or the switch could drop them.
exec 9>"${XDG_RUNTIME_DIR:-/tmp}/omarchy-theme-set.lock"
flock 9
# A choice whose glass cannot be written is taken back, so the saved choice
# is always the one on screen.
previous=$(current_mode)
save_mode "$1" || exit 1
if ! omarchy-theme-set-glass; then
save_mode "$previous"
omarchy-theme-set-glass
exit 1
fi
flock -u 9
hyprctl reload >/dev/null || exit 1
omarchy-restart-terminal
omarchy-theme-set-foot
omarchy-restart-btop
omarchy-notification-send -g 󰂵 "$LABEL"
+28
View File
@@ -0,0 +1,28 @@
#!/bin/bash
# omarchy:summary=Print the layout of a workspace: dwindle, scrolling, or floating
# omarchy:args=[workspace-id]
if [[ -n $1 ]]; then
WORKSPACE_ID=$1
TILED_LAYOUT=$(hyprctl workspaces -j | jq -r --arg id "$WORKSPACE_ID" 'first(.[] | select((.id | tostring) == $id) | .tiledLayout) // empty')
else
read -r WORKSPACE_ID TILED_LAYOUT < <(hyprctl activeworkspace -j | jq -r '"\(.id) \(.tiledLayout // "")"')
fi
MODES_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/omarchy/workspace-layouts"
# Hyprland only reports the tiled layout, and floating is not one, so the mode is
# read back from the file omarchy-hyprland-workspace-layout-toggle wrote. Files
# saved before floating existed name no mode, and a workspace without a mode of
# its own follows the default layout.
if [[ $WORKSPACE_ID =~ ^[0-9]+$ ]] && (( WORKSPACE_ID >= 1 )); then
MODE=$(sed -n 's/.*mode = "\([a-z]*\)".*/\1/p' "$MODES_DIR/$WORKSPACE_ID.lua" 2>/dev/null)
if [[ -z $MODE && -f $MODES_DIR/default.lua ]]; then
MODE=$(omarchy-hyprland-workspace-layout-default)
fi
fi
MODE=${MODE:-$TILED_LAYOUT}
echo "${MODE:-dwindle}"
+35
View File
@@ -0,0 +1,35 @@
#!/bin/bash
# omarchy:summary=Show or set the layout of workspaces without one of their own: dwindle, scrolling, or floating
# omarchy:args=[dwindle|scrolling|floating]
# omarchy:examples=omarchy hyprland workspace layout default | omarchy hyprland workspace layout default scrolling
MODES_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/omarchy/workspace-layouts"
DEFAULT_FILE="$MODES_DIR/default.lua"
# Without a saved default, Hyprland's own layout is the default, which the
# user's looknfeel may have set.
if (( $# == 0 )); then
MODE=$(sed -n 's/.*default = "\([a-z]*\)".*/\1/p' "$DEFAULT_FILE" 2>/dev/null)
[[ -n $MODE ]] || MODE=$(hyprctl getoption general.layout -j 2>/dev/null | jq -r '.str // empty')
echo "${MODE:-dwindle}"
exit 0
fi
case $1 in
dwindle) LABEL=tiling ;;
scrolling | floating) LABEL=$1 ;;
*)
echo "Usage: omarchy-hyprland-workspace-layout-default [dwindle|scrolling|floating]" >&2
exit 1
;;
esac
# Saved even for dwindle, so the choice also wins over a layout set in the
# user's own looknfeel. Workspaces given a layout with Super + L keep theirs.
mkdir -p "$MODES_DIR"
printf 'o.workspace_mode({ default = "%s" })\n' "$1" >"$DEFAULT_FILE"
hyprctl reload >/dev/null
omarchy-shell -q omarchy.workspaces refresh
omarchy-notification-send -g 󱂬 "$(omarchy-text "New workspaces use the %1 layout" "$LABEL")"
+62 -14
View File
@@ -1,21 +1,69 @@
#!/bin/bash
# omarchy:summary=Toggle the layout on the current active workspace between dwindle and scrolling
# omarchy:summary=Cycle the current active workspace through the dwindle, scrolling, and floating layouts
# omarchy:args=[back]
DIRECTION=${1:-next}
if [[ $DIRECTION != "next" && $DIRECTION != "back" ]]; then
echo "Usage: omarchy-hyprland-workspace-layout-toggle [back]" >&2
exit 1
fi
ACTIVE_WORKSPACE=$(hyprctl activeworkspace -j | jq -r '.id')
[[ $ACTIVE_WORKSPACE =~ ^-?[0-9]+$ ]] || exit 1
CURRENT_LAYOUT=$(hyprctl activeworkspace -j | jq -r '.tiledLayout')
LAYOUTS_DIR="$HOME/.local/state/omarchy/workspace-layouts"
LAYOUT_FILE="$LAYOUTS_DIR/$ACTIVE_WORKSPACE.lua"
# Named workspaces report a negative id, and Hyprland reads a leading "-" as a
# selector relative to the current workspace: targeting one lands on workspace 1
# and reconfigures that instead.
[[ $ACTIVE_WORKSPACE =~ ^[0-9]+$ ]] || exit 1
MODES_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/omarchy/workspace-layouts"
MODE_FILE="$MODES_DIR/$ACTIVE_WORKSPACE.lua"
case "$CURRENT_LAYOUT" in
dwindle) NEW_LAYOUT=scrolling ;;
*) NEW_LAYOUT=dwindle ;;
esac
CURRENT_MODE=$(omarchy-hyprland-workspace-layout-current)
mkdir -p "$LAYOUTS_DIR"
printf 'hl.workspace_rule({ workspace = "%s", layout = "%s" })\n' "$ACTIVE_WORKSPACE" "$NEW_LAYOUT" >"$LAYOUT_FILE"
if [[ $DIRECTION == "back" ]]; then
case "$CURRENT_MODE" in
dwindle) NEW_MODE=floating ;;
floating) NEW_MODE=scrolling ;;
*) NEW_MODE=dwindle ;;
esac
else
case "$CURRENT_MODE" in
dwindle) NEW_MODE=scrolling ;;
scrolling) NEW_MODE=floating ;;
*) NEW_MODE=dwindle ;;
esac
fi
hyprctl eval "hl.workspace_rule({ workspace = \"$ACTIVE_WORKSPACE\", layout = \"$NEW_LAYOUT\" })" >/dev/null 2>&1 || \
hyprctl keyword workspace "$ACTIVE_WORKSPACE, layout:$NEW_LAYOUT"
omarchy-notification-send -g 󱂬 "$(omarchy-text "Workspace layout set to %1" "$NEW_LAYOUT")"
APPLY="o.workspace_mode({ workspace = \"$ACTIVE_WORKSPACE\", mode = \"$NEW_MODE\" })"
# Saving a mode that did not apply would leave the file naming one layout while
# the screen shows another, and restore it at the next login.
APPLIED=0
if hyprctl eval "$APPLY" >/dev/null 2>&1; then
APPLIED=1
fi
if (( ! APPLIED )); then
omarchy-notification-send -g 󱂬 "$(omarchy-text "Workspace layout needs a Hyprland reload")"
elif [[ $NEW_MODE == "$(omarchy-hyprland-workspace-layout-default)" ]] && rm -f "$MODE_FILE"; then
# Cycled onto the default, a workspace goes back to following it, so a later
# change of the default reaches it too.
omarchy-notification-send -g 󱂬 "$(omarchy-text "Workspace layout set to %1" "$NEW_MODE")"
elif mkdir -p "$MODES_DIR" && printf '%s\n' "$APPLY" >"$MODE_FILE"; then
omarchy-notification-send -g 󱂬 "$(omarchy-text "Workspace layout set to %1" "$NEW_MODE")"
else
# The mode is on screen but nothing will bring it back, and the next press
# reads the mode from this file, so say so rather than counting it as set.
omarchy-notification-send -g 󱂬 "$(omarchy-text "Workspace layout set to %1 but could not be saved" "$NEW_MODE")"
fi
# The bar shows the focused workspace's layout.
if (( APPLIED )); then
omarchy-shell -q omarchy.workspaces refresh
fi
# Titlebars and shadows are set up while the config loads, and only
# when something floats, so the first floating workspace needs a reload to get
# them. Once the titlebar plugin is in, later ones have them already.
if (( APPLIED )) && [[ $NEW_MODE == "floating" ]] && ! hyprctl plugin list -j 2>/dev/null | jq -e 'any(.[]?; .name == "hyprbars")' >/dev/null 2>&1; then
hyprctl reload >/dev/null
fi
+16 -3
View File
@@ -1,9 +1,22 @@
#!/bin/bash
# omarchy:summary=Configure or inspect the input method selected at installation
# omarchy:args=<status|configure [--seed|--defaults] [none|mozc|hangul|pinyin|chewing]>
# omarchy:summary=Configure, inspect, or switch input methods
# omarchy:args=<status|cycle [back]|configure --seed|--defaults>
# omarchy:hidden=true
set -euo pipefail
python "$OMARCHY_PATH/default/input-methods/configure.py" "$@"
if [[ ${1:-} == "cycle" ]]; then
direction=${2:-next}
if [[ $direction != "next" && $direction != "back" ]]; then
echo "Usage: omarchy-input-method cycle [back]" >&2
exit 1
fi
# The shell's reader retains a choice made while nothing has focus.
if [[ $(omarchy-shell shell cycleInput "$direction") != "ok" ]]; then
echo "The shell's input reader is not running" >&2
exit 1
fi
else
/usr/bin/python3 "$OMARCHY_PATH/default/input-methods/configure.py" "$@"
fi
+22
View File
@@ -8,6 +8,8 @@ set -e
source "$OMARCHY_PATH/install/helpers/browser-policy.sh"
opt_path="${OMARCHY_OPT_PATH:-/opt}"
setup_chromium_policy_directory() {
browser_policy_setup_dir "$1"
}
@@ -17,6 +19,17 @@ announce_browser_installed() {
echo "$1 browser installed. Make it the default via Setup > Defaults > Browser."
}
# The widevine package registers its CDM for Chromium and Firefox only; Brave
# looks for one in its own install directory.
link_widevine_cdm() {
local cdm="$opt_path/WidevineCdm/chromium"
local install_dir="$opt_path/$1"
if [[ -d $cdm && -d $install_dir && ! -e $install_dir/WidevineCdm ]]; then
as_root ln -sfn "$cdm" "$install_dir/WidevineCdm"
fi
}
copy_chromium_flags() {
mkdir -p ~/.config
cp -f "$OMARCHY_PATH/config/chromium-flags.conf" "$1"
@@ -37,6 +50,7 @@ chromium)
setup_chromium_policy_directory /etc/chromium/policies/managed
copy_chromium_flags ~/.config/chromium-flags.conf
omarchy-theme-set-browser
omarchy-lifecycle-dispatch post-install chromium
announce_browser_installed "Chromium"
;;
chrome)
@@ -46,6 +60,7 @@ chrome)
setup_chromium_policy_directory /etc/opt/chrome/policies/managed
copy_chromium_flags ~/.config/chrome-flags.conf
omarchy-theme-set-browser
omarchy-lifecycle-dispatch post-install chrome
announce_browser_installed "Chrome"
;;
edge)
@@ -55,6 +70,7 @@ edge)
setup_chromium_policy_directory /etc/opt/edge/policies/managed
copy_chromium_flags ~/.config/microsoft-edge-stable-flags.conf
omarchy-theme-set-browser
omarchy-lifecycle-dispatch post-install edge
announce_browser_installed "Edge"
;;
brave)
@@ -63,7 +79,9 @@ brave)
setup_chromium_policy_directory /etc/brave/policies/managed
copy_chromium_flags ~/.config/brave-flags.conf
link_widevine_cdm brave-bin
omarchy-theme-set-browser
omarchy-lifecycle-dispatch post-install brave
announce_browser_installed "Brave"
;;
brave-origin)
@@ -72,7 +90,9 @@ brave-origin)
setup_chromium_policy_directory /etc/brave/policies/managed
copy_chromium_flags ~/.config/brave-origin-flags.conf
link_widevine_cdm brave-origin-bin
omarchy-theme-set-browser
omarchy-lifecycle-dispatch post-install brave-origin
announce_browser_installed "Brave Origin"
;;
firefox)
@@ -81,6 +101,7 @@ firefox)
browser_policy_setup_firefox_distribution /usr/lib/firefox/distribution
setup_firefox_wayland
omarchy-lifecycle-dispatch post-install firefox
announce_browser_installed "Firefox"
;;
zen)
@@ -89,6 +110,7 @@ zen)
browser_policy_setup_firefox_distribution /opt/zen-browser/distribution
setup_firefox_wayland
omarchy-lifecycle-dispatch post-install zen
announce_browser_installed "Zen"
;;
*)
+3
View File
@@ -92,6 +92,9 @@ phoenix)
;;
rust)
echo -e "Installing Rust...\n"
# Crates that generate C bindings with bindgen, including Ruby gems built
# with rb-sys, load libclang from clang.
omarchy-pkg-add clang
bash -c "$(curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs)" -- -y
;;
java)
+4 -1
View File
@@ -5,6 +5,8 @@
set -e
voxtype_lib_path="${OMARCHY_VOXTYPE_LIB_PATH:-/usr/lib/voxtype}"
if omarchy-cmd-missing voxtype; then
if gum confirm "Install Voxtype + AI model (~150MB) to enable dictation?"; then
omarchy-pkg-add wtype voxtype-bin
@@ -17,7 +19,8 @@ config_home="${XDG_CONFIG_HOME:-$HOME/.config}"
mkdir -p "$config_home/voxtype"
if [[ ! -e $config_home/voxtype/config.toml ]]; then
cp "$OMARCHY_PATH/default/voxtype/config.toml" "$config_home/voxtype/"
if omarchy-hw-vulkan; then
# A Vulkan runtime is not enough: aarch64 voxtype-bin ships no Vulkan backend.
if omarchy-hw-vulkan && [[ -x $voxtype_lib_path/voxtype-vulkan ]]; then
voxtype setup gpu --enable || true
fi
fi
+1
View File
@@ -15,6 +15,7 @@ echo "Installing Steam..."
# GPU.
omarchy-install-gaming-gpu-lib32
omarchy-pkg-add steam
omarchy-lifecycle-dispatch post-install steam
echo ""
echo "Steam will start automatically now. This might take a while..."
+2 -4
View File
@@ -15,17 +15,15 @@ if gum confirm "Are you sure you want to restore all preinstalled web apps, TUI
aether \
cliamp \
libreoffice-fresh \
xournalpp \
pinta \
obsidian \
obs-studio \
kdenlive \
moonlight-qt \
omacut \
monologue \
omacalc \
omawrite \
hype; then
hype \
signed; then
echo -e "\nPreinstalls are still marked as removed. Fix the errors above and try again."
exit 1
fi
+200
View File
@@ -0,0 +1,200 @@
#!/bin/bash -p
# omarchy:summary=Run a fixed lifecycle operation through the platform's own packages
# omarchy:args=[--resolve] <operation> [arguments...]
# omarchy:requires-sudo=true
# omarchy:hidden=true
# Provisioning, factory reset, update, boot, system and user setup and app
# installs call a fixed set of operations here. A platform that needs its own
# handling registers directories of root-owned entrypoints below; on every other
# platform each operation is a no-op and the caller keeps its generic path. The
# contract is docs/lifecycle-dispatch.md.
#
# --resolve prints the entrypoint an operation would run, or nothing when it is
# a no-op here, so a caller can choose between a platform implementation and
# its own generic path.
#
# Root runs this from provisioning, factory reset and update, and it decides
# what runs as root. -p keeps a root caller's environment from running code in
# this shell before the first line (bash then ignores BASH_ENV and exported
# functions), so a launch without it is refused.
if [[ $- != *p* ]]; then
echo "Refusing an unsafe Bash startup." >&2
exit 126
fi
set -euo pipefail
operations=(
provision-prepare provision-commit provision-verify
reset-prepare reset-verify reset-commit reset-rollback
update-verify update-takeover
luks-slots
setup-boot setup-system setup-user
post-install pre-remove
)
# Run as the user, never as root, with that user's session.
user_operations=(setup-user post-install pre-remove)
usage() {
echo "Usage: omarchy-lifecycle-dispatch [--resolve] <operation> [arguments...]" >&2
echo "Operations: ${operations[*]}" >&2
exit 2
}
listed() {
local item=$1 candidate
shift
for candidate in "$@"; do
[[ $candidate == "$item" ]] && return 0
done
return 1
}
resolve=0
if [[ ${1:-} == "--resolve" ]]; then
resolve=1
shift
fi
operation=${1:-}
[[ -n $operation ]] || usage
shift
listed "$operation" "${operations[@]}" || usage
# A privileged caller must not let its environment choose what runs as root:
# root resolves only the fixed paths below, and the detector beside this file
# ignores its fixture roots. Fixture roots exist for unprivileged tests.
fixed_path=/usr/local/sbin:/usr/local/bin:/usr/bin
detect=("$(PATH=$fixed_path dirname -- "${BASH_SOURCE[0]}")/omarchy-hw-platform")
if (( EUID == 0 )); then
export PATH=$fixed_path
detect=(/usr/bin/env -i PATH="$fixed_path" "${detect[@]}")
root=""
else
root=${OMARCHY_LIFECYCLE_ROOT:-}
root=${root%/}
if [[ -n $root && $root != /* ]]; then
echo "Error: OMARCHY_LIFECYCLE_ROOT must be an absolute path" >&2
exit 1
fi
fi
if ! platform=$("${detect[@]}"); then
echo "Error: cannot determine the hardware platform for $operation" >&2
exit 1
fi
# x86 and plain aarch64 machines (Snapdragon laptops among them) have no
# registered package: every operation is a no-op there and callers keep their
# generic path.
# Apple Silicon's boot operations belong to omarchy-mac-boot, and its system and
# user setup and app-install hooks to omarchy-mac (both omacom/omarchy-mac-pkgs).
# Provisioning and factory reset have no other path on a Mac, only update-verify
# checks what an update left in its boot chain, its boot checks prove the kept
# LUKS slots that luks-slots records, and hardware setup has no other way to run
# the Mac's boot setup (setup-boot), so those operations are required. So is
# update-takeover: the Mac boot chain and its image own files no package does,
# and a Mac whose boot package can't vouch for a takeover keeps them. Setup and
# the app-install hooks are optional: a Mac without omarchy-mac keeps Omarchy's
# generic setup and installs. Moving an operation to required also changes
# apple_optional in test/shell.d/lifecycle-dispatch-test.sh and the Apple column
# and registration table in docs/lifecycle-dispatch.md.
case $platform:$operation in
aarch64-apple:setup-system | aarch64-apple:setup-user | aarch64-apple:post-install | aarch64-apple:pre-remove)
implementation=/usr/lib/omarchy/mac
package=omarchy-mac
required=()
;;
aarch64-apple:*)
implementation=/usr/lib/omarchy/mac-boot
package=omarchy-mac-boot
required=(
provision-prepare provision-commit provision-verify
reset-prepare reset-verify reset-commit reset-rollback
update-verify update-takeover
luks-slots
setup-boot
)
;;
*)
exit 0
;;
esac
# Checked once the platform registers the operation, so an installer run with
# sudo still works where the hook is a no-op.
if listed "$operation" "${user_operations[@]}" && (( EUID == 0 )); then
echo "Error: $operation runs as the user, never as root" >&2
exit 1
fi
# Every path from the entrypoint up to / (or the fixture root) must be a real
# file or directory owned by root, or by the unprivileged caller inside a
# fixture root, and writable by nobody else.
trusted() {
local path=$1 top=${root:-/} owner mode
[[ -f $path && -x $path ]] || return 1
while true; do
[[ ! -L $path ]] || return 1
read -r owner mode < <(stat -c '%u %a' -- "$path") || return 1
if (( owner != 0 )); then
[[ -n $root ]] && (( EUID != 0 && owner == EUID )) || return 1
fi
(( (8#$mode & 8#022) == 0 )) || return 1
[[ $path != "$top" ]] || return 0
[[ $path != "/" ]] || return 1
path=$(dirname -- "$path")
done
}
entrypoint=$root$implementation/$operation
# The package's own pacman record tells a machine without it from one whose
# copy predates the operation.
installed_version() {
local record
for record in "$root/var/lib/pacman/local/$package"-[0-9]*/; do
[[ -d $record ]] || continue
record=${record%/}
printf '%s\n' "${record##*/"$package"-}"
return 0
done
return 1
}
# 3 is a machine without the platform's boot package at all, which a caller may
# tell apart from a failure; 1 is everything else.
if [[ ! -e $entrypoint && ! -L $entrypoint ]]; then
if listed "$operation" "${required[@]}"; then
if version=$(installed_version); then
echo "Error: $operation on $platform needs $implementation/$operation, which $package $version does not provide; update $package" >&2
exit 1
fi
echo "Error: $operation on $platform needs $package, which provides $implementation/$operation; it is not installed" >&2
exit 3
fi
exit 0
fi
if ! trusted "$entrypoint"; then
echo "Error: refusing $implementation/$operation: it must be a root-owned executable file in root-owned directories, writable by no one else" >&2
exit 1
fi
if (( resolve )); then
printf '%s\n' "$entrypoint"
exit 0
fi
# A user operation also gets the user's home and session, when there is one,
# and nothing else.
environment=(PATH="$fixed_path")
if listed "$operation" "${user_operations[@]}"; then
for name in HOME USER XDG_CONFIG_HOME XDG_STATE_HOME XDG_RUNTIME_DIR DBUS_SESSION_BUS_ADDRESS WAYLAND_DISPLAY OMARCHY_PATH; do
[[ -z ${!name:-} ]] || environment+=("$name=${!name}")
done
fi
exec /usr/bin/env -i "${environment[@]}" "$entrypoint" "$@"
+25 -3
View File
@@ -9,12 +9,25 @@ declare -A LUA_BIND_KEY_MAP
declare -A LUA_BIND_DISPATCHER_MAP
declare -A LUA_BIND_ARG_MAP
# A platform package can name keys the way its keyboards print them: one
# "<keysym> <name>" per line (on a MacBook, XF86MonBrightnessUp is F2), in the
# fixed platform root, which no environment variable moves.
key_names_file=/usr/share/omarchy-platform/key-names
# Hyprland reports XKB keycodes for code: bindings. Resolve them to symbols
# via the compiled keymap, with a small fallback for common keys so the menu
# remains readable if xkbcli cannot resolve a symbol.
parse_keycodes() {
awk '
awk -v key_names="$key_names_file" '
BEGIN {
while ((getline line < key_names) > 0) {
# Blanks around a line and a CRLF line end read the same, as in
# displays.conf.
gsub(/^[ \t\r]+|[ \t\r]+$/, "", line)
if (split(line, name_parts, /[ \t]+/) == 2 && name_parts[1] ~ /^[A-Za-z0-9_]+$/ && name_parts[2] ~ /^[A-Za-z0-9_]+$/) key_name[name_parts[1]] = name_parts[2]
}
close(key_names)
split("10=1 11=2 12=3 13=4 14=5 15=6 16=7 17=8 18=9 19=0 20=MINUS 21=EQUAL 59=COMMA 60=PERIOD 61=SLASH", fallbacks, " ")
for (i in fallbacks) {
separator = index(fallbacks[i], "=")
@@ -59,6 +72,13 @@ parse_keycodes() {
symbol = mouse_symbol[code]
if (symbol == "") symbol = "mouse:" code
sub("mouse:" code, symbol)
} else if ((first = index($0, ",")) > 0) {
# Only the key (the second field) is renamed: the description, the
# dispatcher and its argument are left as they are.
rest = substr($0, first + 1)
second = index(rest, ",")
key = second > 0 ? substr(rest, 1, second - 1) : rest
if (key in key_name) $0 = substr($0, 1, first) key_name[key] (second > 0 ? substr(rest, second) : "")
}
print
@@ -81,7 +101,9 @@ build_lua_bind_cache() {
LUA_BIND_DISPATCHER_MAP["$cache_key"]="$dispatcher"
LUA_BIND_ARG_MAP["$cache_key"]="$arg"
done < <(
lua <<'LUA'
# -E keeps LUA_INIT and LUA_PATH from the environment out of the scan, which
# loads the user's config.
lua -E - <<'LUA'
local modifiers = { SHIFT = 1, CTRL = 4, CONTROL = 4, ALT = 8, SUPER = 64 }
local function split_keys(keys)
@@ -332,7 +354,6 @@ dynamic_bindings() {
static_bindings() {
echo "SHIFT ALT,L,Copy URL from Web App,sendshortcut,SHIFT ALT,L,"
echo "SHIFT ALT,D,Download Video from Web App,sendshortcut,SHIFT ALT,D,"
echo "CTRL SHIFT,SPACE,Switch input language,exec,fcitx5-remote -t"
}
# Actions Omarchy binds to a second chord meant as an alternative, named one at
@@ -536,6 +557,7 @@ keybindings_cache_key() {
{
printf 'v15\n'
hyprctl devices 2>/dev/null | grep -F 'active keymap:'
cat "$key_names_file" 2>/dev/null
hyprctl binds 2>/dev/null
} | sha256sum | awk '{ print $1 }'
}
+46 -4
View File
@@ -1,9 +1,9 @@
#!/bin/bash
# omarchy:summary=Pick one option from a menu
# omarchy:summary=Pick one or multiple options from a menu
# omarchy:group=menu
# omarchy:name=select
# omarchy:args=prompt [option...] [-- menu args...]
# omarchy:args=prompt [option...] [-- --multiple --selected <value> --on-change <command-json> --change-key <key> --width <pixels> --maxheight <pixels>]
# omarchy:examples=omarchy menu select Format jpg png|omarchy-menu-select Resolution 4k 1080p 720p -- --width 400
# An option may lead with an icon, as "<glyph><TAB><label>", and may trail a
@@ -12,6 +12,12 @@
# alone; an option with a subtext returns "<label><TAB><subtext>", so callers
# with same-named rows get the subtext back as the stable key.
# With --multiple, clicking/Enter toggles rows and Apply returns a JSON array.
# --on-change takes a JSON command array; each toggle runs it with the selection
# JSON appended, applies immediately, and needs no Apply row. A successful
# command may print a JSON array of actual selections to update the checkmarks.
# --selected takes a returned value (label plus any subtext), and can be repeated.
set -euo pipefail
if (( $# < 1 )); then
@@ -25,12 +31,43 @@ shift
options=()
menu_width=""
menu_maxheight=""
multiple=0
on_change="[]"
change_key=""
selected=()
while (( $# > 0 )); do
if [[ $1 == "--" ]]; then
shift
while (( $# > 0 )); do
case "$1" in
--on-change)
shift
if (( $# == 0 )); then
echo "omarchy-menu-select: --on-change requires a JSON command array" >&2
exit 1
fi
on_change="$1"
;;
--change-key)
shift
if (( $# == 0 )); then
echo "omarchy-menu-select: --change-key requires a value" >&2
exit 1
fi
change_key="$1"
;;
--multiple)
multiple=1
;;
--selected)
shift
if (( $# == 0 )); then
echo "omarchy-menu-select: --selected requires a value" >&2
exit 1
fi
selected+=("$1")
;;
--width)
shift
if (( $# == 0 )); then
@@ -73,20 +110,25 @@ rm -f "$done_file"
trap 'rm -f "$selection_file" "$done_file"' EXIT
options_json=$(perl -MEncode=decode -MJSON::PP=encode_json -e 'print encode_json([map { decode("UTF-8", $_) } @ARGV])' "${options[@]}")
selected_json=$(perl -MEncode=decode -MJSON::PP=encode_json -e 'print encode_json([map { decode("UTF-8", $_) } @ARGV])' "${selected[@]}")
payload=$(perl -MEncode=decode -MJSON::PP=encode_json,decode_json -e '
my $payload = {
mode => "select",
prompt => decode("UTF-8", $ARGV[0]),
options => decode_json($ARGV[1]),
multiple => $ARGV[6] ? JSON::PP::true : JSON::PP::false,
selected => decode_json($ARGV[7]),
onChange => decode_json($ARGV[8]),
changeKey => $ARGV[9],
selectionFile => decode("UTF-8", $ARGV[2]),
doneFile => decode("UTF-8", $ARGV[3])
};
$payload->{width} = int($ARGV[4]) if length($ARGV[4] // "");
$payload->{maxHeight} = int($ARGV[5]) if length($ARGV[5] // "");
print encode_json($payload)
' "$prompt" "$options_json" "$selection_file" "$done_file" "$menu_width" "$menu_maxheight")
' "$prompt" "$options_json" "$selection_file" "$done_file" "$menu_width" "$menu_maxheight" "$multiple" "$selected_json" "$on_change" "$change_key")
omarchy-shell shell summon omarchy.menu "$payload" >/dev/null
omarchy-shell shell summon omarchy.menu "$payload" >/dev/null || exit 2
while [[ ! -e $done_file ]]; do
sleep 0.05
+53
View File
@@ -0,0 +1,53 @@
#!/bin/bash
# omarchy:summary=Print the default package set for this computer's platform, or for the named one: the base list plus its architecture's and platform's additions.
# omarchy:args=[x86|aarch64|aarch64-apple]
# omarchy:hidden=true
set -eo pipefail
platform=${1:-}
if [[ -z $platform ]]; then
platform=$(omarchy-hw-platform)
fi
# Image builders written before the platform names settled pass the old ones.
case $platform in
apple-silicon) platform=aarch64-apple ;;
generic-aarch64) platform=aarch64 ;;
generic) platform=x86 ;;
esac
case $platform in
x86) lists=(base) ;;
aarch64) lists=(base aarch64) ;;
aarch64-apple) lists=(base aarch64 "$platform") ;;
*)
echo "Error: unknown platform '$platform'" >&2
exit 1
;;
esac
# The base list always exists. An architecture or platform with nothing to add
# has no list of its own.
files=()
for list in "${lists[@]}"; do
file="$OMARCHY_PATH/install/omarchy-$list.packages"
if [[ $list == "base" || -f $file ]]; then
files+=("$file")
fi
done
names() {
sed -e 's/[[:space:]]*#.*$//' -e '/^[[:space:]]*$/d' "$@"
}
# One name per line, first occurrence kept, in list order. An aarch64 platform
# leaves out the base packages that only x86_64 builds.
if [[ $platform == "x86" ]]; then
names "${files[@]}" | awk '!seen[$0]++'
else
names "${files[@]}" | awk -v skips=<(names "$OMARCHY_PATH/install/omarchy-x86_64-only.packages") '
BEGIN { while ((getline name < skips) > 0) skip[name] = 1 }
!skip[$0] && !seen[$0]++'
fi
+1
View File
@@ -56,6 +56,7 @@ if [[ -z ${profile:-} ]] || ! profile_available "$profile"; then
fi
powerprofilesctl set "$profile" || exit
omarchy-shell -q battery refreshPowerProfile >/dev/null 2>&1 || true
if [[ -n $requested_profile ]]; then
mkdir -p "$state_dir"
+4
View File
@@ -75,6 +75,10 @@ run_first_run_step "install agent setup post-update hook" \
run_first_run_step "enable user systemd units" \
bash "$OMARCHY_PATH/install/user/first-run/enable-user-units.sh"
# Finalization ran it too, maybe without a session; the platform finishes what
# needs one here.
run_first_run_step "run the platform's user setup" \
omarchy-lifecycle-dispatch setup-user
run_first_run_step "set GNOME theme" \
bash "$OMARCHY_PATH/install/user/first-run/gnome-theme.sh"
run_first_run_step "set GTK primary paste" \
+162
View File
@@ -0,0 +1,162 @@
#!/bin/bash
# omarchy:summary=Finish the hardware setup an image build deferred to this machine
# omarchy:requires-sudo=true
# omarchy:hidden=true
# Started by omarchy-provision-hardware.service on every boot while an image's
# deferred steps remain (see install/helpers/image-target.sh), and safe to call
# from a platform's own first boot as well: runs are serialised. The first run
# retires the image's build manifest, so from then on this machine is itself.
# Steps run in order and each one that succeeds leaves the queue, so a failure
# stops the run and the next run resumes at that step. Once the queue is empty
# the initramfs is rebuilt if any step changed its configuration or asked for
# the rebuild, and the service is disabled. With nothing deferred this does
# nothing. A failed step, or a failure to make the pacman keyring the build
# asked for, exits 75 (it stays queued for the next run); anything else that
# stops the run, such as an untrusted manifest or queue, exits 1.
set -euo pipefail
fail() {
echo "Error: $*" >&2
exit 1
}
if (( EUID == 0 )); then
export OMARCHY_PATH=/usr/share/omarchy
export PATH=/usr/share/omarchy/bin:/usr/local/sbin:/usr/local/bin:/usr/bin
elif [[ -z ${OMARCHY_IMAGE_ROOT:-} || -z ${OMARCHY_PATH:-} ]]; then
fail "omarchy-provision-hardware must run as root"
fi
export OMARCHY_INSTALL=$OMARCHY_PATH/install
export OMARCHY_INSTALL_USER=""
source "$OMARCHY_INSTALL/helpers/logging.sh"
source "$OMARCHY_INSTALL/helpers/image-target.sh"
omarchy_image_init
export OMARCHY_INSTALL_LOG_FILE=$omarchy_image_root/var/log/omarchy-install.log
queue_present() {
[[ -e $omarchy_image_queue || -L $omarchy_image_queue ]]
}
# Anything left for the first boot: the manifest, a queue, or the keyring an
# image build asked for, which is owed even when no step was queued.
work_present() {
omarchy_image_manifest_present || queue_present || [[ -e $omarchy_image_keyring_request ]]
}
work_present || exit 0
omarchy_image_lock || exit 1
# Another run may have finished while this one waited for the lock.
work_present || exit 0
if omarchy_image_manifest_present; then
omarchy_image_read_manifest || exit 1
mv -f "$omarchy_image_manifest" "$omarchy_image_manifest.booted" || fail "could not retire $omarchy_image_manifest"
live_platform=$(omarchy-hw-platform) || live_platform=unknown
echo "First boot of an image built for $omarchy_image_platform, on $live_platform hardware"
fi
remaining=()
if queue_present; then
[[ -f $omarchy_image_queue ]] && omarchy_image_trusted "$omarchy_image_queue" ||
fail "$omarchy_image_queue is not a root-owned regular file"
mapfile -t queued <"$omarchy_image_queue"
for step in "${queued[@]}"; do
[[ -z $step ]] || remaining+=("$step")
done
fi
# What the boot image is built from: mkinitcpio's configuration, the module
# options its modconf hook copies in (leaves such as nvidia.sh write them), and
# the kernel command line Limine's entry tool adds (leaves such as intel/fred.sh
# add to it).
initramfs_inputs() {
local file
for file in "$omarchy_image_root/etc/mkinitcpio.conf" "$omarchy_image_root/etc/mkinitcpio.conf.d/"* \
"$omarchy_image_root/etc/modprobe.d/"* "$omarchy_image_root/usr/lib/modprobe.d/"* \
"$omarchy_image_root/etc/default/limine" "$omarchy_image_root/etc/limine-entry-tool.d/"*; do
[[ -e $file ]] || continue
printf '%s\0' "$file"
cat -- "$file"
done | cksum
}
# Recorded before the first step and kept until a rebuild lands, so a step that
# changed the initramfs and then failed or lost power still gets one.
if [[ ! -f $omarchy_image_initramfs_baseline ]]; then
baseline=$(mktemp "$omarchy_image_dir/.initramfs-inputs.XXXXXX") &&
initramfs_inputs >"$baseline" && chmod 0644 "$baseline" && mv -f "$baseline" "$omarchy_image_initramfs_baseline" ||
fail "could not record the initramfs inputs"
fi
mkdir -p "$(dirname "$OMARCHY_INSTALL_LOG_FILE")"
# A step that would build the boot image itself (a platform's boot setup) asks
# for this run's rebuild instead, so the image is built once, after the last
# step. The request stays on disk until that rebuild lands.
export OMARCHY_IMAGE_BOOT_REBUILD=$omarchy_image_boot_rebuild
# Tells a step it runs on the booted machine, after boot already brought up the
# targets its units hang off (see install/hardware/bluetooth.sh).
export OMARCHY_IMAGE_DEFERRED_HARDWARE=1
# An image build asks for this machine's own pacman keyring (see
# install/post-install/pacman.sh), made before any step installs a package. Any
# keyring the build or an interrupted run left is replaced; the request stays
# until a keyring is complete.
if [[ -e $omarchy_image_keyring_request ]]; then
echo "Creating this machine's pacman keyring"
keyring=$omarchy_image_root/etc/pacman.d/gnupg
if ! { rm -rf "$keyring" && pacman-key --gpgdir "$keyring" --init &&
pacman-key --gpgdir "$keyring" --populate; } </dev/null >>"$OMARCHY_INSTALL_LOG_FILE" 2>&1; then
echo "Error: could not create the pacman keyring (see $OMARCHY_INSTALL_LOG_FILE); the next run retries it" >&2
exit 75
fi
rm -f "$omarchy_image_keyring_request"
fi
while (( ${#remaining[@]} )); do
step=${remaining[0]}
[[ $step == install/hardware/*.sh && $step != *..* ]] || fail "Deferred step is not a hardware setup step: $step"
script=$OMARCHY_PATH/$step
if [[ -L $script ]]; then
fail "Deferred step is a symlink: $step"
elif [[ ! -e $script ]]; then
echo "Skipping deferred hardware step $step: this Omarchy no longer ships it"
else
echo "Running deferred hardware step $step"
if ! run_logged "$script"; then
echo "Error: Deferred hardware step failed: $step (see $OMARCHY_INSTALL_LOG_FILE); the next run resumes here" >&2
exit 75
fi
fi
remaining=("${remaining[@]:1}")
omarchy_image_write_queue "${remaining[@]}" || fail "could not update $omarchy_image_queue"
done
if [[ -e $omarchy_image_boot_rebuild || $(initramfs_inputs) != "$(<"$omarchy_image_initramfs_baseline")" ]]; then
echo "Rebuilding the initramfs for the deferred hardware setup"
if omarchy-cmd-present limine-mkinitcpio; then
limine-mkinitcpio </dev/null >>"$OMARCHY_INSTALL_LOG_FILE" 2>&1
else
mkinitcpio -P </dev/null >>"$OMARCHY_INSTALL_LOG_FILE" 2>&1
fi || fail "Initramfs rebuild failed (see $OMARCHY_INSTALL_LOG_FILE); the next run retries it"
# The new image must be on disk before the queue that owes it is cleared.
sync || fail "could not flush the rebuilt initramfs to disk"
fi
rm -f "$omarchy_image_initramfs_baseline" "$omarchy_image_boot_rebuild"
# Disabled, not deleted: a platform first boot that runs this while the
# service's start job waits would otherwise fail that job at the next
# daemon-reload. The emptied queue keeps the unit from running.
rm -f "$omarchy_image_systemd_dir/multi-user.target.wants/$omarchy_image_unit"
rm -f "$omarchy_image_queue"
if (( EUID == 0 )); then
systemctl daemon-reload 2>/dev/null || true
fi
echo "Deferred hardware setup is complete"
+256 -91
View File
@@ -9,7 +9,9 @@
# behind. Asks for the user (the configurator's Step 2), creates it with the
# groups system setup recorded, finalizes it offline from the stashed Node
# tarball, re-keys LUKS from the throwaway install passphrase to the user's
# password, and hands off to SDDM.
# password, and hands off to SDDM. A platform whose boot package owns its boot
# chain prepares it, drops the install key's unlock from it and records the
# kept key slot through omarchy-lifecycle-dispatch.
set -euo pipefail
@@ -33,6 +35,9 @@ fi
export PATH="$OMARCHY_PATH/bin:$PATH"
REKEY_STATE=$PROVISIONING_DIR/luks-rekey.state
source "$OMARCHY_PATH/install/provisioning/luks-rekey.sh"
# The shared setup form — the same questions, prompts, and validation the ISO
# configurator's user step asks. The ISO vendors this very file out of the
# runtime package it bundles, so an install and the first boot that finishes it
@@ -182,11 +187,11 @@ tips=(
"Super + Space opens the Omarchy menu for apps, settings, and more"
"Super + K shows all the key bindings"
"Super is the Windows or command key on your keyboard"
"Use Xournal++ to sign PDFs"
"Sign PDFs and fill them in with Signed"
"Share files with phones and laptops using LocalSend"
"Turn any website into an app with Install > Web App in the menu"
"Super + Return opens a terminal, Super + Shift + Return the browser"
"Edit images with Pinta, videos with Kdenlive, docs with LibreOffice"
"Edit images with Pinta, trim videos with Omacut, docs with LibreOffice"
"Super + Ctrl + Print grabs text off the screen with OCR"
"Print takes a screenshot, Alt + Print records the screen"
"Set a reminder with Super + Ctrl + R"
@@ -579,14 +584,27 @@ greeter_screen() {
# The keyboard step the ISO configurator runs — deferred to first boot for OEM
# installs, so the machine's owner picks their own layout. Applied immediately
# (live VT + persisted) so the password typed next, and the LUKS re-key below,
# use the chosen layout.
# use the chosen layout. A layout that did not take is asked for again: the
# password form must never be typed in a layout the disk unlock won't use.
# Three failures in a row mean the machine can't set a layout at all (a
# missing or broken localectl, say), so the attempt fails and setup's own
# retry-or-console screen takes over instead of asking forever.
keyboard_form() {
local status
local status failures=0
while true; do
step "Let's setup your keyboard..."
omarchy_prompt_keyboard && status=0 || status=$?
((status == 0)) && break
if ((status == 0)); then
apply_keyboard "$keyboard" && break
if ((++failures >= 3)); then
log_step "no keyboard layout could be set after $failures attempts"
notice "Setup could not set a keyboard layout on this machine." 4
return 1
fi
notice "Could not set the ${keyboard_label:-$keyboard} keyboard layout. Choose it again." 4
continue
fi
# Esc means "back", and nothing precedes the first screen, so re-ask.
((status == OMARCHY_FORM_BACK)) && continue
@@ -598,7 +616,6 @@ keyboard_form() {
confirm_reboot && exec systemctl reboot
done
apply_keyboard "$keyboard"
omarchy_persist_input_selection
}
@@ -610,21 +627,73 @@ confirm_reboot() {
gum confirm --affirmative "Yes, reboot" --negative "No, keep setting up" "Reboot this machine?"
}
# Load the layout on the live VT and persist it for the installed system.
# systemd-firstboot writes both the console KEYMAP and the XKB layout Hyprland
# reads, matching what the ISO's configure_keyboard does at install time. A
# keymap localectl doesn't know keeps the default rather than failing (defensive;
# the picker no longer offers any such layout).
apply_keyboard() {
local keymap="$1"
[[ $(tty 2>/dev/null) == /dev/tty* ]] && loadkeys "$keymap" 2>/dev/null || true
# XKB settings for the form's keymaps that kbd-model-map lacks, as its rows for
# the nearest keymap give them, so the desktop types what the console does:
# kbd's cz is QWERTY and bg-cp1251 phonetic. Non-Latin layouts keep a US layout
# to switch to, so the shared password stays typeable.
keyboard_xkb_settings() {
local layout variant="" options=terminate:ctrl_alt_bksp
local switch=terminate:ctrl_alt_bksp,grp:shifts_toggle,grp_led:scroll
if localectl --no-pager list-keymaps 2>/dev/null | grep -qix "$keymap"; then
systemd-firstboot --keymap="$keymap" --force >>"$LOG_FILE" 2>&1 || \
localectl set-keymap "$keymap" >>"$LOG_FILE" 2>&1 || \
log_step "could not persist keymap $keymap"
else
log_step "keymap $keymap unknown to localectl; keeping the default"
case $1 in
colemak) layout=us variant=colemak ;;
bg-cp1251) layout=bg,us variant=phonetic, options=$switch ;;
cz) layout=cz,us variant=qwerty, options=$switch ;;
de_CH-latin1) layout=ch ;;
kyrgyz) layout=kg,us options=$switch ;;
no-latin1) layout=no ;;
pl) layout=pl ;;
ua) layout=ua,us options=$switch ;;
*) return 1 ;;
esac
printf 'XKBLAYOUT=%s\nXKBMODEL=pc105\n' "$layout"
[[ -z $variant ]] || printf 'XKBVARIANT=%s\n' "$variant"
printf 'XKBOPTIONS=%s\n' "$options"
}
# Load the layout on the live VT and persist it for the installed system, or
# fail. systemd-firstboot writes both the console KEYMAP and the XKB layout
# Hyprland reads, matching what the ISO's configure_keyboard does at install
# time. The boot files ask for the disk password in the layout vconsole.conf
# names, so a keymap localectl doesn't know, a failed loadkeys, or a file that
# does not read back as the chosen keymap with an XKB layout is a failure.
apply_keyboard() {
local keymap=$1 keymaps saved
if [[ $(tty 2>/dev/null) == /dev/tty* ]] && ! loadkeys "$keymap" >>"$LOG_FILE" 2>&1; then
log_step "loadkeys could not load keymap $keymap"
return 1
fi
if ! keymaps=$(localectl --no-pager list-keymaps 2>>"$LOG_FILE"); then
log_step "localectl could not list keymaps to check $keymap"
return 1
fi
if ! grep -qxF -- "$keymap" <<<"$keymaps"; then
log_step "keymap $keymap unknown to localectl"
return 1
fi
if ! systemd-firstboot --keymap="$keymap" --force >>"$LOG_FILE" 2>&1 &&
! localectl set-keymap "$keymap" >>"$LOG_FILE" 2>&1; then
log_step "could not persist keymap $keymap"
return 1
fi
# systemd-firstboot writes an XKB layout only for keymaps in kbd-model-map,
# and several the form offers are not, so Hyprland would come up in US.
if ! grep -q '^XKBLAYOUT=.' /etc/vconsole.conf 2>/dev/null; then
keyboard_xkb_settings "$keymap" >>/etc/vconsole.conf 2>>"$LOG_FILE" ||
log_step "no XKB layout known for keymap $keymap"
fi
# A clean shell, so nothing this process exported stands in for the file.
saved=$(env -i /bin/bash --noprofile --norc -c \
'. /etc/vconsole.conf && [[ -n ${XKBLAYOUT:-} ]] && printf %s "${KEYMAP:-}"' 2>>"$LOG_FILE") || saved=""
if [[ $saved != "$keymap" ]]; then
log_step "/etc/vconsole.conf does not read back as keymap $keymap with an XKB layout"
return 1
fi
}
@@ -715,7 +784,7 @@ luks_device() {
local src part
src=$(findmnt -no SOURCE / | sed 's/\[.*//')
[[ -n $src ]] || return 1
part=$(lsblk -nspo NAME,FSTYPE "$src" 2>/dev/null | awk '$2=="crypto_LUKS"{print $1; exit}')
part=$(lsblk -nsrpo NAME,FSTYPE "$src" 2>/dev/null | awk '$2=="crypto_LUKS"{print $1; exit}')
[[ -n $part ]] && { echo "$part"; return 0; }
return 1
}
@@ -823,13 +892,24 @@ configure_hostname() {
# step, overwriting the placeholder (UTC) the deferred install seeded.
configure_timezone() {
[[ -n ${timezone:-} ]] || return 0
if timedatectl set-timezone "$timezone" 2>&1; then
return 0
if ! timedatectl set-timezone "$timezone" 2>&1; then
# Fallback only if the zone file exists, so a bad zone can't point
# /etc/localtime at nothing and break every localtime read afterward.
[[ -e /usr/share/zoneinfo/$timezone ]] &&
ln -sf "/usr/share/zoneinfo/$timezone" /etc/localtime 2>&1 || true
fi
configure_wireless_regdom
}
# The install's hardware step found only the UTC placeholder, which names no
# country, so the wireless regulatory domain comes from the owner's timezone
# here. No reboot follows setup, so the radios get it now the way udev applies
# it at every boot.
configure_wireless_regdom() {
bash "$OMARCHY_PATH/install/hardware/set-wireless-regdom.sh" 2>&1 || return 0
if omarchy-cmd-present set-wireless-regdom; then
set-wireless-regdom 2>&1 || true
fi
# Fallback only if the zone file exists, so a bad zone can't point
# /etc/localtime at nothing and break every localtime read afterward.
[[ -e /usr/share/zoneinfo/$timezone ]] &&
ln -sf "/usr/share/zoneinfo/$timezone" /etc/localtime 2>&1 || true
}
finalize_user() {
@@ -852,16 +932,99 @@ finalize_user() {
"$OMARCHY_PATH/bin/omarchy-provision-user" --force --first-install
}
# During the provisioning window the staged key rides in the UKI's initramfs,
# named on the kernel cmdline, so boot unlocks unattended.
PROVISIONING_UNLOCK_FILES=(
/etc/omarchy/provisioning.key
/etc/limine-entry-tool.d/99-omarchy-provisioning-unlock.conf
/etc/mkinitcpio.conf.d/99-omarchy-provisioning-key.conf
)
limine_auto_unlock_present() {
local file
for file in "${PROVISIONING_UNLOCK_FILES[@]}"; do
[[ -e $file ]] && return 0
done
return 1
}
# Rebuild the UKI without the embedded keyfile. A failed rebuild restores the
# auto-unlock, so the retry still boots unattended.
limine_auto_unlock_drop() {
rm -f "${PROVISIONING_UNLOCK_FILES[@]}"
reset_limine_config
if limine-update >>"$LOG_FILE" 2>&1; then
return 0
fi
# Once the staged key is destroyed there is no auto-unlock to restore: the
# boot files already ask for the owner's password. The rebuild is still
# retried for the boot entries.
if [[ -f $PROVISIONING_DIR/luks-key ]]; then
log_step "limine-update failed during re-key; restoring auto-unlock for retry"
install -Dm600 "$PROVISIONING_DIR/luks-key" /etc/omarchy/provisioning.key
echo 'KERNEL_CMDLINE[default]+=" cryptkey=rootfs:/etc/omarchy/provisioning.key"' \
>/etc/limine-entry-tool.d/99-omarchy-provisioning-unlock.conf
echo 'FILES+=(/etc/omarchy/provisioning.key)' >/etc/mkinitcpio.conf.d/99-omarchy-provisioning-key.conf
else
log_step "limine-update failed during re-key; the staged key is gone, so no auto-unlock is restored"
fi
limine-update >>"$LOG_FILE" 2>&1 || true
return 1
}
# A platform whose boot package implements provision-commit and provision-verify
# stages the unlock in its own boot chain and owns it; everywhere else it rides
# in the Limine UKI above. Deciding fails closed: the unlock then counts as
# present and cannot be dropped.
UNLOCK_OWNER=""
unlock_owner() {
[[ -z ${UNLOCK_OWNER:-} ]] || return 0
local commit verify
commit=$("$OMARCHY_PATH/bin/omarchy-lifecycle-dispatch" --resolve provision-commit) || return 1
verify=$("$OMARCHY_PATH/bin/omarchy-lifecycle-dispatch" --resolve provision-verify) || return 1
if [[ -n $commit && -n $verify ]]; then
UNLOCK_OWNER=platform
elif [[ -z $commit && -z $verify ]]; then
UNLOCK_OWNER=limine
else
log_step "the platform implements only one of provision-commit and provision-verify"
return 1
fi
}
luks_auto_unlock_present() {
unlock_owner || return 0
if [[ $UNLOCK_OWNER == "platform" ]]; then
! "$OMARCHY_PATH/bin/omarchy-lifecycle-dispatch" provision-verify >>"$LOG_FILE" 2>&1
else
limine_auto_unlock_present
fi
}
luks_auto_unlock_drop() {
unlock_owner || return 1
if [[ $UNLOCK_OWNER == "platform" ]]; then
"$OMARCHY_PATH/bin/omarchy-lifecycle-dispatch" provision-commit >>"$LOG_FILE" 2>&1
else
limine_auto_unlock_drop
fi
}
# The platform records the slot the re-key keeps, and no recovery slot; a no-op
# where nothing does.
luks_record_slots() {
"$OMARCHY_PATH/bin/omarchy-lifecycle-dispatch" luks-slots "owner=$1" "recovery=" >>"$LOG_FILE" 2>&1
}
# Move the LUKS volume from the throwaway install passphrase to the user's
# password: add the user's key, kill every other slot (throwaway + any seller
# keys a reset left behind), then rebuild the UKI without the embedded
# auto-unlock keyfile.
#
# Failing here must be LOUD (abort the attempt, offer retry): silently keeping
# the staged auto-unlock keyfile would leave the disk effectively unencrypted
# forever.
# password and retire every other slot (throwaway + any seller keys a reset
# left behind). luks_rekey journals each step, so an interrupted attempt
# resumes where it stopped.
rekey_luks() {
[[ -f $PROVISIONING_DIR/luks-key ]] || return 0
luks_rekey_pending || return 0
local device
if ! device=$(luks_device) || [[ ! -e $device ]]; then
@@ -870,61 +1033,32 @@ rekey_luks() {
return 1
fi
if ! cryptsetup open --test-passphrase --key-file "$PROVISIONING_DIR/luks-key" "$device" 2>>"$LOG_FILE"; then
log_step "staged LUKS key does not unlock $device"
say --foreground 1 "The staged LUKS key no longer unlocks $device."
return 1
fi
luks_rekey "$device"
}
# Add the user's key (a retry with a different password just adds another
# slot; all but the current one are killed once the rebuild succeeds).
cryptsetup luksAddKey --key-file "$PROVISIONING_DIR/luks-key" "$device" <(printf '%s' "$password")
# Rebuild the no-auto-unlock UKI FIRST, keeping the throwaway key and slot as
# a fallback. Only once that succeeds do we kill the other slots and destroy
# the staged key — so a limine-update failure leaves a recoverable,
# still-auto-unlocking state to retry, never a disk locked to a password the
# user may have just changed.
rm -f /etc/omarchy/provisioning.key \
/etc/limine-entry-tool.d/99-omarchy-provisioning-unlock.conf \
/etc/mkinitcpio.conf.d/99-omarchy-provisioning-key.conf
reset_limine_config
if ! limine-update >>"$LOG_FILE" 2>&1; then
log_step "limine-update failed during re-key; restoring auto-unlock for retry"
install -Dm600 "$PROVISIONING_DIR/luks-key" /etc/omarchy/provisioning.key
echo 'KERNEL_CMDLINE[default]+=" cryptkey=rootfs:/etc/omarchy/provisioning.key"' \
>/etc/limine-entry-tool.d/99-omarchy-provisioning-unlock.conf
echo 'FILES+=(/etc/omarchy/provisioning.key)' >/etc/mkinitcpio.conf.d/99-omarchy-provisioning-key.conf
limine-update >>"$LOG_FILE" 2>&1 || true
return 1
# The re-key's fingerprint of the console keyboard layout the boot files ask for
# the disk password in: a hash of vconsole.conf's keymap and XKB settings, in
# file order since a later duplicate wins, so the journal never names the layout.
luks_boot_layout() {
if [[ ! -f /etc/vconsole.conf ]]; then
echo none
return
fi
awk '/^[ \t]*(KEYMAP|KEYMAP_TOGGLE|XKB[A-Z_]*)=/ { sub(/^[ \t]+/, ""); print }' /etc/vconsole.conf |
sha256sum | cut -d' ' -f1
}
local new_slot slot other_slots
new_slot=$(cryptsetup open --test-passphrase --verbose --key-file <(printf '%s' "$password") "$device" 2>&1 |
grep -o 'Key slot [0-9]* unlocked' | grep -o '[0-9]*' | head -1)
# Retiring the throwaway/seller slots must be all-or-nothing: if we can't
# identify the user's slot or a kill fails, keep the staged key and retry —
# never shred it while a slot the seller knows still unlocks the disk.
if [[ -z $new_slot ]]; then
log_step "could not identify the user's LUKS slot after re-key; keeping the staged key for retry"
say --foreground 1 "Could not confirm the LUKS re-key; will retry."
return 1
# A re-key interrupted after the staged key was retired can only finish with
# the password the disk already holds.
rekey_accepts_password() {
local -
set +x
local device
[[ -f $REKEY_STATE ]] || return 0
if ! device=$(luks_device) || [[ ! -e $device ]]; then
return 0
fi
if ! other_slots=$(cryptsetup luksDump "$device" | awk '/^ +[0-9]+: luks2/ { sub(":", "", $1); print $1 }'); then
log_step "luksDump failed while retiring slots; keeping the staged key for retry"
say --foreground 1 "Could not enumerate LUKS slots; will retry."
return 1
fi
for slot in $other_slots; do
[[ $slot == "$new_slot" ]] && continue
if ! cryptsetup luksKillSlot -q --key-file <(printf '%s' "$password") "$device" "$slot"; then
log_step "failed to kill LUKS slot $slot; keeping the staged key for retry"
say --foreground 1 "Could not remove the throwaway LUKS key; will retry."
return 1
fi
done
shred -u "$PROVISIONING_DIR/luks-key" 2>/dev/null || rm -f "$PROVISIONING_DIR/luks-key"
luks_rekey_accepts_password "$device"
}
# Start the ESP's limine.conf over from the shipped template and drop foreign
@@ -991,7 +1125,7 @@ reset_limine_config() {
cleanup_oem_state() {
# Keep groups + packages: omarchy-system-factory-reset stages the bundled Node
# tarball from these live copies when the factory snapshot predates it.
rm -f "$PROVISIONING_DIR/pending" "$PROVISIONING_DIR/authorized_keys" "$PROVISIONING_DIR/setup-user"
rm -f "$PROVISIONING_DIR/pending" "$PROVISIONING_DIR/authorized_keys" "$PROVISIONING_DIR/setup-user" "$REKEY_STATE"
rm -f /etc/systemd/system/multi-user.target.wants/omarchy-provision-owner.service
systemctl daemon-reload 2>/dev/null || true
@@ -1018,7 +1152,7 @@ run_provisioning() {
touch "$FINALIZE_WARNING_FLAG"
fi
if [[ -f $PROVISIONING_DIR/luks-key ]]; then
if luks_rekey_pending; then
log_step "re-keying LUKS to the user's password"
echo rekey >"$STATE_FILE"
rekey_luks
@@ -1035,16 +1169,42 @@ run_provisioning() {
limine-update
fi
if luks_staged_unlock_remains; then
log_step "refusing to finish setup while the staged LUKS key or its auto-unlock remains"
return 1
fi
log_step "cleaning up provisioning state"
cleanup_oem_state
log_step "first-boot setup complete"
}
# Where the platform's boot package owns the boot chain, it must be ready to
# finish setup, and able to record the kept key slot, before the owner is asked
# anything.
platform_ready() {
local error
if error=$("$OMARCHY_PATH/bin/omarchy-lifecycle-dispatch" provision-prepare 2>&1 >>"$LOG_FILE") &&
error=$("$OMARCHY_PATH/bin/omarchy-lifecycle-dispatch" --resolve luks-slots 2>&1 >/dev/null); then
return 0
fi
error=${error:-"The boot package could not prepare this machine for setup."}
log_step "the boot package is not ready for setup: $error"
clear_logo
echo
say --foreground 1 "$error"
sleep 5
return 1
}
run_setup() {
local status
platform_ready || return 1
while true; do
keyboard_form
keyboard_form || return 1
user_form && status=0 || status=$?
if ((status != 0)); then
# Esc unwinds to the keyboard step. Ctrl+C is the only other way out of a
@@ -1052,7 +1212,12 @@ run_setup() {
((status == OMARCHY_FORM_SIGNAL)) && confirm_reboot && exec systemctl reboot
continue
fi
confirm_form && break
confirm_form || continue
if ! rekey_accepts_password; then
notice "Setup already set the disk password. Enter that same password." 4
continue
fi
break
done
touch "$LOG_FILE"
+15 -17
View File
@@ -24,31 +24,29 @@ omarchy_security_install_sudo_cleanup_traps
omarchy_security_revoke_sudo_timestamp || exit 1
omarchy_security_enable_no_update_sudo
channel="${1:-stable}"
if [[ $channel != "stable" && $channel != "rc" && $channel != "edge" ]]; then
channel="${1:-}"
if [[ -n $channel && $channel != "stable" && $channel != "rc" && $channel != "edge" ]]; then
echo "Invalid channel: $channel" >&2
exit 2
fi
# Render the channel templates plus this machine's regional additions as the
# caller, so root only copies finished files. The scratch directory goes on
# every exit, including a failed render, a refused sudo prompt, or a signal;
# revoking the sudo timestamp stays the last thing the exit path does.
rendered=$(mktemp -d)
cleanup_refresh() {
local status=$?
rm -rf "$rendered"
omarchy_security_exit_with_revoked_sudo "$status"
}
trap cleanup_refresh EXIT
"$OMARCHY_PATH/bin/omarchy-apply-pacman" --render "$channel" "$rendered"
source "$OMARCHY_PATH/install/helpers/pacman.sh"
if ! platform=$(omarchy-hw-platform); then
echo "Cannot tell this machine's platform. No repository configuration changed." >&2
exit 1
fi
templates=$(omarchy_pacman_templates "$platform") || exit 1
[[ -n $channel ]] || channel=$(omarchy_pacman_default_channel "$platform") || exit 1
if [[ ! -f $templates/pacman-$channel.conf || ! -f $templates/mirrorlist-$channel ]]; then
echo "Omarchy has no $channel channel for $platform. No repository configuration changed." >&2
exit 1
fi
sudo cp -f /etc/pacman.conf /etc/pacman.conf.bak
sudo cp -f /etc/pacman.d/mirrorlist /etc/pacman.d/mirrorlist.bak
echo "Setting channel to $channel"
sudo cp -f "$rendered/pacman.conf" /etc/pacman.conf
sudo cp -f "$rendered/mirrorlist" /etc/pacman.d/mirrorlist
rm -rf "$rendered"
sudo cp -f "$templates/pacman-$channel.conf" /etc/pacman.conf
sudo cp -f "$templates/mirrorlist-$channel" /etc/pacman.d/mirrorlist
# Allow user customization of /etc/pacman.conf before the upgrade runs, so
# custom repositories and IgnorePkg entries shape the downgrade-capable
-57
View File
@@ -1,57 +0,0 @@
#!/bin/bash
# omarchy:summary=Show or switch the region this machine is set up for
# omarchy:args=[global|cn]
# omarchy:examples=omarchy region | omarchy region cn
# omarchy:requires-sudo=true
set -euo pipefail
region_file=/etc/omarchy/region
if (($# == 0)); then
if [[ ! -e $region_file ]]; then
echo "Global"
elif [[ $(<"$region_file") == "cn" ]]; then
echo "China"
else
echo "Unknown region in $region_file: $(<"$region_file")" >&2
exit 1
fi
exit 0
fi
if [[ $1 != "global" && $1 != "cn" ]]; then
echo "Usage: omarchy-region [global|cn]" >&2
exit 1
fi
channel=$(omarchy-version-channel)
if [[ $channel != "stable" && $channel != "rc" && $channel != "edge" ]]; then
echo "Cannot tell this machine's package channel ($channel); run omarchy channel set first" >&2
exit 1
fi
if [[ $1 == "cn" ]]; then
# Upgrade first: the keyring install below syncs new databases.
omarchy-update-pacman -Syu --noconfirm
sudo env OMARCHY_REGION=cn OMARCHY_PATH="$OMARCHY_PATH" bash -e "$OMARCHY_PATH/install/config/region.sh"
# Trust ArchLinuxCN before the refresh upgrades through it.
rendered=$(mktemp -d)
trap 'rm -rf "$rendered"' EXIT
omarchy-apply-pacman --render "$channel" "$rendered"
mapfile -t profile_packages < <(grep -Ev '^[[:space:]]*(#|$)' "$OMARCHY_PATH/default/regions/cn/packages")
sudo pacman --config "$rendered/pacman.conf" -Sy --noconfirm --needed "${profile_packages[@]}"
sudo pacman-key --populate archlinuxcn
omarchy-refresh-pacman "$channel"
else
sudo env OMARCHY_REGION=global OMARCHY_PATH="$OMARCHY_PATH" bash -e "$OMARCHY_PATH/install/config/region.sh"
omarchy-refresh-pacman "$channel"
fi
# The developer mirrors reach only a new session, so offer one.
if [[ -t 0 && -t 1 ]] && gum confirm --affirmative "$(omarchy-text "Log out now")" --negative "$(omarchy-text "Later")" "$(omarchy-text "Log out now to switch the developer mirrors?")" 2>&1; then
omarchy-system-logout
else
omarchy-text "Log back in to switch the developer mirrors."
fi
+8 -6
View File
@@ -1,18 +1,20 @@
#!/bin/bash
# omarchy:summary=Reinstall all default Omarchy packages from the stable channel
# omarchy:summary=Reinstall all default Omarchy packages from the stable channel, or on aarch64 the edge channel
# omarchy:requires-sudo=true
set -e
# Reinstall all default Omarchy packages from the stable channel and downgrade any packages that are too new.
# Reinstall all default Omarchy packages from the platform's default channel
# (stable, or edge on aarch64) and downgrade any packages that are too new.
# Set the package repository to the stable mirrors
# Set the package repository to the platform's default channel
omarchy-refresh-pacman
# Downgrade any packages to the stable setup
# Downgrade any packages to that channel's setup
omarchy-update-pacman -Suu --noconfirm
# Ensure all packages are installed
mapfile -t packages < <(grep -v '^#' "$OMARCHY_PATH/install/omarchy-base.packages" | grep -v '^$')
# Ensure this platform's default packages are installed
default_packages=$(omarchy-pkg-defaults)
mapfile -t packages <<<"$default_packages"
omarchy-update-pacman -Syu --noconfirm --needed "${packages[@]}"
+1
View File
@@ -5,6 +5,7 @@
set -e
omarchy-lifecycle-dispatch pre-remove steam
omarchy-pkg-drop steam
rm -rf \
+2 -4
View File
@@ -34,15 +34,13 @@ if gum confirm "Are you sure you want to remove all preinstalled web apps, TUI w
aether \
cliamp \
libreoffice-fresh \
xournalpp \
pinta \
obsidian \
obs-studio \
kdenlive \
moonlight-qt \
omacut \
monologue \
omacalc \
omawrite \
hype
hype \
signed
fi
+8 -3
View File
@@ -10,13 +10,18 @@ fi
omarchy_security_require_privileged_bash_startup() {
[[ $- == *p* ]] || return 1
# The caller is this check's parent, under the number this /proc gives it.
# arch-chroot runs the installer in a new PID namespace beneath the outer
# /proc, where $$ names no process, or someone else's.
/usr/bin/env -i /usr/bin/bash -p -c '
mapfile -d "" -t argv <"/proc/$1/cmdline" || exit 1
executable=$(/usr/bin/readlink -e -- "/proc/$1/exe") || exit 1
stat=$(</proc/self/stat) || exit 1
read -r _ caller _ <<<"${stat##*) }"
mapfile -d "" -t argv <"/proc/$caller/cmdline" || exit 1
executable=$(/usr/bin/readlink -e -- "/proc/$caller/exe") || exit 1
[[ $executable == "/usr/bin/bash" &&
( ${argv[0]:-} == "/bin/bash" || ${argv[0]:-} == "/usr/bin/bash" ) &&
${argv[1]:-} == "-p" ]]
' omarchy-bash-startup "$$"
' omarchy-bash-startup
}
omarchy_security_sanitize_bash_environment() {
+8
View File
@@ -3,6 +3,14 @@
# omarchy:summary=Add or remove an EFI boot entry for the Omarchy UKI, allowing the system to boot directly
# omarchy:requires-sudo=true
# Apple Silicon boots m1n1, then U-Boot, which finds the bootloader on its own
# and creates its boot entries itself, so there is no direct-boot entry to add
# or remove.
if omarchy-hw-aarch64-apple; then
echo "Error: Direct boot is not supported on Apple Silicon: U-Boot finds the bootloader itself" >&2
exit 1
fi
if [[ ! -d /sys/firmware/efi ]]; then
echo "Error: System is not booted in UEFI mode" >&2
exit 1
+5 -6
View File
@@ -1,6 +1,6 @@
#!/bin/bash
# omarchy:summary=Set up Japanese, Korean, or Chinese typing
# omarchy:summary=Select input methods for Japanese, Korean, or Chinese typing
# omarchy:args=[mozc|hangul|pinyin|chewing]
# omarchy:examples=omarchy setup input | omarchy setup input mozc
# omarchy:requires-sudo=true
@@ -14,9 +14,8 @@ fi
method=${1:-}
if [[ -z $method ]]; then
choice=$(jq -r 'to_entries[] | select(.key != "none") | .value.label' "$OMARCHY_PATH/default/input-methods/presets.json" |
gum choose --header "Select input language")
method=$(jq -r --arg label "$choice" 'to_entries[] | select(.value.label == $label) | .key' "$OMARCHY_PATH/default/input-methods/presets.json")
/usr/bin/python3 "$OMARCHY_PATH/default/input-methods/typing_setup.py" input
exit
fi
case "$method" in
@@ -26,5 +25,5 @@ esac
mapfile -t packages < <(jq -r --arg method "$method" '.[$method].packages[]' "$OMARCHY_PATH/default/input-methods/presets.json")
omarchy-pkg-add "${packages[@]}"
omarchy-input-method configure "$method"
echo "Input method added. The default switch is Ctrl+Shift+Space or the Japanese/Korean input key; existing custom keys are preserved."
/usr/bin/python3 "$OMARCHY_PATH/default/input-methods/typing_setup.py" add "$method"
echo "Input method added. Use Super+I or the Japanese/Korean input key to switch; existing custom Fcitx keys are preserved."
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
# omarchy:summary=Select the keyboard layouts available for switching
set -euo pipefail
/usr/bin/python3 "$OMARCHY_PATH/default/input-methods/typing_setup.py" keyboard
+30 -3
View File
@@ -462,7 +462,34 @@ root_dispatch() {
esac
}
# The bar indicator only probes on demand, so every grant change pushes a
# refresh. Root's expiry timer can't reach the user's shell, so a user timer
# refreshes once the deadline has passed. Both are best effort: a stale red
# indicator still revokes cleanly when clicked.
refresh_indicator() {
/usr/bin/omarchy-shell -q omarchy.indicators refresh >/dev/null 2>&1 || true
}
# A failed enable can still change the grant (a renewal that rolls back removes
# the previous policy), so refresh whatever the privileged action returned.
change_grant() {
local status=0
/usr/bin/sudo "$@" || status=$?
refresh_indicator
return "$status"
}
# A wall-clock deadline, like the sudoers rule and root's timer, so expiry
# during suspend still refreshes on resume.
schedule_expiry_refresh() {
[[ $minutes == "permanent" ]] && return 0
/usr/bin/systemd-run --user --quiet --collect --on-calendar="@$((EPOCHSECONDS + 10#$minutes * 60 + 2))" \
--timer-property=AccuracySec=1s \
-- /usr/bin/omarchy-shell -q omarchy.indicators refresh >/dev/null 2>&1 || true
}
show_enabled() {
schedule_expiry_refresh
if [[ $minutes == "permanent" ]]; then
echo "Passwordless sudo has been ENABLED permanently. Run this command again to disable it."
else
@@ -542,12 +569,12 @@ else
fi
fi
if (( status == 0 )) && { (($# == 0)) || [[ $minutes == "--disable" ]]; }; then
/usr/bin/sudo -n -N -- "$INSTALLED_SELF" __disable "$uid"
change_grant -n -N -- "$INSTALLED_SELF" __disable "$uid"
echo "Passwordless sudo has been DISABLED. Sudo will require a password again."
elif (( status == STATUS_INACTIVE )) && [[ $minutes == "--disable" ]]; then
echo "Passwordless sudo is already DISABLED."
elif (( status == 0 )) && [[ $minutes != "permanent" ]]; then
/usr/bin/sudo -N -- "$INSTALLED_SELF" __enable "$uid" "$minutes"
change_grant -N -- "$INSTALLED_SELF" __enable "$uid" "$minutes"
show_enabled
else
if (( status != 0 && status != STATUS_INACTIVE )); then
@@ -587,7 +614,7 @@ else
echo ""
if /usr/bin/gum confirm "Enable passwordless sudo $duration? This is a significant security risk!"; then
/usr/bin/sudo -N -- "$INSTALLED_SELF" __enable "$uid" "$minutes"
change_grant -N -- "$INSTALLED_SELF" __enable "$uid" "$minutes"
echo ""
show_enabled
else
+174 -7
View File
@@ -17,7 +17,9 @@
# The heavy lifting happens on the next boot via omarchy-system-factory-reset-finish, staged
# here. On encrypted installs the LUKS volume is re-keyed to a throwaway
# passphrase with an auto-unlock keyfile for the provisioning window; first-boot setup
# re-keys it to the new owner's password and removes the keyfile.
# re-keys it to the new owner's password and removes the keyfile. A platform
# whose boot chain the Limine UKI path can't drive rebuilds, verifies and
# unlocks it through omarchy-lifecycle-dispatch.
#
# Honest limitations:
# - On unencrypted disks this is deletion, not forensic erasure. fstrim helps
@@ -29,17 +31,25 @@
set -euo pipefail
PROVISIONING_DIR=/var/lib/omarchy/provisioning
PACKAGED_PATH=/usr/bin/omarchy-system-factory-reset
OMARCHY_PATH="${OMARCHY_PATH:-/usr/share/omarchy}"
TOP_MNT=/run/omarchy-system-factory-reset/top
NEXT_NAME=@omarchy-reset-next
LOG_FILE=/var/log/omarchy-system-factory-reset.log
# Never replace a newer checkout reset with a different installed implementation.
if ! /usr/bin/cmp -s -- "${BASH_SOURCE[0]}" "$PACKAGED_PATH"; then
echo "Factory reset differs from the installed command. Install the matching Omarchy package before resetting." >&2
exit 1
fi
# Self-elevate so the menu entry (and a bare shell invocation) need no sudo
# prefix. The caller's gum theme vars are forwarded as `env` arguments rather
# than via `sudo -E`, so styling survives even under an env_reset sudoers.
# Always elevate the packaged copy, including when invoked from a dev checkout.
if (( EUID != 0 )); then
mapfile -t gum_env < <(env | grep '^GUM_' || true)
exec sudo env "${gum_env[@]}" "$0" "$@"
exec sudo env "${gum_env[@]}" "$PACKAGED_PATH" "$@"
fi
export PATH="$OMARCHY_PATH/bin:$PATH"
@@ -62,6 +72,51 @@ root_is_btrfs_at() {
[[ $(findmnt -no FSTYPE /) == btrfs ]] && findmnt -no OPTIONS / | grep -q "subvol=/@\(,\|$\)"
}
DISPATCH="$OMARCHY_PATH/bin/omarchy-lifecycle-dispatch"
# A platform whose boot package implements the reset operations rebuilds,
# verifies and unlocks the factory root's boot chain itself; everywhere else
# the Limine UKI path below does. Deciding fails closed.
RESET_BOOT=""
reset_boot_owner() {
[[ -z $RESET_BOOT ]] || return 0
local operation path found=0 missing=0
for operation in reset-prepare reset-verify reset-commit reset-rollback; do
path=$("$DISPATCH" --resolve "$operation") || return 1
if [[ -n $path ]]; then
found=$((found + 1))
else
missing=$((missing + 1))
fi
done
if (( missing == 0 )); then
RESET_BOOT=platform
elif (( found == 0 )); then
RESET_BOOT=generic
else
echo "Error: the boot package implements only some of the factory reset operations" >&2
return 1
fi
}
# Runs a reset operation with its output in the log. A failure keeps the
# operation's last line for the error shown.
RESET_BOOT_ERROR=""
reset_boot_op() {
local output status=0
output=$("$DISPATCH" "$@" 2>&1) || status=$?
[[ -z $output ]] || printf '%s\n' "$output" >>"$LOG_FILE"
if (( status == 0 )); then
return 0
fi
RESET_BOOT_ERROR=$(tail -n 1 <<<"$output")
RESET_BOOT_ERROR=${RESET_BOOT_ERROR:-"$1 failed"}
return 1
}
# Resolve the crypto_LUKS partition backing the root, or return non-zero if
# the root is not on LUKS. Prefers the cmdline cryptdevice= spec (archinstall
# writes PARTUUID=, the pre-mounted path UUID=), and falls back to walking the
@@ -81,7 +136,7 @@ luks_device() {
local src part
src=$(findmnt -no SOURCE / | sed 's/\[.*//')
[[ -n $src ]] || return 1
part=$(lsblk -nspo NAME,FSTYPE "$src" 2>/dev/null | awk '$2=="crypto_LUKS"{print $1; exit}')
part=$(lsblk -nsrpo NAME,FSTYPE "$src" 2>/dev/null | awk '$2=="crypto_LUKS"{print $1; exit}')
[[ -n $part ]] && { echo "$part"; return 0; }
return 1
}
@@ -99,6 +154,11 @@ generate_passphrase() {
}
cleanup() {
# Before the switch the old root still boots: the boot package gives it back
# its boot state before the reset's throwaway slot is revoked.
if [[ ${reset_committed:-0} == 0 && ${RESET_BOOT_PREPARED:-0} == 1 ]]; then
reset_boot_op reset-rollback || echo "Error: $RESET_BOOT_ERROR (see $LOG_FILE)" >&2
fi
if mountpoint -q "$TOP_MNT" 2>/dev/null; then
if [[ -d $TOP_MNT/$NEXT_NAME && ${swap_done:-0} == 0 ]]; then
btrfs subvolume delete --recursive "$TOP_MNT/$NEXT_NAME" >/dev/null 2>&1 || true
@@ -106,6 +166,9 @@ cleanup() {
umount -R "$TOP_MNT" 2>/dev/null || true
fi
rmdir "$TOP_MNT" 2>/dev/null || true
if [[ ${reset_committed:-0} == 0 ]]; then
revoke_reset_luks
fi
}
trap cleanup EXIT
@@ -127,8 +190,54 @@ confirm_reset() {
[[ $typed == "reset" ]] || fail "Reset not confirmed."
}
# The slot the reset's throwaway key opens, or nothing. Restricting cryptsetup
# to a token type no token has keeps an enrolled token from answering for it.
reset_luks_slot() {
local -
set +x
LC_ALL=C cryptsetup open --test-passphrase --verbose --token-type passphrase-only \
--key-file <(printf '%s' "$RESET_THROWAWAY") "$RESET_LUKS_DEVICE" 2>&1 |
grep -o 'Key slot [0-9]* unlocked' | grep -o '[0-9]*' | head -1 || true
}
# Add the throwaway slot where the platform's boot package owns the unlock.
# Called once reset-verify passed, so a failed rebuild adds no credential.
# reset-commit writes the boot-time key only once the factory root is active:
# a power loss before the switch leaves the old root asking for its password,
# never unlocked unattended.
add_reset_luks_slot() {
local -
set +x
[[ -n ${RESET_LUKS_DEVICE:-} && -n ${RESET_LUKS_AUTH:-} && -n ${RESET_THROWAWAY:-} ]] || fail "the reset is missing its throwaway passphrase"
RESET_LUKS_ADDED=1
{ cryptsetup luksAddKey --key-file <(printf '%s' "$RESET_LUKS_AUTH") "$RESET_LUKS_DEVICE" <(printf '%s' "$RESET_THROWAWAY") ; } 2>>"$LOG_FILE" ||
fail "could not add the throwaway LUKS keyslot"
RESET_LUKS_SLOT=$(reset_luks_slot)
[[ -n $RESET_LUKS_SLOT ]] || fail "could not identify the throwaway LUKS keyslot"
}
# Only the slot this attempt added goes, found by its key if the add was not
# confirmed. The platform's boot-time key is written only after activation, so
# a failed reset has none to revoke.
revoke_reset_luks() {
local -
set +x
[[ ${RESET_LUKS_ADDED:-0} == 1 && -n ${RESET_LUKS_AUTH:-} ]] || return 0
[[ -n ${RESET_LUKS_SLOT:-} ]] || RESET_LUKS_SLOT=$(reset_luks_slot)
if [[ -n $RESET_LUKS_SLOT ]]; then
{ cryptsetup luksKillSlot -q --key-file <(printf '%s' "$RESET_LUKS_AUTH") "$RESET_LUKS_DEVICE" "$RESET_LUKS_SLOT" ; } 2>>"$LOG_FILE" || true
else
echo "Could not find the reset's throwaway LUKS slot on $RESET_LUKS_DEVICE to revoke; check its keyslots" >>"$LOG_FILE"
fi
RESET_LUKS_ADDED=0
RESET_LUKS_SLOT=""
}
# Re-key to a throwaway passphrase whose keyfile auto-unlocks boot during the
# provisioning window. $1 is the root of the staged factory system.
# provisioning window. $1 is the root of the staged factory system. Where the
# platform's boot package owns the unlock, only the staged key is written here:
# reset-prepare stages the unlock with the rest of the boot files, and the
# throwaway slot follows their verification.
stage_luks_rekey() {
local next="$1" device passphrase
@@ -140,19 +249,29 @@ stage_luks_rekey() {
local current
while true; do
current=$(gum input --password --placeholder "Current disk encryption passphrase" --prompt "Passphrase> ") || exit 1
if cryptsetup open --test-passphrase --key-file <(printf '%s' "$current") "$device" 2>/dev/null; then
if cryptsetup open --test-passphrase --token-type passphrase-only --key-file <(printf '%s' "$current") "$device" 2>/dev/null; then
break
fi
gum style --foreground 1 "That passphrase does not unlock $device. Try again."
done
passphrase=$(generate_passphrase)
cryptsetup luksAddKey --key-file <(printf '%s' "$current") "$device" <(printf '%s' "$passphrase")
reset_boot_owner || fail "could not tell how this machine's boot chain is reset"
RESET_LUKS_AUTH=$current
RESET_LUKS_DEVICE=$device
RESET_THROWAWAY=$passphrase
if [[ $RESET_BOOT == "generic" ]]; then
# Revoked like the platform's slot if the reset fails before activation.
RESET_LUKS_ADDED=1
cryptsetup luksAddKey --key-file <(printf '%s' "$current") "$device" <(printf '%s' "$passphrase")
fi
install -d -m 755 "$next$PROVISIONING_DIR"
printf '%s' "$passphrase" >"$next$PROVISIONING_DIR/luks-key"
chmod 600 "$next$PROVISIONING_DIR/luks-key"
[[ $RESET_BOOT == "generic" ]] || return 0
install -d -m 755 "$next/etc/omarchy"
printf '%s' "$passphrase" >"$next/etc/omarchy/provisioning.key"
chmod 600 "$next/etc/omarchy/provisioning.key"
@@ -235,6 +354,12 @@ verify_limine_hashes() {
rebuild_next_boot() {
local next="$1"
reset_boot_owner || fail "could not tell how this machine's boot chain is reset"
if [[ $RESET_BOOT == "platform" ]]; then
rebuild_next_boot_platform "$next"
return
fi
local esp_source esp_mount
esp_mount=$(awk '$3 == "vfat" && $2 ~ /^\/(boot|efi)/ { print $2; exit }' "$next/etc/fstab")
esp_source=$(awk '$3 == "vfat" && $2 ~ /^\/(boot|efi)/ { print $1; exit }' "$next/etc/fstab")
@@ -297,6 +422,23 @@ rebuild_next_boot() {
umount "$next$esp_mount"
}
# The boot package stages the factory root's boot files and unlock, then proves
# they boot it; reset-rollback undoes both if the reset fails before the switch.
# The throwaway slot is added only once they are proven.
rebuild_next_boot_platform() {
local next="$1"
local -a luks=()
[[ -z ${RESET_LUKS_DEVICE:-} ]] || luks=("$RESET_LUKS_DEVICE")
RESET_BOOT_PREPARED=1
log "Rebuilding boot files from the factory system (this can take a minute)"
reset_boot_op reset-prepare "$next" "${luks[@]}" || fail "$RESET_BOOT_ERROR (see $LOG_FILE)"
log "Verifying that the factory system boots"
reset_boot_op reset-verify "$next" || fail "$RESET_BOOT_ERROR (see $LOG_FILE)"
[[ -z ${RESET_LUKS_DEVICE:-} ]] || add_reset_luks_slot
}
# Both the staged system and the retained baseline must lose the old hashes.
scrub_factory_accounts() {
local root="$1" user users
@@ -388,9 +530,30 @@ stage_full_reset() {
log "Activating the factory system"
local old="@omarchy-old-$(date +%s)"
mv "$top/@" "$top/$old"
mv "$next" "$top/@"
# Until the factory root is at @ the reset is not committed: cleanup has the
# platform's boot package roll its boot state back to the old root and
# revokes the throwaway slot, so the old root goes back to @ first.
if ! mv "$next" "$top/@"; then
if mv "$top/$old" "$top/@"; then
[[ $RESET_BOOT == "platform" ]] ||
fail "could not activate the factory system; the current root is back at @, but its boot files were already rebuilt from the factory system: run 'sudo limine-update' before rebooting (see $LOG_FILE)"
fail "could not activate the factory system; the current root is back at @ (see $LOG_FILE)"
fi
# Neither root is at @. Keep the staged one as well for recovery.
swap_done=1
fail "could not activate the factory system or put the current root back; from a live USB, mount the Btrfs top level, rename $old to @, and rebuild its boot files if it does not boot"
fi
# From here the old root is gone, so the reset's unlock must survive.
reset_committed=1
swap_done=1
sync
# The boot package that prepared the reset gets the key on standard input,
# never in arguments. A failure costs the first boot one prompt for the
# current disk password.
if [[ ${RESET_BOOT_PREPARED:-0} == 1 ]] && ! reset_boot_op reset-commit < <(printf '%s' "${RESET_THROWAWAY:-}"); then
log "$RESET_BOOT_ERROR"
fi
}
# A reset is a clone of @factory, so a machine without one has nothing to
@@ -428,6 +591,10 @@ main() {
require_factory_snapshot
# Decided before anything is asked: a platform whose boot package is missing
# stops here, with the dispatcher's error naming the package.
reset_boot_owner || fail "this machine's boot package cannot factory-reset it"
confirm_reset
# The running system's limine-snapper-sync must not rewrite the ESP's
+3 -2
View File
@@ -11,8 +11,9 @@
# omarchy-system-sleep-lock then usually finds the session already secure.
#
# A docked lid close does not suspend (HandleLidSwitchDocked defaults to
# ignore), so it must not lock either: that is clamshell mode, still in use on
# the external display.
# ignore, and omarchy-system-lid-inhibit covers displays logind does not count),
# so it must not lock either: that is clamshell mode, still in use on the
# external display.
if omarchy-hw-laptop-closed && ! omarchy-hw-external-monitors; then
omarchy-system-lock >/dev/null 2>&1 || true
fi
+100
View File
@@ -0,0 +1,100 @@
#!/bin/bash
# omarchy:summary=Keep logind from suspending on lid close while a display it misses is in use
# omarchy:group=system
# omarchy:hidden=true
# systemd-logind treats a closed lid as docked only when an external display is
# on a connector type it lists (HDMI, DisplayPort, DVI, VGA...), and USB-C
# displays on Apple Silicon arrive on "USB" connectors instead. Left alone,
# logind suspends a laptop still in use on one. Holding its handle-lid-switch
# inhibitor while such a display is in use leaves the lid to Omarchy's
# clamshell handling, as HandleLidSwitchDocked=ignore would. Once released, a
# lid that is still shut suspends at once: logind rechecks a closed lid after
# every event.
UNIT="omarchy-lid-inhibit.service"
drm_path="${OMARCHY_DRM_PATH:-/sys/class/drm}"
cgroup_path="${OMARCHY_CGROUP_PATH:-/proc/self/cgroup}"
# Connected connectors of the external types logind does not count, by the name
# Hyprland gives them (card2-USB-2 is USB-2). DVI-A and Composite are on its
# list, but a missing comma there fuses them into one entry.
uncounted_outputs() {
local status connector
for status in "$drm_path"/card*-*/status; do
[[ -r $status ]] || continue
connector=${status%/status}
connector=${connector##*/}
case $connector in
card*-USB-* | card*-DVI-A-* | card*-Composite-*) ;;
*) continue ;;
esac
[[ $(<"$status") == "connected" ]] && printf '%s\n' "${connector#card*-}"
done
}
held() {
systemctl --user is-active --quiet "$UNIT"
}
in_use=0
outputs=$(uncounted_outputs)
# In use means enabled in Hyprland: a display switched off in the monitor config
# should not keep a closed laptop awake, while one blanked by DPMS still should,
# and the connector's own "enabled" flag cannot tell the two apart on every
# driver. A compositor that does not answer, as while it reconfigures outputs
# around a lid close, changes nothing: releasing then would suspend a closed lid.
if [[ -n $outputs ]]; then
monitors=$(hyprctl monitors all -j 2>/dev/null) || exit 0
[[ -n $monitors ]] || exit 0
jq -e --arg outputs "$outputs" \
'($outputs | split("\n")) as $names | any(.[]; .disabled == false and (.name | IN($names[])))' \
<<<"$monitors" >/dev/null 2>&1
case $? in
0) in_use=1 ;;
1) ;;
*) exit 0 ;;
esac
fi
# Only where logind would ignore a docked lid is there anything to stand in for.
# An unanswered query changes nothing, for the same reason as above.
wanted=0
if (( in_use )) && omarchy-hw-laptop; then
docked_action=$(busctl get-property org.freedesktop.login1 /org/freedesktop/login1 \
org.freedesktop.login1.Manager HandleLidSwitchDocked 2>/dev/null) || exit 0
[[ -n $docked_action ]] || exit 0
[[ $docked_action == 's "ignore"' ]] && wanted=1
fi
# Bound to the user unit of the watcher that keeps it in step, so a watcher that
# is gone cannot leave the lid inhibited until logout. systemd-run unquotes
# property values, so the backslashes in an escaped unit name are doubled.
watcher_binding() {
local cgroup="" unit
read -r cgroup 2>/dev/null <"$cgroup_path"
unit=${cgroup##*/}
if [[ $cgroup == */user@*.service/* && ( $unit == *.scope || $unit == *.service ) ]]; then
unit=${unit//\\/\\\\}
printf '%s\n' "--property=BindsTo=$unit" "--property=After=$unit"
fi
}
if (( wanted )); then
if ! held; then
mapfile -t bind < <(watcher_binding)
systemd-run --user --quiet --collect --unit="$UNIT" \
--description="Omarchy clamshell lid inhibitor" \
--property=PartOf=graphical-session.target "${bind[@]}" \
systemd-inhibit --no-ask-password --what=handle-lid-switch --mode=block \
--who=Omarchy --why="External display in use" sleep infinity
fi
elif held; then
systemctl --user stop "$UNIT"
fi
+2 -2
View File
@@ -20,6 +20,6 @@ fi
# Create symlink to the new background media
ln -nsf "$BACKGROUND" "$CURRENT_BACKGROUND_LINK"
# Update the live shell background immediately when it is running. The
# background plugin also polls this symlink, but IPC avoids the visible delay.
# Update the live shell background when it is running. Nothing watches the
# symlink, so this is how the shell learns of the new background.
omarchy-shell -q background set "$BACKGROUND"
+8 -5
View File
@@ -207,11 +207,14 @@ set_theme_background() {
shell_ipc shell applyTheme "$colors_payload" "$shell_payload" || true
ln -nsf "$new_background" "$CURRENT_BACKGROUND_LINK"
(
if ! omarchy-theme-bg-boot-intro --theme-switch "$new_background" "$theme_id" "$intro_prepared"; then
if [[ $(readlink -f "$CURRENT_BACKGROUND_LINK") == "$(readlink -f "$new_background")" &&
$(stat -Lc '%d:%i' "$CURRENT_THEME_PATH" 2>/dev/null) == "$theme_id" ]]; then
shell_ipc background setInstant "$new_background" || true
fi
# Played or not, the shell gets the new still: it still holds the old
# theme's path, which the theme swap above deleted, and loads it again
# when OWE hands the desktop back after the intro. While the intro plays
# the shell's background is suspended under it, so the swap is unseen.
omarchy-theme-bg-boot-intro --theme-switch "$new_background" "$theme_id" "$intro_prepared" || true
if [[ $(readlink -f "$CURRENT_BACKGROUND_LINK") == "$(readlink -f "$new_background")" &&
$(stat -Lc '%d:%i' "$CURRENT_THEME_PATH" 2>/dev/null) == "$theme_id" ]]; then
shell_ipc background setInstant "$new_background" || true
fi
shell_ipc shell finishThemeIntro "$theme_id" || true
wait_for_theme_intro themeIntroStatus "$theme_id" "pending"
+12 -1
View File
@@ -1,9 +1,10 @@
#!/bin/bash
# omarchy:summary=Apply current Omarchy theme colors to running Foot terminals
# omarchy:summary=Apply current Omarchy theme colors and background alpha to running Foot terminals
# omarchy:hidden=true
colors_toml=$HOME/.local/state/omarchy/current/theme/colors.toml
foot_ini=$HOME/.local/state/omarchy/current/theme/foot.ini
if [[ ! -f $colors_toml ]] || ! pgrep -x foot >/dev/null; then
exit 0
@@ -12,6 +13,16 @@ fi
foot_osc=$(omarchy-theme-osc "$colors_toml")
[[ -n $foot_osc ]] || exit 0
# foot takes a background alpha as an urxvt-style [percent] prefix, so a theme
# going see-through or back, as with Glassy windows, reaches running terminals.
# The last alpha in the theme's foot.ini wins, as it does when foot reads it.
alpha=$(sed -n 's/^[[:space:]]*alpha[[:space:]]*=[[:space:]]*\([0-9.]*\).*/\1/p' "$foot_ini" 2>/dev/null | tail -n 1)
background=$(omarchy-theme-color --file "$colors_toml" background 2>/dev/null)
if [[ -n $background ]]; then
percent=$(awk -v alpha="${alpha:-1}" 'BEGIN { printf "%d", alpha * 100 + 0.5 }')
foot_osc+=$(printf '\033]11;[%d]%s\007' "$percent" "$background")
fi
for foot_pid in $(pgrep -x foot); do
for child_pid in $(pgrep -P "$foot_pid"); do
tty=$(readlink "/proc/$child_pid/fd/1" 2>/dev/null)
+122
View File
@@ -0,0 +1,122 @@
#!/bin/bash
# omarchy:summary=Give terminals, btop, and GTK apps a see-through background while windows are glassy
# omarchy:args=[theme-dir]
# omarchy:hidden=true
# A Hyprland opacity rule fades a terminal's text along with its background,
# and foot turns compositor blur down unless its own config asks for it, so the
# glass belongs in each terminal's theme file, as in the glassy Starship
# (#14726). The lines sit between markers so leaving glassy takes them out
# again, and a theme that sets its own opacity keeps it.
THEME_DIR="${1:-$HOME/.local/state/omarchy/current/theme}"
TOGGLES_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/omarchy/toggles/hypr"
GLASSY=false
[[ -f $TOGGLES_DIR/window-glassy.lua ]] && GLASSY=true
status=0
# A literal for a sed address, whose delimiter is /.
regex() {
sed 's/[][\\.*^$/]/\\&/g' <<<"$1"
}
# A block added to a file that did not end in a newline has to supply one, so
# its begin marker says so, and taking the block out takes that newline back.
# Leaving glassy then gives the file back exactly as it was. Markers come as
# an opening and a closing part, so CSS can wrap them in a comment.
remove_block() {
local file="$1" open="$2" close="$3"
local begin end
begin=$(regex "$open omarchy-glass begin")
end=$(regex "$open omarchy-glass end$close")
[[ -f $file ]] && grep -qF "$open omarchy-glass begin" "$file" || return 0
local added_newline=false
grep -qxF "$open omarchy-glass begin after a line without a newline$close" "$file" && added_newline=true
sed -i "/^$begin/,/^$end\$/d" "$file" || return 1
if [[ $added_newline == "true" && -s $file && -z $(tail -c 1 "$file") ]]; then
truncate -s -1 "$file" || return 1
fi
}
append_block() {
local file="$1" open="$2" close="$3" lines="$4"
if [[ -s $file && -n $(tail -c 1 "$file") ]]; then
printf '\n%s\n%s\n%s\n' "$open omarchy-glass begin after a line without a newline$close" "$lines" "$open omarchy-glass end$close" >>"$file"
else
printf '%s\n%s\n%s\n' "$open omarchy-glass begin$close" "$lines" "$open omarchy-glass end$close" >>"$file"
fi
}
# Each setting comes with a pattern for the theme setting it already, which
# keeps the theme's own; an empty pattern always adds it. Lines that belong in
# a section go in under its header. A file that already has the section gets
# them inside it, since TOML refuses a table declared twice; otherwise the
# header comes along with them.
glass() {
local file="$THEME_DIR/$1" section="$2"
shift 2
[[ -f $file ]] || return 0
remove_block "$file" "#" "" || return 1
[[ $GLASSY == "true" ]] || return 0
local lines=""
while (( $# >= 2 )); do
if [[ -z $1 ]] || ! grep -qE "$1" "$file"; then
lines+="${lines:+$'\n'}$2"
fi
shift 2
done
[[ -n $lines ]] || return 0
# A header may carry trailing space or a comment.
local header="^$(regex "$section")[[:space:]]*(#.*)?\$"
if [[ -n $section ]] && grep -qE "$header" "$file"; then
local block
block=$(mktemp) || return 1
printf '# omarchy-glass begin\n%s\n# omarchy-glass end\n' "$lines" >"$block"
sed -i -E "0,/$header/{/$header/r $block
}" "$file"
local result=$?
rm -f "$block"
return $result
fi
append_block "$file" "#" "" "${section:+$section$'\n'}$lines"
}
# Alacritty's own blur works only on macOS and KDE; Hyprland blurs it anyway.
glass alacritty.toml '[window]' \
'^[[:space:]]*opacity[[:space:]]*=' 'opacity = 0.65' || status=1
glass foot.ini '[colors-dark]' \
'^[[:space:]]*alpha[[:space:]]*=' 'alpha=0.65' \
'^[[:space:]]*blur[[:space:]]*=' 'blur=yes' || status=1
glass kitty.conf '' \
'^[[:space:]]*background_opacity[[:space:]]' 'background_opacity 0.65' || status=1
# btop paints its own background over the terminal's glass. An empty main_bg
# uses the terminal background; the last theme value wins, so removing the
# block restores the theme's colour without changing btop's user config.
glass btop.theme '' \
'^[[:space:]]*theme\[main_bg\][[:space:]]*=[[:space:]]*""' 'theme[main_bg]=""' || status=1
# GTK only asks Hyprland for blur under a CSS backdrop-filter, which Ghostty
# takes as custom CSS and reloads live; its background-blur speaks only KDE's
# blur protocol.
glass ghostty.conf '' \
'^[[:space:]]*background-opacity[[:space:]]*=' 'background-opacity = 0.65' \
'' "gtk-custom-css = $OMARCHY_PATH/default/gtk/glass.css" || status=1
# Other GTK apps, such as Nautilus, read the user's gtk.css at launch, so the
# same backdrop blur is imported there. It follows the mode alone, so a theme
# switch, which names the theme it is rendering, leaves it be.
if (( $# == 0 )); then
gtk_css="${XDG_CONFIG_HOME:-$HOME/.config}/gtk-4.0/gtk.css"
remove_block "$gtk_css" "/*" " */" || status=1
if [[ $GLASSY == "true" ]]; then
{ mkdir -p "${gtk_css%/*}" &&
append_block "$gtk_css" "/*" " */" "@import url(\"file://$OMARCHY_PATH/default/gtk/glass.css\");"; } || status=1
fi
fi
exit $status
+1
View File
@@ -451,3 +451,4 @@ if [[ -f $COLORS_FILE ]]; then
fi
apply_shell_section_overrides
omarchy-theme-set-glass "$NEXT_THEME_DIR"
+3 -3
View File
@@ -1,6 +1,6 @@
#!/bin/bash
# omarchy:summary=Toggle animations, transparency and other effects that are slow without a GPU
# omarchy:summary=Toggle animations, blur and shadows, which are slow without a GPU
# omarchy:args=[toggle|on|off]
# omarchy:examples=omarchy toggle animations | omarchy toggle animations off | omarchy toggle animations on
@@ -16,7 +16,7 @@ off) omarchy-hyprland-toggle no-animations on ;;
esac
if omarchy-hyprland-toggle-enabled no-animations; then
omarchy-notification-send -g 󰕟 "$(omarchy-text "Animations disabled")"
omarchy-notification-send -g 󰗘 "$(omarchy-text "Animations disabled")"
else
omarchy-notification-send -g 󰕟 "$(omarchy-text "Animations enabled")"
omarchy-notification-send -g 󰗘 "$(omarchy-text "Animations enabled")"
fi
+1 -1
View File
@@ -1,6 +1,6 @@
#!/bin/bash
# omarchy:summary=Toggle bar visibility without killing the Omarchy shell
# omarchy:summary=Toggle bar visibility without killing the Omarchy shell (on hides the bar, off shows it)
# omarchy:args=[toggle|on|off]
# omarchy:examples=omarchy toggle bar | omarchy toggle bar off | omarchy toggle bar on
+15 -1
View File
@@ -148,6 +148,15 @@ if [[ ${1:-} == "-y" ]] || omarchy-update-confirm; then
PATH="$OMARCHY_PATH/default/omarchy/sudo-no-update:$PATH" OMARCHY_SUDO_NO_UPDATE=1 omarchy-update-aur-pkgs
omarchy_security_revoke_sudo_timestamp
# The last sudo-capable step: the boot files every package step left, AUR
# packages' initramfs rebuilds included, must be shown to boot the updated
# system. It follows third-party build code, so it starts cold and, like AUR,
# authorizes one command through the no-update wrapper without a reusable
# timestamp. Until it passes, the update neither finishes nor offers a reboot.
boot_verified=1
PATH="$OMARCHY_PATH/default/omarchy/sudo-no-update:$PATH" OMARCHY_SUDO_NO_UPDATE=1 omarchy-update-boot || boot_verified=0
omarchy_security_revoke_sudo_timestamp
# The sleep inhibitor covers AUR builds, hooks and mise as well; releasing it
# needs no privilege because the held command already dropped to this user.
# Release it before offering a reboot: a confirmed reboot can terminate this
@@ -156,5 +165,10 @@ if [[ ${1:-} == "-y" ]] || omarchy-update-confirm; then
omarchy-update-stay-awake stop
update_stay_awake_stopped=1
"$OMARCHY_PATH/bin/omarchy-update-restart" --reboot-only
if (( boot_verified )); then
"$OMARCHY_PATH/bin/omarchy-update-restart" --reboot-only
else
echo -e "\e[31m\nThe update is not finished: its boot files did not pass verification (see above). Do not reboot until omarchy update completes.\e[0m" >&2
exit 1
fi
fi
+39
View File
@@ -0,0 +1,39 @@
#!/bin/bash
# omarchy:summary=Verify the platform's boot chain after the update's package changes
# omarchy:hidden=true
# omarchy:requires-sudo=true
# Proves that the boot files the package steps left boot the updated system;
# omarchy update neither finishes nor offers a reboot until this passes. It is
# the update-verify lifecycle dispatch operation: a no-op where the platform
# registers no boot package, x86 included, and the platform's own entrypoint
# says what failed.
set -e
if (( $# )); then
echo "Usage: omarchy-update-boot" >&2
exit 2
fi
operation=update-verify
# Resolving as the user keeps sudo, and its password prompt, out of updates
# with nothing to run. Root resolves again from its own fixed paths before
# anything runs.
errors=$(mktemp)
trap 'rm -f "$errors"' EXIT
status=0
resolved=$(omarchy-lifecycle-dispatch --resolve "$operation" 2>"$errors") || status=$?
# 3: the platform's boot package is not installed at all. Such a machine
# predates it and boots a chain the package does not manage, so verification
# warns instead of failing every update until the package is installed.
if (( status == 3 )); then
echo -e "\e[33m\n$(sed 's/^Error: //' "$errors")\nThe boot files were not verified. This machine predates its platform's boot package; updates verify them once it is installed.\e[0m" >&2
elif (( status != 0 )); then
cat "$errors" >&2
exit "$status"
elif [[ -n $resolved ]]; then
sudo omarchy-lifecycle-dispatch "$operation"
fi
+25 -1
View File
@@ -22,8 +22,32 @@ fi
# Ensure we have the latest archlinux-keyring, maintainer keys might have changed
# Always reinstall, as the keyring can be updated without a package version bump.
# An aarch64 machine's Arch Linux ARM repositories are signed with keys of
# their own, so its keyring is kept current the same way.
keyrings=(archlinux-keyring)
if omarchy-hw-aarch64 && omarchy-pkg-present archlinuxarm-keyring; then
keyrings+=(archlinuxarm-keyring)
fi
# A platform package names the signing keyrings of the repositories it adds,
# one package per line. Only installed ones are refreshed, so this adds no
# repository the machine didn't already trust; a refreshed keyring may carry
# that repository's new keys. A line that isn't one keyring package name is
# skipped with a warning, so a mistake in the list can't stop every update.
platform_keyrings=/usr/share/omarchy-platform/keyrings
if [[ -f $platform_keyrings ]]; then
while read -r keyring extra || [[ -n $keyring ]]; do
[[ -n $keyring && $keyring != \#* ]] || continue
if [[ -n $extra || ! $keyring =~ ^[a-z0-9][a-z0-9@._+-]*-keyring$ ]]; then
echo "Warning: skipping ${keyring}${extra:+ $extra} in $platform_keyrings: not a keyring package name" >&2
continue
fi
if omarchy-pkg-present "$keyring" && [[ " ${keyrings[*]} " != *" $keyring "* ]]; then
keyrings+=("$keyring")
fi
done <"$platform_keyrings"
fi
echo -e "\e[32m\nUpdate Arch signing keys\e[0m"
sudo pacman -Sy --noconfirm archlinux-keyring >/dev/null 2> >(grep -vE '^warning: archlinux-keyring-[^ ]+ is up to date -- reinstalling$' >&2)
sudo pacman -Sy --noconfirm "${keyrings[@]}" >/dev/null 2> >(grep -vE '^warning: [a-z0-9@._+-]+-keyring-[^ ]+ is up to date -- reinstalling$' >&2)
# Say "correct" only once the key verifiably is: before the failure checks
# above, a failed recv or reinstall still ended here with exit 0.
@@ -98,6 +98,17 @@ mapfile -t leftovers < <(
# the moved files inactive for nothing.
((${#leftovers[@]} == $(grep -c ' exists in filesystem' "$errors"))) || exit 1
# A platform whose boot package owns the boot chain may also have files no
# package owns that it can't do without (written by its image or installer).
# Its boot package vouches for the takeover or refuses it, and the files stay.
# Where the platform registers none, nothing resolves and nothing asks for root.
takeover=$(omarchy-lifecycle-dispatch --resolve update-takeover) || exit 1
if [[ -n $takeover ]] && ! sudo omarchy-lifecycle-dispatch update-takeover "${leftovers[@]}"; then
echo -e "\e[33m\nThis platform keeps these files, so the upgrade can't take them over:\e[0m" >&2
printf ' %s\n' "${leftovers[@]}" >&2
exit 1
fi
echo -e "\e[33m\nTaking over files pacman doesn't own yet:\e[0m"
for path in "${leftovers[@]}"; do
sudo mkdir -p "$replaced${path%/*}"
+5
View File
@@ -807,6 +807,11 @@ install_omarchy_quattro_packages() {
if [[ -f /usr/share/omarchy/install/omarchy-base.packages ]]; then
log "Installing Omarchy quattro default package set"
mapfile -t base_packages < <(sed -e 's/[[:space:]]*#.*$//' -e '/^[[:space:]]*$/d' /usr/share/omarchy/install/omarchy-base.packages)
# Some base packages have no aarch64 build.
if [[ $(uname -m) != "x86_64" && -f /usr/share/omarchy/install/omarchy-x86_64-only.packages ]]; then
mapfile -t base_packages < <(printf '%s\n' "${base_packages[@]}" |
grep -vxF -f <(sed -e 's/[[:space:]]*#.*$//' -e '/^[[:space:]]*$/d' /usr/share/omarchy/install/omarchy-x86_64-only.packages))
fi
if (( ${#base_packages[@]} )); then
as_root pacman -S --needed --noconfirm --ask 4 --overwrite='*' "${base_packages[@]}"
mark_packages_explicit "${base_packages[@]}"
+5
View File
@@ -234,6 +234,11 @@ if [[ -n $MIME_TYPES ]]; then
printf 'MimeType=%s\n' "$(desktop_string_escape "$MIME_TYPES")" >>"$DESKTOP_FILE"
fi
# Floating titlebars name the window after this entry rather than its page title.
if [[ -z $CUSTOM_EXEC ]]; then
printf 'StartupWMClass=%s\n' "$(desktop_string_escape "$(omarchy-webapp-window-class "$APP_URL")")" >>"$DESKTOP_FILE"
fi
chmod +x "$DESKTOP_FILE"
if [[ $INTERACTIVE_MODE == "true" ]]; then
+24
View File
@@ -0,0 +1,24 @@
#!/bin/bash
# omarchy:summary=Print the window class Chromium gives a web app opened at a URL
# omarchy:args=<url>
# omarchy:hidden=true
# Chromium names an --app window after the URL's host and path, so a web app's
# launcher can name its window in StartupWMClass, which titlebars use to show
# the app's name rather than the page title.
url=${1:-}
if [[ -z $url ]]; then
echo "Usage: omarchy-webapp-window-class <url>" >&2
exit 1
fi
address=${url#*://}
address=${address%%[?#]*}
host=${address%%/*}
path=${address:${#host}}
host=${host##*@}
host=${host%%:*}
printf 'chrome-%s_%s-Default\n' "${host,,}" "${path:-/}" | sed 's|/|_|g'
+6
View File
@@ -4,6 +4,12 @@
# omarchy:args=<install|remove|launch|stop|status> [options]
# omarchy:requires-sudo=true
# The VM is an x86_64 Windows guest under KVM, which only an x86_64 host runs.
if ! omarchy-hw-x86; then
echo "Windows VM needs an x86_64 machine (it runs an x86_64 guest under KVM)." >&2
exit 1
fi
# The Windows VM runs a privileged container (KVM, /dev/net/tun, NET_ADMIN), so
# it needs the root-owned Docker daemon. By default the user is NOT in the docker
# group (that group is root-equivalent), so Docker access is gated behind a
+5 -4
View File
@@ -1,8 +1,7 @@
# Leave Ctrl+Space to tmux/Herdr and the Super shortcuts to the desktop.
# Omarchy binds Super+I. Leave Ctrl+Space to tmux/Herdr and other Super keys to the desktop.
[Hotkey/TriggerKeys]
0=Control+Shift+space
1=Zenkaku_Hankaku
2=Hangul
0=Zenkaku_Hankaku
1=Hangul
[Hotkey/ActivateKeys]
0=Henkan
@@ -10,7 +9,9 @@
[Hotkey/DeactivateKeys]
0=Muhenkan
# A lone Shift leaves an engine like Pinyin for Latin input and returns to it.
[Hotkey/AltTriggerKeys]
0=Shift_L
[Hotkey/EnumerateGroupForwardKeys]
+6 -3
View File
@@ -6,9 +6,12 @@
name = "terminal"
[theme.custom]
# The active tab is drawn as panel_bg text on an accent background, so panel_bg
# has to be dark for it to read - same colors as status-left's "#[fg=black,bg=blue]"
panel_bg = "black"
# The tab bar sits on the terminal's own background, like tmux's bg=default, so
# it turns see-through with the rest of the terminal under Glassy windows. With
# no panel_bg the active tab's text falls back to surface_dim, so that is black
# for dark text on the accent - same colors as status-left's "#[fg=black,bg=blue]"
panel_bg = "reset"
surface_dim = "black"
[terminal]
# Matches -c "#{pane_current_path}" on every split, window, and session
+1 -1
View File
@@ -7,7 +7,7 @@
-- input = {
-- -- Use multiple keyboard layouts and switch between them with Left Alt + Right Alt.
-- kb_layout = "us,dk,eu",
-- kb_options = "compose:caps,shift:both_capslock_cancel,grp:alts_toggle",
-- kb_options = "omarchy:shift_caps_compose,grp:alts_toggle",
--
-- -- Use a specific keyboard variant if needed (e.g. intl for international keyboards).
-- kb_variant = "intl",
+5 -5
View File
@@ -6,7 +6,7 @@
},
"bar": {
"position": "top",
"transparent": false,
"background": "opaque",
"centerAnchor": "omarchy.clock",
"layout": {
"left": [
@@ -21,9 +21,6 @@
{
"id": "omarchy.indicators"
},
{
"id": "omarchy.elsewhen"
},
{
"id": "omarchy.clock",
"format": "dddd HH:mm",
@@ -31,7 +28,7 @@
"verticalFormat": "HH\n\u2014\nmm"
},
{
"id": "omarchy.keyboard-layout"
"id": "omarchy.elsewhen"
},
{
"id": "omarchy.weather"
@@ -41,6 +38,9 @@
}
],
"right": [
{
"id": "omarchy.keyboard-layout"
},
{
"id": "omarchy.tray"
},
-268
View File
@@ -1,268 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--The Xournal++ settings file. Do not edit this file! Most settings are available in the Settings dialog, the others are commented in this file, but handle with care!-->
<settings>
<property name="pressureSensitivity" value="true"/>
<property name="minimumPressure" value="0.05"/>
<property name="pressureMultiplier" value="1"/>
<property name="zoomGesturesEnabled" value="true"/>
<property name="selectedToolbar" value="Portrait"/>
<property name="lastSavePath" value=""/>
<property name="lastOpenPath" value=""/>
<property name="lastImagePath" value=""/>
<property name="edgePanSpeed" value="20"/>
<property name="edgePanMaxMult" value="5"/>
<property name="zoomStep" value="10"/>
<property name="zoomStepScroll" value="2"/>
<property name="displayDpi" value="72"/>
<property name="mainWndWidth" value="800"/>
<property name="mainWndHeight" value="600"/>
<property name="maximized" value="true"/>
<property name="showToolbar" value="true"/>
<property name="showSidebar" value="true"/>
<property name="sidebarWidth" value="150"/>
<property name="sidebarNumberingStyle" value="1"/>
<property name="sidebarOnRight" value="false"/>
<property name="scrollbarOnLeft" value="false"/>
<property name="menubarVisible" value="true"/>
<property name="filepathShownInTitlebar" value="false"/>
<property name="numColumns" value="1"/>
<property name="numRows" value="1"/>
<property name="viewFixedRows" value="false"/>
<property name="showPairedPages" value="false"/>
<property name="layoutVertical" value="false"/>
<property name="layoutRightToLeft" value="false"/>
<property name="layoutBottomToTop" value="false"/>
<property name="numPairsOffset" value="1"/>
<!--The icon theme, allowed values are "disabled", "onDrawOfLastPage", and "onScrollOfLastPage"-->
<property name="emptyLastPageAppend" value="disabled"/>
<property name="presentationMode" value="false"/>
<!--Which GUI elements are shown in default view mode, separated by a colon (,)-->
<property name="defaultViewModeAttributes" value="showMenubar,showToolbar,showSidebar"/>
<!--Which GUI elements are shown in fullscreen view mode, separated by a colon (,)-->
<property name="fullscreenViewModeAttributes" value="goFullscren,showToolbar,showSidebar"/>
<!--Which GUI elements are shown in presentation view mode, separated by a colon (,)-->
<property name="presentationViewModeAttributes" value="goFullscren,showToolbar"/>
<!--The cursor icon used with a stylus, allowed values are "none", "dot", "big", "arrow"-->
<property name="stylusCursorType" value="dot"/>
<!--The eraser cursor visibility used with a stylus, allowed values are "never", "always", "hover", "touch"-->
<property name="eraserVisibility" value="always"/>
<!--The icon theme, allowed values are "iconsColor", "iconsLucide"-->
<property name="iconTheme" value="iconsColor"/>
<!--Dark/light mode, allowed values are "useSystem", "forceLight", "forceDark"-->
<property name="themeVariant" value="forceDark"/>
<property name="highlightPosition" value="false"/>
<property name="cursorHighlightColor" value="2164260608"/>
<property name="cursorHighlightBorderColor" value="2147483903"/>
<property name="cursorHighlightRadius" value="30"/>
<property name="cursorHighlightBorderWidth" value="0"/>
<property name="useStockIcons" value="false"/>
<property name="disableScrollbarFadeout" value="false"/>
<property name="disableAudio" value="false"/>
<!--Hides scroolbars in the main window, allowed values: "none", "horizontal", "vertical", "both"-->
<property name="scrollbarHideType" value="none"/>
<property name="autoloadMostRecent" value="false"/>
<property name="autoloadPdfXoj" value="true"/>
<property name="defaultSaveName" value="%F-Note-%H-%M"/>
<property name="defaultPdfExportName" value="%{name}_annotated"/>
<property name="autosaveEnabled" value="true"/>
<property name="autosaveTimeout" value="3"/>
<property name="addHorizontalSpace" value="false"/>
<property name="addHorizontalSpaceAmount" value="150"/>
<property name="addVerticalSpace" value="false"/>
<property name="addVerticalSpaceAmount" value="150"/>
<property name="drawDirModsEnabled" value="false"/>
<property name="drawDirModsRadius" value="50"/>
<property name="snapRotation" value="true"/>
<property name="snapRotationTolerance" value="0.3"/>
<property name="snapGrid" value="true"/>
<property name="snapGridTolerance" value="0.5"/>
<property name="snapGridSize" value="14.17"/>
<property name="strokeRecognizerMinSize" value="40"/>
<property name="touchDrawing" value="false"/>
<property name="gtkTouchInertialScrolling" value="true"/>
<property name="pressureGuessing" value="false"/>
<property name="selectionBorderColor" value="4294901760"/>
<property name="backgroundColor" value="4292664021"/>
<property name="selectionMarkerColor" value="4285702095"/>
<property name="touchZoomStartThreshold" value="0"/>
<property name="pageRerenderThreshold" value="5"/>
<!--The count of rendered PDF pages which will be cached.-->
<property name="pdfPageCacheSize" value="10"/>
<property name="preloadPagesBefore" value="3"/>
<property name="preloadPagesAfter" value="5"/>
<property name="eagerPageCleanup" value="true"/>
<!--Config for new pages-->
<property name="pageTemplate" value="xoj/template&#10;copyLastPageSettings=true&#10;size=595.275591x841.889764&#10;backgroundType=lined&#10;backgroundColor=#ffffff&#10;"/>
<property name="sizeUnit" value=""/>
<property name="audioFolder" value=""/>
<property name="audioInputDevice" value="-1"/>
<property name="audioOutputDevice" value="-1"/>
<property name="audioSampleRate" value="44100"/>
<property name="audioGain" value="1"/>
<property name="defaultSeekTime" value="5"/>
<property name="pluginEnabled" value=""/>
<property name="pluginDisabled" value=""/>
<property name="strokeFilterIgnoreTime" value="150"/>
<property name="strokeFilterIgnoreLength" value="1"/>
<property name="strokeFilterSuccessiveTime" value="500"/>
<property name="strokeFilterEnabled" value="false"/>
<property name="doActionOnStrokeFiltered" value="false"/>
<property name="trySelectOnStrokeFiltered" value="false"/>
<property name="snapRecognizedShapesEnabled" value="false"/>
<property name="restoreLineWidthEnabled" value="false"/>
<property name="numIgnoredStylusEvents" value="0"/>
<property name="inputSystemTPCButton" value="false"/>
<property name="inputSystemDrawOutsideWindow" value="true"/>
<property name="preferredLocale" value=""/>
<property name="stabilizerAveragingMethod" value="0"/>
<property name="stabilizerPreprocessor" value="0"/>
<property name="stabilizerBuffersize" value="20"/>
<property name="stabilizerSigma" value="0.5"/>
<property name="stabilizerDeadzoneRadius" value="1.3"/>
<property name="stabilizerDrag" value="0.4"/>
<property name="stabilizerMass" value="5"/>
<property name="stabilizerCuspDetection" value="true"/>
<property name="stabilizerFinalizeStroke" value="true"/>
<property name="latexSettings.autoCheckDependencies" value="true"/>
<property name="latexSettings.defaultText" value="x^2"/>
<property name="latexSettings.globalTemplatePath" value="/usr/share/xournalpp/resources/default_template.tex"/>
<property name="latexSettings.genCmd" value="pdflatex -halt-on-error -interaction=nonstopmode '{}'"/>
<property name="latexSettings.sourceViewThemeId" value=""/>
<property name="latexSettings.editorFont" value="Monospace 12"/>
<property name="latexSettings.useCustomEditorFont" value="false"/>
<property name="latexSettings.editorWordWrap" value="true"/>
<property name="latexSettings.sourceViewAutoIndent" value="true"/>
<property name="latexSettings.sourceViewSyntaxHighlight" value="true"/>
<property name="latexSettings.sourceViewShowLineNumbers" value="false"/>
<property name="font" font="Sans" size="12"/>
<data name="buttonConfig">
<data name="default">
<attribute name="color" type="hex" value="ff000000"/>
<attribute name="drawingType" type="string" value="default"/>
<attribute name="size" type="string" value="thin"/>
<attribute name="tool" type="string" value="pen"/>
</data>
<data name="eraser">
<attribute name="eraserMode" type="string" value="none"/>
<attribute name="size" type="string" value="none"/>
<attribute name="tool" type="string" value="eraser"/>
</data>
<data name="middle">
<attribute name="tool" type="string" value="hand"/>
</data>
<data name="right">
<attribute name="tool" type="string" value="none"/>
</data>
<data name="stylus">
<attribute name="tool" type="string" value="none"/>
</data>
<data name="stylus2">
<attribute name="tool" type="string" value="none"/>
</data>
<data name="touch">
<attribute name="device" type="string" value=""/>
<attribute name="disableDrawing" type="boolean" value="false"/>
<attribute name="tool" type="string" value="none"/>
</data>
</data>
<data name="deviceClasses">
<data name="Wayland Finger Scrolling">
<attribute name="deviceClass" type="int" value="1"/>
<attribute name="deviceSource" type="int" value="6"/>
</data>
<data name="Wayland Pointer">
<attribute name="deviceClass" type="int" value="1"/>
<attribute name="deviceSource" type="int" value="0"/>
</data>
</data>
<data name="tools">
<attribute name="current" type="string" value="text"/>
<data name="drawArrow">
<attribute name="drawingType" type="string" value="default"/>
</data>
<data name="drawCoordinateSystem">
<attribute name="drawingType" type="string" value="default"/>
</data>
<data name="drawDoubleArrow">
<attribute name="drawingType" type="string" value="default"/>
</data>
<data name="drawEllipse">
<attribute name="drawingType" type="string" value="default"/>
</data>
<data name="drawRect">
<attribute name="drawingType" type="string" value="default"/>
</data>
<data name="drawSpline">
<attribute name="drawingType" type="string" value="default"/>
</data>
<data name="eraser">
<attribute name="drawingType" type="string" value="default"/>
<attribute name="size" type="string" value="MEDIUM"/>
<attribute name="type" type="string" value="default"/>
</data>
<data name="hand">
<attribute name="drawingType" type="string" value="default"/>
</data>
<data name="highlighter">
<attribute name="color" type="hex" value="ffffff00"/>
<attribute name="drawingType" type="string" value="default"/>
<attribute name="fill" type="int" value="0"/>
<attribute name="fillAlpha" type="int" value="128"/>
<attribute name="size" type="string" value="MEDIUM"/>
</data>
<data name="image">
<attribute name="drawingType" type="string" value="default"/>
</data>
<data name="pen">
<attribute name="color" type="hex" value="ff3333cc"/>
<attribute name="drawingType" type="string" value="default"/>
<attribute name="fill" type="int" value="0"/>
<attribute name="fillAlpha" type="int" value="128"/>
<attribute name="size" type="string" value="MEDIUM"/>
<attribute name="style" type="string" value="plain"/>
</data>
<data name="playObject">
<attribute name="drawingType" type="string" value="default"/>
</data>
<data name="selectMultiLayerRect">
<attribute name="drawingType" type="string" value="default"/>
</data>
<data name="selectMultiLayerRegion">
<attribute name="drawingType" type="string" value="default"/>
</data>
<data name="selectObject">
<attribute name="drawingType" type="string" value="default"/>
</data>
<data name="selectPdfTextLinear">
<attribute name="color" type="hex" value="ff000000"/>
<attribute name="drawingType" type="string" value="default"/>
</data>
<data name="selectPdfTextRect">
<attribute name="color" type="hex" value="ff000000"/>
<attribute name="drawingType" type="string" value="default"/>
</data>
<data name="selectRect">
<attribute name="drawingType" type="string" value="default"/>
</data>
<data name="selectRegion">
<attribute name="drawingType" type="string" value="default"/>
</data>
<data name="showFloatingToolbox">
<attribute name="drawingType" type="string" value="default"/>
</data>
<data name="text">
<attribute name="color" type="hex" value="ff000000"/>
<attribute name="drawingType" type="string" value="default"/>
</data>
<data name="verticalSpace">
<attribute name="drawingType" type="string" value="default"/>
</data>
</data>
<data name="touch">
<attribute name="cmdDisable" type="string" value=""/>
<attribute name="cmdEnable" type="string" value=""/>
<attribute name="disableTouch" type="boolean" value="false"/>
<attribute name="method" type="string" value="auto"/>
<attribute name="timeout" type="int" value="1000"/>
</data>
</settings>
+5 -23
View File
@@ -2,12 +2,12 @@
name: omarchy
description: >
REQUIRED for end-user customization of Linux desktop, window manager, or system config.
Use when editing ~/.config/hypr/, ~/.config/omarchy/,
Use when editing ANY file in ~/.config/, ~/.config/hypr/, ~/.config/omarchy/,
~/.config/alacritty/, ~/.config/foot/, ~/.config/kitty/, or ~/.config/ghostty/.
Triggers: Hyprland, window rules, animations, keybindings, monitors, gaps, borders,
blur, opacity, omarchy-shell, bar, terminal config, themes, background,
night light, idle, lock screen, screenshots, reminders, layer rules, workspace
settings, display config, and user-facing omarchy commands. Excludes Omarchy
Triggers: Hyprland, window rules, window behavior, animations, keybindings, monitors, gaps, borders,
blur, opacity, omarchy-shell, bar, widgets, terminal config, themes, fonts, background,
night light, idle, lock screen, screenshots, screen recording, reminders, layer rules, workspace
settings, display config, appearance changes, and user-facing omarchy commands. Excludes Omarchy
source development through `omarchy dev link` workflows.
---
@@ -18,24 +18,6 @@ Manage [Omarchy](https://omarchy.org/) Linux systems - a beautiful, fun, agentic
This skill is for end-user customization on installed systems.
It is not for contributing to Omarchy source code.
## When This Skill MUST Be Used
**ALWAYS invoke this skill for end-user requests involving ANY of these:**
- Editing ANY file in `~/.config/hypr/` (window rules, animations, keybindings, monitors, etc.)
- Editing `~/.config/omarchy/shell.json` (status bar layout, widgets)
- Editing terminal configs (alacritty, foot, kitty, ghostty)
- Editing ANY file in `~/.config/omarchy/`
- Window behavior, animations, opacity, blur, gaps, borders
- Layer rules, workspace settings, display/monitor configuration
- Themes, backgrounds, fonts, appearance changes
- User-facing `omarchy` commands (`omarchy theme ...`, `omarchy refresh ...`, `omarchy restart ...`, etc.)
- Screenshots, screen recording, reminders, night light, idle behavior, lock screen
**If you're about to edit a config file in ~/.config/ on this system, STOP and use this skill first.**
**Do NOT use this skill for Omarchy development tasks** (editing the Omarchy source tree, creating migrations, or running `omarchy dev ...` workflows).
## Topic Guides
Deeper instructions for common areas live next to this file. Read the
+19
View File
@@ -17,6 +17,25 @@ changes. `idle.screensaver` and `idle.lock` are seconds since user idle began.
**Commands:** `omarchy restart shell`, `omarchy refresh shell`
## Plugin UI Safety
Plugins run inside Omarchy's long-lived, unsandboxed shell. Treat text from network responses, files, IPC, settings, environment variables, and command output as untrusted.
- QML `Text` defaults to `Text.AutoText`. Do not bind untrusted data to an AutoText sink: markup-shaped input such as `<img src="https://example.invalid/tracker">` can be interpreted as rich text and load a resource.
- For display text owned by the plugin, set `textFormat: Text.PlainText`. Prefer a local `PlainText` component when the plugin has multiple text elements.
```qml
Text {
textFormat: Text.PlainText
text: modelData.name
}
```
- When passing dynamic text to an Omarchy-owned component whose internal `Text` format the plugin cannot set, neutralize markup in the plugin first with a helper of its own, and add a regression test; Omarchy ships no such helper. Never edit the packaged component to add a plain-text option: that belongs in a pull request to Omarchy.
- Bound and validate downloaded data before displaying or saving it: limit response and collection sizes, string lengths, identifiers, and timestamps.
- Keep network access explicit. Use fixed HTTPS sources, timeouts, response-size limits, and validation. For periodic background refreshes, disclose the behavior and let users enable or disable it. Never execute downloaded content or interpolate untrusted values into shell commands.
- Add regression coverage for markup-shaped input and every shared tooltip or notification sink that receives dynamic text.
## Bar Layout
Use the `omarchy bar` group to move and manage widgets:
@@ -1,298 +1,6 @@
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<!-- CJK: fold the language tag onto one tag per Noto CJK variant -->
<match target="pattern">
<test name="lang" qual="first" compare="eq">
<string>zh</string>
</test>
<edit name="lang" mode="assign">
<string>zh-cn</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="eq">
<string>zh-hant</string>
</test>
<edit name="lang" mode="assign">
<string>zh-tw</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-hant-hk</string>
</test>
<edit name="lang" mode="assign">
<string>zh-hk</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-hant-mo</string>
</test>
<edit name="lang" mode="assign">
<string>zh-hk</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-hant</string>
</test>
<edit name="lang" mode="assign">
<string>zh-tw</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-hans</string>
</test>
<edit name="lang" mode="assign">
<string>zh-cn</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-sg</string>
</test>
<edit name="lang" mode="assign">
<string>zh-cn</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-mo</string>
</test>
<edit name="lang" mode="assign">
<string>zh-hk</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="eq">
<string>yue</string>
</test>
<edit name="lang" mode="assign">
<string>zh-hk</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>yue-hans</string>
</test>
<edit name="lang" mode="assign">
<string>zh-cn</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>yue-cn</string>
</test>
<edit name="lang" mode="assign">
<string>zh-cn</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>yue</string>
</test>
<edit name="lang" mode="assign">
<string>zh-hk</string>
</edit>
</match>
<!-- Then append that variant after the generic family it falls back from -->
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-cn</string>
</test>
<test name="family">
<string>sans-serif</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Sans CJK SC</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-cn</string>
</test>
<test name="family">
<string>serif</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Serif CJK SC</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-cn</string>
</test>
<test name="family">
<string>monospace</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Sans Mono CJK SC</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-hk</string>
</test>
<test name="family">
<string>sans-serif</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Sans CJK HK</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-hk</string>
</test>
<test name="family">
<string>serif</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Serif CJK HK</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-hk</string>
</test>
<test name="family">
<string>monospace</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Sans Mono CJK HK</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-tw</string>
</test>
<test name="family">
<string>sans-serif</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Sans CJK TC</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-tw</string>
</test>
<test name="family">
<string>serif</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Serif CJK TC</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-tw</string>
</test>
<test name="family">
<string>monospace</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Sans Mono CJK TC</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<langset><string>ja</string></langset>
</test>
<test name="family">
<string>sans-serif</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Sans CJK JP</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<langset><string>ja</string></langset>
</test>
<test name="family">
<string>serif</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Serif CJK JP</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<langset><string>ja</string></langset>
</test>
<test name="family">
<string>monospace</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Sans Mono CJK JP</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<langset><string>ko</string></langset>
</test>
<test name="family">
<string>sans-serif</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Sans CJK KR</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<langset><string>ko</string></langset>
</test>
<test name="family">
<string>serif</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Serif CJK KR</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<langset><string>ko</string></langset>
</test>
<test name="family">
<string>monospace</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Sans Mono CJK KR</string>
</edit>
</match>
<match target="pattern">
<test name="family" qual="any">
<string>sans-serif</string>
+7
View File
@@ -0,0 +1,7 @@
/* GTK asks the compositor to blur behind a see-through window only where a
backdrop-filter blur covers it, so Glassy windows load this into GTK apps,
such as Ghostty and Nautilus. The compositor's own blur settings decide
how it looks. */
window {
backdrop-filter: blur(20px);
}
+11 -5
View File
@@ -1,9 +1,15 @@
if o.shell_succeeds("omarchy-default-dictation") then
-- Bound only once the chosen backend's own program is installed (Omarchy ships
-- every adapter): an aarch64 machine has no Superwhisper build, and
-- push-to-talk would hold Right Alt (AltGr) for nothing.
local pipe = io.popen("omarchy-default-dictation 2>/dev/null", "r")
local backend = pipe and pipe:read("*l")
if pipe then
pipe:close()
end
if backend and backend:match("^[%w%-]+$") and not o.cmd_missing(backend) then
o.bind("SUPER + CTRL + X", "Toggle dictation", "omarchy-dictation toggle")
o.bind("F9", "Start dictation (push-to-talk)", "omarchy-dictation start")
o.bind("F9", "Stop dictation (push-to-talk)", "omarchy-dictation stop", { release = true })
o.bind_hold("F9", "Start dictation (push-to-talk)", "omarchy-dictation start", "Stop dictation (push-to-talk)", "omarchy-dictation stop")
-- A modifier's mask changes between its press and release. Match the keysym
-- independently of that mask; AltGr layouts use a different keysym.
o.bind("ALT + Alt_R", "Start dictation (push-to-talk)", "omarchy-dictation start", { ignore_mods = true })
o.bind("ALT + Alt_R", "Stop dictation (push-to-talk)", "omarchy-dictation stop", { release = true, ignore_mods = true })
o.bind_hold("ALT + Alt_R", "Start dictation (push-to-talk)", "omarchy-dictation start", "Stop dictation (push-to-talk)", "omarchy-dictation stop", { ignore_mods = true })
end
+5 -42
View File
@@ -1,5 +1,3 @@
local paths = require("default.hypr.paths")
-- Same terminal tag test as clipboard.lua.
local function active_window_is_terminal()
local window = hl.get_active_window()
@@ -16,50 +14,15 @@ local function active_window_is_terminal()
return false
end
-- Read on demand so adding an engine in a live desktop needs no reload.
local function has_input_method()
local file = io.open(paths.config_home .. "/fcitx5/profile", "r")
if not file then return false end
local in_item, found = false, false
for line in file:lines() do
if line:match("^%[") then
in_item = line:match("^%[Groups/%d+/Items/%d+%]$") ~= nil
elseif in_item then
local name = line:match("^Name=(.+)$")
if name and not name:match("^keyboard%-") then found = true end
end
end
file:close()
return found
end
-- Preserve custom Ctrl + Space triggers. Without a
-- [Hotkey/TriggerKeys] list in the config, fcitx5 uses its default, which has
-- Ctrl + Space.
local function fcitx5_toggles_on_ctrl_space()
local file = io.open(paths.config_home .. "/fcitx5/config", "r")
if not file then
return true
end
local in_triggers, has_triggers, toggles = false, false, false
for line in file:lines() do
if line:match("^%[") then
in_triggers = line == "[Hotkey/TriggerKeys]"
has_triggers = has_triggers or in_triggers
elseif in_triggers and line:match("^%d+=Control%+space$") then
toggles = true
end
end
file:close()
return toggles or not has_triggers
end
-- Keep the desktop shortcut independent of Fcitx's custom switching keys.
-- Read the live Fcitx group so setup works without a compositor reload.
o.bind("SUPER + I", "Switch input language", "omarchy-input-method cycle")
o.bind("SUPER + SHIFT + I", "Switch input language back", "omarchy-input-method cycle back")
-- Pass the tmux/Herdr prefix through and return to direct input, so the next
-- key reaches the multiplexer instead of the engine's composition.
o.bind("CTRL + SPACE", "Direct input for the terminal prefix", function()
if active_window_is_terminal() and has_input_method() and not fcitx5_toggles_on_ctrl_space() then
if active_window_is_terminal() then
-- Wait for the local controller reply before dispatching the next key.
o.shell_succeeds("timeout 0.3 fcitx5-remote --check -c")
end
+2 -1
View File
@@ -11,7 +11,8 @@ o.bind("SUPER + ALT + F", "Full width", hl.dsp.window.fullscreen({ mode = "maxim
o.bind("SUPER + O", "Pop window out (float & pin)", "omarchy-hyprland-window-pop")
o.bind("SUPER + ALT + Home", "Save window width", "omarchy-hyprland-window-width save")
o.bind("SUPER + Home", "Restore window width", "omarchy-hyprland-window-width restore")
o.bind("SUPER + L", "Toggle workspace layout", "omarchy-hyprland-workspace-layout-toggle")
o.bind("SUPER + L", "Cycle workspace layout", "omarchy-hyprland-workspace-layout-toggle")
o.bind("SUPER + ALT + L", "Cycle workspace layout back", "omarchy-hyprland-workspace-layout-toggle back")
o.bind("SUPER + LEFT", "Focus on left window", hl.dsp.focus({ direction = "l" }))
o.bind("SUPER + RIGHT", "Focus on right window", hl.dsp.focus({ direction = "r" }))
+3
View File
@@ -34,6 +34,9 @@ o.bind("SUPER + CTRL + Delete", "Toggle laptop display", "omarchy-hyprland-monit
o.bind("SUPER + CTRL + ALT + Delete", "Toggle laptop display mirroring", "omarchy-hyprland-monitor-internal-mirror toggle")
o.bind("switch:on:Lid Switch", nil, "omarchy-system-lid-close", { locked = true })
o.bind("switch:off:Lid Switch", nil, "omarchy-hyprland-monitor-clamshell", { locked = true })
-- Apple Silicon reports its lid through the SMC, under its own switch name.
o.bind("switch:on:Apple SMC power/lid events", nil, "omarchy-system-lid-close", { locked = true })
o.bind("switch:off:Apple SMC power/lid events", nil, "omarchy-hyprland-monitor-clamshell", { locked = true })
o.bind("PRINT", "Screenshot", "omarchy-capture-screenshot")
o.bind("ALT + PRINT", "Screenrecording", "omarchy-capture-screenrecording --stop-recording || omarchy-menu toggle trigger.capture.screenrecord")
+22
View File
@@ -0,0 +1,22 @@
-- Frosted glass from the glassy Starship (#14726), shared by Glassy windows and
-- the Glassy bar: whatever is behind a see-through surface is blurred.
return function()
hl.config({
decoration = {
blur = {
enabled = true,
size = 4,
passes = 2,
new_optimizations = true,
xray = false,
contrast = 1.0,
brightness = 0.75,
noise = 0.01,
vibrancy = 0.35,
vibrancy_darkness = 0.35,
popups = true,
special = true,
},
},
})
end
+60 -2
View File
@@ -206,6 +206,63 @@ function o.bind_toggle(keys, description, toggle, options)
o.bind(keys, description, "omarchy-toggle-" .. toggle, options)
end
-- Bind one action to a key's press and another to its release, as for
-- push-to-talk. Hyprland skips a plain release bind once another key or mouse
-- button is released during the hold, or once the modifiers have changed, so
-- typing while holding the key would never run the release. The release half
-- here is transparent and ignores modifiers, and runs only after its own press
-- did, so letting go of the key without the rest of the chord does nothing. It
-- is also non-consuming: a release bind that ignores modifiers would otherwise
-- take the key from apps whenever it is pressed with other modifiers, such as
-- Shift+F9, or without them, such as a plain x for SUPER + X.
function o.bind_hold(keys, press_description, press, release_description, release, options)
local held = false
press, release = command_from(press, press_description), command_from(release, release_description)
local function run(dispatcher)
if type(dispatcher) == "string" then
hl.exec_cmd(dispatcher)
elseif type(dispatcher) == "function" then
dispatcher()
else
hl.dispatch(dispatcher)
end
end
local function on_press()
held = true
run(press)
end
local function on_release()
if held then
held = false
run(release)
end
end
if type(press) == "string" then
o.bind_commands[on_press] = press
end
if type(release) == "string" then
o.bind_commands[on_release] = release
end
local press_options, release_options = {}, {}
for key, value in pairs(options or {}) do
press_options[key], release_options[key] = value, value
end
-- Options that change when the press half fires would break the release half.
for _, key in ipairs({ "repeating", "long_press", "click", "drag" }) do
release_options[key] = nil
end
release_options.release, release_options.transparent = true, true
release_options.ignore_mods, release_options.non_consuming = true, true
o.bind(keys, press_description, on_press, press_options)
o.bind(keys, release_description, on_release, release_options)
end
function o.notify(message)
return "omarchy-notification-send -u low " .. shell_quote(message)
end
@@ -224,7 +281,8 @@ function o.window(match, rules)
hl.window_rule(rules)
end
-- Opt a window in to Omarchy's standard active/inactive transparency.
-- Opt a window in to Omarchy's standard active/inactive transparency. The tag
-- lets Appearance > Window Transparency turn these windows opaque or glassy.
function o.transparent_window(match, opacity)
o.window(match, { opacity = opacity or "0.985 0.96" })
o.window(match, { opacity = opacity or "0.985 0.96", tag = "+transparent-window" })
end
Loaded 100 of 434 files, more files were not shown because too many files have changed in this diff. Show more