dsh-tui-theme 0.7.0 → 0.7.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 +13 -2
- package/docs/decisions/2026-09-26-settings-generation-adaptation.md +61 -0
- package/lib/types/autoTheme.d.ts +3 -0
- package/lib/types/autoTheme.d.ts.map +1 -1
- package/lib/types/index.d.ts +3 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +70 -39
- package/lib/types/liveConfig.d.ts +88 -0
- package/lib/types/liveConfig.d.ts.map +1 -0
- package/lib/types/liveConfig.js +151 -0
- package/lib/types/settingsSection.d.ts +73 -15
- package/lib/types/settingsSection.d.ts.map +1 -1
- package/lib/types/settingsSection.js +214 -52
- package/lib/types/statusLine.d.ts.map +1 -1
- package/lib/types/statusLine.js +17 -13
- package/lib/types/themeAssets.d.ts +4 -0
- package/lib/types/themeAssets.d.ts.map +1 -1
- package/lib/types/themeAssets.js +8 -3
- package/package.json +5 -4
- package/scripts/runtime-themes-headless.mjs +75 -0
- package/scripts/validate-themes-against-host.mjs +7 -0
- package/scripts/verify-package.mjs +13 -7
- package/scripts/verify-settings-generation.mjs +306 -0
- package/scripts/verify.mjs +1573 -1264
|
@@ -2,12 +2,34 @@
|
|
|
2
2
|
* /settings integration (seam six: tuiSettingsSections over the dsh settings
|
|
3
3
|
* service).
|
|
4
4
|
*
|
|
5
|
-
* Registers the
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
5
|
+
* Registers a declarative editing section for the plugin's settings and, on
|
|
6
|
+
* hosts that still have the registration API, the settings namespace itself.
|
|
7
|
+
* Storage, schema validation, and layered resolution stay on the dsh settings
|
|
8
|
+
* service; the TUI only renders. Fields carry no schema defaults on purpose —
|
|
9
|
+
* an unset user layer must fall through to the cordis config layer (mirrors
|
|
10
|
+
* the host's own lang/fullscreen fields), and format() displays the effective
|
|
11
|
+
* value instead of a misleading blank.
|
|
12
|
+
*
|
|
13
|
+
* Two generations, branched by capability (never by version),
|
|
14
|
+
* `dsh-settings`'s 0.1.7 break being the reason
|
|
15
|
+
* (docs/decisions/2026-09-24-settings-generation-adaptation.md):
|
|
16
|
+
*
|
|
17
|
+
* - **≤0.1.6** (dsh-TUI 0.9.x/0.10.x) — `settings.register(ns, schema)` owns
|
|
18
|
+
* the namespace. The card's paths resolve against the scope schema declared
|
|
19
|
+
* here, and `scope.watch` pushes edits into the plugin.
|
|
20
|
+
* - **≥0.1.7** (dsh-TUI 0.11+) — there is no registration API at all
|
|
21
|
+
* (`SettingsForms` projects each profile entry's *Config* instead). The
|
|
22
|
+
* namespace is this plugin's profile entry id (see
|
|
23
|
+
* {@link resolveSettingsNamespace}) and the form schema is `Config` filtered
|
|
24
|
+
* to its volatile fields (liveConfig.ts). Edits land in the profile patch,
|
|
25
|
+
* the loader rewrites those same refs in place, and
|
|
26
|
+
* `loader/volatile-update` announces it; this module re-reads the live config
|
|
27
|
+
* on that event. The card stays ours, so the auto-generated page is turned
|
|
28
|
+
* off with `configure({ auto: false })`.
|
|
29
|
+
*
|
|
30
|
+
* A miss in either direction is silent and user-visible only as a card that
|
|
31
|
+
* renders but never saves (≤0.1.6) or a `命名空间未注册` badge (≥0.1.7) — hence
|
|
32
|
+
* the branch below warns instead of swallowing the failure.
|
|
11
33
|
*
|
|
12
34
|
* The section splits into two navigation groups (背景跟随 / 状态行). The two
|
|
13
35
|
* text fields (glyph, separator) validate their drafts with parse(): an
|
|
@@ -26,18 +48,54 @@ export type PinkSettingsDoc = StatusOptions & {
|
|
|
26
48
|
/** Apply a cached terminal background: pink-day <-> pink-night. */
|
|
27
49
|
followSystem?: boolean;
|
|
28
50
|
};
|
|
51
|
+
/** The settings namespace this plugin owns by default, and the fallback when
|
|
52
|
+
* the Loader entry id is unusable. On ≥0.1.7 hosts the *entry id* is the
|
|
53
|
+
* namespace the host keys by, so the effective value is
|
|
54
|
+
* {@link resolveSettingsNamespace}: a default install lands on exactly this
|
|
55
|
+
* string (`cordis.patch.yml` pins `id: dsh-tui-theme`), and it stays the name
|
|
56
|
+
* the README documents for settings storage. */
|
|
57
|
+
export declare const SETTINGS_NS = "dsh-tui-theme";
|
|
58
|
+
/**
|
|
59
|
+
* The namespace this plugin's settings live under: the Loader entry id when it
|
|
60
|
+
* satisfies the section grammar, {@link SETTINGS_NS} otherwise.
|
|
61
|
+
*
|
|
62
|
+
* A `dsh-settings` ≥0.1.7 host keys namespaces by the Loader entry id
|
|
63
|
+
* (`SettingsForms.describe()` → `entry.options.id`), so keying the card off
|
|
64
|
+
* anything else breaks the moment the row is renamed — the fragility dsh-TUI
|
|
65
|
+
* #990 records, where even the host's own section had to stop hard-coding its
|
|
66
|
+
* name. Both generations resolve to the same string here, so the card, the
|
|
67
|
+
* legacy registration and the host's projection cannot disagree.
|
|
68
|
+
*/
|
|
69
|
+
export declare function resolveSettingsNamespace(ctx: Context): string;
|
|
70
|
+
/** Everything the wiring needs from the plugin's own activation. */
|
|
71
|
+
export interface PinkSettingsWiring {
|
|
72
|
+
/** The cordis-config layer (the effective value format() falls back to for
|
|
73
|
+
* still-unset fields). */
|
|
74
|
+
readonly cordis: StatusOptions & {
|
|
75
|
+
followSystem?: boolean;
|
|
76
|
+
};
|
|
77
|
+
/** Current plain values of the plugin's own row config. On ≥0.1.7 hosts the
|
|
78
|
+
* loader rewrites the live refs in place, so calling this again after
|
|
79
|
+
* `loader/volatile-update` yields the edited values. */
|
|
80
|
+
readLive(): PinkSettingsDoc;
|
|
81
|
+
/** Whether the row-config schema carries the live marker
|
|
82
|
+
* (`hasLiveConfigFields(Config)`); diagnostics only. */
|
|
83
|
+
readonly hasLiveFields: boolean;
|
|
84
|
+
/** Receives the defined-valued subset of the settings doc — the initial one
|
|
85
|
+
* and every later edit. */
|
|
86
|
+
onDoc(doc: PinkSettingsDoc): void;
|
|
87
|
+
}
|
|
29
88
|
/**
|
|
30
|
-
* Register the settings
|
|
31
|
-
*
|
|
32
|
-
*
|
|
89
|
+
* Register the /settings section and, on hosts that still have the
|
|
90
|
+
* registration API, the settings namespace behind it. Each part waits for its
|
|
91
|
+
* own service; neither is required for the other.
|
|
33
92
|
*
|
|
34
|
-
* @param ctx - The plugin's own activation context
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* @param
|
|
38
|
-
* initially and on every committed edit.
|
|
93
|
+
* @param ctx - The plugin's own activation context: the Config owner the
|
|
94
|
+
* ≥0.1.7 page policy and the volatile-update listener must attach to (their
|
|
95
|
+
* disposers ride the inject child, which is what a service reload recycles).
|
|
96
|
+
* @param wiring - The value source and sink for the card.
|
|
39
97
|
* @param dataDir - The host data directory (~/.dsh-tui), read by the
|
|
40
98
|
* followSystem field's format() to surface the cached follow state.
|
|
41
99
|
*/
|
|
42
|
-
export declare function registerPinkSettings(ctx: Context,
|
|
100
|
+
export declare function registerPinkSettings(ctx: Context, wiring: PinkSettingsWiring, dataDir?: string): void;
|
|
43
101
|
//# sourceMappingURL=settingsSection.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settingsSection.d.ts","sourceRoot":"","sources":["../../src/settingsSection.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"settingsSection.d.ts","sourceRoot":"","sources":["../../src/settingsSection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAMlD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAIpD,+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;AAkBD;;;;;iDAKiD;AACjD,eAAO,MAAM,WAAW,kBAAY,CAAA;AAcpC;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAG7D;AAED,oEAAoE;AACpE,MAAM,WAAW,kBAAkB;IACjC;+BAC2B;IAC3B,QAAQ,CAAC,MAAM,EAAE,aAAa,GAAG;QAAE,YAAY,CAAC,EAAE,OAAO,CAAA;KAAE,CAAA;IAC3D;;6DAEyD;IACzD,QAAQ,IAAI,eAAe,CAAA;IAC3B;6DACyD;IACzD,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAA;IAC/B;gCAC4B;IAC5B,KAAK,CAAC,GAAG,EAAE,eAAe,GAAG,IAAI,CAAA;CAClC;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,kBAAkB,EAC1B,OAAO,GAAE,MAA0B,GAClC,IAAI,CAqCN"}
|
|
@@ -2,12 +2,34 @@
|
|
|
2
2
|
* /settings integration (seam six: tuiSettingsSections over the dsh settings
|
|
3
3
|
* service).
|
|
4
4
|
*
|
|
5
|
-
* Registers the
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
5
|
+
* Registers a declarative editing section for the plugin's settings and, on
|
|
6
|
+
* hosts that still have the registration API, the settings namespace itself.
|
|
7
|
+
* Storage, schema validation, and layered resolution stay on the dsh settings
|
|
8
|
+
* service; the TUI only renders. Fields carry no schema defaults on purpose —
|
|
9
|
+
* an unset user layer must fall through to the cordis config layer (mirrors
|
|
10
|
+
* the host's own lang/fullscreen fields), and format() displays the effective
|
|
11
|
+
* value instead of a misleading blank.
|
|
12
|
+
*
|
|
13
|
+
* Two generations, branched by capability (never by version),
|
|
14
|
+
* `dsh-settings`'s 0.1.7 break being the reason
|
|
15
|
+
* (docs/decisions/2026-09-24-settings-generation-adaptation.md):
|
|
16
|
+
*
|
|
17
|
+
* - **≤0.1.6** (dsh-TUI 0.9.x/0.10.x) — `settings.register(ns, schema)` owns
|
|
18
|
+
* the namespace. The card's paths resolve against the scope schema declared
|
|
19
|
+
* here, and `scope.watch` pushes edits into the plugin.
|
|
20
|
+
* - **≥0.1.7** (dsh-TUI 0.11+) — there is no registration API at all
|
|
21
|
+
* (`SettingsForms` projects each profile entry's *Config* instead). The
|
|
22
|
+
* namespace is this plugin's profile entry id (see
|
|
23
|
+
* {@link resolveSettingsNamespace}) and the form schema is `Config` filtered
|
|
24
|
+
* to its volatile fields (liveConfig.ts). Edits land in the profile patch,
|
|
25
|
+
* the loader rewrites those same refs in place, and
|
|
26
|
+
* `loader/volatile-update` announces it; this module re-reads the live config
|
|
27
|
+
* on that event. The card stays ours, so the auto-generated page is turned
|
|
28
|
+
* off with `configure({ auto: false })`.
|
|
29
|
+
*
|
|
30
|
+
* A miss in either direction is silent and user-visible only as a card that
|
|
31
|
+
* renders but never saves (≤0.1.6) or a `命名空间未注册` badge (≥0.1.7) — hence
|
|
32
|
+
* the branch below warns instead of swallowing the failure.
|
|
11
33
|
*
|
|
12
34
|
* The section splits into two navigation groups (背景跟随 / 状态行). The two
|
|
13
35
|
* text fields (glyph, separator) validate their drafts with parse(): an
|
|
@@ -23,64 +45,74 @@ import { readFollowCache } from './autoTheme.js';
|
|
|
23
45
|
import { parseOrnamentDraft } from './ornament.js';
|
|
24
46
|
import { PLUGIN_ID } from './pluginId.js';
|
|
25
47
|
import { homeDir } from './themeAssets.js';
|
|
48
|
+
/** The settings namespace this plugin owns by default, and the fallback when
|
|
49
|
+
* the Loader entry id is unusable. On ≥0.1.7 hosts the *entry id* is the
|
|
50
|
+
* namespace the host keys by, so the effective value is
|
|
51
|
+
* {@link resolveSettingsNamespace}: a default install lands on exactly this
|
|
52
|
+
* string (`cordis.patch.yml` pins `id: dsh-tui-theme`), and it stays the name
|
|
53
|
+
* the README documents for settings storage. */
|
|
54
|
+
export const SETTINGS_NS = PLUGIN_ID;
|
|
55
|
+
/** The grammar plugin-owned sections must satisfy
|
|
56
|
+
* (`tuiSettingsSections.register`; the host only relaxes it for its own
|
|
57
|
+
* sections, which may carry opaque Loader ids). */
|
|
58
|
+
const NAMESPACE_PATTERN = /^[a-z][a-z0-9_-]*$/;
|
|
59
|
+
/** The plugin's Loader entry id, when the host gives us one. */
|
|
60
|
+
function loaderEntryId(ctx) {
|
|
61
|
+
const fiber = ctx.fiber;
|
|
62
|
+
const id = fiber?.entry?.options?.id;
|
|
63
|
+
return typeof id === 'string' ? id : undefined;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The namespace this plugin's settings live under: the Loader entry id when it
|
|
67
|
+
* satisfies the section grammar, {@link SETTINGS_NS} otherwise.
|
|
68
|
+
*
|
|
69
|
+
* A `dsh-settings` ≥0.1.7 host keys namespaces by the Loader entry id
|
|
70
|
+
* (`SettingsForms.describe()` → `entry.options.id`), so keying the card off
|
|
71
|
+
* anything else breaks the moment the row is renamed — the fragility dsh-TUI
|
|
72
|
+
* #990 records, where even the host's own section had to stop hard-coding its
|
|
73
|
+
* name. Both generations resolve to the same string here, so the card, the
|
|
74
|
+
* legacy registration and the host's projection cannot disagree.
|
|
75
|
+
*/
|
|
76
|
+
export function resolveSettingsNamespace(ctx) {
|
|
77
|
+
const id = loaderEntryId(ctx);
|
|
78
|
+
return id !== undefined && NAMESPACE_PATTERN.test(id) ? id : SETTINGS_NS;
|
|
79
|
+
}
|
|
26
80
|
/**
|
|
27
|
-
* Register the settings
|
|
28
|
-
*
|
|
29
|
-
*
|
|
81
|
+
* Register the /settings section and, on hosts that still have the
|
|
82
|
+
* registration API, the settings namespace behind it. Each part waits for its
|
|
83
|
+
* own service; neither is required for the other.
|
|
30
84
|
*
|
|
31
|
-
* @param ctx - The plugin's own activation context
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* @param
|
|
35
|
-
* initially and on every committed edit.
|
|
85
|
+
* @param ctx - The plugin's own activation context: the Config owner the
|
|
86
|
+
* ≥0.1.7 page policy and the volatile-update listener must attach to (their
|
|
87
|
+
* disposers ride the inject child, which is what a service reload recycles).
|
|
88
|
+
* @param wiring - The value source and sink for the card.
|
|
36
89
|
* @param dataDir - The host data directory (~/.dsh-tui), read by the
|
|
37
90
|
* followSystem field's format() to surface the cached follow state.
|
|
38
91
|
*/
|
|
39
|
-
export function registerPinkSettings(ctx,
|
|
92
|
+
export function registerPinkSettings(ctx, wiring, dataDir = joinHomeDataDir()) {
|
|
93
|
+
const ns = resolveSettingsNamespace(ctx);
|
|
40
94
|
ctx.inject(['settings'], settingsCtx => {
|
|
41
95
|
const settings = settingsCtx.settings;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const scope = settings.register(PLUGIN_ID, z.object({
|
|
48
|
-
followSystem: z.boolean(),
|
|
49
|
-
showGlyph: z.boolean(),
|
|
50
|
-
showClock: z.boolean(),
|
|
51
|
-
showTurns: z.boolean(),
|
|
52
|
-
statusScope: z.union(['pink-only', 'all-themes']),
|
|
53
|
-
statusGlyph: z.string(),
|
|
54
|
-
statusSeparator: z.string(),
|
|
55
|
-
}));
|
|
56
|
-
const emit = (doc) => {
|
|
57
|
-
if (doc === null || typeof doc !== 'object')
|
|
58
|
-
return;
|
|
59
|
-
const clean = {};
|
|
60
|
-
for (const [key, value] of Object.entries(doc)) {
|
|
61
|
-
if (value !== undefined)
|
|
62
|
-
clean[key] = value;
|
|
63
|
-
}
|
|
64
|
-
onDoc(clean);
|
|
65
|
-
};
|
|
66
|
-
// Own the watcher on the inject-scoped ledger so it survives exactly as
|
|
67
|
-
// long as this activation (scope.watch's disposer is otherwise leaked).
|
|
68
|
-
settingsCtx.effect(() => {
|
|
69
|
-
emit(scope.get());
|
|
70
|
-
return scope.watch(emit);
|
|
71
|
-
});
|
|
96
|
+
if (settings === undefined)
|
|
97
|
+
return;
|
|
98
|
+
if (typeof settings.register === 'function') {
|
|
99
|
+
registerNamespaceScope(settingsCtx, settings, wiring, ns);
|
|
100
|
+
return;
|
|
72
101
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
settingsCtx.logger.warn(`dsh-tui-theme: settings namespace registration failed: ${String(error)}`);
|
|
102
|
+
if (typeof settings.configure !== 'function') {
|
|
103
|
+
settingsCtx.logger.info('dsh-tui-theme: the settings service exposes neither the namespace registration nor the Config-derived surface; the settings card stays unavailable this session');
|
|
104
|
+
return;
|
|
77
105
|
}
|
|
106
|
+
diagnoseConfigGeneration(ctx, ns, wiring.hasLiveFields);
|
|
107
|
+
configureOwnPage(ctx, settingsCtx, settings);
|
|
108
|
+
wiring.onDoc(definedOnly(wiring.readLive()));
|
|
109
|
+
watchLiveConfig(ctx, settingsCtx, wiring);
|
|
78
110
|
});
|
|
79
111
|
ctx.inject(['tuiSettingsSections'], sectionsCtx => {
|
|
80
112
|
const sections = sectionsCtx
|
|
81
113
|
.tuiSettingsSections;
|
|
82
114
|
try {
|
|
83
|
-
const unregister = sections.register(sectionDefinition(cordis, dataDir));
|
|
115
|
+
const unregister = sections.register(sectionDefinition(ns, wiring.cordis, dataDir));
|
|
84
116
|
sectionsCtx.effect(() => () => unregister());
|
|
85
117
|
}
|
|
86
118
|
catch (error) {
|
|
@@ -90,6 +122,136 @@ export function registerPinkSettings(ctx, cordis, onDoc, dataDir = joinHomeDataD
|
|
|
90
122
|
}
|
|
91
123
|
});
|
|
92
124
|
}
|
|
125
|
+
/** The defined-valued subset of a settings document: the ≤0.1.6 user layer and
|
|
126
|
+
* the ≥0.1.7 live config both arrive with unset keys present but undefined. */
|
|
127
|
+
function definedOnly(doc) {
|
|
128
|
+
if (doc === null || typeof doc !== 'object')
|
|
129
|
+
return {};
|
|
130
|
+
const clean = {};
|
|
131
|
+
for (const [key, value] of Object.entries(doc)) {
|
|
132
|
+
if (value !== undefined)
|
|
133
|
+
clean[key] = value;
|
|
134
|
+
}
|
|
135
|
+
return clean;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* ≤0.1.6 generation: own the namespace and follow its scope. The schema spells
|
|
139
|
+
* out the same field set the card exposes and deliberately carries no defaults
|
|
140
|
+
* — an unset user layer must fall through to the cordis layer, which format()
|
|
141
|
+
* already displays as the effective value.
|
|
142
|
+
*/
|
|
143
|
+
function registerNamespaceScope(settingsCtx, settings, wiring, ns) {
|
|
144
|
+
try {
|
|
145
|
+
// Call it as a method (optional-call on the property): the provider's
|
|
146
|
+
// register() reads its own state, so a detached reference would lose
|
|
147
|
+
// `this` and throw.
|
|
148
|
+
//
|
|
149
|
+
// The namespace is passed as a plain string: dsh-settings validates the
|
|
150
|
+
// raw value at registration time, and alpha.2 hosts removed the runtime
|
|
151
|
+
// settingsNamespace() helper, so a branded value would be a missing named
|
|
152
|
+
// export there while older hosts keep the same behavior.
|
|
153
|
+
const scope = settings.register?.(ns, z.object({
|
|
154
|
+
followSystem: z.boolean(),
|
|
155
|
+
showGlyph: z.boolean(),
|
|
156
|
+
showClock: z.boolean(),
|
|
157
|
+
showTurns: z.boolean(),
|
|
158
|
+
statusScope: z.union(['pink-only', 'all-themes']),
|
|
159
|
+
statusGlyph: z.string(),
|
|
160
|
+
statusSeparator: z.string(),
|
|
161
|
+
}));
|
|
162
|
+
if (scope === undefined)
|
|
163
|
+
throw new Error('the settings service exposes no register()');
|
|
164
|
+
// Own the watcher on the inject-scoped ledger so it survives exactly as
|
|
165
|
+
// long as this activation (scope.watch's disposer is otherwise leaked).
|
|
166
|
+
const emit = (doc) => {
|
|
167
|
+
wiring.onDoc(definedOnly(doc));
|
|
168
|
+
};
|
|
169
|
+
settingsCtx.effect(() => {
|
|
170
|
+
emit(scope.get());
|
|
171
|
+
return scope.watch(emit);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
// Contained, but never silent: swallowing the provider's error is what
|
|
176
|
+
// made the ≥0.1.7 transition surface as a bare `命名空间未注册` badge with
|
|
177
|
+
// no line in the log to explain it.
|
|
178
|
+
settingsCtx.logger.warn(`dsh-tui-theme: settings namespace registration failed: ${String(error)}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Explain the two ways a ≥0.1.7 host can leave this card unserved, instead of
|
|
183
|
+
* letting `命名空间未注册` be the only clue (the diagnosis cost of dsh-TUI #990).
|
|
184
|
+
* Both are warnings, not failures: the row config and every other seam keep
|
|
185
|
+
* working.
|
|
186
|
+
*/
|
|
187
|
+
function diagnoseConfigGeneration(ctx, ns, hasLiveFields) {
|
|
188
|
+
const entryId = loaderEntryId(ctx);
|
|
189
|
+
if (entryId !== undefined && entryId !== ns) {
|
|
190
|
+
ctx.logger.warn(`dsh-tui-theme: the settings service keys namespaces by Loader entry id "${entryId}", which is not a valid section namespace — the settings card cannot be served; rename the plugin row to a lowercase kebab-case id`);
|
|
191
|
+
}
|
|
192
|
+
if (!hasLiveFields) {
|
|
193
|
+
ctx.logger.warn('dsh-tui-theme: no row-config field carries the live marker — this host cannot serve the settings card (needs a schemastery that accepts volatile fields); the row config keeps working');
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* ≥0.1.7 generation: the namespace is this profile entry and its form schema is
|
|
198
|
+
* the marked slice of `Config`, so there is nothing to register. The card is
|
|
199
|
+
* the plugin's own page, so opt out of the auto-generated one; the policy must
|
|
200
|
+
* be attached to the plugin's own fiber (the entry that owns the Config), not
|
|
201
|
+
* to the injected child.
|
|
202
|
+
*/
|
|
203
|
+
function configureOwnPage(ctx, settingsCtx, settings) {
|
|
204
|
+
try {
|
|
205
|
+
const dispose = settings.configure?.({ auto: false }, ctx.fiber);
|
|
206
|
+
if (typeof dispose === 'function') {
|
|
207
|
+
const stop = dispose;
|
|
208
|
+
settingsCtx.effect(() => stop);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
// Decorative: the card renders and saves either way; a future
|
|
213
|
+
// auto-generated page would merely duplicate it. `configure` throws when
|
|
214
|
+
// this fiber already registered a policy (a second inject pass).
|
|
215
|
+
settingsCtx.logger.info(`dsh-tui-theme: settings page policy not applied (${String(error)})`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Follow the loader's live-config announcements.
|
|
220
|
+
*
|
|
221
|
+
* The *listener* registers on the plugin's own context: the loader emits
|
|
222
|
+
* `loader/volatile-update` on the Config-owning fiber and `Context.filter`
|
|
223
|
+
* delivers it to that fiber alone, which is the same reason the host's compat
|
|
224
|
+
* shim passes the Config owner rather than the injected child.
|
|
225
|
+
*
|
|
226
|
+
* Its *disposal*, however, belongs to the inject child (`owner`). This body
|
|
227
|
+
* runs once per settings-service arrival, in a fiber cordis recycles, while the
|
|
228
|
+
* plugin fiber outlives every pass: an effect owned by the plugin would let a
|
|
229
|
+
* second pass stack a second listener on top of the first (each event then
|
|
230
|
+
* re-reading the config twice). Without the event the initial read stands for
|
|
231
|
+
* the session — logged, because that means `/settings` edits would not reach
|
|
232
|
+
* the running plugin until it reloads.
|
|
233
|
+
*/
|
|
234
|
+
function watchLiveConfig(ctx, owner, wiring) {
|
|
235
|
+
const refresh = () => {
|
|
236
|
+
wiring.onDoc(definedOnly(wiring.readLive()));
|
|
237
|
+
};
|
|
238
|
+
const events = ctx;
|
|
239
|
+
let dispose;
|
|
240
|
+
try {
|
|
241
|
+
dispose = events.on?.('loader/volatile-update', refresh);
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
dispose = undefined;
|
|
245
|
+
}
|
|
246
|
+
if (typeof dispose !== 'function') {
|
|
247
|
+
ctx.logger.info('dsh-tui-theme: loader/volatile-update is unavailable; /settings edits apply at the next plugin reload');
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
// `ctx.on` already ties the listener to the plugin fiber (cordis's own
|
|
251
|
+
// effect inside `Context.on`); this one exists for the recycled inject child,
|
|
252
|
+
// so it must run on that child's ledger rather than the plugin's.
|
|
253
|
+
owner.effect(() => dispose);
|
|
254
|
+
}
|
|
93
255
|
function joinHomeDataDir() {
|
|
94
256
|
return join(homeDir(), '.dsh-tui');
|
|
95
257
|
}
|
|
@@ -111,9 +273,9 @@ function followCacheDate(at) {
|
|
|
111
273
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
112
274
|
}
|
|
113
275
|
/** The declarative /settings block (labels bilingual, zh via descriptions). */
|
|
114
|
-
function sectionDefinition(cordis, dataDir) {
|
|
276
|
+
function sectionDefinition(ns, cordis, dataDir) {
|
|
115
277
|
return {
|
|
116
|
-
ns
|
|
278
|
+
ns,
|
|
117
279
|
title: 'pink-theme',
|
|
118
280
|
descriptions: { zh: 'pink-theme' },
|
|
119
281
|
groups: [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"statusLine.d.ts","sourceRoot":"","sources":["../../src/statusLine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAkBlD,mDAAmD;AACnD,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG,YAAY,CAAA;AAEpD,MAAM,WAAW,aAAa;IAC5B,2EAA2E;IAC3E,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,qEAAqE;IACrE,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,sEAAsE;IACtE,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,4CAA4C;IAC5C,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;AAsCrD;;;;;;GAMG;AACH,wBAAgB,gCAAgC,IAAI,IAAI,CAGvD;
|
|
1
|
+
{"version":3,"file":"statusLine.d.ts","sourceRoot":"","sources":["../../src/statusLine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAkBlD,mDAAmD;AACnD,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG,YAAY,CAAA;AAEpD,MAAM,WAAW,aAAa;IAC5B,2EAA2E;IAC3E,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,qEAAqE;IACrE,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,sEAAsE;IACtE,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,4CAA4C;IAC5C,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;AAsCrD;;;;;;GAMG;AACH,wBAAgB,gCAAgC,IAAI,IAAI,CAGvD;AAuGD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,GAAG,IAAI,CAyIvF"}
|
package/lib/types/statusLine.js
CHANGED
|
@@ -106,8 +106,7 @@ function activeThemeName(dataDir) {
|
|
|
106
106
|
}
|
|
107
107
|
return prefCache.value;
|
|
108
108
|
}
|
|
109
|
-
function
|
|
110
|
-
const name = activeThemeName(dataDir);
|
|
109
|
+
function isPinkTheme(name) {
|
|
111
110
|
return name !== undefined && PINK_THEMES.has(name);
|
|
112
111
|
}
|
|
113
112
|
// ── palette colors ──────────────────────────────────────────────────────────
|
|
@@ -151,13 +150,12 @@ function readPalette(name) {
|
|
|
151
150
|
}
|
|
152
151
|
return NO_COLORS;
|
|
153
152
|
}
|
|
154
|
-
function paletteFor(
|
|
155
|
-
|
|
156
|
-
if (name === undefined || !PINK_THEMES.has(name))
|
|
153
|
+
function paletteFor(themeName) {
|
|
154
|
+
if (!isPinkTheme(themeName))
|
|
157
155
|
return NO_COLORS;
|
|
158
156
|
const now = Date.now();
|
|
159
|
-
if (colorsCache === undefined || colorsCache.name !==
|
|
160
|
-
colorsCache = { at: now, name, colors: readPalette(
|
|
157
|
+
if (colorsCache === undefined || colorsCache.name !== themeName || now - colorsCache.at >= THEME_PREF_TTL_MS) {
|
|
158
|
+
colorsCache = { at: now, name: themeName, colors: readPalette(themeName) };
|
|
161
159
|
}
|
|
162
160
|
return colorsCache.colors;
|
|
163
161
|
}
|
|
@@ -192,7 +190,8 @@ export function startStatusLine(ctx, getEffective) {
|
|
|
192
190
|
// differ in how the result reaches the host.
|
|
193
191
|
const renderScalar = () => {
|
|
194
192
|
const eff = getEffective();
|
|
195
|
-
const
|
|
193
|
+
const themeName = activeThemeName(dataDir);
|
|
194
|
+
const cells = statusCells(eff, themeName);
|
|
196
195
|
const separator = sanitizeOrnament(eff.statusSeparator, SEPARATOR);
|
|
197
196
|
const parts = [cells.glyph, cells.clock, cells.turns].filter((value) => value !== undefined);
|
|
198
197
|
const text = parts.join(` ${separator} `);
|
|
@@ -209,14 +208,18 @@ export function startStatusLine(ctx, getEffective) {
|
|
|
209
208
|
try {
|
|
210
209
|
if (store !== undefined) {
|
|
211
210
|
const eff = getEffective();
|
|
212
|
-
|
|
211
|
+
// One pref read per render, shared by the visibility check and the
|
|
212
|
+
// palette: the module header's "one cheap stat" invariant holds on
|
|
213
|
+
// both paths (statusCells and paletteFor each used to re-resolve).
|
|
214
|
+
const themeName = activeThemeName(dataDir);
|
|
215
|
+
const cells = statusCells(eff, themeName);
|
|
213
216
|
store.push({
|
|
214
217
|
visible: cells.glyph !== undefined || cells.clock !== undefined || cells.turns !== undefined,
|
|
215
218
|
glyph: cells.glyph,
|
|
216
219
|
clock: cells.clock,
|
|
217
220
|
turns: cells.turns,
|
|
218
221
|
separator: sanitizeOrnament(eff.statusSeparator, SEPARATOR),
|
|
219
|
-
colors: paletteFor(
|
|
222
|
+
colors: paletteFor(themeName),
|
|
220
223
|
});
|
|
221
224
|
return;
|
|
222
225
|
}
|
|
@@ -227,9 +230,10 @@ export function startStatusLine(ctx, getEffective) {
|
|
|
227
230
|
}
|
|
228
231
|
};
|
|
229
232
|
/** The three optional cells (all undefined = the line is off: master
|
|
230
|
-
* switch, theme scope, and the toggles fold into this one shape).
|
|
231
|
-
|
|
232
|
-
|
|
233
|
+
* switch, theme scope, and the toggles fold into this one shape).
|
|
234
|
+
* Takes the theme name resolved once by the caller per render. */
|
|
235
|
+
function statusCells(eff, themeName) {
|
|
236
|
+
const enabled = eff.statusEnabled && (eff.statusScope === 'all-themes' || isPinkTheme(themeName));
|
|
233
237
|
if (!enabled)
|
|
234
238
|
return { glyph: undefined, clock: undefined, turns: undefined };
|
|
235
239
|
return {
|
|
@@ -19,6 +19,10 @@ export interface ThemeInstallResult {
|
|
|
19
19
|
readonly repaired: readonly string[];
|
|
20
20
|
/** Files that could not be installed (per-file failures). */
|
|
21
21
|
readonly failed: readonly string[];
|
|
22
|
+
/** Set only when the bundled directory itself was unreadable, so nothing
|
|
23
|
+
* was attempted and no per-file entry exists. Carries the directory and
|
|
24
|
+
* the underlying error for the caller's log. */
|
|
25
|
+
readonly sourceError?: string;
|
|
22
26
|
}
|
|
23
27
|
export interface BundledTheme {
|
|
24
28
|
readonly file: string;
|
|
@@ -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;
|
|
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;IAClC;;qDAEiD;IACjD,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAC9B;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;AAOD,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,CAqCpB;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
|
@@ -15,7 +15,10 @@ import { homedir } from 'node:os';
|
|
|
15
15
|
import { dirname, join } from 'node:path';
|
|
16
16
|
import { fileURLToPath } from 'node:url';
|
|
17
17
|
// Same resolution order as the host's utils/paths.ts homeDir(): os.homedir()
|
|
18
|
-
// first, USERPROFILE/HOME spellings as stripped-down fallbacks.
|
|
18
|
+
// first, USERPROFILE/HOME spellings as stripped-down fallbacks. When all three
|
|
19
|
+
// come up empty the returned '' makes every derived path resolve against the
|
|
20
|
+
// process cwd — the host's own semantics, kept deliberately identical rather
|
|
21
|
+
// than second-guessed here.
|
|
19
22
|
export function homeDir() {
|
|
20
23
|
return homedir() || process.env.USERPROFILE || process.env.HOME || '';
|
|
21
24
|
}
|
|
@@ -87,8 +90,10 @@ export function installBundledThemes(targetDir = themesTargetDir(), sourceDir =
|
|
|
87
90
|
try {
|
|
88
91
|
files = readdirSync(sourceDir).filter(entry => entry.toLowerCase().endsWith('.json'));
|
|
89
92
|
}
|
|
90
|
-
catch {
|
|
91
|
-
|
|
93
|
+
catch (error) {
|
|
94
|
+
// No file was attempted, so `failed` stays empty; the directory-level
|
|
95
|
+
// reason travels separately so the caller can log a precise line.
|
|
96
|
+
return { installed, skipped, repaired, failed, sourceError: `could not read bundled themes from ${sourceDir}: ${String(error)}` };
|
|
92
97
|
}
|
|
93
98
|
for (const file of files) {
|
|
94
99
|
const target = join(targetDir, file);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-tui-theme",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "Sakura-pink themes for dsh-TUI with optional cached background 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",
|
|
@@ -28,8 +28,9 @@
|
|
|
28
28
|
"preverify": "npm run build",
|
|
29
29
|
"verify": "node scripts/verify.mjs",
|
|
30
30
|
"verify:package": "node scripts/verify-package.mjs",
|
|
31
|
+
"verify:settings": "node scripts/verify-settings-generation.mjs",
|
|
31
32
|
"verify:host": "npm run build && node scripts/headless-order-test.mjs && node scripts/runtime-themes-headless.mjs && node --import tsx/esm scripts/validate-themes-against-host.mjs",
|
|
32
|
-
"release:check": "npm run build && npm run verify && npm run verify:package",
|
|
33
|
+
"release:check": "npm run build && npm run verify && npm run verify:package && npm run verify:settings",
|
|
33
34
|
"prepack": "npm run release:check"
|
|
34
35
|
},
|
|
35
36
|
"license": "MIT",
|
|
@@ -58,8 +59,8 @@
|
|
|
58
59
|
},
|
|
59
60
|
"peerDependencies": {
|
|
60
61
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
61
|
-
"@deepseek-ai/dsh-session": "^0.1.0-rc.6 || ^0.1.1-rc.1 || ^0.1.2-alpha.2 || 0.1.
|
|
62
|
-
"@deepseek-ai/dsh-settings": "^0.1.0-rc.6 || ^0.1.1-rc.1 || ^0.1.2-alpha.2 || 0.1.
|
|
62
|
+
"@deepseek-ai/dsh-session": "^0.1.0-rc.6 || ^0.1.1-rc.1 || ^0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.3-alpha.2 || 0.1.5-alpha.1 || 0.1.5-alpha.2 || 0.1.5-rc.1",
|
|
63
|
+
"@deepseek-ai/dsh-settings": "^0.1.0-rc.6 || ^0.1.1-rc.1 || ^0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.3-alpha.2 || 0.1.5-alpha.1 || 0.1.5-alpha.2 || 0.1.5-rc.1",
|
|
63
64
|
"@deepseek-ai/schemastery": "^3.18.1"
|
|
64
65
|
},
|
|
65
66
|
"devDependencies": {
|