Compare commits

...
Author SHA1 Message Date
ZacharyZhang-NY d4551e2767 Check off T31 after one codex round 2026-10-12 00:10:56 -04:00
ZacharyZhang-NY f2d69265d6 Check off T30 after one codex round 2026-10-11 23:54:39 -04:00
ZacharyZhang-NY 375ff9036d Check off T29 after one codex round 2026-10-11 23:50:38 -04:00
ZacharyZhang-NY 6a221de8b3 Plan: local test ISOs no longer pin their builds 2026-10-11 23:49:08 -04:00
ZacharyZhang-NY 70fc7a9e2d Check off T28 after two codex rounds 2026-10-11 23:48:42 -04:00
ZacharyZhang-NY ae065afb66 Drop Claude Code API from the plan's shipping notes 2026-10-11 23:48:00 -04:00
ZacharyZhang-NY f2af41bbc9 Drop the Claude Code API setup, Dim and Kimi, and the pkg-add change 2026-10-11 23:45:12 -04:00
ZacharyZhang-NY dd704a29cd Plan T28–T36 after the maintainers' ISO test 2026-10-11 23:43:59 -04:00
ZacharyZhang-NY 3c6e7cca55 Merge remote-tracking branch 'origin/omarchycn' into omarchycn 2026-10-09 11:47:54 -04:00
ZacharyZhang-NY 02fb6439eb Check off T27 after two codex rounds 2026-10-09 02:22:04 -04:00
ZacharyZhang-NY c8ec83d046 Correct the acceptance checklist: run B timezone, test failure scope, OCR misses, trap condition 2026-10-09 02:21:17 -04:00
ZacharyZhang-NY 8e1f9266ca Check off T26; acceptance checklist for the language and timezone-region model 2026-10-09 02:18:33 -04:00
ZacharyZhang-NY fcd681c099 Merge upstream quattro for the Qt 6.12 palette fix (#14553)
Qt 6.12 shadows the bare Color singleton, so the bar and every Color
role read undefined on fresh installs. Keep our I18n text in the four
conflicting files, qualify #14433's notification button color, give the
new dictation menu rows and notifications Chinese, and drop table
entries for the removed Voxtype and FIDO2 texts.
2026-10-09 01:24:30 -04:00
ZacharyZhang-NY 76b9331d8b Escape commas in the keyboard preselect; gum splits --selected on them 2026-10-09 00:52:26 -04:00
ZacharyZhang-NY c08a1bc3c2 Check off T25 after two codex rounds 2026-10-09 00:25:49 -04:00
ZacharyZhang-NY 2d2aa337e4 Say which mirror files region.sh links and that Global only hides China Apps 2026-10-09 00:25:23 -04:00
ZacharyZhang-NY 738d59e3a9 Describe language and timezone-derived region in the manual and docs 2026-10-09 00:21:33 -04:00
ZacharyZhang-NY 7198a7b711 Check off T24 after two codex rounds 2026-10-09 00:20:21 -04:00
ZacharyZhang-NY e5b01ace6b Ask for the language, not the region, on the installer's first screen
omarchy_prompt_keyboard takes the layout to preselect.
2026-10-09 00:15:06 -04:00
ZacharyZhang-NY 848e9610c0 Check off T23 after two codex rounds 2026-10-09 00:10:00 -04:00
ZacharyZhang-NY 95d4e57db9 Move the region screen's replacement to T24 2026-10-08 23:51:30 -04:00
ZacharyZhang-NY 6da63c8942 Check off T22 after one codex round 2026-10-08 23:50:24 -04:00
ZacharyZhang-NY 562684f9fa Check off T21 after two codex rounds 2026-10-08 23:47:57 -04:00
ZacharyZhang-NY b914d694f2 Read a quoted LANG and append LANG on its own line in omarchy-language 2026-10-08 23:46:59 -04:00
ZacharyZhang-NY 199a7f23fc Split language from region: omarchy-language and Setup > Language own LANG
omarchy-region and install/config/region.sh no longer touch the locale;
both logout prompts are worded through omarchy-text.
2026-10-08 23:44:04 -04:00
ZacharyZhang-NY 1824bc3066 Check off T20 after two codex rounds 2026-10-08 23:35:30 -04:00
ZacharyZhang-NY 371fd05f03 Keep #14433's XPS 13 cirrus firmware in the offline package list 2026-10-08 23:34:22 -04:00
ZacharyZhang-NY 557ba1d270 Give the sleep-lock and power tests omarchy-text, which they found only when installed 2026-10-08 23:30:51 -04:00
ZacharyZhang-NY fdd457c0fa Merge #14433 input methods and remove Rime
The keyboard choice and omarchy-setup-input own Chinese input now. The
new passwordless sudo and input tooltips get Chinese text.
2026-10-08 23:30:41 -04:00
ZacharyZhang-NY 8798461461 Check off T19 after two codex rounds 2026-10-08 22:46:11 -04:00
ZacharyZhang-NY 4521dac023 Merge reworked #13690: timezone-derived region and channel mirror prepends
Drop the .cn servers.sed twins, the USTC Arch fallback and the closed
#14399 timezone preselection. region.sh follows the installer's marker.
2026-10-08 22:43:55 -04:00
ZacharyZhang-NY c7402fe653 Add T19-T27: timezone region, dhh's input methods, language split 2026-10-08 22:39:01 -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
ZacharyZhang-NY 37207f1911 Merge branch 'omacom:quattro' into omarchycn 2026-10-07 14:35:21 -04:00
David Heinemeier Hansson 40642c0d35 Verify input setup and composition stay quiet 2026-10-07 18:10:20 +02:00
David Heinemeier Hansson ee358c251b Show the input switching shortcut in keyboard hints 2026-10-07 18:10:20 +02:00
ZacharyZhang-NY b333308b2d Check off T18 after three codex rounds 2026-10-07 11:54:34 -04:00
ZacharyZhang-NY 8415c8d2a2 Document the China switch order 2026-10-07 11:54:16 -04:00
David Heinemeier Hansson f273eed840 Suppress the Cloudpinyin first-use prompt 2026-10-07 17:48:56 +02:00
ZacharyZhang-NY d62dff95b6 Trust ArchLinuxCN before the China refresh upgrades through it; repair links before the cn rename 2026-10-07 11:46:38 -04:00
David Heinemeier Hansson 036e501e58 Show explicit choices on notification cards 2026-10-07 17:35:03 +02:00
David Heinemeier Hansson 35f8eef1bc Remove the Japanese input welcome notification 2026-10-07 17:35:02 +02:00
ZacharyZhang-NY 9a57690e7d Align the China region to the official cn profile
The region value is cn, the China files live under default/regions/cn/, and
omarchy-apply-pacman renders pacman for every caller: the .cn twins of the
channel servers first, then .org, USTC and ArchLinuxCN. Switching to China
installs and trusts the ArchLinuxCN keyring. A migration renames china to cn
and repoints the drop-in links.
2026-10-07 11:33:01 -04:00
David Heinemeier Hansson 2841563e6c Remember input selections before an app gains focus 2026-10-07 17:17:02 +02:00
David Heinemeier Hansson cd73dc6cfd Keep Pinyin setup prompts visible 2026-10-07 17:17:02 +02:00
ZacharyZhang-NY d2e4a95aed Merge branch 'pr-14399' into omarchycn
# Conflicts:
#	bin/omarchy-refresh-pacman
#	install/post-install/pacman.sh
2026-10-07 11:08:25 -04:00
ZacharyZhang-NY 1abe693d73 Add T18, merging the official region-profile PRs, to the task list 2026-10-07 11:08:19 -04:00
David Heinemeier Hansson fcda55944a Show input modes in the keyboard indicator 2026-10-07 16:57:03 +02:00
David Heinemeier Hansson 22514b59f2 Exercise input engines across desktop toolkits 2026-10-07 15:59:46 +02:00
David Heinemeier Hansson 0f14a84934 Preserve inherited input keys and later keyboard changes 2026-10-07 15:19:06 +02:00
David Heinemeier Hansson cd0025b62f Merge branch 'quattro' into unified-input-method-setup 2026-10-07 15:02:38 +02:00
David Heinemeier Hansson 9fe6a04715 Unify input methods with the installer keyboard selection 2026-10-07 12:59:36 +02:00
ZacharyZhang-NY 5fc227b06c Word every notification at its sender and show notifications as given 2026-10-06 22:41:59 -04:00
ZacharyZhang-NY 051952600d Word parameterized messages at the sender with omarchy-text and translate only exact text in the shell 2026-10-06 22:29:57 -04:00
ZacharyZhang-NY 619979a716 Translate every Omarchy notification and the remaining prompts, matching templates longest first 2026-10-06 22:16:55 -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 1454e04b7f ✨ feat(region): default china installs to Asia/Shanghai
- ✨ offline China first boots preselect `Asia/Shanghai` from the region profile instead of the top of the list
- 📝 profiles may name a `timezone`; first-boot setup reads it for the region in `/etc/omarchy/region`

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 d53b24cfc7 ✨ feat(form): let callers preselect a fallback timezone
- ✨ an offline install has no geo guess, so the timezone list opened on Africa/Abidjan; a caller-supplied zone now stands in for the missing guess
- ✅ a successful geo guess still outranks the fallback

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
ZacharyZhang-NY f63ecb6697 Translate finished messages by template and the world clock, Dropbox and scoped titles at display 2026-10-06 22:07:19 -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
ZacharyZhang-NY 2b2302a279 Translate the remaining shell text at display and keep translated values out of logic 2026-10-06 21:50:11 -04:00
ZacharyZhang-NY e236713d9f Show the desktop shell in Chinese under zh_CN 2026-10-06 21:29:39 -04:00
ZacharyZhang-NY e590f22de6 Mark T16 done in the OmarchyCN task list 2026-10-06 21:25:38 -04:00
ZacharyZhang-NY df44695ea7 Draw the logout prompt on the terminal and describe what a failed China switch leaves 2026-10-06 21:23:13 -04:00
ZacharyZhang-NY 7f0c94d480 Mark T15 done in the OmarchyCN task list 2026-10-06 21:18:29 -04:00
ZacharyZhang-NY f9c36b2f3b Offer to log out after a region switch, in the new region's language 2026-10-06 21:15:32 -04:00
ZacharyZhang-NY a77c246d38 Ask pacman only for the packages pkg-add finds missing 2026-10-06 21:14:50 -04:00
ZacharyZhang-NY a3e40cb8b0 Set up China before moving the mirrors so a failed switch leaves the machine Global 2026-10-06 21:13:37 -04:00
ZacharyZhang-NY 09da70c303 Add T15-T17 from the owner's China testing to the task list 2026-10-06 21:12:52 -04:00
ZacharyZhang-NY df338c4003 Mark T14 done in the OmarchyCN task list 2026-10-06 20:22:05 -04:00
ZacharyZhang-NY 1f5d29a3ac Describe the T14 console flow in the task list 2026-10-06 18:44:56 -04:00
ZacharyZhang-NY 41c491ae4c Keep the setup form's new comments to one line 2026-10-06 18:44:49 -04:00
ZacharyZhang-NY b268838479 Give the setup form Chinese text for the ISO's China installs 2026-10-06 18:29:20 -04:00
ZacharyZhang-NY bf08fedf8e Add T14, the Chinese installer, to the OmarchyCN task list 2026-10-06 18:26:12 -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
ZacharyZhang-NY 7f9711ed9c Name the pin's abort hook in the shipping prerequisites 2026-10-06 09:06:51 -04:00
ZacharyZhang-NY 8956964ee5 Record the local-build pin in the OmarchyCN shipping prerequisites 2026-10-06 08:48:24 -04:00
ZacharyZhang-NY ebb0273584 Qualify the acceptance checklist's agent, cargo and test-run evidence 2026-10-06 03:15:49 -04:00
ZacharyZhang-NY 3c3f23222b Add the OmarchyCN acceptance checklist 2026-10-06 03:10:18 -04:00
ZacharyZhang-NY 4e323342ca Mark T11 and T12 done in the OmarchyCN task list 2026-10-06 02:48:13 -04:00
ZacharyZhang-NY dff33f16e3 Keep the host's region out of the sudo boundary sandboxes 2026-10-06 02:23:31 -04:00
ZacharyZhang-NY 6fe83bfa47 Restart fcitx5 before Rime Ice setup so it loads the new Rime addon 2026-10-06 01:56:46 -04:00
ZacharyZhang-NY dae9edef4c Upgrade the system before installing Rime when switching to China 2026-10-06 01:52:25 -04:00
ZacharyZhang-NY 77c8e3f413 Mark T10 done in the OmarchyCN task list 2026-10-06 01:06:51 -04:00
ZacharyZhang-NY ab6804f8cc Fix the local ISO build command in the shipping prerequisites 2026-10-06 01:06:27 -04:00
ZacharyZhang-NY 2ad241159e Describe the RubyGems override and record the branch's shipping prerequisites 2026-10-06 01:05:09 -04:00
ZacharyZhang-NY 4eafef5025 Correct the China docs and move .cn twins in the rc repository migration 2026-10-06 01:02:03 -04:00
ZacharyZhang-NY e1471efa42 Document the China region for users and contributors 2026-10-06 00:56:05 -04:00
ZacharyZhang-NY b51f36a9fe Mark T8 done in the OmarchyCN task list 2026-10-06 00:53:32 -04:00
ZacharyZhang-NY 07d5f49963 Rebuild .cn twins per server and leave database syncs to omarchy update 2026-10-06 00:52:28 -04:00
ZacharyZhang-NY 5f6c803b21 Switch regions from Setup > Region and show Chinese menu labels under zh_CN 2026-10-06 00:42:55 -04:00
ZacharyZhang-NY 56c8344fd2 Pass the web app installer its icon argument so China web apps skip the wizard 2026-10-06 00:37:41 -04:00
ZacharyZhang-NY 465d4da567 Add an Install > China Apps menu shown only in China 2026-10-06 00:34:51 -04:00
ZacharyZhang-NY 151a530d15 Point npm, PyPI, Go, Rust, RubyGems and Maven at China mirrors on China machines 2026-10-06 00:28:00 -04:00
ZacharyZhang-NY da6045be3f Keep user-managed Dim and Kimi launchers and write Claude settings through fresh private files 2026-10-06 00:19:55 -04:00
ZacharyZhang-NY 23072924c4 Add a China-only Claude Code third-party API setup 2026-10-06 00:06:22 -04:00
ZacharyZhang-NY f4bb97018e Add Dim Agent and Kimi Code to the agent roster 2026-10-06 00:01:58 -04:00
ZacharyZhang-NY 343501011f Indent the font test's Python harness with two spaces 2026-10-05 23:59:36 -04:00
ZacharyZhang-NY 600734790c Match ja and ko by subtag and test the string lang path apps use 2026-10-05 23:56:40 -04:00
ZacharyZhang-NY ab028e80eb Keep Latin families first and read only the tagged language for CJK variants 2026-10-05 23:48:00 -04:00
ZacharyZhang-NY a62771e2b4 Pick the matching Noto CJK variant for language-tagged text
The generic assigns swap sans-serif, serif and monospace for fonts
with no CJK glyphs, so tagged Chinese text ends up on whatever Noto
CJK variant the charset scan finds first, usually the Japanese one.
Fold the many Chinese tags down to one per variant and prepend the
matching family ahead of the assigns.
2026-10-05 23:36:57 -04:00
ZacharyZhang-NY 32ebf6b85b Set up Rime Ice from the ISO's assets on China installs 2026-10-05 23:32:21 -04:00
ZacharyZhang-NY b6bf851af4 Preselect the current region when the form asks again 2026-10-05 23:25:57 -04:00
ZacharyZhang-NY c86af5cea5 Record the installer's region choice during system setup 2026-10-05 23:20:36 -04:00
ZacharyZhang-NY 83d158ad07 List the China mirrors first when the machine's region is China 2026-10-05 23:13:20 -04:00
ZacharyZhang-NY 0c0f4881cb Add the OmarchyCN task list 2026-10-05 23:09:57 -04:00
207 changed files with 5090 additions and 578 deletions

No files matched your search

+10
View File
@@ -96,6 +96,16 @@ Exceptions are allowed for migration and package-helper scripts where the helper
- Do not add `aliases` to new menu entries. Aliases are reserved for
established alternate names users already type, kept for compatibility.
# Region
- `/etc/omarchy/region` holds `cn`; no file means Global. `omarchy-region` prints the region and switches it (`omarchy region cn|global`); the profile lives in `default/regions/cn/`, its contract in `docs/regions.md`, user-facing docs in `manual/52-china.md`.
- `install/config/region.sh` is the one system leaf for both the ISO install (the `/etc/omarchy/region` marker the installer derives from the timezone) and the runtime switch (`OMARCHY_REGION`): the region file and the profile drop-ins it symlinks. Input methods are not part of a region: the installer's keyboard choice and `omarchy-setup-input` own them (`docs/input-methods.md`).
- `omarchy-apply-pacman` renders pacman for every caller: the channel templates, the profile's `pacman/mirrorlist-<channel>.prepend` ahead of the mirrorlist, and its `pacman.conf.append`. Nothing in `omarchy update` or migrations may change the region or `/etc/locale.conf`.
- Language is separate from region: `omarchy-language en|zh` (Setup > Language) owns `LANG` in `/etc/locale.conf`, the installer sets it from its first screen, and nothing keyed by region touches it.
- China-only menu rows carry `"when":"[[ \"$(omarchy-region)\" == \"China\" ]]"` on every row, not only the submenu, because search checks each row's own guard.
- Chinese menu labels live in `default/omarchy/omarchy-menu.zh-cn.jsonc`, merged when `LANG` starts with `zh_CN`; give a new menu row with translatable words a label there.
- Shell text goes through `I18n.tr("English %1", arg)` from `qs.Commons`, with its Chinese in `shell/Commons/I18nZh.js` keyed by the English. Notifications and OSD messages are worded by their sender with `omarchy-text "English %1" "$arg"` (Python: `text()`, QML: `I18n.tr`), which reads the same table; the shell shows them as given, so user text is never translated. `test/shell.d/shell-i18n-test.sh` fails on a missing entry.
# Config Structure
- `config/` - 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)
- [Dual Boot Install](manual/50-dual-boot-install.md)
- [Unattended Installs](manual/51-unattended-installs.md)
- [China](manual/52-china.md)
## 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
`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
- `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[install]="Optional software installers"
GROUP_DESCRIPTIONS[installed]="Installed optional service checks"
GROUP_DESCRIPTIONS[language]="System language"
GROUP_DESCRIPTIONS[launch]="Application launchers"
GROUP_DESCRIPTIONS[menu]="Omarchy menu commands"
GROUP_DESCRIPTIONS[migrate]="Migration runner"
@@ -74,6 +75,7 @@ GROUP_DESCRIPTIONS[plymouth]="Plymouth boot theme management"
GROUP_DESCRIPTIONS[power]="Power supply detection"
GROUP_DESCRIPTIONS[powerprofiles]="Power profile management"
GROUP_DESCRIPTIONS[refresh]="Reset config to defaults"
GROUP_DESCRIPTIONS[region]="Region-specific package sources and setup"
GROUP_DESCRIPTIONS[reinstall]="Reinstall and reset workflows"
GROUP_DESCRIPTIONS[reminder]="Desktop notification reminders"
GROUP_DESCRIPTIONS[remove]="Removal workflows"
+16 -16
View File
@@ -85,7 +85,7 @@ finish() {
fail() {
if [[ $events == "true" ]]; then
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
echo "$1" >&2
fi
@@ -113,9 +113,9 @@ interruptible() {
}
if omarchy-cmd-missing "$provider"; then
say "Installing the $name CLI…"
say "$(omarchy-text "Installing the %1 CLI…" "$name")"
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
mise use -g "$package" || exit 1
fi
@@ -150,26 +150,26 @@ if [[ -n $reauth ]]; then
else
account_home=$(omarchy-agent-account-state homes "$provider" | awk -F'\t' -v id="$reauth" '$1 == id { print $2 }')
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
say "Sign in to $name again in the browser window that opens."
login || fail "The $name sign-in didn't finish."
say "$(omarchy-text "Sign in to %1 again in the browser window that opens." "$name")"
login || fail "$(omarchy-text "The %1 sign-in didn't finish." "$name")"
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
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
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
fi
# 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.
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
# 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
@@ -179,9 +179,9 @@ if ! primary_signed_in "$provider"; then
printf '%s\n' "$provider" >"$HOME/.config/omarchy/defaults/agent"
fi
omarchy-agent-usage-update --force "$provider" >/dev/null 2>&1 &
finish "Signed in to $name."
finish "$(omarchy-text "Signed in to %1." "$name")"
else
fail "The $name sign-in didn't finish."
fail "$(omarchy-text "The %1 sign-in didn't finish." "$name")"
fi
else
if [[ -z $label && $events == "false" && -t 0 ]]; then
@@ -189,7 +189,7 @@ else
fi
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
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
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
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")
omarchy-agent-usage-update --force "$provider" >/dev/null 2>&1 &
if [[ $events == "true" ]]; then
finish "Added $added."
finish "$(omarchy-text "Added %1." "$added")"
else
finish "Added $added."
finish "$(omarchy-text "Added %1." "$added")"
echo "Switch to it with: omarchy agent account use $provider $(jq -r .id <<<"$account")"
fi
else
+29 -22
View File
@@ -73,6 +73,11 @@ class AccountError(Exception):
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
@@ -335,12 +340,12 @@ def begin(provider: str) -> Path:
def register(provider: str, label: str, pending: Path) -> dict[str, Any]:
pending = 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)
if not found["accountId"]:
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:
# 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"]:
if account.get("accountId") == found["accountId"]:
shutil.rmtree(pending, ignore_errors=True)
raise AccountError(
f"That's {account['label']} ({found['email'] or 'already added'}). "
"Try again, and sign in as the other account in the private window."
)
raise AccountError(text(
"That's %1 (%2). 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"
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]:
label = label.strip()
if not label:
raise AccountError("Give the account a name")
raise AccountError(text("Give the account a name"))
with Locked(provider) as registry:
account = find(registry, target)
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)
if not account.get("primary") and not account.get("home"):
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)]
account = find(registry, target)
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["alert"] = ""
if not account.get("primary"):
@@ -456,12 +461,12 @@ def remove(provider: str, target: str) -> dict[str, Any]:
with Locked(provider) as registry:
account = find(registry, target)
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"):
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)
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
# home, and rmtree removes a link without following it.
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]:
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:
registry["switch"] = mode
if threshold is not None:
try:
registry["threshold"] = max(50, min(100, int(threshold)))
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"] = ""
return dict(registry)
@@ -556,10 +561,10 @@ def duration(delta: dt.timedelta) -> str:
hours, minutes = divmod(minutes, 60)
days, hours = divmod(hours, 24)
if days:
return f"{days}d {hours}h"
return text("%1d %2h", days, hours)
if hours:
return f"{hours}h {minutes}m"
return f"{minutes}m"
return text("%1h %2m", hours, minutes)
return text("%1m", minutes)
# 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"]
if decision["action"] == "exhausted":
soonest = decision["soonest"]
when = f" {soonest[1]['label']} resets in {duration(soonest[0] - now)}." if soonest else ""
notify(f"All {name} accounts are over {threshold}%", f"Staying on {source['label']}.{when}")
staying = text("Staying on %1.", source["label"])
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
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":
registry["active"] = target["id"]
if not target.get("primary"):
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
notify(
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"]],
)
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
echo "New $name sessions already use $active."
else
message="Running sessions stay on $previous."
message=$(omarchy-text "Running sessions stay on %1." "$previous")
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.
omarchy-agent-usage-update --limits-only "$1" >/dev/null 2>&1 &
fi
+11 -4
View File
@@ -22,6 +22,7 @@ import json
import os
import re
import sqlite3
import subprocess
import sys
import tempfile
import time
@@ -32,6 +33,11 @@ from typing import Any
AGENT_ID = "claude"
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."
USAGE_ENDPOINT = "https://api.anthropic.com/api/oauth/usage"
PROBE_MIN_INTERVAL_SECONDS = 15
@@ -989,11 +995,12 @@ def probe_limits(access_token: str) -> dict[str, Any]:
except urllib.error.HTTPError as error:
retry_after = error.headers.get("retry-after", "") if error.headers else ""
if error.code == 429:
help_text = "Anthropic's usage endpoint is rate limiting checks right now" + (
f" (retry after {retry_after}s)" if retry_after else ""
) + ". Local Claude Code stats are still shown."
if retry_after:
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)
else:
help_text = text("Anthropic's usage endpoint is rate limiting checks right now. Local Claude Code stats are still shown.")
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}
except Exception:
# 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
omarchy-brightness-keyboard-mute on
omarchy-osd -i microphone-muted -m "Microphone muted"
omarchy-osd -i microphone-muted -m "$(omarchy-text "Microphone muted")"
else
omarchy-brightness-keyboard-mute off
omarchy-osd -i microphone -m "Microphone on"
omarchy-osd -i microphone -m "$(omarchy-text "Microphone on")"
fi
+1 -1
View File
@@ -13,7 +13,7 @@ sinks=$(timeout 2 pactl -f json list sinks |
sinks_count=$(jq 'length' <<<"$sinks")
if (( sinks_count == 0 )); then
omarchy-osd -m "No audio devices found"
omarchy-osd -m "$(omarchy-text "No audio devices found")"
exit 1
fi
+1 -1
View File
@@ -13,5 +13,5 @@ fi
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"
+3 -3
View File
@@ -129,9 +129,9 @@ if [[ $charge_holding == "true" ]]; then
threshold_label="${threshold_end}%"
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
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
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
+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)
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
fi
@@ -33,4 +33,4 @@ fi
# the notification history, and an unmarked copy would be retained by clipboard
# history. Pasting still works; only the recorded copy is given up.
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")"
+4 -4
View File
@@ -23,7 +23,7 @@
OUTPUT_DIR="${OMARCHY_SCREENRECORD_DIR:-${XDG_VIDEOS_DIR:-$HOME/Videos}}"
if [[ ! -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
fi
@@ -76,7 +76,7 @@ start_webcam_overlay() {
if [[ -z $WEBCAM_DEVICE ]]; then
WEBCAM_DEVICE=$(omarchy-capture-webcam-list | sed -n '1s/[[:space:]].*//p')
if [[ -z $WEBCAM_DEVICE ]]; then
omarchy-notification-send -u critical -t 3000 "No webcam devices found"
omarchy-notification-send -u critical -t 3000 "$(omarchy-text "No webcam devices found")"
return 1
fi
fi
@@ -231,7 +231,7 @@ stop_screenrecording() {
if pgrep -f "^gpu-screen-recorder" >/dev/null; then
pkill -9 -f "^gpu-screen-recorder"
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
finalize_recording
local filename=$(cat "$RECORDING_FILE" 2>/dev/null)
@@ -241,7 +241,7 @@ stop_screenrecording() {
# 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
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}" \
--exec mpv -- "$filename"
@@ -5,7 +5,7 @@
mapfile -t devices < <(omarchy-capture-webcam-list)
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
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
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
}
# The dev channel refreshes pacman with the checkout's own tools. A checkout
# without this machine's region profile would silently drop its repositories.
validate_dev_region() {
local checkout="$1" region_file="${OMARCHY_REGION_FILE:-/etc/omarchy/region}" region=global
[[ -f $region_file ]] && region=$(<"$region_file")
[[ $region == "global" ]] && return 0
# The refresh after linking needs a runnable helper and every fragment, not
# just a directory with the region's name: dev refreshes edge, and leaving
# dev later refreshes stable or rc with this checkout's tools.
local profile="$checkout/default/regions/$region/pacman" fragment supported=true
[[ -x $checkout/bin/omarchy-apply-pacman ]] || supported=false
for fragment in pacman.conf.append mirrorlist-stable.prepend mirrorlist-rc.prepend mirrorlist-edge.prepend; do
[[ -r $profile/$fragment ]] || supported=false
done
if ! $supported; then
fail "$checkout does not support the '$region' region, so switching would drop its package repositories. Check out a branch with default/regions/$region, or remove $region_file to use global defaults."
fi
}
link_dev_checkout() {
local checkout="$1" required
[[ -d $checkout/.git ]] || git clone https://github.com/omacom/omarchy.git "$checkout"
validate_dev_region "$checkout"
# Check the destination before changing /etc/omarchy.conf or sudo's path.
# An existing checkout is not pulled automatically and may predate this policy.
+1 -1
View File
@@ -18,7 +18,7 @@ copy_url() {
[[ -n $url ]] || return 1
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() {
+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.
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
fi
@@ -148,7 +148,7 @@ download_url() {
# toast would exit before the thumbnail cleanup below is ever scheduled.
# `--` keeps mpv from parsing a leading-dash filename as an option; the path
# 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}" \
--exec mpv -- "$filepath" || true
@@ -160,7 +160,7 @@ download_url() {
rm -f "$preview"
) &
else
omarchy-notification-send -u critical -g 󰅖 "Download failed" "$url"
omarchy-notification-send -u critical -g 󰅖 "$(omarchy-text "Download failed")" "$url"
fi
exit 0
+2 -2
View File
@@ -40,8 +40,8 @@ announce() {
omarchy-notification-send \
--urgency critical \
--glyph "$CRASH_GLYPH" \
"Process crashed: $comm" \
"Click to diagnose with AI" \
"$(omarchy-text "Process crashed: %1" "$comm")" \
"$(omarchy-text "Click to diagnose with AI")" \
--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
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")"
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
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")
local new_id
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
[[ $new_id =~ ^[0-9]+$ ]] && printf '%s\n' "$new_id" >"$id_file"
fi
+2 -2
View File
@@ -74,11 +74,11 @@ XML
omarchy-restart-shell
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
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
omarchy-hook font-set "$font_name"
+2 -2
View File
@@ -10,7 +10,7 @@ if (( $# == 0 )); then
mapfile -t cores < <(omarchy-games-retro-cores)
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
fi
@@ -69,4 +69,4 @@ EOF
chmod +x "$desktop_file"
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")"
+5 -5
View File
@@ -16,7 +16,7 @@ wake() {
on() {
if omarchy-hyprland-toggle-enabled $TOGGLE; then
omarchy-hyprland-toggle $TOGGLE off
omarchy-notification-send -g 󰍹 "Laptop display enabled"
omarchy-notification-send -g 󰍹 "$(omarchy-text "Laptop display enabled")"
fi
wake
@@ -24,25 +24,25 @@ on() {
off() {
if [[ -z $INTERNAL ]]; then
omarchy-notification-send -g 󰍹 "No laptop display found"
omarchy-notification-send -g 󰍹 "$(omarchy-text "No laptop display found")"
exit 1
fi
# The name is written into generated Lua below, so only a plain connector
# name may pass; anything else could execute on the next reload.
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
fi
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
fi
if omarchy-hyprland-toggle-disabled $TOGGLE && omarchy-hyprland-toggle-disabled $MIRROR_TOGGLE; then
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
fi
}
+5 -5
View File
@@ -13,12 +13,12 @@ EXTERNAL=$(hyprctl monitors -j | jq -r '.[] | select(.name | test("^(eDP|LVDS|DS
on() {
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
fi
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
fi
@@ -26,7 +26,7 @@ on() {
# names may pass; a user-created headless output can carry any name.
for output in "$INTERNAL" "$EXTERNAL"; do
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
fi
done
@@ -35,7 +35,7 @@ on() {
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"
omarchy-notification-send -g 󰍹 "Mirroring enabled ($EXTERNAL)"
omarchy-notification-send -g 󰍹 "$(omarchy-text "Mirroring enabled (%1)" "$EXTERNAL")"
hyprctl reload
fi
}
@@ -43,7 +43,7 @@ on() {
off() {
if omarchy-hyprland-toggle-enabled $TOGGLE; then
omarchy-hyprland-toggle $TOGGLE off
omarchy-notification-send -g 󰍹 "Extended mode restored"
omarchy-notification-send -g 󰍹 "$(omarchy-text "Extended mode restored")"
fi
}
@@ -3,6 +3,6 @@
# omarchy:summary=Toggle single-window square aspect ratio.
case $(omarchy-hyprland-toggle single-window-aspect-ratio) in
on) omarchy-notification-send -g  "Enable single-window square aspect ratio" ;;
off) omarchy-notification-send -g  "Disable single-window square aspect ratio" ;;
on) omarchy-notification-send -g  "$(omarchy-text "Enable single-window square aspect ratio")" ;;
off) omarchy-notification-send -g  "$(omarchy-text "Disable single-window square aspect ratio")" ;;
esac
+2 -2
View File
@@ -40,14 +40,14 @@ notify_missing_width() {
local key="$1"
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() {
local key="$1"
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() {
+1 -1
View File
@@ -18,4 +18,4 @@ printf 'hl.workspace_rule({ workspace = "%s", layout = "%s" })\n' "$ACTIVE_WORKS
hyprctl eval "hl.workspace_rule({ workspace = \"$ACTIVE_WORKSPACE\", layout = \"$NEW_LAYOUT\" })" >/dev/null 2>&1 || \
hyprctl keyword workspace "$ACTIVE_WORKSPACE, layout:$NEW_LAYOUT"
omarchy-notification-send -g 󱂬 "Workspace layout set to $NEW_LAYOUT"
omarchy-notification-send -g 󱂬 "$(omarchy-text "Workspace layout set to %1" "$NEW_LAYOUT")"
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
# omarchy:summary=Configure or inspect the input method selected at installation
# omarchy:args=<status|configure [--seed|--defaults] [none|mozc|hangul|pinyin|chewing]>
# omarchy:hidden=true
set -euo pipefail
python "$OMARCHY_PATH/default/input-methods/configure.py" "$@"
+1 -1
View File
@@ -23,5 +23,5 @@ superwhisper shortcuts set toggle "Alt+Space" >/dev/null
superwhisper shortcuts set hold none >/dev/null
omarchy-dictation-use superwhisper
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
+1 -1
View File
@@ -30,4 +30,4 @@ systemctl --user enable --now voxtype.service
omarchy-dictation-use voxtype
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
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"
+1 -1
View File
@@ -16,7 +16,7 @@ terminal=$(xdg-terminal-exec --print-id)
case $terminal in
*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
;;
esac
+2 -1
View File
@@ -332,6 +332,7 @@ dynamic_bindings() {
static_bindings() {
echo "SHIFT ALT,L,Copy URL from Web App,sendshortcut,SHIFT ALT,L,"
echo "SHIFT ALT,D,Download Video from Web App,sendshortcut,SHIFT ALT,D,"
echo "CTRL SHIFT,SPACE,Switch input language,exec,fcitx5-remote -t"
}
# Actions Omarchy binds to a second chord meant as an alternative, named one at
@@ -533,7 +534,7 @@ output_binding_records_uncached() {
keybindings_cache_key() {
{
printf 'v14\n'
printf 'v15\n'
hyprctl devices 2>/dev/null | grep -F 'active keymap:'
hyprctl binds 2>/dev/null
} | sha256sum | awk '{ print $1 }'
+1 -1
View File
@@ -28,7 +28,7 @@ rows=$(jq -r --arg icon "$PLUGIN_ICON" \
". as \$plugins
| .[] | select($filter)
| \$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
[[ -n $selection ]] || exit 0
+1 -1
View File
@@ -33,7 +33,7 @@ else
picked=$(omarchy-file-select "${select_args[@]}") || status=$?
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
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
sudo timedatectl set-timezone "$timezone"
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)
if (( pending_count == 1 )); then
message="Click to run 1 pending migration."
message=$(omarchy-text "Click to run 1 pending migration.")
else
message="Click to run $pending_count pending migrations."
message=$(omarchy-text "Click to run %1 pending migrations." "$pending_count")
fi
# 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
# 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
# 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-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
clone_complete=1
omarchy-notification-send -g 󰐱 \
"Editing Cloned Plugin" \
"Original plugin has been replace by clone."
"$(omarchy-text "Editing Cloned Plugin")" \
"$(omarchy-text "Original plugin has been replace by clone.")"
echo "Cloned $source_id to $target_dir and switched to $new_id"
if (( edit_clone )); then
# Word-splitting is deliberate: EDITOR may carry flags.
+1
View File
@@ -599,6 +599,7 @@ keyboard_form() {
done
apply_keyboard "$keyboard"
omarchy_persist_input_selection
}
confirm_reboot() {
+16 -2
View File
@@ -30,11 +30,25 @@ if [[ $channel != "stable" && $channel != "rc" && $channel != "edge" ]]; then
exit 2
fi
# Render the channel templates plus this machine's regional additions as the
# caller, so root only copies finished files. The scratch directory goes on
# every exit, including a failed render, a refused sudo prompt, or a signal;
# revoking the sudo timestamp stays the last thing the exit path does.
rendered=$(mktemp -d)
cleanup_refresh() {
local status=$?
rm -rf "$rendered"
omarchy_security_exit_with_revoked_sudo "$status"
}
trap cleanup_refresh EXIT
"$OMARCHY_PATH/bin/omarchy-apply-pacman" --render "$channel" "$rendered"
sudo cp -f /etc/pacman.conf /etc/pacman.conf.bak
sudo cp -f /etc/pacman.d/mirrorlist /etc/pacman.d/mirrorlist.bak
echo "Setting channel to $channel"
sudo cp -f "$OMARCHY_PATH/default/pacman/pacman-$channel.conf" /etc/pacman.conf
sudo cp -f "$OMARCHY_PATH/default/pacman/mirrorlist-$channel" /etc/pacman.d/mirrorlist
sudo cp -f "$rendered/pacman.conf" /etc/pacman.conf
sudo cp -f "$rendered/mirrorlist" /etc/pacman.d/mirrorlist
rm -rf "$rendered"
# Allow user customization of /etc/pacman.conf before the upgrade runs, so
# custom repositories and IgnorePkg entries shape the downgrade-capable
+57
View File
@@ -0,0 +1,57 @@
#!/bin/bash
# omarchy:summary=Show or switch the region this machine is set up for
# omarchy:args=[global|cn]
# omarchy:examples=omarchy region | omarchy region cn
# omarchy:requires-sudo=true
set -euo pipefail
region_file=/etc/omarchy/region
if (($# == 0)); then
if [[ ! -e $region_file ]]; then
echo "Global"
elif [[ $(<"$region_file") == "cn" ]]; then
echo "China"
else
echo "Unknown region in $region_file: $(<"$region_file")" >&2
exit 1
fi
exit 0
fi
if [[ $1 != "global" && $1 != "cn" ]]; then
echo "Usage: omarchy-region [global|cn]" >&2
exit 1
fi
channel=$(omarchy-version-channel)
if [[ $channel != "stable" && $channel != "rc" && $channel != "edge" ]]; then
echo "Cannot tell this machine's package channel ($channel); run omarchy channel set first" >&2
exit 1
fi
if [[ $1 == "cn" ]]; then
# Upgrade first: the keyring install below syncs new databases.
omarchy-update-pacman -Syu --noconfirm
sudo env OMARCHY_REGION=cn OMARCHY_PATH="$OMARCHY_PATH" bash -e "$OMARCHY_PATH/install/config/region.sh"
# Trust ArchLinuxCN before the refresh upgrades through it.
rendered=$(mktemp -d)
trap 'rm -rf "$rendered"' EXIT
omarchy-apply-pacman --render "$channel" "$rendered"
mapfile -t profile_packages < <(grep -Ev '^[[:space:]]*(#|$)' "$OMARCHY_PATH/default/regions/cn/packages")
sudo pacman --config "$rendered/pacman.conf" -Sy --noconfirm --needed "${profile_packages[@]}"
sudo pacman-key --populate archlinuxcn
omarchy-refresh-pacman "$channel"
else
sudo env OMARCHY_REGION=global OMARCHY_PATH="$OMARCHY_PATH" bash -e "$OMARCHY_PATH/install/config/region.sh"
omarchy-refresh-pacman "$channel"
fi
# The developer mirrors reach only a new session, so offer one.
if [[ -t 0 && -t 1 ]] && gum confirm --affirmative "$(omarchy-text "Log out now")" --negative "$(omarchy-text "Later")" "$(omarchy-text "Log out now to switch the developer mirrors?")" 2>&1; then
omarchy-system-logout
else
omarchy-text "Log back in to switch the developer mirrors."
fi
+15 -14
View File
@@ -57,16 +57,16 @@ show_reminders() {
[[ -f $reminder_dir/${timer%.timer}.message ]] && reminder_message=$(<"$reminder_dir/${timer%.timer}.message")
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
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
done < <(active_reminder_timers "$now")
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
omarchy-notification-send -g 󰢌 "Upcoming reminders" "${body%$'\n'}"
omarchy-notification-send -g 󰢌 "$(omarchy-text "Upcoming reminders")" "${body%$'\n'}"
fi
}
@@ -75,7 +75,8 @@ show_json() {
local reminder_dir="${XDG_RUNTIME_DIR:-/tmp}/omarchy-reminders"
local now=$(date +%s)
local count=0
local tooltip="Set Reminder"
local tooltip
tooltip=$(omarchy-text "Set Reminder")
while IFS=$'\t' read -r timer next; do
count=$((count + 1))
@@ -91,7 +92,7 @@ show_json() {
if [[ -n $reminder_message ]]; then
label=$reminder_message
else
label="${reminder_minutes}-min reminder"
label=$(omarchy-text "%1-min reminder" "$reminder_minutes")
fi
item_json=$(jq -cn \
@@ -109,9 +110,9 @@ show_json() {
done < <(active_reminder_timers "$now")
if ((count == 1)); then
tooltip="1 reminder"
tooltip=$(omarchy-text "1 reminder")
elif ((count > 1)); then
tooltip="$count reminders"
tooltip=$(omarchy-text "%1 reminders" "$count")
fi
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
refresh_indicator
omarchy-notification-send -g 󰢌 "All reminders have been cleared"
omarchy-notification-send -g 󰢌 "$(omarchy-text "All reminders have been cleared")"
}
usage() {
@@ -176,7 +177,7 @@ if [[ -z $minutes ]] || [[ ! $minutes =~ ^[0-9]+$ ]] || ((minutes == 0)); then
fi
if [[ -z $message ]]; then
message="Your ${minutes} minutes are up"
message=$(omarchy-text "Your %1 minutes are up" "$minutes")
fi
set_at=$(date +%s)
@@ -184,18 +185,18 @@ remind_at=$(date -d "+${minutes} minutes" +%H:%M)
unit="omarchy-reminder-${minutes}m-$set_at"
reminder_dir="${XDG_RUNTIME_DIR:-/tmp}/omarchy-reminders"
message_file="$reminder_dir/$unit.message"
confirmation="You'll be reminded at $remind_at"
confirmation_title="Reminder set for ${minutes} minutes"
confirmation=$(omarchy-text "You'll be reminded at %1" "$remind_at")
confirmation_title=$(omarchy-text "Reminder set for %1 minutes" "$minutes")
mkdir -p "$reminder_dir"
if [[ -n $custom_message ]]; then
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
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
omarchy-notification-send -g 󰢌 "$confirmation_title" "$confirmation"
+22 -1
View File
@@ -2,9 +2,30 @@
# omarchy:summary=Restart the XCompose input method service (fcitx5) to apply new compose key settings.
set -euo pipefail
# A fcitx5 running outside the unit owns org.fcitx.Fcitx5, so the unit's
# instance would exit on arrival and the stale process would keep serving the
# old ~/.XCompose -- a restart that reports success and changes nothing.
systemctl --user stop omarchy-fcitx5.service
pkill -x fcitx5 >/dev/null 2>&1 || true
pkill -u "$UID" -x fcitx5 >/dev/null 2>&1 || true
for attempt in {1..50}; do
if ! pgrep -u "$UID" -x fcitx5 >/dev/null; then
break
fi
sleep 0.1
done
if pgrep -u "$UID" -x fcitx5 >/dev/null; then
# An activation concurrent with stop may have restarted the unit.
systemctl --user stop omarchy-fcitx5.service
pkill -u "$UID" -x fcitx5 >/dev/null 2>&1 || true
for attempt in {1..50}; do
if ! pgrep -u "$UID" -x fcitx5 >/dev/null; then break; fi
sleep 0.1
done
fi
if pgrep -u "$UID" -x fcitx5 >/dev/null; then
echo "Fcitx did not stop; input service was not restarted." >&2
exit 1
fi
systemctl --user start omarchy-fcitx5.service
+30
View File
@@ -0,0 +1,30 @@
#!/bin/bash
# omarchy:summary=Set up Japanese, Korean, or Chinese typing
# omarchy:args=[mozc|hangul|pinyin|chewing]
# omarchy:examples=omarchy setup input | omarchy setup input mozc
# omarchy:requires-sudo=true
set -euo pipefail
if (( $# > 1 )); then
echo "Usage: omarchy setup input [mozc|hangul|pinyin|chewing]" >&2
exit 1
fi
method=${1:-}
if [[ -z $method ]]; then
choice=$(jq -r 'to_entries[] | select(.key != "none") | .value.label' "$OMARCHY_PATH/default/input-methods/presets.json" |
gum choose --header "Select input language")
method=$(jq -r --arg label "$choice" 'to_entries[] | select(.value.label == $label) | .key' "$OMARCHY_PATH/default/input-methods/presets.json")
fi
case "$method" in
mozc | hangul | pinyin | chewing) ;;
*) echo "Unknown input method: $method" >&2; exit 1 ;;
esac
mapfile -t packages < <(jq -r --arg method "$method" '.[$method].packages[]' "$OMARCHY_PATH/default/input-methods/presets.json")
omarchy-pkg-add "${packages[@]}"
omarchy-input-method configure "$method"
echo "Input method added. The default switch is Ctrl+Shift+Space or the Japanese/Korean input key; existing custom keys are preserved."
+1 -1
View File
@@ -6,7 +6,7 @@
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
omarchy-hyprland-window-close-all
+1 -1
View File
@@ -8,7 +8,7 @@
# 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
omarchy-osd -i reboot -m "Rebooting" -d 5000
omarchy-osd -i reboot -m "$(omarchy-text "Rebooting")" -d 5000
omarchy-state clear re*-required
+2 -2
View File
@@ -7,7 +7,7 @@
if [[ ${1:-} == "--inhibited" ]]; then
# 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-hyprland-window-close-all
) &
@@ -21,7 +21,7 @@ elif [[ ${1:-} == "--inhibit" ]]; then
--who=Omarchy --why="Shutdown in progress" --mode=block \
"$0" --inhibited || {
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"
}
else
+2 -2
View File
@@ -98,8 +98,8 @@ report_unsecured() {
"$1" >&2
omarchy-notification-send -u critical -g 󰌾 \
"Screen did not lock before suspend" \
"The session was left unlocked ($1)." >/dev/null 2>&1 || true
"$(omarchy-text "Screen did not lock before suspend")" \
"$(omarchy-text "The session was left unlocked (%1)." "$1")" >/dev/null 2>&1 || true
exit 1
}
+1 -1
View File
@@ -59,7 +59,7 @@ claim_path() {
announce() {
local path="$1"
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
*.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=$?
if ((${status:-0} > 1)); then
omarchy-notification-send -g "󰒊" -u critical "Could not send to $name" \
"The file chooser did not open"
omarchy-notification-send -g "󰒊" -u critical "$(omarchy-text "Could not send to %1" "$name")" \
"$(omarchy-text "The file chooser did not open")"
exit 1
fi
@@ -43,8 +43,8 @@ else
fi
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
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
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
enable)
rm -f "$toggle"
omarchy-notification-send "Background Intros" "Enabled"
omarchy-notification-send "$(omarchy-text "Background Intros")" "$(omarchy-text "Enabled")"
;;
disable)
mkdir -p "$(dirname "$toggle")"
touch "$toggle"
omarchy-notification-send "Background Intros" "Disabled"
omarchy-notification-send "$(omarchy-text "Background Intros")" "$(omarchy-text "Disabled")"
;;
toggle)
if [[ -f $toggle ]]; then
+1 -1
View File
@@ -17,7 +17,7 @@ mapfile -d '' -t BACKGROUNDS < <(
TOTAL=${#BACKGROUNDS[@]}
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
# Get current background from symlink
if [[ -L $CURRENT_BACKGROUND_LINK ]]; then
+1 -1
View File
@@ -35,5 +35,5 @@ fi
# Now remove the theme directory for THEME_NAME
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"
+1 -1
View File
@@ -190,7 +190,7 @@ set_theme_background() {
if [[ -z $CHOSEN_THEME_BACKGROUND || ! -f $CHOSEN_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
return
fi
+2 -2
View File
@@ -16,7 +16,7 @@ off) omarchy-hyprland-toggle no-animations on ;;
esac
if omarchy-hyprland-toggle-enabled no-animations; then
omarchy-notification-send -g 󰕟 "Animations disabled"
omarchy-notification-send -g 󰕟 "$(omarchy-text "Animations disabled")"
else
omarchy-notification-send -g 󰕟 "Animations enabled"
omarchy-notification-send -g 󰕟 "$(omarchy-text "Animations enabled")"
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.
if omarchy-toggle-enabled crash-capture-off; then
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
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
+2 -2
View File
@@ -5,7 +5,7 @@
omarchy-toggle screensaver-off
if omarchy-toggle-enabled screensaver-off; then
omarchy-notification-send -g 󱄄 "Screensaver disabled"
omarchy-notification-send -g 󱄄 "$(omarchy-text "Screensaver disabled")"
else
omarchy-notification-send -g 󱄄 "Screensaver enabled"
omarchy-notification-send -g 󱄄 "$(omarchy-text "Screensaver enabled")"
fi
+2 -2
View File
@@ -5,7 +5,7 @@
omarchy-toggle suspend-off
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
omarchy-notification-send -g 󰒲 "Suspend now available in system menu"
omarchy-notification-send -g 󰒲 "$(omarchy-text "Suspend now available in system menu")"
fi
+2 -2
View File
@@ -5,7 +5,7 @@
omarchy-toggle herdr-theme-sync
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
omarchy-notification-send -g 󰸌 "Herdr theme sync disabled"
omarchy-notification-send -g 󰸌 "$(omarchy-text "Herdr theme sync disabled")"
fi
+3 -3
View File
@@ -193,14 +193,14 @@ main() {
output=$(output_path "$input" "$format" "$resolution")
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"
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
transcode_picture "$input" "$format" "$resolution" "$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
}
+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 "$ICON_DIR/$icon_name.png" "$ICON_DIR/$APP_NAME.png" "$OLD_ICON_DIR/$APP_NAME.png"
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
+2
View File
@@ -1802,6 +1802,8 @@ copy_missing_config_defaults() {
while IFS= read -r -d '' source_path; do
rel="${source_path#$source_dir/}"
is_retired_config_file "$rel" && continue
# The input migration preserves working shortcuts in existing IME profiles.
[[ $rel == "fcitx5/config" ]] && continue
target_path="$target_dir/$rel"
[[ -e $target_path || -L $target_path ]] && continue
+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"
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
update-desktop-database "$DESKTOP_DIR" &>/dev/null
+1 -1
View File
@@ -1414,7 +1414,7 @@ launch_windows() {
if ! priv up_wait; then
echo "❌ Failed to start Windows VM!"
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
fi
+3
View File
@@ -0,0 +1,3 @@
# Keep Pinyin offline without prompting to enable cloud prediction.
FirstRun=False
CloudPinyinEnabled=False
+2
View File
@@ -0,0 +1,2 @@
[TriggerKey]
0=Super+semicolon
+1
View File
@@ -0,0 +1 @@
Allow Overriding System XKB Settings=False
+20
View File
@@ -0,0 +1,20 @@
# Leave Ctrl+Space to tmux/Herdr and the Super shortcuts to the desktop.
[Hotkey/TriggerKeys]
0=Control+Shift+space
1=Zenkaku_Hankaku
2=Hangul
[Hotkey/ActivateKeys]
0=Henkan
[Hotkey/DeactivateKeys]
0=Muhenkan
[Hotkey/AltTriggerKeys]
[Hotkey/EnumerateGroupForwardKeys]
[Hotkey/EnumerateGroupBackwardKeys]
[Behavior]
ActiveByDefault=False
@@ -1,6 +1,298 @@
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<!-- CJK: fold the language tag onto one tag per Noto CJK variant -->
<match target="pattern">
<test name="lang" qual="first" compare="eq">
<string>zh</string>
</test>
<edit name="lang" mode="assign">
<string>zh-cn</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="eq">
<string>zh-hant</string>
</test>
<edit name="lang" mode="assign">
<string>zh-tw</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-hant-hk</string>
</test>
<edit name="lang" mode="assign">
<string>zh-hk</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-hant-mo</string>
</test>
<edit name="lang" mode="assign">
<string>zh-hk</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-hant</string>
</test>
<edit name="lang" mode="assign">
<string>zh-tw</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-hans</string>
</test>
<edit name="lang" mode="assign">
<string>zh-cn</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-sg</string>
</test>
<edit name="lang" mode="assign">
<string>zh-cn</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-mo</string>
</test>
<edit name="lang" mode="assign">
<string>zh-hk</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="eq">
<string>yue</string>
</test>
<edit name="lang" mode="assign">
<string>zh-hk</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>yue-hans</string>
</test>
<edit name="lang" mode="assign">
<string>zh-cn</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>yue-cn</string>
</test>
<edit name="lang" mode="assign">
<string>zh-cn</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>yue</string>
</test>
<edit name="lang" mode="assign">
<string>zh-hk</string>
</edit>
</match>
<!-- Then append that variant after the generic family it falls back from -->
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-cn</string>
</test>
<test name="family">
<string>sans-serif</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Sans CJK SC</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-cn</string>
</test>
<test name="family">
<string>serif</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Serif CJK SC</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-cn</string>
</test>
<test name="family">
<string>monospace</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Sans Mono CJK SC</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-hk</string>
</test>
<test name="family">
<string>sans-serif</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Sans CJK HK</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-hk</string>
</test>
<test name="family">
<string>serif</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Serif CJK HK</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-hk</string>
</test>
<test name="family">
<string>monospace</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Sans Mono CJK HK</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-tw</string>
</test>
<test name="family">
<string>sans-serif</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Sans CJK TC</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-tw</string>
</test>
<test name="family">
<string>serif</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Serif CJK TC</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-tw</string>
</test>
<test name="family">
<string>monospace</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Sans Mono CJK TC</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<langset><string>ja</string></langset>
</test>
<test name="family">
<string>sans-serif</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Sans CJK JP</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<langset><string>ja</string></langset>
</test>
<test name="family">
<string>serif</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Serif CJK JP</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<langset><string>ja</string></langset>
</test>
<test name="family">
<string>monospace</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Sans Mono CJK JP</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<langset><string>ko</string></langset>
</test>
<test name="family">
<string>sans-serif</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Sans CJK KR</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<langset><string>ko</string></langset>
</test>
<test name="family">
<string>serif</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Serif CJK KR</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<langset><string>ko</string></langset>
</test>
<test name="family">
<string>monospace</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Sans Mono CJK KR</string>
</edit>
</match>
<match target="pattern">
<test name="family" qual="any">
<string>sans-serif</string>
+66
View File
@@ -0,0 +1,66 @@
local paths = require("default.hypr.paths")
-- Same terminal tag test as clipboard.lua.
local function active_window_is_terminal()
local window = hl.get_active_window()
if not window then
return false
end
for _, tag in ipairs(window.tags or {}) do
if tag:gsub("%*$", "") == "terminal" then
return true
end
end
return false
end
-- Read on demand so adding an engine in a live desktop needs no reload.
local function has_input_method()
local file = io.open(paths.config_home .. "/fcitx5/profile", "r")
if not file then return false end
local in_item, found = false, false
for line in file:lines() do
if line:match("^%[") then
in_item = line:match("^%[Groups/%d+/Items/%d+%]$") ~= nil
elseif in_item then
local name = line:match("^Name=(.+)$")
if name and not name:match("^keyboard%-") then found = true end
end
end
file:close()
return found
end
-- Preserve custom Ctrl + Space triggers. Without a
-- [Hotkey/TriggerKeys] list in the config, fcitx5 uses its default, which has
-- Ctrl + Space.
local function fcitx5_toggles_on_ctrl_space()
local file = io.open(paths.config_home .. "/fcitx5/config", "r")
if not file then
return true
end
local in_triggers, has_triggers, toggles = false, false, false
for line in file:lines() do
if line:match("^%[") then
in_triggers = line == "[Hotkey/TriggerKeys]"
has_triggers = has_triggers or in_triggers
elseif in_triggers and line:match("^%d+=Control%+space$") then
toggles = true
end
end
file:close()
return toggles or not has_triggers
end
-- Pass the tmux/Herdr prefix through and return to direct input, so the next
-- key reaches the multiplexer instead of the engine's composition.
o.bind("CTRL + SPACE", "Direct input for the terminal prefix", function()
if active_window_is_terminal() and has_input_method() and not fcitx5_toggles_on_ctrl_space() then
-- Wait for the local controller reply before dispatching the next key.
o.shell_succeeds("timeout 0.3 fcitx5-remote --check -c")
end
end, { non_consuming = true })
+43
View File
@@ -0,0 +1,43 @@
-- Defaults for Japanese (JIS) keyboards, which only apply when the installed
-- layout is jp. See default/hypr/keyboard.lua.
local keyboard = require("default.hypr.keyboard")
if not keyboard.jis() then
return
end
-- Same down/up split as the universal clipboard shortcuts in clipboard.lua.
local function send_shortcut_once(mods, key)
hl.dispatch(hl.dsp.send_key_state({ mods = mods, key = key, state = "down" }))
hl.timer(function()
hl.dispatch(hl.dsp.send_key_state({ mods = mods, key = key, state = "up" }))
end, { timeout = 50, type = "oneshot" })
end
local function active_window_class()
local window = hl.get_active_window()
return ((window and window.class) or ""):lower()
end
-- On JIS, + is Shift + ;, so apps see Ctrl + Shift + semicolon and never zoom
-- in. Hand them Ctrl + keypad plus instead, which Chromium, Electron apps,
-- VS Code, and Firefox read as zoom in. Obsidian only listens for Ctrl + ^,
-- which JIS types without Shift.
local zoom_in_overrides = {
obsidian = { "CTRL", "asciicircum" },
}
o.bind("CTRL + SHIFT + semicolon", "Zoom in (JIS + key)", function()
local class = active_window_class()
for pattern, chord in pairs(zoom_in_overrides) do
if class:find(pattern, 1, true) then
send_shortcut_once(chord[1], chord[2])
return
end
end
send_shortcut_once("CTRL", "KP_Add")
end)
+2 -21
View File
@@ -1,32 +1,13 @@
-- https://wiki.hypr.land/Configuring/Basics/Variables/#input
local function read_vconsole()
local values = {}
local file = io.open("/etc/vconsole.conf", "r")
if not file then
return values
end
for line in file:lines() do
local key, value = line:match("^%s*([%w_]+)%s*=%s*(.-)%s*$")
if key and value then
value = value:gsub("%s+#.*$", "")
value = value:gsub('^"(.*)"$', "%1")
value = value:gsub("^'(.*)'$", "%1")
values[key] = value
end
end
file:close()
return values
end
local keyboard = require("default.hypr.keyboard")
-- Layouts that can't type Latin letters. Keep in sync with the list in
-- etc/mkinitcpio.conf.d/omarchy_hooks.conf.
local non_latin_layouts =
" af am ara bd bg by et ge gr il in iq ir kg kh kz la lk mk mm mn mv np rs ru sy th tj ua "
local vconsole = read_vconsole()
local vconsole = keyboard.installed()
local kb_layout = vconsole.XKBLAYOUT or "us"
local kb_variant = vconsole.XKBVARIANT or ""
+49
View File
@@ -0,0 +1,49 @@
-- The keyboard layout picked at install time. The installer and first-boot
-- setup persist it to /etc/vconsole.conf, so Hyprland reads it from there
-- instead of rewriting every user's config.
local keyboard = {}
local function read_values(path)
local values = {}
local file = io.open(path, "r")
if not file then
return values
end
for line in file:lines() do
local key, value = line:match("^%s*([%w_]+)%s*=%s*(.-)%s*$")
if key and value then
value = value:gsub("%s+#.*$", "")
value = value:gsub('^"(.*)"$', "%1")
value = value:gsub("^'(.*)'$", "%1")
values[key] = value
end
end
file:close()
return values
end
function keyboard.vconsole()
return read_values("/etc/vconsole.conf")
end
function keyboard.installed()
local values = keyboard.vconsole()
if read_values("/etc/omarchy/input-method").XKB_LAYOUT == "kr"
and (values.XKBLAYOUT or "us") == "us"
and (values.XKBVARIANT or "") == "" then
values.XKBLAYOUT, values.XKBVARIANT = "kr", ""
end
return values
end
-- Japanese (JIS) keyboards put +, ^, and the Henkan/Muhenkan keys where US
-- keyboards don't, so a few defaults only apply when that layout leads.
function keyboard.jis()
local layout = keyboard.installed().XKBLAYOUT or "us"
return layout:match("^[^,]*") == "jp"
end
return keyboard
+2
View File
@@ -8,6 +8,8 @@ require("default.hypr.autostart")
if _G.omarchy_default_bindings ~= false then
require("default.hypr.bindings.media")
require("default.hypr.bindings.clipboard")
require("default.hypr.bindings.input-method")
require("default.hypr.bindings.jis")
require("default.hypr.bindings.tiling")
require("default.hypr.bindings.utilities")
require("default.hypr.bindings.dictation")
+389
View File
@@ -0,0 +1,389 @@
"""One Fcitx setup path for offline finalization and a running desktop."""
import argparse
from contextlib import contextmanager
import json
import os
from pathlib import Path
import re
import subprocess
import tempfile
import sys
import time
ROOT = Path(os.environ["OMARCHY_PATH"])
PRESETS = json.loads((ROOT / "default/input-methods/presets.json").read_text())
NON_LATIN = set("af am ara bd bg by et ge gr il in iq ir kg kh kz la lk mk mm mn mv np rs ru sy th tj ua".split())
CONTROLLER = ["busctl", "--user", "call", "org.fcitx.Fcitx5", "/controller", "org.fcitx.Fcitx.Controller1"]
def run(args, **kwargs):
return subprocess.run(args, check=True, text=True, capture_output=True, **kwargs).stdout.strip()
def read(path):
return path.read_text() if path.exists() else ""
def values(text):
result = {}
for line in text.splitlines():
match = re.match(r"^\s*([\w ]+)\s*=\s*(.*?)\s*(?:\s+#.*)?$", line)
if match:
result[match[1].strip()] = match[2].strip("\"'")
return result
def sections(text):
result = {}
current = ""
for line in text.splitlines():
match = re.match(r"^\s*\[([^]]+)\]\s*$", line)
if match:
current = match[1]
result.setdefault(current, {})
elif current:
result[current].update(values(line))
return result
def atomic_write(path, text):
# Follow a user's config symlink rather than replacing the link itself.
path = path.resolve()
if read(path) == text:
return False
path.parent.mkdir(parents=True, exist_ok=True)
mode = path.stat().st_mode & 0o777 if path.exists() else 0o644
fd, temporary = tempfile.mkstemp(prefix=".omarchy-input-", dir=path.parent)
try:
with os.fdopen(fd, "w") as file:
file.write(text)
os.chmod(temporary, mode)
os.replace(temporary, path)
finally:
if os.path.exists(temporary):
os.unlink(temporary)
return True
def key_list(entries):
# Fcitx canonicalizes modifiers (e.g. Shift+Super instead of Super+Shift).
return {frozenset(value.split("+")) for value in entries.values()}
def defaults(config_home, fresh=False):
changed = False
files = ["config", "conf/quickphrase.conf", "conf/wayland.conf", "conf/xcb.conf", "conf/pinyin.conf"]
for name in files:
target = config_home / "fcitx5" / name
original = read(target)
shipped = read(ROOT / "config/fcitx5" / name)
if name == "config" or name == "conf/quickphrase.conf":
existing = sections(original)
additions = []
for section, entries in sections(shipped).items():
# Missing hotkey sections also represent a choice: Fcitx's defaults.
if not fresh and section.startswith("Hotkey/"):
continue
# Existing lists (including explicitly empty lists) are user choices.
# A fresh profile has no working IME shortcut to preserve.
stock_lists = {
"Hotkey/TriggerKeys": {"0": "Control+space", "1": "Zenkaku_Hankaku", "2": "Hangul"},
"Hotkey/ActivateKeys": {"0": "Hangul_Hanja"},
"Hotkey/DeactivateKeys": {"0": "Hangul_Romaja"},
"Hotkey/AltTriggerKeys": {"0": "Shift_L"},
"Hotkey/EnumerateGroupForwardKeys": {"0": "Super+space"},
"Hotkey/EnumerateGroupBackwardKeys": {"0": "Super+Shift+space"},
"TriggerKey": {"0": "Super+grave", "1": "Super+semicolon"},
}
if fresh and section in existing and section in stock_lists and (key_list(existing[section]) == key_list(stock_lists[section]) or (not existing[section] and section in {"Hotkey/ActivateKeys", "Hotkey/DeactivateKeys"})):
original = re.sub(r"(?ms)^\[" + re.escape(section) + r"\]\s*\n.*?(?=^\[|\Z)", "", original)
existing.pop(section)
if section not in existing:
additions.append(f"[{section}]\n" + "".join(f"{key}={value}\n" for key, value in entries.items()))
if additions:
changed |= atomic_write(target, original.rstrip() + ("\n\n" if original.strip() else "") + "\n".join(additions))
elif name == "conf/pinyin.conf":
# Suppress the first-use cloud prompt while preserving any explicit
# cloud-prediction preference and all other existing engine settings.
if not original:
updated = shipped
elif "FirstRun" in values(original):
updated = re.sub(r"(?m)^\s*FirstRun\s*=.*$", "FirstRun=False", original)
else:
updated = "FirstRun=False\n" + original
changed |= atomic_write(target, updated)
elif not target.exists() and not target.is_symlink():
changed |= atomic_write(target, shipped)
return changed
def activation_default():
data_home = Path(os.environ.get("XDG_DATA_HOME") or str(Path.home() / ".local/share"))
path = data_home / "dbus-1/services/org.fcitx.Fcitx5.service"
original = read(path)
# Repair the package's plain activation entry, but retain custom launchers.
packaged = values(original).get("Exec") in ("/usr/bin/fcitx5", "/usr/bin/fcitx5 -d")
if not original or (packaged and "SystemdService" not in values(original)):
return atomic_write(path, read(ROOT / "default/input-methods/org.fcitx.Fcitx5.service"))
return False
@contextmanager
def transaction(config_home, live=False):
data_home = Path(os.environ.get("XDG_DATA_HOME") or str(Path.home() / ".local/share"))
paths = [config_home / "fcitx5" / name for name in ["profile", "config", "conf/quickphrase.conf", "conf/wayland.conf", "conf/xcb.conf", "conf/pinyin.conf"]]
paths += [config_home / "fontconfig/conf.d/50-omarchy-input-method.conf", data_home / "dbus-1/services/org.fcitx.Fcitx5.service"]
before = {path.resolve(): path.read_text() if path.exists() else None for path in paths}
try:
yield
except BaseException as setup_error:
if live:
# Stop before restoring disk: a graceful stop saves the in-memory profile.
try:
run(["systemctl", "--user", "stop", "omarchy-fcitx5.service"])
subprocess.run(["pkill", "-u", str(os.getuid()), "-x", "fcitx5"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
for attempt in range(50):
if subprocess.run(["pgrep", "-u", str(os.getuid()), "-x", "fcitx5"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode != 0:
break
time.sleep(0.1)
else:
raise RuntimeError("Fcitx did not stop for rollback; previous settings could not safely be restored")
except (OSError, subprocess.CalledProcessError, RuntimeError) as error:
raise RuntimeError(f"Could not stop Fcitx for input rollback: {error} (setup failed: {setup_error})") from error
for path, original in before.items():
if original is None:
path.unlink(missing_ok=True)
else:
atomic_write(path, original)
if live:
# Restore memory as well as disk; otherwise Fcitx saves the failed change
# again on exit. Bus activation caches also need to forget the new entry.
reload_bus()
try:
run(["omarchy-restart-xcompose"])
run(CONTROLLER + ["ReloadConfig"])
except (OSError, subprocess.CalledProcessError, RuntimeError) as error:
print(f"Could not reload restored input settings: {error}", file=sys.stderr)
raise
def selection():
vconsole = values(read(Path(os.environ.get("OMARCHY_VCONSOLE", "/etc/vconsole.conf"))))
preference = values(read(Path(os.environ.get("OMARCHY_INPUT_SELECTION", "/etc/omarchy/input-method"))))
layout = vconsole.get("XKBLAYOUT", "us").split(",")[0] or "us"
variant = vconsole.get("XKBVARIANT", "").split(",")[0]
if preference.get("XKB_LAYOUT") == "kr" and vconsole.get("XKBLAYOUT", "us") == "us" and not variant:
layout, variant = "kr", ""
method = preference.get("INPUT_METHOD", "mozc" if layout == "jp" else "none")
if method not in PRESETS:
raise ValueError(f"Unknown installed input method: {method}")
if layout in NON_LATIN:
layout, variant = "us", ""
group_layout = layout + (f"-{variant}" if variant else "")
if not re.fullmatch(r"[A-Za-z0-9_-]+", group_layout):
raise ValueError("Invalid installed keyboard layout")
return method, group_layout
def stock_profile(text):
if not text.strip():
return True
groups = sections(text)
items = [data.get("Name", "") for key, data in groups.items() if re.fullmatch(r"Groups/\d+/Items/\d+", key)]
group = groups.get("Groups/0", {})
return (items == ["keyboard-us"]
and list(key for key in groups if re.fullmatch(r"Groups/\d+", key)) == ["Groups/0"]
and group.get("Name", "Default") == "Default"
and group.get("Default Layout", "us") == "us"
and not groups.get("Groups/0/Items/0", {}).get("Layout"))
def profile_text(layout, method):
default_im = method if method != "none" else f"keyboard-{layout}"
text = f"[Groups/0]\nName=Default\nDefault Layout={layout}\nDefaultIM={default_im}\n\n[Groups/0/Items/0]\nName=keyboard-{layout}\nLayout=\n"
if method != "none":
text += f"\n[Groups/0/Items/1]\nName={method}\nLayout=\n"
return text + "\n[GroupOrder]\n0=Default\n"
def font_default(config_home, method):
variant = PRESETS[method]["font"]
path = config_home / "fontconfig/conf.d/50-omarchy-input-method.conf"
if not variant or path.exists() or path.is_symlink():
return
# Only CJK fallback changes; retain the interface and terminal's Latin font.
rules = []
for families, face in [("sans-serif|Liberation Sans", "Sans"), ("serif|Liberation Serif", "Serif"), ("monospace|JetBrainsMono Nerd Font", "Sans Mono")]:
for family in families.split("|"):
rules.append(f'<alias><family>{family}</family><accept><family>Noto {face} CJK {variant}</family></accept></alias>')
atomic_write(path, '<?xml version="1.0"?>\n<!DOCTYPE fontconfig SYSTEM "urn:fontconfig:fonts.dtd">\n<fontconfig>\n' + "\n".join(rules) + "\n</fontconfig>\n")
def live_group():
group = run(["fcitx5-remote", "-q"])
info = json.loads(run(CONTROLLER[:1] + ["--json=short"] + CONTROLLER[1:] + ["InputMethodGroupInfo", "s", group]))["data"]
layout, items = info[:2]
return group, layout, items
def live_set(group, layout, items):
args = [group, layout, str(len(items))]
for name, item_layout in items:
args.extend([name, item_layout])
run(CONTROLLER + ["SetInputMethodGroupInfo", "ssa(ss)"] + args)
run(CONTROLLER + ["Save"])
def reload_bus():
# Offline finalization and updates over SSH may have no session bus.
try:
run(["busctl", "--user", "call", "org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "ReloadConfig"])
except (OSError, subprocess.CalledProcessError, RuntimeError) as error:
print(f"Input activation reload deferred until login: {error}", file=sys.stderr)
def available_methods():
return json.loads(run(CONTROLLER[:1] + ["--json=short"] + CONTROLLER[1:] + ["AvailableInputMethods"]))["data"][0]
def wait_ready(method):
for attempt in range(100):
try:
available = available_methods()
if any(item[0] == method for item in available) and live_group()[2]:
return
except (OSError, ValueError, KeyError, IndexError, subprocess.CalledProcessError):
pass
time.sleep(0.1)
raise RuntimeError(f"Fcitx did not load the {method} engine after restart")
def live_append(method):
group, layout, items = live_group()
if any(item[0] == method for item in items):
return
try:
live_set(group, layout, items + [[method, ""]])
if not any(item[0] == method for item in live_group()[2]):
raise RuntimeError(f"Fcitx did not retain the {method} engine")
except BaseException:
live_set(group, layout, items)
raise
def select_live_method(method, profile):
# Clients reconnect asynchronously after restart. SetCurrentIM silently does
# nothing without a focused input context, so verify the saved group default.
for attempt in range(30):
run(["fcitx5-remote", "-s", method])
run(["fcitx5-remote", "-c"])
run(CONTROLLER + ["Save"])
group = run(["fcitx5-remote", "-q"])
for section, entries in sections(read(profile)).items():
if re.fullmatch(r"Groups/\d+", section) and entries.get("Name") == group and entries.get("DefaultIM") == method:
return
time.sleep(0.1)
print(f"Input method added; select {method} in Fcitx settings to switch to it.", file=sys.stderr)
def configure(args):
config_home = Path(os.environ.get("XDG_CONFIG_HOME") or str(Path.home() / ".config"))
running = subprocess.run(["pgrep", "-u", str(os.getuid()), "-x", "fcitx5"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0
# Finalization can be retried after login. A live daemon owns its profile.
if args.seed and running:
return
with transaction(config_home, live=running and not args.defaults):
configure_inner(args, config_home, running)
def configure_inner(args, config_home, running):
if args.seed or args.defaults:
method, layout = selection()
else:
method, layout = args.method or selection()[0], None
if args.method:
method = args.method
profile = config_home / "fcitx5/profile"
original = read(profile)
if args.seed and not stock_profile(original):
return
if not args.seed and not args.defaults and not running:
raise RuntimeError("Log in to an Omarchy desktop before changing input methods")
if not args.seed and not args.defaults and method == "none":
raise ValueError("Choose an input engine to add")
if args.seed:
atomic_write(profile, profile_text(layout, method))
defaults(config_home, fresh=True)
activation_default()
font_default(config_home, method)
elif args.defaults:
fresh = stock_profile(original)
changed = defaults(config_home, fresh=fresh)
if activation_default():
reload_bus()
# Upgrades can run through sudo/SSH without access to the live user's bus.
# Never overwrite its profile or restart input in that context.
if fresh:
if running:
try:
group, old_layout, items = live_group()
if old_layout == "us" and items == [["keyboard-us", ""]]:
new_items = [[f"keyboard-{layout}", ""]]
if method != "none":
if any(item[0] == method for item in available_methods()):
new_items.append([method, ""])
else:
print(f"Finish input setup after login: omarchy setup input {method}", file=sys.stderr)
live_set(group, layout, new_items)
if live_group()[1:] != (layout, new_items):
live_set(group, old_layout, items)
raise RuntimeError("Fcitx did not retain the installed keyboard settings")
except (OSError, ValueError, KeyError, IndexError, RuntimeError, subprocess.CalledProcessError) as error:
print(f"Input profile alignment deferred until login: {error}", file=sys.stderr)
else:
atomic_write(profile, profile_text(layout, method))
if running and changed:
try:
run(CONTROLLER + ["ReloadConfig"])
except (OSError, subprocess.CalledProcessError) as error:
print(f"Input defaults reload deferred until login: {error}", file=sys.stderr)
else:
# New addons are discovered only at startup. Supervision remains with the
# existing unit; never spawn a second fcitx instance ourselves.
activation_default()
defaults(config_home, fresh=stock_profile(original))
reload_bus()
run(["omarchy-restart-xcompose"])
wait_ready(method)
live_append(method)
# The selected language becomes the next active engine; leave Latin input
# active until the user deliberately switches. Keep all other group items.
select_live_method(method, profile)
font_default(config_home, method)
def main():
parser = argparse.ArgumentParser(description=__doc__)
commands = parser.add_subparsers(dest="command", required=True)
commands.add_parser("status")
configure_parser = commands.add_parser("configure")
mode = configure_parser.add_mutually_exclusive_group()
mode.add_argument("--seed", action="store_true")
mode.add_argument("--defaults", action="store_true")
configure_parser.add_argument("method", nargs="?", choices=PRESETS)
args = parser.parse_args()
if args.command == "status":
method, layout = selection()
print(json.dumps({"installed_method": method, "layout": layout}))
else:
configure(args)
if __name__ == "__main__":
try:
main()
except (OSError, ValueError, RuntimeError, subprocess.CalledProcessError) as error:
raise SystemExit(f"Input method setup failed: {error}")
+139
View File
@@ -0,0 +1,139 @@
"""Live Fcitx state for the existing keyboard layout widget."""
import json
import os
import sys
from gi.repository import Gio, GLib
BUS = "org.fcitx.Fcitx5"
INTERFACE = "org.fcitx.Fcitx.Controller1"
def call(bus, method, args=None):
# Reading the bar must neither activate a stopped service nor wait indefinitely.
return bus.call_sync(BUS, "/controller", INTERFACE, method, args, None,
Gio.DBusCallFlags.NO_AUTO_START, 1000, None).unpack()
def snapshot(bus):
group = call(bus, "CurrentInputMethodGroup")[0]
methods = list(dict.fromkeys(item[0] for item in
call(bus, "InputMethodGroupInfo", GLib.Variant("(s)", (group,)))[1]))
info = call(bus, "CurrentInputMethodInfo")
return {"methods": methods, "current": info[0], "name": info[1], "label": info[4], "language": info[5]}
def cycle(bus):
state = snapshot(bus)
methods = state["methods"]
if len(methods) > 1 and state["current"] in methods:
following = methods[(methods.index(state["current"]) + 1) % len(methods)]
call(bus, "SetCurrentIM", GLib.Variant("(s)", (following,)))
class Indicator:
def __init__(self, bus):
self.bus = bus
self.pending = ""
self.last = ""
self.entries = {}
def select_next(self):
state = snapshot(self.bus)
methods = state["methods"]
if len(methods) < 2:
return
current = self.pending or state["current"] or self.last or methods[0]
if current not in methods:
current = methods[0]
self.pending = methods[(methods.index(current) + 1) % len(methods)]
self.refresh()
def refresh(self):
state = snapshot(self.bus)
methods = state["methods"]
if self.pending not in methods:
self.pending = ""
if self.pending and state["current"]:
# Before the first application gains focus Fcitx has no input context.
# Retain the click until it has one, without changing startup defaults.
call(self.bus, "SetCurrentIM", GLib.Variant("(s)", (self.pending,)))
state = snapshot(self.bus)
if state["current"] == self.pending:
self.pending = ""
if state["current"]:
self.last = state["current"]
else:
chosen = self.pending or (self.last if self.last in methods else "") or next(iter(methods), "")
if chosen not in self.entries:
self.entries = {entry[0]: entry for entry in call(self.bus, "AvailableInputMethods")[0]}
entry = self.entries.get(chosen)
if entry:
state.update(current=entry[0], name=entry[1], label=entry[4], language=entry[5])
return state
def watch(bus):
previous = None
indicator = Indicator(bus)
loop = GLib.MainLoop()
buffer = ""
timer = 0
def refresh():
nonlocal previous
try:
state = indicator.refresh()
except GLib.Error:
indicator.pending = ""
indicator.last = ""
state = {"methods": [], "current": "", "name": "", "language": ""}
line = json.dumps(state, ensure_ascii=False)
if line != previous:
print(line, flush=True)
previous = line
def poll():
nonlocal timer
refresh()
timer = GLib.timeout_add(50 if indicator.pending else 500, poll)
return GLib.SOURCE_REMOVE
def command(source, condition):
nonlocal buffer, timer
data = os.read(sys.stdin.fileno(), 4096)
if not data:
loop.quit()
return GLib.SOURCE_REMOVE
buffer += data.decode()
while "\n" in buffer:
line, buffer = buffer.split("\n", 1)
if line == "cycle":
try:
indicator.select_next()
except GLib.Error:
indicator.pending = ""
refresh()
GLib.source_remove(timer)
timer = GLib.timeout_add(50 if indicator.pending else 500, poll)
return GLib.SOURCE_CONTINUE
# Fcitx's controller has no current-method-changed signal. Keep one bus
# connection open instead of spawning a command on every poll. Polling also
# follows per-application input contexts and recovers after service restarts.
GLib.io_add_watch(sys.stdin, GLib.IO_IN | GLib.IO_HUP, command)
poll()
loop.run()
if __name__ == "__main__":
try:
bus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
if sys.argv[1:] == ["cycle"]:
cycle(bus)
else:
watch(bus)
except GLib.Error as error:
raise SystemExit(str(error))
@@ -0,0 +1,5 @@
# Installed per-user so D-Bus activation uses the supervised Omarchy unit.
[D-BUS Service]
Name=org.fcitx.Fcitx5
Exec=/usr/bin/fcitx5
SystemdService=omarchy-fcitx5.service
+7
View File
@@ -0,0 +1,7 @@
{
"none": {"label": "Keyboard only", "packages": [], "font": ""},
"mozc": {"label": "Japanese (Mozc)", "packages": ["fcitx5-mozc"], "font": "JP"},
"hangul": {"label": "Korean (Hangul)", "packages": ["fcitx5-hangul"], "font": "KR"},
"pinyin": {"label": "Chinese (Simplified, Pinyin)", "packages": ["fcitx5-chinese-addons"], "font": "SC"},
"chewing": {"label": "Chinese (Traditional, Zhuyin)", "packages": ["fcitx5-chewing"], "font": "TC"}
}
+17
View File
@@ -128,6 +128,7 @@
"setup.monitors": {"icon":"󰍹","label":"Monitors","action":"omarchy-launch-config-editor \"$HOME/.config/hypr/monitors.lua\""},
"setup.keybindings": {"icon":"","label":"Keybindings","when":"[[ -f ~/.config/hypr/bindings.lua ]]","action":"omarchy-launch-config-editor \"$HOME/.config/hypr/bindings.lua\""},
"setup.input": {"icon":"","label":"Input","when":"[[ -f ~/.config/hypr/input.lua ]]","action":"omarchy-launch-config-editor \"$HOME/.config/hypr/input.lua\""},
"setup.input-method": {"icon":"","label":"Input Language","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-setup-input'"},
"setup.network": {"icon":"󰛳","label":"Network","aliases":["network"]},
"setup.network.dns": {"icon":"󰇖","label":"DNS","aliases":["dns"]},
"setup.network.dns.dhcp": {"icon":"󰩟","label":"DHCP","checked":"[[ \"$(omarchy-dns)\" == \"DHCP\" ]]","action":"omarchy-dns DHCP"},
@@ -176,6 +177,12 @@
"setup.default.editor.helix": {"icon":"","label":"Helix","checked":"[[ \"$(omarchy-default-editor)\" == \"helix\" ]]","action":"omarchy-default-editor helix"},
"setup.default.editor.vim": {"icon":"","label":"Vim","checked":"[[ \"$(omarchy-default-editor)\" == \"vim\" ]]","action":"omarchy-default-editor vim"},
"setup.default.editor.emacs": {"icon":" ","label":"Emacs","checked":"[[ \"$(omarchy-default-editor)\" == \"emacs\" ]]","action":"omarchy-default-editor emacs"},
"setup.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.enable": {"icon":"󰄬","label":"Enable Plugin","action":"omarchy-menu-plugin enable"},
"setup.plugin.disable": {"icon":"󰅖","label":"Disable Plugin","action":"omarchy-menu-plugin disable"},
@@ -219,6 +226,16 @@
"install.tui": {"icon":"","label":"TUI","action":"omarchy-launch-floating-terminal-with-presentation omarchy-tui-install"},
"install.windows": {"icon":"󰍲","label":"Windows","disabled":"[[ -f $HOME/.local/share/applications/windows-vm.desktop ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-windows-vm install'"},
"install.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.browser.chrome": {"icon":"","label":"Chrome","disabled":"omarchy-pkg-present google-chrome","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-browser chrome'"},
"install.browser.edge": {"icon":"󰇩","label":"Edge","disabled":"omarchy-pkg-present microsoft-edge-stable-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-browser edge'"},
+182
View File
@@ -0,0 +1,182 @@
// 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.toggle.top-bar": {"label":"菜单栏"},
"trigger.toggle.battery-percentage": {"label":"电量百分比"},
"trigger.toggle.workspace-layout": {"label":"工作区布局"},
"trigger.toggle.window-gaps": {"label":"窗口间距"},
"trigger.toggle.one-window-ratio": {"label":"单窗口比例"},
"trigger.tests.network-speedtest": {"label":"网络测速"},
"trigger.tests.disk-speedtest": {"label":"磁盘测速"},
"style.theme": {"label":"主题"},
"style.background": {"label":"壁纸"},
"style.unlock": {"label":"解锁画面"},
"style.font": {"label":"字体"},
"style.bar": {"label":"菜单栏"},
"style.bar.position": {"label":"位置"},
"style.bar.transparency": {"label":"透明"},
"style.screensaver": {"label":"屏保"},
"style.about": {"label":"关于"},
"style.bar.position.top": {"label":"顶部"},
"style.bar.position.bottom": {"label":"底部"},
"style.bar.position.left": {"label":"左侧"},
"style.bar.position.right": {"label":"右侧"},
"style.about.text": {"label":"编辑文字"},
"style.about.image": {"label":"取自图片"},
"style.about.default": {"label":"恢复默认"},
"style.screensaver.text": {"label":"编辑文字"},
"style.screensaver.image": {"label":"取自图片"},
"style.screensaver.default": {"label":"恢复默认"},
"setup.monitors": {"label":"显示器"},
"setup.keybindings": {"label":"快捷键"},
"setup.input": {"label":"输入设备"},
"setup.input-method": {"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":"用户"},
"trigger.toggle.animations": {"label":"动画"},
"trigger.toggle.theme-sync": {"label":"Herdr 主题同步"},
"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":"石墨文档"},
"update.channel.stable": {"label":"稳定版"},
"update.channel.rc": {"label":"候选版"},
"update.channel.edge": {"label":"尝鲜版"},
"update.channel.dev": {"label":"开发版"},
}
+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
@@ -0,0 +1 @@
Server = https://rc-mirror.omarchy.cn/$repo/os/$arch
@@ -0,0 +1 @@
Server = https://stable-mirror.omarchy.cn/$repo/os/$arch
@@ -0,0 +1,2 @@
[archlinuxcn]
Server = https://mirrors.ustc.edu.cn/archlinuxcn/$arch
+9
View File
@@ -0,0 +1,9 @@
# Installs in these timezones get this region. The installer's timezone list
# includes tzdata's backward-compatible names, so they are listed too.
Asia/Shanghai
Asia/Urumqi
Asia/Chongqing
Asia/Chungking
Asia/Harbin
Asia/Kashgar
PRC
+3 -2
View File
@@ -1,5 +1,5 @@
[Unit]
Description=Fcitx5 input method (XCompose sequences)
Description=Fcitx5 input methods and XCompose sequences
# fcitx5 turns the CapsLock compose sequences in ~/.XCompose into text for
# Wayland clients.
#
@@ -19,7 +19,8 @@ PartOf=graphical-session.target
ConditionEnvironment=WAYLAND_DISPLAY
[Service]
Type=simple
Type=dbus
BusName=org.fcitx.Fcitx5
# notificationitem duplicates the tray entry omarchy already renders itself.
ExecStart=/usr/bin/fcitx5 --disable notificationitem
# always, not on-failure: fcitx5 exits 0 when it detects another instance owning
+21
View File
@@ -0,0 +1,21 @@
# Input methods
The install keyboard picker selects a console keymap plus an optional Fcitx engine. `install/provisioning/setup-form.sh` is shared by the ISO configurator and deferred first-boot owner setup. Its rows are `label|console keymap|input method|desktop XKB override`; the last two fields are optional, so ordinary keyboard choices keep their existing meaning.
The ISO carries the extra fields in `omarchy_install.input_method` and `omarchy_install.input_xkb_layout`. Both installation paths persist them in `/etc/omarchy/input-method` as `INPUT_METHOD` and `XKB_LAYOUT`. This file is parsed as data, never sourced. Korean has a real US console keymap and a `kr` desktop override; Japanese uses `jp106`, or US when the US-keyboard choice is selected. Console and disk-unlock input remain ordinary keymaps.
`install/user/input-method.sh` calls `omarchy-input-method configure --seed` during user finalization, before graphical login. The standard engines are in the base package set so deferred provisioning and factory resets do not need network access to configure typing. `default/input-methods/presets.json` is the engine catalog used by the post-install selector.
`default/input-methods/configure.py` owns the shared configuration behavior. Offline seeding replaces only a missing profile or Fcitx's untouched single `keyboard-us` guess. The group layout matches Hyprland's effective leading layout and variant; non-Latin layouts retain the US-leading behavior used for desktop bindings. The selected composition engine is the group's `DefaultIM`, with `ActiveByDefault=False` so initial typing remains Latin. Existing multilingual profiles and explicitly chosen keyboard layouts are preserved.
`omarchy setup input` installs the selected engine, restarts the supervised Fcitx unit so it discovers new addons, and appends the engine through `InputMethodGroupInfo`, `SetInputMethodGroupInfo`, and `Save`. It keeps the group name, layout, existing methods, and their layout overrides. There is no parallel unsupervised daemon. A per-user D-Bus activation file delegates `org.fcitx.Fcitx5` to `omarchy-fcitx5.service`; it is loaded before live restart.
Shortcut defaults reserve Ctrl+Space for Tmux/Herdr, Super+Space for the menu, Super+Shift+Space for the bar, and Super+grave for the console. Input toggles on Ctrl+Shift+Space, Zenkaku/Hankaku, or Hangul; Henkan activates and Muhenkan deactivates. An existing configured list, including an empty list, is preserved. Stock generated lists are repaired only while the user still has the single-keyboard default profile. A terminal Ctrl+Space returns to direct input before the following key, with a 300 ms timeout and no D-Bus activation; a keyboard-only profile skips this step. Wayland and XCB addons do not override the compositor's XKB settings.
The migration installs the standard engines and applies conservative defaults. It aligns an untouched Fcitx keyboard profile with the install layout, using the live controller when Fcitx is running. It leaves existing multilingual profiles and working trigger keys alone. Atomic file writes follow configuration symlinks, preserve file modes, and roll back on a failed seed or live setup so a retry can complete. Live rollback stops Fcitx before restoring disk, then reloads activation and restarts the unit. Migration bus errors defer profile alignment to the next login and do not restart input. If a pre-update daemon has not discovered a new engine, the migration aligns only its keyboard and prints the setup command.
Input selection does not change the system locale, repository region, desktop language, or Latin interface font. A new language setup seeds a CJK fallback preference once; additional languages do not overwrite it.
Focused checks live in `test/shell.d/input-method-test.sh`, `setup-form-test.sh`, `hyprland-keyboard-layout-test.sh`, and `hyprland-jis-bindings-test.sh`. `test/acceptance.d/input-methods-test.sh` exercises real composition in a disposable installed VM and restores the original user configuration. The ISO repository's keyboard tests cover transport and target persistence. Browser input must be verified against the shipped browser versions; the language selector is not a place to guess flags from older guides.
Default key values are checked against [Fcitx global configuration](https://github.com/fcitx/fcitx5/blob/master/src/lib/fcitx/globalconfig.cpp).
Loaded 100 of 207 files, more files were not shown because too many files have changed in this diff. Show more