From d897cff7bf9a89cc21bea365e4e48d817cae414a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Mon, 31 Aug 2026 20:53:48 -0400 Subject: [PATCH] Add a Setup > Region toggle with Chinese language and input method `omarchy region china` switches the system language to zh_CN, installs the Rime Ice input method through Andy Stewart's rime-ice-installer, makes Rime the default with Ctrl+Space as the toggle, and swaps the Omarchy menu to Chinese labels through the user-extension overlay; `omarchy region world` puts the language and menu back and leaves the input method installed. mergeMenuSources now only overrides the fields an extension declares, as docs/menu.md documents, which the label-only overlay needs. Co-authored-by: Andy Stewart --- bin/omarchy | 1 + bin/omarchy-region | 120 ++++++++++++++++ default/omarchy/omarchy-menu.jsonc | 3 + default/omarchy/omarchy-menu.zh-cn.jsonc | 168 +++++++++++++++++++++++ manual/34-keyboard-mouse-trackpad.md | 2 + shell/plugins/menu/MenuModel.js | 27 +++- test/shell.d/menu-test.sh | 31 ++++- test/shell.d/region-test.sh | 105 ++++++++++++++ 8 files changed, 450 insertions(+), 7 deletions(-) create mode 100755 bin/omarchy-region create mode 100644 default/omarchy/omarchy-menu.zh-cn.jsonc create mode 100644 test/shell.d/region-test.sh diff --git a/bin/omarchy b/bin/omarchy index 4219109b..cda769dc 100755 --- a/bin/omarchy +++ b/bin/omarchy @@ -72,6 +72,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 sources and input method" GROUP_DESCRIPTIONS[reinstall]="Reinstall and reset workflows" GROUP_DESCRIPTIONS[reminder]="Desktop notification reminders" GROUP_DESCRIPTIONS[remove]="Removal workflows" diff --git a/bin/omarchy-region b/bin/omarchy-region new file mode 100755 index 00000000..d0033679 --- /dev/null +++ b/bin/omarchy-region @@ -0,0 +1,120 @@ +#!/bin/bash + +# omarchy:summary=Show or set the region the machine is set up for +# omarchy:args=[world|china] +# omarchy:examples=omarchy region | omarchy region china + +set -euo pipefail + +region_file="$HOME/.config/omarchy/region" + +if (($# == 0)); then + region="World" + if [[ -f $region_file ]] && read -r stored <"$region_file"; then + region=$stored + fi + echo "$region" + exit 0 +fi + +if (($# > 1)); then + echo "Usage: omarchy-region " + exit 1 +fi + +case "$1" in +world | World) region="World" ;; +china | China) region="China" ;; +*) + echo "Usage: omarchy-region " + exit 1 + ;; +esac + +menu_overlay="$HOME/.config/omarchy/extensions/omarchy-menu.jsonc" +menu_sample="$OMARCHY_PATH/config/omarchy/extensions/omarchy-menu.jsonc" +overlay_marker="// Omarchy region managed" + +if [[ $region == "China" ]]; then + # The stock machine carries the commented sample extension; a menu extension + # someone wrote by hand refuses the switch instead of being overwritten. + if [[ -s $menu_overlay ]] && ! grep -qFx "$overlay_marker" "$menu_overlay" && + ! cmp -s "$menu_overlay" "$menu_sample"; then + echo "menu: $menu_overlay has content Omarchy does not manage" >&2 + echo "Resolve it by hand, then rerun: omarchy region china" >&2 + exit 1 + fi + + mkdir -p "${menu_overlay%/*}" + cp "$OMARCHY_PATH/default/omarchy/omarchy-menu.zh-cn.jsonc" "$menu_overlay" + echo "menu -> Chinese labels" + + # localectl writes /etc/locale.conf; the session picks it up at next login. + if ! grep -qsFx 'LANG=zh_CN.UTF-8' /etc/locale.conf; then + sudo sed -i 's/^#\(zh_CN\.UTF-8 UTF-8\)/\1/' /etc/locale.gen + sudo locale-gen + sudo localectl set-locale LANG=zh_CN.UTF-8 + fi + echo "language -> zh_CN.UTF-8 (from the next login)" +else + if [[ -f $menu_overlay ]] && grep -qFx "$overlay_marker" "$menu_overlay"; then + cp "$menu_sample" "$menu_overlay" + echo "menu -> stock labels" + fi + + if grep -qsFx 'LANG=zh_CN.UTF-8' /etc/locale.conf; then + sudo localectl set-locale LANG=en_US.UTF-8 + echo "language -> en_US.UTF-8 (from the next login)" + fi +fi + +# Rime Ice's TUI drives its own sudo and fcitx5 restart; the schema file is +# what a finished install leaves behind. A fresh install has no repo databases +# until its first sync, and both pkg-add and the installer's pacman need them. +if [[ $region == "China" && ! -f $HOME/.local/share/fcitx5/rime/rime_ice.schema.yaml ]]; then + sudo pacman -Sy + omarchy-pkg-add rime-ice-installer + rime-ice-installer +fi + +# Rime first makes Chinese the active default; keyboard-us stays one +# Ctrl+Space away, pinned explicitly rather than left to fcitx5's default. +if [[ $region == "China" ]]; then + fcitx_dir="$HOME/.config/fcitx5" + mkdir -p "$fcitx_dir" + cat >"$fcitx_dir/profile" <"$fcitx_dir/config.tmp" + printf '[Hotkey/TriggerKeys]\n0=Control+space\n' >>"$fcitx_dir/config.tmp" + mv "$fcitx_dir/config.tmp" "$fcitx_dir/config" + echo "input method -> Rime Ice by default, Ctrl+Space toggles" + + if systemctl --user is-active omarchy-fcitx5.service >/dev/null 2>&1; then + systemctl --user restart omarchy-fcitx5.service + fi +fi + +mkdir -p "${region_file%/*}" +printf '%s\n' "$region" >"$region_file" +echo "Region set to $region" diff --git a/default/omarchy/omarchy-menu.jsonc b/default/omarchy/omarchy-menu.jsonc index 10a045af..31c72fa7 100644 --- a/default/omarchy/omarchy-menu.jsonc +++ b/default/omarchy/omarchy-menu.jsonc @@ -167,6 +167,9 @@ "setup.default.editor.helix": {"icon":"","label":"Helix","checked":"[[ \"$(omarchy-default-editor)\" == \"helix\" ]]","action":"omarchy-default-editor helix"}, "setup.default.editor.vim": {"icon":"","label":"Vim","checked":"[[ \"$(omarchy-default-editor)\" == \"vim\" ]]","action":"omarchy-default-editor vim"}, "setup.default.editor.emacs": {"icon":"","label":"Emacs","checked":"[[ \"$(omarchy-default-editor)\" == \"emacs\" ]]","action":"omarchy-default-editor emacs"}, + "setup.region": {"icon":"󰇧","label":"Region"}, + "setup.region.world": {"icon":"󰖟","label":"World","checked":"[[ \"$(omarchy-region)\" == \"World\" ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-region world'"}, + "setup.region.china": {"icon":"󰗊","label":"China","checked":"[[ \"$(omarchy-region)\" == \"China\" ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-region china'"}, "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"}, diff --git a/default/omarchy/omarchy-menu.zh-cn.jsonc b/default/omarchy/omarchy-menu.zh-cn.jsonc new file mode 100644 index 00000000..c48ee905 --- /dev/null +++ b/default/omarchy/omarchy-menu.zh-cn.jsonc @@ -0,0 +1,168 @@ +// Omarchy region managed +// Chinese labels for the shipped menu. `omarchy region china` installs this +// as the user menu extension; ids keep their actions and icons. +{ + "apps": {"label":"应用"}, + "learn": {"label":"学习"}, + "trigger": {"label":"触发"}, + "style": {"label":"风格"}, + "setup": {"label":"设置"}, + "install": {"label":"安装"}, + "remove": {"label":"移除"}, + "update": {"label":"更新"}, + "about": {"label":"关于"}, + "system": {"label":"系统"}, + "system.screensaver": {"label":"屏保"}, + "system.lock": {"label":"锁屏"}, + "system.suspend": {"label":"睡眠"}, + "system.hibernate": {"label":"休眠"}, + "system.logout": {"label":"注销"}, + "system.reboot": {"label":"重启"}, + "system.shutdown": {"label":"关机"}, + "learn.keybindings": {"label":"快捷键"}, + "learn.omarchy": {"label":"Omarchy 手册"}, + "learn.community": {"label":"社区"}, + "trigger.emoji": {"label":"表情"}, + "trigger.reminder": {"label":"提醒"}, + "trigger.capture": {"label":"截取"}, + "trigger.capture.screenshot": {"label":"截图"}, + "trigger.capture.screenrecord.stop": {"label":"停止录屏"}, + "trigger.capture.screenrecord": {"label":"录屏"}, + "trigger.capture.text": {"label":"OCR 取字"}, + "trigger.capture.qr": {"label":"二维码"}, + "trigger.capture.color": {"label":"取色"}, + "trigger.capture.screenrecord.no-audio": {"label":"无音频"}, + "trigger.capture.screenrecord.desktop-audio": {"label":"含桌面音频"}, + "trigger.capture.screenrecord.microphone": {"label":"含桌面与麦克风音频"}, + "trigger.capture.screenrecord.webcam": {"label":"含桌面、麦克风与摄像头"}, + "trigger.transcode": {"label":"转码"}, + "trigger.share": {"label":"分享"}, + "trigger.toggle": {"label":"开关"}, + "trigger.hardware": {"label":"硬件"}, + "trigger.tests": {"label":"测速"}, + "trigger.hardware.laptop-display": {"label":"笔记本屏幕"}, + "trigger.hardware.mirror-display": {"label":"镜像显示"}, + "trigger.hardware.hybrid-gpu": {"label":"混合显卡"}, + "trigger.hardware.touchpad": {"label":"触控板"}, + "trigger.hardware.touchpad-haptics": {"label":"触控板振动"}, + "trigger.hardware.touchpad-haptics.low": {"label":"低"}, + "trigger.hardware.touchpad-haptics.mid": {"label":"中"}, + "trigger.hardware.touchpad-haptics.high": {"label":"高"}, + "trigger.hardware.touchscreen": {"label":"触摸屏"}, + "trigger.reminder.set": {"label":"设一个提醒"}, + "trigger.reminder.show": {"label":"查看全部"}, + "trigger.reminder.clear": {"label":"清除全部"}, + "trigger.share.clipboard": {"label":"剪贴板"}, + "trigger.share.file": {"label":"文件"}, + "trigger.share.folder": {"label":"文件夹"}, + "trigger.share.receive": {"label":"接收"}, + "trigger.toggle.idle-lock": {"label":"保持唤醒"}, + "trigger.toggle.notifications": {"label":"通知"}, + "trigger.toggle.crash-capture": {"label":"崩溃捕获"}, + "trigger.toggle.screensaver": {"label":"屏保"}, + "trigger.toggle.nightlight": {"label":"夜间模式"}, + "trigger.toggle.top-bar": {"label":"菜单栏"}, + "trigger.toggle.battery-percentage": {"label":"电量百分比"}, + "trigger.toggle.workspace-layout": {"label":"工作区布局"}, + "trigger.toggle.window-gaps": {"label":"窗口间距"}, + "trigger.toggle.one-window-ratio": {"label":"单窗口比例"}, + "trigger.tests.network-speedtest": {"label":"网络测速"}, + "trigger.tests.disk-speedtest": {"label":"磁盘测速"}, + "style.theme": {"label":"主题"}, + "style.background": {"label":"壁纸"}, + "style.unlock": {"label":"解锁画面"}, + "style.font": {"label":"字体"}, + "style.bar": {"label":"菜单栏"}, + "style.bar.position": {"label":"位置"}, + "style.bar.transparency": {"label":"透明"}, + "style.screensaver": {"label":"屏保"}, + "style.about": {"label":"关于"}, + "style.bar.position.top": {"label":"顶部"}, + "style.bar.position.bottom": {"label":"底部"}, + "style.bar.position.left": {"label":"左侧"}, + "style.bar.position.right": {"label":"右侧"}, + "style.about.text": {"label":"编辑文字"}, + "style.about.image": {"label":"取自图片"}, + "style.about.default": {"label":"恢复默认"}, + "style.screensaver.text": {"label":"编辑文字"}, + "style.screensaver.image": {"label":"取自图片"}, + "style.screensaver.default": {"label":"恢复默认"}, + "setup.monitors": {"label":"显示器"}, + "setup.keybindings": {"label":"快捷键"}, + "setup.input": {"label":"输入设备"}, + "setup.network": {"label":"网络"}, + "setup.network.dns.custom": {"label":"自定义"}, + "setup.network.qr": {"label":"二维码"}, + "setup.default": {"label":"默认程序"}, + "setup.default.agent": {"title":"默认 Agent"}, + "setup.default.browser": {"label":"浏览器","title":"默认浏览器"}, + "setup.default.terminal": {"label":"终端","title":"默认终端"}, + "setup.default.editor": {"label":"编辑器","title":"默认编辑器"}, + "setup.region": {"label":"区域"}, + "setup.region.world": {"label":"世界"}, + "setup.region.china": {"label":"中国"}, + "setup.plugin": {"label":"插件"}, + "setup.plugin.enable": {"label":"启用插件"}, + "setup.plugin.disable": {"label":"停用插件"}, + "setup.plugin.add": {"label":"添加插件"}, + "setup.plugin.clone": {"label":"克隆插件"}, + "setup.plugin.remove": {"label":"移除插件"}, + "setup.security": {"label":"安全"}, + "setup.config": {"label":"配置"}, + "setup.security.fingerprint": {"label":"指纹"}, + "setup.security.passwordless-sudo": {"label":"免密 sudo"}, + "setup.security.sudoless-docker": {"label":"免 sudo Docker"}, + "setup.direct-boot": {"label":"直接启动"}, + "setup.reset": {"label":"恢复出厂设置"}, + "install.package": {"label":"软件包"}, + "install.service": {"label":"服务"}, + "install.development": {"label":"开发环境"}, + "install.editor": {"label":"编辑器"}, + "install.style": {"label":"风格"}, + "install.style.theme": {"label":"主题"}, + "install.style.background": {"label":"壁纸"}, + "install.style.font": {"label":"字体"}, + "install.gaming": {"label":"游戏"}, + "install.browser": {"label":"浏览器"}, + "install.webapp": {"label":"网页应用"}, + "install.terminal": {"label":"终端"}, + "install.preinstalls": {"label":"预装应用"}, + "install.service.chromium-account": {"label":"Chromium 账号"}, + "install.ai.dictation": {"label":"语音听写"}, + "install.gaming.xbox-cloud": {"label":"Xbox 云游戏"}, + "install.gaming.xbox-controllers": {"label":"Xbox 手柄"}, + "install.gaming.retro-launcher": {"label":"RetroArch 启动器"}, + "install.development.docker-dbs": {"label":"Docker 数据库"}, + "remove.package": {"label":"软件包"}, + "remove.ai": {"title":"移除"}, + "remove.service": {"label":"服务","title":"移除"}, + "remove.development": {"label":"开发环境","title":"移除"}, + "remove.theme": {"label":"主题"}, + "remove.gaming": {"label":"游戏","title":"移除"}, + "remove.browser": {"label":"浏览器","title":"移除"}, + "remove.webapp": {"label":"网页应用"}, + "remove.preinstalls": {"label":"预装应用"}, + "remove.security": {"label":"安全","title":"移除"}, + "remove.security.fingerprint": {"label":"指纹"}, + "remove.security.sudoless-docker": {"label":"免 sudo Docker"}, + "remove.ai.dictation": {"label":"语音听写"}, + "remove.gaming.xbox-cloud": {"label":"Xbox 云游戏"}, + "remove.gaming.xbox-controllers": {"label":"Xbox 手柄 (󰂯)"}, + "remove.development.javascript": {"title":"移除"}, + "remove.development.php": {"title":"移除"}, + "remove.development.elixir": {"title":"移除"}, + "update.channel": {"label":"通道"}, + "update.config": {"label":"配置","title":"重置为默认"}, + "update.themes": {"label":"附加主题"}, + "update.process": {"label":"进程","title":"重启"}, + "update.hardware": {"label":"硬件","title":"重启"}, + "update.firmware": {"label":"固件"}, + "update.password": {"label":"密码"}, + "update.timezone": {"label":"时区"}, + "update.time": {"label":"时间"}, + "update.hardware.audio": {"label":"音频"}, + "update.hardware.bluetooth": {"label":"蓝牙"}, + "update.hardware.trackpad": {"label":"触控板"}, + "update.password.drive": {"label":"磁盘加密"}, + "update.password.user": {"label":"用户"}, +} diff --git a/manual/34-keyboard-mouse-trackpad.md b/manual/34-keyboard-mouse-trackpad.md index 648e7713..c364255c 100644 --- a/manual/34-keyboard-mouse-trackpad.md +++ b/manual/34-keyboard-mouse-trackpad.md @@ -61,6 +61,8 @@ On Dell XPS laptops with a haptic touchpad, you can also set the click strength Omarchy runs the [fcitx5](https://fcitx-im.org/) input method framework as part of every session — it's what powers the CapsLock compose sequences. That means the plumbing for non-Latin input is already in place: install an input engine like `fcitx5-mozc` (Japanese) or `fcitx5-chinese-addons` (Chinese) with `omarchy pkg add`, plus `fcitx5-configtool` to add the engine to your input methods and set the key that switches between them. +For Chinese there's a shortcut: flip _Setup > Region_ to China and Omarchy installs and configures the [Rime Ice](https://github.com/iDvel/rime-ice) input method for you, using [Andy Stewart's installer](https://github.com/manateelazycat/rime-ice-installer), and switches the system language to Chinese from the next login. Rime comes up active by default, `Ctrl+Space` toggles back to plain English input, and the Omarchy menu itself puts on Chinese labels. + ### Use ALT as SUPER On some keyboards, it's not convenient to use the primary meta key (Windows/cmd key) as SUPER. You can change this to be ALT instead using this change: diff --git a/shell/plugins/menu/MenuModel.js b/shell/plugins/menu/MenuModel.js index 28c995ea..f22399d2 100644 --- a/shell/plugins/menu/MenuModel.js +++ b/shell/plugins/menu/MenuModel.js @@ -58,7 +58,9 @@ function parseMenuJsonc(raw) { for (var id in source) { var entry = source[id] if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue - out.push(normalizeItem(id, entry)) + var item = normalizeItem(id, entry) + item.declared = Object.keys(entry) + out.push(item) } return out } @@ -74,11 +76,25 @@ function mergeMenuSources(defaultItems, userItems) { var entry = src[i] if (!entry || !entry.id) continue if (!nextItems[entry.id]) nextOrder.push(entry.id) - var prior = nextItems[entry.id] || {} + var prior = nextItems[entry.id] var merged = {} - for (var k in prior) merged[k] = prior[k] - for (var k2 in entry) merged[k2] = entry[k2] + // Overriding an existing id only replaces the fields the file declared, + // so a label-only entry retitles a row without clearing its action. + if (prior && entry.declared) { + for (var k in prior) merged[k] = prior[k] + for (var d = 0; d < entry.declared.length; d++) { + var key = entry.declared[d] + if (key in entry) merged[key] = entry[key] + } + if (entry.declared.indexOf("action") >= 0 || entry.declared.indexOf("target") >= 0) + merged.kind = merged.action ? "action" : (merged.target ? "link" : "menu") + } else { + prior = prior || {} + for (var k1 in prior) merged[k1] = prior[k1] + for (var k2 in entry) merged[k2] = entry[k2] + } merged.id = entry.id + delete merged.declared nextItems[entry.id] = merged } } @@ -398,7 +414,8 @@ var GUARD_READERS = [ "omarchy-default-browser", "omarchy-default-editor", "omarchy-default-terminal", - "omarchy-dns" + "omarchy-dns", + "omarchy-region" ] // Package and command presence account for most of what the guards ask, and diff --git a/test/shell.d/menu-test.sh b/test/shell.d/menu-test.sh index b3394678..165d8572 100644 --- a/test/shell.d/menu-test.sh +++ b/test/shell.d/menu-test.sh @@ -44,9 +44,10 @@ assertDeepEqual( aliases: ['theme'], when: '', checked: '', - disabled: '' + disabled: '', + declared: ['label', 'aliases', 'description', 'action'] }, - 'menu normalizes parsed items' + 'menu normalizes parsed items and records what the file declared' ) const user = [ @@ -58,6 +59,16 @@ assertEqual(merged.items['style.theme'].label, 'Theme picker', 'menu user entrie assertEqual(merged.items['style.theme'].order, 2, 'menu preserves original order on override') assert(merged.items.root, 'menu injects root when merging sources') +const retitled = menu.mergeMenuSources( + menu.parseMenuJsonc('{"apps": {"icon":"A","label":"Apps","provider":"apps"}, "apps.go": {"label":"Go","action":"run-go"}}'), + menu.parseMenuJsonc('{"apps": {"label":"应用"}, "apps.go": {"label":"出发"}}') +) +assertEqual(retitled.items['apps'].provider, 'apps', 'menu keeps the provider when a user entry only retitles') +assertEqual(retitled.items['apps'].icon, 'A', 'menu keeps the icon when a user entry only retitles') +assertEqual(retitled.items['apps.go'].action, 'run-go', 'menu keeps the action when a user entry only retitles') +assertEqual(retitled.items['apps.go'].kind, 'action', 'menu keeps the kind when a user entry only retitles') +assertEqual(retitled.items['apps.go'].label, '出发', 'menu applies the user retitle') + assertEqual(menu.slugify('Power Saver!'), 'power-saver', 'menu slugifies provider rows') assertEqual(menu.pathFor(merged.items, 'style.theme'), 'Style › Theme picker', 'menu builds item paths') assertEqual(menu.parentPathFor(merged.items, 'style.theme'), 'Style', 'menu builds parent paths') @@ -261,6 +272,22 @@ assert( 'menu always exposes every supported browser, terminal, and editor under Defaults' ) assert(!defaultById['install.ai.crush'], 'menu removes Crush from Install > AI') +const regionEntries = defaultItems.filter(item => item.parent === 'setup.region') +assert( + regionEntries.map(item => item.label).join('\0') === 'World\0China' + && regionEntries.every(item => item.checked.includes(`== \"${item.label}\"`) && !item.when) + && defaultById['setup.region.world'].action.includes('omarchy-region world') + && defaultById['setup.region.china'].action.includes('omarchy-region china'), + 'menu offers World and China under Setup > Region' +) +const zhOverlay = fs.readFileSync(path.join(root, 'default/omarchy/omarchy-menu.zh-cn.jsonc'), 'utf8') +const zhItems = menu.parseMenuJsonc(zhOverlay) +assert(zhItems.length > 100, 'Chinese menu overlay parses') +assert(zhItems.every(item => defaultById[item.id]), 'Chinese menu overlay only retitles shipped entries') +assert( + !/"(action|target|icon|when|checked|disabled|provider|aliases)"/.test(zhOverlay), + 'Chinese menu overlay declares labels and titles only' +) // 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, so diff --git a/test/shell.d/region-test.sh b/test/shell.d/region-test.sh new file mode 100644 index 00000000..4cff687e --- /dev/null +++ b/test/shell.d/region-test.sh @@ -0,0 +1,105 @@ +#!/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 + +mock_bin="$test_tmp/bin" +test_home="$test_tmp/home" +pkg_log="$test_tmp/pkg-add" +installer_log="$test_tmp/installer" +sudo_log="$test_tmp/sudo" +systemctl_log="$test_tmp/systemctl" +mkdir -p "$mock_bin" "$test_home" + +cat >"$mock_bin/omarchy-pkg-add" <<'SH' +#!/bin/bash +printf '%s\n' "$*" >>"$OMARCHY_TEST_PKG_LOG" +SH + +cat >"$mock_bin/sudo" <<'SH' +#!/bin/bash +printf '%s\n' "$*" >>"$OMARCHY_TEST_SUDO_LOG" +SH + +cat >"$mock_bin/systemctl" <<'SH' +#!/bin/bash +printf '%s\n' "$*" >>"$OMARCHY_TEST_SYSTEMCTL_LOG" +SH + +cat >"$mock_bin/rime-ice-installer" <<'SH' +#!/bin/bash +printf 'ran\n' >>"$OMARCHY_TEST_INSTALLER_LOG" +mkdir -p "$HOME/.local/share/fcitx5/rime" +touch "$HOME/.local/share/fcitx5/rime/rime_ice.schema.yaml" +SH + +chmod +x "$mock_bin"/* + +export HOME="$test_home" +export PATH="$mock_bin:$ROOT/bin:$PATH" +export OMARCHY_TEST_PKG_LOG="$pkg_log" +export OMARCHY_TEST_INSTALLER_LOG="$installer_log" +export OMARCHY_TEST_SUDO_LOG="$sudo_log" +export OMARCHY_TEST_SYSTEMCTL_LOG="$systemctl_log" +export OMARCHY_PATH="$ROOT" + +[[ $(omarchy-region) == "World" ]] || fail "region defaults to World before any choice" +pass "region defaults to World before any choice" + +mkdir -p "$test_home/.config/omarchy/extensions" +cp "$ROOT/config/omarchy/extensions/omarchy-menu.jsonc" "$test_home/.config/omarchy/extensions/omarchy-menu.jsonc" + +omarchy-region china >/dev/null +cmp -s "$ROOT/default/omarchy/omarchy-menu.zh-cn.jsonc" "$test_home/.config/omarchy/extensions/omarchy-menu.jsonc" || + fail "china installs the Chinese menu labels over the stock sample" +grep -q "locale-gen" "$sudo_log" || fail "china generates the Chinese locale" +grep -Fx "localectl set-locale LANG=zh_CN.UTF-8" "$sudo_log" >/dev/null || fail "china sets the system language" +grep -Fx "pacman -Sy" "$sudo_log" >/dev/null || fail "china syncs the repo databases before installing" +grep -Fx "rime-ice-installer" "$pkg_log" >/dev/null || fail "china installs the Rime Ice installer package" +(( $(wc -l <"$installer_log") == 1 )) || fail "china runs the Rime Ice installer" +grep -A1 '^\[Groups/0/Items/0\]$' "$test_home/.config/fcitx5/profile" | grep -qFx "Name=rime" || + fail "china makes Rime the first input method" +grep -qFx "DefaultIM=rime" "$test_home/.config/fcitx5/profile" || fail "china makes Rime the group default" +grep -qFx "0=Control+space" "$test_home/.config/fcitx5/config" || fail "china pins the Ctrl+Space toggle" +grep -Fx -- "--user restart omarchy-fcitx5.service" "$systemctl_log" >/dev/null || + fail "china restarts fcitx5 with the new profile" +[[ $(omarchy-region) == "China" ]] || fail "china is stored as the region" +pass "china sets up the language, menu and input method" + +omarchy-region china >/dev/null +(( $(wc -l <"$installer_log") == 1 )) || fail "reapplying china skips the installed input method" +(( $(grep -cFx "pacman -Sy" "$sudo_log") == 1 )) || fail "reapplying china skips the database sync too" +pass "reapplying china is idempotent and leaves the input method alone" + +omarchy-region world >/dev/null +cmp -s "$ROOT/config/omarchy/extensions/omarchy-menu.jsonc" "$test_home/.config/omarchy/extensions/omarchy-menu.jsonc" || + fail "world restores the sample menu extension" +[[ $(omarchy-region) == "World" ]] || fail "world is stored as the region" +[[ -f $test_home/.local/share/fcitx5/rime/rime_ice.schema.yaml ]] || fail "world leaves the input method installed" +grep -qFx "DefaultIM=rime" "$test_home/.config/fcitx5/profile" || fail "world leaves the input method configured" +pass "world restores the menu and keeps the input method" + +printf '{"apps": {"label":"Mine"}}\n' >"$test_home/.config/omarchy/extensions/omarchy-menu.jsonc" +if omarchy-region china >/dev/null 2>"$test_tmp/refusal"; then + fail "china refuses a menu extension someone wrote by hand" +fi +grep -q "menu:" "$test_tmp/refusal" || fail "china names the menu extension it refuses to touch" +[[ $(omarchy-region) == "World" ]] || fail "a refused china run keeps the region" +pass "china refuses a hand-written menu extension" +grep -Fx '{"apps": {"label":"Mine"}}' "$test_home/.config/omarchy/extensions/omarchy-menu.jsonc" >/dev/null || + fail "a refused china run leaves the extension untouched" + +omarchy-region world >/dev/null +grep -Fx '{"apps": {"label":"Mine"}}' "$test_home/.config/omarchy/extensions/omarchy-menu.jsonc" >/dev/null || + fail "world leaves a menu extension someone wrote by hand" +pass "world only restores the extension Omarchy wrote" + +if omarchy-region china unexpected >/dev/null 2>&1; then + fail "region rejects extra arguments" +fi +[[ $(omarchy-region) == "World" ]] || fail "extra arguments change nothing" +pass "region rejects extra arguments"