toolcraft-openapi 0.0.151 → 0.0.153
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/dist/composition.json +2 -2
- package/node_modules/toolcraft-design/README.md +30 -0
- package/node_modules/toolcraft-design/dist/dashboard/terminal-width.d.ts +1 -0
- package/node_modules/toolcraft-design/dist/dashboard/terminal-width.js +18 -1
- package/node_modules/toolcraft-design/dist/explorer/actions.d.ts +1 -1
- package/node_modules/toolcraft-design/dist/explorer/actions.js +22 -4
- package/node_modules/toolcraft-design/dist/explorer/filter.js +1 -1
- package/node_modules/toolcraft-design/dist/explorer/index.d.ts +2 -4
- package/node_modules/toolcraft-design/dist/explorer/index.js +1 -2
- package/node_modules/toolcraft-design/dist/explorer/jobs.d.ts +2 -2
- package/node_modules/toolcraft-design/dist/explorer/jobs.js +3 -5
- package/node_modules/toolcraft-design/dist/explorer/keymap.d.ts +13 -3
- package/node_modules/toolcraft-design/dist/explorer/keymap.js +89 -348
- package/node_modules/toolcraft-design/dist/explorer/layout.d.ts +1 -0
- package/node_modules/toolcraft-design/dist/explorer/layout.js +14 -4
- package/node_modules/toolcraft-design/dist/explorer/reducer.js +58 -9
- package/node_modules/toolcraft-design/dist/explorer/render/detail.d.ts +1 -1
- package/node_modules/toolcraft-design/dist/explorer/render/detail.js +43 -7
- package/node_modules/toolcraft-design/dist/explorer/render/footer.d.ts +1 -1
- package/node_modules/toolcraft-design/dist/explorer/render/footer.js +6 -3
- package/node_modules/toolcraft-design/dist/explorer/render/header.d.ts +1 -1
- package/node_modules/toolcraft-design/dist/explorer/render/header.js +6 -2
- package/node_modules/toolcraft-design/dist/explorer/render/index.d.ts +1 -1
- package/node_modules/toolcraft-design/dist/explorer/render/index.js +2 -1
- package/node_modules/toolcraft-design/dist/explorer/render/list.d.ts +16 -2
- package/node_modules/toolcraft-design/dist/explorer/render/list.js +13 -34
- package/node_modules/toolcraft-design/dist/explorer/render/modal.d.ts +1 -1
- package/node_modules/toolcraft-design/dist/explorer/render/pane.d.ts +5 -2
- package/node_modules/toolcraft-design/dist/explorer/render/pane.js +9 -6
- package/node_modules/toolcraft-design/dist/explorer/render/test-fixtures.js +2 -3
- package/node_modules/toolcraft-design/dist/explorer/runtime.js +68 -63
- package/node_modules/toolcraft-design/dist/explorer/runtime.test-helpers.d.ts +23 -37
- package/node_modules/toolcraft-design/dist/explorer/runtime.test-helpers.js +32 -83
- package/node_modules/toolcraft-design/dist/explorer/state.d.ts +47 -5
- package/node_modules/toolcraft-design/dist/explorer/state.js +56 -12
- package/node_modules/toolcraft-design/dist/index.d.ts +8 -2
- package/node_modules/toolcraft-design/dist/index.js +5 -1
- package/node_modules/toolcraft-design/dist/screen/ansi-text.d.ts +2 -0
- package/node_modules/toolcraft-design/dist/screen/ansi-text.js +35 -0
- package/node_modules/toolcraft-design/dist/screen/screen.d.ts +40 -0
- package/node_modules/toolcraft-design/dist/screen/screen.js +122 -0
- package/node_modules/toolcraft-design/dist/screen/style.d.ts +16 -0
- package/node_modules/toolcraft-design/dist/screen/style.js +55 -0
- package/node_modules/toolcraft-design/dist/terminal/driver.d.ts +33 -0
- package/node_modules/toolcraft-design/dist/terminal/driver.js +61 -0
- package/node_modules/toolcraft-design/dist/terminal/input.d.ts +25 -0
- package/node_modules/toolcraft-design/dist/terminal/input.js +181 -0
- package/node_modules/toolcraft-design/dist/terminal/output.d.ts +8 -0
- package/node_modules/toolcraft-design/dist/terminal/output.js +42 -0
- package/node_modules/toolcraft-schema/package.json +1 -1
- package/package.json +3 -3
- package/node_modules/toolcraft-design/dist/explorer/two-pane.d.ts +0 -102
- package/node_modules/toolcraft-design/dist/explorer/two-pane.js +0 -510
- package/node_modules/toolcraft-design/dist/run-two-pane-explorer.d.ts +0 -2
- package/node_modules/toolcraft-design/dist/run-two-pane-explorer.js +0 -1
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { Screen } from "../screen/screen.js";
|
|
2
|
+
import { appendFileSync } from "node:fs";
|
|
3
|
+
import { createTerminalDriver } from "../terminal/driver.js";
|
|
3
4
|
import { createDetailJobs } from "./jobs.js";
|
|
4
5
|
import { computeExplorerLayout } from "./layout.js";
|
|
5
6
|
import { renderExplorer } from "./render/index.js";
|
|
6
7
|
import { step } from "./reducer.js";
|
|
7
|
-
import { createInitialState, REGION_ALL, REGION_MODAL, REGION_TOAST } from "./state.js";
|
|
8
|
+
import { createInitialState, REGION_ALL, REGION_MODAL, REGION_TOAST, normalizeExplorerConfig } from "./state.js";
|
|
8
9
|
const TOAST_MS = 2500;
|
|
9
10
|
export async function runExplorer(config) {
|
|
10
11
|
if (process.stdout.isTTY !== true) {
|
|
@@ -15,10 +16,10 @@ export async function runExplorer(config) {
|
|
|
15
16
|
return runtime.run();
|
|
16
17
|
}
|
|
17
18
|
class ExplorerRuntime {
|
|
18
|
-
config;
|
|
19
19
|
driver;
|
|
20
|
+
config;
|
|
20
21
|
state;
|
|
21
|
-
|
|
22
|
+
screen;
|
|
22
23
|
detailJobs;
|
|
23
24
|
runtimeHandles;
|
|
24
25
|
pendingEffects = new Set();
|
|
@@ -28,11 +29,14 @@ class ExplorerRuntime {
|
|
|
28
29
|
rowsRequestToken = 0;
|
|
29
30
|
reorderToken = 0;
|
|
30
31
|
stopped = false;
|
|
32
|
+
renderScheduled = false;
|
|
33
|
+
tracePath = process.env.POE_CODE_TUI_TRACE;
|
|
31
34
|
settle;
|
|
32
35
|
constructor(config, driver) {
|
|
33
|
-
this.config = config;
|
|
34
36
|
this.driver = driver;
|
|
35
|
-
this.
|
|
37
|
+
this.config = normalizeExplorerConfig(config);
|
|
38
|
+
this.state = createInitialState(this.config, driver.getSize());
|
|
39
|
+
this.screen = new Screen(driver.getSize());
|
|
36
40
|
this.detailJobs = createDetailJobs((event) => {
|
|
37
41
|
if (event.type === "detailLoaded") {
|
|
38
42
|
this.loadDetailContent(event.rowId, event.token, event.items);
|
|
@@ -69,7 +73,7 @@ class ExplorerRuntime {
|
|
|
69
73
|
// Seed list/detail before first paint so callers with known rows avoid an empty flash.
|
|
70
74
|
this.dispatch({ type: "rowsLoaded", rows: this.config.initialRows });
|
|
71
75
|
}
|
|
72
|
-
this.
|
|
76
|
+
this.renderNow();
|
|
73
77
|
this.loadRows().catch((error) => {
|
|
74
78
|
this.fail(error);
|
|
75
79
|
});
|
|
@@ -80,13 +84,9 @@ class ExplorerRuntime {
|
|
|
80
84
|
});
|
|
81
85
|
}
|
|
82
86
|
startTerminal() {
|
|
83
|
-
this.driver.
|
|
84
|
-
this.driver.enterAltScreen();
|
|
85
|
-
this.driver.disableLineWrap();
|
|
86
|
-
this.driver.hideCursor();
|
|
87
|
+
this.driver.start();
|
|
87
88
|
this.subscribeKeypress();
|
|
88
|
-
this.unsubscribeResize = this.driver.onResize(() => {
|
|
89
|
-
const size = this.driver.getSize();
|
|
89
|
+
this.unsubscribeResize = this.driver.onResize((size) => {
|
|
90
90
|
this.dispatch({ type: "resize", cols: size.cols, rows: size.rows });
|
|
91
91
|
});
|
|
92
92
|
}
|
|
@@ -94,8 +94,19 @@ class ExplorerRuntime {
|
|
|
94
94
|
if (this.unsubscribeKeypress !== undefined) {
|
|
95
95
|
return;
|
|
96
96
|
}
|
|
97
|
-
this.unsubscribeKeypress = this.driver.
|
|
98
|
-
this.
|
|
97
|
+
this.unsubscribeKeypress = this.driver.onEvent((event) => {
|
|
98
|
+
this.trace("input", { event });
|
|
99
|
+
if (event.type === "paste") {
|
|
100
|
+
for (const ch of event.text.replaceAll("\n", "").replaceAll("\r", "")) {
|
|
101
|
+
this.dispatch({ type: "key", key: { ch, name: ch, ctrl: false, meta: false, shift: false } });
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (event.type === "wheel") {
|
|
106
|
+
this.dispatch({ type: "key", key: { name: event.direction, ctrl: false, meta: false, shift: false } });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
this.dispatch({ type: "key", key: { name: event.name, ch: event.ch, ctrl: event.ctrl, meta: event.alt, shift: event.shift } });
|
|
99
110
|
});
|
|
100
111
|
}
|
|
101
112
|
pauseKeypress() {
|
|
@@ -120,13 +131,13 @@ class ExplorerRuntime {
|
|
|
120
131
|
const previousState = this.state;
|
|
121
132
|
const next = step(this.state, event, this.runtimeHandles);
|
|
122
133
|
this.state = next.state;
|
|
123
|
-
this.
|
|
134
|
+
this.scheduleRender();
|
|
124
135
|
this.applyEffects(next.effects, previousState);
|
|
125
136
|
}
|
|
126
137
|
applyEffects(effects, previousState) {
|
|
127
138
|
for (const effect of effects) {
|
|
128
139
|
if (effect.type === "renderDetail") {
|
|
129
|
-
this.renderDetail(effect.rowId);
|
|
140
|
+
this.renderDetail(effect.rowId, effect.token);
|
|
130
141
|
continue;
|
|
131
142
|
}
|
|
132
143
|
if (effect.type === "persistOrder") {
|
|
@@ -142,7 +153,7 @@ class ExplorerRuntime {
|
|
|
142
153
|
}
|
|
143
154
|
}
|
|
144
155
|
}
|
|
145
|
-
renderDetail(rowId) {
|
|
156
|
+
renderDetail(rowId, token) {
|
|
146
157
|
const row = this.state.rows.find((candidate) => candidate.id === rowId);
|
|
147
158
|
if (row === undefined) {
|
|
148
159
|
return;
|
|
@@ -150,10 +161,11 @@ class ExplorerRuntime {
|
|
|
150
161
|
const layout = computeExplorerLayout({
|
|
151
162
|
cols: this.state.size.cols,
|
|
152
163
|
rows: this.state.size.rows,
|
|
153
|
-
detailHidden: this.state.layout === "narrow-list-only" || this.state.layout === "too-narrow"
|
|
164
|
+
detailHidden: this.state.layout === "narrow-list-only" || this.state.layout === "too-narrow",
|
|
165
|
+
focused: this.state.focused
|
|
154
166
|
});
|
|
155
167
|
const reloadDetail = () => this.reloadFocusedDetail(rowId);
|
|
156
|
-
void this.detailJobs.schedule(rowId, (ctx) => this.config.detail.items(row, { ...ctx, reloadDetail }), {
|
|
168
|
+
void this.detailJobs.schedule(rowId, token, (ctx) => this.config.detail.items(row, { ...ctx, reloadDetail }), {
|
|
157
169
|
width: layout.detail.width,
|
|
158
170
|
height: layout.detail.height,
|
|
159
171
|
row,
|
|
@@ -169,7 +181,9 @@ class ExplorerRuntime {
|
|
|
169
181
|
if (rowId !== undefined && rowId !== focusedId) {
|
|
170
182
|
return;
|
|
171
183
|
}
|
|
172
|
-
this.
|
|
184
|
+
const token = this.state.detail.token + 1;
|
|
185
|
+
this.state = { ...this.state, detail: { ...this.state.detail, token } };
|
|
186
|
+
this.renderDetail(focusedId, token);
|
|
173
187
|
}
|
|
174
188
|
loadDetailContent(rowId, token, items) {
|
|
175
189
|
const row = this.state.rows.find((candidate) => candidate.id === rowId);
|
|
@@ -179,7 +193,8 @@ class ExplorerRuntime {
|
|
|
179
193
|
const layout = computeExplorerLayout({
|
|
180
194
|
cols: this.state.size.cols,
|
|
181
195
|
rows: this.state.size.rows,
|
|
182
|
-
detailHidden: this.state.layout === "narrow-list-only" || this.state.layout === "too-narrow"
|
|
196
|
+
detailHidden: this.state.layout === "narrow-list-only" || this.state.layout === "too-narrow",
|
|
197
|
+
focused: this.state.focused
|
|
183
198
|
});
|
|
184
199
|
const context = {
|
|
185
200
|
width: layout.detail.width,
|
|
@@ -243,19 +258,15 @@ class ExplorerRuntime {
|
|
|
243
258
|
}
|
|
244
259
|
async suspendAnd(fn) {
|
|
245
260
|
this.pauseKeypress();
|
|
246
|
-
this.
|
|
247
|
-
this.driver.
|
|
248
|
-
this.driver.showCursor();
|
|
249
|
-
this.driver.exitRawMode();
|
|
261
|
+
this.state = { ...this.state, suspended: true };
|
|
262
|
+
this.driver.stop();
|
|
250
263
|
try {
|
|
251
264
|
return await fn();
|
|
252
265
|
}
|
|
253
266
|
finally {
|
|
254
267
|
if (!this.stopped) {
|
|
255
|
-
this.driver.
|
|
256
|
-
this.
|
|
257
|
-
this.driver.disableLineWrap();
|
|
258
|
-
this.driver.hideCursor();
|
|
268
|
+
this.driver.start();
|
|
269
|
+
this.state = { ...this.state, suspended: false };
|
|
259
270
|
this.subscribeKeypress();
|
|
260
271
|
const size = this.driver.getSize();
|
|
261
272
|
this.dispatch({
|
|
@@ -284,7 +295,7 @@ class ExplorerRuntime {
|
|
|
284
295
|
},
|
|
285
296
|
dirty: REGION_MODAL
|
|
286
297
|
};
|
|
287
|
-
this.
|
|
298
|
+
this.scheduleRender();
|
|
288
299
|
});
|
|
289
300
|
}
|
|
290
301
|
showToast(message, tone = "info") {
|
|
@@ -299,7 +310,7 @@ class ExplorerRuntime {
|
|
|
299
310
|
toast: { message, tone, expiresAt: Date.now() + TOAST_MS },
|
|
300
311
|
dirty: REGION_TOAST
|
|
301
312
|
};
|
|
302
|
-
this.
|
|
313
|
+
this.scheduleRender();
|
|
303
314
|
this.toastTimer = setTimeout(() => {
|
|
304
315
|
this.dispatch({ type: "toastExpired" });
|
|
305
316
|
}, TOAST_MS);
|
|
@@ -307,20 +318,29 @@ class ExplorerRuntime {
|
|
|
307
318
|
currentRow() {
|
|
308
319
|
return this.state.rows[this.state.filtered[this.state.cursor] ?? -1];
|
|
309
320
|
}
|
|
310
|
-
|
|
311
|
-
if (this.stopped) {
|
|
321
|
+
scheduleRender() {
|
|
322
|
+
if (this.stopped || this.state.suspended || this.renderScheduled) {
|
|
312
323
|
return;
|
|
313
324
|
}
|
|
325
|
+
this.renderScheduled = true;
|
|
326
|
+
setImmediate(() => {
|
|
327
|
+
this.renderScheduled = false;
|
|
328
|
+
this.renderNow();
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
renderNow() {
|
|
332
|
+
if (this.stopped || this.state.suspended)
|
|
333
|
+
return;
|
|
314
334
|
const size = this.driver.getSize();
|
|
315
335
|
if (size.cols !== this.state.size.cols || size.rows !== this.state.size.rows) {
|
|
316
336
|
this.state = step(this.state, { type: "resize", cols: size.cols, rows: size.rows }, this.runtimeHandles).state;
|
|
317
337
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
this.driver.
|
|
323
|
-
this.
|
|
338
|
+
if (this.screen.width !== this.state.size.cols || this.screen.height !== this.state.size.rows)
|
|
339
|
+
this.screen.resize(this.state.size);
|
|
340
|
+
renderExplorer({ ...this.state, dirty: REGION_ALL }, this.screen);
|
|
341
|
+
const frame = this.screen.flush();
|
|
342
|
+
this.driver.writeFrame(frame);
|
|
343
|
+
this.trace("frame", { bytes: Buffer.byteLength(frame), cols: this.state.size.cols, rows: this.state.size.rows });
|
|
324
344
|
this.state = { ...this.state, dirty: 0 };
|
|
325
345
|
}
|
|
326
346
|
track(promise) {
|
|
@@ -340,8 +360,8 @@ class ExplorerRuntime {
|
|
|
340
360
|
if (this.toastTimer !== undefined) {
|
|
341
361
|
clearTimeout(this.toastTimer);
|
|
342
362
|
}
|
|
343
|
-
this.driver.
|
|
344
|
-
Promise.
|
|
363
|
+
this.driver.stop();
|
|
364
|
+
Promise.allSettled([...this.pendingEffects])
|
|
345
365
|
.then(() => after?.())
|
|
346
366
|
.then(() => {
|
|
347
367
|
this.settle?.resolve(result);
|
|
@@ -353,28 +373,13 @@ class ExplorerRuntime {
|
|
|
353
373
|
fail(error) {
|
|
354
374
|
if (!this.stopped) {
|
|
355
375
|
this.stopped = true;
|
|
356
|
-
this.driver.
|
|
376
|
+
this.driver.stop();
|
|
357
377
|
}
|
|
358
378
|
this.settle?.reject(error);
|
|
359
379
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
for (let x = 0; x < buffer.width; x += 1) {
|
|
365
|
-
const cell = buffer.get(x, y);
|
|
366
|
-
next.put(x, y, cell.ch, cell.style);
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
return next;
|
|
370
|
-
}
|
|
371
|
-
function changesToAnsi(changes) {
|
|
372
|
-
let output = "";
|
|
373
|
-
for (const change of changes) {
|
|
374
|
-
output += `${cursorPositionAnsi(change.x, change.y)}${cellToAnsi(change.cell)}`;
|
|
380
|
+
trace(type, fields) {
|
|
381
|
+
if (this.tracePath === undefined || this.tracePath.length === 0)
|
|
382
|
+
return;
|
|
383
|
+
appendFileSync(this.tracePath, `${JSON.stringify({ type, timestamp: new Date().toISOString(), ...fields })}\n`);
|
|
375
384
|
}
|
|
376
|
-
return output;
|
|
377
|
-
}
|
|
378
|
-
function cursorPositionAnsi(x, y) {
|
|
379
|
-
return `\u001b[${Math.max(1, y + 1)};${Math.max(1, x + 1)}H`;
|
|
380
385
|
}
|
|
@@ -1,50 +1,36 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { KeypressEvent, TerminalDriver } from "../dashboard/terminal.js";
|
|
1
|
+
import type { TerminalDriver, TerminalInputEvent } from "../terminal/driver.js";
|
|
3
2
|
export declare class FakeTerminalDriver implements TerminalDriver {
|
|
4
3
|
private cols;
|
|
5
4
|
private rows;
|
|
6
|
-
readonly keyQueue: KeypressEvent[];
|
|
7
5
|
readonly writes: string[];
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}>>;
|
|
13
|
-
rawMode: boolean;
|
|
14
|
-
altScreen: boolean;
|
|
15
|
-
lineWrap: boolean;
|
|
16
|
-
cursorVisible: boolean;
|
|
17
|
-
destroyed: boolean;
|
|
18
|
-
enterAltScreenCount: number;
|
|
19
|
-
exitAltScreenCount: number;
|
|
20
|
-
enterRawModeCount: number;
|
|
21
|
-
exitRawModeCount: number;
|
|
22
|
-
private readonly keypressHandlers;
|
|
6
|
+
started: boolean;
|
|
7
|
+
startCount: number;
|
|
8
|
+
stopCount: number;
|
|
9
|
+
private readonly eventHandlers;
|
|
23
10
|
private readonly resizeHandlers;
|
|
24
11
|
constructor(cols?: number, rows?: number);
|
|
25
12
|
get output(): string;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
flush(changes: Array<{
|
|
37
|
-
x: number;
|
|
38
|
-
y: number;
|
|
39
|
-
cell: Cell;
|
|
40
|
-
}>): void;
|
|
13
|
+
get destroyed(): boolean;
|
|
14
|
+
get altScreen(): boolean;
|
|
15
|
+
get enterAltScreenCount(): number;
|
|
16
|
+
start(): void;
|
|
17
|
+
stop(): void;
|
|
18
|
+
onEvent(handler: (event: TerminalInputEvent) => void): () => void;
|
|
19
|
+
onResize(handler: (size: {
|
|
20
|
+
cols: number;
|
|
21
|
+
rows: number;
|
|
22
|
+
}) => void): () => void;
|
|
41
23
|
getSize(): {
|
|
42
24
|
cols: number;
|
|
43
25
|
rows: number;
|
|
44
26
|
};
|
|
27
|
+
writeFrame(ansi: string): void;
|
|
45
28
|
resize(cols: number, rows: number): void;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
29
|
+
press(key: {
|
|
30
|
+
name?: string;
|
|
31
|
+
ch?: string;
|
|
32
|
+
ctrl: boolean;
|
|
33
|
+
meta: boolean;
|
|
34
|
+
shift: boolean;
|
|
35
|
+
}): void;
|
|
50
36
|
}
|
|
@@ -1,101 +1,50 @@
|
|
|
1
1
|
export class FakeTerminalDriver {
|
|
2
2
|
cols;
|
|
3
3
|
rows;
|
|
4
|
-
keyQueue = [];
|
|
5
4
|
writes = [];
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
cursorVisible = true;
|
|
11
|
-
destroyed = false;
|
|
12
|
-
enterAltScreenCount = 0;
|
|
13
|
-
exitAltScreenCount = 0;
|
|
14
|
-
enterRawModeCount = 0;
|
|
15
|
-
exitRawModeCount = 0;
|
|
16
|
-
keypressHandlers = new Set();
|
|
5
|
+
started = false;
|
|
6
|
+
startCount = 0;
|
|
7
|
+
stopCount = 0;
|
|
8
|
+
eventHandlers = new Set();
|
|
17
9
|
resizeHandlers = new Set();
|
|
18
10
|
constructor(cols = 120, rows = 24) {
|
|
19
11
|
this.cols = cols;
|
|
20
12
|
this.rows = rows;
|
|
21
13
|
}
|
|
22
|
-
get output() {
|
|
23
|
-
|
|
14
|
+
get output() { return this.writes.join(""); }
|
|
15
|
+
get destroyed() { return !this.started && this.stopCount > 0; }
|
|
16
|
+
get altScreen() { return this.started; }
|
|
17
|
+
get enterAltScreenCount() { return this.startCount; }
|
|
18
|
+
start() { if (!this.started) {
|
|
19
|
+
this.started = true;
|
|
20
|
+
this.startCount += 1;
|
|
21
|
+
} }
|
|
22
|
+
stop() { if (this.started) {
|
|
23
|
+
this.started = false;
|
|
24
|
+
this.stopCount += 1;
|
|
25
|
+
} }
|
|
26
|
+
onEvent(handler) {
|
|
27
|
+
this.eventHandlers.add(handler);
|
|
28
|
+
return () => { this.eventHandlers.delete(handler); };
|
|
24
29
|
}
|
|
25
|
-
|
|
26
|
-
this.
|
|
27
|
-
this.
|
|
28
|
-
}
|
|
29
|
-
exitRawMode() {
|
|
30
|
-
this.rawMode = false;
|
|
31
|
-
this.exitRawModeCount += 1;
|
|
32
|
-
}
|
|
33
|
-
enterAltScreen() {
|
|
34
|
-
this.altScreen = true;
|
|
35
|
-
this.enterAltScreenCount += 1;
|
|
36
|
-
}
|
|
37
|
-
exitAltScreen() {
|
|
38
|
-
this.altScreen = false;
|
|
39
|
-
this.exitAltScreenCount += 1;
|
|
40
|
-
}
|
|
41
|
-
disableLineWrap() {
|
|
42
|
-
this.lineWrap = false;
|
|
43
|
-
}
|
|
44
|
-
enableLineWrap() {
|
|
45
|
-
this.lineWrap = true;
|
|
46
|
-
}
|
|
47
|
-
hideCursor() {
|
|
48
|
-
this.cursorVisible = false;
|
|
49
|
-
}
|
|
50
|
-
showCursor() {
|
|
51
|
-
this.cursorVisible = true;
|
|
52
|
-
}
|
|
53
|
-
moveTo(x, y) {
|
|
54
|
-
this.write(`\u001b[${Math.max(1, y + 1)};${Math.max(1, x + 1)}H`);
|
|
55
|
-
}
|
|
56
|
-
write(text) {
|
|
57
|
-
if (!this.destroyed) {
|
|
58
|
-
this.writes.push(text);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
flush(changes) {
|
|
62
|
-
this.flushes.push(changes);
|
|
63
|
-
}
|
|
64
|
-
getSize() {
|
|
65
|
-
return { cols: this.cols, rows: this.rows };
|
|
30
|
+
onResize(handler) {
|
|
31
|
+
this.resizeHandlers.add(handler);
|
|
32
|
+
return () => { this.resizeHandlers.delete(handler); };
|
|
66
33
|
}
|
|
34
|
+
getSize() { return { cols: this.cols, rows: this.rows }; }
|
|
35
|
+
writeFrame(ansi) { if (this.started)
|
|
36
|
+
this.writes.push(ansi); }
|
|
67
37
|
resize(cols, rows) {
|
|
68
38
|
this.cols = cols;
|
|
69
39
|
this.rows = rows;
|
|
70
|
-
for (const handler of this.resizeHandlers)
|
|
71
|
-
handler();
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
onResize(handler) {
|
|
75
|
-
this.resizeHandlers.add(handler);
|
|
76
|
-
return () => {
|
|
77
|
-
this.resizeHandlers.delete(handler);
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
onKeypress(handler) {
|
|
81
|
-
this.keypressHandlers.add(handler);
|
|
82
|
-
return () => {
|
|
83
|
-
this.keypressHandlers.delete(handler);
|
|
84
|
-
};
|
|
40
|
+
for (const handler of this.resizeHandlers)
|
|
41
|
+
handler(this.getSize());
|
|
85
42
|
}
|
|
86
43
|
press(key) {
|
|
87
|
-
this.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
destroy() {
|
|
93
|
-
this.destroyed = true;
|
|
94
|
-
this.rawMode = false;
|
|
95
|
-
this.lineWrap = true;
|
|
96
|
-
this.altScreen = false;
|
|
97
|
-
this.cursorVisible = true;
|
|
98
|
-
this.keypressHandlers.clear();
|
|
99
|
-
this.resizeHandlers.clear();
|
|
44
|
+
if (!this.started)
|
|
45
|
+
return;
|
|
46
|
+
const event = { type: "key", name: key.name ?? key.ch ?? "", ch: key.ch, ctrl: key.ctrl, alt: key.meta, shift: key.shift };
|
|
47
|
+
for (const handler of this.eventHandlers)
|
|
48
|
+
handler(event);
|
|
100
49
|
}
|
|
101
50
|
}
|
|
@@ -37,7 +37,9 @@ export interface Action<R> {
|
|
|
37
37
|
id: string;
|
|
38
38
|
label: string | (() => string);
|
|
39
39
|
key?: string | string[];
|
|
40
|
+
accelerator?: string;
|
|
40
41
|
predicate?: (ctx: ActionContext<R>) => boolean;
|
|
42
|
+
visible?: (row: Row) => boolean;
|
|
41
43
|
handler: (ctx: ActionContext<R>) => void | Promise<void>;
|
|
42
44
|
destructive?: boolean;
|
|
43
45
|
primary?: boolean;
|
|
@@ -59,6 +61,31 @@ export interface ActionContext<R> {
|
|
|
59
61
|
toast: (msg: string, tone?: Tone) => void;
|
|
60
62
|
confirm: (prompt: string) => Promise<boolean>;
|
|
61
63
|
exit: (after?: () => void | Promise<void>) => void;
|
|
64
|
+
activePane?: PaneRuntimeState;
|
|
65
|
+
inactivePane?: PaneRuntimeState;
|
|
66
|
+
}
|
|
67
|
+
export interface ListPaneConfig {
|
|
68
|
+
id: string;
|
|
69
|
+
kind: "list";
|
|
70
|
+
title: string;
|
|
71
|
+
rows: () => Promise<Row[]>;
|
|
72
|
+
emptyHint?: string;
|
|
73
|
+
multiSelect?: boolean;
|
|
74
|
+
}
|
|
75
|
+
export interface DetailPaneConfig {
|
|
76
|
+
id: string;
|
|
77
|
+
kind: "detail";
|
|
78
|
+
title: string;
|
|
79
|
+
render: (row: Row | undefined, ctx: DetailCtx) => string | Promise<string>;
|
|
80
|
+
}
|
|
81
|
+
export type PaneConfig = ListPaneConfig | DetailPaneConfig;
|
|
82
|
+
export interface PaneRuntimeState {
|
|
83
|
+
id: string;
|
|
84
|
+
title: string;
|
|
85
|
+
rows: Row[];
|
|
86
|
+
cursor: number;
|
|
87
|
+
selected: Set<string>;
|
|
88
|
+
filter: string;
|
|
62
89
|
}
|
|
63
90
|
export interface ReorderContext {
|
|
64
91
|
refresh: () => Promise<void>;
|
|
@@ -66,9 +93,10 @@ export interface ReorderContext {
|
|
|
66
93
|
}
|
|
67
94
|
export interface ExplorerConfig<R> {
|
|
68
95
|
title: string;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
96
|
+
panes?: PaneConfig[];
|
|
97
|
+
rows?: () => Promise<Row[]>;
|
|
98
|
+
refresh?: () => void | Promise<void>;
|
|
99
|
+
detail?: Detail<R>;
|
|
72
100
|
actions: Action<R>[];
|
|
73
101
|
reorder?: {
|
|
74
102
|
onReorder: (orderedIds: string[], ctx?: ReorderContext) => void | Promise<void>;
|
|
@@ -80,6 +108,11 @@ export interface ExplorerConfig<R> {
|
|
|
80
108
|
/** Synchronous first paint rows; still refreshed via `rows()` after start. */
|
|
81
109
|
initialRows?: Row[];
|
|
82
110
|
}
|
|
111
|
+
export type NormalizedExplorerConfig<R> = ExplorerConfig<R> & {
|
|
112
|
+
rows: () => Promise<Row[]>;
|
|
113
|
+
detail: Detail<R>;
|
|
114
|
+
};
|
|
115
|
+
export declare function normalizeExplorerConfig<R>(config: ExplorerConfig<R>): NormalizedExplorerConfig<R>;
|
|
83
116
|
export declare const REGION_HEADER: number;
|
|
84
117
|
export declare const REGION_LIST: number;
|
|
85
118
|
export declare const REGION_DETAIL: number;
|
|
@@ -97,6 +130,7 @@ export interface ExplorerState {
|
|
|
97
130
|
title: string;
|
|
98
131
|
emptyHint: string;
|
|
99
132
|
rows: Row[];
|
|
133
|
+
rowsLoading: boolean;
|
|
100
134
|
filtered: number[];
|
|
101
135
|
matchPositions: Map<number, number[]>;
|
|
102
136
|
cursor: number;
|
|
@@ -106,6 +140,8 @@ export interface ExplorerState {
|
|
|
106
140
|
detail: {
|
|
107
141
|
rowId: string | null;
|
|
108
142
|
items: DetailItem[] | null;
|
|
143
|
+
allItems?: DetailItem[] | null;
|
|
144
|
+
filter?: string;
|
|
109
145
|
cursor: number;
|
|
110
146
|
scroll: number;
|
|
111
147
|
token: number;
|
|
@@ -140,13 +176,19 @@ export interface ExplorerState {
|
|
|
140
176
|
layout: ExplorerLayoutMode;
|
|
141
177
|
bindings: ResolvedBindings;
|
|
142
178
|
actionState: Map<string, ActionStateEntry>;
|
|
179
|
+
suspended: boolean;
|
|
180
|
+
paneDefinitions: Array<{
|
|
181
|
+
id: string;
|
|
182
|
+
title: string;
|
|
183
|
+
kind: "list" | "detail";
|
|
184
|
+
}>;
|
|
143
185
|
}
|
|
144
186
|
export interface ActionStateEntry {
|
|
145
187
|
available: boolean;
|
|
146
188
|
label: string;
|
|
147
189
|
running?: boolean;
|
|
148
190
|
action?: Action<unknown>;
|
|
149
|
-
source?: "row" | "detail";
|
|
191
|
+
source?: "row" | "detail" | "both";
|
|
150
192
|
}
|
|
151
193
|
export declare function createInitialState<R>(config: ExplorerConfig<R>, size: ExplorerSize): ExplorerState;
|
|
152
|
-
export declare function resolveExplorerLayoutMode(cols: number): ExplorerLayoutMode;
|
|
194
|
+
export declare function resolveExplorerLayoutMode(cols: number, rows?: number): ExplorerLayoutMode;
|