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.
Files changed (104) hide show
  1. package/assets/jetbrains-mono-400-italic.ttf +0 -0
  2. package/assets/jetbrains-mono-400-normal.ttf +0 -0
  3. package/assets/jetbrains-mono-700-italic.ttf +0 -0
  4. package/assets/jetbrains-mono-700-normal.ttf +0 -0
  5. package/dist/cli.js +64 -32
  6. package/dist/cli.js.map +4 -4
  7. package/dist/commands/close-session.js +26 -0
  8. package/dist/commands/close-session.js.map +2 -2
  9. package/dist/commands/create-session.js +26 -0
  10. package/dist/commands/create-session.js.map +2 -2
  11. package/dist/commands/daemon-runtime.js.map +1 -1
  12. package/dist/commands/fill.js +26 -0
  13. package/dist/commands/fill.js.map +2 -2
  14. package/dist/commands/get-session.js +26 -0
  15. package/dist/commands/get-session.js.map +2 -2
  16. package/dist/commands/index.js +39 -6
  17. package/dist/commands/index.js.map +4 -4
  18. package/dist/commands/install.js +42 -3
  19. package/dist/commands/install.js.map +4 -4
  20. package/dist/commands/installer.js +16 -3
  21. package/dist/commands/installer.js.map +4 -4
  22. package/dist/commands/list-sessions.js +26 -0
  23. package/dist/commands/list-sessions.js.map +2 -2
  24. package/dist/commands/press-key.js +26 -0
  25. package/dist/commands/press-key.js.map +2 -2
  26. package/dist/commands/read-history.js +26 -0
  27. package/dist/commands/read-history.js.map +2 -2
  28. package/dist/commands/read-screen.js +26 -0
  29. package/dist/commands/read-screen.js.map +2 -2
  30. package/dist/commands/resize.js +26 -0
  31. package/dist/commands/resize.js.map +2 -2
  32. package/dist/commands/runtime.js.map +1 -1
  33. package/dist/commands/screenshot.js +26 -0
  34. package/dist/commands/screenshot.js.map +2 -2
  35. package/dist/commands/send-signal.js +26 -0
  36. package/dist/commands/send-signal.js.map +2 -2
  37. package/dist/commands/type.js +26 -0
  38. package/dist/commands/type.js.map +2 -2
  39. package/dist/commands/uninstall.js +42 -3
  40. package/dist/commands/uninstall.js.map +4 -4
  41. package/dist/commands/wait-for-exit.js +26 -0
  42. package/dist/commands/wait-for-exit.js.map +2 -2
  43. package/dist/commands/wait-for.js +26 -0
  44. package/dist/commands/wait-for.js.map +2 -2
  45. package/dist/composition.json +1 -1
  46. package/dist/testing/cli-repl.js +64 -32
  47. package/dist/testing/cli-repl.js.map +4 -4
  48. package/dist/testing/qa-cli.js +64 -32
  49. package/dist/testing/qa-cli.js.map +4 -4
  50. package/node_modules/toolcraft-design/README.md +30 -0
  51. package/node_modules/toolcraft-design/dist/dashboard/terminal-width.d.ts +1 -0
  52. package/node_modules/toolcraft-design/dist/dashboard/terminal-width.js +18 -1
  53. package/node_modules/toolcraft-design/dist/explorer/actions.d.ts +3 -2
  54. package/node_modules/toolcraft-design/dist/explorer/actions.js +50 -4
  55. package/node_modules/toolcraft-design/dist/explorer/events.d.ts +7 -2
  56. package/node_modules/toolcraft-design/dist/explorer/filter.js +1 -1
  57. package/node_modules/toolcraft-design/dist/explorer/index.d.ts +2 -4
  58. package/node_modules/toolcraft-design/dist/explorer/index.js +1 -2
  59. package/node_modules/toolcraft-design/dist/explorer/jobs.d.ts +2 -2
  60. package/node_modules/toolcraft-design/dist/explorer/jobs.js +3 -5
  61. package/node_modules/toolcraft-design/dist/explorer/keymap.d.ts +13 -3
  62. package/node_modules/toolcraft-design/dist/explorer/keymap.js +89 -348
  63. package/node_modules/toolcraft-design/dist/explorer/layout.d.ts +1 -0
  64. package/node_modules/toolcraft-design/dist/explorer/layout.js +14 -4
  65. package/node_modules/toolcraft-design/dist/explorer/reducer.js +116 -16
  66. package/node_modules/toolcraft-design/dist/explorer/render/detail.d.ts +1 -1
  67. package/node_modules/toolcraft-design/dist/explorer/render/detail.js +45 -7
  68. package/node_modules/toolcraft-design/dist/explorer/render/footer.d.ts +1 -1
  69. package/node_modules/toolcraft-design/dist/explorer/render/footer.js +18 -3
  70. package/node_modules/toolcraft-design/dist/explorer/render/header.d.ts +1 -1
  71. package/node_modules/toolcraft-design/dist/explorer/render/header.js +6 -2
  72. package/node_modules/toolcraft-design/dist/explorer/render/index.d.ts +1 -1
  73. package/node_modules/toolcraft-design/dist/explorer/render/index.js +2 -1
  74. package/node_modules/toolcraft-design/dist/explorer/render/list.d.ts +16 -2
  75. package/node_modules/toolcraft-design/dist/explorer/render/list.js +13 -34
  76. package/node_modules/toolcraft-design/dist/explorer/render/modal.d.ts +1 -1
  77. package/node_modules/toolcraft-design/dist/explorer/render/modal.js +11 -11
  78. package/node_modules/toolcraft-design/dist/explorer/render/pane.d.ts +5 -2
  79. package/node_modules/toolcraft-design/dist/explorer/render/pane.js +9 -6
  80. package/node_modules/toolcraft-design/dist/explorer/render/test-fixtures.js +2 -3
  81. package/node_modules/toolcraft-design/dist/explorer/runtime.js +115 -73
  82. package/node_modules/toolcraft-design/dist/explorer/runtime.test-helpers.d.ts +23 -37
  83. package/node_modules/toolcraft-design/dist/explorer/runtime.test-helpers.js +32 -83
  84. package/node_modules/toolcraft-design/dist/explorer/state.d.ts +79 -8
  85. package/node_modules/toolcraft-design/dist/explorer/state.js +70 -18
  86. package/node_modules/toolcraft-design/dist/index.d.ts +8 -2
  87. package/node_modules/toolcraft-design/dist/index.js +5 -1
  88. package/node_modules/toolcraft-design/dist/screen/ansi-text.d.ts +2 -0
  89. package/node_modules/toolcraft-design/dist/screen/ansi-text.js +35 -0
  90. package/node_modules/toolcraft-design/dist/screen/screen.d.ts +40 -0
  91. package/node_modules/toolcraft-design/dist/screen/screen.js +122 -0
  92. package/node_modules/toolcraft-design/dist/screen/style.d.ts +16 -0
  93. package/node_modules/toolcraft-design/dist/screen/style.js +55 -0
  94. package/node_modules/toolcraft-design/dist/terminal/driver.d.ts +34 -0
  95. package/node_modules/toolcraft-design/dist/terminal/driver.js +65 -0
  96. package/node_modules/toolcraft-design/dist/terminal/input.d.ts +25 -0
  97. package/node_modules/toolcraft-design/dist/terminal/input.js +181 -0
  98. package/node_modules/toolcraft-design/dist/terminal/output.d.ts +10 -0
  99. package/node_modules/toolcraft-design/dist/terminal/output.js +44 -0
  100. package/package.json +3 -2
  101. package/node_modules/toolcraft-design/dist/explorer/two-pane.d.ts +0 -102
  102. package/node_modules/toolcraft-design/dist/explorer/two-pane.js +0 -510
  103. package/node_modules/toolcraft-design/dist/run-two-pane-explorer.d.ts +0 -2
  104. package/node_modules/toolcraft-design/dist/run-two-pane-explorer.js +0 -1
@@ -1,5 +1,8 @@
1
- import { ScreenBuffer } from "../../dashboard/buffer.js";
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): void;
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 titleSegment = padEndCells(fitToWidth(`─ ${title} `, innerWidth, rect.x + 1), innerWidth, "─", rect.x + 1);
14
- screen.put(rect.x, rect.y, `┌${titleSegment}┐`, style);
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, `└${"".repeat(innerWidth)}┘`, style);
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
- key: "e",
129
+ accelerator: "e",
130
130
  handler: () => undefined,
131
131
  showInFooter: true
132
132
  },
133
133
  {
134
134
  id: "archive",
135
135
  label: "archive",
136
- key: "a",
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 { ScreenBuffer, cellToAnsi, diff } from "../dashboard/buffer.js";
2
- import { createTerminalDriver } from "../dashboard/terminal.js";
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
- previousBuffer = new ScreenBuffer(0, 0);
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.state = createInitialState(config, driver.getSize());
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.render();
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.enterRawMode();
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.onKeypress((key) => {
98
- this.dispatch({ type: "key", key });
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.render();
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.renderDetail(focusedId);
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.driver.exitAltScreen();
247
- this.driver.enableLineWrap();
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.enterRawMode();
256
- this.driver.enterAltScreen();
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 action = {
272
- id: "__confirm__",
273
- label: prompt,
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
- action,
282
- rows: row === undefined ? [] : [row],
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.render();
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.render();
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
- render() {
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
- const nextBuffer = this.state.dirty === REGION_ALL
319
- ? new ScreenBuffer(this.state.size.cols, this.state.size.rows)
320
- : cloneBuffer(this.previousBuffer);
321
- renderExplorer(this.state, nextBuffer);
322
- this.driver.write(changesToAnsi(diff(this.previousBuffer, nextBuffer)));
323
- this.previousBuffer = nextBuffer;
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.destroy();
344
- Promise.resolve()
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.destroy();
413
+ this.driver.stop();
357
414
  }
358
415
  this.settle?.reject(error);
359
416
  }
360
- }
361
- function cloneBuffer(buffer) {
362
- const next = new ScreenBuffer(buffer.width, buffer.height);
363
- for (let y = 0; y < buffer.height; y += 1) {
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 { Cell } from "../dashboard/types.js";
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
- readonly flushes: Array<Array<{
9
- x: number;
10
- y: number;
11
- cell: Cell;
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
- enterRawMode(): void;
27
- exitRawMode(): void;
28
- enterAltScreen(): void;
29
- exitAltScreen(): void;
30
- disableLineWrap(): void;
31
- enableLineWrap(): void;
32
- hideCursor(): void;
33
- showCursor(): void;
34
- moveTo(x: number, y: number): void;
35
- write(text: string): void;
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
- onResize(handler: () => void): () => void;
47
- onKeypress(handler: (key: KeypressEvent) => void): () => void;
48
- press(key: KeypressEvent): void;
49
- destroy(): void;
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
- flushes = [];
7
- rawMode = false;
8
- altScreen = false;
9
- lineWrap = true;
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
- return this.writes.join("");
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
- enterRawMode() {
26
- this.rawMode = true;
27
- this.enterRawModeCount += 1;
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.keyQueue.push(key);
88
- for (const handler of this.keypressHandlers) {
89
- handler(key);
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
  }