@aiwayds/dsh-tui-pi 2.16.0 → 2.18.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 +3 -3
- package/README.zh-CN.md +3 -3
- package/cordis.patch.yml +14 -0
- package/lib/index.js +70 -124
- package/lib/index.js.map +1 -1
- package/lib/selectors.d.ts +7 -5
- package/lib/selectors.js +19 -23
- package/lib/selectors.js.map +1 -1
- package/lib/theme/index.d.ts +6 -3
- package/lib/theme/index.js +30 -2
- package/lib/theme/index.js.map +1 -1
- package/lib/theme/palette.d.ts +2 -0
- package/lib/theme/palette.js +1 -1
- package/lib/theme/palette.js.map +1 -1
- package/lib/theme/registry.d.ts +49 -0
- package/lib/theme/registry.js +211 -0
- package/lib/theme/registry.js.map +1 -0
- package/lib/theme-preview.d.ts +98 -0
- package/lib/theme-preview.js +266 -0
- package/lib/theme-preview.js.map +1 -0
- package/lib/theme-settings.js +6 -5
- package/lib/theme-settings.js.map +1 -1
- package/lib/tui.d.ts +3 -0
- package/lib/tui.js +1 -1
- package/lib/tui.js.map +1 -1
- package/package.json +6 -4
- package/skills/dsh-tui-pi-config/SKILL.md +11 -4
- package/themes/ayu-light.json +25 -0
- package/themes/catppuccin-latte.json +25 -0
- package/themes/catppuccin-mocha.json +25 -0
- package/themes/dracula-light.json +25 -0
- package/themes/dracula.json +25 -0
- package/themes/github-dark.json +25 -0
- package/themes/github-light.json +25 -0
- package/themes/gruvbox-dark.json +25 -0
- package/themes/gruvbox-light.json +25 -0
- package/themes/material-light.json +25 -0
- package/themes/monokai.json +25 -0
- package/themes/nord-dark.json +25 -0
- package/themes/nord-light.json +25 -0
- package/themes/one-dark.json +25 -0
- package/themes/one-light.json +25 -0
- package/themes/solarized-dark.json +25 -0
- package/themes/solarized-light.json +25 -0
- package/themes/synthwave.json +25 -0
- package/themes/tokyo-night-light.json +25 -0
- package/themes/tokyo-night.json +25 -0
package/lib/theme/palette.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"palette.js","sourceRoot":"","sources":["../../src/theme/palette.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAuCH,4EAA4E;AAC5E,MAAM,IAAI,GAAG,mBAAmB,CAAA;AAEhC,sFAAsF;AACtF,
|
|
1
|
+
{"version":3,"file":"palette.js","sourceRoot":"","sources":["../../src/theme/palette.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme file registry: discovers themes from the npm-bundled `themes/`
|
|
3
|
+
* directory and the user's `$DSH_HOME/themes` directory, parses+validates
|
|
4
|
+
* each `*.json` theme file into a full `Palette`, and merges them into one
|
|
5
|
+
* registry keyed by theme id. The user directory wins over the bundled
|
|
6
|
+
* themes on name collisions, and the two hard-coded GitHub palettes always
|
|
7
|
+
* win over a bundled JSON of the same name (their exact values are
|
|
8
|
+
* load-bearing — the e2e suite asserts the canvas RGB).
|
|
9
|
+
*
|
|
10
|
+
* JSON contract (a user-writable theme file):
|
|
11
|
+
* - required: `name` (string, the theme id), `dark` (boolean), and 13 hex
|
|
12
|
+
* colors (`#rrggbb`, six hex digits): `canvas` `canvasSubtle` `canvasInset`
|
|
13
|
+
* `fgDefault` `fgMuted` `fgSubtle` `borderDefault` `borderMuted` `accent`
|
|
14
|
+
* `success` `danger` `attention` `thinking`.
|
|
15
|
+
* - optional (derived when absent): `accentMuted`, `successMuted`,
|
|
16
|
+
* `dangerMuted`, `attentionMuted`, `thinkingPanelBg`, `toolPanelBg`,
|
|
17
|
+
* `panelBorder`, `panelBoxBorder` — see `deriveDefault`.
|
|
18
|
+
*/
|
|
19
|
+
import { type Palette } from './palette.ts';
|
|
20
|
+
/** npm-bundled themes dir (repo-root `themes/`; resolves up from lib/ at runtime). */
|
|
21
|
+
export declare function builtinThemesDir(): string;
|
|
22
|
+
/** User themes dir: `$DSH_HOME/themes` (home is the dshHome() result). */
|
|
23
|
+
export declare function userThemesDir(home: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* Parse + validate one theme JSON file and derive its missing optional
|
|
26
|
+
* fields into a complete `Palette`. Throws an `Error` carrying the `source`
|
|
27
|
+
* (the file path) and the offending field on any invalid input.
|
|
28
|
+
*/
|
|
29
|
+
export declare function parseThemeFile(raw: string, source: string): Palette;
|
|
30
|
+
/**
|
|
31
|
+
* Scan one directory for `*.json` theme files, parsing each into a full
|
|
32
|
+
* `Palette`. Invalid files are skipped with a `warn` line (fail-open); a
|
|
33
|
+
* missing/unreadable directory yields an empty Map (never throws).
|
|
34
|
+
*/
|
|
35
|
+
export declare function scanThemeDir(dir: string, warn?: (msg: string) => void): Map<string, Palette>;
|
|
36
|
+
/**
|
|
37
|
+
* Merge-register all discoverable themes: the bundled `themes/` directory
|
|
38
|
+
* first, then the user's `$DSH_HOME/themes` — user themes override bundled
|
|
39
|
+
* ones of the same name. The hard-coded `github-light`/`github-dark` palettes
|
|
40
|
+
* are always re-seeded over a bundled JSON of the same name (their exact
|
|
41
|
+
* values are load-bearing for the e2e suite).
|
|
42
|
+
*
|
|
43
|
+
* @param opts.home - the dsh home (defaults to `$DSH_HOME` or `~/.dsh`).
|
|
44
|
+
* @param opts.warn - per-file skip reporter (invalid themes are fail-open).
|
|
45
|
+
*/
|
|
46
|
+
export declare function discoverThemes(opts?: {
|
|
47
|
+
home?: string;
|
|
48
|
+
warn?: (msg: string) => void;
|
|
49
|
+
}): Map<string, Palette>;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme file registry: discovers themes from the npm-bundled `themes/`
|
|
3
|
+
* directory and the user's `$DSH_HOME/themes` directory, parses+validates
|
|
4
|
+
* each `*.json` theme file into a full `Palette`, and merges them into one
|
|
5
|
+
* registry keyed by theme id. The user directory wins over the bundled
|
|
6
|
+
* themes on name collisions, and the two hard-coded GitHub palettes always
|
|
7
|
+
* win over a bundled JSON of the same name (their exact values are
|
|
8
|
+
* load-bearing — the e2e suite asserts the canvas RGB).
|
|
9
|
+
*
|
|
10
|
+
* JSON contract (a user-writable theme file):
|
|
11
|
+
* - required: `name` (string, the theme id), `dark` (boolean), and 13 hex
|
|
12
|
+
* colors (`#rrggbb`, six hex digits): `canvas` `canvasSubtle` `canvasInset`
|
|
13
|
+
* `fgDefault` `fgMuted` `fgSubtle` `borderDefault` `borderMuted` `accent`
|
|
14
|
+
* `success` `danger` `attention` `thinking`.
|
|
15
|
+
* - optional (derived when absent): `accentMuted`, `successMuted`,
|
|
16
|
+
* `dangerMuted`, `attentionMuted`, `thinkingPanelBg`, `toolPanelBg`,
|
|
17
|
+
* `panelBorder`, `panelBoxBorder` — see `deriveDefault`.
|
|
18
|
+
*/
|
|
19
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
20
|
+
import { homedir } from 'node:os';
|
|
21
|
+
import { dirname, join, resolve } from 'node:path';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
import { blend, githubDark, githubLight } from "./palette.js";
|
|
24
|
+
/** #rrggbb hex matcher — same shape as palette.ts's internal HEX6. */
|
|
25
|
+
const HEX6 = /^#[0-9a-fA-F]{6}$/;
|
|
26
|
+
/** The 13 required color fields of a user theme file. */
|
|
27
|
+
const REQUIRED_COLORS = [
|
|
28
|
+
'canvas', 'canvasSubtle', 'canvasInset',
|
|
29
|
+
'fgDefault', 'fgMuted', 'fgSubtle',
|
|
30
|
+
'borderDefault', 'borderMuted',
|
|
31
|
+
'accent', 'success', 'danger', 'attention', 'thinking',
|
|
32
|
+
];
|
|
33
|
+
/** True when `value` is a #rrggbb hex string. */
|
|
34
|
+
function isHex(value) {
|
|
35
|
+
return typeof value === 'string' && HEX6.test(value);
|
|
36
|
+
}
|
|
37
|
+
/** Default $DSH_HOME (mirrors append-system.ts's dshHome — kept local to avoid a business-module dependency here). */
|
|
38
|
+
function defaultDshHome() {
|
|
39
|
+
return process.env.DSH_HOME ?? join(homedir(), '.dsh');
|
|
40
|
+
}
|
|
41
|
+
/** npm-bundled themes dir (repo-root `themes/`; resolves up from lib/ at runtime). */
|
|
42
|
+
export function builtinThemesDir() {
|
|
43
|
+
// Compiled: lib/theme/registry.js → ../.. = package root. From src/ the
|
|
44
|
+
// same two hops land on the repo root too.
|
|
45
|
+
return resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', 'themes');
|
|
46
|
+
}
|
|
47
|
+
/** User themes dir: `$DSH_HOME/themes` (home is the dshHome() result). */
|
|
48
|
+
export function userThemesDir(home) {
|
|
49
|
+
return join(home, 'themes');
|
|
50
|
+
}
|
|
51
|
+
/** Derive the optional-field default for one field (see the JSON contract). */
|
|
52
|
+
function deriveDefault(field, dark, canvas, accent, colors, derived) {
|
|
53
|
+
switch (field) {
|
|
54
|
+
case 'accentMuted':
|
|
55
|
+
return dark ? blend(canvas, accent, 0.25) : blend(canvas, accent, 0.18);
|
|
56
|
+
case 'successMuted':
|
|
57
|
+
return blend(canvas, colors['success'], 0.25);
|
|
58
|
+
case 'dangerMuted':
|
|
59
|
+
return blend(canvas, colors['danger'], 0.25);
|
|
60
|
+
case 'attentionMuted':
|
|
61
|
+
return blend(canvas, colors['attention'], 0.25);
|
|
62
|
+
case 'thinkingPanelBg':
|
|
63
|
+
return dark ? blend(canvas, colors['thinking'], 0.25) : blend(canvas, colors['thinking'], 0.12);
|
|
64
|
+
case 'toolPanelBg':
|
|
65
|
+
// Defaults to accentMuted — parseThemeFile derives accentMuted first.
|
|
66
|
+
return derived.get('accentMuted');
|
|
67
|
+
case 'panelBorder':
|
|
68
|
+
return colors['borderDefault'];
|
|
69
|
+
case 'panelBoxBorder':
|
|
70
|
+
return blend(canvas, accent, 0.70);
|
|
71
|
+
default:
|
|
72
|
+
throw new Error(`internal: no derivation for theme field "${field}"`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Parse + validate one theme JSON file and derive its missing optional
|
|
77
|
+
* fields into a complete `Palette`. Throws an `Error` carrying the `source`
|
|
78
|
+
* (the file path) and the offending field on any invalid input.
|
|
79
|
+
*/
|
|
80
|
+
export function parseThemeFile(raw, source) {
|
|
81
|
+
let data;
|
|
82
|
+
try {
|
|
83
|
+
data = JSON.parse(raw);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
throw new Error(`${source}: invalid theme JSON (${error instanceof Error ? error.message : String(error)})`);
|
|
87
|
+
}
|
|
88
|
+
if (data === null || typeof data !== 'object' || Array.isArray(data)) {
|
|
89
|
+
throw new Error(`${source}: theme must be a JSON object`);
|
|
90
|
+
}
|
|
91
|
+
const input = data;
|
|
92
|
+
const name = input['name'];
|
|
93
|
+
if (typeof name !== 'string' || name === '') {
|
|
94
|
+
throw new Error(`${source}: theme "name" must be a non-empty string`);
|
|
95
|
+
}
|
|
96
|
+
const dark = input['dark'];
|
|
97
|
+
if (typeof dark !== 'boolean') {
|
|
98
|
+
throw new Error(`${source}: theme "dark" must be a boolean`);
|
|
99
|
+
}
|
|
100
|
+
const colors = {};
|
|
101
|
+
for (const field of REQUIRED_COLORS) {
|
|
102
|
+
const value = input[field];
|
|
103
|
+
if (!isHex(value)) {
|
|
104
|
+
throw new Error(`${source}: missing or invalid hex color "${field}" (expected #rrggbb)`);
|
|
105
|
+
}
|
|
106
|
+
colors[field] = value;
|
|
107
|
+
}
|
|
108
|
+
// Optional fields: an explicitly provided value is used verbatim (and must
|
|
109
|
+
// be a valid hex — a malformed one is a rejected file, not a silent
|
|
110
|
+
// derivation); absent fields derive per the contract formulas. Derived
|
|
111
|
+
// values are cached so `toolPanelBg` (which defaults to `accentMuted`) can
|
|
112
|
+
// read them back.
|
|
113
|
+
const derived = new Map();
|
|
114
|
+
const optional = (field) => {
|
|
115
|
+
const existing = derived.get(field);
|
|
116
|
+
if (existing !== undefined)
|
|
117
|
+
return existing;
|
|
118
|
+
const value = input[field];
|
|
119
|
+
let resolved;
|
|
120
|
+
if (value === undefined) {
|
|
121
|
+
resolved = deriveDefault(field, dark, colors['canvas'], colors['accent'], colors, derived);
|
|
122
|
+
}
|
|
123
|
+
else if (!isHex(value)) {
|
|
124
|
+
throw new Error(`${source}: invalid hex color "${field}" (expected #rrggbb)`);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
resolved = value;
|
|
128
|
+
}
|
|
129
|
+
derived.set(field, resolved);
|
|
130
|
+
return resolved;
|
|
131
|
+
};
|
|
132
|
+
// toolPanelBg derives from accentMuted, so force accentMuted's derivation
|
|
133
|
+
// before reading it (optional() caches per field, so order matters).
|
|
134
|
+
const accentMuted = optional('accentMuted');
|
|
135
|
+
const toolPanelBg = optional('toolPanelBg');
|
|
136
|
+
const get = (field) => colors[field];
|
|
137
|
+
return {
|
|
138
|
+
name,
|
|
139
|
+
dark,
|
|
140
|
+
canvas: get('canvas'),
|
|
141
|
+
canvasSubtle: get('canvasSubtle'),
|
|
142
|
+
canvasInset: get('canvasInset'),
|
|
143
|
+
fgDefault: get('fgDefault'),
|
|
144
|
+
fgMuted: get('fgMuted'),
|
|
145
|
+
fgSubtle: get('fgSubtle'),
|
|
146
|
+
borderDefault: get('borderDefault'),
|
|
147
|
+
borderMuted: get('borderMuted'),
|
|
148
|
+
accent: get('accent'),
|
|
149
|
+
accentMuted,
|
|
150
|
+
success: get('success'),
|
|
151
|
+
successMuted: optional('successMuted'),
|
|
152
|
+
danger: get('danger'),
|
|
153
|
+
dangerMuted: optional('dangerMuted'),
|
|
154
|
+
attention: get('attention'),
|
|
155
|
+
attentionMuted: optional('attentionMuted'),
|
|
156
|
+
thinking: get('thinking'),
|
|
157
|
+
thinkingPanelBg: optional('thinkingPanelBg'),
|
|
158
|
+
toolPanelBg,
|
|
159
|
+
panelBorder: optional('panelBorder'),
|
|
160
|
+
panelBoxBorder: optional('panelBoxBorder'),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Scan one directory for `*.json` theme files, parsing each into a full
|
|
165
|
+
* `Palette`. Invalid files are skipped with a `warn` line (fail-open); a
|
|
166
|
+
* missing/unreadable directory yields an empty Map (never throws).
|
|
167
|
+
*/
|
|
168
|
+
export function scanThemeDir(dir, warn) {
|
|
169
|
+
const themes = new Map();
|
|
170
|
+
let files;
|
|
171
|
+
try {
|
|
172
|
+
files = readdirSync(dir, { withFileTypes: true })
|
|
173
|
+
.filter(entry => entry.isFile() && entry.name.endsWith('.json'))
|
|
174
|
+
.map(entry => join(dir, entry.name));
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
// Missing/unreadable directory: nothing to register (fail-open).
|
|
178
|
+
return themes;
|
|
179
|
+
}
|
|
180
|
+
for (const file of files) {
|
|
181
|
+
try {
|
|
182
|
+
const palette = parseThemeFile(readFileSync(file, 'utf8'), file);
|
|
183
|
+
themes.set(palette.name, palette);
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
warn?.(`theme registry: skipping ${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return themes;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Merge-register all discoverable themes: the bundled `themes/` directory
|
|
193
|
+
* first, then the user's `$DSH_HOME/themes` — user themes override bundled
|
|
194
|
+
* ones of the same name. The hard-coded `github-light`/`github-dark` palettes
|
|
195
|
+
* are always re-seeded over a bundled JSON of the same name (their exact
|
|
196
|
+
* values are load-bearing for the e2e suite).
|
|
197
|
+
*
|
|
198
|
+
* @param opts.home - the dsh home (defaults to `$DSH_HOME` or `~/.dsh`).
|
|
199
|
+
* @param opts.warn - per-file skip reporter (invalid themes are fail-open).
|
|
200
|
+
*/
|
|
201
|
+
export function discoverThemes(opts) {
|
|
202
|
+
const registry = scanThemeDir(builtinThemesDir(), opts?.warn);
|
|
203
|
+
// The canonical GitHub palettes win over any bundled JSON of the same name.
|
|
204
|
+
registry.set(githubLight.name, githubLight);
|
|
205
|
+
registry.set(githubDark.name, githubDark);
|
|
206
|
+
for (const [name, palette] of scanThemeDir(userThemesDir(opts?.home ?? defaultDshHome()), opts?.warn)) {
|
|
207
|
+
registry.set(name, palette);
|
|
208
|
+
}
|
|
209
|
+
return registry;
|
|
210
|
+
}
|
|
211
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/theme/registry.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme picker's live preview pane: a mock chat page rendered with the
|
|
3
|
+
* SELECTED theme's palette, so the user sees what the TUI will look like
|
|
4
|
+
* before committing to a theme. The left list (a TablePanel) owns the
|
|
5
|
+
* keyboard; every selection change calls `setPalette` and the pane repaints
|
|
6
|
+
* in the selected theme. `auto` (no fixed palette) shows a hint instead of
|
|
7
|
+
* a mock page.
|
|
8
|
+
*
|
|
9
|
+
* The pane is a self-drawn component (like ThinkPanel/ToolPanel): every row
|
|
10
|
+
* is painted with a full-width `ansiBg` backdrop, so surfaces (bubbles,
|
|
11
|
+
* think/tool panels, code blocks, the input dock) read as solid blocks in
|
|
12
|
+
* the theme's real colors — the same canvas the live TUI paints.
|
|
13
|
+
*/
|
|
14
|
+
import type { Component } from '@earendil-works/pi-tui';
|
|
15
|
+
import { type ThemePreference, type TuiTheme } from './theme/index.ts';
|
|
16
|
+
import { type Palette } from './theme/palette.ts';
|
|
17
|
+
/** A mock chat page built from a palette. Fixed row count for the budget. */
|
|
18
|
+
export declare class ThemePreviewPane implements Component {
|
|
19
|
+
private row;
|
|
20
|
+
private readonly getTheme;
|
|
21
|
+
/** `getTheme` is the CURRENT active TuiTheme (used when `auto` is selected). */
|
|
22
|
+
constructor(getTheme: () => TuiTheme);
|
|
23
|
+
/** Switch the preview to a theme row; `undefined` (auto) shows the hint. */
|
|
24
|
+
setRow(row: ThemePickerRow | undefined): void;
|
|
25
|
+
/** The palette the mock page currently renders with (for tests). */
|
|
26
|
+
currentPalette(): Palette | undefined;
|
|
27
|
+
invalidate(): void;
|
|
28
|
+
render(width: number): string[];
|
|
29
|
+
/** `auto` selected: show a short hint in the current theme instead of a mock. */
|
|
30
|
+
private renderAuto;
|
|
31
|
+
}
|
|
32
|
+
/** A theme picker row: the selectable value, name, description and palette. */
|
|
33
|
+
export interface ThemePickerRow {
|
|
34
|
+
value: string;
|
|
35
|
+
label: string;
|
|
36
|
+
description: string;
|
|
37
|
+
palette?: Palette;
|
|
38
|
+
}
|
|
39
|
+
/** Options for the theme picker's registry rows (custom themes). */
|
|
40
|
+
export interface PickThemeOptions {
|
|
41
|
+
/**
|
|
42
|
+
* Full theme registry (theme id → palette), as produced by
|
|
43
|
+
* `discoverThemes` — every entry becomes a selectable row after the
|
|
44
|
+
* constant auto/light/dark rows. Absent: the picker shows only those
|
|
45
|
+
* three rows (the historical behavior).
|
|
46
|
+
*/
|
|
47
|
+
themes?: ReadonlyMap<string, Palette>;
|
|
48
|
+
/**
|
|
49
|
+
* Theme ids that live in the user themes directory. Entries in `themes`
|
|
50
|
+
* whose id appears here are labeled "user theme"; the rest (the bundled
|
|
51
|
+
* themes) are labeled "built-in".
|
|
52
|
+
*/
|
|
53
|
+
userNames?: ReadonlySet<string>;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Build the picker rows — a flat two-level list: `auto` first, then every
|
|
57
|
+
* registered theme. The two GitHub defaults lead the theme block (they are
|
|
58
|
+
* the out-of-the-box choices and keep the e2e navigation stable), user
|
|
59
|
+
* themes follow, the remaining built-ins come last. The old `light`/`dark`
|
|
60
|
+
* alias rows are gone — those names stay valid as stored preferences (they
|
|
61
|
+
* resolve through resolveTheme's alias handling) but no longer get their own
|
|
62
|
+
* rows.
|
|
63
|
+
*/
|
|
64
|
+
export declare function themePickerRows(options?: PickThemeOptions): ThemePickerRow[];
|
|
65
|
+
/** Terminal width at which the picker switches from stacked to side-by-side. */
|
|
66
|
+
export declare const DUAL_PANE_MIN_COLUMNS = 100;
|
|
67
|
+
/** Floor of the left list pane in dual-pane mode. */
|
|
68
|
+
export declare const LEFT_PANE_MIN_COLUMNS = 30;
|
|
69
|
+
/**
|
|
70
|
+
* Content-row budget inside the framed overlay: the overlay is capped at 75%
|
|
71
|
+
* of the terminal, the FramedOverlay adds 4 chrome rows (top/bottom borders +
|
|
72
|
+
* a blank spacer each), and the preview pane renders exactly its own height.
|
|
73
|
+
* Testable; `rows` injected.
|
|
74
|
+
*/
|
|
75
|
+
export declare function overlayContentBudget(rows?: number | undefined): number;
|
|
76
|
+
/**
|
|
77
|
+
* The theme picker overlay: a left TablePanel of themes + a right preview
|
|
78
|
+
* pane that repaints with the SELECTED theme's palette on every cursor move
|
|
79
|
+
* (history.ts's pattern — compare selectedRow before/after handleInput, an
|
|
80
|
+
* explicit action, never the render path). Side-by-side on wide terminals
|
|
81
|
+
* (HStack), stacked on narrow ones (VStack). Renders exactly
|
|
82
|
+
* `overlayContentBudget()` rows.
|
|
83
|
+
*/
|
|
84
|
+
export declare class ThemePickerOverlay implements Component {
|
|
85
|
+
private readonly list;
|
|
86
|
+
private readonly preview;
|
|
87
|
+
private readonly rows;
|
|
88
|
+
private readonly onPick;
|
|
89
|
+
private readonly footer;
|
|
90
|
+
private closed;
|
|
91
|
+
constructor(theme: TuiTheme, rows: readonly ThemePickerRow[], preselect: number, onPick: (value: string | undefined) => void, getTheme: () => TuiTheme);
|
|
92
|
+
invalidate(): void;
|
|
93
|
+
render(width: number): string[];
|
|
94
|
+
handleInput(data: string): void;
|
|
95
|
+
private finish;
|
|
96
|
+
/** The currently selected row (test seam). */
|
|
97
|
+
selectedValue(): ThemePreference | undefined;
|
|
98
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme picker's live preview pane: a mock chat page rendered with the
|
|
3
|
+
* SELECTED theme's palette, so the user sees what the TUI will look like
|
|
4
|
+
* before committing to a theme. The left list (a TablePanel) owns the
|
|
5
|
+
* keyboard; every selection change calls `setPalette` and the pane repaints
|
|
6
|
+
* in the selected theme. `auto` (no fixed palette) shows a hint instead of
|
|
7
|
+
* a mock page.
|
|
8
|
+
*
|
|
9
|
+
* The pane is a self-drawn component (like ThinkPanel/ToolPanel): every row
|
|
10
|
+
* is painted with a full-width `ansiBg` backdrop, so surfaces (bubbles,
|
|
11
|
+
* think/tool panels, code blocks, the input dock) read as solid blocks in
|
|
12
|
+
* the theme's real colors — the same canvas the live TUI paints.
|
|
13
|
+
*/
|
|
14
|
+
import { HStack, VStack } from '@earendil-works/pi-tui';
|
|
15
|
+
import { TablePanel } from "./panels.js";
|
|
16
|
+
import { ansiBg, ansiFg, BOLD, buildTheme, RESET } from "./theme/index.js";
|
|
17
|
+
import { githubDark, githubLight } from "./theme/palette.js";
|
|
18
|
+
import { visibleWidth } from "./text.js";
|
|
19
|
+
/**
|
|
20
|
+
* Paint one full-width row: `content` (which may carry its own SGR) on the
|
|
21
|
+
* `bg` backdrop, with `bg` re-applied after any mid-line RESET and trailing
|
|
22
|
+
* padding kept on the backdrop (frame.ts fillLine's pattern, but on an
|
|
23
|
+
* arbitrary palette surface).
|
|
24
|
+
*/
|
|
25
|
+
function fill(bg, content, width) {
|
|
26
|
+
const pad = Math.max(0, width - visibleWidth(content));
|
|
27
|
+
const prefix = ansiBg(bg);
|
|
28
|
+
const guarded = content.replace(/\x1b\[0m/g, `\x1b[0m${prefix}`);
|
|
29
|
+
return prefix + guarded + prefix + ' '.repeat(pad) + RESET;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* One mock chat page row. `surface` is the palette surface to paint the row
|
|
33
|
+
* on; `content` is the styled text (already carrying its foreground SGR).
|
|
34
|
+
*/
|
|
35
|
+
function surfaceRow(surface, content, width) {
|
|
36
|
+
return fill(surface, content, width);
|
|
37
|
+
}
|
|
38
|
+
/** A mock chat page built from a palette. Fixed row count for the budget. */
|
|
39
|
+
export class ThemePreviewPane {
|
|
40
|
+
row;
|
|
41
|
+
getTheme;
|
|
42
|
+
/** `getTheme` is the CURRENT active TuiTheme (used when `auto` is selected). */
|
|
43
|
+
constructor(getTheme) {
|
|
44
|
+
this.getTheme = getTheme;
|
|
45
|
+
}
|
|
46
|
+
/** Switch the preview to a theme row; `undefined` (auto) shows the hint. */
|
|
47
|
+
setRow(row) {
|
|
48
|
+
this.row = row;
|
|
49
|
+
}
|
|
50
|
+
/** The palette the mock page currently renders with (for tests). */
|
|
51
|
+
currentPalette() {
|
|
52
|
+
return this.row?.palette;
|
|
53
|
+
}
|
|
54
|
+
invalidate() { }
|
|
55
|
+
render(width) {
|
|
56
|
+
const row = this.row;
|
|
57
|
+
if (row === undefined || row.palette === undefined)
|
|
58
|
+
return this.renderAuto(width);
|
|
59
|
+
const p = buildTheme(row.palette).palette;
|
|
60
|
+
const fg = (hex, text) => ansiFg(hex) + text + RESET;
|
|
61
|
+
const bold = (hex, text) => ansiFg(hex) + BOLD + text + RESET;
|
|
62
|
+
const canvas = (content) => surfaceRow(p.canvas, content, width);
|
|
63
|
+
const lines = [];
|
|
64
|
+
// Title bar: the theme label + its description, accent-tinted, on the
|
|
65
|
+
// inset surface (the description carries the "GitHub light palette" /
|
|
66
|
+
// "GitHub dark palette" text the e2e suite asserts).
|
|
67
|
+
const title = `${row.label} — ${row.description}`;
|
|
68
|
+
lines.push(surfaceRow(p.canvasInset, ` ${bold(p.accent, title)} `, width));
|
|
69
|
+
lines.push(canvas(''));
|
|
70
|
+
// User message bubble (canvasSubtle backdrop + fgDefault text).
|
|
71
|
+
lines.push(canvas(` ${fg(p.fgDefault, '▎帮我分析这段代码')} `));
|
|
72
|
+
// Think panel (its own purple surface + italic thinking text).
|
|
73
|
+
lines.push(surfaceRow(p.thinkingPanelBg, ` ${bold(p.thinking, 'thinking')}`, width));
|
|
74
|
+
lines.push(surfaceRow(p.thinkingPanelBg, ` ${fg(p.fgMuted, '让我看看这段代码……')}`, width));
|
|
75
|
+
lines.push(canvas(''));
|
|
76
|
+
// Tool card (blue surface + accent tool name).
|
|
77
|
+
lines.push(surfaceRow(p.toolPanelBg, ` ${bold(p.accent, '⎿ read_file')}`, width));
|
|
78
|
+
lines.push(surfaceRow(p.toolPanelBg, ` ${fg(p.fgMuted, 'reading src/index.ts (1.2k lines)')}`, width));
|
|
79
|
+
lines.push(canvas(''));
|
|
80
|
+
// Code block (canvasSubtle surface, bordered).
|
|
81
|
+
lines.push(surfaceRow(p.canvasSubtle, ` ${fg(p.fgMuted, '┌─ code ──────────────')}`, width));
|
|
82
|
+
lines.push(surfaceRow(p.canvasSubtle, ` ${fg(p.fgDefault, '│ const x = 42;')}`, width));
|
|
83
|
+
lines.push(surfaceRow(p.canvasSubtle, ` ${fg(p.fgDefault, '│ console.log(x * 2);')}`, width));
|
|
84
|
+
lines.push(surfaceRow(p.canvasSubtle, ` ${fg(p.fgMuted, '└─────────────────────')}`, width));
|
|
85
|
+
lines.push(canvas(''));
|
|
86
|
+
// Assistant reply.
|
|
87
|
+
lines.push(canvas(` ${fg(p.fgDefault, '结果:x * 2 = 84。')} `));
|
|
88
|
+
lines.push(canvas(''));
|
|
89
|
+
// Input dock placeholder (inset surface).
|
|
90
|
+
lines.push(surfaceRow(p.canvasInset, ` ${fg(p.fgMuted, 'Type a message…')}`, width));
|
|
91
|
+
return lines;
|
|
92
|
+
}
|
|
93
|
+
/** `auto` selected: show a short hint in the current theme instead of a mock. */
|
|
94
|
+
renderAuto(width) {
|
|
95
|
+
const p = this.getTheme().palette;
|
|
96
|
+
const fg = (hex, text) => ansiFg(hex) + text + RESET;
|
|
97
|
+
const bold = (hex, text) => ansiFg(hex) + BOLD + text + RESET;
|
|
98
|
+
const lines = [];
|
|
99
|
+
lines.push(surfaceRow(p.canvasInset, ` ${bold(p.accent, 'auto')} `, width));
|
|
100
|
+
lines.push(surfaceRow(p.canvas, '', width));
|
|
101
|
+
lines.push(surfaceRow(p.canvas, ` ${fg(p.fgDefault, 'Theme follows the terminal.')}`, width));
|
|
102
|
+
lines.push(surfaceRow(p.canvas, ` ${fg(p.fgMuted, 'Pick light/dark or a named theme to preview it.')}`, width));
|
|
103
|
+
lines.push(surfaceRow(p.canvas, '', width));
|
|
104
|
+
lines.push(surfaceRow(p.canvasInset, ` ${fg(p.fgMuted, 'Type a message…')}`, width));
|
|
105
|
+
return lines;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Build the picker rows — a flat two-level list: `auto` first, then every
|
|
110
|
+
* registered theme. The two GitHub defaults lead the theme block (they are
|
|
111
|
+
* the out-of-the-box choices and keep the e2e navigation stable), user
|
|
112
|
+
* themes follow, the remaining built-ins come last. The old `light`/`dark`
|
|
113
|
+
* alias rows are gone — those names stay valid as stored preferences (they
|
|
114
|
+
* resolve through resolveTheme's alias handling) but no longer get their own
|
|
115
|
+
* rows.
|
|
116
|
+
*/
|
|
117
|
+
export function themePickerRows(options) {
|
|
118
|
+
const rows = [
|
|
119
|
+
{ value: 'auto', label: 'auto', description: 'follow the terminal light/dark signal' },
|
|
120
|
+
];
|
|
121
|
+
const themes = options?.themes;
|
|
122
|
+
if (themes === undefined) {
|
|
123
|
+
// No registry (the legacy no-argument mount): just the two defaults.
|
|
124
|
+
rows.push({ value: 'github-light', label: 'github-light', description: 'GitHub light palette', palette: githubLight }, { value: 'github-dark', label: 'github-dark', description: 'GitHub dark palette', palette: githubDark });
|
|
125
|
+
return rows;
|
|
126
|
+
}
|
|
127
|
+
const userNames = options?.userNames;
|
|
128
|
+
const row = (name, palette, description) => ({
|
|
129
|
+
value: name,
|
|
130
|
+
label: name,
|
|
131
|
+
description,
|
|
132
|
+
palette,
|
|
133
|
+
});
|
|
134
|
+
// The two defaults keep their classic descriptions (the e2e suite asserts
|
|
135
|
+
// the "GitHub light/dark palette" texts); other rows just carry their
|
|
136
|
+
// origin — the name is already in the Theme column.
|
|
137
|
+
// A user theme overriding a default id shows its own palette under the
|
|
138
|
+
// default's name (discoverThemes merges user-over-builtin).
|
|
139
|
+
const origin = (name) => userNames?.has(name) === true ? 'user theme' : 'built-in';
|
|
140
|
+
rows.push(row('github-light', themes.get('github-light') ?? githubLight, 'GitHub light palette'), row('github-dark', themes.get('github-dark') ?? githubDark, 'GitHub dark palette'));
|
|
141
|
+
// Reserved names that resolveTheme handles specially never get rows.
|
|
142
|
+
const reserved = new Set(['auto', 'light', 'dark', 'github-light', 'github-dark']);
|
|
143
|
+
for (const [name, palette] of themes) {
|
|
144
|
+
if (reserved.has(name))
|
|
145
|
+
continue;
|
|
146
|
+
rows.push(row(name, palette, origin(name)));
|
|
147
|
+
}
|
|
148
|
+
return rows;
|
|
149
|
+
}
|
|
150
|
+
/** Terminal width at which the picker switches from stacked to side-by-side. */
|
|
151
|
+
export const DUAL_PANE_MIN_COLUMNS = 100;
|
|
152
|
+
/** Floor of the left list pane in dual-pane mode. */
|
|
153
|
+
export const LEFT_PANE_MIN_COLUMNS = 30;
|
|
154
|
+
/**
|
|
155
|
+
* A one-column divider between the theme list and the preview pane, so the
|
|
156
|
+
* two panes read as distinct surfaces instead of abutting columns.
|
|
157
|
+
*/
|
|
158
|
+
class Divider {
|
|
159
|
+
render() {
|
|
160
|
+
return ['│'];
|
|
161
|
+
}
|
|
162
|
+
invalidate() { }
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Content-row budget inside the framed overlay: the overlay is capped at 75%
|
|
166
|
+
* of the terminal, the FramedOverlay adds 4 chrome rows (top/bottom borders +
|
|
167
|
+
* a blank spacer each), and the preview pane renders exactly its own height.
|
|
168
|
+
* Testable; `rows` injected.
|
|
169
|
+
*/
|
|
170
|
+
export function overlayContentBudget(rows = process.stdout.rows) {
|
|
171
|
+
return Math.max(6, Math.floor((rows ?? 24) * 0.85) - 4);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* The theme picker overlay: a left TablePanel of themes + a right preview
|
|
175
|
+
* pane that repaints with the SELECTED theme's palette on every cursor move
|
|
176
|
+
* (history.ts's pattern — compare selectedRow before/after handleInput, an
|
|
177
|
+
* explicit action, never the render path). Side-by-side on wide terminals
|
|
178
|
+
* (HStack), stacked on narrow ones (VStack). Renders exactly
|
|
179
|
+
* `overlayContentBudget()` rows.
|
|
180
|
+
*/
|
|
181
|
+
export class ThemePickerOverlay {
|
|
182
|
+
list;
|
|
183
|
+
preview;
|
|
184
|
+
rows;
|
|
185
|
+
onPick;
|
|
186
|
+
footer;
|
|
187
|
+
closed = false;
|
|
188
|
+
constructor(theme, rows, preselect, onPick, getTheme) {
|
|
189
|
+
this.rows = rows;
|
|
190
|
+
this.onPick = onPick;
|
|
191
|
+
this.footer = '↑↓ navigate · Enter select · Esc back';
|
|
192
|
+
this.preview = new ThemePreviewPane(getTheme);
|
|
193
|
+
this.preview.setRow(rows[Math.max(0, preselect)]);
|
|
194
|
+
this.list = new TablePanel(theme, {
|
|
195
|
+
title: '● Theme',
|
|
196
|
+
columns: [
|
|
197
|
+
{ key: 'theme', title: 'Theme', width: Math.max(5, ...rows.map(row => row.label.length)) },
|
|
198
|
+
{ key: 'description', title: 'Description', flex: true },
|
|
199
|
+
],
|
|
200
|
+
rows,
|
|
201
|
+
renderCell: (row, column) => column.key === 'description' ? row.description : row.label,
|
|
202
|
+
preselect,
|
|
203
|
+
onSelect: row => this.finish(row.value),
|
|
204
|
+
onCancel: () => this.finish(undefined),
|
|
205
|
+
footer: this.footer,
|
|
206
|
+
maxVisible: 12,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
invalidate() {
|
|
210
|
+
this.list.invalidate();
|
|
211
|
+
this.preview.invalidate();
|
|
212
|
+
}
|
|
213
|
+
render(width) {
|
|
214
|
+
const budget = overlayContentBudget();
|
|
215
|
+
let lines;
|
|
216
|
+
if (width >= DUAL_PANE_MIN_COLUMNS) {
|
|
217
|
+
// Left pane keeps a ~40% column floor (same as the history browser);
|
|
218
|
+
// a one-column divider separates the panes; the preview grows into
|
|
219
|
+
// the rest.
|
|
220
|
+
const leftWidth = Math.max(LEFT_PANE_MIN_COLUMNS, Math.floor(width * 0.4));
|
|
221
|
+
const stack = new HStack([
|
|
222
|
+
{ component: this.list, basis: leftWidth, shrink: 1, minSize: LEFT_PANE_MIN_COLUMNS },
|
|
223
|
+
{ component: new Divider(), basis: 1, grow: 0, shrink: 0 },
|
|
224
|
+
{ component: this.preview, basis: 0, grow: 1, minSize: 24 },
|
|
225
|
+
]);
|
|
226
|
+
lines = stack.render(width);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
// Stacked: the list keeps its intrinsic height; the preview pane gets
|
|
230
|
+
// the remaining budget rows so its mock page stays visible (a fixed
|
|
231
|
+
// basis would overflow the budget on short terminals).
|
|
232
|
+
const listRows = Math.min(12, Math.max(3, budget - 10));
|
|
233
|
+
const stack = new VStack([
|
|
234
|
+
{ component: this.list, basis: listRows, grow: 0, shrink: 0 },
|
|
235
|
+
{ component: this.preview, basis: Math.max(6, budget - listRows), grow: 0, shrink: 0 },
|
|
236
|
+
]);
|
|
237
|
+
lines = stack.render(width);
|
|
238
|
+
}
|
|
239
|
+
if (lines.length > budget)
|
|
240
|
+
return lines.slice(0, budget);
|
|
241
|
+
while (lines.length < budget)
|
|
242
|
+
lines.push('');
|
|
243
|
+
return lines;
|
|
244
|
+
}
|
|
245
|
+
handleInput(data) {
|
|
246
|
+
if (this.closed)
|
|
247
|
+
return;
|
|
248
|
+
const before = this.list.selectedRow();
|
|
249
|
+
this.list.handleInput(data);
|
|
250
|
+
const selected = this.list.selectedRow();
|
|
251
|
+
if (selected !== undefined && selected.value !== before?.value) {
|
|
252
|
+
this.preview.setRow(selected);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
finish(value) {
|
|
256
|
+
if (this.closed)
|
|
257
|
+
return;
|
|
258
|
+
this.closed = true;
|
|
259
|
+
this.onPick(value);
|
|
260
|
+
}
|
|
261
|
+
/** The currently selected row (test seam). */
|
|
262
|
+
selectedValue() {
|
|
263
|
+
return this.list.selectedRow()?.value;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
//# sourceMappingURL=theme-preview.js.map
|