use-selection.ts
1 import { useContext, useMemo, useSyncExternalStore } from 'react' 2 import StdinContext from '../components/StdinContext.js' 3 import instances from '../instances.js' 4 import { 5 type FocusMove, 6 type SelectionState, 7 shiftAnchor, 8 } from '../selection.js' 9 10 /** 11 * Access to text selection operations on the Ink instance (fullscreen only). 12 * Returns no-op functions when fullscreen mode is disabled. 13 */ 14 export function useSelection(): { 15 copySelection: () => string 16 /** Copy without clearing the highlight (for copy-on-select). */ 17 copySelectionNoClear: () => string 18 clearSelection: () => void 19 hasSelection: () => boolean 20 /** Read the raw mutable selection state (for drag-to-scroll). */ 21 getState: () => SelectionState | null 22 /** Subscribe to selection mutations (start/update/finish/clear). */ 23 subscribe: (cb: () => void) => () => void 24 /** Shift the anchor row by dRow, clamped to [minRow, maxRow]. */ 25 shiftAnchor: (dRow: number, minRow: number, maxRow: number) => void 26 /** Shift anchor AND focus by dRow (keyboard scroll: whole selection 27 * tracks content). Clamped points get col reset to the full-width edge 28 * since their content was captured by captureScrolledRows. Reads 29 * screen.width from the ink instance for the col-reset boundary. */ 30 shiftSelection: (dRow: number, minRow: number, maxRow: number) => void 31 /** Keyboard selection extension (shift+arrow): move focus, anchor fixed. 32 * Left/right wrap across rows; up/down clamp at viewport edges. */ 33 moveFocus: (move: FocusMove) => void 34 /** Capture text from rows about to scroll out of the viewport (call 35 * BEFORE scrollBy so the screen buffer still has the outgoing rows). */ 36 captureScrolledRows: ( 37 firstRow: number, 38 lastRow: number, 39 side: 'above' | 'below', 40 ) => void 41 /** Set the selection highlight bg color (theme-piping; solid bg 42 * replaces the old SGR-7 inverse so syntax highlighting stays readable 43 * under selection). Call once on mount + whenever theme changes. */ 44 setSelectionBgColor: (color: string) => void 45 } { 46 // Look up the Ink instance via stdout — same pattern as instances map. 47 // StdinContext is available (it's always provided), and the Ink instance 48 // is keyed by stdout which we can get from process.stdout since there's 49 // only one Ink instance per process in practice. 50 useContext(StdinContext) // anchor to App subtree for hook rules 51 const ink = instances.get(process.stdout) 52 // Memoize so callers can safely use the return value in dependency arrays. 53 // ink is a singleton per stdout — stable across renders. 54 return useMemo(() => { 55 if (!ink) { 56 return { 57 copySelection: () => '', 58 copySelectionNoClear: () => '', 59 clearSelection: () => {}, 60 hasSelection: () => false, 61 getState: () => null, 62 subscribe: () => () => {}, 63 shiftAnchor: () => {}, 64 shiftSelection: () => {}, 65 moveFocus: () => {}, 66 captureScrolledRows: () => {}, 67 setSelectionBgColor: () => {}, 68 } 69 } 70 return { 71 copySelection: () => ink.copySelection(), 72 copySelectionNoClear: () => ink.copySelectionNoClear(), 73 clearSelection: () => ink.clearTextSelection(), 74 hasSelection: () => ink.hasTextSelection(), 75 getState: () => ink.selection, 76 subscribe: (cb: () => void) => ink.subscribeToSelectionChange(cb), 77 shiftAnchor: (dRow: number, minRow: number, maxRow: number) => 78 shiftAnchor(ink.selection, dRow, minRow, maxRow), 79 shiftSelection: (dRow, minRow, maxRow) => 80 ink.shiftSelectionForScroll(dRow, minRow, maxRow), 81 moveFocus: (move: FocusMove) => ink.moveSelectionFocus(move), 82 captureScrolledRows: (firstRow, lastRow, side) => 83 ink.captureScrolledRows(firstRow, lastRow, side), 84 setSelectionBgColor: (color: string) => ink.setSelectionBgColor(color), 85 } 86 }, [ink]) 87 } 88 89 const NO_SUBSCRIBE = () => () => {} 90 const ALWAYS_FALSE = () => false 91 92 /** 93 * Reactive selection-exists state. Re-renders the caller when a text 94 * selection is created or cleared. Always returns false outside 95 * fullscreen mode (selection is only available in alt-screen). 96 */ 97 export function useHasSelection(): boolean { 98 useContext(StdinContext) 99 const ink = instances.get(process.stdout) 100 return useSyncExternalStore( 101 ink ? ink.subscribeToSelectionChange : NO_SUBSCRIBE, 102 ink ? ink.hasTextSelection : ALWAYS_FALSE, 103 ) 104 }