toolcraft-openapi 0.0.152 → 0.0.153
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/composition.json +2 -2
- package/node_modules/toolcraft-design/README.md +30 -0
- package/node_modules/toolcraft-design/dist/dashboard/terminal-width.d.ts +1 -0
- package/node_modules/toolcraft-design/dist/dashboard/terminal-width.js +18 -1
- package/node_modules/toolcraft-design/dist/explorer/actions.d.ts +1 -1
- package/node_modules/toolcraft-design/dist/explorer/actions.js +22 -4
- package/node_modules/toolcraft-design/dist/explorer/filter.js +1 -1
- package/node_modules/toolcraft-design/dist/explorer/index.d.ts +2 -4
- package/node_modules/toolcraft-design/dist/explorer/index.js +1 -2
- package/node_modules/toolcraft-design/dist/explorer/jobs.d.ts +2 -2
- package/node_modules/toolcraft-design/dist/explorer/jobs.js +3 -5
- package/node_modules/toolcraft-design/dist/explorer/keymap.d.ts +13 -3
- package/node_modules/toolcraft-design/dist/explorer/keymap.js +89 -348
- package/node_modules/toolcraft-design/dist/explorer/layout.d.ts +1 -0
- package/node_modules/toolcraft-design/dist/explorer/layout.js +14 -4
- package/node_modules/toolcraft-design/dist/explorer/reducer.js +58 -9
- package/node_modules/toolcraft-design/dist/explorer/render/detail.d.ts +1 -1
- package/node_modules/toolcraft-design/dist/explorer/render/detail.js +43 -7
- package/node_modules/toolcraft-design/dist/explorer/render/footer.d.ts +1 -1
- package/node_modules/toolcraft-design/dist/explorer/render/footer.js +6 -3
- package/node_modules/toolcraft-design/dist/explorer/render/header.d.ts +1 -1
- package/node_modules/toolcraft-design/dist/explorer/render/header.js +6 -2
- package/node_modules/toolcraft-design/dist/explorer/render/index.d.ts +1 -1
- package/node_modules/toolcraft-design/dist/explorer/render/index.js +2 -1
- package/node_modules/toolcraft-design/dist/explorer/render/list.d.ts +16 -2
- package/node_modules/toolcraft-design/dist/explorer/render/list.js +13 -34
- package/node_modules/toolcraft-design/dist/explorer/render/modal.d.ts +1 -1
- package/node_modules/toolcraft-design/dist/explorer/render/pane.d.ts +5 -2
- package/node_modules/toolcraft-design/dist/explorer/render/pane.js +9 -6
- package/node_modules/toolcraft-design/dist/explorer/render/test-fixtures.js +2 -3
- package/node_modules/toolcraft-design/dist/explorer/runtime.js +68 -63
- package/node_modules/toolcraft-design/dist/explorer/runtime.test-helpers.d.ts +23 -37
- package/node_modules/toolcraft-design/dist/explorer/runtime.test-helpers.js +32 -83
- package/node_modules/toolcraft-design/dist/explorer/state.d.ts +47 -5
- package/node_modules/toolcraft-design/dist/explorer/state.js +56 -12
- package/node_modules/toolcraft-design/dist/index.d.ts +8 -2
- package/node_modules/toolcraft-design/dist/index.js +5 -1
- package/node_modules/toolcraft-design/dist/screen/ansi-text.d.ts +2 -0
- package/node_modules/toolcraft-design/dist/screen/ansi-text.js +35 -0
- package/node_modules/toolcraft-design/dist/screen/screen.d.ts +40 -0
- package/node_modules/toolcraft-design/dist/screen/screen.js +122 -0
- package/node_modules/toolcraft-design/dist/screen/style.d.ts +16 -0
- package/node_modules/toolcraft-design/dist/screen/style.js +55 -0
- package/node_modules/toolcraft-design/dist/terminal/driver.d.ts +33 -0
- package/node_modules/toolcraft-design/dist/terminal/driver.js +61 -0
- package/node_modules/toolcraft-design/dist/terminal/input.d.ts +25 -0
- package/node_modules/toolcraft-design/dist/terminal/input.js +181 -0
- package/node_modules/toolcraft-design/dist/terminal/output.d.ts +8 -0
- package/node_modules/toolcraft-design/dist/terminal/output.js +42 -0
- package/node_modules/toolcraft-schema/package.json +1 -1
- package/package.json +3 -3
- package/node_modules/toolcraft-design/dist/explorer/two-pane.d.ts +0 -102
- package/node_modules/toolcraft-design/dist/explorer/two-pane.js +0 -510
- package/node_modules/toolcraft-design/dist/run-two-pane-explorer.d.ts +0 -2
- package/node_modules/toolcraft-design/dist/run-two-pane-explorer.js +0 -1
|
@@ -53,6 +53,10 @@ function stepKey(state, key, runtimeHandles) {
|
|
|
53
53
|
return stepFilterKey(state, key, target, runtimeHandles);
|
|
54
54
|
}
|
|
55
55
|
if (target?.type === "action") {
|
|
56
|
+
if (state.actionState.get(target.id)?.running === true) {
|
|
57
|
+
runtimeHandles.toast("Action already running", "info");
|
|
58
|
+
return mark(state, 0);
|
|
59
|
+
}
|
|
56
60
|
if (state.actionState.get(target.id)?.source === "detail" && state.focused !== "detail") {
|
|
57
61
|
return mark(state, 0);
|
|
58
62
|
}
|
|
@@ -85,6 +89,14 @@ function stepKey(state, key, runtimeHandles) {
|
|
|
85
89
|
return isDetailBlobFocused(state)
|
|
86
90
|
? detailScroll(state, detailBodyHeight(state))
|
|
87
91
|
: moveCursor(state, pageSize(state));
|
|
92
|
+
case "halfPageUp":
|
|
93
|
+
return isDetailBlobFocused(state)
|
|
94
|
+
? detailScroll(state, -Math.max(1, Math.floor(detailBodyHeight(state) / 2)))
|
|
95
|
+
: moveCursor(state, -Math.max(1, Math.floor(pageSize(state) / 2)));
|
|
96
|
+
case "halfPageDown":
|
|
97
|
+
return isDetailBlobFocused(state)
|
|
98
|
+
? detailScroll(state, Math.max(1, Math.floor(detailBodyHeight(state) / 2)))
|
|
99
|
+
: moveCursor(state, Math.max(1, Math.floor(pageSize(state) / 2)));
|
|
88
100
|
case "focusNext":
|
|
89
101
|
return focusNext(state);
|
|
90
102
|
case "escape":
|
|
@@ -112,12 +124,16 @@ function stepKey(state, key, runtimeHandles) {
|
|
|
112
124
|
}
|
|
113
125
|
}
|
|
114
126
|
if (isBackspace(key)) {
|
|
127
|
+
if (isSecondListFocused(state))
|
|
128
|
+
return updateDetailFilter(state, (state.detail.filter ?? "").slice(0, -1));
|
|
115
129
|
return updateFilter(state, state.filter.slice(0, -1));
|
|
116
130
|
}
|
|
117
131
|
if (!state.multiSelect && isSelectionSpace(key)) {
|
|
118
132
|
return mark(state, 0);
|
|
119
133
|
}
|
|
120
134
|
if (isPrintable(key)) {
|
|
135
|
+
if (isSecondListFocused(state))
|
|
136
|
+
return updateDetailFilter(state, `${state.detail.filter ?? ""}${key.ch}`);
|
|
121
137
|
return updateFilter(state, `${state.filter}${key.ch}`);
|
|
122
138
|
}
|
|
123
139
|
return mark(state, 0);
|
|
@@ -201,7 +217,7 @@ function stepModalKey(state, key, target, runtimeHandles) {
|
|
|
201
217
|
}
|
|
202
218
|
function resize(state, cols, rows) {
|
|
203
219
|
const size = { cols: normalizeSize(cols), rows: normalizeSize(rows) };
|
|
204
|
-
const layout = resolveExplorerLayoutMode(size.cols);
|
|
220
|
+
const layout = resolveExplorerLayoutMode(size.cols, size.rows);
|
|
205
221
|
if (state.size.cols === size.cols && state.size.rows === size.rows && state.layout === layout) {
|
|
206
222
|
return mark(state, 0);
|
|
207
223
|
}
|
|
@@ -211,6 +227,7 @@ function resize(state, cols, rows) {
|
|
|
211
227
|
};
|
|
212
228
|
}
|
|
213
229
|
function rowsLoaded(state, rows) {
|
|
230
|
+
const previousRowId = currentRow(state)?.id;
|
|
214
231
|
const rowIds = new Set();
|
|
215
232
|
for (const row of rows) {
|
|
216
233
|
if (rowIds.has(row.id)) {
|
|
@@ -221,14 +238,19 @@ function rowsLoaded(state, rows) {
|
|
|
221
238
|
const matches = filterRows(state.filter, rows);
|
|
222
239
|
const filtered = matches.map((match) => match.index);
|
|
223
240
|
const matchPositions = createMatchPositions(matches);
|
|
224
|
-
const
|
|
241
|
+
const identityCursor = previousRowId === undefined
|
|
242
|
+
? -1
|
|
243
|
+
: filtered.findIndex(index => rows[index]?.id === previousRowId);
|
|
244
|
+
const cursor = identityCursor >= 0
|
|
245
|
+
? identityCursor
|
|
246
|
+
: clamp(state.cursor, 0, Math.max(0, filtered.length - 1));
|
|
225
247
|
const selected = state.multiSelect ? pruneSelection(state.selected, rows) : new Set();
|
|
226
248
|
const detail = resetDetailForCursor(state, rows, filtered, cursor);
|
|
227
249
|
const modal = modalStillValid(state.modal, rows);
|
|
228
250
|
if (state.modal?.kind === "confirm" && modal === null) {
|
|
229
251
|
state.modal.resolver(false);
|
|
230
252
|
}
|
|
231
|
-
const nextView = { ...state, rows, filtered, matchPositions, cursor, selected, detail, modal };
|
|
253
|
+
const nextView = { ...state, rows, rowsLoading: false, filtered, matchPositions, cursor, selected, detail, modal };
|
|
232
254
|
const next = {
|
|
233
255
|
...nextView,
|
|
234
256
|
actionState: recomputeActionState(nextView),
|
|
@@ -253,10 +275,12 @@ function detailLoaded(state, rowId, token, items) {
|
|
|
253
275
|
if (state.detail.rowId !== rowId || state.detail.token !== token) {
|
|
254
276
|
return mark(state, 0);
|
|
255
277
|
}
|
|
278
|
+
const visibleItems = filterDetailItems(items, state.detail.filter ?? "");
|
|
256
279
|
const detail = {
|
|
257
280
|
...state.detail,
|
|
258
|
-
items,
|
|
259
|
-
|
|
281
|
+
items: visibleItems,
|
|
282
|
+
allItems: items,
|
|
283
|
+
cursor: clamp(state.detail.cursor, 0, Math.max(0, visibleItems.length - 1)),
|
|
260
284
|
scroll: 0,
|
|
261
285
|
loading: false
|
|
262
286
|
};
|
|
@@ -410,6 +434,22 @@ function updateFilter(state, filter) {
|
|
|
410
434
|
const effect = detailEffect(next);
|
|
411
435
|
return { state: next, effects: effect === undefined ? NO_EFFECTS : [effect] };
|
|
412
436
|
}
|
|
437
|
+
function updateDetailFilter(state, filter) {
|
|
438
|
+
const allItems = state.detail.allItems ?? state.detail.items ?? [];
|
|
439
|
+
const items = filterDetailItems(allItems, filter);
|
|
440
|
+
const detail = { ...state.detail, filter, items, cursor: 0, scroll: 0 };
|
|
441
|
+
return {
|
|
442
|
+
state: { ...state, detail, actionState: recomputeActionState({ ...state, detail }), dirty: REGION_HEADER | REGION_DETAIL | REGION_FOOTER },
|
|
443
|
+
effects: NO_EFFECTS
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
function filterDetailItems(items, query) {
|
|
447
|
+
const rows = items.map(item => ({ id: item.id, title: item.title ?? item.id, subtitle: item.subtitle }));
|
|
448
|
+
return filterRows(query, rows).map(match => items[match.index]).filter((item) => item !== undefined);
|
|
449
|
+
}
|
|
450
|
+
function isSecondListFocused(state) {
|
|
451
|
+
return state.focused === "detail" && state.paneDefinitions[1]?.kind === "list";
|
|
452
|
+
}
|
|
413
453
|
function focusFilter(state) {
|
|
414
454
|
if (state.filterFocused) {
|
|
415
455
|
return mark(state, 0);
|
|
@@ -432,6 +472,9 @@ function focusNext(state) {
|
|
|
432
472
|
};
|
|
433
473
|
}
|
|
434
474
|
function escape(state, runtimeHandles) {
|
|
475
|
+
if (isSecondListFocused(state) && (state.detail.filter ?? "").length > 0) {
|
|
476
|
+
return updateDetailFilter(state, "");
|
|
477
|
+
}
|
|
435
478
|
if (state.filterFocused || state.filter.length > 0) {
|
|
436
479
|
const cleared = updateFilter({ ...state, filterFocused: false }, "");
|
|
437
480
|
return {
|
|
@@ -455,13 +498,17 @@ function confirmKey(state, runtimeHandles) {
|
|
|
455
498
|
if (state.modal?.kind === "confirm") {
|
|
456
499
|
return modalDismissed(state, true, runtimeHandles);
|
|
457
500
|
}
|
|
458
|
-
|
|
501
|
+
const hasActions = [...state.actionState.values()].some(entry => entry.available && entry.running !== true);
|
|
502
|
+
return hasActions
|
|
503
|
+
? setModal(state, { kind: "palette", query: "", cursor: 0 })
|
|
504
|
+
: dispatchPrimary(state, runtimeHandles);
|
|
459
505
|
}
|
|
460
506
|
function toggleSelect(state) {
|
|
461
507
|
if (!state.multiSelect) {
|
|
462
508
|
return mark(state, 0);
|
|
463
509
|
}
|
|
464
|
-
const
|
|
510
|
+
const detailItem = state.focused === "detail" ? state.detail.items?.[state.detail.cursor] : undefined;
|
|
511
|
+
const row = detailItem === undefined ? currentRow(state) : { id: detailItem.id, title: detailItem.title ?? detailItem.id };
|
|
465
512
|
if (row === undefined) {
|
|
466
513
|
return mark(state, 0);
|
|
467
514
|
}
|
|
@@ -736,7 +783,9 @@ function recomputeActionState(view) {
|
|
|
736
783
|
continue;
|
|
737
784
|
}
|
|
738
785
|
const ctx = buildActionContext(view, action, entry.source ?? actionSource(view, action), DEFAULT_ACTION_HANDLES);
|
|
739
|
-
const
|
|
786
|
+
const row = currentRow(view);
|
|
787
|
+
const visible = action.visible === undefined || (row !== undefined && action.visible(row));
|
|
788
|
+
const available = visible && (action.predicate === undefined ? entry.available : action.predicate(ctx));
|
|
740
789
|
const label = typeof action.label === "function" ? action.label() : action.label;
|
|
741
790
|
next.set(id, { ...entry, available, label });
|
|
742
791
|
}
|
|
@@ -756,7 +805,7 @@ function resetDetailForCursor(state, rows, filtered, cursor) {
|
|
|
756
805
|
}
|
|
757
806
|
return {
|
|
758
807
|
rowId: row.id,
|
|
759
|
-
items:
|
|
808
|
+
items: state.detail.items,
|
|
760
809
|
cursor: 0,
|
|
761
810
|
scroll: 0,
|
|
762
811
|
token: state.detail.token + 1,
|
|
@@ -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();
|
|
@@ -12,11 +13,11 @@ export function renderDetail(state, screen, layout) {
|
|
|
12
13
|
}
|
|
13
14
|
const row = state.rows.find((r) => r.id === state.detail.rowId) ?? null;
|
|
14
15
|
if (layout.mode === "narrow-vertical") {
|
|
15
|
-
drawPaneFrame(screen, rect, "Preview", state.focused === "detail" ? styles.borderFocused : styles.border);
|
|
16
|
+
drawPaneFrame(screen, rect, state.paneDefinitions[1]?.title ?? "Preview", state.focused === "detail" ? styles.borderFocused : styles.border, { focused: state.focused === "detail", indicator: scrollIndicator(state) });
|
|
16
17
|
renderDetailBody(state, screen, paneBodyRect(rect), row);
|
|
17
18
|
return;
|
|
18
19
|
}
|
|
19
|
-
drawPaneFrame(screen, rect, "Preview", state.focused === "detail" ? styles.borderFocused : styles.border);
|
|
20
|
+
drawPaneFrame(screen, rect, state.paneDefinitions[1]?.title ?? "Preview", state.focused === "detail" ? styles.borderFocused : styles.border, { focused: state.focused === "detail", indicator: scrollIndicator(state) });
|
|
20
21
|
renderDetailBody(state, screen, paneBodyRect(rect), row);
|
|
21
22
|
}
|
|
22
23
|
function renderDetailBody(state, screen, rect, row) {
|
|
@@ -47,7 +48,8 @@ function renderListMode(state, screen, rect, items, row) {
|
|
|
47
48
|
const item = items[index];
|
|
48
49
|
const cursor = index === state.detail.cursor;
|
|
49
50
|
const title = item.title ?? item.id;
|
|
50
|
-
const
|
|
51
|
+
const selected = state.multiSelect && state.selected.has(item.id);
|
|
52
|
+
const prefix = `${selected ? "*" : " "}${cursor ? "▌" : " "} `;
|
|
51
53
|
const badge = item.badge ? ` ${item.badge.text}` : "";
|
|
52
54
|
writeLine(screen, rect, y, `${prefix}${title}${badge}`, cursor ? styles.borderFocused : styles.accent);
|
|
53
55
|
y += 1;
|
|
@@ -68,11 +70,23 @@ function renderListMode(state, screen, rect, items, row) {
|
|
|
68
70
|
}
|
|
69
71
|
}
|
|
70
72
|
function renderBlob(screen, rect, text, scroll) {
|
|
71
|
-
const allLines =
|
|
73
|
+
const allLines = [[]];
|
|
74
|
+
for (const cell of ansiToCells(text)) {
|
|
75
|
+
if (cell.ch === "\n")
|
|
76
|
+
allLines.push([]);
|
|
77
|
+
else
|
|
78
|
+
allLines.at(-1).push(cell);
|
|
79
|
+
}
|
|
72
80
|
const start = clamp(scroll, 0, Math.max(0, allLines.length - rect.height));
|
|
73
81
|
const lines = allLines.slice(start);
|
|
74
82
|
for (let row = 0; row < rect.height; row += 1) {
|
|
75
|
-
|
|
83
|
+
let x = rect.x;
|
|
84
|
+
for (const cell of lines[row] ?? []) {
|
|
85
|
+
if (x + cell.width > rect.x + rect.width)
|
|
86
|
+
break;
|
|
87
|
+
screen.put(x, rect.y + row, cell.ch, cell.style);
|
|
88
|
+
x += cell.width;
|
|
89
|
+
}
|
|
76
90
|
}
|
|
77
91
|
}
|
|
78
92
|
function renderItemMarkdown(item, rect, row) {
|
|
@@ -80,7 +94,14 @@ function renderItemMarkdown(item, rect, row) {
|
|
|
80
94
|
if (content.trim().length === 0) {
|
|
81
95
|
return "";
|
|
82
96
|
}
|
|
83
|
-
|
|
97
|
+
const width = Math.max(1, rect.width);
|
|
98
|
+
const key = `${contentHash(content)}:${width}`;
|
|
99
|
+
const cached = markdownCache.get(key);
|
|
100
|
+
if (cached !== undefined)
|
|
101
|
+
return cached;
|
|
102
|
+
const rendered = renderMarkdown(content, { width }).trimEnd();
|
|
103
|
+
markdownCache.set(key, rendered);
|
|
104
|
+
return rendered;
|
|
84
105
|
}
|
|
85
106
|
function renderItem(item, rect, row) {
|
|
86
107
|
if (item.renderedContent !== undefined) {
|
|
@@ -108,3 +129,18 @@ function writeLine(screen, rect, row, text, style = {}) {
|
|
|
108
129
|
function clamp(value, min, max) {
|
|
109
130
|
return Math.min(max, Math.max(min, value));
|
|
110
131
|
}
|
|
132
|
+
function scrollIndicator(state) {
|
|
133
|
+
if (state.detail.loading)
|
|
134
|
+
return "⠋";
|
|
135
|
+
const content = state.detail.items?.[state.detail.cursor]?.renderedContent ?? "";
|
|
136
|
+
const total = Math.max(1, content.split("\n").length);
|
|
137
|
+
return `${Math.min(100, Math.round((state.detail.scroll / Math.max(1, total - 1)) * 100))}%`;
|
|
138
|
+
}
|
|
139
|
+
function contentHash(content) {
|
|
140
|
+
let hash = 2166136261;
|
|
141
|
+
for (let index = 0; index < content.length; index += 1) {
|
|
142
|
+
hash ^= content.charCodeAt(index);
|
|
143
|
+
hash = Math.imul(hash, 16777619);
|
|
144
|
+
}
|
|
145
|
+
return hash >>> 0;
|
|
146
|
+
}
|
|
@@ -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;
|
|
@@ -39,8 +39,8 @@ function footerHints(state) {
|
|
|
39
39
|
const hints = [];
|
|
40
40
|
if (state.focused === "detail") {
|
|
41
41
|
hints.push({ key: "Tab", label: "focus", running: false });
|
|
42
|
-
hints.push({ key: "Enter", label: "sub", running: false });
|
|
43
42
|
}
|
|
43
|
+
hints.push({ key: "Enter", label: "actions", running: false });
|
|
44
44
|
for (const [id, entry] of state.actionState) {
|
|
45
45
|
if (!entry.available || entry.action?.showInFooter === false) {
|
|
46
46
|
continue;
|
|
@@ -51,12 +51,13 @@ function footerHints(state) {
|
|
|
51
51
|
: entry.label;
|
|
52
52
|
hints.push({ key, label, running: entry.running === true });
|
|
53
53
|
}
|
|
54
|
-
hints.push({ key: "?", label: "help", running: false });
|
|
55
54
|
hints.push({ key: "Ctrl+P", label: "palette", running: false });
|
|
56
55
|
if (hasShiftReorderBindings(state)) {
|
|
57
56
|
hints.push({ key: "⇧↑↓", label: "reorder (within state)", running: false, bracketed: false });
|
|
58
57
|
}
|
|
59
|
-
hints.push({ key: "
|
|
58
|
+
hints.push({ key: "Esc", label: "clear/quit", running: false });
|
|
59
|
+
if (state.selected.size > 0)
|
|
60
|
+
hints.push({ key: `${state.selected.size}`, label: "selected", running: false, bracketed: false });
|
|
60
61
|
return hints;
|
|
61
62
|
}
|
|
62
63
|
function hasShiftReorderBindings(state) {
|
|
@@ -65,6 +66,8 @@ function hasShiftReorderBindings(state) {
|
|
|
65
66
|
return up.includes("Shift+up") && down.includes("Shift+down");
|
|
66
67
|
}
|
|
67
68
|
function actionKey(entry, fallback) {
|
|
69
|
+
if (entry.action?.accelerator !== undefined)
|
|
70
|
+
return `Ctrl+${entry.action.accelerator.toUpperCase()}`;
|
|
68
71
|
const key = entry.action?.key;
|
|
69
72
|
if (Array.isArray(key)) {
|
|
70
73
|
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;
|
|
@@ -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
|