terminal-pilot 0.0.50 → 0.0.52
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/assets/jetbrains-mono-400-italic.ttf +0 -0
- package/assets/jetbrains-mono-400-normal.ttf +0 -0
- package/assets/jetbrains-mono-700-italic.ttf +0 -0
- package/assets/jetbrains-mono-700-normal.ttf +0 -0
- package/dist/cli.js +64 -32
- package/dist/cli.js.map +4 -4
- package/dist/commands/close-session.js +26 -0
- package/dist/commands/close-session.js.map +2 -2
- package/dist/commands/create-session.js +26 -0
- package/dist/commands/create-session.js.map +2 -2
- package/dist/commands/daemon-runtime.js.map +1 -1
- package/dist/commands/fill.js +26 -0
- package/dist/commands/fill.js.map +2 -2
- package/dist/commands/get-session.js +26 -0
- package/dist/commands/get-session.js.map +2 -2
- package/dist/commands/index.js +39 -6
- package/dist/commands/index.js.map +4 -4
- package/dist/commands/install.js +42 -3
- package/dist/commands/install.js.map +4 -4
- package/dist/commands/installer.js +16 -3
- package/dist/commands/installer.js.map +4 -4
- package/dist/commands/list-sessions.js +26 -0
- package/dist/commands/list-sessions.js.map +2 -2
- package/dist/commands/press-key.js +26 -0
- package/dist/commands/press-key.js.map +2 -2
- package/dist/commands/read-history.js +26 -0
- package/dist/commands/read-history.js.map +2 -2
- package/dist/commands/read-screen.js +26 -0
- package/dist/commands/read-screen.js.map +2 -2
- package/dist/commands/resize.js +26 -0
- package/dist/commands/resize.js.map +2 -2
- package/dist/commands/runtime.js.map +1 -1
- package/dist/commands/screenshot.js +26 -0
- package/dist/commands/screenshot.js.map +2 -2
- package/dist/commands/send-signal.js +26 -0
- package/dist/commands/send-signal.js.map +2 -2
- package/dist/commands/type.js +26 -0
- package/dist/commands/type.js.map +2 -2
- package/dist/commands/uninstall.js +42 -3
- package/dist/commands/uninstall.js.map +4 -4
- package/dist/commands/wait-for-exit.js +26 -0
- package/dist/commands/wait-for-exit.js.map +2 -2
- package/dist/commands/wait-for.js +26 -0
- package/dist/commands/wait-for.js.map +2 -2
- package/dist/composition.json +1 -1
- package/dist/testing/cli-repl.js +64 -32
- package/dist/testing/cli-repl.js.map +4 -4
- package/dist/testing/qa-cli.js +64 -32
- package/dist/testing/qa-cli.js.map +4 -4
- 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 +3 -2
- package/node_modules/toolcraft-design/dist/explorer/actions.js +50 -4
- package/node_modules/toolcraft-design/dist/explorer/events.d.ts +7 -2
- 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 +116 -16
- package/node_modules/toolcraft-design/dist/explorer/render/detail.d.ts +1 -1
- package/node_modules/toolcraft-design/dist/explorer/render/detail.js +45 -7
- package/node_modules/toolcraft-design/dist/explorer/render/footer.d.ts +1 -1
- package/node_modules/toolcraft-design/dist/explorer/render/footer.js +18 -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/modal.js +11 -11
- 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 +115 -73
- 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 +79 -8
- package/node_modules/toolcraft-design/dist/explorer/state.js +70 -18
- 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 +34 -0
- package/node_modules/toolcraft-design/dist/terminal/driver.js +65 -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 +10 -0
- package/node_modules/toolcraft-design/dist/terminal/output.js +44 -0
- package/package.json +3 -2
- 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,5 +1,8 @@
|
|
|
1
|
-
import { ScreenBuffer } from "../../
|
|
1
|
+
import type { ScreenSurface as ScreenBuffer } from "../../screen/screen.js";
|
|
2
2
|
import type { CellStyle } from "../../dashboard/types.js";
|
|
3
3
|
import type { Rect } from "../layout.js";
|
|
4
|
-
export declare function drawPaneFrame(screen: ScreenBuffer, rect: Rect, title: string, style?: CellStyle
|
|
4
|
+
export declare function drawPaneFrame(screen: ScreenBuffer, rect: Rect, title: string, style?: CellStyle, options?: {
|
|
5
|
+
focused?: boolean;
|
|
6
|
+
indicator?: string;
|
|
7
|
+
}): void;
|
|
5
8
|
export declare function paneBodyRect(rect: Rect): Rect;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { fitToWidth, padEndCells } from "./text.js";
|
|
2
|
-
export function drawPaneFrame(screen, rect, title, style = {}) {
|
|
2
|
+
export function drawPaneFrame(screen, rect, title, style = {}, options = {}) {
|
|
3
3
|
if (rect.width <= 0 || rect.height <= 0) {
|
|
4
4
|
return;
|
|
5
5
|
}
|
|
@@ -10,14 +10,17 @@ export function drawPaneFrame(screen, rect, title, style = {}) {
|
|
|
10
10
|
return;
|
|
11
11
|
}
|
|
12
12
|
const innerWidth = Math.max(0, rect.width - 2);
|
|
13
|
-
const
|
|
14
|
-
|
|
13
|
+
const horizontal = options.focused ? "━" : "─";
|
|
14
|
+
const indicator = options.indicator === undefined ? "" : ` ${options.indicator} `;
|
|
15
|
+
const availableTitle = Math.max(0, innerWidth - indicator.length);
|
|
16
|
+
const titleSegment = padEndCells(`${fitToWidth(`${horizontal} ${title} `, availableTitle, rect.x + 1)}${horizontal.repeat(Math.max(0, availableTitle - fitToWidth(`${horizontal} ${title} `, availableTitle, rect.x + 1).length))}${indicator}`, innerWidth, horizontal, rect.x + 1);
|
|
17
|
+
screen.put(rect.x, rect.y, `${options.focused ? "┏" : "┌"}${titleSegment}${options.focused ? "┓" : "┐"}`, style);
|
|
15
18
|
for (let y = 1; y < rect.height - 1; y += 1) {
|
|
16
|
-
screen.put(rect.x, rect.y + y, "│", style);
|
|
17
|
-
screen.put(rect.x + rect.width - 1, rect.y + y, "│", style);
|
|
19
|
+
screen.put(rect.x, rect.y + y, options.focused ? "┃" : "│", style);
|
|
20
|
+
screen.put(rect.x + rect.width - 1, rect.y + y, options.focused ? "┃" : "│", style);
|
|
18
21
|
}
|
|
19
22
|
if (rect.height > 1) {
|
|
20
|
-
screen.put(rect.x, rect.y + rect.height - 1,
|
|
23
|
+
screen.put(rect.x, rect.y + rect.height - 1, `${options.focused ? "┗" : "└"}${horizontal.repeat(innerWidth)}${options.focused ? "┛" : "┘"}`, style);
|
|
21
24
|
}
|
|
22
25
|
}
|
|
23
26
|
export function paneBodyRect(rect) {
|
|
@@ -126,21 +126,20 @@ function fixtureActions() {
|
|
|
126
126
|
{
|
|
127
127
|
id: "edit",
|
|
128
128
|
label: "edit",
|
|
129
|
-
|
|
129
|
+
accelerator: "e",
|
|
130
130
|
handler: () => undefined,
|
|
131
131
|
showInFooter: true
|
|
132
132
|
},
|
|
133
133
|
{
|
|
134
134
|
id: "archive",
|
|
135
135
|
label: "archive",
|
|
136
|
-
|
|
136
|
+
accelerator: "a",
|
|
137
137
|
handler: () => undefined,
|
|
138
138
|
showInFooter: true
|
|
139
139
|
},
|
|
140
140
|
{
|
|
141
141
|
id: "delete",
|
|
142
142
|
label: "delete",
|
|
143
|
-
key: "d",
|
|
144
143
|
handler: () => undefined,
|
|
145
144
|
destructive: true,
|
|
146
145
|
showInFooter: true
|
|
@@ -1,24 +1,25 @@
|
|
|
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_FOOTER, 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) {
|
|
11
12
|
throw new Error("explorer requires a TTY");
|
|
12
13
|
}
|
|
13
|
-
const driver = createTerminalDriver();
|
|
14
|
+
const driver = createTerminalDriver({ mouse: config.mouse });
|
|
14
15
|
const runtime = new ExplorerRuntime(config, driver);
|
|
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);
|
|
@@ -55,6 +59,7 @@ class ExplorerRuntime {
|
|
|
55
59
|
this.showToast(msg, tone);
|
|
56
60
|
},
|
|
57
61
|
confirm: async (prompt) => this.confirm(prompt),
|
|
62
|
+
promptText: async (options) => this.promptText(options),
|
|
58
63
|
exit: (after) => {
|
|
59
64
|
this.exit(null, after);
|
|
60
65
|
}
|
|
@@ -69,7 +74,7 @@ class ExplorerRuntime {
|
|
|
69
74
|
// Seed list/detail before first paint so callers with known rows avoid an empty flash.
|
|
70
75
|
this.dispatch({ type: "rowsLoaded", rows: this.config.initialRows });
|
|
71
76
|
}
|
|
72
|
-
this.
|
|
77
|
+
this.renderNow();
|
|
73
78
|
this.loadRows().catch((error) => {
|
|
74
79
|
this.fail(error);
|
|
75
80
|
});
|
|
@@ -80,13 +85,9 @@ class ExplorerRuntime {
|
|
|
80
85
|
});
|
|
81
86
|
}
|
|
82
87
|
startTerminal() {
|
|
83
|
-
this.driver.
|
|
84
|
-
this.driver.enterAltScreen();
|
|
85
|
-
this.driver.disableLineWrap();
|
|
86
|
-
this.driver.hideCursor();
|
|
88
|
+
this.driver.start();
|
|
87
89
|
this.subscribeKeypress();
|
|
88
|
-
this.unsubscribeResize = this.driver.onResize(() => {
|
|
89
|
-
const size = this.driver.getSize();
|
|
90
|
+
this.unsubscribeResize = this.driver.onResize((size) => {
|
|
90
91
|
this.dispatch({ type: "resize", cols: size.cols, rows: size.rows });
|
|
91
92
|
});
|
|
92
93
|
}
|
|
@@ -94,8 +95,34 @@ class ExplorerRuntime {
|
|
|
94
95
|
if (this.unsubscribeKeypress !== undefined) {
|
|
95
96
|
return;
|
|
96
97
|
}
|
|
97
|
-
this.unsubscribeKeypress = this.driver.
|
|
98
|
-
this.
|
|
98
|
+
this.unsubscribeKeypress = this.driver.onEvent((event) => {
|
|
99
|
+
this.trace("input", { event });
|
|
100
|
+
if (event.type === "paste") {
|
|
101
|
+
for (const ch of event.text.replaceAll("\n", "").replaceAll("\r", "")) {
|
|
102
|
+
this.dispatch({
|
|
103
|
+
type: "key",
|
|
104
|
+
key: { ch, name: ch, ctrl: false, meta: false, shift: false }
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (event.type === "wheel") {
|
|
110
|
+
this.dispatch({
|
|
111
|
+
type: "key",
|
|
112
|
+
key: { name: event.direction, ctrl: false, meta: false, shift: false }
|
|
113
|
+
});
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
this.dispatch({
|
|
117
|
+
type: "key",
|
|
118
|
+
key: {
|
|
119
|
+
name: event.name,
|
|
120
|
+
ch: event.ch,
|
|
121
|
+
ctrl: event.ctrl,
|
|
122
|
+
meta: event.alt,
|
|
123
|
+
shift: event.shift
|
|
124
|
+
}
|
|
125
|
+
});
|
|
99
126
|
});
|
|
100
127
|
}
|
|
101
128
|
pauseKeypress() {
|
|
@@ -120,13 +147,13 @@ class ExplorerRuntime {
|
|
|
120
147
|
const previousState = this.state;
|
|
121
148
|
const next = step(this.state, event, this.runtimeHandles);
|
|
122
149
|
this.state = next.state;
|
|
123
|
-
this.
|
|
150
|
+
this.scheduleRender();
|
|
124
151
|
this.applyEffects(next.effects, previousState);
|
|
125
152
|
}
|
|
126
153
|
applyEffects(effects, previousState) {
|
|
127
154
|
for (const effect of effects) {
|
|
128
155
|
if (effect.type === "renderDetail") {
|
|
129
|
-
this.renderDetail(effect.rowId);
|
|
156
|
+
this.renderDetail(effect.rowId, effect.token);
|
|
130
157
|
continue;
|
|
131
158
|
}
|
|
132
159
|
if (effect.type === "persistOrder") {
|
|
@@ -142,7 +169,7 @@ class ExplorerRuntime {
|
|
|
142
169
|
}
|
|
143
170
|
}
|
|
144
171
|
}
|
|
145
|
-
renderDetail(rowId) {
|
|
172
|
+
renderDetail(rowId, token) {
|
|
146
173
|
const row = this.state.rows.find((candidate) => candidate.id === rowId);
|
|
147
174
|
if (row === undefined) {
|
|
148
175
|
return;
|
|
@@ -150,10 +177,11 @@ class ExplorerRuntime {
|
|
|
150
177
|
const layout = computeExplorerLayout({
|
|
151
178
|
cols: this.state.size.cols,
|
|
152
179
|
rows: this.state.size.rows,
|
|
153
|
-
detailHidden: this.state.layout === "narrow-list-only" || this.state.layout === "too-narrow"
|
|
180
|
+
detailHidden: this.state.layout === "narrow-list-only" || this.state.layout === "too-narrow",
|
|
181
|
+
focused: this.state.focused
|
|
154
182
|
});
|
|
155
183
|
const reloadDetail = () => this.reloadFocusedDetail(rowId);
|
|
156
|
-
void this.detailJobs.schedule(rowId, (ctx) => this.config.detail.items(row, { ...ctx, reloadDetail }), {
|
|
184
|
+
void this.detailJobs.schedule(rowId, token, (ctx) => this.config.detail.items(row, { ...ctx, reloadDetail }), {
|
|
157
185
|
width: layout.detail.width,
|
|
158
186
|
height: layout.detail.height,
|
|
159
187
|
row,
|
|
@@ -169,7 +197,9 @@ class ExplorerRuntime {
|
|
|
169
197
|
if (rowId !== undefined && rowId !== focusedId) {
|
|
170
198
|
return;
|
|
171
199
|
}
|
|
172
|
-
this.
|
|
200
|
+
const token = this.state.detail.token + 1;
|
|
201
|
+
this.state = { ...this.state, detail: { ...this.state.detail, token } };
|
|
202
|
+
this.renderDetail(focusedId, token);
|
|
173
203
|
}
|
|
174
204
|
loadDetailContent(rowId, token, items) {
|
|
175
205
|
const row = this.state.rows.find((candidate) => candidate.id === rowId);
|
|
@@ -179,7 +209,8 @@ class ExplorerRuntime {
|
|
|
179
209
|
const layout = computeExplorerLayout({
|
|
180
210
|
cols: this.state.size.cols,
|
|
181
211
|
rows: this.state.size.rows,
|
|
182
|
-
detailHidden: this.state.layout === "narrow-list-only" || this.state.layout === "too-narrow"
|
|
212
|
+
detailHidden: this.state.layout === "narrow-list-only" || this.state.layout === "too-narrow",
|
|
213
|
+
focused: this.state.focused
|
|
183
214
|
});
|
|
184
215
|
const context = {
|
|
185
216
|
width: layout.detail.width,
|
|
@@ -243,19 +274,15 @@ class ExplorerRuntime {
|
|
|
243
274
|
}
|
|
244
275
|
async suspendAnd(fn) {
|
|
245
276
|
this.pauseKeypress();
|
|
246
|
-
this.
|
|
247
|
-
this.driver.
|
|
248
|
-
this.driver.showCursor();
|
|
249
|
-
this.driver.exitRawMode();
|
|
277
|
+
this.state = { ...this.state, suspended: true };
|
|
278
|
+
this.driver.stop();
|
|
250
279
|
try {
|
|
251
280
|
return await fn();
|
|
252
281
|
}
|
|
253
282
|
finally {
|
|
254
283
|
if (!this.stopped) {
|
|
255
|
-
this.driver.
|
|
256
|
-
this.
|
|
257
|
-
this.driver.disableLineWrap();
|
|
258
|
-
this.driver.hideCursor();
|
|
284
|
+
this.driver.start();
|
|
285
|
+
this.state = { ...this.state, suspended: false };
|
|
259
286
|
this.subscribeKeypress();
|
|
260
287
|
const size = this.driver.getSize();
|
|
261
288
|
this.dispatch({
|
|
@@ -268,23 +295,40 @@ class ExplorerRuntime {
|
|
|
268
295
|
}
|
|
269
296
|
confirm(prompt) {
|
|
270
297
|
return new Promise((resolve) => {
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
handler: () => undefined
|
|
275
|
-
};
|
|
276
|
-
const row = this.currentRow();
|
|
298
|
+
const options = typeof prompt === "string"
|
|
299
|
+
? { title: "Confirm", message: prompt }
|
|
300
|
+
: prompt;
|
|
277
301
|
this.state = {
|
|
278
302
|
...this.state,
|
|
279
303
|
modal: {
|
|
280
304
|
kind: "confirm",
|
|
281
|
-
|
|
282
|
-
|
|
305
|
+
title: options.title,
|
|
306
|
+
message: options.message,
|
|
307
|
+
confirmLabel: options.confirmLabel ?? "Yes",
|
|
308
|
+
cancelLabel: options.cancelLabel ?? "No",
|
|
309
|
+
destructive: options.destructive ?? false,
|
|
283
310
|
resolver: resolve
|
|
284
311
|
},
|
|
285
|
-
dirty: REGION_MODAL
|
|
312
|
+
dirty: REGION_MODAL | REGION_FOOTER
|
|
286
313
|
};
|
|
287
|
-
this.
|
|
314
|
+
this.scheduleRender();
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
promptText(options) {
|
|
318
|
+
return new Promise((resolve) => {
|
|
319
|
+
this.state = {
|
|
320
|
+
...this.state,
|
|
321
|
+
modal: {
|
|
322
|
+
kind: "input",
|
|
323
|
+
title: options.title,
|
|
324
|
+
label: options.label,
|
|
325
|
+
value: options.initialValue ?? "",
|
|
326
|
+
placeholder: options.placeholder,
|
|
327
|
+
resolver: resolve
|
|
328
|
+
},
|
|
329
|
+
dirty: REGION_MODAL | REGION_FOOTER
|
|
330
|
+
};
|
|
331
|
+
this.scheduleRender();
|
|
288
332
|
});
|
|
289
333
|
}
|
|
290
334
|
showToast(message, tone = "info") {
|
|
@@ -299,7 +343,7 @@ class ExplorerRuntime {
|
|
|
299
343
|
toast: { message, tone, expiresAt: Date.now() + TOAST_MS },
|
|
300
344
|
dirty: REGION_TOAST
|
|
301
345
|
};
|
|
302
|
-
this.
|
|
346
|
+
this.scheduleRender();
|
|
303
347
|
this.toastTimer = setTimeout(() => {
|
|
304
348
|
this.dispatch({ type: "toastExpired" });
|
|
305
349
|
}, TOAST_MS);
|
|
@@ -307,20 +351,33 @@ class ExplorerRuntime {
|
|
|
307
351
|
currentRow() {
|
|
308
352
|
return this.state.rows[this.state.filtered[this.state.cursor] ?? -1];
|
|
309
353
|
}
|
|
310
|
-
|
|
311
|
-
if (this.stopped) {
|
|
354
|
+
scheduleRender() {
|
|
355
|
+
if (this.stopped || this.state.suspended || this.renderScheduled) {
|
|
312
356
|
return;
|
|
313
357
|
}
|
|
358
|
+
this.renderScheduled = true;
|
|
359
|
+
setImmediate(() => {
|
|
360
|
+
this.renderScheduled = false;
|
|
361
|
+
this.renderNow();
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
renderNow() {
|
|
365
|
+
if (this.stopped || this.state.suspended)
|
|
366
|
+
return;
|
|
314
367
|
const size = this.driver.getSize();
|
|
315
368
|
if (size.cols !== this.state.size.cols || size.rows !== this.state.size.rows) {
|
|
316
369
|
this.state = step(this.state, { type: "resize", cols: size.cols, rows: size.rows }, this.runtimeHandles).state;
|
|
317
370
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
this.driver.
|
|
323
|
-
this.
|
|
371
|
+
if (this.screen.width !== this.state.size.cols || this.screen.height !== this.state.size.rows)
|
|
372
|
+
this.screen.resize(this.state.size);
|
|
373
|
+
renderExplorer({ ...this.state, dirty: REGION_ALL }, this.screen);
|
|
374
|
+
const frame = this.screen.flush();
|
|
375
|
+
this.driver.writeFrame(frame);
|
|
376
|
+
this.trace("frame", {
|
|
377
|
+
bytes: Buffer.byteLength(frame),
|
|
378
|
+
cols: this.state.size.cols,
|
|
379
|
+
rows: this.state.size.rows
|
|
380
|
+
});
|
|
324
381
|
this.state = { ...this.state, dirty: 0 };
|
|
325
382
|
}
|
|
326
383
|
track(promise) {
|
|
@@ -340,8 +397,8 @@ class ExplorerRuntime {
|
|
|
340
397
|
if (this.toastTimer !== undefined) {
|
|
341
398
|
clearTimeout(this.toastTimer);
|
|
342
399
|
}
|
|
343
|
-
this.driver.
|
|
344
|
-
Promise.
|
|
400
|
+
this.driver.stop();
|
|
401
|
+
Promise.allSettled([...this.pendingEffects])
|
|
345
402
|
.then(() => after?.())
|
|
346
403
|
.then(() => {
|
|
347
404
|
this.settle?.resolve(result);
|
|
@@ -353,28 +410,13 @@ class ExplorerRuntime {
|
|
|
353
410
|
fail(error) {
|
|
354
411
|
if (!this.stopped) {
|
|
355
412
|
this.stopped = true;
|
|
356
|
-
this.driver.
|
|
413
|
+
this.driver.stop();
|
|
357
414
|
}
|
|
358
415
|
this.settle?.reject(error);
|
|
359
416
|
}
|
|
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)}`;
|
|
417
|
+
trace(type, fields) {
|
|
418
|
+
if (this.tracePath === undefined || this.tracePath.length === 0)
|
|
419
|
+
return;
|
|
420
|
+
appendFileSync(this.tracePath, `${JSON.stringify({ type, timestamp: new Date().toISOString(), ...fields })}\n`);
|
|
375
421
|
}
|
|
376
|
-
return output;
|
|
377
|
-
}
|
|
378
|
-
function cursorPositionAnsi(x, y) {
|
|
379
|
-
return `\u001b[${Math.max(1, y + 1)};${Math.max(1, x + 1)}H`;
|
|
380
422
|
}
|
|
@@ -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
|
}
|