diff --git a/ui-tui/src/app/inputSelectionStore.ts b/ui-tui/src/app/inputSelectionStore.ts new file mode 100644 index 000000000..25b67c428 --- /dev/null +++ b/ui-tui/src/app/inputSelectionStore.ts @@ -0,0 +1,14 @@ +import { atom } from 'nanostores' + +export interface InputSelection { + clear: () => void + end: number + start: number + value: string +} + +export const $inputSelection = atom(null) + +export const setInputSelection = (next: InputSelection | null) => $inputSelection.set(next) + +export const getInputSelection = () => $inputSelection.get() diff --git a/ui-tui/src/app/useInputHandlers.ts b/ui-tui/src/app/useInputHandlers.ts index 0279a203c..b71a1dc39 100644 --- a/ui-tui/src/app/useInputHandlers.ts +++ b/ui-tui/src/app/useInputHandlers.ts @@ -8,6 +8,9 @@ import type { VoiceRecordResponse } from '../gatewayTypes.js' +import { writeOsc52Clipboard } from '../lib/osc52.js' + +import { getInputSelection } from './inputSelectionStore.js' import type { InputHandlerContext, InputHandlerResult } from './interfaces.js' import { $isBlocked, $overlayState, patchOverlayState } from './overlayStore.js' import { turnController } from './turnController.js' @@ -247,6 +250,15 @@ export function useInputHandlers(ctx: InputHandlerContext): InputHandlerResult { return copySelection() } + const inputSel = getInputSelection() + + if (inputSel && inputSel.end > inputSel.start) { + writeOsc52Clipboard(inputSel.value.slice(inputSel.start, inputSel.end)) + inputSel.clear() + + return + } + if (live.busy && live.sid) { return turnController.interruptTurn({ appendMessage: actions.appendMessage, diff --git a/ui-tui/src/components/textInput.tsx b/ui-tui/src/components/textInput.tsx index 3f4564821..dff8121b5 100644 --- a/ui-tui/src/components/textInput.tsx +++ b/ui-tui/src/components/textInput.tsx @@ -2,7 +2,7 @@ import type { InputEvent, Key } from '@hermes/ink' import * as Ink from '@hermes/ink' import { useEffect, useMemo, useRef, useState } from 'react' -import { writeOsc52Clipboard } from '../lib/osc52.js' +import { setInputSelection } from '../app/inputSelectionStore.js' type InkExt = typeof Ink & { stringWidth: (s: string) => number @@ -353,6 +353,28 @@ export function TextInput({ } }, [value]) + useEffect(() => { + if (!focus) { + return + } + + if (selected) { + setInputSelection({ + clear: () => { + selRef.current = null + setSel(null) + }, + end: selected.end, + start: selected.start, + value: vRef.current + }) + } else { + setInputSelection(null) + } + + return () => setInputSelection(null) + }, [focus, selected]) + useEffect( () => () => { if (pasteTimer.current) { @@ -470,20 +492,16 @@ export function TextInput({ return void emitPaste({ cursor: curRef.current, hotkey: true, text: '', value: vRef.current }) } - if (k.ctrl && inp === 'c') { - const range = selRange() - - if (range) { - writeOsc52Clipboard(vRef.current.slice(range.start, range.end)) - clearSel() - - return - } - - return - } - - if (k.upArrow || k.downArrow || k.tab || (k.shift && k.tab) || k.pageUp || k.pageDown || k.escape) { + if ( + k.upArrow || + k.downArrow || + (k.ctrl && inp === 'c') || + k.tab || + (k.shift && k.tab) || + k.pageUp || + k.pageDown || + k.escape + ) { return }