Compare commits

..
Author SHA1 Message Date
ZacharyZhang-NY 75d9ec05e9 T36 passed after two codex rounds 2026-10-12 03:07:13 -04:00
ZacharyZhang-NY 45a27629de Plan: the open PRs 2026-10-12 02:58:23 -04:00
ZacharyZhang-NY d6bea2aeba Check off T35 after two codex rounds 2026-10-12 02:57:07 -04:00
ZacharyZhang-NY 70fb355250 Plan: the full fontconfig test passes on the T34 VM 2026-10-12 02:57:07 -04:00
ZacharyZhang-NY c9494b27ec Plan: acceptance checklist for T28–T34 2026-10-12 02:57:07 -04:00
ZacharyZhang-NY a77ea36fe5 Plan: T34 evidence 2026-10-12 02:57:07 -04:00
ZacharyZhang-NY e9a3f13b9a Check off T33 after one codex round 2026-10-12 02:57:07 -04:00
ZacharyZhang-NY e2dab6ef14 Plan: the ISO PR branch and omarchy-pkgs 2026-10-12 02:57:07 -04:00
ZacharyZhang-NY 7dbaa4914f Check off T32 after two codex rounds 2026-10-12 02:57:07 -04:00
ZacharyZhang-NY 1dbe97d9b7 Refuse the China region off x86 until #13690 renders ARM pacman
Integration only.
2026-10-12 02:57:07 -04:00
ZacharyZhang-NY 84450ffc19 Plan: the PR branches and the integration branch 2026-10-12 02:57:07 -04:00
ZacharyZhang-NY b4de1c0d34 Render x86 pacman through #13690 on refresh, and on install only with a region
Integration only, until omarchy #13690 rebases onto the ARM pacman
templates. pacman-templates-test still expects x86 refresh to copy
the templates directly.
2026-10-12 02:57:07 -04:00
ZacharyZhang-NY c90afe24dc Carry the OmarchyCN task list 2026-10-12 02:57:07 -04:00
ZacharyZhang-NY f5ab724a1c Word Setup > Region and China Apps in Chinese
Needs both the Chinese language and the region switch: the Chinese
labels for Region and China Apps, the region switch's logout prompt
through omarchy-text, and the language half of manual/52-china.md.
2026-10-12 02:57:07 -04:00
ZacharyZhang-NY 096982fe69 Merge cn-region-switch (with omarchy #13690)
# Conflicts:
#	AGENTS.md
#	bin/omarchy-refresh-pacman
#	default/omarchy/omarchy-menu.jsonc
#	install/post-install/pacman.sh
#	shell/plugins/menu/MenuModel.js
#	test/shell.d/menu-test.sh
2026-10-12 02:57:00 -04:00
ZacharyZhang-NY 96e2effbe1 Merge chinese-language 2026-10-12 02:56:55 -04:00
ZacharyZhang-NY 846f7d491f Merge cjk-variant-selection 2026-10-12 02:56:55 -04:00
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
ZacharyZhang-NY 16faf06fcf Switch the region under Setup > Region, and offer China Apps
omarchy region cn|global switches an installed machine: cn upgrades,
trusts ArchLinuxCN through the rendered China config, records the
marker and links the developer mirrors, then refreshes pacman;
global undoes only what Omarchy made. The developer mirrors reach
new sessions only, so a terminal switch offers to log out.

Install > China Apps, shown only in China, installs WeChat, QQ,
Feishu, DingTalk, Tencent Meeting and WPS Office from the AUR and
sets up Tencent Docs, Yuque and Shimo as web apps.

manual/52-china.md describes the China region.
2026-10-12 00:29:40 -04:00
ZacharyZhang-NY 466c2ca14a Point package managers at China mirrors on cn installs
npm, PyPI and uv, Go, rustup and crates.io, RubyGems and Bundler,
and Maven reach their registries slowly or not at all from China.
On a cn install, link the profile's environment, cargo and gem
drop-ins into place. A regular file the admin wrote is never
replaced; the install fails instead.
2026-10-12 00:29:13 -04:00
Kros DaiandClaude Opus 5.5 ec4579a461 🐛 fix(test): skip exact zone match without tzdata.zi
- 🐛 minimal containers lack the link table, so the exact china zone check failed on a correct profile

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuATgjU4mnqpqSLAKMQ14g
2026-10-08 16:44:15 -04:00
Kros DaiandClaude Opus 5.5 d9a781e3c9 📝 docs(region): scope the refresh render boundary
- 📝 state that same-user code is outside the refresh boundary, so reviewers see the trade-off was deliberate

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuATgjU4mnqpqSLAKMQ14g
2026-10-08 16:08:36 -04:00
Kros DaiandClaude Opus 5.5 a6c3480ff8 📝 docs(region): state marker mode and archlinuxcn risk
- 📝 refresh reads the marker unprivileged, and rolling archlinuxcn packages can outpace stable snapshots

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuATgjU4mnqpqSLAKMQ14g
2026-10-08 15:31:04 -04:00
Kros DaiandClaude Opus 5.5 545190596d ♻️ refactor(pacman): branch on render with if/else
- ♻️ follow the repo style instead of exiting early to skip the install block

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuATgjU4mnqpqSLAKMQ14g
2026-10-08 15:31:04 -04:00
Kros DaiandClaude Opus 5.5 b381093e32 ✅ test(region): derive china zones from tzdata
- ✅ dropping a backward-compatible zone name from the profile now fails the test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuATgjU4mnqpqSLAKMQ14g
2026-10-08 15:31:04 -04:00
Kros DaiandClaude Opus 5.5 9d74c813f1 🐛 fix(channel): require every region fragment for dev
- 🐛 leaving dev refreshes stable or rc with the checkout's tools, so a profile missing those fragments would drop regional repositories
- ✅ refuse a profile missing any single fragment

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuATgjU4mnqpqSLAKMQ14g
2026-10-08 15:31:04 -04:00
Kros DaiandClaude Opus 5.5 fce85abf48 📝 docs(region): explain mirror order and fallback
- 📝 pacman's three-error skip and first-server databases decide what the china replicas must serve

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuATgjU4mnqpqSLAKMQ14g
2026-10-08 15:21:51 -04:00
Kros DaiandClaude Opus 5.5 3c68a1bb04 ✨ feat(region): put the china channel mirrors first
- ✨ omarchy now runs per-channel china replicas, so they should lead instead of trailing as a fallback
- ♻️ prepend per-channel fragments so each channel's own mirror stays the fallback
- 🔥 drop the ustc arch mirror, which was never a stable or rc snapshot replica

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuATgjU4mnqpqSLAKMQ14g
2026-10-08 15:21:51 -04:00
Kros DaiandClaude Opus 5.5 b89def6c7f 📝 docs(region): describe timezone-derived regions
- 📝 the --region build flag is gone, so the installer contract and validation plan change with it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuATgjU4mnqpqSLAKMQ14g
2026-10-08 15:03:46 -04:00
Kros DaiandClaude Opus 5.5 0de49b231a ✨ feat(region): map timezones to regions
- ✨ one iso serves every region, so the installer derives it from the timezone the user already picks
- ✅ reject unknown zones and zones claimed by two regions

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuATgjU4mnqpqSLAKMQ14g
2026-10-08 15:03:41 -04:00
Kros DaiandClaude Opus 5.5 3e00a48c00 ✅ test(pacman): sandbox the region for every boundary test
- ✅ The channel and update boundary tests copied the real render and dev-region guard, so on a China install they read the host's `/etc/omarchy/region` and failed before reaching their checks
- ♻️ The shared fixture now points both lookups at a sandbox target that starts global, and the regional test only writes `cn` there

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SdXqTwm63rRMwotZ1xqLC8
2026-10-06 22:14:56 -04:00
Kros DaiandClaude Opus 5.5 a1bdc46119 ✅ test(pacman): isolate the regional refresh boundary test
- ✅ the render read the host's region marker, so a global machine never rendered China and a China machine failed on missing fragments
- ✅ check the finished China files root installs, not only the copy order

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SdXqTwm63rRMwotZ1xqLC8
2026-10-06 22:00:59 -04:00
Kros DaiandClaude Opus 5.5 7a60c8dea4 🐛 fix(channel): require a usable region profile for dev
- 🐛 an empty profile directory or a helper that cannot run passed the guard, linked dev, and then broke the refresh
- ✅ the success case now uses a complete profile

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SdXqTwm63rRMwotZ1xqLC8
2026-10-06 22:00:59 -04:00
Kros DaiandClaude Opus 5.5 6be88d14a5 🐛 fix(pacman): remove the render dir on every refresh exit
- 🐛 a failed render, a refused sudo prompt, or a signal left the scratch directory behind; repeated attempts piled them up
- 🔒 the cleanup runs before the sudo revocation, which stays the last step of the exit path

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SdXqTwm63rRMwotZ1xqLC8
2026-10-06 21:58:48 -04:00
Kros DaiandClaude Opus 5.5 50a26be6a7 📝 docs(region): scope the cn profile to repositories
- 📝 frame USTC and ArchLinuxCN as a bridge until an official China mirror exists
- 📝 record that language, timezone, and input defaults follow as separate changes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3Jna1ogUUviYLJzhsG6Vr
2026-10-06 14:43:25 -04:00
Kros DaiandClaude Opus 5.5 d5ef1c1d15 🔥 refactor(region): drop input defaults from the cn profile
- 🔥 keep this change to the regional build architecture; Chinese input gets its own proposal instead of fixing one engine here
- ✅ assert the shipped profile adds only its repository keyring

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3Jna1ogUUviYLJzhsG6Vr
2026-10-06 14:43:25 -04:00
Kros DaiandClaude Opus 5.5 d024c82a9d 📝 docs(plans): move the china proposal to a discussion
- 📝 upstream plans are implementation plans; the open questions belong in the discussion that links these changes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3Jna1ogUUviYLJzhsG6Vr
2026-10-06 14:43:25 -04:00
Kros DaiandClaude Opus 5.5 d7f17a0424 ♻️ refactor(pacman): render regional config without root
- 🔒 upstream refresh now gives root only command-scoped copies, so regional files are rendered as the caller and installed with the same four `sudo cp` calls
- ✅ exercise refresh only inside the sudo-boundary sandbox; the old host-level test reached real sudo once refresh pinned its PATH

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 14:43:25 -04:00
Kros DaiandClaude Opus 5.5 87a2931209 📝 docs(region): drop the unreachable menu input gate
- 📝 the menu bypasses the input method, so requiring Pinyin there made the release procedure impossible until upstream fixes it
- 📝 record the required `skel` profile and the dev-switch region check

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 14:43:25 -04:00
Kros DaiandClaude Opus 5.5 647979bde7 🐛 fix(pacman): keep existing config file modes
- 🐛 refresh used to copy over the existing files, so a customized mode must survive the shared regional applicator too

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 14:43:25 -04:00
Kros DaiandClaude Opus 5.5 123535ea14 🐛 fix(channel): keep regional repos when switching to dev
- 🛡️ refuse a dev checkout that lacks this machine's region profile before linking it, since its own refresh would silently replace the regional pacman files
- ✅ isolate channel tests from the host region marker

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 14:43:25 -04:00
Kros Dai 645cfc2a36 📝 docs(region): explain china bootstrap choices
- 📝 clarify optional defaults, trust requirements, and opt-out behavior
- ⚠️ document snapshot compatibility and release validation limits

Amp-Thread-ID: T-01a0df38-0ea7-759b-bcff-29da223cc567
2026-10-06 14:43:25 -04:00
Kros Dai e989960895 ✅ test(region): guard runtime defaults
- ✅ protect global behavior and regional mirror ordering
- ⌨️ guard personal settings and reserved input shortcuts

Amp-Thread-ID: T-01a0df38-0ea7-759b-bcff-29da223cc567
2026-10-06 14:43:25 -04:00
Kros Dai 48f7c411c0 ✨ feat(pacman): preserve regional defaults
- ✨ share regional repository generation across install and refresh
- 🛡️ reject invalid profiles before replacing configuration

Amp-Thread-ID: T-01a0df38-0ea7-759b-bcff-29da223cc567
2026-10-06 14:43:25 -04:00
Kros DaiandAmp 7727d40c92 ✨ feat(region): seed offline chinese input
- ✨ make first-login pinyin available without downloads
- ⌨️ preserve compose and terminal prefix shortcuts

Amp-Thread-ID: https://ampcode.com/threads/T-01a0df38-0ea7-759b-bcff-29da223cc567
Co-authored-by: Amp <amp@ampcode.com>
2026-10-06 14:43:25 -04:00
225 changed files with 3645 additions and 6550 deletions

No files matched your search

+13
View File
@@ -124,6 +124,19 @@ 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.
# Region
- `/etc/omarchy/region` holds the region the installer derived from the timezone (`cn`); no file means Global. The profile lives in `default/regions/cn/`, its contract in `docs/regions.md`.
- `install/config/region.sh` links the `cn` developer mirrors into `/etc` and `/.cargo` and refuses to replace a regular file it did not create. It serves both the install (the marker) and `omarchy region cn|global` (`OMARCHY_REGION`), which also records or removes the marker; Global removes only the links it made.
- `omarchy-region` prints the region (`Global` or `China`) and switches it. Nothing in `omarchy update` or migrations may change the region.
- China-only menu rows carry `"when":"[[ \"$(omarchy-region)\" == \"China\" ]]"` on every row, not only the submenu, because search checks each row's own guard. User-facing docs: `manual/52-china.md`.
# Config Structure # Config Structure
- `config/` - default configs copied to `~/.config/` - `config/` - default configs copied to `~/.config/`
+1
View File
@@ -71,6 +71,7 @@ The manual lives in [`manual/`](manual/), which is its authoritative source.
- [Omarchy on...](manual/49-omarchy-on.md) - [Omarchy on...](manual/49-omarchy-on.md)
- [Dual Boot Install](manual/50-dual-boot-install.md) - [Dual Boot Install](manual/50-dual-boot-install.md)
- [Unattended Installs](manual/51-unattended-installs.md) - [Unattended Installs](manual/51-unattended-installs.md)
- [China](manual/52-china.md)
## License ## License
+4
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
+2
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"
@@ -74,6 +75,7 @@ GROUP_DESCRIPTIONS[plymouth]="Plymouth boot theme management"
GROUP_DESCRIPTIONS[power]="Power supply detection" GROUP_DESCRIPTIONS[power]="Power supply detection"
GROUP_DESCRIPTIONS[powerprofiles]="Power profile management" GROUP_DESCRIPTIONS[powerprofiles]="Power profile management"
GROUP_DESCRIPTIONS[refresh]="Reset config to defaults" GROUP_DESCRIPTIONS[refresh]="Reset config to defaults"
GROUP_DESCRIPTIONS[region]="Region-specific package sources and setup"
GROUP_DESCRIPTIONS[reinstall]="Reinstall and reset workflows" GROUP_DESCRIPTIONS[reinstall]="Reinstall and reset workflows"
GROUP_DESCRIPTIONS[reminder]="Desktop notification reminders" GROUP_DESCRIPTIONS[reminder]="Desktop notification reminders"
GROUP_DESCRIPTIONS[remove]="Removal workflows" GROUP_DESCRIPTIONS[remove]="Removal workflows"
+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
+77
View File
@@ -0,0 +1,77 @@
#!/bin/bash
# omarchy:summary=Restore channel and regional pacman defaults in an installed target
# omarchy:args=<stable|rc|edge> [target-root] | --render <stable|rc|edge> <output-dir> [target-root]
# omarchy:requires-sudo=true
# omarchy:hidden=true
set -euo pipefail
usage() {
echo "Usage: omarchy-apply-pacman <stable|rc|edge> [target-root]" >&2
echo " omarchy-apply-pacman --render <stable|rc|edge> <output-dir> [target-root]" >&2
exit 1
}
# --render only builds the two files, so an unprivileged caller can hand root
# nothing but plain copies of the finished result.
render_dir=""
if [[ ${1:-} == "--render" ]]; then
(( $# == 3 || $# == 4 )) || usage
channel="$2"
render_dir="$3"
[[ -d $render_dir ]] || { echo "Render directory not found: $render_dir" >&2; exit 1; }
etc="${4:-/}/etc"
else
(( $# >= 1 && $# <= 2 )) || usage
channel="$1"
etc="${2:-/}/etc"
fi
case "$channel" in
stable|rc|edge) ;;
*) echo "Invalid pacman channel: $channel" >&2; exit 1 ;;
esac
# The ISO writes this plain-text marker before target finalization. Missing
# markers keep existing installations global; never source it as shell code.
region=global
if [[ -e $etc/omarchy/region ]]; then
region=$(<"$etc/omarchy/region")
fi
case "$region" in
global|cn) ;;
*) echo "Unsupported region: $region (expected global or cn)" >&2; exit 1 ;;
esac
# Build both files before replacing either. Explicit refreshes retain backups;
# ordinary updates do not call this command or enforce regional defaults.
if [[ -n $render_dir ]]; then
staging="$render_dir"
else
staging=$(mktemp -d)
trap 'rm -rf "$staging"' EXIT
fi
cp "$OMARCHY_PATH/default/pacman/pacman-$channel.conf" "$staging/pacman.conf"
if [[ $region == "global" ]]; then
cp "$OMARCHY_PATH/default/pacman/mirrorlist-$channel" "$staging/mirrorlist"
else
profile="$OMARCHY_PATH/default/regions/$region/pacman"
printf '\n' >> "$staging/pacman.conf"
cat "$profile/pacman.conf.append" >> "$staging/pacman.conf"
# pacman tries servers in order, so the regional mirror leads and the
# channel's own mirror stays as the fallback.
cat "$profile/mirrorlist-$channel.prepend" "$OMARCHY_PATH/default/pacman/mirrorlist-$channel" > "$staging/mirrorlist"
fi
if [[ -z $render_dir ]]; then
mkdir -p "$etc/pacman.d"
for file in pacman.conf pacman.d/mirrorlist; do
if [[ -e $etc/$file ]]; then
cp -f "$etc/$file" "$etc/$file.bak"
fi
done
# Copying over an existing file keeps its owner and mode, as refresh always has.
cp -f "$staging/pacman.conf" "$etc/pacman.conf"
cp -f "$staging/mirrorlist" "$etc/pacman.d/mirrorlist"
fi
+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
+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")"
+22
View File
@@ -47,9 +47,31 @@ validate_dev_checkout() {
fi fi
} }
# The dev channel refreshes pacman with the checkout's own tools. A checkout
# without this machine's region profile would silently drop its repositories.
validate_dev_region() {
local checkout="$1" region_file="${OMARCHY_REGION_FILE:-/etc/omarchy/region}" region=global
[[ -f $region_file ]] && region=$(<"$region_file")
[[ $region == "global" ]] && return 0
# The refresh after linking needs a runnable helper and every fragment, not
# just a directory with the region's name: dev refreshes edge, and leaving
# dev later refreshes stable or rc with this checkout's tools.
local profile="$checkout/default/regions/$region/pacman" fragment supported=true
[[ -x $checkout/bin/omarchy-apply-pacman ]] || supported=false
for fragment in pacman.conf.append mirrorlist-stable.prepend mirrorlist-rc.prepend mirrorlist-edge.prepend; do
[[ -r $profile/$fragment ]] || supported=false
done
if ! $supported; then
fail "$checkout does not support the '$region' region, so switching would drop its package repositories. Check out a branch with default/regions/$region, or remove $region_file to use global defaults."
fi
}
link_dev_checkout() { link_dev_checkout() {
local checkout="$1" required local checkout="$1" required
[[ -d $checkout/.git ]] || git clone https://github.com/omacom/omarchy.git "$checkout" [[ -d $checkout/.git ]] || git clone https://github.com/omacom/omarchy.git "$checkout"
validate_dev_region "$checkout"
# Check the destination before changing /etc/omarchy.conf or sudo's path. # Check the destination before changing /etc/omarchy.conf or sudo's path.
# An existing checkout is not pulled automatically and may predate this policy. # An existing checkout is not pulled automatically and may predate this policy.
+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")"
+3 -3
View File
@@ -20,9 +20,9 @@ if (( $# == 0 )); then
fi fi
case $1 in case $1 in
theme) LABEL="Corners follow the theme" ;; theme) LABEL=$(omarchy-text "Corners follow the theme") ;;
sharp) LABEL="Sharp corners for all themes" ;; sharp) LABEL=$(omarchy-text "Sharp corners for all themes") ;;
rounded) LABEL="Rounded corners for all themes" ;; rounded) LABEL=$(omarchy-text "Rounded corners for all themes") ;;
*) *)
echo "Usage: omarchy-hyprland-corners [theme|sharp|rounded]" >&2 echo "Usage: omarchy-hyprland-corners [theme|sharp|rounded]" >&2
exit 1 exit 1
+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
} }
@@ -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
+3 -3
View File
@@ -37,9 +37,9 @@ if (( $# == 0 )); then
fi fi
case $1 in case $1 in
opaque) LABEL="Opaque windows" ;; opaque) LABEL=$(omarchy-text "Opaque windows") ;;
transparent) LABEL="Transparent windows" ;; transparent) LABEL=$(omarchy-text "Transparent windows") ;;
glassy) LABEL="Glassy windows" ;; glassy) LABEL=$(omarchy-text "Glassy windows") ;;
*) *)
echo "Usage: omarchy-hyprland-window-transparency [opaque|transparent|glassy]" >&2 echo "Usage: omarchy-hyprland-window-transparency [opaque|transparent|glassy]" >&2
exit 1 exit 1
+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() {
@@ -32,4 +32,4 @@ printf 'o.workspace_mode({ default = "%s" })\n' "$1" >"$DEFAULT_FILE"
hyprctl reload >/dev/null hyprctl reload >/dev/null
omarchy-shell -q omarchy.workspaces refresh omarchy-shell -q omarchy.workspaces refresh
omarchy-notification-send -g 󱂬 "New workspaces use the $LABEL layout" omarchy-notification-send -g 󱂬 "$(omarchy-text "New workspaces use the %1 layout" "$LABEL")"
+4 -4
View File
@@ -43,17 +43,17 @@ if hyprctl eval "$APPLY" >/dev/null 2>&1; then
fi fi
if (( ! APPLIED )); then if (( ! APPLIED )); then
omarchy-notification-send -g 󱂬 "Workspace layout needs a Hyprland reload" 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 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 # Cycled onto the default, a workspace goes back to following it, so a later
# change of the default reaches it too. # change of the default reaches it too.
omarchy-notification-send -g 󱂬 "Workspace layout set to $NEW_MODE" 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 elif mkdir -p "$MODES_DIR" && printf '%s\n' "$APPLY" >"$MODE_FILE"; then
omarchy-notification-send -g 󱂬 "Workspace layout set to $NEW_MODE" omarchy-notification-send -g 󱂬 "$(omarchy-text "Workspace layout set to %1" "$NEW_MODE")"
else else
# The mode is on screen but nothing will bring it back, and the next press # 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. # reads the mode from this file, so say so rather than counting it as set.
omarchy-notification-send -g 󱂬 "Workspace layout set to $NEW_MODE but could not be saved" omarchy-notification-send -g 󱂬 "$(omarchy-text "Workspace layout set to %1 but could not be saved" "$NEW_MODE")"
fi fi
# The bar shows the focused workspace's layout. # The bar shows the focused workspace's layout.
+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
+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.
-9
View File
@@ -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"
+20 -2
View File
@@ -42,11 +42,29 @@ if [[ ! -f $templates/pacman-$channel.conf || ! -f $templates/mirrorlist-$channe
exit 1 exit 1
fi fi
conf=$templates/pacman-$channel.conf mirrorlist=$templates/mirrorlist-$channel
# Regions render x86 pacman only (omarchy #13690).
if [[ $platform == x86 ]]; then
# Render the channel templates plus this machine's regional additions as the
# caller, so root only copies finished files. The scratch directory goes on
# every exit, including a failed render, a refused sudo prompt, or a signal;
# revoking the sudo timestamp stays the last thing the exit path does.
rendered=$(mktemp -d)
cleanup_refresh() {
local status=$?
rm -rf "$rendered"
omarchy_security_exit_with_revoked_sudo "$status"
}
trap cleanup_refresh EXIT
"$OMARCHY_PATH/bin/omarchy-apply-pacman" --render "$channel" "$rendered"
conf=$rendered/pacman.conf mirrorlist=$rendered/mirrorlist
fi
sudo cp -f /etc/pacman.conf /etc/pacman.conf.bak sudo cp -f /etc/pacman.conf /etc/pacman.conf.bak
sudo cp -f /etc/pacman.d/mirrorlist /etc/pacman.d/mirrorlist.bak sudo cp -f /etc/pacman.d/mirrorlist /etc/pacman.d/mirrorlist.bak
echo "Setting channel to $channel" echo "Setting channel to $channel"
sudo cp -f "$templates/pacman-$channel.conf" /etc/pacman.conf sudo cp -f "$conf" /etc/pacman.conf
sudo cp -f "$templates/mirrorlist-$channel" /etc/pacman.d/mirrorlist sudo cp -f "$mirrorlist" /etc/pacman.d/mirrorlist
# Allow user customization of /etc/pacman.conf before the upgrade runs, so # Allow user customization of /etc/pacman.conf before the upgrade runs, so
# custom repositories and IgnorePkg entries shape the downgrade-capable # custom repositories and IgnorePkg entries shape the downgrade-capable
+63
View File
@@ -0,0 +1,63 @@
#!/bin/bash
# omarchy:summary=Show or switch the region this machine is set up for
# omarchy:args=[global|cn]
# omarchy:examples=omarchy region | omarchy region cn
# omarchy:requires-sudo=true
set -euo pipefail
region_file=/etc/omarchy/region
if (($# == 0)); then
if [[ ! -e $region_file ]]; then
echo "Global"
elif [[ $(<"$region_file") == "cn" ]]; then
echo "China"
else
echo "Unknown region in $region_file: $(<"$region_file")" >&2
exit 1
fi
exit 0
fi
if [[ $1 != "global" && $1 != "cn" ]]; then
echo "Usage: omarchy-region [global|cn]" >&2
exit 1
fi
channel=$(omarchy-version-channel)
if [[ $channel != "stable" && $channel != "rc" && $channel != "edge" ]]; then
echo "Cannot tell this machine's package channel ($channel); run omarchy channel set first" >&2
exit 1
fi
# The cn profile renders x86 pacman only (omarchy #13690).
if [[ $1 == "cn" && $(omarchy-hw-platform) != "x86" ]]; then
echo "The China region needs an x86 machine; $(omarchy-hw-platform) has no China profile yet" >&2
exit 1
fi
if [[ $1 == "cn" ]]; then
# Upgrade first: the keyring install below syncs new databases.
omarchy-update-pacman -Syu --noconfirm
sudo env OMARCHY_REGION=cn OMARCHY_PATH="$OMARCHY_PATH" bash -e "$OMARCHY_PATH/install/config/region.sh"
# Trust ArchLinuxCN before the refresh upgrades through it.
rendered=$(mktemp -d)
trap 'rm -rf "$rendered"' EXIT
omarchy-apply-pacman --render "$channel" "$rendered"
mapfile -t profile_packages < <(grep -Ev '^[[:space:]]*(#|$)' "$OMARCHY_PATH/default/regions/cn/packages")
sudo pacman --config "$rendered/pacman.conf" -Sy --noconfirm --needed "${profile_packages[@]}"
sudo pacman-key --populate archlinuxcn
omarchy-refresh-pacman "$channel"
else
sudo env OMARCHY_REGION=global OMARCHY_PATH="$OMARCHY_PATH" bash -e "$OMARCHY_PATH/install/config/region.sh"
omarchy-refresh-pacman "$channel"
fi
# The developer mirrors reach only a new session, so offer one.
if [[ -t 0 && -t 1 ]] && gum confirm --affirmative "$(omarchy-text "Log out now")" --negative "$(omarchy-text "Later")" "$(omarchy-text "Log out now to switch the developer mirrors?")"; then
omarchy-system-logout
else
omarchy-text "Log back in to switch the developer mirrors."
fi
+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"
@@ -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
-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
+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"
+1 -1
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
@@ -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
+2 -2
View File
@@ -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
+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
-137
View File
@@ -1,137 +0,0 @@
#!/bin/bash
# omarchy:hidden=true
# omarchy:summary=Create an approval request for a blocked USB device
set -euo pipefail
case "${USBGUARD_IPC_SIGNAL:-}" in
IPC.Connected) ;;
Device.PolicyApplied)
# PresenceChanged precedes policy evaluation, even for trusted devices.
[[ ${USBGUARD_DEVICE_TARGET_NEW:-} == "block" ]] || exit 0
;;
Device.PresenceChanged)
[[ ${USBGUARD_DEVICE_EVENT:-} == "Remove" ]] || exit 0
;;
*) exit 0 ;;
esac
requests="$HOME/.local/state/omarchy/usb-authorization/requests"
mkdir -p "$requests"
chmod 700 "${requests%/*}" "$requests"
exec 9>"$requests/.lock"
request_generation() {
local daemon_id
# USBGuard reuses device IDs after restarting. Include both the daemon
# invocation and watcher lifetime so old prompts cannot suppress new ones.
daemon_id=$(systemctl show usbguard.service --property=InvocationID --value) || return 1
[[ $daemon_id =~ ^[0-9a-f]{32}$ ]] || return 1
printf '%s:%s' "${OMARCHY_USB_AUTHORIZATION_WATCH_ID:-standalone}" "$daemon_id"
}
if [[ $USBGUARD_IPC_SIGNAL == "IPC.Connected" ]]; then
flock 9
generation=$(request_generation) || exit 1
for pending in "$requests"/request-*.json; do
[[ -f $pending ]] || continue
if ! jq -e --arg generation "$generation" '.generation == $generation' "$pending" >/dev/null 2>&1; then
rm -f "$pending"
fi
done
flock -u 9
# USBGuard scans present devices before accepting IPC connections. Repeat
# this on every reconnect: temporary approvals disappear on daemon restart.
blocked=$(usbguard list-devices --blocked) || exit 1
while IFS= read -r line; do
if [[ $line =~ ^([0-9]+):[[:space:]](block.*)$ ]]; then
USBGUARD_IPC_SIGNAL=Device.PolicyApplied \
USBGUARD_DEVICE_ID="${BASH_REMATCH[1]}" \
USBGUARD_DEVICE_TARGET_NEW=block \
USBGUARD_DEVICE_RULE="${BASH_REMATCH[2]}" \
"$OMARCHY_PATH/bin/omarchy-usb-authorization-event" || true
fi
done <<<"$blocked"
exit 0
fi
[[ ${USBGUARD_DEVICE_ID:-} =~ ^[0-9]+$ ]] || exit 0
[[ -n ${USBGUARD_DEVICE_RULE:-} ]] || exit 0
# A removed device's final target may be allow even though a blocked request
# remains. Its identity attributes are unchanged; normalize the target.
USBGUARD_DEVICE_RULE="block ${USBGUARD_DEVICE_RULE#* }"
# The token is safe to carry through the presentation launcher's command
# string. The device-controlled rule itself stays in a mode-0600 JSON file and
# is never interpolated into a shell command.
token="request-$(printf '%s\0%s' "$USBGUARD_DEVICE_ID" "$USBGUARD_DEVICE_RULE" | sha256sum | cut -d' ' -f1)"
request="$requests/$token.json"
# watch executes handlers concurrently. A reconnect scan and a policy event
# can refer to the same device; publish only one pending request.
if [[ $USBGUARD_IPC_SIGNAL == "Device.PresenceChanged" ]]; then
flock 9
blocked=$(usbguard list-devices --blocked) || exit 1
# A delayed removal event must not erase a request for a reappeared device.
if ! grep -Fqx -- "$USBGUARD_DEVICE_ID: $USBGUARD_DEVICE_RULE" <<<"$blocked"; then
rm -f "$request"
fi
exit 0
fi
# Keep retrying without needing another USB event. Handlers belong to the
# watcher service and are stopped with the graphical session.
while true; do
flock 9
generation=$(request_generation) || exit 1
if [[ -f $request ]] && jq -e --arg generation "$generation" '.generation == $generation' "$request" >/dev/null 2>&1; then
exit 0
fi
# Discard events overtaken by another policy decision or device replacement.
blocked=$(usbguard list-devices --blocked) || exit 1
current=0
while IFS= read -r line; do
if [[ $line == "$USBGUARD_DEVICE_ID: $USBGUARD_DEVICE_RULE" ]]; then
current=1
break
fi
done <<<"$blocked"
(( current )) || exit 0
# Login and shell restarts can leave D-Bus alive before notifications are ready.
# Bound each wait so disappeared devices are rechecked between attempts.
if ! omarchy-notification-wait 1; then
flock -u 9
sleep 1
continue
fi
temporary=$(mktemp "$requests/.request.XXXXXXXXXX")
trap 'rm -f "$temporary"' EXIT
jq -n \
--arg id "$USBGUARD_DEVICE_ID" \
--arg rule "$USBGUARD_DEVICE_RULE" \
--arg generation "$generation" \
'{id: $id, rule: $rule, generation: $generation}' >"$temporary"
chmod 600 "$temporary"
mv -f "$temporary" "$request"
if omarchy-notification-send \
--app-name omarchy-action \
--urgency critical \
--glyph 󰖀 \
"USB accessory blocked" \
"Click to review it. Keep it blocked unless you recognize the device." \
--exec omarchy-launch-floating-terminal-with-presentation \
omarchy-usb-authorization-review "$token"; then
exit 0
fi
rm -f "$request"
flock -u 9
sleep 1
done
@@ -1,23 +0,0 @@
#!/bin/bash
# omarchy:summary=Restore the kernel's default USB authorization state
# omarchy:requires-sudo=true
# omarchy:hidden=true
set -euo pipefail
if (( EUID != 0 )); then
echo "Error: omarchy-usb-authorization-restore-default must run as root" >&2
exit 1
fi
# USBGuard intentionally leaves root hubs in default-deny mode when it exits.
# Restore both the hub defaults and devices that are still connected.
shopt -s nullglob
for attribute in /sys/bus/usb/devices/usb*/authorized_default; do
echo 1 >"$attribute"
done
for attribute in /sys/bus/usb/devices/*/authorized; do
echo 1 >"$attribute"
done
shopt -u nullglob
-142
View File
@@ -1,142 +0,0 @@
#!/bin/bash
# omarchy:hidden=true
# omarchy:summary=Review a blocked USB device authorization request
set -euo pipefail
token="${1:-}"
if [[ ! $token =~ ^request-[0-9a-f]{64}$ ]]; then
echo "This USB authorization request is invalid or expired." >&2
exit 1
fi
request="$HOME/.local/state/omarchy/usb-authorization/requests/$token.json"
# Serialize request updates with watcher events, but release the lock while the
# user chooses. A changed request must never inherit an old dialog's approval.
if [[ ! -d ${request%/*} ]]; then
echo "This USB authorization request is no longer available." >&2
exit 1
fi
exec 9>"${request%/*}/.lock"
flock 9
if [[ ! -f $request || -L $request ]]; then
echo "This USB authorization request is no longer available." >&2
exit 1
fi
snapshot=$(cat "$request")
id=$(jq -er '.id | select(test("^[0-9]+$"))' <<<"$snapshot")
expected_rule=$(jq -er '.rule | select(type == "string" and startswith("block "))' <<<"$snapshot")
choice=$(jq -er '.approval // "" | select(. == "" or . == "Allow once" or . == "Always allow this device")' <<<"$snapshot")
allowed_rule="allow ${expected_rule#block }"
approval_failed() {
echo "Could not confirm USB approval. The request has been kept; try again." >&2
exit 1
}
read_current_rule() {
local devices line
current_rule=""
devices=$(usbguard list-devices) || approval_failed
while IFS= read -r line; do
if [[ $line == "$id: "* ]]; then
current_rule="${line#"$id: "}"
return
fi
done <<<"$devices"
}
validate_device() {
read_current_rule
if [[ $current_rule == "$expected_rule" ]] ||
[[ -n $choice && $current_rule == "$allowed_rule" ]]; then
return
fi
rm -f "$request"
echo "That USB device was removed or changed. Connect it again to review it." >&2
exit 1
}
validate_device
if [[ -z $choice ]]; then
flock -u 9
gum style --foreground 212 --bold "Blocked USB accessory"
echo
echo "The following details come from the device and can be forged:"
printf '%s\n' "$current_rule" | LC_ALL=C tr -c '\11\12\15\40-\176' '?'
echo
choice=$(gum choose \
"Allow once" \
"Always allow this device" \
"Keep blocked") || exit 130
flock 9
if [[ ! -f $request || -L $request || $(cat "$request") != "$snapshot" ]]; then
echo "This USB authorization request changed. Open the latest notification to review it." >&2
exit 1
fi
# A fresh choice can authorize only the blocked identity shown in the dialog.
read_current_rule
if [[ $current_rule != "$expected_rule" ]]; then
rm -f "$request"
echo "That USB device was removed or changed. Connect it again to review it." >&2
exit 1
fi
fi
case "$choice" in
"Allow once" | "Always allow this device")
# Save intent before applying policy: a later query can fail after the device
# was authorized. Retry must verify that approval, including persistent trust.
temporary=$(mktemp "${request%/*}/.request.XXXXXXXXXX")
trap 'rm -f "$temporary"' EXIT
jq --arg approval "$choice" '.approval = $approval' "$request" >"$temporary"
mv -f "$temporary" "$request"
if [[ $choice == "Always allow this device" ]]; then
pkexec /usr/bin/omarchy-usb-authorization-approve "$id" "$current_rule" || approval_failed
elif [[ $current_rule == "$expected_rule" ]]; then
# Match the complete identity, since numeric IDs can be reassigned.
usbguard allow-device "$expected_rule" || approval_failed
fi
# Rule-based USBGuard commands swallow applyDevicePolicy exceptions and can
# return success without authorizing anything. Verify the exact device.
allowed=$(usbguard list-devices --allowed) || approval_failed
grep -Fqx -- "$id: $allowed_rule" <<<"$allowed" || approval_failed
if [[ $choice == "Always allow this device" ]]; then
# Let USBGuard match policy rules: saved rules can omit the device's port.
# --show-devices indents the devices matching each policy rule.
policy=$(usbguard list-rules --label omarchy-usb-authorization-v1 --show-devices) || approval_failed
policy_allows=0
matched=0
while IFS= read -r line; do
if [[ $line =~ ^[0-9]+:[[:space:]] ]]; then
policy_allows=0
if [[ $line =~ ^[0-9]+:[[:space:]]allow[[:space:]] ]]; then
policy_allows=1
fi
elif (( policy_allows )) && [[ $line == $'\t'"$id: $allowed_rule" ]]; then
matched=1
break
fi
done <<<"$policy"
(( matched )) || approval_failed
echo "USB accessory added to the trusted policy."
else
echo "USB accessory allowed until it is disconnected."
fi
;;
"Keep blocked")
echo "USB accessory remains blocked."
;;
*) exit 1 ;;
esac
rm -f "$request"
-17
View File
@@ -1,17 +0,0 @@
#!/bin/bash
# omarchy:hidden=true
# omarchy:summary=Watch USBGuard for devices awaiting user approval
set -euo pipefail
# Starting a new graphical-session watcher must offer fresh prompts even if
# their old notification actions were dismissed and aged out of history.
OMARCHY_USB_AUTHORIZATION_WATCH_ID=$(cat /proc/sys/kernel/random/uuid)
export OMARCHY_USB_AUTHORIZATION_WATCH_ID
event_handler="$OMARCHY_PATH/bin/omarchy-usb-authorization-event"
# The handler scans blocked devices on IPC.Connected, including reconnects
# after a daemon restart, once the event subscription is established.
exec usbguard watch --wait --exec "$event_handler"
+1 -1
View File
@@ -56,6 +56,6 @@ rm -f "${desktop_file:-$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  "Web app removed" "$APP_NAME" omarchy-notification-send -g  "$(omarchy-text "Web app removed")" "$APP_NAME"
fi fi
update-desktop-database "$DESKTOP_DIR" &>/dev/null update-desktop-database "$DESKTOP_DIR" &>/dev/null
+1 -1
View File
@@ -1420,7 +1420,7 @@ launch_windows() {
if ! priv up_wait; then if ! priv up_wait; then
echo "❌ Failed to start Windows VM!" echo "❌ Failed to start Windows VM!"
echo " Try checking: omarchy-windows-vm status" echo " Try checking: omarchy-windows-vm status"
omarchy-notification-send -u critical "Windows VM" "Failed to start Windows VM" omarchy-notification-send -u critical "$(omarchy-text "Windows VM")" "$(omarchy-text "Failed to start Windows VM")"
exit 1 exit 1
fi fi
+19 -14
View File
@@ -10,6 +10,11 @@ import sys
import configure as setup import configure as setup
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")
def keyboard_catalog(): def keyboard_catalog():
# Read the same choices as the ISO and first-boot form, then convert console # Read the same choices as the ISO and first-boot form, then convert console
# keymaps through systemd's mapping, just as localectl does during install. # keymaps through systemd's mapping, just as localectl does during install.
@@ -64,7 +69,7 @@ def choose(title, catalog, selected, kind, overrides=None):
if result.returncode == 1: if result.returncode == 1:
return None return None
if result.returncode: if result.returncode:
raise RuntimeError(result.stderr.strip() or "The selection menu could not open") raise RuntimeError(result.stderr.strip() or text("The selection menu could not open"))
def input_catalog(items): def input_catalog(items):
@@ -78,7 +83,7 @@ def input_catalog(items):
def input_items(items, selected, overrides=None): def input_items(items, selected, overrides=None):
if not any(item[0].startswith("keyboard-") for item in items): if not any(item[0].startswith("keyboard-") for item in items):
raise RuntimeError("The current input group has no keyboard input") raise RuntimeError(text("The current input group has no keyboard input"))
# Retained entries keep their switching order; new engines follow them. # Retained entries keep their switching order; new engines follow them.
kept = [item for item in items if item[0].startswith("keyboard-") or item[0] in selected] kept = [item for item in items if item[0].startswith("keyboard-") or item[0] in selected]
current = dict(items) current = dict(items)
@@ -93,7 +98,7 @@ def set_inputs(group, layout, before, after):
setup.run(["fcitx5-remote", "-c"]) setup.run(["fcitx5-remote", "-c"])
setup.live_set(group, layout, after) setup.live_set(group, layout, after)
if setup.live_group() != (group, layout, after): if setup.live_group() != (group, layout, after):
raise RuntimeError("Fcitx did not retain the input selection") raise RuntimeError(text("Fcitx did not retain the input selection"))
except BaseException: except BaseException:
setup.live_set(group, layout, before) setup.live_set(group, layout, before)
raise raise
@@ -103,7 +108,7 @@ def configure_inputs():
group, layout, items = setup.live_group() group, layout, items = setup.live_group()
catalog, available = input_catalog(items) catalog, available = input_catalog(items)
current = [name for name, override in items if not name.startswith("keyboard-")] current = [name for name, override in items if not name.startswith("keyboard-")]
choose("Input Methods", catalog, current, "input", dict(items)) choose(text("Input Methods"), catalog, current, "input", dict(items))
def load_engines(names, catalog): def load_engines(names, catalog):
@@ -112,7 +117,7 @@ def load_engines(names, catalog):
if subprocess.run(["omarchy-pkg-present", package], capture_output=True).returncode] if subprocess.run(["omarchy-pkg-present", package], capture_output=True).returncode]
if absent or any(name not in setup.PRESETS for name in names): if absent or any(name not in setup.PRESETS for name in names):
labels = ", ".join(catalog.get(name, name) for name in names) labels = ", ".join(catalog.get(name, name) for name in names)
raise RuntimeError(f"Install {' '.join(absent) or 'its engine'} to use {labels}") raise RuntimeError(text("Install %1 to use %2", " ".join(absent) or text("its engine"), labels))
# Fcitx discovers engines only at startup, so one installed while it was # Fcitx discovers engines only at startup, so one installed while it was
# running (by an update, say) needs a restart before it can be selected. # running (by an update, say) needs a restart before it can be selected.
setup.run(["omarchy-restart-xcompose"]) setup.run(["omarchy-restart-xcompose"])
@@ -149,7 +154,7 @@ def add_input(method):
def keyboard_values(selected): def keyboard_values(selected):
if not selected: if not selected:
raise ValueError("Select at least one keyboard layout") raise ValueError(text("Select at least one keyboard layout"))
parts = [value.split(":", 1) for value in selected] parts = [value.split(":", 1) for value in selected]
# Preserve the installer's Latin-leading rule for desktop keybindings. # Preserve the installer's Latin-leading rule for desktop keybindings.
if parts[0][0] in setup.NON_LATIN: if parts[0][0] in setup.NON_LATIN:
@@ -176,19 +181,19 @@ def save_keyboard(selected):
expected = [layout + (":" + variant if variant else "") expected = [layout + (":" + variant if variant else "")
for layout, variant in zip(layouts.split(","), variants.split(","))] for layout, variant in zip(layouts.split(","), variants.split(","))]
if keyboard_selection() != expected: if keyboard_selection() != expected:
raise RuntimeError("Your personal keyboard layout override takes precedence over this selection") raise RuntimeError(text("Your personal keyboard layout override takes precedence over this selection"))
first_layout = layouts.split(",")[0] first_layout = layouts.split(",")[0]
first_variant = variants.split(",")[0] first_variant = variants.split(",")[0]
group_layout = first_layout + ("-" + first_variant if first_variant else "") group_layout = first_layout + ("-" + first_variant if first_variant else "")
primary = next((item[0] for item in before if item[0].startswith("keyboard-")), None) primary = next((item[0] for item in before if item[0].startswith("keyboard-")), None)
if primary is None: if primary is None:
raise RuntimeError("The current input group has no keyboard input") raise RuntimeError(text("The current input group has no keyboard input"))
keyboard = "keyboard-" + group_layout keyboard = "keyboard-" + group_layout
after = [[keyboard, ""]] + [item for item in before if item[0] not in (primary, keyboard)] after = [[keyboard, ""]] + [item for item in before if item[0] not in (primary, keyboard)]
group_changed = True group_changed = True
setup.live_set(group, group_layout, after) setup.live_set(group, group_layout, after)
if setup.live_group() != (group, group_layout, after): if setup.live_group() != (group, group_layout, after):
raise RuntimeError("Fcitx did not retain the keyboard selection") raise RuntimeError(text("Fcitx did not retain the keyboard selection"))
except BaseException: except BaseException:
if original is None: if original is None:
path.resolve().unlink(missing_ok=True) path.resolve().unlink(missing_ok=True)
@@ -205,7 +210,7 @@ def save_keyboard(selected):
def configure_keyboard(): def configure_keyboard():
current = keyboard_selection() current = keyboard_selection()
catalog = keyboard_catalog() catalog = keyboard_catalog()
choose("Keyboard Layouts", catalog, current, "keyboard") choose(text("Keyboard Layouts"), catalog, current, "keyboard")
def apply_selection(kind, context, values): def apply_selection(kind, context, values):
@@ -213,7 +218,7 @@ def apply_selection(kind, context, values):
rows = picker_rows(catalog, []) rows = picker_rows(catalog, [])
valid = {row[1:]: key for key, row in rows.items()} valid = {row[1:]: key for key, row in rows.items()}
if not isinstance(values, list) or any(not isinstance(value, str) or value not in valid for value in values): if not isinstance(values, list) or any(not isinstance(value, str) or value not in valid for value in values):
raise ValueError("The menu returned an invalid selection") raise ValueError(text("The menu returned an invalid selection"))
selected = list(dict.fromkeys(valid[value] for value in values)) selected = list(dict.fromkeys(valid[value] for value in values))
if kind == "input": if kind == "input":
save_inputs(selected, context.get("overrides")) save_inputs(selected, context.get("overrides"))
@@ -228,13 +233,13 @@ def main():
try: try:
add_input(sys.argv[2]) add_input(sys.argv[2])
except (OSError, ValueError, RuntimeError, subprocess.CalledProcessError) as error: except (OSError, ValueError, RuntimeError, subprocess.CalledProcessError) as error:
raise SystemExit(f"Input method could not be added: {error}") raise SystemExit(text("Input method could not be added: %1", error))
return return
applying = len(sys.argv) == 5 and sys.argv[1] == "apply" applying = len(sys.argv) == 5 and sys.argv[1] == "apply"
kind = sys.argv[2] if applying else sys.argv[1] if len(sys.argv) == 2 else "" kind = sys.argv[2] if applying else sys.argv[1] if len(sys.argv) == 2 else ""
if kind not in ("input", "keyboard"): if kind not in ("input", "keyboard"):
raise ValueError("Choose keyboard or input setup") raise ValueError("Choose keyboard or input setup")
title = "Input methods" if kind == "input" else "Keyboard layouts" title = text("Input methods could not be updated") if kind == "input" else text("Keyboard layouts could not be updated")
try: try:
if applying: if applying:
apply_selection(kind, json.loads(sys.argv[3]), json.loads(sys.argv[4])) apply_selection(kind, json.loads(sys.argv[3]), json.loads(sys.argv[4]))
@@ -243,7 +248,7 @@ def main():
else: else:
configure_keyboard() configure_keyboard()
except (OSError, ValueError, RuntimeError, subprocess.CalledProcessError) as error: except (OSError, ValueError, RuntimeError, subprocess.CalledProcessError) as error:
subprocess.run(["omarchy-notification-send", title + " could not be updated", str(error)], check=True) subprocess.run(["omarchy-notification-send", title, str(error)], check=True)
raise SystemExit(1) raise SystemExit(1)
+16 -8
View File
@@ -212,6 +212,12 @@
"setup.default.editor.helix": {"icon":"","label":"Helix","checked":"[[ \"$(omarchy-default-editor)\" == \"helix\" ]]","action":"omarchy-default-editor helix"}, "setup.default.editor.helix": {"icon":"","label":"Helix","checked":"[[ \"$(omarchy-default-editor)\" == \"helix\" ]]","action":"omarchy-default-editor helix"},
"setup.default.editor.vim": {"icon":"","label":"Vim","checked":"[[ \"$(omarchy-default-editor)\" == \"vim\" ]]","action":"omarchy-default-editor vim"}, "setup.default.editor.vim": {"icon":"","label":"Vim","checked":"[[ \"$(omarchy-default-editor)\" == \"vim\" ]]","action":"omarchy-default-editor vim"},
"setup.default.editor.emacs": {"icon":" ","label":"Emacs","checked":"[[ \"$(omarchy-default-editor)\" == \"emacs\" ]]","action":"omarchy-default-editor emacs"}, "setup.default.editor.emacs": {"icon":" ","label":"Emacs","checked":"[[ \"$(omarchy-default-editor)\" == \"emacs\" ]]","action":"omarchy-default-editor emacs"},
"setup.language": {"icon":"󰗊","label":"Language"},
"setup.language.en": {"icon":"󰗊","label":"English","checked":"[[ \"$(omarchy-language)\" == \"en_US.UTF-8\" ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-language en'"},
"setup.language.zh": {"icon":"󰗊","label":"简体中文","checked":"[[ \"$(omarchy-language)\" == \"zh_CN.UTF-8\" ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-language zh'"},
"setup.region": {"icon":"󰇧","label":"Region"},
"setup.region.global": {"icon":"󰖟","label":"Global","checked":"[[ \"$(omarchy-region)\" == \"Global\" ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-region global'"},
"setup.region.china": {"icon":"󰈻","label":"China","checked":"[[ \"$(omarchy-region)\" == \"China\" ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-region cn'"},
"setup.plugin": {"icon":"󰐱","label":"Plugins","aliases":["plugin","plugins"]}, "setup.plugin": {"icon":"󰐱","label":"Plugins","aliases":["plugin","plugins"]},
"setup.plugin.enable": {"icon":"󰄬","label":"Enable Plugin","action":"omarchy-menu-plugin enable"}, "setup.plugin.enable": {"icon":"󰄬","label":"Enable Plugin","action":"omarchy-menu-plugin enable"},
"setup.plugin.disable": {"icon":"󰅖","label":"Disable Plugin","action":"omarchy-menu-plugin disable"}, "setup.plugin.disable": {"icon":"󰅖","label":"Disable Plugin","action":"omarchy-menu-plugin disable"},
@@ -220,12 +226,8 @@
"setup.plugin.remove": {"icon":"󰭌","label":"Remove Plugin","when":"compgen -G \"$HOME/.config/omarchy/plugins/*/manifest.json\"","action":"omarchy-menu-plugin remove"}, "setup.plugin.remove": {"icon":"󰭌","label":"Remove Plugin","when":"compgen -G \"$HOME/.config/omarchy/plugins/*/manifest.json\"","action":"omarchy-menu-plugin remove"},
"setup.security": {"icon":"","label":"Security"}, "setup.security": {"icon":"","label":"Security"},
"setup.config": {"icon":"","label":"Config"}, "setup.config": {"icon":"","label":"Config"},
"setup.security.thunderbolt": {"icon":"󱐋","label":"Thunderbolt","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-thunderbolt-authorization"},
"setup.security.thunderbolt-boot": {"icon":"󱐋","label":"Thunderbolt Boot","when":"[[ -f /etc/omarchy/thunderbolt-authorization.enabled ]]","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-thunderbolt-boot"},
"setup.security.fingerprint": {"icon":"󰈷","label":"Fingerprint","when":"omarchy-hw-fingerprint","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-fingerprint"}, "setup.security.fingerprint": {"icon":"󰈷","label":"Fingerprint","when":"omarchy-hw-fingerprint","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-fingerprint"},
"setup.security.fido2": {"icon":"","label":"Fido2","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-fido2"}, "setup.security.fido2": {"icon":"","label":"Fido2","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-fido2"},
"setup.security.usb-authorization": {"icon":"󰖀","label":"USB Device Authorization","checked":"systemctl is-enabled --quiet usbguard.service && systemctl --user is-enabled --quiet omarchy-usb-authorization.service","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-usb-authorization"},
"setup.security.usb-authorization-boot": {"icon":"󰖀","label":"USB at Boot","when":"! omarchy-hw-aarch64-apple","checked":"grep -Fqx 'KERNEL_CMDLINE[default]+=\" usbcore.authorized_default=0\"' /etc/limine-entry-tool.d/usb-authorization.conf 2>/dev/null","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-setup-security-usb-authorization --boot'"},
"setup.security.sshd": {"icon":"󰣀","label":"SSHD","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-sshd"}, "setup.security.sshd": {"icon":"󰣀","label":"SSHD","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-sshd"},
"setup.security.ssh-agent": {"icon":"󰣀","label":"SSH Agent","when":"! systemctl --user -q is-enabled gcr-ssh-agent.socket","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-ssh-agent"}, "setup.security.ssh-agent": {"icon":"󰣀","label":"SSH Agent","when":"! systemctl --user -q is-enabled gcr-ssh-agent.socket","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-ssh-agent"},
"setup.security.sudoless-docker": {"icon":"󰡨","label":"Sudoless Docker","when":"omarchy-sudo-docker --configured","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-sudoless-docker"}, "setup.security.sudoless-docker": {"icon":"󰡨","label":"Sudoless Docker","when":"omarchy-sudo-docker --configured","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-sudoless-docker"},
@@ -259,6 +261,16 @@
"install.tui": {"icon":"","label":"TUI","action":"omarchy-launch-floating-terminal-with-presentation omarchy-tui-install"}, "install.tui": {"icon":"","label":"TUI","action":"omarchy-launch-floating-terminal-with-presentation omarchy-tui-install"},
"install.windows": {"icon":"󰍲","label":"Windows","when":"omarchy-hw-x86","disabled":"[[ -f $HOME/.local/share/applications/windows-vm.desktop ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-windows-vm install'"}, "install.windows": {"icon":"󰍲","label":"Windows","when":"omarchy-hw-x86","disabled":"[[ -f $HOME/.local/share/applications/windows-vm.desktop ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-windows-vm install'"},
"install.aur": {"icon":"󰣇","label":"AUR","action":"xdg-terminal-exec --app-id=org.omarchy.terminal omarchy-pkg-aur-install"}, "install.aur": {"icon":"󰣇","label":"AUR","action":"xdg-terminal-exec --app-id=org.omarchy.terminal omarchy-pkg-aur-install"},
"install.china": {"icon":"󰓜","label":"China Apps","when":"[[ \"$(omarchy-region)\" == \"China\" ]]"},
"install.china.wechat": {"icon":"󰘑","label":"WeChat","when":"[[ \"$(omarchy-region)\" == \"China\" ]]","disabled":"omarchy-pkg-present wechat-universal-bwrap","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-pkg-aur-add wechat-universal-bwrap'"},
"install.china.qq": {"icon":"󰘅","label":"QQ","when":"[[ \"$(omarchy-region)\" == \"China\" ]]","disabled":"omarchy-pkg-present linuxqq","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-pkg-aur-add linuxqq'"},
"install.china.feishu": {"icon":"󱗆","label":"Feishu","when":"[[ \"$(omarchy-region)\" == \"China\" ]]","disabled":"omarchy-pkg-present feishu-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-pkg-aur-add feishu-bin'"},
"install.china.dingtalk": {"icon":"󰭹","label":"DingTalk","when":"[[ \"$(omarchy-region)\" == \"China\" ]]","disabled":"omarchy-pkg-present dingtalk-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-pkg-aur-add dingtalk-bin'"},
"install.china.wemeet": {"icon":"󰕧","label":"Tencent Meeting","when":"[[ \"$(omarchy-region)\" == \"China\" ]]","disabled":"omarchy-pkg-present wemeet-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-pkg-aur-add wemeet-bin'"},
"install.china.wps": {"icon":"󰈬","label":"WPS Office","when":"[[ \"$(omarchy-region)\" == \"China\" ]]","disabled":"omarchy-pkg-present wps-office-cn","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-pkg-aur-add wps-office-cn'"},
"install.china.tencent-docs": {"icon":"󰈙","label":"Tencent Docs","when":"[[ \"$(omarchy-region)\" == \"China\" ]]","disabled":"[[ -f \"$HOME/.local/share/applications/腾讯文档.desktop\" ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-webapp-install 腾讯文档 https://docs.qq.com \"\"'"},
"install.china.yuque": {"icon":"󰠮","label":"Yuque","when":"[[ \"$(omarchy-region)\" == \"China\" ]]","disabled":"[[ -f \"$HOME/.local/share/applications/语雀.desktop\" ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-webapp-install 语雀 https://www.yuque.com/dashboard \"\"'"},
"install.china.shimo": {"icon":"󰈙","label":"Shimo","when":"[[ \"$(omarchy-region)\" == \"China\" ]]","disabled":"[[ -f \"$HOME/.local/share/applications/石墨文档.desktop\" ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-webapp-install 石墨文档 https://shimo.im/desktop \"\"'"},
"install.preinstalls": {"icon":"󰏓","label":"Preinstalls","disabled":"[[ ! -f $HOME/.local/state/omarchy/preinstalls-removed ]]","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-preinstalls"}, "install.preinstalls": {"icon":"󰏓","label":"Preinstalls","disabled":"[[ ! -f $HOME/.local/state/omarchy/preinstalls-removed ]]","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-preinstalls"},
"install.browser.chrome": {"icon":"","label":"Chrome","disabled":"omarchy-pkg-present google-chrome","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-browser chrome'"}, "install.browser.chrome": {"icon":"","label":"Chrome","disabled":"omarchy-pkg-present google-chrome","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-browser chrome'"},
"install.browser.edge": {"icon":"󰇩","label":"Edge","when":"omarchy-hw-x86","disabled":"omarchy-pkg-present microsoft-edge-stable-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-browser edge'"}, "install.browser.edge": {"icon":"󰇩","label":"Edge","when":"omarchy-hw-x86","disabled":"omarchy-pkg-present microsoft-edge-stable-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-browser edge'"},
@@ -344,12 +356,8 @@
"remove.windows": {"icon":"󰍲","label":"Windows","when":"[[ -f $HOME/.local/share/applications/windows-vm.desktop ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-windows-vm remove'"}, "remove.windows": {"icon":"󰍲","label":"Windows","when":"[[ -f $HOME/.local/share/applications/windows-vm.desktop ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-windows-vm remove'"},
"remove.preinstalls": {"icon":"󰏓","label":"Preinstalls","when":"[[ ! -f $HOME/.local/state/omarchy/preinstalls-removed ]]","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-preinstalls"}, "remove.preinstalls": {"icon":"󰏓","label":"Preinstalls","when":"[[ ! -f $HOME/.local/state/omarchy/preinstalls-removed ]]","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-preinstalls"},
"remove.security": {"icon":"","label":"Security","title":"Remove"}, "remove.security": {"icon":"","label":"Security","title":"Remove"},
"remove.security.thunderbolt": {"icon":"󱐋","label":"Thunderbolt","when":"[[ -f /etc/omarchy/thunderbolt-authorization.enabled || -f /etc/omarchy/thunderbolt-authorization.pending ]]","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-thunderbolt-authorization"},
"remove.security.thunderbolt-boot": {"icon":"󱐋","label":"Thunderbolt Boot","when":"omarchy-thunderbolt-authorization-boot-enabled","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-thunderbolt-boot"},
"remove.security.fingerprint": {"icon":"󰈷","label":"Fingerprint","when":"omarchy-pkg-present fprintd","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-fingerprint"}, "remove.security.fingerprint": {"icon":"󰈷","label":"Fingerprint","when":"omarchy-pkg-present fprintd","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-fingerprint"},
"remove.security.fido2": {"icon":"","label":"Fido2","when":"omarchy-pkg-present pam-u2f","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-fido2"}, "remove.security.fido2": {"icon":"","label":"Fido2","when":"omarchy-pkg-present pam-u2f","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-fido2"},
"remove.security.usb-authorization": {"icon":"󰖀","label":"USB Device Authorization","when":"systemctl is-enabled --quiet usbguard.service","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-usb-authorization"},
"remove.security.usb-authorization-boot": {"icon":"󰖀","label":"USB at Boot","when":"! omarchy-hw-aarch64-apple && grep -Fqx 'KERNEL_CMDLINE[default]+=\" usbcore.authorized_default=0\"' /etc/limine-entry-tool.d/usb-authorization.conf 2>/dev/null","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-remove-security-usb-authorization --boot-only'"},
"remove.security.sshd": {"icon":"󰣀","label":"SSHD","when":"systemctl is-enabled --quiet sshd","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-sshd"}, "remove.security.sshd": {"icon":"󰣀","label":"SSHD","when":"systemctl is-enabled --quiet sshd","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-sshd"},
"remove.security.sudoless-docker": {"icon":"󰡨","label":"Sudoless Docker","when":"! omarchy-sudo-docker --configured","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-sudoless-docker"}, "remove.security.sudoless-docker": {"icon":"󰡨","label":"Sudoless Docker","when":"! omarchy-sudo-docker --configured","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-sudoless-docker"},
"remove.ai.hermes": {"icon":"","iconFont":"omarchy","label":"Hermes Desktop","when":"omarchy-pkg-present hermes-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-ai-hermes"}, "remove.ai.hermes": {"icon":"","iconFont":"omarchy","label":"Hermes Desktop","when":"omarchy-pkg-present hermes-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-ai-hermes"},
+203
View File
@@ -0,0 +1,203 @@
// Chinese menu labels, overlaid on omarchy-menu.jsonc when LANG is zh_CN.
{
"apps": {"label":"应用"},
"learn": {"label":"学习"},
"trigger": {"label":"触发"},
"style": {"label":"风格"},
"setup": {"label":"设置"},
"install": {"label":"安装"},
"remove": {"label":"移除"},
"update": {"label":"更新"},
"about": {"label":"关于"},
"system": {"label":"系统"},
"system.screensaver": {"label":"屏保"},
"system.lock": {"label":"锁屏"},
"system.suspend": {"label":"睡眠"},
"system.hibernate": {"label":"休眠"},
"system.logout": {"label":"注销"},
"system.reboot": {"label":"重启"},
"system.shutdown": {"label":"关机"},
"learn.keybindings": {"label":"快捷键"},
"learn.omarchy": {"label":"Omarchy 手册"},
"learn.community": {"label":"社区"},
"trigger.emoji": {"label":"表情"},
"trigger.reminder": {"label":"提醒"},
"trigger.capture": {"label":"截取"},
"trigger.capture.screenshot": {"label":"截图"},
"trigger.capture.screenrecord.stop": {"label":"停止录屏"},
"trigger.capture.screenrecord": {"label":"录屏"},
"trigger.capture.text": {"label":"OCR 取字"},
"trigger.capture.qr": {"label":"二维码"},
"trigger.capture.color": {"label":"取色"},
"trigger.capture.screenrecord.no-audio": {"label":"无音频"},
"trigger.capture.screenrecord.desktop-audio": {"label":"含桌面音频"},
"trigger.capture.screenrecord.microphone": {"label":"含桌面与麦克风音频"},
"trigger.capture.screenrecord.webcam": {"label":"含桌面、麦克风与摄像头"},
"trigger.transcode": {"label":"转码"},
"trigger.share": {"label":"分享"},
"trigger.toggle": {"label":"开关"},
"trigger.hardware": {"label":"硬件"},
"trigger.tests": {"label":"测速"},
"trigger.hardware.laptop-display": {"label":"笔记本屏幕"},
"trigger.hardware.mirror-display": {"label":"镜像显示"},
"trigger.hardware.hybrid-gpu": {"label":"混合显卡"},
"trigger.hardware.touchpad": {"label":"触控板"},
"trigger.hardware.touchpad-haptics": {"label":"触控板振动"},
"trigger.hardware.touchpad-haptics.low": {"label":"低"},
"trigger.hardware.touchpad-haptics.mid": {"label":"中"},
"trigger.hardware.touchpad-haptics.high": {"label":"高"},
"trigger.hardware.touchscreen": {"label":"触摸屏"},
"trigger.reminder.set": {"label":"设一个提醒"},
"trigger.reminder.show": {"label":"查看全部"},
"trigger.reminder.clear": {"label":"清除全部"},
"trigger.share.clipboard": {"label":"剪贴板"},
"trigger.share.file": {"label":"文件"},
"trigger.share.folder": {"label":"文件夹"},
"trigger.share.receive": {"label":"接收"},
"trigger.toggle.idle-lock": {"label":"保持唤醒"},
"trigger.toggle.notifications": {"label":"通知"},
"trigger.toggle.crash-capture": {"label":"崩溃捕获"},
"trigger.toggle.screensaver": {"label":"屏保"},
"trigger.toggle.nightlight": {"label":"夜间模式"},
"trigger.tests.network-speedtest": {"label":"网络测速"},
"trigger.tests.disk-speedtest": {"label":"磁盘测速"},
"style.theme": {"label":"主题"},
"style.background": {"label":"壁纸"},
"style.unlock": {"label":"解锁画面"},
"style.font": {"label":"字体"},
"style.screensaver": {"label":"屏保"},
"style.about": {"label":"关于"},
"style.about.text": {"label":"编辑文字"},
"style.about.image": {"label":"取自图片"},
"style.about.default": {"label":"恢复默认"},
"style.screensaver.text": {"label":"编辑文字"},
"style.screensaver.image": {"label":"取自图片"},
"style.screensaver.default": {"label":"恢复默认"},
"setup.monitors": {"label":"显示器"},
"setup.keybindings": {"label":"快捷键"},
"setup.input": {"label":"输入设备"},
"setup.language": {"label":"语言"},
"setup.network": {"label":"网络"},
"setup.network.dns.custom": {"label":"自定义"},
"setup.network.qr": {"label":"二维码"},
"setup.default": {"label":"默认程序"},
"setup.default.agent": {"title":"默认 Agent"},
"setup.default.dictation": {"label":"语音听写","title":"默认语音听写"},
"setup.default.browser": {"label":"浏览器","title":"默认浏览器"},
"setup.default.terminal": {"label":"终端","title":"默认终端"},
"setup.default.editor": {"label":"编辑器","title":"默认编辑器"},
"setup.plugin": {"label":"插件"},
"setup.plugin.enable": {"label":"启用插件"},
"setup.plugin.disable": {"label":"停用插件"},
"setup.plugin.add": {"label":"添加插件"},
"setup.plugin.clone": {"label":"克隆插件"},
"setup.plugin.remove": {"label":"移除插件"},
"setup.security": {"label":"安全"},
"setup.config": {"label":"配置"},
"setup.security.fingerprint": {"label":"指纹"},
"setup.security.passwordless-sudo": {"label":"免密 sudo"},
"setup.security.sudoless-docker": {"label":"免 sudo Docker"},
"setup.direct-boot": {"label":"直接启动"},
"setup.reset": {"label":"恢复出厂设置"},
"install.package": {"label":"软件包"},
"install.service": {"label":"服务"},
"install.development": {"label":"开发环境"},
"install.editor": {"label":"编辑器"},
"install.style": {"label":"风格"},
"install.style.theme": {"label":"主题"},
"install.style.background": {"label":"壁纸"},
"install.style.font": {"label":"字体"},
"install.gaming": {"label":"游戏"},
"install.browser": {"label":"浏览器"},
"install.webapp": {"label":"网页应用"},
"install.terminal": {"label":"终端"},
"install.preinstalls": {"label":"预装应用"},
"install.service.chromium-account": {"label":"Chromium 账号"},
"install.gaming.xbox-cloud": {"label":"Xbox 云游戏"},
"install.gaming.xbox-controllers": {"label":"Xbox 手柄"},
"install.gaming.retro-launcher": {"label":"RetroArch 启动器"},
"install.development.docker-dbs": {"label":"Docker 数据库"},
"remove.package": {"label":"软件包"},
"remove.ai": {"title":"移除"},
"remove.service": {"label":"服务","title":"移除"},
"remove.development": {"label":"开发环境","title":"移除"},
"remove.theme": {"label":"主题"},
"remove.gaming": {"label":"游戏","title":"移除"},
"remove.browser": {"label":"浏览器","title":"移除"},
"remove.webapp": {"label":"网页应用"},
"remove.preinstalls": {"label":"预装应用"},
"remove.security": {"label":"安全","title":"移除"},
"remove.security.fingerprint": {"label":"指纹"},
"remove.security.sudoless-docker": {"label":"免 sudo Docker"},
"remove.dictation": {"label":"语音听写"},
"remove.gaming.xbox-cloud": {"label":"Xbox 云游戏"},
"remove.gaming.xbox-controllers": {"label":"Xbox 手柄 (󰂯)"},
"remove.development.javascript": {"title":"移除"},
"remove.development.php": {"title":"移除"},
"remove.development.elixir": {"title":"移除"},
"update.channel": {"label":"通道"},
"update.config": {"label":"配置","title":"重置为默认"},
"update.themes": {"label":"附加主题"},
"update.process": {"label":"进程","title":"重启"},
"update.hardware": {"label":"硬件","title":"重启"},
"update.firmware": {"label":"固件"},
"update.password": {"label":"密码"},
"update.timezone": {"label":"时区"},
"update.time": {"label":"时间"},
"update.hardware.audio": {"label":"音频"},
"update.hardware.bluetooth": {"label":"蓝牙"},
"update.hardware.trackpad": {"label":"触控板"},
"update.password.drive": {"label":"磁盘加密"},
"update.password.user": {"label":"用户"},
"update.channel.stable": {"label":"稳定版"},
"update.channel.rc": {"label":"候选版"},
"update.channel.edge": {"label":"尝鲜版"},
"update.channel.dev": {"label":"开发版"},
"trigger.appearance": {"label":"外观"},
"trigger.appearance.animations": {"label":"动画"},
"trigger.appearance.one-window-ratio": {"label":"单窗口比例"},
"trigger.appearance.theme-sync": {"label":"Herdr 主题同步"},
"trigger.appearance.top-bar": {"label":"菜单栏"},
"trigger.appearance.top-bar.show": {"label":"显示菜单栏"},
"trigger.appearance.top-bar.hide": {"label":"隐藏菜单栏"},
"trigger.appearance.top-bar.position": {"label":"菜单栏位置"},
"trigger.appearance.top-bar.position.top": {"label":"顶部"},
"trigger.appearance.top-bar.position.bottom": {"label":"底部"},
"trigger.appearance.top-bar.position.left": {"label":"左侧"},
"trigger.appearance.top-bar.position.right": {"label":"右侧"},
"trigger.appearance.top-bar.background": {"label":"菜单栏背景"},
"trigger.appearance.top-bar.background.opaque": {"label":"不透明"},
"trigger.appearance.top-bar.background.glassy": {"label":"毛玻璃"},
"trigger.appearance.top-bar.background.clear": {"label":"全透明"},
"trigger.appearance.top-bar.indicators": {"label":"菜单栏指示"},
"trigger.appearance.top-bar.indicators.battery-percentage": {"label":"电量百分比"},
"trigger.appearance.top-bar.indicators.clock": {"label":"时钟"},
"trigger.appearance.window-corners": {"label":"窗口圆角"},
"trigger.appearance.window-corners.theme": {"label":"跟随主题"},
"trigger.appearance.window-corners.rounded": {"label":"圆角"},
"trigger.appearance.window-corners.sharp": {"label":"直角"},
"trigger.appearance.window-gaps": {"label":"窗口间距"},
"trigger.appearance.window-transparency": {"label":"窗口透明度"},
"trigger.appearance.window-transparency.opaque": {"label":"不透明"},
"trigger.appearance.window-transparency.glassy": {"label":"毛玻璃"},
"trigger.appearance.window-transparency.transparent": {"label":"透明"},
"trigger.appearance.workspace-layout": {"label":"工作区布局","title":"默认工作区布局"},
"trigger.appearance.workspace-layout.dwindle": {"label":"平铺"},
"trigger.appearance.workspace-layout.scrolling": {"label":"滚动"},
"trigger.appearance.workspace-layout.floating": {"label":"浮动"},
"trigger.clipboard-history": {"label":"剪贴板历史"},
"setup.typing": {"label":"输入"},
"setup.typing.keyboard": {"label":"键盘布局"},
"setup.typing.input": {"label":"输入法"},
"setup.region": {"label":"地区"},
"setup.region.global": {"label":"全球"},
"setup.region.china": {"label":"中国"},
"install.china": {"label":"国内应用"},
"install.china.wechat": {"label":"微信"},
"install.china.feishu": {"label":"飞书"},
"install.china.dingtalk": {"label":"钉钉"},
"install.china.wemeet": {"label":"腾讯会议"},
"install.china.tencent-docs": {"label":"腾讯文档"},
"install.china.yuque": {"label":"语雀"},
"install.china.shimo": {"label":"石墨文档"},
}
@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN" "http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
<policyconfig>
<action id="org.omarchy.thunderbolt.approve">
<description>Approve a connected Thunderbolt accessory</description>
<message>Authentication is required to approve this Thunderbolt accessory</message>
<annotate key="org.freedesktop.policykit.exec.path">/usr/bin/omarchy-thunderbolt-authorization-approve</annotate>
<defaults>
<allow_any>auth_admin</allow_any>
<allow_inactive>auth_admin</allow_inactive>
<allow_active>auth_admin</allow_active>
</defaults>
</action>
</policyconfig>
-14
View File
@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN" "http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
<policyconfig>
<action id="org.omarchy.usb.approve">
<description>Trust a connected USB accessory on any port</description>
<message>Authentication is required to trust this USB accessory</message>
<annotate key="org.freedesktop.policykit.exec.path">/usr/bin/omarchy-usb-authorization-approve</annotate>
<defaults>
<allow_any>auth_admin</allow_any>
<allow_inactive>auth_admin</allow_inactive>
<allow_active>auth_admin</allow_active>
</defaults>
</action>
</policyconfig>
+5
View File
@@ -0,0 +1,5 @@
[source.crates-io]
replace-with = "aliyun"
[source.aliyun]
registry = "sparse+https://mirrors.aliyun.com/crates.io-index/"
+8
View File
@@ -0,0 +1,8 @@
NPM_CONFIG_REGISTRY=https://registry.npmmirror.com
PIP_INDEX_URL=https://mirrors.aliyun.com/pypi/simple/
UV_DEFAULT_INDEX=https://mirrors.aliyun.com/pypi/simple/
GOPROXY=https://mirrors.aliyun.com/goproxy/,direct
RUSTUP_DIST_SERVER=https://mirrors.aliyun.com/rustup
RUSTUP_UPDATE_ROOT=https://mirrors.aliyun.com/rustup/rustup
BUNDLE_MIRROR__ALL=https://mirrors.aliyun.com/rubygems/
MAVEN_ARGS=--global-settings /usr/share/omarchy/default/regions/cn/maven-settings.xml
+3
View File
@@ -0,0 +1,3 @@
---
:sources:
- https://mirrors.aliyun.com/rubygems/
+9
View File
@@ -0,0 +1,9 @@
<settings>
<mirrors>
<mirror>
<id>aliyun</id>
<mirrorOf>central</mirrorOf>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>
</mirrors>
</settings>
+2
View File
@@ -0,0 +1,2 @@
# Installed from the ISO's offline repository before enabling ArchLinuxCN.
archlinuxcn-keyring
@@ -0,0 +1 @@
Server = https://mirror.omarchy.cn/$repo/os/$arch
Loaded 100 of 225 files, more files were not shown because too many files have changed in this diff. Show more