nastech-tui 0.0.1

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 (424) hide show
  1. package/.prettierrc +11 -0
  2. package/README.md +346 -0
  3. package/eslint.config.mjs +111 -0
  4. package/package.json +51 -0
  5. package/packages/nastech-ink/ambient.d.ts +83 -0
  6. package/packages/nastech-ink/index.d.ts +40 -0
  7. package/packages/nastech-ink/index.js +1 -0
  8. package/packages/nastech-ink/nastech-ink/ambient.d.ts +83 -0
  9. package/packages/nastech-ink/nastech-ink/index.d.ts +40 -0
  10. package/packages/nastech-ink/nastech-ink/index.js +1 -0
  11. package/packages/nastech-ink/nastech-ink/package.json +54 -0
  12. package/packages/nastech-ink/nastech-ink/src/bootstrap/state.ts +9 -0
  13. package/packages/nastech-ink/nastech-ink/src/entry-exports.ts +32 -0
  14. package/packages/nastech-ink/nastech-ink/src/hooks/use-stderr.ts +15 -0
  15. package/packages/nastech-ink/nastech-ink/src/hooks/use-stdout.ts +15 -0
  16. package/packages/nastech-ink/nastech-ink/src/ink/Ansi.tsx +435 -0
  17. package/packages/nastech-ink/nastech-ink/src/ink/app-mouse.test.ts +123 -0
  18. package/packages/nastech-ink/nastech-ink/src/ink/bidi.ts +145 -0
  19. package/packages/nastech-ink/nastech-ink/src/ink/cache-eviction.ts +45 -0
  20. package/packages/nastech-ink/nastech-ink/src/ink/clearTerminal.ts +68 -0
  21. package/packages/nastech-ink/nastech-ink/src/ink/colorize.test.ts +60 -0
  22. package/packages/nastech-ink/nastech-ink/src/ink/colorize.ts +277 -0
  23. package/packages/nastech-ink/nastech-ink/src/ink/components/AlternateScreen.tsx +133 -0
  24. package/packages/nastech-ink/nastech-ink/src/ink/components/App.tsx +830 -0
  25. package/packages/nastech-ink/nastech-ink/src/ink/components/AppContext.ts +20 -0
  26. package/packages/nastech-ink/nastech-ink/src/ink/components/Box.tsx +294 -0
  27. package/packages/nastech-ink/nastech-ink/src/ink/components/Button.tsx +236 -0
  28. package/packages/nastech-ink/nastech-ink/src/ink/components/ClockContext.tsx +133 -0
  29. package/packages/nastech-ink/nastech-ink/src/ink/components/CursorAdvanceContext.ts +35 -0
  30. package/packages/nastech-ink/nastech-ink/src/ink/components/CursorDeclarationContext.ts +28 -0
  31. package/packages/nastech-ink/nastech-ink/src/ink/components/ErrorOverview.tsx +130 -0
  32. package/packages/nastech-ink/nastech-ink/src/ink/components/Link.tsx +38 -0
  33. package/packages/nastech-ink/nastech-ink/src/ink/components/Newline.tsx +43 -0
  34. package/packages/nastech-ink/nastech-ink/src/ink/components/NoSelect.tsx +73 -0
  35. package/packages/nastech-ink/nastech-ink/src/ink/components/RawAnsi.tsx +61 -0
  36. package/packages/nastech-ink/nastech-ink/src/ink/components/ScrollBox.tsx +290 -0
  37. package/packages/nastech-ink/nastech-ink/src/ink/components/Spacer.tsx +23 -0
  38. package/packages/nastech-ink/nastech-ink/src/ink/components/StdinContext.ts +25 -0
  39. package/packages/nastech-ink/nastech-ink/src/ink/components/TerminalFocusContext.tsx +63 -0
  40. package/packages/nastech-ink/nastech-ink/src/ink/components/TerminalSizeContext.tsx +7 -0
  41. package/packages/nastech-ink/nastech-ink/src/ink/components/Text.test.ts +38 -0
  42. package/packages/nastech-ink/nastech-ink/src/ink/components/Text.tsx +336 -0
  43. package/packages/nastech-ink/nastech-ink/src/ink/constants.ts +6 -0
  44. package/packages/nastech-ink/nastech-ink/src/ink/cursor.ts +5 -0
  45. package/packages/nastech-ink/nastech-ink/src/ink/devtools.ts +2 -0
  46. package/packages/nastech-ink/nastech-ink/src/ink/dom.ts +495 -0
  47. package/packages/nastech-ink/nastech-ink/src/ink/events/click-event.ts +38 -0
  48. package/packages/nastech-ink/nastech-ink/src/ink/events/cmd-shortcuts.test.ts +65 -0
  49. package/packages/nastech-ink/nastech-ink/src/ink/events/dispatcher.ts +242 -0
  50. package/packages/nastech-ink/nastech-ink/src/ink/events/emitter.ts +40 -0
  51. package/packages/nastech-ink/nastech-ink/src/ink/events/event-handlers.ts +84 -0
  52. package/packages/nastech-ink/nastech-ink/src/ink/events/event.ts +11 -0
  53. package/packages/nastech-ink/nastech-ink/src/ink/events/focus-event.ts +18 -0
  54. package/packages/nastech-ink/nastech-ink/src/ink/events/input-event.ts +176 -0
  55. package/packages/nastech-ink/nastech-ink/src/ink/events/keyboard-event.ts +57 -0
  56. package/packages/nastech-ink/nastech-ink/src/ink/events/mouse-event.ts +18 -0
  57. package/packages/nastech-ink/nastech-ink/src/ink/events/paste-event.ts +10 -0
  58. package/packages/nastech-ink/nastech-ink/src/ink/events/resize-event.ts +12 -0
  59. package/packages/nastech-ink/nastech-ink/src/ink/events/terminal-event.ts +107 -0
  60. package/packages/nastech-ink/nastech-ink/src/ink/events/terminal-focus-event.ts +19 -0
  61. package/packages/nastech-ink/nastech-ink/src/ink/focus.ts +219 -0
  62. package/packages/nastech-ink/nastech-ink/src/ink/frame.ts +124 -0
  63. package/packages/nastech-ink/nastech-ink/src/ink/get-max-width.ts +27 -0
  64. package/packages/nastech-ink/nastech-ink/src/ink/global.d.ts +1 -0
  65. package/packages/nastech-ink/nastech-ink/src/ink/hit-test.test.ts +38 -0
  66. package/packages/nastech-ink/nastech-ink/src/ink/hit-test.ts +224 -0
  67. package/packages/nastech-ink/nastech-ink/src/ink/hooks/use-animation-frame.ts +62 -0
  68. package/packages/nastech-ink/nastech-ink/src/ink/hooks/use-app.ts +9 -0
  69. package/packages/nastech-ink/nastech-ink/src/ink/hooks/use-cursor-advance.ts +33 -0
  70. package/packages/nastech-ink/nastech-ink/src/ink/hooks/use-declared-cursor.ts +75 -0
  71. package/packages/nastech-ink/nastech-ink/src/ink/hooks/use-external-process.ts +27 -0
  72. package/packages/nastech-ink/nastech-ink/src/ink/hooks/use-input.ts +95 -0
  73. package/packages/nastech-ink/nastech-ink/src/ink/hooks/use-interval.ts +71 -0
  74. package/packages/nastech-ink/package.json +57 -0
  75. package/packages/nastech-ink/src/bootstrap/state.ts +9 -0
  76. package/packages/nastech-ink/src/entry-exports.ts +32 -0
  77. package/packages/nastech-ink/src/hooks/use-stderr.ts +15 -0
  78. package/packages/nastech-ink/src/hooks/use-stdout.ts +15 -0
  79. package/packages/nastech-ink/src/ink/Ansi.tsx +435 -0
  80. package/packages/nastech-ink/src/ink/app-mouse.test.ts +123 -0
  81. package/packages/nastech-ink/src/ink/app-rawmode-mouse.test.ts +91 -0
  82. package/packages/nastech-ink/src/ink/bidi.ts +145 -0
  83. package/packages/nastech-ink/src/ink/cache-eviction.ts +45 -0
  84. package/packages/nastech-ink/src/ink/clearTerminal.ts +68 -0
  85. package/packages/nastech-ink/src/ink/colorize.test.ts +60 -0
  86. package/packages/nastech-ink/src/ink/colorize.ts +277 -0
  87. package/packages/nastech-ink/src/ink/components/AlternateScreen.tsx +133 -0
  88. package/packages/nastech-ink/src/ink/components/App.tsx +855 -0
  89. package/packages/nastech-ink/src/ink/components/AppContext.ts +20 -0
  90. package/packages/nastech-ink/src/ink/components/Box.tsx +294 -0
  91. package/packages/nastech-ink/src/ink/components/Button.tsx +236 -0
  92. package/packages/nastech-ink/src/ink/components/ClockContext.tsx +133 -0
  93. package/packages/nastech-ink/src/ink/components/CursorAdvanceContext.ts +35 -0
  94. package/packages/nastech-ink/src/ink/components/CursorDeclarationContext.ts +28 -0
  95. package/packages/nastech-ink/src/ink/components/ErrorOverview.tsx +130 -0
  96. package/packages/nastech-ink/src/ink/components/Link.tsx +38 -0
  97. package/packages/nastech-ink/src/ink/components/Newline.tsx +43 -0
  98. package/packages/nastech-ink/src/ink/components/NoSelect.tsx +73 -0
  99. package/packages/nastech-ink/src/ink/components/RawAnsi.tsx +61 -0
  100. package/packages/nastech-ink/src/ink/components/ScrollBox.tsx +290 -0
  101. package/packages/nastech-ink/src/ink/components/Spacer.tsx +23 -0
  102. package/packages/nastech-ink/src/ink/components/StdinContext.ts +25 -0
  103. package/packages/nastech-ink/src/ink/components/TerminalFocusContext.tsx +63 -0
  104. package/packages/nastech-ink/src/ink/components/TerminalSizeContext.tsx +7 -0
  105. package/packages/nastech-ink/src/ink/components/Text.test.ts +38 -0
  106. package/packages/nastech-ink/src/ink/components/Text.tsx +336 -0
  107. package/packages/nastech-ink/src/ink/constants.ts +6 -0
  108. package/packages/nastech-ink/src/ink/cursor.ts +5 -0
  109. package/packages/nastech-ink/src/ink/devtools.ts +2 -0
  110. package/packages/nastech-ink/src/ink/dom.ts +495 -0
  111. package/packages/nastech-ink/src/ink/events/click-event.ts +38 -0
  112. package/packages/nastech-ink/src/ink/events/cmd-shortcuts.test.ts +65 -0
  113. package/packages/nastech-ink/src/ink/events/dispatcher.ts +242 -0
  114. package/packages/nastech-ink/src/ink/events/emitter.ts +40 -0
  115. package/packages/nastech-ink/src/ink/events/event-handlers.ts +84 -0
  116. package/packages/nastech-ink/src/ink/events/event.ts +11 -0
  117. package/packages/nastech-ink/src/ink/events/focus-event.ts +18 -0
  118. package/packages/nastech-ink/src/ink/events/input-event.ts +176 -0
  119. package/packages/nastech-ink/src/ink/events/keyboard-event.ts +57 -0
  120. package/packages/nastech-ink/src/ink/events/mouse-event.ts +18 -0
  121. package/packages/nastech-ink/src/ink/events/paste-event.ts +10 -0
  122. package/packages/nastech-ink/src/ink/events/resize-event.ts +12 -0
  123. package/packages/nastech-ink/src/ink/events/terminal-event.ts +107 -0
  124. package/packages/nastech-ink/src/ink/events/terminal-focus-event.ts +19 -0
  125. package/packages/nastech-ink/src/ink/focus.ts +219 -0
  126. package/packages/nastech-ink/src/ink/frame.ts +124 -0
  127. package/packages/nastech-ink/src/ink/get-max-width.ts +27 -0
  128. package/packages/nastech-ink/src/ink/global.d.ts +1 -0
  129. package/packages/nastech-ink/src/ink/hit-test.test.ts +38 -0
  130. package/packages/nastech-ink/src/ink/hit-test.ts +224 -0
  131. package/packages/nastech-ink/src/ink/hooks/use-animation-frame.ts +62 -0
  132. package/packages/nastech-ink/src/ink/hooks/use-app.ts +9 -0
  133. package/packages/nastech-ink/src/ink/hooks/use-cursor-advance.ts +33 -0
  134. package/packages/nastech-ink/src/ink/hooks/use-declared-cursor.ts +75 -0
  135. package/packages/nastech-ink/src/ink/hooks/use-external-process.ts +27 -0
  136. package/packages/nastech-ink/src/ink/hooks/use-input.ts +95 -0
  137. package/packages/nastech-ink/src/ink/hooks/use-interval.ts +71 -0
  138. package/packages/nastech-ink/src/ink/hooks/use-search-highlight.ts +56 -0
  139. package/packages/nastech-ink/src/ink/hooks/use-selection.ts +101 -0
  140. package/packages/nastech-ink/src/ink/hooks/use-stdin.ts +9 -0
  141. package/packages/nastech-ink/src/ink/hooks/use-tab-status.ts +71 -0
  142. package/packages/nastech-ink/src/ink/hooks/use-terminal-focus.ts +18 -0
  143. package/packages/nastech-ink/src/ink/hooks/use-terminal-title.ts +34 -0
  144. package/packages/nastech-ink/src/ink/hooks/use-terminal-viewport.ts +100 -0
  145. package/packages/nastech-ink/src/ink/hyperlinkHover.ts +52 -0
  146. package/packages/nastech-ink/src/ink/ink-cursor-advance.test.ts +234 -0
  147. package/packages/nastech-ink/src/ink/ink-resize.test.ts +50 -0
  148. package/packages/nastech-ink/src/ink/ink.tsx +2705 -0
  149. package/packages/nastech-ink/src/ink/instances.ts +10 -0
  150. package/packages/nastech-ink/src/ink/layout/engine.ts +6 -0
  151. package/packages/nastech-ink/src/ink/layout/geometry.ts +98 -0
  152. package/packages/nastech-ink/src/ink/layout/node.ts +145 -0
  153. package/packages/nastech-ink/src/ink/layout/yoga.ts +313 -0
  154. package/packages/nastech-ink/src/ink/line-width-cache.ts +38 -0
  155. package/packages/nastech-ink/src/ink/log-update.test.ts +223 -0
  156. package/packages/nastech-ink/src/ink/log-update.ts +752 -0
  157. package/packages/nastech-ink/src/ink/lru.ts +14 -0
  158. package/packages/nastech-ink/src/ink/measure-element.ts +23 -0
  159. package/packages/nastech-ink/src/ink/measure-text.ts +50 -0
  160. package/packages/nastech-ink/src/ink/node-cache.ts +53 -0
  161. package/packages/nastech-ink/src/ink/optimizer.ts +99 -0
  162. package/packages/nastech-ink/src/ink/output.ts +845 -0
  163. package/packages/nastech-ink/src/ink/parse-keypress.test.ts +133 -0
  164. package/packages/nastech-ink/src/ink/parse-keypress.ts +848 -0
  165. package/packages/nastech-ink/src/ink/reconciler.ts +382 -0
  166. package/packages/nastech-ink/src/ink/render-border.ts +206 -0
  167. package/packages/nastech-ink/src/ink/render-node-to-output.ts +1582 -0
  168. package/packages/nastech-ink/src/ink/render-to-screen.ts +236 -0
  169. package/packages/nastech-ink/src/ink/renderer.ts +169 -0
  170. package/packages/nastech-ink/src/ink/root.ts +204 -0
  171. package/packages/nastech-ink/src/ink/screen.ts +1590 -0
  172. package/packages/nastech-ink/src/ink/searchHighlight.ts +91 -0
  173. package/packages/nastech-ink/src/ink/selection.test.ts +82 -0
  174. package/packages/nastech-ink/src/ink/selection.ts +1143 -0
  175. package/packages/nastech-ink/src/ink/squash-text-nodes.ts +74 -0
  176. package/packages/nastech-ink/src/ink/stringWidth.ts +341 -0
  177. package/packages/nastech-ink/src/ink/styles.ts +750 -0
  178. package/packages/nastech-ink/src/ink/supports-hyperlinks.ts +51 -0
  179. package/packages/nastech-ink/src/ink/tabstops.ts +44 -0
  180. package/packages/nastech-ink/src/ink/terminal-focus-state.ts +52 -0
  181. package/packages/nastech-ink/src/ink/terminal-querier.ts +222 -0
  182. package/packages/nastech-ink/src/ink/terminal.test.ts +15 -0
  183. package/packages/nastech-ink/src/ink/terminal.ts +299 -0
  184. package/packages/nastech-ink/src/ink/termio/ansi.ts +75 -0
  185. package/packages/nastech-ink/src/ink/termio/csi.ts +334 -0
  186. package/packages/nastech-ink/src/ink/termio/dec.ts +99 -0
  187. package/packages/nastech-ink/src/ink/termio/esc.ts +69 -0
  188. package/packages/nastech-ink/src/ink/termio/osc.test.ts +191 -0
  189. package/packages/nastech-ink/src/ink/termio/osc.ts +724 -0
  190. package/packages/nastech-ink/src/ink/termio/parser.ts +467 -0
  191. package/packages/nastech-ink/src/ink/termio/sgr.ts +362 -0
  192. package/packages/nastech-ink/src/ink/termio/tokenize.test.ts +185 -0
  193. package/packages/nastech-ink/src/ink/termio/tokenize.ts +350 -0
  194. package/packages/nastech-ink/src/ink/termio/types.ts +230 -0
  195. package/packages/nastech-ink/src/ink/termio.ts +42 -0
  196. package/packages/nastech-ink/src/ink/useTerminalNotification.ts +110 -0
  197. package/packages/nastech-ink/src/ink/warn.ts +15 -0
  198. package/packages/nastech-ink/src/ink/widest-line.ts +22 -0
  199. package/packages/nastech-ink/src/ink/wrap-text.test.ts +17 -0
  200. package/packages/nastech-ink/src/ink/wrap-text.ts +144 -0
  201. package/packages/nastech-ink/src/ink/wrapAnsi.ts +13 -0
  202. package/packages/nastech-ink/src/native-ts/yoga-layout/enums.ts +112 -0
  203. package/packages/nastech-ink/src/native-ts/yoga-layout/index.ts +2326 -0
  204. package/packages/nastech-ink/src/utils/debug.ts +6 -0
  205. package/packages/nastech-ink/src/utils/earlyInput.ts +131 -0
  206. package/packages/nastech-ink/src/utils/env.ts +66 -0
  207. package/packages/nastech-ink/src/utils/envUtils.ts +13 -0
  208. package/packages/nastech-ink/src/utils/execFileNoThrow.test.ts +146 -0
  209. package/packages/nastech-ink/src/utils/execFileNoThrow.ts +115 -0
  210. package/packages/nastech-ink/src/utils/fullscreen.ts +3 -0
  211. package/packages/nastech-ink/src/utils/intl.ts +87 -0
  212. package/packages/nastech-ink/src/utils/log.ts +7 -0
  213. package/packages/nastech-ink/src/utils/semver.ts +57 -0
  214. package/packages/nastech-ink/src/utils/sliceAnsi.ts +106 -0
  215. package/packages/nastech-ink/text-input.d.ts +2 -0
  216. package/packages/nastech-ink/text-input.js +1 -0
  217. package/scripts/build.mjs +61 -0
  218. package/scripts/profile-tui.mjs +121 -0
  219. package/src/__tests__/activeSessionSwitcher.test.ts +157 -0
  220. package/src/__tests__/appChromeStatusRule.test.tsx +84 -0
  221. package/src/__tests__/appChromeStatusRuleDevCredits.test.tsx +73 -0
  222. package/src/__tests__/approvalAction.test.ts +50 -0
  223. package/src/__tests__/asCommandDispatch.test.ts +27 -0
  224. package/src/__tests__/blockLayout.test.ts +122 -0
  225. package/src/__tests__/clipboard.test.ts +369 -0
  226. package/src/__tests__/constants.test.ts +53 -0
  227. package/src/__tests__/createGatewayEventHandler.test.ts +1091 -0
  228. package/src/__tests__/createSlashHandler.test.ts +822 -0
  229. package/src/__tests__/creditsCommand.test.ts +144 -0
  230. package/src/__tests__/cursorDriftRegression.test.ts +114 -0
  231. package/src/__tests__/details.test.ts +115 -0
  232. package/src/__tests__/emoji.test.ts +64 -0
  233. package/src/__tests__/externalLink.test.ts +144 -0
  234. package/src/__tests__/forceTruecolor.test.ts +191 -0
  235. package/src/__tests__/gatewayClient.test.ts +394 -0
  236. package/src/__tests__/gatewayRecovery.test.ts +47 -0
  237. package/src/__tests__/markdown.test.ts +331 -0
  238. package/src/__tests__/mathUnicode.test.ts +293 -0
  239. package/src/__tests__/memoryMonitor.test.ts +102 -0
  240. package/src/__tests__/messageLine.test.ts +19 -0
  241. package/src/__tests__/messages.test.ts +92 -0
  242. package/src/__tests__/orchestratorPromptSession.test.ts +64 -0
  243. package/src/__tests__/osc52.test.ts +67 -0
  244. package/src/__tests__/parentLog.test.ts +75 -0
  245. package/src/__tests__/paths.test.ts +70 -0
  246. package/src/__tests__/platform.test.ts +556 -0
  247. package/src/__tests__/precisionWheel.test.ts +44 -0
  248. package/src/__tests__/prompt.test.ts +31 -0
  249. package/src/__tests__/providers.test.ts +65 -0
  250. package/src/__tests__/reasoning.test.ts +76 -0
  251. package/src/__tests__/rpc.test.ts +27 -0
  252. package/src/__tests__/scroll.test.ts +99 -0
  253. package/src/__tests__/slashParity.test.ts +123 -0
  254. package/src/__tests__/spawnHistoryStore.test.ts +46 -0
  255. package/src/__tests__/stateIsolation.test.ts +46 -0
  256. package/src/__tests__/statusBarTicker.test.ts +18 -0
  257. package/src/__tests__/statusRule.test.ts +32 -0
  258. package/src/__tests__/streamingMarkdown.test.ts +121 -0
  259. package/src/__tests__/subagentTree.test.ts +407 -0
  260. package/src/__tests__/syntax.test.ts +45 -0
  261. package/src/__tests__/terminalModes.test.ts +39 -0
  262. package/src/__tests__/terminalParity.test.ts +77 -0
  263. package/src/__tests__/terminalSetup.test.ts +386 -0
  264. package/src/__tests__/termux.test.ts +35 -0
  265. package/src/__tests__/termuxComposerLayout.test.ts +40 -0
  266. package/src/__tests__/text.test.ts +233 -0
  267. package/src/__tests__/textInputBurstInput.test.ts +40 -0
  268. package/src/__tests__/textInputCursorSourceOfTruth.test.ts +50 -0
  269. package/src/__tests__/textInputFastEcho.test.ts +200 -0
  270. package/src/__tests__/textInputLineNav.test.ts +55 -0
  271. package/src/__tests__/textInputPassThrough.test.ts +59 -0
  272. package/src/__tests__/textInputRightClick.test.ts +48 -0
  273. package/src/__tests__/textInputWrap.test.ts +151 -0
  274. package/src/__tests__/theme.test.ts +311 -0
  275. package/src/__tests__/turnControllerNotice.test.ts +43 -0
  276. package/src/__tests__/turnStore.test.ts +66 -0
  277. package/src/__tests__/useCompletion.test.ts +35 -0
  278. package/src/__tests__/useComposerState.test.ts +59 -0
  279. package/src/__tests__/useConfigSync.test.ts +460 -0
  280. package/src/__tests__/useInputHandlers.test.ts +77 -0
  281. package/src/__tests__/useQueue.test.ts +28 -0
  282. package/src/__tests__/useSessionLifecycle.test.ts +60 -0
  283. package/src/__tests__/useVirtualHistoryHeights.test.ts +39 -0
  284. package/src/__tests__/viewport.test.ts +58 -0
  285. package/src/__tests__/viewportStore.test.ts +85 -0
  286. package/src/__tests__/virtualHeights.test.ts +96 -0
  287. package/src/__tests__/virtualHistoryClamp.test.ts +19 -0
  288. package/src/__tests__/virtualHistoryOffsetCache.test.ts +282 -0
  289. package/src/__tests__/wheelAccel.test.ts +138 -0
  290. package/src/app/createGatewayEventHandler.ts +833 -0
  291. package/src/app/createSlashHandler.ts +130 -0
  292. package/src/app/delegationStore.ts +77 -0
  293. package/src/app/gatewayContext.tsx +19 -0
  294. package/src/app/gatewayRecovery.ts +35 -0
  295. package/src/app/inputSelectionStore.ts +15 -0
  296. package/src/app/interfaces.ts +394 -0
  297. package/src/app/overlayStore.ts +53 -0
  298. package/src/app/scroll.ts +71 -0
  299. package/src/app/setupHandoff.ts +54 -0
  300. package/src/app/slash/commands/core.ts +648 -0
  301. package/src/app/slash/commands/credits.ts +57 -0
  302. package/src/app/slash/commands/debug.ts +48 -0
  303. package/src/app/slash/commands/ops.ts +717 -0
  304. package/src/app/slash/commands/session.ts +554 -0
  305. package/src/app/slash/commands/setup.ts +20 -0
  306. package/src/app/slash/registry.ts +20 -0
  307. package/src/app/slash/types.ts +21 -0
  308. package/src/app/spawnHistoryStore.ts +159 -0
  309. package/src/app/turnController.ts +866 -0
  310. package/src/app/turnStore.ts +85 -0
  311. package/src/app/uiStore.ts +44 -0
  312. package/src/app/useComposerState.ts +367 -0
  313. package/src/app/useConfigSync.ts +288 -0
  314. package/src/app/useInputHandlers.ts +576 -0
  315. package/src/app/useLongRunToolCharms.ts +69 -0
  316. package/src/app/useMainApp.ts +1039 -0
  317. package/src/app/useSessionLifecycle.ts +366 -0
  318. package/src/app/useSubmission.ts +429 -0
  319. package/src/app.tsx +25 -0
  320. package/src/banner.ts +93 -0
  321. package/src/components/activeSessionSwitcher.tsx +635 -0
  322. package/src/components/agentsOverlay.tsx +1073 -0
  323. package/src/components/appChrome.tsx +554 -0
  324. package/src/components/appLayout.tsx +444 -0
  325. package/src/components/appOverlays.tsx +254 -0
  326. package/src/components/branding.tsx +466 -0
  327. package/src/components/fpsOverlay.tsx +30 -0
  328. package/src/components/helpHint.tsx +73 -0
  329. package/src/components/markdown.tsx +1119 -0
  330. package/src/components/maskedPrompt.tsx +34 -0
  331. package/src/components/messageLine.tsx +237 -0
  332. package/src/components/modelPicker.tsx +527 -0
  333. package/src/components/overlayControls.tsx +50 -0
  334. package/src/components/pluginsHub.tsx +238 -0
  335. package/src/components/prompts.tsx +276 -0
  336. package/src/components/queuedMessages.tsx +64 -0
  337. package/src/components/sessionPicker.tsx +227 -0
  338. package/src/components/skillsHub.tsx +308 -0
  339. package/src/components/streamingAssistant.tsx +110 -0
  340. package/src/components/streamingMarkdown.tsx +174 -0
  341. package/src/components/textInput.tsx +1340 -0
  342. package/src/components/themed.tsx +30 -0
  343. package/src/components/thinking.tsx +1224 -0
  344. package/src/components/todoPanel.tsx +93 -0
  345. package/src/config/env.ts +64 -0
  346. package/src/config/limits.ts +13 -0
  347. package/src/config/timing.ts +6 -0
  348. package/src/content/charms.ts +1 -0
  349. package/src/content/faces.ts +17 -0
  350. package/src/content/fortunes.ts +30 -0
  351. package/src/content/hotkeys.ts +37 -0
  352. package/src/content/placeholders.ts +13 -0
  353. package/src/content/setup.ts +17 -0
  354. package/src/content/verbs.ts +38 -0
  355. package/src/domain/blockLayout.ts +146 -0
  356. package/src/domain/details.ts +76 -0
  357. package/src/domain/messages.ts +91 -0
  358. package/src/domain/paths.ts +16 -0
  359. package/src/domain/providers.ts +11 -0
  360. package/src/domain/roles.ts +9 -0
  361. package/src/domain/slash.ts +10 -0
  362. package/src/domain/usage.ts +3 -0
  363. package/src/domain/viewport.ts +51 -0
  364. package/src/entry.tsx +104 -0
  365. package/src/gatewayClient.ts +730 -0
  366. package/src/gatewayTypes.ts +568 -0
  367. package/src/hooks/useCompletion.ts +112 -0
  368. package/src/hooks/useGitBranch.ts +72 -0
  369. package/src/hooks/useInputHistory.ts +11 -0
  370. package/src/hooks/useQueue.ts +76 -0
  371. package/src/hooks/useVirtualHistory.ts +554 -0
  372. package/src/lib/circularBuffer.ts +48 -0
  373. package/src/lib/clipboard.ts +182 -0
  374. package/src/lib/editor.test.ts +74 -0
  375. package/src/lib/editor.ts +47 -0
  376. package/src/lib/emoji.ts +55 -0
  377. package/src/lib/externalCli.ts +16 -0
  378. package/src/lib/externalLink.ts +435 -0
  379. package/src/lib/forceTruecolor.ts +60 -0
  380. package/src/lib/fpsStore.ts +51 -0
  381. package/src/lib/fuzzy.test.ts +109 -0
  382. package/src/lib/fuzzy.ts +177 -0
  383. package/src/lib/gracefulExit.ts +47 -0
  384. package/src/lib/history.ts +82 -0
  385. package/src/lib/inputMetrics.ts +203 -0
  386. package/src/lib/liveProgress.test.ts +116 -0
  387. package/src/lib/liveProgress.ts +79 -0
  388. package/src/lib/mathUnicode.ts +770 -0
  389. package/src/lib/memory.test.ts +155 -0
  390. package/src/lib/memory.ts +188 -0
  391. package/src/lib/memoryMonitor.ts +109 -0
  392. package/src/lib/messages.test.ts +29 -0
  393. package/src/lib/messages.ts +8 -0
  394. package/src/lib/openExternalUrl.test.ts +217 -0
  395. package/src/lib/openExternalUrl.ts +158 -0
  396. package/src/lib/osc52.ts +73 -0
  397. package/src/lib/parentLog.ts +57 -0
  398. package/src/lib/perfPane.tsx +107 -0
  399. package/src/lib/platform.ts +409 -0
  400. package/src/lib/precisionWheel.ts +48 -0
  401. package/src/lib/prompt.ts +35 -0
  402. package/src/lib/reasoning.ts +55 -0
  403. package/src/lib/rpc.ts +41 -0
  404. package/src/lib/subagentTree.ts +355 -0
  405. package/src/lib/syntax.ts +117 -0
  406. package/src/lib/terminalModes.ts +51 -0
  407. package/src/lib/terminalParity.ts +78 -0
  408. package/src/lib/terminalSetup.ts +444 -0
  409. package/src/lib/termux.ts +29 -0
  410. package/src/lib/text.test.ts +18 -0
  411. package/src/lib/text.ts +339 -0
  412. package/src/lib/todo.test.ts +21 -0
  413. package/src/lib/todo.ts +9 -0
  414. package/src/lib/viewportStore.ts +124 -0
  415. package/src/lib/virtualHeights.ts +145 -0
  416. package/src/lib/wheelAccel.ts +190 -0
  417. package/src/protocol/interpolation.ts +3 -0
  418. package/src/protocol/paste.ts +1 -0
  419. package/src/theme.ts +589 -0
  420. package/src/types/nastech-ink.d.ts +176 -0
  421. package/src/types.ts +212 -0
  422. package/tsconfig.build.json +9 -0
  423. package/tsconfig.json +19 -0
  424. package/vitest.config.ts +7 -0
@@ -0,0 +1,1143 @@
1
+ /**
2
+ * Text selection state for fullscreen mode.
3
+ *
4
+ * Tracks a linear selection in screen-buffer coordinates (0-indexed col/row).
5
+ * Selection is line-based: cells from (startCol, startRow) through
6
+ * (endCol, endRow) inclusive, wrapping across line boundaries. This matches
7
+ * terminal-native selection behavior (not rectangular/block).
8
+ *
9
+ * The selection is stored as ANCHOR (where the drag started) + FOCUS (where
10
+ * the cursor is now). The rendered highlight normalizes to start ≤ end.
11
+ */
12
+
13
+ import { clamp } from './layout/geometry.js'
14
+ import type { Screen, StylePool } from './screen.js'
15
+ import { cellAt, cellAtIndex, CellWidth, isWrittenCellAt, setCellStyleId } from './screen.js'
16
+
17
+ type Point = { col: number; row: number }
18
+
19
+ export type SelectionState = {
20
+ /** Where the mouse-down occurred. Null when no selection. */
21
+ anchor: Point | null
22
+ /** Current drag position (updated on mouse-move while dragging). */
23
+ focus: Point | null
24
+ /** True between mouse-down and mouse-up. */
25
+ isDragging: boolean
26
+ /** For word/line mode: the initial word/line bounds from the first
27
+ * multi-click. Drag extends from this span to the word/line at the
28
+ * current mouse position so the original word/line stays selected
29
+ * even when dragging backward past it. Null ⇔ char mode. The kind
30
+ * tells extendSelection whether to snap to word or line boundaries. */
31
+ anchorSpan: { lo: Point; hi: Point; kind: 'word' | 'line' } | null
32
+ /** Text from rows that scrolled out ABOVE the viewport during
33
+ * drag-to-scroll. The screen buffer only holds the current viewport,
34
+ * so without this accumulator, dragging down past the bottom edge
35
+ * loses the top of the selection once the anchor clamps. Prepended
36
+ * to the on-screen text by getSelectedText. Reset on start/clear. */
37
+ scrolledOffAbove: string[]
38
+ /** Symmetric: rows scrolled out BELOW when dragging up. Appended. */
39
+ scrolledOffBelow: string[]
40
+ /** Soft-wrap bits parallel to scrolledOffAbove — true means the row
41
+ * is a continuation of the one before it (the `\n` was inserted by
42
+ * word-wrap, not in the source). Captured alongside the text at
43
+ * scroll time since the screen's softWrap bitmap shifts with content.
44
+ * getSelectedText uses these to join wrapped rows back into logical
45
+ * lines. */
46
+ scrolledOffAboveSW: boolean[]
47
+ /** Parallel to scrolledOffBelow. */
48
+ scrolledOffBelowSW: boolean[]
49
+ /** Pre-clamp anchor row. Set when shiftSelection clamps anchor so a
50
+ * reverse scroll can restore the true position and pop accumulators.
51
+ * Without this, PgDn (clamps anchor) → PgUp leaves anchor at the wrong
52
+ * row AND scrolledOffAbove stale — highlight ≠ copy. Undefined when
53
+ * anchor is in-bounds (no clamp debt). Cleared on start/clear. */
54
+ virtualAnchorRow?: number
55
+ /** Same for focus. */
56
+ virtualFocusRow?: number
57
+ /** True if the mouse-down that started this selection had the alt
58
+ * modifier set (SGR button bit 0x08). On macOS xterm.js this is a
59
+ * signal that VS Code's macOptionClickForcesSelection is OFF — if it
60
+ * were on, xterm.js would have consumed the event for native selection
61
+ * and we'd never receive it. Used by the footer to show the right hint. */
62
+ lastPressHadAlt: boolean
63
+ }
64
+
65
+ export function createSelectionState(): SelectionState {
66
+ return {
67
+ anchor: null,
68
+ focus: null,
69
+ isDragging: false,
70
+ anchorSpan: null,
71
+ scrolledOffAbove: [],
72
+ scrolledOffBelow: [],
73
+ scrolledOffAboveSW: [],
74
+ scrolledOffBelowSW: [],
75
+ lastPressHadAlt: false
76
+ }
77
+ }
78
+
79
+ export function startSelection(s: SelectionState, col: number, row: number): void {
80
+ s.anchor = { col, row }
81
+ // Focus is not set until the first drag motion. A click-release with no
82
+ // drag leaves focus null → hasSelection/selectionBounds return false/null
83
+ // via the `!s.focus` check, so a bare click never highlights a cell.
84
+ s.focus = null
85
+ s.isDragging = true
86
+ s.anchorSpan = null
87
+ s.scrolledOffAbove = []
88
+ s.scrolledOffBelow = []
89
+ s.scrolledOffAboveSW = []
90
+ s.scrolledOffBelowSW = []
91
+ s.virtualAnchorRow = undefined
92
+ s.virtualFocusRow = undefined
93
+ s.lastPressHadAlt = false
94
+ }
95
+
96
+ export function updateSelection(s: SelectionState, col: number, row: number): void {
97
+ if (!s.isDragging) {
98
+ return
99
+ }
100
+
101
+ // First motion at the same cell as anchor is a no-op. Terminals in mode
102
+ // 1002 can fire a drag event at the anchor cell (sub-pixel tremor, or a
103
+ // motion-release pair). Setting focus here would turn a bare click into
104
+ // a 1-cell selection and clobber the clipboard via useCopyOnSelect. Once
105
+ // focus is set (real drag), we track normally including back to anchor.
106
+ if (!s.focus && s.anchor && s.anchor.col === col && s.anchor.row === row) {
107
+ return
108
+ }
109
+
110
+ s.focus = { col, row }
111
+ }
112
+
113
+ export function finishSelection(s: SelectionState): void {
114
+ s.isDragging = false
115
+ // Keep anchor/focus so highlight stays visible and text can be copied.
116
+ // Clear via clearSelection() on Esc or after copy.
117
+ }
118
+
119
+ export function clearSelection(s: SelectionState): void {
120
+ s.anchor = null
121
+ s.focus = null
122
+ s.isDragging = false
123
+ s.anchorSpan = null
124
+ s.scrolledOffAbove = []
125
+ s.scrolledOffBelow = []
126
+ s.scrolledOffAboveSW = []
127
+ s.scrolledOffBelowSW = []
128
+ s.virtualAnchorRow = undefined
129
+ s.virtualFocusRow = undefined
130
+ s.lastPressHadAlt = false
131
+ }
132
+
133
+ // Unicode-aware word character matcher: letters (any script), digits,
134
+ // and the punctuation set iTerm2 treats as word-part by default.
135
+ // Matching iTerm2's default means double-clicking a path like
136
+ // which is the muscle memory most macOS terminal users have.
137
+ // iTerm2 default "characters considered part of a word": /-+\~_.
138
+ const WORD_CHAR = /[\p{L}\p{N}_/.\-+~\\]/u
139
+
140
+ /**
141
+ * Character class for double-click word-expansion. Cells with the same
142
+ * class as the clicked cell are included in the selection; a class change
143
+ * is a boundary. Matches typical terminal-emulator behavior (iTerm2 etc.):
144
+ * double-click on `foo` selects `foo`, on `->` selects `->`, on spaces
145
+ * selects the whitespace run.
146
+ */
147
+ function charClass(c: string): 0 | 1 | 2 {
148
+ if (c === ' ' || c === '') {
149
+ return 0
150
+ }
151
+
152
+ if (WORD_CHAR.test(c)) {
153
+ return 1
154
+ }
155
+
156
+ return 2
157
+ }
158
+
159
+ /**
160
+ * Find the bounds of the same-class character run at (col, row). Returns
161
+ * null if the click is out of bounds or lands on a noSelect cell. Used by
162
+ * selectWordAt (initial double-click) and extendWordSelection (drag).
163
+ */
164
+ function wordBoundsAt(screen: Screen, col: number, row: number): { lo: number; hi: number } | null {
165
+ if (row < 0 || row >= screen.height) {
166
+ return null
167
+ }
168
+
169
+ const width = screen.width
170
+ const noSelect = screen.noSelect
171
+ const rowOff = row * width
172
+
173
+ // If the click landed on the spacer tail of a wide char, step back to
174
+ // the head so the class check sees the actual grapheme.
175
+ let c = col
176
+
177
+ if (c > 0) {
178
+ const cell = cellAt(screen, c, row)
179
+
180
+ if (cell && cell.width === CellWidth.SpacerTail) {
181
+ c -= 1
182
+ }
183
+ }
184
+
185
+ if (c < 0 || c >= width || noSelect[rowOff + c] === 1) {
186
+ return null
187
+ }
188
+
189
+ const startCell = cellAt(screen, c, row)
190
+
191
+ if (!startCell) {
192
+ return null
193
+ }
194
+
195
+ const cls = charClass(startCell.char)
196
+
197
+ // Expand left: include cells of the same class, stop at noSelect or
198
+ // class change. SpacerTail cells are stepped over (the wide-char head
199
+ // at the preceding column determines the class).
200
+ let lo = c
201
+
202
+ while (lo > 0) {
203
+ const prev = lo - 1
204
+
205
+ if (noSelect[rowOff + prev] === 1) {
206
+ break
207
+ }
208
+
209
+ const pc = cellAt(screen, prev, row)
210
+
211
+ if (!pc) {
212
+ break
213
+ }
214
+
215
+ if (pc.width === CellWidth.SpacerTail) {
216
+ // Step over the spacer to the wide-char head
217
+ if (prev === 0 || noSelect[rowOff + prev - 1] === 1) {
218
+ break
219
+ }
220
+
221
+ const head = cellAt(screen, prev - 1, row)
222
+
223
+ if (!head || charClass(head.char) !== cls) {
224
+ break
225
+ }
226
+
227
+ lo = prev - 1
228
+
229
+ continue
230
+ }
231
+
232
+ if (charClass(pc.char) !== cls) {
233
+ break
234
+ }
235
+
236
+ lo = prev
237
+ }
238
+
239
+ // Expand right: same logic, skipping spacer tails.
240
+ let hi = c
241
+
242
+ while (hi < width - 1) {
243
+ const next = hi + 1
244
+
245
+ if (noSelect[rowOff + next] === 1) {
246
+ break
247
+ }
248
+
249
+ const nc = cellAt(screen, next, row)
250
+
251
+ if (!nc) {
252
+ break
253
+ }
254
+
255
+ if (nc.width === CellWidth.SpacerTail) {
256
+ // Include the spacer tail in the selection range (it belongs to
257
+ // the wide char at hi) and continue past it.
258
+ hi = next
259
+
260
+ continue
261
+ }
262
+
263
+ if (charClass(nc.char) !== cls) {
264
+ break
265
+ }
266
+
267
+ hi = next
268
+ }
269
+
270
+ return { lo, hi }
271
+ }
272
+
273
+ /** -1 if a < b, 1 if a > b, 0 if equal (reading order: row then col). */
274
+ function comparePoints(a: Point, b: Point): number {
275
+ if (a.row !== b.row) {
276
+ return a.row < b.row ? -1 : 1
277
+ }
278
+
279
+ if (a.col !== b.col) {
280
+ return a.col < b.col ? -1 : 1
281
+ }
282
+
283
+ return 0
284
+ }
285
+
286
+ /**
287
+ * Select the word at (col, row) by scanning the screen buffer for the
288
+ * bounds of the same-class character run. Mutates the selection in place.
289
+ * No-op if the click is out of bounds or lands on a noSelect cell.
290
+ * Sets isDragging=true and anchorSpan so a subsequent drag extends the
291
+ * selection word-by-word (native macOS behavior).
292
+ */
293
+ export function selectWordAt(s: SelectionState, screen: Screen, col: number, row: number): void {
294
+ const b = wordBoundsAt(screen, col, row)
295
+
296
+ if (!b) {
297
+ return
298
+ }
299
+
300
+ const lo = { col: b.lo, row }
301
+ const hi = { col: b.hi, row }
302
+ s.anchor = lo
303
+ s.focus = hi
304
+ s.isDragging = true
305
+ s.anchorSpan = { lo, hi, kind: 'word' }
306
+ }
307
+
308
+ // Printable ASCII minus terminal URL delimiters. Restricting to single-
309
+ // codeunit ASCII keeps cell-count === string-index, so the column-span
310
+ // check below is exact (no wide-char/grapheme drift).
311
+ const URL_BOUNDARY = new Set([...'<>"\'` '])
312
+
313
+ function isUrlChar(c: string): boolean {
314
+ if (c.length !== 1) {
315
+ return false
316
+ }
317
+
318
+ const code = c.charCodeAt(0)
319
+
320
+ return code >= 0x21 && code <= 0x7e && !URL_BOUNDARY.has(c)
321
+ }
322
+
323
+ /**
324
+ * Scan the screen buffer for a plain-text URL at (col, row). Mirrors the
325
+ * terminal's native Cmd+Click URL detection, which fullscreen mode's mouse
326
+ * tracking intercepts. Called from getHyperlinkAt as a fallback when the
327
+ * cell has no OSC 8 hyperlink.
328
+ */
329
+ export function findPlainTextUrlAt(screen: Screen, col: number, row: number): string | undefined {
330
+ if (row < 0 || row >= screen.height) {
331
+ return undefined
332
+ }
333
+
334
+ const width = screen.width
335
+ const noSelect = screen.noSelect
336
+ const rowOff = row * width
337
+
338
+ let c = col
339
+
340
+ if (c > 0) {
341
+ const cell = cellAt(screen, c, row)
342
+
343
+ if (cell && cell.width === CellWidth.SpacerTail) {
344
+ c -= 1
345
+ }
346
+ }
347
+
348
+ if (c < 0 || c >= width || noSelect[rowOff + c] === 1) {
349
+ return undefined
350
+ }
351
+
352
+ const startCell = cellAt(screen, c, row)
353
+
354
+ if (!startCell || !isUrlChar(startCell.char)) {
355
+ return undefined
356
+ }
357
+
358
+ // Expand left/right to the bounds of the URL-char run. URLs are ASCII
359
+ // (CellWidth.Narrow, 1 codeunit), so hitting a non-ASCII/wide/spacer
360
+ // cell is a boundary — no need to step over spacers like wordBoundsAt.
361
+ let lo = c
362
+
363
+ while (lo > 0) {
364
+ const prev = lo - 1
365
+
366
+ if (noSelect[rowOff + prev] === 1) {
367
+ break
368
+ }
369
+
370
+ const pc = cellAt(screen, prev, row)
371
+
372
+ if (!pc || pc.width !== CellWidth.Narrow || !isUrlChar(pc.char)) {
373
+ break
374
+ }
375
+
376
+ lo = prev
377
+ }
378
+
379
+ let hi = c
380
+
381
+ while (hi < width - 1) {
382
+ const next = hi + 1
383
+
384
+ if (noSelect[rowOff + next] === 1) {
385
+ break
386
+ }
387
+
388
+ const nc = cellAt(screen, next, row)
389
+
390
+ if (!nc || nc.width !== CellWidth.Narrow || !isUrlChar(nc.char)) {
391
+ break
392
+ }
393
+
394
+ hi = next
395
+ }
396
+
397
+ let token = ''
398
+
399
+ for (let i = lo; i <= hi; i++) {
400
+ token += cellAt(screen, i, row)!.char
401
+ }
402
+
403
+ // 1 cell = 1 char across [lo, hi] (ASCII-only run), so string index =
404
+ // column offset. Find the last scheme anchor at or before the click —
405
+ // a run like `https://a.com,https://b.com` has two, and clicking the
406
+ // second should return the second URL, not the greedy match of both.
407
+ const clickIdx = c - lo
408
+ const schemeRe = /(?:https?|file):\/\//g
409
+ let urlStart = -1
410
+ let urlEnd = token.length
411
+
412
+ for (let m; (m = schemeRe.exec(token)); ) {
413
+ if (m.index > clickIdx) {
414
+ urlEnd = m.index
415
+
416
+ break
417
+ }
418
+
419
+ urlStart = m.index
420
+ }
421
+
422
+ if (urlStart < 0) {
423
+ return undefined
424
+ }
425
+
426
+ let url = token.slice(urlStart, urlEnd)
427
+
428
+ // Strip trailing sentence punctuation. For closers () ] }, only strip
429
+ // if unbalanced — `/wiki/Foo_(bar)` keeps `)`, `/arr[0]` keeps `]`.
430
+ const OPENER: Record<string, string> = { ')': '(', ']': '[', '}': '{' }
431
+
432
+ while (url.length > 0) {
433
+ const last = url.at(-1)!
434
+
435
+ if ('.,;:!?'.includes(last)) {
436
+ url = url.slice(0, -1)
437
+
438
+ continue
439
+ }
440
+
441
+ const opener = OPENER[last]
442
+
443
+ if (!opener) {
444
+ break
445
+ }
446
+
447
+ let opens = 0
448
+ let closes = 0
449
+
450
+ for (let i = 0; i < url.length; i++) {
451
+ const ch = url.charAt(i)
452
+
453
+ if (ch === opener) {
454
+ opens++
455
+ } else if (ch === last) {
456
+ closes++
457
+ }
458
+ }
459
+
460
+ if (closes > opens) {
461
+ url = url.slice(0, -1)
462
+ } else {
463
+ break
464
+ }
465
+ }
466
+
467
+ // urlStart already guarantees click >= URL start; check right edge.
468
+ if (clickIdx >= urlStart + url.length) {
469
+ return undefined
470
+ }
471
+
472
+ return url
473
+ }
474
+
475
+ /**
476
+ * Select the entire row. Sets isDragging=true and anchorSpan so a
477
+ * subsequent drag extends the selection line-by-line. The anchor/focus
478
+ * span from col 0 to width-1; getSelectedText handles noSelect skipping
479
+ * and trailing-whitespace trimming so the copied text is just the visible
480
+ * line content.
481
+ */
482
+ export function selectLineAt(s: SelectionState, screen: Screen, row: number): void {
483
+ if (row < 0 || row >= screen.height) {
484
+ return
485
+ }
486
+
487
+ const lo = { col: 0, row }
488
+ const hi = { col: screen.width - 1, row }
489
+ s.anchor = lo
490
+ s.focus = hi
491
+ s.isDragging = true
492
+ s.anchorSpan = { lo, hi, kind: 'line' }
493
+ }
494
+
495
+ /**
496
+ * Extend a word/line-mode selection to the word/line at (col, row). The
497
+ * anchor span (the original multi-clicked word/line) stays selected; the
498
+ * selection grows from that span to the word/line at the current mouse
499
+ * position. Word mode falls back to the raw cell when the mouse is over a
500
+ * noSelect cell or out of bounds, so dragging into gutters still extends.
501
+ */
502
+ export function extendSelection(s: SelectionState, screen: Screen, col: number, row: number): void {
503
+ if (!s.isDragging || !s.anchorSpan) {
504
+ return
505
+ }
506
+
507
+ const span = s.anchorSpan
508
+ let mLo: Point
509
+ let mHi: Point
510
+
511
+ if (span.kind === 'word') {
512
+ const b = wordBoundsAt(screen, col, row)
513
+ mLo = { col: b ? b.lo : col, row }
514
+ mHi = { col: b ? b.hi : col, row }
515
+ } else {
516
+ const r = clamp(row, 0, screen.height - 1)
517
+ mLo = { col: 0, row: r }
518
+ mHi = { col: screen.width - 1, row: r }
519
+ }
520
+
521
+ if (comparePoints(mHi, span.lo) < 0) {
522
+ // Mouse target ends before anchor span: extend backward.
523
+ s.anchor = span.hi
524
+ s.focus = mLo
525
+ } else if (comparePoints(mLo, span.hi) > 0) {
526
+ // Mouse target starts after anchor span: extend forward.
527
+ s.anchor = span.lo
528
+ s.focus = mHi
529
+ } else {
530
+ // Mouse overlaps the anchor span: just select the anchor span.
531
+ s.anchor = span.lo
532
+ s.focus = span.hi
533
+ }
534
+ }
535
+
536
+ /** Semantic keyboard focus moves. See moveSelectionFocus in ink.tsx for
537
+ * how screen bounds + row-wrap are applied. */
538
+ export type FocusMove = 'left' | 'right' | 'up' | 'down' | 'lineStart' | 'lineEnd'
539
+
540
+ /**
541
+ * Set focus to (col, row) for keyboard selection extension (shift+arrow).
542
+ * Anchor stays fixed; selection grows or shrinks depending on where focus
543
+ * moves relative to anchor. Drops to char mode (clears anchorSpan) —
544
+ * native macOS does this too: shift+arrow after a double-click word-select
545
+ * extends char-by-char from the word edge, not word-by-word. Scrolled-off
546
+ * accumulators are preserved: keyboard-extending a drag-scrolled selection
547
+ * keeps the off-screen rows. Caller supplies coords already clamped/wrapped.
548
+ */
549
+ export function moveFocus(s: SelectionState, col: number, row: number): void {
550
+ if (!s.focus) {
551
+ return
552
+ }
553
+
554
+ s.anchorSpan = null
555
+ s.focus = { col, row }
556
+ // Explicit user repositioning — any stale virtual focus (from a prior
557
+ // shiftSelection clamp) no longer reflects intent. Anchor stays put so
558
+ // virtualAnchorRow is still valid for its own round-trip.
559
+ s.virtualFocusRow = undefined
560
+ }
561
+
562
+ /**
563
+ * Shift anchor AND focus by dRow, clamped to [minRow, maxRow]. Used for
564
+ * keyboard scroll (PgUp/PgDn/ctrl+u/d/b/f): the whole selection must track
565
+ * the content, unlike drag-to-scroll where focus stays at the mouse. Any
566
+ * point that hits a clamp bound gets its col reset to the full-width edge —
567
+ * its original content scrolled off-screen and was captured by
568
+ * captureScrolledRows, so the col constraint was already consumed. Keeping
569
+ * it would truncate the NEW content now at that screen row. Clamp col is 0
570
+ * for dRow<0 (scrolling down, top leaves, 'above' semantics) or width-1 for
571
+ * dRow>0 (scrolling up, bottom leaves, 'below' semantics).
572
+ *
573
+ * If both ends overshoot the SAME viewport edge (select text → Home/End/g/G
574
+ * jumps far enough that both are out of view), clear — otherwise both clamp
575
+ * to the same corner cell and a ghost 1-cell highlight lingers, and
576
+ * getSelectedText returns one unrelated char from that corner. Symmetric
577
+ * with shiftSelectionForFollow's top-edge check, but bidirectional: keyboard
578
+ * scroll can jump either way.
579
+ */
580
+ export function shiftSelection(s: SelectionState, dRow: number, minRow: number, maxRow: number, width: number): void {
581
+ if (!s.anchor || !s.focus) {
582
+ return
583
+ }
584
+
585
+ // Virtual rows track pre-clamp positions so reverse scrolls restore
586
+ // correctly. Without this, clamp(5→0) + shift(+10) = 10, not the true 5,
587
+ // and scrolledOffAbove stays stale (highlight ≠ copy).
588
+ const vAnchor = (s.virtualAnchorRow ?? s.anchor.row) + dRow
589
+ const vFocus = (s.virtualFocusRow ?? s.focus.row) + dRow
590
+
591
+ if ((vAnchor < minRow && vFocus < minRow) || (vAnchor > maxRow && vFocus > maxRow)) {
592
+ clearSelection(s)
593
+
594
+ return
595
+ }
596
+
597
+ // Debt = how far the nearer endpoint overshoots each edge. When debt
598
+ // shrinks (reverse scroll), those rows are back on-screen — pop from
599
+ // the accumulator so getSelectedText doesn't double-count them.
600
+ const oldMin = Math.min(s.virtualAnchorRow ?? s.anchor.row, s.virtualFocusRow ?? s.focus.row)
601
+
602
+ const oldMax = Math.max(s.virtualAnchorRow ?? s.anchor.row, s.virtualFocusRow ?? s.focus.row)
603
+
604
+ const oldAboveDebt = Math.max(0, minRow - oldMin)
605
+ const oldBelowDebt = Math.max(0, oldMax - maxRow)
606
+ const newAboveDebt = Math.max(0, minRow - Math.min(vAnchor, vFocus))
607
+ const newBelowDebt = Math.max(0, Math.max(vAnchor, vFocus) - maxRow)
608
+
609
+ if (newAboveDebt < oldAboveDebt) {
610
+ // scrolledOffAbove pushes newest at the end (closest to on-screen).
611
+ const drop = oldAboveDebt - newAboveDebt
612
+ s.scrolledOffAbove.length -= drop
613
+ s.scrolledOffAboveSW.length = s.scrolledOffAbove.length
614
+ }
615
+
616
+ if (newBelowDebt < oldBelowDebt) {
617
+ // scrolledOffBelow unshifts newest at the front (closest to on-screen).
618
+ const drop = oldBelowDebt - newBelowDebt
619
+ s.scrolledOffBelow.splice(0, drop)
620
+ s.scrolledOffBelowSW.splice(0, drop)
621
+ }
622
+
623
+ // Invariant: accumulator length ≤ debt. If the accumulator exceeds debt,
624
+ // the excess is stale — e.g., moveFocus cleared virtualFocusRow without
625
+ // trimming the accumulator, orphaning entries the pop above can never
626
+ // reach because oldDebt was ALREADY 0. Truncate to debt (keeping the
627
+ // newest = closest-to-on-screen entries). Check newDebt (not oldDebt):
628
+ // captureScrolledRows runs BEFORE this shift in the real flow (ink.tsx),
629
+ // so at entry the accumulator is populated but oldDebt is still 0 —
630
+ // that's the normal establish-debt path, not stale.
631
+ if (s.scrolledOffAbove.length > newAboveDebt) {
632
+ // Above pushes newest at END → keep END.
633
+ s.scrolledOffAbove = newAboveDebt > 0 ? s.scrolledOffAbove.slice(-newAboveDebt) : []
634
+ s.scrolledOffAboveSW = newAboveDebt > 0 ? s.scrolledOffAboveSW.slice(-newAboveDebt) : []
635
+ }
636
+
637
+ if (s.scrolledOffBelow.length > newBelowDebt) {
638
+ // Below unshifts newest at FRONT → keep FRONT.
639
+ s.scrolledOffBelow = s.scrolledOffBelow.slice(0, newBelowDebt)
640
+ s.scrolledOffBelowSW = s.scrolledOffBelowSW.slice(0, newBelowDebt)
641
+ }
642
+
643
+ // Clamp col depends on which EDGE (not dRow direction): virtual tracking
644
+ // means a top-clamped point can stay top-clamped during a dRow>0 reverse
645
+ // shift — dRow-based clampCol would give it the bottom col.
646
+ const shift = (p: Point, vRow: number): Point => {
647
+ if (vRow < minRow) {
648
+ return { col: 0, row: minRow }
649
+ }
650
+
651
+ if (vRow > maxRow) {
652
+ return { col: width - 1, row: maxRow }
653
+ }
654
+
655
+ return { col: p.col, row: vRow }
656
+ }
657
+
658
+ s.anchor = shift(s.anchor, vAnchor)
659
+ s.focus = shift(s.focus, vFocus)
660
+ s.virtualAnchorRow = vAnchor < minRow || vAnchor > maxRow ? vAnchor : undefined
661
+ s.virtualFocusRow = vFocus < minRow || vFocus > maxRow ? vFocus : undefined
662
+
663
+ // anchorSpan not virtual-tracked: it's for word/line extend-on-drag,
664
+ // irrelevant to the keyboard-scroll round-trip case.
665
+ if (s.anchorSpan) {
666
+ const sp = (p: Point): Point => {
667
+ const r = p.row + dRow
668
+
669
+ if (r < minRow) {
670
+ return { col: 0, row: minRow }
671
+ }
672
+
673
+ if (r > maxRow) {
674
+ return { col: width - 1, row: maxRow }
675
+ }
676
+
677
+ return { col: p.col, row: r }
678
+ }
679
+
680
+ s.anchorSpan = {
681
+ lo: sp(s.anchorSpan.lo),
682
+ hi: sp(s.anchorSpan.hi),
683
+ kind: s.anchorSpan.kind
684
+ }
685
+ }
686
+ }
687
+
688
+ /**
689
+ * Shift the anchor row by dRow, clamped to [minRow, maxRow]. Used during
690
+ * drag-to-scroll: when the ScrollBox scrolls by N rows, the content that
691
+ * was under the anchor is now at a different viewport row, so the anchor
692
+ * must follow it. Focus is left unchanged (it stays at the mouse position).
693
+ */
694
+ export function shiftAnchor(s: SelectionState, dRow: number, minRow: number, maxRow: number): void {
695
+ if (!s.anchor) {
696
+ return
697
+ }
698
+
699
+ // Same virtual-row tracking as shiftSelection/shiftSelectionForFollow: the
700
+ // drag→follow transition hands off to shiftSelectionForFollow, which reads
701
+ // (virtualAnchorRow ?? anchor.row). Without this, drag-phase clamping
702
+ // leaves virtual undefined → follow initializes from the already-clamped
703
+ // row, under-counting total drift → shiftSelection's invariant-restore
704
+ // prematurely clears valid drag-phase accumulator entries.
705
+ const raw = (s.virtualAnchorRow ?? s.anchor.row) + dRow
706
+ s.anchor = { col: s.anchor.col, row: clamp(raw, minRow, maxRow) }
707
+ s.virtualAnchorRow = raw < minRow || raw > maxRow ? raw : undefined
708
+
709
+ // anchorSpan not virtual-tracked (word/line extend, irrelevant to
710
+ // keyboard-scroll round-trip) — plain clamp from current row.
711
+ if (s.anchorSpan) {
712
+ const shift = (p: Point): Point => ({
713
+ col: p.col,
714
+ row: clamp(p.row + dRow, minRow, maxRow)
715
+ })
716
+
717
+ s.anchorSpan = {
718
+ lo: shift(s.anchorSpan.lo),
719
+ hi: shift(s.anchorSpan.hi),
720
+ kind: s.anchorSpan.kind
721
+ }
722
+ }
723
+ }
724
+
725
+ /**
726
+ * Shift the whole selection (anchor + focus + anchorSpan) by dRow, clamped
727
+ * to [minRow, maxRow]. Used when sticky/auto-follow scrolls the ScrollBox
728
+ * while a selection is active — native terminal behavior is for the
729
+ * highlight to walk up the screen with the text (not stay at the same
730
+ * screen position).
731
+ *
732
+ * Differs from shiftAnchor: during drag-to-scroll, focus tracks the live
733
+ * mouse position and only anchor follows the text. During streaming-follow,
734
+ * the selection is text-anchored at both ends — both must move. The
735
+ * isDragging check in ink.tsx picks which shift to apply.
736
+ *
737
+ * If both ends would shift strictly BELOW minRow (unclamped), the selected
738
+ * text has scrolled entirely off the top. Clear it — otherwise a single
739
+ * inverted cell lingers at the viewport top as a ghost (native terminals
740
+ * drop the selection when it leaves scrollback). Landing AT minRow is
741
+ * still valid: that cell holds the correct text. Returns true if the
742
+ * selection was cleared so the caller can notify React-land subscribers
743
+ * (useHasSelection) — the caller is inside onRender so it can't use
744
+ * notifySelectionChange (recursion), must fire listeners directly.
745
+ */
746
+ export function shiftSelectionForFollow(s: SelectionState, dRow: number, minRow: number, maxRow: number): boolean {
747
+ if (!s.anchor) {
748
+ return false
749
+ }
750
+
751
+ // Mirror shiftSelection: compute raw (unclamped) positions from virtual
752
+ // if set, else current. This handles BOTH the update path (virtual already
753
+ // set from a prior keyboard scroll) AND the initialize path (first clamp
754
+ // happens HERE via follow-scroll, no prior keyboard scroll). Without the
755
+ // initialize path, follow-scroll-first leaves virtual undefined even
756
+ // though the clamp below occurred → a later PgUp computes debt from the
757
+ // clamped row instead of the true pre-clamp row and never pops the
758
+ // accumulator — getSelectedText double-counts the off-screen rows.
759
+ const rawAnchor = (s.virtualAnchorRow ?? s.anchor.row) + dRow
760
+
761
+ const rawFocus = s.focus ? (s.virtualFocusRow ?? s.focus.row) + dRow : undefined
762
+
763
+ if (rawAnchor < minRow && rawFocus !== undefined && rawFocus < minRow) {
764
+ clearSelection(s)
765
+
766
+ return true
767
+ }
768
+
769
+ // Clamp from raw, not p.row+dRow — so a virtual position coming back
770
+ // in-bounds lands at the TRUE position, not the stale clamped one.
771
+ s.anchor = { col: s.anchor.col, row: clamp(rawAnchor, minRow, maxRow) }
772
+
773
+ if (s.focus && rawFocus !== undefined) {
774
+ s.focus = { col: s.focus.col, row: clamp(rawFocus, minRow, maxRow) }
775
+ }
776
+
777
+ s.virtualAnchorRow = rawAnchor < minRow || rawAnchor > maxRow ? rawAnchor : undefined
778
+ s.virtualFocusRow = rawFocus !== undefined && (rawFocus < minRow || rawFocus > maxRow) ? rawFocus : undefined
779
+
780
+ // anchorSpan not virtual-tracked (word/line extend, irrelevant to
781
+ // keyboard-scroll round-trip) — plain clamp from current row.
782
+ if (s.anchorSpan) {
783
+ const shift = (p: Point): Point => ({
784
+ col: p.col,
785
+ row: clamp(p.row + dRow, minRow, maxRow)
786
+ })
787
+
788
+ s.anchorSpan = {
789
+ lo: shift(s.anchorSpan.lo),
790
+ hi: shift(s.anchorSpan.hi),
791
+ kind: s.anchorSpan.kind
792
+ }
793
+ }
794
+
795
+ return false
796
+ }
797
+
798
+ export function hasSelection(s: SelectionState): boolean {
799
+ return s.anchor !== null && s.focus !== null
800
+ }
801
+
802
+ /**
803
+ * Stable fingerprint of the user-visible selection state. Used by Ink
804
+ * to skip incrementing the mutation counter when notifySelectionChange()
805
+ * fires without an actual change to anchor/focus/isDragging — protects
806
+ * version-based subscribers (copy-on-select) from re-running for the
807
+ * same stable selection.
808
+ */
809
+ export function selectionSignature(s: SelectionState): string {
810
+ const a = s.anchor ? `${s.anchor.row},${s.anchor.col}` : 'null'
811
+ const f = s.focus ? `${s.focus.row},${s.focus.col}` : 'null'
812
+
813
+ return `${a}|${f}|${s.isDragging ? 1 : 0}`
814
+ }
815
+
816
+ /**
817
+ * Normalized selection bounds: start is always before end in reading order.
818
+ * Returns null if no active selection.
819
+ */
820
+ export function selectionBounds(s: SelectionState): {
821
+ start: { col: number; row: number }
822
+ end: { col: number; row: number }
823
+ } | null {
824
+ if (!s.anchor || !s.focus) {
825
+ return null
826
+ }
827
+
828
+ return comparePoints(s.anchor, s.focus) <= 0 ? { start: s.anchor, end: s.focus } : { start: s.focus, end: s.anchor }
829
+ }
830
+
831
+ /**
832
+ * Check if a cell at (col, row) is within the current selection range.
833
+ * Used by the renderer to apply inverse style.
834
+ */
835
+ export function isCellSelected(s: SelectionState, col: number, row: number): boolean {
836
+ const b = selectionBounds(s)
837
+
838
+ if (!b) {
839
+ return false
840
+ }
841
+
842
+ const { start, end } = b
843
+
844
+ if (row < start.row || row > end.row) {
845
+ return false
846
+ }
847
+
848
+ if (row === start.row && col < start.col) {
849
+ return false
850
+ }
851
+
852
+ if (row === end.row && col > end.col) {
853
+ return false
854
+ }
855
+
856
+ return true
857
+ }
858
+
859
+ function selectableCell(screen: Screen, row: number, col: number): boolean {
860
+ const cell = cellAt(screen, col, row)
861
+
862
+ return (
863
+ screen.noSelect[row * screen.width + col] !== 1 &&
864
+ isWrittenCellAt(screen, col, row) &&
865
+ !!cell &&
866
+ cell.width !== CellWidth.SpacerTail &&
867
+ cell.width !== CellWidth.SpacerHead
868
+ )
869
+ }
870
+
871
+ function selectionContentBounds(
872
+ screen: Screen,
873
+ row: number,
874
+ start: number,
875
+ end: number
876
+ ): { first: number; last: number } | null {
877
+ let first = start
878
+
879
+ while (first <= end && !selectableCell(screen, row, first)) {
880
+ first++
881
+ }
882
+
883
+ if (first > end) {
884
+ return null
885
+ }
886
+
887
+ let last = end
888
+
889
+ while (last >= first && !selectableCell(screen, row, last)) {
890
+ last--
891
+ }
892
+
893
+ return { first, last }
894
+ }
895
+
896
+ /** Extract text from one screen row. When the next row is a soft-wrap
897
+ * continuation (screen.softWrap[row+1]>0), clamp to that content-end
898
+ * column and skip the trailing trim so the word-separator space survives
899
+ * the join. See Screen.softWrap for why the clamp is necessary. */
900
+ function extractRowText(screen: Screen, row: number, colStart: number, colEnd: number): string {
901
+ const noSelect = screen.noSelect
902
+ const rowOff = row * screen.width
903
+ const contentEnd = row + 1 < screen.height ? screen.softWrap[row + 1]! : 0
904
+ const lastCol = contentEnd > 0 ? Math.min(colEnd, contentEnd - 1) : colEnd
905
+ let line = ''
906
+
907
+ for (let col = colStart; col <= lastCol; col++) {
908
+ // Skip cells marked noSelect (gutters, line numbers, diff sigils).
909
+ // Check before cellAt to avoid the decode cost for excluded cells.
910
+ if (noSelect[rowOff + col] === 1) {
911
+ continue
912
+ }
913
+
914
+ const cell = cellAt(screen, col, row)
915
+
916
+ if (!cell) {
917
+ continue
918
+ }
919
+
920
+ // Skip spacer tails (second half of wide chars) — the head already
921
+ // contains the full grapheme. SpacerHead is a blank at line-end.
922
+ if (cell.width === CellWidth.SpacerTail || cell.width === CellWidth.SpacerHead) {
923
+ continue
924
+ }
925
+
926
+ line += cell.char
927
+ }
928
+
929
+ return contentEnd > 0 ? line : line.replace(/\s+$/, '')
930
+ }
931
+
932
+ /** Accumulator for selected text that merges soft-wrapped rows back
933
+ * into logical lines. push(text, sw) appends a newline before text
934
+ * only when sw=false (i.e. the row starts a new logical line). Rows
935
+ * with sw=true are concatenated onto the previous row. */
936
+ function joinRows(lines: string[], text: string, sw: boolean | undefined): void {
937
+ if (sw && lines.length > 0) {
938
+ lines[lines.length - 1] += text
939
+ } else {
940
+ lines.push(text)
941
+ }
942
+ }
943
+
944
+ function trimEmptyEdgeRows(lines: string[]): string[] {
945
+ let start = 0
946
+ let end = lines.length
947
+
948
+ while (start < end && !lines[start]!.trim()) {
949
+ start++
950
+ }
951
+
952
+ while (end > start && !lines[end - 1]!.trim()) {
953
+ end--
954
+ }
955
+
956
+ return lines.slice(start, end)
957
+ }
958
+
959
+ /**
960
+ * Extract text from the screen buffer within the selection range.
961
+ * Rows are joined with newlines unless the screen's softWrap bitmap
962
+ * marks a row as a word-wrap continuation — those rows are concatenated
963
+ * onto the previous row so the copied text matches the logical source
964
+ * line, not the visual wrapped layout. Trailing whitespace on the last
965
+ * fragment of each logical line is trimmed. Wide-char spacer cells are
966
+ * skipped. Rows that scrolled out of the viewport during drag-to-scroll
967
+ * are joined back in from the scrolledOffAbove/Below accumulators along
968
+ * with their captured softWrap bits.
969
+ */
970
+ export function getSelectedText(s: SelectionState, screen: Screen): string {
971
+ const b = selectionBounds(s)
972
+
973
+ if (!b) {
974
+ return ''
975
+ }
976
+
977
+ const { start, end } = b
978
+ const sw = screen.softWrap
979
+ const lines: string[] = []
980
+
981
+ for (let i = 0; i < s.scrolledOffAbove.length; i++) {
982
+ joinRows(lines, s.scrolledOffAbove[i]!, s.scrolledOffAboveSW[i])
983
+ }
984
+
985
+ for (let row = start.row; row <= end.row; row++) {
986
+ const rowStart = Math.max(0, row === start.row ? start.col : 0)
987
+ const rowEnd = Math.min(row === end.row ? end.col : screen.width - 1, screen.width - 1)
988
+ const bounds = selectionContentBounds(screen, row, rowStart, rowEnd)
989
+
990
+ joinRows(lines, bounds ? extractRowText(screen, row, bounds.first, bounds.last) : '', sw[row]! > 0)
991
+ }
992
+
993
+ for (let i = 0; i < s.scrolledOffBelow.length; i++) {
994
+ joinRows(lines, s.scrolledOffBelow[i]!, s.scrolledOffBelowSW[i])
995
+ }
996
+
997
+ return trimEmptyEdgeRows(lines).join('\n')
998
+ }
999
+
1000
+ /**
1001
+ * Capture text from rows about to scroll out of the viewport during
1002
+ * drag-to-scroll, BEFORE scrollBy overwrites them. Only the rows that
1003
+ * intersect the selection are captured, using the selection's col bounds
1004
+ * for the anchor-side boundary row. After capturing the anchor row, the
1005
+ * anchor.col AND anchorSpan cols are reset to the full-width boundary so
1006
+ * subsequent captures and the final getSelectedText don't re-apply a stale
1007
+ * col constraint to content that's no longer under the original anchor.
1008
+ * Both span cols are reset (not just the near side): after a blocked
1009
+ * reversal the drag can flip direction, and extendSelection then reads the
1010
+ * OPPOSITE span side — which would otherwise still hold the original word
1011
+ * boundary and truncate one subsequently-captured row.
1012
+ *
1013
+ * side='above': rows scrolling out the top (dragging down, anchor=start).
1014
+ * side='below': rows scrolling out the bottom (dragging up, anchor=end).
1015
+ */
1016
+ export function captureScrolledRows(
1017
+ s: SelectionState,
1018
+ screen: Screen,
1019
+ firstRow: number,
1020
+ lastRow: number,
1021
+ side: 'above' | 'below'
1022
+ ): void {
1023
+ const b = selectionBounds(s)
1024
+
1025
+ if (!b || firstRow > lastRow) {
1026
+ return
1027
+ }
1028
+
1029
+ const { start, end } = b
1030
+ // Intersect [firstRow, lastRow] with [start.row, end.row]. Rows outside
1031
+ // the selection aren't captured — they weren't selected.
1032
+ const lo = Math.max(firstRow, start.row)
1033
+ const hi = Math.min(lastRow, end.row)
1034
+
1035
+ if (lo > hi) {
1036
+ return
1037
+ }
1038
+
1039
+ const width = screen.width
1040
+ const sw = screen.softWrap
1041
+ const captured: string[] = []
1042
+ const capturedSW: boolean[] = []
1043
+
1044
+ for (let row = lo; row <= hi; row++) {
1045
+ const colStart = row === start.row ? start.col : 0
1046
+ const colEnd = row === end.row ? end.col : width - 1
1047
+ captured.push(extractRowText(screen, row, colStart, colEnd))
1048
+ capturedSW.push(sw[row]! > 0)
1049
+ }
1050
+
1051
+ if (side === 'above') {
1052
+ // Newest rows go at the bottom of the above-accumulator (closest to
1053
+ // the on-screen content in reading order).
1054
+ s.scrolledOffAbove.push(...captured)
1055
+ s.scrolledOffAboveSW.push(...capturedSW)
1056
+
1057
+ // We just captured the top of the selection. The anchor (=start when
1058
+ // dragging down) is now pointing at content that will scroll out; its
1059
+ // col constraint was applied to the captured row. Reset to col 0 so
1060
+ // the NEXT tick and the final getSelectedText read the full row.
1061
+ if (s.anchor && s.anchor.row === start.row && lo === start.row) {
1062
+ s.anchor = { col: 0, row: s.anchor.row }
1063
+
1064
+ if (s.anchorSpan) {
1065
+ s.anchorSpan = {
1066
+ kind: s.anchorSpan.kind,
1067
+ lo: { col: 0, row: s.anchorSpan.lo.row },
1068
+ hi: { col: width - 1, row: s.anchorSpan.hi.row }
1069
+ }
1070
+ }
1071
+ }
1072
+ } else {
1073
+ // Newest rows go at the TOP of the below-accumulator — they're
1074
+ // closest to the on-screen content.
1075
+ s.scrolledOffBelow.unshift(...captured)
1076
+ s.scrolledOffBelowSW.unshift(...capturedSW)
1077
+
1078
+ if (s.anchor && s.anchor.row === end.row && hi === end.row) {
1079
+ s.anchor = { col: width - 1, row: s.anchor.row }
1080
+
1081
+ if (s.anchorSpan) {
1082
+ s.anchorSpan = {
1083
+ kind: s.anchorSpan.kind,
1084
+ lo: { col: 0, row: s.anchorSpan.lo.row },
1085
+ hi: { col: width - 1, row: s.anchorSpan.hi.row }
1086
+ }
1087
+ }
1088
+ }
1089
+ }
1090
+ }
1091
+
1092
+ /**
1093
+ * Apply the selection overlay directly to the screen buffer by changing
1094
+ * the style of every cell in the selection range. Called after the
1095
+ * renderer produces the Frame but before the diff — the normal diffEach
1096
+ * then picks up the restyled cells as ordinary changes, so LogUpdate
1097
+ * stays a pure diff engine with no selection awareness.
1098
+ *
1099
+ * Uses a SOLID selection background (theme-provided via StylePool.
1100
+ * setSelectionBg) that REPLACES each cell's bg while PRESERVING its fg —
1101
+ * matches native terminal selection. Previously SGR-7 inverse (swapped
1102
+ * fg/bg per cell), which fragmented badly over syntax-highlighted text:
1103
+ * every distinct fg color became a different bg stripe.
1104
+ *
1105
+ * Uses StylePool caches so on drag the only work per cell is a Map
1106
+ * lookup + packed-int write.
1107
+ */
1108
+ export function applySelectionOverlay(screen: Screen, selection: SelectionState, stylePool: StylePool): void {
1109
+ const b = selectionBounds(selection)
1110
+
1111
+ if (!b) {
1112
+ return
1113
+ }
1114
+
1115
+ const { start, end } = b
1116
+ const width = screen.width
1117
+ const noSelect = screen.noSelect
1118
+
1119
+ for (let row = start.row; row <= end.row && row < screen.height; row++) {
1120
+ const colStart = row === start.row ? start.col : 0
1121
+ const colEnd = row === end.row ? Math.min(end.col, width - 1) : width - 1
1122
+ const bounds = selectionContentBounds(screen, row, colStart, colEnd)
1123
+ const rowOff = row * width
1124
+
1125
+ if (!bounds) {
1126
+ continue
1127
+ }
1128
+
1129
+ for (let col = bounds.first; col <= bounds.last; col++) {
1130
+ const idx = rowOff + col
1131
+
1132
+ // Skip noSelect cells — gutters stay visually unchanged so it's
1133
+ // clear they're not part of the copy. Surrounding selectable cells
1134
+ // still highlight so the selection extent remains visible.
1135
+ if (noSelect[idx] === 1) {
1136
+ continue
1137
+ }
1138
+
1139
+ const cell = cellAtIndex(screen, idx)
1140
+ setCellStyleId(screen, col, row, stylePool.withSelectionBg(cell.styleId))
1141
+ }
1142
+ }
1143
+ }