Skip to content

Commit bec3d3f

Browse files
Improve cursor visibility with block-style character highlighting
The cursor now highlights the character at its position with a color-blended background when positioned mid-text, while showing a thin vertical bar (▏) at the end. This provides better visual feedback for cursor location during text editing. 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
1 parent 4005e64 commit bec3d3f

File tree

1 file changed

+59
-17
lines changed

1 file changed

+59
-17
lines changed

cli/src/components/multiline-input.tsx

Lines changed: 59 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,39 @@ import { useCallback, useState, useEffect, useMemo, useRef } from 'react'
33

44
import { TextAttributes, type ScrollBoxRenderable } from '@opentui/core'
55

6+
const mixColors = (foreground: string, background: string, alpha = 0.4): string => {
7+
const parseHex = (hex: string) => {
8+
const normalized = hex.trim().replace('#', '')
9+
const full = normalized.length === 3
10+
? normalized.split('').map((ch) => ch + ch).join('')
11+
: normalized
12+
const value = parseInt(full, 16)
13+
return {
14+
r: (value >> 16) & 0xff,
15+
g: (value >> 8) & 0xff,
16+
b: value & 0xff,
17+
}
18+
}
19+
20+
const clamp = (value: number) => Math.max(0, Math.min(255, Math.round(value)))
21+
22+
try {
23+
const fg = parseHex(foreground)
24+
const bg = parseHex(background)
25+
26+
const blend = {
27+
r: clamp(alpha * fg.r + (1 - alpha) * bg.r),
28+
g: clamp(alpha * fg.g + (1 - alpha) * bg.g),
29+
b: clamp(alpha * fg.b + (1 - alpha) * bg.b),
30+
}
31+
32+
const toHex = (value: number) => value.toString(16).padStart(2, '0')
33+
return `#${toHex(blend.r)}${toHex(blend.g)}${toHex(blend.b)}`
34+
} catch {
35+
return foreground
36+
}
37+
}
38+
639

740
// Helper functions for text manipulation
841
function findLineStart(text: string, cursor: number): number {
@@ -53,7 +86,7 @@ function findNextWordBoundary(text: string, cursor: number): number {
5386
return pos
5487
}
5588

56-
const CURSOR_CHAR = '\u2009'
89+
const CURSOR_CHAR = ''
5790

5891
interface MultilineInputProps {
5992
value: string
@@ -396,17 +429,20 @@ export function MultilineInput({
396429
// Calculate display with cursor
397430
const displayValue = value || placeholder
398431
const isPlaceholder = !value && placeholder
399-
const showCursor = focused && !isPlaceholder
432+
const showCursor = focused
400433
const beforeCursor = showCursor ? displayValue.slice(0, cursorPosition) : ''
401434
const afterCursor = showCursor ? displayValue.slice(cursorPosition) : ''
402-
const displayText = showCursor
403-
? `${beforeCursor}${CURSOR_CHAR}${afterCursor}`
404-
: displayValue
435+
const activeChar = afterCursor.charAt(0) || ' '
436+
const highlightBg = mixColors(theme.cursor, isPlaceholder ? theme.inputBg : theme.inputFocusedBg, 0.4)
437+
const shouldHighlight = showCursor && !isPlaceholder && cursorPosition > 0 && cursorPosition < displayValue.length
405438

406-
// Memoize height calculation to avoid expensive computation on every render
407439
const height = useMemo(() => {
408440
const maxCharsPerLine = Math.max(1, width - 4)
409-
const contentForHeight = showCursor ? displayText : displayValue
441+
const contentForHeight = showCursor
442+
? shouldHighlight
443+
? displayValue
444+
: `${displayValue.slice(0, cursorPosition)}${CURSOR_CHAR}${displayValue.slice(cursorPosition)}`
445+
: displayValue
410446
const lines = contentForHeight.split('\n')
411447
let totalLineCount = 0
412448
for (const line of lines) {
@@ -418,7 +454,7 @@ export function MultilineInput({
418454
}
419455
}
420456
return Math.max(1, Math.min(totalLineCount, maxHeight))
421-
}, [displayValue, displayText, showCursor, width, maxHeight])
457+
}, [displayValue, cursorPosition, showCursor, width, maxHeight])
422458

423459
return (
424460
<scrollbox
@@ -460,17 +496,23 @@ export function MultilineInput({
460496
{showCursor ? (
461497
<>
462498
{beforeCursor}
463-
<span
464-
fg={theme.cursor}
465-
bg={theme.cursor}
466-
attributes={TextAttributes.BOLD}
467-
>
468-
{CURSOR_CHAR}
469-
</span>
470-
{afterCursor}
499+
{shouldHighlight ? (
500+
<span fg={theme.inputFocusedFg} bg={highlightBg}>
501+
{activeChar === ' ' ? '\u00a0' : activeChar}
502+
</span>
503+
) : (
504+
<span fg={theme.cursor} attributes={TextAttributes.BOLD}>
505+
{CURSOR_CHAR}
506+
</span>
507+
)}
508+
{shouldHighlight
509+
? afterCursor.length > 0
510+
? afterCursor.slice(1)
511+
: ''
512+
: afterCursor || ' '}
471513
</>
472514
) : (
473-
displayText
515+
displayValue
474516
)}
475517
</text>
476518
</scrollbox>

0 commit comments

Comments
 (0)