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
346 changed files with 7195 additions and 7251 deletions

No files matched your search

+11 -1
View File
@@ -104,7 +104,7 @@ Use these instead of raw shell commands:
- `omarchy-cmd-missing` / `omarchy-cmd-present` - check for 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-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-pkg-drop` - remove packages; use this instead of raw `pacman -R*`
- `omarchy-notification-send` - send desktop notifications; do not call `notify-send` directly - `omarchy-notification-send` - send desktop notifications; do not call `notify-send` directly
- `omarchy-hw-asus-rog` - detect ASUS ROG hardware (and similar `hw-*` commands) - `omarchy-hw-asus-rog` - detect ASUS ROG hardware (and similar `hw-*` commands)
@@ -113,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. 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 # Menu
- The menu definition lives in `default/omarchy/omarchy-menu.jsonc`; - The menu definition lives in `default/omarchy/omarchy-menu.jsonc`;
@@ -120,6 +124,12 @@ Exceptions are allowed for migration and package-helper scripts where the helper
- Do not add `aliases` to new menu entries. Aliases are reserved for - Do not add `aliases` to new menu entries. Aliases are reserved for
established alternate names users already type, kept for compatibility. established alternate names users already type, kept for compatibility.
# Language
- `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.
# Config Structure # Config Structure
- `config/` - default configs copied to `~/.config/` - `config/` - default configs copied to `~/.config/`
+15
View File
@@ -24,6 +24,10 @@ Run `omarchy-restart-shell` after making changes to QML files.
- Panel / overlay / menu plugins must expose `open(payloadJson)` and - Panel / overlay / menu plugins must expose `open(payloadJson)` and
`close()` lifecycle methods for `shell summon` and `shell hide`. `close()` lifecycle methods for `shell summon` and `shell hide`.
## Text
Wrap every user-visible string in `I18n.tr()` (`import qs.Commons`) and add its Chinese to `shell/Commons/I18nZh.js`; plain JS models that Node tests load stay English, and their text is wrapped where QML shows it.
## IPC ## IPC
- `bin/omarchy-shell` is the canonical IPC entry point. It forwards to - `bin/omarchy-shell` is the canonical IPC entry point. It forwards to
@@ -38,6 +42,17 @@ Run `omarchy-restart-shell` after making changes to QML files.
bar widgets register one target each — `omarchy.indicators`, bar widgets register one target each — `omarchy.indicators`,
`omarchy.system-update`, `omarchy.clock`. There is no `bar` target. `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 ## Editing widget files with glyphs
Widget files in `shell/plugins/bar/widgets/` contain Nerd Font glyphs as raw Widget files in `shell/plugins/bar/widgets/` contain Nerd Font glyphs as raw
+1
View File
@@ -2,6 +2,7 @@
Version=1.0 Version=1.0
Name=Basecamp Name=Basecamp
Exec=omarchy-launch-webapp https://launchpad.37signals.com Exec=omarchy-launch-webapp https://launchpad.37signals.com
StartupWMClass=chrome-launchpad.37signals.com__-Default
Terminal=false Terminal=false
Type=Application Type=Application
Icon=basecamp Icon=basecamp
+1
View File
@@ -2,6 +2,7 @@
Version=1.0 Version=1.0
Name=Discord Name=Discord
Exec=omarchy-launch-webapp https://discord.com/channels/@me Exec=omarchy-launch-webapp https://discord.com/channels/@me
StartupWMClass=chrome-discord.com__channels_@me-Default
Terminal=false Terminal=false
Type=Application Type=Application
Icon=omarchy-discord Icon=omarchy-discord
+1
View File
@@ -2,6 +2,7 @@
Version=1.0 Version=1.0
Name=Google Contacts Name=Google Contacts
Exec=omarchy-launch-webapp https://contacts.google.com/ Exec=omarchy-launch-webapp https://contacts.google.com/
StartupWMClass=chrome-contacts.google.com__-Default
Terminal=false Terminal=false
Type=Application Type=Application
Icon=google-contacts Icon=google-contacts
+1
View File
@@ -2,6 +2,7 @@
Version=1.0 Version=1.0
Name=Google Maps Name=Google Maps
Exec=omarchy-launch-webapp https://maps.google.com Exec=omarchy-launch-webapp https://maps.google.com
StartupWMClass=chrome-maps.google.com__-Default
Terminal=false Terminal=false
Type=Application Type=Application
Icon=google-maps Icon=google-maps
+1
View File
@@ -2,6 +2,7 @@
Version=1.0 Version=1.0
Name=Google Messages Name=Google Messages
Exec=omarchy-launch-webapp https://messages.google.com/web/conversations Exec=omarchy-launch-webapp https://messages.google.com/web/conversations
StartupWMClass=chrome-messages.google.com__web_conversations-Default
Terminal=false Terminal=false
Type=Application Type=Application
Icon=google-messages Icon=google-messages
+1
View File
@@ -2,6 +2,7 @@
Version=1.0 Version=1.0
Name=Google Photos Name=Google Photos
Exec=omarchy-launch-webapp https://photos.google.com/ Exec=omarchy-launch-webapp https://photos.google.com/
StartupWMClass=chrome-photos.google.com__-Default
Terminal=false Terminal=false
Type=Application Type=Application
Icon=google-photos Icon=google-photos
+1
View File
@@ -2,6 +2,7 @@
Version=1.0 Version=1.0
Name=WhatsApp Name=WhatsApp
Exec=omarchy-launch-webapp https://web.whatsapp.com/ Exec=omarchy-launch-webapp https://web.whatsapp.com/
StartupWMClass=chrome-web.whatsapp.com__-Default
Terminal=false Terminal=false
Type=Application Type=Application
Icon=whatsapp Icon=whatsapp
+1
View File
@@ -2,6 +2,7 @@
Version=1.0 Version=1.0
Name=X Name=X
Exec=omarchy-launch-webapp https://x.com/ Exec=omarchy-launch-webapp https://x.com/
StartupWMClass=chrome-x.com__-Default
Terminal=false Terminal=false
Type=Application Type=Application
Icon=x Icon=x
+1
View File
@@ -2,6 +2,7 @@
Version=1.0 Version=1.0
Name=YouTube Name=YouTube
Exec=omarchy-launch-webapp https://youtube.com/ Exec=omarchy-launch-webapp https://youtube.com/
StartupWMClass=chrome-youtube.com__-Default
Terminal=false Terminal=false
Type=Application Type=Application
Icon=youtube Icon=youtube
+1
View File
@@ -59,6 +59,7 @@ GROUP_DESCRIPTIONS[hw]="Hardware detection and controls"
GROUP_DESCRIPTIONS[hyprland]="Hyprland window, monitor, and toggle controls" GROUP_DESCRIPTIONS[hyprland]="Hyprland window, monitor, and toggle controls"
GROUP_DESCRIPTIONS[install]="Optional software installers" GROUP_DESCRIPTIONS[install]="Optional software installers"
GROUP_DESCRIPTIONS[installed]="Installed optional service checks" GROUP_DESCRIPTIONS[installed]="Installed optional service checks"
GROUP_DESCRIPTIONS[language]="System language"
GROUP_DESCRIPTIONS[launch]="Application launchers" GROUP_DESCRIPTIONS[launch]="Application launchers"
GROUP_DESCRIPTIONS[menu]="Omarchy menu commands" GROUP_DESCRIPTIONS[menu]="Omarchy menu commands"
GROUP_DESCRIPTIONS[migrate]="Migration runner" GROUP_DESCRIPTIONS[migrate]="Migration runner"
+16 -16
View File
@@ -85,7 +85,7 @@ finish() {
fail() { fail() {
if [[ $events == "true" ]]; then if [[ $events == "true" ]]; then
printf '@@omarchy error %s\n' "$1" printf '@@omarchy error %s\n' "$1"
omarchy-notification-send -g 󰚩 -u critical "Couldn't add the $name account" "$1" omarchy-notification-send -g 󰚩 -u critical "$(omarchy-text "Couldn't add the %1 account" "$name")" "$1"
else else
echo "$1" >&2 echo "$1" >&2
fi fi
@@ -113,9 +113,9 @@ interruptible() {
} }
if omarchy-cmd-missing "$provider"; then if omarchy-cmd-missing "$provider"; then
say "Installing the $name CLI…" say "$(omarchy-text "Installing the %1 CLI…" "$name")"
if [[ $events == "true" ]]; then if [[ $events == "true" ]]; then
mise use -g "$package" >/dev/null 2>&1 || fail "Installing the $name CLI didn't finish." mise use -g "$package" >/dev/null 2>&1 || fail "$(omarchy-text "Installing the %1 CLI didn't finish." "$name")"
else else
mise use -g "$package" || exit 1 mise use -g "$package" || exit 1
fi fi
@@ -150,26 +150,26 @@ if [[ -n $reauth ]]; then
else else
account_home=$(omarchy-agent-account-state homes "$provider" | awk -F'\t' -v id="$reauth" '$1 == id { print $2 }') account_home=$(omarchy-agent-account-state homes "$provider" | awk -F'\t' -v id="$reauth" '$1 == id { print $2 }')
fi fi
[[ -n $account_home ]] || fail "No $name account named $reauth." [[ -n $account_home ]] || fail "$(omarchy-text "No %1 account named %2." "$name" "$reauth")"
if [[ $account_home == "$HOME/.$provider" ]]; then if [[ $account_home == "$HOME/.$provider" ]]; then
say "Sign in to $name again in the browser window that opens." say "$(omarchy-text "Sign in to %1 again in the browser window that opens." "$name")"
login || fail "The $name sign-in didn't finish." login || fail "$(omarchy-text "The %1 sign-in didn't finish." "$name")"
else else
say "Sign in again as this account in the private window that opens." say "$(omarchy-text "Sign in again as this account in the private window that opens.")"
private_browser private_browser
interruptible env BROWSER="$browser" "$home_env=$account_home" "${login_command[@]}" || fail "The $name sign-in didn't finish." interruptible env BROWSER="$browser" "$home_env=$account_home" "${login_command[@]}" || fail "$(omarchy-text "The %1 sign-in didn't finish." "$name")"
fi fi
omarchy-agent-usage-update --force "$provider" >/dev/null 2>&1 & omarchy-agent-usage-update --force "$provider" >/dev/null 2>&1 &
finish "Signed in to $name again." finish "$(omarchy-text "Signed in to %1 again." "$name")"
exit 0 exit 0
fi fi
# The first subscription signs in where the CLI always looks, through your # The first subscription signs in where the CLI always looks, through your
# normal browser: nothing is signed in there yet that it could be mistaken for. # normal browser: nothing is signed in there yet that it could be mistaken for.
if ! primary_signed_in "$provider"; then if ! primary_signed_in "$provider"; then
say "Sign in to $name in the browser window that opens." say "$(omarchy-text "Sign in to %1 in the browser window that opens." "$name")"
if login && primary_signed_in "$provider"; then if login && primary_signed_in "$provider"; then
# The first agent signed in on a machine with none chosen becomes the # The first agent signed in on a machine with none chosen becomes the
# default, so `omarchy agent` and the panel's starter prompts have one to # default, so `omarchy agent` and the panel's starter prompts have one to
@@ -179,9 +179,9 @@ if ! primary_signed_in "$provider"; then
printf '%s\n' "$provider" >"$HOME/.config/omarchy/defaults/agent" printf '%s\n' "$provider" >"$HOME/.config/omarchy/defaults/agent"
fi fi
omarchy-agent-usage-update --force "$provider" >/dev/null 2>&1 & omarchy-agent-usage-update --force "$provider" >/dev/null 2>&1 &
finish "Signed in to $name." finish "$(omarchy-text "Signed in to %1." "$name")"
else else
fail "The $name sign-in didn't finish." fail "$(omarchy-text "The %1 sign-in didn't finish." "$name")"
fi fi
else else
if [[ -z $label && $events == "false" && -t 0 ]]; then if [[ -z $label && $events == "false" && -t 0 ]]; then
@@ -189,7 +189,7 @@ else
fi fi
if [[ $events == "true" ]]; then if [[ $events == "true" ]]; then
say "Sign in as the account you're adding in the private window that opens." say "$(omarchy-text "Sign in as the account you're adding in the private window that opens.")"
else else
cat <<EOF cat <<EOF
@@ -201,7 +201,7 @@ Don't log out of the $name CLI to do this: logging out revokes the saved sign-in
EOF EOF
fi fi
home=$(omarchy-agent-account-state begin "$provider") || fail "Couldn't set up a home for the new account." home=$(omarchy-agent-account-state begin "$provider") || fail "$(omarchy-text "Couldn't set up a home for the new account.")"
private_browser private_browser
interruptible env BROWSER="$browser" "$home_env=$home" "${login_command[@]}" interruptible env BROWSER="$browser" "$home_env=$home" "${login_command[@]}"
@@ -211,9 +211,9 @@ EOF
added=$(jq -r '.label + (if .email != "" then " (" + .email + ")" else "" end)' <<<"$account") added=$(jq -r '.label + (if .email != "" then " (" + .email + ")" else "" end)' <<<"$account")
omarchy-agent-usage-update --force "$provider" >/dev/null 2>&1 & omarchy-agent-usage-update --force "$provider" >/dev/null 2>&1 &
if [[ $events == "true" ]]; then if [[ $events == "true" ]]; then
finish "Added $added." finish "$(omarchy-text "Added %1." "$added")"
else else
finish "Added $added." finish "$(omarchy-text "Added %1." "$added")"
echo "Switch to it with: omarchy agent account use $provider $(jq -r .id <<<"$account")" echo "Switch to it with: omarchy agent account use $provider $(jq -r .id <<<"$account")"
fi fi
else else
+29 -22
View File
@@ -73,6 +73,11 @@ class AccountError(Exception):
pass pass
def text(english: str, *args: object) -> str:
"""English with %1.. filled, in the session's language, from the shell's table."""
return subprocess.run(["omarchy-text", english, *map(str, args)], capture_output=True, text=True, check=True).stdout.rstrip("\n")
# ------------------------------------------------------------------- paths # ------------------------------------------------------------------- paths
@@ -335,12 +340,12 @@ def begin(provider: str) -> Path:
def register(provider: str, label: str, pending: Path) -> dict[str, Any]: def register(provider: str, label: str, pending: Path) -> dict[str, Any]:
pending = pending.resolve() pending = pending.resolve()
if pending.parent != (accounts_root() / provider / ".pending").resolve(): if pending.parent != (accounts_root() / provider / ".pending").resolve():
raise AccountError(f"{pending} is not a pending {PROVIDERS[provider]['name']} login") raise AccountError(text("%1 is not a pending %2 login", pending, PROVIDERS[provider]['name']))
found = identity(provider, pending) found = identity(provider, pending)
if not found["accountId"]: if not found["accountId"]:
shutil.rmtree(pending, ignore_errors=True) shutil.rmtree(pending, ignore_errors=True)
raise AccountError("The login didn't finish, so no account was added.") raise AccountError(text("The login didn't finish, so no account was added."))
with Locked(provider) as registry: with Locked(provider) as registry:
# Each home is asked who it is now, not trusted from when it was added: a # Each home is asked who it is now, not trusted from when it was added: a
@@ -350,10 +355,10 @@ def register(provider: str, label: str, pending: Path) -> dict[str, Any]:
for account in registry["accounts"]: for account in registry["accounts"]:
if account.get("accountId") == found["accountId"]: if account.get("accountId") == found["accountId"]:
shutil.rmtree(pending, ignore_errors=True) shutil.rmtree(pending, ignore_errors=True)
raise AccountError( raise AccountError(text(
f"That's {account['label']} ({found['email'] or 'already added'}). " "That's %1 (%2). Try again, and sign in as the other account in the private window.",
"Try again, and sign in as the other account in the private window." account["label"], found["email"] or text("already added"),
) ))
label = label.strip() or found["email"].split("@")[0] or "Account" label = label.strip() or found["email"].split("@")[0] or "Account"
account_id = unique_id(provider, registry, label) account_id = unique_id(provider, registry, label)
@@ -395,11 +400,11 @@ def unique_id(provider: str, registry: dict[str, Any], label: str, keep: dict[st
def rename(provider: str, target: str, label: str) -> dict[str, Any]: def rename(provider: str, target: str, label: str) -> dict[str, Any]:
label = label.strip() label = label.strip()
if not label: if not label:
raise AccountError("Give the account a name") raise AccountError(text("Give the account a name"))
with Locked(provider) as registry: with Locked(provider) as registry:
account = find(registry, target) account = find(registry, target)
if not account: if not account:
raise AccountError(f"No {PROVIDERS[provider]['name']} account named {target}") raise AccountError(text("No %1 account named %2", PROVIDERS[provider]['name'], target))
new_id = unique_id(provider, registry, label, keep=account) new_id = unique_id(provider, registry, label, keep=account)
if not account.get("primary") and not account.get("home"): if not account.get("primary") and not account.get("home"):
account["home"] = str(account_home(provider, account)) account["home"] = str(account_home(provider, account))
@@ -428,7 +433,7 @@ def use(provider: str, target: str) -> tuple[dict[str, Any], dict[str, Any]]:
target = ids[(ids.index(previous["id"]) + 1) % len(ids)] target = ids[(ids.index(previous["id"]) + 1) % len(ids)]
account = find(registry, target) account = find(registry, target)
if not account: if not account:
raise AccountError(f"No {PROVIDERS[provider]['name']} account named {target}") raise AccountError(text("No %1 account named %2", PROVIDERS[provider]['name'], target))
registry["active"] = account["id"] registry["active"] = account["id"]
registry["alert"] = "" registry["alert"] = ""
if not account.get("primary"): if not account.get("primary"):
@@ -456,12 +461,12 @@ def remove(provider: str, target: str) -> dict[str, Any]:
with Locked(provider) as registry: with Locked(provider) as registry:
account = find(registry, target) account = find(registry, target)
if not account: if not account:
raise AccountError(f"No {PROVIDERS[provider]['name']} account named {target}") raise AccountError(text("No %1 account named %2", PROVIDERS[provider]['name'], target))
if account.get("primary"): if account.get("primary"):
raise AccountError(f"{account['label']} is the login in {primary_home(provider)}; it can't be removed here.") raise AccountError(text("%1 is the login in %2; it can't be removed here.", account["label"], primary_home(provider)))
home = account_home(provider, account) home = account_home(provider, account)
if home_in_use(provider, home): if home_in_use(provider, home):
raise AccountError(f"A running {PROVIDERS[provider]['name']} session uses {account['label']}. Quit it first, then remove the account.") raise AccountError(text("A running %1 session uses %2. Quit it first, then remove the account.", PROVIDERS[provider]['name'], account["label"]))
# Only the account's own directory goes. Its links point into the primary # Only the account's own directory goes. Its links point into the primary
# home, and rmtree removes a link without following it. # home, and rmtree removes a link without following it.
if home.resolve().parent == (accounts_root() / provider).resolve(): if home.resolve().parent == (accounts_root() / provider).resolve():
@@ -474,14 +479,14 @@ def remove(provider: str, target: str) -> dict[str, Any]:
def set_mode(provider: str, mode: str, threshold: str | None) -> dict[str, Any]: def set_mode(provider: str, mode: str, threshold: str | None) -> dict[str, Any]:
if mode not in SWITCH_MODES: if mode not in SWITCH_MODES:
raise AccountError("Switch mode is manual or auto") raise AccountError(text("Switch mode is manual or auto"))
with Locked(provider) as registry: with Locked(provider) as registry:
registry["switch"] = mode registry["switch"] = mode
if threshold is not None: if threshold is not None:
try: try:
registry["threshold"] = max(50, min(100, int(threshold))) registry["threshold"] = max(50, min(100, int(threshold)))
except ValueError: except ValueError:
raise AccountError("The threshold is a percentage between 50 and 100") raise AccountError(text("The threshold is a percentage between 50 and 100"))
registry["alert"] = "" registry["alert"] = ""
return dict(registry) return dict(registry)
@@ -556,10 +561,10 @@ def duration(delta: dt.timedelta) -> str:
hours, minutes = divmod(minutes, 60) hours, minutes = divmod(minutes, 60)
days, hours = divmod(hours, 24) days, hours = divmod(hours, 24)
if days: if days:
return f"{days}d {hours}h" return text("%1d %2h", days, hours)
if hours: if hours:
return f"{hours}h {minutes}m" return text("%1h %2m", hours, minutes)
return f"{minutes}m" return text("%1m", minutes)
# The whole switching policy, with no I/O: given the registry and the per # The whole switching policy, with no I/O: given the registry and the per
@@ -659,21 +664,23 @@ def autoswitch(provider: str, now: dt.datetime | None = None) -> bool:
threshold = registry["threshold"] threshold = registry["threshold"]
if decision["action"] == "exhausted": if decision["action"] == "exhausted":
soonest = decision["soonest"] soonest = decision["soonest"]
when = f" {soonest[1]['label']} resets in {duration(soonest[0] - now)}." if soonest else "" staying = text("Staying on %1.", source["label"])
notify(f"All {name} accounts are over {threshold}%", f"Staying on {source['label']}.{when}") if soonest:
staying = text("Staying on %1. %2 resets in %3.", source["label"], soonest[1]["label"], duration(soonest[0] - now))
notify(text("All %1 accounts are over %2%", name, threshold), staying)
return False return False
target = decision["to"] target = decision["to"]
headline = f"{source['label']} is at {round(decision['fromPeak'])}% of a {name} limit" headline = text("%1 is at %2% of a %3 limit", source["label"], round(decision["fromPeak"]), name)
if decision["action"] == "switch": if decision["action"] == "switch":
registry["active"] = target["id"] registry["active"] = target["id"]
if not target.get("primary"): if not target.get("primary"):
link_shared(provider, account_home(provider, target)) link_shared(provider, account_home(provider, target))
notify(headline, f"New {name} sessions now use {target['label']} ({round(decision['toPeak'])}%). Running sessions stay on {source['label']}.") notify(headline, text("New %1 sessions now use %2 (%3%). Running sessions stay on %4.", name, target["label"], round(decision["toPeak"]), source["label"]))
return True return True
notify( notify(
headline, headline,
f"{target['label']} is at {round(decision['toPeak'])}%. Click to switch new sessions to it.", text("%1 is at %2%. Click to switch new sessions to it.", target["label"], round(decision["toPeak"])),
["omarchy-agent-account-use", provider, target["id"]], ["omarchy-agent-account-use", provider, target["id"]],
) )
return False return False
+2 -2
View File
@@ -31,9 +31,9 @@ active=$(jq -r '.active.label + (if (.active.plan // "") != "" then " (" + .acti
if [[ $(jq -r .previous.id <<<"$result") == "$(jq -r .active.id <<<"$result")" ]]; then if [[ $(jq -r .previous.id <<<"$result") == "$(jq -r .active.id <<<"$result")" ]]; then
echo "New $name sessions already use $active." echo "New $name sessions already use $active."
else else
message="Running sessions stay on $previous." message=$(omarchy-text "Running sessions stay on %1." "$previous")
echo "New $name sessions now use $active. $message" echo "New $name sessions now use $active. $message"
omarchy-notification-send -g 󰚩 "New $name sessions now use $active" "$message" omarchy-notification-send -g 󰚩 "$(omarchy-text "New %1 sessions now use %2" "$name" "$active")" "$message"
# The usage record's own limits describe the active account. # The usage record's own limits describe the active account.
omarchy-agent-usage-update --limits-only "$1" >/dev/null 2>&1 & omarchy-agent-usage-update --limits-only "$1" >/dev/null 2>&1 &
fi fi
+11 -4
View File
@@ -22,6 +22,7 @@ import json
import os import os
import re import re
import sqlite3 import sqlite3
import subprocess
import sys import sys
import tempfile import tempfile
import time import time
@@ -32,6 +33,11 @@ from typing import Any
AGENT_ID = "claude" AGENT_ID = "claude"
AGENT_NAME = "Claude Code" AGENT_NAME = "Claude Code"
def text(english, *args):
"""English with %1.. filled, in the session's language, from the shell's table."""
return subprocess.run(["omarchy-text", english, *map(str, args)], capture_output=True, text=True, check=True).stdout.rstrip("\n")
AUTH_HELP = "Run `claude auth login` to restore authoritative usage." AUTH_HELP = "Run `claude auth login` to restore authoritative usage."
USAGE_ENDPOINT = "https://api.anthropic.com/api/oauth/usage" USAGE_ENDPOINT = "https://api.anthropic.com/api/oauth/usage"
PROBE_MIN_INTERVAL_SECONDS = 15 PROBE_MIN_INTERVAL_SECONDS = 15
@@ -989,11 +995,12 @@ def probe_limits(access_token: str) -> dict[str, Any]:
except urllib.error.HTTPError as error: except urllib.error.HTTPError as error:
retry_after = error.headers.get("retry-after", "") if error.headers else "" retry_after = error.headers.get("retry-after", "") if error.headers else ""
if error.code == 429: if error.code == 429:
help_text = "Anthropic's usage endpoint is rate limiting checks right now" + ( if retry_after:
f" (retry after {retry_after}s)" if retry_after else "" help_text = text("Anthropic's usage endpoint is rate limiting checks right now (retry after %1s). Local Claude Code stats are still shown.", retry_after)
) + ". Local Claude Code stats are still shown." else:
help_text = text("Anthropic's usage endpoint is rate limiting checks right now. Local Claude Code stats are still shown.")
else: else:
help_text = f"Anthropic's usage endpoint returned status {error.code}. Local Claude Code stats are still shown." help_text = text("Anthropic's usage endpoint returned status %1. Local Claude Code stats are still shown.", error.code)
return {"ok": False, "helpText": help_text} return {"ok": False, "helpText": help_text}
except Exception: except Exception:
# A transport failure reached no server at all — no route, no DNS. Any # A transport failure reached no server at all — no route, no DNS. Any
+2 -2
View File
@@ -6,8 +6,8 @@ wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle >/dev/null
if wpctl get-volume @DEFAULT_AUDIO_SOURCE@ | grep -q MUTED; then if wpctl get-volume @DEFAULT_AUDIO_SOURCE@ | grep -q MUTED; then
omarchy-brightness-keyboard-mute on omarchy-brightness-keyboard-mute on
omarchy-osd -i microphone-muted -m "Microphone muted" omarchy-osd -i microphone-muted -m "$(omarchy-text "Microphone muted")"
else else
omarchy-brightness-keyboard-mute off omarchy-brightness-keyboard-mute off
omarchy-osd -i microphone -m "Microphone on" omarchy-osd -i microphone -m "$(omarchy-text "Microphone on")"
fi fi
+1 -1
View File
@@ -13,7 +13,7 @@ sinks=$(timeout 2 pactl -f json list sinks |
sinks_count=$(jq 'length' <<<"$sinks") sinks_count=$(jq 'length' <<<"$sinks")
if (( sinks_count == 0 )); then if (( sinks_count == 0 )); then
omarchy-osd -m "No audio devices found" omarchy-osd -m "$(omarchy-text "No audio devices found")"
exit 1 exit 1
fi fi
+104 -10
View File
@@ -2,7 +2,7 @@
# omarchy:summary=Configure the bar and its widget layout # omarchy:summary=Configure the bar and its widget layout
# omarchy:group=bar # 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 # 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 set -euo pipefail
@@ -16,13 +16,19 @@ Usage: omarchy bar <command> [args...]
use <id> Use a bar option as the active bar use <id> Use a bar option as the active bar
reset Return to the built-in Omarchy bar reset Return to the built-in Omarchy bar
defaults Restore the default bar and service widgets defaults Restore the default bar and service widgets
position <top|bottom|left|right> Bar position position [top|bottom|left|right] Bar position; prints the current one
transparent <true|false|toggle> Bar transparency 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 put <id> [placement] Put a widget on the bar, leaving one
that is already there where it is that is already there where it is
move <id> [placement] Move a widget within or between sections move <id> [placement] Move a widget within or between sections
set <id> <key> <value> [--json] [placement] set <id> <key> <value> [--json] [placement]
Set a per-widget option Set a per-widget option
get <id> <key> Print a per-widget option
Placement: Placement:
--section <left|center|right> Target section --section <left|center|right> Target section
@@ -192,30 +198,92 @@ cmd_defaults() {
" \ " \
--slurpfile defaults "$DEFAULTS_FILE" \ --slurpfile defaults "$DEFAULTS_FILE" \
--argjson widgets "$optional_widgets" --argjson widgets "$optional_widgets"
sync_glass || fail "could not change the blur behind the bar"
echo "Restored the default Omarchy bar" echo "Restored the default Omarchy bar"
} }
cmd_position() { cmd_position() {
local position="${1:-}" if (( $# == 0 )); then
[[ -n $position ]] || fail "position is required" jq -r '.bar.position // "top"' "$(source_file)"
return
fi
local position="$1"
(( $# == 1 )) || fail "position takes a single value" (( $# == 1 )) || fail "position takes a single value"
[[ $position =~ ^(top|bottom|left|right)$ ]] || fail "position must be top, bottom, left, or right" [[ $position =~ ^(top|bottom|left|right)$ ]] || fail "position must be top, bottom, left, or right"
commit "$NORMALIZE | .bar.position = \$position" --arg position "$position" commit "$NORMALIZE | .bar.position = \$position" --arg position "$position"
echo "Bar position set to $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() { cmd_transparent() {
local transparent="${1:-}" local transparent="${1:-}"
[[ -n $transparent ]] || fail "transparent is required" [[ -n $transparent ]] || fail "transparent is required"
(( $# == 1 )) || fail "transparent takes a single value" (( $# == 1 )) || fail "transparent takes a single value"
[[ $transparent =~ ^(true|false|toggle)$ ]] || fail "transparent must be true, false, or toggle" [[ $transparent =~ ^(true|false|toggle)$ ]] || fail "transparent must be true, false, or toggle"
if [[ $transparent == "toggle" ]]; then if [[ $transparent == "toggle" ]]; then
commit "$NORMALIZE | .bar.transparent = (.bar.transparent != true)" [[ $(current_background) == "none" ]] && transparent="false" || transparent="true"
echo "Bar transparency toggled"
else
commit "$NORMALIZE | .bar.transparent = \$transparent" --argjson transparent "$transparent"
echo "Bar transparency set to $transparent"
fi 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() { bar_widget_default_section() {
@@ -363,6 +431,21 @@ cmd_set() {
echo "Set $key on $id" 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 # --------------------------------------------------------------------- dispatch
command="${1:-}" command="${1:-}"
@@ -385,6 +468,14 @@ case "$command" in
transparent) transparent)
cmd_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) put)
cmd_put "$@" cmd_put "$@"
;; ;;
@@ -394,6 +485,9 @@ case "$command" in
set) set)
cmd_set "$@" cmd_set "$@"
;; ;;
get)
cmd_get "$@"
;;
-h | --help | help | "") -h | --help | help | "")
usage usage
;; ;;
+1 -1
View File
@@ -13,5 +13,5 @@ fi
level=$1 level=$1
omarchy-notification-send -g 󱐋 -u critical "Time to recharge!" "Battery is down to ${level}%" -i battery-caution -t 30000 omarchy-notification-send -g 󱐋 -u critical "$(omarchy-text "Time to recharge!")" "$(omarchy-text "Battery is down to %1%" "$level")" -i battery-caution -t 30000
omarchy-hook battery-low "$level" omarchy-hook battery-low "$level"
+3 -3
View File
@@ -150,9 +150,9 @@ if [[ $charge_holding == "true" ]]; then
threshold_label="${threshold_end}%" threshold_label="${threshold_end}%"
fi fi
echo "Battery ${percentage}% · Holding at ${threshold_label} · ${power_rate}W / ${capacity}Wh" omarchy-text "Battery %1% · Holding at %2 · %3W / %4Wh" "$percentage" "$threshold_label" "$power_rate" "$capacity"
elif [[ $state == "charging" ]]; then elif [[ $state == "charging" ]]; then
echo "Battery ${percentage}% · ${time_remaining} to full ·  ${power_rate}W / ${capacity}Wh" omarchy-text "Battery %1% · %2 to full ·  %3W / %4Wh" "$percentage" "$time_remaining" "$power_rate" "$capacity"
else else
echo "Battery ${percentage}% · ${time_remaining} left ·  ${power_rate}W / ${capacity}Wh" omarchy-text "Battery %1% · %2 left ·  %3W / %4Wh" "$percentage" "$time_remaining" "$power_rate" "$capacity"
fi fi
+2 -2
View File
@@ -23,7 +23,7 @@ SELECTION=$(slurp 2>/dev/null)
RESULT=$(grim -g "$SELECTION" - | zbarimg -q --raw -Sdisable -Sqrcode.enable - 2>/dev/null) RESULT=$(grim -g "$SELECTION" - | zbarimg -q --raw -Sdisable -Sqrcode.enable - 2>/dev/null)
if [[ -z $RESULT ]]; then if [[ -z $RESULT ]]; then
omarchy-notification-send -g 󰐲 -u critical "No QR code found" "Select a region containing a QR code" omarchy-notification-send -g 󰐲 -u critical "$(omarchy-text "No QR code found")" "$(omarchy-text "Select a region containing a QR code")"
exit 1 exit 1
fi fi
@@ -33,4 +33,4 @@ fi
# the notification history, and an unmarked copy would be retained by clipboard # the notification history, and an unmarked copy would be retained by clipboard
# history. Pasting still works; only the recorded copy is given up. # history. Pasting still works; only the recorded copy is given up.
printf '%s' "$RESULT" | wl-copy --sensitive printf '%s' "$RESULT" | wl-copy --sensitive
omarchy-notification-send -g 󰐲 "QR code copied to clipboard" omarchy-notification-send -g 󰐲 "$(omarchy-text "QR code copied to clipboard")"
+7 -5
View File
@@ -86,7 +86,7 @@ esac
# --status only answers whether our recording runs, for the bar and the menu, so # --status only answers whether our recording runs, for the bar and the menu, so
# it needs no recordings directory and never notifies. # it needs no recordings directory and never notifies.
if [[ $STATUS != "true" && ! -d $OUTPUT_DIR ]]; then if [[ $STATUS != "true" && ! -d $OUTPUT_DIR ]]; then
omarchy-notification-send -u critical -t 3000 "Screen recording directory does not exist: $OUTPUT_DIR" omarchy-notification-send -u critical -t 3000 "$(omarchy-text "Screen recording directory does not exist: %1" "$OUTPUT_DIR")"
exit 1 exit 1
fi fi
@@ -97,7 +97,7 @@ start_webcam_overlay() {
if [[ -z $WEBCAM_DEVICE ]]; then if [[ -z $WEBCAM_DEVICE ]]; then
WEBCAM_DEVICE=$(omarchy-capture-webcam-list | sed -n '1s/[[:space:]].*//p') WEBCAM_DEVICE=$(omarchy-capture-webcam-list | sed -n '1s/[[:space:]].*//p')
if [[ -z $WEBCAM_DEVICE ]]; then if [[ -z $WEBCAM_DEVICE ]]; then
omarchy-notification-send -u critical -t 3000 "No webcam devices found" omarchy-notification-send -u critical -t 3000 "$(omarchy-text "No webcam devices found")"
return 1 return 1
fi fi
fi fi
@@ -235,7 +235,9 @@ wf_audio_device() {
} }
wf_audio_unavailable() { wf_audio_unavailable() {
omarchy-notification-send -u critical -t 5000 "Recording without $1 audio" "The $1 audio could not be added to this recording." >/dev/null 2>&1 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() { wf_audio_cleanup() {
@@ -424,7 +426,7 @@ stop_screenrecording() {
if omarchy-capture-screenrecording-process "${RECORDER[@]}"; then if omarchy-capture-screenrecording-process "${RECORDER[@]}"; then
omarchy-capture-screenrecording-process "${RECORDER[@]}" --signal KILL omarchy-capture-screenrecording-process "${RECORDER[@]}" --signal KILL
omarchy-notification-send -u critical -t 5000 "Screen recording error" "Recording process had to be force-killed. Video may be corrupted." 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 else
finalize_recording finalize_recording
local filename=$(cat "$RECORDING_FILE" 2>/dev/null) local filename=$(cat "$RECORDING_FILE" 2>/dev/null)
@@ -434,7 +436,7 @@ stop_screenrecording() {
# Generate a preview thumbnail from the first frame # Generate a preview thumbnail from the first frame
ffmpeg -y -i "$filename" -ss 00:00:00.1 -vframes 1 -q:v 2 "$preview" -loglevel quiet 2>/dev/null ffmpeg -y -i "$filename" -ss 00:00:00.1 -vframes 1 -q:v 2 "$preview" -loglevel quiet 2>/dev/null
omarchy-notification-send "Screen recording saved" "Open with Super + Alt + , (or click this)" \ omarchy-notification-send "$(omarchy-text "Screen recording saved")" "$(omarchy-text "Open with Super + Alt + , (or click this)")" \
-t 10000 --image "${preview:-$filename}" \ -t 10000 --image "${preview:-$filename}" \
--exec mpv -- "$filename" --exec mpv -- "$filename"
@@ -5,7 +5,7 @@
mapfile -t devices < <(omarchy-capture-webcam-list) mapfile -t devices < <(omarchy-capture-webcam-list)
if (( ${#devices[@]} == 0 )); then if (( ${#devices[@]} == 0 )); then
omarchy-notification-send "No webcam devices found" -u critical -t 3000 omarchy-notification-send "$(omarchy-text "No webcam devices found")" -u critical -t 3000
exit 1 exit 1
fi fi
+1 -1
View File
@@ -23,4 +23,4 @@ TEXT=$(grim -g "$SELECTION" - | tesseract stdin stdout --oem 1 --psm 6 -l "${OMA
[[ -z $TEXT ]] && exit 1 [[ -z $TEXT ]] && exit 1
printf "%s" "$TEXT" | wl-copy printf "%s" "$TEXT" | wl-copy
omarchy-notification-send -g 󰴑 "Copied text from selection to clipboard" omarchy-notification-send -g 󰴑 "$(omarchy-text "Copied text from selection to clipboard")"
+1 -1
View File
@@ -18,7 +18,7 @@ copy_url() {
[[ -n $url ]] || return 1 [[ -n $url ]] || return 1
printf '%s' "$url" | wl-copy --type text/plain printf '%s' "$url" | wl-copy --type text/plain
omarchy-notification-send -g 󰅍 "URL copied to clipboard" omarchy-notification-send -g 󰅍 "$(omarchy-text "URL copied to clipboard")"
} }
reply_copied() { reply_copied() {
+3 -3
View File
@@ -87,7 +87,7 @@ download_url() {
# Don't show anything until yt-dlp confirms there's actually a video to grab. # Don't show anything until yt-dlp confirms there's actually a video to grab.
if ! yt-dlp --no-playlist --simulate --quiet --no-warnings --no-exec --no-exec-before-download -- "$url" >/dev/null 2>&1; then if ! yt-dlp --no-playlist --simulate --quiet --no-warnings --no-exec --no-exec-before-download -- "$url" >/dev/null 2>&1; then
omarchy-notification-send -u critical -g 󰅖 "No video found for download" "$url" omarchy-notification-send -u critical -g 󰅖 "$(omarchy-text "No video found for download")" "$url"
exit 0 exit 0
fi fi
@@ -148,7 +148,7 @@ download_url() {
# toast would exit before the thumbnail cleanup below is ever scheduled. # toast would exit before the thumbnail cleanup below is ever scheduled.
# `--` keeps mpv from parsing a leading-dash filename as an option; the path # `--` keeps mpv from parsing a leading-dash filename as an option; the path
# is one discrete argument, so it never reaches a shell. # is one discrete argument, so it never reaches a shell.
omarchy-notification-send -g 󰄬 "Download complete" "$title" \ omarchy-notification-send -g 󰄬 "$(omarchy-text "Download complete")" "$title" \
-t 10000 --image "${preview:-$filepath}" \ -t 10000 --image "${preview:-$filepath}" \
--exec mpv -- "$filepath" || true --exec mpv -- "$filepath" || true
@@ -160,7 +160,7 @@ download_url() {
rm -f "$preview" rm -f "$preview"
) & ) &
else else
omarchy-notification-send -u critical -g 󰅖 "Download failed" "$url" omarchy-notification-send -u critical -g 󰅖 "$(omarchy-text "Download failed")" "$url"
fi fi
exit 0 exit 0
+2 -2
View File
@@ -40,8 +40,8 @@ announce() {
omarchy-notification-send \ omarchy-notification-send \
--urgency critical \ --urgency critical \
--glyph "$CRASH_GLYPH" \ --glyph "$CRASH_GLYPH" \
"Process crashed: $comm" \ "$(omarchy-text "Process crashed: %1" "$comm")" \
"Click to diagnose with AI" \ "$(omarchy-text "Click to diagnose with AI")" \
--exec omarchy-agent-crash "$pid" "$comm" "$exe" "$signal" --exec omarchy-agent-crash "$pid" "$comm" "$exe" "$signal"
} }
+1 -1
View File
@@ -48,4 +48,4 @@ fi
env -u BROWSER xdg-settings set default-web-browser "$desktop_id" || exit 1 env -u BROWSER xdg-settings set default-web-browser "$desktop_id" || exit 1
omarchy-notification-send -g $glyph "$name is now the default browser" omarchy-notification-send -g $glyph "$(omarchy-text "%1 is now the default browser" "$name")"
+1 -1
View File
@@ -59,4 +59,4 @@ fi
mkdir -p "$(dirname "$editor_file")" mkdir -p "$(dirname "$editor_file")"
printf '%s\n' "$editor" >"$editor_file" printf '%s\n' "$editor" >"$editor_file"
omarchy-notification-send -g $glyph "$name is now the default editor" omarchy-notification-send -g $glyph "$(omarchy-text "%1 is now the default editor" "$name")"
+1 -1
View File
@@ -48,4 +48,4 @@ cat >~/.config/xdg-terminals.list <<EOF
$desktop_id $desktop_id
EOF EOF
omarchy-notification-send -g $glyph "$name is now the default terminal" omarchy-notification-send -g $glyph "$(omarchy-text "%1 is now the default terminal" "$name")"
+1 -1
View File
@@ -169,7 +169,7 @@ set_terminal_size() {
[[ $prev_id =~ ^[0-9]+$ ]] && replace=(-r "$prev_id") [[ $prev_id =~ ^[0-9]+$ ]] && replace=(-r "$prev_id")
local new_id local new_id
new_id="$(omarchy-notification-send \ new_id="$(omarchy-notification-send \
"Restart Foot to apply the new terminal font size" \ "$(omarchy-text "Restart Foot to apply the new terminal font size")" \
"${replace[@]}" -p 2>/dev/null)" || true "${replace[@]}" -p 2>/dev/null)" || true
[[ $new_id =~ ^[0-9]+$ ]] && printf '%s\n' "$new_id" >"$id_file" [[ $new_id =~ ^[0-9]+$ ]] && printf '%s\n' "$new_id" >"$id_file"
fi fi
+2 -2
View File
@@ -74,11 +74,11 @@ XML
omarchy-restart-shell omarchy-restart-shell
if pgrep -x ghostty; then if pgrep -x ghostty; then
omarchy-notification-send -g "You must restart Ghostty to see font change" omarchy-notification-send -g "$(omarchy-text "You must restart Ghostty to see font change")"
fi fi
if pgrep -x foot; then if pgrep -x foot; then
omarchy-notification-send -g "You must restart Foot to see font change" omarchy-notification-send -g "$(omarchy-text "You must restart Foot to see font change")"
fi fi
omarchy-hook font-set "$font_name" omarchy-hook font-set "$font_name"
+2 -2
View File
@@ -10,7 +10,7 @@ if (( $# == 0 )); then
mapfile -t cores < <(omarchy-games-retro-cores) mapfile -t cores < <(omarchy-games-retro-cores)
if (( ${#cores[@]} == 0 )); then if (( ${#cores[@]} == 0 )); then
omarchy-notification-send -g 󰯉 "No RetroArch cores found" "/usr/lib/libretro" omarchy-notification-send -g 󰯉 "$(omarchy-text "No RetroArch cores found")" "/usr/lib/libretro"
exit 1 exit 1
fi fi
@@ -69,4 +69,4 @@ EOF
chmod +x "$desktop_file" chmod +x "$desktop_file"
update-desktop-database "$desktop_dir" &>/dev/null || true update-desktop-database "$desktop_dir" &>/dev/null || true
omarchy-notification-send -g 󰯉 "$game_name installed" "Start it with Super + Space" omarchy-notification-send -g 󰯉 "$(omarchy-text "%1 installed" "$game_name")" "$(omarchy-text "Start it with Super + Space")"
+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"
+5 -5
View File
@@ -16,7 +16,7 @@ wake() {
on() { on() {
if omarchy-hyprland-toggle-enabled $TOGGLE; then if omarchy-hyprland-toggle-enabled $TOGGLE; then
omarchy-hyprland-toggle $TOGGLE off omarchy-hyprland-toggle $TOGGLE off
omarchy-notification-send -g 󰍹 "Laptop display enabled" omarchy-notification-send -g 󰍹 "$(omarchy-text "Laptop display enabled")"
fi fi
wake wake
@@ -24,25 +24,25 @@ on() {
off() { off() {
if [[ -z $INTERNAL ]]; then if [[ -z $INTERNAL ]]; then
omarchy-notification-send -g 󰍹 "No laptop display found" omarchy-notification-send -g 󰍹 "$(omarchy-text "No laptop display found")"
exit 1 exit 1
fi fi
# The name is written into generated Lua below, so only a plain connector # The name is written into generated Lua below, so only a plain connector
# name may pass; anything else could execute on the next reload. # name may pass; anything else could execute on the next reload.
if [[ ! $INTERNAL =~ ^[A-Za-z0-9._-]+$ ]]; then if [[ ! $INTERNAL =~ ^[A-Za-z0-9._-]+$ ]]; then
omarchy-notification-send -g 󰍹 "Refusing unsafe monitor name" omarchy-notification-send -g 󰍹 "$(omarchy-text "Refusing unsafe monitor name")"
exit 1 exit 1
fi fi
if ! omarchy-hyprland-monitor-external-active; then if ! omarchy-hyprland-monitor-external-active; then
omarchy-notification-send -g 󰍹 "Can't disable the only active display" omarchy-notification-send -g 󰍹 "$(omarchy-text "Can't disable the only active display")"
exit 1 exit 1
fi fi
if omarchy-hyprland-toggle-disabled $TOGGLE && omarchy-hyprland-toggle-disabled $MIRROR_TOGGLE; then if omarchy-hyprland-toggle-disabled $TOGGLE && omarchy-hyprland-toggle-disabled $MIRROR_TOGGLE; then
printf 'hl.monitor({ output = "%s", disabled = true })\n' "$INTERNAL" >"$TOGGLE_FLAG" printf 'hl.monitor({ output = "%s", disabled = true })\n' "$INTERNAL" >"$TOGGLE_FLAG"
omarchy-notification-send -g 󰍹 "Laptop display disabled" omarchy-notification-send -g 󰍹 "$(omarchy-text "Laptop display disabled")"
hyprctl reload hyprctl reload
fi fi
} }
+5 -5
View File
@@ -13,12 +13,12 @@ EXTERNAL=$(hyprctl monitors -j | jq -r '.[] | select(.name | test("^(eDP|LVDS|DS
on() { on() {
if [[ -z $EXTERNAL ]]; then if [[ -z $EXTERNAL ]]; then
omarchy-notification-send -g 󰍹 "No external monitors found for mirror" omarchy-notification-send -g 󰍹 "$(omarchy-text "No external monitors found for mirror")"
exit 1 exit 1
fi fi
if [[ -z $INTERNAL ]]; then if [[ -z $INTERNAL ]]; then
omarchy-notification-send -g 󰍹 "No laptop monitor found to mirror" omarchy-notification-send -g 󰍹 "$(omarchy-text "No laptop monitor found to mirror")"
exit 1 exit 1
fi fi
@@ -26,7 +26,7 @@ on() {
# names may pass; a user-created headless output can carry any name. # names may pass; a user-created headless output can carry any name.
for output in "$INTERNAL" "$EXTERNAL"; do for output in "$INTERNAL" "$EXTERNAL"; do
if [[ ! $output =~ ^[A-Za-z0-9._-]+$ ]]; then if [[ ! $output =~ ^[A-Za-z0-9._-]+$ ]]; then
omarchy-notification-send -g 󰍹 "Refusing unsafe monitor name" omarchy-notification-send -g 󰍹 "$(omarchy-text "Refusing unsafe monitor name")"
exit 1 exit 1
fi fi
done done
@@ -35,7 +35,7 @@ on() {
if omarchy-hyprland-toggle-disabled $TOGGLE; then if omarchy-hyprland-toggle-disabled $TOGGLE; then
printf 'hl.monitor({ output = "%s", mode = "preferred", position = "auto", scale = 1, mirror = "%s" })\n' "$EXTERNAL" "$INTERNAL" >"$TOGGLE_FLAG" printf 'hl.monitor({ output = "%s", mode = "preferred", position = "auto", scale = 1, mirror = "%s" })\n' "$EXTERNAL" "$INTERNAL" >"$TOGGLE_FLAG"
omarchy-notification-send -g 󰍹 "Mirroring enabled ($EXTERNAL)" omarchy-notification-send -g 󰍹 "$(omarchy-text "Mirroring enabled (%1)" "$EXTERNAL")"
hyprctl reload hyprctl reload
fi fi
} }
@@ -43,7 +43,7 @@ on() {
off() { off() {
if omarchy-hyprland-toggle-enabled $TOGGLE; then if omarchy-hyprland-toggle-enabled $TOGGLE; then
omarchy-hyprland-toggle $TOGGLE off omarchy-hyprland-toggle $TOGGLE off
omarchy-notification-send -g 󰍹 "Extended mode restored" omarchy-notification-send -g 󰍹 "$(omarchy-text "Extended mode restored")"
fi fi
} }
+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
@@ -3,6 +3,6 @@
# omarchy:summary=Toggle single-window square aspect ratio. # omarchy:summary=Toggle single-window square aspect ratio.
case $(omarchy-hyprland-toggle single-window-aspect-ratio) in case $(omarchy-hyprland-toggle single-window-aspect-ratio) in
on) omarchy-notification-send -g  "Enable single-window square aspect ratio" ;; on) omarchy-notification-send -g  "$(omarchy-text "Enable single-window square aspect ratio")" ;;
off) omarchy-notification-send -g  "Disable single-window square aspect ratio" ;; off) omarchy-notification-send -g  "$(omarchy-text "Disable single-window square aspect ratio")" ;;
esac esac
+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"
+2 -2
View File
@@ -40,14 +40,14 @@ notify_missing_width() {
local key="$1" local key="$1"
local workspace="$2" local workspace="$2"
omarchy-notification-send -g  "No saved width found for $key on workspace $workspace" "Use Super + Alt + Home to save one for this workspace." omarchy-notification-send -g  "$(omarchy-text "No saved width found for %1 on workspace %2" "$key" "$workspace")" "Use Super + Alt + Home to save one for this workspace."
} }
notify_saved_width() { notify_saved_width() {
local key="$1" local key="$1"
local workspace="$2" local workspace="$2"
omarchy-notification-send -g  "Saved width for $key on workspace $workspace" "Restore using Super + Home on this workspace." omarchy-notification-send -g  "$(omarchy-text "Saved width for %1 on workspace %2" "$key" "$workspace")" "Restore using Super + Home on this workspace."
} }
hypr_dispatch() { hypr_dispatch() {
+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 #!/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=$(hyprctl activeworkspace -j | jq -r '.id')
[[ $ACTIVE_WORKSPACE =~ ^-?[0-9]+$ ]] || exit 1 # Named workspaces report a negative id, and Hyprland reads a leading "-" as a
CURRENT_LAYOUT=$(hyprctl activeworkspace -j | jq -r '.tiledLayout') # selector relative to the current workspace: targeting one lands on workspace 1
LAYOUTS_DIR="$HOME/.local/state/omarchy/workspace-layouts" # and reconfigures that instead.
LAYOUT_FILE="$LAYOUTS_DIR/$ACTIVE_WORKSPACE.lua" [[ $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 CURRENT_MODE=$(omarchy-hyprland-workspace-layout-current)
dwindle) NEW_LAYOUT=scrolling ;;
*) NEW_LAYOUT=dwindle ;;
esac
mkdir -p "$LAYOUTS_DIR" if [[ $DIRECTION == "back" ]]; then
printf 'hl.workspace_rule({ workspace = "%s", layout = "%s" })\n' "$ACTIVE_WORKSPACE" "$NEW_LAYOUT" >"$LAYOUT_FILE" 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 || \ APPLY="o.workspace_mode({ workspace = \"$ACTIVE_WORKSPACE\", mode = \"$NEW_MODE\" })"
hyprctl keyword workspace "$ACTIVE_WORKSPACE, layout:$NEW_LAYOUT"
omarchy-notification-send -g 󱂬 "Workspace layout set to $NEW_LAYOUT" # 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
+3
View File
@@ -92,6 +92,9 @@ phoenix)
;; ;;
rust) rust)
echo -e "Installing Rust...\n" 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 bash -c "$(curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs)" -- -y
;; ;;
java) java)
+1 -1
View File
@@ -23,5 +23,5 @@ superwhisper shortcuts set toggle "Alt+Space" >/dev/null
superwhisper shortcuts set hold none >/dev/null superwhisper shortcuts set hold none >/dev/null
omarchy-dictation-use superwhisper omarchy-dictation-use superwhisper
hyprctl reload >/dev/null hyprctl reload >/dev/null
omarchy-notification-send -g  "Superwhisper Dictation Ready" "Hold Right Alt or F9 to dictate (or toggle with Super + Ctrl + X)." -t 10000 omarchy-notification-send -g  "$(omarchy-text "Superwhisper Dictation Ready")" "$(omarchy-text "Hold Right Alt or F9 to dictate (or toggle with Super + Ctrl + X).")" -t 10000
superwhisper settings superwhisper settings
+1 -1
View File
@@ -33,4 +33,4 @@ systemctl --user enable --now voxtype.service
omarchy-dictation-use voxtype omarchy-dictation-use voxtype
hyprctl reload >/dev/null hyprctl reload >/dev/null
omarchy-notification-send -g  "Voxtype Dictation Ready" "Hold Right Alt or F9 to dictate (or toggle with Super + Ctrl + X)." -t 10000 omarchy-notification-send -g  "$(omarchy-text "Voxtype Dictation Ready")" "$(omarchy-text "Hold Right Alt or F9 to dictate (or toggle with Super + Ctrl + X).")" -t 10000
+2 -4
View File
@@ -15,17 +15,15 @@ if gum confirm "Are you sure you want to restore all preinstalled web apps, TUI
aether \ aether \
cliamp \ cliamp \
libreoffice-fresh \ libreoffice-fresh \
xournalpp \
pinta \ pinta \
obsidian \ obsidian \
obs-studio \
kdenlive \
moonlight-qt \ moonlight-qt \
omacut \ omacut \
monologue \ monologue \
omacalc \ omacalc \
omawrite \ omawrite \
hype; then hype \
signed; then
echo -e "\nPreinstalls are still marked as removed. Fix the errors above and try again." echo -e "\nPreinstalls are still marked as removed. Fix the errors above and try again."
exit 1 exit 1
fi fi
+46
View File
@@ -0,0 +1,46 @@
#!/bin/bash
# omarchy:summary=Show or switch the system language between English and Simplified Chinese
# omarchy:args=[en|zh]
# omarchy:examples=omarchy language | omarchy language zh
# omarchy:requires-sudo=true
set -euo pipefail
locale_conf=/etc/locale.conf
if (($# == 0)); then
lang=$(sed -n 's/^LANG=//p' "$locale_conf")
# locale.conf may quote the value.
lang=${lang#[\"\']}
printf '%s\n' "${lang%[\"\']}"
exit 0
fi
case ${1:-} in
en) lang=en_US.UTF-8 ;;
zh) lang=zh_CN.UTF-8 ;;
*)
echo "Usage: omarchy-language [en|zh]" >&2
exit 1
;;
esac
sudo sed -i "s/^#\(${lang/./\\.} UTF-8\)/\1/" /etc/locale.gen
sudo locale-gen
if grep -q '^LANG=' "$locale_conf"; then
sudo sed -i "s/^LANG=.*/LANG=$lang/" "$locale_conf"
else
sudo sed -i '$a\' "$locale_conf"
echo "LANG=$lang" | sudo tee -a "$locale_conf" >/dev/null
fi
# The language reaches only a new session, so offer one in the new language.
prompt=$(LANG=$lang omarchy-text "Log out now to switch the language?")
log_out=$(LANG=$lang omarchy-text "Log out now")
later=$(LANG=$lang omarchy-text "Later")
if [[ -t 0 && -t 1 ]] && gum confirm --affirmative "$log_out" --negative "$later" "$prompt" 2>&1; then
omarchy-system-logout
else
LANG=$lang omarchy-text "Log back in to switch the language."
fi
+1 -1
View File
@@ -11,5 +11,5 @@ if [[ -z $path ]]; then
exit 1 exit 1
fi fi
omarchy-notification-send -u low "Editing config file" "$path" omarchy-notification-send -u low "$(omarchy-text "Editing config file")" "$path"
exec omarchy-launch-editor "$path" exec omarchy-launch-editor "$path"
+1 -1
View File
@@ -16,7 +16,7 @@ terminal=$(xdg-terminal-exec --print-id)
case $terminal in case $terminal in
*Alacritty* | *ghostty* | *foot* | *kitty*) ;; *Alacritty* | *ghostty* | *foot* | *kitty*) ;;
*) *)
omarchy-notification-send -g ✋ "Screensaver only runs in Alacritty, Foot, Ghostty, or Kitty" omarchy-notification-send -g ✋ "$(omarchy-text "Screensaver only runs in Alacritty, Foot, Ghostty, or Kitty")"
exit 1 exit 1
;; ;;
esac esac
+1 -1
View File
@@ -28,7 +28,7 @@ rows=$(jq -r --arg icon "$PLUGIN_ICON" \
". as \$plugins ". as \$plugins
| .[] | select($filter) | .[] | select($filter)
| \$icon + \"\\t\" + .name + \"\\t\" + .id" <<<"$plugins") | \$icon + \"\\t\" + .name + \"\\t\" + .id" <<<"$plugins")
[[ -n $rows ]] || { omarchy-notification-send "No plugin to ${1}"; exit 0; } [[ -n $rows ]] || { omarchy-notification-send "$(omarchy-text "No plugin to $1")"; exit 0; }
selection=$(omarchy-menu-select "${1^} plugin" <<<"$rows") || exit 0 selection=$(omarchy-menu-select "${1^} plugin" <<<"$rows") || exit 0
[[ -n $selection ]] || exit 0 [[ -n $selection ]] || exit 0
+1 -1
View File
@@ -33,7 +33,7 @@ else
picked=$(omarchy-file-select "${select_args[@]}") || status=$? picked=$(omarchy-file-select "${select_args[@]}") || status=$?
if ((${status:-0} > 1)); then if ((${status:-0} > 1)); then
omarchy-notification-send -g "" -u critical "Could not share" "The file chooser did not open" omarchy-notification-send -g "" -u critical "$(omarchy-text "Could not share")" "$(omarchy-text "The file chooser did not open")"
exit 1 exit 1
fi fi
+1 -1
View File
@@ -9,4 +9,4 @@ set -e
timezone=$(timedatectl list-timezones | omarchy-menu-select "Set timezone" -- --width 520 --maxheight 520) || exit 1 timezone=$(timedatectl list-timezones | omarchy-menu-select "Set timezone" -- --width 520 --maxheight 520) || exit 1
sudo timedatectl set-timezone "$timezone" sudo timedatectl set-timezone "$timezone"
omarchy-shell -q omarchy.clock refresh omarchy-shell -q omarchy.clock refresh
omarchy-notification-send "Timezone is now set to $timezone" omarchy-notification-send "$(omarchy-text "Timezone is now set to %1" "$timezone")"
+3 -3
View File
@@ -21,9 +21,9 @@ pending_migrations=$(omarchy-migrate --pending 2>/dev/null) || exit 0
pending_count=$(printf '%s\n' "$pending_migrations" | sed '/^[[:space:]]*$/d' | wc -l) pending_count=$(printf '%s\n' "$pending_migrations" | sed '/^[[:space:]]*$/d' | wc -l)
if (( pending_count == 1 )); then if (( pending_count == 1 )); then
message="Click to run 1 pending migration." message=$(omarchy-text "Click to run 1 pending migration.")
else else
message="Click to run $pending_count pending migrations." message=$(omarchy-text "Click to run %1 pending migrations." "$pending_count")
fi fi
# This runs from omarchy-migrate-notify.service after graphical-session.target, # This runs from omarchy-migrate-notify.service after graphical-session.target,
@@ -40,7 +40,7 @@ fi
# The shell keeps the click command with the toast, so this oneshot can hand the # The shell keeps the click command with the toast, so this oneshot can hand the
# invitation over and exit instead of staying activated until it is answered. # invitation over and exit instead of staying activated until it is answered.
omarchy-notification-send -u critical -g  "Pending Omarchy Migrations" "$message" \ omarchy-notification-send -u critical -g  "$(omarchy-text "Pending Omarchy Migrations")" "$message" \
--exec omarchy-launch-floating-terminal-with-presentation omarchy-migrate && exit 0 --exec omarchy-launch-floating-terminal-with-presentation omarchy-migrate && exit 0
# Reached when the notification could not be handed off, so fall back to telling # Reached when the notification could not be handed off, so fall back to telling
+1 -1
View File
@@ -2,4 +2,4 @@
# omarchy:summary=Show the current time and date notification # omarchy:summary=Show the current time and date notification
omarchy-notification-send -g  -u low "$(date +"%A %H:%M · %d %B %Y · Week %V")" omarchy-notification-send -g  -u low "$(date +"%A %H:%M · %d %B %Y") · $(omarchy-text "Week %1" "$(date +%V)")"
+2 -2
View File
@@ -159,8 +159,8 @@ done
omarchy-plugin-enable "$new_id" >/dev/null omarchy-plugin-enable "$new_id" >/dev/null
clone_complete=1 clone_complete=1
omarchy-notification-send -g 󰐱 \ omarchy-notification-send -g 󰐱 \
"Editing Cloned Plugin" \ "$(omarchy-text "Editing Cloned Plugin")" \
"Original plugin has been replace by clone." "$(omarchy-text "Original plugin has been replace by clone.")"
echo "Cloned $source_id to $target_dir and switched to $new_id" echo "Cloned $source_id to $target_dir and switched to $new_id"
if (( edit_clone )); then if (( edit_clone )); then
# Word-splitting is deliberate: EDITOR may carry flags. # Word-splitting is deliberate: EDITOR may carry flags.
+1
View File
@@ -56,6 +56,7 @@ if [[ -z ${profile:-} ]] || ! profile_available "$profile"; then
fi fi
powerprofilesctl set "$profile" || exit powerprofilesctl set "$profile" || exit
omarchy-shell -q battery refreshPowerProfile >/dev/null 2>&1 || true
if [[ -n $requested_profile ]]; then if [[ -n $requested_profile ]]; then
mkdir -p "$state_dir" mkdir -p "$state_dir"
+2 -11
View File
@@ -187,11 +187,11 @@ tips=(
"Super + Space opens the Omarchy menu for apps, settings, and more" "Super + Space opens the Omarchy menu for apps, settings, and more"
"Super + K shows all the key bindings" "Super + K shows all the key bindings"
"Super is the Windows or command key on your keyboard" "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" "Share files with phones and laptops using LocalSend"
"Turn any website into an app with Install > Web App in the menu" "Turn any website into an app with Install > Web App in the menu"
"Super + Return opens a terminal, Super + Shift + Return the browser" "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" "Super + Ctrl + Print grabs text off the screen with OCR"
"Print takes a screenshot, Alt + Print records the screen" "Print takes a screenshot, Alt + Print records the screen"
"Set a reminder with Super + Ctrl + R" "Set a reminder with Super + Ctrl + R"
@@ -1174,15 +1174,6 @@ run_provisioning() {
return 1 return 1
fi fi
if omarchy-pkg-present usbguard; then
log_step "enrolling the owner's USB devices"
source "$OMARCHY_PATH/install/helpers/usb-authorization.sh"
usb_authorization_provision_owner "$username"
fi
log_step "enrolling the owner's Thunderbolt accessories"
/usr/bin/omarchy-thunderbolt-authorization-admin owner
log_step "cleaning up provisioning state" log_step "cleaning up provisioning state"
cleanup_oem_state cleanup_oem_state
log_step "first-boot setup complete" log_step "first-boot setup complete"
+15 -14
View File
@@ -57,16 +57,16 @@ show_reminders() {
[[ -f $reminder_dir/${timer%.timer}.message ]] && reminder_message=$(<"$reminder_dir/${timer%.timer}.message") [[ -f $reminder_dir/${timer%.timer}.message ]] && reminder_message=$(<"$reminder_dir/${timer%.timer}.message")
if [[ -n $reminder_message ]]; then if [[ -n $reminder_message ]]; then
body+="$reminder_message in $(format_remaining $remaining) ($(date -d "@$next" +%-H:%M))"$'\n' body+="$(omarchy-text "%1 in %2 (%3)" "$reminder_message" "$(format_remaining $remaining)" "$(date -d "@$next" +%-H:%M)")"$'\n'
else else
body+="${reminder_minutes}-min reminder in $(format_remaining $remaining) ($(date -d "@$next" +%-H:%M))"$'\n' body+="$(omarchy-text "%1-min reminder in %2 (%3)" "$reminder_minutes" "$(format_remaining $remaining)" "$(date -d "@$next" +%-H:%M)")"$'\n'
fi fi
done < <(active_reminder_timers "$now") done < <(active_reminder_timers "$now")
if [[ -z $body ]]; then if [[ -z $body ]]; then
omarchy-notification-send -g 󰢌 "Upcoming reminders" "No outstanding reminders" omarchy-notification-send -g 󰢌 "$(omarchy-text "Upcoming reminders")" "$(omarchy-text "No outstanding reminders")"
else else
omarchy-notification-send -g 󰢌 "Upcoming reminders" "${body%$'\n'}" omarchy-notification-send -g 󰢌 "$(omarchy-text "Upcoming reminders")" "${body%$'\n'}"
fi fi
} }
@@ -75,7 +75,8 @@ show_json() {
local reminder_dir="${XDG_RUNTIME_DIR:-/tmp}/omarchy-reminders" local reminder_dir="${XDG_RUNTIME_DIR:-/tmp}/omarchy-reminders"
local now=$(date +%s) local now=$(date +%s)
local count=0 local count=0
local tooltip="Set Reminder" local tooltip
tooltip=$(omarchy-text "Set Reminder")
while IFS=$'\t' read -r timer next; do while IFS=$'\t' read -r timer next; do
count=$((count + 1)) count=$((count + 1))
@@ -91,7 +92,7 @@ show_json() {
if [[ -n $reminder_message ]]; then if [[ -n $reminder_message ]]; then
label=$reminder_message label=$reminder_message
else else
label="${reminder_minutes}-min reminder" label=$(omarchy-text "%1-min reminder" "$reminder_minutes")
fi fi
item_json=$(jq -cn \ item_json=$(jq -cn \
@@ -109,9 +110,9 @@ show_json() {
done < <(active_reminder_timers "$now") done < <(active_reminder_timers "$now")
if ((count == 1)); then if ((count == 1)); then
tooltip="1 reminder" tooltip=$(omarchy-text "1 reminder")
elif ((count > 1)); then elif ((count > 1)); then
tooltip="$count reminders" tooltip=$(omarchy-text "%1 reminders" "$count")
fi fi
jq -cn --argjson count "$count" --arg tooltip "$tooltip" --argjson reminders "$reminders_json" '{count:$count,active:($count > 0),tooltip:$tooltip,reminders:$reminders}' jq -cn --argjson count "$count" --arg tooltip "$tooltip" --argjson reminders "$reminders_json" '{count:$count,active:($count > 0),tooltip:$tooltip,reminders:$reminders}'
@@ -129,7 +130,7 @@ clear_reminders() {
rm -f "$reminder_dir"/omarchy-reminder-*.message 2>/dev/null || true rm -f "$reminder_dir"/omarchy-reminder-*.message 2>/dev/null || true
refresh_indicator refresh_indicator
omarchy-notification-send -g 󰢌 "All reminders have been cleared" omarchy-notification-send -g 󰢌 "$(omarchy-text "All reminders have been cleared")"
} }
usage() { usage() {
@@ -176,7 +177,7 @@ if [[ -z $minutes ]] || [[ ! $minutes =~ ^[0-9]+$ ]] || ((minutes == 0)); then
fi fi
if [[ -z $message ]]; then if [[ -z $message ]]; then
message="Your ${minutes} minutes are up" message=$(omarchy-text "Your %1 minutes are up" "$minutes")
fi fi
set_at=$(date +%s) set_at=$(date +%s)
@@ -184,18 +185,18 @@ remind_at=$(date -d "+${minutes} minutes" +%H:%M)
unit="omarchy-reminder-${minutes}m-$set_at" unit="omarchy-reminder-${minutes}m-$set_at"
reminder_dir="${XDG_RUNTIME_DIR:-/tmp}/omarchy-reminders" reminder_dir="${XDG_RUNTIME_DIR:-/tmp}/omarchy-reminders"
message_file="$reminder_dir/$unit.message" message_file="$reminder_dir/$unit.message"
confirmation="You'll be reminded at $remind_at" confirmation=$(omarchy-text "You'll be reminded at %1" "$remind_at")
confirmation_title="Reminder set for ${minutes} minutes" confirmation_title=$(omarchy-text "Reminder set for %1 minutes" "$minutes")
mkdir -p "$reminder_dir" mkdir -p "$reminder_dir"
if [[ -n $custom_message ]]; then if [[ -n $custom_message ]]; then
printf "%s" "$custom_message" >"$message_file" printf "%s" "$custom_message" >"$message_file"
confirmation_title="$custom_message in ${minutes} minutes" confirmation_title=$(omarchy-text "%1 in %2 minutes" "$custom_message" "$minutes")
fi fi
systemd-run --user --quiet --collect --on-active="${minutes}m" --unit="$unit" \ systemd-run --user --quiet --collect --on-active="${minutes}m" --unit="$unit" \
bash -c 'omarchy-notification-send -g 󰢌 "Reminder" "$1"; rm -f "$2"; omarchy-shell -q omarchy.indicators refresh >/dev/null 2>&1 || true' bash "$message" "$message_file" bash -c 'omarchy-notification-send -g 󰢌 "$(omarchy-text "Reminder")" "$1"; rm -f "$2"; omarchy-shell -q omarchy.indicators refresh >/dev/null 2>&1 || true' bash "$message" "$message_file"
refresh_indicator refresh_indicator
omarchy-notification-send -g 󰢌 "$confirmation_title" "$confirmation" omarchy-notification-send -g 󰢌 "$confirmation_title" "$confirmation"
+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 \ aether \
cliamp \ cliamp \
libreoffice-fresh \ libreoffice-fresh \
xournalpp \
pinta \ pinta \
obsidian \ obsidian \
obs-studio \
kdenlive \
moonlight-qt \ moonlight-qt \
omacut \ omacut \
monologue \ monologue \
omacalc \ omacalc \
omawrite \ omawrite \
hype hype \
signed
fi fi
@@ -1,10 +0,0 @@
#!/bin/bash
# omarchy:summary=Restore Bolt's default authorization behavior
set -euo pipefail
gum confirm "Restore Bolt's previous policy? New accessories may be authorized automatically when IOMMU protection is active." || exit 0
sudo /usr/bin/omarchy-thunderbolt-authorization-admin disable
systemctl --user disable --now omarchy-thunderbolt-authorization.service
echo "Previous Bolt policy restored. Saved Omarchy trust is retained for re-enabling."
@@ -1,9 +0,0 @@
#!/bin/bash
# omarchy:summary=Restore saved Thunderbolt firmware boot permissions
set -euo pipefail
gum confirm "Restore the previous Thunderbolt firmware boot allowlists? Device approval will remain enabled." || exit 0
sudo /usr/bin/omarchy-thunderbolt-authorization-admin boot-disable
echo "Previous firmware boot permissions restored and verified."
@@ -1,89 +0,0 @@
#!/bin/bash
# omarchy:summary=Stop asking before new USB devices can access the system
# omarchy:args=[--boot-only] [--yes]
# omarchy:requires-sudo=true
set -euo pipefail
assume_yes=0
boot_only=0
while (( $# > 0 )); do
case "$1" in
--yes) assume_yes=1 ;;
--boot-only) boot_only=1 ;;
-h | --help)
echo "Usage: omarchy-remove-security-usb-authorization [--boot-only] [--yes]"
echo
echo "Disables Omarchy's USB approval prompts and restores the kernel's"
echo "normal default-allow behavior."
echo "Use --boot-only to keep USBGuard enabled after the system starts."
exit 0
;;
*)
echo "omarchy-remove-security-usb-authorization: unknown option '$1'. Try --help." >&2
exit 2
;;
esac
shift
done
if (( boot_only )); then
if (( ! assume_yes )); then
gum style --foreground 212 --bold "Remove USB authorization from boot"
echo
echo "USBGuard will continue blocking unknown devices after its service starts."
echo "Devices can reach kernel drivers earlier during boot."
echo
gum confirm "Disable USB authorization from kernel boot?" || exit 130
fi
sudo omarchy-usb-authorization-boot disable
echo
echo "Boot-time USB authorization is disabled and takes effect after reboot."
exit 0
fi
state_dir="$HOME/.local/state/omarchy/usb-authorization"
unit="$HOME/.config/systemd/user/omarchy-usb-authorization.service"
if (( ! assume_yes )); then
gum style --foreground 212 --bold "Remove USB device authorization"
echo
echo "New USB devices will be allowed to bind to kernel drivers without approval."
echo
gum confirm "Disable USB device authorization?" || exit 130
fi
# Rebuild a permissive boot image before stopping USBGuard. If rebuilding
# fails, the command stops here and leaves the complete policy enabled.
sudo omarchy-usb-authorization-boot disable
if omarchy-pkg-present usbguard; then
# Keep the watcher and its IPC access until blocking has actually stopped.
# In particular, a failed stop must not strand a running daemon without UI.
sudo systemctl disable --now usbguard.service
if systemctl is-active --quiet usbguard.service || systemctl is-enabled --quiet usbguard.service; then
echo "USBGuard is still active or enabled; approval prompts have been preserved." >&2
exit 1
fi
sudo omarchy-usb-authorization-restore-default
sudo usbguard remove-user "$USER"
fi
systemctl --user disable --now omarchy-usb-authorization.service 2>/dev/null || true
rm -f "$unit"
systemctl --user daemon-reload
if [[ -d $state_dir/requests ]]; then
find "$state_dir/requests" -mindepth 1 -maxdepth 1 -type f -delete
rmdir "$state_dir/requests" 2>/dev/null || true
fi
rm -f "$state_dir/package-installed-by-omarchy" \
"$state_dir/policy-generated-by-omarchy" \
"$state_dir/service-enabled-by-omarchy"
rmdir "$state_dir" 2>/dev/null || true
echo "USB device authorization is disabled."
@@ -1,42 +0,0 @@
#!/bin/bash
# omarchy:summary=Require approval for new Thunderbolt PCIe accessories
# omarchy:args=[--quiet|status|recover]
set -euo pipefail
source "$OMARCHY_PATH/install/helpers/thunderbolt-policy.sh"
source "$OMARCHY_PATH/install/helpers/thunderbolt-review.sh"
if [[ ${1:-} == "status" ]]; then
tb_status
elif [[ ${1:-} == "recover" && $# == 1 ]]; then
sudo /usr/bin/omarchy-thunderbolt-authorization-admin recover
elif [[ $# == 0 || ( $# == 1 && $1 == "--quiet" ) ]]; then
if [[ ${1:-} != "--quiet" ]]; then
if [[ -f $TB_PENDING ]]; then tb_status; fi
echo "First setup, including a deferred setup, trusts connected accessories. Re-enabling a completed setup preserves saved trust."
gum confirm "Require approval for new Thunderbolt accessories?" || exit 0
fi
# System units and rules are shipped by omarchy-settings.
sudo install -Dm644 "$OMARCHY_PATH/default/polkit/org.omarchy.thunderbolt.policy" /usr/share/polkit-1/actions/org.omarchy.thunderbolt.policy
sudo systemctl daemon-reload
action=enable
if [[ ${1:-} == "--quiet" ]]; then
action=migrate
fi
sudo /usr/bin/omarchy-thunderbolt-authorization-admin "$action"
systemctl --user daemon-reload
systemctl --user enable --now omarchy-thunderbolt-authorization.service
if [[ -f /etc/omarchy/thunderbolt-authorization.enabled ]]; then
for ((attempt=0; attempt<10; attempt++)); do
if tb_public_snapshot >/dev/null 2>&1; then break; fi
sleep 1
done
tb_status
elif [[ -f $TB_PENDING ]]; then
tb_status
fi
else
echo "Usage: omarchy-setup-security-thunderbolt-authorization [--quiet|status|recover]" >&2
exit 1
fi
@@ -1,23 +0,0 @@
#!/bin/bash
# omarchy:summary=Clear supported Thunderbolt firmware boot allowlists
# omarchy:args=[recover]
set -euo pipefail
if [[ ${1:-} == "recover" && $# == 1 ]]; then
sudo /usr/bin/omarchy-thunderbolt-authorization-admin boot-recover
elif [[ $# == 0 ]]; then
gum style --bold "Thunderbolt at Boot"
echo "This trusts connected accessories for use after Linux starts, then clears the firmware boot allowlist."
echo "Trusted Thunderbolt keyboards, storage, and network adapters may be unavailable at disk unlock."
echo "You must be able to unlock and recover without Thunderbolt accessories."
echo "All controllers must be connected and support a boot allowlist in user or secure mode."
echo "Older snapshots or another operating system may restore firmware boot permissions."
gum confirm "Clear firmware boot access with these recovery limits?" || exit 0
sudo /usr/bin/omarchy-thunderbolt-authorization-admin boot-enable
echo "Firmware boot allowlists cleared and verified. Current connections were not disconnected."
else
echo "Usage: omarchy-setup-security-thunderbolt-boot [recover]" >&2
exit 1
fi
@@ -1,191 +0,0 @@
#!/bin/bash
# omarchy:summary=Ask before new USB devices can access the system
# omarchy:args=[--boot] [--yes] [--review-existing]
# omarchy:requires-sudo=true
set -euo pipefail
assume_yes=0
from_boot=0
review_existing=0
while (( $# > 0 )); do
case "$1" in
--yes) assume_yes=1 ;;
--boot) from_boot=1 ;;
--review-existing) review_existing=1 ;;
-h | --help)
echo "Usage: omarchy-setup-security-usb-authorization [--boot] [--yes] [--review-existing]"
echo
echo "Trusts USB devices that are connected now, then blocks new devices"
echo "until you approve them from an Omarchy notification."
echo
echo "--boot also blocks USB devices from the start of kernel boot. Connected"
echo "devices are added to USBGuard's policy, but remain unavailable before"
echo "USBGuard starts, including at an encrypted-disk password prompt."
echo "Older snapshots without USBGuard keep all USB disabled after startup,"
echo "including keyboards, network adapters, and storage. Use --boot only if"
echo "disk unlock and recovery work without any USB devices."
echo
echo "--review-existing reviews connected devices with older port-bound approvals."
echo "Each portable approval requires confirmation; existing rules are preserved."
exit 0
;;
*)
echo "omarchy-setup-security-usb-authorization: unknown option '$1'. Try --help." >&2
exit 2
;;
esac
shift
done
# Boot-time blocking goes into Limine's own configuration, which a Mac's boot
# package owns, so it is not offered there.
if (( from_boot )) && omarchy-hw-aarch64-apple; then
echo "USB authorization from boot is not available on Apple Silicon yet; USB device authorization after login still works." >&2
exit 2
fi
state_dir="$HOME/.local/state/omarchy/usb-authorization"
unit_source="$OMARCHY_PATH/default/systemd/user/omarchy-usb-authorization.service"
unit="$HOME/.config/systemd/user/omarchy-usb-authorization.service"
rules_file=/etc/usbguard/rules.conf
daemon_config=/etc/usbguard/usbguard-daemon.conf
source "$OMARCHY_PATH/install/helpers/usb-authorization.sh"
if (( review_existing )); then
# Older Omarchy rules have no per-rule ownership marker. They cannot safely
# be distinguished from intentionally restricted administrator rules.
(( ! assume_yes && ! from_boot )) || { echo "Review requires individual confirmation; do not combine it with --yes or --boot." >&2; exit 2; }
policy=$(usbguard list-rules)
declare -A reviewed=()
# Keep stdin attached to the terminal for gum (and any sudo prompt).
mapfile -t policy_lines <<<"$policy"
for line in "${policy_lines[@]}"; do
if [[ $line =~ ^[0-9]+:[[:space:]] ]]; then
rule=${line#*: }
[[ $rule == allow\ * ]] || continue
portable=$(usb_authorization_portable_rule "$rule") || continue
[[ ${portable% label *} != "$rule" ]] || continue
devices=$(usbguard list-devices "$rule")
mapfile -t candidates <<<"$devices"
for device in "${candidates[@]}"; do
[[ $device =~ ^([0-9]+):[[:space:]](allow[[:space:]].*)$ ]] || continue
id=${BASH_REMATCH[1]} current=${BASH_REMATCH[2]}
[[ ! -v reviewed[$id] ]] || continue
reviewed[$id]=1
echo
printf '%s\n' "$current" | LC_ALL=C tr -c '\11\12\15\40-\176' '?'
echo "This device has a location-bound approval. Details are supplied by the device."
if gum confirm "Trust this device on any USB port or hub?"; then
sudo /usr/bin/omarchy-usb-authorization-approve "$id" "$current"
fi
done
fi
done
echo "Review complete. Disconnected devices can be approved when reconnected."
exit 0
fi
# The watcher runs in the session's environment, which may differ from a
# command invoked in a checkout. Fail before enabling blocking on mismatched
# or incomplete installations, including dangling dev-link unit symlinks.
session_environment=$(systemctl --user show-environment)
grep -Fqx -- "OMARCHY_PATH=$OMARCHY_PATH" <<<"$session_environment" || {
echo "The session and this command use different Omarchy installations. Update the session before enabling USB protection." >&2
exit 1
}
for required in "$unit_source" /usr/share/omarchy/install/helpers/usb-authorization-policy.sh; do
[[ -r $required ]] || { echo "USB authorization installation is incomplete: $required" >&2; exit 1; }
done
for required in "$OMARCHY_PATH/bin/omarchy-usb-authorization-watch" \
"$OMARCHY_PATH/bin/omarchy-usb-authorization-event" \
"$OMARCHY_PATH/bin/omarchy-usb-authorization-review" \
/usr/bin/omarchy-usb-authorization-approve; do
[[ -r $required && -x $required ]] || { echo "USB authorization command is unavailable: $required" >&2; exit 1; }
done
if (( ! assume_yes )); then
if (( from_boot )); then
gum style --foreground 212 --bold "USB authorization from boot"
else
gum style --foreground 212 --bold "USB device authorization"
fi
echo
echo "Devices connected now will be trusted. Disconnect anything you do not trust."
echo "New USB devices will remain blocked until you approve them."
if (( from_boot )); then
echo
echo "Boot-time protection blocks all USB devices until USBGuard starts."
echo "A USB keyboard will not work at an encrypted-disk password prompt, even"
echo "when it is connected now."
echo
echo "Older snapshots without USBGuard keep ALL USB disabled after startup,"
echo "including keyboards, network adapters, and storage."
echo "Continue only if disk unlock and recovery work without any USB devices."
fi
echo
if (( from_boot )); then
gum confirm "Enable USB authorization from kernel boot?" || exit 130
else
gum confirm "Enable USB device authorization?" || exit 130
fi
fi
omarchy-pkg-add usbguard
usb_authorization_require_secure_settings "$daemon_config"
mkdir -p "$state_dir" "${unit%/*}"
chmod 700 "$state_dir"
if ! usb_authorization_rules_present "$rules_file"; then
policy=$(mktemp "${TMPDIR:-/tmp}/omarchy-usb-policy.XXXXXXXXXX")
trap 'rm -f "$policy"' EXIT
chmod 600 "$policy"
usb_authorization_generate_policy "$policy"
sudo install -Dm600 -o root -g root "$policy" "$rules_file"
touch "$state_dir/policy-generated-by-omarchy"
fi
# This user can inspect USBGuard events and approve the device named by an
# event. Policy and daemon parameters remain unavailable through the IPC ACL.
sudo usbguard add-user "$USER" \
--devices=list,listen,modify \
--policy=list \
--exceptions=listen
sudo install -Dm644 "$OMARCHY_PATH/default/polkit/org.omarchy.usb.policy" /usr/share/polkit-1/actions/org.omarchy.usb.policy
# Install a real file: a link into a checkout can become dangling on a branch
# switch. Start the waiting watcher before USBGuard can block new devices.
unit_temporary=$(mktemp "${unit}.XXXXXXXXXX")
install -m644 "$unit_source" "$unit_temporary"
mv -fT "$unit_temporary" "$unit"
systemctl --user daemon-reload
systemctl --user enable omarchy-usb-authorization.service
systemctl --user restart omarchy-usb-authorization.service
systemctl --user is-active --quiet omarchy-usb-authorization.service
sudo systemctl enable usbguard.service
# add-user ACL changes are loaded only when the daemon starts, so restart an
# existing service as well as starting a fresh one.
sudo systemctl restart usbguard.service
systemctl is-active --quiet usbguard.service
systemctl --user is-active --quiet omarchy-usb-authorization.service
if (( from_boot )); then
# The fixed helper independently verifies each full live device snapshot.
usb_authorization_trust_present_devices
sudo omarchy-usb-authorization-boot enable
fi
echo
echo "USB device authorization is enabled."
echo "Devices connected during setup remain trusted; new devices require approval."
echo "To review older port-bound approvals, run: omarchy-setup-security-usb-authorization --review-existing"
if (( from_boot )); then
echo "Boot-time authorization is enabled and takes effect after reboot."
fi
+30 -3
View File
@@ -462,7 +462,34 @@ root_dispatch() {
esac 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() { show_enabled() {
schedule_expiry_refresh
if [[ $minutes == "permanent" ]]; then if [[ $minutes == "permanent" ]]; then
echo "Passwordless sudo has been ENABLED permanently. Run this command again to disable it." echo "Passwordless sudo has been ENABLED permanently. Run this command again to disable it."
else else
@@ -542,12 +569,12 @@ else
fi fi
fi fi
if (( status == 0 )) && { (($# == 0)) || [[ $minutes == "--disable" ]]; }; then 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." echo "Passwordless sudo has been DISABLED. Sudo will require a password again."
elif (( status == STATUS_INACTIVE )) && [[ $minutes == "--disable" ]]; then elif (( status == STATUS_INACTIVE )) && [[ $minutes == "--disable" ]]; then
echo "Passwordless sudo is already DISABLED." echo "Passwordless sudo is already DISABLED."
elif (( status == 0 )) && [[ $minutes != "permanent" ]]; then 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 show_enabled
else else
if (( status != 0 && status != STATUS_INACTIVE )); then if (( status != 0 && status != STATUS_INACTIVE )); then
@@ -587,7 +614,7 @@ else
echo "" echo ""
if /usr/bin/gum confirm "Enable passwordless sudo $duration? This is a significant security risk!"; then 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 "" echo ""
show_enabled show_enabled
else else
-21
View File
@@ -462,12 +462,6 @@ scrub_factory_accounts() {
sanitize_factory_baseline() { sanitize_factory_baseline() {
local factory="$1" local factory="$1"
btrfs property set -ts "$factory" ro false btrfs property set -ts "$factory" ro false
if [[ -d $factory/var/lib/omarchy/thunderbolt-authorization || -f $factory/etc/omarchy/thunderbolt-authorization.enabled || -f $factory/etc/omarchy/thunderbolt-authorization.pending ]]; then
if ! /usr/bin/omarchy-thunderbolt-authorization-admin reset-root "$factory"; then
btrfs property set -ts "$factory" ro true
fail "could not reset Thunderbolt authorization in the factory baseline"
fi
fi
if ! scrub_factory_accounts "$factory"; then if ! scrub_factory_accounts "$factory"; then
btrfs property set -ts "$factory" ro true btrfs property set -ts "$factory" ro true
@@ -493,14 +487,6 @@ stage_full_reset() {
[[ -f $unit_src/omarchy-provision-owner.service && -x $next/usr/bin/omarchy-provision-owner ]] || [[ -f $unit_src/omarchy-provision-owner.service && -x $next/usr/bin/omarchy-provision-owner ]] ||
fail "the factory snapshot predates provisioning support; cannot reset from it" fail "the factory snapshot predates provisioning support; cannot reset from it"
# A normal install's factory snapshot includes the original owner's USB
# policy and enabled daemon. Keep input usable during the new owner's setup;
# provisioning enrolls their devices before enabling USBGuard again.
if [[ -f $next/usr/lib/systemd/system/usbguard.service ]]; then
systemctl --root="$next" disable usbguard.service >>"$LOG_FILE" 2>&1 ||
fail "could not disable USBGuard in the factory system (see $LOG_FILE)"
fi
log "Scrubbing machine identity from the factory system" log "Scrubbing machine identity from the factory system"
systemd-id128 new >"$next/etc/machine-id" systemd-id128 new >"$next/etc/machine-id"
rm -f "$next"/etc/ssh/ssh_host_* rm -f "$next"/etc/ssh/ssh_host_*
@@ -516,13 +502,6 @@ stage_full_reset() {
scrub_factory_accounts "$next" || scrub_factory_accounts "$next" ||
fail "could not remove account credentials from the factory system (see $LOG_FILE)" fail "could not remove account credentials from the factory system (see $LOG_FILE)"
# Leave the new owner's input available before interactive enrollment. Clear
# inherited accessory identities in both roots; never change the live daemon.
if [[ -d $next/var/lib/omarchy/thunderbolt-authorization || -f $next/etc/omarchy/thunderbolt-authorization.enabled || -f $next/etc/omarchy/thunderbolt-authorization.pending ]]; then
/usr/bin/omarchy-thunderbolt-authorization-admin reset-root "$next" ||
fail "could not reset Thunderbolt authorization in the staged root"
fi
# @factory itself survives the wipe as the baseline for future resets. If it # @factory itself survives the wipe as the baseline for future resets. If it
# came from a normal install it still holds the seller's account and # came from a normal install it still holds the seller's account and
# /etc/shadow, which the new wheel user could mount and read — and a second # /etc/shadow, which the new wheel user could mount and read — and a second
+1 -1
View File
@@ -6,7 +6,7 @@
nohup bash -c "sleep 2 && uwsm stop" >/dev/null 2>&1 & nohup bash -c "sleep 2 && uwsm stop" >/dev/null 2>&1 &
omarchy-osd -i logout -m "Logging out" -d 5000 omarchy-osd -i logout -m "$(omarchy-text "Logging out")" -d 5000
# Now close all windows # Now close all windows
omarchy-hyprland-window-close-all omarchy-hyprland-window-close-all
+1 -1
View File
@@ -8,7 +8,7 @@
# scope cannot terminate it before it runs. # scope cannot terminate it before it runs.
systemd-run --user --collect --quiet --on-active="2s" --timer-property=AccuracySec=100ms systemctl reboot --no-wall || exit 1 systemd-run --user --collect --quiet --on-active="2s" --timer-property=AccuracySec=100ms systemctl reboot --no-wall || exit 1
omarchy-osd -i reboot -m "Rebooting" -d 5000 omarchy-osd -i reboot -m "$(omarchy-text "Rebooting")" -d 5000
omarchy-state clear re*-required omarchy-state clear re*-required
+2 -2
View File
@@ -7,7 +7,7 @@
if [[ ${1:-} == "--inhibited" ]]; then if [[ ${1:-} == "--inhibited" ]]; then
# Close windows in the background so they cannot delay power-off. # Close windows in the background so they cannot delay power-off.
( (
omarchy-osd -i shutdown -m "Shutting down" -d 5000 omarchy-osd -i shutdown -m "$(omarchy-text "Shutting down")" -d 5000
omarchy-state clear re*-required omarchy-state clear re*-required
omarchy-hyprland-window-close-all omarchy-hyprland-window-close-all
) & ) &
@@ -21,7 +21,7 @@ elif [[ ${1:-} == "--inhibit" ]]; then
--who=Omarchy --why="Shutdown in progress" --mode=block \ --who=Omarchy --why="Shutdown in progress" --mode=block \
"$0" --inhibited || { "$0" --inhibited || {
status=$? status=$?
omarchy-notification-send -u critical "Shutdown failed" "Could not complete shutdown. Please try again." omarchy-notification-send -u critical "$(omarchy-text "Shutdown failed")" "$(omarchy-text "Could not complete shutdown. Please try again.")"
exit "$status" exit "$status"
} }
else else
+2 -2
View File
@@ -98,8 +98,8 @@ report_unsecured() {
"$1" >&2 "$1" >&2
omarchy-notification-send -u critical -g 󰌾 \ omarchy-notification-send -u critical -g 󰌾 \
"Screen did not lock before suspend" \ "$(omarchy-text "Screen did not lock before suspend")" \
"The session was left unlocked ($1)." >/dev/null 2>&1 || true "$(omarchy-text "The session was left unlocked (%1)." "$1")" >/dev/null 2>&1 || true
exit 1 exit 1
} }
+1 -1
View File
@@ -59,7 +59,7 @@ claim_path() {
announce() { announce() {
local path="$1" local path="$1"
local name="${path##*/}" local name="${path##*/}"
local args=("Received $name" "Saved to ${dir/#$HOME/~}" -u critical) local args=("$(omarchy-text "Received %1" "$name")" "$(omarchy-text "Saved to %1" "${dir/#$HOME/~}")" -u critical)
case "${name,,}" in case "${name,,}" in
*.png | *.jpg | *.jpeg | *.gif | *.webp | *.avif | *.bmp | *.tif | *.tiff) *.png | *.jpg | *.jpeg | *.gif | *.webp | *.avif | *.bmp | *.tif | *.tiff)
+4 -4
View File
@@ -27,8 +27,8 @@ if ((${#files[@]} == 0)); then
picked=$(omarchy-file-select --title "Send to $name" --multiple) || status=$? picked=$(omarchy-file-select --title "Send to $name" --multiple) || status=$?
if ((${status:-0} > 1)); then if ((${status:-0} > 1)); then
omarchy-notification-send -g "󰒊" -u critical "Could not send to $name" \ omarchy-notification-send -g "󰒊" -u critical "$(omarchy-text "Could not send to %1" "$name")" \
"The file chooser did not open" "$(omarchy-text "The file chooser did not open")"
exit 1 exit 1
fi fi
@@ -43,8 +43,8 @@ else
fi fi
if error=$(tailscale file cp --update-interval=0 -- "${files[@]}" "$machine:" 2>&1); then if error=$(tailscale file cp --update-interval=0 -- "${files[@]}" "$machine:" 2>&1); then
omarchy-notification-send -g "󰒊" "Sent to $name" "$what" omarchy-notification-send -g "󰒊" "$(omarchy-text "Sent to %1" "$name")" "$what"
else else
omarchy-notification-send -g "󰒊" -u critical "Could not send to $name" "${error:-Taildrop transfer failed}" omarchy-notification-send -g "󰒊" -u critical "$(omarchy-text "Could not send to %1" "$name")" "${error:-$(omarchy-text "Taildrop transfer failed")}"
exit 1 exit 1
fi fi
+38
View File
@@ -0,0 +1,38 @@
#!/bin/bash
# omarchy:summary=Print an Omarchy message in the session's language, with %1.. filled from the arguments
# omarchy:args=<english> [args...]
# omarchy:examples=omarchy-text "Sent to %1" phone
# omarchy:hidden=true
set -euo pipefail
if (($# < 1)); then
echo "Usage: omarchy-text <english> [args...]" >&2
exit 1
fi
text=$1
shift
# The shell's table is the one source: one JSON-encoded "english": "chinese" pair per line.
if [[ ${LANG:-} == zh_CN* ]]; then
translated=$(grep -E '^ ".*": ".*",?$' "$OMARCHY_PATH/shell/Commons/I18nZh.js" | sed 's/,$//' | paste -sd, |
sed 's/^/{/; s/$/}/' | jq -r --arg key "$text" '.[$key] // empty')
[[ -z $translated ]] || text=$translated
fi
# One pass over the template, so an argument that holds %2 is printed as given.
out=""
rest=$text
while [[ $rest =~ ^([^%]*)%(.)(.*)$ ]]; do
out+=${BASH_REMATCH[1]}
mark=${BASH_REMATCH[2]}
rest=${BASH_REMATCH[3]}
if [[ $mark == [1-9] ]] && ((mark <= $#)); then
out+=${!mark}
else
out+="%$mark"
fi
done
printf '%s\n' "$out$rest"
+2 -2
View File
@@ -10,12 +10,12 @@ toggle="$HOME/.local/state/omarchy/toggles/background-intros-off"
case ${1:-} in case ${1:-} in
enable) enable)
rm -f "$toggle" rm -f "$toggle"
omarchy-notification-send "Background Intros" "Enabled" omarchy-notification-send "$(omarchy-text "Background Intros")" "$(omarchy-text "Enabled")"
;; ;;
disable) disable)
mkdir -p "$(dirname "$toggle")" mkdir -p "$(dirname "$toggle")"
touch "$toggle" touch "$toggle"
omarchy-notification-send "Background Intros" "Disabled" omarchy-notification-send "$(omarchy-text "Background Intros")" "$(omarchy-text "Disabled")"
;; ;;
toggle) toggle)
if [[ -f $toggle ]]; then if [[ -f $toggle ]]; then
+1 -1
View File
@@ -17,7 +17,7 @@ mapfile -d '' -t BACKGROUNDS < <(
TOTAL=${#BACKGROUNDS[@]} TOTAL=${#BACKGROUNDS[@]}
if (( TOTAL == 0 )); then if (( TOTAL == 0 )); then
omarchy-notification-send "No background was found for theme" -t 2000 omarchy-notification-send "$(omarchy-text "No background was found for theme")" -t 2000
else else
# Get current background from symlink # Get current background from symlink
if [[ -L $CURRENT_BACKGROUND_LINK ]]; then if [[ -L $CURRENT_BACKGROUND_LINK ]]; then
+2 -2
View File
@@ -20,6 +20,6 @@ fi
# Create symlink to the new background media # Create symlink to the new background media
ln -nsf "$BACKGROUND" "$CURRENT_BACKGROUND_LINK" ln -nsf "$BACKGROUND" "$CURRENT_BACKGROUND_LINK"
# Update the live shell background immediately when it is running. The # Update the live shell background when it is running. Nothing watches the
# background plugin also polls this symlink, but IPC avoids the visible delay. # symlink, so this is how the shell learns of the new background.
omarchy-shell -q background set "$BACKGROUND" omarchy-shell -q background set "$BACKGROUND"
+1 -1
View File
@@ -35,5 +35,5 @@ fi
# Now remove the theme directory for THEME_NAME # Now remove the theme directory for THEME_NAME
rm -rf "$THEME_PATH" rm -rf "$THEME_PATH"
omarchy-notification-send "Theme removed" "$THEME_NAME" || true omarchy-notification-send "$(omarchy-text "Theme removed")" "$THEME_NAME" || true
echo "Removed $THEME_NAME" echo "Removed $THEME_NAME"
+9 -6
View File
@@ -190,7 +190,7 @@ set_theme_background() {
if [[ -z $CHOSEN_THEME_BACKGROUND || ! -f $CHOSEN_THEME_BACKGROUND ]]; then if [[ -z $CHOSEN_THEME_BACKGROUND || ! -f $CHOSEN_THEME_BACKGROUND ]]; then
if ! choose_theme_background; then if ! choose_theme_background; then
omarchy-notification-send "No background was found for theme" -t 2000 omarchy-notification-send "$(omarchy-text "No background was found for theme")" -t 2000
shell_ipc shell applyTheme "$colors_payload" "$shell_payload" || true shell_ipc shell applyTheme "$colors_payload" "$shell_payload" || true
return return
fi fi
@@ -207,11 +207,14 @@ set_theme_background() {
shell_ipc shell applyTheme "$colors_payload" "$shell_payload" || true shell_ipc shell applyTheme "$colors_payload" "$shell_payload" || true
ln -nsf "$new_background" "$CURRENT_BACKGROUND_LINK" ln -nsf "$new_background" "$CURRENT_BACKGROUND_LINK"
( (
if ! omarchy-theme-bg-boot-intro --theme-switch "$new_background" "$theme_id" "$intro_prepared"; then # Played or not, the shell gets the new still: it still holds the old
if [[ $(readlink -f "$CURRENT_BACKGROUND_LINK") == "$(readlink -f "$new_background")" && # theme's path, which the theme swap above deleted, and loads it again
$(stat -Lc '%d:%i' "$CURRENT_THEME_PATH" 2>/dev/null) == "$theme_id" ]]; then # when OWE hands the desktop back after the intro. While the intro plays
shell_ipc background setInstant "$new_background" || true # the shell's background is suspended under it, so the swap is unseen.
fi 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 fi
shell_ipc shell finishThemeIntro "$theme_id" || true shell_ipc shell finishThemeIntro "$theme_id" || true
wait_for_theme_intro themeIntroStatus "$theme_id" "pending" wait_for_theme_intro themeIntroStatus "$theme_id" "pending"
+12 -1
View File
@@ -1,9 +1,10 @@
#!/bin/bash #!/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 # omarchy:hidden=true
colors_toml=$HOME/.local/state/omarchy/current/theme/colors.toml 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 if [[ ! -f $colors_toml ]] || ! pgrep -x foot >/dev/null; then
exit 0 exit 0
@@ -12,6 +13,16 @@ fi
foot_osc=$(omarchy-theme-osc "$colors_toml") foot_osc=$(omarchy-theme-osc "$colors_toml")
[[ -n $foot_osc ]] || exit 0 [[ -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 foot_pid in $(pgrep -x foot); do
for child_pid in $(pgrep -P "$foot_pid"); do for child_pid in $(pgrep -P "$foot_pid"); do
tty=$(readlink "/proc/$child_pid/fd/1" 2>/dev/null) 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 fi
apply_shell_section_overrides apply_shell_section_overrides
omarchy-theme-set-glass "$NEXT_THEME_DIR"
@@ -1,17 +0,0 @@
#!/bin/bash -p
# omarchy:summary=Manage system Thunderbolt authorization settings
# omarchy:hidden=true
# This root entrypoint uses installed code and rejects unsafe Bash startup.
if [[ $- != *p* ]]; then exit 126; fi
source /usr/bin/omarchy-security-functions || exit 126
omarchy_security_require_privileged_bash_startup || exit 126
omarchy_security_sanitize_bash_environment "$0" "$@"
set -euo pipefail
(( EUID == 0 )) || exit 126
export PATH=/usr/bin:/bin LC_ALL=C
unset DBUS_SYSTEM_BUS_ADDRESS DBUS_SESSION_BUS_ADDRESS
source /usr/share/omarchy/install/helpers/thunderbolt-policy.sh
source /usr/share/omarchy/install/helpers/thunderbolt-setup.sh
tb_admin "$@"
@@ -1,25 +0,0 @@
#!/bin/bash -p
# omarchy:summary=Approve one verified Thunderbolt connection
# omarchy:hidden=true
# pkexec permits this exact program for an active local wheel user. Suppress
# startup injection before reading input; it cannot dispatch general root work.
if [[ $- != *p* ]]; then exit 126; fi
source /usr/bin/omarchy-security-functions || exit 126
omarchy_security_require_privileged_bash_startup || exit 126
omarchy_security_sanitize_bash_environment "$0" "$@"
set -euo pipefail
(( EUID == 0 )) || exit 126
export PATH=/usr/bin:/bin LC_ALL=C
unset DBUS_SYSTEM_BUS_ADDRESS DBUS_SESSION_BUS_ADDRESS
source /usr/share/omarchy/install/helpers/thunderbolt-policy.sh
[[ $# == 2 && ( $1 == "true" || $1 == "false" ) ]] || exit 64
(( ${#2} <= 16384 )) || exit 64
jq -e 'type == "object"' <<< "$2" >/dev/null
exec 9> "$TB_LOCK"
flock -x 9
result=$(tb_approve "$2" "$1" true)
tb_json_write "$TB_RUNTIME/snapshot.json" 644 <<< "$result"
printf '%s\n' "$result"
@@ -1,8 +0,0 @@
#!/bin/bash
# omarchy:hidden=true
# omarchy:summary=Check whether Thunderbolt firmware boot access is restricted
set -euo pipefail
source "$OMARCHY_PATH/install/helpers/thunderbolt-policy.sh"
tb_public_snapshot | jq -e .boot_protection >/dev/null
@@ -1,16 +0,0 @@
#!/bin/bash -p
# omarchy:summary=Restore trusted Thunderbolt devices and publish their status
# omarchy:hidden=true
# Keep root startup independent of a caller's environment or checkout.
if [[ $- != *p* ]]; then exit 126; fi
source /usr/bin/omarchy-security-functions || exit 126
omarchy_security_require_privileged_bash_startup || exit 126
omarchy_security_sanitize_bash_environment "$0" "$@"
set -euo pipefail
(( EUID == 0 )) || exit 126
export PATH=/usr/bin:/bin LC_ALL=C
unset DBUS_SYSTEM_BUS_ADDRESS DBUS_SESSION_BUS_ADDRESS
source /usr/share/omarchy/install/helpers/thunderbolt-policy.sh
tb_daemon
@@ -1,10 +0,0 @@
#!/bin/bash
# omarchy:hidden=true
# omarchy:summary=Handle Thunderbolt device approval review
set -euo pipefail
source "$OMARCHY_PATH/install/helpers/thunderbolt-policy.sh"
source "$OMARCHY_PATH/install/helpers/thunderbolt-review.sh"
[[ $# == 1 ]] || exit 64
tb_review "$1"
@@ -1,9 +0,0 @@
#!/bin/bash
# omarchy:hidden=true
# omarchy:summary=Handle Thunderbolt device approval watch
set -euo pipefail
source "$OMARCHY_PATH/install/helpers/thunderbolt-policy.sh"
source "$OMARCHY_PATH/install/helpers/thunderbolt-review.sh"
tb_watch
+3 -3
View File
@@ -1,6 +1,6 @@
#!/bin/bash #!/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:args=[toggle|on|off]
# omarchy:examples=omarchy toggle animations | omarchy toggle animations off | omarchy toggle animations on # 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 esac
if omarchy-hyprland-toggle-enabled no-animations; then if omarchy-hyprland-toggle-enabled no-animations; then
omarchy-notification-send -g 󰕟 "Animations disabled" omarchy-notification-send -g 󰗘 "$(omarchy-text "Animations disabled")"
else else
omarchy-notification-send -g 󰕟 "Animations enabled" omarchy-notification-send -g 󰗘 "$(omarchy-text "Animations enabled")"
fi fi
+1 -1
View File
@@ -1,6 +1,6 @@
#!/bin/bash #!/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:args=[toggle|on|off]
# omarchy:examples=omarchy toggle bar | omarchy toggle bar off | omarchy toggle bar on # omarchy:examples=omarchy toggle bar | omarchy toggle bar off | omarchy toggle bar on
+2 -2
View File
@@ -11,8 +11,8 @@ omarchy-toggle crash-capture-off
# starting only settles this session; the next login reads the flag itself. # starting only settles this session; the next login reads the flag itself.
if omarchy-toggle-enabled crash-capture-off; then if omarchy-toggle-enabled crash-capture-off; then
systemctl --user stop omarchy-crash-watch.service systemctl --user stop omarchy-crash-watch.service
omarchy-notification-send -g "$CRASH_GLYPH" "Crash capture disabled" omarchy-notification-send -g "$CRASH_GLYPH" "$(omarchy-text "Crash capture disabled")"
else else
systemctl --user start omarchy-crash-watch.service systemctl --user start omarchy-crash-watch.service
omarchy-notification-send -g "$CRASH_GLYPH" "Crash capture enabled" omarchy-notification-send -g "$CRASH_GLYPH" "$(omarchy-text "Crash capture enabled")"
fi fi
+2 -2
View File
@@ -5,7 +5,7 @@
omarchy-toggle screensaver-off omarchy-toggle screensaver-off
if omarchy-toggle-enabled screensaver-off; then if omarchy-toggle-enabled screensaver-off; then
omarchy-notification-send -g 󱄄 "Screensaver disabled" omarchy-notification-send -g 󱄄 "$(omarchy-text "Screensaver disabled")"
else else
omarchy-notification-send -g 󱄄 "Screensaver enabled" omarchy-notification-send -g 󱄄 "$(omarchy-text "Screensaver enabled")"
fi fi
+2 -2
View File
@@ -5,7 +5,7 @@
omarchy-toggle suspend-off omarchy-toggle suspend-off
if omarchy-toggle-enabled suspend-off; then if omarchy-toggle-enabled suspend-off; then
omarchy-notification-send -g 󰒲 "Suspend removed from system menu" omarchy-notification-send -g 󰒲 "$(omarchy-text "Suspend removed from system menu")"
else else
omarchy-notification-send -g 󰒲 "Suspend now available in system menu" omarchy-notification-send -g 󰒲 "$(omarchy-text "Suspend now available in system menu")"
fi fi
+2 -2
View File
@@ -5,7 +5,7 @@
omarchy-toggle herdr-theme-sync omarchy-toggle herdr-theme-sync
if omarchy-toggle-enabled herdr-theme-sync; then if omarchy-toggle-enabled herdr-theme-sync; then
omarchy-notification-send -g 󰸌 "Herdr theme sync enabled" omarchy-notification-send -g 󰸌 "$(omarchy-text "Herdr theme sync enabled")"
else else
omarchy-notification-send -g 󰸌 "Herdr theme sync disabled" omarchy-notification-send -g 󰸌 "$(omarchy-text "Herdr theme sync disabled")"
fi fi
+3 -3
View File
@@ -193,14 +193,14 @@ main() {
output=$(output_path "$input" "$format" "$resolution") output=$(output_path "$input" "$format" "$resolution")
if [[ $type == "video" ]]; then if [[ $type == "video" ]]; then
omarchy-notification-send -g  "Transcoding video…" "$(basename -- "$input") to $format ($resolution)" omarchy-notification-send -g  "$(omarchy-text "Transcoding video…")" "$(omarchy-text "%1 to %2 (%3)" "$(basename -- "$input")" "$format" "$resolution")"
transcode_video "$input" "$format" "$resolution" "$output" transcode_video "$input" "$format" "$resolution" "$output"
copy_to_clipboard "$output" copy_to_clipboard "$output"
omarchy-notification-send -g  "Transcoded to $resolution $format" "Saved and copied to clipboard." omarchy-notification-send -g  "$(omarchy-text "Transcoded to %1 %2" "$resolution" "$format")" "Saved and copied to clipboard."
else else
transcode_picture "$input" "$format" "$resolution" "$output" transcode_picture "$input" "$format" "$resolution" "$output"
copy_to_clipboard "$output" copy_to_clipboard "$output"
omarchy-notification-send -g  "Transcoded to $resolution $format" "Saved and copied to clipboard." omarchy-notification-send -g  "$(omarchy-text "Transcoded to %1 %2" "$resolution" "$format")" "Saved and copied to clipboard."
fi fi
} }
+1 -1
View File
@@ -37,5 +37,5 @@ icon_name=$(printf '%s\n' "$APP_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^[:
rm -f "$DESKTOP_DIR/$APP_NAME.desktop" rm -f "$DESKTOP_DIR/$APP_NAME.desktop"
rm -f "$ICON_DIR/$icon_name.png" "$ICON_DIR/$APP_NAME.png" "$OLD_ICON_DIR/$APP_NAME.png" rm -f "$ICON_DIR/$icon_name.png" "$ICON_DIR/$APP_NAME.png" "$OLD_ICON_DIR/$APP_NAME.png"
if [[ ${OMARCHY_REMOVE_NOTIFY:-true} != "false" ]]; then if [[ ${OMARCHY_REMOVE_NOTIFY:-true} != "false" ]]; then
omarchy-notification-send -g  "TUI removed" "$APP_NAME" omarchy-notification-send -g  "$(omarchy-text "TUI removed")" "$APP_NAME"
fi fi
-25
View File
@@ -1,25 +0,0 @@
#!/bin/bash -p
# omarchy:hidden=true
# omarchy:summary=Save portable trust for one verified USB device
# This narrow Polkit boundary accepts a live device snapshot, never a policy or
# file path. Privileged Bash suppresses startup code from the caller's environment.
if [[ $- != *p* ]]; then exit 126; fi
source /usr/bin/omarchy-security-functions || exit 126
omarchy_security_require_privileged_bash_startup || exit 126
omarchy_security_sanitize_bash_environment "$0" "$@"
set -euo pipefail
(( EUID == 0 )) || exit 126
export PATH=/usr/bin:/bin LC_ALL=C
unset USBGUARD_DAEMON_CONF USBGUARD_DEBUG
source /usr/share/omarchy/install/helpers/usb-authorization-policy.sh
(( $# == 2 )) || exit 64
# Omarchy persists to this file. Refuse custom daemon storage rather than
# mistaking a stale copy of a rule for a successful persistent decision.
rules_file=$(awk -F= '/^[[:space:]]*RuleFile[[:space:]]*=/ { value=$2; gsub(/^[[:space:]]+|[[:space:]]+$/, "", value); print value; exit }' /etc/usbguard/usbguard-daemon.conf)
[[ $rules_file == "/etc/usbguard/rules.conf" ]] || exit 1
exec 8>/run/omarchy-usb-authorization.lock
flock -x 8
usb_authorization_permanent "$1" "$2"
-827
View File
@@ -1,827 +0,0 @@
#!/bin/bash
# omarchy:summary=Manage USB authorization from the start of kernel boot
# omarchy:args=<enable|disable>
# omarchy:requires-sudo=true
# omarchy:hidden=true
set -euo pipefail
drop_in=/etc/limine-entry-tool.d/usb-authorization.conf
setting='KERNEL_CMDLINE[default]+=" usbcore.authorized_default=0"'
limine_conf=/boot/limine.conf
limine_defaults=/etc/default/limine
boot_path=/boot
kernel_parameter=usbcore.authorized_default=0
snapshot_begin='# Omarchy USB authorization begin'
snapshot_setting='SNAPSHOT_KERNEL_PARAMETERS+=usbcore.authorized_default=0'
snapshot_end='# Omarchy USB authorization end'
usb_authorization_cmdline_has_parameter() {
local cmdline="$1" parameter="$2" word
for word in $cmdline; do
[[ $word == "$parameter" ]] && return 0
done
return 1
}
usb_authorization_secure_boot_enabled() {
local value variable
for variable in /sys/firmware/efi/efivars/SecureBoot-*; do
[[ -r $variable ]] || continue
value=$(od -An -t u1 -j 4 -N 1 "$variable")
value=${value//[[:space:]]/}
[[ $value == "1" ]] && return 0
done
return 1
}
usb_authorization_verify_boot_entries() {
local expected="$1" config="$2" esp="$3"
local machine_id="${4:-$(</etc/machine-id)}"
local secure_boot="${5:-auto}"
local actual_hash embedded_cmdline expected_hash image mode path paths section
if [[ $secure_boot == "auto" ]]; then
if usb_authorization_secure_boot_enabled; then
secure_boot=enabled
else
secure_boot=disabled
fi
fi
if [[ ! -r $config ]]; then
echo "Cannot verify the generated Limine boot entries in $config." >&2
return 1
fi
paths=$(mktemp /tmp/omarchy-usb-boot-paths.XXXXXXXXXX)
section=$(mktemp /tmp/omarchy-usb-boot-cmdline.XXXXXXXXXX)
if ! awk -v expected="$expected" -v machine_id="$machine_id" -v parameter="$kernel_parameter" '
function contains_parameter(value, count, fields, field_index) {
count = split(value, fields, /[[:space:]]+/)
for (field_index = 1; field_index <= count; field_index++) {
if (fields[field_index] == parameter) return 1
}
return 0
}
function finish_entry(found) {
if (!target_os || !is_kernel) return
entries++
if (has_cmdline) {
found = contains_parameter(cmdline)
if ((expected == "enabled" && !found) || (expected == "disabled" && found)) bad = 1
}
if (path == "") {
bad = 1
} else {
print (has_cmdline ? "external" : "embedded") "\t" path
}
}
/^\/[^/]/ {
finish_entry()
target_os = 0
is_kernel = 0
has_cmdline = 0
cmdline = ""
path = ""
next
}
/^[[:space:]]*comment:[[:space:]]*machine-id=/ {
target_os = index($0, "machine-id=" machine_id) > 0
next
}
/^[[:space:]]*\/\// {
finish_entry()
is_kernel = 0
has_cmdline = 0
cmdline = ""
path = ""
next
}
/^[[:space:]]*comment:[[:space:]]*kernel-id=/ {
is_kernel = target_os
next
}
is_kernel && /^[[:space:]]*path:[[:space:]]*/ {
path = $0
sub(/^[[:space:]]*path:[[:space:]]*/, "", path)
next
}
is_kernel && /^[[:space:]]*cmdline:[[:space:]]*/ {
has_cmdline = 1
cmdline = $0
sub(/^[[:space:]]*cmdline:[[:space:]]*/, "", cmdline)
next
}
END {
finish_entry()
if (entries == 0) bad = 1
exit bad
}
' "$config" >"$paths"; then
rm -f "$paths" "$section"
echo "The generated Limine kernel entries do not all have USB authorization $expected." >&2
return 1
fi
while IFS=$'\t' read -r mode path; do
if [[ $path != 'boot():/'* ]]; then
rm -f "$paths" "$section"
echo "Cannot verify the kernel image referenced by Limine: $path" >&2
return 1
fi
image="$esp/${path#'boot():/'}"
image=${image%%#*}
if [[ ! -r $image ]]; then
rm -f "$paths" "$section"
echo "Cannot verify the kernel image referenced by Limine: $image" >&2
return 1
fi
if [[ $path == *'#'* ]]; then
expected_hash=${path##*#}
actual_hash=$(b2sum "$image")
actual_hash=${actual_hash%% *}
if [[ $actual_hash != "$expected_hash" ]]; then
rm -f "$paths" "$section"
echo "The verification hash for $image does not match the generated Limine entry." >&2
return 1
fi
fi
: >"$section"
if ! objcopy -O binary --only-section=.cmdline "$image" "$section" 2>/dev/null; then
[[ $mode == "external" ]] && continue
rm -f "$paths" "$section"
echo "Cannot inspect the embedded kernel command line in $image." >&2
return 1
fi
embedded_cmdline=$(tr '\0' '\n' <"$section")
# Without Secure Boot, Limine's external command line takes precedence.
# Under Secure Boot, systemd-stub uses a nonempty embedded .cmdline.
if [[ $mode == "external" && $secure_boot == "disabled" ]]; then
continue
fi
# A signed UKI can ignore Limine's external cmdline and use its embedded
# .cmdline instead. If one exists, both representations must agree.
if [[ -z $embedded_cmdline ]]; then
if [[ $mode == "external" || $expected == "disabled" ]]; then
continue
fi
rm -f "$paths" "$section"
echo "The generated kernel image $image has no verifiable kernel command line." >&2
return 1
fi
if [[ $expected == "enabled" ]]; then
if ! usb_authorization_cmdline_has_parameter "$embedded_cmdline" "$kernel_parameter"; then
rm -f "$paths" "$section"
echo "The generated kernel image $image does not enable boot-time USB authorization." >&2
return 1
fi
elif usb_authorization_cmdline_has_parameter "$embedded_cmdline" "$kernel_parameter"; then
rm -f "$paths" "$section"
echo "The generated kernel image $image still enables boot-time USB authorization." >&2
return 1
fi
done <"$paths"
rm -f "$paths" "$section"
}
usb_authorization_rebuild_and_verify() {
local expected="$1" config="$2" esp="$3"
local machine_id="${4:-$(</etc/machine-id)}" secure_boot="${5:-auto}"
if ! limine-mkinitcpio; then
return 1
fi
# limine-mkinitcpio-hook can swallow an individual kernel build failure and
# still return success, so its exit status alone is not a safety boundary.
usb_authorization_verify_boot_entries "$expected" "$config" "$esp" "$machine_id" "$secure_boot"
}
usb_authorization_snapshot_block_status() {
local config="$1"
[[ -r $config ]] || return 2
awk -v begin="$snapshot_begin" -v setting="$snapshot_setting" -v end="$snapshot_end" '
$0 == begin { begin_count++; state = 1; next }
$0 == setting { setting_count++; if (state == 1) state = 2; next }
$0 == end { end_count++; if (state == 2) valid_count++; state = 0; next }
{ state = 0 }
END {
if (begin_count == 0 && setting_count == 0 && end_count == 0) exit 1
if (begin_count == 1 && setting_count == 1 && end_count == 1 && valid_count == 1) exit 0
exit 2
}
' "$config"
}
usb_authorization_enable_snapshot_setting() {
local config="$1" status temporary
if usb_authorization_snapshot_block_status "$config"; then
return 0
else
status=$?
fi
if (( status != 1 )); then
echo "Refusing to modify the USB authorization block in $config." >&2
return 1
fi
temporary=$(mktemp "${config}.XXXXXXXXXX")
cp --preserve=mode,ownership "$config" "$temporary"
{
cat "$config"
printf '\n%s\n%s\n%s\n' "$snapshot_begin" "$snapshot_setting" "$snapshot_end"
} >"$temporary"
mv -f "$temporary" "$config"
}
usb_authorization_disable_snapshot_setting() {
local config="$1" status temporary
if usb_authorization_snapshot_block_status "$config"; then
status=0
else
status=$?
fi
(( status == 1 )) && return 0
if (( status != 0 )); then
echo "Refusing to modify the USB authorization block in $config." >&2
return 1
fi
temporary=$(mktemp "${config}.XXXXXXXXXX")
cp --preserve=mode,ownership "$config" "$temporary"
awk -v begin="$snapshot_begin" '
$0 == begin { skip = 2; next }
skip > 0 { skip--; next }
{ print }
' "$config" >"$temporary"
mv -f "$temporary" "$config"
}
usb_authorization_rewrite_boot_cmdlines() {
local expected="$1" config="$2" machine_id="${3:-$(</etc/machine-id)}"
local backup temporary
backup=$(mktemp "${config}.omarchy-usb-backup.XXXXXXXXXX")
temporary=$(mktemp "${config}.omarchy-usb-new.XXXXXXXXXX")
cp --preserve=mode,ownership "$config" "$backup"
cp --preserve=mode,ownership "$config" "$temporary"
if ! awk -v expected="$expected" -v machine_id="$machine_id" -v parameter="$kernel_parameter" '
function rewrite_cmdline(line, prefix, value, count, fields, field_index, result, found) {
prefix = line
sub(/cmdline:.*/, "cmdline:", prefix)
value = line
sub(/^[[:space:]]*cmdline:[[:space:]]*/, "", value)
count = split(value, fields, /[[:space:]]+/)
result = ""
found = 0
for (field_index = 1; field_index <= count; field_index++) {
if (fields[field_index] == parameter) {
found = 1
continue
}
if (fields[field_index] != "") result = result (result == "" ? "" : " ") fields[field_index]
}
if (expected == "enabled") result = result (result == "" ? "" : " ") parameter
return prefix (result == "" ? "" : " ") result
}
/^\/[^/]/ { target_os = 0 }
/^[[:space:]]*comment:[[:space:]]*machine-id=/ {
target_os = index($0, "machine-id=" machine_id) > 0
}
target_os && /^[[:space:]]*cmdline:[[:space:]]*/ {
print rewrite_cmdline($0)
next
}
{ print }
' "$config" >"$temporary"; then
rm -f "$backup" "$temporary"
return 1
fi
mv -f "$temporary" "$config"
if ! usb_authorization_enroll_config "$config" "${config%/*}"; then
mv -f "$backup" "$config"
if ! usb_authorization_secure_boot_enabled; then
limine-enroll-config >/dev/null 2>&1 || true
fi
echo "Restored $config because its bootloader verification data could not be updated." >&2
return 1
fi
rm -f "$backup"
}
usb_authorization_rewrite_snapshot_manifest() {
local expected="$1" manifest="$2" temporary
[[ -e $manifest ]] || return 0
temporary=$(mktemp "${manifest}.omarchy-usb.XXXXXXXXXX")
if ! cp --preserve=mode,ownership "$manifest" "$temporary"; then
rm -f "$temporary"
return 1
fi
# Limine uses the cached lines when present, and structured command lines
# otherwise. Update both, including nested kernels and restore entries.
if ! jq --arg expected "$expected" --arg parameter "$kernel_parameter" '
def cmdline:
if type != "string" then error("Invalid snapshot command line") else
[splits("[[:space:]]+") | select(length > 0 and . != $parameter)]
| if $expected == "enabled" then . + [$parameter] else . end
| join(" ")
end;
def lines:
map(if test("^\\s*(cmdline|kernel_cmdline):"; "i") then
capture("^(?<prefix>\\s*(?:cmdline|kernel_cmdline):\\s*)(?<value>.*)$"; "i")
| .prefix + (.value | cmdline)
else . end);
def kernel:
.cmdlineDetails |= map(.cmdline |= cmdline | .snapshotCmdline |= cmdline)
| .allInConfig |= lines
| .allInSnapshotConfig |= lines
| .subKernels |= map(kernel);
if .jsonFormatVersion != "1.3.0" or (.snapshotEntries | type) != "array" then
error("Unsupported Limine snapshot manifest format")
else
.snapshotEntries |= map(.kernelEntries |= map(kernel))
end
' "$manifest" >"$temporary"; then
rm -f "$temporary"
echo "Cannot update stored USB boot policy in $manifest." >&2
return 1
fi
mv -f "$temporary" "$manifest"
}
usb_authorization_verify_signature() {
# sbctl verify can return zero for unsigned or unreadable files. Check its
# structured result, including the exact path, instead of just the exit code.
if ! sbctl verify --json "$1" | jq -e --arg file "$1" \
'length == 1 and .[0].file_name == $file and .[0].is_signed == 1' >/dev/null; then
echo "Cannot verify the Secure Boot signature of $1 using this machine's signing keys." >&2
return 1
fi
}
# Read the same architecture and enrollment setting as the installed Limine tool.
usb_authorization_limine_settings() (
source /usr/lib/limine/limine-common-functions
load_config || return 1
printf '%s %s\n' "$(limine_efi_arch | tr '[:upper:]' '[:lower:]')" "${ENABLE_ENROLL_LIMINE_CONFIG:-no}"
)
usb_authorization_verify_bootloader() (
local config="$1" esp="$2" architecture enrollment image check digest
read -r architecture enrollment < <(usb_authorization_limine_settings) || return 1
[[ -n $architecture ]] || return 1
image="$esp/EFI/limine/limine_$architecture.efi"
usb_authorization_verify_signature "$image" || return 1
check=$(mktemp) || return 1
trap 'rm -f "$check"' EXIT
cp "$image" "$check" || return 1
if [[ $enrollment == "yes" ]]; then
digest=$(b2sum "$config") || return 1
limine enroll-config "$check" "${digest%% *}" --quiet || return 1
else
limine enroll-config "$check" --reset --quiet || return 1
fi
# Enrollment edits fixed bytes in the executable. Reapplying the expected
# value must be a no-op, including when enrollment is explicitly disabled.
if ! cmp -s "$image" "$check"; then
echo "Limine's signed executable does not match the configured menu enrollment." >&2
return 1
fi
)
usb_authorization_enroll_config() {
limine-enroll-config || return 1
if usb_authorization_secure_boot_enabled; then
usb_authorization_verify_bootloader "$1" "$2" || return 1
fi
}
# Kernel rebuild hooks also reset and re-sign Limine. Checkpoint the complete
# boot files, not just the menu: rebuilding can overwrite images it references.
# The outer process holds the real boot lock; the private mount namespace gives
# nested Limine tools their own lock inode so they cannot deadlock against it.
usb_authorization_boot_transaction() (
local action="$1" state architecture enrollment index manifest changed=0 committed=0 restored=1
local machine_id="$(</etc/machine-id)"
local cache="${2:-/var/cache/boot/$machine_id}"
local -a paths=("$boot_path" "$cache" "$limine_defaults" "$drop_in")
exec 8>/run/lock/boot-partition.lock
flock --timeout 30 8 || return 1
read -r architecture enrollment < <(usb_authorization_limine_settings) || return 1
[[ -n $architecture ]] || return 1
usb_authorization_verify_signature "$boot_path/EFI/limine/limine_$architecture.efi" || return 1
state=$(mktemp -d /var/tmp/omarchy-usb-boot.XXXXXXXXXX) || return 1
trap '
if (( changed && ! committed )); then
echo "USB boot policy failed; restoring the previous boot files and settings." >&2
for ((index=0; index<${#paths[@]}; index++)); do
if [[ -d $state/$index && ! -L $state/$index ]]; then
mkdir -p "${paths[index]}" && cp -a "$state/$index/." "${paths[index]}/" || restored=0
elif [[ -e $state/$index || -L $state/$index ]]; then
cp -a --remove-destination "$state/$index" "${paths[index]}" || restored=0
else
rm -rf -- "${paths[index]}" || restored=0
fi
done
# New unreferenced images are harmless, but a newly created cached backup
# manifest must not reintroduce the failed policy on a later recovery.
for manifest in snapshots.json snapshots.json.old; do
[[ -e $state/0/$machine_id/limine_history/$manifest ]] ||
rm -f "$boot_path/$machine_id/limine_history/$manifest" || restored=0
[[ -e $state/1/lss/$manifest ]] || rm -f "$cache/lss/$manifest" || restored=0
done
sync -f "$boot_path" || restored=0
fi
if (( restored )); then
rm -rf "$state"
else
echo "Boot recovery was incomplete. Preserve and restore the checkpoint at $state before rebooting." >&2
fi
' EXIT
trap 'exit 1' HUP INT TERM
for ((index=0; index<${#paths[@]}; index++)); do
if [[ -d ${paths[index]} ]]; then
cp -a "${paths[index]}/." "$state/$index" || return 1
elif [[ -e ${paths[index]} || -L ${paths[index]} ]]; then
cp -a "${paths[index]}" "$state/$index" || return 1
fi
done
: >"$state/lock"
changed=1
usb_authorization_run_transaction "$state" "$action" || return 1
usb_authorization_verify_bootloader "$limine_conf" "$boot_path" || return 1
sync -f "$boot_path" || return 1
committed=1
)
usb_authorization_run_transaction() {
unshare --mount --propagation private /bin/bash -c '
mount --bind "$1/lock" /run/lock/boot-partition.lock || exit 1
source "$2"
usb_authorization_main "$3"
' bash "$1" "${BASH_SOURCE[0]}" "$2"
}
# Called with Limine's boot-partition lock held. Prepare signed copies of
# conflicting historical UKIs before changing the selected boot policy.
usb_authorization_reconcile_archived_images() (
local expected="$1" config="$2" esp="$3" machine_id="$4"
local cache="${5:-/var/cache/boot/$machine_id/lss/snapshots.json}"
local history="$esp/$machine_id/limine_history" staging manifest names name image
local cmdline hash_command old_digest new_digest old_hash new_hash replacement index=0
local published=0 committed=0 section layout image_base image_size alignment address
local -a manifests=() originals=()
usb_authorization_secure_boot_enabled || return 0
[[ -d $history ]] || return 0
staging=$(mktemp -d "$esp/.omarchy-usb-uki.XXXXXXXXXX") || return 1
trap '
if (( published && ! committed )); then
for ((index=0; index<${#originals[@]}; index++)); do
cp --preserve=mode,ownership "$staging/original.$index" "${originals[index]}" || true
done
fi
rm -rf "$staging"
' EXIT
for manifest in "$history/snapshots.json" "$history/snapshots.json.old" "$cache"; do
[[ -f $manifest ]] || continue
jq -e '.jsonFormatVersion == "1.3.0" and (.snapshotEntries | type == "array")' "$manifest" >/dev/null || return 1
manifests+=("$manifest")
done
(( ${#manifests[@]} )) || return 0
names=$(jq -sr '[.[] | .snapshotEntries[] | .. | objects | .imageDetails? // empty | .[] | .fileHashName] | unique[]' "${manifests[@]}") || return 1
printf '[]\n' >"$staging/replacements.json"
while IFS= read -r name; do
[[ -n $name ]] || continue
# Only image names, never arbitrary paths supplied by a manifest.
if [[ $name == */* || $name == "." || $name == ".." ]]; then
echo "Invalid archived kernel image name: $name" >&2
return 1
fi
image="$history/$name"
[[ -f $image && ! -L $image ]] || continue
: >"$staging/cmdline"
objcopy -O binary --only-section=.cmdline "$image" "$staging/cmdline" 2>/dev/null || continue
cmdline=$(tr '\0' '\n' <"$staging/cmdline")
[[ -n $cmdline ]] || continue
if usb_authorization_cmdline_has_parameter "$cmdline" "$kernel_parameter"; then
[[ $expected == "disabled" ]] || continue
else
[[ $expected == "enabled" ]] || continue
fi
# Multi-profile images and signed PCR policies need their original builder.
# Do not silently invalidate their additional boot/unlock guarantees.
for section in .profile .pcrsig; do
objcopy -O binary --only-section="$section" "$image" "$staging/section" || return 1
if [[ -s $staging/section ]]; then
echo "Rebuild archived image $image with its original signing policy before changing USB boot protection ($section)." >&2
return 1
fi
done
if [[ ! $name =~ ^(.+)_(sha1|sha256|b3|xxh)_([0-9a-f]+)$ ]]; then
echo "Cannot verify the content hash of archived image $image." >&2
return 1
fi
case "${BASH_REMATCH[2]}" in
sha1) hash_command=sha1sum ;;
sha256) hash_command=sha256sum ;;
b3) hash_command=b3sum ;;
xxh) hash_command=xxhsum ;;
esac
old_digest=${BASH_REMATCH[3]}
new_digest=$("$hash_command" "$image") || return 1
if [[ ${new_digest%% *} != "$old_digest" ]]; then
echo "Archived image hash mismatch: $image" >&2
return 1
fi
# Verify the old signature before signing a modified copy with this machine's key.
usb_authorization_verify_signature "$image" || return 1
cp "$image" "$staging/unsigned.efi" || return 1
sbattach --remove "$staging/unsigned.efi" || return 1
# Preserve the embedded boot arguments and change only our exact USB
# parameter. Do not turn a signed command line into an external override.
printf '%s' "$cmdline" | jq -Rrs --arg parameter "$kernel_parameter" --arg expected "$expected" '
[scan("[^[:space:]]+|[[:space:]]+") | select(. != $parameter)]
| join("") | sub("[[:space:]]+$"; "")
| if $expected == "enabled" then . + " " + $parameter else . end
' >"$staging/new-cmdline" || return 1
printf '\0' >>"$staging/new-cmdline"
# objcopy --update-section retains PE VirtualSize and can truncate a longer
# command line. Add a fresh section after the existing virtual image instead.
layout=$(objdump -p "$staging/unsigned.efi" | awk '
$1 == "ImageBase" { base = $2 }
$1 == "SizeOfImage" { size = $2 }
$1 == "SectionAlignment" { alignment = $2 }
END {
if (base !~ /^[0-9a-fA-F]+$/ || size !~ /^[0-9a-fA-F]+$/ || alignment !~ /^[0-9a-fA-F]+$/) exit 1
print "0x" base, "0x" size, "0x" alignment
}
') || return 1
read -r image_base image_size alignment <<<"$layout"
(( alignment > 0 )) || return 1
address=$((image_base + (image_size + alignment - 1) / alignment * alignment))
objcopy --remove-section=.cmdline "$staging/unsigned.efi" "$staging/without-cmdline.efi" || return 1
objcopy --add-section .cmdline="$staging/new-cmdline" \
--change-section-vma .cmdline="$address" \
--set-section-flags .cmdline=alloc,load,readonly,data,contents \
"$staging/without-cmdline.efi" "$staging/image.efi" || return 1
sbctl sign "$staging/image.efi" || return 1
usb_authorization_verify_signature "$staging/image.efi" || return 1
objcopy -O binary --only-section=.cmdline "$staging/image.efi" "$staging/cmdline" || return 1
cmp -s "$staging/cmdline" "$staging/new-cmdline" || return 1
new_digest=$("$hash_command" "$staging/image.efi") || return 1
replacement="${name%"$old_digest"}${new_digest%% *}"
old_hash=$(b2sum "$image") || return 1
new_hash=$(b2sum "$staging/image.efi") || return 1
# Publish copies first: every old reference remains usable until its manifest changes.
if [[ -e $history/$replacement || -L $history/$replacement ]]; then
[[ ! -L $history/$replacement ]] && cmp -s "$staging/image.efi" "$history/$replacement" || return 1
else
mv "$staging/image.efi" "$history/$replacement" || return 1
fi
jq --arg old "$name" --arg new "$replacement" \
--arg old_hash "${old_hash%% *}" --arg new_hash "${new_hash%% *}" \
'. + [{old: $old, new: $new, old_hash: $old_hash, new_hash: $new_hash}]' \
"$staging/replacements.json" >"$staging/next.json" || return 1
mv "$staging/next.json" "$staging/replacements.json" || return 1
done <<<"$names"
jq -e 'length > 0' "$staging/replacements.json" >/dev/null || return 0
originals=("$config" "${manifests[@]}")
for ((index=0; index<${#originals[@]}; index++)); do
cp --preserve=mode,ownership "${originals[index]}" "$staging/original.$index" || return 1
cp --preserve=mode,ownership "${originals[index]}" "$staging/updated.$index" || return 1
# Update cached menu lines, structured image metadata, restore paths and
# both hashes. The new file name remains a valid Limine content address.
if (( index == 0 )); then
jq -Rrs --slurpfile replacements "$staging/replacements.json" '
split("\n") | map(reduce $replacements[0][] as $r (. ;
if contains($r.old) then split($r.old) | join($r.new) | split($r.old_hash) | join($r.new_hash)
else . end)) | join("\n")
' "$staging/original.$index" >"$staging/updated.$index" || return 1
else
jq --slurpfile replacements "$staging/replacements.json" '
walk(if type == "string" then
reduce $replacements[0][] as $r (. ; split($r.old) | join($r.new) | split($r.old_hash) | join($r.new_hash))
else . end)
' "$staging/original.$index" >"$staging/updated.$index" || return 1
fi
done
published=1
for ((index=0; index<${#originals[@]}; index++)); do
mv -f "$staging/updated.$index" "${originals[index]}" || return 1
done
usb_authorization_enroll_config "$config" "$esp" || return 1
committed=1
)
usb_authorization_prepare_archived_images() (
local expected="$1" config="$2" esp="$3" machine_id="${4:-$(</etc/machine-id)}"
usb_authorization_secure_boot_enabled || return 0
# Package hooks acquire the boot lock too; install this optional tool before
# taking it, never while rewriting archived images.
if omarchy-cmd-missing sbattach; then
omarchy-pkg-add sbsigntools || return 1
fi
exec 9>/run/lock/boot-partition.lock
flock --timeout 30 9 || return 1
limine-snapper-sync --no-mutex --no-hooks || return 1
usb_authorization_reconcile_archived_images "$expected" "$config" "$esp" "$machine_id"
)
usb_authorization_sync_and_verify() (
local expected="$1" config="$2" esp="$3"
local machine_id="${4:-$(</etc/machine-id)}" manifest
# Share Limine's lock so a concurrent watcher cannot restore an old manifest
# while we update it. The sync wrapper must not reacquire our lock.
exec 9>/run/lock/boot-partition.lock
flock --timeout 30 9 || return 1
# Recover/upgrade the manifest first, including backups if necessary.
limine-snapper-sync --no-mutex --no-hooks || return 1
usb_authorization_reconcile_archived_images "$expected" "$config" "$esp" "$machine_id" || return 1
for manifest in \
"$esp/$machine_id/limine_history/snapshots.json" \
"$esp/$machine_id/limine_history/snapshots.json.old" \
"/var/cache/boot/$machine_id/lss/snapshots.json"; do
usb_authorization_rewrite_snapshot_manifest "$expected" "$manifest" || return 1
done
usb_authorization_rewrite_boot_cmdlines "$expected" "$config" "$machine_id" || return 1
# Verify the regenerated menu, so synchronization cannot mask stale stored
# command lines with a one-time edit to limine.conf.
limine-snapper-sync --no-mutex --no-hooks || return 1
usb_authorization_enroll_config "$config" "$esp" || return 1
usb_authorization_verify_boot_entries "$expected" "$config" "$esp" "$machine_id"
)
# A Mac's boot chain belongs to its boot package, so boot-time denial is not
# managed here. Disabling what is not configured succeeds, so turning USB
# authorization off still works; anything configured by another route is left
# to the owner, since this cannot rebuild a Mac's boot files.
usb_authorization_apple_boot() {
if [[ ${1:-} == "enable" ]]; then
echo "USB authorization from boot is not available on Apple Silicon yet." >&2
return 2
fi
if [[ -e $drop_in ]] || grep -Fqx "$snapshot_begin" "$limine_defaults" 2>/dev/null; then
echo "Boot-time USB authorization is configured in $drop_in or $limine_defaults, which this command cannot remove on Apple Silicon." >&2
echo "Remove it by hand: delete $drop_in and the lines from '$snapshot_begin' to '$snapshot_end' in $limine_defaults, then rebuild the boot files." >&2
return 1
fi
}
usb_authorization_main() {
local had_drop_in=0 had_snapshot_setting=0 snapshot_status temporary
if (( EUID != 0 )); then
echo "Error: omarchy-usb-authorization-boot must run as root" >&2
exit 1
fi
case "${1:-}" in
enable)
if usb_authorization_snapshot_block_status "$limine_defaults"; then
snapshot_status=0
else
snapshot_status=$?
fi
if (( snapshot_status == 2 )); then
echo "Refusing to modify the USB authorization block in $limine_defaults." >&2
exit 1
fi
# Quote the expected value because the Limine key contains glob brackets.
if [[ -e $drop_in ]] && [[ $(<"$drop_in") != "$setting" ]]; then
echo "Refusing to replace the existing $drop_in." >&2
exit 1
fi
usb_authorization_prepare_archived_images enabled "$limine_conf" "$boot_path" || exit 1
usb_authorization_enable_snapshot_setting "$limine_defaults"
if [[ ! -e $drop_in ]]; then
install -d -m755 "${drop_in%/*}"
temporary=$(mktemp "${drop_in}.XXXXXXXXXX")
trap 'rm -f "$temporary"' EXIT
printf '%s\n' "$setting" >"$temporary"
chmod 644 "$temporary"
mv -f "$temporary" "$drop_in"
trap - EXIT
fi
if ! limine-mkinitcpio ||
! usb_authorization_sync_and_verify enabled "$limine_conf" "$boot_path"; then
echo "Boot-time USB authorization remains configured in $drop_in; rerun this command after fixing the boot-image error." >&2
exit 1
fi
;;
disable)
if usb_authorization_snapshot_block_status "$limine_defaults"; then
had_snapshot_setting=1
else
snapshot_status=$?
if (( snapshot_status == 2 )); then
echo "Refusing to modify the USB authorization block in $limine_defaults." >&2
exit 1
fi
fi
if [[ -e $drop_in ]]; then
if [[ $(<"$drop_in") != "$setting" ]]; then
echo "Refusing to remove the modified $drop_in." >&2
exit 1
fi
had_drop_in=1
fi
usb_authorization_prepare_archived_images disabled "$limine_conf" "$boot_path" || exit 1
if (( had_drop_in )); then
rm -f "$drop_in"
fi
usb_authorization_disable_snapshot_setting "$limine_defaults"
if ! limine-mkinitcpio ||
! usb_authorization_sync_and_verify disabled "$limine_conf" "$boot_path"; then
if (( had_drop_in )); then
printf '%s\n' "$setting" >"$drop_in"
chmod 644 "$drop_in"
fi
if (( had_snapshot_setting )); then
usb_authorization_enable_snapshot_setting "$limine_defaults"
fi
echo "USBGuard remains required because the boot images could not be verified as default-allow." >&2
exit 1
fi
;;
*)
echo "Usage: omarchy-usb-authorization-boot <enable|disable>" >&2
exit 2
;;
esac
}
main() {
if (( EUID != 0 )); then
echo "Error: omarchy-usb-authorization-boot must run as root" >&2
return 1
fi
case "${1:-}" in
enable|disable)
if /usr/bin/omarchy-hw-aarch64-apple; then
usb_authorization_apple_boot "$1"
elif usb_authorization_secure_boot_enabled; then
usb_authorization_boot_transaction "$1"
else
usb_authorization_main "$1"
fi
;;
*)
echo "Usage: omarchy-usb-authorization-boot <enable|disable>" >&2
return 2
;;
esac
}
if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
main "$@"
fi
Loaded 100 of 346 files, more files were not shown because too many files have changed in this diff. Show more