init
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useTheme } from '../components/design-system/ThemeProvider.js'
|
||||
import type { useSelection } from '../ink/hooks/use-selection.js'
|
||||
import { getGlobalConfig } from '../utils/config.js'
|
||||
import { getTheme } from '../utils/theme.js'
|
||||
|
||||
type Selection = ReturnType<typeof useSelection>
|
||||
|
||||
/**
|
||||
* Auto-copy the selection to the clipboard when the user finishes dragging
|
||||
* (mouse-up with a non-empty selection) or multi-clicks to select a word/line.
|
||||
* Mirrors iTerm2's "Copy to pasteboard on selection" — the highlight is left
|
||||
* intact so the user can see what was copied. Only fires in alt-screen mode
|
||||
* (selection state is ink-instance-owned; outside alt-screen, the native
|
||||
* terminal handles selection and this hook is a no-op via the ink stub).
|
||||
*
|
||||
* selection.subscribe fires on every mutation (start/update/finish/clear/
|
||||
* multiclick). Both char drags and multi-clicks set isDragging=true while
|
||||
* pressed, so a selection appearing with isDragging=false is always a
|
||||
* drag-finish. copiedRef guards against double-firing on spurious notifies.
|
||||
*
|
||||
* onCopied is optional — when omitted, copy is silent (clipboard is written
|
||||
* but no toast/notification fires). FleetView uses this silent mode; the
|
||||
* fullscreen REPL passes showCopiedToast for user feedback.
|
||||
*/
|
||||
export function useCopyOnSelect(
|
||||
selection: Selection,
|
||||
isActive: boolean,
|
||||
onCopied?: (text: string) => void,
|
||||
): void {
|
||||
// Tracks whether the *previous* notification had a visible selection with
|
||||
// isDragging=false (i.e., we already auto-copied it). Without this, the
|
||||
// finish→clear transition would look like a fresh selection-gone-idle
|
||||
// event and we'd toast twice for a single drag.
|
||||
const copiedRef = useRef(false)
|
||||
// onCopied is a fresh closure each render; read through a ref so the
|
||||
// effect doesn't re-subscribe (which would reset copiedRef via unmount).
|
||||
const onCopiedRef = useRef(onCopied)
|
||||
onCopiedRef.current = onCopied
|
||||
|
||||
useEffect(() => {
|
||||
if (!isActive) return
|
||||
|
||||
const unsubscribe = selection.subscribe(() => {
|
||||
const sel = selection.getState()
|
||||
const has = selection.hasSelection()
|
||||
// Drag in progress — wait for finish. Reset copied flag so a new drag
|
||||
// that ends on the same range still triggers a fresh copy.
|
||||
if (sel?.isDragging) {
|
||||
copiedRef.current = false
|
||||
return
|
||||
}
|
||||
// No selection (cleared, or click-without-drag) — reset.
|
||||
if (!has) {
|
||||
copiedRef.current = false
|
||||
return
|
||||
}
|
||||
// Selection settled (drag finished OR multi-click). Already copied
|
||||
// this one — the only way to get here again without going through
|
||||
// isDragging or !has is a spurious notify (shouldn't happen, but safe).
|
||||
if (copiedRef.current) return
|
||||
|
||||
// Default true: macOS users expect cmd+c to work. It can't — the
|
||||
// terminal's Edit > Copy intercepts it before the pty sees it, and
|
||||
// finds no native selection (mouse tracking disabled it). Auto-copy
|
||||
// on mouse-up makes cmd+c a no-op that leaves the clipboard intact
|
||||
// with the right content, so paste works as expected.
|
||||
const enabled = getGlobalConfig().copyOnSelect ?? true
|
||||
if (!enabled) return
|
||||
|
||||
const text = selection.copySelectionNoClear()
|
||||
// Whitespace-only (e.g., blank-line multi-click) — not worth a
|
||||
// clipboard write or toast. Still set copiedRef so we don't retry.
|
||||
if (!text || !text.trim()) {
|
||||
copiedRef.current = true
|
||||
return
|
||||
}
|
||||
copiedRef.current = true
|
||||
onCopiedRef.current?.(text)
|
||||
})
|
||||
return unsubscribe
|
||||
}, [isActive, selection])
|
||||
}
|
||||
|
||||
/**
|
||||
* Pipe the theme's selectionBg color into the Ink StylePool so the
|
||||
* selection overlay renders a solid blue bg instead of SGR-7 inverse.
|
||||
* Ink is theme-agnostic (layering: colorize.ts "theme resolution happens
|
||||
* at component layer, not here") — this is the bridge. Fires on mount
|
||||
* (before any mouse input is possible) and again whenever /theme flips,
|
||||
* so the selection color tracks the theme live.
|
||||
*/
|
||||
export function useSelectionBgColor(selection: Selection): void {
|
||||
const [themeName] = useTheme()
|
||||
useEffect(() => {
|
||||
selection.setSelectionBgColor(getTheme(themeName).selectionBg)
|
||||
}, [selection, themeName])
|
||||
}
|
||||
Reference in New Issue
Block a user