dsh-tui-theme 0.3.0 → 0.3.2

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
@@ -1,120 +1,121 @@
1
- # dsh-tui-theme 🌸
2
-
3
- [dsh-TUI](https://github.com/ccch1mneyyy/dsh-TUI) 的樱花粉主题插件。一个包带来四种个性化,全部走官方接缝:
4
-
5
- | 个性化 | 接缝 | 说明 |
6
- | --- | --- | --- |
7
- | **三套粉色主题** | 主题(静态资产) | `pink-night` 夜樱 / `pink-day` 昼樱 / `pink-ansi` 樱·ANSI,启动时自动装进 `~/.dsh-tui/themes/` |
8
- | **自动跟随背景** | 启动时序 | 检测终端/系统背景色(OSC 11,与宿主同阈值),在昼樱/夜樱间自动切换——pink 版的 auto |
9
- | **花符状态行** | `tuiStatus` | 输入框上方一行小装饰:✿ · 时钟 · 实时轮数(默认仅粉主题下显示) |
10
- | **设置面板** | `tuiSettingsSections` | `/settings` 里一个可编辑区块,改完即时生效 |
11
-
12
- **明确不做的事**:不注册快捷键、不注册/修改任何命令、不拦截输入、不追加会话事件、不注入 system prompt。卸载即无痕(可选删除主题文件)。
13
-
14
- ## 主题预览
15
-
16
- | 主题 | 基底 | 风格 |
17
- | --- | --- | --- |
18
- | `pink-night` 夜樱 | dark | 深梅紫底、玫瑰粉强调、粉鲸鱼吉祥物,96 键全覆盖 |
19
- | `pink-day` 昼樱 | light | 象牙粉底、墨梅正文、柔和玫瑰强调(已通过宿主浅色身份判定) |
20
- | `pink-ansi` 樱·ANSI | dark-ansi | 16 色 ANSI 回退,品牌色映射到 magenta 系 |
21
-
22
- 三套均通过 dsh-TUI 官方校验器(零警告、全键覆盖)与 WCAG 对比度检查(正文 ≥ 11:1)。
23
-
24
- > 小知识:`pink-day` 的 `text` 写成 `rgb(61,43,51)` 而非 hex——宿主按 `text` 墨色亮度自动判定主题深浅,且只认 `rgb()` 格式。
25
-
26
- ## 截图
27
-
28
- 实测于 dsh-tui 0.9.0:
29
-
30
- | 昼樱 `pink-day` | 夜樱 `pink-night` |
31
- | :---: | :---: |
32
- | ![pink-day 主题界面](docs/screenshots/pink-day.png) | ![pink-night 主题界面](docs/screenshots/pink-night.png) |
33
-
34
- `/settings` 里的 pink-theme 区块(跟随终端背景 / 花符 / 时钟 / 轮数,保存即时生效):
35
-
36
- ![pink-theme 设置区块](docs/screenshots/settings.png)
37
-
38
- > 图中底栏上下文进度条的蓝色分段与 ❯ 提示符是宿主硬编码的,见下文[宿主限制](#受宿主限制目前无法定制的部分)。
39
-
40
- ## 自动跟随终端背景
41
-
42
- 设置里的“跟随终端背景”开启后(本部署默认开启):
43
-
44
- - 每次启动检测终端背景(跟随系统主题的终端会随系统深浅变化),亮 → `pink-day`,暗 → `pink-night`,写入 `~/.dsh-tui/theme.json`,与宿主 `auto` 伪主题同款行为、同一亮度阈值;
45
- - 检测结果缓存在 `~/.dsh-tui/theme-follow.json`,因此切换在**启动瞬间**就生效;首次开启或系统刚翻转后的那次启动可能滞后一拍(与宿主 auto “重新选择或重启后跟上”一致);
46
- - 开启此功能后 `/theme` 的选择由插件接管(每次启动都会按背景覆写);关闭它即可恢复手动选择;
47
- - `DSH_TUI_THEME` 环境变量仍然最优先(宿主行为,插件不覆盖环境变量)。
48
-
49
- ## 安装
50
-
51
- ```sh
52
- # 方式一:从 npm(已发布)
53
- dsh plugin --profile dsh-tui add -w dsh-tui-theme
54
-
55
- # 方式二:从 GitHub
56
- dsh plugin --profile dsh-tui add -w github:xiaoxiaohaigui/dsh-tui-theme
57
-
58
- # 方式三:本地路径(开发/自用)
59
- dsh plugin --profile dsh-tui add -w /path/to/dsh-tui-theme
60
- ```
61
-
62
- 重启 dsh-TUI 后插件自动把三套主题复制进 `~/.dsh-tui/themes/`(**仅缺失时复制,绝不覆盖你已有的同名文件**),然后:
63
-
64
- ```sh
65
- # 在 dsh-TUI 里
66
- /theme # 选择器:夜樱 / 昼樱 / 樱·ANSI
67
- /theme pink-night # 或直接切换(开启跟随后由插件接管)
68
- ```
69
-
70
- ## 配置
71
-
72
- 配置有三层,优先级:`/settings` 用户层 > `cordis.yml` 配置层 > 内置默认值。
73
-
74
- `/settings` 里找到 **pink-theme** 区块即可编辑:
75
-
76
- | 字段 | 默认 | 说明 |
77
- | --- | --- | --- |
78
- | `followSystem` | 部署默认开 | 跟随终端背景:昼樱 ↔ 夜樱自动切换 |
79
- | `showGlyph` | `true` | 花符:开 = ✿ 开头,关 = 不显示 |
80
- | `showClock` | `true` | 显示 HH:MM 时钟 |
81
- | `showTurns` | `true` | 显示当前会话轮数(`N✦`,自本次启动起计) |
82
- | `showOnNonPinkThemes` | `false` | 非粉主题下也显示状态行;关 = 仅在樱花粉三主题下显示(推荐保持关闭) |
83
-
84
- 三项装饰全关时状态行整体消失。另有仅 profile 层的开关(`cordis.patch.yml`,不出现在 /settings):`autoInstallThemes`、`statusEnabled`。
85
-
86
- ## 受宿主限制、目前无法定制的部分
87
-
88
- 以下元素的颜色/形态由 dsh-TUI 宿主**硬编码**,不读取任何主题键,主题 JSON 与插件接缝都覆盖不到(dsh-TUI 0.9.0 实测):
89
-
90
- | 元素 | 现状 | 位置(宿主源码) |
91
- | --- | --- | --- |
92
- | 输入框 ❯ 提示符 | 默认态无颜色参数(终端默认前景色,模型工作时变暗);最高推理档充能动画用**写死的蓝色 ramp**(深色端 `#82B9FF` / 浅色端 `#1E5FEB`) | `EffortChargeGlyph.tsx`、`trajectory/effortIgnition.ts` |
93
- | 底栏上下文进度条分段色 | system / prompt / assistant / thinking / tools 五段为**写死的藏青→品牌蓝系**(`#22305F`→`#5A7CFF`),永远不随主题变化 | `screens/StatusMetrics.ts` |
94
- | 进度条空余段配色 | 宿主按 `themeName === 'light'` **字符串比较**取浅色配色——自定义浅色主题(如 pink-day)不等于 `'light'`,会拿到深色空余段,在浅色终端上偏深 | `screens/StatusLine.tsx` |
95
- | 状态行文字颜色 | 插件状态行(tuiStatus)由宿主统一以**无色 + 终端 dim** 渲染,插件无法指定颜色(✿ 行因此继承终端默认前景色) | `screens/Chat.tsx` |
96
- | 主界面组件与布局 | 顶栏像素鲸鱼、工具卡、输入框等宿主组件不可被插件替换或改布局——平台规则(内建优先,无组件替换接缝);主题能碰的只有颜色层 | 宿主架构约定 |
97
-
98
- 这些都需要上游 dsh-TUI 修改(例如:把充能色/进度条分段色接入主题键、空余段判断改用 `isLightThemeActive()`)。上游修复前,任何社区主题包都受同样约束。
99
-
100
- ## 卸载
101
-
102
- ```sh
103
- dsh plugin --profile dsh-tui remove -w dsh-tui-theme # 移除插件
104
- rm ~/.dsh-tui/themes/pink-{night,day,ansi}.json # 可选:删除主题文件
105
- rm ~/.dsh-tui/theme-follow.json # 可选:删除跟随缓存
106
- ```
107
-
108
- ## 开发
109
-
110
- ```sh
111
- pnpm install && pnpm build # tsc -> lib/types/
112
- npm run verify # 沙盒测试(临时 HOME + 伪造 TTY,不碰真实 ~/.dsh-tui)
113
- ```
114
-
115
- 主题调色板改起来最直接:编辑 `themes/*.json` 后重新 `npm run verify`,再删掉 `~/.dsh-tui/themes/` 下对应文件让插件重装。
116
-
117
- ## 兼容性
118
-
119
- - **dsh-TUI 版本下限:0.8.8**(全功能,含状态行与设置面板;0.9.0 实测)。更旧的宿主缺 `dsh-tui-extensions` 扩展面时,插件自动降级为“仅安装三套主题 + 背景跟随”,不报错。
120
- - Node `^22.19 || >=24`,纯 ESM,MIT。
1
+ # dsh-tui-theme 🌸
2
+
3
+ [dsh-TUI](https://github.com/ccch1mneyyy/dsh-TUI) 的樱花粉主题插件。一个包带来四种个性化,全部走官方接缝:
4
+
5
+ | 个性化 | 接缝 | 说明 |
6
+ | --- | --- | --- |
7
+ | **三套粉色主题** | 主题(静态资产) | `pink-night` 夜樱 / `pink-day` 昼樱 / `pink-ansi` 樱·ANSI,启动时自动装进 `~/.dsh-tui/themes/` |
8
+ | **自动跟随背景** | 启动时序 | 检测终端/系统背景色(OSC 11,与宿主同阈值),在昼樱/夜樱间自动切换——pink 版的 auto |
9
+ | **花符状态行** | `tuiStatus` | 输入框上方一行小装饰:✿ · 时钟 · 实时轮数(默认仅粉主题下显示) |
10
+ | **设置面板** | `tuiSettingsSections` | `/settings` 里一个可编辑区块,改完即时生效 |
11
+
12
+ **明确不做的事**:不注册快捷键、不注册/修改任何命令、不拦截输入、不追加会话事件、不注入 system prompt。卸载即无痕(可选删除主题文件)。
13
+
14
+ ## 主题预览
15
+
16
+ | 主题 | 基底 | 风格 |
17
+ | --- | --- | --- |
18
+ | `pink-night` 夜樱 | dark | 深梅紫底、玫瑰粉强调、粉鲸鱼吉祥物,96 键全覆盖 |
19
+ | `pink-day` 昼樱 | light | 象牙粉底、墨梅正文、柔和玫瑰强调(已通过宿主浅色身份判定) |
20
+ | `pink-ansi` 樱·ANSI | dark-ansi | 16 色 ANSI 回退,品牌色映射到 magenta 系 |
21
+
22
+ 三套均通过 dsh-TUI 官方校验器(零警告、全键覆盖)与 WCAG 对比度检查(正文 ≥ 11:1)。
23
+
24
+ > 小知识:`pink-day` 的 `text` 写成 `rgb(61,43,51)` 而非 hex——宿主按 `text` 墨色亮度自动判定主题深浅,且只认 `rgb()` 格式。
25
+
26
+ ## 截图
27
+
28
+ 实测于 dsh-tui 0.9.0:
29
+
30
+ | 昼樱 `pink-day` | 夜樱 `pink-night` |
31
+ | :---: | :---: |
32
+ | ![pink-day 主题界面](docs/screenshots/pink-day.png) | ![pink-night 主题界面](docs/screenshots/pink-night.png) |
33
+
34
+ `/settings` 里的 pink-theme 区块(跟随终端背景 / 花符 / 时钟 / 轮数,保存即时生效):
35
+
36
+ ![pink-theme 设置区块](docs/screenshots/settings.png)
37
+
38
+ > 图中底栏上下文进度条的蓝色分段与 ❯ 提示符是宿主硬编码的,见下文[宿主限制](#受宿主限制目前无法定制的部分)。
39
+
40
+ ## 自动跟随终端背景
41
+
42
+ 设置里的“跟随终端背景”开启后(本部署默认开启):
43
+
44
+ - 每次启动检测终端背景(跟随系统主题的终端会随系统深浅变化),亮 → `pink-day`,暗 → `pink-night`,写入 `~/.dsh-tui/theme.json`,与宿主 `auto` 伪主题同款行为、同一亮度阈值;
45
+ - 检测结果缓存在 `~/.dsh-tui/theme-follow.json`,因此切换在**启动瞬间**就生效;首次开启或系统刚翻转后的那次启动可能滞后一拍(与宿主 auto “重新选择或重启后跟上”一致);
46
+ - 开启此功能后 `/theme` 的选择由插件接管(每次启动都会按背景覆写);关闭它即可恢复手动选择;
47
+ - `DSH_TUI_THEME` 环境变量仍然最优先(宿主行为,插件不覆盖环境变量)。
48
+
49
+ ## 安装
50
+
51
+ ```sh
52
+ # 方式一:从 npm(已发布)
53
+ dsh plugin --profile dsh-tui add -w dsh-tui-theme
54
+
55
+ # 方式二:从 GitHub
56
+ dsh plugin --profile dsh-tui add -w github:xiaoxiaohaigui/dsh-tui-theme
57
+
58
+ # 方式三:本地路径(开发/自用)
59
+ dsh plugin --profile dsh-tui add -w /path/to/dsh-tui-theme
60
+ ```
61
+
62
+ 重启 dsh-TUI 后插件自动把三套主题复制进 `~/.dsh-tui/themes/`(**仅缺失时复制,绝不覆盖你已有的同名文件**),然后:
63
+
64
+ ```sh
65
+ # 在 dsh-TUI 里
66
+ /theme # 选择器:夜樱 / 昼樱 / 樱·ANSI
67
+ /theme pink-night # 或直接切换(开启跟随后由插件接管)
68
+ ```
69
+
70
+ ## 配置
71
+
72
+ 配置有三层,优先级:`/settings` 用户层 > `cordis.yml` 配置层 > 内置默认值。
73
+
74
+ `/settings` 里找到 **pink-theme** 区块即可编辑:
75
+
76
+ | 字段 | 默认 | 说明 |
77
+ | --- | --- | --- |
78
+ | `followSystem` | 部署默认开 | 跟随终端背景:昼樱 ↔ 夜樱自动切换 |
79
+ | `showGlyph` | `true` | 花符:开 = ✿ 开头,关 = 不显示 |
80
+ | `showClock` | `true` | 显示 HH:MM 时钟 |
81
+ | `showTurns` | `true` | 显示当前会话轮数(`N✦`,自本次启动起计) |
82
+ | `statusScope` | `pink-only` | 状态行展示:`pink-only` 仅樱花粉主题 / `all-themes` 所有主题 |
83
+
84
+ 三项装饰全关时状态行整体消失。另有仅 profile 层的开关(`cordis.patch.yml`,不出现在 /settings):`autoInstallThemes`、`statusEnabled`。
85
+
86
+ ## 受宿主限制、目前无法定制的部分
87
+
88
+ 以下元素的颜色/形态由 dsh-TUI 宿主**硬编码**,不读取任何主题键,主题 JSON 与插件接缝都覆盖不到(dsh-TUI 0.9.0 实测):
89
+
90
+ | 元素 | 现状 | 位置(宿主源码) |
91
+ | --- | --- | --- |
92
+ | 输入框 ❯ 提示符 | 默认态无颜色参数(终端默认前景色,模型工作时变暗);最高推理档充能动画用**写死的蓝色 ramp**(深色端 `#82B9FF` / 浅色端 `#1E5FEB`) | `EffortChargeGlyph.tsx`、`trajectory/effortIgnition.ts` |
93
+ | 底栏上下文进度条分段色 | system / prompt / assistant / thinking / tools 五段为**写死的藏青→品牌蓝系**(`#22305F`→`#5A7CFF`),永远不随主题变化 | `screens/StatusMetrics.ts` |
94
+ | 进度条空余段配色 | 宿主按 `themeName === 'light'` **字符串比较**取浅色配色——自定义浅色主题(如 pink-day)不等于 `'light'`,会拿到深色空余段,在浅色终端上偏深 | `screens/StatusLine.tsx` |
95
+ | 状态行文字颜色 | 插件状态行(tuiStatus)由宿主统一以**无色 + 终端 dim** 渲染,插件无法指定颜色(✿ 行因此继承终端默认前景色) | `screens/Chat.tsx` |
96
+ | 输入框块状光标 | 宿主挂载期间隐藏终端原生光标(`?25l`),输入框光标由应用以**反色字符**自绘(`<Text inverse>`),颜色即主题 text/background 的反色——OSC 12 光标色只能染到不可见的原生光标,插件无法给输入光标上色(辅助功能模式 `CLAUDE_CODE_ACCESSIBILITY=1` 下原生光标才可见) | `ink/components/App.tsx`、`components/PromptInput.tsx` |
97
+ | 主界面组件与布局 | 顶栏像素鲸鱼、工具卡、输入框等宿主组件不可被插件替换或改布局——平台规则(内建优先,无组件替换接缝);主题能碰的只有颜色层 | 宿主架构约定 |
98
+
99
+ 这些都需要上游 dsh-TUI 修改(例如:把充能色/进度条分段色接入主题键、空余段判断改用 `isLightThemeActive()`、给输入光标增加主题键)。上游修复前,任何社区主题包都受同样约束。
100
+
101
+ ## 卸载
102
+
103
+ ```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 # 可选:删除跟随缓存
107
+ ```
108
+
109
+ ## 开发
110
+
111
+ ```sh
112
+ npm install && npm run build # tsc -> lib/types/
113
+ npm run verify # 沙盒测试(临时 HOME + 伪造 TTY,不碰真实 ~/.dsh-tui)
114
+ ```
115
+
116
+ 主题调色板改起来最直接:编辑 `themes/*.json` 后重新 `npm run verify`,再删掉 `~/.dsh-tui/themes/` 下对应文件让插件重装。
117
+
118
+ ## 兼容性
119
+
120
+ - **dsh-TUI 版本下限:0.8.8**(全功能,含状态行与设置面板;0.9.0 实测)。更旧的宿主缺 `dsh-tui-extensions` 扩展面时,插件自动降级为“仅安装三套主题 + 背景跟随”,不报错。
121
+ - Node `^22.19 || >=24`,纯 ESM,MIT。
package/cordis.patch.yml CHANGED
@@ -1,16 +1,16 @@
1
- # Bundle patch for dsh-tui-theme: insert the plugin as a profile row.
2
- # The plugin only consumes optional seams (themes dir, tuiStatus,
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).
6
- - insert:
7
- - id: dsh-tui-theme
8
- name: 'dsh-tui-theme'
9
- config:
10
- autoInstallThemes: true
11
- statusEnabled: true
12
- followSystem: true
13
- showGlyph: true
14
- showClock: true
15
- showTurns: true
16
- showOnNonPinkThemes: false
1
+ # Bundle patch for dsh-tui-theme: insert the plugin as a profile row.
2
+ # The plugin only consumes optional seams (themes dir, tuiStatus,
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).
6
+ - insert:
7
+ - id: dsh-tui-theme
8
+ name: 'dsh-tui-theme'
9
+ config:
10
+ autoInstallThemes: true
11
+ statusEnabled: true
12
+ followSystem: true
13
+ showGlyph: true
14
+ showClock: true
15
+ showTurns: true
16
+ statusScope: pink-only
Binary file
@@ -45,19 +45,29 @@ export declare function applyCachedFollow(dataDir: string): string | undefined;
45
45
  * Best effort: no TTY, an unresponsive terminal, or a parse failure just
46
46
  * leaves the previous state intact. Runs before the host's own stdin
47
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.
48
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.
49
57
  * @param stdout - Injectable for tests.
50
58
  * @param stdin - Injectable for tests.
51
59
  * @param setTimeoutFn - Injectable for tests.
52
60
  * @returns The detected light-ness, or undefined when unavailable.
53
61
  */
54
- export declare function refreshDetectedBackground(dataDir: string, stdout?: NodeJS.WriteStream, stdin?: NodeJS.ReadStream, setTimeoutFn?: typeof setTimeout): Promise<boolean | undefined>;
62
+ export declare function refreshDetectedBackground(dataDir: string, isActive?: () => boolean, stdout?: NodeJS.WriteStream, stdin?: NodeJS.ReadStream, setTimeoutFn?: typeof setTimeout): Promise<boolean | undefined>;
55
63
  /**
56
64
  * The whole follow sequence for apply(): cached value now (pre-mount),
57
65
  * fresh detection for the next boot.
58
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).
59
69
  * @param log - Info sink for the applied/refreshed outcomes.
60
70
  */
61
- export declare function runFollowSystem(dataDir: string, log: (message: string) => void): void;
71
+ export declare function runFollowSystem(dataDir: string, isActive: () => boolean, log: (message: string) => void): void;
62
72
  export {};
63
73
  //# sourceMappingURL=autoTheme.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"autoTheme.d.ts","sourceRoot":"","sources":["../../src/autoTheme.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAeH,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;;;;;;;;;;GAUG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,MAAM,EACf,MAAM,GAAE,MAAM,CAAC,WAA4B,EAC3C,KAAK,GAAE,MAAM,CAAC,UAA0B,EACxC,YAAY,GAAE,OAAO,UAAuB,GAC3C,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAmD9B;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,EACf,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;;;;;;;;;;;;;;;;;;;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"}
@@ -27,6 +27,12 @@ const QUERY = '\x1b]11;?\x07';
27
27
  // OSC 11 reply: rgb:RRRR/GGGG/BBBB with 1-4 hex digits per channel, BEL or
28
28
  // ST terminated.
29
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/;
30
36
  const DETECT_TIMEOUT_MS = 400;
31
37
  function prefPath(dir) {
32
38
  return join(dir, 'theme.json');
@@ -108,13 +114,21 @@ function isLightBackground(r, g, b) {
108
114
  * Best effort: no TTY, an unresponsive terminal, or a parse failure just
109
115
  * leaves the previous state intact. Runs before the host's own stdin
110
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.
111
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.
112
126
  * @param stdout - Injectable for tests.
113
127
  * @param stdin - Injectable for tests.
114
128
  * @param setTimeoutFn - Injectable for tests.
115
129
  * @returns The detected light-ness, or undefined when unavailable.
116
130
  */
117
- export function refreshDetectedBackground(dataDir, stdout = process.stdout, stdin = process.stdin, setTimeoutFn = setTimeout) {
131
+ export function refreshDetectedBackground(dataDir, isActive = () => true, stdout = process.stdout, stdin = process.stdin, setTimeoutFn = setTimeout) {
118
132
  return new Promise(resolve => {
119
133
  if (stdout.isTTY !== true || stdin.isTTY !== true) {
120
134
  resolve(undefined);
@@ -130,19 +144,41 @@ export function refreshDetectedBackground(dataDir, stdout = process.stdout, stdi
130
144
  }
131
145
  let buffer = '';
132
146
  let settled = false;
133
- const finish = (light) => {
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) => {
134
169
  if (settled)
135
170
  return;
136
171
  settled = true;
137
172
  clearTimeout(timer);
138
173
  stdin.removeListener('data', onData);
174
+ replayLeftover(leftover);
139
175
  try {
140
176
  stdin.setRawMode?.(wasRaw);
141
177
  }
142
178
  catch {
143
179
  // Stream already torn down — nothing to restore.
144
180
  }
145
- if (light !== undefined) {
181
+ if (light !== undefined && isActive()) {
146
182
  writeFollowCache(light, dataDir);
147
183
  const target = themeForBackground(light);
148
184
  if (readThemePref(dataDir) !== target) {
@@ -154,29 +190,42 @@ export function refreshDetectedBackground(dataDir, stdout = process.stdout, stdi
154
190
  const onData = (chunk) => {
155
191
  buffer += typeof chunk === 'string' ? chunk : chunk.toString('latin1');
156
192
  const match = REPLY.exec(buffer);
157
- if (match === null)
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);
158
197
  return;
159
- finish(isLightBackground(channel8(match[1] ?? ''), channel8(match[2] ?? ''), channel8(match[3] ?? '')));
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
+ }
160
207
  };
161
- const timer = setTimeoutFn(() => finish(undefined), DETECT_TIMEOUT_MS);
208
+ const timer = setTimeoutFn(() => finish(undefined, buffer), DETECT_TIMEOUT_MS);
162
209
  if (stdin.isPaused())
163
210
  stdin.resume();
164
211
  stdin.on('data', onData);
165
- stdout.write(QUERY);
212
+ stdout.write(QUERY + DA1_QUERY);
166
213
  });
167
214
  }
168
215
  /**
169
216
  * The whole follow sequence for apply(): cached value now (pre-mount),
170
217
  * fresh detection for the next boot.
171
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).
172
221
  * @param log - Info sink for the applied/refreshed outcomes.
173
222
  */
174
- export function runFollowSystem(dataDir, log) {
223
+ export function runFollowSystem(dataDir, isActive, log) {
175
224
  const applied = applyCachedFollow(dataDir);
176
225
  if (applied !== undefined && existsSync(prefPath(dataDir))) {
177
226
  log(`follow: applied cached background (${applied})`);
178
227
  }
179
- void refreshDetectedBackground(dataDir).then(light => {
228
+ void refreshDetectedBackground(dataDir, isActive).then(light => {
180
229
  if (light === undefined) {
181
230
  log('follow: terminal background unavailable, keeping current choice');
182
231
  }
@@ -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,CAiE7D"}
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"}
@@ -35,7 +35,7 @@ export const Config = z.object({
35
35
  showGlyph: z.boolean().default(true),
36
36
  showClock: z.boolean().default(true),
37
37
  showTurns: z.boolean().default(true),
38
- showOnNonPinkThemes: z.boolean().default(false),
38
+ statusScope: z.union(['pink-only', 'all-themes']).default('pink-only'),
39
39
  });
40
40
  const DEFAULTS = {
41
41
  autoInstallThemes: true,
@@ -44,7 +44,7 @@ const DEFAULTS = {
44
44
  showGlyph: true,
45
45
  showClock: true,
46
46
  showTurns: true,
47
- showOnNonPinkThemes: false,
47
+ statusScope: 'pink-only',
48
48
  };
49
49
  /**
50
50
  * Backstop delay for the cordis-layer follow decision on hosts without a
@@ -67,7 +67,7 @@ export function apply(ctx, config = {}) {
67
67
  showGlyph: config.showGlyph ?? DEFAULTS.showGlyph,
68
68
  showClock: config.showClock ?? DEFAULTS.showClock,
69
69
  showTurns: config.showTurns ?? DEFAULTS.showTurns,
70
- showOnNonPinkThemes: config.showOnNonPinkThemes ?? DEFAULTS.showOnNonPinkThemes,
70
+ statusScope: config.statusScope ?? DEFAULTS.statusScope,
71
71
  };
72
72
  if (cordis.autoInstallThemes) {
73
73
  const result = installBundledThemes();
@@ -90,8 +90,11 @@ export function apply(ctx, config = {}) {
90
90
  // write would, and toggling followSystem in /settings re-decides live.
91
91
  let followActive;
92
92
  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;
93
96
  const startFollow = () => {
94
- runFollowSystem(dataDir, message => {
97
+ runFollowSystem(dataDir, followEnabled, message => {
95
98
  ctx.logger.info(`dsh-tui-theme: ${message}`);
96
99
  });
97
100
  };
@@ -112,11 +115,13 @@ export function apply(ctx, config = {}) {
112
115
  // applies instead. The delay lets any pending service registration (and
113
116
  // its callback) land first — it no-ops once followActive is settled, and
114
117
  // stays inside the host's own 300ms pre-mount settings gate either way.
115
- setTimeout(() => {
118
+ const fallbackTimer = setTimeout(() => {
116
119
  if (followActive === undefined && cordis.followSystem) {
117
120
  followActive = true;
118
121
  startFollow();
119
122
  }
120
123
  }, FOLLOW_FALLBACK_MS);
124
+ fallbackTimer.unref?.();
125
+ ctx.effect(() => () => clearTimeout(fallbackTimer));
121
126
  startStatusLine(ctx, () => effective);
122
127
  }
@@ -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,CA4CN"}
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"}
@@ -30,29 +30,36 @@ import z from '@deepseek-ai/schemastery';
30
30
  export function registerPinkSettings(ctx, cordis, onDoc) {
31
31
  ctx.inject(['settings'], settingsCtx => {
32
32
  const settings = settingsCtx.settings;
33
- const scope = settings.register(settingsNamespace('dsh-tui-theme'), z.object({
34
- followSystem: z.boolean(),
35
- showGlyph: z.boolean(),
36
- showClock: z.boolean(),
37
- showTurns: z.boolean(),
38
- showOnNonPinkThemes: z.boolean(),
39
- }));
40
- const emit = (doc) => {
41
- if (doc === null || typeof doc !== 'object')
42
- return;
43
- const clean = {};
44
- for (const [key, value] of Object.entries(doc)) {
45
- if (value !== undefined)
46
- clean[key] = value;
47
- }
48
- onDoc(clean);
49
- };
50
- // Own the watcher on the inject-scoped ledger so it survives exactly as
51
- // long as this activation (scope.watch's disposer is otherwise leaked).
52
- settingsCtx.effect(() => {
53
- emit(scope.get());
54
- return scope.watch(emit);
55
- });
33
+ try {
34
+ const scope = settings.register(settingsNamespace('dsh-tui-theme'), z.object({
35
+ followSystem: z.boolean(),
36
+ showGlyph: z.boolean(),
37
+ showClock: z.boolean(),
38
+ showTurns: z.boolean(),
39
+ statusScope: z.union(['pink-only', 'all-themes']),
40
+ }));
41
+ const emit = (doc) => {
42
+ if (doc === null || typeof doc !== 'object')
43
+ return;
44
+ const clean = {};
45
+ for (const [key, value] of Object.entries(doc)) {
46
+ if (value !== undefined)
47
+ clean[key] = value;
48
+ }
49
+ onDoc(clean);
50
+ };
51
+ // Own the watcher on the inject-scoped ledger so it survives exactly as
52
+ // long as this activation (scope.watch's disposer is otherwise leaked).
53
+ settingsCtx.effect(() => {
54
+ emit(scope.get());
55
+ return scope.watch(emit);
56
+ });
57
+ }
58
+ catch (error) {
59
+ // A duplicate registration (hot reload race) or a stricter host must
60
+ // not take the plugin — or the TUI — down.
61
+ settingsCtx.logger.warn(`dsh-tui-pink-theme: settings namespace registration failed: ${String(error)}`);
62
+ }
56
63
  });
57
64
  ctx.inject(['tuiSettingsSections'], sectionsCtx => {
58
65
  const sections = sectionsCtx
@@ -112,15 +119,19 @@ function sectionDefinition(cordis) {
112
119
  format: (value) => String(value ?? cordis.showTurns),
113
120
  },
114
121
  {
115
- path: ['showOnNonPinkThemes'],
116
- label: 'Show on non-pink themes',
117
- descriptions: { zh: '非粉主题下也显示' },
118
- hint: 'The blossom line belongs to the pink palettes; off keeps it hidden while another theme is active.',
122
+ path: ['statusScope'],
123
+ label: 'Status line display',
124
+ descriptions: { zh: '状态行展示' },
125
+ hint: 'The blossom line is exclusive to the pink palettes by default; all-themes keeps it visible under other themes.',
119
126
  hintDescriptions: {
120
- zh: '状态行属于樱花粉主题;关闭时在其他主题下不显示。',
127
+ zh: '状态行默认为樱花粉主题专属;所有主题时在其他主题下同样显示。',
121
128
  },
122
- kind: 'boolean',
123
- format: (value) => String(value ?? cordis.showOnNonPinkThemes),
129
+ kind: 'select',
130
+ options: [
131
+ { value: 'pink-only', label: 'Pink themes only', descriptions: { zh: '樱花粉主题' } },
132
+ { value: 'all-themes', label: 'All themes', descriptions: { zh: '所有主题' } },
133
+ ],
134
+ format: (value) => typeof value === 'string' ? value : (cordis.statusScope ?? 'pink-only'),
124
135
  },
125
136
  ],
126
137
  };
@@ -9,10 +9,12 @@
9
9
  * The line belongs to the pink palettes: by default it only renders while a
10
10
  * pink theme is active (checked per render with the host's own theme
11
11
  * precedence, so a mid-session /theme switch takes effect on the next tick);
12
- * `showOnNonPinkThemes` opts it into every other theme too.
12
+ * `statusScope: 'all-themes'` opts it into every other theme too.
13
13
  * @module dsh-tui-pink-theme/statusLine
14
14
  */
15
15
  import type { Context } from '@deepseek-ai/cordis';
16
+ /** Which themes the blossom line renders under. */
17
+ export type StatusScope = 'pink-only' | 'all-themes';
16
18
  export interface StatusOptions {
17
19
  /** Master switch (cordis-config layer only; not surfaced in /settings). */
18
20
  statusEnabled?: boolean;
@@ -22,8 +24,8 @@ export interface StatusOptions {
22
24
  showClock?: boolean;
23
25
  /** Include the live turn count of the current session. */
24
26
  showTurns?: boolean;
25
- /** Also render while a non-pink theme is active (default: pink only). */
26
- showOnNonPinkThemes?: boolean;
27
+ /** Theme scope of the line (default: the pink palettes only). */
28
+ statusScope?: StatusScope;
27
29
  }
28
30
  /** Fully-resolved status knobs. */
29
31
  export type EffectiveStatus = Required<StatusOptions>;
@@ -1 +1 @@
1
- {"version":3,"file":"statusLine.d.ts","sourceRoot":"","sources":["../../src/statusLine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAOlD,MAAM,WAAW,aAAa;IAC5B,2EAA2E;IAC3E,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,sCAAsC;IACtC,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,+BAA+B;IAC/B,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,0DAA0D;IAC1D,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,yEAAyE;IACzE,mBAAmB,CAAC,EAAE,OAAO,CAAA;CAC9B;AAED,mCAAmC;AACnC,MAAM,MAAM,eAAe,GAAG,QAAQ,CAAC,aAAa,CAAC,CAAA;AAwCrD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,GAAG,IAAI,CA2DvF"}
1
+ {"version":3,"file":"statusLine.d.ts","sourceRoot":"","sources":["../../src/statusLine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAOlD,mDAAmD;AACnD,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG,YAAY,CAAA;AAEpD,MAAM,WAAW,aAAa;IAC5B,2EAA2E;IAC3E,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,sCAAsC;IACtC,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,+BAA+B;IAC/B,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,0DAA0D;IAC1D,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,iEAAiE;IACjE,WAAW,CAAC,EAAE,WAAW,CAAA;CAC1B;AAED,mCAAmC;AACnC,MAAM,MAAM,eAAe,GAAG,QAAQ,CAAC,aAAa,CAAC,CAAA;AAwCrD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,GAAG,IAAI,CA8DvF"}
@@ -9,7 +9,7 @@
9
9
  * The line belongs to the pink palettes: by default it only renders while a
10
10
  * pink theme is active (checked per render with the host's own theme
11
11
  * precedence, so a mid-session /theme switch takes effect on the next tick);
12
- * `showOnNonPinkThemes` opts it into every other theme too.
12
+ * `statusScope: 'all-themes'` opts it into every other theme too.
13
13
  * @module dsh-tui-pink-theme/statusLine
14
14
  */
15
15
  import { join } from 'node:path';
@@ -61,6 +61,7 @@ export function startStatusLine(ctx, getEffective) {
61
61
  const turns = new Map();
62
62
  let current;
63
63
  let dispose;
64
+ let lastText;
64
65
  const render = () => {
65
66
  try {
66
67
  const eff = getEffective();
@@ -68,7 +69,7 @@ export function startStatusLine(ctx, getEffective) {
68
69
  // The line is pink garnish: off on other themes unless opted in.
69
70
  // The theme check runs here (not once at startup) so a mid-session
70
71
  // /theme switch lands on the next tick or session event.
71
- const themeAllows = eff.showOnNonPinkThemes || isPinkThemeActive(dataDir);
72
+ const themeAllows = eff.statusScope === 'all-themes' || isPinkThemeActive(dataDir);
72
73
  if (eff.statusEnabled && themeAllows) {
73
74
  if (eff.showGlyph)
74
75
  parts.push(GLYPH);
@@ -79,6 +80,9 @@ export function startStatusLine(ctx, getEffective) {
79
80
  }
80
81
  }
81
82
  const text = parts.join(' · ');
83
+ if (text === lastText)
84
+ return;
85
+ lastText = text;
82
86
  // The trailing identity must be the inject-scoped context (the same
83
87
  // activation the traceable binds as caller) — the plugin's outer ctx
84
88
  // is a different activation view and would be silently rejected.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-tui-theme",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Sakura-pink themes for dsh-TUI with terminal-background auto-follow (pink-day/pink-night), a blossom status line, and a /settings section. No shortcuts, no commands.",
5
5
  "type": "module",
6
6
  "main": "lib/types/index.js",
@@ -24,6 +24,7 @@
24
24
  },
25
25
  "scripts": {
26
26
  "build": "tsc -p tsconfig.json",
27
+ "preverify": "npm run build",
27
28
  "verify": "node scripts/verify.mjs"
28
29
  },
29
30
  "license": "MIT",