diff --git a/bin/omarchy-region b/bin/omarchy-region index fad6bf33..f3006f40 100755 --- a/bin/omarchy-region +++ b/bin/omarchy-region @@ -1,16 +1,51 @@ #!/bin/bash -# omarchy:summary=Show the region this machine is set up for +# omarchy:summary=Show or switch the region this machine is set up for +# omarchy:args=[global|china] +# omarchy:examples=omarchy region | omarchy region china +# omarchy:requires-sudo=true set -euo pipefail region_file=/etc/omarchy/region -if [[ ! -e $region_file ]]; then - echo "Global" -elif [[ $(<"$region_file") == "china" ]]; then - echo "China" -else - echo "Unknown region in $region_file: $(<"$region_file")" >&2 +if (($# == 0)); then + if [[ ! -e $region_file ]]; then + echo "Global" + elif [[ $(<"$region_file") == "china" ]]; then + echo "China" + else + echo "Unknown region in $region_file: $(<"$region_file")" >&2 + exit 1 + fi + exit 0 +fi + +if [[ $1 != "global" && $1 != "china" ]]; then + echo "Usage: omarchy-region [global|china]" >&2 exit 1 fi + +pacman_files=(/etc/pacman.conf /etc/pacman.d/mirrorlist) + +if [[ $1 == "china" ]]; then + for file in "${pacman_files[@]}"; do + if ! grep -q '^Server = https://[a-z-]*\.omarchy\.cn/' "$file"; then + sudo sed -i -f "$OMARCHY_PATH/default/region/china/pacman.sed" "$file" + fi + done + echo "Package mirrors -> .cn first, .org after" + sudo pacman -Sy + sudo env OMARCHY_REGION=china OMARCHY_PATH="$OMARCHY_PATH" bash -e "$OMARCHY_PATH/install/config/region.sh" + if [[ ! -f $HOME/.local/share/fcitx5/rime/rime_ice.schema.yaml ]]; then + rime-ice-installer --yes + fi +else + sudo sed -i '/^Server = https:\/\/[a-z-]*\.omarchy\.cn\//d' "${pacman_files[@]}" + echo "Package mirrors -> .org only" + sudo pacman -Sy + sudo env OMARCHY_REGION=global OMARCHY_PATH="$OMARCHY_PATH" bash -e "$OMARCHY_PATH/install/config/region.sh" + omarchy-setup-claude-api --remove +fi + +echo "Region set to ${1^}. Log out and back in to switch the language and developer mirrors." diff --git a/default/omarchy/omarchy-menu.jsonc b/default/omarchy/omarchy-menu.jsonc index d2ba79b7..bbe9a1b2 100644 --- a/default/omarchy/omarchy-menu.jsonc +++ b/default/omarchy/omarchy-menu.jsonc @@ -176,6 +176,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.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 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..4e459bba --- /dev/null +++ b/default/omarchy/omarchy-menu.zh-cn.jsonc @@ -0,0 +1,180 @@ +// Chinese menu labels, overlaid on omarchy-menu.jsonc when LANG is zh_CN. +{ + "apps": {"label":"应用"}, + "learn": {"label":"学习"}, + "trigger": {"label":"触发"}, + "style": {"label":"风格"}, + "setup": {"label":"设置"}, + "install": {"label":"安装"}, + "remove": {"label":"移除"}, + "update": {"label":"更新"}, + "about": {"label":"关于"}, + "system": {"label":"系统"}, + "system.screensaver": {"label":"屏保"}, + "system.lock": {"label":"锁屏"}, + "system.suspend": {"label":"睡眠"}, + "system.hibernate": {"label":"休眠"}, + "system.logout": {"label":"注销"}, + "system.reboot": {"label":"重启"}, + "system.shutdown": {"label":"关机"}, + "learn.keybindings": {"label":"快捷键"}, + "learn.omarchy": {"label":"Omarchy 手册"}, + "learn.community": {"label":"社区"}, + "trigger.emoji": {"label":"表情"}, + "trigger.reminder": {"label":"提醒"}, + "trigger.capture": {"label":"截取"}, + "trigger.capture.screenshot": {"label":"截图"}, + "trigger.capture.screenrecord.stop": {"label":"停止录屏"}, + "trigger.capture.screenrecord": {"label":"录屏"}, + "trigger.capture.text": {"label":"OCR 取字"}, + "trigger.capture.qr": {"label":"二维码"}, + "trigger.capture.color": {"label":"取色"}, + "trigger.capture.screenrecord.no-audio": {"label":"无音频"}, + "trigger.capture.screenrecord.desktop-audio": {"label":"含桌面音频"}, + "trigger.capture.screenrecord.microphone": {"label":"含桌面与麦克风音频"}, + "trigger.capture.screenrecord.webcam": {"label":"含桌面、麦克风与摄像头"}, + "trigger.transcode": {"label":"转码"}, + "trigger.share": {"label":"分享"}, + "trigger.toggle": {"label":"开关"}, + "trigger.hardware": {"label":"硬件"}, + "trigger.tests": {"label":"测速"}, + "trigger.hardware.laptop-display": {"label":"笔记本屏幕"}, + "trigger.hardware.mirror-display": {"label":"镜像显示"}, + "trigger.hardware.hybrid-gpu": {"label":"混合显卡"}, + "trigger.hardware.touchpad": {"label":"触控板"}, + "trigger.hardware.touchpad-haptics": {"label":"触控板振动"}, + "trigger.hardware.touchpad-haptics.low": {"label":"低"}, + "trigger.hardware.touchpad-haptics.mid": {"label":"中"}, + "trigger.hardware.touchpad-haptics.high": {"label":"高"}, + "trigger.hardware.touchscreen": {"label":"触摸屏"}, + "trigger.reminder.set": {"label":"设一个提醒"}, + "trigger.reminder.show": {"label":"查看全部"}, + "trigger.reminder.clear": {"label":"清除全部"}, + "trigger.share.clipboard": {"label":"剪贴板"}, + "trigger.share.file": {"label":"文件"}, + "trigger.share.folder": {"label":"文件夹"}, + "trigger.share.receive": {"label":"接收"}, + "trigger.toggle.idle-lock": {"label":"保持唤醒"}, + "trigger.toggle.notifications": {"label":"通知"}, + "trigger.toggle.crash-capture": {"label":"崩溃捕获"}, + "trigger.toggle.screensaver": {"label":"屏保"}, + "trigger.toggle.nightlight": {"label":"夜间模式"}, + "trigger.toggle.top-bar": {"label":"菜单栏"}, + "trigger.toggle.battery-percentage": {"label":"电量百分比"}, + "trigger.toggle.workspace-layout": {"label":"工作区布局"}, + "trigger.toggle.window-gaps": {"label":"窗口间距"}, + "trigger.toggle.one-window-ratio": {"label":"单窗口比例"}, + "trigger.tests.network-speedtest": {"label":"网络测速"}, + "trigger.tests.disk-speedtest": {"label":"磁盘测速"}, + "style.theme": {"label":"主题"}, + "style.background": {"label":"壁纸"}, + "style.unlock": {"label":"解锁画面"}, + "style.font": {"label":"字体"}, + "style.bar": {"label":"菜单栏"}, + "style.bar.position": {"label":"位置"}, + "style.bar.transparency": {"label":"透明"}, + "style.screensaver": {"label":"屏保"}, + "style.about": {"label":"关于"}, + "style.bar.position.top": {"label":"顶部"}, + "style.bar.position.bottom": {"label":"底部"}, + "style.bar.position.left": {"label":"左侧"}, + "style.bar.position.right": {"label":"右侧"}, + "style.about.text": {"label":"编辑文字"}, + "style.about.image": {"label":"取自图片"}, + "style.about.default": {"label":"恢复默认"}, + "style.screensaver.text": {"label":"编辑文字"}, + "style.screensaver.image": {"label":"取自图片"}, + "style.screensaver.default": {"label":"恢复默认"}, + "setup.monitors": {"label":"显示器"}, + "setup.keybindings": {"label":"快捷键"}, + "setup.input": {"label":"输入设备"}, + "setup.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.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":"用户"}, + "trigger.toggle.animations": {"label":"动画"}, + "trigger.toggle.theme-sync": {"label":"Herdr 主题同步"}, + "setup.region": {"label":"地区"}, + "setup.region.global": {"label":"全球"}, + "setup.region.china": {"label":"中国"}, + "install.china": {"label":"国内应用"}, + "install.china.wechat": {"label":"微信"}, + "install.china.feishu": {"label":"飞书"}, + "install.china.dingtalk": {"label":"钉钉"}, + "install.china.wemeet": {"label":"腾讯会议"}, + "install.china.tencent-docs": {"label":"腾讯文档"}, + "install.china.yuque": {"label":"语雀"}, + "install.china.shimo": {"label":"石墨文档"}, + "update.channel.stable": {"label":"稳定版"}, + "update.channel.rc": {"label":"候选版"}, + "update.channel.edge": {"label":"尝鲜版"}, + "update.channel.dev": {"label":"开发版"}, +} diff --git a/docs/menu.md b/docs/menu.md index 480d128c..fc3593a2 100644 --- a/docs/menu.md +++ b/docs/menu.md @@ -56,6 +56,8 @@ or re-icon a row without re-declaring its action, and an overridden entry keeps its original position in the list. New ids append. A `root` entry is injected if neither file declares one. +When `LANG` starts with `zh_CN`, `default/omarchy/omarchy-menu.zh-cn.jsonc` is merged between the defaults and the user extension. It only retitles shipped ids, so a user extension still wins, and the language follows `/etc/locale.conf` instead of any file `omarchy update` rewrites. + The sample extension at `config/omarchy/extensions/omarchy-menu.jsonc` (refreshed into `~/.config/`) documents the format in its header and ships only comments, so the default state adds nothing. diff --git a/install/config/region.sh b/install/config/region.sh index 65bfefec..b5ddeb99 100644 --- a/install/config/region.sh +++ b/install/config/region.sh @@ -7,6 +7,7 @@ china_links=( case ${OMARCHY_REGION:-global} in global) rm -f /etc/omarchy/region + sed -i 's/^LANG=zh_CN\.UTF-8$/LANG=en_US.UTF-8/' /etc/locale.conf for link in "${china_links[@]}"; do read -r _ target <<<"$link" if [[ -L $target && $(readlink "$target") == */default/region/china/* ]]; then @@ -27,6 +28,13 @@ china) ln -sfn "$OMARCHY_PATH/default/region/china/$source" "$target" done omarchy-pkg-add fcitx5-configtool fcitx5-rime librime opencc rime-ice-installer + sed -i 's/^#\(zh_CN\.UTF-8 UTF-8\)/\1/' /etc/locale.gen + locale-gen + if grep -q '^LANG=' /etc/locale.conf; then + sed -i 's/^LANG=.*/LANG=zh_CN.UTF-8/' /etc/locale.conf + else + echo LANG=zh_CN.UTF-8 >>/etc/locale.conf + fi ;; *) echo "Unknown OMARCHY_REGION: $OMARCHY_REGION" >&2 diff --git a/plans/omarchycn.md b/plans/omarchycn.md index 017e5ee7..b8d01fb5 100644 --- a/plans/omarchycn.md +++ b/plans/omarchycn.md @@ -20,7 +20,7 @@ Progress source of truth for the `omarchycn` branches of `omarchy`, `omarchy-iso - [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/` -- [ ] T7 China software (PRD 7): Install menu entry shown only in China +- [x] T7 China software (PRD 7): Install menu entry shown only in China - [ ] T8 Region switching and Chinese UI (PRD 8): `omarchy-region china|global`, Setup > Region, locale, Chinese menu overlay chosen by `LANG` - [ ] T9 Logo (PRD 9): stock Omarchy logo everywhere - [ ] T10 Docs: `AGENTS.md`, manual diff --git a/shell/plugins/menu/Menu.qml b/shell/plugins/menu/Menu.qml index 1625375e..5f614c64 100644 --- a/shell/plugins/menu/Menu.qml +++ b/shell/plugins/menu/Menu.qml @@ -37,6 +37,7 @@ Item { function refresh() { defaultMenuFile.reload() + localeMenuFile.reload() userMenuFile.reload() return "ok" } @@ -49,7 +50,9 @@ Item { // path doesn't have to shell out to bash + jq on every open. property string defaultMenuPath: omarchyPath + "/default/omarchy/omarchy-menu.jsonc" property string userMenuPath: Quickshell.env("HOME") + "/.config/omarchy/extensions/omarchy-menu.jsonc" + property string localeMenuPath: (Quickshell.env("LANG") || "").startsWith("zh_CN") ? omarchyPath + "/default/omarchy/omarchy-menu.zh-cn.jsonc" : "" property var defaultMenuItems: [] + property var localeMenuItems: [] property var userMenuItems: [] property bool opened: false property string mode: "menu" @@ -247,7 +250,7 @@ Item { // on a per-key basis (so the user can tweak label/icon/action without // re-declaring the whole row). function rebuildItemsFromSources() { - var mergedMenu = MenuModel.mergeMenuSources(root.defaultMenuItems, root.userMenuItems) + var mergedMenu = MenuModel.mergeMenuSources(root.defaultMenuItems, root.localeMenuItems.concat(root.userMenuItems)) root.providerRevision += 1 root.providersLoaded = ({}) root.providerQueue = [] @@ -974,6 +977,16 @@ Item { onFileChanged: reload() } + FileView { + id: localeMenuFile + path: root.localeMenuPath + watchChanges: true + printErrors: false + onLoaded: { root.localeMenuItems = root.parseMenuJsonc(text()); root.rebuildItemsFromSources() } + onLoadFailed: { root.localeMenuItems = []; root.rebuildItemsFromSources() } + onFileChanged: reload() + } + FileView { id: userMenuFile path: root.userMenuPath diff --git a/shell/plugins/menu/MenuModel.js b/shell/plugins/menu/MenuModel.js index b5b22ac4..db871242 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,24 @@ 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] + // An override replaces only the fields its file declared. + 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 } } diff --git a/test/shell.d/menu-locale-test.sh b/test/shell.d/menu-locale-test.sh new file mode 100755 index 00000000..a5cbc653 --- /dev/null +++ b/test/shell.d/menu-locale-test.sh @@ -0,0 +1,44 @@ +#!/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 read = name => model.parseMenuJsonc(fs.readFileSync(`${root}/default/omarchy/${name}`, 'utf8')) +const fail = message => { console.error(`not ok - ${message}`); process.exit(1) } + +const defaults = read('omarchy-menu.jsonc') +const chinese = read('omarchy-menu.zh-cn.jsonc') +const byId = Object.fromEntries(defaults.map(item => [item.id, item])) + +const unknown = chinese.filter(item => !byId[item.id]).map(item => item.id) +if (unknown.length) fail(`every Chinese label names a shipped menu id: ${unknown}`) +const extra = chinese.filter(item => item.declared.some(key => key !== 'label' && key !== 'title')).map(item => item.id) +if (extra.length) fail(`the Chinese overlay only retitles rows: ${extra}`) +console.log('ok - the Chinese overlay only retitles shipped menu rows') + +const user = model.parseMenuJsonc('{"system.lock": {"label": "Mine"}}') +const merged = model.mergeMenuSources(defaults, chinese.concat(user)).items +for (const item of defaults) { + const result = merged[item.id] + const zh = chinese.find(entry => entry.id === item.id) + for (const key of ['action', 'when', 'checked', 'disabled', 'target', 'provider', 'kind', 'icon', 'parent']) { + if (JSON.stringify(result[key]) !== JSON.stringify(item[key])) fail(`${item.id} keeps its ${key} under the Chinese overlay`) + } + 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['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 + +grep -qF 'root.localeMenuItems.concat(root.userMenuItems)' "$ROOT/shell/plugins/menu/Menu.qml" && + grep -qF 'startsWith("zh_CN") ? omarchyPath + "/default/omarchy/omarchy-menu.zh-cn.jsonc"' "$ROOT/shell/plugins/menu/Menu.qml" || + fail "the menu loads the Chinese overlay under zh_CN, beneath the user extension" +pass "the menu loads the Chinese overlay under zh_CN, beneath the user extension" diff --git a/test/shell.d/menu-test.sh b/test/shell.d/menu-test.sh index c87b1a1e..f2bcbd6d 100644 --- a/test/shell.d/menu-test.sh +++ b/test/shell.d/menu-test.sh @@ -62,9 +62,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 = [ @@ -76,6 +77,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') @@ -286,6 +297,14 @@ 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') === '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 china'), + '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, so diff --git a/test/shell.d/region-switch-test.sh b/test/shell.d/region-switch-test.sh new file mode 100755 index 00000000..a58f4c38 --- /dev/null +++ b/test/shell.d/region-switch-test.sh @@ -0,0 +1,60 @@ +#!/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/region" "$test_tmp/bin" "$test_tmp/home" +cp -r "$ROOT/default/region/china" "$sandbox/default/region/" +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 pacman omarchy-pkg-add locale-gen rime-ice-installer omarchy-setup-claude-api; do + printf '#!/bin/bash\necho "%s $*" >>"%s"\n' "$command" "$calls" >"$test_tmp/bin/$command" +done +printf '#!/bin/bash\n[[ $1 == env ]] && shift\nexec env "$@"\n' >"$test_tmp/bin/sudo" +chmod +x "$test_tmp/bin"/* +export PATH="$test_tmp/bin:$sandbox/bin:$PATH" OMARCHY_PATH="$sandbox" HOME="$test_tmp/home" + +cp "$ROOT/default/pacman/pacman-stable.conf" "$root/etc/pacman.conf" +cp "$ROOT/default/pacman/mirrorlist-stable" "$root/etc/pacman.d/mirrorlist" +printf '#zh_CN.UTF-8 UTF-8\nen_US.UTF-8 UTF-8\n' >"$root/etc/locale.gen" +printf 'LANG=en_US.UTF-8\n' >"$root/etc/locale.conf" + +china_conf=$(sed -f "$ROOT/default/region/china/pacman.sed" "$ROOT/default/pacman/pacman-stable.conf") +china_mirrors=$(sed -f "$ROOT/default/region/china/pacman.sed" "$ROOT/default/pacman/mirrorlist-stable") + +omarchy-region china >/dev/null +[[ $(<"$root/etc/pacman.conf") == "$china_conf" && $(<"$root/etc/pacman.d/mirrorlist") == "$china_mirrors" ]] || + fail "switching to China lists the .cn mirrors first" +[[ $(omarchy-region) == "China" && $(<"$root/etc/locale.conf") == "LANG=zh_CN.UTF-8" ]] || fail "switching to China records it and sets the language" +[[ $(<"$calls") == $'pacman -Sy\nomarchy-pkg-add fcitx5-configtool fcitx5-rime librime opencc rime-ice-installer\nlocale-gen \nrime-ice-installer --yes' ]] || + fail "China syncs the .cn databases before installing Rime, then sets Rime Ice up" "$(<"$calls")" +pass "switching to China sets mirrors, language and Rime Ice" + +: >"$calls" +mkdir -p "$HOME/.local/share/fcitx5/rime" && touch "$HOME/.local/share/fcitx5/rime/rime_ice.schema.yaml" +omarchy-region china >/dev/null +[[ $(<"$root/etc/pacman.conf") == "$china_conf" && $(<"$root/etc/pacman.d/mirrorlist") == "$china_mirrors" ]] || + fail "switching to China twice adds no second .cn line" +grep -q '^rime-ice-installer' "$calls" && fail "an existing Rime Ice setup is not redone" +pass "switching to China again changes nothing it already did" + +: >"$calls" +omarchy-region global >/dev/null +[[ $(<"$root/etc/pacman.conf") == "$(<"$ROOT/default/pacman/pacman-stable.conf")" && $(<"$root/etc/pacman.d/mirrorlist") == "$(<"$ROOT/default/pacman/mirrorlist-stable")" ]] || + fail "switching to Global drops exactly the .cn mirrors" +[[ $(omarchy-region) == "Global" && $(<"$root/etc/locale.conf") == "LANG=en_US.UTF-8" ]] || fail "switching to Global records it and restores English" +[[ $(<"$calls") == $'pacman -Sy\nomarchy-setup-claude-api --remove' ]] || fail "Global removes the Claude Code third-party API and keeps Rime" "$(<"$calls")" +[[ -f $HOME/.local/share/fcitx5/rime/rime_ice.schema.yaml ]] || fail "Global keeps the input method" +pass "switching to Global restores mirrors and language, keeps Rime, removes the Claude API setup" + +if output=$(omarchy-region mars 2>&1); then + fail "an unknown region is refused" "$output" +fi +pass "an unknown region is refused" diff --git a/test/shell.d/region-test.sh b/test/shell.d/region-test.sh index e251ae94..1b4f785f 100755 --- a/test/shell.d/region-test.sh +++ b/test/shell.d/region-test.sh @@ -58,7 +58,7 @@ pass "an unknown region fails and names the file and value" stub_bin="$test_tmp/bin" calls="$test_tmp/calls" mkdir -p "$stub_bin" -for command in omarchy-pkg-add rime-ice-installer; do +for command in omarchy-pkg-add rime-ice-installer locale-gen; do printf '#!/bin/bash\necho "%s $*" >>"%s"\n' "$command" "$calls" >"$stub_bin/$command" done printf '#!/bin/bash\necho "$STUB_REGION"\n' >"$stub_bin/omarchy-region" @@ -69,12 +69,17 @@ 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" +printf '#zh_CN.UTF-8 UTF-8\nen_US.UTF-8 UTF-8\n' >"$root/etc/locale.gen" +printf 'LANG=en_US.UTF-8\nLC_TIME=en_DK.UTF-8\n' >"$root/etc/locale.conf" : >"$calls" OMARCHY_REGION=china bash -e "$region_setup" [[ $(<"$root/etc/omarchy/region") == "china" ]] || fail "install records the China region" -[[ $(<"$calls") == "omarchy-pkg-add fcitx5-configtool fcitx5-rime librime opencc rime-ice-installer" ]] || - fail "China installs the Rime input method packages" "$(<"$calls")" +[[ $(<"$calls") == $'omarchy-pkg-add fcitx5-configtool fcitx5-rime librime opencc rime-ice-installer\nlocale-gen ' ]] || + fail "China installs the Rime input method packages and generates the Chinese locale" "$(<"$calls")" +[[ $(<"$root/etc/locale.gen") == $'zh_CN.UTF-8 UTF-8\nen_US.UTF-8 UTF-8' ]] || fail "China enables zh_CN in locale.gen" +[[ $(<"$root/etc/locale.conf") == $'LANG=zh_CN.UTF-8\nLC_TIME=en_DK.UTF-8' ]] || fail "China sets LANG and keeps other locale settings" "$(<"$root/etc/locale.conf")" for package in fcitx5-configtool fcitx5-rime librime opencc rime-ice-installer; do grep -qx "$package" "$ROOT/install/omarchy-other.packages" || fail "the ISO mirror carries $package" done @@ -90,6 +95,12 @@ pass "China links the developer mirror drop-ins" OMARCHY_REGION=global bash -e "$region_setup" [[ ! -e $root/etc/omarchy/region ]] || fail "install records Global by leaving no region file" [[ ! -s $calls ]] || fail "Global installs no input method packages" "$(<"$calls")" +[[ $(<"$root/etc/locale.conf") == $'LANG=en_US.UTF-8\nLC_TIME=en_DK.UTF-8' ]] || fail "Global turns a Chinese LANG back to English" "$(<"$root/etc/locale.conf")" +printf 'LANG=de_DE.UTF-8\n' >"$root/etc/locale.conf" +OMARCHY_REGION=global bash -e "$region_setup" +[[ $(<"$root/etc/locale.conf") == "LANG=de_DE.UTF-8" ]] || fail "Global leaves a language China did not set" +printf 'LANG=en_US.UTF-8\n' >"$root/etc/locale.conf" +pass "the region sets the language and Global only undoes Chinese" [[ ! -e $root/etc/environment.d/90-omarchy-china.conf && ! -e $root/.cargo/config.toml && ! -e $root/etc/gemrc ]] || fail "Global removes the developer mirror drop-ins" pass "Global removes the developer mirror drop-ins"