toolcraft-openapi 0.0.17 → 0.0.19
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/bin/generate.js +7 -0
- package/dist/define-client.js +2 -2
- package/dist/generate.js +2 -2
- package/dist/http.d.ts +21 -2
- package/dist/http.js +147 -22
- package/dist/index.d.ts +1 -1
- package/dist/lock.d.ts +1 -1
- package/dist/lock.js +109 -5
- package/dist/mock/fetch.js +1 -1
- package/dist/network-error.d.ts +2 -0
- package/dist/network-error.js +83 -0
- package/dist/spec-source.js +103 -3
- package/node_modules/@poe-code/design-system/dist/acp/components.js +15 -13
- package/node_modules/@poe-code/design-system/dist/components/color.d.ts +31 -0
- package/node_modules/@poe-code/design-system/dist/components/color.js +101 -0
- package/node_modules/@poe-code/design-system/dist/components/help-formatter-plain.d.ts +1 -0
- package/node_modules/@poe-code/design-system/dist/components/help-formatter-plain.js +1 -1
- package/node_modules/@poe-code/design-system/dist/components/index.d.ts +4 -0
- package/node_modules/@poe-code/design-system/dist/components/index.js +2 -0
- package/node_modules/@poe-code/design-system/dist/components/logger.js +2 -2
- package/node_modules/@poe-code/design-system/dist/components/symbols.js +3 -3
- package/node_modules/@poe-code/design-system/dist/components/table.js +191 -40
- package/node_modules/@poe-code/design-system/dist/components/template.d.ts +6 -0
- package/node_modules/@poe-code/design-system/dist/components/template.js +271 -0
- package/node_modules/@poe-code/design-system/dist/components/text.d.ts +1 -0
- package/node_modules/@poe-code/design-system/dist/components/text.js +11 -3
- package/node_modules/@poe-code/design-system/dist/dashboard/buffer.js +20 -13
- package/node_modules/@poe-code/design-system/dist/dashboard/keymap.d.ts +5 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/keymap.js +146 -12
- package/node_modules/@poe-code/design-system/dist/dashboard/terminal.js +31 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/types.d.ts +1 -0
- package/node_modules/@poe-code/design-system/dist/explorer/actions.d.ts +16 -0
- package/node_modules/@poe-code/design-system/dist/explorer/actions.js +39 -0
- package/node_modules/@poe-code/design-system/dist/explorer/demo.d.ts +13 -0
- package/node_modules/@poe-code/design-system/dist/explorer/demo.js +297 -0
- package/node_modules/@poe-code/design-system/dist/explorer/events.d.ts +61 -0
- package/node_modules/@poe-code/design-system/dist/explorer/events.js +1 -0
- package/node_modules/@poe-code/design-system/dist/explorer/filter.d.ts +10 -0
- package/node_modules/@poe-code/design-system/dist/explorer/filter.js +95 -0
- package/node_modules/@poe-code/design-system/dist/explorer/index.d.ts +8 -0
- package/node_modules/@poe-code/design-system/dist/explorer/index.js +8 -0
- package/node_modules/@poe-code/design-system/dist/explorer/jobs.d.ts +7 -0
- package/node_modules/@poe-code/design-system/dist/explorer/jobs.js +59 -0
- package/node_modules/@poe-code/design-system/dist/explorer/keymap.d.ts +21 -0
- package/node_modules/@poe-code/design-system/dist/explorer/keymap.js +363 -0
- package/node_modules/@poe-code/design-system/dist/explorer/layout.d.ts +20 -0
- package/node_modules/@poe-code/design-system/dist/explorer/layout.js +73 -0
- package/node_modules/@poe-code/design-system/dist/explorer/reducer.d.ts +9 -0
- package/node_modules/@poe-code/design-system/dist/explorer/reducer.js +704 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/detail.d.ts +4 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/detail.js +96 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/footer.d.ts +4 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/footer.js +49 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/header.d.ts +4 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/header.js +56 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/index.d.ts +8 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/index.js +61 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/list.d.ts +4 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/list.js +106 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/modal.d.ts +3 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/modal.js +91 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/test-fixtures.d.ts +8 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/test-fixtures.js +156 -0
- package/node_modules/@poe-code/design-system/dist/explorer/runtime.d.ts +2 -0
- package/node_modules/@poe-code/design-system/dist/explorer/runtime.js +282 -0
- package/node_modules/@poe-code/design-system/dist/explorer/runtime.test-helpers.d.ts +50 -0
- package/node_modules/@poe-code/design-system/dist/explorer/runtime.test-helpers.js +101 -0
- package/node_modules/@poe-code/design-system/dist/explorer/state.d.ts +130 -0
- package/node_modules/@poe-code/design-system/dist/explorer/state.js +87 -0
- package/node_modules/@poe-code/design-system/dist/explorer/theme.d.ts +27 -0
- package/node_modules/@poe-code/design-system/dist/explorer/theme.js +97 -0
- package/node_modules/@poe-code/design-system/dist/index.d.ts +7 -0
- package/node_modules/@poe-code/design-system/dist/index.js +5 -0
- package/node_modules/@poe-code/design-system/dist/internal/color-support.d.ts +9 -0
- package/node_modules/@poe-code/design-system/dist/internal/color-support.js +12 -0
- package/node_modules/@poe-code/design-system/dist/prompts/index.js +2 -2
- package/node_modules/@poe-code/design-system/dist/prompts/primitives/cancel.js +2 -2
- package/node_modules/@poe-code/design-system/dist/prompts/primitives/intro.js +2 -2
- package/node_modules/@poe-code/design-system/dist/prompts/primitives/log.js +4 -4
- package/node_modules/@poe-code/design-system/dist/prompts/primitives/note.js +5 -5
- package/node_modules/@poe-code/design-system/dist/prompts/primitives/outro.js +2 -2
- package/node_modules/@poe-code/design-system/dist/prompts/primitives/spinner.js +3 -3
- package/node_modules/@poe-code/design-system/dist/static/menu.js +5 -5
- package/node_modules/@poe-code/design-system/dist/static/spinner.js +8 -8
- package/node_modules/@poe-code/design-system/dist/tokens/colors.js +29 -29
- package/node_modules/@poe-code/design-system/dist/tokens/typography.js +6 -6
- package/node_modules/@poe-code/design-system/package.json +6 -3
- package/package.json +2 -4
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { ScreenBuffer, cellToAnsi, diff } from "../dashboard/buffer.js";
|
|
2
|
+
import { createTerminalDriver } from "../dashboard/terminal.js";
|
|
3
|
+
import { createDetailJobs } from "./jobs.js";
|
|
4
|
+
import { computeExplorerLayout } from "./layout.js";
|
|
5
|
+
import { renderExplorer } from "./render/index.js";
|
|
6
|
+
import { step } from "./reducer.js";
|
|
7
|
+
import { createInitialState, REGION_ALL, REGION_MODAL, REGION_TOAST } from "./state.js";
|
|
8
|
+
const TOAST_MS = 2500;
|
|
9
|
+
export async function runExplorer(config) {
|
|
10
|
+
if (process.stdout.isTTY !== true) {
|
|
11
|
+
throw new Error("explorer requires a TTY");
|
|
12
|
+
}
|
|
13
|
+
const driver = createTerminalDriver();
|
|
14
|
+
const runtime = new ExplorerRuntime(config, driver);
|
|
15
|
+
return runtime.run();
|
|
16
|
+
}
|
|
17
|
+
class ExplorerRuntime {
|
|
18
|
+
config;
|
|
19
|
+
driver;
|
|
20
|
+
state;
|
|
21
|
+
previousBuffer = new ScreenBuffer(0, 0);
|
|
22
|
+
detailJobs;
|
|
23
|
+
runtimeHandles;
|
|
24
|
+
pendingEffects = new Set();
|
|
25
|
+
unsubscribeKeypress;
|
|
26
|
+
unsubscribeResize;
|
|
27
|
+
toastTimer;
|
|
28
|
+
stopped = false;
|
|
29
|
+
settle;
|
|
30
|
+
constructor(config, driver) {
|
|
31
|
+
this.config = config;
|
|
32
|
+
this.driver = driver;
|
|
33
|
+
this.state = createInitialState(config, driver.getSize());
|
|
34
|
+
this.detailJobs = createDetailJobs((event) => {
|
|
35
|
+
this.dispatch(event);
|
|
36
|
+
});
|
|
37
|
+
this.runtimeHandles = {
|
|
38
|
+
refresh: async () => {
|
|
39
|
+
await this.refreshRows();
|
|
40
|
+
},
|
|
41
|
+
suspendAnd: async (fn) => this.suspendAnd(fn),
|
|
42
|
+
toast: (msg, tone) => {
|
|
43
|
+
this.showToast(msg, tone);
|
|
44
|
+
},
|
|
45
|
+
confirm: async (prompt) => this.confirm(prompt),
|
|
46
|
+
exit: (after) => {
|
|
47
|
+
this.exit(null, after);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
run() {
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
this.settle = { resolve, reject };
|
|
54
|
+
try {
|
|
55
|
+
this.startTerminal();
|
|
56
|
+
this.render();
|
|
57
|
+
this.refreshRows().catch((error) => {
|
|
58
|
+
this.fail(error);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
this.fail(error);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
startTerminal() {
|
|
67
|
+
this.driver.enterRawMode();
|
|
68
|
+
this.driver.enterAltScreen();
|
|
69
|
+
this.driver.disableLineWrap();
|
|
70
|
+
this.driver.hideCursor();
|
|
71
|
+
this.unsubscribeKeypress = this.driver.onKeypress((key) => {
|
|
72
|
+
this.dispatch({ type: "key", key });
|
|
73
|
+
});
|
|
74
|
+
this.unsubscribeResize = this.driver.onResize(() => {
|
|
75
|
+
const size = this.driver.getSize();
|
|
76
|
+
this.dispatch({ type: "resize", cols: size.cols, rows: size.rows });
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
async refreshRows() {
|
|
80
|
+
const rows = await this.config.rows();
|
|
81
|
+
this.dispatch({ type: "rowsLoaded", rows });
|
|
82
|
+
}
|
|
83
|
+
dispatch(event) {
|
|
84
|
+
if (this.stopped) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const previousState = this.state;
|
|
88
|
+
const next = step(this.state, event, this.runtimeHandles);
|
|
89
|
+
this.state = next.state;
|
|
90
|
+
this.render();
|
|
91
|
+
this.applyEffects(next.effects, previousState);
|
|
92
|
+
}
|
|
93
|
+
applyEffects(effects, previousState) {
|
|
94
|
+
for (const effect of effects) {
|
|
95
|
+
if (effect.type === "renderDetail") {
|
|
96
|
+
this.renderDetail(effect.rowId);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (effect.type === "persistOrder") {
|
|
100
|
+
this.track(this.persistOrder(effect.orderedIds, previousState.rows));
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (effect.type === "suspend") {
|
|
104
|
+
this.track(this.runActionEffect(effect));
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (effect.type === "exit") {
|
|
108
|
+
this.exit(effect.result, effect.after);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
renderDetail(rowId) {
|
|
113
|
+
const row = this.state.rows.find((candidate) => candidate.id === rowId);
|
|
114
|
+
if (row === undefined) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const layout = computeExplorerLayout({
|
|
118
|
+
cols: this.state.size.cols,
|
|
119
|
+
rows: this.state.size.rows,
|
|
120
|
+
detailHidden: this.state.layout === "narrow-list-only" || this.state.layout === "too-narrow"
|
|
121
|
+
});
|
|
122
|
+
void this.detailJobs.schedule(rowId, (ctx) => this.config.detail.items(row, ctx), {
|
|
123
|
+
width: layout.detail.width,
|
|
124
|
+
height: layout.detail.height,
|
|
125
|
+
row,
|
|
126
|
+
signal: new AbortController().signal
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
async persistOrder(orderedIds, previousRows) {
|
|
130
|
+
try {
|
|
131
|
+
await this.config.reorder?.onReorder(orderedIds);
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
this.showToast(error instanceof Error ? error.message : "Could not persist order", "error");
|
|
135
|
+
this.dispatch({ type: "rowsLoaded", rows: previousRows });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async runActionEffect(effect) {
|
|
139
|
+
try {
|
|
140
|
+
const value = await effect.fn();
|
|
141
|
+
this.dispatch(effect.resumeWith(value));
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
this.showToast(error instanceof Error ? error.message : "Action failed", "error");
|
|
145
|
+
this.dispatch(effect.resumeWith(error));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async suspendAnd(fn) {
|
|
149
|
+
this.driver.exitAltScreen();
|
|
150
|
+
this.driver.enableLineWrap();
|
|
151
|
+
this.driver.showCursor();
|
|
152
|
+
this.driver.exitRawMode();
|
|
153
|
+
try {
|
|
154
|
+
return await fn();
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
if (!this.stopped) {
|
|
158
|
+
this.driver.enterRawMode();
|
|
159
|
+
this.driver.enterAltScreen();
|
|
160
|
+
this.driver.disableLineWrap();
|
|
161
|
+
this.driver.hideCursor();
|
|
162
|
+
const size = this.driver.getSize();
|
|
163
|
+
this.dispatch({
|
|
164
|
+
type: "suspendResumed",
|
|
165
|
+
value: null,
|
|
166
|
+
emit: { type: "resize", cols: size.cols, rows: size.rows }
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
confirm(prompt) {
|
|
172
|
+
return new Promise((resolve) => {
|
|
173
|
+
const action = {
|
|
174
|
+
id: "__confirm__",
|
|
175
|
+
label: prompt,
|
|
176
|
+
handler: () => undefined
|
|
177
|
+
};
|
|
178
|
+
const row = this.currentRow();
|
|
179
|
+
this.state = {
|
|
180
|
+
...this.state,
|
|
181
|
+
modal: {
|
|
182
|
+
kind: "confirm",
|
|
183
|
+
action,
|
|
184
|
+
rows: row === undefined ? [] : [row],
|
|
185
|
+
resolver: resolve
|
|
186
|
+
},
|
|
187
|
+
dirty: REGION_MODAL
|
|
188
|
+
};
|
|
189
|
+
this.render();
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
showToast(message, tone = "info") {
|
|
193
|
+
if (this.stopped) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (this.toastTimer !== undefined) {
|
|
197
|
+
clearTimeout(this.toastTimer);
|
|
198
|
+
}
|
|
199
|
+
this.state = {
|
|
200
|
+
...this.state,
|
|
201
|
+
toast: { message, tone, expiresAt: Date.now() + TOAST_MS },
|
|
202
|
+
dirty: REGION_TOAST
|
|
203
|
+
};
|
|
204
|
+
this.render();
|
|
205
|
+
this.toastTimer = setTimeout(() => {
|
|
206
|
+
this.dispatch({ type: "toastExpired" });
|
|
207
|
+
}, TOAST_MS);
|
|
208
|
+
}
|
|
209
|
+
currentRow() {
|
|
210
|
+
return this.state.rows[this.state.filtered[this.state.cursor] ?? -1];
|
|
211
|
+
}
|
|
212
|
+
render() {
|
|
213
|
+
if (this.stopped) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const size = this.driver.getSize();
|
|
217
|
+
if (size.cols !== this.state.size.cols || size.rows !== this.state.size.rows) {
|
|
218
|
+
this.state = step(this.state, { type: "resize", cols: size.cols, rows: size.rows }, this.runtimeHandles).state;
|
|
219
|
+
}
|
|
220
|
+
const nextBuffer = this.state.dirty === REGION_ALL
|
|
221
|
+
? new ScreenBuffer(this.state.size.cols, this.state.size.rows)
|
|
222
|
+
: cloneBuffer(this.previousBuffer);
|
|
223
|
+
renderExplorer(this.state, nextBuffer);
|
|
224
|
+
this.driver.write(changesToAnsi(diff(this.previousBuffer, nextBuffer)));
|
|
225
|
+
this.previousBuffer = nextBuffer;
|
|
226
|
+
this.state = { ...this.state, dirty: 0 };
|
|
227
|
+
}
|
|
228
|
+
track(promise) {
|
|
229
|
+
this.pendingEffects.add(promise);
|
|
230
|
+
promise.finally(() => {
|
|
231
|
+
this.pendingEffects.delete(promise);
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
exit(result, after) {
|
|
235
|
+
if (this.stopped) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
this.stopped = true;
|
|
239
|
+
this.unsubscribeKeypress?.();
|
|
240
|
+
this.unsubscribeResize?.();
|
|
241
|
+
this.detailJobs.abort();
|
|
242
|
+
if (this.toastTimer !== undefined) {
|
|
243
|
+
clearTimeout(this.toastTimer);
|
|
244
|
+
}
|
|
245
|
+
this.driver.destroy();
|
|
246
|
+
Promise.resolve()
|
|
247
|
+
.then(() => after?.())
|
|
248
|
+
.then(() => {
|
|
249
|
+
this.settle?.resolve(result);
|
|
250
|
+
})
|
|
251
|
+
.catch((error) => {
|
|
252
|
+
this.settle?.reject(error);
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
fail(error) {
|
|
256
|
+
if (!this.stopped) {
|
|
257
|
+
this.stopped = true;
|
|
258
|
+
this.driver.destroy();
|
|
259
|
+
}
|
|
260
|
+
this.settle?.reject(error);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function cloneBuffer(buffer) {
|
|
264
|
+
const next = new ScreenBuffer(buffer.width, buffer.height);
|
|
265
|
+
for (let y = 0; y < buffer.height; y += 1) {
|
|
266
|
+
for (let x = 0; x < buffer.width; x += 1) {
|
|
267
|
+
const cell = buffer.get(x, y);
|
|
268
|
+
next.put(x, y, cell.ch, cell.style);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return next;
|
|
272
|
+
}
|
|
273
|
+
function changesToAnsi(changes) {
|
|
274
|
+
let output = "";
|
|
275
|
+
for (const change of changes) {
|
|
276
|
+
output += `${cursorPositionAnsi(change.x, change.y)}${cellToAnsi(change.cell)}`;
|
|
277
|
+
}
|
|
278
|
+
return output;
|
|
279
|
+
}
|
|
280
|
+
function cursorPositionAnsi(x, y) {
|
|
281
|
+
return `\u001b[${Math.max(1, y + 1)};${Math.max(1, x + 1)}H`;
|
|
282
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Cell } from "../dashboard/types.js";
|
|
2
|
+
import type { KeypressEvent, TerminalDriver } from "../dashboard/terminal.js";
|
|
3
|
+
export declare class FakeTerminalDriver implements TerminalDriver {
|
|
4
|
+
private cols;
|
|
5
|
+
private rows;
|
|
6
|
+
readonly keyQueue: KeypressEvent[];
|
|
7
|
+
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;
|
|
23
|
+
private readonly resizeHandlers;
|
|
24
|
+
constructor(cols?: number, rows?: number);
|
|
25
|
+
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;
|
|
41
|
+
getSize(): {
|
|
42
|
+
cols: number;
|
|
43
|
+
rows: number;
|
|
44
|
+
};
|
|
45
|
+
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;
|
|
50
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
export class FakeTerminalDriver {
|
|
2
|
+
cols;
|
|
3
|
+
rows;
|
|
4
|
+
keyQueue = [];
|
|
5
|
+
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();
|
|
17
|
+
resizeHandlers = new Set();
|
|
18
|
+
constructor(cols = 120, rows = 24) {
|
|
19
|
+
this.cols = cols;
|
|
20
|
+
this.rows = rows;
|
|
21
|
+
}
|
|
22
|
+
get output() {
|
|
23
|
+
return this.writes.join("");
|
|
24
|
+
}
|
|
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 };
|
|
66
|
+
}
|
|
67
|
+
resize(cols, rows) {
|
|
68
|
+
this.cols = cols;
|
|
69
|
+
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
|
+
};
|
|
85
|
+
}
|
|
86
|
+
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();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { type ResolvedBindings } from "./keymap.js";
|
|
2
|
+
export type Tone = "success" | "warning" | "error" | "info" | "muted";
|
|
3
|
+
export interface Row {
|
|
4
|
+
id: string;
|
|
5
|
+
title: string;
|
|
6
|
+
subtitle?: string;
|
|
7
|
+
badge?: {
|
|
8
|
+
text: string;
|
|
9
|
+
tone?: Tone;
|
|
10
|
+
};
|
|
11
|
+
group?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface DetailItem {
|
|
14
|
+
id: string;
|
|
15
|
+
title?: string;
|
|
16
|
+
subtitle?: string;
|
|
17
|
+
badge?: {
|
|
18
|
+
text: string;
|
|
19
|
+
tone?: Tone;
|
|
20
|
+
};
|
|
21
|
+
render: (ctx: DetailCtx) => string | Promise<string>;
|
|
22
|
+
}
|
|
23
|
+
export interface Detail<R> {
|
|
24
|
+
items: (row: Row, ctx: DetailCtx) => Promise<DetailItem[]>;
|
|
25
|
+
actions?: Action<R>[];
|
|
26
|
+
}
|
|
27
|
+
export interface DetailCtx {
|
|
28
|
+
width: number;
|
|
29
|
+
height: number;
|
|
30
|
+
signal: AbortSignal;
|
|
31
|
+
row: Row;
|
|
32
|
+
}
|
|
33
|
+
export interface Action<R> {
|
|
34
|
+
id: string;
|
|
35
|
+
label: string | (() => string);
|
|
36
|
+
key?: string | string[];
|
|
37
|
+
predicate?: (ctx: ActionContext<R>) => boolean;
|
|
38
|
+
handler: (ctx: ActionContext<R>) => void | Promise<void>;
|
|
39
|
+
destructive?: boolean;
|
|
40
|
+
primary?: boolean;
|
|
41
|
+
showInFooter?: boolean;
|
|
42
|
+
}
|
|
43
|
+
export interface ActionContext<R> {
|
|
44
|
+
row: Row;
|
|
45
|
+
rows: Row[];
|
|
46
|
+
item?: DetailItem;
|
|
47
|
+
filter: string;
|
|
48
|
+
refresh: () => Promise<void>;
|
|
49
|
+
suspendAnd: <T>(fn: () => Promise<T>) => Promise<T>;
|
|
50
|
+
toast: (msg: string, tone?: Tone) => void;
|
|
51
|
+
confirm: (prompt: string) => Promise<boolean>;
|
|
52
|
+
exit: (after?: () => void | Promise<void>) => void;
|
|
53
|
+
}
|
|
54
|
+
export interface ExplorerConfig<R> {
|
|
55
|
+
title: string;
|
|
56
|
+
rows: () => Promise<Row[]>;
|
|
57
|
+
detail: Detail<R>;
|
|
58
|
+
actions: Action<R>[];
|
|
59
|
+
reorder?: {
|
|
60
|
+
onReorder: (orderedIds: string[]) => void | Promise<void>;
|
|
61
|
+
};
|
|
62
|
+
multiSelect?: boolean;
|
|
63
|
+
keybindOverrides?: Record<string, string | string[]>;
|
|
64
|
+
emptyHint?: string;
|
|
65
|
+
initialFilter?: string;
|
|
66
|
+
}
|
|
67
|
+
export declare const REGION_HEADER: number;
|
|
68
|
+
export declare const REGION_LIST: number;
|
|
69
|
+
export declare const REGION_DETAIL: number;
|
|
70
|
+
export declare const REGION_FOOTER: number;
|
|
71
|
+
export declare const REGION_MODAL: number;
|
|
72
|
+
export declare const REGION_TOAST: number;
|
|
73
|
+
export declare const REGION_ALL: number;
|
|
74
|
+
export type Dirty = number;
|
|
75
|
+
export type ExplorerLayoutMode = "wide" | "medium" | "narrow-vertical" | "narrow-list-only" | "too-narrow";
|
|
76
|
+
export interface ExplorerSize {
|
|
77
|
+
cols: number;
|
|
78
|
+
rows: number;
|
|
79
|
+
}
|
|
80
|
+
export interface ExplorerState {
|
|
81
|
+
title: string;
|
|
82
|
+
emptyHint: string;
|
|
83
|
+
rows: Row[];
|
|
84
|
+
filtered: number[];
|
|
85
|
+
matchPositions: Map<number, number[]>;
|
|
86
|
+
cursor: number;
|
|
87
|
+
filter: string;
|
|
88
|
+
filterFocused: boolean;
|
|
89
|
+
focused: "list" | "detail";
|
|
90
|
+
detail: {
|
|
91
|
+
rowId: string | null;
|
|
92
|
+
items: DetailItem[] | null;
|
|
93
|
+
cursor: number;
|
|
94
|
+
scroll: number;
|
|
95
|
+
token: number;
|
|
96
|
+
loading: boolean;
|
|
97
|
+
};
|
|
98
|
+
selected: Set<string>;
|
|
99
|
+
modal: null | {
|
|
100
|
+
kind: "help";
|
|
101
|
+
} | {
|
|
102
|
+
kind: "confirm";
|
|
103
|
+
action: Action<unknown>;
|
|
104
|
+
rows: Row[];
|
|
105
|
+
resolver: (ok: boolean) => void;
|
|
106
|
+
} | {
|
|
107
|
+
kind: "palette";
|
|
108
|
+
query: string;
|
|
109
|
+
cursor: number;
|
|
110
|
+
};
|
|
111
|
+
toast: {
|
|
112
|
+
message: string;
|
|
113
|
+
tone: Tone;
|
|
114
|
+
expiresAt: number;
|
|
115
|
+
} | null;
|
|
116
|
+
dirty: Dirty;
|
|
117
|
+
size: ExplorerSize;
|
|
118
|
+
layout: ExplorerLayoutMode;
|
|
119
|
+
bindings: ResolvedBindings;
|
|
120
|
+
actionState: Map<string, ActionStateEntry>;
|
|
121
|
+
}
|
|
122
|
+
export interface ActionStateEntry {
|
|
123
|
+
available: boolean;
|
|
124
|
+
label: string;
|
|
125
|
+
running?: boolean;
|
|
126
|
+
action?: Action<unknown>;
|
|
127
|
+
source?: "row" | "detail";
|
|
128
|
+
}
|
|
129
|
+
export declare function createInitialState<R>(config: ExplorerConfig<R>, size: ExplorerSize): ExplorerState;
|
|
130
|
+
export declare function resolveExplorerLayoutMode(cols: number): ExplorerLayoutMode;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { resolveBindings } from "./keymap.js";
|
|
2
|
+
export const REGION_HEADER = 1 << 0;
|
|
3
|
+
export const REGION_LIST = 1 << 1;
|
|
4
|
+
export const REGION_DETAIL = 1 << 2;
|
|
5
|
+
export const REGION_FOOTER = 1 << 3;
|
|
6
|
+
export const REGION_MODAL = 1 << 4;
|
|
7
|
+
export const REGION_TOAST = 1 << 5;
|
|
8
|
+
export const REGION_ALL = REGION_HEADER |
|
|
9
|
+
REGION_LIST |
|
|
10
|
+
REGION_DETAIL |
|
|
11
|
+
REGION_FOOTER |
|
|
12
|
+
REGION_MODAL |
|
|
13
|
+
REGION_TOAST;
|
|
14
|
+
export function createInitialState(config, size) {
|
|
15
|
+
const normalizedSize = {
|
|
16
|
+
cols: normalizeSize(size.cols),
|
|
17
|
+
rows: normalizeSize(size.rows)
|
|
18
|
+
};
|
|
19
|
+
return {
|
|
20
|
+
title: config.title,
|
|
21
|
+
emptyHint: config.emptyHint ?? "No detail",
|
|
22
|
+
rows: [],
|
|
23
|
+
filtered: [],
|
|
24
|
+
matchPositions: new Map(),
|
|
25
|
+
cursor: 0,
|
|
26
|
+
filter: config.initialFilter ?? "",
|
|
27
|
+
filterFocused: false,
|
|
28
|
+
focused: "list",
|
|
29
|
+
detail: {
|
|
30
|
+
rowId: null,
|
|
31
|
+
items: null,
|
|
32
|
+
cursor: 0,
|
|
33
|
+
scroll: 0,
|
|
34
|
+
token: 0,
|
|
35
|
+
loading: false
|
|
36
|
+
},
|
|
37
|
+
selected: new Set(),
|
|
38
|
+
modal: null,
|
|
39
|
+
toast: null,
|
|
40
|
+
dirty: REGION_ALL,
|
|
41
|
+
size: normalizedSize,
|
|
42
|
+
layout: resolveExplorerLayoutMode(normalizedSize.cols),
|
|
43
|
+
bindings: resolveBindings(config),
|
|
44
|
+
actionState: createInitialActionState(config)
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function createInitialActionState(config) {
|
|
48
|
+
const state = new Map();
|
|
49
|
+
for (const action of config.actions) {
|
|
50
|
+
state.set(action.id, {
|
|
51
|
+
available: true,
|
|
52
|
+
label: typeof action.label === "function" ? action.id : action.label,
|
|
53
|
+
action: action,
|
|
54
|
+
source: "row"
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
for (const action of config.detail.actions ?? []) {
|
|
58
|
+
state.set(action.id, {
|
|
59
|
+
available: true,
|
|
60
|
+
label: typeof action.label === "function" ? action.id : action.label,
|
|
61
|
+
action: action,
|
|
62
|
+
source: "detail"
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return state;
|
|
66
|
+
}
|
|
67
|
+
export function resolveExplorerLayoutMode(cols) {
|
|
68
|
+
if (cols < 40) {
|
|
69
|
+
return "too-narrow";
|
|
70
|
+
}
|
|
71
|
+
if (cols < 80) {
|
|
72
|
+
return "narrow-list-only";
|
|
73
|
+
}
|
|
74
|
+
if (cols < 100) {
|
|
75
|
+
return "narrow-vertical";
|
|
76
|
+
}
|
|
77
|
+
if (cols < 120) {
|
|
78
|
+
return "medium";
|
|
79
|
+
}
|
|
80
|
+
return "wide";
|
|
81
|
+
}
|
|
82
|
+
function normalizeSize(value) {
|
|
83
|
+
if (!Number.isFinite(value)) {
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
return Math.max(0, Math.floor(value));
|
|
87
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Tone } from "./state.js";
|
|
2
|
+
type CellStyle = {
|
|
3
|
+
fg?: string;
|
|
4
|
+
bg?: string;
|
|
5
|
+
bold?: boolean;
|
|
6
|
+
dim?: boolean;
|
|
7
|
+
underline?: boolean;
|
|
8
|
+
};
|
|
9
|
+
export interface ExplorerTheme {
|
|
10
|
+
accent: (text: string) => string;
|
|
11
|
+
muted: (text: string) => string;
|
|
12
|
+
border: (text: string) => string;
|
|
13
|
+
borderFocused: (text: string) => string;
|
|
14
|
+
badge: (text: string, tone: Tone) => string;
|
|
15
|
+
matchHighlight: (text: string) => string;
|
|
16
|
+
}
|
|
17
|
+
export interface ExplorerStyles {
|
|
18
|
+
accent: CellStyle;
|
|
19
|
+
muted: CellStyle;
|
|
20
|
+
border: CellStyle;
|
|
21
|
+
borderFocused: CellStyle;
|
|
22
|
+
matchHighlight: CellStyle;
|
|
23
|
+
tones: Record<Tone, CellStyle>;
|
|
24
|
+
}
|
|
25
|
+
export declare function getExplorerTheme(): ExplorerTheme;
|
|
26
|
+
export declare function getExplorerStyles(): ExplorerStyles;
|
|
27
|
+
export {};
|