| 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 | |
| 15 | import 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 | */ |
| 25 | export 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. */ |
| 46 | export function stickyTarget(command: ShellCommand): number | null { |
| 47 | const start = (command.promptStart ?? command.commandStart)?.screenPoint(); |
| 48 | return start?.y ?? null; |
| 49 | } |