1- import { useKeyboard , /* usePaste */ } from '@opentui/react'
1+ import { TextAttributes } from '@opentui/core'
2+ import { useKeyboard } from '@opentui/react'
23import { useCallback , useState , useEffect , useMemo , useRef } from 'react'
34
4- import { TextAttributes , type ScrollBoxRenderable } from '@opentui/core '
5+ import { useOpentuiPaste } from '../hooks/use-opentui-paste '
56
6- import { logger } from '../utils/logger '
7+ import type { PasteEvent , ScrollBoxRenderable } from '@opentui/core '
78
8- const mixColors = ( foreground : string , background : string , alpha = 0.4 ) : string => {
9+ const mixColors = (
10+ foreground : string ,
11+ background : string ,
12+ alpha = 0.4 ,
13+ ) : string => {
914 const parseHex = ( hex : string ) => {
1015 const normalized = hex . trim ( ) . replace ( '#' , '' )
11- const full = normalized . length === 3
12- ? normalized . split ( '' ) . map ( ( ch ) => ch + ch ) . join ( '' )
13- : normalized
16+ const full =
17+ normalized . length === 3
18+ ? normalized
19+ . split ( '' )
20+ . map ( ( ch ) => ch + ch )
21+ . join ( '' )
22+ : normalized
1423 const value = parseInt ( full , 16 )
1524 return {
1625 r : ( value >> 16 ) & 0xff ,
@@ -38,7 +47,6 @@ const mixColors = (foreground: string, background: string, alpha = 0.4): string
3847 }
3948}
4049
41-
4250// Helper functions for text manipulation
4351function findLineStart ( text : string , cursor : number ) : number {
4452 let pos = Math . max ( 0 , Math . min ( cursor , text . length ) )
@@ -138,13 +146,14 @@ export function MultilineInput({
138146 }
139147 } , [ value . length , cursorPosition ] )
140148
141- /*
142- usePaste(
149+ useOpentuiPaste (
143150 useCallback (
144- (event) => {
151+ ( event : PasteEvent ) => {
145152 if ( ! focused ) return
146-
147- const text = event.text
153+
154+ const text = event . text ?? ''
155+ if ( ! text ) return
156+
148157 const newValue =
149158 value . slice ( 0 , cursorPosition ) + text + value . slice ( cursorPosition )
150159 onChange ( newValue )
@@ -153,7 +162,6 @@ export function MultilineInput({
153162 [ focused , value , cursorPosition , onChange ] ,
154163 ) ,
155164 )
156- */
157165
158166 // Auto-scroll to bottom when content changes
159167 useEffect ( ( ) => {
@@ -217,9 +225,9 @@ export function MultilineInput({
217225 ! hasEscapePrefix &&
218226 key . sequence === '\r'
219227 const isShiftEnter =
220- isEnterKey &&
221- ( Boolean ( key . shift ) || key . sequence === '\n' )
222- const isOptionEnter = isEnterKey && ( isAltLikeModifier || hasEscapePrefix )
228+ isEnterKey && ( Boolean ( key . shift ) || key . sequence === '\n' )
229+ const isOptionEnter =
230+ isEnterKey && ( isAltLikeModifier || hasEscapePrefix )
223231 const isCtrlJ =
224232 key . ctrl &&
225233 ! key . meta &&
@@ -259,9 +267,6 @@ export function MultilineInput({
259267 } catch {
260268 // ignore property introspection errors
261269 }
262- logger . info ( '[input-debug] keypress' , {
263- ...snapshot ,
264- } )
265270 }
266271
267272 const shouldInsertNewline = isShiftEnter || isOptionEnter || isCtrlJ
@@ -292,10 +297,34 @@ export function MultilineInput({
292297 // Ctrl+U: Delete to line start (also triggered by Cmd+Delete on macOS)
293298 if ( key . ctrl && lowerKeyName === 'u' && ! key . meta && ! key . option ) {
294299 if ( 'preventDefault' in key ) ( key as any ) . preventDefault ( )
295- const newValue =
296- value . slice ( 0 , lineStart ) + value . slice ( cursorPosition )
300+
301+ const originalValue = value
302+ let newValue = originalValue
303+ let nextCursor = cursorPosition
304+
305+ if ( cursorPosition > lineStart ) {
306+ newValue = value . slice ( 0 , lineStart ) + value . slice ( cursorPosition )
307+ nextCursor = lineStart
308+ } else if (
309+ cursorPosition === lineStart &&
310+ cursorPosition > 0 &&
311+ value [ cursorPosition - 1 ] === '\n'
312+ ) {
313+ newValue =
314+ value . slice ( 0 , cursorPosition - 1 ) + value . slice ( cursorPosition )
315+ nextCursor = cursorPosition - 1
316+ } else if ( cursorPosition > 0 ) {
317+ newValue =
318+ value . slice ( 0 , cursorPosition - 1 ) + value . slice ( cursorPosition )
319+ nextCursor = cursorPosition - 1
320+ }
321+
322+ if ( newValue === originalValue ) {
323+ return
324+ }
325+
297326 onChange ( newValue )
298- setCursorPosition ( lineStart )
327+ setCursorPosition ( Math . max ( 0 , nextCursor ) )
299328 return
300329 }
301330
@@ -310,12 +339,40 @@ export function MultilineInput({
310339 onChange ( newValue )
311340 setCursorPosition ( wordStart )
312341 return
313- } // Cmd+Delete: Delete everything before cursor
342+ } // Cmd+Delete: Delete to line start; fallback to single delete if nothing changes
314343 if ( key . name === 'delete' && key . meta && ! isAltLikeModifier ) {
315344 if ( 'preventDefault' in key ) ( key as any ) . preventDefault ( )
316- const newValue = value . slice ( cursorPosition )
345+
346+ const originalValue = value
347+ let newValue = originalValue
348+ let nextCursor = cursorPosition
349+
350+ if ( cursorPosition > 0 ) {
351+ if (
352+ cursorPosition === lineStart &&
353+ value [ cursorPosition - 1 ] === '\n'
354+ ) {
355+ newValue =
356+ value . slice ( 0 , cursorPosition - 1 ) + value . slice ( cursorPosition )
357+ nextCursor = cursorPosition - 1
358+ } else {
359+ newValue = value . slice ( 0 , lineStart ) + value . slice ( cursorPosition )
360+ nextCursor = lineStart
361+ }
362+ }
363+
364+ if ( newValue === originalValue && cursorPosition > 0 ) {
365+ newValue =
366+ value . slice ( 0 , cursorPosition - 1 ) + value . slice ( cursorPosition )
367+ nextCursor = cursorPosition - 1
368+ }
369+
370+ if ( newValue === originalValue ) {
371+ return
372+ }
373+
317374 onChange ( newValue )
318- setCursorPosition ( 0 )
375+ setCursorPosition ( Math . max ( 0 , nextCursor ) )
319376 return
320377 } // Alt+Delete: Delete word forward
321378 if ( key . name === 'delete' && isAltLikeModifier ) {
@@ -500,8 +557,16 @@ export function MultilineInput({
500557 const beforeCursor = showCursor ? displayValue . slice ( 0 , cursorPosition ) : ''
501558 const afterCursor = showCursor ? displayValue . slice ( cursorPosition ) : ''
502559 const activeChar = afterCursor . charAt ( 0 ) || ' '
503- const highlightBg = mixColors ( theme . cursor , isPlaceholder ? theme . inputBg : theme . inputFocusedBg , 0.4 )
504- const shouldHighlight = showCursor && ! isPlaceholder && cursorPosition > 0 && cursorPosition < displayValue . length
560+ const highlightBg = mixColors (
561+ theme . cursor ,
562+ isPlaceholder ? theme . inputBg : theme . inputFocusedBg ,
563+ 0.4 ,
564+ )
565+ const shouldHighlight =
566+ showCursor &&
567+ ! isPlaceholder &&
568+ cursorPosition > 0 &&
569+ cursorPosition < displayValue . length
505570
506571 const height = useMemo ( ( ) => {
507572 const maxCharsPerLine = Math . max ( 1 , width - 4 )
@@ -521,7 +586,14 @@ export function MultilineInput({
521586 }
522587 }
523588 return Math . max ( 1 , Math . min ( totalLineCount , maxHeight ) )
524- } , [ displayValue , cursorPosition , showCursor , shouldHighlight , width , maxHeight ] )
589+ } , [
590+ displayValue ,
591+ cursorPosition ,
592+ showCursor ,
593+ shouldHighlight ,
594+ width ,
595+ maxHeight ,
596+ ] )
525597
526598 return (
527599 < scrollbox
0 commit comments