dsh-tui-theme 0.3.2 → 0.5.0
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 +49 -25
- package/cordis.patch.yml +3 -3
- package/docs/screenshots/settings.png +0 -0
- package/lib/types/autoTheme.d.ts +16 -50
- package/lib/types/autoTheme.d.ts.map +1 -1
- package/lib/types/autoTheme.js +40 -176
- package/lib/types/index.d.ts +2 -2
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +36 -42
- package/lib/types/pluginId.d.ts +11 -0
- package/lib/types/pluginId.d.ts.map +1 -0
- package/lib/types/pluginId.js +10 -0
- package/lib/types/settingsSection.d.ts +2 -2
- package/lib/types/settingsSection.d.ts.map +1 -1
- package/lib/types/settingsSection.js +10 -9
- package/lib/types/statusLine.d.ts +1 -1
- package/lib/types/statusLine.d.ts.map +1 -1
- package/lib/types/statusLine.js +12 -4
- package/lib/types/themeAssets.d.ts +8 -3
- package/lib/types/themeAssets.d.ts.map +1 -1
- package/lib/types/themeAssets.js +59 -13
- package/package.json +12 -7
- package/scripts/expected-settings-contract.mjs +53 -0
- package/scripts/headless-order-test.mjs +132 -0
- package/scripts/validate-themes-against-host.mjs +199 -0
- package/scripts/verify-package.mjs +82 -0
- package/scripts/verify.mjs +545 -0
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
| 个性化 | 接缝 | 说明 |
|
|
6
6
|
| --- | --- | --- |
|
|
7
7
|
| **三套粉色主题** | 主题(静态资产) | `pink-night` 夜樱 / `pink-day` 昼樱 / `pink-ansi` 樱·ANSI,启动时自动装进 `~/.dsh-tui/themes/` |
|
|
8
|
-
|
|
|
8
|
+
| **缓存背景跟随** | 设置 + 本地缓存 | 可选地应用已有 `theme-follow.json` 的昼樱/夜樱结果;不直接读取终端输入或发送 OSC 查询 |
|
|
9
9
|
| **花符状态行** | `tuiStatus` | 输入框上方一行小装饰:✿ · 时钟 · 实时轮数(默认仅粉主题下显示) |
|
|
10
10
|
| **设置面板** | `tuiSettingsSections` | `/settings` 里一个可编辑区块,改完即时生效 |
|
|
11
11
|
|
|
@@ -15,50 +15,62 @@
|
|
|
15
15
|
|
|
16
16
|
| 主题 | 基底 | 风格 |
|
|
17
17
|
| --- | --- | --- |
|
|
18
|
-
| `pink-night` 夜樱 | dark |
|
|
18
|
+
| `pink-night` 夜樱 | dark | 暗梅底、玫瑰粉强调,95 键全覆盖 |
|
|
19
19
|
| `pink-day` 昼樱 | light | 象牙粉底、墨梅正文、柔和玫瑰强调(已通过宿主浅色身份判定) |
|
|
20
20
|
| `pink-ansi` 樱·ANSI | dark-ansi | 16 色 ANSI 回退,品牌色映射到 magenta 系 |
|
|
21
21
|
|
|
22
22
|
三套均通过 dsh-TUI 官方校验器(零警告、全键覆盖)与 WCAG 对比度检查(正文 ≥ 11:1)。
|
|
23
23
|
|
|
24
|
-
> 小知识:`pink-day` 的 `text` 写成 `rgb(61,43,51)` 而非 hex——宿主按 `text` 墨色亮度自动判定主题深浅,且只认 `rgb()` 格式。
|
|
25
|
-
|
|
26
24
|
## 截图
|
|
27
25
|
|
|
28
|
-
|
|
26
|
+
主题主界面截图实测于 dsh-tui 0.9.2;0.9.3 的 `/settings` 卡片界面见下图:
|
|
29
27
|
|
|
30
28
|
| 昼樱 `pink-day` | 夜樱 `pink-night` |
|
|
31
29
|
| :---: | :---: |
|
|
32
30
|
|  |  |
|
|
33
31
|
|
|
34
|
-
`/settings` 里的 pink-theme
|
|
32
|
+
`/settings` 里的 pink-theme 区块(终端背景 / 花符 / 时钟 / 轮数 / 状态行展示,保存即时生效):
|
|
35
33
|
|
|
36
34
|

|
|
37
35
|
|
|
38
|
-
>
|
|
36
|
+
> 图中 ❯ 提示符与链接是宿主硬编码的,见下文[宿主限制](#受宿主限制目前无法定制的部分)。
|
|
37
|
+
|
|
38
|
+
## 缓存背景跟随
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
`dsh-TUI` 没有向插件公开安全的终端查询接缝。为了不与宿主的 stdin/raw-mode 生命周期竞争,插件不会直接发送 OSC 11,也不会读取终端输入。
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
“应用上次保存的终端背景”开启后:
|
|
43
43
|
|
|
44
|
-
-
|
|
45
|
-
-
|
|
46
|
-
-
|
|
44
|
+
- 启动时读取 `~/.dsh-tui/theme-follow.json` 中已有的 `light` 结果,亮 → `pink-day`,暗 → `pink-night`,并将选择写入 `~/.dsh-tui/theme.json`;
|
|
45
|
+
- 不存在缓存时完全保留当前 `/theme` 选择;插件不会自行创建或刷新缓存;
|
|
46
|
+
- 默认关闭。该缓存可由之前的兼容版本留下;未来 dsh-TUI 提供宿主拥有的查询接缝后,插件才会安全地恢复刷新能力;
|
|
47
|
+
- 开启后该缓存会在启动时覆盖 `/theme` 的持久选择;关闭即可恢复手动选择;
|
|
47
48
|
- `DSH_TUI_THEME` 环境变量仍然最优先(宿主行为,插件不覆盖环境变量)。
|
|
48
49
|
|
|
49
50
|
## 安装
|
|
50
51
|
|
|
51
52
|
```sh
|
|
52
53
|
# 方式一:从 npm(已发布)
|
|
53
|
-
dsh plugin --profile dsh-tui add -w dsh-tui-theme
|
|
54
|
+
dsh plugin --profile dsh-tui add -w dsh-tui-theme@latest
|
|
55
|
+
|
|
56
|
+
# 方式二:本地 tarball(开发/自用;不要直接安装源码目录)
|
|
57
|
+
cd /path/to/dsh-tui-theme
|
|
58
|
+
npm run build
|
|
59
|
+
npm pack
|
|
60
|
+
dsh plugin --profile dsh-tui add -w ./dsh-tui-theme-<版本号>.tgz
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
不要以本地源码目录作为依赖安装:其开发 `node_modules` 可能与 dsh-TUI 宿主解析出不同的 Cordis/DSH framework instance,造成插件无法注册服务。
|
|
54
64
|
|
|
55
|
-
|
|
56
|
-
dsh plugin --profile dsh-tui add -w github:xiaoxiaohaigui/dsh-tui-theme
|
|
65
|
+
### 升级
|
|
57
66
|
|
|
58
|
-
|
|
59
|
-
|
|
67
|
+
```sh
|
|
68
|
+
# npm 已发布版本:请求最新版本并刷新 profile 依赖
|
|
69
|
+
dsh plugin --profile dsh-tui add -w dsh-tui-theme@latest
|
|
60
70
|
```
|
|
61
71
|
|
|
72
|
+
插件只在主题文件缺失时复制,绝不覆盖你编辑过的 `~/.dsh-tui/themes/pink-*.json`。唯一的例外是已损坏的目标文件(无法解析为 JSON,例如安装中途中断留下的残文件):插件会把它改名为 `<文件名>.corrupt-<时间戳>` 保留现场,再重新安装内置副本,并记录一条警告。升级若需要采用包内的新主题资产,请先备份自己的修改,再删除对应主题文件并重启 dsh-TUI 让插件重装。
|
|
73
|
+
|
|
62
74
|
重启 dsh-TUI 后插件自动把三套主题复制进 `~/.dsh-tui/themes/`(**仅缺失时复制,绝不覆盖你已有的同名文件**),然后:
|
|
63
75
|
|
|
64
76
|
```sh
|
|
@@ -75,7 +87,7 @@ dsh plugin --profile dsh-tui add -w /path/to/dsh-tui-theme
|
|
|
75
87
|
|
|
76
88
|
| 字段 | 默认 | 说明 |
|
|
77
89
|
| --- | --- | --- |
|
|
78
|
-
| `followSystem` |
|
|
90
|
+
| `followSystem` | `false` | 启动时应用上次保存的终端背景结果(昼樱 ↔ 夜樱);插件不刷新缓存 |
|
|
79
91
|
| `showGlyph` | `true` | 花符:开 = ✿ 开头,关 = 不显示 |
|
|
80
92
|
| `showClock` | `true` | 显示 HH:MM 时钟 |
|
|
81
93
|
| `showTurns` | `true` | 显示当前会话轮数(`N✦`,自本次启动起计) |
|
|
@@ -85,7 +97,7 @@ dsh plugin --profile dsh-tui add -w /path/to/dsh-tui-theme
|
|
|
85
97
|
|
|
86
98
|
## 受宿主限制、目前无法定制的部分
|
|
87
99
|
|
|
88
|
-
以下元素的颜色/形态由 dsh-TUI 宿主**硬编码**,不读取任何主题键,主题 JSON 与插件接缝都覆盖不到(dsh-TUI 0.9.
|
|
100
|
+
以下元素的颜色/形态由 dsh-TUI 宿主**硬编码**,不读取任何主题键,主题 JSON 与插件接缝都覆盖不到(dsh-TUI 0.9.3 实测):
|
|
89
101
|
|
|
90
102
|
| 元素 | 现状 | 位置(宿主源码) |
|
|
91
103
|
| --- | --- | --- |
|
|
@@ -94,6 +106,7 @@ dsh plugin --profile dsh-tui add -w /path/to/dsh-tui-theme
|
|
|
94
106
|
| 进度条空余段配色 | 宿主按 `themeName === 'light'` **字符串比较**取浅色配色——自定义浅色主题(如 pink-day)不等于 `'light'`,会拿到深色空余段,在浅色终端上偏深 | `screens/StatusLine.tsx` |
|
|
95
107
|
| 状态行文字颜色 | 插件状态行(tuiStatus)由宿主统一以**无色 + 终端 dim** 渲染,插件无法指定颜色(✿ 行因此继承终端默认前景色) | `screens/Chat.tsx` |
|
|
96
108
|
| 输入框块状光标 | 宿主挂载期间隐藏终端原生光标(`?25l`),输入框光标由应用以**反色字符**自绘(`<Text inverse>`),颜色即主题 text/background 的反色——OSC 12 光标色只能染到不可见的原生光标,插件无法给输入光标上色(辅助功能模式 `CLAUDE_CODE_ACCESSIBILITY=1` 下原生光标才可见) | `ink/components/App.tsx`、`components/PromptInput.tsx` |
|
|
109
|
+
| 正文链接 | OSC 8 超链接默认**写死的 ANSI 蓝**(`chalk.blue`);注释说明 wrap-ansi 无法跨 OSC 8 保留主题 RGB 色,故链接色不读主题键 | `cc/hyperlink.ts` |
|
|
97
110
|
| 主界面组件与布局 | 顶栏像素鲸鱼、工具卡、输入框等宿主组件不可被插件替换或改布局——平台规则(内建优先,无组件替换接缝);主题能碰的只有颜色层 | 宿主架构约定 |
|
|
98
111
|
|
|
99
112
|
这些都需要上游 dsh-TUI 修改(例如:把充能色/进度条分段色接入主题键、空余段判断改用 `isLightThemeActive()`、给输入光标增加主题键)。上游修复前,任何社区主题包都受同样约束。
|
|
@@ -101,21 +114,32 @@ dsh plugin --profile dsh-tui add -w /path/to/dsh-tui-theme
|
|
|
101
114
|
## 卸载
|
|
102
115
|
|
|
103
116
|
```sh
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
117
|
+
# 先在 dsh-TUI 内切换到非 pink-* 主题,例如:
|
|
118
|
+
/theme auto
|
|
119
|
+
|
|
120
|
+
# 再移除插件和可选的本地主题资产
|
|
121
|
+
dsh plugin --profile dsh-tui remove -w dsh-tui-theme
|
|
122
|
+
rm ~/.dsh-tui/themes/pink-{night,day,ansi}.json
|
|
123
|
+
rm ~/.dsh-tui/theme-follow.json
|
|
107
124
|
```
|
|
108
125
|
|
|
109
126
|
## 开发
|
|
110
127
|
|
|
111
128
|
```sh
|
|
112
|
-
npm install
|
|
113
|
-
npm run
|
|
129
|
+
npm install
|
|
130
|
+
npm run build
|
|
131
|
+
npm run verify
|
|
132
|
+
npm run verify:package
|
|
133
|
+
DSH_TUI_ADAPTER_DIR=/path/to/dsh-TUI/lib/types/dsh-adapter \
|
|
134
|
+
DSH_TUI_SOURCE_ROOT=/path/to/dsh-TUI-source \
|
|
135
|
+
npm run verify:host
|
|
114
136
|
```
|
|
115
137
|
|
|
138
|
+
`verify:host` 必须显式指向同一版本的宿主 adapter 与源码;缺少其中任一项会失败,避免把跳过误报为验证成功。日常开发会验证两者版本一致但不固定版本号;需要锁定一次发布基线时,额外设置 `DSH_TUI_EXPECTED_VERSION=0.9.3`。
|
|
139
|
+
|
|
116
140
|
主题调色板改起来最直接:编辑 `themes/*.json` 后重新 `npm run verify`,再删掉 `~/.dsh-tui/themes/` 下对应文件让插件重装。
|
|
117
141
|
|
|
118
142
|
## 兼容性
|
|
119
143
|
|
|
120
|
-
- **dsh-TUI 版本下限:0.8.8
|
|
144
|
+
- **dsh-TUI 版本下限:0.8.8**(状态行与设置面板;0.9.3 实测)。更旧的宿主缺 `dsh-tui-extensions` 扩展面时,插件自动降级为“仅安装三套主题”,不报错。
|
|
121
145
|
- Node `^22.19 || >=24`,纯 ESM,MIT。
|
package/cordis.patch.yml
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# Bundle patch for dsh-tui-theme: insert the plugin as a profile row.
|
|
2
2
|
# The plugin only consumes optional seams (themes dir, tuiStatus,
|
|
3
3
|
# tuiSettingsSections, settings) and never registers shortcuts or commands.
|
|
4
|
-
# followSystem
|
|
5
|
-
#
|
|
4
|
+
# followSystem stays off until dsh-TUI exposes a plugin-safe terminal query.
|
|
5
|
+
# Existing caches can still be applied when users explicitly enable it.
|
|
6
6
|
- insert:
|
|
7
7
|
- id: dsh-tui-theme
|
|
8
8
|
name: 'dsh-tui-theme'
|
|
9
9
|
config:
|
|
10
10
|
autoInstallThemes: true
|
|
11
11
|
statusEnabled: true
|
|
12
|
-
followSystem:
|
|
12
|
+
followSystem: false
|
|
13
13
|
showGlyph: true
|
|
14
14
|
showClock: true
|
|
15
15
|
showTurns: true
|
|
Binary file
|
package/lib/types/autoTheme.d.ts
CHANGED
|
@@ -1,21 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Cached terminal-background follow for the pink theme pair.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* - the caller wires this from the settings inject callback (see index.ts):
|
|
10
|
-
* that fires before the React tree mounts and reads ~/.dsh-tui/theme.json,
|
|
11
|
-
* so the cached detection still decides this boot;
|
|
12
|
-
* - a fresh OSC 11 query then refreshes the cache for the next boot. The
|
|
13
|
-
* very first enabling (or a system flip between boots) lands one boot
|
|
14
|
-
* late — the same cadence as the host's own "re-select auto or restart".
|
|
4
|
+
* dsh-TUI does not expose a plugin terminal-query seam. Plugins must
|
|
5
|
+
* therefore not create their own stdin consumers or raw-mode leases: the host
|
|
6
|
+
* owns both through Ink. This module only applies a previously stored result
|
|
7
|
+
* before mount. A future host-owned query service can refresh that cache
|
|
8
|
+
* without changing the preference format used here.
|
|
15
9
|
*
|
|
16
10
|
* The pref write mirrors the host's writeThemePref byte-for-byte
|
|
17
|
-
* ({"theme": name}, 2-space indent)
|
|
18
|
-
* luminance threshold so both sides always agree on light/dark.
|
|
11
|
+
* ({"theme": name}, 2-space indent).
|
|
19
12
|
* @module dsh-tui-theme/autoTheme
|
|
20
13
|
*/
|
|
21
14
|
interface FollowCache {
|
|
@@ -26,48 +19,21 @@ interface FollowCache {
|
|
|
26
19
|
export declare function readThemePref(dataDir: string): string | undefined;
|
|
27
20
|
/** Persist the theme pref in the host's exact format. */
|
|
28
21
|
export declare function writeThemePref(name: string, dataDir: string): boolean;
|
|
29
|
-
/** The cached
|
|
22
|
+
/** The cached terminal-background result, if a prior compatible writer stored one. */
|
|
30
23
|
export declare function readFollowCache(dataDir: string): FollowCache | undefined;
|
|
31
|
-
/**
|
|
32
|
-
* Map a detected background to this pair's theme name.
|
|
33
|
-
* @param light - True for a light terminal background.
|
|
34
|
-
*/
|
|
24
|
+
/** Map a cached background to this pair's theme name. */
|
|
35
25
|
export declare function themeForBackground(light: boolean): string;
|
|
36
26
|
/**
|
|
37
|
-
* Apply the follow behavior synchronously
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* @returns The applied theme name, or undefined when no cache exists yet.
|
|
27
|
+
* Apply the cached follow behavior synchronously before mount. It returns the
|
|
28
|
+
* resolved name only when the existing preference already matched or the new
|
|
29
|
+
* preference committed successfully.
|
|
41
30
|
*/
|
|
42
31
|
export declare function applyCachedFollow(dataDir: string): string | undefined;
|
|
43
32
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* parsing is mounted; raw mode is restored to whatever it was.
|
|
48
|
-
*
|
|
49
|
-
* Keystroke safety: the 'data' listener consumes every byte of the window
|
|
50
|
-
* (the terminal multiplexes replies and keypresses on one stream). Anything
|
|
51
|
-
* that is not part of the OSC 11 reply is re-emitted through stdin.emit on
|
|
52
|
-
* teardown, so input typed mid-window reaches the host parser intact.
|
|
53
|
-
* @param dataDir - The host data directory (~/.dsh-tui).
|
|
54
|
-
* @param isActive - Live follow gate: a false return (follow turned off
|
|
55
|
-
* mid-window) makes finish() skip the pref write — off preserves the
|
|
56
|
-
* manual choice even for an in-flight detection.
|
|
57
|
-
* @param stdout - Injectable for tests.
|
|
58
|
-
* @param stdin - Injectable for tests.
|
|
59
|
-
* @param setTimeoutFn - Injectable for tests.
|
|
60
|
-
* @returns The detected light-ness, or undefined when unavailable.
|
|
61
|
-
*/
|
|
62
|
-
export declare function refreshDetectedBackground(dataDir: string, isActive?: () => boolean, stdout?: NodeJS.WriteStream, stdin?: NodeJS.ReadStream, setTimeoutFn?: typeof setTimeout): Promise<boolean | undefined>;
|
|
63
|
-
/**
|
|
64
|
-
* The whole follow sequence for apply(): cached value now (pre-mount),
|
|
65
|
-
* fresh detection for the next boot.
|
|
66
|
-
* @param dataDir - The host data directory (~/.dsh-tui).
|
|
67
|
-
* @param isActive - Live follow gate, re-queried when an in-flight reply
|
|
68
|
-
* lands (follow may be switched off in /settings during the window).
|
|
69
|
-
* @param log - Info sink for the applied/refreshed outcomes.
|
|
33
|
+
* Apply a previously detected background without touching terminal I/O. A
|
|
34
|
+
* future host-owned terminal query service may refresh theme-follow.json; this
|
|
35
|
+
* plugin intentionally does not access stdin, stdout, or raw mode directly.
|
|
70
36
|
*/
|
|
71
|
-
export declare function runFollowSystem(dataDir: string,
|
|
37
|
+
export declare function runFollowSystem(dataDir: string, isCurrent: () => boolean, log: (message: string) => void): void;
|
|
72
38
|
export {};
|
|
73
39
|
//# sourceMappingURL=autoTheme.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"autoTheme.d.ts","sourceRoot":"","sources":["../../src/autoTheme.ts"],"names":[],"mappings":"AAAA
|
|
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;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,OAAO,EACxB,GAAG,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAC7B,IAAI,CASN"}
|
package/lib/types/autoTheme.js
CHANGED
|
@@ -1,39 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Cached terminal-background follow for the pink theme pair.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* - the caller wires this from the settings inject callback (see index.ts):
|
|
10
|
-
* that fires before the React tree mounts and reads ~/.dsh-tui/theme.json,
|
|
11
|
-
* so the cached detection still decides this boot;
|
|
12
|
-
* - a fresh OSC 11 query then refreshes the cache for the next boot. The
|
|
13
|
-
* very first enabling (or a system flip between boots) lands one boot
|
|
14
|
-
* late — the same cadence as the host's own "re-select auto or restart".
|
|
4
|
+
* dsh-TUI does not expose a plugin terminal-query seam. Plugins must
|
|
5
|
+
* therefore not create their own stdin consumers or raw-mode leases: the host
|
|
6
|
+
* owns both through Ink. This module only applies a previously stored result
|
|
7
|
+
* before mount. A future host-owned query service can refresh that cache
|
|
8
|
+
* without changing the preference format used here.
|
|
15
9
|
*
|
|
16
10
|
* The pref write mirrors the host's writeThemePref byte-for-byte
|
|
17
|
-
* ({"theme": name}, 2-space indent)
|
|
18
|
-
* luminance threshold so both sides always agree on light/dark.
|
|
11
|
+
* ({"theme": name}, 2-space indent).
|
|
19
12
|
* @module dsh-tui-theme/autoTheme
|
|
20
13
|
*/
|
|
21
|
-
import {
|
|
22
|
-
import { join } from 'node:path';
|
|
23
|
-
/** The pair this feature switches between (light terminal
|
|
14
|
+
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
15
|
+
import { basename, dirname, join } from 'node:path';
|
|
16
|
+
/** The pair this feature switches between (light terminal -> day). */
|
|
24
17
|
const LIGHT_THEME = 'pink-day';
|
|
25
18
|
const DARK_THEME = 'pink-night';
|
|
26
|
-
const QUERY = '\x1b]11;?\x07';
|
|
27
|
-
// OSC 11 reply: rgb:RRRR/GGGG/BBBB with 1-4 hex digits per channel, BEL or
|
|
28
|
-
// ST terminated.
|
|
29
|
-
const REPLY = /\x1b\]11;rgb:([0-9a-fA-F]{1,4})\/([0-9a-fA-F]{1,4})\/([0-9a-fA-F]{1,4})(?:\x07|\x1b\\)/;
|
|
30
|
-
// DA1 sentinel: every terminal since VT100 answers CSI c. The host's own
|
|
31
|
-
// terminal-querier uses this to avoid timeouts — a DA1 reply arriving before
|
|
32
|
-
// any OSC 11 reply proves the terminal will never answer OSC 11, so the
|
|
33
|
-
// window closes immediately instead of running out the full timeout.
|
|
34
|
-
const DA1_QUERY = '\x1b[c';
|
|
35
|
-
const DA1_REPLY = /\x1b\[\?[0-9;]*c/;
|
|
36
|
-
const DETECT_TIMEOUT_MS = 400;
|
|
37
19
|
function prefPath(dir) {
|
|
38
20
|
return join(dir, 'theme.json');
|
|
39
21
|
}
|
|
@@ -49,12 +31,20 @@ function readJsonSync(path) {
|
|
|
49
31
|
}
|
|
50
32
|
}
|
|
51
33
|
function writeJsonSync(path, value) {
|
|
34
|
+
const temporary = join(dirname(path), `.${basename(path)}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`);
|
|
52
35
|
try {
|
|
53
|
-
mkdirSync(
|
|
54
|
-
writeFileSync(
|
|
36
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
37
|
+
writeFileSync(temporary, JSON.stringify(value, null, 2), { flag: 'wx' });
|
|
38
|
+
renameSync(temporary, path);
|
|
55
39
|
return true;
|
|
56
40
|
}
|
|
57
41
|
catch {
|
|
42
|
+
try {
|
|
43
|
+
rmSync(temporary, { force: true });
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// The filesystem is already failing; a stale temp file is harmless.
|
|
47
|
+
}
|
|
58
48
|
return false;
|
|
59
49
|
}
|
|
60
50
|
}
|
|
@@ -67,170 +57,44 @@ export function readThemePref(dataDir) {
|
|
|
67
57
|
export function writeThemePref(name, dataDir) {
|
|
68
58
|
return writeJsonSync(prefPath(dataDir), { theme: name });
|
|
69
59
|
}
|
|
70
|
-
/** The cached
|
|
60
|
+
/** The cached terminal-background result, if a prior compatible writer stored one. */
|
|
71
61
|
export function readFollowCache(dataDir) {
|
|
72
62
|
const cached = readJsonSync(cachePath(dataDir));
|
|
73
63
|
return typeof cached?.light === 'boolean' ? cached : undefined;
|
|
74
64
|
}
|
|
75
|
-
|
|
76
|
-
writeJsonSync(cachePath(dataDir), { light, at: Date.now() });
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* Map a detected background to this pair's theme name.
|
|
80
|
-
* @param light - True for a light terminal background.
|
|
81
|
-
*/
|
|
65
|
+
/** Map a cached background to this pair's theme name. */
|
|
82
66
|
export function themeForBackground(light) {
|
|
83
67
|
return light ? LIGHT_THEME : DARK_THEME;
|
|
84
68
|
}
|
|
85
69
|
/**
|
|
86
|
-
* Apply the follow behavior synchronously
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* @returns The applied theme name, or undefined when no cache exists yet.
|
|
70
|
+
* Apply the cached follow behavior synchronously before mount. It returns the
|
|
71
|
+
* resolved name only when the existing preference already matched or the new
|
|
72
|
+
* preference committed successfully.
|
|
90
73
|
*/
|
|
91
74
|
export function applyCachedFollow(dataDir) {
|
|
92
75
|
const cached = readFollowCache(dataDir);
|
|
93
76
|
if (cached === undefined)
|
|
94
77
|
return undefined;
|
|
95
78
|
const target = themeForBackground(cached.light);
|
|
96
|
-
if (readThemePref(dataDir)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return target;
|
|
100
|
-
}
|
|
101
|
-
/** Scale a 1-4 digit hex OSC channel to 8-bit. */
|
|
102
|
-
function channel8(hex) {
|
|
103
|
-
const value = parseInt(hex, 16);
|
|
104
|
-
if (hex.length >= 3)
|
|
105
|
-
return Math.round(value / (16 ** hex.length - 1) * 255);
|
|
106
|
-
return value * (hex.length === 2 ? 1 : 17);
|
|
107
|
-
}
|
|
108
|
-
/** The host's luminance test (ThemeProvider.isLightBackground). */
|
|
109
|
-
function isLightBackground(r, g, b) {
|
|
110
|
-
return 0.299 * r + 0.587 * g + 0.114 * b > 140;
|
|
79
|
+
if (readThemePref(dataDir) === target)
|
|
80
|
+
return target;
|
|
81
|
+
return writeThemePref(target, dataDir) ? target : undefined;
|
|
111
82
|
}
|
|
112
83
|
/**
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
* parsing is mounted; raw mode is restored to whatever it was.
|
|
117
|
-
*
|
|
118
|
-
* Keystroke safety: the 'data' listener consumes every byte of the window
|
|
119
|
-
* (the terminal multiplexes replies and keypresses on one stream). Anything
|
|
120
|
-
* that is not part of the OSC 11 reply is re-emitted through stdin.emit on
|
|
121
|
-
* teardown, so input typed mid-window reaches the host parser intact.
|
|
122
|
-
* @param dataDir - The host data directory (~/.dsh-tui).
|
|
123
|
-
* @param isActive - Live follow gate: a false return (follow turned off
|
|
124
|
-
* mid-window) makes finish() skip the pref write — off preserves the
|
|
125
|
-
* manual choice even for an in-flight detection.
|
|
126
|
-
* @param stdout - Injectable for tests.
|
|
127
|
-
* @param stdin - Injectable for tests.
|
|
128
|
-
* @param setTimeoutFn - Injectable for tests.
|
|
129
|
-
* @returns The detected light-ness, or undefined when unavailable.
|
|
84
|
+
* Apply a previously detected background without touching terminal I/O. A
|
|
85
|
+
* future host-owned terminal query service may refresh theme-follow.json; this
|
|
86
|
+
* plugin intentionally does not access stdin, stdout, or raw mode directly.
|
|
130
87
|
*/
|
|
131
|
-
export function
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
resolve(undefined);
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
const wasRaw = stdin.isRaw === true;
|
|
138
|
-
try {
|
|
139
|
-
stdin.setRawMode?.(true);
|
|
140
|
-
}
|
|
141
|
-
catch {
|
|
142
|
-
resolve(undefined);
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
let buffer = '';
|
|
146
|
-
let settled = false;
|
|
147
|
-
// Push the window's leftover bytes back for the host's input parser.
|
|
148
|
-
// unshift() returns them to the stream's internal buffer, which both the
|
|
149
|
-
// host's pull-mode 'readable' pump (mounted later) and any flowing-mode
|
|
150
|
-
// 'data' listener consume; the emit() fallback covers a torn-down stream
|
|
151
|
-
// where unshift refuses to work. Keystrokes typed during detection used
|
|
152
|
-
// to be silently dropped here. latin1 is byte-exact.
|
|
153
|
-
const replayLeftover = (leftover) => {
|
|
154
|
-
if (leftover === '')
|
|
155
|
-
return;
|
|
156
|
-
try {
|
|
157
|
-
stdin.unshift(Buffer.from(leftover, 'latin1'));
|
|
158
|
-
}
|
|
159
|
-
catch {
|
|
160
|
-
try {
|
|
161
|
-
stdin.emit('data', Buffer.from(leftover, 'latin1'));
|
|
162
|
-
}
|
|
163
|
-
catch {
|
|
164
|
-
// Best effort: input preservation must never throw from a detector.
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
};
|
|
168
|
-
const finish = (light, leftover) => {
|
|
169
|
-
if (settled)
|
|
170
|
-
return;
|
|
171
|
-
settled = true;
|
|
172
|
-
clearTimeout(timer);
|
|
173
|
-
stdin.removeListener('data', onData);
|
|
174
|
-
replayLeftover(leftover);
|
|
175
|
-
try {
|
|
176
|
-
stdin.setRawMode?.(wasRaw);
|
|
177
|
-
}
|
|
178
|
-
catch {
|
|
179
|
-
// Stream already torn down — nothing to restore.
|
|
180
|
-
}
|
|
181
|
-
if (light !== undefined && isActive()) {
|
|
182
|
-
writeFollowCache(light, dataDir);
|
|
183
|
-
const target = themeForBackground(light);
|
|
184
|
-
if (readThemePref(dataDir) !== target) {
|
|
185
|
-
writeThemePref(target, dataDir);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
resolve(light);
|
|
189
|
-
};
|
|
190
|
-
const onData = (chunk) => {
|
|
191
|
-
buffer += typeof chunk === 'string' ? chunk : chunk.toString('latin1');
|
|
192
|
-
const match = REPLY.exec(buffer);
|
|
193
|
-
if (match !== null) {
|
|
194
|
-
const light = isLightBackground(channel8(match[1] ?? ''), channel8(match[2] ?? ''), channel8(match[3] ?? ''));
|
|
195
|
-
const leftover = buffer.slice(0, match.index) + buffer.slice(match.index + match[0].length);
|
|
196
|
-
finish(light, leftover);
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
// DA1 arrived before any OSC 11 reply: this terminal skips OSC 11
|
|
200
|
-
// entirely (host querier pattern) — stop waiting. Bytes after the DA1
|
|
201
|
-
// reply can only be input, so they come along.
|
|
202
|
-
const da1 = DA1_REPLY.exec(buffer);
|
|
203
|
-
if (da1 !== null) {
|
|
204
|
-
const leftover = buffer.slice(0, da1.index) + buffer.slice(da1.index + da1[0].length);
|
|
205
|
-
finish(undefined, leftover);
|
|
206
|
-
}
|
|
207
|
-
};
|
|
208
|
-
const timer = setTimeoutFn(() => finish(undefined, buffer), DETECT_TIMEOUT_MS);
|
|
209
|
-
if (stdin.isPaused())
|
|
210
|
-
stdin.resume();
|
|
211
|
-
stdin.on('data', onData);
|
|
212
|
-
stdout.write(QUERY + DA1_QUERY);
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
/**
|
|
216
|
-
* The whole follow sequence for apply(): cached value now (pre-mount),
|
|
217
|
-
* fresh detection for the next boot.
|
|
218
|
-
* @param dataDir - The host data directory (~/.dsh-tui).
|
|
219
|
-
* @param isActive - Live follow gate, re-queried when an in-flight reply
|
|
220
|
-
* lands (follow may be switched off in /settings during the window).
|
|
221
|
-
* @param log - Info sink for the applied/refreshed outcomes.
|
|
222
|
-
*/
|
|
223
|
-
export function runFollowSystem(dataDir, isActive, log) {
|
|
88
|
+
export function runFollowSystem(dataDir, isCurrent, log) {
|
|
89
|
+
if (!isCurrent())
|
|
90
|
+
return;
|
|
224
91
|
const applied = applyCachedFollow(dataDir);
|
|
225
|
-
if (
|
|
92
|
+
if (!isCurrent())
|
|
93
|
+
return;
|
|
94
|
+
if (applied === undefined) {
|
|
95
|
+
log('follow: cached background unavailable or preference write failed; keeping current choice');
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
226
98
|
log(`follow: applied cached background (${applied})`);
|
|
227
99
|
}
|
|
228
|
-
void refreshDetectedBackground(dataDir, isActive).then(light => {
|
|
229
|
-
if (light === undefined) {
|
|
230
|
-
log('follow: terminal background unavailable, keeping current choice');
|
|
231
|
-
}
|
|
232
|
-
else {
|
|
233
|
-
log(`follow: detected ${light ? 'light' : 'dark'} terminal → ${themeForBackground(light)}`);
|
|
234
|
-
}
|
|
235
|
-
});
|
|
236
100
|
}
|
package/lib/types/index.d.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* 个性化全部走既有接缝,不注册快捷键、不注册命令、不拦截任何输入:
|
|
5
5
|
* - 接缝四(主题):内置三套粉色 JSON(pink-night / pink-day / pink-ansi),
|
|
6
6
|
* 启动时复制进 ~/.dsh-tui/themes/(仅缺失时,绝不覆盖用户已有文件);
|
|
7
|
-
* -
|
|
8
|
-
*
|
|
7
|
+
* - 缓存背景跟随:安全应用已有的终端背景缓存,在昼樱/夜樱间切换;
|
|
8
|
+
* dsh-TUI 未提供插件终端查询接缝时,不直接读写 stdin、raw mode 或 OSC 11;
|
|
9
9
|
* - 接缝十一(状态行):输入框上方一行小装饰(✿ · 时钟 · 本轮轮数);
|
|
10
10
|
* - 接缝六(设置区块):/settings 里一个可编辑面板,即时生效。
|
|
11
11
|
*
|
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;AAKlD,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;AAKlD,OAAO,EAAwB,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAGjF,eAAO,MAAM,IAAI,kBAAY,CAAA;AAE7B,wDAAwD;AACxD,MAAM,MAAM,MAAM,GAAG,eAAe,CAAA;AAUpC,eAAO,MAAM,MAAM,EAAE,WAAW,CAAC,MAAM,CAQrC,CAAA;AAkBF;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,GAAE,MAAW,GAAG,IAAI,CA0E7D"}
|