1// Sticky scroll: which command's header belongs pinned at the top of the
2// viewport right now.
3//
4// The question only has an answer because command boundaries are anchored in
5// screen space and survive scrolling (`commands.ts`). Scroll into the middle of
6// a long build and the command that produced it has left the screen; pinning it
7// is what turns scrollback into something you can read without losing your
8// place.
9//
10// Pure: takes the viewport's top row and the command list, returns the command
11// (or null). No DOM, no wasm — the renderer supplies `viewportTop` from the
12// scrollbar, whose `offset` is the viewport's top row in the same screen
13// coordinate space anchors report.
14
15import type { ShellCommand } from "./commands.ts";
16
17/**
18 * The command whose output the viewport is currently inside, if that command's
19 * own line has scrolled off the top.
20 *
21 * Returns null when the command's line is still visible (pinning it would show
22 * it twice), when the viewport is above every command, or when the shell is
23 * uninstrumented.
24 */
25export function stickyCommand(
26 commands: readonly ShellCommand[],
27 viewportTop: number,
28): ShellCommand | null {
29 let sticky: ShellCommand | null = null;
30 for (const command of commands) {
31 const start = (command.commandStart ?? command.promptStart)?.screenPoint();
32 if (start === null || start === undefined) continue;
33 // Commands are in order, so the first one starting at or below the
34 // viewport top ends the search.
35 if (start.y >= viewportTop) break;
36 // A finished command whose output ended above the viewport is behind us.
37 // A running command has no end yet and extends to the bottom.
38 const end = command.outputEnd?.screenPoint();
39 if (end !== null && end !== undefined && end.y < viewportTop) continue;
40 sticky = command;
41 }
42 return sticky;
43}
44
45/** The row to scroll to when the pinned header is activated. */
46export function stickyTarget(command: ShellCommand): number | null {
47 const start = (command.promptStart ?? command.commandStart)?.screenPoint();
48 return start?.y ?? null;
49}