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
|
@@ -9,6 +9,7 @@ const DEFAULT_ACTION_HANDLES = {
|
|
|
9
9
|
openModal: () => undefined,
|
|
10
10
|
toast: () => undefined,
|
|
11
11
|
confirm: async () => false,
|
|
12
|
+
promptText: async () => null,
|
|
12
13
|
exit: () => undefined
|
|
13
14
|
};
|
|
14
15
|
export function step(state, event, runtimeHandles = DEFAULT_ACTION_HANDLES) {
|
|
@@ -53,6 +54,10 @@ function stepKey(state, key, runtimeHandles) {
|
|
|
53
54
|
return stepFilterKey(state, key, target, runtimeHandles);
|
|
54
55
|
}
|
|
55
56
|
if (target?.type === "action") {
|
|
57
|
+
if (state.actionState.get(target.id)?.running === true) {
|
|
58
|
+
runtimeHandles.toast("Action already running", "info");
|
|
59
|
+
return mark(state, 0);
|
|
60
|
+
}
|
|
56
61
|
if (state.actionState.get(target.id)?.source === "detail" && state.focused !== "detail") {
|
|
57
62
|
return mark(state, 0);
|
|
58
63
|
}
|
|
@@ -85,6 +90,14 @@ function stepKey(state, key, runtimeHandles) {
|
|
|
85
90
|
return isDetailBlobFocused(state)
|
|
86
91
|
? detailScroll(state, detailBodyHeight(state))
|
|
87
92
|
: moveCursor(state, pageSize(state));
|
|
93
|
+
case "halfPageUp":
|
|
94
|
+
return isDetailBlobFocused(state)
|
|
95
|
+
? detailScroll(state, -Math.max(1, Math.floor(detailBodyHeight(state) / 2)))
|
|
96
|
+
: moveCursor(state, -Math.max(1, Math.floor(pageSize(state) / 2)));
|
|
97
|
+
case "halfPageDown":
|
|
98
|
+
return isDetailBlobFocused(state)
|
|
99
|
+
? detailScroll(state, Math.max(1, Math.floor(detailBodyHeight(state) / 2)))
|
|
100
|
+
: moveCursor(state, Math.max(1, Math.floor(pageSize(state) / 2)));
|
|
88
101
|
case "focusNext":
|
|
89
102
|
return focusNext(state);
|
|
90
103
|
case "escape":
|
|
@@ -112,12 +125,16 @@ function stepKey(state, key, runtimeHandles) {
|
|
|
112
125
|
}
|
|
113
126
|
}
|
|
114
127
|
if (isBackspace(key)) {
|
|
128
|
+
if (isSecondListFocused(state))
|
|
129
|
+
return updateDetailFilter(state, (state.detail.filter ?? "").slice(0, -1));
|
|
115
130
|
return updateFilter(state, state.filter.slice(0, -1));
|
|
116
131
|
}
|
|
117
132
|
if (!state.multiSelect && isSelectionSpace(key)) {
|
|
118
133
|
return mark(state, 0);
|
|
119
134
|
}
|
|
120
135
|
if (isPrintable(key)) {
|
|
136
|
+
if (isSecondListFocused(state))
|
|
137
|
+
return updateDetailFilter(state, `${state.detail.filter ?? ""}${key.ch}`);
|
|
121
138
|
return updateFilter(state, `${state.filter}${key.ch}`);
|
|
122
139
|
}
|
|
123
140
|
return mark(state, 0);
|
|
@@ -141,8 +158,23 @@ function stepFilterKey(state, key, target, runtimeHandles) {
|
|
|
141
158
|
return mark(state, 0);
|
|
142
159
|
}
|
|
143
160
|
function stepModalKey(state, key, target, runtimeHandles) {
|
|
161
|
+
if (state.modal?.kind === "input") {
|
|
162
|
+
if (target?.type === "builtin" && (target.id === "escape" || target.id === "quit")) {
|
|
163
|
+
return modalDismissed(state, null, runtimeHandles);
|
|
164
|
+
}
|
|
165
|
+
if (target?.type === "builtin" && target.id === "confirm") {
|
|
166
|
+
return modalDismissed(state, state.modal.value, runtimeHandles);
|
|
167
|
+
}
|
|
168
|
+
if (isBackspace(key)) {
|
|
169
|
+
return setModal(state, { ...state.modal, value: state.modal.value.slice(0, -1) });
|
|
170
|
+
}
|
|
171
|
+
if (isPrintable(key)) {
|
|
172
|
+
return setModal(state, { ...state.modal, value: `${state.modal.value}${key.ch}` });
|
|
173
|
+
}
|
|
174
|
+
return mark(state, 0);
|
|
175
|
+
}
|
|
144
176
|
if (state.modal?.kind === "confirm") {
|
|
145
|
-
if (target?.type === "builtin" && target.id === "escape") {
|
|
177
|
+
if (target?.type === "builtin" && (target.id === "escape" || target.id === "quit")) {
|
|
146
178
|
return modalDismissed(state, false, runtimeHandles);
|
|
147
179
|
}
|
|
148
180
|
if (target?.type === "builtin" && target.id === "confirm") {
|
|
@@ -201,7 +233,7 @@ function stepModalKey(state, key, target, runtimeHandles) {
|
|
|
201
233
|
}
|
|
202
234
|
function resize(state, cols, rows) {
|
|
203
235
|
const size = { cols: normalizeSize(cols), rows: normalizeSize(rows) };
|
|
204
|
-
const layout = resolveExplorerLayoutMode(size.cols);
|
|
236
|
+
const layout = resolveExplorerLayoutMode(size.cols, size.rows);
|
|
205
237
|
if (state.size.cols === size.cols && state.size.rows === size.rows && state.layout === layout) {
|
|
206
238
|
return mark(state, 0);
|
|
207
239
|
}
|
|
@@ -211,6 +243,7 @@ function resize(state, cols, rows) {
|
|
|
211
243
|
};
|
|
212
244
|
}
|
|
213
245
|
function rowsLoaded(state, rows) {
|
|
246
|
+
const previousRowId = currentRow(state)?.id;
|
|
214
247
|
const rowIds = new Set();
|
|
215
248
|
for (const row of rows) {
|
|
216
249
|
if (rowIds.has(row.id)) {
|
|
@@ -221,14 +254,27 @@ function rowsLoaded(state, rows) {
|
|
|
221
254
|
const matches = filterRows(state.filter, rows);
|
|
222
255
|
const filtered = matches.map((match) => match.index);
|
|
223
256
|
const matchPositions = createMatchPositions(matches);
|
|
224
|
-
const
|
|
257
|
+
const identityCursor = previousRowId === undefined
|
|
258
|
+
? -1
|
|
259
|
+
: filtered.findIndex((index) => rows[index]?.id === previousRowId);
|
|
260
|
+
const cursor = identityCursor >= 0 ? identityCursor : clamp(state.cursor, 0, Math.max(0, filtered.length - 1));
|
|
225
261
|
const selected = state.multiSelect ? pruneSelection(state.selected, rows) : new Set();
|
|
226
262
|
const detail = resetDetailForCursor(state, rows, filtered, cursor);
|
|
227
263
|
const modal = modalStillValid(state.modal, rows);
|
|
228
264
|
if (state.modal?.kind === "confirm" && modal === null) {
|
|
229
265
|
state.modal.resolver(false);
|
|
230
266
|
}
|
|
231
|
-
const nextView = {
|
|
267
|
+
const nextView = {
|
|
268
|
+
...state,
|
|
269
|
+
rows,
|
|
270
|
+
rowsLoading: false,
|
|
271
|
+
filtered,
|
|
272
|
+
matchPositions,
|
|
273
|
+
cursor,
|
|
274
|
+
selected,
|
|
275
|
+
detail,
|
|
276
|
+
modal
|
|
277
|
+
};
|
|
232
278
|
const next = {
|
|
233
279
|
...nextView,
|
|
234
280
|
actionState: recomputeActionState(nextView),
|
|
@@ -253,10 +299,12 @@ function detailLoaded(state, rowId, token, items) {
|
|
|
253
299
|
if (state.detail.rowId !== rowId || state.detail.token !== token) {
|
|
254
300
|
return mark(state, 0);
|
|
255
301
|
}
|
|
302
|
+
const visibleItems = filterDetailItems(items, state.detail.filter ?? "");
|
|
256
303
|
const detail = {
|
|
257
304
|
...state.detail,
|
|
258
|
-
items,
|
|
259
|
-
|
|
305
|
+
items: visibleItems,
|
|
306
|
+
allItems: items,
|
|
307
|
+
cursor: clamp(state.detail.cursor, 0, Math.max(0, visibleItems.length - 1)),
|
|
260
308
|
scroll: 0,
|
|
261
309
|
loading: false
|
|
262
310
|
};
|
|
@@ -324,10 +372,13 @@ function modalDismissed(state, result, runtimeHandles) {
|
|
|
324
372
|
if (modal?.kind === "confirm") {
|
|
325
373
|
modal.resolver(result === true);
|
|
326
374
|
}
|
|
327
|
-
if (modal?.kind
|
|
375
|
+
if (modal?.kind === "input") {
|
|
376
|
+
modal.resolver(typeof result === "string" ? result : null);
|
|
377
|
+
}
|
|
378
|
+
if (modal?.kind !== "confirm" || result !== true || modal.action === undefined) {
|
|
328
379
|
return { state: closed, effects: NO_EFFECTS };
|
|
329
380
|
}
|
|
330
|
-
return dispatchAction(closed, modal.action, true, runtimeHandles, modal.rows);
|
|
381
|
+
return dispatchAction(closed, modal.action, true, runtimeHandles, modal.rows ?? []);
|
|
331
382
|
}
|
|
332
383
|
function moveCursor(state, delta) {
|
|
333
384
|
if (state.focused === "detail" && hasDetailCursor(state)) {
|
|
@@ -339,9 +390,7 @@ function moveCursor(state, delta) {
|
|
|
339
390
|
return setCursor(state, state.cursor + delta);
|
|
340
391
|
}
|
|
341
392
|
function isDetailBlobFocused(state) {
|
|
342
|
-
return (state.focused === "detail" &&
|
|
343
|
-
!hasDetailCursor(state) &&
|
|
344
|
-
(state.detail.items?.length ?? 0) > 0);
|
|
393
|
+
return (state.focused === "detail" && !hasDetailCursor(state) && (state.detail.items?.length ?? 0) > 0);
|
|
345
394
|
}
|
|
346
395
|
function moveDetailCursor(state, delta) {
|
|
347
396
|
const max = Math.max(0, (state.detail.items?.length ?? 0) - 1);
|
|
@@ -410,6 +459,33 @@ function updateFilter(state, filter) {
|
|
|
410
459
|
const effect = detailEffect(next);
|
|
411
460
|
return { state: next, effects: effect === undefined ? NO_EFFECTS : [effect] };
|
|
412
461
|
}
|
|
462
|
+
function updateDetailFilter(state, filter) {
|
|
463
|
+
const allItems = state.detail.allItems ?? state.detail.items ?? [];
|
|
464
|
+
const items = filterDetailItems(allItems, filter);
|
|
465
|
+
const detail = { ...state.detail, filter, items, cursor: 0, scroll: 0 };
|
|
466
|
+
return {
|
|
467
|
+
state: {
|
|
468
|
+
...state,
|
|
469
|
+
detail,
|
|
470
|
+
actionState: recomputeActionState({ ...state, detail }),
|
|
471
|
+
dirty: REGION_HEADER | REGION_DETAIL | REGION_FOOTER
|
|
472
|
+
},
|
|
473
|
+
effects: NO_EFFECTS
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
function filterDetailItems(items, query) {
|
|
477
|
+
const rows = items.map((item) => ({
|
|
478
|
+
id: item.id,
|
|
479
|
+
title: item.title ?? item.id,
|
|
480
|
+
subtitle: item.subtitle
|
|
481
|
+
}));
|
|
482
|
+
return filterRows(query, rows)
|
|
483
|
+
.map((match) => items[match.index])
|
|
484
|
+
.filter((item) => item !== undefined);
|
|
485
|
+
}
|
|
486
|
+
function isSecondListFocused(state) {
|
|
487
|
+
return state.focused === "detail" && state.paneDefinitions[1]?.kind === "list";
|
|
488
|
+
}
|
|
413
489
|
function focusFilter(state) {
|
|
414
490
|
if (state.filterFocused) {
|
|
415
491
|
return mark(state, 0);
|
|
@@ -432,6 +508,9 @@ function focusNext(state) {
|
|
|
432
508
|
};
|
|
433
509
|
}
|
|
434
510
|
function escape(state, runtimeHandles) {
|
|
511
|
+
if (isSecondListFocused(state) && (state.detail.filter ?? "").length > 0) {
|
|
512
|
+
return updateDetailFilter(state, "");
|
|
513
|
+
}
|
|
435
514
|
if (state.filterFocused || state.filter.length > 0) {
|
|
436
515
|
const cleared = updateFilter({ ...state, filterFocused: false }, "");
|
|
437
516
|
return {
|
|
@@ -455,13 +534,19 @@ function confirmKey(state, runtimeHandles) {
|
|
|
455
534
|
if (state.modal?.kind === "confirm") {
|
|
456
535
|
return modalDismissed(state, true, runtimeHandles);
|
|
457
536
|
}
|
|
458
|
-
|
|
537
|
+
const hasActions = [...state.actionState.values()].some((entry) => entry.available && entry.running !== true);
|
|
538
|
+
return hasActions
|
|
539
|
+
? setModal(state, { kind: "palette", query: "", cursor: 0 })
|
|
540
|
+
: dispatchPrimary(state, runtimeHandles);
|
|
459
541
|
}
|
|
460
542
|
function toggleSelect(state) {
|
|
461
543
|
if (!state.multiSelect) {
|
|
462
544
|
return mark(state, 0);
|
|
463
545
|
}
|
|
464
|
-
const
|
|
546
|
+
const detailItem = state.focused === "detail" ? state.detail.items?.[state.detail.cursor] : undefined;
|
|
547
|
+
const row = detailItem === undefined
|
|
548
|
+
? currentRow(state)
|
|
549
|
+
: { id: detailItem.id, title: detailItem.title ?? detailItem.id };
|
|
465
550
|
if (row === undefined) {
|
|
466
551
|
return mark(state, 0);
|
|
467
552
|
}
|
|
@@ -701,10 +786,21 @@ function dispatchAction(state, action, confirmed, runtimeHandles, modalRows) {
|
|
|
701
786
|
return mark(state, 0);
|
|
702
787
|
}
|
|
703
788
|
if (action.destructive === true && !confirmed) {
|
|
789
|
+
const actionLabel = typeof action.label === "function" ? action.label() : action.label;
|
|
704
790
|
return {
|
|
705
791
|
state: {
|
|
706
792
|
...state,
|
|
707
|
-
modal: {
|
|
793
|
+
modal: {
|
|
794
|
+
kind: "confirm",
|
|
795
|
+
title: action.destructive ? "Confirm destructive action" : "Confirm action",
|
|
796
|
+
message: `${actionLabel} ${rows.length === 1 ? rows[0].title : `${rows.length} items`}?`,
|
|
797
|
+
confirmLabel: actionLabel,
|
|
798
|
+
cancelLabel: "Cancel",
|
|
799
|
+
destructive: action.destructive === true,
|
|
800
|
+
action,
|
|
801
|
+
rows,
|
|
802
|
+
resolver: () => undefined
|
|
803
|
+
},
|
|
708
804
|
dirty: REGION_MODAL | REGION_FOOTER
|
|
709
805
|
},
|
|
710
806
|
effects: NO_EFFECTS
|
|
@@ -736,7 +832,9 @@ function recomputeActionState(view) {
|
|
|
736
832
|
continue;
|
|
737
833
|
}
|
|
738
834
|
const ctx = buildActionContext(view, action, entry.source ?? actionSource(view, action), DEFAULT_ACTION_HANDLES);
|
|
739
|
-
const
|
|
835
|
+
const row = currentRow(view);
|
|
836
|
+
const visible = action.visible === undefined || (row !== undefined && action.visible(row));
|
|
837
|
+
const available = visible && (action.predicate === undefined ? entry.available : action.predicate(ctx));
|
|
740
838
|
const label = typeof action.label === "function" ? action.label() : action.label;
|
|
741
839
|
next.set(id, { ...entry, available, label });
|
|
742
840
|
}
|
|
@@ -756,7 +854,7 @@ function resetDetailForCursor(state, rows, filtered, cursor) {
|
|
|
756
854
|
}
|
|
757
855
|
return {
|
|
758
856
|
rowId: row.id,
|
|
759
|
-
items:
|
|
857
|
+
items: state.detail.items,
|
|
760
858
|
cursor: 0,
|
|
761
859
|
scroll: 0,
|
|
762
860
|
token: state.detail.token + 1,
|
|
@@ -816,6 +914,8 @@ function modalStillValid(modal, rows) {
|
|
|
816
914
|
if (modal?.kind !== "confirm") {
|
|
817
915
|
return modal;
|
|
818
916
|
}
|
|
917
|
+
if (modal.rows === undefined)
|
|
918
|
+
return modal;
|
|
819
919
|
const ids = new Set(rows.map((row) => row.id));
|
|
820
920
|
return modal.rows.every((row) => ids.has(row.id)) ? modal : null;
|
|
821
921
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ScreenBuffer } from "../../
|
|
1
|
+
import type { ScreenSurface as ScreenBuffer } from "../../screen/screen.js";
|
|
2
2
|
import type { ExplorerLayout } from "../layout.js";
|
|
3
3
|
import type { ExplorerState } from "../state.js";
|
|
4
4
|
export declare function renderDetail(state: ExplorerState, screen: ScreenBuffer, layout: ExplorerLayout): void;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ansiToCells } from "../../screen/ansi-text.js";
|
|
2
2
|
import { renderMarkdown } from "../../terminal-markdown/index.js";
|
|
3
3
|
import { getExplorerStyles } from "../theme.js";
|
|
4
4
|
import { drawPaneFrame, paneBodyRect } from "./pane.js";
|
|
5
5
|
import { fitToWidth } from "./text.js";
|
|
6
|
+
const markdownCache = new Map();
|
|
6
7
|
export function renderDetail(state, screen, layout) {
|
|
7
8
|
const rect = layout.detail;
|
|
8
9
|
const styles = getExplorerStyles();
|
|
@@ -11,12 +12,14 @@ export function renderDetail(state, screen, layout) {
|
|
|
11
12
|
return;
|
|
12
13
|
}
|
|
13
14
|
const row = state.rows.find((r) => r.id === state.detail.rowId) ?? null;
|
|
15
|
+
const pane = state.paneDefinitions[1];
|
|
16
|
+
const title = pane?.titleForRow?.(row ?? undefined) ?? pane?.title ?? "Preview";
|
|
14
17
|
if (layout.mode === "narrow-vertical") {
|
|
15
|
-
drawPaneFrame(screen, rect,
|
|
18
|
+
drawPaneFrame(screen, rect, title, state.focused === "detail" ? styles.borderFocused : styles.border, { focused: state.focused === "detail", indicator: scrollIndicator(state) });
|
|
16
19
|
renderDetailBody(state, screen, paneBodyRect(rect), row);
|
|
17
20
|
return;
|
|
18
21
|
}
|
|
19
|
-
drawPaneFrame(screen, rect,
|
|
22
|
+
drawPaneFrame(screen, rect, title, state.focused === "detail" ? styles.borderFocused : styles.border, { focused: state.focused === "detail", indicator: scrollIndicator(state) });
|
|
20
23
|
renderDetailBody(state, screen, paneBodyRect(rect), row);
|
|
21
24
|
}
|
|
22
25
|
function renderDetailBody(state, screen, rect, row) {
|
|
@@ -47,7 +50,8 @@ function renderListMode(state, screen, rect, items, row) {
|
|
|
47
50
|
const item = items[index];
|
|
48
51
|
const cursor = index === state.detail.cursor;
|
|
49
52
|
const title = item.title ?? item.id;
|
|
50
|
-
const
|
|
53
|
+
const selected = state.multiSelect && state.selected.has(item.id);
|
|
54
|
+
const prefix = `${selected ? "*" : " "}${cursor ? "▌" : " "} `;
|
|
51
55
|
const badge = item.badge ? ` ${item.badge.text}` : "";
|
|
52
56
|
writeLine(screen, rect, y, `${prefix}${title}${badge}`, cursor ? styles.borderFocused : styles.accent);
|
|
53
57
|
y += 1;
|
|
@@ -68,11 +72,23 @@ function renderListMode(state, screen, rect, items, row) {
|
|
|
68
72
|
}
|
|
69
73
|
}
|
|
70
74
|
function renderBlob(screen, rect, text, scroll) {
|
|
71
|
-
const allLines =
|
|
75
|
+
const allLines = [[]];
|
|
76
|
+
for (const cell of ansiToCells(text)) {
|
|
77
|
+
if (cell.ch === "\n")
|
|
78
|
+
allLines.push([]);
|
|
79
|
+
else
|
|
80
|
+
allLines.at(-1).push(cell);
|
|
81
|
+
}
|
|
72
82
|
const start = clamp(scroll, 0, Math.max(0, allLines.length - rect.height));
|
|
73
83
|
const lines = allLines.slice(start);
|
|
74
84
|
for (let row = 0; row < rect.height; row += 1) {
|
|
75
|
-
|
|
85
|
+
let x = rect.x;
|
|
86
|
+
for (const cell of lines[row] ?? []) {
|
|
87
|
+
if (x + cell.width > rect.x + rect.width)
|
|
88
|
+
break;
|
|
89
|
+
screen.put(x, rect.y + row, cell.ch, cell.style);
|
|
90
|
+
x += cell.width;
|
|
91
|
+
}
|
|
76
92
|
}
|
|
77
93
|
}
|
|
78
94
|
function renderItemMarkdown(item, rect, row) {
|
|
@@ -80,7 +96,14 @@ function renderItemMarkdown(item, rect, row) {
|
|
|
80
96
|
if (content.trim().length === 0) {
|
|
81
97
|
return "";
|
|
82
98
|
}
|
|
83
|
-
|
|
99
|
+
const width = Math.max(1, rect.width);
|
|
100
|
+
const key = `${contentHash(content)}:${width}`;
|
|
101
|
+
const cached = markdownCache.get(key);
|
|
102
|
+
if (cached !== undefined)
|
|
103
|
+
return cached;
|
|
104
|
+
const rendered = renderMarkdown(content, { width }).trimEnd();
|
|
105
|
+
markdownCache.set(key, rendered);
|
|
106
|
+
return rendered;
|
|
84
107
|
}
|
|
85
108
|
function renderItem(item, rect, row) {
|
|
86
109
|
if (item.renderedContent !== undefined) {
|
|
@@ -108,3 +131,18 @@ function writeLine(screen, rect, row, text, style = {}) {
|
|
|
108
131
|
function clamp(value, min, max) {
|
|
109
132
|
return Math.min(max, Math.max(min, value));
|
|
110
133
|
}
|
|
134
|
+
function scrollIndicator(state) {
|
|
135
|
+
if (state.detail.loading)
|
|
136
|
+
return "⠋";
|
|
137
|
+
const content = state.detail.items?.[state.detail.cursor]?.renderedContent ?? "";
|
|
138
|
+
const total = Math.max(1, content.split("\n").length);
|
|
139
|
+
return `${Math.min(100, Math.round((state.detail.scroll / Math.max(1, total - 1)) * 100))}%`;
|
|
140
|
+
}
|
|
141
|
+
function contentHash(content) {
|
|
142
|
+
let hash = 2166136261;
|
|
143
|
+
for (let index = 0; index < content.length; index += 1) {
|
|
144
|
+
hash ^= content.charCodeAt(index);
|
|
145
|
+
hash = Math.imul(hash, 16777619);
|
|
146
|
+
}
|
|
147
|
+
return hash >>> 0;
|
|
148
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ScreenBuffer } from "../../
|
|
1
|
+
import type { ScreenSurface as ScreenBuffer } from "../../screen/screen.js";
|
|
2
2
|
import type { ExplorerLayout } from "../layout.js";
|
|
3
3
|
import type { ExplorerState } from "../state.js";
|
|
4
4
|
export declare function renderFooter(state: ExplorerState, screen: ScreenBuffer, layout: ExplorerLayout): void;
|
|
@@ -36,11 +36,23 @@ function putFooterText(screen, x, y, endX, text, style = {}) {
|
|
|
36
36
|
return cellWidth(fitted, x);
|
|
37
37
|
}
|
|
38
38
|
function footerHints(state) {
|
|
39
|
+
if (state.modal?.kind === "input") {
|
|
40
|
+
return [
|
|
41
|
+
{ key: "Enter", label: "submit", running: false },
|
|
42
|
+
{ key: "Esc", label: "cancel", running: false }
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
if (state.modal?.kind === "confirm") {
|
|
46
|
+
return [
|
|
47
|
+
{ key: "Y/Enter", label: state.modal.confirmLabel, running: false },
|
|
48
|
+
{ key: "N/Esc", label: state.modal.cancelLabel, running: false }
|
|
49
|
+
];
|
|
50
|
+
}
|
|
39
51
|
const hints = [];
|
|
40
52
|
if (state.focused === "detail") {
|
|
41
53
|
hints.push({ key: "Tab", label: "focus", running: false });
|
|
42
|
-
hints.push({ key: "Enter", label: "sub", running: false });
|
|
43
54
|
}
|
|
55
|
+
hints.push({ key: "Enter", label: "actions", running: false });
|
|
44
56
|
for (const [id, entry] of state.actionState) {
|
|
45
57
|
if (!entry.available || entry.action?.showInFooter === false) {
|
|
46
58
|
continue;
|
|
@@ -51,12 +63,13 @@ function footerHints(state) {
|
|
|
51
63
|
: entry.label;
|
|
52
64
|
hints.push({ key, label, running: entry.running === true });
|
|
53
65
|
}
|
|
54
|
-
hints.push({ key: "?", label: "help", running: false });
|
|
55
66
|
hints.push({ key: "Ctrl+P", label: "palette", running: false });
|
|
56
67
|
if (hasShiftReorderBindings(state)) {
|
|
57
68
|
hints.push({ key: "⇧↑↓", label: "reorder (within state)", running: false, bracketed: false });
|
|
58
69
|
}
|
|
59
|
-
hints.push({ key: "
|
|
70
|
+
hints.push({ key: "Esc", label: "clear/quit", running: false });
|
|
71
|
+
if (state.selected.size > 0)
|
|
72
|
+
hints.push({ key: `${state.selected.size}`, label: "selected", running: false, bracketed: false });
|
|
60
73
|
return hints;
|
|
61
74
|
}
|
|
62
75
|
function hasShiftReorderBindings(state) {
|
|
@@ -65,6 +78,8 @@ function hasShiftReorderBindings(state) {
|
|
|
65
78
|
return up.includes("Shift+up") && down.includes("Shift+down");
|
|
66
79
|
}
|
|
67
80
|
function actionKey(entry, fallback) {
|
|
81
|
+
if (entry.action?.accelerator !== undefined)
|
|
82
|
+
return `Ctrl+${entry.action.accelerator.toUpperCase()}`;
|
|
68
83
|
const key = entry.action?.key;
|
|
69
84
|
if (Array.isArray(key)) {
|
|
70
85
|
return key[0] ?? fallback;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ScreenBuffer } from "../../
|
|
1
|
+
import type { ScreenSurface as ScreenBuffer } from "../../screen/screen.js";
|
|
2
2
|
import type { ExplorerLayout } from "../layout.js";
|
|
3
3
|
import type { ExplorerState } from "../state.js";
|
|
4
4
|
export declare function renderHeader(state: ExplorerState, screen: ScreenBuffer, layout: ExplorerLayout): void;
|
|
@@ -14,8 +14,12 @@ export function renderHeader(state, screen, layout) {
|
|
|
14
14
|
drawTopBorder(screen, state.title, rect.width, styles.border);
|
|
15
15
|
if (rect.height > 1) {
|
|
16
16
|
const prompt = `${state.title.toLocaleLowerCase()}>`;
|
|
17
|
-
const
|
|
18
|
-
const
|
|
17
|
+
const secondList = state.focused === "detail" && state.paneDefinitions[1]?.kind === "list";
|
|
18
|
+
const activeFilter = secondList ? (state.detail.filter ?? "") : state.filter;
|
|
19
|
+
const filter = activeFilter.length > 0 ? ` ${activeFilter}` : "";
|
|
20
|
+
const count = secondList
|
|
21
|
+
? `${state.detail.items?.length ?? 0}/${state.detail.allItems?.length ?? state.detail.items?.length ?? 0}`
|
|
22
|
+
: `${state.filtered.length}/${state.rows.length}`;
|
|
19
23
|
const selected = state.multiSelect && state.selected.size > 0 ? ` (${state.selected.size} selected)` : "";
|
|
20
24
|
const spinner = state.detail.loading ? " *" : "";
|
|
21
25
|
const right = `${count}${selected}${spinner}`;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ScreenBuffer } from "../../
|
|
1
|
+
import type { ScreenSurface as ScreenBuffer } from "../../screen/screen.js";
|
|
2
2
|
import { type ExplorerState } from "../state.js";
|
|
3
3
|
export declare function renderExplorer(state: ExplorerState, screen: ScreenBuffer): void;
|
|
4
4
|
export { renderDetail } from "./detail.js";
|
|
@@ -18,7 +18,8 @@ export function renderExplorer(state, screen) {
|
|
|
18
18
|
const layout = computeExplorerLayout({
|
|
19
19
|
cols: state.size.cols,
|
|
20
20
|
rows: state.size.rows,
|
|
21
|
-
detailHidden: state.layout === "narrow-list-only" || state.layout === "too-narrow"
|
|
21
|
+
detailHidden: state.layout === "narrow-list-only" || state.layout === "too-narrow",
|
|
22
|
+
focused: state.focused
|
|
22
23
|
});
|
|
23
24
|
const dirty = state.dirty === 0 ? REGION_ALL : state.dirty;
|
|
24
25
|
for (const [region, render] of REGION_RENDERERS) {
|
|
@@ -1,4 +1,18 @@
|
|
|
1
|
-
import { ScreenBuffer } from "../../
|
|
1
|
+
import type { ScreenSurface as ScreenBuffer } from "../../screen/screen.js";
|
|
2
2
|
import type { ExplorerLayout } from "../layout.js";
|
|
3
|
-
import type { ExplorerState } from "../state.js";
|
|
3
|
+
import type { ExplorerState, Row } from "../state.js";
|
|
4
4
|
export declare function renderList(state: ExplorerState, screen: ScreenBuffer, layout: ExplorerLayout): void;
|
|
5
|
+
export type DisplayLine = {
|
|
6
|
+
kind: "group";
|
|
7
|
+
label: string;
|
|
8
|
+
} | {
|
|
9
|
+
kind: "row";
|
|
10
|
+
rowIndex: number;
|
|
11
|
+
row: Row;
|
|
12
|
+
cursor: boolean;
|
|
13
|
+
} | {
|
|
14
|
+
kind: "subtitle";
|
|
15
|
+
row: Row;
|
|
16
|
+
text: string;
|
|
17
|
+
};
|
|
18
|
+
export declare function visibleStart(lines: DisplayLine[], height: number, scrolloff?: number): number;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { getExplorerStyles } from "../theme.js";
|
|
2
2
|
import { drawPaneFrame, paneBodyRect } from "./pane.js";
|
|
3
3
|
import { cellWidth, centerCells, fitToWidth, splitGraphemeCells, stripAnsi } from "./text.js";
|
|
4
|
-
const listLineCache = new WeakMap();
|
|
5
4
|
export function renderList(state, screen, layout) {
|
|
6
5
|
const styles = getExplorerStyles();
|
|
7
6
|
const rect = layout.list;
|
|
@@ -13,19 +12,14 @@ export function renderList(state, screen, layout) {
|
|
|
13
12
|
writeLine(screen, rect, 0, "Terminal too narrow", styles.muted);
|
|
14
13
|
return;
|
|
15
14
|
}
|
|
16
|
-
drawPaneFrame(screen, rect, state.title, state.focused === "list" ? styles.borderFocused : styles.border);
|
|
15
|
+
drawPaneFrame(screen, rect, state.paneDefinitions[0]?.title ?? state.title, state.focused === "list" ? styles.borderFocused : styles.border, { focused: state.focused === "list", indicator: state.rowsLoading ? "⠋" : `${state.filtered.length}/${state.rows.length}` });
|
|
17
16
|
const bodyRect = paneBodyRect(rect);
|
|
18
17
|
if (bodyRect.width <= 0 || bodyRect.height <= 0) {
|
|
19
18
|
return;
|
|
20
19
|
}
|
|
21
|
-
const rectKey = `${bodyRect.x}:${bodyRect.y}:${bodyRect.width}:${bodyRect.height}`;
|
|
22
|
-
const cached = listLineCache.get(screen);
|
|
23
|
-
const cache = cached?.rectKey === rectKey ? cached.lines : new Map();
|
|
24
|
-
listLineCache.set(screen, { rectKey, lines: cache });
|
|
25
20
|
if (state.filtered.length === 0) {
|
|
26
|
-
const hint = state.emptyHint;
|
|
21
|
+
const hint = state.rowsLoading ? "Loading…" : state.emptyHint;
|
|
27
22
|
writeLine(screen, bodyRect, Math.floor(bodyRect.height / 2), centerCells(hint, bodyRect.width, bodyRect.x), styles.muted);
|
|
28
|
-
cache.clear();
|
|
29
23
|
return;
|
|
30
24
|
}
|
|
31
25
|
const lines = buildDisplayLines(state);
|
|
@@ -34,20 +28,12 @@ export function renderList(state, screen, layout) {
|
|
|
34
28
|
for (let lineIndex = start; lineIndex < lines.length && y < bodyRect.height; lineIndex += 1) {
|
|
35
29
|
const line = lines[lineIndex];
|
|
36
30
|
if (line.kind === "group") {
|
|
37
|
-
|
|
38
|
-
if (cache.get(y) !== hash) {
|
|
39
|
-
writeLine(screen, bodyRect, y, formatGroupHeader(line.label, bodyRect), styles.muted);
|
|
40
|
-
cache.set(y, hash);
|
|
41
|
-
}
|
|
31
|
+
writeLine(screen, bodyRect, y, formatGroupHeader(line.label, bodyRect), styles.muted);
|
|
42
32
|
y += 1;
|
|
43
33
|
continue;
|
|
44
34
|
}
|
|
45
35
|
if (line.kind === "subtitle") {
|
|
46
|
-
|
|
47
|
-
if (cache.get(y) !== subtitleHash) {
|
|
48
|
-
writeLine(screen, bodyRect, y, ` ${line.text}`, styles.muted);
|
|
49
|
-
cache.set(y, subtitleHash);
|
|
50
|
-
}
|
|
36
|
+
writeLine(screen, bodyRect, y, ` ${line.text}`, styles.muted);
|
|
51
37
|
y += 1;
|
|
52
38
|
continue;
|
|
53
39
|
}
|
|
@@ -59,16 +45,12 @@ export function renderList(state, screen, layout) {
|
|
|
59
45
|
const selected = state.multiSelect && state.selected.has(row.id);
|
|
60
46
|
const cursor = rowIndex === state.filtered[state.cursor];
|
|
61
47
|
const positions = state.matchPositions.get(rowIndex) ?? [];
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
positions
|
|
69
|
-
});
|
|
70
|
-
cache.set(y, hash);
|
|
71
|
-
}
|
|
48
|
+
renderRow(screen, bodyRect, y, row, {
|
|
49
|
+
selected,
|
|
50
|
+
cursor,
|
|
51
|
+
focused: state.focused === "list",
|
|
52
|
+
positions
|
|
53
|
+
});
|
|
72
54
|
y += 1;
|
|
73
55
|
}
|
|
74
56
|
}
|
|
@@ -91,7 +73,7 @@ function buildDisplayLines(state) {
|
|
|
91
73
|
}
|
|
92
74
|
return lines;
|
|
93
75
|
}
|
|
94
|
-
function visibleStart(lines, height) {
|
|
76
|
+
export function visibleStart(lines, height, scrolloff = 3) {
|
|
95
77
|
if (height <= 0) {
|
|
96
78
|
return 0;
|
|
97
79
|
}
|
|
@@ -99,7 +81,7 @@ function visibleStart(lines, height) {
|
|
|
99
81
|
if (cursorLine < 0) {
|
|
100
82
|
return 0;
|
|
101
83
|
}
|
|
102
|
-
return Math.max(0, Math.
|
|
84
|
+
return Math.max(0, cursorLine - Math.max(0, height - 1 - scrolloff));
|
|
103
85
|
}
|
|
104
86
|
function formatGroupHeader(label, rect) {
|
|
105
87
|
const text = ` ${label} `;
|
|
@@ -111,7 +93,7 @@ function formatGroupHeader(label, rect) {
|
|
|
111
93
|
}
|
|
112
94
|
function renderRow(screen, rect, rowY, row, opts) {
|
|
113
95
|
const styles = getExplorerStyles();
|
|
114
|
-
const marker = opts.selected ? "
|
|
96
|
+
const marker = opts.selected ? "*" : " ";
|
|
115
97
|
const cursor = opts.cursor ? "●" : "◌";
|
|
116
98
|
const focus = opts.cursor && opts.focused ? " ▌" : "";
|
|
117
99
|
const prefix = `${marker} ${cursor} `;
|
|
@@ -149,9 +131,6 @@ function renderRow(screen, rect, rowY, row, opts) {
|
|
|
149
131
|
function writeLine(screen, rect, row, text, style = {}) {
|
|
150
132
|
screen.put(rect.x, rect.y + row, fitToWidth(text, rect.width, rect.x), style);
|
|
151
133
|
}
|
|
152
|
-
function lineHash(row, selected, cursor, positions) {
|
|
153
|
-
return `${row.id}:${selected ? 1 : 0}:${cursor ? 1 : 0}:${positions.join(",")}`;
|
|
154
|
-
}
|
|
155
134
|
function hasMatchPosition(start, end, positions) {
|
|
156
135
|
for (let position = start; position < end; position += 1) {
|
|
157
136
|
if (positions.has(position)) {
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { ScreenBuffer } from "../../
|
|
1
|
+
import type { ScreenSurface as ScreenBuffer } from "../../screen/screen.js";
|
|
2
2
|
import type { ExplorerState } from "../state.js";
|
|
3
3
|
export declare function renderModal(state: ExplorerState, screen: ScreenBuffer): void;
|
|
@@ -29,16 +29,16 @@ function modalLines(state) {
|
|
|
29
29
|
}
|
|
30
30
|
if (state.modal?.kind === "confirm") {
|
|
31
31
|
return [
|
|
32
|
-
state.modal.
|
|
33
|
-
|
|
34
|
-
"Enter confirms · Esc cancels"
|
|
32
|
+
state.modal.message,
|
|
33
|
+
` [ ${state.modal.confirmLabel} ] ${state.modal.cancelLabel}`
|
|
35
34
|
];
|
|
36
35
|
}
|
|
36
|
+
if (state.modal?.kind === "input") {
|
|
37
|
+
const value = state.modal.value.length > 0 ? state.modal.value : (state.modal.placeholder ?? "");
|
|
38
|
+
return [state.modal.label, ` ${value}▌`];
|
|
39
|
+
}
|
|
37
40
|
if (state.modal?.kind === "palette") {
|
|
38
|
-
return [
|
|
39
|
-
`Query: ${state.modal.query}`,
|
|
40
|
-
...paletteLines(state)
|
|
41
|
-
];
|
|
41
|
+
return [`Query: ${state.modal.query}`, ...paletteLines(state)];
|
|
42
42
|
}
|
|
43
43
|
if (state.modal?.kind === "content") {
|
|
44
44
|
return state.modal.content.split("\n").slice(state.modal.scroll);
|
|
@@ -50,7 +50,10 @@ function title(state) {
|
|
|
50
50
|
return "Keybindings";
|
|
51
51
|
}
|
|
52
52
|
if (state.modal?.kind === "confirm") {
|
|
53
|
-
return
|
|
53
|
+
return state.modal.title;
|
|
54
|
+
}
|
|
55
|
+
if (state.modal?.kind === "input") {
|
|
56
|
+
return state.modal.title;
|
|
54
57
|
}
|
|
55
58
|
if (state.modal?.kind === "content") {
|
|
56
59
|
return state.modal.title;
|
|
@@ -95,6 +98,3 @@ function paletteLines(state) {
|
|
|
95
98
|
}
|
|
96
99
|
return lines.length === 0 ? [" No actions"] : lines;
|
|
97
100
|
}
|
|
98
|
-
function labelFor(action) {
|
|
99
|
-
return typeof action.label === "function" ? action.label() : action.label;
|
|
100
|
-
}
|