dsh-tui-theme 0.5.0 → 0.6.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 +25 -6
- package/lib/types/autoTheme.d.ts +14 -1
- package/lib/types/autoTheme.d.ts.map +1 -1
- package/lib/types/autoTheme.js +10 -5
- package/lib/types/index.d.ts +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +83 -8
- package/lib/types/runtimeThemes.d.ts +9 -0
- package/lib/types/runtimeThemes.d.ts.map +1 -0
- package/lib/types/runtimeThemes.js +39 -0
- package/lib/types/settingsSection.d.ts.map +1 -1
- package/lib/types/settingsSection.js +5 -2
- package/lib/types/themeAssets.d.ts +23 -0
- package/lib/types/themeAssets.d.ts.map +1 -1
- package/lib/types/themeAssets.js +126 -12
- package/lib/types/toast.d.ts +40 -0
- package/lib/types/toast.d.ts.map +1 -0
- package/lib/types/toast.js +91 -0
- package/package.json +49 -7
- package/scripts/headless-order-test.mjs +23 -11
- package/scripts/runtime-themes-headless.mjs +180 -0
- package/scripts/validate-themes-against-host.mjs +22 -14
- package/scripts/verify-package.mjs +14 -1
- package/scripts/verify.mjs +314 -18
package/README.md
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# dsh-tui-theme 🌸
|
|
2
2
|
|
|
3
|
-
[dsh-TUI](https://github.com/ccch1mneyyy/dsh-TUI)
|
|
3
|
+
[dsh-TUI](https://github.com/ccch1mneyyy/dsh-TUI) 的樱花粉主题插件。一个包带来五种个性化,全部走官方接缝:
|
|
4
4
|
|
|
5
5
|
| 个性化 | 接缝 | 说明 |
|
|
6
6
|
| --- | --- | --- |
|
|
7
|
-
| **三套粉色主题** |
|
|
7
|
+
| **三套粉色主题** | 主题运行时(dsh-TUI ≥ 0.10.0)/ 静态资产(旧宿主) | `pink-night` 夜樱 / `pink-day` 昼樱 / `pink-ansi` 樱·ANSI;新宿主优先即时注册,服务晚到时短暂回退并清理本次文件,旧宿主使用 `~/.dsh-tui/themes/` |
|
|
8
8
|
| **缓存背景跟随** | 设置 + 本地缓存 | 可选地应用已有 `theme-follow.json` 的昼樱/夜樱结果;不直接读取终端输入或发送 OSC 查询 |
|
|
9
9
|
| **花符状态行** | `tuiStatus` | 输入框上方一行小装饰:✿ · 时钟 · 实时轮数(默认仅粉主题下显示) |
|
|
10
10
|
| **设置面板** | `tuiSettingsSections` | `/settings` 里一个可编辑区块,改完即时生效 |
|
|
11
|
+
| **屏幕提示** | `tuiToast`(dsh-TUI ≥ 0.10.0) | 背景跟随结果、主题文件自愈、旧文件遮蔽提醒各弹一行短提示;旧宿主自动静默降级 |
|
|
11
12
|
|
|
12
13
|
**明确不做的事**:不注册快捷键、不注册/修改任何命令、不拦截输入、不追加会话事件、不注入 system prompt。卸载即无痕(可选删除主题文件)。
|
|
13
14
|
|
|
@@ -47,6 +48,22 @@
|
|
|
47
48
|
- 开启后该缓存会在启动时覆盖 `/theme` 的持久选择;关闭即可恢复手动选择;
|
|
48
49
|
- `DSH_TUI_THEME` 环境变量仍然最优先(宿主行为,插件不覆盖环境变量)。
|
|
49
50
|
|
|
51
|
+
dsh-TUI ≥ 0.10.0 上,跟随结果会以 toast 短提示呈现:真正改写了 `theme.json` 时提示「已按保存的终端背景改用 …,/reload 即时生效」(当前画面仍显示旧主题,`/reload` 或重启后生效);在 `/settings` 里手动开启但无缓存时如实提示「没有保存的终端背景缓存」。启动基线未变化时保持安静,避免每次开机重复打扰。
|
|
52
|
+
|
|
53
|
+
## 屏幕提示(toast)
|
|
54
|
+
|
|
55
|
+
插件日志对 TUI 用户不可见,因此少数值得知道的事件会走宿主 toast(dsh-TUI ≥ 0.10.0 的 `tuiToast` 接缝,纯输出、不拦截任何输入;旧宿主没有该接缝,自动退回纯日志行为):
|
|
56
|
+
|
|
57
|
+
| 事件 | 提示 |
|
|
58
|
+
| --- | --- |
|
|
59
|
+
| 背景跟随改写了主题偏好 | ✿ 已按保存的终端背景改用 …,/reload 即时生效 |
|
|
60
|
+
| 手动开启跟随且缓存与当前一致 | ✿ 已按保存的终端背景保持 … |
|
|
61
|
+
| 手动开启跟随但无缓存 | ✿ 没有保存的终端背景缓存,保持当前主题(警告色) |
|
|
62
|
+
| 主题文件损坏被自动修复 | ✿ 已修复损坏的主题文件:…(警告色) |
|
|
63
|
+
| 旧版遗留的同名主题文件与内置版本完全相同 | ✿ … 与插件内置相同,删除后配色将随插件自动更新 |
|
|
64
|
+
|
|
65
|
+
最后一条只针对与内置副本逐字节相同的文件——你手动调过色的文件永远不会被提及或删除。
|
|
66
|
+
|
|
50
67
|
## 安装
|
|
51
68
|
|
|
52
69
|
```sh
|
|
@@ -69,9 +86,11 @@ dsh plugin --profile dsh-tui add -w ./dsh-tui-theme-<版本号>.tgz
|
|
|
69
86
|
dsh plugin --profile dsh-tui add -w dsh-tui-theme@latest
|
|
70
87
|
```
|
|
71
88
|
|
|
72
|
-
|
|
89
|
+
在 dsh-TUI ≥ 0.10.0 中,三套主题通过 `ctx.tuiThemes` 运行时注册:调色板随插件即时生效,正常挂载时不写入用户目录,主题选择器显示中文 `displayName`。若服务晚到,插件会先同步回退到静态路径,并在确认运行时服务后删除本次写入且仍未被改写的文件。旧宿主使用静态文件路径:插件只在主题文件缺失时复制,绝不覆盖你编辑过的 `~/.dsh-tui/themes/pink-*.json`。唯一的例外是已损坏的目标文件(无法解析为 JSON,例如安装中途中断留下的残文件):插件会把它改名为 `<文件名>.corrupt-<时间戳>` 保留现场,再重新安装内置副本,记录一条警告,并在支持 toast 的宿主上弹一条屏幕提示。
|
|
90
|
+
|
|
91
|
+
从旧宿主升级后,若希望改用运行时托管,请先备份并删除 `~/.dsh-tui/themes/pink-{night,day,ansi}.json`;插件不会自动删除用户文件。遗留文件若与内置副本完全相同,插件会在运行时托管确认时弹一条一次性提醒,指出哪些文件正在遮蔽配色更新。
|
|
73
92
|
|
|
74
|
-
|
|
93
|
+
旧宿主重启 dsh-TUI 后插件会把三套主题复制进 `~/.dsh-tui/themes/`(**仅缺失时复制,绝不覆盖你已有的同名文件**);0.10.0 及更新宿主则直接使用运行时注册,然后:
|
|
75
94
|
|
|
76
95
|
```sh
|
|
77
96
|
# 在 dsh-TUI 里
|
|
@@ -135,11 +154,11 @@ DSH_TUI_SOURCE_ROOT=/path/to/dsh-TUI-source \
|
|
|
135
154
|
npm run verify:host
|
|
136
155
|
```
|
|
137
156
|
|
|
138
|
-
`verify:host`
|
|
157
|
+
`verify:host` 默认使用开发依赖中的 dsh-TUI(当前为 0.10.0-beta.4)进行零配置验证;需要验证旧版或发布基线时,再显式指向同一版本的宿主 adapter 与源码。需要锁定版本时,额外设置 `DSH_TUI_EXPECTED_VERSION`。
|
|
139
158
|
|
|
140
159
|
主题调色板改起来最直接:编辑 `themes/*.json` 后重新 `npm run verify`,再删掉 `~/.dsh-tui/themes/` 下对应文件让插件重装。
|
|
141
160
|
|
|
142
161
|
## 兼容性
|
|
143
162
|
|
|
144
|
-
- **dsh-TUI 版本下限:0.8.8**(状态行与设置面板;0.9.3
|
|
163
|
+
- **dsh-TUI 版本下限:0.8.8**(状态行与设置面板;0.9.3 实测)。0.10.0 及更新版本使用运行时主题注册;更旧的宿主缺 `dsh-tui-extensions` 扩展面时,插件自动降级为“仅安装三套主题”,不报错。
|
|
145
164
|
- Node `^22.19 || >=24`,纯 ESM,MIT。
|
package/lib/types/autoTheme.d.ts
CHANGED
|
@@ -29,11 +29,24 @@ export declare function themeForBackground(light: boolean): string;
|
|
|
29
29
|
* preference committed successfully.
|
|
30
30
|
*/
|
|
31
31
|
export declare function applyCachedFollow(dataDir: string): string | undefined;
|
|
32
|
+
/** What one follow application actually did (log and toast both derive from it). */
|
|
33
|
+
export type FollowOutcome = {
|
|
34
|
+
kind: 'applied';
|
|
35
|
+
theme: string;
|
|
36
|
+
changed: boolean;
|
|
37
|
+
} | {
|
|
38
|
+
kind: 'unavailable';
|
|
39
|
+
};
|
|
32
40
|
/**
|
|
33
41
|
* Apply a previously detected background without touching terminal I/O. A
|
|
34
42
|
* future host-owned terminal query service may refresh theme-follow.json; this
|
|
35
43
|
* plugin intentionally does not access stdin, stdout, or raw mode directly.
|
|
44
|
+
*
|
|
45
|
+
* The structured outcome lets the entry point keep one source of truth for
|
|
46
|
+
* the log line and the user-visible toast: `changed` distinguishes a real
|
|
47
|
+
* preference write (the live TUI still shows the previous palette until
|
|
48
|
+
* /reload or a restart) from a no-op confirmation.
|
|
36
49
|
*/
|
|
37
|
-
export declare function runFollowSystem(dataDir: string, isCurrent: () => boolean,
|
|
50
|
+
export declare function runFollowSystem(dataDir: string, isCurrent: () => boolean, report: (outcome: FollowOutcome) => void): void;
|
|
38
51
|
export {};
|
|
39
52
|
//# sourceMappingURL=autoTheme.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"autoTheme.d.ts","sourceRoot":"","sources":["../../src/autoTheme.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AASH,UAAU,WAAW;IACnB,KAAK,EAAE,OAAO,CAAA;IACd,EAAE,EAAE,MAAM,CAAA;CACX;AAsCD,+DAA+D;AAC/D,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGjE;AAED,yDAAyD;AACzD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAErE;AAED,sFAAsF;AACtF,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAGxE;AAED,yDAAyD;AACzD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEzD;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAMrE;AAED
|
|
1
|
+
{"version":3,"file":"autoTheme.d.ts","sourceRoot":"","sources":["../../src/autoTheme.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AASH,UAAU,WAAW;IACnB,KAAK,EAAE,OAAO,CAAA;IACd,EAAE,EAAE,MAAM,CAAA;CACX;AAsCD,+DAA+D;AAC/D,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGjE;AAED,yDAAyD;AACzD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAErE;AAED,sFAAsF;AACtF,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAGxE;AAED,yDAAyD;AACzD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEzD;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAMrE;AAED,oFAAoF;AACpF,MAAM,MAAM,aAAa,GACrB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GACpD;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,CAAA;AAE3B;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,OAAO,EACxB,MAAM,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,GACvC,IAAI,CAUN"}
|
package/lib/types/autoTheme.js
CHANGED
|
@@ -84,17 +84,22 @@ export function applyCachedFollow(dataDir) {
|
|
|
84
84
|
* Apply a previously detected background without touching terminal I/O. A
|
|
85
85
|
* future host-owned terminal query service may refresh theme-follow.json; this
|
|
86
86
|
* plugin intentionally does not access stdin, stdout, or raw mode directly.
|
|
87
|
+
*
|
|
88
|
+
* The structured outcome lets the entry point keep one source of truth for
|
|
89
|
+
* the log line and the user-visible toast: `changed` distinguishes a real
|
|
90
|
+
* preference write (the live TUI still shows the previous palette until
|
|
91
|
+
* /reload or a restart) from a no-op confirmation.
|
|
87
92
|
*/
|
|
88
|
-
export function runFollowSystem(dataDir, isCurrent,
|
|
93
|
+
export function runFollowSystem(dataDir, isCurrent, report) {
|
|
89
94
|
if (!isCurrent())
|
|
90
95
|
return;
|
|
96
|
+
const before = readThemePref(dataDir);
|
|
91
97
|
const applied = applyCachedFollow(dataDir);
|
|
92
98
|
if (!isCurrent())
|
|
93
99
|
return;
|
|
94
100
|
if (applied === undefined) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
else {
|
|
98
|
-
log(`follow: applied cached background (${applied})`);
|
|
101
|
+
report({ kind: 'unavailable' });
|
|
102
|
+
return;
|
|
99
103
|
}
|
|
104
|
+
report({ kind: 'applied', theme: applied, changed: before !== applied });
|
|
100
105
|
}
|
package/lib/types/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 个性化全部走既有接缝,不注册快捷键、不注册命令、不拦截任何输入:
|
|
5
5
|
* - 接缝四(主题):内置三套粉色 JSON(pink-night / pink-day / pink-ansi),
|
|
6
|
-
*
|
|
6
|
+
* 新宿主通过运行时服务注册,旧宿主同步复制进 ~/.dsh-tui/themes/(仅缺失时);
|
|
7
7
|
* - 缓存背景跟随:安全应用已有的终端背景缓存,在昼樱/夜樱间切换;
|
|
8
8
|
* dsh-TUI 未提供插件终端查询接缝时,不直接读写 stdin、raw mode 或 OSC 11;
|
|
9
9
|
* - 接缝十一(状态行):输入框上方一行小装饰(✿ · 时钟 · 本轮轮数);
|
package/lib/types/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAMlD,OAAO,EAAwB,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAIjF,eAAO,MAAM,IAAI,kBAAY,CAAA;AAE7B,wDAAwD;AACxD,MAAM,MAAM,MAAM,GAAG,eAAe,CAAA;AAUpC,eAAO,MAAM,MAAM,EAAE,WAAW,CAAC,MAAM,CAQrC,CAAA;AAkBF;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,GAAE,MAAW,GAAG,IAAI,CA+I7D"}
|
package/lib/types/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 个性化全部走既有接缝,不注册快捷键、不注册命令、不拦截任何输入:
|
|
5
5
|
* - 接缝四(主题):内置三套粉色 JSON(pink-night / pink-day / pink-ansi),
|
|
6
|
-
*
|
|
6
|
+
* 新宿主通过运行时服务注册,旧宿主同步复制进 ~/.dsh-tui/themes/(仅缺失时);
|
|
7
7
|
* - 缓存背景跟随:安全应用已有的终端背景缓存,在昼樱/夜樱间切换;
|
|
8
8
|
* dsh-TUI 未提供插件终端查询接缝时,不直接读写 stdin、raw mode 或 OSC 11;
|
|
9
9
|
* - 接缝十一(状态行):输入框上方一行小装饰(✿ · 时钟 · 本轮轮数);
|
|
@@ -15,10 +15,12 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { join } from 'node:path';
|
|
17
17
|
import z from '@deepseek-ai/schemastery';
|
|
18
|
-
import { installBundledThemes, homeDir } from './themeAssets.js';
|
|
18
|
+
import { installBundledThemes, homeDir, removeBundledThemes, findShadowedBundledThemes } from './themeAssets.js';
|
|
19
|
+
import { startRuntimeThemes } from './runtimeThemes.js';
|
|
19
20
|
import { startStatusLine } from './statusLine.js';
|
|
20
21
|
import { runFollowSystem } from './autoTheme.js';
|
|
21
22
|
import { registerPinkSettings } from './settingsSection.js';
|
|
23
|
+
import { startToastRelay } from './toast.js';
|
|
22
24
|
import { PLUGIN_ID } from './pluginId.js';
|
|
23
25
|
export const name = PLUGIN_ID;
|
|
24
26
|
// Explicit annotation: the inferred z.dict output references cosmokit's Dict
|
|
@@ -64,7 +66,21 @@ export function apply(ctx, config = {}) {
|
|
|
64
66
|
showTurns: config.showTurns ?? DEFAULTS.showTurns,
|
|
65
67
|
statusScope: config.statusScope ?? DEFAULTS.statusScope,
|
|
66
68
|
};
|
|
67
|
-
|
|
69
|
+
// User-visible one-liners for the few events worth surfacing (the plugin
|
|
70
|
+
// logger is invisible to a TUI user). Hosts without the 0.10 toast seam
|
|
71
|
+
// degrade to the log-only behavior of previous releases.
|
|
72
|
+
const sendToast = startToastRelay(ctx);
|
|
73
|
+
// New hosts own the palette in memory. Install synchronously first so an old
|
|
74
|
+
// host can resolve a persisted theme during its first render. If a runtime
|
|
75
|
+
// service appears afterwards, remove only files written by this activation
|
|
76
|
+
// before registering the in-memory palettes.
|
|
77
|
+
let runtimeConfirmed = false;
|
|
78
|
+
const ownedStaticFiles = new Set();
|
|
79
|
+
const installStatic = () => {
|
|
80
|
+
if (runtimeConfirmed)
|
|
81
|
+
return;
|
|
82
|
+
if (!cordis.autoInstallThemes)
|
|
83
|
+
return;
|
|
68
84
|
const result = installBundledThemes();
|
|
69
85
|
for (const file of result.installed) {
|
|
70
86
|
ctx.logger.info(`${PLUGIN_ID}: installed bundled theme "${file}" into ~/.dsh-tui/themes/`);
|
|
@@ -75,6 +91,46 @@ export function apply(ctx, config = {}) {
|
|
|
75
91
|
for (const file of result.failed) {
|
|
76
92
|
ctx.logger.warn(`${PLUGIN_ID}: could not install bundled theme "${file}"`);
|
|
77
93
|
}
|
|
94
|
+
for (const file of [...result.installed, ...result.repaired])
|
|
95
|
+
ownedStaticFiles.add(file);
|
|
96
|
+
if (result.repaired.length > 0) {
|
|
97
|
+
sendToast(`✿ 已修复损坏的主题文件:${result.repaired.join('、')}`, 'warning');
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
const markRuntimeAvailable = () => {
|
|
101
|
+
const firstConfirmation = !runtimeConfirmed;
|
|
102
|
+
runtimeConfirmed = true;
|
|
103
|
+
if (ownedStaticFiles.size > 0) {
|
|
104
|
+
removeBundledThemes([...ownedStaticFiles]);
|
|
105
|
+
ownedStaticFiles.clear();
|
|
106
|
+
}
|
|
107
|
+
// Same-named static files win over runtime registrations. User-edited
|
|
108
|
+
// files stay unmentioned; files byte-identical to the bundled copy add
|
|
109
|
+
// nothing but permanently block palette updates, so point them out once.
|
|
110
|
+
if (!firstConfirmation)
|
|
111
|
+
return;
|
|
112
|
+
const shadowed = findShadowedBundledThemes();
|
|
113
|
+
if (shadowed.length > 0) {
|
|
114
|
+
ctx.logger.info(`${PLUGIN_ID}: legacy static files shadow the runtime registry: ${shadowed.join(', ')}`);
|
|
115
|
+
sendToast(`✿ ${shadowed.join('、')} 与插件内置相同,删除后配色将随插件自动更新`);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
let runtimePresent = false;
|
|
119
|
+
try {
|
|
120
|
+
runtimePresent = ctx.get('tuiThemes', false) !== undefined;
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
runtimePresent = false;
|
|
124
|
+
}
|
|
125
|
+
if (runtimePresent) {
|
|
126
|
+
startRuntimeThemes(ctx, markRuntimeAvailable);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
installStatic();
|
|
130
|
+
ctx.effect(() => () => {
|
|
131
|
+
ownedStaticFiles.clear();
|
|
132
|
+
});
|
|
133
|
+
startRuntimeThemes(ctx, markRuntimeAvailable);
|
|
78
134
|
}
|
|
79
135
|
// The /settings user layer (settings.yaml) overrides the cordis layer and
|
|
80
136
|
// lands live through scope.watch; both override the hardcoded defaults.
|
|
@@ -85,9 +141,28 @@ export function apply(ctx, config = {}) {
|
|
|
85
141
|
// whether the cached result may control this startup.
|
|
86
142
|
let followActive;
|
|
87
143
|
const dataDir = join(homeDir(), '.dsh-tui');
|
|
88
|
-
const applyFollow = () => {
|
|
89
|
-
runFollowSystem(dataDir, () => followActive === true,
|
|
90
|
-
|
|
144
|
+
const applyFollow = (userInitiated) => {
|
|
145
|
+
runFollowSystem(dataDir, () => followActive === true, outcome => {
|
|
146
|
+
if (outcome.kind === 'unavailable') {
|
|
147
|
+
ctx.logger.info(`${PLUGIN_ID}: follow: cached background unavailable or preference write failed; keeping current choice`);
|
|
148
|
+
// Startup baselines stay quiet: the follow feature has no cache
|
|
149
|
+
// producer on modern hosts, and an identical warning every boot
|
|
150
|
+
// would be noise, not feedback. An explicit toggle-on deserves the
|
|
151
|
+
// honest answer that nothing was applied.
|
|
152
|
+
if (userInitiated) {
|
|
153
|
+
sendToast('✿ 没有保存的终端背景缓存,保持当前主题', 'warning');
|
|
154
|
+
}
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
ctx.logger.info(`${PLUGIN_ID}: follow: applied cached background (${outcome.theme})`);
|
|
158
|
+
if (outcome.changed) {
|
|
159
|
+
// A real pref write: the live TUI resolved its palette from the
|
|
160
|
+
// previous pref, so the new choice needs /reload or a restart.
|
|
161
|
+
sendToast(`✿ 已按保存的终端背景改用 ${outcome.theme},/reload 即时生效`, 'success');
|
|
162
|
+
}
|
|
163
|
+
else if (userInitiated) {
|
|
164
|
+
sendToast(`✿ 已按保存的终端背景保持 ${outcome.theme}`, 'success');
|
|
165
|
+
}
|
|
91
166
|
});
|
|
92
167
|
};
|
|
93
168
|
registerPinkSettings(ctx, cordis, doc => {
|
|
@@ -99,13 +174,13 @@ export function apply(ctx, config = {}) {
|
|
|
99
174
|
// still applies the cache immediately.
|
|
100
175
|
followActive = follow;
|
|
101
176
|
if (follow)
|
|
102
|
-
applyFollow();
|
|
177
|
+
applyFollow(false);
|
|
103
178
|
return;
|
|
104
179
|
}
|
|
105
180
|
if (followActive !== follow) {
|
|
106
181
|
followActive = follow;
|
|
107
182
|
if (follow) {
|
|
108
|
-
applyFollow();
|
|
183
|
+
applyFollow(true);
|
|
109
184
|
}
|
|
110
185
|
else {
|
|
111
186
|
ctx.logger.info(`${PLUGIN_ID}: follow: disabled, manual /theme choice preserved`);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Runtime theme registration for dsh-TUI 0.10.0 and newer. */
|
|
2
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
3
|
+
/**
|
|
4
|
+
* Register every bundled palette through the host runtime seam. The optional
|
|
5
|
+
* callback is used by the entry point to remove its legacy static fallback as
|
|
6
|
+
* soon as the service becomes available.
|
|
7
|
+
*/
|
|
8
|
+
export declare function startRuntimeThemes(ctx: Context, onAvailable?: () => void): void;
|
|
9
|
+
//# sourceMappingURL=runtimeThemes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtimeThemes.d.ts","sourceRoot":"","sources":["../../src/runtimeThemes.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAE/D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAQlD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,EAAE,WAAW,CAAC,EAAE,MAAM,IAAI,GAAG,IAAI,CA+B/E"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Runtime theme registration for dsh-TUI 0.10.0 and newer. */
|
|
2
|
+
import { readBundledThemes } from './themeAssets.js';
|
|
3
|
+
/**
|
|
4
|
+
* Register every bundled palette through the host runtime seam. The optional
|
|
5
|
+
* callback is used by the entry point to remove its legacy static fallback as
|
|
6
|
+
* soon as the service becomes available.
|
|
7
|
+
*/
|
|
8
|
+
export function startRuntimeThemes(ctx, onAvailable) {
|
|
9
|
+
ctx.inject(['tuiThemes'], themesCtx => {
|
|
10
|
+
onAvailable?.();
|
|
11
|
+
const themes = themesCtx.tuiThemes;
|
|
12
|
+
const disposers = [];
|
|
13
|
+
for (const bundled of readBundledThemes()) {
|
|
14
|
+
try {
|
|
15
|
+
const dispose = themes.register({
|
|
16
|
+
name: bundled.name,
|
|
17
|
+
...(bundled.displayName === undefined ? {} : { displayName: bundled.displayName }),
|
|
18
|
+
base: bundled.base,
|
|
19
|
+
colors: bundled.colors,
|
|
20
|
+
}, themesCtx);
|
|
21
|
+
if (typeof dispose === 'function')
|
|
22
|
+
disposers.push(dispose);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
themesCtx.logger.warn(`dsh-tui-theme: runtime theme registration failed for "${bundled.name}": ${String(error)}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
themesCtx.effect(() => () => {
|
|
29
|
+
for (const dispose of disposers.splice(0)) {
|
|
30
|
+
try {
|
|
31
|
+
dispose();
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// A host teardown may already have removed the registry.
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -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;
|
|
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;AAIlD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAGpD,+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,CAwDN"}
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
* fires whenever each service actually registers.
|
|
15
15
|
* @module dsh-tui-theme/settingsSection
|
|
16
16
|
*/
|
|
17
|
-
import { settingsNamespace } from '@deepseek-ai/dsh-settings';
|
|
18
17
|
import z from '@deepseek-ai/schemastery';
|
|
19
18
|
import { PLUGIN_ID } from './pluginId.js';
|
|
20
19
|
/**
|
|
@@ -32,7 +31,11 @@ export function registerPinkSettings(ctx, cordis, onDoc) {
|
|
|
32
31
|
ctx.inject(['settings'], settingsCtx => {
|
|
33
32
|
const settings = settingsCtx.settings;
|
|
34
33
|
try {
|
|
35
|
-
|
|
34
|
+
// dsh-settings validates the raw namespace at registration time. Keep
|
|
35
|
+
// this as a plain string so alpha.2 hosts, which removed the runtime
|
|
36
|
+
// settingsNamespace() helper, can load the plugin without a missing
|
|
37
|
+
// named export while older hosts retain the same behavior.
|
|
38
|
+
const scope = settings.register(PLUGIN_ID, z.object({
|
|
36
39
|
followSystem: z.boolean(),
|
|
37
40
|
showGlyph: z.boolean(),
|
|
38
41
|
showClock: z.boolean(),
|
|
@@ -20,11 +20,20 @@ export interface ThemeInstallResult {
|
|
|
20
20
|
/** Files that could not be installed (per-file failures). */
|
|
21
21
|
readonly failed: readonly string[];
|
|
22
22
|
}
|
|
23
|
+
export interface BundledTheme {
|
|
24
|
+
readonly file: string;
|
|
25
|
+
readonly name: string;
|
|
26
|
+
readonly displayName?: string;
|
|
27
|
+
readonly base: 'light' | 'dark' | 'dark-ansi';
|
|
28
|
+
readonly colors: Record<string, string>;
|
|
29
|
+
}
|
|
23
30
|
export declare function homeDir(): string;
|
|
24
31
|
/** The package's bundled themes/ directory (sibling of the built lib/). */
|
|
25
32
|
export declare function bundledThemesDir(): string;
|
|
26
33
|
/** The host's user-theme directory (~/.dsh-tui/themes). */
|
|
27
34
|
export declare function themesTargetDir(): string;
|
|
35
|
+
/** Read bundled descriptors for hosts that support runtime theme registration. */
|
|
36
|
+
export declare function readBundledThemes(sourceDir?: string): BundledTheme[];
|
|
28
37
|
/**
|
|
29
38
|
* Install every bundled theme JSON that the target directory is missing.
|
|
30
39
|
* @param targetDir - Destination directory (defaults to ~/.dsh-tui/themes).
|
|
@@ -32,4 +41,18 @@ export declare function themesTargetDir(): string;
|
|
|
32
41
|
* @returns Per-file outcome. Never throws.
|
|
33
42
|
*/
|
|
34
43
|
export declare function installBundledThemes(targetDir?: string, sourceDir?: string): ThemeInstallResult;
|
|
44
|
+
/**
|
|
45
|
+
* Remove files that this activation installed when a runtime theme service
|
|
46
|
+
* becomes available. A byte-for-byte check preserves edits made in the small
|
|
47
|
+
* window between installation and runtime confirmation.
|
|
48
|
+
*/
|
|
49
|
+
export declare function removeBundledThemes(files: readonly string[], targetDir?: string, sourceDir?: string): string[];
|
|
50
|
+
/**
|
|
51
|
+
* Bundled theme files present in the target directory byte-for-byte
|
|
52
|
+
* identical to the bundled copy. On runtime-theme hosts such files shadow
|
|
53
|
+
* the registry while adding nothing, so the entry point can point them out
|
|
54
|
+
* once. User-edited or foreign same-named files are never reported — the
|
|
55
|
+
* never-overwrite rule keeps them, and no toast may nag about them.
|
|
56
|
+
*/
|
|
57
|
+
export declare function findShadowedBundledThemes(targetDir?: string, sourceDir?: string): string[];
|
|
35
58
|
//# sourceMappingURL=themeAssets.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"themeAssets.d.ts","sourceRoot":"","sources":["../../src/themeAssets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;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,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;IACpC,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,
|
|
1
|
+
{"version":3,"file":"themeAssets.d.ts","sourceRoot":"","sources":["../../src/themeAssets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;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,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;IACpC,6DAA6D;IAC7D,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;CACnC;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,WAAW,CAAA;IAC7C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACxC;AAID,wBAAgB,OAAO,IAAI,MAAM,CAEhC;AAED,2EAA2E;AAC3E,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED,2DAA2D;AAC3D,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED,kFAAkF;AAClF,wBAAgB,iBAAiB,CAAC,SAAS,GAAE,MAA2B,GAAG,YAAY,EAAE,CA0CxF;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,GAAE,MAA0B,EACrC,SAAS,GAAE,MAA2B,GACrC,kBAAkB,CAmCpB;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,SAAS,GAAE,MAA0B,EACrC,SAAS,GAAE,MAA2B,GACrC,MAAM,EAAE,CAuBV;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,GAAE,MAA0B,EACrC,SAAS,GAAE,MAA2B,GACrC,MAAM,EAAE,CAkBV"}
|
package/lib/types/themeAssets.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* contained per file: a theme garnish must never break the TUI's boot.
|
|
11
11
|
* @module dsh-tui-theme/themeAssets
|
|
12
12
|
*/
|
|
13
|
-
import { mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { mkdirSync, readFileSync, readdirSync, renameSync, rmdirSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
14
14
|
import { homedir } from 'node:os';
|
|
15
15
|
import { dirname, join } from 'node:path';
|
|
16
16
|
import { fileURLToPath } from 'node:url';
|
|
@@ -27,6 +27,51 @@ export function bundledThemesDir() {
|
|
|
27
27
|
export function themesTargetDir() {
|
|
28
28
|
return join(homeDir(), '.dsh-tui', 'themes');
|
|
29
29
|
}
|
|
30
|
+
/** Read bundled descriptors for hosts that support runtime theme registration. */
|
|
31
|
+
export function readBundledThemes(sourceDir = bundledThemesDir()) {
|
|
32
|
+
let files;
|
|
33
|
+
try {
|
|
34
|
+
files = readdirSync(sourceDir).filter(entry => entry.toLowerCase().endsWith('.json'));
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
console.warn(`dsh-tui-theme: could not read bundled themes from ${sourceDir}: ${String(error)}`);
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
const themes = [];
|
|
41
|
+
for (const file of files) {
|
|
42
|
+
try {
|
|
43
|
+
const value = JSON.parse(readFileSync(join(sourceDir, file), 'utf8'));
|
|
44
|
+
const name = value.name;
|
|
45
|
+
const base = value.base;
|
|
46
|
+
const colors = value.colors;
|
|
47
|
+
if (typeof name !== 'string' ||
|
|
48
|
+
!/^[a-z][a-z0-9_-]*(?::[a-z][a-z0-9_-]*)?$/u.test(name) ||
|
|
49
|
+
(base !== 'light' && base !== 'dark' && base !== 'dark-ansi') ||
|
|
50
|
+
colors === null ||
|
|
51
|
+
typeof colors !== 'object' ||
|
|
52
|
+
Array.isArray(colors)) {
|
|
53
|
+
throw new Error('invalid theme descriptor');
|
|
54
|
+
}
|
|
55
|
+
const normalizedColors = {};
|
|
56
|
+
for (const [key, color] of Object.entries(colors)) {
|
|
57
|
+
if (typeof color !== 'string')
|
|
58
|
+
throw new Error(`invalid color for ${key}`);
|
|
59
|
+
normalizedColors[key] = color;
|
|
60
|
+
}
|
|
61
|
+
themes.push({
|
|
62
|
+
file,
|
|
63
|
+
name,
|
|
64
|
+
...(typeof value.displayName === 'string' ? { displayName: value.displayName } : {}),
|
|
65
|
+
base,
|
|
66
|
+
colors: normalizedColors,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
console.warn(`dsh-tui-theme: skipping bundled theme ${file}: ${String(error)}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return themes;
|
|
74
|
+
}
|
|
30
75
|
/**
|
|
31
76
|
* Install every bundled theme JSON that the target directory is missing.
|
|
32
77
|
* @param targetDir - Destination directory (defaults to ~/.dsh-tui/themes).
|
|
@@ -57,8 +102,11 @@ export function installBundledThemes(targetDir = themesTargetDir(), sourceDir =
|
|
|
57
102
|
}
|
|
58
103
|
catch (error) {
|
|
59
104
|
if (error.code === 'EEXIST') {
|
|
60
|
-
|
|
105
|
+
const healed = healCorruptTarget(target, text);
|
|
106
|
+
if (healed === 'repaired')
|
|
61
107
|
repaired.push(file);
|
|
108
|
+
else if (healed === 'failed')
|
|
109
|
+
failed.push(file);
|
|
62
110
|
else
|
|
63
111
|
skipped.push(file);
|
|
64
112
|
}
|
|
@@ -73,14 +121,73 @@ export function installBundledThemes(targetDir = themesTargetDir(), sourceDir =
|
|
|
73
121
|
}
|
|
74
122
|
return { installed, skipped, repaired, failed };
|
|
75
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* Remove files that this activation installed when a runtime theme service
|
|
126
|
+
* becomes available. A byte-for-byte check preserves edits made in the small
|
|
127
|
+
* window between installation and runtime confirmation.
|
|
128
|
+
*/
|
|
129
|
+
export function removeBundledThemes(files, targetDir = themesTargetDir(), sourceDir = bundledThemesDir()) {
|
|
130
|
+
const removed = [];
|
|
131
|
+
for (const file of files) {
|
|
132
|
+
try {
|
|
133
|
+
const bundled = readFileSync(join(sourceDir, file), 'utf8');
|
|
134
|
+
const target = join(targetDir, file);
|
|
135
|
+
if (readFileSync(target, 'utf8') !== bundled)
|
|
136
|
+
continue;
|
|
137
|
+
unlinkSync(target);
|
|
138
|
+
removed.push(file);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// Runtime registration must remain best-effort if a file disappears or
|
|
142
|
+
// the target directory becomes unavailable during teardown.
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (removed.length > 0) {
|
|
146
|
+
try {
|
|
147
|
+
if (readdirSync(targetDir).length === 0)
|
|
148
|
+
rmdirSync(targetDir);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// An empty directory is only a cosmetic artifact; leave it in place if
|
|
152
|
+
// it was removed concurrently or is no longer empty.
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return removed;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Bundled theme files present in the target directory byte-for-byte
|
|
159
|
+
* identical to the bundled copy. On runtime-theme hosts such files shadow
|
|
160
|
+
* the registry while adding nothing, so the entry point can point them out
|
|
161
|
+
* once. User-edited or foreign same-named files are never reported — the
|
|
162
|
+
* never-overwrite rule keeps them, and no toast may nag about them.
|
|
163
|
+
*/
|
|
164
|
+
export function findShadowedBundledThemes(targetDir = themesTargetDir(), sourceDir = bundledThemesDir()) {
|
|
165
|
+
const shadowed = [];
|
|
166
|
+
let files;
|
|
167
|
+
try {
|
|
168
|
+
files = readdirSync(sourceDir).filter(entry => entry.toLowerCase().endsWith('.json'));
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return shadowed;
|
|
172
|
+
}
|
|
173
|
+
for (const file of files) {
|
|
174
|
+
try {
|
|
175
|
+
if (readFileSync(join(targetDir, file), 'utf8') === readFileSync(join(sourceDir, file), 'utf8')) {
|
|
176
|
+
shadowed.push(file);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
// Missing or unreadable target: not a shadow.
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return shadowed;
|
|
184
|
+
}
|
|
76
185
|
/**
|
|
77
186
|
* Self-heal an existing target that fails to parse as JSON — the leftover of
|
|
78
187
|
* a torn write from an interrupted installation. The damaged file is kept as
|
|
79
|
-
* <target>.corrupt-<timestamp> and the bundled copy installed
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
* corrupt), or when the backup/replace itself fails (degrade to the plain
|
|
83
|
-
* silent skip).
|
|
188
|
+
* <target>.corrupt-<timestamp>-<pid>-<random> and the bundled copy installed
|
|
189
|
+
* fresh. Returns `skipped` when the target is valid JSON or unreadable, and
|
|
190
|
+
* `failed` when the corrupt target was moved but replacement could not finish.
|
|
84
191
|
*/
|
|
85
192
|
function healCorruptTarget(target, text) {
|
|
86
193
|
let existing;
|
|
@@ -88,21 +195,28 @@ function healCorruptTarget(target, text) {
|
|
|
88
195
|
existing = readFileSync(target, 'utf8');
|
|
89
196
|
}
|
|
90
197
|
catch {
|
|
91
|
-
return
|
|
198
|
+
return 'skipped';
|
|
92
199
|
}
|
|
93
200
|
try {
|
|
94
201
|
JSON.parse(existing);
|
|
95
|
-
return
|
|
202
|
+
return 'skipped';
|
|
96
203
|
}
|
|
97
204
|
catch {
|
|
98
205
|
// Proven corrupt: fall through to backup and reinstall.
|
|
99
206
|
}
|
|
207
|
+
let backup;
|
|
208
|
+
try {
|
|
209
|
+
backup = `${target}.corrupt-${Date.now()}-${process.pid}-${Math.random().toString(16).slice(2)}`;
|
|
210
|
+
renameSync(target, backup);
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return 'skipped';
|
|
214
|
+
}
|
|
100
215
|
try {
|
|
101
|
-
renameSync(target, `${target}.corrupt-${Date.now()}`);
|
|
102
216
|
writeFileSync(target, text, { flag: 'wx' });
|
|
103
|
-
return
|
|
217
|
+
return 'repaired';
|
|
104
218
|
}
|
|
105
219
|
catch {
|
|
106
|
-
return
|
|
220
|
+
return 'failed';
|
|
107
221
|
}
|
|
108
222
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Toast relay for the plugin-facing transient-notification seam
|
|
3
|
+
* (dsh-TUI 0.10: `ctx.tuiToast`).
|
|
4
|
+
*
|
|
5
|
+
* The plugin's own logger output is invisible to a TUI user, so the few
|
|
6
|
+
* events worth surfacing — a follow-system pref write, a repaired theme
|
|
7
|
+
* file, a shadowing legacy static install — go through the host's toast
|
|
8
|
+
* surface instead. Sending is fire-and-forget: hosts without the seam
|
|
9
|
+
* (dsh-TUI < 0.10) never fire the inject and every send is a silent no-op,
|
|
10
|
+
* exactly like the other soft-probed seams.
|
|
11
|
+
*
|
|
12
|
+
* The host registers its toast sink late (the dsh-tui row applies after the
|
|
13
|
+
* extensions row, and both may trail this plugin), so a toast dropped for
|
|
14
|
+
* having no sink is retried on a short schedule before giving up. The
|
|
15
|
+
* tuiToast service itself arrives with the extensions row, so a send fired
|
|
16
|
+
* before the seam even exists waits on the same schedule. Retries are
|
|
17
|
+
* bounded, unref'd, and cleared with the activation; a toast lost to a host
|
|
18
|
+
* rate limit is not worth fighting — the next real event re-reports.
|
|
19
|
+
* @module dsh-tui-theme/toast
|
|
20
|
+
*/
|
|
21
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
22
|
+
/** Host toast color vocabulary (neutral when omitted). */
|
|
23
|
+
export type ToastColor = 'success' | 'warning' | 'error';
|
|
24
|
+
/** Send one transient notification; resolves to whether it was delivered. */
|
|
25
|
+
export type ToastSend = (text: string, color?: ToastColor) => boolean;
|
|
26
|
+
/**
|
|
27
|
+
* @internal Shrink the retry schedule for hermetic tests (verify.mjs only;
|
|
28
|
+
* not part of the plugin's behavioral contract).
|
|
29
|
+
*/
|
|
30
|
+
export declare function setToastRetryDelaysForTests(delays: readonly number[]): void;
|
|
31
|
+
/**
|
|
32
|
+
* Start the toast relay. Returns a sender that is callable immediately and
|
|
33
|
+
* from any later context (settings watches, timers): `show()` binds through
|
|
34
|
+
* the service's own activation, so unlike `tuiStatus.set` it takes no
|
|
35
|
+
* identity argument — but the service object itself must still be read
|
|
36
|
+
* inside the inject (same rule as every other seam).
|
|
37
|
+
* @param ctx - The plugin's own activation context.
|
|
38
|
+
*/
|
|
39
|
+
export declare function startToastRelay(ctx: Context): ToastSend;
|
|
40
|
+
//# sourceMappingURL=toast.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"toast.d.ts","sourceRoot":"","sources":["../../src/toast.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAElD,0DAA0D;AAC1D,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAA;AAUxD,6EAA6E;AAC7E,MAAM,MAAM,SAAS,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,UAAU,KAAK,OAAO,CAAA;AAMrE;;;GAGG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAE3E;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,SAAS,CAoDvD"}
|