dsh-tui-theme 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 xiaoxiaohaigui
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,90 @@
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
+ 设置里的“跟随终端背景”开启后(本部署默认开启):
29
+
30
+ - 每次启动检测终端背景(跟随系统主题的终端会随系统深浅变化),亮 → `pink-day`,暗 → `pink-night`,写入 `~/.dsh-tui/theme.json`,与宿主 `auto` 伪主题同款行为、同一亮度阈值;
31
+ - 检测结果缓存在 `~/.dsh-tui/theme-follow.json`,因此切换在**启动瞬间**就生效;首次开启或系统刚翻转后的那次启动可能滞后一拍(与宿主 auto “重新选择或重启后跟上”一致);
32
+ - 开启此功能后 `/theme` 的选择由插件接管(每次启动都会按背景覆写);关闭它即可恢复手动选择;
33
+ - `DSH_TUI_THEME` 环境变量仍然最优先(宿主行为,插件不覆盖环境变量)。
34
+
35
+ ## 安装
36
+
37
+ ```sh
38
+ # 方式一:本地路径(开发/自用)
39
+ dsh plugin --profile dsh-tui add -w /path/to/dsh-tui-theme
40
+
41
+ # 方式二:从 GitHub(发布后)
42
+ dsh plugin --profile dsh-tui add -w github:<你的用户名>/dsh-tui-theme
43
+ ```
44
+
45
+ 重启 dsh-TUI 后插件自动把三套主题复制进 `~/.dsh-tui/themes/`(**仅缺失时复制,绝不覆盖你已有的同名文件**),然后:
46
+
47
+ ```sh
48
+ # 在 dsh-TUI 里
49
+ /theme # 选择器:夜樱 / 昼樱 / 樱·ANSI
50
+ /theme pink-night # 或直接切换(开启跟随后由插件接管)
51
+ ```
52
+
53
+ ## 配置
54
+
55
+ 配置有三层,优先级:`/settings` 用户层 > `cordis.yml` 配置层 > 内置默认值。
56
+
57
+ `/settings` 里找到 **pink-theme** 区块即可编辑:
58
+
59
+ | 字段 | 默认 | 说明 |
60
+ | --- | --- | --- |
61
+ | `followSystem` | 部署默认开 | 跟随终端背景:昼樱 ↔ 夜樱自动切换 |
62
+ | `showGlyph` | `true` | 花符:开 = ✿ 开头,关 = 不显示 |
63
+ | `showClock` | `true` | 显示 HH:MM 时钟 |
64
+ | `showTurns` | `true` | 显示当前会话轮数(`N✦`,自本次启动起计) |
65
+
66
+ 三项装饰全关时状态行整体消失。另有仅 profile 层的开关(`cordis.patch.yml`,不出现在 /settings):`autoInstallThemes`、`statusEnabled`。
67
+
68
+ > 已知宿主限制:底栏的上下文进度条分段色(蓝色系)与空余段配色是宿主硬编码的,主题与插件均无法覆盖;自定义浅色主题(pink-day)会拿到深色空余段配色(宿主按 `themeName === 'light'` 判断,不识别自定义浅色主题)。这需要上游修复。
69
+
70
+ ## 卸载
71
+
72
+ ```sh
73
+ dsh plugin --profile dsh-tui remove -w dsh-tui-theme # 移除插件
74
+ rm ~/.dsh-tui/themes/pink-{night,day,ansi}.json # 可选:删除主题文件
75
+ rm ~/.dsh-tui/theme-follow.json # 可选:删除跟随缓存
76
+ ```
77
+
78
+ ## 开发
79
+
80
+ ```sh
81
+ pnpm install && pnpm build # tsc -> lib/types/
82
+ npm run verify # 沙盒测试(临时 HOME + 伪造 TTY,不碰真实 ~/.dsh-tui)
83
+ ```
84
+
85
+ 主题调色板改起来最直接:编辑 `themes/*.json` 后重新 `npm run verify`,再删掉 `~/.dsh-tui/themes/` 下对应文件让插件重装。
86
+
87
+ ## 兼容性
88
+
89
+ - dsh-TUI 的 `dsh-tui-extensions` 扩展面(tuiStatus / tuiSettingsSections);旧版 profile 缺这些服务时插件自动退化为“仅安装主题”,不会报错。
90
+ - Node `^22.19 || >=24`,纯 ESM,MIT。
@@ -0,0 +1,15 @@
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
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Terminal-background follow: the pink pair's `auto`.
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
+ * - at apply() (which runs before the React tree mounts and reads
10
+ * ~/.dsh-tui/theme.json), the cached detection writes the resolved theme
11
+ * name into the pref synchronously — always in time for 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".
15
+ *
16
+ * 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.
19
+ * @module dsh-tui-theme/autoTheme
20
+ */
21
+ interface FollowCache {
22
+ light: boolean;
23
+ at: number;
24
+ }
25
+ /** The persisted theme pref ({ theme: name }), host format. */
26
+ export declare function readThemePref(dataDir: string): string | undefined;
27
+ /** Persist the theme pref in the host's exact format. */
28
+ export declare function writeThemePref(name: string, dataDir: string): boolean;
29
+ /** The cached detection, if one exists. */
30
+ 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
+ */
35
+ export declare function themeForBackground(light: boolean): string;
36
+ /**
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.
41
+ */
42
+ export declare function applyCachedFollow(dataDir: string): string | undefined;
43
+ /**
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
+ * @param dataDir - The host data directory (~/.dsh-tui).
49
+ * @param stdout - Injectable for tests.
50
+ * @param stdin - Injectable for tests.
51
+ * @param setTimeoutFn - Injectable for tests.
52
+ * @returns The detected light-ness, or undefined when unavailable.
53
+ */
54
+ export declare function refreshDetectedBackground(dataDir: string, stdout?: NodeJS.WriteStream, stdin?: NodeJS.ReadStream, setTimeoutFn?: typeof setTimeout): Promise<boolean | undefined>;
55
+ /**
56
+ * The whole follow sequence for apply(): cached value now (pre-mount),
57
+ * fresh detection for the next boot.
58
+ * @param dataDir - The host data directory (~/.dsh-tui).
59
+ * @param log - Info sink for the applied/refreshed outcomes.
60
+ */
61
+ export declare function runFollowSystem(dataDir: string, log: (message: string) => void): void;
62
+ export {};
63
+ //# sourceMappingURL=autoTheme.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"autoTheme.d.ts","sourceRoot":"","sources":["../../src/autoTheme.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAgBH,UAAU,WAAW;IACnB,KAAK,EAAE,OAAO,CAAA;IACd,EAAE,EAAE,MAAM,CAAA;CACX;AAgCD,+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"}
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Terminal-background follow: the pink pair's `auto`.
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
+ * - at apply() (which runs before the React tree mounts and reads
10
+ * ~/.dsh-tui/theme.json), the cached detection writes the resolved theme
11
+ * name into the pref synchronously — always in time for 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".
15
+ *
16
+ * 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.
19
+ * @module dsh-tui-theme/autoTheme
20
+ */
21
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
22
+ import { homedir } from 'node:os';
23
+ import { join } from 'node:path';
24
+ /** The pair this feature switches between (light terminal → day). */
25
+ const LIGHT_THEME = 'pink-day';
26
+ const DARK_THEME = 'pink-night';
27
+ const QUERY = '\x1b]11;?\x07';
28
+ // OSC 11 reply: rgb:RRRR/GGGG/BBBB with 1-4 hex digits per channel, BEL or
29
+ // ST terminated.
30
+ 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\\)/;
31
+ const DETECT_TIMEOUT_MS = 400;
32
+ function homeDir() {
33
+ return homedir() || process.env.USERPROFILE || process.env.HOME || '';
34
+ }
35
+ function prefPath(dir) {
36
+ return join(dir, 'theme.json');
37
+ }
38
+ function cachePath(dir) {
39
+ return join(dir, 'theme-follow.json');
40
+ }
41
+ function readJsonSync(path) {
42
+ try {
43
+ return JSON.parse(readFileSync(path, 'utf8'));
44
+ }
45
+ catch {
46
+ return undefined;
47
+ }
48
+ }
49
+ function writeJsonSync(path, value) {
50
+ try {
51
+ mkdirSync(join(path, '..'), { recursive: true });
52
+ writeFileSync(path, JSON.stringify(value, null, 2));
53
+ return true;
54
+ }
55
+ catch {
56
+ return false;
57
+ }
58
+ }
59
+ /** The persisted theme pref ({ theme: name }), host format. */
60
+ export function readThemePref(dataDir) {
61
+ const parsed = readJsonSync(prefPath(dataDir));
62
+ return typeof parsed?.theme === 'string' ? parsed.theme : undefined;
63
+ }
64
+ /** Persist the theme pref in the host's exact format. */
65
+ export function writeThemePref(name, dataDir) {
66
+ return writeJsonSync(prefPath(dataDir), { theme: name });
67
+ }
68
+ /** The cached detection, if one exists. */
69
+ export function readFollowCache(dataDir) {
70
+ const cached = readJsonSync(cachePath(dataDir));
71
+ return typeof cached?.light === 'boolean' ? cached : undefined;
72
+ }
73
+ function writeFollowCache(light, dataDir) {
74
+ writeJsonSync(cachePath(dataDir), { light, at: Date.now() });
75
+ }
76
+ /**
77
+ * Map a detected background to this pair's theme name.
78
+ * @param light - True for a light terminal background.
79
+ */
80
+ export function themeForBackground(light) {
81
+ return light ? LIGHT_THEME : DARK_THEME;
82
+ }
83
+ /**
84
+ * Apply the follow behavior synchronously from cache: writes the resolved
85
+ * theme name into the pref when it differs. Safe before mount — pure fs.
86
+ * @param dataDir - The host data directory (~/.dsh-tui).
87
+ * @returns The applied theme name, or undefined when no cache exists yet.
88
+ */
89
+ export function applyCachedFollow(dataDir) {
90
+ const cached = readFollowCache(dataDir);
91
+ if (cached === undefined)
92
+ return undefined;
93
+ const target = themeForBackground(cached.light);
94
+ if (readThemePref(dataDir) !== target) {
95
+ writeThemePref(target, dataDir);
96
+ }
97
+ return target;
98
+ }
99
+ /** Scale a 1-4 digit hex OSC channel to 8-bit. */
100
+ function channel8(hex) {
101
+ const value = parseInt(hex, 16);
102
+ if (hex.length >= 3)
103
+ return Math.round(value / (16 ** hex.length - 1) * 255);
104
+ return value * (hex.length === 2 ? 1 : 17);
105
+ }
106
+ /** The host's luminance test (ThemeProvider.isLightBackground). */
107
+ function isLightBackground(r, g, b) {
108
+ return 0.299 * r + 0.587 * g + 0.114 * b > 140;
109
+ }
110
+ /**
111
+ * Query the terminal background (OSC 11) and refresh the cache + pref.
112
+ * Best effort: no TTY, an unresponsive terminal, or a parse failure just
113
+ * leaves the previous state intact. Runs before the host's own stdin
114
+ * parsing is mounted; raw mode is restored to whatever it was.
115
+ * @param dataDir - The host data directory (~/.dsh-tui).
116
+ * @param stdout - Injectable for tests.
117
+ * @param stdin - Injectable for tests.
118
+ * @param setTimeoutFn - Injectable for tests.
119
+ * @returns The detected light-ness, or undefined when unavailable.
120
+ */
121
+ export function refreshDetectedBackground(dataDir, stdout = process.stdout, stdin = process.stdin, setTimeoutFn = setTimeout) {
122
+ return new Promise(resolve => {
123
+ if (stdout.isTTY !== true || stdin.isTTY !== true) {
124
+ resolve(undefined);
125
+ return;
126
+ }
127
+ const wasRaw = stdin.isRaw === true;
128
+ try {
129
+ stdin.setRawMode?.(true);
130
+ }
131
+ catch {
132
+ resolve(undefined);
133
+ return;
134
+ }
135
+ let buffer = '';
136
+ let settled = false;
137
+ const finish = (light) => {
138
+ if (settled)
139
+ return;
140
+ settled = true;
141
+ clearTimeout(timer);
142
+ stdin.removeListener('data', onData);
143
+ try {
144
+ stdin.setRawMode?.(wasRaw);
145
+ }
146
+ catch {
147
+ // Stream already torn down — nothing to restore.
148
+ }
149
+ if (light !== undefined) {
150
+ writeFollowCache(light, dataDir);
151
+ const target = themeForBackground(light);
152
+ if (readThemePref(dataDir) !== target) {
153
+ writeThemePref(target, dataDir);
154
+ }
155
+ }
156
+ resolve(light);
157
+ };
158
+ const onData = (chunk) => {
159
+ buffer += typeof chunk === 'string' ? chunk : chunk.toString('latin1');
160
+ const match = REPLY.exec(buffer);
161
+ if (match === null)
162
+ return;
163
+ finish(isLightBackground(channel8(match[1] ?? ''), channel8(match[2] ?? ''), channel8(match[3] ?? '')));
164
+ };
165
+ const timer = setTimeoutFn(() => finish(undefined), DETECT_TIMEOUT_MS);
166
+ if (stdin.isPaused())
167
+ stdin.resume();
168
+ stdin.on('data', onData);
169
+ stdout.write(QUERY);
170
+ });
171
+ }
172
+ /**
173
+ * The whole follow sequence for apply(): cached value now (pre-mount),
174
+ * fresh detection for the next boot.
175
+ * @param dataDir - The host data directory (~/.dsh-tui).
176
+ * @param log - Info sink for the applied/refreshed outcomes.
177
+ */
178
+ export function runFollowSystem(dataDir, log) {
179
+ const applied = applyCachedFollow(dataDir);
180
+ if (applied !== undefined && existsSync(prefPath(dataDir))) {
181
+ log(`follow: applied cached background (${applied})`);
182
+ }
183
+ void refreshDetectedBackground(dataDir).then(light => {
184
+ if (light === undefined) {
185
+ log('follow: terminal background unavailable, keeping current choice');
186
+ }
187
+ else {
188
+ log(`follow: detected ${light ? 'light' : 'dark'} terminal → ${themeForBackground(light)}`);
189
+ }
190
+ });
191
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * dsh-tui-theme — 樱花粉主题插件(dsh-TUI 生态)。
3
+ *
4
+ * 个性化全部走既有接缝,不注册快捷键、不注册命令、不拦截任何输入:
5
+ * - 接缝四(主题):内置三套粉色 JSON(pink-night / pink-day / pink-ansi),
6
+ * 启动时复制进 ~/.dsh-tui/themes/(仅缺失时,绝不覆盖用户已有文件);
7
+ * - 自动跟随:检测终端/系统背景色(OSC 11,与宿主同阈值),在昼樱/夜樱
8
+ * 间自动切换——pink 版的 auto;
9
+ * - 接缝十一(状态行):输入框上方一行小装饰(✿ · 时钟 · 本轮轮数);
10
+ * - 接缝六(设置区块):/settings 里一个可编辑面板,即时生效。
11
+ *
12
+ * 遵循 #183 纪律:所有宿主服务用 ctx.inject 软探测,缺席即静默降级;
13
+ * 插件缺失或宿主较旧时行为退化为“什么都没发生”,绝不拖垮启动。
14
+ * @module dsh-tui-theme
15
+ */
16
+ import type { Context } from '@deepseek-ai/cordis';
17
+ import { type PinkSettingsDoc } from './settingsSection.js';
18
+ export declare const name = "dsh-tui-theme";
19
+ /** Configurable knobs; every key has a sane default. */
20
+ export type Config = PinkSettingsDoc;
21
+ export declare const Config: Schemastery<Config>;
22
+ /**
23
+ * Wire the pink theme plugin.
24
+ * @param ctx - Cordis context (the plugin's own activation).
25
+ * @param config - Validated plugin config (schema defaults applied).
26
+ */
27
+ export declare function apply(ctx: Context, config?: Config): void;
28
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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,CAOrC,CAAA;AAiBF;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,GAAE,MAAW,GAAG,IAAI,CAsC7D"}
@@ -0,0 +1,86 @@
1
+ /**
2
+ * dsh-tui-theme — 樱花粉主题插件(dsh-TUI 生态)。
3
+ *
4
+ * 个性化全部走既有接缝,不注册快捷键、不注册命令、不拦截任何输入:
5
+ * - 接缝四(主题):内置三套粉色 JSON(pink-night / pink-day / pink-ansi),
6
+ * 启动时复制进 ~/.dsh-tui/themes/(仅缺失时,绝不覆盖用户已有文件);
7
+ * - 自动跟随:检测终端/系统背景色(OSC 11,与宿主同阈值),在昼樱/夜樱
8
+ * 间自动切换——pink 版的 auto;
9
+ * - 接缝十一(状态行):输入框上方一行小装饰(✿ · 时钟 · 本轮轮数);
10
+ * - 接缝六(设置区块):/settings 里一个可编辑面板,即时生效。
11
+ *
12
+ * 遵循 #183 纪律:所有宿主服务用 ctx.inject 软探测,缺席即静默降级;
13
+ * 插件缺失或宿主较旧时行为退化为“什么都没发生”,绝不拖垮启动。
14
+ * @module dsh-tui-theme
15
+ */
16
+ import { join } from 'node:path';
17
+ import z from '@deepseek-ai/schemastery';
18
+ import { installBundledThemes, homeDir } from './themeAssets.js';
19
+ import { startStatusLine } from './statusLine.js';
20
+ import { runFollowSystem } from './autoTheme.js';
21
+ import { registerPinkSettings } from './settingsSection.js';
22
+ export const name = 'dsh-tui-theme';
23
+ // Explicit annotation: the inferred z.dict output references cosmokit's Dict
24
+ // through a pnpm-virtual path, which is not portable in declaration emit
25
+ // (TS2883). Mirrors the official plugin-template's workaround.
26
+ //
27
+ // statusEnabled / autoInstallThemes stay cordis-config knobs only (invisible
28
+ // in /settings): turning the garnish off entirely is what uninstalling is
29
+ // for, per user feedback. followSystem defaults off for generic installs;
30
+ // the shipped cordis.patch.yml opts this deployment in.
31
+ export const Config = z.object({
32
+ autoInstallThemes: z.boolean().default(true),
33
+ statusEnabled: z.boolean().default(true),
34
+ followSystem: z.boolean().default(false),
35
+ showGlyph: z.boolean().default(true),
36
+ showClock: z.boolean().default(true),
37
+ showTurns: z.boolean().default(true),
38
+ });
39
+ const DEFAULTS = {
40
+ autoInstallThemes: true,
41
+ statusEnabled: true,
42
+ followSystem: false,
43
+ showGlyph: true,
44
+ showClock: true,
45
+ showTurns: true,
46
+ };
47
+ /**
48
+ * Wire the pink theme plugin.
49
+ * @param ctx - Cordis context (the plugin's own activation).
50
+ * @param config - Validated plugin config (schema defaults applied).
51
+ */
52
+ export function apply(ctx, config = {}) {
53
+ // The cordis.yml layer: schema defaults plus explicit ?? fallbacks so a
54
+ // bare composition still resolves every knob.
55
+ const cordis = {
56
+ autoInstallThemes: config.autoInstallThemes ?? DEFAULTS.autoInstallThemes,
57
+ statusEnabled: config.statusEnabled ?? DEFAULTS.statusEnabled,
58
+ followSystem: config.followSystem ?? DEFAULTS.followSystem,
59
+ showGlyph: config.showGlyph ?? DEFAULTS.showGlyph,
60
+ showClock: config.showClock ?? DEFAULTS.showClock,
61
+ showTurns: config.showTurns ?? DEFAULTS.showTurns,
62
+ };
63
+ if (cordis.autoInstallThemes) {
64
+ const result = installBundledThemes();
65
+ for (const file of result.installed) {
66
+ ctx.logger.info(`dsh-tui-theme: installed bundled theme "${file}" into ~/.dsh-tui/themes/`);
67
+ }
68
+ for (const file of result.failed) {
69
+ ctx.logger.warn(`dsh-tui-theme: could not install bundled theme "${file}"`);
70
+ }
71
+ }
72
+ // Background follow runs before the React tree mounts (which reads the
73
+ // theme pref), so the cached detection decides this boot.
74
+ if (cordis.followSystem) {
75
+ runFollowSystem(join(homeDir(), '.dsh-tui'), message => {
76
+ ctx.logger.info(`dsh-tui-theme: ${message}`);
77
+ });
78
+ }
79
+ // The /settings user layer (settings.yaml) overrides the cordis layer and
80
+ // lands live through scope.watch; both override the hardcoded defaults.
81
+ let effective = cordis;
82
+ registerPinkSettings(ctx, cordis, doc => {
83
+ effective = { ...cordis, ...doc };
84
+ });
85
+ startStatusLine(ctx, () => effective);
86
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * /settings integration (seam six: tuiSettingsSections over the dsh settings
3
+ * service).
4
+ *
5
+ * Registers the `pink-theme` settings namespace and a declarative editing
6
+ * section for it. Storage, schema validation, and layered resolution stay on
7
+ * the dsh settings service; the TUI only renders. Fields carry no schema
8
+ * defaults on purpose — an unset user layer must fall through to the cordis
9
+ * config layer (mirrors the host's own lang/fullscreen fields), and format()
10
+ * displays the effective value instead of a misleading blank.
11
+ *
12
+ * Both services are consumed through `ctx.inject`, not apply-time `get`
13
+ * probes: this row may start before the host's service rows, and the inject
14
+ * fires whenever each service actually registers.
15
+ * @module dsh-tui-pink-theme/settingsSection
16
+ */
17
+ import type { Context } from '@deepseek-ai/cordis';
18
+ import type { StatusOptions } from './statusLine.js';
19
+ /** The plugin's settings document (every field optional at the user layer). */
20
+ export type PinkSettingsDoc = StatusOptions & {
21
+ /** Install bundled theme JSONs on boot (cordis-config layer only). */
22
+ autoInstallThemes?: boolean;
23
+ /** Follow the terminal/system background: pink-day ↔ pink-night. */
24
+ followSystem?: boolean;
25
+ };
26
+ /**
27
+ * Register the settings namespace (mirror the resolved document to the
28
+ * caller) and, separately, the /settings section for it. Each part waits for
29
+ * its own service; neither is required for the other.
30
+ *
31
+ * @param ctx - The plugin's own activation context.
32
+ * @param cordis - The cordis-config layer (shown as the effective value for
33
+ * still-unset fields).
34
+ * @param onDoc - Called with the defined-valued subset of the settings doc,
35
+ * initially and on every committed edit.
36
+ */
37
+ export declare function registerPinkSettings(ctx: Context, cordis: StatusOptions, onDoc: (doc: PinkSettingsDoc) => void): void;
38
+ //# sourceMappingURL=settingsSection.d.ts.map
@@ -0,0 +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,CAuCN"}
@@ -0,0 +1,111 @@
1
+ /**
2
+ * /settings integration (seam six: tuiSettingsSections over the dsh settings
3
+ * service).
4
+ *
5
+ * Registers the `pink-theme` settings namespace and a declarative editing
6
+ * section for it. Storage, schema validation, and layered resolution stay on
7
+ * the dsh settings service; the TUI only renders. Fields carry no schema
8
+ * defaults on purpose — an unset user layer must fall through to the cordis
9
+ * config layer (mirrors the host's own lang/fullscreen fields), and format()
10
+ * displays the effective value instead of a misleading blank.
11
+ *
12
+ * Both services are consumed through `ctx.inject`, not apply-time `get`
13
+ * probes: this row may start before the host's service rows, and the inject
14
+ * fires whenever each service actually registers.
15
+ * @module dsh-tui-pink-theme/settingsSection
16
+ */
17
+ import { settingsNamespace } from '@deepseek-ai/dsh-settings';
18
+ import z from '@deepseek-ai/schemastery';
19
+ /**
20
+ * Register the settings namespace (mirror the resolved document to the
21
+ * caller) and, separately, the /settings section for it. Each part waits for
22
+ * its own service; neither is required for the other.
23
+ *
24
+ * @param ctx - The plugin's own activation context.
25
+ * @param cordis - The cordis-config layer (shown as the effective value for
26
+ * still-unset fields).
27
+ * @param onDoc - Called with the defined-valued subset of the settings doc,
28
+ * initially and on every committed edit.
29
+ */
30
+ export function registerPinkSettings(ctx, cordis, onDoc) {
31
+ ctx.inject(['settings'], settingsCtx => {
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
+ }));
39
+ const emit = (doc) => {
40
+ if (doc === null || typeof doc !== 'object')
41
+ return;
42
+ const clean = {};
43
+ for (const [key, value] of Object.entries(doc)) {
44
+ if (value !== undefined)
45
+ clean[key] = value;
46
+ }
47
+ onDoc(clean);
48
+ };
49
+ emit(scope.get());
50
+ scope.watch(emit);
51
+ });
52
+ ctx.inject(['tuiSettingsSections'], sectionsCtx => {
53
+ const sections = sectionsCtx
54
+ .tuiSettingsSections;
55
+ try {
56
+ const unregister = sections.register(sectionDefinition(cordis));
57
+ sectionsCtx.effect(() => () => unregister());
58
+ }
59
+ catch (error) {
60
+ // A duplicate registration (hot reload race) or a stricter host must
61
+ // not take the plugin — or the TUI — down.
62
+ sectionsCtx.logger.warn(`dsh-tui-pink-theme: settings section registration failed: ${String(error)}`);
63
+ }
64
+ });
65
+ }
66
+ /** The declarative /settings block (labels bilingual, zh via descriptions). */
67
+ function sectionDefinition(cordis) {
68
+ return {
69
+ ns: 'dsh-tui-theme',
70
+ title: 'pink-theme',
71
+ descriptions: { zh: 'pink-theme' },
72
+ fields: [
73
+ {
74
+ path: ['followSystem'],
75
+ label: 'Follow terminal background',
76
+ descriptions: { zh: '跟随终端背景' },
77
+ hint: 'Auto-switch between Pink Day and Pink Night by terminal/system background. First enabling or a background flip applies from the next boot.',
78
+ hintDescriptions: {
79
+ zh: '按终端/系统背景色在昼樱与夜樱间自动切换。首次开启或背景刚翻转后,自下次启动生效。',
80
+ },
81
+ kind: 'boolean',
82
+ format: (value) => String(value ?? cordis.followSystem),
83
+ },
84
+ {
85
+ path: ['showGlyph'],
86
+ label: 'Blossom ✿',
87
+ descriptions: { zh: '花符 ✿' },
88
+ hint: 'Lead the decorative line above the prompt with ✿.',
89
+ hintDescriptions: { zh: '输入框上方装饰行的开头花符。' },
90
+ kind: 'boolean',
91
+ format: (value) => String(value ?? cordis.showGlyph),
92
+ },
93
+ {
94
+ path: ['showClock'],
95
+ label: 'Clock',
96
+ descriptions: { zh: '时钟' },
97
+ kind: 'boolean',
98
+ format: (value) => String(value ?? cordis.showClock),
99
+ },
100
+ {
101
+ path: ['showTurns'],
102
+ label: 'Turn count',
103
+ descriptions: { zh: '轮数' },
104
+ hint: 'Turns of the live session counted since the TUI started.',
105
+ hintDescriptions: { zh: '自本次启动以来当前会话的轮数。' },
106
+ kind: 'boolean',
107
+ format: (value) => String(value ?? cordis.showTurns),
108
+ },
109
+ ],
110
+ };
111
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Blossom status line (seam eleven: tuiStatus).
3
+ *
4
+ * One keyed contribution above the prompt: a ✿ glyph, a wall clock, and the
5
+ * live turn count of the current session (seam one, read-only — nothing is
6
+ * ever appended to the session log). The host owns rendering and
7
+ * sanitization; text is scalars only.
8
+ * @module dsh-tui-pink-theme/statusLine
9
+ */
10
+ import type { Context } from '@deepseek-ai/cordis';
11
+ export interface StatusOptions {
12
+ /** Master switch (cordis-config layer only; not surfaced in /settings). */
13
+ statusEnabled?: boolean;
14
+ /** Lead the line with the ✿ glyph. */
15
+ showGlyph?: boolean;
16
+ /** Include the HH:MM clock. */
17
+ showClock?: boolean;
18
+ /** Include the live turn count of the current session. */
19
+ showTurns?: boolean;
20
+ }
21
+ /** Fully-resolved status knobs. */
22
+ export type EffectiveStatus = Required<StatusOptions>;
23
+ /**
24
+ * Start the status line inside the `tuiStatus` inject.
25
+ *
26
+ * The inject (not a ctx.get probe) is load-bearing: this plugin's row may
27
+ * start before the dsh-tui-extensions row mounts its services, and a plain
28
+ * `get(name, false)` at apply-time silently misses that ordering. The inject
29
+ * callback fires when the service registers — now or later — and property
30
+ * access inside it carries the caller binding the host's caller checks and
31
+ * effect ledger want. Hosts that never provide the seam simply never run
32
+ * the callback: the intended degradation, not an error.
33
+ * @param ctx - The plugin's own activation context.
34
+ * @param getEffective - Current knobs (re-read on every render so /settings
35
+ * edits land live without restarting anything).
36
+ */
37
+ export declare function startStatusLine(ctx: Context, getEffective: () => EffectiveStatus): void;
38
+ //# sourceMappingURL=statusLine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"statusLine.d.ts","sourceRoot":"","sources":["../../src/statusLine.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAKlD,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;CACpB;AAED,mCAAmC;AACnC,MAAM,MAAM,eAAe,GAAG,QAAQ,CAAC,aAAa,CAAC,CAAA;AAoBrD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,GAAG,IAAI,CAoDvF"}
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Blossom status line (seam eleven: tuiStatus).
3
+ *
4
+ * One keyed contribution above the prompt: a ✿ glyph, a wall clock, and the
5
+ * live turn count of the current session (seam one, read-only — nothing is
6
+ * ever appended to the session log). The host owns rendering and
7
+ * sanitization; text is scalars only.
8
+ * @module dsh-tui-pink-theme/statusLine
9
+ */
10
+ const GLYPH = '✿';
11
+ const STATUS_KEY = 'dsh-tui-theme';
12
+ const CLOCK_TICK_MS = 15_000;
13
+ function clockText() {
14
+ // "HH:MM" from toTimeString()'s "HH:MM:SS GMT…" prefix — locale independent.
15
+ return new Date().toTimeString().slice(0, 5);
16
+ }
17
+ /**
18
+ * Start the status line inside the `tuiStatus` inject.
19
+ *
20
+ * The inject (not a ctx.get probe) is load-bearing: this plugin's row may
21
+ * start before the dsh-tui-extensions row mounts its services, and a plain
22
+ * `get(name, false)` at apply-time silently misses that ordering. The inject
23
+ * callback fires when the service registers — now or later — and property
24
+ * access inside it carries the caller binding the host's caller checks and
25
+ * effect ledger want. Hosts that never provide the seam simply never run
26
+ * the callback: the intended degradation, not an error.
27
+ * @param ctx - The plugin's own activation context.
28
+ * @param getEffective - Current knobs (re-read on every render so /settings
29
+ * edits land live without restarting anything).
30
+ */
31
+ export function startStatusLine(ctx, getEffective) {
32
+ ctx.inject(['tuiStatus'], statusCtx => {
33
+ const status = statusCtx.tuiStatus;
34
+ const turns = new Map();
35
+ let current;
36
+ let dispose;
37
+ const render = () => {
38
+ try {
39
+ const eff = getEffective();
40
+ const parts = [];
41
+ if (eff.showGlyph)
42
+ parts.push(GLYPH);
43
+ if (eff.showClock)
44
+ parts.push(clockText());
45
+ if (eff.showTurns && current !== undefined) {
46
+ parts.push(`${turns.get(current) ?? 0}✦`);
47
+ }
48
+ const text = parts.join(' · ');
49
+ // The trailing identity must be the inject-scoped context (the same
50
+ // activation the traceable binds as caller) — the plugin's outer ctx
51
+ // is a different activation view and would be silently rejected.
52
+ dispose = status.set(STATUS_KEY, text === '' ? undefined : text, statusCtx) ?? dispose;
53
+ }
54
+ catch {
55
+ // Display garnish only: a rendering hiccup must never travel upward.
56
+ }
57
+ };
58
+ ctx.on('session/event', (session, event) => {
59
+ current = session;
60
+ if (event?.type === 'turn/end') {
61
+ turns.set(session, (turns.get(session) ?? 0) + 1);
62
+ }
63
+ render();
64
+ });
65
+ ctx.on('session/disposed', session => {
66
+ turns.delete(session);
67
+ if (current === session)
68
+ current = undefined;
69
+ render();
70
+ });
71
+ const timer = setInterval(render, CLOCK_TICK_MS);
72
+ timer.unref?.();
73
+ statusCtx.effect(() => () => {
74
+ clearInterval(timer);
75
+ try {
76
+ dispose?.();
77
+ }
78
+ catch {
79
+ // The host store is already gone on teardown — nothing to clear.
80
+ }
81
+ });
82
+ render();
83
+ });
84
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Bundled-theme installation (seam four: static theme assets).
3
+ *
4
+ * Copies the package's themes/*.json into ~/.dsh-tui/themes/ on boot. Only
5
+ * files that do not exist yet are written — a user's edited or same-named
6
+ * theme file is never overwritten. Every failure is contained per file: a
7
+ * theme garnish must never break the TUI's boot.
8
+ * @module dsh-tui-pink-theme/themeAssets
9
+ */
10
+ export interface ThemeInstallResult {
11
+ /** Files newly written into the target directory. */
12
+ readonly installed: readonly string[];
13
+ /** Files already present in the target directory (left untouched). */
14
+ readonly skipped: readonly string[];
15
+ /** Files that could not be installed (per-file failures). */
16
+ readonly failed: readonly string[];
17
+ }
18
+ export declare function homeDir(): string;
19
+ /** The package's bundled themes/ directory (sibling of the built lib/). */
20
+ export declare function bundledThemesDir(): string;
21
+ /** The host's user-theme directory (~/.dsh-tui/themes). */
22
+ export declare function themesTargetDir(): string;
23
+ /**
24
+ * Install every bundled theme JSON that the target directory is missing.
25
+ * @param targetDir - Destination directory (defaults to ~/.dsh-tui/themes).
26
+ * @param sourceDir - Bundled assets (defaults to this package's themes/).
27
+ * @returns Per-file outcome. Never throws.
28
+ */
29
+ export declare function installBundledThemes(targetDir?: string, sourceDir?: string): ThemeInstallResult;
30
+ //# sourceMappingURL=themeAssets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"themeAssets.d.ts","sourceRoot":"","sources":["../../src/themeAssets.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAOH,MAAM,WAAW,kBAAkB;IACjC,qDAAqD;IACrD,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAA;IACrC,sEAAsE;IACtE,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;IACnC,6DAA6D;IAC7D,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;CACnC;AAID,wBAAgB,OAAO,IAAI,MAAM,CAEhC;AAED,2EAA2E;AAC3E,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED,2DAA2D;AAC3D,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,GAAE,MAA0B,EACrC,SAAS,GAAE,MAA2B,GACrC,kBAAkB,CA2BpB"}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Bundled-theme installation (seam four: static theme assets).
3
+ *
4
+ * Copies the package's themes/*.json into ~/.dsh-tui/themes/ on boot. Only
5
+ * files that do not exist yet are written — a user's edited or same-named
6
+ * theme file is never overwritten. Every failure is contained per file: a
7
+ * theme garnish must never break the TUI's boot.
8
+ * @module dsh-tui-pink-theme/themeAssets
9
+ */
10
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
11
+ import { homedir } from 'node:os';
12
+ import { dirname, join } from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+ // Same resolution order as the host's utils/paths.ts homeDir(): os.homedir()
15
+ // first, USERPROFILE/HOME spellings as stripped-down fallbacks.
16
+ export function homeDir() {
17
+ return homedir() || process.env.USERPROFILE || process.env.HOME || '';
18
+ }
19
+ /** The package's bundled themes/ directory (sibling of the built lib/). */
20
+ export function bundledThemesDir() {
21
+ return join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'themes');
22
+ }
23
+ /** The host's user-theme directory (~/.dsh-tui/themes). */
24
+ export function themesTargetDir() {
25
+ return join(homeDir(), '.dsh-tui', 'themes');
26
+ }
27
+ /**
28
+ * Install every bundled theme JSON that the target directory is missing.
29
+ * @param targetDir - Destination directory (defaults to ~/.dsh-tui/themes).
30
+ * @param sourceDir - Bundled assets (defaults to this package's themes/).
31
+ * @returns Per-file outcome. Never throws.
32
+ */
33
+ export function installBundledThemes(targetDir = themesTargetDir(), sourceDir = bundledThemesDir()) {
34
+ const installed = [];
35
+ const skipped = [];
36
+ const failed = [];
37
+ let files;
38
+ try {
39
+ files = readdirSync(sourceDir).filter(entry => entry.toLowerCase().endsWith('.json'));
40
+ }
41
+ catch {
42
+ return { installed, skipped, failed: [sourceDir] };
43
+ }
44
+ for (const file of files) {
45
+ try {
46
+ const target = join(targetDir, file);
47
+ if (existsSync(target)) {
48
+ skipped.push(file);
49
+ continue;
50
+ }
51
+ const text = readFileSync(join(sourceDir, file), 'utf8');
52
+ JSON.parse(text); // our own asset, but never write a corrupt file out
53
+ mkdirSync(targetDir, { recursive: true });
54
+ writeFileSync(target, text);
55
+ installed.push(file);
56
+ }
57
+ catch {
58
+ failed.push(file);
59
+ }
60
+ }
61
+ return { installed, skipped, failed };
62
+ }
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "dsh-tui-theme",
3
+ "version": "0.2.0",
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
+ "type": "module",
6
+ "main": "lib/types/index.js",
7
+ "types": "lib/types/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/types/index.d.ts",
11
+ "import": "./lib/types/index.js"
12
+ },
13
+ "./cordis.patch.yml": "./cordis.patch.yml",
14
+ "./package.json": "./package.json"
15
+ },
16
+ "files": [
17
+ "lib",
18
+ "themes",
19
+ "cordis.patch.yml"
20
+ ],
21
+ "engines": {
22
+ "node": "^22.19 || >=24"
23
+ },
24
+ "scripts": {
25
+ "build": "tsc -p tsconfig.json",
26
+ "verify": "node scripts/verify.mjs"
27
+ },
28
+ "license": "MIT",
29
+ "author": "xiaoxiaohaigui",
30
+ "keywords": [
31
+ "dsh",
32
+ "dsh-tui",
33
+ "theme",
34
+ "plugin",
35
+ "terminal",
36
+ "pink",
37
+ "deepseek-harness"
38
+ ],
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/xiaoxiaohaigui/dsh-tui-theme.git"
42
+ },
43
+ "bugs": {
44
+ "url": "https://github.com/xiaoxiaohaigui/dsh-tui-theme/issues"
45
+ },
46
+ "homepage": "https://github.com/xiaoxiaohaigui/dsh-tui-theme#readme",
47
+ "dsh": {
48
+ "bundle": {
49
+ "patch": "./cordis.patch.yml"
50
+ }
51
+ },
52
+ "dependencies": {
53
+ "@deepseek-ai/schemastery": "^3.18.1"
54
+ },
55
+ "peerDependencies": {
56
+ "@deepseek-ai/cordis": "^4.0.1",
57
+ "@deepseek-ai/dsh-session": "^0.1.0-rc.6 || ^0.1.1-rc.1",
58
+ "@deepseek-ai/dsh-settings": "^0.1.0-rc.6 || ^0.1.1-rc.1"
59
+ },
60
+ "devDependencies": {
61
+ "@deepseek-ai/cordis": "^4.0.1",
62
+ "@deepseek-ai/dsh-session": "^0.1.0-rc.6 || ^0.1.1-rc.1",
63
+ "@deepseek-ai/dsh-settings": "^0.1.0-rc.6 || ^0.1.1-rc.1",
64
+ "@types/node": "^22.0.0",
65
+ "typescript": "^6.0.3"
66
+ }
67
+ }
@@ -0,0 +1,102 @@
1
+ {
2
+ "name": "pink-ansi",
3
+ "displayName": "樱 · Pink ANSI",
4
+ "base": "dark-ansi",
5
+ "colors": {
6
+ "autoAccept": "ansi:magentaBright",
7
+ "bashBorder": "ansi:magentaBright",
8
+ "claude": "ansi:magentaBright",
9
+ "toolNameMutate": "ansi:yellowBright",
10
+ "toolNameExec": "ansi:cyanBright",
11
+ "claudeShimmer": "ansi:magentaBright",
12
+ "claudeBlue_FOR_SYSTEM_SPINNER": "ansi:magentaBright",
13
+ "claudeBlueShimmer_FOR_SYSTEM_SPINNER": "ansi:magentaBright",
14
+ "permission": "ansi:magentaBright",
15
+ "permissionShimmer": "ansi:magentaBright",
16
+ "planMode": "ansi:cyanBright",
17
+ "ide": "ansi:magenta",
18
+ "promptBorder": "ansi:white",
19
+ "promptBorderShimmer": "ansi:whiteBright",
20
+ "text": "ansi:whiteBright",
21
+ "inverseText": "ansi:black",
22
+ "inactive": "ansi:white",
23
+ "inactiveShimmer": "ansi:whiteBright",
24
+ "subtle": "ansi:white",
25
+ "suggestion": "ansi:magentaBright",
26
+ "remember": "ansi:magentaBright",
27
+ "background": "ansi:magentaBright",
28
+ "success": "ansi:greenBright",
29
+ "error": "ansi:redBright",
30
+ "warning": "ansi:yellowBright",
31
+ "merged": "ansi:magentaBright",
32
+ "warningShimmer": "ansi:yellowBright",
33
+ "diffAdded": "ansi:green",
34
+ "diffRemoved": "ansi:red",
35
+ "diffAddedDimmed": "ansi:green",
36
+ "diffRemovedDimmed": "ansi:red",
37
+ "diffAddedWord": "ansi:greenBright",
38
+ "diffRemovedWord": "ansi:redBright",
39
+ "toolCardBackground": "ansi:blackBright",
40
+ "toolCardBackgroundDim": "ansi:black",
41
+ "toolDotExec": "ansi:greenBright",
42
+ "toolDotRead": "ansi:cyanBright",
43
+ "toolDotWrite": "ansi:magentaBright",
44
+ "toolDotWeb": "ansi:magenta",
45
+ "toolDotTask": "ansi:redBright",
46
+ "syntaxKeyword": "ansi:magentaBright",
47
+ "syntaxString": "ansi:greenBright",
48
+ "syntaxComment": "ansi:blackBright",
49
+ "syntaxNumber": "ansi:yellowBright",
50
+ "syntaxFunction": "ansi:cyanBright",
51
+ "syntaxType": "ansi:blueBright",
52
+ "syntaxVariable": "ansi:white",
53
+ "syntaxOperator": "ansi:white",
54
+ "syntaxPunctuation": "ansi:blackBright",
55
+ "syntaxConstant": "ansi:redBright",
56
+ "red_FOR_SUBAGENTS_ONLY": "ansi:redBright",
57
+ "blue_FOR_SUBAGENTS_ONLY": "ansi:blueBright",
58
+ "green_FOR_SUBAGENTS_ONLY": "ansi:greenBright",
59
+ "yellow_FOR_SUBAGENTS_ONLY": "ansi:yellowBright",
60
+ "purple_FOR_SUBAGENTS_ONLY": "ansi:magentaBright",
61
+ "orange_FOR_SUBAGENTS_ONLY": "ansi:redBright",
62
+ "pink_FOR_SUBAGENTS_ONLY": "ansi:magentaBright",
63
+ "cyan_FOR_SUBAGENTS_ONLY": "ansi:cyanBright",
64
+ "professionalBlue": "ansi:magentaBright",
65
+ "chromeYellow": "ansi:yellowBright",
66
+ "clawd_body": "ansi:magentaBright",
67
+ "clawd_background": "ansi:black",
68
+ "userMessageBackgroundHover": "ansi:magenta",
69
+ "messageActionsBackground": "ansi:blackBright",
70
+ "selectionBg": "ansi:magenta",
71
+ "bashMessageBackgroundColor": "ansi:black",
72
+ "memoryBackgroundColor": "ansi:blackBright",
73
+ "rate_limit_fill": "ansi:magentaBright",
74
+ "rate_limit_empty": "ansi:white",
75
+ "fastMode": "ansi:redBright",
76
+ "fastModeShimmer": "ansi:redBright",
77
+ "briefLabelYou": "ansi:yellowBright",
78
+ "briefLabelClaude": "ansi:magentaBright",
79
+ "rainbow_red": "ansi:red",
80
+ "rainbow_orange": "ansi:redBright",
81
+ "rainbow_yellow": "ansi:yellow",
82
+ "rainbow_green": "ansi:green",
83
+ "rainbow_blue": "ansi:cyan",
84
+ "rainbow_indigo": "ansi:blue",
85
+ "rainbow_violet": "ansi:magenta",
86
+ "rainbow_red_shimmer": "ansi:redBright",
87
+ "rainbow_orange_shimmer": "ansi:yellow",
88
+ "rainbow_yellow_shimmer": "ansi:yellowBright",
89
+ "rainbow_green_shimmer": "ansi:greenBright",
90
+ "rainbow_blue_shimmer": "ansi:cyanBright",
91
+ "rainbow_indigo_shimmer": "ansi:blueBright",
92
+ "rainbow_violet_shimmer": "ansi:magentaBright",
93
+ "subagentBullet": "ansi:magentaBright",
94
+ "subagentDescription": "ansi:whiteBright",
95
+ "subagentModel": "ansi:white",
96
+ "subagentElapsed": "ansi:white",
97
+ "subagentToolName": "ansi:cyanBright",
98
+ "subagentStatusRunning": "ansi:magentaBright",
99
+ "subagentStatusCompleted": "ansi:greenBright",
100
+ "subagentStatusFailed": "ansi:redBright"
101
+ }
102
+ }
@@ -0,0 +1,102 @@
1
+ {
2
+ "name": "pink-day",
3
+ "displayName": "昼樱 Pink Day",
4
+ "base": "light",
5
+ "colors": {
6
+ "autoAccept": "#8F6BAC",
7
+ "bashBorder": "#C05C7E",
8
+ "claude": "#D5517F",
9
+ "toolNameMutate": "#8A6A00",
10
+ "toolNameExec": "#0E7A8A",
11
+ "claudeShimmer": "#E879A0",
12
+ "claudeBlue_FOR_SYSTEM_SPINNER": "#D5517F",
13
+ "claudeBlueShimmer_FOR_SYSTEM_SPINNER": "#E879A0",
14
+ "permission": "#D5517F",
15
+ "permissionShimmer": "#E879A0",
16
+ "planMode": "#43916B",
17
+ "ide": "#C43D6C",
18
+ "promptBorder": "#EFC4D4",
19
+ "promptBorderShimmer": "#D988A9",
20
+ "text": "rgb(61,43,51)",
21
+ "inverseText": "#FBF3F0",
22
+ "inactive": "#9A8790",
23
+ "inactiveShimmer": "#9E6E82",
24
+ "subtle": "#B3A3AC",
25
+ "suggestion": "#D5517F",
26
+ "remember": "#B03A63",
27
+ "background": "#D5517F",
28
+ "success": "#3E9468",
29
+ "error": "#C24D5C",
30
+ "warning": "#B97F2E",
31
+ "merged": "#8F6BAC",
32
+ "warningShimmer": "#CE9840",
33
+ "diffAdded": "#DFF0E2",
34
+ "diffRemoved": "#F7E0E4",
35
+ "diffAddedDimmed": "#E8F3EA",
36
+ "diffRemovedDimmed": "#F9E9EC",
37
+ "diffAddedWord": "#8CCFA4",
38
+ "diffRemovedWord": "#E6A3B0",
39
+ "toolCardBackground": "#F9ECF1",
40
+ "toolCardBackgroundDim": "#F2E0E9",
41
+ "toolDotExec": "#4E7A5A",
42
+ "toolDotRead": "#3F7E8F",
43
+ "toolDotWrite": "#7A5CA8",
44
+ "toolDotWeb": "#C24D78",
45
+ "toolDotTask": "#B04A5A",
46
+ "syntaxKeyword": "#C24470",
47
+ "syntaxString": "#3F805F",
48
+ "syntaxComment": "#8B7F88",
49
+ "syntaxNumber": "#A6602B",
50
+ "syntaxFunction": "#2E7E8A",
51
+ "syntaxType": "#7E55A4",
52
+ "syntaxVariable": "#3D2B33",
53
+ "syntaxOperator": "#5F5260",
54
+ "syntaxPunctuation": "#A3959E",
55
+ "syntaxConstant": "#A84472",
56
+ "red_FOR_SUBAGENTS_ONLY": "#BE5A52",
57
+ "blue_FOR_SUBAGENTS_ONLY": "#4A6BB8",
58
+ "green_FOR_SUBAGENTS_ONLY": "#4E9675",
59
+ "yellow_FOR_SUBAGENTS_ONLY": "#B98A34",
60
+ "purple_FOR_SUBAGENTS_ONLY": "#9678B4",
61
+ "orange_FOR_SUBAGENTS_ONLY": "#C97F4A",
62
+ "pink_FOR_SUBAGENTS_ONLY": "#C24D78",
63
+ "cyan_FOR_SUBAGENTS_ONLY": "#4E95A0",
64
+ "professionalBlue": "#CE5A80",
65
+ "chromeYellow": "#C99A3F",
66
+ "clawd_body": "#E879A0",
67
+ "clawd_background": "#FBF3F0",
68
+ "userMessageBackgroundHover": "#F0DCE6",
69
+ "messageActionsBackground": "#F3E4EB",
70
+ "selectionBg": "#F3D7E0",
71
+ "bashMessageBackgroundColor": "#F5EAE0",
72
+ "memoryBackgroundColor": "#F3E4EB",
73
+ "rate_limit_fill": "#D988A9",
74
+ "rate_limit_empty": "#E8DAD2",
75
+ "fastMode": "#D98E4A",
76
+ "fastModeShimmer": "#E2A465",
77
+ "briefLabelYou": "#A67600",
78
+ "briefLabelClaude": "#D5517F",
79
+ "rainbow_red": "#D07F7A",
80
+ "rainbow_orange": "#D09C6E",
81
+ "rainbow_yellow": "#C9B25C",
82
+ "rainbow_green": "#7FB08C",
83
+ "rainbow_blue": "#7E96C9",
84
+ "rainbow_indigo": "#948CC2",
85
+ "rainbow_violet": "#B586B0",
86
+ "rainbow_red_shimmer": "#DFA4A0",
87
+ "rainbow_orange_shimmer": "#DFBCA8",
88
+ "rainbow_yellow_shimmer": "#DCCB92",
89
+ "rainbow_green_shimmer": "#A3CBB0",
90
+ "rainbow_blue_shimmer": "#AEBFE0",
91
+ "rainbow_indigo_shimmer": "#B4ACDA",
92
+ "rainbow_violet_shimmer": "#CBAEC7",
93
+ "subagentBullet": "#C05C7E",
94
+ "subagentDescription": "#3D2B33",
95
+ "subagentModel": "#9A8790",
96
+ "subagentElapsed": "#9A8790",
97
+ "subagentToolName": "#D5517F",
98
+ "subagentStatusRunning": "#D5517F",
99
+ "subagentStatusCompleted": "#3E9468",
100
+ "subagentStatusFailed": "#C24D5C"
101
+ }
102
+ }
@@ -0,0 +1,102 @@
1
+ {
2
+ "name": "pink-night",
3
+ "displayName": "夜樱 Pink Night",
4
+ "base": "dark",
5
+ "colors": {
6
+ "autoAccept": "#B598D9",
7
+ "bashBorder": "#E08CA8",
8
+ "claude": "#F27BA6",
9
+ "toolNameMutate": "#E5C07B",
10
+ "toolNameExec": "#72C4CF",
11
+ "claudeShimmer": "#F8AFC6",
12
+ "claudeBlue_FOR_SYSTEM_SPINNER": "#F27BA6",
13
+ "claudeBlueShimmer_FOR_SYSTEM_SPINNER": "#F8AFC6",
14
+ "permission": "#F2A0BF",
15
+ "permissionShimmer": "#F8C1D6",
16
+ "planMode": "#7FB596",
17
+ "ide": "#E0709A",
18
+ "promptBorder": "#6E4A58",
19
+ "promptBorderShimmer": "#E08CA8",
20
+ "text": "#F0E4E9",
21
+ "inverseText": "#2B1E25",
22
+ "inactive": "#A8929C",
23
+ "inactiveShimmer": "#C4B0B9",
24
+ "subtle": "#77646D",
25
+ "suggestion": "#F2A0BF",
26
+ "remember": "#F2A0BF",
27
+ "background": "#D9648F",
28
+ "success": "#8FC9A4",
29
+ "error": "#E66A6A",
30
+ "warning": "#E3B56C",
31
+ "merged": "#B598D9",
32
+ "warningShimmer": "#F0CB8E",
33
+ "diffAdded": "#2E3A2F",
34
+ "diffRemoved": "#402A31",
35
+ "diffAddedDimmed": "#2F3630",
36
+ "diffRemovedDimmed": "#392830",
37
+ "diffAddedWord": "#5E9B72",
38
+ "diffRemovedWord": "#C96B7B",
39
+ "toolCardBackground": "#2E2229",
40
+ "toolCardBackgroundDim": "#241A20",
41
+ "toolDotExec": "#7FB596",
42
+ "toolDotRead": "#7FB6C9",
43
+ "toolDotWrite": "#C29BDF",
44
+ "toolDotWeb": "#F27BA6",
45
+ "toolDotTask": "#E0A0AE",
46
+ "syntaxKeyword": "#E08CA8",
47
+ "syntaxString": "#8FC9A4",
48
+ "syntaxComment": "#857481",
49
+ "syntaxNumber": "#E3B56C",
50
+ "syntaxFunction": "#7FC4CE",
51
+ "syntaxType": "#C29BDF",
52
+ "syntaxVariable": "#DDD2D8",
53
+ "syntaxOperator": "#A8929C",
54
+ "syntaxPunctuation": "#8A7680",
55
+ "syntaxConstant": "#E0709A",
56
+ "red_FOR_SUBAGENTS_ONLY": "#D9756C",
57
+ "blue_FOR_SUBAGENTS_ONLY": "#8FA6DA",
58
+ "green_FOR_SUBAGENTS_ONLY": "#6FBB8E",
59
+ "yellow_FOR_SUBAGENTS_ONLY": "#D9B25E",
60
+ "purple_FOR_SUBAGENTS_ONLY": "#B598D9",
61
+ "orange_FOR_SUBAGENTS_ONLY": "#DE9464",
62
+ "pink_FOR_SUBAGENTS_ONLY": "#F2A0C0",
63
+ "cyan_FOR_SUBAGENTS_ONLY": "#6FB8BE",
64
+ "professionalBlue": "#E08CA8",
65
+ "chromeYellow": "#E3B56C",
66
+ "clawd_body": "#F2A0C0",
67
+ "clawd_background": "#000000",
68
+ "userMessageBackgroundHover": "#7E3A55",
69
+ "messageActionsBackground": "#362A31",
70
+ "selectionBg": "#55303E",
71
+ "bashMessageBackgroundColor": "#2E242A",
72
+ "memoryBackgroundColor": "#322630",
73
+ "rate_limit_fill": "#F27BA6",
74
+ "rate_limit_empty": "#43333B",
75
+ "fastMode": "#E09A58",
76
+ "fastModeShimmer": "#ECB478",
77
+ "briefLabelYou": "#FFDF80",
78
+ "briefLabelClaude": "#F27BA6",
79
+ "rainbow_red": "#DE9490",
80
+ "rainbow_orange": "#DEAC84",
81
+ "rainbow_yellow": "#DCC28A",
82
+ "rainbow_green": "#9CC3AA",
83
+ "rainbow_blue": "#A3AEDB",
84
+ "rainbow_indigo": "#B3A7DC",
85
+ "rainbow_violet": "#CFA3C8",
86
+ "rainbow_red_shimmer": "#EBADAA",
87
+ "rainbow_orange_shimmer": "#EBC3A8",
88
+ "rainbow_yellow_shimmer": "#E9D3A8",
89
+ "rainbow_green_shimmer": "#B4D2C0",
90
+ "rainbow_blue_shimmer": "#BCC6E6",
91
+ "rainbow_indigo_shimmer": "#C6BDE9",
92
+ "rainbow_violet_shimmer": "#DCBDD8",
93
+ "subagentBullet": "#E0A0BE",
94
+ "subagentDescription": "#F0E4E9",
95
+ "subagentModel": "#A8929C",
96
+ "subagentElapsed": "#A8929C",
97
+ "subagentToolName": "#F27BA6",
98
+ "subagentStatusRunning": "#F27BA6",
99
+ "subagentStatusCompleted": "#8FC9A4",
100
+ "subagentStatusFailed": "#E66A6A"
101
+ }
102
+ }