dsh-tui-theme 0.6.1 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -13
- package/cordis.patch.yml +2 -0
- package/docs/decisions/2026-09-12-pink-day-claude-family-lightening.md +49 -0
- package/lib/types/autoTheme.d.ts +3 -0
- package/lib/types/autoTheme.d.ts.map +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +19 -2
- package/lib/types/ornament.d.ts +38 -0
- package/lib/types/ornament.d.ts.map +1 -0
- package/lib/types/ornament.js +88 -0
- package/lib/types/settingsSection.d.ts +9 -4
- package/lib/types/settingsSection.d.ts.map +1 -1
- package/lib/types/settingsSection.js +88 -7
- package/lib/types/shadowCleanup.d.ts +39 -0
- package/lib/types/shadowCleanup.d.ts.map +1 -0
- package/lib/types/shadowCleanup.js +122 -0
- package/lib/types/statusLine.d.ts +27 -11
- package/lib/types/statusLine.d.ts.map +1 -1
- package/lib/types/statusLine.js +181 -43
- package/lib/types/statusView.d.ts +82 -0
- package/lib/types/statusView.d.ts.map +1 -0
- package/lib/types/statusView.js +126 -0
- package/lib/types/themeAssets.d.ts +4 -0
- package/lib/types/themeAssets.d.ts.map +1 -1
- package/lib/types/themeAssets.js +8 -3
- package/package.json +42 -42
- package/scripts/expected-settings-contract.mjs +21 -1
- package/scripts/headless-order-test.mjs +33 -10
- package/scripts/runtime-themes-headless.mjs +160 -23
- package/scripts/validate-themes-against-host.mjs +33 -6
- package/scripts/verify-package.mjs +16 -7
- package/scripts/verify.mjs +377 -9
- package/themes/pink-day.json +4 -4
- package/themes/pink-night.json +4 -4
package/README.md
CHANGED
|
@@ -6,17 +6,18 @@
|
|
|
6
6
|
| --- | --- | --- |
|
|
7
7
|
| **三套粉色主题** | 主题运行时(dsh-TUI ≥ 0.10.0)/ 静态资产(旧宿主) | `pink-night` 夜樱 / `pink-day` 昼樱 / `pink-ansi` 樱·ANSI;新宿主优先即时注册,服务晚到时短暂回退并清理本次文件,旧宿主使用 `~/.dsh-tui/themes/` |
|
|
8
8
|
| **缓存背景跟随** | 设置 + 本地缓存 | 可选地应用已有 `theme-follow.json` 的昼樱/夜樱结果;不直接读取终端输入或发送 OSC 查询 |
|
|
9
|
-
| **花符状态行** | `tuiStatus` | 输入框上方一行小装饰:✿ · 时钟 ·
|
|
10
|
-
| **设置面板** | `tuiSettingsSections` | `/settings`
|
|
9
|
+
| **花符状态行** | `tuiStatus` | 输入框上方一行小装饰:✿ · 时钟 · 实时轮数(默认仅粉主题下显示);dsh-TUI ≥ 0.10.1 经富状态视图按主题配色渲染,旧宿主为无色标量行 |
|
|
10
|
+
| **设置面板** | `tuiSettingsSections` | `/settings` 里一个可编辑区块(背景跟随 / 状态行两组子页),改完即时生效 |
|
|
11
11
|
| **屏幕提示** | `tuiToast`(dsh-TUI ≥ 0.10.0) | 背景跟随结果、主题文件自愈、旧文件遮蔽提醒各弹一行短提示;旧宿主自动静默降级 |
|
|
12
|
+
| **旧文件清理** | `tuiDialogs`(dsh-TUI ≥ 0.9.3) | 检测到遮蔽旧主题文件时,额外提供一次性宿主确认对话框,确认后清理逐字节相同的副本;拒绝/忽略则不做任何改动 |
|
|
12
13
|
|
|
13
|
-
**明确不做的事**:不注册快捷键、不注册/修改任何命令、不拦截输入、不追加会话事件、不注入 system prompt
|
|
14
|
+
**明确不做的事**:不注册快捷键、不注册/修改任何命令、不拦截输入、不追加会话事件、不注入 system prompt(`tuiDialogs` 是宿主托管的中性确认面板:宿主自渲染、宿主拥有键盘,插件只提交请求,不属于输入拦截)。卸载即无痕(可选删除主题文件)。
|
|
14
15
|
|
|
15
16
|
## 主题预览
|
|
16
17
|
|
|
17
18
|
| 主题 | 基底 | 风格 |
|
|
18
19
|
| --- | --- | --- |
|
|
19
|
-
| `pink-night` 夜樱 | dark |
|
|
20
|
+
| `pink-night` 夜樱 | dark | 暗梅底、玫瑰粉强调,宿主 Theme 全键覆盖(0.10.x 为 73 语义键,旧键名经宿主别名映射兼容) |
|
|
20
21
|
| `pink-day` 昼樱 | light | 象牙粉底、墨梅正文、柔和玫瑰强调(已通过宿主浅色身份判定) |
|
|
21
22
|
| `pink-ansi` 樱·ANSI | dark-ansi | 16 色 ANSI 回退,品牌色映射到 magenta 系 |
|
|
22
23
|
|
|
@@ -62,7 +63,7 @@ dsh-TUI ≥ 0.10.0 上,跟随结果会以 toast 短提示呈现:真正改写
|
|
|
62
63
|
| 主题文件损坏被自动修复 | ✿ 已修复损坏的主题文件:…(警告色) |
|
|
63
64
|
| 旧版遗留的同名主题文件与内置版本完全相同 | ✿ … 与插件内置相同,删除后配色将随插件自动更新 |
|
|
64
65
|
|
|
65
|
-
|
|
66
|
+
最后一条只针对与内置副本逐字节相同的文件——你手动调过色的文件永远不会被提及或删除。遮蔽场景还会在支持该接缝的宿主上(dsh-TUI ≥ 0.9.3)提供一次性确认对话框:确认后插件按同样的逐字节校验清理副本并以 toast 反馈结果,拒绝/忽略则一切维持现状。
|
|
66
67
|
|
|
67
68
|
## 安装
|
|
68
69
|
|
|
@@ -88,7 +89,7 @@ dsh plugin --profile dsh-tui add -w dsh-tui-theme@latest
|
|
|
88
89
|
|
|
89
90
|
在 dsh-TUI ≥ 0.10.0 中,三套主题通过 `ctx.tuiThemes` 运行时注册:调色板随插件即时生效,正常挂载时不写入用户目录,主题选择器显示中文 `displayName`。若服务晚到,插件会先同步回退到静态路径,并在确认运行时服务后删除本次写入且仍未被改写的文件。旧宿主使用静态文件路径:插件只在主题文件缺失时复制,绝不覆盖你编辑过的 `~/.dsh-tui/themes/pink-*.json`。唯一的例外是已损坏的目标文件(无法解析为 JSON,例如安装中途中断留下的残文件):插件会把它改名为 `<文件名>.corrupt-<时间戳>` 保留现场,再重新安装内置副本,记录一条警告,并在支持 toast 的宿主上弹一条屏幕提示。
|
|
90
91
|
|
|
91
|
-
从旧宿主升级后,若希望改用运行时托管,请先备份并删除 `~/.dsh-tui/themes/pink-{night,day,ansi}.json
|
|
92
|
+
从旧宿主升级后,若希望改用运行时托管,请先备份并删除 `~/.dsh-tui/themes/pink-{night,day,ansi}.json`;插件不会自动删除用户文件。遗留文件若与内置副本完全相同,插件会在运行时托管确认时弹一条一次性提醒,并额外提供一次性确认对话框(宿主 `tuiDialogs` 面板):确认删除则清理这些遮蔽副本,拒绝或忽略则不做任何改动;你手动调过色的文件永远不会被提及或删除。
|
|
92
93
|
|
|
93
94
|
旧宿主重启 dsh-TUI 后插件会把三套主题复制进 `~/.dsh-tui/themes/`(**仅缺失时复制,绝不覆盖你已有的同名文件**);0.10.0 及更新宿主则直接使用运行时注册,然后:
|
|
94
95
|
|
|
@@ -102,14 +103,16 @@ dsh plugin --profile dsh-tui add -w dsh-tui-theme@latest
|
|
|
102
103
|
|
|
103
104
|
配置有三层,优先级:`/settings` 用户层 > `cordis.yml` 配置层 > 内置默认值。
|
|
104
105
|
|
|
105
|
-
`/settings` 里找到 **pink-theme**
|
|
106
|
+
`/settings` 里找到 **pink-theme** 区块即可编辑(分「背景跟随」「状态行」两组子页):
|
|
106
107
|
|
|
107
108
|
| 字段 | 默认 | 说明 |
|
|
108
109
|
| --- | --- | --- |
|
|
109
|
-
| `followSystem` | `false` | 启动时应用上次保存的终端背景结果(昼樱 ↔
|
|
110
|
-
| `showGlyph` | `true` |
|
|
110
|
+
| `followSystem` | `false` | 启动时应用上次保存的终端背景结果(昼樱 ↔ 夜樱);插件不刷新缓存。开启时该字段会直接显示缓存状态,如 `on(缓存: light · 2026-09-12)` 或 `on(无缓存,启动时不动)` |
|
|
111
|
+
| `showGlyph` | `true` | 花符开关:开 = 以花符开头,关 = 不显示 |
|
|
112
|
+
| `statusGlyph` | `✿` | 花符字符(文本字段):1–2 个显示单元、不接受控制字符,留空恢复默认 |
|
|
111
113
|
| `showClock` | `true` | 显示 HH:MM 时钟 |
|
|
112
114
|
| `showTurns` | `true` | 显示当前会话轮数(`N✦`,自本次启动起计) |
|
|
115
|
+
| `statusSeparator` | `·` | 各段之间的分隔符(文本字段):同花符字符的校验规则 |
|
|
113
116
|
| `statusScope` | `pink-only` | 状态行展示:`pink-only` 仅樱花粉主题 / `all-themes` 所有主题 |
|
|
114
117
|
|
|
115
118
|
三项装饰全关时状态行整体消失。另有仅 profile 层的开关(`cordis.patch.yml`,不出现在 /settings):`autoInstallThemes`、`statusEnabled`。
|
|
@@ -123,9 +126,11 @@ dsh plugin --profile dsh-tui add -w dsh-tui-theme@latest
|
|
|
123
126
|
| 输入框 ❯ 提示符 | 默认态无颜色参数(终端默认前景色,模型工作时变暗);最高推理档充能动画用**写死的蓝色 ramp**(深色端 `#82B9FF` / 浅色端 `#1E5FEB`) | `EffortChargeGlyph.tsx`、`trajectory/effortIgnition.ts` |
|
|
124
127
|
| 底栏上下文进度条分段色 | system / prompt / assistant / thinking / tools 五段为**写死的藏青→品牌蓝系**(`#22305F`→`#5A7CFF`),永远不随主题变化 | `screens/StatusMetrics.ts` |
|
|
125
128
|
| 进度条空余段配色 | 宿主按 `themeName === 'light'` **字符串比较**取浅色配色——自定义浅色主题(如 pink-day)不等于 `'light'`,会拿到深色空余段,在浅色终端上偏深 | `screens/StatusLine.tsx` |
|
|
126
|
-
| 状态行文字颜色 |
|
|
129
|
+
| 状态行文字颜色 | 旧路径标量状态行(`tuiStatus.set`)由宿主统一以**无色 + 终端 dim** 渲染;dsh-TUI ≥ 0.10.1 插件已改用 `tuiStatus.registerView` 富状态视图按主题配色渲染(✿ 品牌色 / 正文 text 色 / 分隔符 subtle 色,颜色取自生效主题文件,pink-ansi 为具名 ANSI 值),旧宿主自动回退标量路径(无色 + dim) | `screens/Chat.tsx`、`dsh-adapter/status.ts` |
|
|
127
130
|
| 输入框块状光标 | 宿主挂载期间隐藏终端原生光标(`?25l`),输入框光标由应用以**反色字符**自绘(`<Text inverse>`),颜色即主题 text/background 的反色——OSC 12 光标色只能染到不可见的原生光标,插件无法给输入光标上色(辅助功能模式 `CLAUDE_CODE_ACCESSIBILITY=1` 下原生光标才可见) | `ink/components/App.tsx`、`components/PromptInput.tsx` |
|
|
128
131
|
| 正文链接 | OSC 8 超链接默认**写死的 ANSI 蓝**(`chalk.blue`);注释说明 wrap-ansi 无法跨 OSC 8 保留主题 RGB 色,故链接色不读主题键 | `cc/hyperlink.ts` |
|
|
132
|
+
| 顶栏像素鲸鱼颜色 | 四色调色板(描边/身体/腹部/嘴)**写死**且模块加载时预渲染,不读取任何主题键——任何主题都无法改变鲸鱼配色 | `components/Whale.tsx` |
|
|
133
|
+
| 顶栏文字色 | ✦ dsh-TUI 字标与欢迎语经 `claude`(0.10.1 起 `accent`)跟主题;`DEEPSEEK` 像素字主色与渐变结尾色分别跟 `claude` / `claudeBlue_FOR_SYSTEM_SPINNER`(0.10.1 起 `accent` / `activity`);**`HARNESS` 像素字只有主色跟主题——渐变结尾色是宿主固定常量 `PALE`,不读任何主题键**,主题覆盖不到它的结尾段;两词的扫过高光(`FLASH`)同为宿主常量。另:宿主 `parseRGB` 只认 `rgb(r,g,b)` 格式,hex/ansi 值会静默回退固定品牌蓝(pink-ansi 因此顶栏仍为蓝色) | `components/LogoV2.tsx`、`components/bigfont.ts`、`components/shimmer.ts`、`components/Spinner/spinnerUtils.ts` |
|
|
129
134
|
| 主界面组件与布局 | 顶栏像素鲸鱼、工具卡、输入框等宿主组件不可被插件替换或改布局——平台规则(内建优先,无组件替换接缝);主题能碰的只有颜色层 | 宿主架构约定 |
|
|
130
135
|
|
|
131
136
|
这些都需要上游 dsh-TUI 修改(例如:把充能色/进度条分段色接入主题键、空余段判断改用 `isLightThemeActive()`、给输入光标增加主题键)。上游修复前,任何社区主题包都受同样约束。
|
|
@@ -145,7 +150,7 @@ rm ~/.dsh-tui/theme-follow.json
|
|
|
145
150
|
## 开发
|
|
146
151
|
|
|
147
152
|
```sh
|
|
148
|
-
npm install
|
|
153
|
+
npm install --include=dev # 见下方安装前提,本仓不提交 lockfile
|
|
149
154
|
npm run build
|
|
150
155
|
npm run verify
|
|
151
156
|
npm run verify:package
|
|
@@ -154,11 +159,16 @@ DSH_TUI_SOURCE_ROOT=/path/to/dsh-TUI-source \
|
|
|
154
159
|
npm run verify:host
|
|
155
160
|
```
|
|
156
161
|
|
|
157
|
-
`
|
|
162
|
+
安装前提(这两条会让普通 `npm install` 失败或静默不装依赖):
|
|
163
|
+
|
|
164
|
+
- **本仓有意不提交 `package-lock.json`。** devDependency 里的宿主 tarball 内嵌 `@dsh-std/*`,其包内 manifest 仍声明 `workspace:*`;提交的 lockfile 会被 npm 忠实重放,于是 `npm install` 与 `npm ci` 都报 `EUNSUPPORTEDPROTOCOL`。删掉 lockfile 后 npm 会直接跳过 tarball 内的 bundle 目录,正常安装。请勿把 lockfile 提交回来(CI 的 `contract` 档会直接拦下)。
|
|
165
|
+
- **安装时不要带 `NODE_ENV=production`。** 该变量会让 npm 按 `omit=dev` 静默省略 devDependencies,表现为 `up to date` 但 `node_modules` 为空、随后 `tsc` 找不到。所以显式写 `--include=dev`(CI 里另外固定 `NODE_ENV=development`)。
|
|
166
|
+
|
|
167
|
+
`verify:host` 默认使用开发依赖中的 dsh-TUI(当前为 0.10.1)进行零配置验证,无需本地宿主源码检出;需要验证旧版或发布基线时,再显式指向同一版本的宿主 adapter 与源码。需要锁定版本时,额外设置 `DSH_TUI_EXPECTED_VERSION`。CI 只覆盖这条 0.10.1 零配置线,0.9.x 兼容回归仍需本机按上面的 env 指向对应宿主 worktree。
|
|
158
168
|
|
|
159
169
|
主题调色板改起来最直接:编辑 `themes/*.json` 后重新 `npm run verify`,再删掉 `~/.dsh-tui/themes/` 下对应文件让插件重装。
|
|
160
170
|
|
|
161
171
|
## 兼容性
|
|
162
172
|
|
|
163
|
-
- **dsh-TUI 版本下限:0.8.8**(状态行与设置面板;0.9.3 实测)。0.10.0
|
|
173
|
+
- **dsh-TUI 版本下限:0.8.8**(状态行与设置面板;0.9.3 实测)。0.10.0 及更新版本使用运行时主题注册;0.10.1 起状态行经富状态视图按主题配色(更旧宿主自动回退无色标量行);更旧的宿主缺 `dsh-tui-extensions` 扩展面时,插件自动降级为“仅安装三套主题”,不报错。
|
|
164
174
|
- Node `^22.19 || >=24`,纯 ESM,MIT。
|
package/cordis.patch.yml
CHANGED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# 决策:pink-day claude* 家族整体提亮一档(渐变结尾对比度接受 2.47)
|
|
2
|
+
|
|
3
|
+
- 日期:2026-09-12
|
|
4
|
+
- 状态:已采纳(Accepted-risk)
|
|
5
|
+
- 来源:提交 `bbc7ee7`;代码审查条目 L12(`REVIEW.md`)
|
|
6
|
+
|
|
7
|
+
## 背景
|
|
8
|
+
|
|
9
|
+
宿主 `parseRGB` 只接受 `rgb(r,g,b)`,hex 形式的 claude\* 键在顶栏像素字路径上会静默回退固定品牌蓝。`bbc7ee7` 将 pink-night / pink-day 的 claude\* 四键改为 rgb() 值;提交信息只记录了 `claude` 的提亮(对比度地板 3.0→2.5,"lighter sakura-day large-text identity"),pink-day 另外三键的值变更未在提交信息中列出,审查无法直接判定其意图。
|
|
10
|
+
|
|
11
|
+
## 变更事实
|
|
12
|
+
|
|
13
|
+
pink-day 四键沿既有粉色阶梯 `#D5517F → #DE6E96 → #E879A0 → #F2A0BF` 整体上移一档:
|
|
14
|
+
|
|
15
|
+
| 键 | 旧值 | 新值 |
|
|
16
|
+
| --- | --- | --- |
|
|
17
|
+
| `claude` | #D5517F | rgb(222,110,150)(#DE6E96,新设中间档) |
|
|
18
|
+
| `claudeShimmer` | #E879A0 | rgb(242,160,191)(#F2A0BF) |
|
|
19
|
+
| `claudeBlue_FOR_SYSTEM_SPINNER` | #D5517F | rgb(232,121,160)(#E879A0,即旧 claudeShimmer 档) |
|
|
20
|
+
| `claudeBlueShimmer_FOR_SYSTEM_SPINNER` | #E879A0 | rgb(242,160,191)(#F2A0BF) |
|
|
21
|
+
|
|
22
|
+
pink-night 同提交为纯格式转换,值不变(`claudeBlue == claude`、`claudeBlueShimmer == claudeShimmer` 的平色结构保持)。
|
|
23
|
+
|
|
24
|
+
## 判定与理由
|
|
25
|
+
|
|
26
|
+
判定为**有意的昼樱整体提亮**,非误改:
|
|
27
|
+
|
|
28
|
+
1. 四键沿同一条色阶一致上移,方向与提交信息为 `claude` 记录的 "lighter sakura-day identity" 完全一致;偶发误改不会产生这种连贯模式。
|
|
29
|
+
2. 新结构有明确设计语义:昼樱下 DEEPSEEK 像素字渐变从主色 #DE6E96 到结尾 #E879A0 读作向浅色 fade,与宿主自身 HARNESS 渐变以浅色常量 `PALE` 收尾的语言一致(见 README「顶栏文字色」条目);shimmer 对统一为 #F2A0BF 则与 pink-night 的 shimmer 平色结构对齐。
|
|
30
|
+
3. 校验脚本中 `claude` 用例的注释(2.5 地板)与本次方向互证。
|
|
31
|
+
|
|
32
|
+
## 接受的风险
|
|
33
|
+
|
|
34
|
+
对 pink-day 终端背景 #F6F3ED 的实测对比度(WCAG 相对亮度比):
|
|
35
|
+
|
|
36
|
+
- `claude` #DE6E96 = 2.80(守卫地板 2.5,通过)
|
|
37
|
+
- `claudeBlue_FOR_SYSTEM_SPINNER` #E879A0 = **2.47**,低于为 `claude` 采纳的 2.5 大字号地板 0.03
|
|
38
|
+
- `claudeShimmer` / `claudeBlueShimmer` #F2A0BF = 1.80(动画高光,不设数值地板)
|
|
39
|
+
|
|
40
|
+
接受理由:该键只用于 DEEPSEEK 像素字的装饰性渐变结尾段,同字主色仍由 2.5 地板守卫,正文可读性由 4.5 的 `text` 用例守卫;0.03 的差额在像素字尺寸下不可感知。
|
|
41
|
+
|
|
42
|
+
## 落地与守卫
|
|
43
|
+
|
|
44
|
+
- `scripts/validate-themes-against-host.mjs` 新增用例:`['pink-day', 'claudeBlue_FOR_SYSTEM_SPINNER', '#F6F3ED', 2.4]`(0.10.1 起经语义别名读 `activity`),防止后续继续提亮越过已接受点。
|
|
45
|
+
- 替代方案(恢复 `rgb(213,81,127)`)被否决:渐变结尾深于主色与昼樱提亮方向矛盾,且该值是旧 identity 色,与新家族并读不一致。
|
|
46
|
+
|
|
47
|
+
## 影响范围
|
|
48
|
+
|
|
49
|
+
仅 pink-day 顶栏 DEEPSEEK 像素字渐变结尾段的观感;无公共契约、接口或行为变更。pink-night / pink-ansi 不受影响。
|
package/lib/types/autoTheme.d.ts
CHANGED
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
*/
|
|
14
14
|
interface FollowCache {
|
|
15
15
|
light: boolean;
|
|
16
|
+
/** Diagnostic only (the /settings panel shows its date); never an expiry
|
|
17
|
+
* signal — the cache is applied as-is whenever the user opts in, and this
|
|
18
|
+
* plugin has no producer to refresh it (see the module header). */
|
|
16
19
|
at: number;
|
|
17
20
|
}
|
|
18
21
|
/** The persisted theme pref ({ theme: name }), host format. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"autoTheme.d.ts","sourceRoot":"","sources":["../../src/autoTheme.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AASH,UAAU,WAAW;IACnB,KAAK,EAAE,OAAO,CAAA;IACd,EAAE,EAAE,MAAM,CAAA;CACX;AAsCD,+DAA+D;AAC/D,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGjE;AAED,yDAAyD;AACzD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAErE;AAED,sFAAsF;AACtF,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAGxE;AAED,yDAAyD;AACzD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEzD;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAMrE;AAED,oFAAoF;AACpF,MAAM,MAAM,aAAa,GACrB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GACpD;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,CAAA;AAE3B;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,OAAO,EACxB,MAAM,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,GACvC,IAAI,CAUN"}
|
|
1
|
+
{"version":3,"file":"autoTheme.d.ts","sourceRoot":"","sources":["../../src/autoTheme.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AASH,UAAU,WAAW;IACnB,KAAK,EAAE,OAAO,CAAA;IACd;;wEAEoE;IACpE,EAAE,EAAE,MAAM,CAAA;CACX;AAsCD,+DAA+D;AAC/D,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGjE;AAED,yDAAyD;AACzD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAErE;AAED,sFAAsF;AACtF,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAGxE;AAED,yDAAyD;AACzD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEzD;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAMrE;AAED,oFAAoF;AACpF,MAAM,MAAM,aAAa,GACrB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GACpD;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,CAAA;AAE3B;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,OAAO,EACxB,MAAM,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,GACvC,IAAI,CAUN"}
|
package/lib/types/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAMlD,OAAO,EAAwB,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAMlD,OAAO,EAAwB,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAKjF,eAAO,MAAM,IAAI,kBAAY,CAAA;AAE7B,wDAAwD;AACxD,MAAM,MAAM,MAAM,GAAG,eAAe,CAAA;AAUpC,eAAO,MAAM,MAAM,EAAE,WAAW,CAAC,MAAM,CAUrC,CAAA;AAoBF;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,GAAE,MAAW,GAAG,IAAI,CAoK7D"}
|
package/lib/types/index.js
CHANGED
|
@@ -20,6 +20,7 @@ import { startRuntimeThemes } from './runtimeThemes.js';
|
|
|
20
20
|
import { startStatusLine } from './statusLine.js';
|
|
21
21
|
import { runFollowSystem } from './autoTheme.js';
|
|
22
22
|
import { registerPinkSettings } from './settingsSection.js';
|
|
23
|
+
import { startShadowCleanup } from './shadowCleanup.js';
|
|
23
24
|
import { startToastRelay } from './toast.js';
|
|
24
25
|
import { PLUGIN_ID } from './pluginId.js';
|
|
25
26
|
export const name = PLUGIN_ID;
|
|
@@ -35,6 +36,8 @@ export const Config = z.object({
|
|
|
35
36
|
autoInstallThemes: z.boolean().default(true),
|
|
36
37
|
statusEnabled: z.boolean().default(true),
|
|
37
38
|
followSystem: z.boolean().default(false),
|
|
39
|
+
statusGlyph: z.string().default('✿'),
|
|
40
|
+
statusSeparator: z.string().default('·'),
|
|
38
41
|
showGlyph: z.boolean().default(true),
|
|
39
42
|
showClock: z.boolean().default(true),
|
|
40
43
|
showTurns: z.boolean().default(true),
|
|
@@ -44,6 +47,8 @@ const DEFAULTS = {
|
|
|
44
47
|
autoInstallThemes: true,
|
|
45
48
|
statusEnabled: true,
|
|
46
49
|
followSystem: false,
|
|
50
|
+
statusGlyph: '✿',
|
|
51
|
+
statusSeparator: '·',
|
|
47
52
|
showGlyph: true,
|
|
48
53
|
showClock: true,
|
|
49
54
|
showTurns: true,
|
|
@@ -61,6 +66,8 @@ export function apply(ctx, config = {}) {
|
|
|
61
66
|
autoInstallThemes: config.autoInstallThemes ?? DEFAULTS.autoInstallThemes,
|
|
62
67
|
statusEnabled: config.statusEnabled ?? DEFAULTS.statusEnabled,
|
|
63
68
|
followSystem: config.followSystem ?? DEFAULTS.followSystem,
|
|
69
|
+
statusGlyph: config.statusGlyph ?? DEFAULTS.statusGlyph,
|
|
70
|
+
statusSeparator: config.statusSeparator ?? DEFAULTS.statusSeparator,
|
|
64
71
|
showGlyph: config.showGlyph ?? DEFAULTS.showGlyph,
|
|
65
72
|
showClock: config.showClock ?? DEFAULTS.showClock,
|
|
66
73
|
showTurns: config.showTurns ?? DEFAULTS.showTurns,
|
|
@@ -70,6 +77,11 @@ export function apply(ctx, config = {}) {
|
|
|
70
77
|
// logger is invisible to a TUI user). Hosts without the 0.10 toast seam
|
|
71
78
|
// degrade to the log-only behavior of previous releases.
|
|
72
79
|
const sendToast = startToastRelay(ctx);
|
|
80
|
+
// One-shot guided cleanup for byte-identical legacy theme files that shadow
|
|
81
|
+
// the runtime registry; a host-managed confirm dialog (tuiDialogs), never
|
|
82
|
+
// raw input interception. Hosts without the seam keep the toast-only
|
|
83
|
+
// behavior of previous releases.
|
|
84
|
+
const offerShadowCleanup = startShadowCleanup(ctx, sendToast);
|
|
73
85
|
// New hosts own the palette in memory. Install synchronously first so an old
|
|
74
86
|
// host can resolve a persisted theme during its first render. If a runtime
|
|
75
87
|
// service appears afterwards, remove only files written by this activation
|
|
@@ -82,6 +94,9 @@ export function apply(ctx, config = {}) {
|
|
|
82
94
|
if (!cordis.autoInstallThemes)
|
|
83
95
|
return;
|
|
84
96
|
const result = installBundledThemes();
|
|
97
|
+
if (result.sourceError !== undefined) {
|
|
98
|
+
ctx.logger.warn(`${PLUGIN_ID}: ${result.sourceError}`);
|
|
99
|
+
}
|
|
85
100
|
for (const file of result.installed) {
|
|
86
101
|
ctx.logger.info(`${PLUGIN_ID}: installed bundled theme "${file}" into ~/.dsh-tui/themes/`);
|
|
87
102
|
}
|
|
@@ -113,13 +128,15 @@ export function apply(ctx, config = {}) {
|
|
|
113
128
|
}
|
|
114
129
|
// Same-named static files win over runtime registrations. User-edited
|
|
115
130
|
// files stay unmentioned; files byte-identical to the bundled copy add
|
|
116
|
-
// nothing but permanently block palette updates, so point them out once
|
|
131
|
+
// nothing but permanently block palette updates, so point them out once
|
|
132
|
+
// and offer the guided cleanup.
|
|
117
133
|
if (!firstConfirmation)
|
|
118
134
|
return;
|
|
119
135
|
const shadowed = findShadowedBundledThemes();
|
|
120
136
|
if (shadowed.length > 0) {
|
|
121
137
|
ctx.logger.info(`${PLUGIN_ID}: legacy static files shadow the runtime registry: ${shadowed.join(', ')}`);
|
|
122
138
|
sendToast(`✿ ${shadowed.join('、')} 与插件内置相同,删除后配色将随插件自动更新`);
|
|
139
|
+
offerShadowCleanup(shadowed);
|
|
123
140
|
}
|
|
124
141
|
};
|
|
125
142
|
let runtimePresent = false;
|
|
@@ -193,7 +210,7 @@ export function apply(ctx, config = {}) {
|
|
|
193
210
|
ctx.logger.info(`${PLUGIN_ID}: follow: disabled, manual /theme choice preserved`);
|
|
194
211
|
}
|
|
195
212
|
}
|
|
196
|
-
});
|
|
213
|
+
}, dataDir);
|
|
197
214
|
// The follow decision is owned entirely by the /settings layer above: there
|
|
198
215
|
// is no timer or fallback path on hosts without a settings service — the
|
|
199
216
|
// plugin keeps the user's existing theme choice and degrades to static
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation and sanitization for the two user-configurable ornaments of the
|
|
3
|
+
* status line (the blossom glyph and the separator).
|
|
4
|
+
*
|
|
5
|
+
* The /settings layer validates drafts strictly (1–2 display cells, no
|
|
6
|
+
* control characters — an invalid draft blocks the save). The render layer
|
|
7
|
+
* additionally sanitizes whatever actually arrives in the merged config:
|
|
8
|
+
* hand-edited cordis.yml/settings.yaml values bypass the draft gate, so the
|
|
9
|
+
* strip mirrors the host's own cleanRenderText (complete ANSI sequences
|
|
10
|
+
* first, then C0/C1) before a hard 2-cell cap. A broken ornament must never
|
|
11
|
+
* reach the status line.
|
|
12
|
+
* @module dsh-tui-theme/ornament
|
|
13
|
+
*/
|
|
14
|
+
/** Rendered width of `text` in terminal cells (code points, wide = 2). */
|
|
15
|
+
export declare function displayCells(text: string): number;
|
|
16
|
+
/** A /settings draft verdict: accept, clear (re-inherit the default), or
|
|
17
|
+
* reject (the host blocks the save and keeps the editor open). */
|
|
18
|
+
export type OrnamentDraft = {
|
|
19
|
+
kind: 'set';
|
|
20
|
+
value: string;
|
|
21
|
+
} | {
|
|
22
|
+
kind: 'clear';
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Validate a glyph/separator draft from the /settings text editor. Empty
|
|
26
|
+
* resets to the built-in default; anything with control/format characters or
|
|
27
|
+
* outside the 1–2 cell budget is rejected (undefined).
|
|
28
|
+
*/
|
|
29
|
+
export declare function parseOrnamentDraft(text: string): OrnamentDraft | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* Sanitize an ornament value from any config layer (draft validation only
|
|
32
|
+
* guards /settings edits): strip complete ANSI sequences, then every control
|
|
33
|
+
* or format character; fall back when nothing renderable remains, and cap
|
|
34
|
+
* the rest at 2 cells so a hand-edited 50-character glyph cannot wreck the
|
|
35
|
+
* line.
|
|
36
|
+
*/
|
|
37
|
+
export declare function sanitizeOrnament(value: unknown, fallback: string): string;
|
|
38
|
+
//# sourceMappingURL=ornament.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ornament.d.ts","sourceRoot":"","sources":["../../src/ornament.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAyBH,0EAA0E;AAC1E,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMjD;AAcD;mEACmE;AACnE,MAAM,MAAM,aAAa,GACrB;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,CAAA;AAErB;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAO1E;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CASzE"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation and sanitization for the two user-configurable ornaments of the
|
|
3
|
+
* status line (the blossom glyph and the separator).
|
|
4
|
+
*
|
|
5
|
+
* The /settings layer validates drafts strictly (1–2 display cells, no
|
|
6
|
+
* control characters — an invalid draft blocks the save). The render layer
|
|
7
|
+
* additionally sanitizes whatever actually arrives in the merged config:
|
|
8
|
+
* hand-edited cordis.yml/settings.yaml values bypass the draft gate, so the
|
|
9
|
+
* strip mirrors the host's own cleanRenderText (complete ANSI sequences
|
|
10
|
+
* first, then C0/C1) before a hard 2-cell cap. A broken ornament must never
|
|
11
|
+
* reach the status line.
|
|
12
|
+
* @module dsh-tui-theme/ornament
|
|
13
|
+
*/
|
|
14
|
+
/** Whether a code point renders double-width in a typical terminal (a
|
|
15
|
+
* pragmatic subset of Unicode East Asian Width W/F: CJK, Hangul, fullwidth
|
|
16
|
+
* forms, and the common emoji planes). */
|
|
17
|
+
function isWide(code) {
|
|
18
|
+
return ((code >= 0x1100 && code <= 0x115f) ||
|
|
19
|
+
(code >= 0x2e80 && code <= 0x303e) ||
|
|
20
|
+
(code >= 0x3041 && code <= 0x33ff) ||
|
|
21
|
+
(code >= 0x3400 && code <= 0x4dbf) ||
|
|
22
|
+
(code >= 0x4e00 && code <= 0x9fff) ||
|
|
23
|
+
(code >= 0xa000 && code <= 0xa4cf) ||
|
|
24
|
+
(code >= 0xa960 && code <= 0xa97f) ||
|
|
25
|
+
(code >= 0xac00 && code <= 0xd7a3) ||
|
|
26
|
+
(code >= 0xf900 && code <= 0xfaff) ||
|
|
27
|
+
(code >= 0xfe10 && code <= 0xfe19) ||
|
|
28
|
+
(code >= 0xfe30 && code <= 0xfe6f) ||
|
|
29
|
+
(code >= 0xff00 && code <= 0xff60) ||
|
|
30
|
+
(code >= 0xffe0 && code <= 0xffe6) ||
|
|
31
|
+
(code >= 0x1f300 && code <= 0x1faff) ||
|
|
32
|
+
(code >= 0x20000 && code <= 0x3fffd));
|
|
33
|
+
}
|
|
34
|
+
/** Rendered width of `text` in terminal cells (code points, wide = 2). */
|
|
35
|
+
export function displayCells(text) {
|
|
36
|
+
let cells = 0;
|
|
37
|
+
for (const ch of text) {
|
|
38
|
+
cells += isWide(ch.codePointAt(0) ?? 0) ? 2 : 1;
|
|
39
|
+
}
|
|
40
|
+
return cells;
|
|
41
|
+
}
|
|
42
|
+
function capToCells(text, maxCells) {
|
|
43
|
+
let out = '';
|
|
44
|
+
let cells = 0;
|
|
45
|
+
for (const ch of text) {
|
|
46
|
+
const width = isWide(ch.codePointAt(0) ?? 0) ? 2 : 1;
|
|
47
|
+
if (cells + width > maxCells)
|
|
48
|
+
break;
|
|
49
|
+
out += ch;
|
|
50
|
+
cells += width;
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Validate a glyph/separator draft from the /settings text editor. Empty
|
|
56
|
+
* resets to the built-in default; anything with control/format characters or
|
|
57
|
+
* outside the 1–2 cell budget is rejected (undefined).
|
|
58
|
+
*/
|
|
59
|
+
export function parseOrnamentDraft(text) {
|
|
60
|
+
const trimmed = text.trim();
|
|
61
|
+
if (trimmed === '')
|
|
62
|
+
return { kind: 'clear' };
|
|
63
|
+
if (/[\p{C}]/u.test(trimmed))
|
|
64
|
+
return undefined;
|
|
65
|
+
const cells = displayCells(trimmed);
|
|
66
|
+
if (cells < 1 || cells > 2)
|
|
67
|
+
return undefined;
|
|
68
|
+
return { kind: 'set', value: trimmed };
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Sanitize an ornament value from any config layer (draft validation only
|
|
72
|
+
* guards /settings edits): strip complete ANSI sequences, then every control
|
|
73
|
+
* or format character; fall back when nothing renderable remains, and cap
|
|
74
|
+
* the rest at 2 cells so a hand-edited 50-character glyph cannot wreck the
|
|
75
|
+
* line.
|
|
76
|
+
*/
|
|
77
|
+
export function sanitizeOrnament(value, fallback) {
|
|
78
|
+
if (typeof value !== 'string')
|
|
79
|
+
return fallback;
|
|
80
|
+
const flat = value
|
|
81
|
+
.replace(/\u001B\][^\u0007]*(?:\u0007|\u001B\\)/gu, '')
|
|
82
|
+
.replace(/\u001B\[[0-?]*[ -/]*[@-~]/gu, '')
|
|
83
|
+
.replace(/[\p{C}]/gu, '')
|
|
84
|
+
.trim();
|
|
85
|
+
if (flat === '')
|
|
86
|
+
return fallback;
|
|
87
|
+
return capToCells(flat, 2);
|
|
88
|
+
}
|
|
@@ -9,9 +9,12 @@
|
|
|
9
9
|
* config layer (mirrors the host's own lang/fullscreen fields), and format()
|
|
10
10
|
* displays the effective value instead of a misleading blank.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* The section splits into two navigation groups (背景跟随 / 状态行). The two
|
|
13
|
+
* text fields (glyph, separator) validate their drafts with parse(): an
|
|
14
|
+
* invalid draft blocks the save, an empty draft clears back to the cordis
|
|
15
|
+
* default. Both services are consumed through `ctx.inject`, not apply-time
|
|
16
|
+
* `get` probes: this row may start before the host's service rows, and the
|
|
17
|
+
* inject fires whenever each service actually registers.
|
|
15
18
|
* @module dsh-tui-theme/settingsSection
|
|
16
19
|
*/
|
|
17
20
|
import type { Context } from '@deepseek-ai/cordis';
|
|
@@ -33,6 +36,8 @@ export type PinkSettingsDoc = StatusOptions & {
|
|
|
33
36
|
* still-unset fields).
|
|
34
37
|
* @param onDoc - Called with the defined-valued subset of the settings doc,
|
|
35
38
|
* initially and on every committed edit.
|
|
39
|
+
* @param dataDir - The host data directory (~/.dsh-tui), read by the
|
|
40
|
+
* followSystem field's format() to surface the cached follow state.
|
|
36
41
|
*/
|
|
37
|
-
export declare function registerPinkSettings(ctx: Context, cordis: StatusOptions, onDoc: (doc: PinkSettingsDoc) => void): void;
|
|
42
|
+
export declare function registerPinkSettings(ctx: Context, cordis: StatusOptions, onDoc: (doc: PinkSettingsDoc) => void, dataDir?: string): void;
|
|
38
43
|
//# sourceMappingURL=settingsSection.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settingsSection.d.ts","sourceRoot":"","sources":["../../src/settingsSection.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"settingsSection.d.ts","sourceRoot":"","sources":["../../src/settingsSection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAMlD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAIpD,+EAA+E;AAC/E,MAAM,MAAM,eAAe,GAAG,aAAa,GAAG;IAC5C,sEAAsE;IACtE,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,mEAAmE;IACnE,YAAY,CAAC,EAAE,OAAO,CAAA;CACvB,CAAA;AAcD;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE,CAAC,GAAG,EAAE,eAAe,KAAK,IAAI,EACrC,OAAO,GAAE,MAA0B,GAClC,IAAI,CA0DN"}
|
|
@@ -9,13 +9,20 @@
|
|
|
9
9
|
* config layer (mirrors the host's own lang/fullscreen fields), and format()
|
|
10
10
|
* displays the effective value instead of a misleading blank.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* The section splits into two navigation groups (背景跟随 / 状态行). The two
|
|
13
|
+
* text fields (glyph, separator) validate their drafts with parse(): an
|
|
14
|
+
* invalid draft blocks the save, an empty draft clears back to the cordis
|
|
15
|
+
* default. Both services are consumed through `ctx.inject`, not apply-time
|
|
16
|
+
* `get` probes: this row may start before the host's service rows, and the
|
|
17
|
+
* inject fires whenever each service actually registers.
|
|
15
18
|
* @module dsh-tui-theme/settingsSection
|
|
16
19
|
*/
|
|
20
|
+
import { join } from 'node:path';
|
|
17
21
|
import z from '@deepseek-ai/schemastery';
|
|
22
|
+
import { readFollowCache } from './autoTheme.js';
|
|
23
|
+
import { parseOrnamentDraft } from './ornament.js';
|
|
18
24
|
import { PLUGIN_ID } from './pluginId.js';
|
|
25
|
+
import { homeDir } from './themeAssets.js';
|
|
19
26
|
/**
|
|
20
27
|
* Register the settings namespace (mirror the resolved document to the
|
|
21
28
|
* caller) and, separately, the /settings section for it. Each part waits for
|
|
@@ -26,8 +33,10 @@ import { PLUGIN_ID } from './pluginId.js';
|
|
|
26
33
|
* still-unset fields).
|
|
27
34
|
* @param onDoc - Called with the defined-valued subset of the settings doc,
|
|
28
35
|
* initially and on every committed edit.
|
|
36
|
+
* @param dataDir - The host data directory (~/.dsh-tui), read by the
|
|
37
|
+
* followSystem field's format() to surface the cached follow state.
|
|
29
38
|
*/
|
|
30
|
-
export function registerPinkSettings(ctx, cordis, onDoc) {
|
|
39
|
+
export function registerPinkSettings(ctx, cordis, onDoc, dataDir = joinHomeDataDir()) {
|
|
31
40
|
ctx.inject(['settings'], settingsCtx => {
|
|
32
41
|
const settings = settingsCtx.settings;
|
|
33
42
|
try {
|
|
@@ -41,6 +50,8 @@ export function registerPinkSettings(ctx, cordis, onDoc) {
|
|
|
41
50
|
showClock: z.boolean(),
|
|
42
51
|
showTurns: z.boolean(),
|
|
43
52
|
statusScope: z.union(['pink-only', 'all-themes']),
|
|
53
|
+
statusGlyph: z.string(),
|
|
54
|
+
statusSeparator: z.string(),
|
|
44
55
|
}));
|
|
45
56
|
const emit = (doc) => {
|
|
46
57
|
if (doc === null || typeof doc !== 'object')
|
|
@@ -69,7 +80,7 @@ export function registerPinkSettings(ctx, cordis, onDoc) {
|
|
|
69
80
|
const sections = sectionsCtx
|
|
70
81
|
.tuiSettingsSections;
|
|
71
82
|
try {
|
|
72
|
-
const unregister = sections.register(sectionDefinition(cordis));
|
|
83
|
+
const unregister = sections.register(sectionDefinition(cordis, dataDir));
|
|
73
84
|
sectionsCtx.effect(() => () => unregister());
|
|
74
85
|
}
|
|
75
86
|
catch (error) {
|
|
@@ -79,15 +90,40 @@ export function registerPinkSettings(ctx, cordis, onDoc) {
|
|
|
79
90
|
}
|
|
80
91
|
});
|
|
81
92
|
}
|
|
93
|
+
function joinHomeDataDir() {
|
|
94
|
+
return join(homeDir(), '.dsh-tui');
|
|
95
|
+
}
|
|
96
|
+
/** Draft gate shared by the two ornament fields (see ornament.ts). */
|
|
97
|
+
function ornamentParse(text) {
|
|
98
|
+
const draft = parseOrnamentDraft(text);
|
|
99
|
+
if (draft === undefined)
|
|
100
|
+
return undefined;
|
|
101
|
+
return draft.kind === 'clear' ? { kind: 'clear' } : { kind: 'set', value: draft.value };
|
|
102
|
+
}
|
|
103
|
+
/** The cached follow result as a date (undefined when absent/invalid). */
|
|
104
|
+
function followCacheDate(at) {
|
|
105
|
+
if (typeof at !== 'number' || !Number.isFinite(at))
|
|
106
|
+
return undefined;
|
|
107
|
+
const date = new Date(at);
|
|
108
|
+
if (Number.isNaN(date.getTime()))
|
|
109
|
+
return undefined;
|
|
110
|
+
const pad = (value) => String(value).padStart(2, '0');
|
|
111
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
112
|
+
}
|
|
82
113
|
/** The declarative /settings block (labels bilingual, zh via descriptions). */
|
|
83
|
-
function sectionDefinition(cordis) {
|
|
114
|
+
function sectionDefinition(cordis, dataDir) {
|
|
84
115
|
return {
|
|
85
116
|
ns: PLUGIN_ID,
|
|
86
117
|
title: 'pink-theme',
|
|
87
118
|
descriptions: { zh: 'pink-theme' },
|
|
119
|
+
groups: [
|
|
120
|
+
{ id: 'follow', title: 'Background follow', descriptions: { zh: '背景跟随' } },
|
|
121
|
+
{ id: 'status-line', title: 'Status line', descriptions: { zh: '状态行' } },
|
|
122
|
+
],
|
|
88
123
|
fields: [
|
|
89
124
|
{
|
|
90
125
|
path: ['followSystem'],
|
|
126
|
+
group: 'follow',
|
|
91
127
|
label: 'Apply saved terminal background',
|
|
92
128
|
descriptions: { zh: '应用上次保存的终端背景' },
|
|
93
129
|
hint: 'Apply a previously saved terminal background result at startup. dsh-TUI does not expose a safe plugin query, so this plugin does not refresh the cache.',
|
|
@@ -95,10 +131,24 @@ function sectionDefinition(cordis) {
|
|
|
95
131
|
zh: '启动时应用此前保存的终端背景结果。dsh-TUI 未提供安全的插件查询接缝,因此本插件不会刷新该缓存。',
|
|
96
132
|
},
|
|
97
133
|
kind: 'boolean',
|
|
98
|
-
format: (value) =>
|
|
134
|
+
format: (value) => {
|
|
135
|
+
const on = typeof value === 'boolean' ? value : (cordis.followSystem ?? false);
|
|
136
|
+
if (!on)
|
|
137
|
+
return 'off';
|
|
138
|
+
// A low-frequency, synchronous read of a tiny file on the settings
|
|
139
|
+
// screen only (never on the render path), putting the "toggled on,
|
|
140
|
+
// nothing happened" reason on the surface: the state of the cache
|
|
141
|
+
// this startup would consult.
|
|
142
|
+
const cache = readFollowCache(dataDir);
|
|
143
|
+
if (cache === undefined)
|
|
144
|
+
return 'on(无缓存,启动时不动)';
|
|
145
|
+
const day = followCacheDate(cache.at);
|
|
146
|
+
return `on(缓存: ${cache.light ? 'light' : 'dark'}${day === undefined ? '' : ` · ${day}`})`;
|
|
147
|
+
},
|
|
99
148
|
},
|
|
100
149
|
{
|
|
101
150
|
path: ['showGlyph'],
|
|
151
|
+
group: 'status-line',
|
|
102
152
|
label: 'Blossom ✿',
|
|
103
153
|
descriptions: { zh: '花符 ✿' },
|
|
104
154
|
hint: 'Lead the decorative line above the prompt with ✿.',
|
|
@@ -106,8 +156,23 @@ function sectionDefinition(cordis) {
|
|
|
106
156
|
kind: 'boolean',
|
|
107
157
|
format: (value) => String(value ?? cordis.showGlyph),
|
|
108
158
|
},
|
|
159
|
+
{
|
|
160
|
+
path: ['statusGlyph'],
|
|
161
|
+
group: 'status-line',
|
|
162
|
+
label: 'Blossom glyph',
|
|
163
|
+
descriptions: { zh: '花符字符' },
|
|
164
|
+
hint: 'The character leading the decorative line. 1–2 display cells, no control characters; empty resets to the default.',
|
|
165
|
+
hintDescriptions: {
|
|
166
|
+
zh: '装饰行开头的字符。支持 1–2 个显示单元,不接受控制字符;留空恢复默认。',
|
|
167
|
+
},
|
|
168
|
+
kind: 'text',
|
|
169
|
+
placeholder: '✿',
|
|
170
|
+
format: (value) => typeof value === 'string' && value !== '' ? value : String(cordis.statusGlyph ?? '✿'),
|
|
171
|
+
parse: ornamentParse,
|
|
172
|
+
},
|
|
109
173
|
{
|
|
110
174
|
path: ['showClock'],
|
|
175
|
+
group: 'status-line',
|
|
111
176
|
label: 'Clock',
|
|
112
177
|
descriptions: { zh: '时钟' },
|
|
113
178
|
kind: 'boolean',
|
|
@@ -115,6 +180,7 @@ function sectionDefinition(cordis) {
|
|
|
115
180
|
},
|
|
116
181
|
{
|
|
117
182
|
path: ['showTurns'],
|
|
183
|
+
group: 'status-line',
|
|
118
184
|
label: 'Turn count',
|
|
119
185
|
descriptions: { zh: '轮数' },
|
|
120
186
|
hint: 'Turns of the live session counted since the TUI started.',
|
|
@@ -122,8 +188,23 @@ function sectionDefinition(cordis) {
|
|
|
122
188
|
kind: 'boolean',
|
|
123
189
|
format: (value) => String(value ?? cordis.showTurns),
|
|
124
190
|
},
|
|
191
|
+
{
|
|
192
|
+
path: ['statusSeparator'],
|
|
193
|
+
group: 'status-line',
|
|
194
|
+
label: 'Separator',
|
|
195
|
+
descriptions: { zh: '分隔符' },
|
|
196
|
+
hint: 'The character between the line’s cells. 1–2 display cells, no control characters; empty resets to the default.',
|
|
197
|
+
hintDescriptions: {
|
|
198
|
+
zh: '装饰行各段之间的分隔字符。支持 1–2 个显示单元,不接受控制字符;留空恢复默认。',
|
|
199
|
+
},
|
|
200
|
+
kind: 'text',
|
|
201
|
+
placeholder: '·',
|
|
202
|
+
format: (value) => typeof value === 'string' && value !== '' ? value : String(cordis.statusSeparator ?? '·'),
|
|
203
|
+
parse: ornamentParse,
|
|
204
|
+
},
|
|
125
205
|
{
|
|
126
206
|
path: ['statusScope'],
|
|
207
|
+
group: 'status-line',
|
|
127
208
|
label: 'Status line display',
|
|
128
209
|
descriptions: { zh: '状态行展示' },
|
|
129
210
|
hint: 'The blossom line is exclusive to the pink palettes by default; all-themes keeps it visible under other themes.',
|