@@ -14,6 +14,13 @@ import {
|
||||
} from "../helpers/clipboard";
|
||||
import { allocatePaste, resolvePlaceholders } from "../helpers/pasteRegistry";
|
||||
|
||||
// Global timestamp for forward delete coordination
|
||||
// Use globalThis to ensure singleton across bundle
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
var __lettaForwardDeleteTimestamp: number | undefined;
|
||||
}
|
||||
|
||||
interface PasteAwareTextInputProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
@@ -340,19 +347,24 @@ export function PasteAwareTextInput({
|
||||
caretOffsetRef.current = wordStart;
|
||||
};
|
||||
|
||||
const forwardDeleteAtCursor = () => {
|
||||
const curPos = caretOffsetRef.current;
|
||||
if (curPos >= displayValueRef.current.length) return;
|
||||
// Forward delete: delete character AFTER cursor
|
||||
const forwardDeleteAtCursor = (cursorPos: number) => {
|
||||
if (cursorPos >= displayValueRef.current.length) return;
|
||||
|
||||
const newDisplay =
|
||||
displayValueRef.current.slice(0, curPos) +
|
||||
displayValueRef.current.slice(curPos + 1);
|
||||
displayValueRef.current.slice(0, cursorPos) +
|
||||
displayValueRef.current.slice(cursorPos + 1);
|
||||
const resolvedActual = resolvePlaceholders(newDisplay);
|
||||
|
||||
// Update refs synchronously for consecutive operations
|
||||
displayValueRef.current = newDisplay;
|
||||
caretOffsetRef.current = cursorPos;
|
||||
|
||||
setDisplayValue(newDisplay);
|
||||
setActualValue(resolvedActual);
|
||||
onChangeRef.current(newDisplay);
|
||||
// Cursor stays in place
|
||||
// Cursor stays in place, sync it
|
||||
setNudgeCursorOffset(cursorPos);
|
||||
};
|
||||
|
||||
const insertNewlineAtCursor = () => {
|
||||
@@ -433,10 +445,54 @@ export function PasteAwareTextInput({
|
||||
}
|
||||
}
|
||||
|
||||
// Kitty keyboard protocol: Ctrl+C
|
||||
// Format: ESC[99;5u (key=99='c', modifier=5=ctrl)
|
||||
// Kitty also sends key release events: ESC[99;5:3u (:3 = release)
|
||||
// Only handle key PRESS, not release (to avoid double-triggering)
|
||||
if (sequence === "\x1b[99;5u") {
|
||||
// Emit raw Ctrl+C byte for Ink to handle
|
||||
internal_eventEmitter.emit("input", "\x03");
|
||||
return;
|
||||
}
|
||||
// Ignore Ctrl+C key release/repeat events
|
||||
if (sequence.startsWith("\x1b[99;5:")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Kitty keyboard protocol: Arrow keys
|
||||
// Format: ESC[1;modifier:event_typeX where X is A/B/C/D for up/down/right/left
|
||||
// Event types: 1=press, 2=repeat, 3=release
|
||||
// Handle press AND repeat events, ignore release
|
||||
{
|
||||
// Match ESC[1;N:1X or ESC[1;N:2X (press or repeat)
|
||||
// biome-ignore lint/suspicious/noControlCharactersInRegex: ESC sequence matching
|
||||
const arrowMatch = sequence.match(/^\x1b\[1;\d+:[12]([ABCD])$/);
|
||||
if (arrowMatch) {
|
||||
// Emit standard arrow key sequence
|
||||
internal_eventEmitter.emit("input", `\x1b[${arrowMatch[1]}`);
|
||||
return;
|
||||
}
|
||||
// Ignore arrow key release events only
|
||||
// biome-ignore lint/suspicious/noControlCharactersInRegex: ESC sequence matching
|
||||
if (/^\x1b\[1;\d+:3[ABCD]$/.test(sequence)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// fn+Delete (forward delete): ESC[3~ - standard ANSI escape sequence
|
||||
// This deletes the character AFTER the cursor (unlike regular backspace)
|
||||
if (sequence === "\x1b[3~") {
|
||||
forwardDeleteAtCursor();
|
||||
// Also handle kitty extended format: ESC[3;modifier:event_type~
|
||||
// Event types: 1=press, 2=repeat, 3=release
|
||||
// Use caretOffsetRef which is updated synchronously via onCursorOffsetChange
|
||||
// biome-ignore lint/suspicious/noControlCharactersInRegex: ESC sequence matching
|
||||
if (sequence === "\x1b[3~" || /^\x1b\[3;\d+:[12]~$/.test(sequence)) {
|
||||
// Set timestamp so ink-text-input skips its delete handling
|
||||
globalThis.__lettaForwardDeleteTimestamp = Date.now();
|
||||
forwardDeleteAtCursor(caretOffsetRef.current);
|
||||
return;
|
||||
}
|
||||
// Ignore forward delete release events
|
||||
// biome-ignore lint/suspicious/noControlCharactersInRegex: ESC sequence matching
|
||||
if (/^\x1b\[3;\d+:3~$/.test(sequence)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -576,12 +632,13 @@ export function PasteAwareTextInput({
|
||||
}
|
||||
|
||||
// Normal typing/edits - update display and compute actual by substituting placeholders
|
||||
// Update displayValueRef synchronously for raw input handlers
|
||||
displayValueRef.current = newValue;
|
||||
setDisplayValue(newValue);
|
||||
const resolved = resolvePlaceholders(newValue);
|
||||
setActualValue(resolved);
|
||||
onChange(newValue);
|
||||
// Default: cursor moves to end (most common case)
|
||||
caretOffsetRef.current = newValue.length;
|
||||
// Note: caretOffsetRef is updated by onCursorOffsetChange callback (called before onChange)
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
|
||||
Reference in New Issue
Block a user