Compare commits
154
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b686ed892d | ||
|
|
5c03dc8c09 | ||
|
|
3c2a24b248 | ||
|
|
986962bb64 | ||
|
|
a93ee6a433 | ||
|
|
ca4f596a14 | ||
|
|
279f4d6b95 | ||
|
|
243fe1c9d9 | ||
|
|
bae189861f | ||
|
|
981274b20a | ||
|
|
fe1325202b | ||
|
|
432b5e3e24 | ||
|
|
99ec17acfa | ||
|
|
9ca8f90e91 | ||
|
|
d3a5e69162 | ||
|
|
d6130394fa | ||
|
|
7aceb388e7 | ||
|
|
e1fc502286 | ||
|
|
e68994680a | ||
|
|
55a3906f4c | ||
|
|
f8d7fae7a8 | ||
|
|
3def390764 | ||
|
|
a24064c720 | ||
|
|
71d7ac81ae | ||
|
|
4271b880c3 | ||
|
|
884ca49340 | ||
|
|
21b27c5aed | ||
|
|
df819a6f98 | ||
|
|
943d2fcbe9 | ||
|
|
58c399de30 | ||
|
|
15f26cbe1b | ||
|
|
4c23077f80 | ||
|
|
40d0c9bbdf | ||
|
|
8add7b49de | ||
|
|
625c4a1603 | ||
|
|
e3b566bae8 | ||
|
|
96404be37b | ||
|
|
844f320bbe | ||
|
|
8e41961c7d | ||
|
|
002c70a544 | ||
|
|
e711066522 | ||
|
|
f91d2e5453 | ||
|
|
4d697a063c | ||
|
|
4996941513 | ||
|
|
7ed761fb31 | ||
|
|
80e7c25b37 | ||
|
|
394c1371c9 | ||
|
|
d593847728 | ||
|
|
96ed473ce1 | ||
|
|
cd519283fe | ||
|
|
a75551de4e | ||
|
|
158e8cfb3a | ||
|
|
e229927671 | ||
|
|
70e79c40ae | ||
|
|
c720f0b981 | ||
|
|
5236f4426c | ||
|
|
2002fb35ed | ||
|
|
e47784be11 | ||
|
|
96d5682460 | ||
|
|
bf20c94ea0 | ||
|
|
bb5b178e5f | ||
|
|
1b92b7f5be | ||
|
|
aac1b009e4 | ||
|
|
1dbc7d5bce | ||
|
|
d67a7c00bc | ||
|
|
24c18df5b7 | ||
|
|
e9ba17e52e | ||
|
|
eb76684c60 | ||
|
|
56fbaf4689 | ||
|
|
363db1f569 | ||
|
|
11fa6b9809 | ||
|
|
bf10b75150 | ||
|
|
079d116511 | ||
|
|
d3b7810a76 | ||
|
|
05fb36d3cc | ||
|
|
a465dfa681 | ||
|
|
20a23b8c16 | ||
|
|
0f15e2330f | ||
|
|
2b91fdc0d3 | ||
|
|
0b3f1b7ead | ||
|
|
f20bf0a21b | ||
|
|
9da8824098 | ||
|
|
169ad00a84 | ||
|
|
74997fd523 | ||
|
|
f0672772d2 | ||
|
|
62eb5182d0 | ||
|
|
6b10dbf191 | ||
|
|
5925929cb6 | ||
|
|
7d58bb9a62 | ||
|
|
6dd9aa55cc | ||
|
|
468b511249 | ||
|
|
4fc14173b7 | ||
|
|
06e32d243d | ||
|
|
a165185a3f | ||
|
|
d1845245d3 | ||
|
|
9d8c0176d1 | ||
|
|
9382410026 | ||
|
|
b07374f03c | ||
|
|
68fc0cf6e6 | ||
|
|
2b923cf5bd | ||
|
|
521f1ae9ac | ||
|
|
c2587dff08 | ||
|
|
83881e979b | ||
|
|
9ece53cede | ||
|
|
5c336885d2 | ||
|
|
946704f309 | ||
|
|
c5a5e14e99 | ||
|
|
4be440b501 | ||
|
|
7026ede90b | ||
|
|
9d02bb08f8 | ||
|
|
b68d4142d7 | ||
|
|
ea6ee9440a | ||
|
|
eeb4206c7b | ||
|
|
8d14869689 | ||
|
|
eb7ecd13f3 | ||
|
|
877f1e96ef | ||
|
|
3b0e899029 | ||
|
|
187c268d68 | ||
|
|
0260d2accb | ||
|
|
75e51f0b95 | ||
|
|
e428dc2627 | ||
|
|
6e962b4466 | ||
|
|
3af7675a10 | ||
|
|
0ae1694830 | ||
|
|
77305ed3b9 | ||
|
|
45749c5b68 | ||
|
|
bafc9a1000 | ||
|
|
44a186afe4 | ||
|
|
bebe19bc70 | ||
|
|
87dfa14c56 | ||
|
|
b0e6611c70 | ||
|
|
95b791af16 | ||
|
|
c34d20ca14 | ||
|
|
2c93e66b0c | ||
|
|
9301092404 | ||
|
|
4cd8a081cb | ||
|
|
b91180a808 | ||
|
|
e53548fae2 | ||
|
|
fe56d68e90 | ||
|
|
23dab9ec4d | ||
|
|
9285b19d6a | ||
|
|
4637735aa2 | ||
|
|
68ab12f77d | ||
|
|
12c350e404 | ||
|
|
30471bf35a | ||
|
|
8bee78bc63 | ||
|
|
597f57a198 | ||
|
|
b86d4505c1 | ||
|
|
06a3dbca42 | ||
|
|
e66c27f1e7 | ||
|
|
44b00a4e80 | ||
|
|
7c896d3521 | ||
|
|
94a7b70ed4 | ||
|
|
ec9da050ee |
@@ -1,50 +0,0 @@
|
||||
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.\"}"
|
||||
@@ -0,0 +1,2 @@
|
||||
# Merges to protected branches need sign-off from an org owner.
|
||||
* @dhh @ryanrhughes
|
||||
@@ -0,0 +1,47 @@
|
||||
# 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,7 +2,3 @@
|
||||
# Python bytecode (orchestrator)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Local working documents
|
||||
OmarchyCN PRD.md
|
||||
OmarchyCN-TASKS.md
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
# 行为准则
|
||||
|
||||
参与 OmarchyCN 社区(issue、PR、讨论)时:
|
||||
|
||||
- 尊重他人,就事论事,不进行人身攻击、骚扰或歧视
|
||||
- 欢迎新手提问,回答保持耐心
|
||||
- 技术分歧用证据和代码说话
|
||||
- 不发布垃圾信息、广告或与项目无关的内容
|
||||
|
||||
违反者由维护者视情节警告、删除内容或封禁账号。
|
||||
举报渠道见 [SECURITY.md](SECURITY.md) 中的联系方式。
|
||||
@@ -1,23 +0,0 @@
|
||||
# 贡献指南
|
||||
|
||||
## 仓库结构
|
||||
|
||||
- 上游 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 与代码审查后合并
|
||||
@@ -1,14 +0,0 @@
|
||||
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,44 +1,79 @@
|
||||
# OmarchyCN
|
||||
# Omarchy
|
||||
|
||||
面向中国开发者的 [Omarchy](https://omarchy.org) 下游发行版:完整保留 Omarchy 的 Arch + Hyprland 桌面体验,为中国网络环境、中文使用习惯与国内 AI 服务做系统级增强。
|
||||
Omarchy is a beautiful, modern & opinionated Linux distribution by DHH.
|
||||
|
||||
当前版本 `4.0.0.alpha-cn.1`(基于上游 `quattro` 分支构建的基线 ISO)。
|
||||
Read more at [omarchy.org](https://omarchy.org).
|
||||
|
||||
## 特性
|
||||
## The Omarchy Manual
|
||||
|
||||
继承自 Omarchy:
|
||||
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.
|
||||
|
||||
- Hyprland 动态平铺桌面 + Quickshell 顶栏、菜单与系统面板,键盘驱动工作流
|
||||
- `omarchy` CLI 与 `Super + Space` 系统菜单,CLI 与 GUI 同构
|
||||
- 内置主题系统与一键换肤,终端 / 编辑器 / 桌面配色统一
|
||||
- AI coding agent 桌面集成(Claude Code、Codex、OpenCode 等,`mise` 按需安装)
|
||||
- Btrfs + Snapper 更新前快照与系统回滚
|
||||
- archiso 离线安装镜像,支持无人值守安装与双系统引导
|
||||
- [Welcome to Omarchy!](manual/01-welcome-to-omarchy.md)
|
||||
|
||||
OmarchyCN 增强(开发中,按路线图逐步交付):
|
||||
**The Basics**
|
||||
|
||||
- **中国镜像管理**:Arch / npm / pip / Cargo / Go 等软件源测速、自动选择与故障切换
|
||||
- **中文环境开箱即用**:zh_CN locale、思源黑体 / 宋体字体栈、高分屏分数缩放预设
|
||||
- **中文输入法**:Fcitx5 + Rime 预配置,Wayland / GTK / Qt / Electron 全栈兼容,快捷键冲突自动处理
|
||||
- **AI Hub**:Kimi、DeepSeek、Z.AI/GLM 一等 Provider 支持,与 Claude Code、Codex、OpenCode、Kimi Code、Deep Code 等 Harness 的统一配置向导、凭据安全存储与连接诊断
|
||||
- **国内应用中心**:微信、QQ、飞书、钉钉、腾讯会议、WPS 等应用的可信安装入口
|
||||
- **Overlay 安装器**:在现有 Omarchy 上叠加 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**
|
||||
|
||||
## 上游文档
|
||||
- [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 完整英文手册在 [`manual/`](manual/) 目录,亦见 [learn.omacom.io](https://learn.omacom.io/2/the-omarchy-manual)。
|
||||
**The Rest**
|
||||
|
||||
## 声明与许可证
|
||||
- [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 is an independent community distribution based on Omarchy. OmarchyCN is not affiliated with or endorsed by Basecamp, 37signals, or the Omarchy maintainers.
|
||||
## License
|
||||
|
||||
代码沿用上游 [MIT License](LICENSE),保留 Omarchy 原始版权声明。
|
||||
Omarchy is released under the [MIT License](https://opensource.org/licenses/MIT).
|
||||
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
# 安全政策
|
||||
|
||||
## 报告漏洞
|
||||
|
||||
请勿在公开 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,6 +126,7 @@ 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.
|
||||
@@ -165,3 +166,5 @@ 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
-1
@@ -39,8 +39,8 @@ 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"
|
||||
GROUP_DESCRIPTIONS[finalize]="Finalize user setup"
|
||||
GROUP_DESCRIPTIONS[default]="Default application selection"
|
||||
|
||||
+3
-1
@@ -92,8 +92,10 @@ omp)
|
||||
;;
|
||||
ori)
|
||||
# Ori is a harness launcher, and `ori code` is the agent it runs itself.
|
||||
# A prompt alone means one headless turn there, printed after the turn ends,
|
||||
# so --interactive is what seeds the session with it and keeps the window.
|
||||
command=(ori code)
|
||||
[[ -n ${prompt:-} ]] && command+=(--prompt "$prompt")
|
||||
[[ -n ${prompt:-} ]] && command+=(--interactive --prompt "$prompt")
|
||||
;;
|
||||
pi)
|
||||
command=(pi)
|
||||
|
||||
@@ -528,7 +528,7 @@ def fetch_codex_rpc():
|
||||
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
[codex, "-s", "read-only", "-a", "untrusted", "app-server"],
|
||||
[codex, "-s", "read-only", "-a", "on-request", "app-server"],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
|
||||
@@ -4,7 +4,13 @@
|
||||
# omarchy:args=[--no-osd] [+N%|N%-|N%]
|
||||
# omarchy:examples=omarchy brightness display apple | omarchy brightness display apple +5% | omarchy brightness display apple --no-osd 50%
|
||||
|
||||
device_cache="${XDG_RUNTIME_DIR:-/tmp}/omarchy-brightness-display-apple.device"
|
||||
# Only cache under the user-private runtime dir. With no XDG_RUNTIME_DIR we skip
|
||||
# caching (detect every run) rather than fall back to a predictable, world-writable
|
||||
# /tmp path another user could pre-create.
|
||||
device_cache=""
|
||||
if [[ -n ${XDG_RUNTIME_DIR:-} ]]; then
|
||||
device_cache="$XDG_RUNTIME_DIR/omarchy-brightness-display-apple.device"
|
||||
fi
|
||||
no_osd=0
|
||||
if [[ ${1:-} == "--no-osd" ]]; then
|
||||
no_osd=1
|
||||
@@ -28,9 +34,14 @@ find_apple_display_device() {
|
||||
local cached=""
|
||||
local device=""
|
||||
|
||||
if [[ -r $device_cache ]]; then
|
||||
if [[ -n $device_cache && -r $device_cache ]]; then
|
||||
read -r cached <"$device_cache" || true
|
||||
if [[ -n $cached && -e $cached ]]; then
|
||||
# Trust a cached value only if it still names a hiddev character device. A
|
||||
# stale or unexpected cache (a regular file, a non-hiddev node) is ignored and
|
||||
# we re-detect instead of handing an arbitrary path to asdcontrol. The globs
|
||||
# are left unquoted on purpose: [[ ]] pattern-matches an unquoted right side,
|
||||
# and quoting them would turn the match into a literal string comparison.
|
||||
if [[ ( $cached == /dev/hiddev* || $cached == /dev/usb/hiddev* ) && -c $cached ]]; then
|
||||
printf '%s\n' "$cached"
|
||||
return 0
|
||||
fi
|
||||
@@ -39,7 +50,9 @@ find_apple_display_device() {
|
||||
device="$(detect_apple_display_device)" || return 1
|
||||
[[ -n $device ]] || return 1
|
||||
|
||||
printf '%s\n' "$device" >"$device_cache"
|
||||
if [[ -n $device_cache ]]; then
|
||||
printf '%s\n' "$device" >"$device_cache"
|
||||
fi
|
||||
printf '%s\n' "$device"
|
||||
}
|
||||
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
#!/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=$(omarchy-cn-ai-secret get "$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)
|
||||
fast=$(cn_ai_fast_model "$provider")
|
||||
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' 将以该配置启动"
|
||||
@@ -1,59 +0,0 @@
|
||||
#!/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 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))
|
||||
@@ -1,52 +0,0 @@
|
||||
#!/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=$(omarchy-cn-ai-secret get "$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
|
||||
@@ -1,73 +0,0 @@
|
||||
#!/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 [[ -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 ! omarchy-cn-ai-secret get "$provider" > /dev/null 2>&1; then
|
||||
echo "提示: 尚未存储 $provider 的 API Key,运行: omarchycn ai secret set $provider"
|
||||
fi
|
||||
@@ -1,30 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,19 +0,0 @@
|
||||
#!/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"
|
||||
@@ -1,104 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,76 +0,0 @@
|
||||
#!/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 [[ -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 自管鉴权:首次启动时按其官方流程登录/配置"
|
||||
echo "完成。启动: omarchycn ai launch"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! omarchy-cn-ai-secret get "$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"
|
||||
echo "完成。启动: omarchycn ai launch"
|
||||
@@ -1,94 +0,0 @@
|
||||
#!/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=$(omarchy-cn-ai-secret get "$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))
|
||||
@@ -1,46 +0,0 @@
|
||||
#!/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)
|
||||
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 安装完成"
|
||||
@@ -1,15 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,36 +0,0 @@
|
||||
#!/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"
|
||||
@@ -1,32 +0,0 @@
|
||||
#!/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))
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,20 +0,0 @@
|
||||
#!/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"
|
||||
@@ -1,61 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,97 +0,0 @@
|
||||
#!/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
|
||||
}
|
||||
|
||||
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 ;;
|
||||
dev-mirror) check_dev_mirror ;;
|
||||
ime) check_ime ;;
|
||||
all)
|
||||
check_network
|
||||
check_mirror
|
||||
check_dev_mirror
|
||||
check_ime
|
||||
;;
|
||||
esac
|
||||
|
||||
if (( failures > 0 )); then
|
||||
echo "Doctor: $failures failure(s)"
|
||||
exit 1
|
||||
fi
|
||||
echo "Doctor: all checks passed"
|
||||
@@ -1,15 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,36 +0,0 @@
|
||||
#!/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))
|
||||
@@ -1,28 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,63 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,54 +0,0 @@
|
||||
#!/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))
|
||||
@@ -1,148 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,22 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,36 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,27 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,9 +0,0 @@
|
||||
#!/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>}"
|
||||
@@ -1,19 +0,0 @@
|
||||
#!/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##*/}"
|
||||
@@ -1,22 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,33 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,31 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,120 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,20 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,43 +0,0 @@
|
||||
#!/bin/bash
|
||||
# omarchy:summary=Update an overlay install: pull source, reinstall, run migrations
|
||||
# omarchy:examples=omarchycn update
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
MANIFEST="$HOME/.local/state/omarchycn/overlay-manifest"
|
||||
MIGRATION_DONE_DIR="$HOME/.local/state/omarchycn/migrations-done"
|
||||
|
||||
if [[ ! -f $MANIFEST ]]; then
|
||||
echo "Not an overlay install (no $MANIFEST)." >&2
|
||||
echo "Package-based installs update through 'omarchy update' / pacman." >&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
|
||||
|
||||
old=$(git -C "$src" rev-parse --short HEAD)
|
||||
git -C "$src" pull --ff-only
|
||||
new=$(git -C "$src" rev-parse --short HEAD)
|
||||
echo "Source: $old -> $new"
|
||||
|
||||
"$src/bin/omarchy-cn-install-overlay"
|
||||
|
||||
# Run each not-yet-completed migration in filename order, one marker per file
|
||||
if [[ -d $src/cn/migrations ]]; then
|
||||
mkdir -p "$MIGRATION_DONE_DIR"
|
||||
for m in "$src"/cn/migrations/*.sh; do
|
||||
[[ -f $m ]] || continue
|
||||
name="${m##*/}"
|
||||
if [[ ! -f $MIGRATION_DONE_DIR/$name ]]; then
|
||||
echo "Migration: $name"
|
||||
bash -euo pipefail "$m"
|
||||
touch "$MIGRATION_DONE_DIR/$name"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
echo "OmarchyCN update complete ($(omarchy-cn-version 2>/dev/null || echo unknown))"
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/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}"
|
||||
Executable
+73
@@ -0,0 +1,73 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Silence crash notifications for one program, or list what is silenced
|
||||
# omarchy:args=[--] [<program>] [on|off|toggle]
|
||||
# omarchy:examples=omarchy crash mute | omarchy crash mute hyprland | omarchy crash mute /usr/bin/hyprland | omarchy crash mute hyprland off
|
||||
|
||||
# The flag omarchy-crash-watch reads before announcing a crash. Muting is per
|
||||
# program; Trigger > Toggle > Crash Capture is the switch for all of them.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
readonly MUTES="$HOME/.local/state/omarchy/toggles/crash-ignore"
|
||||
|
||||
usage() {
|
||||
echo "Usage: omarchy crash mute [--] [<program>] [on|off|toggle]" >&2
|
||||
}
|
||||
|
||||
# Only regular files, because that is all the watcher honours: anything else in
|
||||
# there would be reported as muted while the crashes kept arriving. The dotted
|
||||
# glob is for a program legitimately called .hidden, and `.` and `..` fail the
|
||||
# same -f test that keeps them out.
|
||||
list() {
|
||||
local entry found=0
|
||||
|
||||
for entry in "$MUTES"/* "$MUTES"/.*; do
|
||||
[[ -f $entry ]] || continue
|
||||
printf '%s\n' "${entry##*/}"
|
||||
found=1
|
||||
done
|
||||
|
||||
((found)) || echo "No programs muted. Crashes all notify."
|
||||
}
|
||||
|
||||
# A program may be named -h, and the router answers that with its own help
|
||||
# before this ever runs. `omarchy crash mute -- -h` is the way through.
|
||||
[[ ${1:-} == "--" ]] && shift
|
||||
|
||||
if (($# == 0)); then
|
||||
list
|
||||
exit 0
|
||||
fi
|
||||
|
||||
program=$1
|
||||
action=${2:-on}
|
||||
|
||||
# The watcher keys the mute on the executable's basename, so accept the path it
|
||||
# reports as readily as the name, and reduce either the same way it does.
|
||||
program=${program##*/}
|
||||
|
||||
if [[ -z $program || $program == "." || $program == ".." ]]; then
|
||||
echo "Not a program name: $1" >&2
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "$action" in
|
||||
on|off|toggle) ;;
|
||||
*)
|
||||
echo "Not an action: $action" >&2
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
omarchy-toggle "crash-ignore/$program" "$action" || exit 1
|
||||
|
||||
# Report what is now true rather than what was asked for: the flag is what the
|
||||
# watcher reads, and a toggle does not say which way it went.
|
||||
if omarchy-toggle-enabled "crash-ignore/$program"; then
|
||||
echo "Muted crash notifications for $program."
|
||||
else
|
||||
echo "Crash notifications for $program are back on."
|
||||
fi
|
||||
+28
-5
@@ -48,12 +48,17 @@ announce() {
|
||||
# -n 0 so a restart does not re-announce crashes already dealt with.
|
||||
journalctl -f -n 0 -o json "MESSAGE_ID=$COREDUMP_MESSAGE_ID" 2>/dev/null |
|
||||
while IFS= read -r entry; do
|
||||
# A dash for a field that is empty as well as one that is missing: tab is
|
||||
# IFS whitespace, so an empty field collapses into the next delimiter and
|
||||
# every field after it shifts along one. A process can set its own comm to
|
||||
# nothing, and that crash used to be read as somebody else's and dropped.
|
||||
IFS=$'\t' read -r uid comm pid exe signal < <(
|
||||
jq -r '[(._UID // "-"),
|
||||
(.COREDUMP_COMM // "-"),
|
||||
(.COREDUMP_PID // "-"),
|
||||
(.COREDUMP_EXE // "-"),
|
||||
(.COREDUMP_SIGNAL_NAME // "-")] | @tsv' <<<"$entry" 2>/dev/null
|
||||
jq -r 'def field: if . == null or . == "" then "-" else . end;
|
||||
[(._UID | field),
|
||||
(.COREDUMP_COMM | field),
|
||||
(.COREDUMP_PID | field),
|
||||
(.COREDUMP_EXE | field),
|
||||
(.COREDUMP_SIGNAL_NAME | field)] | @tsv' <<<"$entry" 2>/dev/null
|
||||
)
|
||||
|
||||
[[ $pid =~ ^[0-9]+$ ]] || continue
|
||||
@@ -71,11 +76,29 @@ journalctl -f -n 0 -o json "MESSAGE_ID=$COREDUMP_MESSAGE_ID" 2>/dev/null |
|
||||
name=$comm
|
||||
[[ $exe == /* ]] && name=${exe##*/}
|
||||
|
||||
# A process can set its own comm to anything prctl takes, slashes included,
|
||||
# and a crash with no recorded executable falls back to it. The mute below
|
||||
# turns this name into a path, so keep it one component: a crash must not
|
||||
# reach a flag outside crash-ignore/, nor have a diagnosis write one there.
|
||||
name=${name##*/}
|
||||
|
||||
# What that leaves is not always a name. "/" leaves nothing, which is no
|
||||
# kind of array subscript and no kind of toast; a dot component names a
|
||||
# directory rather than a flag, so a mute on it would touch that directory
|
||||
# and then never match; and a dash is what the read above puts there when
|
||||
# the crash recorded no name at all.
|
||||
[[ -n $name && $name != "-" && $name != "." && $name != ".." ]] || name=unknown
|
||||
|
||||
[[ -n $ignore_pattern && $name =~ $ignore_pattern ]] && continue
|
||||
|
||||
# Never announce our own machinery, or it notifies about itself.
|
||||
[[ $name == omarchy-crash-* || $name == omarchy-agent-* ]] && continue
|
||||
|
||||
# Muted at the end of a diagnosis, when the user was offered it and said
|
||||
# yes. A flag per program rather than one list, so omarchy-crash-mute can
|
||||
# lift one without reading, rewriting and re-parsing the rest.
|
||||
omarchy-toggle-enabled "crash-ignore/$name" && continue
|
||||
|
||||
now=$EPOCHSECONDS
|
||||
(((now - ${last_notified[$name]:-0}) < dedupe_seconds)) && continue
|
||||
|
||||
|
||||
@@ -34,8 +34,12 @@ 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, /usr/share/plymouth) are NOT covered — for those, use
|
||||
omarchy-dev-pkg-test to build and install the package from the checkout.
|
||||
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.
|
||||
|
||||
Also writes $sudoers_file so sudo resolves omarchy-*
|
||||
from the checkout instead of the packaged copies. That part takes effect
|
||||
|
||||
@@ -6,6 +6,18 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Whenever this runs as root — invoked directly through the passwordless
|
||||
# sudoers rule, or re-execed by require_root below — sudo's secure_path decides
|
||||
# where a bare helper resolves, and a dev link (etc/sudoers.d/omarchy-dev-path)
|
||||
# prepends a user-writable checkout bin/ to it. Every helper this script calls
|
||||
# by bare name (dirname, install, tee, rm, nmcli, systemctl, awk) is a system
|
||||
# tool, never an omarchy-* command, so pin PATH to trusted system directories
|
||||
# and keep root from resolving one out of that checkout. The unprivileged
|
||||
# wrapper phase keeps the caller's PATH so it can still find sudo/pkexec.
|
||||
if (( EUID == 0 )); then
|
||||
export PATH=/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin:/bin:/sbin
|
||||
fi
|
||||
|
||||
NM_DNS_CONF=/etc/NetworkManager/conf.d/20-omarchy-dns.conf
|
||||
|
||||
provider_from_arg() {
|
||||
@@ -137,6 +149,9 @@ 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]
|
||||
@@ -290,6 +305,9 @@ 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//,/ }
|
||||
|
||||
Executable
+50
@@ -0,0 +1,50 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Check that a git URL names a repository, not a transport helper
|
||||
# omarchy:args=<git-url>
|
||||
# omarchy:hidden=true
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# git picks a remote helper -- an executable it runs at clone time -- out of a URL
|
||||
# in exactly two shapes, and no others: `<helper>::<address>`, and
|
||||
# `<scheme>://<address>` for any scheme git does not handle itself. A single
|
||||
# colon is always scp-style ssh, and a bare path is always a path; neither can
|
||||
# reach a helper. So constraining those two shapes covers the whole surface.
|
||||
#
|
||||
# The `::` shape is refused outright, because no helper reachable that way is one
|
||||
# a theme or plugin URL has business naming, and `ext::` runs a shell command.
|
||||
# The `://` shape cannot be refused the same way, since it is also how every
|
||||
# legitimate URL arrives -- so it is allowlisted instead. The list is the
|
||||
# transports git still connects itself, `git+ssh` and `ssh+git` included: those
|
||||
# two are spelled like a helper but are read as plain ssh. `ext` and `fd` are
|
||||
# left out deliberately -- git ships a helper for each, and `ext` runs whatever
|
||||
# command the URL carries.
|
||||
TRANSPORTS=(ssh git git+ssh ssh+git http https ftp ftps file)
|
||||
|
||||
fail() {
|
||||
echo "omarchy-git-url-check: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
url="${1-}"
|
||||
|
||||
if [[ -z $url ]]; then
|
||||
fail "a git URL is required"
|
||||
fi
|
||||
|
||||
if [[ $url == -* || $url =~ ^[A-Za-z0-9][A-Za-z0-9+.-]*:: ]]; then
|
||||
fail "'$url' names a git option or transport helper, not a repository."
|
||||
fi
|
||||
|
||||
if [[ $url =~ ^([A-Za-z0-9][A-Za-z0-9+.-]*):// ]]; then
|
||||
scheme="${BASH_REMATCH[1]}"
|
||||
|
||||
for transport in "${TRANSPORTS[@]}"; do
|
||||
if [[ $scheme == "$transport" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
|
||||
fail "'$url' names the '$scheme' transport, which Omarchy does not clone from."
|
||||
fi
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Match the Dell XPS 13 DX13260 that requires the sidecar amplifier workaround.
|
||||
|
||||
product_sku="${OMARCHY_DMI_PRODUCT_SKU:-/sys/class/dmi/id/product_sku}"
|
||||
|
||||
omarchy-hw-match "DX13260" &&
|
||||
grep -qix "0E53" "$product_sku" 2>/dev/null
|
||||
@@ -11,6 +11,14 @@ MONITOR_LUA="$HOME/.config/hypr/monitors.lua"
|
||||
|
||||
INTERNAL=$(omarchy-hyprland-monitor-laptop)
|
||||
|
||||
# INTERNAL is written into generated Lua and hyprctl eval/dispatch below, so a
|
||||
# name that is not a plain connector string could execute on the next reload.
|
||||
# Names come from hyprctl; a user-created headless output can carry anything.
|
||||
if [[ -n $INTERNAL && ! $INTERNAL =~ ^[A-Za-z0-9._-]+$ ]]; then
|
||||
echo "Refusing unsafe internal monitor name" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
valid_scale() {
|
||||
[[ $1 =~ ^[0-9]+([.][0-9]+)?$ ]]
|
||||
}
|
||||
|
||||
@@ -28,6 +28,13 @@ off() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The name is written into generated Lua below, so only a plain connector
|
||||
# name may pass; anything else could execute on the next reload.
|
||||
if [[ ! $INTERNAL =~ ^[A-Za-z0-9._-]+$ ]]; then
|
||||
omarchy-notification-send -g "Refusing unsafe monitor name"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! omarchy-hyprland-monitor-external-active; then
|
||||
omarchy-notification-send -g "Can't disable the only active display"
|
||||
exit 1
|
||||
|
||||
@@ -22,6 +22,15 @@ on() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Both names are written into generated Lua below, so only plain connector
|
||||
# names may pass; a user-created headless output can carry any name.
|
||||
for output in "$INTERNAL" "$EXTERNAL"; do
|
||||
if [[ ! $output =~ ^[A-Za-z0-9._-]+$ ]]; then
|
||||
omarchy-notification-send -g "Refusing unsafe monitor name"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
omarchy-hyprland-toggle $DISABLE_TOGGLE off
|
||||
|
||||
if omarchy-hyprland-toggle-disabled $TOGGLE; then
|
||||
|
||||
@@ -80,6 +80,14 @@ set_scale() {
|
||||
local width="$(echo "$monitor_info" | jq -r '.width')"
|
||||
local height="$(echo "$monitor_info" | jq -r '.height')"
|
||||
local refresh_rate="$(echo "$monitor_info" | jq -r '.refreshRate')"
|
||||
|
||||
# active_monitor is written into the Lua string eval'd below, so only a plain
|
||||
# connector name may pass; a hostile output name could execute otherwise.
|
||||
if [[ ! $active_monitor =~ ^[A-Za-z0-9._-]+$ ]]; then
|
||||
echo "Refusing unsafe monitor name" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local new_scale="$(clean_scale "$requested_scale" "$width" "$height")"
|
||||
# GTK only honors integer GDK_SCALE values, so persist the nearest whole
|
||||
# factor even when the monitor scale itself is fractional.
|
||||
|
||||
@@ -16,6 +16,12 @@ 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} && (setsid uwsm-app -- gtk-launch ${desktop_id_arg} >/dev/null 2>&1 &)"
|
||||
"echo ${install_message}; omarchy-pkg-add ${packages_arg} && (setsid uwsm-app -- gtk-launch ${desktop_id_arg} >/dev/null 2>&1 &)"
|
||||
|
||||
+10
-1
@@ -12,4 +12,13 @@ if [[ -z $name || -z $packages ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exec omarchy-launch-floating-terminal-with-presentation "echo 'Installing ${name}...'; omarchy-pkg-add ${packages}"
|
||||
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}"
|
||||
|
||||
+11
-17
@@ -6,9 +6,10 @@
|
||||
|
||||
set -e
|
||||
|
||||
setup_policy_directory() {
|
||||
sudo mkdir -p "$1"
|
||||
sudo chmod a+rw "$1"
|
||||
source "$OMARCHY_PATH/install/helpers/browser-policy.sh"
|
||||
|
||||
setup_chromium_policy_directory() {
|
||||
browser_policy_setup_dir "$1"
|
||||
}
|
||||
|
||||
announce_browser_installed() {
|
||||
@@ -23,13 +24,6 @@ copy_chromium_flags() {
|
||||
omarchy-install-chromium-ytdlp
|
||||
}
|
||||
|
||||
setup_firefox_preferences() {
|
||||
local distribution_dir="$1"
|
||||
|
||||
setup_policy_directory "$distribution_dir"
|
||||
sudo cp -f "$OMARCHY_PATH/default/firefox/policies.json" "$distribution_dir/policies.json"
|
||||
}
|
||||
|
||||
setup_firefox_wayland() {
|
||||
mkdir -p ~/.config/environment.d
|
||||
echo "MOZ_ENABLE_WAYLAND=1" > ~/.config/environment.d/omarchy-firefox-wayland.conf
|
||||
@@ -40,7 +34,7 @@ chromium)
|
||||
echo "Installing Chromium..."
|
||||
omarchy-pkg-add chromium
|
||||
|
||||
setup_policy_directory /etc/chromium/policies/managed
|
||||
setup_chromium_policy_directory /etc/chromium/policies/managed
|
||||
copy_chromium_flags ~/.config/chromium-flags.conf
|
||||
omarchy-theme-set-browser
|
||||
announce_browser_installed "Chromium"
|
||||
@@ -49,7 +43,7 @@ chrome)
|
||||
echo "Installing Chrome..."
|
||||
omarchy-pkg-aur-add google-chrome || exit 1
|
||||
|
||||
setup_policy_directory /etc/opt/chrome/policies/managed
|
||||
setup_chromium_policy_directory /etc/opt/chrome/policies/managed
|
||||
copy_chromium_flags ~/.config/chrome-flags.conf
|
||||
omarchy-theme-set-browser
|
||||
announce_browser_installed "Chrome"
|
||||
@@ -58,7 +52,7 @@ edge)
|
||||
echo "Installing Edge..."
|
||||
omarchy-pkg-aur-add microsoft-edge-stable-bin || exit 1
|
||||
|
||||
setup_policy_directory /etc/opt/edge/policies/managed
|
||||
setup_chromium_policy_directory /etc/opt/edge/policies/managed
|
||||
copy_chromium_flags ~/.config/microsoft-edge-stable-flags.conf
|
||||
omarchy-theme-set-browser
|
||||
announce_browser_installed "Edge"
|
||||
@@ -67,7 +61,7 @@ brave)
|
||||
echo "Installing Brave..."
|
||||
omarchy-pkg-aur-add brave-bin || exit 1
|
||||
|
||||
setup_policy_directory /etc/brave/policies/managed
|
||||
setup_chromium_policy_directory /etc/brave/policies/managed
|
||||
copy_chromium_flags ~/.config/brave-flags.conf
|
||||
omarchy-theme-set-browser
|
||||
announce_browser_installed "Brave"
|
||||
@@ -76,7 +70,7 @@ brave-origin)
|
||||
echo "Installing Brave Origin..."
|
||||
omarchy-pkg-aur-add brave-origin-bin || exit 1
|
||||
|
||||
setup_policy_directory /etc/brave/policies/managed
|
||||
setup_chromium_policy_directory /etc/brave/policies/managed
|
||||
copy_chromium_flags ~/.config/brave-origin-flags.conf
|
||||
omarchy-theme-set-browser
|
||||
announce_browser_installed "Brave Origin"
|
||||
@@ -85,7 +79,7 @@ firefox)
|
||||
echo "Installing Firefox..."
|
||||
omarchy-pkg-add firefox || exit 1
|
||||
|
||||
setup_firefox_preferences /usr/lib/firefox/distribution
|
||||
browser_policy_setup_firefox_distribution /usr/lib/firefox/distribution
|
||||
setup_firefox_wayland
|
||||
announce_browser_installed "Firefox"
|
||||
;;
|
||||
@@ -93,7 +87,7 @@ zen)
|
||||
echo "Installing Zen..."
|
||||
omarchy-pkg-aur-add zen-browser-bin || exit 1
|
||||
|
||||
setup_firefox_preferences /opt/zen-browser/distribution
|
||||
browser_policy_setup_firefox_distribution /opt/zen-browser/distribution
|
||||
setup_firefox_wayland
|
||||
announce_browser_installed "Zen"
|
||||
;;
|
||||
|
||||
@@ -13,5 +13,9 @@ 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 'Installing ${name}...'; omarchy-pkg-add ${package} && sleep 2 && omarchy-font-set '${family}'"
|
||||
"echo ${install_message}; omarchy-pkg-add ${package_arg} && sleep 2 && omarchy-font-set ${family_arg}"
|
||||
|
||||
@@ -10,4 +10,4 @@ echo "Enabling ONCE background service..."
|
||||
sudo systemctl enable --now once-background.service
|
||||
|
||||
echo -e "\nLaunching ONCE..."
|
||||
once
|
||||
sudo once
|
||||
|
||||
@@ -9,10 +9,12 @@
|
||||
# no prompt. lazydocker needs the root-owned Docker socket, so when the group is
|
||||
# absent, gate that access behind a polkit prompt. If the user has opted into
|
||||
# sudoless Docker (omarchy-setup-security-sudoless-docker), the socket is already
|
||||
# reachable, so run lazydocker directly. pkexec sanitizes the environment, so
|
||||
# carry TERM through for the TUI to render and run lazydocker from root's PATH.
|
||||
if id -nG 2>/dev/null | grep -qw docker; then
|
||||
exec lazydocker
|
||||
else
|
||||
# reachable, so run lazydocker directly — omarchy-sudo-docker answers that for
|
||||
# this session, so the prompt stays until the reboot that grants the group.
|
||||
# pkexec sanitizes the environment, so carry TERM through for the TUI to render
|
||||
# and run lazydocker from root's PATH.
|
||||
if omarchy-sudo-docker; then
|
||||
exec pkexec /usr/bin/env TERM="${TERM:-xterm-256color}" lazydocker
|
||||
else
|
||||
exec lazydocker
|
||||
fi
|
||||
|
||||
+3
-3
@@ -85,16 +85,16 @@ wait_for_pacman_transaction
|
||||
mkdir -p "$STATE_DIR"
|
||||
[[ -d $MIGRATIONS_DIR ]] || exit 0
|
||||
|
||||
while IFS=$'\t' read -r name file marker; do
|
||||
while IFS=$'\t' read -r name file marker <&3; 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"
|
||||
OMARCHY_PATH="$OMARCHY_PATH" bash -euo pipefail "$file" 3<&-
|
||||
mkdir -p "$(dirname "$marker")"
|
||||
touch "$marker"
|
||||
fi
|
||||
done < <(migration_entries)
|
||||
done 3< <(migration_entries)
|
||||
|
||||
# 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
|
||||
|
||||
@@ -93,6 +93,13 @@ if [[ -z $url ]]; then
|
||||
[[ -n $url ]] || fail "a git URL is required"
|
||||
fi
|
||||
|
||||
# Refuse a URL that names a git option or a transport helper before cloning, so
|
||||
# an untrusted URL cannot run a command before the plugin is validated or
|
||||
# enabled. The check is shared with omarchy-theme-install and explains itself; a
|
||||
# missing checker leaves this non-zero, which refuses the URL rather than
|
||||
# cloning it.
|
||||
omarchy-git-url-check "$url" || exit 1
|
||||
|
||||
if (( ! ASSUME_YES )); then
|
||||
cat >&2 <<WARN
|
||||
|
||||
|
||||
@@ -3,5 +3,7 @@
|
||||
# omarchy:summary=Restore the default Omarchy Plymouth boot theme and SDDM login screen
|
||||
# omarchy:requires-sudo=true
|
||||
|
||||
omarchy-refresh-plymouth
|
||||
omarchy-refresh-sddm
|
||||
set -euo pipefail
|
||||
|
||||
"$OMARCHY_PATH/bin/omarchy-refresh-plymouth"
|
||||
"$OMARCHY_PATH/bin/omarchy-refresh-sddm"
|
||||
|
||||
+365
-76
@@ -5,88 +5,377 @@
|
||||
# omarchy:examples=omarchy plymouth set '#1d2021' '#ebdbb2' ~/.local/state/omarchy/current/theme/plymouth/logo.png
|
||||
# omarchy:requires-sudo=true
|
||||
|
||||
# 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.
|
||||
set -euo pipefail
|
||||
|
||||
if (( $# != 3 )); then
|
||||
# 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() {
|
||||
echo "Usage: omarchy-plymouth-set <background-hex> <text-hex> <path-to-logo.png>" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
# 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
|
||||
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
|
||||
sudo mkinitcpio -P
|
||||
usage
|
||||
fi
|
||||
|
||||
# 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"
|
||||
if (( EUID == 0 )); then
|
||||
echo "Error: run omarchy-plymouth-set as your user, not under sudo." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sed \
|
||||
-e "s/#1a1b26/#$bg_hex/g" \
|
||||
-e "s/#ffffff/#$text_hex/g" \
|
||||
"$sddm_template" | sudo tee "$sddm_dir/Main.qml" >/dev/null
|
||||
logo_fd=
|
||||
if [[ $mode != "set" ]]; then
|
||||
bg_hex=
|
||||
text_hex=
|
||||
else
|
||||
bg_hex="${1#\#}"
|
||||
text_hex="${2#\#}"
|
||||
logo_path="$3"
|
||||
|
||||
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"
|
||||
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
|
||||
fi
|
||||
|
||||
run_root_transaction() {
|
||||
sudo /bin/bash -c '
|
||||
set -eEuo pipefail
|
||||
PATH=/usr/bin:/bin
|
||||
export PATH
|
||||
umask 077
|
||||
|
||||
# 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
|
||||
|
||||
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
|
||||
|
||||
@@ -677,11 +677,15 @@ user_groups() {
|
||||
if [[ -f $PROVISIONING_DIR/groups ]]; then
|
||||
while IFS= read -r group; do
|
||||
[[ -n $group ]] || continue
|
||||
# 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.
|
||||
# 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.
|
||||
[[ $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"
|
||||
@@ -742,6 +746,12 @@ create_user() {
|
||||
# for specific commands), and a duplicate grant is harmless.
|
||||
echo "%wheel ALL=(ALL:ALL) ALL" >/etc/sudoers.d/00-omarchy-wheel
|
||||
chmod 440 /etc/sudoers.d/00-omarchy-wheel
|
||||
|
||||
source "$OMARCHY_PATH/install/helpers/browser-policy.sh"
|
||||
for dir in "${BROWSER_POLICY_MANAGED_DIRS[@]}"; do
|
||||
[[ -d $dir || -L $dir ]] || continue
|
||||
browser_policy_setup_dir "$dir"
|
||||
done
|
||||
}
|
||||
|
||||
install_authorized_keys() {
|
||||
@@ -782,7 +792,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
|
||||
cat >"/etc/systemd/system/$unit" <<UNIT
|
||||
sed "s|@UNIT@|$unit|g" >"/etc/systemd/system/$unit" <<'UNIT'
|
||||
[Unit]
|
||||
Description=Drop the first-boot autologin before the next login
|
||||
Before=display-manager.service
|
||||
@@ -791,7 +801,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
|
||||
|
||||
@@ -3,11 +3,6 @@
|
||||
# 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
|
||||
|
||||
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
|
||||
# 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
|
||||
|
||||
@@ -3,5 +3,6 @@
|
||||
# omarchy:summary=Refresh the SDDM theme from default
|
||||
# omarchy:requires-sudo=true
|
||||
|
||||
sudo rm -rf /usr/share/sddm/themes/omarchy
|
||||
sudo cp -r "$OMARCHY_PATH/default/sddm/omarchy" /usr/share/sddm/themes/omarchy
|
||||
# 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
|
||||
|
||||
@@ -24,9 +24,14 @@ echo -e "\e[32mRemoving FIDO2 device from authentication.\n\e[0m"
|
||||
|
||||
remove_pam_config
|
||||
|
||||
if [[ -d /etc/fido2 ]]; then
|
||||
authdir=/etc/fido2
|
||||
|
||||
# -d follows symlinks, so a dangling link at /etc/fido2 would survive this and
|
||||
# a later setup would install the authfile through it. rm -rf on a symlink
|
||||
# removes the link itself, never the directory it points at.
|
||||
if [[ -e $authdir || -L $authdir ]]; then
|
||||
echo "Removing FIDO2 configuration..."
|
||||
sudo rm -rf /etc/fido2
|
||||
sudo rm -rf "$authdir"
|
||||
fi
|
||||
|
||||
echo "Removing FIDO2 packages..."
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
|
||||
set -e
|
||||
|
||||
if ! id -nG "$USER" 2>/dev/null | grep -qw docker; then
|
||||
# Ask about the configured groups, not this session's: right after enabling,
|
||||
# sudoless Docker is on for the account even though the running session still
|
||||
# needs a prompt, and this command is what turns it back off.
|
||||
if omarchy-sudo-docker --configured; then
|
||||
echo "Sudoless Docker is not enabled: $USER is not in the docker group."
|
||||
exit 0
|
||||
fi
|
||||
@@ -13,13 +16,19 @@ fi
|
||||
echo "Removing $USER from the docker group..."
|
||||
sudo gpasswd -d "$USER" docker >/dev/null
|
||||
|
||||
# Group membership is fixed at login, so the running session keeps its docker
|
||||
# access until it ends. Flag a reboot so omarchy-update-restart prompts for one
|
||||
# (and the bar shows it pending); a plain log out and back in works too.
|
||||
# Group membership is only re-read by a fresh session, and in practice logging
|
||||
# out or newgrp isn't enough — only a reboot reliably applies it. Record it so a
|
||||
# later `omarchy update` still prompts (omarchy-update-restart reads this), then
|
||||
# offer to do it now.
|
||||
omarchy-state set reboot-required
|
||||
|
||||
echo ""
|
||||
echo "Sudoless Docker DISABLED. Reboot (or log out and back in) for the change to take effect."
|
||||
echo "Docker access now goes through a polkit/sudo prompt again: the Docker TUI"
|
||||
echo "(Super + Shift + D) and the Windows VM will ask when they need it, and the"
|
||||
echo "plain 'docker' CLI runs under sudo."
|
||||
echo "Sudoless Docker DISABLED. Docker access goes through a polkit/sudo prompt"
|
||||
echo "again: the Docker TUI (Super + Shift + D) and the Windows VM ask when they"
|
||||
echo "need it, and the plain 'docker' CLI runs under sudo. It takes effect after a reboot."
|
||||
echo ""
|
||||
# The migration reuses this command during 'omarchy update' and defers the
|
||||
# reboot to omarchy-update-restart, so it doesn't cut the update short.
|
||||
if [[ -z ${OMARCHY_DEFER_REBOOT:-} ]] && gum confirm "Reboot now to apply?"; then
|
||||
omarchy-system-reboot
|
||||
fi
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
# omarchy:requires-sudo=true
|
||||
|
||||
set -e
|
||||
set -o pipefail
|
||||
|
||||
|
||||
check_fido2_hardware() {
|
||||
@@ -50,13 +51,79 @@ if ! check_fido2_hardware; then
|
||||
fi
|
||||
|
||||
# Create the pamu2fcfg file
|
||||
if [[ ! -f /etc/fido2/fido2 ]]; then
|
||||
sudo mkdir -p /etc/fido2
|
||||
authdir=/etc/fido2
|
||||
authfile=/etc/fido2/fido2
|
||||
|
||||
# install -d follows a symlink here and applies the mode and ownership to
|
||||
# whatever it points at, so the credential would be staged and published inside
|
||||
# the link target and that directory reopened to root:root 755. This is the
|
||||
# threat omarchy-remove-security-fido2 already names on its side.
|
||||
if [[ -L $authdir || ( -e $authdir && ! -d $authdir ) ]]; then
|
||||
echo -e "\e[31m\n$authdir is not a FIDO2 configuration directory.\e[0m"
|
||||
echo "Run omarchy-remove-security-fido2 first, then set FIDO2 up again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# -f follows symlinks, so the already-registered check below reads a symlinked
|
||||
# authfile as a registration and leaves it in place, and is false for a
|
||||
# directory, so it tries to register over one. Only a regular file is a valid
|
||||
# pam_u2f authfile.
|
||||
if [[ -L $authfile || ( -e $authfile && ! -f $authfile ) ]]; then
|
||||
echo -e "\e[31m\n$authfile is not a FIDO2 registration file.\e[0m"
|
||||
echo "Run omarchy-remove-security-fido2 first, then set FIDO2 up again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f $authfile ]]; then
|
||||
sudo install -d -m 755 -o root -g root "$authdir"
|
||||
echo -e "\e[32m\nLet's setup your device by confirming on the device now.\e[0m"
|
||||
echo -e "Touch your FIDO2 key when it lights up...\n"
|
||||
|
||||
if pamu2fcfg >/tmp/fido2; then
|
||||
sudo mv /tmp/fido2 /etc/fido2/fido2
|
||||
# A unique sibling created by root cannot be replaced by another process
|
||||
# running as this user. Stream pamu2fcfg into it instead of asking root to
|
||||
# reopen a caller-owned path: an observed temporary name could otherwise be
|
||||
# replaced with a symlink before the privileged copy. The final rename is
|
||||
# atomic, and -T refuses a directory at the destination. Mode 644 keeps the
|
||||
# root-owned global authfile readable when pam_u2f uses openasuser; only root
|
||||
# can still rewrite it.
|
||||
stage=""
|
||||
|
||||
# mktemp's output is an operand for four privileged commands below, one of
|
||||
# them an rm. Take only the name this script asked for rather than whatever
|
||||
# came back on stdout.
|
||||
safe_stage_path() {
|
||||
local candidate=$1
|
||||
local prefix="$authfile.new."
|
||||
local suffix
|
||||
|
||||
[[ $candidate == "$prefix"* ]] || return 1
|
||||
suffix=${candidate#"$prefix"}
|
||||
[[ $suffix =~ ^[[:alnum:]]{6}$ ]]
|
||||
}
|
||||
|
||||
cleanup_stage() {
|
||||
local status=$?
|
||||
|
||||
if safe_stage_path "$stage"; then
|
||||
sudo rm -f -- "$stage" || true
|
||||
fi
|
||||
|
||||
return "$status"
|
||||
}
|
||||
|
||||
trap cleanup_stage EXIT
|
||||
stage=$(sudo mktemp "$authfile.new.XXXXXX")
|
||||
|
||||
if ! safe_stage_path "$stage" || [[ ! -f $stage || -L $stage ]]; then
|
||||
echo -e "\e[31m\nCould not create a safe staging file beside $authfile.\e[0m"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if pamu2fcfg | sudo tee "$stage" >/dev/null && [[ -s $stage ]]; then
|
||||
sudo chmod 644 "$stage"
|
||||
sudo mv -Tf "$stage" "$authfile"
|
||||
stage=""
|
||||
trap - EXIT
|
||||
echo -e "\e[32mFIDO2 device registered successfully!\e[0m"
|
||||
else
|
||||
echo -e "\e[31m\nFIDO2 registration failed. Please try again.\e[0m"
|
||||
|
||||
@@ -41,6 +41,9 @@ 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,6 +143,50 @@ 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
|
||||
@@ -161,5 +205,8 @@ 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)"
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
|
||||
set -e
|
||||
|
||||
if id -nG "$USER" 2>/dev/null | grep -qw docker; then
|
||||
# Ask about the configured groups, not this session's: once enabled it stays
|
||||
# enabled for the account even before the reboot that lets this session use it.
|
||||
if ! omarchy-sudo-docker --configured; then
|
||||
echo "Sudoless Docker is already enabled: $USER is in the docker group."
|
||||
echo "To disable it again, run: omarchy-remove-security-sudoless-docker"
|
||||
exit 0
|
||||
@@ -28,14 +30,20 @@ echo ""
|
||||
|
||||
if gum confirm "Enable sudoless Docker? This gives anything running as you passwordless root."; then
|
||||
sudo usermod -aG docker "$USER"
|
||||
# Group membership is fixed at login, so docker won't be reachable without a
|
||||
# prompt until the session restarts. Flag a reboot so omarchy-update-restart
|
||||
# prompts for one (and the bar shows it pending).
|
||||
# A new docker group membership is only picked up by a fresh session, and in
|
||||
# practice logging out or newgrp isn't enough — only a reboot reliably applies
|
||||
# it. Record it so a later `omarchy update` still prompts
|
||||
# (omarchy-update-restart reads this), then offer to do it now.
|
||||
omarchy-state set reboot-required
|
||||
echo ""
|
||||
echo "Sudoless Docker ENABLED. Reboot, or log out and back in (or run 'newgrp docker'),"
|
||||
echo "for the new group membership to take effect."
|
||||
echo "To disable it again, run: omarchy-remove-security-sudoless-docker"
|
||||
echo "Sudoless Docker ENABLED. It takes effect after a reboot."
|
||||
echo "To disable it again: Setup > Security > Sudoless Docker."
|
||||
echo ""
|
||||
# The migration reuses this command during 'omarchy update' and defers the
|
||||
# reboot to omarchy-update-restart, so it doesn't cut the update short.
|
||||
if [[ -z ${OMARCHY_DEFER_REBOOT:-} ]] && gum confirm "Reboot now to apply?"; then
|
||||
omarchy-system-reboot
|
||||
fi
|
||||
else
|
||||
echo "Aborted. No changes made. Docker access still goes through a prompt."
|
||||
fi
|
||||
|
||||
Executable
+44
@@ -0,0 +1,44 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Succeed when Docker needs sudo, fail when it can be used directly
|
||||
# omarchy:args=[--configured]
|
||||
# omarchy:examples=omarchy-sudo-docker && echo "needs sudo" | omarchy-sudo-docker --configured
|
||||
# omarchy:hidden=true
|
||||
|
||||
# The docker group is root-equivalent, so Omarchy leaves users out of it by
|
||||
# default and reaches the daemon through a prompt instead. Everything that has
|
||||
# to make that choice asks here rather than testing group membership itself.
|
||||
#
|
||||
# Two questions, because they have different answers between toggling sudoless
|
||||
# Docker and the reboot that applies it (group membership is fixed when the
|
||||
# session is created):
|
||||
#
|
||||
# (default) Does Docker need sudo *right now*? Answered by whether this
|
||||
# process can actually reach the socket, which is what decides
|
||||
# if a command must elevate. Still true in the window after
|
||||
# sudoless Docker is enabled but before the reboot.
|
||||
# --configured Will it need sudo once the account's groups take effect?
|
||||
# Answered from the account's configured groups, so the menu
|
||||
# offers the toggle that can actually change state.
|
||||
#
|
||||
# Succeeds (exit 0) when sudo is needed, so it reads as `if omarchy-sudo-docker`.
|
||||
|
||||
DOCKER_SOCKET="${OMARCHY_DOCKER_SOCKET:-/var/run/docker.sock}"
|
||||
|
||||
case "${1:-}" in
|
||||
--configured)
|
||||
# An account in the docker group will not need sudo after the next login.
|
||||
id -nG "$USER" 2>/dev/null | grep -qw docker && exit 1
|
||||
exit 0
|
||||
;;
|
||||
"")
|
||||
# A socket we can write is a daemon we can drive without elevating. A missing
|
||||
# socket counts as needing sudo: reaching it means starting it as root anyway.
|
||||
[[ -w $DOCKER_SOCKET ]] && exit 1
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Usage: omarchy-sudo-docker [--configured]" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
+20
-14
@@ -16,27 +16,33 @@ if [[ -z $REPO_URL ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# git reads a leading dash as an option, and `<helper>::<address>` as a remote
|
||||
# helper to run. The helper name is a bare word at the very start, which is what
|
||||
# this matches; an scp-style IPv6 host such as git@[2001:db8::1]:org/repo.git
|
||||
# carries `::` too and must still clone.
|
||||
if [[ $REPO_URL == -* || $REPO_URL =~ ^[A-Za-z0-9][A-Za-z0-9+.-]*:: ]]; then
|
||||
echo "Error: '$REPO_URL' names a git option or transport helper, not a repository."
|
||||
exit 1
|
||||
fi
|
||||
# Refuse a URL that names a git option or a transport helper before cloning. The
|
||||
# check is shared with omarchy-plugin-add and explains itself; a missing checker
|
||||
# leaves this non-zero, which refuses the URL rather than cloning it.
|
||||
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
|
||||
# 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.
|
||||
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 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
|
||||
# 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
|
||||
echo "Error: '$REPO_URL' does not give a usable theme name."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -3,23 +3,15 @@
|
||||
# omarchy:summary=Apply the current theme color to Chromium, Chrome, Edge, and Brave
|
||||
# omarchy:hidden=true
|
||||
|
||||
source "$OMARCHY_PATH/install/helpers/browser-policy.sh"
|
||||
|
||||
CHROMIUM_THEME=$HOME/.local/state/omarchy/current/theme/chromium.theme
|
||||
THEME_HEX_COLOR=$BROWSER_POLICY_DEFAULT_COLOR
|
||||
|
||||
if [[ -f $CHROMIUM_THEME ]]; then
|
||||
THEME_RGB_COLOR=$(<$CHROMIUM_THEME)
|
||||
THEME_HEX_COLOR=$(printf '#%02x%02x%02x' ${THEME_RGB_COLOR//,/ })
|
||||
else
|
||||
# Use a default, neutral grey if theme doesn't have a color
|
||||
THEME_HEX_COLOR="#1c2027"
|
||||
THEME_HEX_COLOR=$(browser_policy_theme_hex "$(<$CHROMIUM_THEME)")
|
||||
fi
|
||||
|
||||
set_browser_policy() {
|
||||
local policy_dir="$1"
|
||||
|
||||
[[ -d $policy_dir ]] || return
|
||||
echo "{\"BrowserThemeColor\": \"$THEME_HEX_COLOR\", \"BrowserColorScheme\": \"device\"}" | tee "$policy_dir/color.json" >/dev/null
|
||||
}
|
||||
|
||||
refresh_running_browser() {
|
||||
local process="$1"
|
||||
local command="$2"
|
||||
@@ -30,17 +22,15 @@ refresh_running_browser() {
|
||||
fi
|
||||
}
|
||||
|
||||
set_browser_policy /etc/chromium/policies/managed
|
||||
failed=0
|
||||
omarchy-theme-set-browser-policy "${THEME_HEX_COLOR#\#}" || failed=1
|
||||
|
||||
refresh_running_browser chromium chromium
|
||||
|
||||
set_browser_policy /etc/opt/chrome/policies/managed
|
||||
refresh_running_browser chrome google-chrome-stable || refresh_running_browser chrome google-chrome
|
||||
|
||||
set_browser_policy /etc/opt/edge/policies/managed
|
||||
refresh_running_browser msedge microsoft-edge-stable
|
||||
|
||||
set_browser_policy /etc/brave/policies/managed
|
||||
refresh_running_browser brave brave
|
||||
# Match on the binary path: the running process is named plain "brave", and a
|
||||
# bare -f brave-origin pattern would also match the installer's own terminal.
|
||||
refresh_running_browser /opt/brave-origin-bin/ brave-origin -f
|
||||
|
||||
exit "$failed"
|
||||
|
||||
Executable
+123
@@ -0,0 +1,123 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Write the current theme color into the browser policy directories
|
||||
# omarchy:args=<rrggbb>
|
||||
# omarchy:hidden=true
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Whenever this runs as root — invoked directly through the passwordless
|
||||
# sudoers rule, or re-execed by require_root below — sudo's secure_path decides
|
||||
# where a bare helper resolves, and a dev link (etc/sudoers.d/omarchy-dev-path)
|
||||
# prepends a user-writable checkout bin/ to it. Every helper this script calls
|
||||
# by bare name (printf's builtin aside: install, mktemp, rm) is a system tool,
|
||||
# never an omarchy-* command, so pin PATH to trusted system directories and keep
|
||||
# root from resolving one out of that checkout. The unprivileged wrapper phase
|
||||
# keeps the caller's PATH so it can still find sudo/pkexec.
|
||||
if (( EUID == 0 )); then
|
||||
export PATH=/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin:/bin:/sbin
|
||||
fi
|
||||
|
||||
# Enterprise policy trust roots. The list is fixed here rather than taken from
|
||||
# the caller: the caller chooses a color, never a path.
|
||||
POLICY_DIRS=(
|
||||
/etc/chromium/policies/managed
|
||||
/etc/opt/chrome/policies/managed
|
||||
/etc/opt/edge/policies/managed
|
||||
/etc/brave/policies/managed
|
||||
)
|
||||
|
||||
# The path etc/sudoers.d/omarchy-theme-browser names. The privileged half always
|
||||
# runs from there rather than from whichever copy was invoked, so the rule
|
||||
# matches even where $OMARCHY_PATH points at a checkout.
|
||||
PACKAGED_PATH=/usr/bin/omarchy-theme-set-browser-policy
|
||||
|
||||
usage() {
|
||||
echo "Usage: omarchy-theme-set-browser-policy <rrggbb>" >&2
|
||||
}
|
||||
|
||||
if (( $# != 1 )); then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
color="$1"
|
||||
|
||||
# Six lowercase hex digits is the whole of what this accepts. The leading "#"
|
||||
# is added when the JSON is written rather than passed in: "#" opens a comment
|
||||
# in sudoers, and keeping it out of argv lets the sudoers rule spell the
|
||||
# argument as a plain six-character glob.
|
||||
if [[ ! $color =~ ^[0-9a-f]{6}$ ]]; then
|
||||
echo "omarchy-theme-set-browser-policy: expected six lowercase hex digits, got '$color'" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# True when sudo would run this exact command without stopping for a password.
|
||||
# `sudo -l` on its own reports whether a command is permitted, which the blanket
|
||||
# %wheel rule answers yes to for everything; the long listing prints the matched
|
||||
# entry's tags, so !authenticate is the grant in
|
||||
# etc/sudoers.d/omarchy-theme-browser and nothing else. Listing runs nothing
|
||||
# and, under -n, prompts for nothing.
|
||||
sudo_grants_passwordless() {
|
||||
sudo -n -l -l "$PACKAGED_PATH" "$@" 2>/dev/null | grep -q '!authenticate'
|
||||
}
|
||||
|
||||
require_root() {
|
||||
if (( EUID == 0 )); then
|
||||
return
|
||||
elif [[ -t 0 ]] || sudo_grants_passwordless "$@"; then
|
||||
exec sudo "$PACKAGED_PATH" "$@"
|
||||
else
|
||||
exec pkexec "$PACKAGED_PATH" "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
require_root "$color"
|
||||
|
||||
failed=0
|
||||
staged=""
|
||||
# Bash 5.3 makes the EXIT trap's last command decide the script's exit status,
|
||||
# so this handler must not end on a false test. Every successful run clears
|
||||
# staged, and a trailing `[[ -n $staged ]] && ...` would report that as failure.
|
||||
cleanup() {
|
||||
if [[ -n $staged ]]; then
|
||||
rm -f "$staged"
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
for policy_dir in "${POLICY_DIRS[@]}"; do
|
||||
# Only browsers Omarchy has installed have a policy directory. Creating one
|
||||
# here would hand a browser a managed-policy root it does not otherwise have.
|
||||
[[ -d $policy_dir && ! -L $policy_dir ]] || continue
|
||||
|
||||
dest=$policy_dir/color.json
|
||||
staged=$(mktemp) || {
|
||||
failed=1
|
||||
continue
|
||||
}
|
||||
printf '{"BrowserThemeColor": "#%s", "BrowserColorScheme": "device"}\n' "$color" >"$staged"
|
||||
|
||||
if [[ -L $dest || -d $dest ]]; then
|
||||
if ! rm -rf -- "$dest"; then
|
||||
rm -f "$staged"
|
||||
staged=""
|
||||
echo "omarchy-theme-set-browser-policy: cannot replace $dest" >&2
|
||||
failed=1
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! install -m 0644 -o root -g root -T "$staged" "$dest"; then
|
||||
rm -f "$staged"
|
||||
staged=""
|
||||
echo "omarchy-theme-set-browser-policy: cannot write $dest" >&2
|
||||
failed=1
|
||||
continue
|
||||
fi
|
||||
|
||||
rm -f "$staged"
|
||||
staged=""
|
||||
done
|
||||
|
||||
exit "$failed"
|
||||
@@ -5,3 +5,9 @@
|
||||
# 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
|
||||
|
||||
@@ -7,44 +7,70 @@
|
||||
KIND="${1:-}"
|
||||
ACTION="${2:-toggle}"
|
||||
|
||||
usage() {
|
||||
echo "Usage: omarchy-toggle-input-device <touchpad|touchscreen> [on|off|toggle]" >&2
|
||||
}
|
||||
|
||||
case "$KIND" in
|
||||
touchpad) LABEL="Touchpad" ICON="touchpad" ;;
|
||||
touchscreen) LABEL="Touchscreen" ICON="touch" ;;
|
||||
*)
|
||||
echo "Usage: omarchy-toggle-input-device <touchpad|touchscreen> [on|off|toggle]" >&2
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Hyprland sources this directory on reload, so the disabled state survives restarts
|
||||
STATE_FILE="$HOME/.local/state/omarchy/toggles/hypr/$KIND-disabled.lua"
|
||||
# The persisted disable is the device name stored as plain data; on every
|
||||
# reload default/hypr/disabled-input-device.lua reads it back and disables the
|
||||
# device. Names come from USB descriptors and must not be interpolated into
|
||||
# shell or Lua. The path is hardcoded to ~/.local/state like the sibling
|
||||
# toggle tools, so it keeps working when XDG_STATE_HOME diverges.
|
||||
NAME_FILE="$HOME/.local/state/omarchy/toggles/hypr/$KIND-disabled-name"
|
||||
|
||||
device="$("omarchy-hw-$KIND")"
|
||||
|
||||
if [[ -z $device ]]; then
|
||||
echo "No $KIND device found" >&2
|
||||
exit 1
|
||||
fi
|
||||
require_device() {
|
||||
if [[ -z $device ]]; then
|
||||
echo "No $KIND device found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $device == *[[:cntrl:]]* ]]; then
|
||||
echo "Invalid $KIND device name" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
apply_device() {
|
||||
local enabled=$1
|
||||
local quoted=${device//\\/\\\\}
|
||||
quoted=${quoted//\"/\\\"}
|
||||
hyprctl eval "hl.device({ name = \"$quoted\", enabled = $enabled })" >/dev/null
|
||||
}
|
||||
|
||||
enable() {
|
||||
hyprctl eval "hl.device({ name = \"$device\", enabled = true })" >/dev/null
|
||||
rm -f "$STATE_FILE"
|
||||
# Clear the persisted state before requiring a usable device, so a device
|
||||
# that stops reporting a valid name can never wedge the disable in place.
|
||||
rm -f "$NAME_FILE"
|
||||
require_device
|
||||
apply_device true
|
||||
omarchy-osd -i "$ICON" -m "$LABEL enabled"
|
||||
}
|
||||
|
||||
disable() {
|
||||
hyprctl eval "hl.device({ name = \"$device\", enabled = false })" >/dev/null
|
||||
mkdir -p "$(dirname "$STATE_FILE")"
|
||||
printf 'hl.device({ name = "%s", enabled = false })\n' "$device" >"$STATE_FILE"
|
||||
require_device
|
||||
apply_device false
|
||||
mkdir -p "$(dirname "$NAME_FILE")"
|
||||
printf '%s\n' "$device" >"$NAME_FILE"
|
||||
omarchy-osd -i "$ICON" -m "$LABEL disabled"
|
||||
}
|
||||
|
||||
case "$ACTION" in
|
||||
on) enable ;;
|
||||
off) disable ;;
|
||||
toggle) if [[ -f $STATE_FILE ]]; then enable; else disable; fi ;;
|
||||
toggle) if [[ -f $NAME_FILE ]]; then enable; else disable; fi ;;
|
||||
*)
|
||||
echo "Usage: omarchy-toggle-input-device <touchpad|touchscreen> [on|off|toggle]" >&2
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -597,6 +597,9 @@ 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
|
||||
@@ -1187,11 +1190,19 @@ ensure_sleep_lock_service() {
|
||||
}
|
||||
|
||||
run_post_upgrade_migrations() {
|
||||
PATH="$package_path" command -v omarchy-migrate >/dev/null 2>&1 || return 0
|
||||
local pending_status
|
||||
|
||||
log "Running Omarchy migrations"
|
||||
if ! run_as_user_omarchy OMARCHY_UPGRADE_TO_QUATTRO_LIVE=1 omarchy-migrate; then
|
||||
warn "Could not run Omarchy migrations; the user may be prompted to run them after login."
|
||||
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
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -1312,9 +1323,23 @@ apply_system_transition() {
|
||||
/usr/share/icons/Yaru/scalable/actions/go-next-symbolic.svg
|
||||
as_root gtk-update-icon-cache /usr/share/icons/Yaru >/dev/null 2>&1 || true
|
||||
|
||||
as_root install -d -m 0777 /etc/chromium/policies/managed
|
||||
local browser_policy_helper=/usr/share/omarchy/install/helpers/browser-policy.sh
|
||||
if ! as_root test -f "$browser_policy_helper"; then
|
||||
warn "$browser_policy_helper is unavailable; Chromium policy directories were not hardened."
|
||||
else
|
||||
as_root env OMARCHY_PATH=/usr/share/omarchy \
|
||||
bash -euo pipefail -c '
|
||||
source "$OMARCHY_PATH/install/helpers/browser-policy.sh"
|
||||
browser_policy_setup_dir /etc/chromium/policies/managed
|
||||
for dir in "${BROWSER_POLICY_MANAGED_DIRS[@]}"; do
|
||||
[[ $dir == "/etc/chromium/policies/managed" ]] && continue
|
||||
[[ -d $dir || -L $dir ]] || continue
|
||||
browser_policy_setup_dir "$dir"
|
||||
done
|
||||
'
|
||||
fi
|
||||
as_root install -d -m 0755 /usr/lib/chromium
|
||||
printf '%s\n' '{"browser":{"theme":{"color_scheme":0,"color_scheme2":0}}}' | \
|
||||
printf '%s\n' '{"distribution":{"require_eula":false},"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
|
||||
@@ -1350,7 +1375,6 @@ 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
|
||||
@@ -1382,6 +1406,9 @@ 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
|
||||
@@ -1392,6 +1419,8 @@ 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
|
||||
@@ -2306,6 +2335,11 @@ refresh_current_theme_after_upgrade() {
|
||||
# hooks because one of them runs `hyprctl reload`. Still poke terminal
|
||||
# emulators so the active upgrade terminal picks up generated theme files.
|
||||
run_as_user_omarchy omarchy-restart-terminal >/dev/null 2>&1 || true
|
||||
|
||||
# apply_system_transition purged user-owned color.json. Headless theme-set
|
||||
# skipped omarchy-theme-set-browser, so rewrite the colour here.
|
||||
run_as_user_omarchy omarchy-theme-set-browser >/dev/null 2>&1 ||
|
||||
warn "Could not apply browser theme colour. Run 'omarchy theme set \"$theme_name\"' after reboot if Chromium's theme looks stale."
|
||||
}
|
||||
|
||||
# Everything below mutates the system, so a non-zero exit from here on leaves a
|
||||
@@ -2340,8 +2374,8 @@ run_as_user_omarchy omarchy-bar defaults ||
|
||||
cleanup_retired_services
|
||||
ensure_sleep_lock_service
|
||||
remove_retired_default_packages
|
||||
run_post_upgrade_migrations
|
||||
run_final_system_package_upgrade
|
||||
run_post_upgrade_migrations
|
||||
run_post_upgrade_update_steps
|
||||
refresh_current_theme_after_upgrade
|
||||
# Do not force-reload Hyprland in the live upgraded session. The legacy
|
||||
|
||||
@@ -14,6 +14,8 @@ 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
|
||||
|
||||
+98
-16
@@ -13,6 +13,18 @@ safe_icon_name() {
|
||||
| sed 's/[^[:alnum:]]\+/-/g; s/^-//; s/-$//'
|
||||
}
|
||||
|
||||
require_plain_name() {
|
||||
# The name becomes a filename. A slash would turn it into directory levels, so
|
||||
# the launcher lands somewhere omarchy-webapp-remove cannot address and the app
|
||||
# is stuck in the launcher; a leading ../ leaves the applications directory
|
||||
# altogether. Refuse rather than silently renaming what the user typed -- most
|
||||
# often it is a URL entered in the name field.
|
||||
if [[ $1 == */* ]]; then
|
||||
echo "App name cannot contain '/': $1"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
icon_name_from_ref() {
|
||||
local ref="$1"
|
||||
local name
|
||||
@@ -42,6 +54,34 @@ 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
|
||||
@@ -65,13 +105,44 @@ 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")
|
||||
if [[ ! $APP_URL =~ ^[a-zA-Z][a-zA-Z0-9+.-]*: ]]; then
|
||||
APP_URL="https://$APP_URL"
|
||||
fi
|
||||
APP_URL=$(normalize_webapp_url "$APP_URL")
|
||||
require_http_url "$APP_URL"
|
||||
|
||||
# Try to fetch the site's icon automatically first.
|
||||
mkdir -p "$ICON_DIR"
|
||||
@@ -88,10 +159,8 @@ if (( $# < 3 )); then
|
||||
INTERACTIVE_MODE=true
|
||||
else
|
||||
APP_NAME="$1"
|
||||
APP_URL="$2"
|
||||
if [[ ! $APP_URL =~ ^[a-zA-Z][a-zA-Z0-9+.-]*: ]]; then
|
||||
APP_URL="https://$APP_URL"
|
||||
fi
|
||||
APP_URL=$(normalize_webapp_url "$2")
|
||||
require_http_url "$APP_URL"
|
||||
ICON_REF="$3"
|
||||
CUSTOM_EXEC="$4" # Optional custom exec command
|
||||
MIME_TYPES="$5" # Optional mime types
|
||||
@@ -104,6 +173,8 @@ if [[ -z $APP_NAME || -z $APP_URL ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
require_plain_name "$APP_NAME"
|
||||
|
||||
if [[ -z $ICON_REF ]]; then
|
||||
ICON_VALUE=$(safe_icon_name "$APP_NAME")
|
||||
mkdir -p "$ICON_DIR"
|
||||
@@ -128,28 +199,39 @@ else
|
||||
ICON_VALUE=$(icon_name_from_ref "$ICON_REF")
|
||||
fi
|
||||
|
||||
# Use custom exec if provided, otherwise default behavior
|
||||
EXEC_COMMAND="${CUSTOM_EXEC:-omarchy-launch-webapp $APP_URL}"
|
||||
# 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
|
||||
|
||||
# Create application .desktop file
|
||||
DESKTOP_FILE="$HOME/.local/share/applications/$APP_NAME.desktop"
|
||||
mkdir -p "$(dirname "$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=$APP_NAME
|
||||
Comment=$APP_NAME
|
||||
Exec=$EXEC_COMMAND
|
||||
Name=$name_field
|
||||
Comment=$name_field
|
||||
Exec=$exec_field
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Icon=$ICON_VALUE
|
||||
Icon=$icon_field
|
||||
StartupNotify=true
|
||||
EOF
|
||||
|
||||
# Add mime types if provided
|
||||
if [[ -n $MIME_TYPES ]]; then
|
||||
echo "MimeType=$MIME_TYPES" >>"$DESKTOP_FILE"
|
||||
printf 'MimeType=%s\n' "$(desktop_string_escape "$MIME_TYPES")" >>"$DESKTOP_FILE"
|
||||
fi
|
||||
|
||||
chmod +x "$DESKTOP_FILE"
|
||||
|
||||
@@ -9,14 +9,31 @@ ICON_DIR="$HOME/.local/share/icons/hicolor/256x256/apps"
|
||||
OLD_ICON_DIR="$HOME/.local/share/applications/icons"
|
||||
DESKTOP_DIR="$HOME/.local/share/applications/"
|
||||
|
||||
if (( $# == 0 )); then
|
||||
# Find all web apps
|
||||
while IFS= read -r -d '' file; do
|
||||
if grep -q '^Exec=.*\(omarchy-launch-webapp\|omarchy-webapp-handler\).*' "$file"; then
|
||||
WEB_APPS+=("$(basename "${file%.desktop}")")
|
||||
fi
|
||||
done < <(find "$DESKTOP_DIR" -name '*.desktop' -print0)
|
||||
# Always index the launchers, so removal deletes the file that was found rather
|
||||
# than a path rebuilt from the displayed name. Installs predating the name
|
||||
# validation could nest the launcher inside directories, and those are exactly
|
||||
# the ones a reconstructed path cannot reach.
|
||||
WEB_APP_PATHS=()
|
||||
while IFS= read -r -d '' file; do
|
||||
if grep -q '^Exec=.*\(omarchy-launch-webapp\|omarchy-webapp-handler\).*' "$file"; then
|
||||
WEB_APPS+=("$(basename "${file%.desktop}")")
|
||||
WEB_APP_PATHS+=("$file")
|
||||
fi
|
||||
done < <(find "$DESKTOP_DIR" -name '*.desktop' -print0 2>/dev/null)
|
||||
|
||||
# The launcher matching a chosen name, or empty when nothing was indexed under
|
||||
# it (an app removed between the scan and the pick, say).
|
||||
path_for_web_app() {
|
||||
local wanted="$1" i
|
||||
for i in "${!WEB_APPS[@]}"; do
|
||||
if [[ ${WEB_APPS[$i]} == "$wanted" ]]; then
|
||||
printf '%s\n' "${WEB_APP_PATHS[$i]}"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
if (( $# == 0 )); then
|
||||
if ((${#WEB_APPS[@]})); then
|
||||
mapfile -t SORTED_WEB_APPS < <(printf '%s\n' "${WEB_APPS[@]}" | sort)
|
||||
APP_NAME=$(omarchy-menu-select "Select web app to remove" "${SORTED_WEB_APPS[@]}" -- --width 520 --maxheight 520)
|
||||
@@ -34,7 +51,8 @@ if [[ -z $APP_NAME ]]; then
|
||||
fi
|
||||
|
||||
icon_name=$(printf '%s\n' "$APP_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^[:alnum:]]\+/-/g; s/^-//; s/-$//')
|
||||
rm -f "$DESKTOP_DIR/$APP_NAME.desktop"
|
||||
desktop_file=$(path_for_web_app "$APP_NAME")
|
||||
rm -f "${desktop_file:-$DESKTOP_DIR/$APP_NAME.desktop}"
|
||||
rm -f "$ICON_DIR/$icon_name.png" "$ICON_DIR/$APP_NAME.png" "$OLD_ICON_DIR/$APP_NAME.png"
|
||||
|
||||
if [[ ${OMARCHY_REMOVE_NOTIFY:-true} != "false" ]]; then
|
||||
|
||||
+903
-127
File diff suppressed because it is too large
Load Diff
@@ -1,8 +0,0 @@
|
||||
#!/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 "$@"
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"_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" }
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"npm": {
|
||||
"china": "https://registry.npmmirror.com",
|
||||
"official": "https://registry.npmjs.org/"
|
||||
},
|
||||
"pip": {
|
||||
"china": "https://pypi.tuna.tsinghua.edu.cn/simple",
|
||||
"official": "https://pypi.org/simple"
|
||||
},
|
||||
"cargo": {
|
||||
"china": "sparse+https://rsproxy.cn/index/",
|
||||
"official": ""
|
||||
},
|
||||
"go": {
|
||||
"china": "https://goproxy.cn,direct",
|
||||
"official": "https://proxy.golang.org,direct"
|
||||
},
|
||||
"gem": {
|
||||
"china": "https://mirrors.tuna.tsinghua.edu.cn/rubygems/",
|
||||
"official": "https://rubygems.org/"
|
||||
},
|
||||
"docker": {
|
||||
"china": "https://docker.m.daocloud.io",
|
||||
"official": ""
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
[Groups/0]
|
||||
Name=Default
|
||||
Default Layout=us
|
||||
DefaultIM=rime
|
||||
|
||||
[Groups/0/Items/0]
|
||||
Name=keyboard-us
|
||||
Layout=
|
||||
|
||||
[Groups/0/Items/1]
|
||||
Name=rime
|
||||
Layout=
|
||||
|
||||
[GroupOrder]
|
||||
0=Default
|
||||
@@ -1,62 +0,0 @@
|
||||
<?xml version="1.0"?>
|
||||
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
|
||||
<fontconfig>
|
||||
<!-- 日文/韩文标签内容保持原生字形(仅约束 generic 请求,避免波及未标签路径) -->
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains"><string>ja</string></test>
|
||||
<test name="family"><string>sans-serif</string></test>
|
||||
<edit name="family" mode="prepend" binding="strong"><string>Noto Sans CJK JP</string></edit>
|
||||
</match>
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains"><string>ko</string></test>
|
||||
<test name="family"><string>sans-serif</string></test>
|
||||
<edit name="family" mode="prepend" binding="strong"><string>Noto Sans CJK KR</string></edit>
|
||||
</match>
|
||||
|
||||
<!-- 简体中文内容强制简体字形 -->
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains"><string>zh-cn</string></test>
|
||||
<test name="family"><string>sans-serif</string></test>
|
||||
<edit name="family" mode="prepend" binding="strong"><string>Noto Sans CJK SC</string></edit>
|
||||
</match>
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains"><string>zh-cn</string></test>
|
||||
<test name="family"><string>serif</string></test>
|
||||
<edit name="family" mode="prepend" binding="strong"><string>Noto Serif CJK SC</string></edit>
|
||||
</match>
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains"><string>zh-cn</string></test>
|
||||
<test name="family"><string>monospace</string></test>
|
||||
<edit name="family" mode="prepend" binding="strong"><string>Noto Sans Mono CJK SC</string></edit>
|
||||
</match>
|
||||
|
||||
<!-- 无语言标签的任意字体请求:汉字回退到 SC 而非 JP(Chromium/Electron 常见路径) -->
|
||||
<match target="pattern">
|
||||
<edit name="family" mode="append" binding="weak"><string>Noto Sans CJK SC</string></edit>
|
||||
</match>
|
||||
|
||||
<!-- generic 家族展开时 SC 优先于其他 CJK 变体 -->
|
||||
<alias>
|
||||
<family>sans-serif</family>
|
||||
<prefer>
|
||||
<family>Noto Sans</family>
|
||||
<family>Noto Sans CJK SC</family>
|
||||
<family>Noto Color Emoji</family>
|
||||
</prefer>
|
||||
</alias>
|
||||
<alias>
|
||||
<family>serif</family>
|
||||
<prefer>
|
||||
<family>Noto Serif</family>
|
||||
<family>Noto Serif CJK SC</family>
|
||||
<family>Noto Color Emoji</family>
|
||||
</prefer>
|
||||
</alias>
|
||||
<alias>
|
||||
<family>monospace</family>
|
||||
<prefer>
|
||||
<family>Noto Sans Mono CJK SC</family>
|
||||
<family>Noto Color Emoji</family>
|
||||
</prefer>
|
||||
</alias>
|
||||
</fontconfig>
|
||||
@@ -1,32 +0,0 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
xsBNBGqMzPYBCADOcNGiPxC8AgP1kG1s6obLZi5NzfkVzlqkWH9/7JgSokRr//IO
|
||||
L8L2eMQ5bvEou4Mc+eujPI+5Tm8TwfECkjhKWbEOk09yn9kUXVlc/+iROJXF9z4u
|
||||
Cp6BIiZ5YzoadZysCqSLlYXejp38LGXdi4xyh5SfXII/VD/036MBwvwZkb6qoFsp
|
||||
1AS4BRLcE0BgD2QjL5MIFU+yVblUZl8ss1r9AVNNgAllGzFpYHFt+PknnPDldj/j
|
||||
dv5vihekC1Wy+w46w15vdBM04i6t1h72N3naFfuPkMJFuXo0NLWMNLOeDp+bsej8
|
||||
Kwf31yn6Vz0XLw8wCmLBwUtBaahvE/TqYUJPABEBAAHNDyhBcmNoIFJlZ2lzdHJ5
|
||||
KcLAuwQTAQgAbwWCaozM9gILBwkQvThgSIZ7M7Q1FAAAAAAAHAAQc2FsdEBub3Rh
|
||||
dGlvbnMub3BlbnBncGpzLm9yZwbZDf7U+gBf+H+EPPm1SmYCFQgCFgACGQECmwMC
|
||||
HgEWIQR03PV6zYErJNlZ8Ua9OGBIhnsztAAATuEH/2LQxWP9JXXjloS2HwsD/jBb
|
||||
OX/LPP4inkbUmkxaPx9hzecKPIZttzRoIXK8FxviPxwifQYPRShD5t1UwI1vwTnY
|
||||
BCWs4VUf+vMlcaKrmMhgL4z3Wfi1Cpn7GcEYaCB+9BPbNlVTy4KzQJKXk8RkrOPz
|
||||
+r2YynBH2PynjoqVqVvgAnU1vc5gdRUdZIBQIt8RqpwF/Sc0+WwWf9bdW1VzB0Pc
|
||||
QTvuCaNzQOtSTYgDko9XJSa5VNQJrYuwYg97DmTfx+PYm2dFBTeR8Ut9wWiynItZ
|
||||
4ywSm6qE9w1OuBknUx80jev2LGlNNCD0/xBTWPokLf0ZsgLG9f302raZ+WY78GzO
|
||||
wE0EaozM9gEIAN8JV920AR4+TY1v1FVenZbSq74nCe2UNtEHWf97cwZye8ivUbg2
|
||||
mbDEJp+09asoLWRGwzDvD6rUicqihvTgAOB5cZQU1D9wxXfowfG5aB/czVA0MzPU
|
||||
bj5BXS8lA0pN4zcTamCHnrfoQpCGk+eB48xwBPLfxxVtwttaBtvG2YAPdiwultJu
|
||||
13IX4lsK/LS425NYwzuGw5m7Amy/rzamAUZiymIiG45RQ8y3/1UYRMW4yXOVMrtV
|
||||
fmtaL/hzsG0sGVEyiuseR/ZPqZKZrveYeqzy3fCOyCGsPwWH9rtfDekGBGbJLoiY
|
||||
wwA3ikMifFYPr1A1PMOBR5jAXYV/VG/pQQkAEQEAAcLArAQYAQgAYAWCaozM9gkQ
|
||||
vThgSIZ7M7Q1FAAAAAAAHAAQc2FsdEBub3RhdGlvbnMub3BlbnBncGpzLm9yZyFs
|
||||
5xT4PpfIzf6Y7ke/Br8CmwwWIQR03PV6zYErJNlZ8Ua9OGBIhnsztAAATqIH/26g
|
||||
dx2lJNHIYXlS/Ld8JXCC42IctWPeESegQPmJp96GbvF4Z7dVB0e+kqy7YgNYY8dv
|
||||
ibc7GicmQrAtsNDmm+fB1nmd316HxrtiVsBKgeNWTh7EN8cH79lNB4IqwWf6zkny
|
||||
1A1Ntbtqv0tERg/fXEuY6y+9f67JBM5P20iWRij3NNODPHpy5Vr7Ftm7pQ7FgDSj
|
||||
tntrOKMvAwKsWA+avGt55eQe9kbbFhbOqQEXfiqIgIQki0yg8o5VrIz5ZEQm7AXU
|
||||
fYr6IxJPrD7KhBeXO2Z0/TdKjZ5c1kpWEC2W0x8TUH9s2v5q4UeTlyHg5fyFzte6
|
||||
oAEZesYWyQ10m7ewZTg=
|
||||
=9OK8
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@@ -1,16 +0,0 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mDMEaozDJhYJKwYBBAHaRw8BAQdA3l2d/6gj2z96cit0NGYg419BmDDFH6SYhkzL
|
||||
3OMVCem0PU9tYXJjaHlDTiBSZWxlYXNlIFNpZ25pbmcgS2V5IDx6aGFuZ3lhbmdo
|
||||
YWhhMDQwN0BvdXRsb29rLmNvbT6ImQQTFgoAQRYhBARJDwZfat0mKnJDUG7fe4YD
|
||||
tdJHBQJqjMMmAhsBBQkDwmcABQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAAAoJ
|
||||
EG7fe4YDtdJHuSMBAI3/samJVmhfdr/HYS8y266Xq82tof9lL9izImd/YibkAQCQ
|
||||
Fjw97J4LPnPQEm93j/3kmHC+EGTQu3EDbXEoI/AUC7gzBGqMwyYWCSsGAQQB2kcP
|
||||
AQEHQCRBOFrQ9UQVtv3XawZIMm93j3IV45K8kk5AGLg+JnMUiPUEGBYKACYWIQQE
|
||||
SQ8GX2rdJipyQ1Bu33uGA7XSRwUCaozDJgIbAgUJA8JnAACBCRBu33uGA7XSR3Yg
|
||||
BBkWCgAdFiEEUarKC/SJIll7SA5+IRubguF8+2cFAmqMwyYACgkQIRubguF8+2cP
|
||||
FQD6A6Xgt7L5bXR/kSJXvDHEgjsQG5xfAEEaZ8/VZ0clWxwA/ikOgsx8VQkZhwrJ
|
||||
Hb/3iG5mb138yHf3Ug00N1GwL6gOHn0A/3OSdXKvdmQhMEUf8TpZNf/ouRal2NsI
|
||||
BB1FBGxZWCYGAQDJRR2W0FEYbEuzUj9XGCHb8O9TjabNN0QduQj78ZNNAQ==
|
||||
=Awb4
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
-159
@@ -1,159 +0,0 @@
|
||||
# shellcheck shell=bash
|
||||
# Sourced helpers for omarchy-cn-ai-* commands
|
||||
|
||||
CN_AI_PROVIDERS="$OMARCHY_PATH/cn/registry/ai-providers.json"
|
||||
CN_AI_HARNESSES="$OMARCHY_PATH/cn/registry/ai-harnesses.json"
|
||||
CN_AI_COMPAT="$OMARCHY_PATH/cn/registry/ai-compatibility.json"
|
||||
CN_AI_PROFILE_DIR="$HOME/.config/omarchycn/ai/profiles"
|
||||
CN_AI_CURRENT="$HOME/.config/omarchycn/ai/current"
|
||||
|
||||
cn_ai_provider_ids() { jq -r '.providers | keys[]' "$CN_AI_PROVIDERS"; }
|
||||
cn_ai_harness_ids() { jq -r '.harnesses | keys[]' "$CN_AI_HARNESSES"; }
|
||||
|
||||
cn_ai_endpoint() {
|
||||
jq -re --arg p "$1" --arg proto "$2" '.providers[$p].endpoints[$proto]' "$CN_AI_PROVIDERS"
|
||||
}
|
||||
|
||||
cn_ai_models() {
|
||||
jq -r --arg p "$1" '.providers[$p].models[].id' "$CN_AI_PROVIDERS"
|
||||
}
|
||||
|
||||
cn_ai_model_by_alias() {
|
||||
jq -re --arg p "$1" --arg a "$2" \
|
||||
'.providers[$p].models[] | select(.aliases // [] | index($a)) | .id' "$CN_AI_PROVIDERS"
|
||||
}
|
||||
|
||||
cn_ai_fast_model() {
|
||||
cn_ai_model_by_alias "$1" fast 2>/dev/null || cn_ai_model_by_alias "$1" default-coding
|
||||
}
|
||||
|
||||
cn_ai_harness_field() {
|
||||
jq -re --arg h "$1" --arg f "$2" '.harnesses[$h][$f]' "$CN_AI_HARNESSES"
|
||||
}
|
||||
|
||||
cn_ai_combo_level() {
|
||||
jq -r --arg h "$1" --arg p "$2" '.combos[$h][$p].level // "unsupported"' "$CN_AI_COMPAT"
|
||||
}
|
||||
|
||||
# Combo-specific model allowlist; empty output means no restriction
|
||||
cn_ai_combo_models() {
|
||||
jq -r --arg h "$1" --arg p "$2" '.combos[$h][$p].models // [] | .[]' "$CN_AI_COMPAT"
|
||||
}
|
||||
|
||||
cn_ai_model_protocol() {
|
||||
jq -r --arg p "$1" --arg m "$2" \
|
||||
'.providers[$p].models[] | select(.id == $m) | .protocol // ""' "$CN_AI_PROVIDERS"
|
||||
}
|
||||
|
||||
# Profile files are flat key="value" lines
|
||||
cn_ai_profile_field() {
|
||||
local file="$CN_AI_PROFILE_DIR/$1.toml" key="$2"
|
||||
grep -E "^$key = " "$file" | head -1 | cut -d\" -f2
|
||||
}
|
||||
|
||||
cn_ai_current_profile() {
|
||||
if [[ ! -f $CN_AI_CURRENT ]]; then
|
||||
echo "No default AI profile set (run: omarchycn ai profile use <name>)" >&2
|
||||
return 1
|
||||
fi
|
||||
cat "$CN_AI_CURRENT"
|
||||
}
|
||||
|
||||
# Print export statements for the profile's harness x provider combo.
|
||||
# Secrets are resolved at launch time only; nothing is written to disk.
|
||||
cn_ai_render_env() {
|
||||
local harness="$1" provider="$2" model="$3" key="$4"
|
||||
local base fast
|
||||
|
||||
case "$harness" in
|
||||
claude-code)
|
||||
base=$(cn_ai_endpoint "$provider" anthropic)
|
||||
fast=$(cn_ai_fast_model "$provider")
|
||||
printf 'export ANTHROPIC_BASE_URL=%q\n' "$base"
|
||||
# Docs vary between AUTH_TOKEN (deepseek/zai) and API_KEY (kimi): set both
|
||||
printf 'export ANTHROPIC_AUTH_TOKEN=%q\n' "$key"
|
||||
printf 'export ANTHROPIC_API_KEY=%q\n' "$key"
|
||||
printf 'export ANTHROPIC_MODEL=%q\n' "$model"
|
||||
printf 'export ANTHROPIC_DEFAULT_SONNET_MODEL=%q\n' "$model"
|
||||
printf 'export ANTHROPIC_DEFAULT_OPUS_MODEL=%q\n' "$model"
|
||||
printf 'export ANTHROPIC_DEFAULT_HAIKU_MODEL=%q\n' "$fast"
|
||||
;;
|
||||
opencode)
|
||||
if [[ $provider != "deepseek" ]]; then
|
||||
echo "opencode adapter renders only the deepseek combo (native provider)" >&2
|
||||
return 1
|
||||
fi
|
||||
printf 'export DEEPSEEK_API_KEY=%q\n' "$key"
|
||||
;;
|
||||
*)
|
||||
echo "No env renderer for harness: $harness" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Write ~/.codex/config.toml + models.json per DeepSeek's official codex
|
||||
# integration contract (mirrors cdn.deepseek.com codex-deepseek-setup script)
|
||||
cn_ai_render_codex_config() {
|
||||
local provider="$1" model="$2" key="$3"
|
||||
local base cfg="$HOME/.codex/config.toml"
|
||||
|
||||
if [[ $provider != "deepseek" ]]; then
|
||||
echo "codex adapter renders only the deepseek combo (official integration)" >&2
|
||||
return 1
|
||||
fi
|
||||
base="https://api.deepseek.com/"
|
||||
|
||||
mkdir -p "${cfg%/*}"
|
||||
touch "$cfg"
|
||||
|
||||
# Strip our old blocks first, then check for an unmanaged provider table
|
||||
rm -f "$cfg.omarchycn-tmp" "$cfg.omarchycn-new"
|
||||
(
|
||||
umask 077
|
||||
awk '
|
||||
/^# OmarchyCN ai (model|provider) begin$/ { skip = 1; next }
|
||||
/^# OmarchyCN ai (model|provider) end$/ { skip = 0; next }
|
||||
skip { next }
|
||||
/^[[:space:]]*\[/ { in_section = 1 }
|
||||
!in_section && /^[[:space:]]*(model|model_provider|model_catalog_json|preferred_auth_method|forced_login_method|model_reasoning_effort)[[:space:]]*=/ { next }
|
||||
{ print }
|
||||
' "$cfg" > "$cfg.omarchycn-tmp"
|
||||
)
|
||||
|
||||
if grep -qE "^[[:space:]]*\[[[:space:]]*model_providers[[:space:]]*\.[[:space:]]*\"?$provider\"?[[:space:]]*\]" "$cfg.omarchycn-tmp"; then
|
||||
rm -f "$cfg.omarchycn-tmp"
|
||||
echo "codex: $cfg 已有非托管的 [model_providers.$provider],请手动清理后重试" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ -s $cfg ]]; then
|
||||
cp "$cfg" "$cfg.omarchycn-bak-$(date +%Y%m%d-%H%M%S)"
|
||||
fi
|
||||
|
||||
cp "$OMARCHY_PATH/cn/registry/codex-deepseek-models.json" "$HOME/.codex/models.json"
|
||||
|
||||
(
|
||||
umask 077
|
||||
{
|
||||
echo "# OmarchyCN ai model begin"
|
||||
echo "model = \"$model\""
|
||||
echo "model_provider = \"$provider\""
|
||||
echo "model_catalog_json = \"~/.codex/models.json\""
|
||||
echo "preferred_auth_method = \"apikey\""
|
||||
echo "forced_login_method = \"api\""
|
||||
echo "model_reasoning_effort = \"high\""
|
||||
echo "# OmarchyCN ai model end"
|
||||
cat "$cfg.omarchycn-tmp"
|
||||
echo "# OmarchyCN ai provider begin"
|
||||
echo "[model_providers.$provider]"
|
||||
echo "name = \"$provider\""
|
||||
echo "base_url = \"$base\""
|
||||
echo "wire_api = \"responses\""
|
||||
echo "experimental_bearer_token = \"$key\""
|
||||
echo "# OmarchyCN ai provider end"
|
||||
} > "$cfg.omarchycn-new"
|
||||
)
|
||||
mv "$cfg.omarchycn-new" "$cfg"
|
||||
rm -f "$cfg.omarchycn-tmp"
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
# shellcheck shell=bash
|
||||
# Sourced helpers for omarchy-cn-dev-mirror-* commands
|
||||
|
||||
CN_DEV_MIRRORS_JSON="$OMARCHY_PATH/cn/dev-mirrors.json"
|
||||
# shellcheck disable=SC2034 # consumed by sourcing commands
|
||||
CN_DM_TARGETS=(npm pip cargo go gem docker)
|
||||
CN_DM_BACKUP_ROOT="$HOME/.local/state/omarchycn/backups/dev-mirror"
|
||||
|
||||
cn_dm_url() {
|
||||
jq -re --arg t "$1" --arg p "$2" '.[$t][$p]' "$CN_DEV_MIRRORS_JSON"
|
||||
}
|
||||
|
||||
cn_dm_backup() {
|
||||
local file="$1"
|
||||
local stamp="$2"
|
||||
|
||||
if [[ -f $file ]]; then
|
||||
mkdir -p "$CN_DM_BACKUP_ROOT/$stamp"
|
||||
cp "$file" "$CN_DM_BACKUP_ROOT/$stamp/${file##*/}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Replace-or-append one key=value style line matched by a regex
|
||||
cn_dm_set_line() {
|
||||
local file="$1" match="$2" line="$3"
|
||||
|
||||
mkdir -p "${file%/*}"
|
||||
touch "$file"
|
||||
grep -vE "$match" "$file" > "$file.omarchycn-tmp" || true
|
||||
printf '%s\n' "$line" >> "$file.omarchycn-tmp"
|
||||
mv "$file.omarchycn-tmp" "$file"
|
||||
}
|
||||
|
||||
cn_dm_get_npm() { grep -sE '^registry=' "$HOME/.npmrc" | cut -d= -f2- || true; }
|
||||
cn_dm_set_npm() { cn_dm_set_line "$HOME/.npmrc" '^registry=' "registry=$1"; }
|
||||
|
||||
cn_dm_get_pip() {
|
||||
python3 - <<'EOF'
|
||||
import configparser, os
|
||||
c = configparser.ConfigParser()
|
||||
c.read(os.path.expanduser("~/.config/pip/pip.conf"))
|
||||
print(c.get("global", "index-url", fallback=""))
|
||||
EOF
|
||||
}
|
||||
|
||||
cn_dm_set_pip() {
|
||||
CN_DM_PIP_URL="$1" python3 - <<'EOF'
|
||||
import configparser, os
|
||||
path = os.path.expanduser("~/.config/pip/pip.conf")
|
||||
c = configparser.ConfigParser()
|
||||
c.read(path)
|
||||
if not c.has_section("global"):
|
||||
c.add_section("global")
|
||||
c.set("global", "index-url", os.environ["CN_DM_PIP_URL"])
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
with open(path, "w") as f:
|
||||
c.write(f)
|
||||
EOF
|
||||
}
|
||||
|
||||
cn_dm_get_cargo() {
|
||||
local f="$HOME/.cargo/config.toml"
|
||||
grep -sA1 '^\[source\.omarchycn\]' "$f" | grep -sE '^registry' | cut -d\" -f2 || true
|
||||
}
|
||||
|
||||
cn_dm_set_cargo() {
|
||||
local url="$1" f="$HOME/.cargo/config.toml"
|
||||
|
||||
mkdir -p "${f%/*}"
|
||||
touch "$f"
|
||||
sed -i '/^# OmarchyCN dev-mirror begin$/,/^# OmarchyCN dev-mirror end$/d' "$f"
|
||||
if [[ -z $url ]]; then
|
||||
return 0
|
||||
fi
|
||||
if grep -q '^\[source\.crates-io\]' "$f"; then
|
||||
echo "cargo: $f already defines [source.crates-io]; edit it manually" >&2
|
||||
return 1
|
||||
fi
|
||||
cat >> "$f" <<EOF
|
||||
# OmarchyCN dev-mirror begin
|
||||
[source.crates-io]
|
||||
replace-with = "omarchycn"
|
||||
|
||||
[source.omarchycn]
|
||||
registry = "$url"
|
||||
# OmarchyCN dev-mirror end
|
||||
EOF
|
||||
}
|
||||
|
||||
cn_dm_get_go() { grep -sE '^GOPROXY=' "$HOME/.config/go/env" | cut -d= -f2- || true; }
|
||||
cn_dm_set_go() { cn_dm_set_line "$HOME/.config/go/env" '^GOPROXY=' "GOPROXY=$1"; }
|
||||
|
||||
cn_dm_get_gem() { grep -sE '^- ' "$HOME/.gemrc" | head -1 | sed 's/^- //' || true; }
|
||||
|
||||
cn_dm_set_gem() {
|
||||
local f="$HOME/.gemrc"
|
||||
|
||||
if [[ -s $f ]] && ! grep -q '^# OmarchyCN dev-mirror managed$' "$f"; then
|
||||
echo "gem: $f has existing user content; set :sources: manually" >&2
|
||||
return 1
|
||||
fi
|
||||
cat > "$f" <<EOF
|
||||
# OmarchyCN dev-mirror managed
|
||||
---
|
||||
:sources:
|
||||
- $1
|
||||
EOF
|
||||
}
|
||||
|
||||
cn_dm_get_docker() {
|
||||
jq -re '."registry-mirrors"[0] // ""' /etc/docker/daemon.json 2>/dev/null || true
|
||||
}
|
||||
|
||||
cn_dm_set_docker() {
|
||||
local url="$1" current="{}"
|
||||
|
||||
sudo mkdir -p /etc/docker
|
||||
if sudo test -f /etc/docker/daemon.json; then
|
||||
current=$(sudo cat /etc/docker/daemon.json)
|
||||
fi
|
||||
if [[ -n $url ]]; then
|
||||
jq --arg u "$url" '."registry-mirrors" = [$u]' <<<"$current" | sudo tee /etc/docker/daemon.json > /dev/null
|
||||
else
|
||||
jq 'del(."registry-mirrors")' <<<"$current" | sudo tee /etc/docker/daemon.json > /dev/null
|
||||
fi
|
||||
echo "docker: restart the docker daemon to take effect"
|
||||
}
|
||||
|
||||
cn_dm_config_file() {
|
||||
case "$1" in
|
||||
npm) echo "$HOME/.npmrc" ;;
|
||||
pip) echo "$HOME/.config/pip/pip.conf" ;;
|
||||
cargo) echo "$HOME/.cargo/config.toml" ;;
|
||||
go) echo "$HOME/.config/go/env" ;;
|
||||
gem) echo "$HOME/.gemrc" ;;
|
||||
docker) echo "/etc/docker/daemon.json" ;;
|
||||
esac
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
# shellcheck shell=bash
|
||||
# Sourced helpers for omarchy-cn-mirror-* commands
|
||||
|
||||
CN_MIRRORS_JSON="$OMARCHY_PATH/cn/mirrors.json"
|
||||
CN_MIRRORLIST="/etc/pacman.d/mirrorlist"
|
||||
CN_PROFILE_FILE="$HOME/.config/omarchycn/mirror-profile"
|
||||
|
||||
cn_mirror_ids() {
|
||||
jq -r '.mirrors[].id' "$CN_MIRRORS_JSON"
|
||||
}
|
||||
|
||||
cn_mirror_url() {
|
||||
jq -r --arg id "$1" '.mirrors[] | select(.id == $id) | .url' "$CN_MIRRORS_JSON"
|
||||
}
|
||||
|
||||
cn_mirror_name() {
|
||||
jq -r --arg id "$1" '.mirrors[] | select(.id == $id) | .name' "$CN_MIRRORS_JSON"
|
||||
}
|
||||
|
||||
cn_mirror_ids_by_region() {
|
||||
jq -r --arg r "$1" '.mirrors[] | select(.region == $r) | .id' "$CN_MIRRORS_JSON"
|
||||
}
|
||||
|
||||
# Probe one mirror: prints "<speed_bytes_s> <ttfb_s> <sync_age_s|stale|unknown>"
|
||||
cn_mirror_probe() {
|
||||
local url="$1"
|
||||
local out speed=0 ttfb=0 lastsync age="unknown" now
|
||||
|
||||
if out=$(curl -sSf -o /dev/null -m 12 --connect-timeout 5 \
|
||||
-w '%{speed_download} %{time_starttransfer}' \
|
||||
"$url/core/os/x86_64/core.db" 2>/dev/null); then
|
||||
read -r speed ttfb <<<"$out"
|
||||
speed="${speed%%.*}"
|
||||
fi
|
||||
|
||||
if lastsync=$(curl -sSf -m 5 --connect-timeout 5 "$url/lastsync" 2>/dev/null); then
|
||||
lastsync=$(tr -cd '0-9' <<<"$lastsync")
|
||||
if [[ -n $lastsync ]]; then
|
||||
now=$(date +%s)
|
||||
age=$((now - lastsync))
|
||||
if (( age > 86400 )); then
|
||||
age="stale"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "$speed $ttfb $age"
|
||||
}
|
||||
|
||||
# Rank ids by probe speed, excluding dead (speed 0) and stale mirrors
|
||||
cn_mirror_rank() {
|
||||
local id url speed ttfb age
|
||||
|
||||
for id in "$@"; do
|
||||
url=$(cn_mirror_url "$id")
|
||||
read -r speed ttfb age < <(cn_mirror_probe "$url")
|
||||
(( speed > 0 )) || continue
|
||||
[[ $age == "stale" ]] && continue
|
||||
echo "$speed $id"
|
||||
done | sort -rn | awk '{print $2}'
|
||||
}
|
||||
|
||||
# Write mirrorlist from mirror ids (first = primary), with timestamped backup
|
||||
cn_mirror_write_list() {
|
||||
local profile="$1"
|
||||
shift
|
||||
local stamp id url content=""
|
||||
|
||||
stamp=$(date +%Y%m%d-%H%M%S)
|
||||
content="## OmarchyCN mirrorlist (profile: $profile, generated $stamp)\n"
|
||||
for id in "$@"; do
|
||||
url=$(cn_mirror_url "$id")
|
||||
content+="## $id: $(cn_mirror_name "$id")\nServer = $url/\$repo/os/\$arch\n"
|
||||
done
|
||||
|
||||
sudo cp "$CN_MIRRORLIST" "$CN_MIRRORLIST.omarchycn-bak-$stamp"
|
||||
printf '%b' "$content" | sudo tee "$CN_MIRRORLIST" > /dev/null
|
||||
|
||||
mkdir -p "${CN_PROFILE_FILE%/*}"
|
||||
echo "$profile" > "$CN_PROFILE_FILE"
|
||||
echo "Mirrorlist written ($CN_MIRRORLIST), backup: $CN_MIRRORLIST.omarchycn-bak-$stamp"
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"mirrors": [
|
||||
{ "id": "tuna", "name": "清华大学 TUNA", "region": "cn", "url": "https://mirrors.tuna.tsinghua.edu.cn/archlinux" },
|
||||
{ "id": "ustc", "name": "中国科学技术大学", "region": "cn", "url": "https://mirrors.ustc.edu.cn/archlinux" },
|
||||
{ "id": "aliyun", "name": "阿里云", "region": "cn", "url": "https://mirrors.aliyun.com/archlinux" },
|
||||
{ "id": "tencent", "name": "腾讯云", "region": "cn", "url": "https://mirrors.cloud.tencent.com/archlinux" },
|
||||
{ "id": "netease", "name": "网易", "region": "cn", "url": "https://mirrors.163.com/archlinux" },
|
||||
{ "id": "sjtu", "name": "上海交通大学", "region": "cn", "url": "https://mirror.sjtu.edu.cn/archlinux" },
|
||||
{ "id": "geo", "name": "Arch 官方 Geo Mirror", "region": "global", "url": "https://geo.mirror.pkgbuild.com" },
|
||||
{ "id": "worldwide", "name": "Arch 官方 Worldwide", "region": "global", "url": "https://mirror.rackspace.com/archlinux" }
|
||||
]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user