tickmarkr 1.83.0 → 1.84.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/tui/cockpit/components.d.ts +28 -1
- package/dist/tui/cockpit/components.js +19 -3
- package/dist/tui/cockpit/layout.d.ts +75 -6
- package/dist/tui/cockpit/layout.js +97 -19
- package/dist/tui/cockpit/live.d.ts +14 -1
- package/dist/tui/cockpit/live.js +223 -29
- package/dist/tui/cockpit/pointer.d.ts +261 -0
- package/dist/tui/cockpit/pointer.js +610 -0
- package/dist/tui/cockpit/run-cockpit.d.ts +36 -5
- package/dist/tui/cockpit/run-cockpit.js +145 -27
- package/package.json +1 -1
package/dist/tui/cockpit/live.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { render, useApp, useInput } from "ink";
|
|
4
|
-
import { createElement, useEffect, useRef, useSyncExternalStore } from "react";
|
|
4
|
+
import { createElement, useCallback, useEffect, useRef, useSyncExternalStore } from "react";
|
|
5
5
|
import { graphPath, loadGraph, stateDirName } from "../../graph/graph.js";
|
|
6
6
|
import { Journal, parseRunId } from "../../run/journal.js";
|
|
7
7
|
import { deriveRunCockpitData } from "./derive.js";
|
|
8
|
-
import { deriveRunViewRows, runKeyColumns, RunCockpitFrame } from "./run-cockpit.js";
|
|
8
|
+
import { deriveRunViewRows, drawnRunViewRowIds, planRunCockpitFrame, runKeyColumns, RunCockpitFrame, } from "./run-cockpit.js";
|
|
9
9
|
import { dispatchRunSurfaceKey, openingRunSurfaceState, reconcileRunInteraction, RUN_INPUT_BINDINGS, selectableRunViewRowIds, } from "./keys.js";
|
|
10
|
+
import { applyPointerReport, borrowPointerTracking, createPointerReportReader, } from "./pointer.js";
|
|
10
11
|
// ponytail: fixed 2s re-derive cadence, matching `status --watch`; promote to a
|
|
11
12
|
// config knob only if an operator asks.
|
|
12
13
|
const REFRESH_MS = 2_000;
|
|
@@ -110,7 +111,44 @@ export function liveRunViewRowIds(interaction, data) {
|
|
|
110
111
|
deriveRunViewRows(data, interaction.activeView, interaction.filterQuery)
|
|
111
112
|
.map((row) => row.id));
|
|
112
113
|
}
|
|
113
|
-
|
|
114
|
+
/**
|
|
115
|
+
* The pointer surface a committed frame carries: the plan the paint drew and
|
|
116
|
+
* the rows drawn into its body, both read off the renderer's own output. This
|
|
117
|
+
* is the only composition the live input path resolves through — it holds no
|
|
118
|
+
* planner and no geometry of its own, so a hit can never land on bands or
|
|
119
|
+
* rows the operator has not seen.
|
|
120
|
+
*/
|
|
121
|
+
function committedRunPointerSurface(planned, data) {
|
|
122
|
+
if (planned.plan.kind !== "frame" || planned.content === undefined) {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
interaction: planned.interaction,
|
|
127
|
+
// The plan is the whole geometry the pointer receives: the item rows it
|
|
128
|
+
// resolves a hit through are the plan's own `items` band, the same region
|
|
129
|
+
// the paint draws its list into, so there is no second reading of where the
|
|
130
|
+
// list starts.
|
|
131
|
+
plan: planned.plan,
|
|
132
|
+
drawnRowIds: drawnRunViewRowIds(data, planned.interaction, planned.content.items.rows),
|
|
133
|
+
rowIds: liveRunViewRowIds(planned.interaction, data),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* The suite's own planning of the surface a frame at this data, state and
|
|
138
|
+
* measured size commits — the same composition the renderer publishes through
|
|
139
|
+
* its commit seam, so a hit is tested against the frame production actually
|
|
140
|
+
* painted. The live input path never calls this: it resolves through the
|
|
141
|
+
* committed frame, and what nothing has painted yet has no geometry.
|
|
142
|
+
*/
|
|
143
|
+
export function liveRunPointerSurface(data, interaction, size) {
|
|
144
|
+
return committedRunPointerSurface(planRunCockpitFrame({
|
|
145
|
+
data,
|
|
146
|
+
columns: size.columns,
|
|
147
|
+
rows: size.rows,
|
|
148
|
+
interaction,
|
|
149
|
+
}), data);
|
|
150
|
+
}
|
|
151
|
+
function createLiveCockpitDelivery({ cwd, runId, binaryVersion, now, size, }) {
|
|
114
152
|
// Both sources are read every refresh: a recompile between refreshes changes
|
|
115
153
|
// which tasks exist, and the surface must draw the graph the repository has
|
|
116
154
|
// now rather than the one it had when the cockpit opened.
|
|
@@ -124,6 +162,15 @@ function createLiveCockpitDelivery({ cwd, runId, binaryVersion, now, }) {
|
|
|
124
162
|
const listeners = new Set();
|
|
125
163
|
let batchDepth = 0;
|
|
126
164
|
let pendingDraw = false;
|
|
165
|
+
/**
|
|
166
|
+
* The frame on the screen, published by the renderer each time it commits
|
|
167
|
+
* one. Pointer reports resolve through this and only this: the input path
|
|
168
|
+
* plans nothing and caches no geometry, so a refresh, resize or key that
|
|
169
|
+
* nothing has painted yet cannot move a hit — the target set is the bands
|
|
170
|
+
* and rows the operator is actually looking at, until the paint replaces
|
|
171
|
+
* them.
|
|
172
|
+
*/
|
|
173
|
+
let committedPointerSurface;
|
|
127
174
|
const publish = () => {
|
|
128
175
|
if (batchDepth > 0) {
|
|
129
176
|
pendingDraw = true;
|
|
@@ -154,6 +201,9 @@ function createLiveCockpitDelivery({ cwd, runId, binaryVersion, now, }) {
|
|
|
154
201
|
finally {
|
|
155
202
|
batchDepth -= 1;
|
|
156
203
|
if (batchDepth === 0 && pendingDraw) {
|
|
204
|
+
// The batch ends and the surface is drawn again; the renderer's
|
|
205
|
+
// commit of that draw publishes the frame the next report resolves
|
|
206
|
+
// through.
|
|
157
207
|
pendingDraw = false;
|
|
158
208
|
publish();
|
|
159
209
|
}
|
|
@@ -178,10 +228,12 @@ function createLiveCockpitDelivery({ cwd, runId, binaryVersion, now, }) {
|
|
|
178
228
|
// can be in. The prompt is a scope inside that registry rather than an
|
|
179
229
|
// owner in front of it, so a global key — Tab above all — reaches its
|
|
180
230
|
// handler with a prompt open exactly as it does without one.
|
|
181
|
-
key: (event
|
|
231
|
+
key: (event) => transition((current) => {
|
|
182
232
|
const next = dispatchRunSurfaceKey(event, { interaction: current.interaction, stashed: current.stashed }, RUN_INPUT_BINDINGS, liveRunViewRowIds(current.interaction, current.data),
|
|
183
|
-
// The dispatcher decides on rail visibility
|
|
184
|
-
|
|
233
|
+
// The dispatcher decides on rail visibility from the width the surface
|
|
234
|
+
// is measured at. A default here routed every key as if the rail were
|
|
235
|
+
// drawn, at widths where the plan draws no rail at all.
|
|
236
|
+
runKeyColumns(size().columns));
|
|
185
237
|
if (next === undefined)
|
|
186
238
|
return current;
|
|
187
239
|
const interaction = reconcileLiveRunInteraction(next.interaction, current.data);
|
|
@@ -189,9 +241,40 @@ function createLiveCockpitDelivery({ cwd, runId, binaryVersion, now, }) {
|
|
|
189
241
|
? current
|
|
190
242
|
: { ...current, interaction, stashed: next.stashed };
|
|
191
243
|
}),
|
|
244
|
+
// A pointer report resolves through the committed frame — the plan and
|
|
245
|
+
// the drawn rows the paint has on the screen, and nothing the input path
|
|
246
|
+
// derives for itself. Adjacent reports — a double click above all —
|
|
247
|
+
// arrive in one batch, which defers the redraw: they all resolve through
|
|
248
|
+
// the ONE committed frame still on the screen, and only the state each
|
|
249
|
+
// transition is dispatched FROM moves. The first press's own new scroll
|
|
250
|
+
// window cannot slide the list under the second press, because that
|
|
251
|
+
// window does not exist until the paint draws it.
|
|
252
|
+
pointer: (report) => transition((current) => {
|
|
253
|
+
const drawn = committedPointerSurface;
|
|
254
|
+
if (drawn === undefined)
|
|
255
|
+
return current;
|
|
256
|
+
const next = applyPointerReport(report, {
|
|
257
|
+
...drawn,
|
|
258
|
+
interaction: current.interaction,
|
|
259
|
+
});
|
|
260
|
+
if (next === undefined)
|
|
261
|
+
return current;
|
|
262
|
+
const interaction = reconcileLiveRunInteraction(next, current.data);
|
|
263
|
+
return interaction === current.interaction
|
|
264
|
+
? current
|
|
265
|
+
: { ...current, interaction };
|
|
266
|
+
}),
|
|
267
|
+
commitPointerSurface: (surface) => {
|
|
268
|
+
committedPointerSurface = surface;
|
|
269
|
+
},
|
|
192
270
|
};
|
|
193
271
|
return delivery;
|
|
194
272
|
}
|
|
273
|
+
/** The measured size a snapshot states, in the whole cells a plan is made of. */
|
|
274
|
+
function parseMeasuredSize(snapshot) {
|
|
275
|
+
const [columns, rows] = snapshot.split(":").map((part) => Math.max(0, Math.floor(Number(part))));
|
|
276
|
+
return { columns, rows };
|
|
277
|
+
}
|
|
195
278
|
function measureOutput(output) {
|
|
196
279
|
const read = () => `${output.columns ?? 80}:${output.rows ?? 24}`;
|
|
197
280
|
let size = read();
|
|
@@ -245,11 +328,83 @@ function measureOutput(output) {
|
|
|
245
328
|
close: () => output.off("resize", resized),
|
|
246
329
|
};
|
|
247
330
|
}
|
|
331
|
+
/**
|
|
332
|
+
* The stdin the surface's keyboard reads: the real stream with every pointer
|
|
333
|
+
* report taken out of it. Reports and keys arrive interleaved on one stream, and
|
|
334
|
+
* a reader beside Ink's would not be enough — Ink would still see the report
|
|
335
|
+
* bytes and read a whole one as an unnameable escape sequence, or half of one as
|
|
336
|
+
* the single character its chunk happens to end on, dispatching either into
|
|
337
|
+
* whatever scope is live. So the split sits in the pull Ink already does
|
|
338
|
+
* (`read()` under its `readable` listener): reports are handed to the pointer
|
|
339
|
+
* boundary, and only what was left is ever read as a keystroke. A chunk that was
|
|
340
|
+
* nothing but reports reads as no input at all rather than as empty input.
|
|
341
|
+
*/
|
|
342
|
+
function pointerFilteredInput(input, deliver) {
|
|
343
|
+
const reader = createPointerReportReader();
|
|
344
|
+
const tokens = [];
|
|
345
|
+
let pendingEscape;
|
|
346
|
+
const clearPendingEscape = () => {
|
|
347
|
+
if (pendingEscape === undefined)
|
|
348
|
+
return;
|
|
349
|
+
clearImmediate(pendingEscape);
|
|
350
|
+
pendingEscape = undefined;
|
|
351
|
+
};
|
|
352
|
+
const wake = () => {
|
|
353
|
+
input.emit("readable");
|
|
354
|
+
};
|
|
355
|
+
const schedulePendingEscape = () => {
|
|
356
|
+
clearPendingEscape();
|
|
357
|
+
// The same next-turn grace Ink gives an ambiguous ESC: a continuation that
|
|
358
|
+
// arrives first completes the report; otherwise ESC becomes keyboard input.
|
|
359
|
+
if (reader.pending() !== "\x1b")
|
|
360
|
+
return;
|
|
361
|
+
pendingEscape = setImmediate(() => {
|
|
362
|
+
pendingEscape = undefined;
|
|
363
|
+
tokens.push(...reader.flush().tokens);
|
|
364
|
+
wake();
|
|
365
|
+
});
|
|
366
|
+
};
|
|
367
|
+
const read = (...args) => {
|
|
368
|
+
while (true) {
|
|
369
|
+
const token = tokens.shift();
|
|
370
|
+
if (token?.type === "pointer") {
|
|
371
|
+
const reports = [token.report];
|
|
372
|
+
while (tokens[0]?.type === "pointer") {
|
|
373
|
+
const adjacent = tokens.shift();
|
|
374
|
+
if (adjacent?.type === "pointer")
|
|
375
|
+
reports.push(adjacent.report);
|
|
376
|
+
}
|
|
377
|
+
deliver(reports);
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
if (token?.type === "keys")
|
|
381
|
+
return token.bytes;
|
|
382
|
+
const chunk = input.read(...args);
|
|
383
|
+
if (chunk === null || chunk === undefined)
|
|
384
|
+
return null;
|
|
385
|
+
clearPendingEscape();
|
|
386
|
+
const readout = reader(String(chunk));
|
|
387
|
+
tokens.push(...readout.tokens);
|
|
388
|
+
schedulePendingEscape();
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
const stdin = new Proxy(input, {
|
|
392
|
+
get(target, property) {
|
|
393
|
+
if (property === "read")
|
|
394
|
+
return read;
|
|
395
|
+
const value = Reflect.get(target, property);
|
|
396
|
+
return typeof value === "function" && !Object.hasOwn(target, property)
|
|
397
|
+
? value.bind(target)
|
|
398
|
+
: value;
|
|
399
|
+
},
|
|
400
|
+
});
|
|
401
|
+
return { stdin, close: clearPendingEscape };
|
|
402
|
+
}
|
|
248
403
|
function LiveApp({ delivery, size, refreshMs, }) {
|
|
249
404
|
const { exit } = useApp();
|
|
250
405
|
const surface = useSyncExternalStore(delivery.subscribe, delivery.snapshot, delivery.snapshot);
|
|
251
406
|
const measuredSize = useSyncExternalStore(size.subscribe, size.snapshot, size.snapshot);
|
|
252
|
-
const
|
|
407
|
+
const { columns, rows } = parseMeasuredSize(measuredSize);
|
|
253
408
|
const drawnSize = useRef(measuredSize);
|
|
254
409
|
useEffect(() => {
|
|
255
410
|
if (drawnSize.current === measuredSize)
|
|
@@ -270,42 +425,81 @@ function LiveApp({ delivery, size, refreshMs, }) {
|
|
|
270
425
|
exit();
|
|
271
426
|
return;
|
|
272
427
|
}
|
|
273
|
-
delivery.key({ input, key }
|
|
428
|
+
delivery.key({ input, key });
|
|
274
429
|
});
|
|
430
|
+
// Every frame the renderer commits is published to the delivery, so a
|
|
431
|
+
// pointer report resolves against the plan and the drawn rows of the frame
|
|
432
|
+
// on the screen — the input path never plans one of its own.
|
|
433
|
+
const commitFrame = useCallback((planned, frameData) => {
|
|
434
|
+
delivery.commitPointerSurface(committedRunPointerSurface(planned, frameData));
|
|
435
|
+
}, [delivery]);
|
|
275
436
|
return createElement(RunCockpitFrame, {
|
|
276
437
|
data: surface.data,
|
|
277
438
|
columns,
|
|
278
439
|
rows,
|
|
279
440
|
interaction: surface.interaction,
|
|
441
|
+
onCommittedFrame: commitFrame,
|
|
280
442
|
});
|
|
281
443
|
}
|
|
282
444
|
export async function runLiveCockpit({ input, output, cwd, runId, binaryVersion, refreshMs = REFRESH_MS, now = Date.now, debug = false, onDelivery, }) {
|
|
283
|
-
const delivery = createLiveCockpitDelivery({
|
|
284
|
-
cwd,
|
|
285
|
-
runId,
|
|
286
|
-
binaryVersion,
|
|
287
|
-
now,
|
|
288
|
-
});
|
|
289
|
-
onDelivery?.(delivery);
|
|
290
445
|
// Subscribe before Ink mounts so no stale-size repaint can overtake the
|
|
291
|
-
// surface's newly planned frame
|
|
446
|
+
// surface's newly planned frame — and before the delivery exists, which
|
|
447
|
+
// routes every input against this measurement.
|
|
292
448
|
const size = measureOutput(output);
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
refreshMs,
|
|
297
|
-
}), {
|
|
298
|
-
stdin: input,
|
|
299
|
-
stdout: size.stdout,
|
|
300
|
-
debug,
|
|
301
|
-
exitOnCtrlC: false,
|
|
302
|
-
patchConsole: false,
|
|
303
|
-
});
|
|
449
|
+
let app;
|
|
450
|
+
let filteredInput;
|
|
451
|
+
let releasePointerTracking;
|
|
304
452
|
try {
|
|
453
|
+
const delivery = createLiveCockpitDelivery({
|
|
454
|
+
cwd,
|
|
455
|
+
runId,
|
|
456
|
+
binaryVersion,
|
|
457
|
+
now,
|
|
458
|
+
size: () => parseMeasuredSize(size.snapshot()),
|
|
459
|
+
});
|
|
460
|
+
onDelivery?.(delivery);
|
|
461
|
+
// Pointer reports are their own input class: they are taken off the stream
|
|
462
|
+
// before the keyboard reads it and delivered to the pointer boundary, never
|
|
463
|
+
// handed to a key handler. A batch keeps adjacent reports to one redraw.
|
|
464
|
+
filteredInput = pointerFilteredInput(input, (reports) => {
|
|
465
|
+
delivery.batch(() => {
|
|
466
|
+
for (const report of reports)
|
|
467
|
+
delivery.pointer(report);
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
// One loan owns the complete live lifecycle. Its signal repayments stand
|
|
471
|
+
// before the ask, then remain installed through mount, paint, input and
|
|
472
|
+
// unmount. Non-tty output borrows nothing and writes nothing.
|
|
473
|
+
releasePointerTracking = borrowPointerTracking(output);
|
|
474
|
+
app = render(createElement(LiveApp, {
|
|
475
|
+
delivery,
|
|
476
|
+
size,
|
|
477
|
+
refreshMs,
|
|
478
|
+
}), {
|
|
479
|
+
stdin: filteredInput.stdin,
|
|
480
|
+
stdout: size.stdout,
|
|
481
|
+
debug,
|
|
482
|
+
exitOnCtrlC: false,
|
|
483
|
+
patchConsole: false,
|
|
484
|
+
});
|
|
305
485
|
await app.waitUntilExit();
|
|
306
486
|
}
|
|
307
487
|
finally {
|
|
308
|
-
|
|
309
|
-
|
|
488
|
+
try {
|
|
489
|
+
filteredInput?.close();
|
|
490
|
+
}
|
|
491
|
+
finally {
|
|
492
|
+
try {
|
|
493
|
+
app?.unmount();
|
|
494
|
+
}
|
|
495
|
+
finally {
|
|
496
|
+
try {
|
|
497
|
+
releasePointerTracking?.();
|
|
498
|
+
}
|
|
499
|
+
finally {
|
|
500
|
+
size.close();
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
310
504
|
}
|
|
311
505
|
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { type RunInteractionState } from "./keys.js";
|
|
2
|
+
import { type FrameRegion, type PlannedFrame } from "./layout.js";
|
|
3
|
+
/**
|
|
4
|
+
* The terminal reports no pointer at all until it is asked to, so the parser
|
|
5
|
+
* above reads nothing until these bytes are written. DECSET 1000 turns on normal
|
|
6
|
+
* tracking — presses, releases and the wheel. DECSET 1006 asks for them in SGR
|
|
7
|
+
* encoding: the one grammar `SGR_POINTER_REPORT` reads, and the only one that
|
|
8
|
+
* can state a cell past column 223 at all. DECSET 1003 widens that to motion
|
|
9
|
+
* with no button held — the only way a terminal ever says where the pointer is
|
|
10
|
+
* merely resting, and therefore the whole reason a hover highlight can exist
|
|
11
|
+
* at all. It is asked for last, so the widest tracking mode is the one asked
|
|
12
|
+
* for in the grammar already selected. The request lives beside the parser so a
|
|
13
|
+
* change to what is read cannot leave what is asked for behind.
|
|
14
|
+
*/
|
|
15
|
+
export declare const POINTER_TRACKING_ON = "\u001B[?1000h\u001B[?1006h\u001B[?1003h";
|
|
16
|
+
/** The same three modes turned off, in exact reverse of the order they were
|
|
17
|
+
* asked for — a terminal left tracking writes reports into whatever runs after
|
|
18
|
+
* the cockpit exits. */
|
|
19
|
+
export declare const POINTER_TRACKING_OFF = "\u001B[?1003l\u001B[?1006l\u001B[?1000l";
|
|
20
|
+
/**
|
|
21
|
+
* The signals that end a process where nothing else repays the loan: they run
|
|
22
|
+
* no `finally`, unwind no stack and unmount no renderer, so a surface killed by
|
|
23
|
+
* one would leave the operator's terminal reporting a pointer into whatever
|
|
24
|
+
* shell comes next. Listening for them is the only way to hand the modes back.
|
|
25
|
+
*
|
|
26
|
+
* The roster is every catchable POSIX terminator an interactive cockpit is
|
|
27
|
+
* actually killed by — ⌃C, a `kill`, a closed terminal, and ⌃\ — not a
|
|
28
|
+
* shortlist of the three that came to mind. A signal missing from here is a
|
|
29
|
+
* terminal left reporting, so the test that guards it names its own required
|
|
30
|
+
* set rather than reading this one back.
|
|
31
|
+
*/
|
|
32
|
+
export declare const POINTER_RELEASE_SIGNALS: readonly ["SIGINT", "SIGTERM", "SIGHUP", "SIGQUIT"];
|
|
33
|
+
/** A cell the pointer can be at, counted from zero like every planned region. */
|
|
34
|
+
export type PointerCell = {
|
|
35
|
+
readonly column: number;
|
|
36
|
+
readonly row: number;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* The cell the pointer is resting on, or none. The same reference for as long
|
|
40
|
+
* as it has not moved, so a watcher redraws when the pointer moves and at no
|
|
41
|
+
* other time.
|
|
42
|
+
*/
|
|
43
|
+
export declare function pointerRestingCell(): PointerCell | null;
|
|
44
|
+
/** Watch the resting cell. The returned call stops watching. */
|
|
45
|
+
export declare function watchPointerRest(watcher: () => void): () => void;
|
|
46
|
+
/**
|
|
47
|
+
* The rail columns the session dragged the boundary to, or none. The same
|
|
48
|
+
* reference for as long as it has not changed, so a watcher redraws when the
|
|
49
|
+
* drag moves the boundary and at no other time.
|
|
50
|
+
*/
|
|
51
|
+
export declare function sessionRailOverride(): number | null;
|
|
52
|
+
/** Watch the session override. The returned call stops watching. */
|
|
53
|
+
export declare function watchSessionRailOverride(watcher: () => void): () => void;
|
|
54
|
+
/**
|
|
55
|
+
* The session boundary the relaunch contract is stated against: the override
|
|
56
|
+
* and any drag in progress are gone, exactly as a new process starts. The
|
|
57
|
+
* production marker is the tracking loan — a fresh borrow is a fresh session.
|
|
58
|
+
*/
|
|
59
|
+
export declare function resetSessionRailOverride(): void;
|
|
60
|
+
/** The stream the modes are asked of — a real terminal, or something that is not one. */
|
|
61
|
+
export type PointerTrackingTerminal = {
|
|
62
|
+
readonly isTTY?: boolean;
|
|
63
|
+
readonly write: (bytes: string) => unknown;
|
|
64
|
+
};
|
|
65
|
+
/** The process the loan registers its last-resort repayment with. */
|
|
66
|
+
export type PointerTrackingHost = {
|
|
67
|
+
readonly pid: number;
|
|
68
|
+
readonly on: (signal: NodeJS.Signals, listener: () => void) => unknown;
|
|
69
|
+
readonly off: (signal: NodeJS.Signals, listener: () => void) => unknown;
|
|
70
|
+
readonly kill: (pid: number, signal: NodeJS.Signals) => unknown;
|
|
71
|
+
/** Which signals this host can be killed by at all — `process.platform`. */
|
|
72
|
+
readonly platform?: NodeJS.Platform;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Borrow pointer reporting, and return the one way to repay it.
|
|
76
|
+
*
|
|
77
|
+
* The loan is the whole lifecycle in one expression, so no caller can hold half
|
|
78
|
+
* of it. Only an interactive terminal is asked at all: writing mode requests
|
|
79
|
+
* into a pipe puts escape bytes in a capture rather than a mouse on a surface
|
|
80
|
+
* nobody is pointing at, so the non-tty and CI surfaces carry no enable
|
|
81
|
+
* sequence because none is ever written — not because a later guard strips one.
|
|
82
|
+
*
|
|
83
|
+
* Repayment is idempotent and reachable from every exit: the returned release
|
|
84
|
+
* for an ordinary return, the same release run from a `finally` or a renderer's
|
|
85
|
+
* cleanup for a thrown failure, and the signal listeners registered here for
|
|
86
|
+
* the exits that run neither. A listener repays and then re-raises its own
|
|
87
|
+
* signal with our listener removed, so the process still ends exactly the way
|
|
88
|
+
* the signal meant it to.
|
|
89
|
+
*/
|
|
90
|
+
export declare function borrowPointerTracking(terminal: PointerTrackingTerminal, host?: PointerTrackingHost): () => void;
|
|
91
|
+
export type PointerAction = "press" | "release" | "move" | "wheel-up" | "wheel-down";
|
|
92
|
+
/** One reported pointer event, its cell zero-based like every planned region. */
|
|
93
|
+
export type PointerReport = {
|
|
94
|
+
readonly action: PointerAction;
|
|
95
|
+
readonly column: number;
|
|
96
|
+
readonly row: number;
|
|
97
|
+
/**
|
|
98
|
+
* The raw SGR button code the report arrived with. The action alone throws
|
|
99
|
+
* information away that a drag latch cannot survive without: under DECSET
|
|
100
|
+
* 1003 a motion with NO button held reports button 35 — `(button & 3) === 3`
|
|
101
|
+
* — and that no-button motion is the only signal that a release lost
|
|
102
|
+
* outside the window (the pointer let go past the terminal's edge, which
|
|
103
|
+
* sends no release at all) has already happened. Absent only on a report
|
|
104
|
+
* built by hand rather than parsed off the wire; a hand-built move is read
|
|
105
|
+
* as a drag-motion, the reading that cannot end a drag its producer had no
|
|
106
|
+
* way to describe.
|
|
107
|
+
*/
|
|
108
|
+
readonly button?: number;
|
|
109
|
+
};
|
|
110
|
+
/** What one chunk of terminal input turned out to be: the reports it carried,
|
|
111
|
+
* and the bytes that were not part of any — the keyboard's own input. */
|
|
112
|
+
export type PointerReadout = {
|
|
113
|
+
/** Keyboard and pointer input in the byte order the terminal delivered it. */
|
|
114
|
+
readonly tokens: readonly PointerInputToken[];
|
|
115
|
+
readonly reports: readonly PointerReport[];
|
|
116
|
+
readonly keys: string;
|
|
117
|
+
};
|
|
118
|
+
export type PointerInputToken = {
|
|
119
|
+
readonly type: "keys";
|
|
120
|
+
readonly bytes: string;
|
|
121
|
+
} | {
|
|
122
|
+
readonly type: "pointer";
|
|
123
|
+
readonly report: PointerReport;
|
|
124
|
+
};
|
|
125
|
+
export type PointerReportReader = {
|
|
126
|
+
(chunk: string): PointerReadout;
|
|
127
|
+
/** Release a prefix that did not receive the bytes needed to become a report. */
|
|
128
|
+
flush(): PointerReadout;
|
|
129
|
+
/** The incomplete report prefix currently withheld from the keyboard. */
|
|
130
|
+
pending(): string;
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* The pointer reports carried by a chunk of terminal input. Bytes that are not
|
|
134
|
+
* a report are not pointer input and are left for whoever else reads the
|
|
135
|
+
* stream; a torn or malformed sequence simply reports nothing.
|
|
136
|
+
*/
|
|
137
|
+
export declare function parsePointerReports(bytes: string): readonly PointerReport[];
|
|
138
|
+
/**
|
|
139
|
+
* A reader over the input stream rather than over one chunk. The terminal
|
|
140
|
+
* writes bytes, not messages: a single report is free to arrive split across
|
|
141
|
+
* chunks, and chunk-at-a-time parsing silently drops every report that lands on
|
|
142
|
+
* a boundary. The reader carries the torn tail into the next chunk and reports
|
|
143
|
+
* only sequences that have completed.
|
|
144
|
+
*
|
|
145
|
+
* It also states what was left — the bytes of that chunk that belonged to no
|
|
146
|
+
* report, including none at all while a torn one is still arriving. Reports and
|
|
147
|
+
* keys share one stream, so this is the only place that can tell them apart:
|
|
148
|
+
* downstream, a whole report is an unnameable escape sequence and half a report
|
|
149
|
+
* is the single character it happens to end on, either of which would be read as
|
|
150
|
+
* a keystroke by whatever scope is live.
|
|
151
|
+
*/
|
|
152
|
+
export declare function createPointerReportReader(): PointerReportReader;
|
|
153
|
+
/**
|
|
154
|
+
* The width the key registry resolves at for the bands THIS plan drew. The
|
|
155
|
+
* focus order follows the plan rather than a second reading of the terminal:
|
|
156
|
+
* between 64 and 79 columns the plan draws the rail at a width the key floor
|
|
157
|
+
* alone would deny it, and the click targets and the focus order have to agree
|
|
158
|
+
* about that band or a click would act on a panel the frame does not show.
|
|
159
|
+
*/
|
|
160
|
+
export declare function plannedKeyColumns(plan: PlannedFrame): number;
|
|
161
|
+
export type PointerTarget = {
|
|
162
|
+
/** The tightest planned region containing the cell. */
|
|
163
|
+
readonly region: FrameRegion;
|
|
164
|
+
/** The rail's menu row under the cell, when the cell is on one. */
|
|
165
|
+
readonly view?: number;
|
|
166
|
+
/** The body's drawn row under the cell, counted from the planned item region's own first row. */
|
|
167
|
+
readonly row?: number;
|
|
168
|
+
};
|
|
169
|
+
/**
|
|
170
|
+
* What the plan says is under a cell. The rail's view rows are
|
|
171
|
+
* `plan.sidebar.viewRows` — frame rows planFrame itself computed, where the
|
|
172
|
+
* label row was decided — so the menu's shape is looked up rather than
|
|
173
|
+
* reconstructed from `menuRows` out here. The body's item rows are a planned
|
|
174
|
+
* band of their own, so the tightest-region lookup lands on them directly and
|
|
175
|
+
* the first item's offset is the region's own row. The view strip the plan draws
|
|
176
|
+
* instead of a rail below 64 columns is one span with no per-view columns in it,
|
|
177
|
+
* so nothing here invents any: a strip cell resolves to the strip band and no
|
|
178
|
+
* further.
|
|
179
|
+
*/
|
|
180
|
+
export declare function resolvePointerTarget(plan: PlannedFrame, cell: {
|
|
181
|
+
readonly column: number;
|
|
182
|
+
readonly row: number;
|
|
183
|
+
}): PointerTarget | undefined;
|
|
184
|
+
/**
|
|
185
|
+
* The focus index a target's band carries at the width the plan drew — the same
|
|
186
|
+
* order the frame paints its focus ring from, so a click can never focus a
|
|
187
|
+
* panel the frame does not show as focusable.
|
|
188
|
+
*/
|
|
189
|
+
export declare function pointerFocusPanel(plan: PlannedFrame, target: PointerTarget): number | undefined;
|
|
190
|
+
/**
|
|
191
|
+
* The half of a surface a row hit resolves through: the plan the paint drew,
|
|
192
|
+
* and the identities the rows drawn into it carry.
|
|
193
|
+
*/
|
|
194
|
+
export type PointerRowSurface = {
|
|
195
|
+
readonly plan: PlannedFrame;
|
|
196
|
+
/** The identities the body's drawn rows carry, top to bottom. */
|
|
197
|
+
readonly drawnRowIds: readonly string[];
|
|
198
|
+
};
|
|
199
|
+
/**
|
|
200
|
+
* The item a cell acts on — THE resolution, shared by every pointer path.
|
|
201
|
+
* A click reads it to decide what to select, and a hover highlight reads the
|
|
202
|
+
* same call to decide what to mark, so the highlight cannot name a row a click
|
|
203
|
+
* at that cell would miss: there is one answer, not two agreeing ones. A cell
|
|
204
|
+
* the plan does not place on an item row, and an item row the paint drew
|
|
205
|
+
* nothing into, are both nothing here rather than a nearest guess.
|
|
206
|
+
*/
|
|
207
|
+
export declare function pointerRowAt(surface: PointerRowSurface, cell: {
|
|
208
|
+
readonly column: number;
|
|
209
|
+
readonly row: number;
|
|
210
|
+
}): string | undefined;
|
|
211
|
+
/** The surface a pointer report acts on: the plan the paint drew, and the rows drawn into it. */
|
|
212
|
+
export type PointerSurface = {
|
|
213
|
+
/** planFrame's own output — the only geometry a hit resolves through. */
|
|
214
|
+
readonly plan: PlannedFrame;
|
|
215
|
+
readonly interaction: RunInteractionState;
|
|
216
|
+
/** The identities the body's drawn rows carry, top to bottom. */
|
|
217
|
+
readonly drawnRowIds: readonly string[];
|
|
218
|
+
/** The rows a key may stand on, in the active view's own order. */
|
|
219
|
+
readonly rowIds: readonly string[];
|
|
220
|
+
};
|
|
221
|
+
/**
|
|
222
|
+
* The transition a pointer report makes, or undefined when nothing owns the
|
|
223
|
+
* cell. A click on a view name is that view's own number key; a click on the
|
|
224
|
+
* row already marked is ⏎; moving between panels is the advertised dive/back
|
|
225
|
+
* grammar; and a wheel is ↑↓ only when the panel under it already owns ↑↓.
|
|
226
|
+
* An off-focus wheel is not an action: borrowing focus or prompt scope and then
|
|
227
|
+
* restoring it would create a frame no advertised key sequence can reach.
|
|
228
|
+
*
|
|
229
|
+
* A drag of the panel boundary is the exception that proves the second law:
|
|
230
|
+
* it is no transition at all. The press landing on the boundary grabs it; each
|
|
231
|
+
* move hands the plan a new input through the session override; the release
|
|
232
|
+
* lets go — and when the release never arrives because the pointer was let go
|
|
233
|
+
* outside the window, the first no-button motion says so in its stead, so no
|
|
234
|
+
* latch outlives the drag it latched. The interaction comes back untouched —
|
|
235
|
+
* the drag re-plans the frame rather than moving anything inside it.
|
|
236
|
+
*/
|
|
237
|
+
export declare function applyPointerReport(report: PointerReport, surface: PointerSurface): RunInteractionState | undefined;
|
|
238
|
+
/**
|
|
239
|
+
* The item a hover highlight marks: the one a click at that cell would ACT ON.
|
|
240
|
+
*
|
|
241
|
+
* `pointerRowAt` answers a smaller question — which drawn row the plan places
|
|
242
|
+
* under the cell — and a click is not only that lookup. It is a route: the /
|
|
243
|
+
* prompt applied, focus dived or backed to the panel under the pointer, and only
|
|
244
|
+
* then the row. Any of those may legitimately land somewhere else, the plainest
|
|
245
|
+
* case is the rail marker standing on a view the body is not drawing: the click
|
|
246
|
+
* that dives into content opens the MARKED view and selects no row at all, while
|
|
247
|
+
* the rows under the pointer belong to the view being left behind.
|
|
248
|
+
*
|
|
249
|
+
* So the highlight is not decided by the lookup. It is decided by running the
|
|
250
|
+
* click's own transition and asking what it settled on — the same
|
|
251
|
+
* `pointerTransition` the live report path dispatches through, on the same
|
|
252
|
+
* surface, so an answer these two could disagree about is not expressible. A
|
|
253
|
+
* click that acts on something else, or on nothing, lights nothing up.
|
|
254
|
+
*
|
|
255
|
+
* It reports no rest and returns no state: asking is not pointing and not
|
|
256
|
+
* clicking, so the surface is exactly as it was.
|
|
257
|
+
*/
|
|
258
|
+
export declare function pointerHoverRow(surface: PointerSurface, cell: {
|
|
259
|
+
readonly column: number;
|
|
260
|
+
readonly row: number;
|
|
261
|
+
}): string | undefined;
|