@asm-agent/tui 0.8.2

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.
Files changed (130) hide show
  1. package/README.md +795 -0
  2. package/dist/autocomplete.d.ts +61 -0
  3. package/dist/autocomplete.d.ts.map +1 -0
  4. package/dist/autocomplete.js +601 -0
  5. package/dist/autocomplete.js.map +1 -0
  6. package/dist/components/box.d.ts +24 -0
  7. package/dist/components/box.d.ts.map +1 -0
  8. package/dist/components/box.js +114 -0
  9. package/dist/components/box.js.map +1 -0
  10. package/dist/components/cancellable-loader.d.ts +22 -0
  11. package/dist/components/cancellable-loader.d.ts.map +1 -0
  12. package/dist/components/cancellable-loader.js +35 -0
  13. package/dist/components/cancellable-loader.js.map +1 -0
  14. package/dist/components/editor.d.ts +275 -0
  15. package/dist/components/editor.d.ts.map +1 -0
  16. package/dist/components/editor.js +1968 -0
  17. package/dist/components/editor.js.map +1 -0
  18. package/dist/components/image.d.ts +34 -0
  19. package/dist/components/image.d.ts.map +1 -0
  20. package/dist/components/image.js +96 -0
  21. package/dist/components/image.js.map +1 -0
  22. package/dist/components/input.d.ts +37 -0
  23. package/dist/components/input.d.ts.map +1 -0
  24. package/dist/components/input.js +385 -0
  25. package/dist/components/input.js.map +1 -0
  26. package/dist/components/loader.d.ts +29 -0
  27. package/dist/components/loader.d.ts.map +1 -0
  28. package/dist/components/loader.js +69 -0
  29. package/dist/components/loader.js.map +1 -0
  30. package/dist/components/markdown.d.ts +100 -0
  31. package/dist/components/markdown.d.ts.map +1 -0
  32. package/dist/components/markdown.js +773 -0
  33. package/dist/components/markdown.js.map +1 -0
  34. package/dist/components/select-list.d.ts +61 -0
  35. package/dist/components/select-list.d.ts.map +1 -0
  36. package/dist/components/select-list.js +206 -0
  37. package/dist/components/select-list.js.map +1 -0
  38. package/dist/components/settings-list.d.ts +49 -0
  39. package/dist/components/settings-list.d.ts.map +1 -0
  40. package/dist/components/settings-list.js +170 -0
  41. package/dist/components/settings-list.js.map +1 -0
  42. package/dist/components/spacer.d.ts +12 -0
  43. package/dist/components/spacer.d.ts.map +1 -0
  44. package/dist/components/spacer.js +21 -0
  45. package/dist/components/spacer.js.map +1 -0
  46. package/dist/components/text.d.ts +19 -0
  47. package/dist/components/text.d.ts.map +1 -0
  48. package/dist/components/text.js +77 -0
  49. package/dist/components/text.js.map +1 -0
  50. package/dist/components/truncated-text.d.ts +14 -0
  51. package/dist/components/truncated-text.d.ts.map +1 -0
  52. package/dist/components/truncated-text.js +44 -0
  53. package/dist/components/truncated-text.js.map +1 -0
  54. package/dist/editor-component.d.ts +51 -0
  55. package/dist/editor-component.d.ts.map +1 -0
  56. package/dist/editor-component.js +2 -0
  57. package/dist/editor-component.js.map +1 -0
  58. package/dist/fullscreen.d.ts +105 -0
  59. package/dist/fullscreen.d.ts.map +1 -0
  60. package/dist/fullscreen.js +597 -0
  61. package/dist/fullscreen.js.map +1 -0
  62. package/dist/fuzzy.d.ts +26 -0
  63. package/dist/fuzzy.d.ts.map +1 -0
  64. package/dist/fuzzy.js +114 -0
  65. package/dist/fuzzy.js.map +1 -0
  66. package/dist/index.d.ts +29 -0
  67. package/dist/index.d.ts.map +1 -0
  68. package/dist/index.js +27 -0
  69. package/dist/index.js.map +1 -0
  70. package/dist/keybindings.d.ts +239 -0
  71. package/dist/keybindings.d.ts.map +1 -0
  72. package/dist/keybindings.js +222 -0
  73. package/dist/keybindings.js.map +1 -0
  74. package/dist/keys.d.ts +184 -0
  75. package/dist/keys.d.ts.map +1 -0
  76. package/dist/keys.js +1163 -0
  77. package/dist/keys.js.map +1 -0
  78. package/dist/kill-ring.d.ts +26 -0
  79. package/dist/kill-ring.d.ts.map +1 -0
  80. package/dist/kill-ring.js +42 -0
  81. package/dist/kill-ring.js.map +1 -0
  82. package/dist/latex.d.ts +21 -0
  83. package/dist/latex.d.ts.map +1 -0
  84. package/dist/latex.js +797 -0
  85. package/dist/latex.js.map +1 -0
  86. package/dist/mouse.d.ts +24 -0
  87. package/dist/mouse.d.ts.map +1 -0
  88. package/dist/mouse.js +34 -0
  89. package/dist/mouse.js.map +1 -0
  90. package/dist/render-cache.d.ts +9 -0
  91. package/dist/render-cache.d.ts.map +1 -0
  92. package/dist/render-cache.js +23 -0
  93. package/dist/render-cache.js.map +1 -0
  94. package/dist/selection-metadata.d.ts +22 -0
  95. package/dist/selection-metadata.d.ts.map +1 -0
  96. package/dist/selection-metadata.js +141 -0
  97. package/dist/selection-metadata.js.map +1 -0
  98. package/dist/slash-command-context.d.ts +12 -0
  99. package/dist/slash-command-context.d.ts.map +1 -0
  100. package/dist/slash-command-context.js +32 -0
  101. package/dist/slash-command-context.js.map +1 -0
  102. package/dist/stdin-buffer.d.ts +50 -0
  103. package/dist/stdin-buffer.d.ts.map +1 -0
  104. package/dist/stdin-buffer.js +313 -0
  105. package/dist/stdin-buffer.js.map +1 -0
  106. package/dist/terminal-colors.d.ts +31 -0
  107. package/dist/terminal-colors.d.ts.map +1 -0
  108. package/dist/terminal-colors.js +161 -0
  109. package/dist/terminal-colors.js.map +1 -0
  110. package/dist/terminal-image.d.ts +85 -0
  111. package/dist/terminal-image.d.ts.map +1 -0
  112. package/dist/terminal-image.js +324 -0
  113. package/dist/terminal-image.js.map +1 -0
  114. package/dist/terminal.d.ts +113 -0
  115. package/dist/terminal.d.ts.map +1 -0
  116. package/dist/terminal.js +486 -0
  117. package/dist/terminal.js.map +1 -0
  118. package/dist/tui.d.ts +310 -0
  119. package/dist/tui.d.ts.map +1 -0
  120. package/dist/tui.js +1673 -0
  121. package/dist/tui.js.map +1 -0
  122. package/dist/undo-stack.d.ts +16 -0
  123. package/dist/undo-stack.d.ts.map +1 -0
  124. package/dist/undo-stack.js +26 -0
  125. package/dist/undo-stack.js.map +1 -0
  126. package/dist/utils.d.ts +98 -0
  127. package/dist/utils.d.ts.map +1 -0
  128. package/dist/utils.js +1233 -0
  129. package/dist/utils.js.map +1 -0
  130. package/package.json +52 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fullscreen.d.ts","sourceRoot":"","sources":["../src/fullscreen.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Fullscreen (alternate-screen) viewport: a scrollable window over the\n * transcript with a dock (editor/footer) pinned to the bottom rows. Frames\n * are a fixed grid painted with absolute addressing and diffed row-by-row;\n * scroll position is application state, not terminal scrollback.\n */\n\nimport type { TableCellSelectionRegion } from \"./selection-metadata.js\";\nimport { isImageLine } from \"./terminal-image.js\";\nimport { sliceByColumn, stripAnsi, urlAtColumn, visibleWidth } from \"./utils.js\";\n\nexport const FULLSCREEN_MIN_TRANSCRIPT_ROWS = 3;\n\nexport function clippedFullscreenDockHeight(dockLength: number, height: number): number {\n\tconst maxDock = Math.max(0, height - FULLSCREEN_MIN_TRANSCRIPT_ROWS);\n\treturn Math.min(dockLength, maxDock);\n}\n\n// Kitty images span multiple physical rows and cannot be clipped to a window.\nconst IMAGE_PLACEHOLDER = \"\\x1b[2m[image — view in inline mode]\\x1b[0m\";\n\nexport interface ScrollInfo {\n\tfollowing: boolean;\n\tlinesBelow: number;\n\tlinesAbove: number;\n}\n\nexport type SelectionScrollDirection = -1 | 1;\n\n// Transcript-anchored selection endpoint (line index + visible column), so\n// streaming appends and scrolling never shift what is selected.\ninterface SelectionPoint {\n\tline: number;\n\tcol: number;\n}\n\ninterface FrameSelectionRegion {\n\tline: number;\n\tcol: number;\n\twidth: number;\n}\n\ninterface ColumnSpan {\n\tfrom: number;\n\tto: number;\n}\n\ninterface FrameSelectionSnapshot {\n\tframe: string[];\n\tregions: FrameSelectionRegion[];\n\tvisibleStart: number;\n\tvisibleHeight: number;\n}\n\ninterface TableCellPosition {\n\trow: number;\n\tcolumn: number;\n}\n\ninterface ActiveTableSelection {\n\ttable: object;\n\tanchor: TableCellPosition;\n}\n\ninterface TableSelectionRange {\n\tfromRow: number;\n\ttoRow: number;\n\tfromColumn: number;\n\ttoColumn: number;\n}\n\ntype SelectionMode = \"transcript\" | \"table\" | \"frame\";\n\nexport class FullscreenViewport {\n\tprivate scrollTop = 0;\n\tprivate following = true;\n\tprivate prevFrame: string[] = [];\n\tprivate prevWidth = 0;\n\tprivate prevHeight = 0;\n\tprivate lastMaxScroll = 0;\n\tprivate lastWindowHeight = 0;\n\tprivate lastTranscript: string[] = [];\n\tprivate lastFrame: string[] = [];\n\tprivate lastFrameVisibleStart = 0;\n\tprivate lastFrameVisibleHeight = 0;\n\tprivate frameSelectionRegions: ReadonlyArray<FrameSelectionRegion> = [];\n\tprivate tableCellSelectionRegions: ReadonlyArray<TableCellSelectionRegion> = [];\n\tprivate activeFrameSelection: FrameSelectionSnapshot | null = null;\n\tprivate activeTableSelection: ActiveTableSelection | null = null;\n\tprivate selectionAnchor: SelectionPoint | null = null;\n\tprivate selectionHead: SelectionPoint | null = null;\n\tprivate selectionMode: SelectionMode | null = null;\n\n\t/**\n\t * Compose a frame of exactly `height` lines: scrolled transcript window on\n\t * top, dock pinned to the bottom. Following pins the window to the\n\t * transcript end; otherwise it stays frozen while content appends.\n\t */\n\tcomposeFrame(\n\t\ttranscript: string[],\n\t\tdock: string[],\n\t\theight: number,\n\t\ttableCellSelectionRegions: ReadonlyArray<TableCellSelectionRegion> = [],\n\t): string[] {\n\t\tlet dockLines = dock;\n\t\tconst dockHeight = clippedFullscreenDockHeight(dockLines.length, height);\n\t\tif (dockLines.length > dockHeight) {\n\t\t\tdockLines = dockLines.slice(dockLines.length - dockHeight);\n\t\t}\n\t\tconst windowHeight = height - dockLines.length;\n\t\tconst maxScroll = Math.max(0, transcript.length - windowHeight);\n\n\t\tif (this.following) {\n\t\t\tthis.scrollTop = maxScroll;\n\t\t} else {\n\t\t\tthis.scrollTop = Math.max(0, Math.min(this.scrollTop, maxScroll));\n\t\t}\n\t\tthis.lastMaxScroll = maxScroll;\n\t\tthis.lastWindowHeight = windowHeight;\n\t\tthis.lastTranscript = transcript;\n\t\tthis.tableCellSelectionRegions = tableCellSelectionRegions;\n\n\t\tconst window = transcript.slice(this.scrollTop, this.scrollTop + windowHeight);\n\t\tfor (let i = 0; i < window.length; i++) {\n\t\t\tif (isImageLine(window[i])) window[i] = IMAGE_PLACEHOLDER;\n\t\t}\n\t\tthis.highlightSelection(window);\n\t\twhile (window.length < windowHeight) {\n\t\t\twindow.push(\"\");\n\t\t}\n\t\treturn [...window, ...dockLines];\n\t}\n\n\tprivate orderedSelection(): { start: SelectionPoint; end: SelectionPoint } | null {\n\t\tconst a = this.selectionAnchor;\n\t\tconst b = this.selectionHead;\n\t\tif (!a || !b || (a.line === b.line && a.col === b.col)) return null;\n\t\tconst flipped = a.line > b.line || (a.line === b.line && a.col > b.col);\n\t\treturn flipped ? { start: b, end: a } : { start: a, end: b };\n\t}\n\n\tprivate selectionSpan(lineIndex: number, sel: { start: SelectionPoint; end: SelectionPoint }): ColumnSpan | null {\n\t\tif (lineIndex < sel.start.line || lineIndex > sel.end.line) return null;\n\t\treturn {\n\t\t\tfrom: lineIndex === sel.start.line ? sel.start.col : 0,\n\t\t\tto: lineIndex === sel.end.line ? sel.end.col : Number.MAX_SAFE_INTEGER,\n\t\t};\n\t}\n\n\tprivate highlightSelection(window: string[]): void {\n\t\tif (this.selectionMode !== \"transcript\" && this.selectionMode !== \"table\") return;\n\t\tconst sel = this.orderedSelection();\n\t\tif (!sel) return;\n\t\tfor (let i = 0; i < window.length; i++) {\n\t\t\tconst lineIndex = this.scrollTop + i;\n\t\t\tconst spans =\n\t\t\t\tthis.selectionMode === \"table\"\n\t\t\t\t\t? this.selectedTableSpans(lineIndex, sel)\n\t\t\t\t\t: [this.selectionSpan(lineIndex, sel)].filter((span): span is ColumnSpan => span !== null);\n\t\t\tfor (let spanIndex = spans.length - 1; spanIndex >= 0; spanIndex--) {\n\t\t\t\twindow[i] = this.highlightLine(window[i], spans[spanIndex]);\n\t\t\t}\n\t\t}\n\t}\n\n\t/** Begin a selection at a screen position; false when outside the transcript window. */\n\tbeginSelection(screenRow: number, screenCol: number): boolean {\n\t\tconst line = this.transcriptLineForScreenRow(screenRow, false);\n\t\tif (line === null) {\n\t\t\tthis.clearSelection();\n\t\t\treturn false;\n\t\t}\n\t\tconst point = { line, col: Math.max(0, screenCol) };\n\t\tthis.selectionAnchor = point;\n\t\tthis.selectionHead = { ...point };\n\t\tconst table = this.tableAtPoint(point);\n\t\tconst tableCell = table ? this.closestTableCell(table, point) : null;\n\t\tif (table && tableCell) {\n\t\t\tthis.activeTableSelection = { table, anchor: tableCell };\n\t\t\tthis.selectionMode = \"table\";\n\t\t} else {\n\t\t\tthis.activeTableSelection = null;\n\t\t\tthis.selectionMode = \"transcript\";\n\t\t}\n\t\treturn true;\n\t}\n\n\textendSelection(screenRow: number, screenCol: number): void {\n\t\tif (!this.selectionAnchor || (this.selectionMode !== \"transcript\" && this.selectionMode !== \"table\")) return;\n\t\tconst line = this.transcriptLineForScreenRow(screenRow, true);\n\t\tif (line === null) return;\n\t\tthis.selectionHead = { line, col: Math.max(0, screenCol) };\n\t}\n\n\tselectionAutoScrollDirection(screenRow: number): SelectionScrollDirection | null {\n\t\tif (\n\t\t\t!this.selectionAnchor ||\n\t\t\t!this.selectionHead ||\n\t\t\t(this.selectionMode !== \"transcript\" && this.selectionMode !== \"table\")\n\t\t)\n\t\t\treturn null;\n\t\tconst bounds = this.transcriptScreenBounds();\n\t\tif (!bounds) return null;\n\t\tif (this.selectionHead.line < this.selectionAnchor.line && screenRow <= bounds.firstRow && this.scrollTop > 0) {\n\t\t\treturn -1;\n\t\t}\n\t\tif (\n\t\t\tthis.selectionHead.line > this.selectionAnchor.line &&\n\t\t\tscreenRow >= bounds.lastRow &&\n\t\t\tthis.scrollTop < this.lastMaxScroll\n\t\t) {\n\t\t\treturn 1;\n\t\t}\n\t\treturn null;\n\t}\n\n\tscrollSelection(direction: SelectionScrollDirection, screenCol: number): boolean {\n\t\tif (!this.selectionAnchor || (this.selectionMode !== \"transcript\" && this.selectionMode !== \"table\"))\n\t\t\treturn false;\n\t\tconst previousScrollTop = this.scrollTop;\n\t\tthis.scrollBy(direction);\n\t\tif (this.scrollTop === previousScrollTop) return false;\n\t\tconst bounds = this.transcriptScreenBounds();\n\t\tif (!bounds) return false;\n\t\tthis.extendSelection(direction === -1 ? bounds.firstRow : bounds.lastRow, screenCol);\n\t\treturn true;\n\t}\n\n\t/** Finish the selection and return its plain text (null when empty). */\n\tendSelection(): string | null {\n\t\tif (this.selectionMode !== \"transcript\" && this.selectionMode !== \"table\") {\n\t\t\tthis.clearSelection();\n\t\t\treturn null;\n\t\t}\n\t\tconst sel = this.orderedSelection();\n\t\tconst text = sel\n\t\t\t? this.selectionMode === \"table\"\n\t\t\t\t? this.extractTableSelectionText(this.lastTranscript, sel)\n\t\t\t\t: this.extractSelectionText(this.lastTranscript, sel)\n\t\t\t: null;\n\t\tthis.clearSelection();\n\t\treturn text;\n\t}\n\n\textendActiveSelection(screenRow: number, screenCol: number): void {\n\t\tif (this.selectionMode === \"frame\") {\n\t\t\tthis.extendFrameSelection(screenRow, screenCol);\n\t\t} else if (this.selectionMode === \"transcript\" || this.selectionMode === \"table\") {\n\t\t\tthis.extendSelection(screenRow, screenCol);\n\t\t}\n\t}\n\n\tendActiveSelection(): string | null {\n\t\tif (this.selectionMode === \"frame\") {\n\t\t\treturn this.endFrameSelection();\n\t\t}\n\t\tif (this.selectionMode === \"transcript\" || this.selectionMode === \"table\") {\n\t\t\treturn this.endSelection();\n\t\t}\n\t\tthis.clearSelection();\n\t\treturn null;\n\t}\n\n\t/** Snapshot and highlight the final screen frame for overlay/dock selection. */\n\tapplyFrameSelection(frame: string[], height: number, selectableRegions: ReadonlyArray<FrameSelectionRegion>): void {\n\t\tthis.lastFrame = frame;\n\t\tthis.lastFrameVisibleHeight = Math.min(Math.max(0, height), frame.length);\n\t\tthis.lastFrameVisibleStart = Math.max(0, frame.length - this.lastFrameVisibleHeight);\n\t\tthis.frameSelectionRegions = selectableRegions;\n\t\tif (this.selectionMode !== \"frame\") return;\n\t\tconst sel = this.orderedSelection();\n\t\tif (!sel) return;\n\t\tfor (let lineIndex = sel.start.line; lineIndex <= sel.end.line; lineIndex++) {\n\t\t\tlet line = frame[lineIndex];\n\t\t\tif (line === undefined) continue;\n\t\t\tconst spans = this.selectedFrameSpans(lineIndex, sel);\n\t\t\tfor (let i = spans.length - 1; i >= 0; i--) {\n\t\t\t\tline = this.highlightLine(line, spans[i]);\n\t\t\t}\n\t\t\tframe[lineIndex] = line;\n\t\t}\n\t}\n\n\tbeginFrameSelection(screenRow: number, screenCol: number): boolean {\n\t\tconst point = this.framePoint(screenRow, screenCol);\n\t\tif (!point || !this.isFrameSelectable(point)) {\n\t\t\tthis.clearSelection();\n\t\t\treturn false;\n\t\t}\n\t\tthis.activeFrameSelection = {\n\t\t\tframe: [...this.lastFrame],\n\t\t\tregions: this.frameSelectionRegions.map((region) => ({ ...region })),\n\t\t\tvisibleStart: this.lastFrameVisibleStart,\n\t\t\tvisibleHeight: this.lastFrameVisibleHeight,\n\t\t};\n\t\tthis.selectionAnchor = point;\n\t\tthis.selectionHead = { ...point };\n\t\tthis.selectionMode = \"frame\";\n\t\treturn true;\n\t}\n\n\textendFrameSelection(screenRow: number, screenCol: number): void {\n\t\tif (!this.selectionAnchor || this.selectionMode !== \"frame\") return;\n\t\tconst point = this.framePoint(screenRow, screenCol, this.activeFrameSelection);\n\t\tif (!point) return;\n\t\tconst clamped = this.clampFrameSelectionPoint(point);\n\t\tif (!clamped) return;\n\t\tthis.selectionHead = clamped;\n\t}\n\n\tendFrameSelection(): string | null {\n\t\tif (this.selectionMode !== \"frame\") {\n\t\t\tthis.clearSelection();\n\t\t\treturn null;\n\t\t}\n\t\tconst sel = this.orderedSelection();\n\t\tconst active = this.activeFrameSelection;\n\t\tconst sourceLines = active?.frame ?? this.lastFrame;\n\t\tconst regions = active?.regions ?? this.frameSelectionRegions;\n\t\tthis.clearSelection();\n\t\tif (!sel) return null;\n\t\treturn this.extractFrameSelectionText(sourceLines, regions, sel);\n\t}\n\n\tprivate highlightLine(line: string, span: ColumnSpan): string {\n\t\tconst width = visibleWidth(line);\n\t\tconst from = Math.min(span.from, width);\n\t\tconst to = Math.min(span.to, width);\n\t\tif (to <= from) return line;\n\t\tconst before = sliceByColumn(line, 0, from);\n\t\tconst selected = stripAnsi(sliceByColumn(line, from, to - from));\n\t\tconst after = sliceByColumn(line, to, Math.max(0, width - to));\n\t\treturn `${before}\\x1b[0m\\x1b[7m${selected}\\x1b[27m${after}`;\n\t}\n\n\tprivate framePoint(\n\t\tscreenRow: number,\n\t\tscreenCol: number,\n\t\tsnapshot: FrameSelectionSnapshot | null = null,\n\t): SelectionPoint | null {\n\t\tconst visibleHeight = snapshot?.visibleHeight ?? this.lastFrameVisibleHeight;\n\t\tif (visibleHeight === 0) return null;\n\t\tconst visibleStart = snapshot?.visibleStart ?? this.lastFrameVisibleStart;\n\t\tconst frameLength = snapshot?.frame.length ?? this.lastFrame.length;\n\t\tconst row = Math.max(0, Math.min(screenRow, visibleHeight - 1));\n\t\tconst line = visibleStart + row;\n\t\tif (line < 0 || line >= frameLength) return null;\n\t\treturn { line, col: Math.max(0, screenCol) };\n\t}\n\n\tprivate transcriptScreenBounds(): {\n\t\tfirstRow: number;\n\t\tlastRow: number;\n\t\tvisibleStart: number;\n\t\tvisibleHeight: number;\n\t\ttranscriptStart: number;\n\t\ttranscriptEnd: number;\n\t} | null {\n\t\tif (this.lastWindowHeight <= 0) return null;\n\t\tconst visibleHeight = this.lastFrameVisibleHeight > 0 ? this.lastFrameVisibleHeight : this.lastWindowHeight;\n\t\tif (visibleHeight <= 0) return null;\n\t\tconst visibleStart = this.lastFrameVisibleHeight > 0 ? this.lastFrameVisibleStart : 0;\n\t\tconst visibleEnd = visibleStart + visibleHeight - 1;\n\t\tconst transcriptStart = Math.max(0, visibleStart);\n\t\tconst transcriptEnd = Math.min(this.lastWindowHeight - 1, visibleEnd);\n\t\tif (transcriptStart > transcriptEnd) return null;\n\t\treturn {\n\t\t\tfirstRow: transcriptStart - visibleStart,\n\t\t\tlastRow: transcriptEnd - visibleStart,\n\t\t\tvisibleStart,\n\t\t\tvisibleHeight,\n\t\t\ttranscriptStart,\n\t\t\ttranscriptEnd,\n\t\t};\n\t}\n\n\tprivate transcriptLineForScreenRow(screenRow: number, clamp: boolean): number | null {\n\t\tconst bounds = this.transcriptScreenBounds();\n\t\tif (!bounds) return null;\n\t\tif (!clamp && (screenRow < 0 || screenRow >= bounds.visibleHeight)) return null;\n\t\tconst row = clamp ? Math.max(0, Math.min(screenRow, bounds.visibleHeight - 1)) : screenRow;\n\t\tconst frameLine = bounds.visibleStart + row;\n\t\tif (!clamp && (frameLine < bounds.transcriptStart || frameLine > bounds.transcriptEnd)) return null;\n\t\treturn this.scrollTop + Math.max(bounds.transcriptStart, Math.min(frameLine, bounds.transcriptEnd));\n\t}\n\n\tprivate isFrameSelectable(point: SelectionPoint): boolean {\n\t\treturn this.frameSelectionRegions.some(\n\t\t\t(region) => region.line === point.line && point.col >= region.col && point.col < region.col + region.width,\n\t\t);\n\t}\n\n\tprivate tableRegions(table: object): TableCellSelectionRegion[] {\n\t\treturn this.tableCellSelectionRegions.filter((region) => region.table === table);\n\t}\n\n\tprivate tableAtPoint(point: SelectionPoint): object | null {\n\t\tconst tables = new Set(this.tableCellSelectionRegions.map((region) => region.table));\n\t\tfor (const table of tables) {\n\t\t\tconst region = this.tableRegions(table)[0];\n\t\t\tif (\n\t\t\t\tregion &&\n\t\t\t\tpoint.line >= region.tableTop &&\n\t\t\t\tpoint.line <= region.tableBottom &&\n\t\t\t\tpoint.col >= Math.max(0, region.tableLeft - 1) &&\n\t\t\t\tpoint.col <= region.tableRight\n\t\t\t) {\n\t\t\t\treturn table;\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\n\tprivate closestTableCell(table: object, point: SelectionPoint): TableCellPosition | null {\n\t\tlet closest: TableCellPosition | null = null;\n\t\tlet closestLineDistance = Number.POSITIVE_INFINITY;\n\t\tlet closestColumnDistance = Number.POSITIVE_INFINITY;\n\t\tfor (const region of this.tableRegions(table)) {\n\t\t\tconst lineDistance = Math.abs(point.line - region.line);\n\t\t\tconst end = region.col + region.width;\n\t\t\tconst columnDistance = point.col < region.col ? region.col - point.col : point.col > end ? point.col - end : 0;\n\t\t\tif (\n\t\t\t\tlineDistance < closestLineDistance ||\n\t\t\t\t(lineDistance === closestLineDistance && columnDistance < closestColumnDistance)\n\t\t\t) {\n\t\t\t\tclosest = { row: region.row, column: region.column };\n\t\t\t\tclosestLineDistance = lineDistance;\n\t\t\t\tclosestColumnDistance = columnDistance;\n\t\t\t}\n\t\t}\n\t\treturn closest;\n\t}\n\n\tprivate activeTableRange(): TableSelectionRange | null {\n\t\tconst active = this.activeTableSelection;\n\t\tconst head = this.selectionHead;\n\t\tif (!active || !head) return null;\n\t\tconst headCell = this.closestTableCell(active.table, head);\n\t\tif (!headCell) return null;\n\t\treturn {\n\t\t\tfromRow: Math.min(active.anchor.row, headCell.row),\n\t\t\ttoRow: Math.max(active.anchor.row, headCell.row),\n\t\t\tfromColumn: Math.min(active.anchor.column, headCell.column),\n\t\t\ttoColumn: Math.max(active.anchor.column, headCell.column),\n\t\t};\n\t}\n\n\tprivate selectedTableSpans(lineIndex: number, sel: { start: SelectionPoint; end: SelectionPoint }): ColumnSpan[] {\n\t\tconst active = this.activeTableSelection;\n\t\tconst range = this.activeTableRange();\n\t\tif (!active || !range) return [];\n\t\tconst singleCell = range.fromRow === range.toRow && range.fromColumn === range.toColumn;\n\t\tconst selectionSpan = singleCell ? this.selectionSpan(lineIndex, sel) : null;\n\t\tif (singleCell && !selectionSpan) return [];\n\n\t\tconst spans: ColumnSpan[] = [];\n\t\tfor (const region of this.tableCellSelectionRegions) {\n\t\t\tif (\n\t\t\t\tregion.line !== lineIndex ||\n\t\t\t\tregion.table !== active.table ||\n\t\t\t\tregion.row < range.fromRow ||\n\t\t\t\tregion.row > range.toRow ||\n\t\t\t\tregion.column < range.fromColumn ||\n\t\t\t\tregion.column > range.toColumn\n\t\t\t)\n\t\t\t\tcontinue;\n\t\t\tconst from = selectionSpan ? Math.max(selectionSpan.from, region.col) : region.col;\n\t\t\tconst to = selectionSpan ? Math.min(selectionSpan.to, region.col + region.width) : region.col + region.width;\n\t\t\tif (to > from) spans.push({ from, to });\n\t\t}\n\t\treturn spans.sort((a, b) => a.from - b.from);\n\t}\n\n\tprivate frameRegionsForLine(\n\t\tline: number,\n\t\tregions = this.activeFrameSelection?.regions ?? this.frameSelectionRegions,\n\t): FrameSelectionRegion[] {\n\t\treturn regions.filter((region) => region.line === line && region.width > 0).sort((a, b) => a.col - b.col);\n\t}\n\n\tprivate clampFrameSelectionPoint(point: SelectionPoint): SelectionPoint | null {\n\t\tconst regions = this.frameRegionsForLine(point.line);\n\t\tif (regions.length === 0) return null;\n\t\tlet closest = regions[0].col;\n\t\tlet distance = Number.POSITIVE_INFINITY;\n\t\tfor (const region of regions) {\n\t\t\tconst start = region.col;\n\t\t\tconst end = region.col + region.width;\n\t\t\tif (point.col >= start && point.col <= end) {\n\t\t\t\treturn { line: point.line, col: Math.max(start, Math.min(point.col, end)) };\n\t\t\t}\n\t\t\tfor (const col of [start, end]) {\n\t\t\t\tconst nextDistance = Math.abs(point.col - col);\n\t\t\t\tif (nextDistance < distance) {\n\t\t\t\t\tclosest = col;\n\t\t\t\t\tdistance = nextDistance;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn { line: point.line, col: closest };\n\t}\n\n\tprivate selectedFrameSpans(\n\t\tlineIndex: number,\n\t\tsel: { start: SelectionPoint; end: SelectionPoint },\n\t\tregions = this.activeFrameSelection?.regions ?? this.frameSelectionRegions,\n\t): ColumnSpan[] {\n\t\tconst span = this.selectionSpan(lineIndex, sel);\n\t\tif (!span) return [];\n\t\tconst spans: ColumnSpan[] = [];\n\t\tfor (const region of this.frameRegionsForLine(lineIndex, regions)) {\n\t\t\tconst from = Math.max(span.from, region.col);\n\t\t\tconst to = Math.min(span.to, region.col + region.width);\n\t\t\tif (to > from) spans.push({ from, to });\n\t\t}\n\t\treturn spans;\n\t}\n\n\tprivate extractFrameSelectionText(\n\t\tsourceLines: string[],\n\t\tregions: ReadonlyArray<FrameSelectionRegion>,\n\t\tsel: { start: SelectionPoint; end: SelectionPoint },\n\t): string | null {\n\t\tconst lines: string[] = [];\n\t\tfor (let lineIndex = sel.start.line; lineIndex <= sel.end.line; lineIndex++) {\n\t\t\tconst line = sourceLines[lineIndex] ?? \"\";\n\t\t\tconst spans = this.selectedFrameSpans(lineIndex, sel, regions);\n\t\t\tif (spans.length === 0) continue;\n\t\t\tconst parts: string[] = [];\n\t\t\tfor (const span of spans) {\n\t\t\t\tparts.push(stripAnsi(sliceByColumn(line, span.from, Math.max(0, span.to - span.from))));\n\t\t\t}\n\t\t\tlines.push(parts.join(\"\").trimEnd());\n\t\t}\n\t\tconst text = lines.join(\"\\n\");\n\t\treturn text.trim().length > 0 ? text : null;\n\t}\n\n\tprivate extractSelectionText(\n\t\tsourceLines: string[],\n\t\tsel: { start: SelectionPoint; end: SelectionPoint },\n\t): string | null {\n\t\tconst lines: string[] = [];\n\t\tfor (let lineIndex = sel.start.line; lineIndex <= sel.end.line; lineIndex++) {\n\t\t\tconst line = sourceLines[lineIndex] ?? \"\";\n\t\t\tconst span = this.selectionSpan(lineIndex, sel);\n\t\t\tif (!span) continue;\n\t\t\tconst width = visibleWidth(line);\n\t\t\tconst from = Math.min(span.from, width);\n\t\t\tconst to = Math.min(span.to, width);\n\t\t\tlines.push(stripAnsi(sliceByColumn(line, from, Math.max(0, to - from))).trimEnd());\n\t\t}\n\t\tconst text = lines.join(\"\\n\");\n\t\treturn text.trim().length > 0 ? text : null;\n\t}\n\n\tprivate compareSelectionPoints(a: SelectionPoint, b: SelectionPoint): number {\n\t\treturn a.line === b.line ? a.col - b.col : a.line - b.line;\n\t}\n\n\tprivate extractTableSelectionText(\n\t\tsourceLines: string[],\n\t\tsel: { start: SelectionPoint; end: SelectionPoint },\n\t): string | null {\n\t\tconst active = this.activeTableSelection;\n\t\tconst range = this.activeTableRange();\n\t\tif (!active || !range) return null;\n\n\t\tif (range.fromRow !== range.toRow || range.fromColumn !== range.toColumn) {\n\t\t\tconst contents = new Map<string, string>();\n\t\t\tfor (const region of this.tableRegions(active.table)) {\n\t\t\t\tcontents.set(`${region.row}:${region.column}`, region.content);\n\t\t\t}\n\t\t\tconst rows: string[] = [];\n\t\t\tfor (let row = range.fromRow; row <= range.toRow; row++) {\n\t\t\t\tconst cells: string[] = [];\n\t\t\t\tfor (let column = range.fromColumn; column <= range.toColumn; column++) {\n\t\t\t\t\tcells.push(contents.get(`${row}:${column}`) ?? \"\");\n\t\t\t\t}\n\t\t\t\trows.push(cells.join(\"\\t\"));\n\t\t\t}\n\t\t\tconst text = rows.join(\"\\n\");\n\t\t\treturn text.trim().length > 0 ? text : null;\n\t\t}\n\n\t\tconst cellRegions = this.tableRegions(active.table)\n\t\t\t.filter((region) => region.row === range.fromRow && region.column === range.fromColumn)\n\t\t\t.sort((a, b) => a.line - b.line || a.segment - b.segment);\n\t\tconst first = cellRegions[0];\n\t\tconst last = cellRegions.at(-1);\n\t\tif (first && last) {\n\t\t\tconst cellStart = { line: first.line, col: first.col };\n\t\t\tconst cellEnd = { line: last.line, col: last.col + last.width };\n\t\t\tif (\n\t\t\t\tthis.compareSelectionPoints(sel.start, cellStart) <= 0 &&\n\t\t\t\tthis.compareSelectionPoints(sel.end, cellEnd) >= 0\n\t\t\t) {\n\t\t\t\treturn first.content.trim().length > 0 ? first.content : null;\n\t\t\t}\n\t\t}\n\n\t\tconst lines: string[] = [];\n\t\tfor (let lineIndex = sel.start.line; lineIndex <= sel.end.line; lineIndex++) {\n\t\t\tconst line = sourceLines[lineIndex] ?? \"\";\n\t\t\tconst spans = this.selectedTableSpans(lineIndex, sel);\n\t\t\tif (spans.length === 0) continue;\n\t\t\tconst parts = spans.map((span) => stripAnsi(sliceByColumn(line, span.from, Math.max(0, span.to - span.from))));\n\t\t\tlines.push(parts.join(\"\").trimEnd());\n\t\t}\n\t\tconst text = lines.join(\"\\n\");\n\t\treturn text.trim().length > 0 ? text : null;\n\t}\n\n\tclearSelection(): void {\n\t\tthis.selectionAnchor = null;\n\t\tthis.selectionHead = null;\n\t\tthis.selectionMode = null;\n\t\tthis.activeFrameSelection = null;\n\t\tthis.activeTableSelection = null;\n\t}\n\n\thasSelection(): boolean {\n\t\treturn this.orderedSelection() !== null;\n\t}\n\n\t/**\n\t * OSC 8 hyperlink URL at a screen position in the last painted frame, or\n\t * null when the position is not over a hyperlink. Covers the transcript\n\t * window, the dock, and composited overlays.\n\t */\n\thyperlinkAt(screenRow: number, screenCol: number): string | null {\n\t\tif (screenRow < 0 || screenCol < 0 || this.lastFrameVisibleHeight === 0) return null;\n\t\tif (screenRow >= this.lastFrameVisibleHeight) return null;\n\t\tconst line = this.lastFrame[this.lastFrameVisibleStart + screenRow];\n\t\tif (line === undefined || isImageLine(line)) return null;\n\t\treturn urlAtColumn(line, screenCol);\n\t}\n\n\t/** Row-diff a composed frame against the previous one with absolute addressing. */\n\tpaint(\n\t\twrite: (data: string) => void,\n\t\tframe: string[],\n\t\twidth: number,\n\t\theight: number,\n\t\tcursorPos: { row: number; col: number } | null,\n\t): void {\n\t\tif (frame.length > height) {\n\t\t\tframe = frame.slice(frame.length - height);\n\t\t}\n\n\t\tlet buffer = \"\\x1b[?2026h\";\n\t\tif (width !== this.prevWidth || height !== this.prevHeight || this.prevFrame.length === 0) {\n\t\t\tbuffer += \"\\x1b[2J\\x1b[H\";\n\t\t\tthis.prevFrame = [];\n\t\t}\n\t\tfor (let row = 0; row < height; row++) {\n\t\t\tconst line = frame[row] ?? \"\";\n\t\t\tif (this.prevFrame[row] === line) continue;\n\t\t\tbuffer += `\\x1b[${row + 1};1H\\x1b[2K`;\n\t\t\t// an overwide line would wrap and shear the grid; clamp instead of crash\n\t\t\tbuffer += visibleWidth(line) > width ? sliceByColumn(line, 0, width, true) : line;\n\t\t}\n\t\tif (cursorPos) {\n\t\t\tbuffer += `\\x1b[${Math.min(cursorPos.row, height - 1) + 1};${cursorPos.col + 1}H`;\n\t\t}\n\t\tbuffer += \"\\x1b[?2026l\";\n\t\twrite(buffer);\n\n\t\tthis.prevFrame = frame;\n\t\tthis.prevWidth = width;\n\t\tthis.prevHeight = height;\n\t}\n\n\t/** Force the next paint to clear and repaint the whole screen. */\n\treset(): void {\n\t\tthis.prevFrame = [];\n\t}\n\n\t/** Scrolling up pauses following; reaching the bottom resumes it. */\n\tscrollBy(delta: number): void {\n\t\tconst base = this.following ? this.lastMaxScroll : this.scrollTop;\n\t\tthis.scrollTop = Math.max(0, Math.min(base + delta, this.lastMaxScroll));\n\t\tthis.following = this.scrollTop >= this.lastMaxScroll;\n\t}\n\n\tscrollToTop(): void {\n\t\tthis.scrollTop = 0;\n\t\tthis.following = this.lastMaxScroll === 0;\n\t}\n\n\tscrollToBottom(): void {\n\t\tthis.scrollTop = this.lastMaxScroll;\n\t\tthis.following = true;\n\t}\n\n\tpageSize(): number {\n\t\treturn Math.max(1, this.lastWindowHeight - 1);\n\t}\n\n\twindowHeight(): number {\n\t\treturn this.lastWindowHeight;\n\t}\n\n\tisFollowing(): boolean {\n\t\treturn this.following;\n\t}\n\n\tscrollInfo(): ScrollInfo {\n\t\treturn {\n\t\t\tfollowing: this.following,\n\t\t\tlinesBelow: Math.max(0, this.lastMaxScroll - this.scrollTop),\n\t\t\tlinesAbove: this.scrollTop,\n\t\t};\n\t}\n}\n"]}
@@ -0,0 +1,597 @@
1
+ /**
2
+ * Fullscreen (alternate-screen) viewport: a scrollable window over the
3
+ * transcript with a dock (editor/footer) pinned to the bottom rows. Frames
4
+ * are a fixed grid painted with absolute addressing and diffed row-by-row;
5
+ * scroll position is application state, not terminal scrollback.
6
+ */
7
+ import { isImageLine } from "./terminal-image.js";
8
+ import { sliceByColumn, stripAnsi, urlAtColumn, visibleWidth } from "./utils.js";
9
+ export const FULLSCREEN_MIN_TRANSCRIPT_ROWS = 3;
10
+ export function clippedFullscreenDockHeight(dockLength, height) {
11
+ const maxDock = Math.max(0, height - FULLSCREEN_MIN_TRANSCRIPT_ROWS);
12
+ return Math.min(dockLength, maxDock);
13
+ }
14
+ // Kitty images span multiple physical rows and cannot be clipped to a window.
15
+ const IMAGE_PLACEHOLDER = "\x1b[2m[image — view in inline mode]\x1b[0m";
16
+ export class FullscreenViewport {
17
+ scrollTop = 0;
18
+ following = true;
19
+ prevFrame = [];
20
+ prevWidth = 0;
21
+ prevHeight = 0;
22
+ lastMaxScroll = 0;
23
+ lastWindowHeight = 0;
24
+ lastTranscript = [];
25
+ lastFrame = [];
26
+ lastFrameVisibleStart = 0;
27
+ lastFrameVisibleHeight = 0;
28
+ frameSelectionRegions = [];
29
+ tableCellSelectionRegions = [];
30
+ activeFrameSelection = null;
31
+ activeTableSelection = null;
32
+ selectionAnchor = null;
33
+ selectionHead = null;
34
+ selectionMode = null;
35
+ /**
36
+ * Compose a frame of exactly `height` lines: scrolled transcript window on
37
+ * top, dock pinned to the bottom. Following pins the window to the
38
+ * transcript end; otherwise it stays frozen while content appends.
39
+ */
40
+ composeFrame(transcript, dock, height, tableCellSelectionRegions = []) {
41
+ let dockLines = dock;
42
+ const dockHeight = clippedFullscreenDockHeight(dockLines.length, height);
43
+ if (dockLines.length > dockHeight) {
44
+ dockLines = dockLines.slice(dockLines.length - dockHeight);
45
+ }
46
+ const windowHeight = height - dockLines.length;
47
+ const maxScroll = Math.max(0, transcript.length - windowHeight);
48
+ if (this.following) {
49
+ this.scrollTop = maxScroll;
50
+ }
51
+ else {
52
+ this.scrollTop = Math.max(0, Math.min(this.scrollTop, maxScroll));
53
+ }
54
+ this.lastMaxScroll = maxScroll;
55
+ this.lastWindowHeight = windowHeight;
56
+ this.lastTranscript = transcript;
57
+ this.tableCellSelectionRegions = tableCellSelectionRegions;
58
+ const window = transcript.slice(this.scrollTop, this.scrollTop + windowHeight);
59
+ for (let i = 0; i < window.length; i++) {
60
+ if (isImageLine(window[i]))
61
+ window[i] = IMAGE_PLACEHOLDER;
62
+ }
63
+ this.highlightSelection(window);
64
+ while (window.length < windowHeight) {
65
+ window.push("");
66
+ }
67
+ return [...window, ...dockLines];
68
+ }
69
+ orderedSelection() {
70
+ const a = this.selectionAnchor;
71
+ const b = this.selectionHead;
72
+ if (!a || !b || (a.line === b.line && a.col === b.col))
73
+ return null;
74
+ const flipped = a.line > b.line || (a.line === b.line && a.col > b.col);
75
+ return flipped ? { start: b, end: a } : { start: a, end: b };
76
+ }
77
+ selectionSpan(lineIndex, sel) {
78
+ if (lineIndex < sel.start.line || lineIndex > sel.end.line)
79
+ return null;
80
+ return {
81
+ from: lineIndex === sel.start.line ? sel.start.col : 0,
82
+ to: lineIndex === sel.end.line ? sel.end.col : Number.MAX_SAFE_INTEGER,
83
+ };
84
+ }
85
+ highlightSelection(window) {
86
+ if (this.selectionMode !== "transcript" && this.selectionMode !== "table")
87
+ return;
88
+ const sel = this.orderedSelection();
89
+ if (!sel)
90
+ return;
91
+ for (let i = 0; i < window.length; i++) {
92
+ const lineIndex = this.scrollTop + i;
93
+ const spans = this.selectionMode === "table"
94
+ ? this.selectedTableSpans(lineIndex, sel)
95
+ : [this.selectionSpan(lineIndex, sel)].filter((span) => span !== null);
96
+ for (let spanIndex = spans.length - 1; spanIndex >= 0; spanIndex--) {
97
+ window[i] = this.highlightLine(window[i], spans[spanIndex]);
98
+ }
99
+ }
100
+ }
101
+ /** Begin a selection at a screen position; false when outside the transcript window. */
102
+ beginSelection(screenRow, screenCol) {
103
+ const line = this.transcriptLineForScreenRow(screenRow, false);
104
+ if (line === null) {
105
+ this.clearSelection();
106
+ return false;
107
+ }
108
+ const point = { line, col: Math.max(0, screenCol) };
109
+ this.selectionAnchor = point;
110
+ this.selectionHead = { ...point };
111
+ const table = this.tableAtPoint(point);
112
+ const tableCell = table ? this.closestTableCell(table, point) : null;
113
+ if (table && tableCell) {
114
+ this.activeTableSelection = { table, anchor: tableCell };
115
+ this.selectionMode = "table";
116
+ }
117
+ else {
118
+ this.activeTableSelection = null;
119
+ this.selectionMode = "transcript";
120
+ }
121
+ return true;
122
+ }
123
+ extendSelection(screenRow, screenCol) {
124
+ if (!this.selectionAnchor || (this.selectionMode !== "transcript" && this.selectionMode !== "table"))
125
+ return;
126
+ const line = this.transcriptLineForScreenRow(screenRow, true);
127
+ if (line === null)
128
+ return;
129
+ this.selectionHead = { line, col: Math.max(0, screenCol) };
130
+ }
131
+ selectionAutoScrollDirection(screenRow) {
132
+ if (!this.selectionAnchor ||
133
+ !this.selectionHead ||
134
+ (this.selectionMode !== "transcript" && this.selectionMode !== "table"))
135
+ return null;
136
+ const bounds = this.transcriptScreenBounds();
137
+ if (!bounds)
138
+ return null;
139
+ if (this.selectionHead.line < this.selectionAnchor.line && screenRow <= bounds.firstRow && this.scrollTop > 0) {
140
+ return -1;
141
+ }
142
+ if (this.selectionHead.line > this.selectionAnchor.line &&
143
+ screenRow >= bounds.lastRow &&
144
+ this.scrollTop < this.lastMaxScroll) {
145
+ return 1;
146
+ }
147
+ return null;
148
+ }
149
+ scrollSelection(direction, screenCol) {
150
+ if (!this.selectionAnchor || (this.selectionMode !== "transcript" && this.selectionMode !== "table"))
151
+ return false;
152
+ const previousScrollTop = this.scrollTop;
153
+ this.scrollBy(direction);
154
+ if (this.scrollTop === previousScrollTop)
155
+ return false;
156
+ const bounds = this.transcriptScreenBounds();
157
+ if (!bounds)
158
+ return false;
159
+ this.extendSelection(direction === -1 ? bounds.firstRow : bounds.lastRow, screenCol);
160
+ return true;
161
+ }
162
+ /** Finish the selection and return its plain text (null when empty). */
163
+ endSelection() {
164
+ if (this.selectionMode !== "transcript" && this.selectionMode !== "table") {
165
+ this.clearSelection();
166
+ return null;
167
+ }
168
+ const sel = this.orderedSelection();
169
+ const text = sel
170
+ ? this.selectionMode === "table"
171
+ ? this.extractTableSelectionText(this.lastTranscript, sel)
172
+ : this.extractSelectionText(this.lastTranscript, sel)
173
+ : null;
174
+ this.clearSelection();
175
+ return text;
176
+ }
177
+ extendActiveSelection(screenRow, screenCol) {
178
+ if (this.selectionMode === "frame") {
179
+ this.extendFrameSelection(screenRow, screenCol);
180
+ }
181
+ else if (this.selectionMode === "transcript" || this.selectionMode === "table") {
182
+ this.extendSelection(screenRow, screenCol);
183
+ }
184
+ }
185
+ endActiveSelection() {
186
+ if (this.selectionMode === "frame") {
187
+ return this.endFrameSelection();
188
+ }
189
+ if (this.selectionMode === "transcript" || this.selectionMode === "table") {
190
+ return this.endSelection();
191
+ }
192
+ this.clearSelection();
193
+ return null;
194
+ }
195
+ /** Snapshot and highlight the final screen frame for overlay/dock selection. */
196
+ applyFrameSelection(frame, height, selectableRegions) {
197
+ this.lastFrame = frame;
198
+ this.lastFrameVisibleHeight = Math.min(Math.max(0, height), frame.length);
199
+ this.lastFrameVisibleStart = Math.max(0, frame.length - this.lastFrameVisibleHeight);
200
+ this.frameSelectionRegions = selectableRegions;
201
+ if (this.selectionMode !== "frame")
202
+ return;
203
+ const sel = this.orderedSelection();
204
+ if (!sel)
205
+ return;
206
+ for (let lineIndex = sel.start.line; lineIndex <= sel.end.line; lineIndex++) {
207
+ let line = frame[lineIndex];
208
+ if (line === undefined)
209
+ continue;
210
+ const spans = this.selectedFrameSpans(lineIndex, sel);
211
+ for (let i = spans.length - 1; i >= 0; i--) {
212
+ line = this.highlightLine(line, spans[i]);
213
+ }
214
+ frame[lineIndex] = line;
215
+ }
216
+ }
217
+ beginFrameSelection(screenRow, screenCol) {
218
+ const point = this.framePoint(screenRow, screenCol);
219
+ if (!point || !this.isFrameSelectable(point)) {
220
+ this.clearSelection();
221
+ return false;
222
+ }
223
+ this.activeFrameSelection = {
224
+ frame: [...this.lastFrame],
225
+ regions: this.frameSelectionRegions.map((region) => ({ ...region })),
226
+ visibleStart: this.lastFrameVisibleStart,
227
+ visibleHeight: this.lastFrameVisibleHeight,
228
+ };
229
+ this.selectionAnchor = point;
230
+ this.selectionHead = { ...point };
231
+ this.selectionMode = "frame";
232
+ return true;
233
+ }
234
+ extendFrameSelection(screenRow, screenCol) {
235
+ if (!this.selectionAnchor || this.selectionMode !== "frame")
236
+ return;
237
+ const point = this.framePoint(screenRow, screenCol, this.activeFrameSelection);
238
+ if (!point)
239
+ return;
240
+ const clamped = this.clampFrameSelectionPoint(point);
241
+ if (!clamped)
242
+ return;
243
+ this.selectionHead = clamped;
244
+ }
245
+ endFrameSelection() {
246
+ if (this.selectionMode !== "frame") {
247
+ this.clearSelection();
248
+ return null;
249
+ }
250
+ const sel = this.orderedSelection();
251
+ const active = this.activeFrameSelection;
252
+ const sourceLines = active?.frame ?? this.lastFrame;
253
+ const regions = active?.regions ?? this.frameSelectionRegions;
254
+ this.clearSelection();
255
+ if (!sel)
256
+ return null;
257
+ return this.extractFrameSelectionText(sourceLines, regions, sel);
258
+ }
259
+ highlightLine(line, span) {
260
+ const width = visibleWidth(line);
261
+ const from = Math.min(span.from, width);
262
+ const to = Math.min(span.to, width);
263
+ if (to <= from)
264
+ return line;
265
+ const before = sliceByColumn(line, 0, from);
266
+ const selected = stripAnsi(sliceByColumn(line, from, to - from));
267
+ const after = sliceByColumn(line, to, Math.max(0, width - to));
268
+ return `${before}\x1b[0m\x1b[7m${selected}\x1b[27m${after}`;
269
+ }
270
+ framePoint(screenRow, screenCol, snapshot = null) {
271
+ const visibleHeight = snapshot?.visibleHeight ?? this.lastFrameVisibleHeight;
272
+ if (visibleHeight === 0)
273
+ return null;
274
+ const visibleStart = snapshot?.visibleStart ?? this.lastFrameVisibleStart;
275
+ const frameLength = snapshot?.frame.length ?? this.lastFrame.length;
276
+ const row = Math.max(0, Math.min(screenRow, visibleHeight - 1));
277
+ const line = visibleStart + row;
278
+ if (line < 0 || line >= frameLength)
279
+ return null;
280
+ return { line, col: Math.max(0, screenCol) };
281
+ }
282
+ transcriptScreenBounds() {
283
+ if (this.lastWindowHeight <= 0)
284
+ return null;
285
+ const visibleHeight = this.lastFrameVisibleHeight > 0 ? this.lastFrameVisibleHeight : this.lastWindowHeight;
286
+ if (visibleHeight <= 0)
287
+ return null;
288
+ const visibleStart = this.lastFrameVisibleHeight > 0 ? this.lastFrameVisibleStart : 0;
289
+ const visibleEnd = visibleStart + visibleHeight - 1;
290
+ const transcriptStart = Math.max(0, visibleStart);
291
+ const transcriptEnd = Math.min(this.lastWindowHeight - 1, visibleEnd);
292
+ if (transcriptStart > transcriptEnd)
293
+ return null;
294
+ return {
295
+ firstRow: transcriptStart - visibleStart,
296
+ lastRow: transcriptEnd - visibleStart,
297
+ visibleStart,
298
+ visibleHeight,
299
+ transcriptStart,
300
+ transcriptEnd,
301
+ };
302
+ }
303
+ transcriptLineForScreenRow(screenRow, clamp) {
304
+ const bounds = this.transcriptScreenBounds();
305
+ if (!bounds)
306
+ return null;
307
+ if (!clamp && (screenRow < 0 || screenRow >= bounds.visibleHeight))
308
+ return null;
309
+ const row = clamp ? Math.max(0, Math.min(screenRow, bounds.visibleHeight - 1)) : screenRow;
310
+ const frameLine = bounds.visibleStart + row;
311
+ if (!clamp && (frameLine < bounds.transcriptStart || frameLine > bounds.transcriptEnd))
312
+ return null;
313
+ return this.scrollTop + Math.max(bounds.transcriptStart, Math.min(frameLine, bounds.transcriptEnd));
314
+ }
315
+ isFrameSelectable(point) {
316
+ return this.frameSelectionRegions.some((region) => region.line === point.line && point.col >= region.col && point.col < region.col + region.width);
317
+ }
318
+ tableRegions(table) {
319
+ return this.tableCellSelectionRegions.filter((region) => region.table === table);
320
+ }
321
+ tableAtPoint(point) {
322
+ const tables = new Set(this.tableCellSelectionRegions.map((region) => region.table));
323
+ for (const table of tables) {
324
+ const region = this.tableRegions(table)[0];
325
+ if (region &&
326
+ point.line >= region.tableTop &&
327
+ point.line <= region.tableBottom &&
328
+ point.col >= Math.max(0, region.tableLeft - 1) &&
329
+ point.col <= region.tableRight) {
330
+ return table;
331
+ }
332
+ }
333
+ return null;
334
+ }
335
+ closestTableCell(table, point) {
336
+ let closest = null;
337
+ let closestLineDistance = Number.POSITIVE_INFINITY;
338
+ let closestColumnDistance = Number.POSITIVE_INFINITY;
339
+ for (const region of this.tableRegions(table)) {
340
+ const lineDistance = Math.abs(point.line - region.line);
341
+ const end = region.col + region.width;
342
+ const columnDistance = point.col < region.col ? region.col - point.col : point.col > end ? point.col - end : 0;
343
+ if (lineDistance < closestLineDistance ||
344
+ (lineDistance === closestLineDistance && columnDistance < closestColumnDistance)) {
345
+ closest = { row: region.row, column: region.column };
346
+ closestLineDistance = lineDistance;
347
+ closestColumnDistance = columnDistance;
348
+ }
349
+ }
350
+ return closest;
351
+ }
352
+ activeTableRange() {
353
+ const active = this.activeTableSelection;
354
+ const head = this.selectionHead;
355
+ if (!active || !head)
356
+ return null;
357
+ const headCell = this.closestTableCell(active.table, head);
358
+ if (!headCell)
359
+ return null;
360
+ return {
361
+ fromRow: Math.min(active.anchor.row, headCell.row),
362
+ toRow: Math.max(active.anchor.row, headCell.row),
363
+ fromColumn: Math.min(active.anchor.column, headCell.column),
364
+ toColumn: Math.max(active.anchor.column, headCell.column),
365
+ };
366
+ }
367
+ selectedTableSpans(lineIndex, sel) {
368
+ const active = this.activeTableSelection;
369
+ const range = this.activeTableRange();
370
+ if (!active || !range)
371
+ return [];
372
+ const singleCell = range.fromRow === range.toRow && range.fromColumn === range.toColumn;
373
+ const selectionSpan = singleCell ? this.selectionSpan(lineIndex, sel) : null;
374
+ if (singleCell && !selectionSpan)
375
+ return [];
376
+ const spans = [];
377
+ for (const region of this.tableCellSelectionRegions) {
378
+ if (region.line !== lineIndex ||
379
+ region.table !== active.table ||
380
+ region.row < range.fromRow ||
381
+ region.row > range.toRow ||
382
+ region.column < range.fromColumn ||
383
+ region.column > range.toColumn)
384
+ continue;
385
+ const from = selectionSpan ? Math.max(selectionSpan.from, region.col) : region.col;
386
+ const to = selectionSpan ? Math.min(selectionSpan.to, region.col + region.width) : region.col + region.width;
387
+ if (to > from)
388
+ spans.push({ from, to });
389
+ }
390
+ return spans.sort((a, b) => a.from - b.from);
391
+ }
392
+ frameRegionsForLine(line, regions = this.activeFrameSelection?.regions ?? this.frameSelectionRegions) {
393
+ return regions.filter((region) => region.line === line && region.width > 0).sort((a, b) => a.col - b.col);
394
+ }
395
+ clampFrameSelectionPoint(point) {
396
+ const regions = this.frameRegionsForLine(point.line);
397
+ if (regions.length === 0)
398
+ return null;
399
+ let closest = regions[0].col;
400
+ let distance = Number.POSITIVE_INFINITY;
401
+ for (const region of regions) {
402
+ const start = region.col;
403
+ const end = region.col + region.width;
404
+ if (point.col >= start && point.col <= end) {
405
+ return { line: point.line, col: Math.max(start, Math.min(point.col, end)) };
406
+ }
407
+ for (const col of [start, end]) {
408
+ const nextDistance = Math.abs(point.col - col);
409
+ if (nextDistance < distance) {
410
+ closest = col;
411
+ distance = nextDistance;
412
+ }
413
+ }
414
+ }
415
+ return { line: point.line, col: closest };
416
+ }
417
+ selectedFrameSpans(lineIndex, sel, regions = this.activeFrameSelection?.regions ?? this.frameSelectionRegions) {
418
+ const span = this.selectionSpan(lineIndex, sel);
419
+ if (!span)
420
+ return [];
421
+ const spans = [];
422
+ for (const region of this.frameRegionsForLine(lineIndex, regions)) {
423
+ const from = Math.max(span.from, region.col);
424
+ const to = Math.min(span.to, region.col + region.width);
425
+ if (to > from)
426
+ spans.push({ from, to });
427
+ }
428
+ return spans;
429
+ }
430
+ extractFrameSelectionText(sourceLines, regions, sel) {
431
+ const lines = [];
432
+ for (let lineIndex = sel.start.line; lineIndex <= sel.end.line; lineIndex++) {
433
+ const line = sourceLines[lineIndex] ?? "";
434
+ const spans = this.selectedFrameSpans(lineIndex, sel, regions);
435
+ if (spans.length === 0)
436
+ continue;
437
+ const parts = [];
438
+ for (const span of spans) {
439
+ parts.push(stripAnsi(sliceByColumn(line, span.from, Math.max(0, span.to - span.from))));
440
+ }
441
+ lines.push(parts.join("").trimEnd());
442
+ }
443
+ const text = lines.join("\n");
444
+ return text.trim().length > 0 ? text : null;
445
+ }
446
+ extractSelectionText(sourceLines, sel) {
447
+ const lines = [];
448
+ for (let lineIndex = sel.start.line; lineIndex <= sel.end.line; lineIndex++) {
449
+ const line = sourceLines[lineIndex] ?? "";
450
+ const span = this.selectionSpan(lineIndex, sel);
451
+ if (!span)
452
+ continue;
453
+ const width = visibleWidth(line);
454
+ const from = Math.min(span.from, width);
455
+ const to = Math.min(span.to, width);
456
+ lines.push(stripAnsi(sliceByColumn(line, from, Math.max(0, to - from))).trimEnd());
457
+ }
458
+ const text = lines.join("\n");
459
+ return text.trim().length > 0 ? text : null;
460
+ }
461
+ compareSelectionPoints(a, b) {
462
+ return a.line === b.line ? a.col - b.col : a.line - b.line;
463
+ }
464
+ extractTableSelectionText(sourceLines, sel) {
465
+ const active = this.activeTableSelection;
466
+ const range = this.activeTableRange();
467
+ if (!active || !range)
468
+ return null;
469
+ if (range.fromRow !== range.toRow || range.fromColumn !== range.toColumn) {
470
+ const contents = new Map();
471
+ for (const region of this.tableRegions(active.table)) {
472
+ contents.set(`${region.row}:${region.column}`, region.content);
473
+ }
474
+ const rows = [];
475
+ for (let row = range.fromRow; row <= range.toRow; row++) {
476
+ const cells = [];
477
+ for (let column = range.fromColumn; column <= range.toColumn; column++) {
478
+ cells.push(contents.get(`${row}:${column}`) ?? "");
479
+ }
480
+ rows.push(cells.join("\t"));
481
+ }
482
+ const text = rows.join("\n");
483
+ return text.trim().length > 0 ? text : null;
484
+ }
485
+ const cellRegions = this.tableRegions(active.table)
486
+ .filter((region) => region.row === range.fromRow && region.column === range.fromColumn)
487
+ .sort((a, b) => a.line - b.line || a.segment - b.segment);
488
+ const first = cellRegions[0];
489
+ const last = cellRegions.at(-1);
490
+ if (first && last) {
491
+ const cellStart = { line: first.line, col: first.col };
492
+ const cellEnd = { line: last.line, col: last.col + last.width };
493
+ if (this.compareSelectionPoints(sel.start, cellStart) <= 0 &&
494
+ this.compareSelectionPoints(sel.end, cellEnd) >= 0) {
495
+ return first.content.trim().length > 0 ? first.content : null;
496
+ }
497
+ }
498
+ const lines = [];
499
+ for (let lineIndex = sel.start.line; lineIndex <= sel.end.line; lineIndex++) {
500
+ const line = sourceLines[lineIndex] ?? "";
501
+ const spans = this.selectedTableSpans(lineIndex, sel);
502
+ if (spans.length === 0)
503
+ continue;
504
+ const parts = spans.map((span) => stripAnsi(sliceByColumn(line, span.from, Math.max(0, span.to - span.from))));
505
+ lines.push(parts.join("").trimEnd());
506
+ }
507
+ const text = lines.join("\n");
508
+ return text.trim().length > 0 ? text : null;
509
+ }
510
+ clearSelection() {
511
+ this.selectionAnchor = null;
512
+ this.selectionHead = null;
513
+ this.selectionMode = null;
514
+ this.activeFrameSelection = null;
515
+ this.activeTableSelection = null;
516
+ }
517
+ hasSelection() {
518
+ return this.orderedSelection() !== null;
519
+ }
520
+ /**
521
+ * OSC 8 hyperlink URL at a screen position in the last painted frame, or
522
+ * null when the position is not over a hyperlink. Covers the transcript
523
+ * window, the dock, and composited overlays.
524
+ */
525
+ hyperlinkAt(screenRow, screenCol) {
526
+ if (screenRow < 0 || screenCol < 0 || this.lastFrameVisibleHeight === 0)
527
+ return null;
528
+ if (screenRow >= this.lastFrameVisibleHeight)
529
+ return null;
530
+ const line = this.lastFrame[this.lastFrameVisibleStart + screenRow];
531
+ if (line === undefined || isImageLine(line))
532
+ return null;
533
+ return urlAtColumn(line, screenCol);
534
+ }
535
+ /** Row-diff a composed frame against the previous one with absolute addressing. */
536
+ paint(write, frame, width, height, cursorPos) {
537
+ if (frame.length > height) {
538
+ frame = frame.slice(frame.length - height);
539
+ }
540
+ let buffer = "\x1b[?2026h";
541
+ if (width !== this.prevWidth || height !== this.prevHeight || this.prevFrame.length === 0) {
542
+ buffer += "\x1b[2J\x1b[H";
543
+ this.prevFrame = [];
544
+ }
545
+ for (let row = 0; row < height; row++) {
546
+ const line = frame[row] ?? "";
547
+ if (this.prevFrame[row] === line)
548
+ continue;
549
+ buffer += `\x1b[${row + 1};1H\x1b[2K`;
550
+ // an overwide line would wrap and shear the grid; clamp instead of crash
551
+ buffer += visibleWidth(line) > width ? sliceByColumn(line, 0, width, true) : line;
552
+ }
553
+ if (cursorPos) {
554
+ buffer += `\x1b[${Math.min(cursorPos.row, height - 1) + 1};${cursorPos.col + 1}H`;
555
+ }
556
+ buffer += "\x1b[?2026l";
557
+ write(buffer);
558
+ this.prevFrame = frame;
559
+ this.prevWidth = width;
560
+ this.prevHeight = height;
561
+ }
562
+ /** Force the next paint to clear and repaint the whole screen. */
563
+ reset() {
564
+ this.prevFrame = [];
565
+ }
566
+ /** Scrolling up pauses following; reaching the bottom resumes it. */
567
+ scrollBy(delta) {
568
+ const base = this.following ? this.lastMaxScroll : this.scrollTop;
569
+ this.scrollTop = Math.max(0, Math.min(base + delta, this.lastMaxScroll));
570
+ this.following = this.scrollTop >= this.lastMaxScroll;
571
+ }
572
+ scrollToTop() {
573
+ this.scrollTop = 0;
574
+ this.following = this.lastMaxScroll === 0;
575
+ }
576
+ scrollToBottom() {
577
+ this.scrollTop = this.lastMaxScroll;
578
+ this.following = true;
579
+ }
580
+ pageSize() {
581
+ return Math.max(1, this.lastWindowHeight - 1);
582
+ }
583
+ windowHeight() {
584
+ return this.lastWindowHeight;
585
+ }
586
+ isFollowing() {
587
+ return this.following;
588
+ }
589
+ scrollInfo() {
590
+ return {
591
+ following: this.following,
592
+ linesBelow: Math.max(0, this.lastMaxScroll - this.scrollTop),
593
+ linesAbove: this.scrollTop,
594
+ };
595
+ }
596
+ }
597
+ //# sourceMappingURL=fullscreen.js.map