Compare commits

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

No files matched your search

+7
View File
@@ -130,6 +130,13 @@ Exceptions are allowed for migration and package-helper scripts where the helper
- Chinese menu labels live in `default/omarchy/omarchy-menu.zh-cn.jsonc`, merged when `LANG` starts with `zh_CN`; give a new menu row with translatable words a label there.
- Shell text goes through `I18n.tr("English %1", arg)` from `qs.Commons`, with its Chinese in `shell/Commons/I18nZh.js` keyed by the English. Notifications and OSD messages are worded by their sender with `omarchy-text "English %1" "$arg"` (Python: `text()`, QML: `I18n.tr`), which reads the same table; the shell shows them as given, so user text is never translated. `test/shell.d/shell-i18n-test.sh` fails on a missing entry.
# Region
- `/etc/omarchy/region` holds the region the installer derived from the timezone (`cn`); no file means Global. The profile lives in `default/regions/cn/`, its contract in `docs/regions.md`.
- `install/config/region.sh` links the `cn` developer mirrors into `/etc` and `/.cargo` and refuses to replace a regular file it did not create. It serves both the install (the marker) and `omarchy region cn|global` (`OMARCHY_REGION`), which also records or removes the marker; Global removes only the links it made.
- `omarchy-region` prints the region (`Global` or `China`) and switches it. Nothing in `omarchy update` or migrations may change the region.
- China-only menu rows carry `"when":"[[ \"$(omarchy-region)\" == \"China\" ]]"` on every row, not only the submenu, because search checks each row's own guard. User-facing docs: `manual/52-china.md`.
# Config Structure
- `config/` - 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
+1
View File
@@ -75,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"
+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
+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.
+20 -2
View File
@@ -42,11 +42,29 @@ if [[ ! -f $templates/pacman-$channel.conf || ! -f $templates/mirrorlist-$channe
exit 1
fi
conf=$templates/pacman-$channel.conf mirrorlist=$templates/mirrorlist-$channel
# Regions render x86 pacman only (omarchy #13690).
if [[ $platform == x86 ]]; then
# Render the channel templates plus this machine's regional additions as the
# caller, so root only copies finished files. The scratch directory goes on
# every exit, including a failed render, a refused sudo prompt, or a signal;
# revoking the sudo timestamp stays the last thing the exit path does.
rendered=$(mktemp -d)
cleanup_refresh() {
local status=$?
rm -rf "$rendered"
omarchy_security_exit_with_revoked_sudo "$status"
}
trap cleanup_refresh EXIT
"$OMARCHY_PATH/bin/omarchy-apply-pacman" --render "$channel" "$rendered"
conf=$rendered/pacman.conf mirrorlist=$rendered/mirrorlist
fi
sudo cp -f /etc/pacman.conf /etc/pacman.conf.bak
sudo cp -f /etc/pacman.d/mirrorlist /etc/pacman.d/mirrorlist.bak
echo "Setting channel to $channel"
sudo cp -f "$templates/pacman-$channel.conf" /etc/pacman.conf
sudo cp -f "$templates/mirrorlist-$channel" /etc/pacman.d/mirrorlist
sudo cp -f "$conf" /etc/pacman.conf
sudo cp -f "$mirrorlist" /etc/pacman.d/mirrorlist
# Allow user customization of /etc/pacman.conf before the upgrade runs, so
# custom repositories and IgnorePkg entries shape the downgrade-capable
+63
View File
@@ -0,0 +1,63 @@
#!/bin/bash
# omarchy:summary=Show or switch the region this machine is set up for
# omarchy:args=[global|cn]
# omarchy:examples=omarchy region | omarchy region cn
# omarchy:requires-sudo=true
set -euo pipefail
region_file=/etc/omarchy/region
if (($# == 0)); then
if [[ ! -e $region_file ]]; then
echo "Global"
elif [[ $(<"$region_file") == "cn" ]]; then
echo "China"
else
echo "Unknown region in $region_file: $(<"$region_file")" >&2
exit 1
fi
exit 0
fi
if [[ $1 != "global" && $1 != "cn" ]]; then
echo "Usage: omarchy-region [global|cn]" >&2
exit 1
fi
channel=$(omarchy-version-channel)
if [[ $channel != "stable" && $channel != "rc" && $channel != "edge" ]]; then
echo "Cannot tell this machine's package channel ($channel); run omarchy channel set first" >&2
exit 1
fi
# The cn profile renders x86 pacman only (omarchy #13690).
if [[ $1 == "cn" && $(omarchy-hw-platform) != "x86" ]]; then
echo "The China region needs an x86 machine; $(omarchy-hw-platform) has no China profile yet" >&2
exit 1
fi
if [[ $1 == "cn" ]]; then
# Upgrade first: the keyring install below syncs new databases.
omarchy-update-pacman -Syu --noconfirm
sudo env OMARCHY_REGION=cn OMARCHY_PATH="$OMARCHY_PATH" bash -e "$OMARCHY_PATH/install/config/region.sh"
# Trust ArchLinuxCN before the refresh upgrades through it.
rendered=$(mktemp -d)
trap 'rm -rf "$rendered"' EXIT
omarchy-apply-pacman --render "$channel" "$rendered"
mapfile -t profile_packages < <(grep -Ev '^[[:space:]]*(#|$)' "$OMARCHY_PATH/default/regions/cn/packages")
sudo pacman --config "$rendered/pacman.conf" -Sy --noconfirm --needed "${profile_packages[@]}"
sudo pacman-key --populate archlinuxcn
omarchy-refresh-pacman "$channel"
else
sudo env OMARCHY_REGION=global OMARCHY_PATH="$OMARCHY_PATH" bash -e "$OMARCHY_PATH/install/config/region.sh"
omarchy-refresh-pacman "$channel"
fi
# The developer mirrors reach only a new session, so offer one.
if [[ -t 0 && -t 1 ]] && gum confirm --affirmative "$(omarchy-text "Log out now")" --negative "$(omarchy-text "Later")" "$(omarchy-text "Log out now to switch the developer mirrors?")"; then
omarchy-system-logout
else
omarchy-text "Log back in to switch the developer mirrors."
fi
@@ -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>
+13
View File
@@ -215,6 +215,9 @@
"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"},
@@ -258,6 +261,16 @@
"install.tui": {"icon":"","label":"TUI","action":"omarchy-launch-floating-terminal-with-presentation omarchy-tui-install"},
"install.windows": {"icon":"󰍲","label":"Windows","when":"omarchy-hw-x86","disabled":"[[ -f $HOME/.local/share/applications/windows-vm.desktop ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-windows-vm install'"},
"install.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","when":"omarchy-hw-x86","disabled":"omarchy-pkg-present microsoft-edge-stable-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-browser edge'"},
+11
View File
@@ -189,4 +189,15 @@
"setup.typing": {"label":"输入"},
"setup.typing.keyboard": {"label":"键盘布局"},
"setup.typing.input": {"label":"输入法"},
"setup.region": {"label":"地区"},
"setup.region.global": {"label":"全球"},
"setup.region.china": {"label":"中国"},
"install.china": {"label":"国内应用"},
"install.china.wechat": {"label":"微信"},
"install.china.feishu": {"label":"飞书"},
"install.china.dingtalk": {"label":"钉钉"},
"install.china.wemeet": {"label":"腾讯会议"},
"install.china.tencent-docs": {"label":"腾讯文档"},
"install.china.yuque": {"label":"语雀"},
"install.china.shimo": {"label":"石墨文档"},
}
+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
+49
View File
@@ -0,0 +1,49 @@
# Regional defaults
Regions customize bootstrap defaults independently of update channels and of the installer's language. Country identifiers are lowercase, officially assigned ISO 3166-1 alpha-2 codes. `global` is a reserved value for no country-specific additions. Only implemented profiles are accepted; currently `global` and `cn`.
## Runtime configuration
The ISO installer writes the region it derived to `/etc/omarchy/region` before target finalization. It is plain text, not executable configuration, owned by root and world-readable (`0644`), because refresh renders as the calling user. A missing file means `global`; an unsupported value is an error.
`omarchy-apply-pacman <stable|rc|edge> [target-root]` restores the selected channel's two pacman templates, appends the region's `pacman.conf.append` to `pacman.conf`, and puts the region's `mirrorlist-<channel>.prepend` ahead of the channel's mirrorlist. Both files are generated before either live file is replaced. Existing files are backed up with `.bak`. Dev uses the edge pacman channel, as before.
Both installation finalization and `omarchy-refresh-pacman` use this command. Explicit refreshes and channel changes restore regional defaults; ordinary updates do not enforce them. The existing `pre-refresh-pacman` user hook runs after regeneration and before the update, so users can maintain overrides there. Switching to the dev channel refuses a checkout that lacks this machine's region profile, because that checkout's own refresh would otherwise drop the regional repositories. Users can edit the files directly or set the region to `global` before a refresh to stop applying regional repository defaults. This does not uninstall packages or rewrite anything in existing home directories.
Refresh renders both files as the calling user and gives root only the finished copies, so the sudo boundary stays command-scoped. That boundary does not defend against other code already running as the same user: such code could change the rendered files while refresh waits for the password, as it could alter that user's own environment in other ways.
The China profile puts Omarchy's China replica of the channel's Arch mirror first (`stable-mirror.omarchy.cn`, `rc-mirror.omarchy.cn`, or `mirror.omarchy.cn` for edge and dev) and keeps the channel's `.org` mirror as the fallback. pacman tries servers in order and moves to the next one when a download fails, including a 404. After three failed downloads from one server, it skips that server for the rest of the transaction. It also takes the package databases from the first server that answers, without comparing them, so the China replicas must carry complete, current metadata. The profile also adds the ArchLinuxCN community repository from USTC's mirror, run by the University of Science and Technology of China. It does not change the `[omarchy]` package repository. ArchLinuxCN is rolling and built against current Arch, while stable and RC are snapshots, so its packages can need newer libraries than a stable machine has.
## Profile layout
Each profile lives in `default/regions/<code>/`:
- `timezones`: the timezones that select this region, one per line, with blank lines and `#` comments allowed. List tzdata's backward-compatible names too, since the installer's timezone list includes them. A timezone may belong to at most one profile.
- `packages`: additional target packages, in the same format. Every ISO carries every profile's packages in its offline repository; the installer installs them only on targets in that region. A package named `<name>-keyring` is the profile's trust anchor: the builder installs it through Arch's existing trust chain, and a regional target populates the `<name>` keyring.
- `pacman/`: `pacman.conf.append`, added after the channel's `pacman.conf`, and one `mirrorlist-<channel>.prepend` per channel (`stable`, `rc`, `edge`), placed ahead of the channel's mirrorlist so the regional mirror is tried first.
## ISO installer contract
There is one ISO for every region. The companion `omarchy-iso` repository reads the profiles from the runtime it bundles, so a region and its repository defaults can never disagree.
- **Build:** every profile's `packages` go into the offline repository, but not into the base package list. The builder adds each profile's community repository to its writable online config to download them; it does not change its Arch channel mirrors.
- **Install:** the installer maps the timezone the user picked to a region through the `timezones` files; a timezone no profile lists means `global`. It shows the result on the summary screen. Unattended installs can set the region explicitly instead. For a regional install it installs the profile's packages from the offline repository and writes `/etc/omarchy/region`; the runtime finalizer then applies the fragments.
- **Deferred installs** have no timezone until first boot, so they are `global` unless the unattended configuration sets a region.
For China, the builder installs `archlinuxcn-keyring` using the existing Arch trust chain before downloading ArchLinuxCN packages. Signature or keyring failures abort the build; no unverified keyserver trust or online signature bypass is added. On a China target the installer installs the keyring from the offline repository, then initializes and populates it before the online repositories are enabled. Global targets never populate ArchLinuxCN's keys. The ISO's existing offline repository verification policy is unchanged.
## Scope of the China profile
The China profile provides repository defaults and developer mirrors. Language is a separate installer choice, not part of a region: a Chinese speaker outside China keeps global mirrors, and a China install in English still gets the regional ones.
## Developer mirrors
The `cn` profile also points the common package managers at mirrors inside China: `environment.conf` (npm, PyPI and uv, Go, rustup, Bundler, and Maven through `MAVEN_ARGS`), `cargo.toml` for crates.io, `gemrc` for RubyGems, and `maven-settings.xml`, which Maven reads in place. During installation `install/config/region.sh` links the first three to `/etc/environment.d/90-omarchy-china.conf`, `/.cargo/config.toml` and `/etc/gemrc` when the marker says `cn`. It refuses to replace a regular file it did not create. The environment reaches sessions started after login.
`omarchy region cn|global` (_Setup > Region_) switches an installed machine. `cn` upgrades, installs and trusts the profile's keyring through a rendered China config, records the marker and links the developer mirrors through the same script, then refreshes pacman. `global` removes the marker and only the links that point into the profile, then refreshes. The region also shows _Install > China Apps_.
## Remaining release validation
Unit tests cover region/channel independence, the timezone mapping, generated pacman contents, repeat application, and refresh ordering; refresh itself is exercised only inside the sudo-boundary sandbox. They do not establish live mirror availability, real package trust, or successful offline installation. Before distribution, build from matching local checkouts on an x86_64 builder and run the disposable-VM acceptance workflow in `agents/skills/acceptance-tests.md`.
The acceptance run must install without network access in both directions: a non-China timezone must produce pacman files byte for byte identical to the channel templates, and `Asia/Shanghai` must produce the China profile, boot to a new user, and keep the regional mirrors, repository, marker, and keyring trust through `stable`, `rc`, and `edge` refreshes.
+1
View File
@@ -1,3 +1,4 @@
run_logged "$OMARCHY_INSTALL/config/region.sh"
run_logged "$OMARCHY_INSTALL/config/theme-system.sh"
run_logged "$OMARCHY_INSTALL/config/browser-policy.sh"
run_logged "$OMARCHY_INSTALL/config/increase-lockout-limit.sh"
+44
View File
@@ -0,0 +1,44 @@
cn_links=(
"environment.conf /etc/environment.d/90-omarchy-china.conf"
"cargo.toml /.cargo/config.toml"
"gemrc /etc/gemrc"
)
# The installer writes the marker from the timezone; omarchy-region passes the region it switches to.
region=global
if [[ -e /etc/omarchy/region ]]; then
region=$(</etc/omarchy/region)
fi
region=${OMARCHY_REGION:-$region}
case $region in
global)
rm -f /etc/omarchy/region
for link in "${cn_links[@]}"; do
read -r _ target <<<"$link"
if [[ -L $target && $(readlink "$target") == */default/regions/cn/* ]]; then
rm "$target"
fi
done
;;
cn)
for link in "${cn_links[@]}"; do
read -r _ target <<<"$link"
if [[ -e $target && ! -L $target ]]; then
echo "Refusing to replace $target, which Omarchy did not create" >&2
exit 1
fi
done
mkdir -p /etc/omarchy
echo cn >/etc/omarchy/region
for link in "${cn_links[@]}"; do
read -r source target <<<"$link"
mkdir -p "${target%/*}"
ln -sfn "$OMARCHY_PATH/default/regions/cn/$source" "$target"
done
;;
*)
echo "Unknown region: $region" >&2
exit 1
;;
esac
+7 -2
View File
@@ -21,8 +21,13 @@ fi
# rc on aarch64) gets the platform's default channel instead.
channel=${OMARCHY_MIRROR:-}
[[ -n $channel && -f $templates/pacman-$channel.conf ]] || channel=$(omarchy_pacman_default_channel "$platform")
cp -f "$templates/pacman-$channel.conf" /etc/pacman.conf
cp -f "$templates/mirrorlist-$channel" /etc/pacman.d/mirrorlist
# Regions render x86 pacman only (omarchy #13690).
if [[ $platform == x86 && -e /etc/omarchy/region ]]; then
"$OMARCHY_PATH/bin/omarchy-apply-pacman" "$channel"
else
cp -f "$templates/pacman-$channel.conf" /etc/pacman.conf
cp -f "$templates/mirrorlist-$channel" /etc/pacman.d/mirrorlist
fi
# An image build leaves the keyring to each machine's first boot, so no two
# share a master key (install/helpers/image-target.sh).
+4
View File
@@ -20,6 +20,10 @@ Now you're ready to Omarchy!
The full-disk encryption won't allow you to enter the password from a Bluetooth keyboard at startup. Just like you can't use a Bluetooth keyboard to enter the BIOS on a PC. You'll need a keyboard that either uses a 2.4ghz dongle or a cable (which is much nicer for latency anyway!). I personally love the [Lofree Flow84](https://www.lofree.co/products/lofree-flow-the-smoothest-mechanical-keyboard)!
### Installing in China
The installer's first screen asks for your language; pick _Chinese_ for a Chinese installer and system. The timezone you pick later decides the region: `Asia/Shanghai` brings Chinese package and developer mirrors and the China apps. See [China](52-china.md).
### Installing for another owner
If you're setting up a machine for someone else — a family member, a new employee, a buyer — you shouldn't be answering the personal questions on their behalf. Hit `Ctrl + C` on the very first screen of the installer (the language and keyboard selection), and Omarchy will offer to prepare the machine for another owner instead. The system installs right away, but all the personal setup — keyboard layout, username, password — is deferred until the machine boots for the first time. The drive is still encrypted by default, and the password the new owner picks on that first boot becomes the encryption password too. (A machine you've already been using can be handed over without a reinstall too — see [resetting the computer](48-security.md).)
+43
View File
@@ -0,0 +1,43 @@
# China
Omarchy works in mainland China without a VPN. Two installer answers set a machine up for China, and each stands on its own:
- **Language.** The installer's first screen asks for English or Simplified Chinese. Chinese puts the installer, the system language (`LANG=zh_CN.UTF-8`), the menus and the desktop shell in Chinese.
- **Region.** The timezone you pick decides the region. `Asia/Shanghai`, `Asia/Urumqi` and their older names select China; any other timezone is Global. The summary screen shows _Region: cn_ before anything touches the disk.
A Chinese speaker abroad keeps the global mirrors, and an English install in Shanghai still gets the China mirrors.
## What the China region sets up
| Area | China | Global |
|------|-------|--------|
| Arch packages | The channel's `.omarchy.cn` mirror first, its `.omarchy.org` mirror behind it, and the ArchLinuxCN repository with its keyring | `.omarchy.org` mirrors |
| Omarchy packages | `pkgs.omarchy.org` | `pkgs.omarchy.org` |
| Developer mirrors | npm, PyPI and uv, Go, rustup and crates.io, RubyGems and Bundler, Maven | Upstream registries |
| Software | _Install > China Apps_ | Hidden |
pacman tries the `.cn` mirror first. If it is unreachable or lacks a package, pacman falls through to `.org`, which works for anyone with their own VPN. The installer carries the China packages on the ISO, so a China install needs no network.
## Chinese input
After choosing Chinese, the installer's keyboard screen opens on _Chinese (Simplified, Pinyin)_; keep it and Fcitx5 Pinyin is ready at first login. Press `Super + I` to switch to Pinyin, and tap `Shift` to step between Chinese and Latin letters. Add or change an input method later under _Setup > Typing > Input Methods_. See [Keyboard, Mouse, Trackpad](34-keyboard-mouse-trackpad.md).
Chinese text renders in the right Noto CJK variant everywhere: Simplified for Chinese, Traditional for Taiwan and Hong Kong, Japanese and Korean for those languages.
## Developer mirrors
China machines point the common package managers at mirrors inside China. Environment variables in `/etc/environment.d/90-omarchy-china.conf` cover npm, PyPI and uv, Go, rustup, Bundler and Maven; they reach everything started from the desktop after your next login (not SSH or TTY shells), and they take precedence over npm, pip, uv and Go config files, so unset the variable in a shell to opt out. `/.cargo/config.toml` points crates.io at the mirror unless a project's `.cargo/config.toml` says otherwise, and `/etc/gemrc` sets the RubyGems source unless your `~/.gemrc` sets another. Omarchy never replaces one of those two files if it is a regular file you created. Toolchain downloads by mise, such as Node.js or Go themselves, still come from upstream.
## China Apps
_Install > China Apps_ installs WeChat, QQ, Feishu, DingTalk, Tencent Meeting and WPS Office from the AUR, and sets up Tencent Docs, Yuque and Shimo as web apps.
## Switching later
_Setup > Region_, or `omarchy region cn` and `omarchy region global`, switches the region. China runs a full system update, trusts the ArchLinuxCN keyring, records the region, moves pacman to the `.cn` mirrors and links the developer mirrors. Global moves pacman back to `.org`, removes the developer mirrors and hides _Install > China Apps_; apps you installed from it stay. The developer mirrors change at the next login, so the switch offers to log out.
_Setup > Language_, or `omarchy language zh` and `omarchy language en`, switches the system language. The menus and the desktop shell follow at the next login, so it offers to log out too.
Neither switch touches the other, the input methods or the CJK fonts. `omarchy update` never changes the region or the language.
A machine prepared for another owner keeps the language picked in the installer but installs Global, since its timezone is only set on first boot. Switch it under _Setup > Region_ once it is online.
+141
View File
@@ -0,0 +1,141 @@
# OmarchyCN task list
Progress source of truth for the `omarchycn` branches of `omarchy`, `omarchy-iso` and `omarchy-pkgs`. Work top to bottom, one item at a time. Each item closes only after a codex review (GPT 6.1 Sol, xhigh, in a herdr split) passes; at most 5 reviews per item.
## Decisions (confirmed with the owner, 2026-10-05)
- Region is machine state in `/etc/omarchy/region` (`china`); absent means Global.
- Arch and Omarchy repos follow the domain plan: every `*.omarchy.org` server gets a `*.omarchy.cn` twin listed first. `pkgs.omarchy.cn` is live; `stable-mirror`, `rc-mirror` and `mirror.omarchy.cn` do not resolve yet, so pacman falls through to `.org`.
- China means a Chinese UI: `LANG=zh_CN.UTF-8` and Chinese menu labels. Global means `en_US.UTF-8`. `omarchy update` never touches the locale.
- Input method: rime-ice-installer (omarchy-pkgs PR 265, not merged yet). The ISO bundles the package, the Rime Ice `full.zip` and the Wanxiang grammar model; the install finishes with Rime and US keyboard ready, Ctrl+Space toggles. The installer gets a local patch: `--asset-dir` and no sudo when its packages are present.
- China to Global: switch sources and language, keep CJK fonts and the input method, remove the Claude Code third-party provider entry and the env it wrote, hide the China software entry. Dim and Kimi stay for every region.
- Deferred provisioning and factory reset get the region's system setup but no offline input method (deferred).
## Decisions (owner and maintainers, 2026-10-08)
These replace the 2026-10-05 decisions where they disagree.
- Region and language are separate. Region comes from the timezone the user picks (omarchy #13690, omarchy-iso #241); the installer adds no region screen. Language is the first installer screen.
- The `.cn` pacman mirrors are #13690's `mirrorlist-<channel>.prepend` files. Our `servers.sed` twins and the USTC Arch fallback go; `[omarchy]` keeps only `pkgs.omarchy.org`, as in #13690.
- Input methods come from dhh's #14433 and omarchy-iso #237. Rime, `rime-ice-installer` and its bundled assets go.
- The language picker offers English and Simplified Chinese. Chinese reruns the installer in cage + foot with fonts unpacked from the offline repository's `noto-fonts-cjk`; the target gets `LANG=zh_CN.UTF-8`.
- Setup > Region switches mirrors, developer mirrors, the China apps entry and the Claude Code API entry. Setup > Language switches `LANG` and the Chinese UI. Neither touches the other.
## Decisions (owner, 2026-10-11, after the maintainers tested the ISO)
- Drop _Setup > Claude Code API_ (`omarchy-setup-claude-api`) and the Dim/Kimi agent roster and migration. `omarchy region global` no longer touches Claude settings.
- Drop the ISO's local-build pin (`pin_local_packages`, `IgnorePkg` and its hooks).
- Drop the `omarchy-pkg-add` change; it served Rime.
- PRs carry only our changes and never overlap Yunjie Dai's (#13690, iso #241) or David's (#14433, iso #237). Region work stacks on #13690; ISO work stacks on iso #241.
## Shipping prerequisites
- The region profile and timezone mapping wait on omarchy #13690 and omarchy-iso #241; input methods (omarchy #14433, omarchy-iso #237) are merged. The language screen, CJK console, Chinese UI, developer mirrors and China apps exist only on the `omarchycn` branches: build a test ISO with `./bin/omarchy-iso-make --local-source ../omarchy ../omarchy-pkgs` from `omarchy-iso/`.
- A local-source ISO builds `omarchy-dev` and `omarchy-settings-dev` under the published version number and does not pin them, so the next published release replaces them on `omarchy update`. Local test ISOs are for testing only.
- `stable-mirror.omarchy.cn`, `rc-mirror.omarchy.cn` and `mirror.omarchy.cn` resolve and serve `core.db` (checked 2026-10-09).
## PR branches (T32, 2026-10-12)
omarchy, each a fork branch:
- `cjk-variant-selection` on upstream `quattro`: the Noto CJK variant fontconfig.
- `chinese-language` on upstream `quattro`: `omarchy language`, Setup > Language, the installer's language prompt, the Chinese menu overlay, shell and notification text.
- `cn-developer-mirrors` on #13690: the `cn` developer mirrors at install.
- `cn-region-switch` on `cn-developer-mirrors`: `omarchy region`, Setup > Region, China Apps, `manual/52-china.md`; English only.
omarchy-iso: `chinese-installer` on iso #241 (the language screen, the CJK console, the translated installer, the target locale, the summary's keyboard label); `omarchycn` merges it onto iso `quattro`. omarchy-pkgs: `omarchycn` is upstream `master`.
omarchy `omarchycn` merges them onto `quattro` and adds what no PR can carry yet: the Chinese wording for Region and China Apps and the language half of the China manual (needs both language and region), and, until #13690 rebases onto upstream's ARM templates, an integration-only pacman resolution and a guard that refuses `omarchy region cn` off x86. Known failure there: `pacman-templates-test` expects x86 refresh to copy templates directly, which #13690 replaces with a render.
## Tasks
- [x] T0 Branches and this list
- [x] T1 China package sources (PRD 1): region state, `omarchy-region` reader, `default/region/china/pacman.sed`, applied by `omarchy-refresh-pacman` and `install/post-install/pacman.sh`; tests
- [x] T2 Region choice in the installer (PRD 2): `omarchy_prompt_region` in `setup-form.sh`, ISO configurator screen, orchestrator passes `OMARCHY_REGION`, `install/config/region.sh` records it
- [x] T3 Input method (PRD 3): rime-ice-installer patch and PKGBUILD in omarchy-pkgs, IME packages in `omarchy-other.packages`, install-time package add, user step runs the installer from bundled assets, ISO build bundles assets and the package
- [x] T4 CJK fontconfig fix (PRD 4): rebuild PR 9693 on the current base
- [x] T5 AI (PRD 5): Dim and Kimi Code for every region (rebuild PR 9694), Claude Code third-party provider setup shown only in China
- [x] T6 Developer mirrors (PRD 6): npm, PyPI/uv, Go, rustup, crates.io, RubyGems, Maven via system drop-ins under `default/region/china/`
- [x] T7 China software (PRD 7): Install menu entry shown only in China
- [x] T8 Region switching and Chinese UI (PRD 8): `omarchy-region china|global`, Setup > Region, locale, Chinese menu overlay chosen by `LANG`
- [x] T9 Logo (PRD 9): stock Omarchy logo everywhere
- [x] T10 Docs: `AGENTS.md`, manual
- [x] T11 Test ISO build (local source)
- [x] T12 QEMU install and verification: China and Global installs, region switch, `omarchy update` keeps region and language
- [x] T13 Acceptance checklist
- [x] T14 Chinese installer (owner request, parity with the old OmarchyCN ISO): choosing China on the VT reruns the installer in cage + foot with CJK fonts and a US keyboard, falling back to the English VT; there the region screen is bilingual and Global returns to the VT; every later installer screen, the shared setup form and the install dashboard read in Chinese
- [x] T15 Global to China fails on local-source installs (owner report): `rime-ice-installer` lives only in the ISO's offline mirror, so a Global install cannot fetch it later; local-source installs install every pinned local build, and `install/config/region.sh` installs the China packages before it writes any region state
- [x] T16 A region switch offers to log out at once, since the language and the developer mirrors only reach a new session (owner request)
- [x] T17 The desktop shell reads in Chinese under `zh_CN` (owner report): notifications, the bar panels (agents, network, display and the rest), the menu's search prompt — review cap reached: five codex rounds; the round-5 findings (user text translated at display, custom reminder list lines, reminder tooltips and errors) are fixed by wording every notification at its sender and verified by tests and in the China VM, without a sixth review
- [x] T18 Merge the official region-profile PRs (omarchy #13690, #14399; omarchy-iso #206, #236) and align to them: region code `cn` in `/etc/omarchy/region`, China files under `default/regions/cn/`, pacman rendered by `omarchy-apply-pacman` (`.cn` twins first, USTC fallback, ArchLinuxCN with its keyring), offline timezone `Asia/Shanghai`; the ISO keeps the first-screen region choice and bundles the China profile, `--region cn` preselects China
- [x] T19 Merge the reworked #13690 (timezone-derived region) and align: `mirrorlist-<channel>.prepend` and `timezones` replace `servers.sed`, `mirrorlist.append` and `timezone`; drop #14399's timezone preselection (closed upstream); `install/config/region.sh` reads `/etc/omarchy/region` instead of `OMARCHY_REGION`; drop the dead `china` rename migration
- [x] T20 Merge #14433 (input methods) and remove Rime: packages, `install/user/region.sh`, the Rime steps in `install/config/region.sh` and `omarchy-region`
- [x] T21 Split language from region: `omarchy-language en|zh` and Setup > Language own `LANG`; `omarchy-region` and `install/config/region.sh` stop touching the locale
- [x] T22 omarchy-pkgs: revert the `rime-ice-installer` package
- [x] T23 omarchy-iso: merge upstream `quattro` (#237) and #241; remove `--region`, `prepare-region.sh`, the region skel, the region preselected timezone, Rime bundling and its pin
- [x] T24 omarchy-iso: the language screen replaces the region screen; Chinese reruns in cage + foot with `noto-fonts-cjk` unpacked from the offline repository (no font packages added to the ISO); the CJK console lists only US-keymap keyboards and preselects Chinese (Simplified, Pinyin); the target gets `zh_CN.UTF-8` through `locale_config`; the harness takes `--language zh`
- [x] T25 Docs: `AGENTS.md`, `docs/regions.md`, `manual/52-china.md`, omarchy-iso README
- [x] T26 Build the local-source ISO and test in QEMU: Chinese + `Asia/Shanghai`, English + `America/New_York`, runtime region and language switches
- [x] T27 Acceptance checklist for T19–T26
- [x] T28 Drop the Claude Code API setup, the Dim/Kimi roster and migration, and the `omarchy-pkg-add` change (omarchy)
- [x] T29 Drop `pin_local_packages` and the pin list (omarchy-iso)
- [x] T30 The CJK console skips the second language screen; the summary's keyboard row shows the chosen layout, not its keymap (omarchy-iso)
- [x] T31 The CJK console fills the screen and the summary keeps the logo in view (omarchy-iso)
- [x] T32 Rebuild omarchy as PR branches on current upstream: CJK fontconfig, language and menus, shell text, cn developer mirrors (on #13690), Setup > Region and China Apps; `omarchycn` becomes their merge
- [x] T33 Rebuild omarchy-iso as one PR branch on iso #241 and current upstream; `omarchycn` becomes it; omarchy-pkgs follows upstream master
- [x] T34 Build the local-source ISO and test in QEMU: Chinese offline, English
- [x] T35 Acceptance checklist for T28–T34
- [x] T36 Open the PRs one at a time from Raiden-MeixElysia
## Open PRs (T36, 2026-10-12, from Raiden-MeixElysia)
- omacom/omarchy#15116 `cjk-variant-selection`: Noto CJK variant fontconfig. Mergeable.
- omacom/omarchy#15117 `chinese-language`: Simplified Chinese as a system language. Mergeable; rebased onto #15091, which removed USB and Thunderbolt approval and with it their Chinese text.
- omacom/omarchy-iso#263 `chinese-installer`: language first, CJK console, Chinese installer. Stacked on iso #241, upstream merged in; needs #15117.
- omacom/omarchy#15118 `cn-developer-mirrors`: stacked on #13690; shows #13690's conflicts until it rebases.
- omacom/omarchy#15119 `cn-region-switch`: stacked on #15118.
- Not yet opened: the Chinese wording for Region and China Apps (commit in `omarchycn`), once #15117 and #15119 land.
## Acceptance checklist for T28–T34 (maintainers' ISO test, 2026-10-11)
| # | Request | Result | Evidence |
|---|---|---|---|
| 1 | Drop `pin_local_packages` and its hooks | Pass | omarchy-iso 2a589e9; installed `pacman.conf` has no `IgnorePkg`, `/etc/pacman.d/hooks` holds only upstream hooks (30-state). |
| 2 | Drop `omarchy-setup-claude-api` and the Dim/Kimi roster and migration | Pass | omarchy f2af41bb; nothing of them in any PR branch; `omarchy region global` touches no Claude settings. |
| 3 | No second language screen in the CJK console | Pass | omarchy-iso 2c523af; cage + foot opens on the keyboard screen (04-keyboard). |
| 4 | No black strip; logo whole on the summary; keyboard row shows Pinyin | Pass | omarchy-iso 411acd6 (`[csd] preferred=none`, `line-height=22px`) and 2c523af (`keyboard_label`); 04-keyboard, 14-summary. |
| 5 | PRs carry only our changes, stacked on #13690; ISO bundles David's #14433 | Pass | T32/T33 branches; #14433 and iso #237 come from upstream `quattro`; region branches stack on #13690, the ISO branch on iso #241. |
| - | Rebuilt ISO, Chinese offline and English | Pass | T34 evidence below. |
| - | Automated tests | No new failures | Per-branch shell tests pass on their bases; `fontconfig-cjk-test` needs the installed font set and passes in full on the T34 VM (48-fontconfig-test); the integration suite fails only where upstream fails, plus `pacman-templates-test` until #13690 rebases; ISO unit tests fail only `test_keyboard` (no `localectl`), as upstream. |
Open: `pacman-templates-test` and the ARM guard wait on #13690's rebase; the Chinese wording for Region and China Apps waits on the language and region PRs; the upstream menu acceptance test still walks Style > Menu Bar.
## T34 evidence (2026-10-12)
ISO `release/omarchy-2026.10.12-x86_64-local.iso` from omarchy `omarchycn`, omarchy-iso `omarchycn` (c407101), omarchy-pkgs upstream master (d47199f2). Screenshots in `~/omarchy-vm/shots/`.
- Chinese, offline (`OFFLINE=on`): the VT language screen, then cage + foot opens straight on the keyboard screen with Pinyin preselected and no strip (04-keyboard); the summary keeps the whole logo and shows `键盘 Chinese (Simplified, Pinyin)` (14-summary); installed in 4m 0s (18-progress). First boot: Chinese desktop and notifications (21-desktop); `LANG=zh_CN.UTF-8`, region `cn`, `mirror.omarchy.cn` first, `[archlinuxcn]`, no `IgnorePkg` or pin hooks, developer mirror links and `NPM_CONFIG_REGISTRY`/`GOPROXY` in the session, Fcitx5 profile with Pinyin (30-state); `Super + I`, `nihao`, Space typed 你好 (31-pinyin); Setup shows 语言 and 地区 (34-setup); `fc-match` gives SC for untagged Han under zh_CN and TC, HK, JP, KR for tagged text, Liberation Sans for tagged Latin (36-fonts).
- Runtime, online: `omarchy region global` → Global, no marker, `.org` mirror, no ArchLinuxCN, links gone, `LANG` kept, Chinese logout prompt (42, 43); `omarchy language en` → `en_US.UTF-8`, English logout prompt (44); `omarchy region cn` → China, `.cn` first, ArchLinuxCN, links back, `LANG` kept (45, 46); `omarchy language zh` → `zh_CN.UTF-8` (47). The `.cn` edge mirror lagged `.org` by one sqlite release, which the cn refresh downgraded.
- English, harness: install and first boot pass; four acceptance failures, all upstream: Mozc Qt, weather OCR, `omarchy-bore` vs `linux-omarchy`, and the menu test still walking Style > Menu Bar, which upstream moved to Trigger > Appearance.
## Acceptance checklist
Evidence comes from the T26 runs of `omarchy-iso/release/omarchy-2026.10.09-x86_64-local.iso` (omarchy fcd681c0, omarchy-iso 30f63b6, omarchy-pkgs a4cc3234): run A in host QEMU with the guest cut off from the network during install, run B in the ISO test harness.
| # | Requirement (discussion of 2026-10-08) | Result | Evidence |
|---|---|---|---|
| 1 | Language picker on the first screen; Chinese gets cage + foot | Pass | Run A: the VT screen offers English and Chinese; Chinese reruns in cage + foot on a bilingual language screen, and every later screen and the install dashboard read in Chinese. |
| 2 | CJK font from the offline repository, not added to the ISO | Pass | `unpack_cjk_font` extracts `NotoSansCJK-Regular.ttc` from the offline mirror's `noto-fonts-cjk`; the ISO package list drops `ttf-jetbrains-mono` and `wqy-microhei` and keeps only `cage` and `foot`. |
| 3 | Translated installer strings and the target locale | Pass | Run A: summary, finish screen and dashboard in Chinese; installed `LANG=zh_CN.UTF-8` through `locale_config`. Run B: English throughout, `LANG=en_US.UTF-8`. |
| 4 | Region from the timezone, no extra step (#13690, #241) | Pass | Run A: `Asia/Shanghai` shows 区域 cn on the summary and writes `/etc/omarchy/region` = `cn`. Run B: `America/New_York` installs Global with no marker. |
| 5 | Offline install ready without network | Pass | Run A installed in 8m13s with the guest network cut off; ArchLinuxCN keyring trusted, `mirror.omarchy.cn` first, Pinyin ready at first login. |
| 6 | `.cn` pacman mirrors from #13690; ours dropped | Pass | China: `mirror.omarchy.cn` before `mirror.omarchy.org`, `[archlinuxcn]` from USTC. Global: mirrorlist equals the channel template; `pacman.conf` differs only by the local-build `IgnorePkg` pin. |
| 7 | Developer mirrors on the cn profile | Pass | China links `/etc/environment.d/90-omarchy-china.conf`, `/.cargo/config.toml`, `/etc/gemrc`; Global removes them and `NPM_CONFIG_REGISTRY` is gone after relogin. |
| 8 | Input methods from dhh's #14433; Rime removed | Pass | Chinese preselects Chinese (Simplified, Pinyin); `INPUT_METHOD=pinyin`; `Ctrl + Shift + Space` then `nihao` typed 你好. No Rime package, asset or pin remains. |
| 9 | Region and language switch separately at runtime | Pass | `omarchy region global` and `cn` leave `LANG` alone; `omarchy language en` and `zh` leave the region alone; each offers logout in the right language; Claude Code API and China Apps follow the region. |
| 10 | `omarchy update` keeps region and language | Pass | After `omarchy update -y`: `LANG=zh_CN.UTF-8`, region `cn`, `mirror.omarchy.cn` first, `[archlinuxcn]` present. |
| - | Automated tests | Pass, no new regressions | omarchy shell suite in an Arch container: every failure is environmental and predates these tasks; some sit in branch-only tests (`fontconfig-cjk-test` needs the Omarchy font set, `hyprland-jis-bindings-test` needs `lua`, `menu-plugin-test` and `tailscale-receive-test` chmod a symlink on the read-only mount). omarchy-iso `./test/all` passes apart from `test_keyboard` (needs systemd's `localectl`). Harness acceptance: four failures outside this branch (Mozc Qt; weather and reminder prompts on screen but missed by OCR; `linux-omarchy` vs `omarchy-bore` kernel). |
Open items outside this branch: the shipping prerequisites above; the four acceptance failures listed; `.automated_script.sh` exits 1 when its dashboard returns without rebooting after the mirror warm-up has finished, from upstream's `EXIT` trap under `set -e`; each `omarchy region` switch asks for the sudo password several times through refresh's command-scoped `sudo` calls.
+2
View File
@@ -634,6 +634,8 @@ var table = {
"Later": "稍后",
"Log out now to switch the language?": "立即注销以切换语言?",
"Log back in to switch the language.": "重新登录后切换语言。",
"Log out now to switch the developer mirrors?": "立即注销以切换开发工具镜像?",
"Log back in to switch the developer mirrors.": "重新登录后切换开发工具镜像。",
"Superwhisper Dictation Ready": "Superwhisper 语音输入就绪",
"Hold Right Alt or F9 to dictate (or toggle with Super + Ctrl + X).": "按住右 Alt 或 F9 说话(或用 Super + Ctrl + X 切换)。",
"Background Intros": "背景开场动画",
+2 -1
View File
@@ -422,7 +422,8 @@ var GUARD_READERS = [
"omarchy-hyprland-corners",
"omarchy-hyprland-window-transparency",
"omarchy-hyprland-workspace-layout-default",
"omarchy-language"
"omarchy-language",
"omarchy-region"
]
// Package and command presence account for most of what the guards ask, and
@@ -6,6 +6,7 @@ source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
source "$SHELL_TEST_DIR/fixtures/sudo-boundary-test.sh"
copy_boundary_file bin/omarchy-channel-set
copy_boundary_file bin/omarchy-refresh-pacman
copy_boundary_file bin/omarchy-apply-pacman
copy_boundary_file bin/omarchy-update
export OMARCHY_UPDATE_LOGGED=1
+53
View File
@@ -113,6 +113,7 @@ run_channel() {
: >"$log_file"
OMARCHY_CHANNEL_TEST_LOG="$log_file" \
OMARCHY_PATH="${OMARCHY_TEST_PATH:-$package_root}" \
OMARCHY_REGION_FILE="${OMARCHY_TEST_REGION_FILE:-$test_tmp/no-region}" \
HOME="$test_tmp/home" \
PATH="$stub_bin:$ROOT/bin:$PATH" \
"${OMARCHY_TEST_PATH:-$package_root}/bin/omarchy-channel-set" "$@"
@@ -190,6 +191,58 @@ fi
assert_log_line $'link\t'"$checkout"$'\t--no-reboot' "switching back to dev links ~/omarchy"
pass "switching back to dev reuses the existing ~/omarchy checkout"
printf 'cn\n' >"$test_tmp/region"
if OMARCHY_TEST_REGION_FILE="$test_tmp/region" run_channel dev >"$test_tmp/region.out" 2>"$test_tmp/region.err"; then
fail "dev refuses a checkout without this machine's region"
fi
grep -q "does not support the 'cn' region" "$test_tmp/region.err" || fail "dev explains the missing region" "$(cat "$test_tmp/region.err")"
if grep -Eq $'^(link|refresh)\t' "$log_file"; then
fail "dev refuses before linking or refreshing pacman" "$(cat "$log_file")"
fi
pass "dev refuses a checkout without the region before touching pacman"
# A directory with the region's name, or a helper that cannot run, is not
# support: the refresh after linking would fail with dev already linked.
mkdir -p "$checkout/default/regions/cn"
touch "$checkout/bin/omarchy-apply-pacman"
chmod +x "$checkout/bin/omarchy-apply-pacman"
if OMARCHY_TEST_REGION_FILE="$test_tmp/region" run_channel dev >"$test_tmp/region.out" 2>"$test_tmp/region.err"; then
fail "dev refuses an empty region profile"
fi
if grep -Eq $'^(link|refresh)\t' "$log_file"; then
fail "dev refuses an empty region profile before linking or refreshing pacman" "$(cat "$log_file")"
fi
mkdir -p "$checkout/default/regions/cn/pacman"
cn_fragments=(pacman.conf.append mirrorlist-stable.prepend mirrorlist-rc.prepend mirrorlist-edge.prepend)
for missing in "${cn_fragments[@]}"; do
for fragment in "${cn_fragments[@]}"; do
if [[ $fragment == "$missing" ]]; then
rm -f "$checkout/default/regions/cn/pacman/$fragment"
else
touch "$checkout/default/regions/cn/pacman/$fragment"
fi
done
if OMARCHY_TEST_REGION_FILE="$test_tmp/region" run_channel dev >"$test_tmp/region.out" 2>"$test_tmp/region.err"; then
fail "dev refuses a region profile without $missing"
fi
if grep -Eq $'^(link|refresh)\t' "$log_file"; then
fail "dev refuses a profile without $missing before linking or refreshing pacman" "$(cat "$log_file")"
fi
done
touch "${cn_fragments[@]/#/$checkout/default/regions/cn/pacman/}"
chmod -x "$checkout/bin/omarchy-apply-pacman"
if OMARCHY_TEST_REGION_FILE="$test_tmp/region" run_channel dev >"$test_tmp/region.out" 2>"$test_tmp/region.err"; then
fail "dev refuses a checkout whose pacman helper cannot run"
fi
if grep -Eq $'^(link|refresh)\t' "$log_file"; then
fail "dev refuses incomplete region support before linking or refreshing pacman" "$(cat "$log_file")"
fi
pass "dev refuses a region profile missing any fragment or a helper that cannot run"
chmod +x "$checkout/bin/omarchy-apply-pacman"
OMARCHY_TEST_REGION_FILE="$test_tmp/region" run_channel dev
assert_log_line $'refresh\tedge' "dev proceeds with a checkout that supports the region"
current_channel() {
OMARCHY_TEST_VERSION_CHANNEL="$1" \
OMARCHY_TEST_PACKAGES="$2" \
+75
View File
@@ -0,0 +1,75 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
require_command node
node - "$ROOT" <<'JS'
const fs = require('fs')
const root = process.argv[2]
const model = require(`${root}/shell/plugins/menu/MenuModel.js`)
const items = model.parseMenuJsonc(fs.readFileSync(`${root}/default/omarchy/omarchy-menu.jsonc`, 'utf8'))
const china = items.filter(item => item.id === 'install.china' || item.id.startsWith('install.china.'))
const guard = '[[ "$(omarchy-region)" == "China" ]]'
const fail = message => { console.error(`not ok - ${message}`); process.exit(1) }
const aur = {
wechat: 'wechat-universal-bwrap', qq: 'linuxqq', feishu: 'feishu-bin',
dingtalk: 'dingtalk-bin', wemeet: 'wemeet-bin', wps: 'wps-office-cn',
}
const web = {
'tencent-docs': ['腾讯文档', 'https://docs.qq.com'],
yuque: ['语雀', 'https://www.yuque.com/dashboard'],
shimo: ['石墨文档', 'https://shimo.im/desktop'],
}
if (china.length !== 1 + Object.keys(aur).length + Object.keys(web).length) fail(`China Apps has exactly the expected entries: ${china.map(i => i.id)}`)
if (!china.every(item => item.when === guard)) fail('every China Apps entry, including search hits, shows only in China')
for (const [id, pkg] of Object.entries(aur)) {
const item = china.find(i => i.id === `install.china.${id}`)
if (!item || item.action !== `omarchy-launch-floating-terminal-with-presentation 'omarchy-pkg-aur-add ${pkg}'` || item.disabled !== `omarchy-pkg-present ${pkg}`)
fail(`${id} installs and checks the AUR package ${pkg}`)
}
for (const [id, [name, url]] of Object.entries(web)) {
const item = china.find(i => i.id === `install.china.${id}`)
if (!item || item.action !== `omarchy-launch-floating-terminal-with-presentation 'omarchy-webapp-install ${name} ${url} ""'` || item.disabled !== `[[ -f "$HOME/.local/share/applications/${name}.desktop" ]]`)
fail(`${id} installs and checks the ${name} web app`)
}
console.log('ok - China Apps lists the AUR packages and web apps, shown only in China')
JS
aur_names="wechat-universal-bwrap linuxqq feishu-bin dingtalk-bin wemeet-bin wps-office-cn"
if [[ ${OMARCHY_TEST_NETWORK:-} == "1" ]]; then
query=$(printf '&arg[]=%s' $aur_names)
found=$(curl -fsSL "https://aur.archlinux.org/rpc/v5/info?${query#&}" | jq -r '.results[].Name' | sort | tr '\n' ' ')
[[ $found == "$(printf '%s\n' $aur_names | sort | tr '\n' ' ')" ]] || fail "every China Apps package exists in the AUR" "$found"
pass "every China Apps package exists in the AUR"
else
skip "AUR existence check needs OMARCHY_TEST_NETWORK=1"
fi
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
mkdir -p "$test_tmp/bin" "$test_tmp/home"
cat >"$test_tmp/bin/curl" <<'SH'
#!/bin/bash
while (($#)); do [[ $1 == "-o" ]] && out=$2; shift; done
[[ -n ${out:-} ]] && printf '\x89PNG\r\n\x1a\n\0\0\0\rIHDR\0\0\0\x01\0\0\0\x01\x08\x06\0\0\0\x1f\x15\xc4\x89' >"$out"
exit 0
SH
printf '#!/bin/bash\necho "the web app wizard opened" >&2\nexit 1\n' >"$test_tmp/bin/gum"
chmod +x "$test_tmp/bin/curl" "$test_tmp/bin/gum"
grep -o "omarchy-launch-floating-terminal-with-presentation 'omarchy-webapp-install [^']*'" "$ROOT/default/omarchy/omarchy-menu.jsonc" |
sed -e "s/^[^']*'//" -e "s/'$//" -e 's/\\"/"/g' >"$test_tmp/actions"
(($(wc -l <"$test_tmp/actions") == 3)) || fail "the menu has three China web app actions"
while read -r action; do
HOME="$test_tmp/home" PATH="$test_tmp/bin:$ROOT/bin:$PATH" bash -c "$action" >/dev/null ||
fail "the menu action installs its web app: $action"
done <"$test_tmp/actions"
for app in 腾讯文档 语雀 石墨文档; do
[[ -f $test_tmp/home/.local/share/applications/$app.desktop ]] || fail "$app gets a launcher"
done
pass "each China web app action creates its launcher without the wizard"
+9 -1
View File
@@ -9,7 +9,11 @@ export SUDO_TEST_LOG="$boundary_tmp/events"
export SUDO_TEST_CACHE="$boundary_tmp/cache"
export OMARCHY_PATH="$SUDO_TEST_ROOT"
export SUDO_TEST_HOME="$boundary_tmp/home"
mkdir -p "$SUDO_TEST_HOME"
# The machine the render reads its region marker from. It starts global; a test
# writes etc/omarchy/region here to model a regional install, never the host's.
export SUDO_TEST_TARGET="$boundary_tmp/target"
export OMARCHY_REGION_FILE="$SUDO_TEST_TARGET/etc/omarchy/region"
mkdir -p "$SUDO_TEST_HOME" "$SUDO_TEST_TARGET/etc"
mkdir -p "$SUDO_TEST_ROOT/bin" "$SUDO_TEST_ROOT/mock" "$SUDO_TEST_ROOT/default/omarchy/sudo-no-update"
: >"$SUDO_TEST_LOG"
@@ -24,6 +28,10 @@ s=(source/name).read_text().replace('$HOME', '$SUDO_TEST_HOME')
for command in ['sudo','pkexec','pacman','omarchy-pkg-missing','systemd-inhibit','setpriv','snapper']:
s=s.replace('/usr/bin/'+command, str(target/'mock'/command))
s=s.replace('PATH=/usr/bin:/usr/sbin:/bin:/sbin', 'PATH="'+str(target/'bin')+':/usr/bin:/usr/sbin:/bin:/sbin"')
if name.name == 'omarchy-apply-pacman':
render_etc='etc="${4:-/}/etc"'
assert s.count(render_etc)==1, 'render target lookup moved'
s=s.replace(render_etc, 'etc="${4:-$SUDO_TEST_TARGET}/etc"')
p.write_text(s)
p.chmod((source/name).stat().st_mode & 0o777)
PY
+133
View File
@@ -0,0 +1,133 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
if ! command -v fc-match >/dev/null || [[ $(fc-list : file) != *NotoSansCJK-Regular.ttc* ]]; then
skip "fontconfig CJK variants need fc-match and noto-fonts-cjk"
exit 0
fi
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
candidate="$ROOT/default/fontconfig/conf.avail/50-omarchy.conf"
{
echo '<?xml version="1.0"?><!DOCTYPE fontconfig SYSTEM "fonts.dtd"><fontconfig>'
echo "<dir>/usr/share/fonts</dir><cachedir>$test_tmp/cache</cachedir>"
placed=false
for conf in /etc/fonts/conf.d/*.conf; do
name=${conf##*/}
[[ $name == "50-omarchy.conf" ]] && continue
if ! $placed && [[ $name > "50-omarchy.conf" ]]; then
echo "<include>$candidate</include>"
placed=true
fi
echo "<include>$conf</include>"
done
$placed || echo "<include>$candidate</include>"
echo '</fontconfig>'
} >"$test_tmp/fonts.conf"
export FONTCONFIG_FILE="$test_tmp/fonts.conf"
fc-cache >/dev/null
# Pango, Qt and Skia add lang as a string, which fc-match never does.
cat >"$test_tmp/string-lang.py" <<'PY'
import ctypes, sys
fc = ctypes.CDLL("libfontconfig.so.1")
fc.FcPatternCreate.restype = ctypes.c_void_p
fc.FcPatternAddString.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p]
fc.FcConfigSubstitute.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int]
fc.FcDefaultSubstitute.argtypes = [ctypes.c_void_p]
fc.FcPatternFormat.restype = ctypes.c_char_p
fc.FcPatternFormat.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
fc.FcCharSetCreate.restype = ctypes.c_void_p
fc.FcCharSetAddChar.argtypes = [ctypes.c_void_p, ctypes.c_uint32]
fc.FcPatternAddCharSet.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_void_p]
fc.FcFontMatch.restype = ctypes.c_void_p
fc.FcFontMatch.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(ctypes.c_int)]
for line in sys.stdin:
family, lang, char = line.split()
pattern = fc.FcPatternCreate()
fc.FcPatternAddString(pattern, b"family", family.encode())
if lang != "-":
fc.FcPatternAddString(pattern, b"lang", lang.encode())
if char == "families":
fc.FcConfigSubstitute(None, pattern, 0)
print(fc.FcPatternFormat(pattern, b"%{family}").decode())
continue
if char != "-":
charset = fc.FcCharSetCreate()
fc.FcCharSetAddChar(charset, int(char, 16))
fc.FcPatternAddCharSet(pattern, b"charset", charset)
fc.FcConfigSubstitute(None, pattern, 0)
fc.FcDefaultSubstitute(pattern)
result = ctypes.c_int()
print(fc.FcPatternFormat(fc.FcFontMatch(None, pattern, ctypes.byref(result)), b"%{family[0]}").decode())
PY
# resolve <locale> <string|langset> reads "family lang char" lines
resolve() {
if [[ $2 == "string" ]]; then
LANG=$1 python3 "$test_tmp/string-lang.py"
else
local family lang char query
while read -r family lang char; do
query=$family
[[ $lang == "-" ]] || query+=":lang=$lang"
[[ $char == "-" ]] || query+=":charset=$char"
LANG=$1 fc-match -f '%{family[0]}\n' "$query"
done
fi
}
declare -A latin=([sans-serif]="Liberation Sans" [serif]="Liberation Serif" [monospace]="JetBrainsMono Nerd Font")
declare -A cjk=([sans-serif]="Noto Sans CJK" [serif]="Noto Serif CJK" [monospace]="Noto Sans Mono CJK")
declare -A variant=(
[zh]=SC [zh-cn]=SC [zh-sg]=SC [zh-hans]=SC [zh-hans-cn]=SC [zh-hans-hk]=SC
[zh-tw]=TC [zh-hant]=TC [zh-hant-tw]=TC
[zh-hk]=HK [zh-mo]=HK [zh-hant-hk]=HK [zh-hant-mo]=HK
[yue]=HK [yue-hk]=HK [yue-hant]=HK [yue-hant-hk]=HK [yue-hant-cn]=HK
[yue-hans]=SC [yue-hans-cn]=SC [yue-hans-hk]=SC [yue-cn]=SC
[ja]=JP [ja-jp]=JP [ko]=KR [ko-kr]=KR
)
queries=() expected=()
for tag in "${!variant[@]}"; do
for generic in sans-serif serif monospace; do
queries+=("$generic $tag -") expected+=("${latin[$generic]}")
queries+=("$generic $tag 9aa8") expected+=("${cjk[$generic]} ${variant[$tag]}")
done
done
for generic in sans-serif serif monospace; do
queries+=("$generic - -") expected+=("${latin[$generic]}")
done
for locale in C.UTF-8 zh_CN.UTF-8; do
for path in string langset; do
mapfile -t actual < <(printf '%s\n' "${queries[@]}" | resolve "$locale" "$path")
for i in "${!queries[@]}"; do
[[ ${actual[i]} == "${expected[i]}" ]] ||
fail "$locale $path lang: ${queries[i]} resolves to ${expected[i]}" "${actual[i]}"
done
done
pass "$locale: tagged CJK keeps Omarchy's Latin families and sets Han in the tag's variant"
done
for path in string langset; do
for generic in sans-serif serif monospace; do
actual=$(echo "$generic - 9aa8" | resolve zh_CN.UTF-8 "$path")
[[ $actual == "${cjk[$generic]} SC" ]] || fail "zh_CN $path: untagged Han in $generic is SC" "$actual"
done
done
pass "a Chinese locale sets untagged Han in SC"
untouched=$(echo "sans-serif en families" | resolve C.UTF-8 string)
[[ $(echo "sans-serif ja families" | resolve C.UTF-8 string) != "$untouched" ]] ||
fail "lang=ja changes the family list, so the prefix check below can see a match"
for tag in jam jav kok koi; do
actual=$(echo "sans-serif $tag families" | resolve C.UTF-8 string)
[[ $actual == "$untouched" ]] || fail "string lang=$tag is not taken for Japanese or Korean" "$actual"
done
pass "languages that only share a prefix with ja or ko get no CJK variant"
+1 -1
View File
@@ -34,7 +34,7 @@ for (const item of defaults) {
if (item.id !== 'system.lock' && zh && zh.declared.includes('label') && result.label !== zh.label) fail(`${item.id} shows its Chinese label`)
}
if (merged['system.lock'].label !== 'Mine') fail('a user extension still overrides the Chinese label')
if (merged['system.reboot'].label !== '重启' || merged['setup.language'].label !== '语言') fail('Chinese labels reach the merged menu')
if (merged['system.reboot'].label !== '重启' || merged['setup.language'].label !== '语言' || merged['install.china.wechat'].label !== '微信') fail('Chinese labels reach the merged menu')
console.log('ok - Chinese labels replace only the label, and user extensions still win')
JS
+10 -1
View File
@@ -304,6 +304,14 @@ assert(
&& defaultById['setup.language.zh'].action.includes('omarchy-language zh'),
'menu offers English and Simplified Chinese under Setup > Language'
)
const regionEntries = defaultItems.filter(item => item.parent === 'setup.region')
assert(
regionEntries.map(item => item.label).join('\0') === 'Global\0China'
&& regionEntries.every(item => item.checked.includes(`== \"${item.label}\"`) && !item.when)
&& defaultById['setup.region.global'].action.includes('omarchy-region global')
&& defaultById['setup.region.china'].action.includes('omarchy-region cn'),
'menu offers Global and China under Setup > Region'
)
// Software you already have keeps its place in Install, dimmed rather than
// dropped, so the list reads as a catalog of what Omarchy can install.
// Chromium Account is the sole Install row with anything left to hide for. The
@@ -314,7 +322,8 @@ const windowsGuard = 'omarchy-hw-x86'
const x86OnlyInstalls = ['install.windows', 'install.browser.edge', 'install.service.dropbox', 'install.service.spotify', 'install.gaming.minecraft', 'install.gaming.heroic']
assertDeepEqual(
defaultItems
.filter(item => item.id.startsWith('install.') && item.action && item.when)
// China Apps rows hide by region, not because the software is there.
.filter(item => item.id.startsWith('install.') && item.action && item.when && item.when !== '[[ "$(omarchy-region)" == "China" ]]')
.map(item => item.id)
.sort(),
[...x86OnlyInstalls, 'install.service.chromium-account'].sort(),
+141
View File
@@ -0,0 +1,141 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
scratch=$(mktemp -d)
trap 'rm -rf "$scratch"' EXIT
export OMARCHY_PATH="$ROOT"
export TARGET_ROOT="$scratch/target root"
mkdir -p "$TARGET_ROOT/etc/omarchy"
apply() { "$ROOT/bin/omarchy-apply-pacman" "$1" "$TARGET_ROOT"; }
for channel in stable rc edge; do
apply "$channel"
cmp "$ROOT/default/pacman/pacman-$channel.conf" "$TARGET_ROOT/etc/pacman.conf"
cmp "$ROOT/default/pacman/mirrorlist-$channel" "$TARGET_ROOT/etc/pacman.d/mirrorlist"
done
pass "missing region preserves all global channel templates byte for byte"
chmod 600 "$TARGET_ROOT/etc/pacman.conf"
apply stable
[[ $(stat -c %a "$TARGET_ROOT/etc/pacman.conf") == 600 ]] || fail "refresh keeps a customized pacman.conf mode"
chmod 644 "$TARGET_ROOT/etc/pacman.conf"
pass "refresh keeps existing config file modes"
printf 'cn\n' > "$TARGET_ROOT/etc/omarchy/region"
for channel in stable rc edge; do
case "$channel" in
stable) host=stable-mirror.omarchy ;;
rc) host=rc-mirror.omarchy ;;
edge) host=mirror.omarchy ;;
esac
apply "$channel"
expected=$(printf 'Server = https://%s.cn/$repo/os/$arch\nServer = https://%s.org/$repo/os/$arch' "$host" "$host")
[[ $(<"$TARGET_ROOT/etc/pacman.d/mirrorlist") == "$expected" ]] || fail "$channel mirror order and variables"
{
cat "$ROOT/default/pacman/pacman-$channel.conf"
printf '\n[archlinuxcn]\nServer = https://mirrors.ustc.edu.cn/archlinuxcn/$arch\n'
} > "$scratch/expected.conf"
cmp "$scratch/expected.conf" "$TARGET_ROOT/etc/pacman.conf"
apply "$channel"
cmp "$TARGET_ROOT/etc/pacman.conf.bak" "$TARGET_ROOT/etc/pacman.conf"
cmp "$TARGET_ROOT/etc/pacman.d/mirrorlist.bak" "$TARGET_ROOT/etc/pacman.d/mirrorlist"
done
pass "China puts its channel mirror first, keeps the global one as fallback, and is idempotent"
cp "$TARGET_ROOT/etc/pacman.conf" "$scratch/before.conf"
cp "$TARGET_ROOT/etc/pacman.d/mirrorlist" "$scratch/before.mirrors"
for region in '' CN chn zz '../cn' '$(touch NEVER_EXECUTE)'; do
printf '%s\n' "$region" > "$TARGET_ROOT/etc/omarchy/region"
if apply stable > "$scratch/error" 2>&1; then
fail "invalid region accepted: $region"
fi
cmp "$scratch/before.conf" "$TARGET_ROOT/etc/pacman.conf"
cmp "$scratch/before.mirrors" "$TARGET_ROOT/etc/pacman.d/mirrorlist"
done
pass "unsupported and malformed regions fail before replacing either config"
printf 'cn\n' > "$TARGET_ROOT/etc/omarchy/region"
mkdir -p "$scratch/incomplete/default/regions/cn/pacman"
cp -a "$ROOT/default/pacman" "$scratch/incomplete/default/"
cp "$ROOT/default/regions/cn/pacman/pacman.conf.append" "$scratch/incomplete/default/regions/cn/pacman/"
if OMARCHY_PATH="$scratch/incomplete" apply stable > "$scratch/error" 2>&1; then
fail "incomplete region profile accepted"
fi
cmp "$scratch/before.conf" "$TARGET_ROOT/etc/pacman.conf"
cmp "$scratch/before.mirrors" "$TARGET_ROOT/etc/pacman.d/mirrorlist"
pass "missing fragment cannot leave a half-generated config"
# Refresh renders these files unprivileged and hands root plain copies; its
# privileged path is covered in the sudo-boundary sandbox, never on the host.
cp "$TARGET_ROOT/etc/pacman.conf" "$scratch/before.conf"
mkdir -p "$scratch/rendered"
"$ROOT/bin/omarchy-apply-pacman" --render rc "$scratch/rendered" "$TARGET_ROOT"
rg -qxF 'Server = https://pkgs.omarchy.org/rc/$arch' "$scratch/rendered/pacman.conf"
rg -qxF '[archlinuxcn]' "$scratch/rendered/pacman.conf"
[[ $(<"$scratch/rendered/mirrorlist") == $'Server = https://rc-mirror.omarchy.cn/$repo/os/$arch\nServer = https://rc-mirror.omarchy.org/$repo/os/$arch' ]] ||
fail "render puts the regional channel mirror first"
cmp "$scratch/before.conf" "$TARGET_ROOT/etc/pacman.conf"
pass "render builds the regional channel files without touching the target"
if "$ROOT/bin/omarchy-apply-pacman" --render dev "$scratch/rendered" "$TARGET_ROOT" > "$scratch/error" 2>&1; then
fail "render accepts a nonexistent dev pacman template"
fi
pass "render rejects channels without pacman templates"
printf 'global\n' > "$TARGET_ROOT/etc/omarchy/region"
apply stable
cmp "$ROOT/default/pacman/pacman-stable.conf" "$TARGET_ROOT/etc/pacman.conf"
cmp "$ROOT/default/pacman/mirrorlist-stable" "$TARGET_ROOT/etc/pacman.d/mirrorlist"
pass "users can opt out of regional repository defaults"
# The shipped China profile is repository defaults only. Language, locale, and
# input methods follow the installer's language choice, not the region.
python3 - <<'PY'
import os
from pathlib import Path
region = Path(os.environ["ROOT"]) / "default/regions/cn"
packages = {line.strip() for line in (region / "packages").read_text().splitlines() if line.strip() and not line.startswith("#")}
assert packages == {"archlinuxcn-keyring"}, packages
assert not (region / "skel").exists()
PY
pass "China profile adds only its repository keyring"
# The installer derives the region from the chosen timezone, so every profile
# needs a mapping, and no timezone may select two regions.
python3 - <<'PY'
import os
from pathlib import Path
zoneinfo = Path("/usr/share/zoneinfo")
owners = {}
for profile in sorted((Path(os.environ["ROOT"]) / "default/regions").iterdir()):
lines = (profile / "timezones").read_text().splitlines()
zones = [line.strip() for line in lines if line.strip() and not line.startswith("#")]
assert zones, f"{profile.name} maps no timezones"
for zone in zones:
assert zone not in owners, f"{zone} maps to {owners[zone]} and {profile.name}"
owners[zone] = profile.name
if zoneinfo.is_dir():
assert (zoneinfo / zone).is_file(), f"{profile.name} lists unknown timezone {zone}"
# The installer offers tzdata's backward-compatible names too, so China
# covers every name that links to its two zones and nothing else.
# Without tzdata.zi (minimal containers) only the canonical zones can be
# checked, so completeness is enforced where the host has the link table.
china = {zone for zone, region in owners.items() if region == "cn"}
expected = {"Asia/Shanghai", "Asia/Urumqi"}
tzdata = zoneinfo / "tzdata.zi"
if tzdata.is_file():
for line in tzdata.read_text().splitlines():
fields = line.split()
if len(fields) == 3 and fields[0] == "L" and fields[1] in {"Asia/Shanghai", "Asia/Urumqi"}:
expected.add(fields[2])
assert china == expected, (china, expected)
else:
assert expected <= china, (china, expected)
PY
pass "region profiles map each timezone to at most one region"
+101
View File
@@ -0,0 +1,101 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
source "$SHELL_TEST_DIR/fixtures/sudo-boundary-test.sh"
copy_boundary_file bin/omarchy-refresh-pacman
copy_boundary_file bin/omarchy-apply-pacman
for file in default/pacman/pacman-rc.conf default/pacman/mirrorlist-rc \
default/regions/cn/pacman/pacman.conf.append default/regions/cn/pacman/mirrorlist-rc.prepend; do
copy_boundary_file "$file"
done
# A sandboxed China machine: the render reads this region marker, never the
# host's /etc/omarchy/region.
mkdir -p "$SUDO_TEST_TARGET/etc/omarchy"
printf 'cn\n' >"$SUDO_TEST_TARGET/etc/omarchy/region"
# cp still logs its step, but now really copies, so the rendered files exist.
# Root's copies into /etc land in a capture directory instead of the host.
export SUDO_TEST_CAPTURE="$boundary_tmp/capture"
rm "$SUDO_TEST_ROOT/bin/cp"
cat >"$SUDO_TEST_ROOT/bin/cp" <<'STUB'
#!/bin/bash
set -euo pipefail
printf 'step:cp %s\n' "$*" >>"$SUDO_TEST_LOG"
destination=${!#}
if [[ $destination == /etc/* ]]; then
source=${*: -2:1}
[[ $source == /etc/* ]] && exit 0
/usr/bin/mkdir -p "$SUDO_TEST_CAPTURE${destination%/*}"
exec /usr/bin/cp "$source" "$SUDO_TEST_CAPTURE$destination"
fi
exec /usr/bin/cp "$@"
STUB
chmod +x "$SUDO_TEST_ROOT/bin/cp"
# Regional rendering must stay unprivileged: root only copies finished files,
# exactly as the channel refresh did before regions existed.
reset_boundary
"$SUDO_TEST_ROOT/bin/omarchy-refresh-pacman" rc >"$boundary_tmp/output" 2>&1 || fail "refresh failed" "$(<"$boundary_tmp/output")"
assert_boundary_cold "regional refresh"
python3 - "$SUDO_TEST_LOG" "$SUDO_TEST_ROOT" <<'PY'
import re
import sys
events = open(sys.argv[1]).read().splitlines()
root = sys.argv[2]
render = [i for i, e in enumerate(events) if e.startswith('step:cp ' + root + '/default/pacman/pacman-rc.conf ')]
assert len(render) == 1, events
rendered = events[render[0]].split()[-1].rsplit('/', 1)[0]
copies = [i for i, e in enumerate(events) if e.startswith('sudo -N cp ')]
assert [events[i] for i in copies[2:]] == [
f'sudo -N cp -f {rendered}/pacman.conf /etc/pacman.conf',
f'sudo -N cp -f {rendered}/mirrorlist /etc/pacman.d/mirrorlist',
], events
assert not any(e.startswith('sudo ') and 'omarchy-apply-pacman' in e for e in events), events
assert render[0] < copies[0], events
transaction = next(i for i, e in enumerate(events) if e.startswith('step:pacman '))
assert max(copies) < transaction, events
PY
{
cat "$ROOT/default/pacman/pacman-rc.conf"
printf '\n'
cat "$ROOT/default/regions/cn/pacman/pacman.conf.append"
} >"$boundary_tmp/expected-pacman.conf"
cat "$ROOT/default/regions/cn/pacman/mirrorlist-rc.prepend" "$ROOT/default/pacman/mirrorlist-rc" >"$boundary_tmp/expected-mirrorlist"
cmp -s "$boundary_tmp/expected-pacman.conf" "$SUDO_TEST_CAPTURE/etc/pacman.conf" ||
fail "root installs the rendered China pacman.conf" "$(diff "$boundary_tmp/expected-pacman.conf" "$SUDO_TEST_CAPTURE/etc/pacman.conf" 2>&1)"
cmp -s "$boundary_tmp/expected-mirrorlist" "$SUDO_TEST_CAPTURE/etc/pacman.d/mirrorlist" ||
fail "root installs the rendered China mirrorlist" "$(diff "$boundary_tmp/expected-mirrorlist" "$SUDO_TEST_CAPTURE/etc/pacman.d/mirrorlist" 2>&1)"
pass "refresh renders regional config unprivileged and gives root only plain copies"
reset_boundary
if "$SUDO_TEST_ROOT/bin/omarchy-refresh-pacman" dev >"$boundary_tmp/output" 2>&1; then
fail "refresh accepts an invalid channel"
fi
if grep -Eq '^(sudo -N cp |step:pacman )' "$SUDO_TEST_LOG"; then
fail "invalid channel reached privileged work" "$(<"$SUDO_TEST_LOG")"
fi
pass "failed refresh does not proceed to privileged copies or a package update"
# A failed render still removes its scratch directory and exits cold.
reset_boundary
mv "$SUDO_TEST_ROOT/bin/omarchy-apply-pacman" "$boundary_tmp/omarchy-apply-pacman"
cat >"$SUDO_TEST_ROOT/bin/omarchy-apply-pacman" <<'STUB'
#!/bin/bash
: >"$3/pacman.conf"
exit 1
STUB
chmod +x "$SUDO_TEST_ROOT/bin/omarchy-apply-pacman"
mkdir -p "$boundary_tmp/scratch"
if TMPDIR="$boundary_tmp/scratch" "$SUDO_TEST_ROOT/bin/omarchy-refresh-pacman" rc >"$boundary_tmp/output" 2>&1; then
fail "refresh succeeds after a failed render"
fi
mv "$boundary_tmp/omarchy-apply-pacman" "$SUDO_TEST_ROOT/bin/omarchy-apply-pacman"
[[ -z $(ls -A "$boundary_tmp/scratch") ]] || fail "failed render leaves its scratch directory" "$(ls -AR "$boundary_tmp/scratch")"
if grep -Eq '^(sudo -N cp |step:pacman )' "$SUDO_TEST_LOG"; then
fail "failed render reached privileged work" "$(<"$SUDO_TEST_LOG")"
fi
assert_boundary_cold "failed render"
pass "failed render removes its scratch directory and exits cold"
+88
View File
@@ -0,0 +1,88 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
root="$test_tmp/root" sandbox="$test_tmp/omarchy" calls="$test_tmp/calls"
mkdir -p "$root/etc/pacman.d" "$sandbox/bin" "$sandbox/install/config" "$sandbox/default/regions" "$test_tmp/bin" "$test_tmp/home"
cp -r "$ROOT/default/regions/cn" "$sandbox/default/regions/"
mkdir -p "$sandbox/shell/Commons" && cp "$ROOT/shell/Commons/I18nZh.js" "$sandbox/shell/Commons/"
rewrite() { sed -e "s|/etc/|$root/etc/|g" -e "s| /\\.cargo/| $root/.cargo/|g" "$1" >"$2"; chmod +x "$2"; }
rewrite "$ROOT/install/config/region.sh" "$sandbox/install/config/region.sh"
rewrite "$ROOT/bin/omarchy-region" "$sandbox/bin/omarchy-region"
for command in omarchy-refresh-pacman omarchy-system-logout pacman-key pacman omarchy-apply-pacman; do
printf '#!/bin/bash\necho "%s $*" >>"%s"\n' "$command" "$calls" >"$test_tmp/bin/$command"
done
printf '#!/bin/bash\necho "omarchy-update-pacman $*" >>"%s"\n[[ -z ${STUB_UPDATE_FAIL:-} ]]\n' "$calls" >"$test_tmp/bin/omarchy-update-pacman"
printf '#!/bin/bash\necho "${STUB_CHANNEL:-stable}"\n' >"$test_tmp/bin/omarchy-version-channel"
printf '#!/bin/bash\necho "${STUB_PLATFORM:-x86}"\n' >"$test_tmp/bin/omarchy-hw-platform"
printf '#!/bin/bash\n[[ $1 == env ]] && shift\nexec env "$@"\n' >"$test_tmp/bin/sudo"
printf '#!/bin/bash\necho "gum $*" >>"%s"\nexit "${GUM_ANSWER:-1}"\n' "$calls" >"$test_tmp/bin/gum"
chmod +x "$test_tmp/bin"/*
ln -s "$ROOT/bin/omarchy-text" "$test_tmp/bin/omarchy-text"
mkdir -p "$test_tmp/tmp"
export PATH="$test_tmp/bin:$sandbox/bin:$PATH" OMARCHY_PATH="$sandbox" HOME="$test_tmp/home" TMPDIR="$test_tmp/tmp"
switch_calls() { sed "s|$test_tmp/tmp/[^/ ]*|RENDERED|g" "$calls"; }
omarchy-region cn >/dev/null
[[ $(omarchy-region) == "China" && $(<"$root/etc/omarchy/region") == "cn" ]] ||
fail "switching to China records cn"
[[ $(switch_calls) == $'omarchy-update-pacman -Syu --noconfirm\nomarchy-apply-pacman --render stable RENDERED\npacman --config RENDERED/pacman.conf -Sy --noconfirm --needed archlinuxcn-keyring\npacman-key --populate archlinuxcn\nomarchy-refresh-pacman stable' ]] ||
fail "China upgrades, trusts ArchLinuxCN through the rendered cn config, then refreshes" "$(switch_calls)"
[[ -z $(ls -A "$test_tmp/tmp") ]] || fail "the rendered config is removed"
pass "switching to China trusts ArchLinuxCN before the refresh upgrades through it"
: >"$calls"
STUB_CHANNEL=edge omarchy-region cn >/dev/null
grep -qx 'omarchy-refresh-pacman edge' "$calls" || fail "the refresh keeps the machine's channel" "$(<"$calls")"
pass "switching to China again keeps the channel"
: >"$calls"
omarchy-region global >/dev/null
[[ $(omarchy-region) == "Global" && ! -e $root/etc/omarchy/region ]] ||
fail "switching to Global removes the region"
[[ $(<"$calls") == 'omarchy-refresh-pacman stable' ]] ||
fail "Global refreshes pacman back to the channel defaults" "$(<"$calls")"
pass "switching to Global restores the channel's pacman defaults"
if output=$(omarchy-region mars 2>&1); then
fail "an unknown region is refused" "$output"
fi
if output=$(STUB_CHANNEL="stable / edge" omarchy-region cn 2>&1); then
fail "a mixed channel is refused" "$output"
fi
[[ $(omarchy-region) == "Global" ]] || fail "a refused switch changes nothing"
pass "an unknown region or an unclear channel is refused"
: >"$calls"
if output=$(STUB_PLATFORM=aarch64-apple omarchy-region cn 2>&1); then
fail "an ARM machine refuses the China region" "$output"
fi
[[ $(omarchy-region) == "Global" && ! -s $calls ]] || fail "a refused ARM switch changes nothing" "$(<"$calls")"
pass "an ARM machine refuses the China region before changing anything"
: >"$calls"
if output=$(STUB_UPDATE_FAIL=1 omarchy-region cn 2>&1); then
fail "switching to China fails when the upgrade fails" "$output"
fi
[[ $(omarchy-region) == "Global" ]] && ! grep -q '^omarchy-refresh-pacman' "$calls" ||
fail "a failed switch to China leaves the region Global and pacman untouched" "$(<"$calls")"
pass "a failed switch to China leaves the machine Global"
grep -q '^gum' "$calls" && fail "a switch without a terminal asks nothing" "$(<"$calls")"
: >"$calls"
output=$(LANG=zh_CN.UTF-8 GUM_ANSWER=1 script -qec "omarchy-region cn" /dev/null)
grep -qxF 'gum confirm --affirmative 立即注销 --negative 稍后 立即注销以切换开发工具镜像?' "$calls" ||
fail "a Chinese session offers to log out in Chinese" "$(<"$calls")"
[[ $output == *"重新登录后切换开发工具镜像。"* ]] && ! grep -q '^omarchy-system-logout' "$calls" ||
fail "declining the logout leaves a reminder and the session" "$output"
: >"$calls"
LANG=en_US.UTF-8 GUM_ANSWER=0 script -qec "omarchy-region global" /dev/null >/dev/null
grep -qxF 'gum confirm --affirmative Log out now --negative Later Log out now to switch the developer mirrors?' "$calls" &&
grep -qx 'omarchy-system-logout ' "$calls" || fail "a terminal switch offers to log out and logs out on yes" "$(<"$calls")"
pass "a switch in a terminal offers to log out in the session's language"
+75
View File
@@ -0,0 +1,75 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
region_setup="$test_tmp/region.sh"
root="$test_tmp/root"
sed -e "s|/etc/|$root/etc/|g" -e "s| /\.cargo/| $root/.cargo/|g" "$ROOT/install/config/region.sh" >"$region_setup"
export OMARCHY_PATH="$ROOT"
mkdir -p "$root/etc/omarchy"
bash -e "$region_setup"
[[ -z $(find "$root" -type l) ]] || fail "no marker links nothing"
pass "no marker installs Global"
echo cn >"$root/etc/omarchy/region"
bash -e "$region_setup"
for link in "environment.conf etc/environment.d/90-omarchy-china.conf" "cargo.toml .cargo/config.toml" "gemrc etc/gemrc"; do
read -r source target <<<"$link"
[[ $(readlink "$root/$target") == "$ROOT/default/regions/cn/$source" ]] || fail "China links /$target to the shipped $source"
done
bash -e "$region_setup"
pass "the cn marker links the developer mirror drop-ins, again without error"
rm "$root/etc/gemrc"
echo ":sources: []" >"$root/etc/gemrc"
if output=$(bash -e "$region_setup" 2>&1); then
fail "China refuses to replace an admin's /etc/gemrc" "$output"
fi
[[ $output == "Refusing to replace $root/etc/gemrc, which Omarchy did not create" && $(<"$root/etc/gemrc") == ":sources: []" ]] ||
fail "China names the file it refuses to replace and leaves it" "$output"
rm "$root/etc/gemrc"
pass "region setup never replaces mirror config Omarchy did not create"
echo mars >"$root/etc/omarchy/region"
if output=$(bash -e "$region_setup" 2>&1); then
fail "an unknown region fails" "$output"
fi
[[ $output == "Unknown region: mars" ]] || fail "region setup names the unknown region" "$output"
pass "region setup rejects an unknown region"
echo cn >"$root/etc/omarchy/region"
bash -e "$region_setup"
OMARCHY_REGION=global bash -e "$region_setup"
[[ ! -e $root/etc/omarchy/region && -z $(find "$root" -type l) ]] || fail "Global removes the marker and the developer mirror links"
ln -s /usr/share/somewhere/gemrc "$root/etc/gemrc"
OMARCHY_REGION=global bash -e "$region_setup"
[[ -L $root/etc/gemrc ]] || fail "Global keeps a gemrc link Omarchy did not create"
rm "$root/etc/gemrc"
pass "switching to Global removes only what Omarchy linked"
OMARCHY_REGION=cn bash -e "$region_setup"
[[ $(<"$root/etc/omarchy/region") == "cn" && $(readlink "$root/etc/gemrc") == "$ROOT/default/regions/cn/gemrc" ]] ||
fail "switching to China records cn and links the drop-ins"
pass "switching to China records cn and links the drop-ins"
region_bin="$test_tmp/omarchy-region"
sed "s|/etc/omarchy/region|$root/etc/omarchy/region|" "$ROOT/bin/omarchy-region" >"$region_bin"
chmod +x "$region_bin"
[[ $("$region_bin") == "China" ]] || fail "the cn marker reads as China"
rm "$root/etc/omarchy/region"
[[ $("$region_bin") == "Global" ]] || fail "no marker reads as Global"
echo mars >"$root/etc/omarchy/region"
if output=$("$region_bin" 2>&1); then
fail "an unknown marker fails" "$output"
fi
[[ $output == "Unknown region in $root/etc/omarchy/region: mars" ]] || fail "the reader names the file and value" "$output"
pass "omarchy region reads Global, China, and fails on anything else"
grep -qx 'run_logged "$OMARCHY_INSTALL/config/region.sh"' "$ROOT/install/config/all.sh" || fail "the install runs region setup"
pass "the install runs region setup"
@@ -6,6 +6,7 @@ source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
source "$SHELL_TEST_DIR/fixtures/sudo-boundary-test.sh"
copy_boundary_file bin/omarchy-update
copy_boundary_file bin/omarchy-refresh-pacman
copy_boundary_file bin/omarchy-apply-pacman
# Replace the step symlink, preserving the real fixture dispatcher.
rm "$SUDO_TEST_ROOT/bin/omarchy-update-aur-pkgs"
copy_boundary_file bin/omarchy-update-aur-pkgs