use-selection.ts
ink/hooks/use-selection.ts
105
Lines
4421
Bytes
2
Exports
4
Imports
10
Keywords
What this is
This page documents one file from the repository and includes its full source so you can read it without leaving the docs site.
Beginner explanation
This file is one piece of the larger system. Its name, directory, imports, and exports show where it fits. Start by reading the exports and related files first.
How it is used
Start from the exports list and related files. Those are the easiest clues for where this file fits into the system.
Expert explanation
Architecturally, this file intersects with ui-flow. It contains 105 lines, 4 detected imports, and 2 detected exports.
Important relationships
Detected exports
useSelectionuseHasSelection
Keywords
selectionvoiddrowminrowmaxrowstdoutstdincontextinstancesshiftanchorinstance
Detected imports
react../components/StdinContext.js../instances.js../selection.js
Source notes
This page embeds the full file contents. Small or leaf files are still indexed honestly instead of being over-explained.
Full source
import { useContext, useMemo, useSyncExternalStore } from 'react'
import StdinContext from '../components/StdinContext.js'
import instances from '../instances.js'
import {
type FocusMove,
type SelectionState,
shiftAnchor,
} from '../selection.js'
/**
* Access to text selection operations on the Ink instance (fullscreen only).
* Returns no-op functions when fullscreen mode is disabled.
*/
export function useSelection(): {
copySelection: () => string
/** Copy without clearing the highlight (for copy-on-select). */
copySelectionNoClear: () => string
clearSelection: () => void
hasSelection: () => boolean
/** Read the raw mutable selection state (for drag-to-scroll). */
getState: () => SelectionState | null
/** Subscribe to selection mutations (start/update/finish/clear). */
subscribe: (cb: () => void) => () => void
/** Shift the anchor row by dRow, clamped to [minRow, maxRow]. */
shiftAnchor: (dRow: number, minRow: number, maxRow: number) => void
/** Shift anchor AND focus by dRow (keyboard scroll: whole selection
* tracks content). Clamped points get col reset to the full-width edge
* since their content was captured by captureScrolledRows. Reads
* screen.width from the ink instance for the col-reset boundary. */
shiftSelection: (dRow: number, minRow: number, maxRow: number) => void
/** Keyboard selection extension (shift+arrow): move focus, anchor fixed.
* Left/right wrap across rows; up/down clamp at viewport edges. */
moveFocus: (move: FocusMove) => void
/** Capture text from rows about to scroll out of the viewport (call
* BEFORE scrollBy so the screen buffer still has the outgoing rows). */
captureScrolledRows: (
firstRow: number,
lastRow: number,
side: 'above' | 'below',
) => void
/** Set the selection highlight bg color (theme-piping; solid bg
* replaces the old SGR-7 inverse so syntax highlighting stays readable
* under selection). Call once on mount + whenever theme changes. */
setSelectionBgColor: (color: string) => void
} {
// Look up the Ink instance via stdout — same pattern as instances map.
// StdinContext is available (it's always provided), and the Ink instance
// is keyed by stdout which we can get from process.stdout since there's
// only one Ink instance per process in practice.
useContext(StdinContext) // anchor to App subtree for hook rules
const ink = instances.get(process.stdout)
// Memoize so callers can safely use the return value in dependency arrays.
// ink is a singleton per stdout — stable across renders.
return useMemo(() => {
if (!ink) {
return {
copySelection: () => '',
copySelectionNoClear: () => '',
clearSelection: () => {},
hasSelection: () => false,
getState: () => null,
subscribe: () => () => {},
shiftAnchor: () => {},
shiftSelection: () => {},
moveFocus: () => {},
captureScrolledRows: () => {},
setSelectionBgColor: () => {},
}
}
return {
copySelection: () => ink.copySelection(),
copySelectionNoClear: () => ink.copySelectionNoClear(),
clearSelection: () => ink.clearTextSelection(),
hasSelection: () => ink.hasTextSelection(),
getState: () => ink.selection,
subscribe: (cb: () => void) => ink.subscribeToSelectionChange(cb),
shiftAnchor: (dRow: number, minRow: number, maxRow: number) =>
shiftAnchor(ink.selection, dRow, minRow, maxRow),
shiftSelection: (dRow, minRow, maxRow) =>
ink.shiftSelectionForScroll(dRow, minRow, maxRow),
moveFocus: (move: FocusMove) => ink.moveSelectionFocus(move),
captureScrolledRows: (firstRow, lastRow, side) =>
ink.captureScrolledRows(firstRow, lastRow, side),
setSelectionBgColor: (color: string) => ink.setSelectionBgColor(color),
}
}, [ink])
}
const NO_SUBSCRIBE = () => () => {}
const ALWAYS_FALSE = () => false
/**
* Reactive selection-exists state. Re-renders the caller when a text
* selection is created or cleared. Always returns false outside
* fullscreen mode (selection is only available in alt-screen).
*/
export function useHasSelection(): boolean {
useContext(StdinContext)
const ink = instances.get(process.stdout)
return useSyncExternalStore(
ink ? ink.subscribeToSelectionChange : NO_SUBSCRIBE,
ink ? ink.hasTextSelection : ALWAYS_FALSE,
)
}