source dump of claude code
at main 98 lines 4.3 kB view raw
1import { useEffect, useRef } from 'react' 2import { useTheme } from '../components/design-system/ThemeProvider.js' 3import type { useSelection } from '../ink/hooks/use-selection.js' 4import { getGlobalConfig } from '../utils/config.js' 5import { getTheme } from '../utils/theme.js' 6 7type Selection = ReturnType<typeof useSelection> 8 9/** 10 * Auto-copy the selection to the clipboard when the user finishes dragging 11 * (mouse-up with a non-empty selection) or multi-clicks to select a word/line. 12 * Mirrors iTerm2's "Copy to pasteboard on selection" — the highlight is left 13 * intact so the user can see what was copied. Only fires in alt-screen mode 14 * (selection state is ink-instance-owned; outside alt-screen, the native 15 * terminal handles selection and this hook is a no-op via the ink stub). 16 * 17 * selection.subscribe fires on every mutation (start/update/finish/clear/ 18 * multiclick). Both char drags and multi-clicks set isDragging=true while 19 * pressed, so a selection appearing with isDragging=false is always a 20 * drag-finish. copiedRef guards against double-firing on spurious notifies. 21 * 22 * onCopied is optional — when omitted, copy is silent (clipboard is written 23 * but no toast/notification fires). FleetView uses this silent mode; the 24 * fullscreen REPL passes showCopiedToast for user feedback. 25 */ 26export function useCopyOnSelect( 27 selection: Selection, 28 isActive: boolean, 29 onCopied?: (text: string) => void, 30): void { 31 // Tracks whether the *previous* notification had a visible selection with 32 // isDragging=false (i.e., we already auto-copied it). Without this, the 33 // finish→clear transition would look like a fresh selection-gone-idle 34 // event and we'd toast twice for a single drag. 35 const copiedRef = useRef(false) 36 // onCopied is a fresh closure each render; read through a ref so the 37 // effect doesn't re-subscribe (which would reset copiedRef via unmount). 38 const onCopiedRef = useRef(onCopied) 39 onCopiedRef.current = onCopied 40 41 useEffect(() => { 42 if (!isActive) return 43 44 const unsubscribe = selection.subscribe(() => { 45 const sel = selection.getState() 46 const has = selection.hasSelection() 47 // Drag in progress — wait for finish. Reset copied flag so a new drag 48 // that ends on the same range still triggers a fresh copy. 49 if (sel?.isDragging) { 50 copiedRef.current = false 51 return 52 } 53 // No selection (cleared, or click-without-drag) — reset. 54 if (!has) { 55 copiedRef.current = false 56 return 57 } 58 // Selection settled (drag finished OR multi-click). Already copied 59 // this one — the only way to get here again without going through 60 // isDragging or !has is a spurious notify (shouldn't happen, but safe). 61 if (copiedRef.current) return 62 63 // Default true: macOS users expect cmd+c to work. It can't — the 64 // terminal's Edit > Copy intercepts it before the pty sees it, and 65 // finds no native selection (mouse tracking disabled it). Auto-copy 66 // on mouse-up makes cmd+c a no-op that leaves the clipboard intact 67 // with the right content, so paste works as expected. 68 const enabled = getGlobalConfig().copyOnSelect ?? true 69 if (!enabled) return 70 71 const text = selection.copySelectionNoClear() 72 // Whitespace-only (e.g., blank-line multi-click) — not worth a 73 // clipboard write or toast. Still set copiedRef so we don't retry. 74 if (!text || !text.trim()) { 75 copiedRef.current = true 76 return 77 } 78 copiedRef.current = true 79 onCopiedRef.current?.(text) 80 }) 81 return unsubscribe 82 }, [isActive, selection]) 83} 84 85/** 86 * Pipe the theme's selectionBg color into the Ink StylePool so the 87 * selection overlay renders a solid blue bg instead of SGR-7 inverse. 88 * Ink is theme-agnostic (layering: colorize.ts "theme resolution happens 89 * at component layer, not here") — this is the bridge. Fires on mount 90 * (before any mouse input is possible) and again whenever /theme flips, 91 * so the selection color tracks the theme live. 92 */ 93export function useSelectionBgColor(selection: Selection): void { 94 const [themeName] = useTheme() 95 useEffect(() => { 96 selection.setSelectionBgColor(getTheme(themeName).selectionBg) 97 }, [selection, themeName]) 98}