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.
@@ -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
- function createLiveCockpitDelivery({ cwd, runId, binaryVersion, now, }) {
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, columns = Number.MAX_SAFE_INTEGER) => transition((current) => {
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, and the plan owns that.
184
- runKeyColumns(columns));
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 [columns, rows] = measuredSize.split(":").map((part) => Math.max(0, Math.floor(Number(part))));
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 }, columns);
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
- const app = render(createElement(LiveApp, {
294
- delivery,
295
- size,
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
- size.close();
309
- app.unmount();
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;