Compare commits
@@ -0,0 +1,24 @@
|
||||
name: pkg-repo-sync
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "17 */6 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: quattro
|
||||
|
||||
- name: Mirror upstream [omarchy] stable into the Gitea Arch registry
|
||||
env:
|
||||
PKG_SYNC_TOKEN: ${{ secrets.PKG_SYNC_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if ! command -v zstd > /dev/null; then
|
||||
apt-get update -qq && apt-get install -y -qq zstd
|
||||
fi
|
||||
bash packages/sync-omarchy-repo.sh
|
||||
@@ -0,0 +1,50 @@
|
||||
name: upstream-sync
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 3 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: quattro
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Fetch upstream and open sync PR
|
||||
env:
|
||||
TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
API: ${{ github.server_url }}/api/v1
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git config user.name "omarchycn-sync"
|
||||
git config user.email "sync@noreply.git.zacharyzhang.com"
|
||||
git remote add upstream https://github.com/basecamp/omarchy.git
|
||||
git fetch upstream quattro
|
||||
UP=$(git rev-parse upstream/quattro)
|
||||
echo "upstream quattro: $UP"
|
||||
if git merge-base --is-ancestor "$UP" HEAD; then
|
||||
echo "Already up to date with upstream"
|
||||
exit 0
|
||||
fi
|
||||
BR="sync/upstream-${UP:0:8}"
|
||||
if git ls-remote --exit-code --heads origin "$BR" > /dev/null; then
|
||||
echo "Sync branch $BR already exists, PR pending review"
|
||||
exit 0
|
||||
fi
|
||||
# Trial merge only to report conflict status in the PR body
|
||||
MERGE="clean"
|
||||
if ! git merge --no-commit --no-ff "upstream/quattro" > /dev/null 2>&1; then
|
||||
MERGE="CONFLICTS (resolve manually)"
|
||||
fi
|
||||
git merge --abort 2> /dev/null || true
|
||||
# Branch points at upstream HEAD so the PR always gets created
|
||||
git push origin "$UP:refs/heads/$BR"
|
||||
curl -sS --fail-with-body -X POST \
|
||||
-H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
|
||||
"$API/repos/$REPO/pulls" \
|
||||
-d "{\"base\":\"quattro\",\"head\":\"$BR\",\"title\":\"Sync upstream omarchy ${UP:0:8}\",\"body\":\"Automated sync of basecamp/omarchy quattro @ $UP. Trial merge: $MERGE. Review, run ./test/all, then merge.\"}"
|
||||
@@ -1,2 +0,0 @@
|
||||
# Merges to protected branches need sign-off from an org owner.
|
||||
* @dhh @ryanrhughes
|
||||
@@ -1,47 +0,0 @@
|
||||
# Security at Omarchy
|
||||
|
||||
## Report a vulnerability
|
||||
|
||||
If you believe you’ve found a security vulnerability in Omarchy, please tell the [Omarchy Security Team](https://omarchy.org/teams/#security) privately so we have an opportunity to investigate and fix it before it is made public.
|
||||
|
||||
[security@omarchy.org](mailto:security@omarchy.org?subject=Security%20report)
|
||||
|
||||
Please don’t report potential vulnerabilities publicly in GitHub Issues, Discord, or social media before they’ve been resolved.
|
||||
|
||||
## What is a vulnerability?
|
||||
|
||||
We consider a bug a security vulnerability when it can be exploited to cross a meaningful security boundary: an untrusted or lower-privileged party gains access, permissions, or control they didn’t already have.
|
||||
|
||||
Code that could be more robust but does not cross a security boundary is an improvement rather than a security vulnerability. We may still merge a proposed fix and credit the reporter in our release notes.
|
||||
|
||||
Eligibility for our [security credits](https://omarchy.org/security/credits/) page depends on whether a report identifies a confirmed security vulnerability, not on its severity.
|
||||
|
||||
## What to include
|
||||
|
||||
Give us enough information to understand and reproduce the issue:
|
||||
|
||||
- The affected component and Omarchy version.
|
||||
- An explanation of what an attacker can do before and after exploitation.
|
||||
- Steps to reproduce the issue and any proof of concept.
|
||||
- Your preferred contact details for follow-up.
|
||||
|
||||
## Responsible disclosure
|
||||
|
||||
Please act in good faith while investigating and reporting vulnerabilities:
|
||||
|
||||
- Only test systems and accounts you own or have explicit permission to test.
|
||||
- Avoid privacy violations, disruption, data destruction, and service degradation.
|
||||
- Don’t exploit a vulnerability beyond what is needed to demonstrate it.
|
||||
- Give us a reasonable opportunity to investigate and address the issue before publishing details.
|
||||
|
||||
We’ll review your report and keep you informed as we’re able while we work toward a resolution.
|
||||
|
||||
## Credits
|
||||
|
||||
Researchers who privately report a confirmed security vulnerability and give us the chance to ship a fix are thanked on the [security credits](https://omarchy.org/security/credits/) page. Accepted improvements that don’t cross a security boundary may still be credited in our release notes.
|
||||
|
||||
Credits link to each reporter’s X profile and show their avatar. For duplicate reports, only the first reporter is eligible for credit.
|
||||
|
||||
## Regular bugs and support
|
||||
|
||||
For anything that isn’t a security vulnerability, please use the [Omarchy issue tracker](https://github.com/omacom/omarchy/issues).
|
||||
@@ -2,3 +2,7 @@
|
||||
# Python bytecode (orchestrator)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Local working documents
|
||||
OmarchyCN PRD.md
|
||||
OmarchyCN-TASKS.md
|
||||
|
||||
@@ -11,13 +11,32 @@ matching guide before starting:
|
||||
- [`agents/skills/visual-verification.md`](agents/skills/visual-verification.md) - verifying any change with a visual effect in the running UI
|
||||
- [`agents/skills/migrations.md`](agents/skills/migrations.md) - creating or changing migrations under `migrations/`
|
||||
|
||||
# OmarchyCN Layer
|
||||
|
||||
This fork carries the OmarchyCN China-integration layer on top of upstream `basecamp/omarchy`:
|
||||
|
||||
- `bin/omarchycn` routes to `omarchy cn <command>`; all cn commands are `bin/omarchy-cn-*` and follow the upstream bin conventions (metadata, helpers, `$OMARCHY_PATH` — bootstrap exceptions: the overlay installer resolves its own checkout, and `omarchy-cn-convert` / `omarchy-cn-revert` are curl-able standalone scripts that default `OMARCHY_PATH` because they run before/while the cn tree exists)
|
||||
- Convert/revert: `omarchy-cn-convert` turns a vanilla package-based Omarchy into OmarchyCN (registry key + [omarchycn] repo, `--ask=4` package swap to the cn-built omarchy-dev/omarchy-settings-dev published by `packages/publish-cn-packages.sh`, [omarchy] mirror line, zh_CN locale, fcitx5 seeding, plymouth rebrand) recording pre-state in `/var/lib/omarchycn/convert-state`; `omarchy-cn-revert` restores that state; checkout installs are refused toward the overlay
|
||||
- `cn/` holds the data layer: `mirrors.json`, `dev-mirrors.json`, `apps.json`, `registry/` (AI providers/harnesses/compatibility), `fcitx5/`, `fontconfig/`, `keys/`, `lib/` (sourced helpers), `release` (cn release number)
|
||||
- cn migrations live in `cn/migrations/*.sh`, run by `omarchy-cn-migrate` with per-file completion markers under `~/.local/state/omarchycn/`; `bin/omarchy-migrate` calls it after the upstream migrations, and `omarchy-cn-update` serves both install forms (overlay: git pull + reinstall + migrate; package: exec `omarchy-update`)
|
||||
- Packaging: `packages/omarchy-pkgs-cn.patch` must be applied to the sibling `omarchy-pkgs` checkout so `omarchy-dev` ships `cn/`; keyring in `packages/omarchycn-keyring/`
|
||||
- Upstream repo mirror: `packages/sync-omarchy-repo.sh` + `.gitea/workflows/pkg-repo-sync.yml` mirror the upstream `[omarchy]` stable channel into the Gitea Arch registry every 6h; `pacman-stable.conf` lists the mirror first (upstream fallback), clients must trust the registry key (install import + cn migration), `omarchycn doctor mirror --fix` re-heals the line; see `docs/pacman-repo.md`
|
||||
- ISO: `packages/omarchy-iso-cn.patch` must be applied to the sibling `omarchy-iso` checkout — Chinese installer (cage+foot graphical console with English VT fallback) and live-env packages
|
||||
- Release process: `docs/release-checklist.md`; signing: `docs/release-signing.md`; pacman repo: `docs/pacman-repo.md`; ISO distribution: Cloudflare R2 bucket behind `dl.zacharyzhang.com` via `packages/upload-release-r2.sh` (Gitea releases carry only the small artifacts)
|
||||
- Site: `site/` is the omarchycn.zacharyzhang.com landing page (Vite 8 vanilla + GSAP + self-hosted Fusion Pixel + reicon, kami palette on a 12-col grid); deploy with `npx wrangler deploy` from `site/`, rendered-state checks via `node shots.mjs` against a preview or the live URL
|
||||
- Chinese-first defaults: OmarchyCN ships Simplified Chinese as the default UX. User-visible strings in `default/omarchy/omarchy-menu.jsonc` (all labels), `default/hypr/bindings/*.lua` (bind descriptions, including the generated workspace/group/panel loops), `bin/omarchy-menu-keybindings` (its merge list and priority patterns must match the shipped Chinese descriptions), `bin/omarchy-update-confirm`, and `install/user/first-run/*` notifications are Chinese with brand names kept in English; menu search stays reachable in English through leaf ids. When syncing upstream, translate new strings in these files and resolve string conflicts toward our Chinese text.
|
||||
- Default set changes vs upstream: `install/omarchy-base.packages` adds fcitx5-rime/chinese-addons/configtool and drops aether/libreoffice-fresh/obs-studio; the Basecamp/Discord/HEY/Google/WhatsApp/X launchers, their webapp keybindings, the whatsapp-slim extension, and the HEY mailto handler are removed (preinstall add/remove lists, launcher.hides, chromium flags, and mimeapps stay in sync); `install/user/cn-chinese.sh` seeds IME/font config on zh_CN systems
|
||||
- AI: `cn/registry/` drives providers (DeepSeek/Kimi/Z.AI/MiniMax/Ollama-local) with zero-code adapters; Ollama uses a fixed `static_token` and runtime-listed models (`models_dynamic`); `omarchy-default-agent` also accepts kimi (official installer script, not mise), deepcode, dim, and dsh (mise npm); the Default Agent menu carries them plus an AI Hub combo entry
|
||||
- Upstream sync: `.gitea/workflows/upstream-sync.yml` opens a PR per upstream change; keep upstream file edits minimal and inventoried (currently: one `GROUP_DESCRIPTIONS[cn]` line in `bin/omarchy`, the `omarchy-cn-migrate` line in `bin/omarchy-migrate`, the localized menu/bindings/update/first-run surfaces above, the western-app removals above, the cn agent roster in `bin/omarchy-default-agent` / `bin/omarchy-agent` (kimi/deepcode/dim/dsh), the `[omarchy]` mirror line in `default/pacman/pacman-stable.conf`, the registry-key import in `install/post-install/pacman.sh`, a rewritten `README.md` (known recurring sync conflict, resolve toward ours), `AGENTS.md` additions, and two `.gitignore` lines)
|
||||
- cn tests: `test/shell.d/omarchycn-test.sh`, `test/shell.d/omarchycn-ai-test.sh`; localized UI expectations live in the upstream suites (menu, keybindings-menu, hyprland-default-config, binding-conflicts, clock, screenrecording, update-disk-space)
|
||||
|
||||
# Documentation Layout
|
||||
|
||||
Three documentation trees, split by genre and audience:
|
||||
|
||||
- `agents/skills/` - task procedure ("do this when doing X"), for anyone working on the codebase
|
||||
- `docs/` - reference on how the system is shaped (file layout, update pipeline, theming, shell architecture), for anyone working on the codebase; skills link here for depth
|
||||
- `manual/` - end-user documentation for using Omarchy, published; never codebase internals
|
||||
- `manual/` - end-user documentation for using Omarchy, published; never codebase internals. Chinese user manual for the cn layer lives in `manual/zh-cn/`
|
||||
|
||||
# Style
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# 行为准则
|
||||
|
||||
参与 OmarchyCN 社区(issue、PR、讨论)时:
|
||||
|
||||
- 尊重他人,就事论事,不进行人身攻击、骚扰或歧视
|
||||
- 欢迎新手提问,回答保持耐心
|
||||
- 技术分歧用证据和代码说话
|
||||
- 不发布垃圾信息、广告或与项目无关的内容
|
||||
|
||||
违反者由维护者视情节警告、删除内容或封禁账号。
|
||||
举报渠道见 [SECURITY.md](SECURITY.md) 中的联系方式。
|
||||
@@ -0,0 +1,23 @@
|
||||
# 贡献指南
|
||||
|
||||
## 仓库结构
|
||||
|
||||
- 上游 Omarchy 代码尽量不动;OmarchyCN 改动集中在 `bin/omarchy-cn-*`、`bin/omarchycn`、`cn/`、`install/cn/`
|
||||
- 上游代码定期与 `basecamp/omarchy` 同步:同步 PR 由维护者或 CI 工作流发起,人工审查合并
|
||||
|
||||
## 提交规范
|
||||
|
||||
- 遵循 [AGENTS.md](AGENTS.md) 的全部代码与命令约定(Bash 5、`[[ ]]`/`(( ))`、两空格缩进、`#!/bin/bash`)
|
||||
- 提交保持原子:一个提交只做一件事
|
||||
- 涉及系统修改的命令必须支持 `--dry-run`,写配置前先备份
|
||||
|
||||
## 测试
|
||||
|
||||
- CLI 与 shell 改动跑 `./test/all`
|
||||
- OmarchyCN 新增测试放在 `test/shell.d/*-test.sh`
|
||||
|
||||
## 提交流程
|
||||
|
||||
1. Fork 或在 issue 中讨论
|
||||
2. 提交 PR 到 `quattro` 分支,说明动机与验证方式
|
||||
3. 通过 CI 与代码审查后合并
|
||||
@@ -0,0 +1,14 @@
|
||||
OmarchyCN
|
||||
|
||||
Copyright (c) 2026 Zachary Zhang / OmarchyCN Community
|
||||
|
||||
本发行版基于 Omarchy(https://github.com/basecamp/omarchy),
|
||||
Copyright (c) 2025 David Heinemeier Hansson,MIT License。
|
||||
上游文件保留其原始版权与许可证声明(见 LICENSE)。
|
||||
|
||||
OmarchyCN 原创代码(bin/omarchy-cn-*、cn/、install/cn/ 及相关文档)
|
||||
采用 MIT License 发布。
|
||||
|
||||
OmarchyCN is an independent community distribution based on Omarchy.
|
||||
OmarchyCN is not affiliated with or endorsed by Basecamp, 37signals,
|
||||
or the Omarchy maintainers.
|
||||
@@ -1,79 +1,59 @@
|
||||
# Omarchy
|
||||
# OmarchyCN
|
||||
|
||||
Omarchy is a beautiful, modern & opinionated Linux distribution by DHH.
|
||||
面向中国开发者的 [Omarchy](https://omarchy.org) 下游发行版:完整保留 Omarchy 的 Arch + Hyprland 桌面体验,为中国网络环境、中文使用习惯与国内 AI 服务做系统级增强。
|
||||
|
||||
Read more at [omarchy.org](https://omarchy.org).
|
||||
当前版本 `4.0.0.alpha-cn.7`(基于上游 `quattro` 分支构建的基线 ISO)。
|
||||
|
||||
## The Omarchy Manual
|
||||
## 特性
|
||||
|
||||
The manual lives in [`manual/`](manual/), which is its authoritative source. It's
|
||||
mirrored to [learn.omacom.io](https://learn.omacom.io/2/the-omarchy-manual), where
|
||||
its screenshots are also hosted.
|
||||
继承自 Omarchy:
|
||||
|
||||
- [Welcome to Omarchy!](manual/01-welcome-to-omarchy.md)
|
||||
- Hyprland 动态平铺桌面 + Quickshell 顶栏、菜单与系统面板,键盘驱动工作流
|
||||
- `omarchy` CLI 与 `Super + Space` 系统菜单,CLI 与 GUI 同构
|
||||
- 内置主题系统与一键换肤,终端 / 编辑器 / 桌面配色统一
|
||||
- AI coding agent 桌面集成(Claude Code、Codex、OpenCode 等,`mise` 按需安装)
|
||||
- Btrfs + Snapper 更新前快照与系统回滚
|
||||
- archiso 离线安装镜像,支持无人值守安装与双系统引导
|
||||
|
||||
**The Basics**
|
||||
- **中国镜像管理**:Arch / npm / pip / Cargo / Go 等软件源测速、自动选择与故障切换
|
||||
- **中文环境开箱即用**:zh_CN locale、思源黑体 / 宋体字体栈、高分屏分数缩放预设
|
||||
- **中文输入法**:Fcitx5 + Rime 预配置,Wayland / GTK / Qt / Electron 全栈兼容,快捷键冲突自动处理
|
||||
- **AI Hub**:Kimi、DeepSeek、Z.AI/GLM、MiniMax 与本地 Ollama 的一等 Provider 支持,与 Claude Code、Codex、OpenCode、Kimi Code、Deep Code 等 Harness 的统一配置向导、凭据安全存储与连接诊断;另有 Dim、DSH 等自管登录的 Agent CLI 一键安装
|
||||
- **国内应用中心**:微信、QQ、飞书、钉钉、腾讯会议、WPS 等应用的可信安装入口
|
||||
- **一键转换**:原版 Omarchy(包安装)一条命令转成 OmarchyCN,logo/菜单/键位/输入法/软件源全套切换,`omarchy cn revert` 一键还原
|
||||
- **Overlay 安装器**:在现有 Omarchy checkout 上叠加 OmarchyCN,全程可逆、可卸载
|
||||
- **统一诊断**:`omarchycn doctor` 覆盖网络、镜像、输入法、显示与 AI 配置
|
||||
|
||||
- [Getting Started](manual/02-getting-started.md)
|
||||
- [Coming From Mac or Windows](manual/03-coming-from-mac-or-windows.md)
|
||||
- [Navigation](manual/04-navigation.md)
|
||||
- [The top bar](manual/05-the-top-bar.md)
|
||||
- [Themes](manual/06-themes.md)
|
||||
- [Hotkeys](manual/07-hotkeys.md)
|
||||
- [Unified Clipboard & History](manual/08-unified-clipboard-history.md)
|
||||
- [Reminders](manual/09-reminders.md)
|
||||
- [Notices](manual/10-notices.md)
|
||||
- [Text Extraction & Dictation](manual/11-text-extraction-dictation.md)
|
||||
- [Screenshots & Recording](manual/12-screenshots-recording.md)
|
||||
- [Toggles, idle & screensaver](manual/13-toggles-idle-screensaver.md)
|
||||
- [Omarchy CLI](manual/14-omarchy-cli.md)
|
||||
## 下载与安装
|
||||
|
||||
**The Applications**
|
||||
从 [Releases](https://git.zacharyzhang.com/ZacharyZhang-NY/omarchycn/releases) 页面下载 ISO 与 `SHA256SUMS.txt`,校验后写入 U 盘,UEFI 启动安装:
|
||||
|
||||
- [Terminal](manual/15-terminal.md)
|
||||
- [Neovim](manual/16-neovim.md)
|
||||
- [AI](manual/17-ai.md)
|
||||
- [Development Tools](manual/18-development-tools.md)
|
||||
- [Shell Tools](manual/19-shell-tools.md)
|
||||
- [Shell Functions](manual/20-shell-functions.md)
|
||||
- [TUIs](manual/21-tuis.md)
|
||||
- [GUIs](manual/22-guis.md)
|
||||
- [Browsers](manual/23-browsers.md)
|
||||
- [Commercial apps/services](manual/24-commercial-apps-services.md)
|
||||
- [Web Apps](manual/25-web-apps.md)
|
||||
- [Gaming](manual/26-gaming.md)
|
||||
- [Filling out PDFs](manual/27-filling-out-pdfs.md)
|
||||
- [Windows VM](manual/28-windows-vm.md)
|
||||
- [Other Packages](manual/29-other-packages.md)
|
||||
```bash
|
||||
sha256sum -c SHA256SUMS.txt
|
||||
```
|
||||
|
||||
**Configuration**
|
||||
## 已有原版 Omarchy?一键转换
|
||||
|
||||
- [Updates](manual/30-updates.md)
|
||||
- [Dotfiles](manual/31-dotfiles.md)
|
||||
- [Shell plugins](manual/32-shell-plugins.md)
|
||||
- [Monitors](manual/33-monitors.md)
|
||||
- [Keyboard, Mouse, Trackpad](manual/34-keyboard-mouse-trackpad.md)
|
||||
- [Networking](manual/35-networking.md)
|
||||
- [System sleep](manual/36-system-sleep.md)
|
||||
- [Hardware authentication](manual/37-hardware-authentication.md)
|
||||
- [Fonts](manual/38-fonts.md)
|
||||
- [Backgrounds](manual/39-backgrounds.md)
|
||||
- [Prompt](manual/40-prompt.md)
|
||||
- [Branding](manual/41-branding.md)
|
||||
- [Common tweaks](manual/42-common-tweaks.md)
|
||||
- [Making your own theme](manual/43-making-your-own-theme.md)
|
||||
包安装的原版 Omarchy(quattro)可直接转换,无需重装系统:
|
||||
|
||||
**The Rest**
|
||||
```bash
|
||||
curl -fsSL https://git.zacharyzhang.com/ZacharyZhang-NY/omarchycn/raw/branch/quattro/bin/omarchy-cn-convert | bash
|
||||
```
|
||||
|
||||
- [Mac support](manual/44-mac-support.md)
|
||||
- [Troubleshooting](manual/45-troubleshooting.md)
|
||||
- [FAQ](manual/46-faq.md)
|
||||
- [System snapshots](manual/47-system-snapshots.md)
|
||||
- [Security](manual/48-security.md)
|
||||
- [Omarchy on...](manual/49-omarchy-on.md)
|
||||
- [Dual Boot Install](manual/50-dual-boot-install.md)
|
||||
- [Unattended Installs](manual/51-unattended-installs.md)
|
||||
转换内容:换装 OmarchyCN 构建的系统包(中文菜单/键位/更新界面、OMARCHY CN 品牌、AI 全家桶)、启用 [omarchy] 自建镜像与 [omarchycn] 仓库、zh_CN 语言与 Fcitx5+Rime 输入法、国内 Arch 镜像自动测速。重新登录后生效。
|
||||
|
||||
## License
|
||||
随时一键还原(按转换时记录的状态逐项回退):
|
||||
|
||||
Omarchy is released under the [MIT License](https://opensource.org/licenses/MIT).
|
||||
```bash
|
||||
omarchy cn revert
|
||||
```
|
||||
|
||||
## 上游文档
|
||||
|
||||
Omarchy 完整英文手册在 [`manual/`](manual/) 目录,亦见 [learn.omacom.io](https://learn.omacom.io/2/the-omarchy-manual)。
|
||||
|
||||
## 声明与许可证
|
||||
|
||||
OmarchyCN is an independent community distribution based on Omarchy. OmarchyCN is not affiliated with or endorsed by Basecamp, 37signals, or the Omarchy maintainers.
|
||||
|
||||
代码沿用上游 [MIT License](LICENSE),保留 Omarchy 原始版权声明。
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# 安全政策
|
||||
|
||||
## 报告漏洞
|
||||
|
||||
请勿在公开 issue 中披露安全漏洞。发送邮件至:
|
||||
|
||||
**zhangyanghaha0407@outlook.com**(主题注明 [OmarchyCN Security])
|
||||
|
||||
请附:影响版本、复现步骤、影响面评估。我们在 72 小时内确认,修复后在
|
||||
Release Notes 中致谢(除非你要求匿名)。
|
||||
|
||||
## 范围
|
||||
|
||||
- OmarchyCN 软件包、ISO、安装与更新脚本、Registry 与签名链路
|
||||
- 上游 Omarchy / Arch / Hyprland 的漏洞请报给对应上游项目
|
||||
|
||||
## 签名验证
|
||||
|
||||
Release 产物的 PGP 公钥与验证方法见 [docs/release-signing.md](docs/release-signing.md)。
|
||||
@@ -126,7 +126,6 @@ New migration format:
|
||||
- Start with an `echo` describing what the migration does.
|
||||
- Use `$OMARCHY_PATH` to reference the Omarchy directory.
|
||||
- Be idempotent. Check existing state before changing it.
|
||||
- Migrations are strictly ordered and synchronous. A migration that cannot finish must exit non-zero, remain pending, and stop the queue; never mark later migrations complete against state an earlier migration has not established.
|
||||
- Use helper commands such as `omarchy-cmd-present`, `omarchy-cmd-missing`,
|
||||
`omarchy-pkg-add`, `omarchy-pkg-drop`, `omarchy-pkg-present`, and
|
||||
`omarchy-pkg-missing` when appropriate.
|
||||
@@ -166,5 +165,3 @@ omarchy-migrate
|
||||
Omarchy 4.0 is upgraded through `bin/omarchy-upgrade-to-quattro`, not through the
|
||||
normal migration runner. Do not add compatibility migrations for old installer
|
||||
layouts; put pre-4 package-layout transition work in the upgrade command instead.
|
||||
|
||||
Clearing a privileged file that a retired installer left on disk is the exception, and belongs in a migration whether or not that installer was part of a package layout transition. The upgrade command only runs on a machine still making the 3 to 4 crossing, so anything put there never reaches an install that crossed already, and it never runs at all for an installer that was retired on its own — while the file the installer wrote is still sitting on those machines. The upgrade command finishes by running `omarchy-migrate` (`run_post_upgrade_migrations`), so one migration reaches every population; a copy in the upgrade command would only be a second copy of the same predicate to keep correct. Such a migration must name the defect it clears and match what the old installer actually produced before deleting it. Leave safe administrator-authored files alone; if one still contains the vulnerable privileged action, preserve it under an inactive name rather than discarding custom content or leaving the action executable. A user config that depends on the same retired compatibility path may be repaired in that migration when doing so eliminates an overlapping migration, but only by matching and replacing the exact legacy path while preserving the rest of the file.
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Name=Basecamp
|
||||
Exec=omarchy-launch-webapp https://launchpad.37signals.com
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Icon=basecamp
|
||||
StartupNotify=true
|
||||
@@ -1,8 +0,0 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Name=Discord
|
||||
Exec=omarchy-launch-webapp https://discord.com/channels/@me
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Icon=omarchy-discord
|
||||
StartupNotify=true
|
||||
@@ -1,8 +0,0 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Name=Google Contacts
|
||||
Exec=omarchy-launch-webapp https://contacts.google.com/
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Icon=google-contacts
|
||||
StartupNotify=true
|
||||
@@ -1,8 +0,0 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Name=Google Maps
|
||||
Exec=omarchy-launch-webapp https://maps.google.com
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Icon=google-maps
|
||||
StartupNotify=true
|
||||
@@ -1,8 +0,0 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Name=Google Messages
|
||||
Exec=omarchy-launch-webapp https://messages.google.com/web/conversations
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Icon=google-messages
|
||||
StartupNotify=true
|
||||
@@ -1,8 +0,0 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Name=Google Photos
|
||||
Exec=omarchy-launch-webapp https://photos.google.com/
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Icon=google-photos
|
||||
StartupNotify=true
|
||||
@@ -1,9 +0,0 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Name=HEY
|
||||
Exec=omarchy-webapp-handler-hey %u
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Icon=hey
|
||||
StartupNotify=true
|
||||
MimeType=x-scheme-handler/mailto
|
||||
@@ -1,8 +0,0 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Name=WhatsApp
|
||||
Exec=omarchy-launch-webapp https://web.whatsapp.com/
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Icon=whatsapp
|
||||
StartupNotify=true
|
||||
@@ -1,8 +0,0 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Name=X
|
||||
Exec=omarchy-launch-webapp https://x.com/
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Icon=x
|
||||
StartupNotify=true
|
||||
|
Before Width: | Height: | Size: 147 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 196 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 22 KiB |
@@ -39,6 +39,7 @@ GROUP_DESCRIPTIONS[capture]="Screenshots and screen recording"
|
||||
GROUP_DESCRIPTIONS[channel]="Omarchy release channel management"
|
||||
GROUP_DESCRIPTIONS[clipboard]="Clipboard helpers"
|
||||
GROUP_DESCRIPTIONS[cmd]="Command and shortcut helpers"
|
||||
GROUP_DESCRIPTIONS[cn]="OmarchyCN China environment integration"
|
||||
GROUP_DESCRIPTIONS[config]="System configuration helpers"
|
||||
GROUP_DESCRIPTIONS[crash]="Crash notification controls"
|
||||
GROUP_DESCRIPTIONS[debug]="Diagnostics and support logs"
|
||||
|
||||
@@ -101,6 +101,30 @@ pi)
|
||||
command=(pi)
|
||||
[[ -n ${prompt:-} ]] && command+=("$prompt")
|
||||
;;
|
||||
kimi)
|
||||
command=(kimi)
|
||||
[[ -n ${prompt:-} ]] && command+=(--prompt "$prompt")
|
||||
;;
|
||||
deepcode)
|
||||
command=(deepcode)
|
||||
[[ -n ${prompt:-} ]] && command+=("$prompt")
|
||||
;;
|
||||
dim)
|
||||
# `dim exec` is the documented one-shot prompt form
|
||||
if [[ -n ${prompt:-} ]]; then
|
||||
command=(dim exec "$prompt")
|
||||
else
|
||||
command=(dim)
|
||||
fi
|
||||
;;
|
||||
dsh)
|
||||
# dsh has no TUI: `dsh web` is the interactive form, headless the one-shot
|
||||
if [[ -n ${prompt:-} ]]; then
|
||||
command=(dsh --profile headless "$prompt")
|
||||
else
|
||||
command=(dsh web)
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported default agent: $agent" >&2
|
||||
exit 1
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
#!/bin/bash
|
||||
# omarchy:summary=Map an AI profile onto Omarchy's default agent and hotkey
|
||||
# omarchy:args=[profile]
|
||||
# omarchy:examples=omarchycn ai default | omarchycn ai default work
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$OMARCHY_PATH/cn/lib/ai.sh"
|
||||
|
||||
profile="${1:-$(cn_ai_current_profile)}"
|
||||
|
||||
if [[ ! -f $CN_AI_PROFILE_DIR/$profile.toml ]]; then
|
||||
echo "No such profile: $profile" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
harness=$(cn_ai_profile_field "$profile" harness)
|
||||
provider=$(cn_ai_profile_field "$profile" provider)
|
||||
model=$(cn_ai_profile_field "$profile" model)
|
||||
|
||||
# Only harnesses whose provider config persists in their own files can back
|
||||
# the upstream hotkey; opencode is session-env only, natives self-auth
|
||||
case "$harness" in
|
||||
claude-code) agent="claude" ;;
|
||||
codex) agent="codex" ;;
|
||||
*)
|
||||
echo "$harness 无法映射为上游默认 Agent;用 omarchycn ai launch $profile 启动" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
command=$(cn_ai_harness_field "$harness" command)
|
||||
if omarchy-cmd-missing "$command"; then
|
||||
install=$(cn_ai_harness_field "$harness" install)
|
||||
echo "Installing $harness: $install"
|
||||
$install
|
||||
fi
|
||||
|
||||
key=$(cn_ai_resolve_key "$provider")
|
||||
|
||||
case "$harness" in
|
||||
claude-code)
|
||||
# Persist provider env in Claude Code's own settings so the upstream
|
||||
# Super+Shift+Ctrl+A -> omarchy-agent path launches fully configured
|
||||
settings="$HOME/.claude/settings.json"
|
||||
mkdir -p "${settings%/*}"
|
||||
base=$(cn_ai_endpoint "$provider" anthropic)
|
||||
# Dynamic-model providers have no fast alias: every tier maps to the model
|
||||
fast=$(cn_ai_fast_model "$provider" 2>/dev/null || printf '%s' "$model")
|
||||
current="{}"
|
||||
if [[ -s $settings ]]; then
|
||||
current=$(cat "$settings")
|
||||
cp "$settings" "$settings.omarchycn-bak-$(date +%Y%m%d-%H%M%S)"
|
||||
fi
|
||||
# Key reaches jq via environment, never the argument list
|
||||
rm -f "$settings.omarchycn-new"
|
||||
(
|
||||
umask 077
|
||||
CN_AI_KEY="$key" jq --arg base "$base" --arg model "$model" --arg fast "$fast" \
|
||||
'.env = (.env // {}) + {
|
||||
ANTHROPIC_BASE_URL: $base,
|
||||
ANTHROPIC_AUTH_TOKEN: env.CN_AI_KEY,
|
||||
ANTHROPIC_API_KEY: env.CN_AI_KEY,
|
||||
ANTHROPIC_MODEL: $model,
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: $model,
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: $model,
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: $fast
|
||||
}' <<<"$current" > "$settings.omarchycn-new"
|
||||
)
|
||||
mv "$settings.omarchycn-new" "$settings"
|
||||
;;
|
||||
codex)
|
||||
cn_ai_render_codex_config "$provider" "$model" "$key"
|
||||
;;
|
||||
esac
|
||||
|
||||
agent_file="$HOME/.config/omarchy/defaults/agent"
|
||||
mkdir -p "${agent_file%/*}"
|
||||
printf '%s\n' "$agent" > "$agent_file"
|
||||
|
||||
echo "默认 Agent: $agent($profile: $provider x $model)"
|
||||
echo "快捷键 Super+Shift+Ctrl+A 或 'omarchy agent' 将以该配置启动"
|
||||
@@ -0,0 +1,61 @@
|
||||
#!/bin/bash
|
||||
# omarchy:summary=Check AI harness installation, credentials, and endpoint health
|
||||
# omarchy:examples=omarchycn ai doctor
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$OMARCHY_PATH/cn/lib/ai.sh"
|
||||
|
||||
failures=0
|
||||
|
||||
profile=$(cn_ai_current_profile 2>/dev/null || true)
|
||||
if [[ -z $profile ]]; then
|
||||
echo "INFO ai: 未设置默认 Profile (omarchycn ai profile use <name>)"
|
||||
exit 0
|
||||
fi
|
||||
echo "INFO ai: current profile $profile"
|
||||
|
||||
if [[ ! -f $CN_AI_PROFILE_DIR/$profile.toml ]]; then
|
||||
echo "FAIL ai: 默认 Profile $profile 的文件不存在 (omarchycn ai profile list)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
harness=$(cn_ai_profile_field "$profile" harness)
|
||||
provider=$(cn_ai_profile_field "$profile" provider)
|
||||
command=$(cn_ai_harness_field "$harness" command)
|
||||
proto=$(cn_ai_harness_field "$harness" protocol)
|
||||
|
||||
if omarchy-cmd-present "$command"; then
|
||||
echo "PASS ai: harness $harness installed ($command)"
|
||||
else
|
||||
install=$(cn_ai_harness_field "$harness" install)
|
||||
if [[ $install == doc:* ]]; then
|
||||
echo "WARN ai: harness $harness 未安装,需手动安装: ${install#doc:}"
|
||||
else
|
||||
echo "WARN ai: harness $harness 未安装,首次 launch 时自动安装"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ $proto == "native" ]]; then
|
||||
echo "INFO ai: $harness 自管鉴权,跳过 OmarchyCN Key 检查"
|
||||
elif cn_ai_static_token "$provider" > /dev/null 2>&1; then
|
||||
echo "PASS ai: $provider 使用内置 token,无需存储 Key"
|
||||
elif omarchy-cn-ai-secret get "$provider" > /dev/null 2>&1; then
|
||||
echo "PASS ai: $provider API Key stored"
|
||||
else
|
||||
echo "FAIL ai: $provider API Key 未存储 (omarchycn ai secret set $provider)"
|
||||
failures=$((failures + 1))
|
||||
fi
|
||||
|
||||
if [[ $proto != "native" ]]; then
|
||||
base=$(cn_ai_endpoint "$provider" "$proto")
|
||||
host=$(sed -E 's|https?://([^/:]+).*|\1|' <<<"$base")
|
||||
if getent hosts "$host" > /dev/null 2>&1; then
|
||||
echo "PASS ai: endpoint DNS resolves ($host)"
|
||||
else
|
||||
echo "FAIL ai: endpoint DNS 无法解析 ($host)"
|
||||
failures=$((failures + 1))
|
||||
fi
|
||||
fi
|
||||
|
||||
exit $((failures > 0 ? 1 : 0))
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/bin/bash
|
||||
# omarchy:summary=Launch the AI harness configured by a profile
|
||||
# omarchy:args=[profile]
|
||||
# omarchy:examples=omarchycn ai launch | omarchycn ai launch work
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$OMARCHY_PATH/cn/lib/ai.sh"
|
||||
|
||||
profile="${1:-$(cn_ai_current_profile)}"
|
||||
|
||||
if [[ ! -f $CN_AI_PROFILE_DIR/$profile.toml ]]; then
|
||||
echo "No such profile: $profile" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
harness=$(cn_ai_profile_field "$profile" harness)
|
||||
provider=$(cn_ai_profile_field "$profile" provider)
|
||||
model=$(cn_ai_profile_field "$profile" model)
|
||||
command=$(cn_ai_harness_field "$harness" command)
|
||||
config_method=$(cn_ai_harness_field "$harness" config_method)
|
||||
|
||||
if omarchy-cmd-missing "$command"; then
|
||||
install=$(cn_ai_harness_field "$harness" install)
|
||||
if [[ $install == doc:* ]]; then
|
||||
echo "$harness 未安装,安装方法见官方文档: ${install#doc:}" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Installing $harness: $install"
|
||||
$install
|
||||
fi
|
||||
|
||||
case "$config_method" in
|
||||
env)
|
||||
key=$(cn_ai_resolve_key "$provider")
|
||||
if [[ $harness == "codex" ]]; then
|
||||
cn_ai_render_codex_config "$provider" "$model" "$key"
|
||||
exec "$command"
|
||||
fi
|
||||
env_exports=$(cn_ai_render_env "$harness" "$provider" "$model" "$key")
|
||||
eval "$env_exports"
|
||||
exec "$command"
|
||||
;;
|
||||
native)
|
||||
# Native harnesses (kimi-code, deep-code) manage their own auth
|
||||
exec "$command"
|
||||
;;
|
||||
*)
|
||||
echo "Unknown config method: $config_method" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,75 @@
|
||||
#!/bin/bash
|
||||
# omarchy:summary=Create an AI profile binding a harness, provider, and model
|
||||
# omarchy:args=<name> <harness> <provider> <model|default-coding|fast>
|
||||
# omarchy:examples=omarchycn ai profile create work claude-code deepseek default-coding
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$OMARCHY_PATH/cn/lib/ai.sh"
|
||||
|
||||
name="${1:?usage: omarchycn ai profile create <name> <harness> <provider> <model>}"
|
||||
harness="${2:?missing harness}"
|
||||
provider="${3:?missing provider}"
|
||||
model="${4:?missing model}"
|
||||
|
||||
if [[ ! $name =~ ^[a-z0-9-]+$ ]]; then
|
||||
echo "Profile name must be lowercase alphanumeric/dashes: $name" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cn_ai_harness_field "$harness" command > /dev/null || {
|
||||
echo "Unknown harness: $harness (known: $(cn_ai_harness_ids | tr '\n' ' '))" >&2
|
||||
exit 1
|
||||
}
|
||||
jq -e --arg p "$provider" '.providers[$p]' "$CN_AI_PROVIDERS" > /dev/null || {
|
||||
echo "Unknown provider: $provider (known: $(cn_ai_provider_ids | tr '\n' ' '))" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
level=$(cn_ai_combo_level "$harness" "$provider")
|
||||
if [[ $level == "unsupported" ]]; then
|
||||
echo "Combo $harness x $provider is not in the compatibility matrix" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
allowlist=$(cn_ai_combo_models "$harness" "$provider")
|
||||
if cn_ai_models_dynamic "$provider"; then
|
||||
: # runtime-listed models; omarchycn ai test validates against the live server
|
||||
elif [[ -n $allowlist ]]; then
|
||||
# Combo-restricted model set (e.g. codex uses its own catalog slugs)
|
||||
if ! grep -qxF "$model" <<<"$allowlist"; then
|
||||
echo "Model $model not supported for $harness x $provider" >&2
|
||||
echo "Supported: $(tr '\n' ' ' <<<"$allowlist")" >&2
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
resolved=$(cn_ai_model_by_alias "$provider" "$model" 2>/dev/null || true)
|
||||
if [[ -n $resolved ]]; then
|
||||
model="$resolved"
|
||||
elif ! cn_ai_models "$provider" | grep -qxF "$model"; then
|
||||
echo "Unknown model for $provider: $model" >&2
|
||||
echo "Available: $(cn_ai_models "$provider" | tr '\n' ' ')" >&2
|
||||
exit 1
|
||||
fi
|
||||
model_proto=$(cn_ai_model_protocol "$provider" "$model")
|
||||
harness_proto=$(cn_ai_harness_field "$harness" protocol)
|
||||
if [[ -n $model_proto && $model_proto != "$harness_proto" ]]; then
|
||||
echo "Model $model speaks $model_proto, but $harness needs $harness_proto" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
mkdir -p "$CN_AI_PROFILE_DIR"
|
||||
cat > "$CN_AI_PROFILE_DIR/$name.toml" <<EOF
|
||||
schema_version = 1
|
||||
name = "$name"
|
||||
harness = "$harness"
|
||||
provider = "$provider"
|
||||
model = "$model"
|
||||
secret_ref = "omarchycn://ai/$provider"
|
||||
EOF
|
||||
|
||||
echo "Profile $name: $harness x $provider x $model (compat: $level)"
|
||||
if ! cn_ai_resolve_key "$provider" > /dev/null 2>&1; then
|
||||
echo "提示: 尚未存储 $provider 的 API Key,运行: omarchycn ai secret set $provider"
|
||||
fi
|
||||
@@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
# omarchy:summary=List AI profiles and the current default
|
||||
# omarchy:examples=omarchycn ai profile list
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$OMARCHY_PATH/cn/lib/ai.sh"
|
||||
|
||||
current=""
|
||||
if [[ -f $CN_AI_CURRENT ]]; then
|
||||
current=$(<"$CN_AI_CURRENT")
|
||||
fi
|
||||
|
||||
found="false"
|
||||
for f in "$CN_AI_PROFILE_DIR"/*.toml; do
|
||||
[[ -f $f ]] || continue
|
||||
found="true"
|
||||
name="${f##*/}"
|
||||
name="${name%.toml}"
|
||||
marker=" "
|
||||
[[ $name == "$current" ]] && marker="*"
|
||||
printf '%s %-14s %s x %s x %s\n' "$marker" "$name" \
|
||||
"$(cn_ai_profile_field "$name" harness)" \
|
||||
"$(cn_ai_profile_field "$name" provider)" \
|
||||
"$(cn_ai_profile_field "$name" model)"
|
||||
done
|
||||
|
||||
if [[ $found == "false" ]]; then
|
||||
echo "No profiles (create: omarchycn ai profile create <name> <harness> <provider> <model>)"
|
||||
fi
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
# omarchy:summary=Set the default AI profile
|
||||
# omarchy:args=<name>
|
||||
# omarchy:examples=omarchycn ai profile use work
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$OMARCHY_PATH/cn/lib/ai.sh"
|
||||
|
||||
name="${1:?usage: omarchycn ai profile use <name>}"
|
||||
|
||||
if [[ ! -f $CN_AI_PROFILE_DIR/$name.toml ]]; then
|
||||
echo "No such profile: $name (see: omarchycn ai profile list)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "${CN_AI_CURRENT%/*}"
|
||||
echo "$name" > "$CN_AI_CURRENT"
|
||||
echo "Default AI profile: $name"
|
||||
@@ -0,0 +1,104 @@
|
||||
#!/bin/bash
|
||||
# omarchy:summary=Store, read, or delete AI provider API keys
|
||||
# omarchy:args=<set|get|rm> <provider>
|
||||
# omarchy:examples=omarchycn ai secret set deepseek | omarchycn ai secret get deepseek
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
action="${1:?usage: omarchycn ai secret <set|get|rm> <provider>}"
|
||||
provider="${2:?usage: omarchycn ai secret <set|get|rm> <provider>}"
|
||||
|
||||
if [[ ! $provider =~ ^[a-z0-9-]+$ ]]; then
|
||||
echo "Invalid provider name: $provider" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
FILE_DIR="$HOME/.local/state/omarchycn/secrets"
|
||||
FILE_PATH="$FILE_DIR/$provider"
|
||||
|
||||
secret_service_ok() {
|
||||
omarchy-cmd-present secret-tool && secret-tool search service omarchycn > /dev/null 2>&1
|
||||
}
|
||||
|
||||
pass_ok() {
|
||||
omarchy-cmd-present pass && pass ls > /dev/null 2>&1
|
||||
}
|
||||
|
||||
case "$action" in
|
||||
set)
|
||||
if [[ -t 0 ]]; then
|
||||
read -rs -p "API key for $provider: " key
|
||||
echo
|
||||
else
|
||||
IFS= read -r key || true
|
||||
fi
|
||||
if [[ -z $key ]]; then
|
||||
echo "Empty key refused" >&2
|
||||
exit 1
|
||||
fi
|
||||
if secret_service_ok; then
|
||||
printf '%s' "$key" | secret-tool store --label "OmarchyCN AI: $provider" service omarchycn key "ai/$provider"
|
||||
# Purge stale copies in lower-priority backends so get never falls
|
||||
# through to an outdated key; a failed purge fails the set.
|
||||
# Existence check is decryption-free (store file on disk).
|
||||
if pass_ok && [[ -f ${PASSWORD_STORE_DIR:-$HOME/.password-store}/omarchycn/ai/$provider.gpg ]]; then
|
||||
if ! pass rm -f "omarchycn/ai/$provider" > /dev/null; then
|
||||
echo "存储成功但 pass 中的旧副本清除失败,请手动执行: pass rm omarchycn/ai/$provider" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
rm -f "$FILE_PATH"
|
||||
echo "Stored in Secret Service (secret-tool)"
|
||||
elif pass_ok; then
|
||||
printf '%s\n' "$key" | pass insert -m -f "omarchycn/ai/$provider" > /dev/null
|
||||
rm -f "$FILE_PATH"
|
||||
echo "Stored in pass (omarchycn/ai/$provider)"
|
||||
else
|
||||
mkdir -p "$FILE_DIR"
|
||||
chmod 700 "$FILE_DIR"
|
||||
rm -f "$FILE_PATH"
|
||||
(umask 177 && printf '%s' "$key" > "$FILE_PATH")
|
||||
echo "Stored in $FILE_PATH (0600 file fallback — 安装 libsecret 或 pass 可获得更安全的存储)"
|
||||
fi
|
||||
;;
|
||||
get)
|
||||
if secret_service_ok && secret-tool lookup service omarchycn key "ai/$provider" 2>/dev/null; then
|
||||
exit 0
|
||||
fi
|
||||
if pass_ok && pass show "omarchycn/ai/$provider" 2>/dev/null; then
|
||||
exit 0
|
||||
fi
|
||||
if [[ -f $FILE_PATH ]]; then
|
||||
cat "$FILE_PATH"
|
||||
exit 0
|
||||
fi
|
||||
echo "No secret stored for $provider (run: omarchycn ai secret set $provider)" >&2
|
||||
exit 1
|
||||
;;
|
||||
rm)
|
||||
removed=0
|
||||
if secret_service_ok && secret-tool clear service omarchycn key "ai/$provider" 2>/dev/null; then
|
||||
removed=1
|
||||
fi
|
||||
if pass_ok && pass rm -f "omarchycn/ai/$provider" > /dev/null 2>&1; then
|
||||
removed=1
|
||||
fi
|
||||
if [[ -f $FILE_PATH ]]; then
|
||||
rm -f "$FILE_PATH"
|
||||
removed=1
|
||||
fi
|
||||
if omarchy-cmd-present secret-tool && ! secret_service_ok; then
|
||||
echo "Secret Service 不可达,无法确认其中的副本已清除;服务恢复后重新执行 rm" >&2
|
||||
exit 1
|
||||
fi
|
||||
if (( removed == 0 )); then
|
||||
echo "No secret stored for $provider" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Secret for $provider removed"
|
||||
;;
|
||||
*)
|
||||
echo "Unknown action: $action (expected set, get, or rm)" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,109 @@
|
||||
#!/bin/bash
|
||||
# omarchy:summary=Interactive wizard: pick harness, provider, model, store key, test
|
||||
# omarchy:examples=omarchycn ai setup
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$OMARCHY_PATH/cn/lib/ai.sh"
|
||||
|
||||
if [[ ! -t 0 ]]; then
|
||||
echo "omarchycn ai setup 需要交互终端;非交互场景用 omarchycn ai profile create" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
harness=$(cn_ai_harness_ids | gum choose --header "选择 Harness")
|
||||
|
||||
# Only combos whose adapter actually renders at launch
|
||||
mapfile -t providers < <(jq -r --arg h "$harness" \
|
||||
'.combos[$h] | to_entries[] | select(.value.adapter) | .key' "$CN_AI_COMPAT")
|
||||
if (( ${#providers[@]} == 0 )); then
|
||||
echo "兼容矩阵中没有 $harness 的可用 Provider 组合" >&2
|
||||
exit 1
|
||||
fi
|
||||
provider=$(printf '%s\n' "${providers[@]}" | gum choose --header "选择 Provider(兼容级别见 docs)")
|
||||
|
||||
harness_proto=$(cn_ai_harness_field "$harness" protocol)
|
||||
allowlist=$(cn_ai_combo_models "$harness" "$provider")
|
||||
if cn_ai_models_dynamic "$provider"; then
|
||||
base=$(cn_ai_endpoint "$provider" "$harness_proto")
|
||||
if [[ $provider == "ollama" ]]; then
|
||||
if omarchy-cmd-missing ollama; then
|
||||
gum confirm "Ollama 未安装,现在安装本地服务?" || exit 1
|
||||
if omarchy-cmd-present nvidia-smi; then
|
||||
ollama_pkg=ollama-cuda
|
||||
elif omarchy-cmd-present rocminfo; then
|
||||
ollama_pkg=ollama-rocm
|
||||
else
|
||||
ollama_pkg=ollama
|
||||
fi
|
||||
omarchy-pkg-add "$ollama_pkg"
|
||||
fi
|
||||
if ! curl -sf --connect-timeout 3 "$base/api/tags" > /dev/null; then
|
||||
sudo systemctl enable --now ollama
|
||||
fi
|
||||
fi
|
||||
models=$(curl -sf --connect-timeout 3 "$base/api/tags" | jq -r '.models[].name' || true)
|
||||
if [[ -z $models ]]; then
|
||||
echo "无法列出本地模型(服务未运行或尚未拉取):先 ollama pull <模型>(如 qwen3-coder)再重试" >&2
|
||||
exit 1
|
||||
fi
|
||||
model=$(gum choose --header "选择本地模型" <<<"$models")
|
||||
elif [[ -n $allowlist ]]; then
|
||||
model=$(gum choose --header "选择模型" <<<"$allowlist")
|
||||
else
|
||||
# Hide models bound to a different wire protocol than the harness speaks
|
||||
model=$(jq -r --arg p "$provider" --arg hp "$harness_proto" \
|
||||
'.providers[$p].models[] | select((.protocol // $hp) == $hp) | .id' \
|
||||
"$CN_AI_PROVIDERS" | gum choose --header "选择模型")
|
||||
fi
|
||||
|
||||
default_name="$harness-$provider"
|
||||
name=$(gum input --header "Profile 名称" --value "$default_name")
|
||||
|
||||
proto=$(cn_ai_harness_field "$harness" protocol)
|
||||
if [[ $proto == "native" ]]; then
|
||||
omarchy-cn-ai-profile-create "$name" "$harness" "$provider" "$model"
|
||||
omarchy-cn-ai-profile-use "$name"
|
||||
echo "$harness 自管鉴权:首次启动时在 CLI 内完成官方登录"
|
||||
if gum confirm "立即启动并完成官方登录?"; then
|
||||
exec omarchy-cn-ai-launch "$name"
|
||||
fi
|
||||
echo "完成。启动: omarchycn ai launch"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! cn_ai_resolve_key "$provider" > /dev/null 2>&1; then
|
||||
key=$(gum input --password --header "$provider API Key(获取: $(jq -r --arg p "$provider" '.providers[$p].key_url' "$CN_AI_PROVIDERS"))")
|
||||
printf '%s' "$key" | omarchy-cn-ai-secret set "$provider"
|
||||
fi
|
||||
|
||||
# Keep any existing profile intact until the new config proves itself
|
||||
profile_file="$CN_AI_PROFILE_DIR/$name.toml"
|
||||
if [[ -f $profile_file ]]; then
|
||||
cp "$profile_file" "$profile_file.omarchycn-prev"
|
||||
fi
|
||||
|
||||
omarchy-cn-ai-profile-create "$name" "$harness" "$provider" "$model"
|
||||
|
||||
if gum confirm "运行连接测试?"; then
|
||||
if ! omarchy-cn-ai-test "$name"; then
|
||||
if [[ -f $profile_file.omarchycn-prev ]]; then
|
||||
mv "$profile_file.omarchycn-prev" "$profile_file"
|
||||
echo "测试未通过,已恢复原 Profile $name" >&2
|
||||
else
|
||||
rm -f "$profile_file"
|
||||
echo "测试未通过,Profile $name 未保留" >&2
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
rm -f "$profile_file.omarchycn-prev"
|
||||
omarchy-cn-ai-profile-use "$name"
|
||||
|
||||
if [[ $harness == "claude-code" || $harness == "codex" ]]; then
|
||||
if gum confirm "设为系统默认 Agent(Super+Shift+Ctrl+A)?"; then
|
||||
omarchy-cn-ai-default "$name"
|
||||
fi
|
||||
fi
|
||||
echo "完成。启动: omarchycn ai launch"
|
||||
@@ -0,0 +1,94 @@
|
||||
#!/bin/bash
|
||||
# omarchy:summary=Test connectivity, auth, and model availability for an AI profile
|
||||
# omarchy:args=[profile]
|
||||
# omarchy:examples=omarchycn ai test | omarchycn ai test work
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$OMARCHY_PATH/cn/lib/ai.sh"
|
||||
|
||||
profile="${1:-$(cn_ai_current_profile)}"
|
||||
|
||||
if [[ ! -f $CN_AI_PROFILE_DIR/$profile.toml ]]; then
|
||||
echo "No such profile: $profile" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
harness=$(cn_ai_profile_field "$profile" harness)
|
||||
provider=$(cn_ai_profile_field "$profile" provider)
|
||||
model=$(cn_ai_profile_field "$profile" model)
|
||||
proto=$(cn_ai_harness_field "$harness" protocol)
|
||||
# Codex speaks the Responses API (wire_api=responses), not chat/completions
|
||||
[[ $harness == "codex" ]] && proto="openai-responses"
|
||||
failures=0
|
||||
|
||||
if [[ $proto == "native" ]]; then
|
||||
echo "INFO ai-test: $harness 自管鉴权,本测试仅覆盖 env/config 型 Harness"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Endpoint override for mock-server tests
|
||||
base="${OMARCHYCN_AI_TEST_BASE_URL:-$(cn_ai_endpoint "$provider" "${proto%-responses}")}"
|
||||
echo "INFO ai-test: $profile ($harness x $provider x $model) -> $base"
|
||||
|
||||
if ! key=$(cn_ai_resolve_key "$provider" 2>/dev/null); then
|
||||
echo "FAIL ai-test: 未存储 $provider 的 API Key (omarchycn ai secret set $provider)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
body=$(mktemp)
|
||||
trap 'rm -f "$body"' EXIT
|
||||
|
||||
case "$proto" in
|
||||
anthropic)
|
||||
code=$(curl -sS -o "$body" -w '%{http_code}' -m 30 --connect-timeout 8 \
|
||||
-X POST "${base%/}/v1/messages" \
|
||||
-H "x-api-key: $key" -H "authorization: Bearer $key" \
|
||||
-H "anthropic-version: 2023-06-01" -H "content-type: application/json" \
|
||||
-d "{\"model\":\"$model\",\"max_tokens\":8,\"messages\":[{\"role\":\"user\",\"content\":\"ping\"}]}" || true)
|
||||
;;
|
||||
openai)
|
||||
code=$(curl -sS -o "$body" -w '%{http_code}' -m 30 --connect-timeout 8 \
|
||||
-X POST "${base%/}/chat/completions" \
|
||||
-H "Authorization: Bearer $key" -H "content-type: application/json" \
|
||||
-d "{\"model\":\"$model\",\"max_tokens\":8,\"messages\":[{\"role\":\"user\",\"content\":\"ping\"}]}" || true)
|
||||
;;
|
||||
openai-responses)
|
||||
code=$(curl -sS -o "$body" -w '%{http_code}' -m 30 --connect-timeout 8 \
|
||||
-X POST "${base%/}/responses" \
|
||||
-H "Authorization: Bearer $key" -H "content-type: application/json" \
|
||||
-d "{\"model\":\"$model\",\"input\":\"ping\",\"max_output_tokens\":16}" || true)
|
||||
;;
|
||||
*)
|
||||
echo "FAIL ai-test: unknown protocol $proto"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
[[ -z $code ]] && code=000
|
||||
|
||||
case "$code" in
|
||||
200)
|
||||
echo "PASS ai-test: 连接、鉴权与模型调用成功 (HTTP 200)"
|
||||
;;
|
||||
401 | 403)
|
||||
echo "FAIL ai-test: 鉴权失败 (HTTP $code),检查 API Key"
|
||||
failures=$((failures + 1))
|
||||
;;
|
||||
404 | 400 | 422)
|
||||
echo "FAIL ai-test: 端点可达但请求被拒 (HTTP $code),可能是模型名问题"
|
||||
head -c 200 "$body" 2>/dev/null && echo
|
||||
failures=$((failures + 1))
|
||||
;;
|
||||
000)
|
||||
echo "FAIL ai-test: 无法连接 $base (DNS/TLS/网络)"
|
||||
failures=$((failures + 1))
|
||||
;;
|
||||
*)
|
||||
echo "FAIL ai-test: HTTP $code"
|
||||
head -c 200 "$body" 2>/dev/null && echo
|
||||
failures=$((failures + 1))
|
||||
;;
|
||||
esac
|
||||
|
||||
rm -f $body
|
||||
exit $((failures > 0 ? 1 : 0))
|
||||
@@ -0,0 +1,57 @@
|
||||
#!/bin/bash
|
||||
# omarchy:summary=Install a China app from the catalog with source confirmation
|
||||
# omarchy:args=<app-id> [--yes]
|
||||
# omarchy:examples=omarchycn app install wechat | omarchycn app install tencent-docs
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
APPS_JSON="$OMARCHY_PATH/cn/apps.json"
|
||||
|
||||
app="${1:?usage: omarchycn app install <app-id> (see: omarchycn app list)}"
|
||||
yes="${2:-}"
|
||||
|
||||
entry=$(jq -e --arg a "$app" '.apps[$a]' "$APPS_JSON") || {
|
||||
echo "Unknown app: $app (see: omarchycn app list)" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
name=$(jq -r '.name' <<<"$entry")
|
||||
source_type=$(jq -r '.source' <<<"$entry")
|
||||
license=$(jq -r '.license' <<<"$entry")
|
||||
|
||||
if [[ $license == "proprietary" && $yes != "--yes" ]]; then
|
||||
pkg=$(jq -r '.package' <<<"$entry")
|
||||
echo "$name 为专有软件,来源: AUR/$pkg(构建脚本公开,二进制来自厂商)"
|
||||
if [[ -t 0 ]]; then
|
||||
gum confirm "确认安装?" || exit 1
|
||||
else
|
||||
echo "非交互环境需显式加 --yes 确认专有软件安装" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
case "$source_type" in
|
||||
aur)
|
||||
# Offline installs leave only offline.db in sync; without the official
|
||||
# repo dbs yay treats repo deps as AUR. Upgrades must go through the
|
||||
# omarchy-update pipeline (pacman guard).
|
||||
if [[ ! -f /var/lib/pacman/sync/core.db ]]; then
|
||||
if [[ ! -t 0 ]]; then
|
||||
echo "首次安装应用前请先完成一次系统更新: omarchy-update" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "首次安装应用前需要完成一次系统更新。"
|
||||
omarchy-update
|
||||
fi
|
||||
omarchy-pkg-aur-add "$(jq -r '.package' <<<"$entry")"
|
||||
;;
|
||||
webapp)
|
||||
omarchy-webapp-install "$name" "$(jq -r '.url' <<<"$entry")" "$(jq -r '.icon' <<<"$entry")"
|
||||
;;
|
||||
*)
|
||||
echo "Unknown source type: $source_type" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "$name 安装完成"
|
||||
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
# omarchy:summary=List China app catalog entries and their sources
|
||||
# omarchy:examples=omarchycn app list
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
APPS_JSON="$OMARCHY_PATH/cn/apps.json"
|
||||
|
||||
printf '%-14s %-10s %-8s %-12s %s\n' "ID" "NAME" "SOURCE" "LICENSE" "PACKAGE/URL"
|
||||
jq -r '.apps | to_entries[] |
|
||||
[.key, .value.name, .value.source, .value.license, (.value.package // .value.url)] | @tsv' \
|
||||
"$APPS_JSON" |
|
||||
while IFS=$'\t' read -r id name source license ref; do
|
||||
printf '%-14s %-10s %-8s %-12s %s\n' "$id" "$name" "$source" "$license" "$ref"
|
||||
done
|
||||
@@ -0,0 +1,29 @@
|
||||
#!/bin/bash
|
||||
# omarchy:summary=Show or set the OmarchyCN release channel
|
||||
# omarchy:args=[stable|beta|nightly]
|
||||
# omarchy:examples=omarchycn channel | omarchycn channel beta
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
CHANNEL_FILE="$HOME/.config/omarchycn/channel"
|
||||
|
||||
if (( $# == 0 )); then
|
||||
if [[ -f $CHANNEL_FILE ]]; then
|
||||
cat "$CHANNEL_FILE"
|
||||
else
|
||||
echo "beta (default)"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
case "$1" in
|
||||
stable | beta | nightly)
|
||||
mkdir -p "${CHANNEL_FILE%/*}"
|
||||
echo "$1" > "$CHANNEL_FILE"
|
||||
echo "Channel: $1"
|
||||
;;
|
||||
*)
|
||||
echo "Unknown channel: $1 (stable|beta|nightly)" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,107 @@
|
||||
#!/bin/bash
|
||||
# omarchy:summary=Convert a vanilla package-based Omarchy install into OmarchyCN
|
||||
# omarchy:examples=omarchy cn convert
|
||||
# Curl-able bootstrap (like the overlay installer): everything before the
|
||||
# package swap must run without the cn tree, so no $OMARCHY_PATH until then.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
GITEA=${GITEA:-https://git.zacharyzhang.com}
|
||||
OWNER=${OWNER:-ZacharyZhang-NY}
|
||||
REGISTRY="$GITEA/api/packages/$OWNER/arch"
|
||||
REG_KEY_FPR=74DCF57ACD812B24D959F146BD386048867B33B4
|
||||
STATE_DIR=/var/lib/omarchycn
|
||||
STATE=$STATE_DIR/convert-state
|
||||
# Bootstrap exception: curl|bash runs outside a session, so default the path
|
||||
export OMARCHY_PATH=${OMARCHY_PATH:-/usr/share/omarchy}
|
||||
|
||||
main() {
|
||||
command -v pacman > /dev/null || { echo "需要 Arch Linux(未找到 pacman)" >&2; exit 1; }
|
||||
if (( EUID == 0 )); then
|
||||
echo "请以普通用户运行(脚本内部按需 sudo)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local prev_omarchy prev_settings
|
||||
prev_omarchy=$(pacman -Q omarchy 2> /dev/null || pacman -Q omarchy-dev 2> /dev/null || true)
|
||||
prev_settings=$(pacman -Q omarchy-settings 2> /dev/null || pacman -Q omarchy-settings-dev 2> /dev/null || true)
|
||||
if [[ -z $prev_omarchy || -z $prev_settings ]]; then
|
||||
if [[ -d $HOME/.local/share/omarchy/.git ]]; then
|
||||
echo "检测到 checkout 安装(非包安装):请改用 overlay(bin/omarchycn-install-overlay)" >&2
|
||||
else
|
||||
echo "未检测到 Omarchy 包安装(需要 omarchy 与 omarchy-settings)" >&2
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
[[ -f $STATE ]] && echo "已转换过($STATE 在案):本次仅同步更新,不覆盖回退状态"
|
||||
|
||||
echo "==> 信任 OmarchyCN registry 公钥并配置 [omarchycn] 仓库"
|
||||
local tmpkey
|
||||
tmpkey=$(mktemp)
|
||||
curl -fsSL "$REGISTRY/repository.key" -o "$tmpkey"
|
||||
sudo pacman-key --add "$tmpkey"
|
||||
sudo pacman-key --lsign-key "$REG_KEY_FPR"
|
||||
rm -f "$tmpkey"
|
||||
if ! grep -q '^\[omarchycn\]' /etc/pacman.conf; then
|
||||
printf '\n[omarchycn]\nSigLevel = Required DatabaseOptional\nServer = %s/omarchycn/x86_64\n' \
|
||||
"$REGISTRY" | sudo tee -a /etc/pacman.conf > /dev/null
|
||||
fi
|
||||
|
||||
if [[ ! -f $STATE ]]; then
|
||||
echo "==> 记录转换前状态(供 omarchy cn revert 还原)"
|
||||
sudo mkdir -p "$STATE_DIR"
|
||||
if [[ -f /etc/locale.conf ]]; then
|
||||
sudo cp /etc/locale.conf "$STATE_DIR/locale.conf.pre"
|
||||
fi
|
||||
sudo cp /etc/pacman.d/mirrorlist "$STATE_DIR/mirrorlist.pre"
|
||||
local p new_pkgs=""
|
||||
for p in fcitx5-rime fcitx5-chinese-addons fcitx5-configtool omarchycn-keyring; do
|
||||
pacman -Q "$p" > /dev/null 2>&1 || new_pkgs+="$p "
|
||||
done
|
||||
# Values are quoted because revert sources this file; publish atomically
|
||||
{
|
||||
echo "prev_omarchy=\"${prev_omarchy%% *}\""
|
||||
echo "prev_settings=\"${prev_settings%% *}\""
|
||||
echo "new_pkgs=\"${new_pkgs% }\""
|
||||
echo "converted_at=\"$(date +%s)\""
|
||||
} | sudo tee "$STATE.new" > /dev/null
|
||||
sudo mv "$STATE.new" "$STATE"
|
||||
fi
|
||||
|
||||
echo "==> 系统更新并换装 OmarchyCN 软件包(替换 ${prev_omarchy%% *}/${prev_settings%% *})"
|
||||
sudo pacman -Syu --noconfirm
|
||||
sudo pacman -S --noconfirm omarchycn/omarchycn-keyring
|
||||
# --ask=4 auto-confirms removing the conflicting upstream omarchy packages
|
||||
sudo pacman -S --noconfirm --ask=4 omarchycn/omarchy-dev omarchycn/omarchy-settings-dev
|
||||
sudo pacman -S --needed --noconfirm fcitx5-rime fcitx5-chinese-addons fcitx5-configtool
|
||||
|
||||
echo "==> [omarchy] 仓库启用自建镜像(Cloudflare 上游兜底)"
|
||||
source /usr/share/omarchy/cn/lib/mirror.sh
|
||||
cn_mirror_omarchy_repo_fix
|
||||
|
||||
echo "==> 系统语言 zh_CN.UTF-8"
|
||||
grep -q '^zh_CN.UTF-8 UTF-8' /etc/locale.gen ||
|
||||
echo 'zh_CN.UTF-8 UTF-8' | sudo tee -a /etc/locale.gen > /dev/null
|
||||
sudo locale-gen > /dev/null
|
||||
if grep -q '^LANG=' /etc/locale.conf 2> /dev/null; then
|
||||
sudo sed -i 's/^LANG=.*/LANG=zh_CN.UTF-8/' /etc/locale.conf
|
||||
else
|
||||
echo 'LANG=zh_CN.UTF-8' | sudo tee -a /etc/locale.conf > /dev/null
|
||||
fi
|
||||
|
||||
echo "==> 中文输入法与字体(用户级)"
|
||||
OMARCHY_PATH=/usr/share/omarchy bash /usr/share/omarchy/install/user/cn-chinese.sh
|
||||
systemctl --user restart omarchy-fcitx5.service 2> /dev/null || true
|
||||
|
||||
echo "==> Arch 官方仓库切换国内镜像(自动测速,失败则保留现状)"
|
||||
omarchy-cn-mirror-apply china || echo "国内镜像不可达,保留当前 mirrorlist"
|
||||
|
||||
echo "==> OMARCHY CN 开机画面"
|
||||
omarchy-refresh-plymouth
|
||||
|
||||
omarchy-notification-send "OmarchyCN" "转换完成:重新登录后菜单/键位/输入法全中文" 2> /dev/null || true
|
||||
echo
|
||||
echo "转换完成(重新登录生效)。回退: omarchy cn revert"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/bin/bash
|
||||
# omarchy:summary=Apply china or official registry profile to development tools
|
||||
# omarchy:args=<china|official> [--target npm,pip,cargo,go,gem,docker]
|
||||
# omarchy:examples=omarchycn dev-mirror apply china | omarchycn dev-mirror apply official --target npm,pip
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$OMARCHY_PATH/cn/lib/dev-mirror.sh"
|
||||
|
||||
profile="${1:?usage: omarchycn dev-mirror apply <china|official> [--target a,b]}"
|
||||
shift
|
||||
|
||||
if [[ $profile != "china" && $profile != "official" ]]; then
|
||||
echo "Unknown profile: $profile (expected china or official)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
targets=("${CN_DM_TARGETS[@]}")
|
||||
if [[ ${1:-} == "--target" ]]; then
|
||||
IFS=',' read -ra targets <<<"${2:?--target needs a comma-separated list}"
|
||||
fi
|
||||
|
||||
stamp=$(date +%Y%m%d-%H%M%S)
|
||||
|
||||
for target in "${targets[@]}"; do
|
||||
if [[ ! " ${CN_DM_TARGETS[*]} " == *" $target "* ]]; then
|
||||
echo "Unknown target: $target (known: ${CN_DM_TARGETS[*]})" >&2
|
||||
exit 1
|
||||
fi
|
||||
url=$(cn_dm_url "$target" "$profile")
|
||||
cn_dm_backup "$(cn_dm_config_file "$target")" "$stamp"
|
||||
"cn_dm_set_$target" "$url"
|
||||
echo "$target -> ${url:-<default>}"
|
||||
done
|
||||
|
||||
echo "Backups (if any): $CN_DM_BACKUP_ROOT/$stamp"
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/bin/bash
|
||||
# omarchy:summary=Check reachability of each configured development registry
|
||||
# omarchy:examples=omarchycn dev-mirror doctor
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$OMARCHY_PATH/cn/lib/dev-mirror.sh"
|
||||
|
||||
failures=0
|
||||
|
||||
for target in "${CN_DM_TARGETS[@]}"; do
|
||||
current=$("cn_dm_get_$target")
|
||||
if [[ -z $current ]]; then
|
||||
echo "INFO $target: using tool default"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Reduce to a probe-able https URL
|
||||
probe="${current#sparse+}"
|
||||
probe="${probe%%,*}"
|
||||
if curl -sSf -o /dev/null -m 8 --connect-timeout 5 "$probe" 2>/dev/null; then
|
||||
echo "PASS $target: $current"
|
||||
elif curl -sS -o /dev/null -m 8 --connect-timeout 5 -w '%{http_code}' "$probe" 2>/dev/null | grep -qE '^[34]'; then
|
||||
# Registry roots often answer 3xx/404 to bare GET while the service works
|
||||
echo "PASS $target: $current"
|
||||
else
|
||||
echo "FAIL $target: $current unreachable"
|
||||
failures=$((failures + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
exit $((failures > 0 ? 1 : 0))
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
# omarchy:summary=Show configured registry for each development ecosystem
|
||||
# omarchy:examples=omarchycn dev-mirror list
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$OMARCHY_PATH/cn/lib/dev-mirror.sh"
|
||||
|
||||
printf '%-8s %-52s %s\n' "TARGET" "CURRENT" "CONFIG"
|
||||
|
||||
for target in "${CN_DM_TARGETS[@]}"; do
|
||||
current=$("cn_dm_get_$target")
|
||||
printf '%-8s %-52s %s\n' "$target" "${current:-<default>}" "$(cn_dm_config_file "$target")"
|
||||
done
|
||||
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
# omarchy:summary=Point one development ecosystem at a custom registry URL
|
||||
# omarchy:args=<target> <url>
|
||||
# omarchy:examples=omarchycn dev-mirror set npm https://registry.example.com
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$OMARCHY_PATH/cn/lib/dev-mirror.sh"
|
||||
|
||||
target="${1:?usage: omarchycn dev-mirror set <target> <url>}"
|
||||
url="${2:?usage: omarchycn dev-mirror set <target> <url>}"
|
||||
|
||||
if [[ ! " ${CN_DM_TARGETS[*]} " == *" $target "* ]]; then
|
||||
echo "Unknown target: $target (known: ${CN_DM_TARGETS[*]})" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cn_dm_backup "$(cn_dm_config_file "$target")" "$(date +%Y%m%d-%H%M%S)"
|
||||
"cn_dm_set_$target" "$url"
|
||||
echo "$target -> $url"
|
||||
@@ -0,0 +1,61 @@
|
||||
#!/bin/bash
|
||||
# omarchy:summary=Apply a display scale preset with 15s revert confirmation
|
||||
# omarchy:args=<1.0|1.25|1.5|1.6|1.75|2.0>
|
||||
# omarchy:examples=omarchycn display scale 1.6
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
preset="${1:?usage: omarchycn display scale <1.0|1.25|1.5|1.6|1.75|2.0>}"
|
||||
|
||||
case "$preset" in
|
||||
1.0 | 1.25 | 1.5 | 1.6 | 1.75 | 2.0) ;;
|
||||
*)
|
||||
echo "Unknown preset: $preset (supported: 1.0 1.25 1.5 1.6 1.75 2.0)" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
monitors="$HOME/.config/hypr/monitors.lua"
|
||||
if [[ ! -f $monitors ]]; then
|
||||
omarchy-refresh-config hypr/monitors.lua
|
||||
fi
|
||||
|
||||
gdk=$(printf '%.0f' "$preset")
|
||||
backup="$monitors.omarchycn-prev"
|
||||
cp "$monitors" "$backup"
|
||||
|
||||
sed -i -E \
|
||||
-e "s|^local omarchy_monitor_scale = .*|local omarchy_monitor_scale = $preset|" \
|
||||
-e "s|^local omarchy_gdk_scale = .*|local omarchy_gdk_scale = $gdk|" \
|
||||
"$monitors"
|
||||
|
||||
if ! grep -q "omarchy_monitor_scale = $preset" "$monitors" ||
|
||||
! grep -q "omarchy_gdk_scale = $gdk" "$monitors"; then
|
||||
mv "$backup" "$monitors"
|
||||
echo "monitors.lua lacks the omarchy scale lines; reverted, edit it manually" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Monitor scale: $preset, GDK scale: $gdk"
|
||||
|
||||
if [[ -z ${HYPRLAND_INSTANCE_SIGNATURE:-} ]]; then
|
||||
rm -f "$backup"
|
||||
echo "当前不在 Hyprland 会话内,重登录后生效"
|
||||
elif ! hyprctl reload > /dev/null 2>&1; then
|
||||
mv "$backup" "$monitors"
|
||||
echo "hyprctl reload 失败,已恢复原配置" >&2
|
||||
exit 1
|
||||
elif [[ -t 0 ]]; then
|
||||
echo "15 秒内按 y 保留新缩放,超时或按其他键自动恢复"
|
||||
if read -r -t 15 -n 1 answer && [[ $answer == "y" ]]; then
|
||||
rm -f "$backup"
|
||||
echo "已保留"
|
||||
else
|
||||
mv "$backup" "$monitors"
|
||||
hyprctl reload > /dev/null 2>&1
|
||||
echo "已恢复原缩放"
|
||||
fi
|
||||
else
|
||||
omarchy-notification-send "OmarchyCN" "缩放已改为 $preset;如异常执行: mv $backup $monitors && hyprctl reload" 2> /dev/null || true
|
||||
echo "无终端交互:已应用;恢复命令: mv $backup $monitors && hyprctl reload"
|
||||
fi
|
||||
@@ -0,0 +1,124 @@
|
||||
#!/bin/bash
|
||||
# omarchy:summary=Check network, pacman mirror, and dev registries; --fix fails over mirrors
|
||||
# omarchy:args=[network|mirror|dev-mirror] [--fix]
|
||||
# omarchy:examples=omarchycn doctor | omarchycn doctor mirror --fix
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$OMARCHY_PATH/cn/lib/mirror.sh"
|
||||
|
||||
module="all"
|
||||
fix="false"
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
all | network | mirror | dev-mirror | ime) module="$arg" ;;
|
||||
--fix) fix="true" ;;
|
||||
*)
|
||||
echo "Unknown argument: $arg" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
failures=0
|
||||
|
||||
check_network() {
|
||||
local host
|
||||
for host in mirrors.tuna.tsinghua.edu.cn archlinux.org; do
|
||||
if getent hosts "$host" > /dev/null 2>&1; then
|
||||
echo "PASS network: DNS resolves $host"
|
||||
else
|
||||
echo "FAIL network: cannot resolve $host"
|
||||
failures=$((failures + 1))
|
||||
fi
|
||||
done
|
||||
if curl -sSf -o /dev/null -m 8 --connect-timeout 5 https://www.baidu.com 2>/dev/null; then
|
||||
echo "PASS network: HTTPS reachable (baidu.com)"
|
||||
else
|
||||
echo "FAIL network: HTTPS unreachable (baidu.com)"
|
||||
failures=$((failures + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
check_mirror() {
|
||||
local primary speed ttfb age
|
||||
|
||||
primary=$(grep -sE '^Server' "$CN_MIRRORLIST" | head -1 | sed -E 's/^Server = //; s|/\$repo/os/\$arch/?$||' || true)
|
||||
if [[ -z $primary ]]; then
|
||||
echo "FAIL mirror: no Server entries in $CN_MIRRORLIST"
|
||||
failures=$((failures + 1))
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "INFO mirror: primary $primary"
|
||||
read -r speed ttfb age < <(cn_mirror_probe "$primary")
|
||||
if (( speed > 0 )) && [[ $age != "stale" ]]; then
|
||||
echo "PASS mirror: primary healthy ($((speed / 1024)) KB/s, sync age ${age}s)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "FAIL mirror: primary unhealthy (speed=$speed age=$age)"
|
||||
failures=$((failures + 1))
|
||||
|
||||
if [[ $fix == "true" && -f $CN_PROFILE_FILE && $(<"$CN_PROFILE_FILE") == "china" ]]; then
|
||||
echo "INFO mirror: re-applying china profile (auto-failover)"
|
||||
if omarchy-cn-mirror-apply china; then
|
||||
failures=$((failures - 1))
|
||||
omarchy-notification-send "OmarchyCN" "pacman 镜像已自动切换" 2> /dev/null || true
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# refresh-pacman restores the checkout's template, which on overlay installs
|
||||
# has no OmarchyCN mirror line; this check makes that loss visible and fixable
|
||||
check_omarchy_repo() {
|
||||
local conf="${OMARCHYCN_PACMAN_CONF:-/etc/pacman.conf}"
|
||||
|
||||
if ! grep -q "^Server = https://pkgs.omarchy.org/stable/" "$conf"; then
|
||||
echo "INFO omarchy-repo: 非 stable 通道,跳过 OmarchyCN 镜像检查"
|
||||
return 0
|
||||
fi
|
||||
# --fix runs the helper even when the line is present: it also heals a missing key
|
||||
if [[ $fix == "true" ]]; then
|
||||
if cn_mirror_omarchy_repo_fix && cn_mirror_omarchy_repo_ok; then
|
||||
echo "PASS omarchy-repo: OmarchyCN 镜像行与 registry 公钥在位"
|
||||
return 0
|
||||
fi
|
||||
elif cn_mirror_omarchy_repo_ok; then
|
||||
echo "PASS omarchy-repo: OmarchyCN 镜像行在位"
|
||||
return 0
|
||||
fi
|
||||
echo "FAIL omarchy-repo: [omarchy] 缺 OmarchyCN 镜像行(omarchycn doctor mirror --fix 可修复)"
|
||||
failures=$((failures + 1))
|
||||
}
|
||||
|
||||
check_dev_mirror() {
|
||||
omarchy-cn-dev-mirror-doctor || failures=$((failures + 1))
|
||||
}
|
||||
|
||||
check_ime() {
|
||||
omarchy-cn-ime-status || failures=$((failures + 1))
|
||||
}
|
||||
|
||||
case "$module" in
|
||||
network) check_network ;;
|
||||
mirror)
|
||||
check_mirror
|
||||
check_omarchy_repo
|
||||
;;
|
||||
dev-mirror) check_dev_mirror ;;
|
||||
ime) check_ime ;;
|
||||
all)
|
||||
check_network
|
||||
check_mirror
|
||||
check_omarchy_repo
|
||||
check_dev_mirror
|
||||
check_ime
|
||||
;;
|
||||
esac
|
||||
|
||||
if (( failures > 0 )); then
|
||||
echo "Doctor: $failures failure(s)"
|
||||
exit 1
|
||||
fi
|
||||
echo "Doctor: all checks passed"
|
||||
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
# omarchy:summary=Install CJK fonts and the OmarchyCN fontconfig priority
|
||||
# omarchy:examples=omarchycn font apply
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
omarchy-pkg-add noto-fonts-cjk noto-fonts-emoji
|
||||
|
||||
conf_dir="$HOME/.config/fontconfig/conf.d"
|
||||
mkdir -p "$conf_dir"
|
||||
cp "$OMARCHY_PATH/cn/fontconfig/64-omarchycn-cjk.conf" "$conf_dir/"
|
||||
fc-cache -f > /dev/null
|
||||
|
||||
echo "CJK fonts installed, fontconfig priority applied ($conf_dir/64-omarchycn-cjk.conf)"
|
||||
omarchy-cn-font-status
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/bin/bash
|
||||
# omarchy:summary=Check Chinese font availability and fallback correctness
|
||||
# omarchy:examples=omarchycn font status
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
failures=0
|
||||
|
||||
for family in "Noto Sans CJK SC" "Noto Serif CJK SC" "Noto Sans Mono CJK SC"; do
|
||||
matched=$(fc-match --format '%{family}' "$family")
|
||||
if [[ $matched == *"$family"* ]]; then
|
||||
echo "PASS font: $family"
|
||||
else
|
||||
echo "FAIL font: $family missing (got: $matched)"
|
||||
failures=$((failures + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
zh_sans=$(fc-match --format '%{family}' sans-serif:lang=zh-cn)
|
||||
if [[ $zh_sans == *"CJK SC"* ]]; then
|
||||
echo "PASS fallback: zh-cn sans-serif -> $zh_sans"
|
||||
else
|
||||
echo "FAIL fallback: zh-cn sans-serif -> $zh_sans (expected SC variant)"
|
||||
failures=$((failures + 1))
|
||||
fi
|
||||
|
||||
# Untagged requests (Chromium/Electron path) must reach SC before JP/KR
|
||||
untagged=$(fc-match --sort sans-serif | grep -m1 -oE 'CJK (SC|JP|TC|KR|HK)' || true)
|
||||
if [[ $untagged == "CJK SC" ]]; then
|
||||
echo "PASS fallback: untagged CJK -> SC"
|
||||
else
|
||||
echo "FAIL fallback: untagged CJK -> ${untagged:-none} (expected SC)"
|
||||
failures=$((failures + 1))
|
||||
fi
|
||||
|
||||
exit $((failures > 0 ? 1 : 0))
|
||||
@@ -0,0 +1,28 @@
|
||||
#!/bin/bash
|
||||
# omarchy:summary=Install Fcitx5 Rime and Chinese addons with a default profile
|
||||
# omarchy:examples=omarchycn ime apply
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
omarchy-pkg-add fcitx5-rime fcitx5-chinese-addons fcitx5-configtool
|
||||
|
||||
profile_dir="$HOME/.config/fcitx5"
|
||||
mkdir -p "$profile_dir"
|
||||
|
||||
if [[ -f $profile_dir/profile && "$(<"$profile_dir/profile")" != "$(<"$OMARCHY_PATH/cn/fcitx5/profile")" ]]; then
|
||||
backup_dir="$HOME/.local/state/omarchycn/backups/ime/$(date +%Y%m%d-%H%M%S)"
|
||||
mkdir -p "$backup_dir"
|
||||
cp "$profile_dir/profile" "$backup_dir/profile"
|
||||
echo "Existing profile backed up to $backup_dir"
|
||||
fi
|
||||
|
||||
cp "$OMARCHY_PATH/cn/fcitx5/profile" "$profile_dir/profile"
|
||||
|
||||
if systemctl --user is-active omarchy-fcitx5.service > /dev/null 2>&1; then
|
||||
systemctl --user restart omarchy-fcitx5.service
|
||||
echo "Fcitx5 restarted with Rime profile"
|
||||
else
|
||||
echo "Fcitx5 profile installed (takes effect at next graphical session)"
|
||||
fi
|
||||
|
||||
omarchy-cn-ime-status
|
||||
@@ -0,0 +1,63 @@
|
||||
#!/bin/bash
|
||||
# omarchy:summary=Set the input method toggle key, migrating the menu key if needed
|
||||
# omarchy:args=<ctrl-space|super-space>
|
||||
# omarchy:examples=omarchycn ime hotkey ctrl-space | omarchycn ime hotkey super-space
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
choice="${1:?usage: omarchycn ime hotkey <ctrl-space|super-space>}"
|
||||
fcitx_config="$HOME/.config/fcitx5/config"
|
||||
bindings="$HOME/.config/hypr/bindings.lua"
|
||||
|
||||
# Replace or append the [Hotkey/TriggerKeys] section in fcitx5 global config
|
||||
set_trigger() {
|
||||
local key="$1"
|
||||
|
||||
mkdir -p "${fcitx_config%/*}"
|
||||
touch "$fcitx_config"
|
||||
awk '
|
||||
/^\[Hotkey\/TriggerKeys\]$/ { skip = 1; next }
|
||||
/^\[/ { skip = 0 }
|
||||
!skip { print }
|
||||
' "$fcitx_config" > "$fcitx_config.omarchycn-tmp"
|
||||
printf '[Hotkey/TriggerKeys]\n0=%s\n' "$key" >> "$fcitx_config.omarchycn-tmp"
|
||||
mv "$fcitx_config.omarchycn-tmp" "$fcitx_config"
|
||||
}
|
||||
|
||||
remove_menu_migration() {
|
||||
if [[ -f $bindings ]]; then
|
||||
sed -i '/^-- OmarchyCN ime hotkey begin$/,/^-- OmarchyCN ime hotkey end$/d' "$bindings"
|
||||
fi
|
||||
}
|
||||
|
||||
case "$choice" in
|
||||
ctrl-space)
|
||||
set_trigger "Control+space"
|
||||
remove_menu_migration
|
||||
echo "输入法切换键: Ctrl+Space(Omarchy 菜单保持 Super+Space)"
|
||||
;;
|
||||
super-space)
|
||||
set_trigger "Super+space"
|
||||
mkdir -p "${bindings%/*}"
|
||||
touch "$bindings"
|
||||
remove_menu_migration
|
||||
cat >> "$bindings" <<'EOF'
|
||||
-- OmarchyCN ime hotkey begin
|
||||
hl.unbind("SUPER + SPACE")
|
||||
hl.unbind("SUPER + ALT + SPACE")
|
||||
o.bind("SUPER + ALT + SPACE", "Omarchy menu", "omarchy-menu toggle")
|
||||
-- OmarchyCN ime hotkey end
|
||||
EOF
|
||||
echo "输入法切换键: Super+Space"
|
||||
echo "Omarchy 菜单已迁移至 Super+Alt+Space(写入 $bindings)"
|
||||
echo "原 Super+Alt+Space 的 Apps 菜单让位,可从根菜单进入或自行改绑"
|
||||
;;
|
||||
*)
|
||||
echo "Unknown hotkey choice: $choice (expected ctrl-space or super-space)" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if systemctl --user is-active omarchy-fcitx5.service > /dev/null 2>&1; then
|
||||
systemctl --user restart omarchy-fcitx5.service
|
||||
fi
|
||||
@@ -0,0 +1,54 @@
|
||||
#!/bin/bash
|
||||
# omarchy:summary=Check Fcitx5 Rime installation and configuration
|
||||
# omarchy:examples=omarchycn ime status
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
failures=0
|
||||
|
||||
for pkg in fcitx5 fcitx5-gtk fcitx5-qt fcitx5-rime fcitx5-chinese-addons; do
|
||||
if pacman -Q "$pkg" > /dev/null 2>&1; then
|
||||
echo "PASS ime: $pkg installed"
|
||||
else
|
||||
echo "FAIL ime: $pkg missing"
|
||||
failures=$((failures + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
if grep -sq '^Name=rime$' "$HOME/.config/fcitx5/profile"; then
|
||||
echo "PASS ime: rime in fcitx5 profile"
|
||||
else
|
||||
echo "FAIL ime: rime not in fcitx5 profile (run: omarchycn ime apply)"
|
||||
failures=$((failures + 1))
|
||||
fi
|
||||
|
||||
if grep -sq '^DefaultIM=rime$' "$HOME/.config/fcitx5/profile"; then
|
||||
echo "PASS ime: rime is the default input method"
|
||||
else
|
||||
echo "FAIL ime: DefaultIM is not rime"
|
||||
failures=$((failures + 1))
|
||||
fi
|
||||
|
||||
env_file="/usr/share/omarchy/default/environment.d/10-omarchy-fcitx.conf"
|
||||
if [[ -f $env_file ]] || [[ -f $OMARCHY_PATH/default/environment.d/10-omarchy-fcitx.conf ]]; then
|
||||
echo "PASS ime: fcitx environment file present"
|
||||
else
|
||||
echo "FAIL ime: fcitx environment file missing"
|
||||
failures=$((failures + 1))
|
||||
fi
|
||||
|
||||
trigger=$(grep -sA2 '^\[Hotkey/TriggerKeys\]' "$HOME/.config/fcitx5/config" | grep -sE '^0=' | cut -d= -f2- || true)
|
||||
if [[ $trigger == "Super+space" ]] && ! grep -sq 'OmarchyCN ime hotkey begin' "$HOME/.config/hypr/bindings.lua"; then
|
||||
echo "FAIL ime: Super+space 与 Omarchy 菜单冲突 (run: omarchycn ime hotkey super-space)"
|
||||
failures=$((failures + 1))
|
||||
else
|
||||
echo "PASS ime: trigger key ${trigger:-Control+space (fcitx5 default)}"
|
||||
fi
|
||||
|
||||
if pgrep -x fcitx5 > /dev/null 2>&1; then
|
||||
echo "PASS ime: fcitx5 running"
|
||||
else
|
||||
echo "INFO ime: fcitx5 not running (headless session?)"
|
||||
fi
|
||||
|
||||
exit $((failures > 0 ? 1 : 0))
|
||||
@@ -0,0 +1,148 @@
|
||||
#!/bin/bash
|
||||
# omarchy:summary=Install or remove the OmarchyCN layer on an existing Omarchy
|
||||
# omarchy:args=[--uninstall]
|
||||
# omarchy:examples=omarchy-cn-install-overlay | omarchy-cn-install-overlay --uninstall
|
||||
# Bootstraps onto a foreign OMARCHY_PATH, so it resolves its own checkout root.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SRC=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
DEST="${OMARCHY_PATH:?OMARCHY_PATH must point at the target Omarchy install}"
|
||||
MANIFEST="$HOME/.local/state/omarchycn/overlay-manifest"
|
||||
|
||||
run_in() {
|
||||
local root="$1"
|
||||
shift
|
||||
if [[ -w $root ]]; then
|
||||
"$@"
|
||||
else
|
||||
sudo "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
valid_entry() {
|
||||
[[ $1 == "cn" || $1 =~ ^bin/(omarchycn|omarchy-cn-[a-z0-9-]+)$ ]]
|
||||
}
|
||||
|
||||
MENU_EXT="$HOME/.config/omarchy/extensions/omarchy-menu.jsonc"
|
||||
MENU_MARKER="// OmarchyCN overlay menu extension"
|
||||
|
||||
# Comments-only files (upstream ships such a template) hold no user content
|
||||
menu_ext_has_user_content() {
|
||||
[[ -f $MENU_EXT ]] || return 1
|
||||
grep -qF "$MENU_MARKER" "$MENU_EXT" && return 1
|
||||
[[ -n $(grep -vE '^[[:space:]]*(//.*)?$' "$MENU_EXT" | tr -d '[:space:]{}') ]]
|
||||
}
|
||||
|
||||
install_menu_extension() {
|
||||
if menu_ext_has_user_content; then
|
||||
echo "跳过菜单扩展:$MENU_EXT 已有用户内容,请手动合并 default/omarchy/omarchy-menu.jsonc 的 OmarchyCN 段"
|
||||
return 0
|
||||
fi
|
||||
mkdir -p "${MENU_EXT%/*}"
|
||||
{
|
||||
echo "$MENU_MARKER"
|
||||
echo "{"
|
||||
sed -n '/^ \/\/ OmarchyCN$/,$p' "$SRC/default/omarchy/omarchy-menu.jsonc" | sed '$d'
|
||||
echo "}"
|
||||
} > "$MENU_EXT"
|
||||
echo "OmarchyCN 菜单扩展已写入 $MENU_EXT"
|
||||
}
|
||||
|
||||
remove_menu_extension() {
|
||||
if [[ -f $MENU_EXT ]] && grep -qF "$MENU_MARKER" "$MENU_EXT"; then
|
||||
rm -f "$MENU_EXT"
|
||||
fi
|
||||
}
|
||||
|
||||
uninstall() {
|
||||
if [[ ! -f $MANIFEST ]]; then
|
||||
echo "No overlay manifest at $MANIFEST" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local dest="" rel=""
|
||||
dest=$(grep -m1 '^dest=' "$MANIFEST" | cut -d= -f2-)
|
||||
if [[ -z $dest || ! -d $dest ]]; then
|
||||
echo "Manifest has no valid dest= record" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate every entry before deleting anything
|
||||
while IFS= read -r rel; do
|
||||
[[ $rel == source=* || $rel == dest=* ]] && continue
|
||||
if ! valid_entry "$rel"; then
|
||||
echo "Refusing suspicious manifest entry: $rel (nothing deleted)" >&2
|
||||
exit 1
|
||||
fi
|
||||
done < "$MANIFEST"
|
||||
|
||||
while IFS= read -r rel; do
|
||||
[[ $rel == source=* || $rel == dest=* ]] && continue
|
||||
if [[ $rel == "cn" ]]; then
|
||||
run_in "$dest" rm -rf "$dest/cn"
|
||||
else
|
||||
run_in "$dest" rm -f "$dest/$rel"
|
||||
fi
|
||||
done < "$MANIFEST"
|
||||
|
||||
remove_menu_extension
|
||||
rm -f "$MANIFEST"
|
||||
echo "OmarchyCN overlay removed from $dest"
|
||||
}
|
||||
|
||||
install() {
|
||||
if [[ ! -f $DEST/version || ! -x $DEST/bin/omarchy ]]; then
|
||||
echo "No Omarchy install found at $DEST" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ $SRC == "$DEST" ]]; then
|
||||
echo "Source checkout and target are the same tree; nothing to overlay" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Reinstall: remove the previous overlay first so upgrades leave no residue
|
||||
if [[ -f $MANIFEST ]]; then
|
||||
uninstall
|
||||
fi
|
||||
|
||||
if [[ -e $DEST/cn ]]; then
|
||||
echo "Refusing to take over unmanaged $DEST/cn" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local f rel entries=()
|
||||
for f in "$SRC/bin/omarchycn" "$SRC"/bin/omarchy-cn-*; do
|
||||
rel="bin/${f##*/}"
|
||||
if [[ -e $DEST/$rel ]]; then
|
||||
echo "Refusing to overwrite unmanaged $DEST/$rel" >&2
|
||||
exit 1
|
||||
fi
|
||||
entries+=("$rel")
|
||||
done
|
||||
|
||||
# Manifest is the intent record, published BEFORE copying: if a copy fails
|
||||
# midway, a rerun sees the manifest, uninstalls the partial state, retries.
|
||||
mkdir -p "${MANIFEST%/*}"
|
||||
{
|
||||
echo "source=$SRC"
|
||||
echo "dest=$DEST"
|
||||
echo "cn"
|
||||
printf '%s\n' "${entries[@]}"
|
||||
} > "$MANIFEST"
|
||||
|
||||
run_in "$DEST" cp -r "$SRC/cn" "$DEST/cn"
|
||||
for rel in "${entries[@]}"; do
|
||||
run_in "$DEST" cp "$SRC/$rel" "$DEST/$rel"
|
||||
done
|
||||
|
||||
install_menu_extension
|
||||
echo "OmarchyCN overlay installed into $DEST (${#entries[@]} commands)"
|
||||
echo "Verify: omarchy cn version"
|
||||
}
|
||||
|
||||
if [[ ${1:-} == "--uninstall" ]]; then
|
||||
uninstall
|
||||
else
|
||||
install
|
||||
fi
|
||||
@@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
# omarchy:summary=Install Kimi Code with the official installer script
|
||||
# omarchy:examples=omarchycn kimi install
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if omarchy-cmd-present kimi; then
|
||||
echo "Kimi Code 已安装"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
installer=$(mktemp)
|
||||
trap 'rm -f "$installer"' EXIT
|
||||
curl -fsSL https://code.kimi.com/kimi-code/install.sh -o "$installer"
|
||||
bash "$installer"
|
||||
|
||||
# The official installer only touches .bashrc PATH; expose kimi now
|
||||
if [[ ! -x $HOME/.kimi-code/bin/kimi ]]; then
|
||||
echo "kimi binary missing after the official installer" >&2
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p "$HOME/.local/bin"
|
||||
ln -sf "$HOME/.kimi-code/bin/kimi" "$HOME/.local/bin/kimi"
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
# omarchy:summary=Generate zh_CN.UTF-8 and en_US.UTF-8 locales
|
||||
# omarchy:examples=omarchycn locale apply
|
||||
# omarchy:requires-sudo=true
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
for loc in zh_CN en_US; do
|
||||
if ! grep -qE "^${loc}\.UTF-8 UTF-8" /etc/locale.gen; then
|
||||
echo "${loc}.UTF-8 UTF-8" | sudo tee -a /etc/locale.gen > /dev/null
|
||||
fi
|
||||
done
|
||||
|
||||
sudo locale-gen
|
||||
|
||||
for loc in zh_CN en_US; do
|
||||
if ! locale -a | grep -qiE "^${loc}\.utf-?8$"; then
|
||||
echo "FAIL locale: ${loc}.UTF-8 not generated" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "PASS locale: ${loc}.UTF-8"
|
||||
done
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
# omarchy:summary=Run pending OmarchyCN migrations
|
||||
# omarchy:examples=omarchycn migrate
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
MIGRATIONS_DIR="$OMARCHY_PATH/cn/migrations"
|
||||
DONE_DIR="$HOME/.local/state/omarchycn/migrations-done"
|
||||
|
||||
[[ -d $MIGRATIONS_DIR ]] || exit 0
|
||||
|
||||
mkdir -p "$DONE_DIR"
|
||||
for m in "$MIGRATIONS_DIR"/*.sh; do
|
||||
[[ -f $m ]] || continue
|
||||
name="${m##*/}"
|
||||
if [[ ! -f $DONE_DIR/$name ]]; then
|
||||
echo "OmarchyCN migration: $name"
|
||||
bash -euo pipefail "$m"
|
||||
touch "$DONE_DIR/$name"
|
||||
fi
|
||||
done
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/bin/bash
|
||||
# omarchy:summary=Apply a mirror profile or pin one mirror for pacman
|
||||
# omarchy:args=<china|official|mirror-id>
|
||||
# omarchy:examples=omarchycn mirror apply china | omarchycn mirror apply official | omarchycn mirror apply tuna
|
||||
# omarchy:requires-sudo=true
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$OMARCHY_PATH/cn/lib/mirror.sh"
|
||||
|
||||
target="${1:?usage: omarchycn mirror apply <china|official|mirror-id>}"
|
||||
|
||||
case "$target" in
|
||||
china)
|
||||
echo "Benchmarking China mirrors..."
|
||||
mapfile -t cn_ids < <(cn_mirror_ids_by_region cn)
|
||||
mapfile -t ranked < <(cn_mirror_rank "${cn_ids[@]}")
|
||||
if (( ${#ranked[@]} == 0 )); then
|
||||
echo "No healthy China mirror reachable; keeping current mirrorlist" >&2
|
||||
exit 1
|
||||
fi
|
||||
# Primary + up to two backups
|
||||
cn_mirror_write_list china "${ranked[@]:0:3}"
|
||||
;;
|
||||
official)
|
||||
cn_mirror_write_list official geo worldwide
|
||||
;;
|
||||
*)
|
||||
url=$(cn_mirror_url "$target")
|
||||
if [[ -z $url ]]; then
|
||||
echo "Unknown mirror id: $target (see: omarchycn mirror benchmark)" >&2
|
||||
exit 1
|
||||
fi
|
||||
cn_mirror_write_list "pin:$target" "$target"
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
# omarchy:summary=Benchmark Arch mirrors for speed, latency, and sync freshness
|
||||
# omarchy:examples=omarchycn mirror benchmark
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$OMARCHY_PATH/cn/lib/mirror.sh"
|
||||
|
||||
printf '%-11s %-22s %12s %8s %10s\n' "ID" "NAME" "SPEED" "TTFB" "SYNC"
|
||||
|
||||
for id in $(cn_mirror_ids); do
|
||||
url=$(cn_mirror_url "$id")
|
||||
read -r speed ttfb age < <(cn_mirror_probe "$url")
|
||||
|
||||
if (( speed > 0 )); then
|
||||
speed_h="$((speed / 1024)) KB/s"
|
||||
else
|
||||
speed_h="FAIL"
|
||||
fi
|
||||
case "$age" in
|
||||
stale) sync_h="STALE" ;;
|
||||
unknown) sync_h="?" ;;
|
||||
*) sync_h="$((age / 60))m ago" ;;
|
||||
esac
|
||||
|
||||
printf '%-11s %-22s %12s %7ss %10s\n' "$id" "$(cn_mirror_name "$id")" "$speed_h" "${ttfb:0:5}" "$sync_h"
|
||||
done
|
||||
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
# omarchy:summary=Pin pacman to a single mirror
|
||||
# omarchy:args=<mirror-id>
|
||||
# omarchy:examples=omarchycn mirror pin tuna
|
||||
# omarchy:requires-sudo=true
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
exec omarchy-cn-mirror-apply "${1:?usage: omarchycn mirror pin <mirror-id>}"
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
# omarchy:summary=Restore the most recent mirrorlist backup
|
||||
# omarchy:examples=omarchycn mirror restore
|
||||
# omarchy:requires-sudo=true
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$OMARCHY_PATH/cn/lib/mirror.sh"
|
||||
|
||||
backups=("$CN_MIRRORLIST".omarchycn-bak-*)
|
||||
if [[ ! -e ${backups[0]} ]]; then
|
||||
echo "No OmarchyCN mirrorlist backups found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
latest="${backups[-1]}"
|
||||
sudo cp "$latest" "$CN_MIRRORLIST"
|
||||
rm -f "$CN_PROFILE_FILE"
|
||||
echo "Restored $CN_MIRRORLIST from ${latest##*/}"
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
# omarchy:summary=Show current pacman mirror profile and servers
|
||||
# omarchy:examples=omarchycn mirror status
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$OMARCHY_PATH/cn/lib/mirror.sh"
|
||||
|
||||
if [[ -f $CN_PROFILE_FILE ]]; then
|
||||
echo "Profile: $(<"$CN_PROFILE_FILE")"
|
||||
else
|
||||
echo "Profile: not managed by OmarchyCN"
|
||||
fi
|
||||
|
||||
echo "Mirrorlist: $CN_MIRRORLIST"
|
||||
echo "Servers:"
|
||||
grep -E '^Server' "$CN_MIRRORLIST" | sed 's/^/ /'
|
||||
|
||||
backups=("$CN_MIRRORLIST".omarchycn-bak-*)
|
||||
if [[ -e ${backups[0]} ]]; then
|
||||
echo "Backups: ${#backups[@]} (latest: ${backups[-1]##*/})"
|
||||
fi
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/bin/bash
|
||||
# omarchy:summary=Restore development registry configs from a backup stamp
|
||||
# omarchy:args=<stamp>
|
||||
# omarchy:examples=omarchycn restore config 20260824-184243
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$OMARCHY_PATH/cn/lib/dev-mirror.sh"
|
||||
|
||||
stamp="${1:?usage: omarchycn restore config <stamp> (see: omarchycn restore list)}"
|
||||
dir="$CN_DM_BACKUP_ROOT/$stamp"
|
||||
|
||||
if [[ ! -d $dir ]]; then
|
||||
echo "No backup at $dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
restored=0
|
||||
for target in "${CN_DM_TARGETS[@]}"; do
|
||||
dest=$(cn_dm_config_file "$target")
|
||||
src="$dir/${dest##*/}"
|
||||
if [[ -f $src && $dest != /etc/* ]]; then
|
||||
mkdir -p "${dest%/*}"
|
||||
cp "$src" "$dest"
|
||||
echo "restored $target: $dest"
|
||||
restored=$((restored + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
if (( restored == 0 )); then
|
||||
echo "Nothing restored from $dir (system files like docker are not auto-restored)" >&2
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
# omarchy:summary=List OmarchyCN configuration backups
|
||||
# omarchy:examples=omarchycn restore list
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$OMARCHY_PATH/cn/lib/dev-mirror.sh"
|
||||
|
||||
echo "Dev-mirror backups ($CN_DM_BACKUP_ROOT):"
|
||||
if [[ -d $CN_DM_BACKUP_ROOT ]]; then
|
||||
for dir in "$CN_DM_BACKUP_ROOT"/*/; do
|
||||
[[ -d $dir ]] || continue
|
||||
stamp="${dir%/}"
|
||||
stamp="${stamp##*/}"
|
||||
echo " $stamp: $(ls "$dir" | tr '\n' ' ')"
|
||||
done
|
||||
else
|
||||
echo " (none)"
|
||||
fi
|
||||
|
||||
echo "Mirrorlist backups (/etc/pacman.d):"
|
||||
found="false"
|
||||
for f in /etc/pacman.d/mirrorlist.omarchycn-bak-*; do
|
||||
if [[ -e $f ]]; then
|
||||
echo " ${f##*/}"
|
||||
found="true"
|
||||
fi
|
||||
done
|
||||
if [[ $found == "false" ]]; then
|
||||
echo " (none)"
|
||||
fi
|
||||
@@ -0,0 +1,89 @@
|
||||
#!/bin/bash
|
||||
# omarchy:summary=Revert an OmarchyCN conversion back to vanilla Omarchy
|
||||
# omarchy:examples=omarchy cn revert
|
||||
# Curl-able and self-deleting-safe: main() is fully parsed before the package
|
||||
# swap removes this file from the cn tree it may be running from.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
STATE_DIR=/var/lib/omarchycn
|
||||
STATE=$STATE_DIR/convert-state
|
||||
MIRROR_LINE="Server = https://git.zacharyzhang.com/api/packages/ZacharyZhang-NY/arch/omarchy/x86_64"
|
||||
REG_KEY_FPR=74DCF57ACD812B24D959F146BD386048867B33B4
|
||||
# Bootstrap exception: curl|bash runs outside a session, so default the path
|
||||
export OMARCHY_PATH=${OMARCHY_PATH:-/usr/share/omarchy}
|
||||
|
||||
main() {
|
||||
if [[ ! -f $STATE ]]; then
|
||||
echo "未发现转换状态($STATE):本机不是由 omarchy cn convert 转换而来" >&2
|
||||
exit 1
|
||||
fi
|
||||
if (( EUID == 0 )); then
|
||||
echo "请以普通用户运行(脚本内部按需 sudo)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local prev_omarchy="" prev_settings="" new_pkgs=""
|
||||
# State is trusted root-owned key=value lines written by convert
|
||||
source "$STATE"
|
||||
[[ -n $prev_omarchy && -n $prev_settings ]] || { echo "转换状态损坏: $STATE" >&2; exit 1; }
|
||||
|
||||
echo "==> 清理转换时播种的用户配置(与 cn 层原件一致才删除)"
|
||||
local seeded
|
||||
for seeded in \
|
||||
"$HOME/.config/fcitx5/profile:/usr/share/omarchy/cn/fcitx5/profile" \
|
||||
"$HOME/.config/fontconfig/conf.d/64-omarchycn-cjk.conf:/usr/share/omarchy/cn/fontconfig/64-omarchycn-cjk.conf"; do
|
||||
if cmp -s "${seeded%%:*}" "${seeded#*:}" 2> /dev/null; then
|
||||
rm -f "${seeded%%:*}"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "==> 还原 mirrorlist 与系统语言"
|
||||
sudo cp "$STATE_DIR/mirrorlist.pre" /etc/pacman.d/mirrorlist
|
||||
if [[ -f $STATE_DIR/locale.conf.pre ]]; then
|
||||
sudo cp "$STATE_DIR/locale.conf.pre" /etc/locale.conf
|
||||
fi
|
||||
|
||||
echo "==> pacman.conf 移除 [omarchycn] 与 [omarchy] 镜像行"
|
||||
sudo sed -i "\|^$MIRROR_LINE\$|d" /etc/pacman.conf
|
||||
# Delete the section header and body, but keep any section that follows
|
||||
sudo sed -i '/^\[omarchycn\]$/,/^\[/{ /^\[omarchycn\]$/d; /^\[/!d; }' /etc/pacman.conf
|
||||
|
||||
echo "==> 换回上游软件包($prev_omarchy/$prev_settings)"
|
||||
sudo pacman -Sy
|
||||
# --ask=4 auto-confirms removing the conflicting cn packages
|
||||
sudo pacman -S --noconfirm --ask=4 "omarchy/$prev_omarchy" "omarchy/$prev_settings"
|
||||
if [[ -n $new_pkgs ]]; then
|
||||
# Only what is still installed: a user may have removed some already,
|
||||
# and a -R failure here would strand the swap-completed system
|
||||
local p still=""
|
||||
for p in $new_pkgs; do
|
||||
# grep without -q consumes the whole list: -q's early exit would
|
||||
# SIGPIPE pacman and read as "not installed" under pipefail
|
||||
pacman -Qq | grep -xF "$p" > /dev/null && still+="$p "
|
||||
done
|
||||
if [[ -n $still ]]; then
|
||||
echo "==> 移除转换时新装的包: ${still% }"
|
||||
# shellcheck disable=SC2086
|
||||
sudo pacman -R --noconfirm ${still% }
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "==> 移除 registry 公钥信任"
|
||||
sudo pacman-key --delete "$REG_KEY_FPR" > /dev/null 2>&1 || true
|
||||
|
||||
# The system is vanilla again: clear state before the cosmetic tail so a
|
||||
# plymouth failure cannot strand a reverted machine in "converted" state
|
||||
systemctl --user restart omarchy-fcitx5.service 2> /dev/null || true
|
||||
sudo rm -rf "$STATE_DIR"
|
||||
|
||||
echo "==> 还原上游开机画面"
|
||||
if ! omarchy-refresh-plymouth; then
|
||||
echo "开机画面还原失败:手动执行 omarchy-refresh-plymouth 即可" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo
|
||||
echo "已回退为原版 Omarchy(重新登录生效)。再次转换: omarchy cn convert"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -0,0 +1,120 @@
|
||||
#!/bin/bash
|
||||
# omarchy:summary=First-run wizard: language, timezone, scale, Chinese env, mirrors, apps, AI
|
||||
# omarchy:examples=omarchycn setup
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [[ ! -t 0 ]]; then
|
||||
echo "omarchycn setup 需要交互终端" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
STATE_DIR="$HOME/.local/state/omarchycn/setup-done"
|
||||
mkdir -p "$STATE_DIR"
|
||||
|
||||
run_step() {
|
||||
local step="$1" title="$2"
|
||||
shift 2
|
||||
|
||||
if [[ -f $STATE_DIR/$step ]]; then
|
||||
gum confirm "「$title」已完成,重新运行?" --default=false || return 0
|
||||
elif ! gum confirm "运行「$title」?(可跳过,稍后重进)"; then
|
||||
return 0
|
||||
fi
|
||||
echo "==> $title"
|
||||
if "$@"; then
|
||||
touch "$STATE_DIR/$step"
|
||||
else
|
||||
gum confirm "「$title」失败,继续后面的步骤?" || exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
step_language() {
|
||||
local choice env_file="$HOME/.config/environment.d/90-omarchycn-lang.conf"
|
||||
choice=$(gum choose --header "界面语言 LANG" "zh_CN.UTF-8" "en_US.UTF-8" "保持当前")
|
||||
if [[ $choice == "保持当前" ]]; then
|
||||
return 0
|
||||
fi
|
||||
mkdir -p "${env_file%/*}"
|
||||
echo "LANG=$choice" > "$env_file"
|
||||
echo "LANG=$choice 写入 $env_file(重登录生效)"
|
||||
}
|
||||
|
||||
step_timezone() {
|
||||
local choice
|
||||
choice=$(gum choose --header "时区" "Asia/Shanghai" "保持当前" "手动输入")
|
||||
if [[ $choice == "保持当前" ]]; then
|
||||
return 0
|
||||
fi
|
||||
if [[ $choice == "手动输入" ]]; then
|
||||
choice=$(timedatectl list-timezones | gum filter --header "选择时区")
|
||||
fi
|
||||
sudo timedatectl set-timezone "$choice"
|
||||
echo "时区: $(timedatectl show -p Timezone --value)"
|
||||
}
|
||||
|
||||
step_scale() {
|
||||
local choice
|
||||
choice=$(gum choose --header "显示缩放" "1.0" "1.25" "1.5" "1.6" "1.75" "2.0" "保持当前")
|
||||
if [[ $choice == "保持当前" ]]; then
|
||||
return 0
|
||||
fi
|
||||
omarchy-cn-display-scale "$choice"
|
||||
}
|
||||
|
||||
step_hotkey() {
|
||||
local choice
|
||||
choice=$(gum choose --header "输入法切换键" "ctrl-space" "super-space")
|
||||
omarchy-cn-ime-hotkey "$choice"
|
||||
}
|
||||
|
||||
step_mirror() {
|
||||
local choice
|
||||
choice=$(gum choose --header "pacman 镜像策略" "china" "official")
|
||||
omarchy-cn-mirror-apply "$choice"
|
||||
}
|
||||
|
||||
step_dev_mirror() {
|
||||
local choice
|
||||
choice=$(gum choose --header "开发工具镜像(npm/pip/cargo/go/gem)" "china" "official")
|
||||
omarchy-cn-dev-mirror-apply "$choice" --target npm,pip,cargo,go,gem
|
||||
}
|
||||
|
||||
step_apps() {
|
||||
local catalog picks app rc=0
|
||||
catalog=$(omarchy-cn-app-list | tail -n +2 | awk '{print $1}') || return 1
|
||||
picks=$(gum choose --no-limit --header "选择要安装的国内应用(空格多选,回车确认)" <<<"$catalog" || true)
|
||||
for app in $picks; do
|
||||
omarchy-cn-app-install "$app" || rc=1
|
||||
done
|
||||
return $rc
|
||||
}
|
||||
|
||||
step_privacy() {
|
||||
echo "隐私状态(无可配置项,如实告知):"
|
||||
echo " - OmarchyCN 不含遥测代码,默认无任何数据上报"
|
||||
echo " - 诊断信息仅在你手动运行 doctor 时本地生成,不上传"
|
||||
gum confirm "已了解" --affirmative "了解" --negative "" || true
|
||||
}
|
||||
|
||||
run_step language "语言 Language" step_language
|
||||
run_step timezone "时区 Timezone" step_timezone
|
||||
run_step scale "显示缩放 Display Scale" step_scale
|
||||
run_step locale "中文 Locale 生成" omarchy-cn-locale-apply
|
||||
run_step fonts "中文字体与 fallback" omarchy-cn-font-apply
|
||||
run_step ime "Fcitx5 + Rime 输入法" omarchy-cn-ime-apply
|
||||
run_step hotkey "输入法切换键" step_hotkey
|
||||
run_step mirror "pacman 镜像" step_mirror
|
||||
run_step dev-mirror "开发工具镜像" step_dev_mirror
|
||||
run_step apps "国内应用" step_apps
|
||||
run_step ai "AI Hub(Harness/Provider/Key)" omarchy-cn-ai-setup
|
||||
run_step privacy "隐私与诊断" step_privacy
|
||||
|
||||
echo
|
||||
echo "==> 最终检查"
|
||||
if omarchy-cn-doctor all; then
|
||||
echo "OmarchyCN 初始化完成。随时可用 omarchycn setup 重进任一步骤。"
|
||||
else
|
||||
echo "初始化步骤已执行,但诊断存在失败项(见上),修复后可重跑 omarchycn setup" >&2
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
# omarchy:summary=Show OmarchyCN version, paths, and configured state
|
||||
# omarchy:examples=omarchycn status
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
echo "OmarchyCN $(omarchy-cn-version)"
|
||||
echo "Omarchy upstream: $(<"$OMARCHY_PATH/version")"
|
||||
echo "Root: $OMARCHY_PATH"
|
||||
|
||||
config="$HOME/.config/omarchycn"
|
||||
state="$HOME/.local/state/omarchycn"
|
||||
echo "User config: $config $([[ -d $config ]] && echo "(present)" || echo "(not created)")"
|
||||
echo "State: $state $([[ -d $state ]] && echo "(present)" || echo "(not created)")"
|
||||
|
||||
if [[ -f $config/mirror-profile ]]; then
|
||||
echo "Mirror profile: $(<"$config/mirror-profile")"
|
||||
else
|
||||
echo "Mirror profile: not configured"
|
||||
fi
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/bin/bash
|
||||
# omarchy:summary=Full OmarchyCN update: system, cn data, and migrations
|
||||
# omarchy:examples=omarchycn update
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
MANIFEST="$HOME/.local/state/omarchycn/overlay-manifest"
|
||||
|
||||
if [[ ! -f $MANIFEST ]]; then
|
||||
# Package install: the pipeline brings the new cn tree and runs cn migrations
|
||||
if [[ -d $OMARCHY_PATH/cn ]]; then
|
||||
exec omarchy-update
|
||||
fi
|
||||
echo "未检测到 OmarchyCN($OMARCHY_PATH/cn 不存在,也没有 overlay manifest)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
src=$(grep -m1 '^source=' "$MANIFEST" | cut -d= -f2-)
|
||||
if [[ ! -d $src/.git ]]; then
|
||||
echo "Overlay source $src is not a git checkout" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
channel=$(omarchy-cn-channel)
|
||||
channel=${channel%% *}
|
||||
|
||||
old=$(git -C "$src" rev-parse --short HEAD)
|
||||
git -C "$src" fetch -q --tags origin
|
||||
|
||||
case "$channel" in
|
||||
nightly)
|
||||
git -C "$src" checkout -q quattro
|
||||
git -C "$src" pull --ff-only origin quattro
|
||||
;;
|
||||
beta)
|
||||
tag=$(git -C "$src" tag -l "*-cn.*" | sort -V | tail -1)
|
||||
if [[ -z $tag ]]; then
|
||||
echo "beta 通道无可用发布 tag" >&2
|
||||
exit 1
|
||||
fi
|
||||
git -C "$src" checkout -q "$tag"
|
||||
;;
|
||||
stable)
|
||||
tag=$(git -C "$src" tag -l "*-cn.*" | grep -vE "alpha|beta|rc" | sort -V | tail -1 || true)
|
||||
if [[ -z $tag ]]; then
|
||||
echo "当前尚无 stable 发布(omarchycn channel beta 可跟随预发布)" >&2
|
||||
exit 1
|
||||
fi
|
||||
git -C "$src" checkout -q "$tag"
|
||||
;;
|
||||
*)
|
||||
echo "Unknown channel: $channel" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Never move onto a tree that predates the channel or migrate mechanism:
|
||||
# it cannot switch back or finish this update's migration step
|
||||
if [[ ! -f $src/bin/omarchy-cn-channel || ! -f $src/bin/omarchy-cn-migrate ]]; then
|
||||
git -C "$src" checkout -q "$old"
|
||||
echo "$channel 通道的目标发布早于当前更新机制,已回退;请使用 nightly 或更新的发布" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
new=$(git -C "$src" rev-parse --short HEAD)
|
||||
echo "Channel: $channel, source: $old -> $new"
|
||||
|
||||
"$src/bin/omarchy-cn-install-overlay"
|
||||
|
||||
"$src/bin/omarchy-cn-migrate"
|
||||
|
||||
echo "OmarchyCN update complete ($(omarchy-cn-version 2>/dev/null || echo unknown))"
|
||||
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
# omarchy:summary=Show the OmarchyCN version
|
||||
# omarchy:examples=omarchycn version
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
upstream=$(<"$OMARCHY_PATH/version")
|
||||
release=$(<"$OMARCHY_PATH/cn/release")
|
||||
|
||||
echo "${upstream}-cn.${release}"
|
||||
@@ -1,8 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Set and launch the default coding agent
|
||||
# omarchy:args=[pi|omp|opencode|ori|claude|codex|grok|agy|copilot|crush]
|
||||
# omarchy:examples=omarchy default agent | omarchy default agent codex | omarchy default agent claude
|
||||
# omarchy:args=[pi|omp|opencode|ori|claude|codex|grok|agy|copilot|crush|kimi|deepcode|dim|dsh]
|
||||
# omarchy:examples=omarchy default agent | omarchy default agent codex | omarchy default agent kimi
|
||||
|
||||
installing=false
|
||||
if [[ ${1:-} == "--install" ]]; then
|
||||
@@ -34,25 +34,42 @@ crush) agent="crush"; name="Crush" ;;
|
||||
grok) agent="grok"; name="Grok"; agent_package="npm:@xai-official/grok" ;;
|
||||
agy | antigravity | antigravity-cli | gemini | gemini-cli) agent="agy"; name="Antigravity"; agent_package="antigravity-cli" ;;
|
||||
copilot | github-copilot) agent="copilot"; name="GitHub Copilot" ;;
|
||||
kimi | kimi-code) agent="kimi"; name="Kimi Code" ;;
|
||||
deepcode | deep-code) agent="deepcode"; name="Deep Code"; agent_package="npm:@vegamo/deepcode-cli" ;;
|
||||
dim | dimcode | dim-agent) agent="dim"; name="Dim Agent"; agent_package="npm:dimcode" ;;
|
||||
dsh | deepseek-harness) agent="dsh"; name="DSH"; agent_package="npm:@deepseek-ai/dsh" ;;
|
||||
*)
|
||||
echo "Usage: omarchy-default-agent <pi|omp|opencode|ori|claude|codex|grok|agy|copilot|crush>"
|
||||
echo "Usage: omarchy-default-agent <pi|omp|opencode|ori|claude|codex|grok|agy|copilot|crush|kimi|deepcode|dim|dsh>"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
agent_package=${agent_package:-$agent}
|
||||
|
||||
if [[ $installing == "false" ]] && ! mise where "$agent_package" &>/dev/null; then
|
||||
exec omarchy-launch-floating-terminal-with-presentation omarchy-default-agent --install "$agent"
|
||||
fi
|
||||
# Kimi Code installs via its official script, not mise
|
||||
if [[ $agent == "kimi" ]]; then
|
||||
if omarchy-cmd-missing kimi; then
|
||||
if [[ $installing == "false" ]]; then
|
||||
exec omarchy-launch-floating-terminal-with-presentation omarchy-default-agent --install kimi
|
||||
fi
|
||||
if ! omarchy-cn-kimi-install; then
|
||||
echo "Could not install $name with the official installer" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
if [[ $installing == "false" ]] && ! mise where "$agent_package" &>/dev/null; then
|
||||
exec omarchy-launch-floating-terminal-with-presentation omarchy-default-agent --install "$agent"
|
||||
fi
|
||||
|
||||
if ! mise use -g "$agent_package"; then
|
||||
if [[ $installing == "true" ]]; then
|
||||
echo "Could not install $name with mise" >&2
|
||||
else
|
||||
echo "Could not set $name as the default coding agent" >&2
|
||||
if ! mise use -g "$agent_package"; then
|
||||
if [[ $installing == "true" ]]; then
|
||||
echo "Could not install $name with mise" >&2
|
||||
else
|
||||
echo "Could not set $name as the default coding agent" >&2
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$agent_file")"
|
||||
|
||||
@@ -34,12 +34,8 @@ systemd, shell, or app-launcher environment; reboot to make every layer agree.
|
||||
Affects only \$OMARCHY_PATH-resolved trees: bin/, default/, shell/,
|
||||
themes/, applications/, config/. Files installed at fixed system paths
|
||||
(/etc/, /usr/lib/systemd/, udev rule bodies, /etc/skel after user
|
||||
creation) are NOT covered — for those, use omarchy-dev-pkg-test to build
|
||||
and install the package from the checkout.
|
||||
|
||||
The Plymouth and SDDM themes under /usr/share are the exception: omarchy
|
||||
plymouth set and omarchy refresh plymouth republish them from the checkout,
|
||||
reading this link's authorization out of the root-owned /etc/omarchy.conf.
|
||||
creation, /usr/share/plymouth) are NOT covered — for those, use
|
||||
omarchy-dev-pkg-test to build and install the package from the checkout.
|
||||
|
||||
Also writes $sudoers_file so sudo resolves omarchy-*
|
||||
from the checkout instead of the packaged copies. That part takes effect
|
||||
|
||||
@@ -149,9 +149,6 @@ write_networkmanager_dns() {
|
||||
local servers="$1"
|
||||
|
||||
install -d -m 0755 "$(dirname "$NM_DNS_CONF")"
|
||||
# omarchy:heredoc-expands paths=none -- $servers is a normalized, single-line
|
||||
# DNS server list written as data, not a path or command; nothing user-writable
|
||||
# is resolved or executed from the root-owned drop-in.
|
||||
cat >"$NM_DNS_CONF" <<EOF
|
||||
# Managed by omarchy-dns. Remove this file or run omarchy dns DHCP to use DHCP DNS again.
|
||||
[global-dns]
|
||||
@@ -305,9 +302,6 @@ Custom)
|
||||
split_dns_servers "$dns_servers"
|
||||
write_networkmanager_dns "$dns_servers"
|
||||
set_connection_dns "$ipv4_dns" "$ipv6_dns"
|
||||
# omarchy:heredoc-expands paths=none -- $dns_servers is a normalized,
|
||||
# single-line DNS server list; the //,/ turns its comma separators into the
|
||||
# spaces resolved.conf wants. It is written as data, not a path or command.
|
||||
tee /etc/systemd/resolved.conf >/dev/null <<EOF
|
||||
[Resolve]
|
||||
DNS=${dns_servers//,/ }
|
||||
|
||||
@@ -16,12 +16,6 @@ fi
|
||||
printf -v install_message '%q' "Installing ${name}..."
|
||||
printf -v desktop_id_arg '%q' "$desktop_id"
|
||||
|
||||
# The list has to reach omarchy-pkg-add as several words, so each word is quoted
|
||||
# rather than the whole string; -d '' reads past newlines and always ends at EOF.
|
||||
read -r -d '' -a package_list <<<"$packages" || true
|
||||
printf -v packages_arg '%q ' "${package_list[@]}"
|
||||
packages_arg="${packages_arg% }"
|
||||
|
||||
# The subshell keeps & from backgrounding the package installation too.
|
||||
exec omarchy-launch-floating-terminal-with-presentation \
|
||||
"echo ${install_message}; omarchy-pkg-add ${packages_arg} && (setsid uwsm-app -- gtk-launch ${desktop_id_arg} >/dev/null 2>&1 &)"
|
||||
"echo ${install_message}; omarchy-pkg-add ${packages} && (setsid uwsm-app -- gtk-launch ${desktop_id_arg} >/dev/null 2>&1 &)"
|
||||
|
||||
@@ -12,13 +12,4 @@ if [[ -z $name || -z $packages ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf -v install_message '%q' "Installing ${name}..."
|
||||
|
||||
# The list has to reach omarchy-pkg-add as several words, so each word is quoted
|
||||
# rather than the whole string; -d '' reads past newlines and always ends at EOF.
|
||||
read -r -d '' -a package_list <<<"$packages" || true
|
||||
printf -v packages_arg '%q ' "${package_list[@]}"
|
||||
packages_arg="${packages_arg% }"
|
||||
|
||||
exec omarchy-launch-floating-terminal-with-presentation \
|
||||
"echo ${install_message}; omarchy-pkg-add ${packages_arg}"
|
||||
exec omarchy-launch-floating-terminal-with-presentation "echo 'Installing ${name}...'; omarchy-pkg-add ${packages}"
|
||||
|
||||
@@ -13,9 +13,5 @@ if [[ -z $name || -z $package || -z $family ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf -v install_message '%q' "Installing ${name}..."
|
||||
printf -v package_arg '%q' "$package"
|
||||
printf -v family_arg '%q' "$family"
|
||||
|
||||
exec omarchy-launch-floating-terminal-with-presentation \
|
||||
"echo ${install_message}; omarchy-pkg-add ${package_arg} && sleep 2 && omarchy-font-set ${family_arg}"
|
||||
"echo 'Installing ${name}...'; omarchy-pkg-add ${package} && sleep 2 && omarchy-font-set '${family}'"
|
||||
|
||||
@@ -12,13 +12,10 @@ if gum confirm "Are you sure you want to restore all preinstalled web apps, TUI
|
||||
|
||||
# Mirrors the list in omarchy-remove-preinstalls; both track omarchy-base.packages
|
||||
if ! omarchy-pkg-add \
|
||||
aether \
|
||||
cliamp \
|
||||
libreoffice-fresh \
|
||||
xournalpp \
|
||||
pinta \
|
||||
obsidian \
|
||||
obs-studio \
|
||||
kdenlive \
|
||||
moonlight-qt \
|
||||
lazydocker \
|
||||
|
||||
@@ -338,10 +338,10 @@ static_bindings() {
|
||||
# for interchangeably belong on one row.
|
||||
alternative_chord_actions() {
|
||||
cat <<'ACTIONS'
|
||||
Close window
|
||||
Calculator
|
||||
Toggle scratchpad
|
||||
Move window to scratchpad
|
||||
关闭窗口
|
||||
计算器
|
||||
切换便签区
|
||||
窗口移入便签区
|
||||
ACTIONS
|
||||
}
|
||||
|
||||
@@ -442,58 +442,58 @@ prioritize_entries() {
|
||||
sub(/ \/ [^→]*→/, " →", line)
|
||||
|
||||
prio = 50
|
||||
if (match(line, /Keybindings/)) prio = 0
|
||||
if (match(line, /Omarchy menu/)) prio = 1
|
||||
if (match(line, /Terminal/)) prio = 2
|
||||
if (match(line, /Browser/) && !match(line, /Browser[[:space:]]*\(/) && !match(line, /SUPER SHIFT.*\+.*B.*→.*Browser/)) prio = 3
|
||||
if (match(line, /File manager/) && !match(line, /File manager \(cwd\)/)) prio = 4
|
||||
if (match(line, /快捷键/)) prio = 0
|
||||
if (match(line, /Omarchy 菜单/)) prio = 1
|
||||
if (match(line, /终端/)) prio = 2
|
||||
if (match(line, /浏览器/) && !match(line, /浏览器(/) && !match(line, /SUPER SHIFT.*\+.*B.*→.*浏览器/)) prio = 3
|
||||
if (match(line, /文件管理器/) && !match(line, /文件管理器(当前目录)/)) prio = 4
|
||||
if (match(line, /Launch apps/)) prio = 5
|
||||
if (match(line, /System menu/)) prio = 6
|
||||
if (match(line, /Theme menu/)) prio = 7
|
||||
if (match(line, /Full screen/)) prio = 8
|
||||
if (match(line, /Full width/)) prio = 9
|
||||
if (match(line, /Close window/)) prio = 10
|
||||
if (match(line, /Close all windows/)) prio = 11
|
||||
if (match(line, /Lock system/)) prio = 12
|
||||
if (match(line, /Toggle window floating/)) prio = 13
|
||||
if (match(line, /Toggle window split/)) prio = 14
|
||||
if (match(line, /Pop window/)) prio = 15
|
||||
if (match(line, /Universal/)) prio = 16
|
||||
if (match(line, /Clipboard/)) prio = 17
|
||||
if (match(line, /系统菜单/)) prio = 6
|
||||
if (match(line, /主题菜单/)) prio = 7
|
||||
if (match(line, /全屏/)) prio = 8
|
||||
if (match(line, /全宽/)) prio = 9
|
||||
if (match(line, /关闭窗口/)) prio = 10
|
||||
if (match(line, /关闭所有窗口/)) prio = 11
|
||||
if (match(line, /锁定系统/)) prio = 12
|
||||
if (match(line, /切换窗口浮动/)) prio = 13
|
||||
if (match(line, /切换窗口分割/)) prio = 14
|
||||
if (match(line, /弹出窗口/)) prio = 15
|
||||
if (match(line, /通用/)) prio = 16
|
||||
if (match(line, /剪贴板/)) prio = 17
|
||||
if (match(line, /Audio controls/)) prio = 18
|
||||
if (match(line, /Bluetooth controls/)) prio = 19
|
||||
if (match(line, /Wifi controls/)) prio = 20
|
||||
if (match(line, /Emojis/)) prio = 21
|
||||
if (match(line, /Color picker/)) prio = 22
|
||||
if (match(line, /Screenshot/)) prio = 23
|
||||
if (match(line, /Screenrecording/)) prio = 24
|
||||
if (match(line, /表情/)) prio = 21
|
||||
if (match(line, /取色器/)) prio = 22
|
||||
if (match(line, /截图/)) prio = 23
|
||||
if (match(line, /录屏/)) prio = 24
|
||||
if (match(line, /Tmux/)) prio = 25
|
||||
if (match(line, /Herdr/)) prio = 26
|
||||
if (match(line, /SUPER SHIFT.*\+.*B.*→.*Browser/)) prio = 27
|
||||
if (match(line, /File manager \(cwd\)/)) prio = 28
|
||||
if (match(line, /(Switch|Next|Former|Previous).*workspace/)) prio = 29
|
||||
if (match(line, /Move window to workspace/)) prio = 30
|
||||
if (match(line, /Move window silently to workspace/)) prio = 31
|
||||
if (match(line, /Swap window/)) prio = 32
|
||||
if (match(line, /Focus/)) prio = 33
|
||||
if (match(line, /Move window$/)) prio = 34
|
||||
if (match(line, /Resize window/)) prio = 35
|
||||
if (match(line, /Expand window/)) prio = 36
|
||||
if (match(line, /Shrink window/)) prio = 37
|
||||
if (match(line, /scratchpad/)) prio = 38
|
||||
if (match(line, /notification/)) prio = 39
|
||||
if (match(line, /Toggle window transparency/)) prio = 40
|
||||
if (match(line, /Toggle workspace gaps/)) prio = 41
|
||||
if (match(line, /Toggle nightlight/)) prio = 42
|
||||
if (match(line, /Toggle locking/)) prio = 43
|
||||
if (match(line, /group/)) prio = 94
|
||||
if (match(line, /Scroll active workspace/)) prio = 95
|
||||
if (match(line, /SUPER SHIFT.*\+.*B.*→.*浏览器/)) prio = 27
|
||||
if (match(line, /文件管理器(当前目录)/)) prio = 28
|
||||
if (match(line, /(切换到|下一|上一|上次)工作区/)) prio = 29
|
||||
if (match(line, /窗口移到工作区/)) prio = 30
|
||||
if (match(line, /窗口静默移到工作区/)) prio = 31
|
||||
if (match(line, /互换/)) prio = 32
|
||||
if (match(line, /聚焦/)) prio = 33
|
||||
if (match(line, /移动窗口$/)) prio = 34
|
||||
if (match(line, /调整窗口大小/)) prio = 35
|
||||
if (match(line, /扩展窗口/)) prio = 36
|
||||
if (match(line, /收缩窗口/)) prio = 37
|
||||
if (match(line, /便签区/)) prio = 38
|
||||
if (match(line, /通知/)) prio = 39
|
||||
if (match(line, /切换窗口透明/)) prio = 40
|
||||
if (match(line, /切换窗口间距/)) prio = 41
|
||||
if (match(line, /切换夜间模式/)) prio = 42
|
||||
if (match(line, /切换空闲锁定/)) prio = 43
|
||||
if (match(line, /组内|分组/)) prio = 94
|
||||
if (match(line, /工作区向(前|后)滚动/)) prio = 95
|
||||
if (match(line, /Cycle to/)) prio = 96
|
||||
if (match(line, /Reveal active/)) prio = 97
|
||||
if (match(line, /置顶显示/)) prio = 97
|
||||
if (match(line, /Apple Display/)) prio = 98
|
||||
if (match(line, /XF86/)) prio = 99
|
||||
if (match(line, /Tmux keybindings/)) prio = 100
|
||||
if (match(line, /Herdr keybindings/)) prio = 101
|
||||
if (match(line, /Tmux 快捷键/)) prio = 100
|
||||
if (match(line, /Herdr 快捷键/)) prio = 101
|
||||
|
||||
# print "priority<TAB>record"
|
||||
printf "%d\t%s\n", prio, $0
|
||||
|
||||
@@ -85,16 +85,19 @@ wait_for_pacman_transaction
|
||||
mkdir -p "$STATE_DIR"
|
||||
[[ -d $MIGRATIONS_DIR ]] || exit 0
|
||||
|
||||
while IFS=$'\t' read -r name file marker <&3; do
|
||||
while IFS=$'\t' read -r name file marker; do
|
||||
[[ -n $name ]] || continue
|
||||
|
||||
if [[ ! -f $marker ]]; then
|
||||
echo -e "\e[32m\nRunning migration (${name%.sh})\e[0m"
|
||||
OMARCHY_PATH="$OMARCHY_PATH" bash -euo pipefail "$file" 3<&-
|
||||
OMARCHY_PATH="$OMARCHY_PATH" bash -euo pipefail "$file"
|
||||
mkdir -p "$(dirname "$marker")"
|
||||
touch "$marker"
|
||||
fi
|
||||
done 3< <(migration_entries)
|
||||
done < <(migration_entries)
|
||||
|
||||
# Here (not omarchy-update) so the first update from a pre-cn package runs them
|
||||
OMARCHY_PATH="$OMARCHY_PATH" omarchy-cn-migrate
|
||||
|
||||
# Clear a login-time notification the user left sitting there and then resolved
|
||||
# by running migrations some other way. The substring matches both the current
|
||||
|
||||
@@ -3,7 +3,5 @@
|
||||
# omarchy:summary=Restore the default Omarchy Plymouth boot theme and SDDM login screen
|
||||
# omarchy:requires-sudo=true
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
"$OMARCHY_PATH/bin/omarchy-refresh-plymouth"
|
||||
"$OMARCHY_PATH/bin/omarchy-refresh-sddm"
|
||||
omarchy-refresh-plymouth
|
||||
omarchy-refresh-sddm
|
||||
|
||||
@@ -5,377 +5,88 @@
|
||||
# omarchy:examples=omarchy plymouth set '#1d2021' '#ebdbb2' ~/.local/state/omarchy/current/theme/plymouth/logo.png
|
||||
# omarchy:requires-sudo=true
|
||||
|
||||
set -euo pipefail
|
||||
# Configure the Plymouth boot theme with a custom background color, text color, and logo.
|
||||
# Stages the change in a temp dir, then commits the staged files to /usr/share and
|
||||
# rebuilds the initramfs. Also syncs the SDDM login screen (the post-logout
|
||||
# screen) with the same colors and logo so boot/login stay visually unified.
|
||||
|
||||
# Build the authoritative theme in a root-owned directory, then publish each
|
||||
# fixed destination atomically. The caller opens the selected logo before sudo,
|
||||
# so the privileged process never resolves a user-controlled input path.
|
||||
|
||||
usage() {
|
||||
if (( $# != 3 )); then
|
||||
echo "Usage: omarchy-plymouth-set <background-hex> <text-hex> <path-to-logo.png>" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
if (( $# == 3 )); then
|
||||
mode=set
|
||||
elif (( $# == 1 )); then
|
||||
case "$1" in
|
||||
--refresh-default)
|
||||
mode=refresh-plymouth
|
||||
;;
|
||||
--refresh-sddm-default)
|
||||
mode=refresh-sddm
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
else
|
||||
usage
|
||||
fi
|
||||
|
||||
if (( EUID == 0 )); then
|
||||
echo "Error: run omarchy-plymouth-set as your user, not under sudo." >&2
|
||||
bg_hex="${1#\#}"
|
||||
text_hex="${2#\#}"
|
||||
logo_path="$3"
|
||||
|
||||
if ! [[ $bg_hex =~ ^[0-9a-fA-F]{6}$ ]]; then
|
||||
echo "Invalid background color: $1 (expected #RRGGBB)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
logo_fd=
|
||||
if [[ $mode != "set" ]]; then
|
||||
bg_hex=
|
||||
text_hex=
|
||||
if ! [[ $text_hex =~ ^[0-9a-fA-F]{6}$ ]]; then
|
||||
echo "Invalid text color: $2 (expected #RRGGBB)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f $logo_path ]]; then
|
||||
echo "Logo file not found: $logo_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# omarchy-plymouth-set-by-theme passes a theme's unlock.png straight from
|
||||
# ~/.config/omarchy/themes, where an installed theme can make it a symlink to
|
||||
# anything. The copies below land in world-readable /usr/share, so following one
|
||||
# would republish whatever it points at.
|
||||
if [[ -L $logo_path ]]; then
|
||||
echo "Logo file is a symlink, which is not accepted: $logo_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
bg_r=$(awk -v n=$((16#${bg_hex:0:2})) 'BEGIN{printf "%.3f", n/255}')
|
||||
bg_g=$(awk -v n=$((16#${bg_hex:2:2})) 'BEGIN{printf "%.3f", n/255}')
|
||||
bg_b=$(awk -v n=$((16#${bg_hex:4:2})) 'BEGIN{printf "%.3f", n/255}')
|
||||
|
||||
theme_dir="/usr/share/plymouth/themes/omarchy"
|
||||
staging_dir=$(mktemp -d)
|
||||
trap 'rm -rf "$staging_dir"' EXIT
|
||||
|
||||
find "$OMARCHY_PATH/default/plymouth" -maxdepth 1 -type f -exec cp -t "$staging_dir/" {} +
|
||||
cp "$logo_path" "$staging_dir/logo.png"
|
||||
|
||||
sed -i \
|
||||
-e "s/^Window.SetBackgroundTopColor.*/Window.SetBackgroundTopColor($bg_r, $bg_g, $bg_b);/" \
|
||||
-e "s/^Window.SetBackgroundBottomColor.*/Window.SetBackgroundBottomColor($bg_r, $bg_g, $bg_b);/" \
|
||||
"$staging_dir/omarchy.script"
|
||||
|
||||
for asset in bullet.png entry.png lock.png progress_bar.png; do
|
||||
magick "$staging_dir/$asset" -channel RGB +level-colors "#$text_hex","#$text_hex" "$staging_dir/$asset"
|
||||
done
|
||||
|
||||
sudo cp -a --no-preserve=mode,ownership "$staging_dir/." "$theme_dir/"
|
||||
sudo plymouth-set-default-theme omarchy
|
||||
|
||||
if omarchy-cmd-present limine-mkinitcpio; then
|
||||
sudo limine-mkinitcpio
|
||||
else
|
||||
bg_hex="${1#\#}"
|
||||
text_hex="${2#\#}"
|
||||
logo_path="$3"
|
||||
|
||||
if ! [[ $bg_hex =~ ^[0-9a-fA-F]{6}$ ]]; then
|
||||
echo "Invalid background color: $1 (expected #RRGGBB)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! [[ $text_hex =~ ^[0-9a-fA-F]{6}$ ]]; then
|
||||
echo "Invalid text color: $2 (expected #RRGGBB)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f $logo_path ]]; then
|
||||
echo "Logo file not found: $logo_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -L $logo_path ]]; then
|
||||
echo "Logo file is a symlink, which is not accepted: $logo_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Open the logo while still unprivileged. A replacement symlink to a root-only
|
||||
# file therefore fails here instead of being followed after sudo starts.
|
||||
if ! exec {logo_fd}<"$logo_path"; then
|
||||
echo "Unable to open logo file as the current user: $logo_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f /proc/$$/fd/$logo_fd ]]; then
|
||||
echo "Logo input is no longer a regular file: $logo_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
sudo mkinitcpio -P
|
||||
fi
|
||||
|
||||
run_root_transaction() {
|
||||
sudo /bin/bash -c '
|
||||
set -eEuo pipefail
|
||||
PATH=/usr/bin:/bin
|
||||
export PATH
|
||||
umask 077
|
||||
# Sync the SDDM login screen with the same colors and logo.
|
||||
sddm_dir="/usr/share/sddm/themes/omarchy"
|
||||
sddm_template="$OMARCHY_PATH/default/sddm/omarchy/Main.qml"
|
||||
|
||||
# Every check below is a bare assertion that aborts under set -e. Name the
|
||||
# subject of each one so a refusal reaches the user instead of exiting mute.
|
||||
failure_context="the privileged Plymouth transaction"
|
||||
failure_reported=
|
||||
report_failure() {
|
||||
[[ -z $failure_reported ]] || return 0
|
||||
failure_reported=1
|
||||
printf "omarchy-plymouth-set: refusing to publish: %s failed validation\n" "$failure_context" >&2
|
||||
if [[ -n ${failure_hint:-} ]]; then
|
||||
printf "omarchy-plymouth-set: %s\n" "$failure_hint" >&2
|
||||
fi
|
||||
}
|
||||
trap report_failure ERR
|
||||
sed \
|
||||
-e "s/#1a1b26/#$bg_hex/g" \
|
||||
-e "s/#ffffff/#$text_hex/g" \
|
||||
"$sddm_template" | sudo tee "$sddm_dir/Main.qml" >/dev/null
|
||||
|
||||
mode=$1
|
||||
source_root=$2
|
||||
bg_hex=$3
|
||||
text_hex=$4
|
||||
max_asset_size=$5
|
||||
|
||||
failure_context="the arguments of the privileged transaction"
|
||||
[[ $mode == "set" || $mode == "refresh-plymouth" || $mode == "refresh-sddm" ]]
|
||||
[[ $source_root == /* ]]
|
||||
[[ $max_asset_size =~ ^[0-9]+$ ]]
|
||||
(( max_asset_size > 0 ))
|
||||
|
||||
failure_context="the Omarchy source tree $source_root"
|
||||
canonical_source_root=$(realpath -e -- "$source_root")
|
||||
[[ $canonical_source_root == "$source_root" ]]
|
||||
|
||||
validate_trusted_directory() {
|
||||
local directory=$1 canonical uid directory_mode
|
||||
|
||||
failure_context="directory $directory"
|
||||
canonical=$(realpath -e -- "$directory")
|
||||
[[ $canonical == "$directory" && -d $directory && ! -L $directory ]]
|
||||
|
||||
while :; do
|
||||
failure_context="directory $directory (must be root-owned and not group- or world-writable)"
|
||||
uid=$(stat -c %u -- "$directory")
|
||||
directory_mode=$(stat -c %a -- "$directory")
|
||||
(( uid == 0 ))
|
||||
(( (8#$directory_mode & 0022) == 0 ))
|
||||
[[ $directory == "/" ]] && break
|
||||
directory=${directory%/*}
|
||||
[[ -n $directory ]] || directory=/
|
||||
done
|
||||
}
|
||||
|
||||
validate_trusted_configuration_file() {
|
||||
local configuration=$1 canonical uid configuration_mode size
|
||||
|
||||
failure_context="root configuration $configuration"
|
||||
[[ -f $configuration && ! -L $configuration ]]
|
||||
canonical=$(realpath -e -- "$configuration")
|
||||
[[ $canonical == "$configuration" ]]
|
||||
validate_trusted_directory "${configuration%/*}"
|
||||
uid=$(stat -c %u -- "$configuration")
|
||||
configuration_mode=$(stat -c %a -- "$configuration")
|
||||
size=$(stat -c %s -- "$configuration")
|
||||
(( uid == 0 ))
|
||||
(( (8#$configuration_mode & 0022) == 0 ))
|
||||
(( size > 0 && size <= 4096 ))
|
||||
}
|
||||
|
||||
# A packaged tree must be root-owned. A development checkout is the one
|
||||
# deliberate exception: omarchy dev link records its canonical path in a
|
||||
# root-owned /etc/omarchy.conf. That is already an explicit decision to run
|
||||
# privileged Omarchy commands from user-editable code in the checkout, so
|
||||
# reading its packaged assets does not widen the development trust boundary.
|
||||
development_source=false
|
||||
source_root_uid=$(stat -c %u -- "$source_root")
|
||||
if (( source_root_uid != 0 )); then
|
||||
omarchy_conf=/etc/omarchy.conf
|
||||
failure_context="$source_root is user-owned and $omarchy_conf must contain its trusted dev-link authorization; run omarchy dev link to authorize it"
|
||||
failure_hint="$source_root is user-owned; run omarchy dev link to authorize this development checkout, or omarchy dev unlink to use the packaged tree"
|
||||
validate_trusted_configuration_file "$omarchy_conf"
|
||||
|
||||
# validate_trusted_configuration_file walks /etc up to / and leaves its own
|
||||
# subject behind in failure_context. Without restoring ours, a checkout
|
||||
# that simply is not the authorized one refuses with "directory / must be
|
||||
# root-owned and not group- or world-writable" -- naming a directory that
|
||||
# passed, and sending the reader after a filesystem problem that is not
|
||||
# there.
|
||||
failure_context="the dev-link authorization in $omarchy_conf, which must name $source_root"
|
||||
|
||||
quoted_source_root=$source_root
|
||||
quoted_source_root=${quoted_source_root//\\/\\\\}
|
||||
quoted_source_root=${quoted_source_root//\"/\\\"}
|
||||
quoted_source_root=${quoted_source_root//\$/\\\$}
|
||||
quoted_source_root=${quoted_source_root//\`/\\\`}
|
||||
expected_config_line="export OMARCHY_PATH=\"$quoted_source_root\""
|
||||
mapfile -t omarchy_config_lines <"$omarchy_conf"
|
||||
(( ${#omarchy_config_lines[@]} == 1 ))
|
||||
[[ ${omarchy_config_lines[0]} == "$expected_config_line" ]]
|
||||
development_source=true
|
||||
failure_hint=
|
||||
fi
|
||||
|
||||
if [[ $mode == "set" ]]; then
|
||||
[[ $bg_hex =~ ^[0-9a-fA-F]{6}$ ]]
|
||||
[[ $text_hex =~ ^[0-9a-fA-F]{6}$ ]]
|
||||
fi
|
||||
|
||||
theme_dir=/usr/share/plymouth/themes/omarchy
|
||||
sddm_dir=/usr/share/sddm/themes/omarchy
|
||||
plymouth_theme_assets=(
|
||||
bullet.png
|
||||
entry.png
|
||||
lock.png
|
||||
logo.png
|
||||
omarchy.plymouth
|
||||
omarchy.script
|
||||
preview-unlock.png
|
||||
progress_bar.png
|
||||
progress_box.png
|
||||
)
|
||||
plymouth_default_assets=("${plymouth_theme_assets[@]}" logos/oma.png)
|
||||
sddm_theme_assets=(Main.qml bullet.png entry-failed.png entry.png lock-failed.png lock.png logo.png)
|
||||
sddm_default_assets=("${sddm_theme_assets[@]}" metadata.desktop theme.conf)
|
||||
|
||||
plymouth_assets=()
|
||||
sddm_assets=()
|
||||
case "$mode" in
|
||||
set)
|
||||
plymouth_assets=("${plymouth_theme_assets[@]}")
|
||||
sddm_assets=("${sddm_theme_assets[@]}")
|
||||
;;
|
||||
refresh-plymouth)
|
||||
plymouth_assets=("${plymouth_default_assets[@]}")
|
||||
;;
|
||||
refresh-sddm)
|
||||
sddm_assets=("${sddm_default_assets[@]}")
|
||||
;;
|
||||
esac
|
||||
|
||||
validate_trusted_file() {
|
||||
local source=$1 canonical uid file_mode size
|
||||
|
||||
failure_context="packaged source file $source"
|
||||
[[ -f $source && ! -L $source ]]
|
||||
canonical=$(realpath -e -- "$source")
|
||||
[[ $canonical == "$source" ]]
|
||||
file_mode=$(stat -c %a -- "$source")
|
||||
size=$(stat -c %s -- "$source")
|
||||
(( size > 0 && size <= max_asset_size ))
|
||||
|
||||
if ! $development_source; then
|
||||
validate_trusted_directory "${source%/*}"
|
||||
uid=$(stat -c %u -- "$source")
|
||||
(( uid == 0 ))
|
||||
(( (8#$file_mode & 0022) == 0 ))
|
||||
fi
|
||||
}
|
||||
|
||||
copy_trusted_file() {
|
||||
local source=$1 destination=$2
|
||||
|
||||
validate_trusted_file "$source"
|
||||
install -o 0 -g 0 -m 0600 -- "$source" "$destination"
|
||||
}
|
||||
|
||||
staging_dir=$(mktemp -d /tmp/omarchy-plymouth.XXXXXXXX)
|
||||
temporary=
|
||||
cleanup() {
|
||||
[[ -z $temporary ]] || rm -f -- "$temporary"
|
||||
rm -rf -- "$staging_dir"
|
||||
}
|
||||
trap cleanup EXIT HUP INT TERM
|
||||
chown 0:0 -- "$staging_dir"
|
||||
chmod 0700 -- "$staging_dir"
|
||||
|
||||
plymouth_stage=$staging_dir/plymouth
|
||||
sddm_stage=$staging_dir/sddm
|
||||
mkdir -m 0700 -p -- "$plymouth_stage/logos" "$sddm_stage"
|
||||
|
||||
for asset in "${plymouth_assets[@]}"; do
|
||||
copy_trusted_file "$source_root/default/plymouth/$asset" "$plymouth_stage/$asset"
|
||||
done
|
||||
|
||||
if [[ $mode == "set" ]]; then
|
||||
# stdin was opened by the unprivileged caller. Read no more than the
|
||||
# documented limit into the root-owned stage before doing other work.
|
||||
failure_context="the selected logo (expected 1 to $max_asset_size bytes)"
|
||||
head -c "$((max_asset_size + 1))" >"$plymouth_stage/logo.png"
|
||||
logo_size=$(stat -c %s -- "$plymouth_stage/logo.png")
|
||||
(( logo_size > 0 && logo_size <= max_asset_size ))
|
||||
chown 0:0 -- "$plymouth_stage/logo.png"
|
||||
chmod 0600 -- "$plymouth_stage/logo.png"
|
||||
cp --reflink=never -- "$plymouth_stage/logo.png" "$sddm_stage/logo.png"
|
||||
|
||||
bg_r=$(awk -v n=$((16#${bg_hex:0:2})) "BEGIN{printf \"%.3f\", n/255}")
|
||||
bg_g=$(awk -v n=$((16#${bg_hex:2:2})) "BEGIN{printf \"%.3f\", n/255}")
|
||||
bg_b=$(awk -v n=$((16#${bg_hex:4:2})) "BEGIN{printf \"%.3f\", n/255}")
|
||||
|
||||
sed -i \
|
||||
-e "s/^Window.SetBackgroundTopColor.*/Window.SetBackgroundTopColor($bg_r, $bg_g, $bg_b);/" \
|
||||
-e "s/^Window.SetBackgroundBottomColor.*/Window.SetBackgroundBottomColor($bg_r, $bg_g, $bg_b);/" \
|
||||
"$plymouth_stage/omarchy.script"
|
||||
|
||||
for asset in bullet.png entry.png lock.png progress_bar.png; do
|
||||
magick "$plymouth_stage/$asset" -channel RGB +level-colors "#$text_hex","#$text_hex" "$plymouth_stage/$asset"
|
||||
done
|
||||
|
||||
copy_trusted_file "$source_root/default/sddm/omarchy/Main.qml" "$sddm_stage/Main.qml"
|
||||
sed -i \
|
||||
-e "s/#1a1b26/#__OMARCHY_SDDM_BG__/g" \
|
||||
-e "s/#ffffff/#__OMARCHY_SDDM_TEXT__/g" \
|
||||
-e "s/#__OMARCHY_SDDM_BG__/#$bg_hex/g" \
|
||||
-e "s/#__OMARCHY_SDDM_TEXT__/#$text_hex/g" \
|
||||
"$sddm_stage/Main.qml"
|
||||
|
||||
for asset in bullet.png entry.png lock.png; do
|
||||
cp --reflink=never -- "$plymouth_stage/$asset" "$sddm_stage/$asset"
|
||||
done
|
||||
for asset in entry lock; do
|
||||
magick "$plymouth_stage/$asset.png" -channel RGB +level-colors "#f7768e","#f7768e" "$sddm_stage/$asset-failed.png"
|
||||
done
|
||||
chown -R 0:0 -- "$staging_dir"
|
||||
find "$staging_dir" -type f -exec chmod 0600 -- {} +
|
||||
elif (( ${#sddm_assets[@]} )); then
|
||||
for asset in "${sddm_assets[@]}"; do
|
||||
copy_trusted_file "$source_root/default/sddm/omarchy/$asset" "$sddm_stage/$asset"
|
||||
done
|
||||
fi
|
||||
|
||||
if (( ${#plymouth_assets[@]} )); then
|
||||
validate_trusted_directory "$theme_dir"
|
||||
if [[ $mode == "refresh-plymouth" ]]; then
|
||||
validate_trusted_directory "$theme_dir/logos"
|
||||
fi
|
||||
fi
|
||||
if (( ${#sddm_assets[@]} )); then
|
||||
validate_trusted_directory "$sddm_dir"
|
||||
fi
|
||||
|
||||
publish_asset() {
|
||||
local source=$1 destination=$2 parent filename source_size copied_size
|
||||
|
||||
failure_context="destination $destination"
|
||||
[[ -f $source && ! -L $source ]]
|
||||
(( $(stat -c %u -- "$source") == 0 ))
|
||||
source_size=$(stat -c %s -- "$source")
|
||||
(( source_size > 0 && source_size <= max_asset_size ))
|
||||
|
||||
[[ $destination == /* && $destination != */ && $destination != *"/../"* ]]
|
||||
parent=${destination%/*}
|
||||
filename=${destination##*/}
|
||||
[[ -n $parent && -n $filename && $filename != "." && $filename != ".." ]]
|
||||
validate_trusted_directory "$parent"
|
||||
|
||||
temporary=$(mktemp --tmpdir="$parent" ".$filename.omarchy-new.XXXXXXXX")
|
||||
install -o 0 -g 0 -m 0644 -- "$source" "$temporary"
|
||||
copied_size=$(stat -c %s -- "$temporary")
|
||||
(( copied_size == source_size ))
|
||||
cmp -s -- "$source" "$temporary"
|
||||
sync -f -- "$temporary"
|
||||
mv --no-copy -fT -- "$temporary" "$destination"
|
||||
temporary=
|
||||
}
|
||||
|
||||
if (( ${#plymouth_assets[@]} )); then
|
||||
for asset in "${plymouth_assets[@]}"; do
|
||||
publish_asset "$plymouth_stage/$asset" "$theme_dir/$asset"
|
||||
done
|
||||
fi
|
||||
|
||||
if (( ${#sddm_assets[@]} )); then
|
||||
for asset in "${sddm_assets[@]}"; do
|
||||
publish_asset "$sddm_stage/$asset" "$sddm_dir/$asset"
|
||||
done
|
||||
validate_trusted_directory "$sddm_dir"
|
||||
rm -f -- "$sddm_dir/logo.svg"
|
||||
fi
|
||||
' bash "$mode" "$OMARCHY_PATH" "$bg_hex" "$text_hex" "$((64 * 1024 * 1024))"
|
||||
}
|
||||
|
||||
if [[ $mode == "set" ]]; then
|
||||
run_root_transaction <&"$logo_fd"
|
||||
else
|
||||
run_root_transaction </dev/null
|
||||
fi
|
||||
|
||||
if [[ $mode != "refresh-sddm" ]]; then
|
||||
sudo plymouth-set-default-theme omarchy
|
||||
|
||||
if omarchy-cmd-present limine-mkinitcpio; then
|
||||
sudo limine-mkinitcpio
|
||||
else
|
||||
sudo mkinitcpio -P
|
||||
fi
|
||||
fi
|
||||
sudo cp "$staging_dir/logo.png" "$sddm_dir/logo.png"
|
||||
for asset in bullet.png entry.png lock.png; do
|
||||
sudo cp "$staging_dir/$asset" "$sddm_dir/$asset"
|
||||
done
|
||||
for asset in entry lock; do
|
||||
magick "$staging_dir/$asset.png" -channel RGB +level-colors "#f7768e","#f7768e" "$staging_dir/$asset-failed.png"
|
||||
sudo cp "$staging_dir/$asset-failed.png" "$sddm_dir/$asset-failed.png"
|
||||
done
|
||||
sudo rm -f "$sddm_dir/logo.svg"
|
||||
|
||||
@@ -186,7 +186,7 @@ tips=(
|
||||
"Share files with phones and laptops using LocalSend"
|
||||
"Turn any website into an app with Install > Web App in the menu"
|
||||
"Super + Return opens a terminal, Super + Shift + Return the browser"
|
||||
"Edit images with Pinta, videos with Kdenlive, docs with LibreOffice"
|
||||
"Edit images with Pinta and videos with Kdenlive"
|
||||
"Super + Ctrl + Print grabs text off the screen with OCR"
|
||||
"Print takes a screenshot, Alt + Print records the screen"
|
||||
"Set a reminder with Super + Ctrl + R"
|
||||
@@ -677,15 +677,11 @@ user_groups() {
|
||||
if [[ -f $PROVISIONING_DIR/groups ]]; then
|
||||
while IFS= read -r group; do
|
||||
[[ -n $group ]] || continue
|
||||
# Never replay old privileged group defaults. Docker is always opt-in.
|
||||
# Input is only retained when the factory image has one of the features
|
||||
# whose installer deliberately grants access to raw input devices.
|
||||
# Never grant docker at first boot, even if an older install recorded it
|
||||
# (or a factory snapshot predating the opt-in default carries it): the
|
||||
# docker group is root-equivalent. It is opt-in via
|
||||
# omarchy-setup-security-sudoless-docker.
|
||||
[[ $group == "docker" ]] && continue
|
||||
if [[ $group == "input" ]] &&
|
||||
! pacman -Qq xpadneo-dkms &>/dev/null &&
|
||||
! pacman -Qq ydotool &>/dev/null; then
|
||||
continue
|
||||
fi
|
||||
getent group "$group" >/dev/null || continue
|
||||
[[ ",$groups," == *",$group,"* ]] || groups+=",$group"
|
||||
done <"$PROVISIONING_DIR/groups"
|
||||
@@ -792,7 +788,7 @@ configure_login() {
|
||||
# After=) is what makes it deterministic — no sleep/race against SDDM's startup.
|
||||
install_autologin_once_cleanup() {
|
||||
local unit=omarchy-provision-autologin-once.service
|
||||
sed "s|@UNIT@|$unit|g" >"/etc/systemd/system/$unit" <<'UNIT'
|
||||
cat >"/etc/systemd/system/$unit" <<UNIT
|
||||
[Unit]
|
||||
Description=Drop the first-boot autologin before the next login
|
||||
Before=display-manager.service
|
||||
@@ -801,7 +797,7 @@ ConditionPathExists=/etc/sddm.conf.d/autologin.conf
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/bin/rm -f /etc/sddm.conf.d/autologin.conf
|
||||
ExecStartPost=/usr/bin/rm -f /etc/systemd/system/graphical.target.wants/@UNIT@ /etc/systemd/system/@UNIT@
|
||||
ExecStartPost=/usr/bin/rm -f /etc/systemd/system/graphical.target.wants/$unit /etc/systemd/system/$unit
|
||||
|
||||
[Install]
|
||||
WantedBy=graphical.target
|
||||
|
||||
@@ -11,7 +11,7 @@ Usage: omarchy finalize user [--force] [--first-install]
|
||||
|
||||
Runs the per-user setup steps that /etc/skel can't seed:
|
||||
dev-aware skill symlinks, xdg-user-dirs + gtk bookmarks (need \$HOME),
|
||||
default browser/mailto, and install/user/all.sh.
|
||||
default browser, and install/user/all.sh.
|
||||
|
||||
For shipped configs see /etc/skel (new users) and omarchy-reinstall-configs
|
||||
(existing users explicitly resyncing).
|
||||
@@ -110,7 +110,6 @@ source "$OMARCHY_INSTALL/user/all.sh"
|
||||
|
||||
omarchy-refresh-applications
|
||||
env -u BROWSER xdg-settings set default-web-browser chromium.desktop
|
||||
xdg-mime default HEY.desktop x-scheme-handler/mailto
|
||||
|
||||
if (( first_install )); then
|
||||
mkdir -p "$state_dir/migrations"
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
# omarchy:summary=Overwrite the user config for the Plymouth drive decryption and boot sequence with the Omarchy default and rebuild it.
|
||||
# omarchy:requires-sudo=true
|
||||
|
||||
# Reuse the fixed-file publisher so root never resolves the source checkout or
|
||||
# follows a destination symlink while restoring the packaged assets.
|
||||
exec "$OMARCHY_PATH/bin/omarchy-plymouth-set" --refresh-default
|
||||
sudo cp -r "$OMARCHY_PATH/default/plymouth/." /usr/share/plymouth/themes/omarchy/
|
||||
sudo plymouth-set-default-theme omarchy
|
||||
|
||||
if omarchy-cmd-present limine-mkinitcpio; then
|
||||
sudo limine-mkinitcpio
|
||||
else
|
||||
sudo mkinitcpio -P
|
||||
fi
|
||||
|
||||
@@ -3,6 +3,5 @@
|
||||
# omarchy:summary=Refresh the SDDM theme from default
|
||||
# omarchy:requires-sudo=true
|
||||
|
||||
# Reuse the fixed-file publisher so root never resolves an untrusted source or
|
||||
# follows a destination symlink while restoring the packaged SDDM theme.
|
||||
exec "$OMARCHY_PATH/bin/omarchy-plymouth-set" --refresh-sddm-default
|
||||
sudo rm -rf /usr/share/sddm/themes/omarchy
|
||||
sudo cp -r "$OMARCHY_PATH/default/sddm/omarchy" /usr/share/sddm/themes/omarchy
|
||||
|
||||
@@ -18,13 +18,10 @@ if gum confirm "Are you sure you want to remove all preinstalled web apps, TUI w
|
||||
~/.local/bin/omp ~/.local/bin/ori ~/.local/bin/grok ~/.local/bin/crush ~/.local/bin/ghui ~/.local/bin/hunk
|
||||
|
||||
omarchy-pkg-drop \
|
||||
aether \
|
||||
cliamp \
|
||||
libreoffice-fresh \
|
||||
xournalpp \
|
||||
pinta \
|
||||
obsidian \
|
||||
obs-studio \
|
||||
kdenlive \
|
||||
moonlight-qt \
|
||||
lazydocker \
|
||||
|
||||
@@ -41,9 +41,6 @@ setup_pam_config() {
|
||||
fi
|
||||
else
|
||||
echo "Creating polkit configuration with fingerprint authentication..."
|
||||
# omarchy:heredoc-expands paths=none -- $fprintd_gate is the literal PAM
|
||||
# line defined above, shared with the two sed insertions so the gate cannot
|
||||
# drift between files. The only path in it is the fixed /usr/bin one.
|
||||
sudo tee /etc/pam.d/polkit-1 >/dev/null <<EOF
|
||||
$fprintd_gate
|
||||
auth sufficient pam_fprintd.so
|
||||
|
||||
@@ -143,50 +143,6 @@ authorize_pasted_key() {
|
||||
authorize_key "$key" || exit 1
|
||||
}
|
||||
|
||||
# Only called after a key is authorized. Disabling password authentication
|
||||
# before then could lock the owner out of the machine.
|
||||
disable_password_auth() {
|
||||
local config=/etc/ssh/sshd_config.d/10-omarchy-hardening.conf
|
||||
local effective_config
|
||||
|
||||
if [[ ! -s $AUTHORIZED_KEYS ]]; then
|
||||
echo -e "\e[31mCannot disable SSH password authentication without an authorized key.\e[0m" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "Disabling SSH password authentication, now that a key is authorized..."
|
||||
sudo install -Dm644 /dev/stdin "$config" <<'CONF'
|
||||
# Written by omarchy-setup-security-sshd once an SSH key was authorized.
|
||||
# Delete this file and reload sshd to allow password logins again.
|
||||
PasswordAuthentication no
|
||||
KbdInteractiveAuthentication no
|
||||
CONF
|
||||
|
||||
# Validate before reloading: a config sshd rejects would otherwise take the
|
||||
# service down on its next restart, potentially stranding a remote owner.
|
||||
if ! sudo sshd -t; then
|
||||
echo -e "\e[31msshd rejected the hardening config; removing it and leaving passwords on.\e[0m" >&2
|
||||
sudo rm -f "$config"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Syntax alone is insufficient because sshd uses the first value it reads for
|
||||
# these settings. An earlier administrator rule could leave passwords enabled.
|
||||
# Match keywords case-insensitively: OpenSSH 9.x dumps them lowercase, 10.x
|
||||
# in CamelCase.
|
||||
if ! effective_config=$(sudo sshd -T) ||
|
||||
! grep -qixF "passwordauthentication no" <<<"$effective_config" ||
|
||||
! grep -qixF "kbdinteractiveauthentication no" <<<"$effective_config"; then
|
||||
echo -e "\e[31msshd did not apply the password-authentication restrictions; removing the ineffective config.\e[0m" >&2
|
||||
sudo rm -f "$config"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Reload rather than restart so an administrator already connected keeps
|
||||
# their session.
|
||||
sudo systemctl reload sshd.service
|
||||
}
|
||||
|
||||
echo -e "\e[32mSetting up SSH server access with key-based authentication.\n\e[0m"
|
||||
|
||||
setup_sshd
|
||||
@@ -205,8 +161,5 @@ else
|
||||
esac
|
||||
fi
|
||||
|
||||
disable_password_auth
|
||||
|
||||
echo -e "\e[32m\nPerfect! The SSH server is running and your key is authorized.\e[0m"
|
||||
echo "Password logins are off; this machine now accepts authorized keys only."
|
||||
echo "You can now connect with: ssh $USER@$(hostname)"
|
||||
|
||||
@@ -23,26 +23,16 @@ omarchy-git-url-check "$REPO_URL" || exit 1
|
||||
|
||||
THEMES_DIR="$HOME/.config/omarchy/themes"
|
||||
|
||||
# Strip user@host: prefix from scp-style SSH URLs so basename sees just the path.
|
||||
# git reads a URL as scp-style when a colon appears before any slash, so the path
|
||||
# after it need not hold one: `git@host:omarchy-blue-theme.git` is a repo in that
|
||||
# user's home, and leaving its prefix on names the theme after the whole URL.
|
||||
# Strip user@host: prefix from scp-style SSH URLs so basename sees just the path
|
||||
REPO_PATH="$REPO_URL"
|
||||
[[ $REPO_PATH != *"://"* && $REPO_PATH == *:* && ${REPO_PATH%%:*} != */* ]] && REPO_PATH="${REPO_PATH#*:}"
|
||||
[[ $REPO_PATH != *"://"* && $REPO_PATH == *:*/* ]] && REPO_PATH="${REPO_PATH#*:}"
|
||||
THEME_NAME=$(basename -- "$REPO_PATH" .git | sed -E 's/^omarchy-//; s/-theme$//' | tr '[:upper:]' '[:lower:]')
|
||||
THEME_PATH="$THEMES_DIR/$THEME_NAME"
|
||||
|
||||
# The name comes from the URL, is joined into a path that is about to be
|
||||
# removed, and then names a directory the rest of Omarchy passes around by
|
||||
# name: Style > Unlock builds a command line out of the one the picker
|
||||
# returned. So it is held to the characters a theme name needs rather than
|
||||
# screened for the harm of the day -- a repo called `..` would take
|
||||
# ~/.config/omarchy with it, and one called `a';'id` would carry its own
|
||||
# command into that picker. The leading character is kept out of `.` and `-`,
|
||||
# which also covers `host:-s/foo.git` leaving basename with `.git`.
|
||||
# A bracket range follows the locale's collation, not ASCII: `[a-z]` takes in
|
||||
# `é` under en_US.UTF-8. Pin the locale so the set is the one written here.
|
||||
if ! (LC_ALL=C; [[ $THEME_NAME =~ ^[a-z0-9_][a-z0-9._+-]*$ ]]); then
|
||||
# The name comes from the URL and is joined into a path that is about to be
|
||||
# removed, so a repo called `..` would take ~/.config/omarchy with it. A leading
|
||||
# dot is refused with it: `host:-s/foo.git` leaves basename with `.git`.
|
||||
if [[ -z $THEME_NAME || $THEME_NAME == .* || $THEME_NAME == */* ]]; then
|
||||
echo "Error: '$REPO_URL' does not give a usable theme name."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -5,9 +5,3 @@
|
||||
# omarchy:examples=omarchy toggle bar | omarchy toggle bar off | omarchy toggle bar on
|
||||
|
||||
omarchy-toggle bar-off "${1:-toggle}"
|
||||
|
||||
# The shell's watch on the toggles directory can miss flag changes that land in
|
||||
# quick succession, stranding the bar off screen until the shell restarts.
|
||||
# Nudge the bar to re-read the flag; quiet best-effort so the toggle still
|
||||
# works when the shell is not up.
|
||||
omarchy-shell -q omarchy.bar syncHidden
|
||||
|
||||
@@ -3,16 +3,16 @@
|
||||
# omarchy:summary=Prompt for confirmation before starting an update
|
||||
|
||||
gum style --border normal --padding "1 2" \
|
||||
"Ready to update?" \
|
||||
"准备好更新了吗?" \
|
||||
"" \
|
||||
"• You cannot stop the update once you start!" \
|
||||
"• Make sure you're connected to power or have a full battery" \
|
||||
"• 更新一旦开始就不能中止!" \
|
||||
"• 请确保已接通电源或电量充足" \
|
||||
"" \
|
||||
"What's new: https://github.com/basecamp/omarchy/releases/latest"
|
||||
"更新内容: https://git.zacharyzhang.com/ZacharyZhang-NY/omarchycn/releases"
|
||||
|
||||
echo
|
||||
|
||||
if ! gum confirm "Continue with update?"; then
|
||||
echo "Update cancelled"
|
||||
if ! gum confirm "继续更新?"; then
|
||||
echo "已取消更新"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -597,9 +597,6 @@ preserve_kernel_cmdline_root() {
|
||||
fi
|
||||
|
||||
log "Preserving the kernel cmdline root parameters in $default_conf"
|
||||
# omarchy:heredoc-expands paths=none -- ${boot_params[*]} is the kernel
|
||||
# cmdline fragment assembled above from findmnt output (root=, rootflags=),
|
||||
# not a filesystem path this script controls.
|
||||
as_root tee -a "$default_conf" >/dev/null <<EOF
|
||||
# Written by omarchy-upgrade-to-quattro. The += drop-ins in
|
||||
# /etc/limine-entry-tool.d/ stop limine-entry-tool from reading
|
||||
@@ -1190,19 +1187,11 @@ ensure_sleep_lock_service() {
|
||||
}
|
||||
|
||||
run_post_upgrade_migrations() {
|
||||
local pending_status
|
||||
PATH="$package_path" command -v omarchy-migrate >/dev/null 2>&1 || return 0
|
||||
|
||||
log "Running Omarchy migrations"
|
||||
if ! run_as_user_omarchy OMARCHY_UPGRADE_TO_QUATTRO_LIVE=1 omarchy-migrate; then
|
||||
fail "Omarchy migrations did not complete. Fix the error above and rerun the upgrade before rebooting."
|
||||
fi
|
||||
if run_as_user_omarchy omarchy-migrate --pending >/dev/null; then
|
||||
fail "Omarchy migrations are still pending. Rerun the upgrade before rebooting."
|
||||
else
|
||||
pending_status=$?
|
||||
if (( pending_status != 1 )); then
|
||||
fail "Could not verify that Omarchy migrations completed. Rerun the upgrade before rebooting."
|
||||
fi
|
||||
warn "Could not run Omarchy migrations; the user may be prompted to run them after login."
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -1339,7 +1328,7 @@ apply_system_transition() {
|
||||
'
|
||||
fi
|
||||
as_root install -d -m 0755 /usr/lib/chromium
|
||||
printf '%s\n' '{"distribution":{"require_eula":false},"browser":{"theme":{"color_scheme":0,"color_scheme2":0}}}' | \
|
||||
printf '%s\n' '{"browser":{"theme":{"color_scheme":0,"color_scheme2":0}}}' | \
|
||||
as_root tee /usr/lib/chromium/initial_preferences >/dev/null
|
||||
|
||||
# Deliberately do NOT add the user to the docker group. That group is
|
||||
@@ -1375,6 +1364,7 @@ EOF
|
||||
as_root systemctl disable docker.service >/dev/null 2>&1 || true
|
||||
|
||||
enable_system_service cups.service
|
||||
enable_system_service cups-browsed.service
|
||||
enable_system_service avahi-daemon.service
|
||||
enable_system_service linux-modules-cleanup.service
|
||||
enable_system_service docker.socket
|
||||
@@ -1406,9 +1396,6 @@ EOF
|
||||
autologin_user=$(as_root awk -F= '/^User=/ { print $2; exit }' /etc/sddm.conf.d/autologin.conf 2>/dev/null || true)
|
||||
fi
|
||||
[[ -n ${autologin_user:-} ]] || autologin_user="$target_user"
|
||||
# omarchy:heredoc-expands paths=none -- $autologin_user is a username, read
|
||||
# back from the root-owned drop-in or falling back to $target_user. Same
|
||||
# mechanism as the old getty override: a name expands, no path does.
|
||||
cat <<EOF | as_root tee /etc/sddm.conf.d/autologin.conf >/dev/null
|
||||
[Autologin]
|
||||
User=$autologin_user
|
||||
@@ -1419,8 +1406,6 @@ EOF
|
||||
fi
|
||||
|
||||
as_root install -d -m 0755 -o sddm -g sddm /var/lib/sddm 2>/dev/null || as_root install -d -m 0755 /var/lib/sddm
|
||||
# omarchy:heredoc-expands paths=none -- $target_user is a username, not a
|
||||
# path; SDDM's state file records who logged in last.
|
||||
cat <<EOF | as_root tee /var/lib/sddm/state.conf >/dev/null
|
||||
[Last]
|
||||
Session=omarchy.desktop
|
||||
@@ -2225,10 +2210,6 @@ fi
|
||||
if command -v xdg-settings >/dev/null 2>&1; then
|
||||
env -u BROWSER xdg-settings set default-web-browser chromium.desktop || true
|
||||
fi
|
||||
if command -v xdg-mime >/dev/null 2>&1; then
|
||||
xdg-mime default HEY.desktop x-scheme-handler/mailto || true
|
||||
fi
|
||||
|
||||
# This script is fetched from the branch and runs against whatever packaged
|
||||
# tree the channel currently serves, so the packaged tree can be older than the
|
||||
# script. The completion markers are written directly rather than through
|
||||
@@ -2374,8 +2355,8 @@ run_as_user_omarchy omarchy-bar defaults ||
|
||||
cleanup_retired_services
|
||||
ensure_sleep_lock_service
|
||||
remove_retired_default_packages
|
||||
run_final_system_package_upgrade
|
||||
run_post_upgrade_migrations
|
||||
run_final_system_package_upgrade
|
||||
run_post_upgrade_update_steps
|
||||
refresh_current_theme_after_upgrade
|
||||
# Do not force-reload Hyprland in the live upgraded session. The legacy
|
||||
|
||||
@@ -14,8 +14,6 @@ fi
|
||||
|
||||
if grep -q "https://pkgs.omarchy.org/stable/" /etc/pacman.conf; then
|
||||
pkgs="stable"
|
||||
elif grep -q "https://pkgs.omarchy.org/rc/" /etc/pacman.conf; then
|
||||
pkgs="rc"
|
||||
elif grep -q "https://pkgs.omarchy.org/edge/" /etc/pacman.conf; then
|
||||
pkgs="edge"
|
||||
else
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Open HEY webmail and translate mailto links
|
||||
# omarchy:args=[url]
|
||||
|
||||
url="$1"
|
||||
web_url="https://app.hey.com"
|
||||
|
||||
# Handle mailto: URLs
|
||||
if [[ $url =~ ^mailto: ]]; then
|
||||
email=$(echo "$url" | sed 's/mailto://')
|
||||
web_url="https://app.hey.com/messages/new?to=$email"
|
||||
fi
|
||||
|
||||
exec omarchy-launch-webapp "$web_url"
|
||||
@@ -54,34 +54,6 @@ download_icon() {
|
||||
[[ -s $2 && $(file -b --mime-type "$2") == image/* ]]
|
||||
}
|
||||
|
||||
# Chromium --app= treats javascript:, file:, and data: as a document to
|
||||
# run. Prefix schemeless input with https as before, then refuse anything
|
||||
# that is not http(s).
|
||||
normalize_webapp_url() {
|
||||
local url=$1
|
||||
if [[ ! $url =~ ^[a-zA-Z][a-zA-Z0-9+.-]*: ]]; then
|
||||
url="https://$url"
|
||||
fi
|
||||
printf '%s' "$url"
|
||||
}
|
||||
|
||||
# Raw whitespace must be percent-encoded in a URL. Refuse it before serializing
|
||||
# the desktop entry; before Exec argument quoting, it also split browser flags
|
||||
# and additional URLs into separate arguments. Schemes are case-insensitive.
|
||||
require_http_url() {
|
||||
local url=$1
|
||||
|
||||
if [[ $url =~ [[:space:]] ]]; then
|
||||
echo "Error: web app URL must not contain whitespace." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! ${url,,} =~ ^https?:// ]]; then
|
||||
echo "Error: web app URL must be http or https." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
fetch_site_icon() {
|
||||
local site_url="$1" dest="$2"
|
||||
local origin page icon_url
|
||||
@@ -105,44 +77,14 @@ fetch_site_icon() {
|
||||
download_icon "https://www.google.com/s2/favicons?domain=${site_url}&sz=256" "$dest"
|
||||
}
|
||||
|
||||
desktop_string_escape() {
|
||||
# Desktop Entry "string" value (freedesktop Desktop Entry Spec, "Value types"):
|
||||
# a raw newline would start a new key line and let a value inject a second
|
||||
# Exec=. Escape backslash first, then tab/CR/LF and a leading space. Every value
|
||||
# written into the .desktop file passes through here.
|
||||
#
|
||||
# Parameter expansion rather than sed: GNU sed's N auto-prints the pattern space
|
||||
# and exits at end of input, so a `:a;N;$!ba` slurp skips every following s///
|
||||
# for a value with no newline in it - which is every value except the injection
|
||||
# attempt this exists to stop.
|
||||
local value="$1"
|
||||
|
||||
value=${value//\\/\\\\}
|
||||
value=${value//$'\t'/\\t}
|
||||
value=${value//$'\r'/\\r}
|
||||
value=${value//$'\n'/\\n}
|
||||
[[ $value == " "* ]] && value="\\s${value# }"
|
||||
|
||||
printf '%s' "$value"
|
||||
}
|
||||
|
||||
desktop_exec_arg() {
|
||||
# One Exec argument, double-quoted per the freedesktop Exec spec: inside quotes
|
||||
# " ` $ \ take a backslash and a literal % becomes %%. Only the default Exec's
|
||||
# URL needs this; $CUSTOM_EXEC stays a whole command line (file-syntax only).
|
||||
local escaped
|
||||
escaped=$(printf '%s' "$1" \
|
||||
| sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/`/\\`/g' -e 's/\$/\\$/g' -e 's/%/%%/g')
|
||||
printf '"%s"' "$escaped"
|
||||
}
|
||||
|
||||
if (( $# < 3 )); then
|
||||
echo -e "\e[32mLet's create a new web app you can start with the app launcher.\n\e[0m"
|
||||
APP_NAME=$(gum input --prompt "Name> " --placeholder "My favorite web app")
|
||||
require_plain_name "$APP_NAME"
|
||||
APP_URL=$(gum input --prompt "URL> " --placeholder "https://example.com")
|
||||
APP_URL=$(normalize_webapp_url "$APP_URL")
|
||||
require_http_url "$APP_URL"
|
||||
if [[ ! $APP_URL =~ ^[a-zA-Z][a-zA-Z0-9+.-]*: ]]; then
|
||||
APP_URL="https://$APP_URL"
|
||||
fi
|
||||
|
||||
# Try to fetch the site's icon automatically first.
|
||||
mkdir -p "$ICON_DIR"
|
||||
@@ -159,8 +101,10 @@ if (( $# < 3 )); then
|
||||
INTERACTIVE_MODE=true
|
||||
else
|
||||
APP_NAME="$1"
|
||||
APP_URL=$(normalize_webapp_url "$2")
|
||||
require_http_url "$APP_URL"
|
||||
APP_URL="$2"
|
||||
if [[ ! $APP_URL =~ ^[a-zA-Z][a-zA-Z0-9+.-]*: ]]; then
|
||||
APP_URL="https://$APP_URL"
|
||||
fi
|
||||
ICON_REF="$3"
|
||||
CUSTOM_EXEC="$4" # Optional custom exec command
|
||||
MIME_TYPES="$5" # Optional mime types
|
||||
@@ -199,39 +143,29 @@ else
|
||||
ICON_VALUE=$(icon_name_from_ref "$ICON_REF")
|
||||
fi
|
||||
|
||||
# Default Exec quotes the URL as one Exec-spec argument; the whole line then gets
|
||||
# the file-syntax escaping below (unescaped first at read time per spec, so the
|
||||
# layers compose). $CUSTOM_EXEC is a full command line, so it gets file-syntax only.
|
||||
if [[ -n $CUSTOM_EXEC ]]; then
|
||||
EXEC_COMMAND=$CUSTOM_EXEC
|
||||
else
|
||||
EXEC_COMMAND="omarchy-launch-webapp $(desktop_exec_arg "$APP_URL")"
|
||||
fi
|
||||
# Use custom exec if provided, otherwise default behavior
|
||||
EXEC_COMMAND="${CUSTOM_EXEC:-omarchy-launch-webapp $APP_URL}"
|
||||
|
||||
# Create application .desktop file
|
||||
DESKTOP_DIR="$HOME/.local/share/applications"
|
||||
DESKTOP_FILE="$DESKTOP_DIR/$APP_NAME.desktop"
|
||||
mkdir -p "$DESKTOP_DIR"
|
||||
|
||||
name_field=$(desktop_string_escape "$APP_NAME")
|
||||
exec_field=$(desktop_string_escape "$EXEC_COMMAND")
|
||||
icon_field=$(desktop_string_escape "$ICON_VALUE")
|
||||
|
||||
cat >"$DESKTOP_FILE" <<EOF
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Name=$name_field
|
||||
Comment=$name_field
|
||||
Exec=$exec_field
|
||||
Name=$APP_NAME
|
||||
Comment=$APP_NAME
|
||||
Exec=$EXEC_COMMAND
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Icon=$icon_field
|
||||
Icon=$ICON_VALUE
|
||||
StartupNotify=true
|
||||
EOF
|
||||
|
||||
# Add mime types if provided
|
||||
if [[ -n $MIME_TYPES ]]; then
|
||||
printf 'MimeType=%s\n' "$(desktop_string_escape "$MIME_TYPES")" >>"$DESKTOP_FILE"
|
||||
echo "MimeType=$MIME_TYPES" >>"$DESKTOP_FILE"
|
||||
fi
|
||||
|
||||
chmod +x "$DESKTOP_FILE"
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
# OmarchyCN command center: routes to omarchy cn <command>
|
||||
|
||||
set -o pipefail
|
||||
|
||||
OMARCHY_BIN_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
||||
|
||||
exec "$OMARCHY_BIN_DIR/omarchy" cn "$@"
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"_verified": "2026-08-24, AUR RPC 实证在维护且未 out-of-date",
|
||||
"apps": {
|
||||
"wechat": { "name": "微信", "source": "aur", "package": "wechat-universal-bwrap", "license": "proprietary" },
|
||||
"qq": { "name": "QQ", "source": "aur", "package": "linuxqq", "license": "proprietary" },
|
||||
"feishu": { "name": "飞书", "source": "aur", "package": "feishu-bin", "license": "proprietary" },
|
||||
"dingtalk": { "name": "钉钉", "source": "aur", "package": "dingtalk-bin", "license": "proprietary" },
|
||||
"wemeet": { "name": "腾讯会议", "source": "aur", "package": "wemeet-bin", "license": "proprietary" },
|
||||
"wps": { "name": "WPS Office", "source": "aur", "package": "wps-office-cn", "license": "proprietary" },
|
||||
"tencent-docs": { "name": "腾讯文档", "source": "webapp", "url": "https://docs.qq.com", "icon": "https://docs.qq.com/favicon.ico", "license": "web" },
|
||||
"yuque": { "name": "语雀", "source": "webapp", "url": "https://www.yuque.com/dashboard", "icon": "https://www.yuque.com/favicon.ico", "license": "web" },
|
||||
"shimo": { "name": "石墨文档", "source": "webapp", "url": "https://shimo.im/desktop", "icon": "https://shimo.im/favicon.ico", "license": "web" }
|
||||
}
|
||||
}
|
||||