tickmarkr 1.72.0 → 1.74.0
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/cli/commands/approve.js +21 -3
- package/dist/cli/commands/resume.js +10 -2
- package/dist/cli/commands/status.d.ts +2 -0
- package/dist/cli/commands/status.js +260 -41
- package/dist/cli/commands/ui.d.ts +6 -2
- package/dist/cli/commands/ui.js +2 -4
- package/dist/config/config.d.ts +4 -0
- package/dist/config/config.js +6 -1
- package/dist/drivers/herdr.d.ts +5 -0
- package/dist/drivers/herdr.js +100 -10
- package/dist/drivers/subprocess.d.ts +3 -3
- package/dist/drivers/subprocess.js +10 -5
- package/dist/gates/acceptance.js +19 -1
- package/dist/gates/llm.d.ts +4 -0
- package/dist/gates/llm.js +16 -3
- package/dist/gates/run-gates.d.ts +1 -1
- package/dist/gates/run-gates.js +25 -3
- package/dist/route/preference.d.ts +2 -1
- package/dist/route/preference.js +8 -2
- package/dist/run/daemon.d.ts +1 -0
- package/dist/run/daemon.js +139 -4
- package/dist/run/git.d.ts +2 -0
- package/dist/run/git.js +55 -0
- package/dist/run/journal.d.ts +21 -1
- package/dist/run/journal.js +83 -5
- package/dist/tui/ink/components.d.ts +18 -0
- package/dist/tui/ink/components.js +30 -2
- package/dist/tui/ink/studio-app.d.ts +59 -0
- package/dist/tui/ink/studio-app.js +320 -0
- package/package.json +1 -1
- package/dist/tui/app.d.ts +0 -72
- package/dist/tui/app.js +0 -334
- package/dist/tui/engine.d.ts +0 -52
- package/dist/tui/engine.js +0 -92
- package/dist/tui/frame.d.ts +0 -22
- package/dist/tui/frame.js +0 -56
- package/dist/tui/input.d.ts +0 -41
- package/dist/tui/input.js +0 -100
- package/dist/tui/views/consult-dossier.d.ts +0 -49
- package/dist/tui/views/consult-dossier.js +0 -169
- package/dist/tui/views/diff-modal.d.ts +0 -9
- package/dist/tui/views/diff-modal.js +0 -19
- package/dist/tui/views/fleet-view.d.ts +0 -91
- package/dist/tui/views/fleet-view.js +0 -337
- package/dist/tui/views/preview-view.d.ts +0 -7
- package/dist/tui/views/preview-view.js +0 -80
- package/dist/tui/views/profile-view.d.ts +0 -15
- package/dist/tui/views/profile-view.js +0 -93
- package/dist/tui/views/routing-view.d.ts +0 -48
- package/dist/tui/views/routing-view.js +0 -234
- package/dist/tui/views/runs-view.d.ts +0 -70
- package/dist/tui/views/runs-view.js +0 -387
package/dist/tui/app.js
DELETED
|
@@ -1,334 +0,0 @@
|
|
|
1
|
-
import { TerminalEngine } from "../tui/engine.js";
|
|
2
|
-
import { createFleetView } from "./views/fleet-view.js";
|
|
3
|
-
import { createPreviewView } from "./views/preview-view.js";
|
|
4
|
-
import { createProfileView } from "./views/profile-view.js";
|
|
5
|
-
import { createRoutingView } from "./views/routing-view.js";
|
|
6
|
-
import { createRunsView } from "./views/runs-view.js";
|
|
7
|
-
import { renderDiffModal } from "./views/diff-modal.js";
|
|
8
|
-
import { buildSaveProposal, confirmSave } from "./save.js";
|
|
9
|
-
import { FleetStaging } from "./staging.js";
|
|
10
|
-
const MUTATION_KEYS = ["r", "m", "return"];
|
|
11
|
-
const READONLY_NOTICE = "read-only in v1.66 — write path ships in v1.67";
|
|
12
|
-
function emptyFleetEditable() {
|
|
13
|
-
return {
|
|
14
|
-
denyAdapters: [],
|
|
15
|
-
denyModels: [],
|
|
16
|
-
tiers: {},
|
|
17
|
-
map: {},
|
|
18
|
-
floors: {},
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
export class StudioApp {
|
|
22
|
-
engine;
|
|
23
|
-
views;
|
|
24
|
-
active = 0;
|
|
25
|
-
showingHelp = false;
|
|
26
|
-
confirmingQuit = false;
|
|
27
|
-
showingSave = false;
|
|
28
|
-
saveTarget = "repo";
|
|
29
|
-
saveProposal = null;
|
|
30
|
-
notice = null;
|
|
31
|
-
staging;
|
|
32
|
-
repoRoot;
|
|
33
|
-
globalDir;
|
|
34
|
-
_lines = [];
|
|
35
|
-
resolveExit = null;
|
|
36
|
-
exited;
|
|
37
|
-
constructor(opts) {
|
|
38
|
-
this.engine = new TerminalEngine({
|
|
39
|
-
input: opts.input,
|
|
40
|
-
output: opts.output,
|
|
41
|
-
onResize: () => this.paint(),
|
|
42
|
-
});
|
|
43
|
-
this.repoRoot = opts.repoRoot ?? process.cwd();
|
|
44
|
-
this.globalDir = opts.globalDir;
|
|
45
|
-
this.staging = new FleetStaging(opts.loaded ?? emptyFleetEditable());
|
|
46
|
-
this.views = [createFleetView(), createRoutingView(), createPreviewView(), createProfileView(), createRunsView()];
|
|
47
|
-
this.exited = new Promise((resolve) => {
|
|
48
|
-
this.resolveExit = resolve;
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
/** Current line model last rendered or prepared. */
|
|
52
|
-
get lines() {
|
|
53
|
-
return this._lines;
|
|
54
|
-
}
|
|
55
|
-
/** Labels of every registered view, in order. */
|
|
56
|
-
get viewLabels() {
|
|
57
|
-
return this.views.map((v) => v.label);
|
|
58
|
-
}
|
|
59
|
-
/** Enter the alternate screen and start routing input. */
|
|
60
|
-
start() {
|
|
61
|
-
this.bindKeys();
|
|
62
|
-
this.paint();
|
|
63
|
-
this.engine.start(this._lines);
|
|
64
|
-
}
|
|
65
|
-
/** Restore the terminal and release the input stream. */
|
|
66
|
-
stop() {
|
|
67
|
-
this.engine.stop();
|
|
68
|
-
if (this.resolveExit) {
|
|
69
|
-
this.resolveExit();
|
|
70
|
-
this.resolveExit = null;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
/** Apply an edit to the staging buffer and repaint. Intended as the test seam and future edit hook. */
|
|
74
|
-
stageEdit(edit) {
|
|
75
|
-
this.staging.apply(edit);
|
|
76
|
-
this.paint();
|
|
77
|
-
}
|
|
78
|
-
get isModalOpen() {
|
|
79
|
-
return this.showingHelp || this.confirmingQuit || this.showingSave;
|
|
80
|
-
}
|
|
81
|
-
bindKeys() {
|
|
82
|
-
// view switching
|
|
83
|
-
// Tab is decoded as the raw "\t" sequence by the micro-engine.
|
|
84
|
-
this.engine.key("\t", () => {
|
|
85
|
-
if (!this.isModalOpen)
|
|
86
|
-
this.setView((this.active + 1) % this.views.length);
|
|
87
|
-
});
|
|
88
|
-
for (let i = 1; i <= this.views.length; i++) {
|
|
89
|
-
const idx = i - 1;
|
|
90
|
-
this.engine.key(String(i), () => {
|
|
91
|
-
if (!this.isModalOpen)
|
|
92
|
-
this.setView(idx);
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
// per-view navigation
|
|
96
|
-
for (const name of ["up", "down"]) {
|
|
97
|
-
this.engine.key(name, () => {
|
|
98
|
-
if (!this.isModalOpen)
|
|
99
|
-
this.forwardViewKey(name);
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
// help overlay
|
|
103
|
-
this.engine.key("?", () => {
|
|
104
|
-
if (!this.confirmingQuit && !this.showingSave) {
|
|
105
|
-
this.showingHelp = true;
|
|
106
|
-
this.paint();
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
// dismissal / quit
|
|
110
|
-
this.engine.key("escape", () => this.handleEscape());
|
|
111
|
-
this.engine.key("q", () => this.handleQuit());
|
|
112
|
-
// revert staged changes
|
|
113
|
-
this.engine.key("u", () => this.handleRevert());
|
|
114
|
-
// save modal
|
|
115
|
-
this.engine.key("s", () => this.handleSaveOpen());
|
|
116
|
-
this.engine.key("t", () => this.handleSaveToggle());
|
|
117
|
-
// quit-confirmation / save-confirmation answers
|
|
118
|
-
this.engine.key("y", () => this.handleConfirm());
|
|
119
|
-
this.engine.key("n", () => this.handleCancel());
|
|
120
|
-
// remaining mutation keys — read-only until wired
|
|
121
|
-
for (const key of MUTATION_KEYS) {
|
|
122
|
-
this.engine.key(key, () => {
|
|
123
|
-
if (!this.isModalOpen)
|
|
124
|
-
this.showNotice();
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
handleEscape() {
|
|
129
|
-
if (this.showingHelp) {
|
|
130
|
-
this.showingHelp = false;
|
|
131
|
-
this.paint();
|
|
132
|
-
}
|
|
133
|
-
else if (this.showingSave) {
|
|
134
|
-
this.closeSave(false);
|
|
135
|
-
}
|
|
136
|
-
else if (this.confirmingQuit) {
|
|
137
|
-
this.confirmingQuit = false;
|
|
138
|
-
this.paint();
|
|
139
|
-
}
|
|
140
|
-
else if (this.staging.isDirty) {
|
|
141
|
-
this.confirmingQuit = true;
|
|
142
|
-
this.paint();
|
|
143
|
-
}
|
|
144
|
-
else {
|
|
145
|
-
this.stop();
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
handleQuit() {
|
|
149
|
-
if (this.showingHelp || this.showingSave)
|
|
150
|
-
return; // modals block q
|
|
151
|
-
if (this.confirmingQuit) {
|
|
152
|
-
this.stop();
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
if (this.staging.isDirty) {
|
|
156
|
-
this.confirmingQuit = true;
|
|
157
|
-
this.paint();
|
|
158
|
-
}
|
|
159
|
-
else {
|
|
160
|
-
this.stop();
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
handleSaveOpen() {
|
|
164
|
-
if (this.isModalOpen)
|
|
165
|
-
return;
|
|
166
|
-
if (!this.staging.isDirty) {
|
|
167
|
-
this.notice = "no staged changes";
|
|
168
|
-
this.paint();
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
this.showingSave = true;
|
|
172
|
-
this.saveTarget = "repo";
|
|
173
|
-
this.saveProposal = buildSaveProposal({
|
|
174
|
-
repoRoot: this.repoRoot,
|
|
175
|
-
loaded: this.staging.loadedState,
|
|
176
|
-
staged: this.staging.current,
|
|
177
|
-
target: this.saveTarget,
|
|
178
|
-
globalDir: this.globalDir,
|
|
179
|
-
});
|
|
180
|
-
this.paint();
|
|
181
|
-
}
|
|
182
|
-
handleSaveToggle() {
|
|
183
|
-
if (!this.showingSave)
|
|
184
|
-
return;
|
|
185
|
-
this.saveTarget = this.saveTarget === "repo" ? "global" : "repo";
|
|
186
|
-
this.saveProposal = buildSaveProposal({
|
|
187
|
-
repoRoot: this.repoRoot,
|
|
188
|
-
loaded: this.staging.loadedState,
|
|
189
|
-
staged: this.staging.current,
|
|
190
|
-
target: this.saveTarget,
|
|
191
|
-
globalDir: this.globalDir,
|
|
192
|
-
});
|
|
193
|
-
this.paint();
|
|
194
|
-
}
|
|
195
|
-
handleConfirm() {
|
|
196
|
-
if (this.showingSave) {
|
|
197
|
-
this.executeSave();
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
if (this.confirmingQuit) {
|
|
201
|
-
this.stop();
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
handleCancel() {
|
|
205
|
-
if (this.showingSave) {
|
|
206
|
-
this.closeSave(false);
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
if (this.confirmingQuit) {
|
|
210
|
-
this.confirmingQuit = false;
|
|
211
|
-
this.paint();
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
executeSave() {
|
|
215
|
-
if (!this.saveProposal)
|
|
216
|
-
return;
|
|
217
|
-
const result = confirmSave({
|
|
218
|
-
repoRoot: this.repoRoot,
|
|
219
|
-
loaded: this.staging.loadedState,
|
|
220
|
-
staged: this.staging.current,
|
|
221
|
-
target: this.saveTarget,
|
|
222
|
-
globalDir: this.globalDir,
|
|
223
|
-
});
|
|
224
|
-
if (result.kind === "written") {
|
|
225
|
-
this.staging = new FleetStaging(this.staging.current);
|
|
226
|
-
this.closeSave(true, `wrote ${result.path}`);
|
|
227
|
-
}
|
|
228
|
-
else if (result.kind === "refused") {
|
|
229
|
-
this.closeSave(true, `save refused: ${result.reason}`);
|
|
230
|
-
}
|
|
231
|
-
else {
|
|
232
|
-
this.closeSave(false);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
closeSave(notify, message) {
|
|
236
|
-
this.showingSave = false;
|
|
237
|
-
this.saveProposal = null;
|
|
238
|
-
if (notify && message)
|
|
239
|
-
this.notice = message;
|
|
240
|
-
this.paint();
|
|
241
|
-
}
|
|
242
|
-
handleRevert() {
|
|
243
|
-
if (this.isModalOpen)
|
|
244
|
-
return;
|
|
245
|
-
if (this.staging.isDirty) {
|
|
246
|
-
this.staging.revert();
|
|
247
|
-
this.notice = null;
|
|
248
|
-
this.paint();
|
|
249
|
-
}
|
|
250
|
-
else {
|
|
251
|
-
this.notice = "no staged changes";
|
|
252
|
-
this.paint();
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
setView(idx) {
|
|
256
|
-
this.active = idx;
|
|
257
|
-
this.showingHelp = false;
|
|
258
|
-
this.notice = null;
|
|
259
|
-
this.paint();
|
|
260
|
-
}
|
|
261
|
-
forwardViewKey(name) {
|
|
262
|
-
this.views[this.active]?.key?.(name);
|
|
263
|
-
this.paint();
|
|
264
|
-
}
|
|
265
|
-
showNotice() {
|
|
266
|
-
this.notice = READONLY_NOTICE;
|
|
267
|
-
this.paint();
|
|
268
|
-
}
|
|
269
|
-
paint() {
|
|
270
|
-
this._lines = this.buildFrame();
|
|
271
|
-
this.engine.render(this._lines);
|
|
272
|
-
}
|
|
273
|
-
buildFrame() {
|
|
274
|
-
const { cols, rows } = this.engine.size;
|
|
275
|
-
const lines = [];
|
|
276
|
-
lines.push(this.renderTabBar());
|
|
277
|
-
if (this.showingHelp) {
|
|
278
|
-
lines.push(...this.renderHelp());
|
|
279
|
-
}
|
|
280
|
-
else if (this.showingSave && this.saveProposal) {
|
|
281
|
-
lines.push(...renderDiffModal({
|
|
282
|
-
target: this.saveProposal.target,
|
|
283
|
-
targetPath: this.saveProposal.targetPath,
|
|
284
|
-
diff: this.saveProposal.diff,
|
|
285
|
-
liveRun: this.saveProposal.liveRun,
|
|
286
|
-
}));
|
|
287
|
-
}
|
|
288
|
-
else if (this.confirmingQuit) {
|
|
289
|
-
lines.push(...this.renderQuitConfirm());
|
|
290
|
-
}
|
|
291
|
-
else {
|
|
292
|
-
const view = this.views[this.active];
|
|
293
|
-
lines.push(...view.render({ cols, rows: Math.max(rows - 2, 1) }));
|
|
294
|
-
}
|
|
295
|
-
if (this.notice) {
|
|
296
|
-
lines.push(this.notice);
|
|
297
|
-
}
|
|
298
|
-
lines.push(this.renderStatusBar());
|
|
299
|
-
return lines;
|
|
300
|
-
}
|
|
301
|
-
renderTabBar() {
|
|
302
|
-
const parts = ["tickmarkr ui ──"];
|
|
303
|
-
for (let i = 0; i < this.views.length; i++) {
|
|
304
|
-
const view = this.views[i];
|
|
305
|
-
const marker = i === this.active ? `*${view.label}` : view.label;
|
|
306
|
-
parts.push(`[${i + 1}]${marker}`);
|
|
307
|
-
}
|
|
308
|
-
return parts.join(" ");
|
|
309
|
-
}
|
|
310
|
-
renderStatusBar() {
|
|
311
|
-
if (this.staging.isDirty) {
|
|
312
|
-
const n = this.staging.changeCount;
|
|
313
|
-
return `● ${n} staged change${n === 1 ? "" : "s"}`;
|
|
314
|
-
}
|
|
315
|
-
return "no staged changes";
|
|
316
|
-
}
|
|
317
|
-
renderHelp() {
|
|
318
|
-
return [
|
|
319
|
-
"Key bindings",
|
|
320
|
-
"1-5 switch view",
|
|
321
|
-
"tab cycle views",
|
|
322
|
-
"? show this help",
|
|
323
|
-
"esc / q quit",
|
|
324
|
-
"u revert staged changes",
|
|
325
|
-
"r re-probe (read-only)",
|
|
326
|
-
"s save",
|
|
327
|
-
"m mode (read-only)",
|
|
328
|
-
];
|
|
329
|
-
}
|
|
330
|
-
renderQuitConfirm() {
|
|
331
|
-
const n = this.staging.changeCount;
|
|
332
|
-
return [`Quit with ${n} staged change${n === 1 ? "" : "s"}? (y/n)`];
|
|
333
|
-
}
|
|
334
|
-
}
|
package/dist/tui/engine.d.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { type Key, type KeyHandler, type InputStream } from "./input.js";
|
|
2
|
-
export type { Key, KeyHandler };
|
|
3
|
-
/** Writable stream with the small surface the engine needs. */
|
|
4
|
-
export interface OutputStream {
|
|
5
|
-
isTTY?: boolean;
|
|
6
|
-
columns?: number;
|
|
7
|
-
rows?: number;
|
|
8
|
-
write: (chunk: string) => unknown;
|
|
9
|
-
on?(event: "resize", listener: () => void): this;
|
|
10
|
-
off?(event: "resize", listener: () => void): this;
|
|
11
|
-
}
|
|
12
|
-
export interface EngineOptions {
|
|
13
|
-
input: InputStream;
|
|
14
|
-
output: OutputStream;
|
|
15
|
-
onResize?: (cols: number, rows: number) => void;
|
|
16
|
-
onError?: (error: unknown) => void;
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* Dependency-free terminal engine: alternate-screen line-diff renderer, named-key routing,
|
|
20
|
-
* resize tracking, and terminal restoration on every exit path including thrown handlers.
|
|
21
|
-
*/
|
|
22
|
-
export declare class TerminalEngine {
|
|
23
|
-
private opts;
|
|
24
|
-
private frame;
|
|
25
|
-
private input;
|
|
26
|
-
private lastLines;
|
|
27
|
-
private _cols;
|
|
28
|
-
private _rows;
|
|
29
|
-
private running;
|
|
30
|
-
private userHandlers;
|
|
31
|
-
constructor(opts: EngineOptions);
|
|
32
|
-
/** Enter the alternate screen, begin listening for keys and resize events. */
|
|
33
|
-
start(lines: string[]): void;
|
|
34
|
-
/** Restore the terminal and stop listening. Safe to call multiple times. */
|
|
35
|
-
stop(): void;
|
|
36
|
-
/** Update the line model; only changed lines are repainted. */
|
|
37
|
-
render(lines: string[]): void;
|
|
38
|
-
/** Register a handler for a named key. */
|
|
39
|
-
key(name: string, handler: KeyHandler): void;
|
|
40
|
-
/** Unregister a handler. */
|
|
41
|
-
unkey(name: string, handler: KeyHandler): void;
|
|
42
|
-
/** Trigger a full repaint at the new size. */
|
|
43
|
-
resize(cols: number, rows: number): void;
|
|
44
|
-
/** Current terminal size as last seen or reported by a resize event. */
|
|
45
|
-
get size(): {
|
|
46
|
-
cols: number;
|
|
47
|
-
rows: number;
|
|
48
|
-
};
|
|
49
|
-
private onResize;
|
|
50
|
-
private attachResize;
|
|
51
|
-
private detachResize;
|
|
52
|
-
}
|
package/dist/tui/engine.js
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { Frame } from "./frame.js";
|
|
2
|
-
import { InputRouter } from "./input.js";
|
|
3
|
-
/**
|
|
4
|
-
* Dependency-free terminal engine: alternate-screen line-diff renderer, named-key routing,
|
|
5
|
-
* resize tracking, and terminal restoration on every exit path including thrown handlers.
|
|
6
|
-
*/
|
|
7
|
-
export class TerminalEngine {
|
|
8
|
-
opts;
|
|
9
|
-
frame;
|
|
10
|
-
input;
|
|
11
|
-
lastLines = [];
|
|
12
|
-
_cols;
|
|
13
|
-
_rows;
|
|
14
|
-
running = false;
|
|
15
|
-
userHandlers = new Map();
|
|
16
|
-
constructor(opts) {
|
|
17
|
-
this.opts = opts;
|
|
18
|
-
this.frame = new Frame(opts.output);
|
|
19
|
-
this.input = new InputRouter(opts.input);
|
|
20
|
-
this._cols = opts.output.columns ?? 80;
|
|
21
|
-
this._rows = opts.output.rows ?? 24;
|
|
22
|
-
}
|
|
23
|
-
/** Enter the alternate screen, begin listening for keys and resize events. */
|
|
24
|
-
start(lines) {
|
|
25
|
-
if (this.running)
|
|
26
|
-
return;
|
|
27
|
-
this.running = true;
|
|
28
|
-
this.lastLines = lines.slice();
|
|
29
|
-
this.frame.enter(this.lastLines);
|
|
30
|
-
this.input.start();
|
|
31
|
-
this.attachResize();
|
|
32
|
-
}
|
|
33
|
-
/** Restore the terminal and stop listening. Safe to call multiple times. */
|
|
34
|
-
stop() {
|
|
35
|
-
if (!this.running)
|
|
36
|
-
return;
|
|
37
|
-
this.detachResize();
|
|
38
|
-
this.input.stop();
|
|
39
|
-
this.frame.exit();
|
|
40
|
-
this.running = false;
|
|
41
|
-
}
|
|
42
|
-
/** Update the line model; only changed lines are repainted. */
|
|
43
|
-
render(lines) {
|
|
44
|
-
this.lastLines = lines.slice();
|
|
45
|
-
this.frame.render(this.lastLines);
|
|
46
|
-
}
|
|
47
|
-
/** Register a handler for a named key. */
|
|
48
|
-
key(name, handler) {
|
|
49
|
-
const wrapped = async (k) => {
|
|
50
|
-
try {
|
|
51
|
-
await handler(k);
|
|
52
|
-
}
|
|
53
|
-
catch (e) {
|
|
54
|
-
this.stop();
|
|
55
|
-
this.opts.onError?.(e);
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
this.userHandlers.set(handler, wrapped);
|
|
59
|
-
this.input.on(name, wrapped);
|
|
60
|
-
}
|
|
61
|
-
/** Unregister a handler. */
|
|
62
|
-
unkey(name, handler) {
|
|
63
|
-
const wrapped = this.userHandlers.get(handler);
|
|
64
|
-
if (wrapped) {
|
|
65
|
-
this.input.off(name, wrapped);
|
|
66
|
-
this.userHandlers.delete(handler);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
/** Trigger a full repaint at the new size. */
|
|
70
|
-
resize(cols, rows) {
|
|
71
|
-
this._cols = cols;
|
|
72
|
-
this._rows = rows;
|
|
73
|
-
this.frame.resize(this.lastLines);
|
|
74
|
-
this.opts.onResize?.(cols, rows);
|
|
75
|
-
}
|
|
76
|
-
/** Current terminal size as last seen or reported by a resize event. */
|
|
77
|
-
get size() {
|
|
78
|
-
return { cols: this._cols, rows: this._rows };
|
|
79
|
-
}
|
|
80
|
-
onResize = () => {
|
|
81
|
-
this._cols = this.opts.output.columns ?? this._cols;
|
|
82
|
-
this._rows = this.opts.output.rows ?? this._rows;
|
|
83
|
-
this.frame.resize(this.lastLines);
|
|
84
|
-
this.opts.onResize?.(this._cols, this._rows);
|
|
85
|
-
};
|
|
86
|
-
attachResize() {
|
|
87
|
-
this.opts.output.on?.("resize", this.onResize);
|
|
88
|
-
}
|
|
89
|
-
detachResize() {
|
|
90
|
-
this.opts.output.off?.("resize", this.onResize);
|
|
91
|
-
}
|
|
92
|
-
}
|
package/dist/tui/frame.d.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/** A write target — anything with a write(chunk: string) method. */
|
|
2
|
-
export interface OutputSink {
|
|
3
|
-
write: (chunk: string) => unknown;
|
|
4
|
-
}
|
|
5
|
-
/** Frame state + diff rendering. */
|
|
6
|
-
export declare class Frame {
|
|
7
|
-
private out;
|
|
8
|
-
private prev;
|
|
9
|
-
private active;
|
|
10
|
-
constructor(out: OutputSink);
|
|
11
|
-
/** Enter the alternate screen, hide the cursor, and paint the initial frame. */
|
|
12
|
-
enter(lines: string[]): void;
|
|
13
|
-
/**
|
|
14
|
-
* Paint only the lines that differ from the previous model. Lines are addressed with
|
|
15
|
-
* absolute cursor positioning; unchanged rows produce no output.
|
|
16
|
-
*/
|
|
17
|
-
render(lines: string[]): void;
|
|
18
|
-
/** Invalidate the cached frame so the next render writes every line. */
|
|
19
|
-
resize(lines: string[]): void;
|
|
20
|
-
/** Restore the terminal: show cursor and return to the normal screen buffer. */
|
|
21
|
-
exit(): void;
|
|
22
|
-
}
|
package/dist/tui/frame.js
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
// Line-diff renderer for the alternate screen. Maintains the previous line model and
|
|
2
|
-
// repaints only rows that changed since the last render, keeping terminal traffic small.
|
|
3
|
-
/** Frame state + diff rendering. */
|
|
4
|
-
export class Frame {
|
|
5
|
-
out;
|
|
6
|
-
prev = [];
|
|
7
|
-
active = false;
|
|
8
|
-
constructor(out) {
|
|
9
|
-
this.out = out;
|
|
10
|
-
}
|
|
11
|
-
/** Enter the alternate screen, hide the cursor, and paint the initial frame. */
|
|
12
|
-
enter(lines) {
|
|
13
|
-
this.active = true;
|
|
14
|
-
this.prev = [];
|
|
15
|
-
this.out.write("\x1b[?1049h"); // alternate screen buffer
|
|
16
|
-
this.out.write("\x1b[?25l"); // hide cursor
|
|
17
|
-
this.render(lines);
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Paint only the lines that differ from the previous model. Lines are addressed with
|
|
21
|
-
* absolute cursor positioning; unchanged rows produce no output.
|
|
22
|
-
*/
|
|
23
|
-
render(lines) {
|
|
24
|
-
if (!this.active)
|
|
25
|
-
return;
|
|
26
|
-
const curr = lines.slice();
|
|
27
|
-
const max = Math.max(this.prev.length, curr.length);
|
|
28
|
-
for (let i = 0; i < max; i++) {
|
|
29
|
-
if (this.prev[i] !== curr[i]) {
|
|
30
|
-
this.out.write(`\x1b[${i + 1};1H`); // row i+1, column 1
|
|
31
|
-
this.out.write(curr[i] ?? "");
|
|
32
|
-
this.out.write("\x1b[K"); // clear to end of line
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
if (curr.length < this.prev.length) {
|
|
36
|
-
// The frame shrank — clear everything below the new last line.
|
|
37
|
-
this.out.write(`\x1b[${curr.length + 1};1H`);
|
|
38
|
-
this.out.write("\x1b[J");
|
|
39
|
-
}
|
|
40
|
-
this.prev = curr;
|
|
41
|
-
}
|
|
42
|
-
/** Invalidate the cached frame so the next render writes every line. */
|
|
43
|
-
resize(lines) {
|
|
44
|
-
this.prev = [];
|
|
45
|
-
this.render(lines);
|
|
46
|
-
}
|
|
47
|
-
/** Restore the terminal: show cursor and return to the normal screen buffer. */
|
|
48
|
-
exit() {
|
|
49
|
-
if (!this.active)
|
|
50
|
-
return;
|
|
51
|
-
this.out.write("\x1b[?25h"); // show cursor
|
|
52
|
-
this.out.write("\x1b[?1049l"); // normal screen buffer
|
|
53
|
-
this.active = false;
|
|
54
|
-
this.prev = [];
|
|
55
|
-
}
|
|
56
|
-
}
|
package/dist/tui/input.d.ts
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
/** Decoded keypress shape. */
|
|
2
|
-
export interface Key {
|
|
3
|
-
name?: string;
|
|
4
|
-
sequence?: string;
|
|
5
|
-
ctrl?: boolean;
|
|
6
|
-
meta?: boolean;
|
|
7
|
-
shift?: boolean;
|
|
8
|
-
}
|
|
9
|
-
/** Handler for a named key. May be async. */
|
|
10
|
-
export type KeyHandler = (key: Key) => void | Promise<void>;
|
|
11
|
-
/** Readable stream with the small TTY surface the engine needs. */
|
|
12
|
-
export interface InputStream extends NodeJS.ReadableStream {
|
|
13
|
-
isTTY?: boolean;
|
|
14
|
-
setRawMode?: (mode: boolean) => unknown;
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Decode the next key at the start of a byte buffer. Returns the decoded key and
|
|
18
|
-
* how many bytes were consumed. Undefined when more bytes are needed.
|
|
19
|
-
*/
|
|
20
|
-
export declare function decodeOneKey(buf: Buffer): {
|
|
21
|
-
key: Key;
|
|
22
|
-
consumed: number;
|
|
23
|
-
} | undefined;
|
|
24
|
-
/** Routes decoded keypress events to handlers registered by key name. */
|
|
25
|
-
export declare class InputRouter {
|
|
26
|
-
private input;
|
|
27
|
-
private handlers;
|
|
28
|
-
private started;
|
|
29
|
-
private buffer;
|
|
30
|
-
constructor(input: InputStream);
|
|
31
|
-
/** Register a handler for a named key (e.g. "up", "escape", "q"). */
|
|
32
|
-
on(name: string, handler: KeyHandler): void;
|
|
33
|
-
/** Remove a previously registered handler. */
|
|
34
|
-
off(name: string, handler: KeyHandler): void;
|
|
35
|
-
/** Start decoding keypresses from the input stream. Idempotent. */
|
|
36
|
-
start(): void;
|
|
37
|
-
/** Stop decoding and release the stream. Idempotent. */
|
|
38
|
-
stop(): void;
|
|
39
|
-
private onData;
|
|
40
|
-
private dispatch;
|
|
41
|
-
}
|
package/dist/tui/input.js
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Decode the next key at the start of a byte buffer. Returns the decoded key and
|
|
3
|
-
* how many bytes were consumed. Undefined when more bytes are needed.
|
|
4
|
-
*/
|
|
5
|
-
export function decodeOneKey(buf) {
|
|
6
|
-
if (buf.length === 0)
|
|
7
|
-
return undefined;
|
|
8
|
-
const b0 = buf[0];
|
|
9
|
-
if (b0 === 0x1b) {
|
|
10
|
-
// CSI arrow sequences: ESC [ A/B/C/D. Keep an incomplete CSI buffered so
|
|
11
|
-
// arbitrary stream chunking cannot turn an arrow into Escape + plain keys.
|
|
12
|
-
if (buf[1] === 0x5b) {
|
|
13
|
-
if (buf.length < 3)
|
|
14
|
-
return undefined;
|
|
15
|
-
const name = buf[2] === 0x41 ? "up"
|
|
16
|
-
: buf[2] === 0x42 ? "down"
|
|
17
|
-
: buf[2] === 0x43 ? "right"
|
|
18
|
-
: buf[2] === 0x44 ? "left"
|
|
19
|
-
: undefined;
|
|
20
|
-
if (name) {
|
|
21
|
-
return { key: { name, sequence: buf.subarray(0, 3).toString("binary") }, consumed: 3 };
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
// A lone ESC (or ESC followed by something we do not recognise) is Escape.
|
|
25
|
-
return { key: { name: "escape", sequence: "\x1b" }, consumed: 1 };
|
|
26
|
-
}
|
|
27
|
-
if (b0 === 0x0d || b0 === 0x0a) {
|
|
28
|
-
return { key: { name: "return", sequence: String.fromCharCode(b0) }, consumed: 1 };
|
|
29
|
-
}
|
|
30
|
-
if (b0 === 0x03) {
|
|
31
|
-
return { key: { name: "c", ctrl: true, sequence: "\x03" }, consumed: 1 };
|
|
32
|
-
}
|
|
33
|
-
if (b0 >= 0x20 && b0 < 0x7f) {
|
|
34
|
-
return { key: { name: String.fromCharCode(b0), sequence: String.fromCharCode(b0) }, consumed: 1 };
|
|
35
|
-
}
|
|
36
|
-
// Unknown byte: consume it without a name so it does not block the stream.
|
|
37
|
-
return { key: { sequence: String.fromCharCode(b0) }, consumed: 1 };
|
|
38
|
-
}
|
|
39
|
-
/** Routes decoded keypress events to handlers registered by key name. */
|
|
40
|
-
export class InputRouter {
|
|
41
|
-
input;
|
|
42
|
-
handlers = new Map();
|
|
43
|
-
started = false;
|
|
44
|
-
buffer = Buffer.alloc(0);
|
|
45
|
-
constructor(input) {
|
|
46
|
-
this.input = input;
|
|
47
|
-
}
|
|
48
|
-
/** Register a handler for a named key (e.g. "up", "escape", "q"). */
|
|
49
|
-
on(name, handler) {
|
|
50
|
-
let set = this.handlers.get(name);
|
|
51
|
-
if (!set) {
|
|
52
|
-
set = new Set();
|
|
53
|
-
this.handlers.set(name, set);
|
|
54
|
-
}
|
|
55
|
-
set.add(handler);
|
|
56
|
-
}
|
|
57
|
-
/** Remove a previously registered handler. */
|
|
58
|
-
off(name, handler) {
|
|
59
|
-
this.handlers.get(name)?.delete(handler);
|
|
60
|
-
}
|
|
61
|
-
/** Start decoding keypresses from the input stream. Idempotent. */
|
|
62
|
-
start() {
|
|
63
|
-
if (this.started)
|
|
64
|
-
return;
|
|
65
|
-
this.started = true;
|
|
66
|
-
this.input.setRawMode?.(true);
|
|
67
|
-
this.input.on("data", this.onData);
|
|
68
|
-
this.input.resume();
|
|
69
|
-
}
|
|
70
|
-
/** Stop decoding and release the stream. Idempotent. */
|
|
71
|
-
stop() {
|
|
72
|
-
if (!this.started)
|
|
73
|
-
return;
|
|
74
|
-
this.input.off("data", this.onData);
|
|
75
|
-
this.input.setRawMode?.(false);
|
|
76
|
-
this.input.pause();
|
|
77
|
-
this.started = false;
|
|
78
|
-
}
|
|
79
|
-
onData = (chunk) => {
|
|
80
|
-
const data = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, "binary");
|
|
81
|
-
this.buffer = Buffer.concat([this.buffer, data]);
|
|
82
|
-
while (this.buffer.length > 0) {
|
|
83
|
-
const result = decodeOneKey(this.buffer);
|
|
84
|
-
if (!result)
|
|
85
|
-
break;
|
|
86
|
-
this.dispatch(result.key);
|
|
87
|
-
this.buffer = this.buffer.subarray(result.consumed);
|
|
88
|
-
}
|
|
89
|
-
};
|
|
90
|
-
dispatch(key) {
|
|
91
|
-
const name = key.name ?? key.sequence;
|
|
92
|
-
if (!name)
|
|
93
|
-
return;
|
|
94
|
-
const set = this.handlers.get(name);
|
|
95
|
-
if (!set)
|
|
96
|
-
return;
|
|
97
|
-
for (const handler of set)
|
|
98
|
-
void handler(key);
|
|
99
|
-
}
|
|
100
|
-
}
|