dsh-tui-theme 0.3.2 → 0.4.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 CHANGED
@@ -5,7 +5,7 @@
5
5
  | 个性化 | 接缝 | 说明 |
6
6
  | --- | --- | --- |
7
7
  | **三套粉色主题** | 主题(静态资产) | `pink-night` 夜樱 / `pink-day` 昼樱 / `pink-ansi` 樱·ANSI,启动时自动装进 `~/.dsh-tui/themes/` |
8
- | **自动跟随背景** | 启动时序 | 检测终端/系统背景色(OSC 11,与宿主同阈值),在昼樱/夜樱间自动切换——pink 版的 auto |
8
+ | **缓存背景跟随** | 设置 + 本地缓存 | 可选地应用已有 `theme-follow.json` 的昼樱/夜樱结果;不直接读取终端输入或发送 OSC 查询 |
9
9
  | **花符状态行** | `tuiStatus` | 输入框上方一行小装饰:✿ · 时钟 · 实时轮数(默认仅粉主题下显示) |
10
10
  | **设置面板** | `tuiSettingsSections` | `/settings` 里一个可编辑区块,改完即时生效 |
11
11
 
@@ -15,7 +15,7 @@
15
15
 
16
16
  | 主题 | 基底 | 风格 |
17
17
  | --- | --- | --- |
18
- | `pink-night` 夜樱 | dark | 深梅紫底、玫瑰粉强调、粉鲸鱼吉祥物,96 键全覆盖 |
18
+ | `pink-night` 夜樱 | dark | 深梅紫底、玫瑰粉强调、粉鲸鱼吉祥物,95 键全覆盖 |
19
19
  | `pink-day` 昼樱 | light | 象牙粉底、墨梅正文、柔和玫瑰强调(已通过宿主浅色身份判定) |
20
20
  | `pink-ansi` 樱·ANSI | dark-ansi | 16 色 ANSI 回退,品牌色映射到 magenta 系 |
21
21
 
@@ -25,40 +25,54 @@
25
25
 
26
26
  ## 截图
27
27
 
28
- 实测于 dsh-tui 0.9.0
28
+ 实测于 dsh-tui 0.9.2
29
29
 
30
30
  | 昼樱 `pink-day` | 夜樱 `pink-night` |
31
31
  | :---: | :---: |
32
32
  | ![pink-day 主题界面](docs/screenshots/pink-day.png) | ![pink-night 主题界面](docs/screenshots/pink-night.png) |
33
33
 
34
- `/settings` 里的 pink-theme 区块(跟随终端背景 / 花符 / 时钟 / 轮数,保存即时生效):
34
+ `/settings` 里的 pink-theme 区块(已缓存背景 / 花符 / 时钟 / 轮数,保存即时生效):
35
35
 
36
36
  ![pink-theme 设置区块](docs/screenshots/settings.png)
37
37
 
38
38
  > 图中底栏上下文进度条的蓝色分段与 ❯ 提示符是宿主硬编码的,见下文[宿主限制](#受宿主限制目前无法定制的部分)。
39
39
 
40
- ## 自动跟随终端背景
40
+ ## 缓存背景跟随
41
41
 
42
- 设置里的“跟随终端背景”开启后(本部署默认开启):
42
+ `dsh-TUI 0.9.2` 没有向插件公开安全的终端查询接缝。为了不与宿主的 stdin/raw-mode 生命周期竞争,插件不会直接发送 OSC 11,也不会读取终端输入。
43
43
 
44
- - 每次启动检测终端背景(跟随系统主题的终端会随系统深浅变化),亮 → `pink-day`,暗 → `pink-night`,写入 `~/.dsh-tui/theme.json`,与宿主 `auto` 伪主题同款行为、同一亮度阈值;
45
- - 检测结果缓存在 `~/.dsh-tui/theme-follow.json`,因此切换在**启动瞬间**就生效;首次开启或系统刚翻转后的那次启动可能滞后一拍(与宿主 auto “重新选择或重启后跟上”一致);
46
- - 开启此功能后 `/theme` 的选择由插件接管(每次启动都会按背景覆写);关闭它即可恢复手动选择;
44
+ “使用已缓存的终端背景”开启后:
45
+
46
+ - 启动时读取 `~/.dsh-tui/theme-follow.json` 中已有的 `light` 结果,亮 → `pink-day`,暗 → `pink-night`,并将选择写入 `~/.dsh-tui/theme.json`;
47
+ - 不存在缓存时完全保留当前 `/theme` 选择;0.4.0 不会自行创建或刷新缓存;
48
+ - 默认关闭。该缓存可由之前的兼容版本留下;未来 dsh-TUI 提供宿主拥有的查询接缝后,插件才会安全地恢复刷新能力;
49
+ - 开启后该缓存会在启动时覆盖 `/theme` 的持久选择;关闭即可恢复手动选择;
47
50
  - `DSH_TUI_THEME` 环境变量仍然最优先(宿主行为,插件不覆盖环境变量)。
48
51
 
49
52
  ## 安装
50
53
 
51
54
  ```sh
52
55
  # 方式一:从 npm(已发布)
53
- dsh plugin --profile dsh-tui add -w dsh-tui-theme
56
+ dsh plugin --profile dsh-tui add -w dsh-tui-theme@latest
57
+
58
+ # 方式二:本地 tarball(开发/自用;不要直接安装源码目录)
59
+ cd /path/to/dsh-tui-theme
60
+ npm run build
61
+ npm pack
62
+ dsh plugin --profile dsh-tui add -w ./dsh-tui-theme-0.4.0.tgz
63
+ ```
64
+
65
+ 不要以本地源码目录作为依赖安装:其开发 `node_modules` 可能与 dsh-TUI 宿主解析出不同的 Cordis/DSH framework instance,造成插件无法注册服务。
54
66
 
55
- # 方式二:从 GitHub
56
- dsh plugin --profile dsh-tui add -w github:xiaoxiaohaigui/dsh-tui-theme
67
+ ### 升级
57
68
 
58
- # 方式三:本地路径(开发/自用)
59
- dsh plugin --profile dsh-tui add -w /path/to/dsh-tui-theme
69
+ ```sh
70
+ # npm 已发布版本:请求最新版本并刷新 profile 依赖
71
+ dsh plugin --profile dsh-tui add -w dsh-tui-theme@latest
60
72
  ```
61
73
 
74
+ 插件只在主题文件缺失时复制,绝不覆盖你编辑过的 `~/.dsh-tui/themes/pink-*.json`。升级若需要采用包内的新主题资产,请先备份自己的修改,再删除对应主题文件并重启 dsh-TUI 让插件重装。
75
+
62
76
  重启 dsh-TUI 后插件自动把三套主题复制进 `~/.dsh-tui/themes/`(**仅缺失时复制,绝不覆盖你已有的同名文件**),然后:
63
77
 
64
78
  ```sh
@@ -75,7 +89,7 @@ dsh plugin --profile dsh-tui add -w /path/to/dsh-tui-theme
75
89
 
76
90
  | 字段 | 默认 | 说明 |
77
91
  | --- | --- | --- |
78
- | `followSystem` | 部署默认开 | 跟随终端背景:昼樱夜樱自动切换 |
92
+ | `followSystem` | `false` | 启动时应用已缓存的终端背景结果(昼樱夜樱);0.4.0 不刷新缓存 |
79
93
  | `showGlyph` | `true` | 花符:开 = ✿ 开头,关 = 不显示 |
80
94
  | `showClock` | `true` | 显示 HH:MM 时钟 |
81
95
  | `showTurns` | `true` | 显示当前会话轮数(`N✦`,自本次启动起计) |
@@ -85,7 +99,7 @@ dsh plugin --profile dsh-tui add -w /path/to/dsh-tui-theme
85
99
 
86
100
  ## 受宿主限制、目前无法定制的部分
87
101
 
88
- 以下元素的颜色/形态由 dsh-TUI 宿主**硬编码**,不读取任何主题键,主题 JSON 与插件接缝都覆盖不到(dsh-TUI 0.9.0 实测):
102
+ 以下元素的颜色/形态由 dsh-TUI 宿主**硬编码**,不读取任何主题键,主题 JSON 与插件接缝都覆盖不到(dsh-TUI 0.9.2 实测):
89
103
 
90
104
  | 元素 | 现状 | 位置(宿主源码) |
91
105
  | --- | --- | --- |
@@ -101,21 +115,32 @@ dsh plugin --profile dsh-tui add -w /path/to/dsh-tui-theme
101
115
  ## 卸载
102
116
 
103
117
  ```sh
104
- dsh plugin --profile dsh-tui remove -w dsh-tui-theme # 移除插件
105
- rm ~/.dsh-tui/themes/pink-{night,day,ansi}.json # 可选:删除主题文件
106
- rm ~/.dsh-tui/theme-follow.json # 可选:删除跟随缓存
118
+ # 先在 dsh-TUI 内切换到非 pink-* 主题,例如:
119
+ /theme auto
120
+
121
+ # 再移除插件和可选的本地主题资产
122
+ dsh plugin --profile dsh-tui remove -w dsh-tui-theme
123
+ rm ~/.dsh-tui/themes/pink-{night,day,ansi}.json
124
+ rm ~/.dsh-tui/theme-follow.json
107
125
  ```
108
126
 
109
127
  ## 开发
110
128
 
111
129
  ```sh
112
- npm install && npm run build # tsc -> lib/types/
113
- npm run verify # 沙盒测试(临时 HOME + 伪造 TTY,不碰真实 ~/.dsh-tui)
130
+ npm install
131
+ npm run build
132
+ npm run verify
133
+ npm run verify:package
134
+ DSH_TUI_ADAPTER_DIR=/path/to/dsh-TUI/lib/types/dsh-adapter \
135
+ DSH_TUI_SOURCE_ROOT=/path/to/dsh-TUI-source \
136
+ npm run verify:host
114
137
  ```
115
138
 
139
+ `verify:host` 必须显式指向待兼容的宿主 adapter 与源码;缺少其中任一项会失败,避免把跳过误报为验证成功。
140
+
116
141
  主题调色板改起来最直接:编辑 `themes/*.json` 后重新 `npm run verify`,再删掉 `~/.dsh-tui/themes/` 下对应文件让插件重装。
117
142
 
118
143
  ## 兼容性
119
144
 
120
- - **dsh-TUI 版本下限:0.8.8**(全功能,含状态行与设置面板;0.9.0 实测)。更旧的宿主缺 `dsh-tui-extensions` 扩展面时,插件自动降级为“仅安装三套主题 + 背景跟随”,不报错。
145
+ - **dsh-TUI 版本下限:0.8.8**(状态行与设置面板;0.9.2 实测)。更旧的宿主缺 `dsh-tui-extensions` 扩展面时,插件自动降级为“仅安装三套主题”,不报错。
121
146
  - 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 opts this deployment into background-follow (the schema
5
- # default is off for generic installs).
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: true
12
+ followSystem: false
13
13
  showGlyph: true
14
14
  showClock: true
15
15
  showTurns: true
@@ -1,21 +1,14 @@
1
1
  /**
2
- * Terminal-background follow: the pink pair's `auto`.
2
+ * Cached terminal-background follow for the pink theme pair.
3
3
  *
4
- * The host's `auto` pseudo-theme resolves builtin light/dark via an OSC 11
5
- * query at mount and keeps the result in memory only user themes cannot
6
- * ride it. This module gives pink-day/pink-night the same behavior from the
7
- * plugin side:
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 v0.9.2 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) and the light test mirrors the host's
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 detection, if one exists. */
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 from cache: writes the resolved
38
- * theme name into the pref when it differs. Safe before mount pure fs.
39
- * @param dataDir - The host data directory (~/.dsh-tui).
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
- * Query the terminal background (OSC 11) and refresh the cache + pref.
45
- * Best effort: no TTY, an unresponsive terminal, or a parse failure just
46
- * leaves the previous state intact. Runs before the host's own stdin
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, isActive: () => boolean, log: (message: string) => void): void;
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;;;;;;;;;;;;;;;;;;;GAmBG;AAqBH,UAAU,WAAW;IACnB,KAAK,EAAE,OAAO,CAAA;IACd,EAAE,EAAE,MAAM,CAAA;CACX;AA4BD,+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,2CAA2C;AAC3C,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAGxE;AAMD;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEzD;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAQrE;AAcD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,MAAM,EACf,QAAQ,GAAE,MAAM,OAAoB,EACpC,MAAM,GAAE,MAAM,CAAC,WAA4B,EAC3C,KAAK,GAAE,MAAM,CAAC,UAA0B,EACxC,YAAY,GAAE,OAAO,UAAuB,GAC3C,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAgF9B;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,OAAO,EACvB,GAAG,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAC7B,IAAI,CAYN"}
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"}
@@ -1,39 +1,21 @@
1
1
  /**
2
- * Terminal-background follow: the pink pair's `auto`.
2
+ * Cached terminal-background follow for the pink theme pair.
3
3
  *
4
- * The host's `auto` pseudo-theme resolves builtin light/dark via an OSC 11
5
- * query at mount and keeps the result in memory only user themes cannot
6
- * ride it. This module gives pink-day/pink-night the same behavior from the
7
- * plugin side:
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 v0.9.2 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) and the light test mirrors the host's
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 { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
22
- import { join } from 'node:path';
23
- /** The pair this feature switches between (light terminal day). */
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(join(path, '..'), { recursive: true });
54
- writeFileSync(path, JSON.stringify(value, null, 2));
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 detection, if one exists. */
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
- function writeFollowCache(light, dataDir) {
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 from cache: writes the resolved
87
- * theme name into the pref when it differs. Safe before mount pure fs.
88
- * @param dataDir - The host data directory (~/.dsh-tui).
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) !== target) {
97
- writeThemePref(target, dataDir);
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
- * Query the terminal background (OSC 11) and refresh the cache + pref.
114
- * Best effort: no TTY, an unresponsive terminal, or a parse failure just
115
- * leaves the previous state intact. Runs before the host's own stdin
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 refreshDetectedBackground(dataDir, isActive = () => true, stdout = process.stdout, stdin = process.stdin, setTimeoutFn = setTimeout) {
132
- return new Promise(resolve => {
133
- if (stdout.isTTY !== true || stdin.isTTY !== true) {
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 (applied !== undefined && existsSync(prefPath(dataDir))) {
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
  }
@@ -4,8 +4,8 @@
4
4
  * 个性化全部走既有接缝,不注册快捷键、不注册命令、不拦截任何输入:
5
5
  * - 接缝四(主题):内置三套粉色 JSON(pink-night / pink-day / pink-ansi),
6
6
  * 启动时复制进 ~/.dsh-tui/themes/(仅缺失时,绝不覆盖用户已有文件);
7
- * - 自动跟随:检测终端/系统背景色(OSC 11,与宿主同阈值),在昼樱/夜樱
8
- * 间自动切换——pink 版的 auto
7
+ * - 缓存背景跟随:安全应用已有的终端背景缓存,在昼樱/夜樱间切换;
8
+ * dsh-TUI 未提供插件终端查询接缝时,不直接读写 stdin、raw mode 或 OSC 11
9
9
  * - 接缝十一(状态行):输入框上方一行小装饰(✿ · 时钟 · 本轮轮数);
10
10
  * - 接缝六(设置区块):/settings 里一个可编辑面板,即时生效。
11
11
  *
@@ -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;AAEjF,eAAO,MAAM,IAAI,kBAAkB,CAAA;AAEnC,wDAAwD;AACxD,MAAM,MAAM,MAAM,GAAG,eAAe,CAAA;AAUpC,eAAO,MAAM,MAAM,EAAE,WAAW,CAAC,MAAM,CAQrC,CAAA;AAyBF;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,GAAE,MAAW,GAAG,IAAI,CAsE7D"}
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;AAEjF,eAAO,MAAM,IAAI,kBAAkB,CAAA;AAEnC,wDAAwD;AACxD,MAAM,MAAM,MAAM,GAAG,eAAe,CAAA;AAUpC,eAAO,MAAM,MAAM,EAAE,WAAW,CAAC,MAAM,CAQrC,CAAA;AAyBF;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,GAAE,MAAW,GAAG,IAAI,CA2D7D"}
@@ -4,8 +4,8 @@
4
4
  * 个性化全部走既有接缝,不注册快捷键、不注册命令、不拦截任何输入:
5
5
  * - 接缝四(主题):内置三套粉色 JSON(pink-night / pink-day / pink-ansi),
6
6
  * 启动时复制进 ~/.dsh-tui/themes/(仅缺失时,绝不覆盖用户已有文件);
7
- * - 自动跟随:检测终端/系统背景色(OSC 11,与宿主同阈值),在昼樱/夜樱
8
- * 间自动切换——pink 版的 auto
7
+ * - 缓存背景跟随:安全应用已有的终端背景缓存,在昼樱/夜樱间切换;
8
+ * dsh-TUI 未提供插件终端查询接缝时,不直接读写 stdin、raw mode 或 OSC 11
9
9
  * - 接缝十一(状态行):输入框上方一行小装饰(✿ · 时钟 · 本轮轮数);
10
10
  * - 接缝六(设置区块):/settings 里一个可编辑面板,即时生效。
11
11
  *
@@ -81,20 +81,14 @@ export function apply(ctx, config = {}) {
81
81
  // The /settings user layer (settings.yaml) overrides the cordis layer and
82
82
  // lands live through scope.watch; both override the hardcoded defaults.
83
83
  let effective = cordis;
84
- // Background follow honors the MERGED knob (cordis layer overlaid by the
85
- // /settings user layer), so the decision cannot be taken synchronously at
86
- // apply(): the user layer is only readable once the settings service
87
- // answers. The settings callback still fires before the host's React tree
88
- // mounts (the host gates its own mount on the same service, probed
89
- // against 0.9.0), so the pref write decides this boot exactly like a sync
90
- // write would, and toggling followSystem in /settings re-decides live.
84
+ // Background follow applies only an existing cache. dsh-TUI v0.9.2 exposes
85
+ // no plugin terminal-query seam, so a theme plugin must not compete with
86
+ // Ink's stdin reader or raw-mode lease. The /settings layer still determines
87
+ // whether the cached result may control this startup.
91
88
  let followActive;
92
89
  const dataDir = join(homeDir(), '.dsh-tui');
93
- // Live gate for in-flight detections: a reply that lands after the user
94
- // turned follow off must not rewrite the pref.
95
- const followEnabled = () => followActive === true;
96
- const startFollow = () => {
97
- runFollowSystem(dataDir, followEnabled, message => {
90
+ const applyFollow = () => {
91
+ runFollowSystem(dataDir, () => followActive === true, message => {
98
92
  ctx.logger.info(`dsh-tui-theme: ${message}`);
99
93
  });
100
94
  };
@@ -103,25 +97,16 @@ export function apply(ctx, config = {}) {
103
97
  if (effective.followSystem !== followActive) {
104
98
  followActive = effective.followSystem;
105
99
  if (followActive) {
106
- startFollow();
100
+ applyFollow();
107
101
  }
108
102
  else {
109
103
  ctx.logger.info('dsh-tui-theme: follow: disabled, manual /theme choice preserved');
110
104
  }
111
105
  }
112
106
  });
113
- // Degradation backstop for hosts that never provide a settings service:
114
- // the inject callback never fires there, so the cordis-layer decision
115
- // applies instead. The delay lets any pending service registration (and
116
- // its callback) land first — it no-ops once followActive is settled, and
117
- // stays inside the host's own 300ms pre-mount settings gate either way.
118
- const fallbackTimer = setTimeout(() => {
119
- if (followActive === undefined && cordis.followSystem) {
120
- followActive = true;
121
- startFollow();
122
- }
123
- }, FOLLOW_FALLBACK_MS);
124
- fallbackTimer.unref?.();
125
- ctx.effect(() => () => clearTimeout(fallbackTimer));
107
+ // Cached following needs the merged settings document; without the settings
108
+ // seam the plugin keeps the user's existing theme choice and degrades to
109
+ // static assets rather than applying a profile default before that document
110
+ // could arrive.
126
111
  startStatusLine(ctx, () => effective);
127
112
  }
@@ -20,7 +20,7 @@ import type { StatusOptions } from './statusLine.js';
20
20
  export type PinkSettingsDoc = StatusOptions & {
21
21
  /** Install bundled theme JSONs on boot (cordis-config layer only). */
22
22
  autoInstallThemes?: boolean;
23
- /** Follow the terminal/system background: pink-day pink-night. */
23
+ /** Apply a cached terminal background: pink-day <-> pink-night. */
24
24
  followSystem?: boolean;
25
25
  };
26
26
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"settingsSection.d.ts","sourceRoot":"","sources":["../../src/settingsSection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAGlD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAEpD,+EAA+E;AAC/E,MAAM,MAAM,eAAe,GAAG,aAAa,GAAG;IAC5C,sEAAsE;IACtE,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,oEAAoE;IACpE,YAAY,CAAC,EAAE,OAAO,CAAA;CACvB,CAAA;AAcD;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE,CAAC,GAAG,EAAE,eAAe,KAAK,IAAI,GACpC,IAAI,CAoDN"}
1
+ {"version":3,"file":"settingsSection.d.ts","sourceRoot":"","sources":["../../src/settingsSection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAGlD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAEpD,+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;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE,CAAC,GAAG,EAAE,eAAe,KAAK,IAAI,GACpC,IAAI,CAoDN"}