mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-09 04:41:19 -07:00
737 lines
21 KiB
TypeScript
737 lines
21 KiB
TypeScript
/**
|
||
* @license
|
||
* Copyright 2025 Google LLC
|
||
* SPDX-License-Identifier: Apache-2.0
|
||
*/
|
||
|
||
import type { Config } from '@google/gemini-cli-core';
|
||
import {
|
||
KittySequenceOverflowEvent,
|
||
logKittySequenceOverflow,
|
||
} from '@google/gemini-cli-core';
|
||
import { useStdin } from 'ink';
|
||
import type React from 'react';
|
||
import {
|
||
createContext,
|
||
useCallback,
|
||
useContext,
|
||
useEffect,
|
||
useRef,
|
||
} from 'react';
|
||
import readline from 'node:readline';
|
||
import { PassThrough } from 'node:stream';
|
||
import {
|
||
BACKSLASH_ENTER_DETECTION_WINDOW_MS,
|
||
CHAR_CODE_ESC,
|
||
KITTY_CTRL_C,
|
||
KITTY_KEYCODE_BACKSPACE,
|
||
KITTY_KEYCODE_ENTER,
|
||
KITTY_KEYCODE_NUMPAD_ENTER,
|
||
KITTY_KEYCODE_TAB,
|
||
MAX_KITTY_SEQUENCE_LENGTH,
|
||
KITTY_MODIFIER_BASE,
|
||
KITTY_MODIFIER_EVENT_TYPES_OFFSET,
|
||
MODIFIER_SHIFT_BIT,
|
||
MODIFIER_ALT_BIT,
|
||
MODIFIER_CTRL_BIT,
|
||
} from '../utils/platformConstants.js';
|
||
|
||
import { FOCUS_IN, FOCUS_OUT } from '../hooks/useFocus.js';
|
||
|
||
const ESC = '\u001B';
|
||
export const PASTE_MODE_PREFIX = `${ESC}[200~`;
|
||
export const PASTE_MODE_SUFFIX = `${ESC}[201~`;
|
||
export const DRAG_COMPLETION_TIMEOUT_MS = 100; // Broadcast full path after 100ms if no more input
|
||
export const SINGLE_QUOTE = "'";
|
||
export const DOUBLE_QUOTE = '"';
|
||
|
||
export interface Key {
|
||
name: string;
|
||
ctrl: boolean;
|
||
meta: boolean;
|
||
shift: boolean;
|
||
paste: boolean;
|
||
sequence: string;
|
||
kittyProtocol?: boolean;
|
||
}
|
||
|
||
export type KeypressHandler = (key: Key) => void;
|
||
|
||
interface KeypressContextValue {
|
||
subscribe: (handler: KeypressHandler) => void;
|
||
unsubscribe: (handler: KeypressHandler) => void;
|
||
}
|
||
|
||
const KeypressContext = createContext<KeypressContextValue | undefined>(
|
||
undefined,
|
||
);
|
||
|
||
export function useKeypressContext() {
|
||
const context = useContext(KeypressContext);
|
||
if (!context) {
|
||
throw new Error(
|
||
'useKeypressContext must be used within a KeypressProvider',
|
||
);
|
||
}
|
||
return context;
|
||
}
|
||
|
||
export function KeypressProvider({
|
||
children,
|
||
kittyProtocolEnabled,
|
||
config,
|
||
debugKeystrokeLogging,
|
||
}: {
|
||
children: React.ReactNode;
|
||
kittyProtocolEnabled: boolean;
|
||
config?: Config;
|
||
debugKeystrokeLogging?: boolean;
|
||
}) {
|
||
const { stdin, setRawMode } = useStdin();
|
||
const subscribers = useRef<Set<KeypressHandler>>(new Set()).current;
|
||
const isDraggingRef = useRef(false);
|
||
const dragBufferRef = useRef('');
|
||
const draggingTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||
|
||
const subscribe = useCallback(
|
||
(handler: KeypressHandler) => {
|
||
subscribers.add(handler);
|
||
},
|
||
[subscribers],
|
||
);
|
||
|
||
const unsubscribe = useCallback(
|
||
(handler: KeypressHandler) => {
|
||
subscribers.delete(handler);
|
||
},
|
||
[subscribers],
|
||
);
|
||
|
||
useEffect(() => {
|
||
const clearDraggingTimer = () => {
|
||
if (draggingTimerRef.current) {
|
||
clearTimeout(draggingTimerRef.current);
|
||
draggingTimerRef.current = null;
|
||
}
|
||
};
|
||
|
||
const wasRaw = stdin.isRaw;
|
||
if (wasRaw === false) {
|
||
setRawMode(true);
|
||
}
|
||
|
||
const keypressStream = new PassThrough();
|
||
let usePassthrough = false;
|
||
const nodeMajorVersion = parseInt(process.versions.node.split('.')[0], 10);
|
||
if (
|
||
nodeMajorVersion < 20 ||
|
||
process.env['PASTE_WORKAROUND'] === '1' ||
|
||
process.env['PASTE_WORKAROUND'] === 'true'
|
||
) {
|
||
usePassthrough = true;
|
||
}
|
||
|
||
let isPaste = false;
|
||
let pasteBuffer = Buffer.alloc(0);
|
||
let kittySequenceBuffer = '';
|
||
let backslashTimeout: NodeJS.Timeout | null = null;
|
||
let waitingForEnterAfterBackslash = false;
|
||
|
||
// Parse a single complete kitty sequence from the start (prefix) of the
|
||
// buffer and return both the Key and the number of characters consumed.
|
||
// This lets us "peel off" one complete event when multiple sequences arrive
|
||
// in a single chunk, preventing buffer overflow and fragmentation.
|
||
// Parse a single complete kitty/parameterized/legacy sequence from the start
|
||
// of the buffer and return both the parsed Key and the number of characters
|
||
// consumed. This enables peel-and-continue parsing for batched input.
|
||
const parseKittyPrefix = (
|
||
buffer: string,
|
||
): { key: Key; length: number } | null => {
|
||
// In older terminals ESC [ Z was used as Cursor Backward Tabulation (CBT)
|
||
// In newer terminals the same functionality of key combination for moving
|
||
// backward through focusable elements is Shift+Tab, hence we will
|
||
// map ESC [ Z to Shift+Tab
|
||
// 0) Reverse Tab (legacy): ESC [ Z
|
||
// Treat as Shift+Tab for UI purposes.
|
||
// Regex parts:
|
||
// ^ - start of buffer
|
||
// ESC [ - CSI introducer
|
||
// Z - legacy reverse tab
|
||
const revTabLegacy = new RegExp(`^${ESC}\\[Z`);
|
||
let m = buffer.match(revTabLegacy);
|
||
if (m) {
|
||
return {
|
||
key: {
|
||
name: 'tab',
|
||
ctrl: false,
|
||
meta: false,
|
||
shift: true,
|
||
paste: false,
|
||
sequence: buffer.slice(0, m[0].length),
|
||
kittyProtocol: true,
|
||
},
|
||
length: m[0].length,
|
||
};
|
||
}
|
||
|
||
// 1) Reverse Tab (parameterized): ESC [ 1 ; <mods> Z
|
||
// Parameterized reverse Tab: ESC [ 1 ; <mods> Z
|
||
const revTabParam = new RegExp(`^${ESC}\\[1;(\\d+)Z`);
|
||
m = buffer.match(revTabParam);
|
||
if (m) {
|
||
let mods = parseInt(m[1], 10);
|
||
if (mods >= KITTY_MODIFIER_EVENT_TYPES_OFFSET) {
|
||
mods -= KITTY_MODIFIER_EVENT_TYPES_OFFSET;
|
||
}
|
||
const bits = mods - KITTY_MODIFIER_BASE;
|
||
const alt = (bits & MODIFIER_ALT_BIT) === MODIFIER_ALT_BIT;
|
||
const ctrl = (bits & MODIFIER_CTRL_BIT) === MODIFIER_CTRL_BIT;
|
||
return {
|
||
key: {
|
||
name: 'tab',
|
||
ctrl,
|
||
meta: alt,
|
||
// Reverse tab implies Shift behavior; force shift regardless of mods
|
||
shift: true,
|
||
paste: false,
|
||
sequence: buffer.slice(0, m[0].length),
|
||
kittyProtocol: true,
|
||
},
|
||
length: m[0].length,
|
||
};
|
||
}
|
||
|
||
// 2) Parameterized functional: ESC [ 1 ; <mods> (A|B|C|D|H|F|P|Q|R|S)
|
||
// 2) Parameterized functional: ESC [ 1 ; <mods> (A|B|C|D|H|F|P|Q|R|S)
|
||
// Arrows, Home/End, F1–F4 with modifiers encoded in <mods>.
|
||
const arrowPrefix = new RegExp(`^${ESC}\\[1;(\\d+)([ABCDHFPQSR])`);
|
||
m = buffer.match(arrowPrefix);
|
||
if (m) {
|
||
let mods = parseInt(m[1], 10);
|
||
if (mods >= KITTY_MODIFIER_EVENT_TYPES_OFFSET) {
|
||
mods -= KITTY_MODIFIER_EVENT_TYPES_OFFSET;
|
||
}
|
||
const bits = mods - KITTY_MODIFIER_BASE;
|
||
const shift = (bits & MODIFIER_SHIFT_BIT) === MODIFIER_SHIFT_BIT;
|
||
const alt = (bits & MODIFIER_ALT_BIT) === MODIFIER_ALT_BIT;
|
||
const ctrl = (bits & MODIFIER_CTRL_BIT) === MODIFIER_CTRL_BIT;
|
||
const sym = m[2];
|
||
const symbolToName: { [k: string]: string } = {
|
||
A: 'up',
|
||
B: 'down',
|
||
C: 'right',
|
||
D: 'left',
|
||
H: 'home',
|
||
F: 'end',
|
||
P: 'f1',
|
||
Q: 'f2',
|
||
R: 'f3',
|
||
S: 'f4',
|
||
};
|
||
const name = symbolToName[sym] || '';
|
||
if (!name) return null;
|
||
return {
|
||
key: {
|
||
name,
|
||
ctrl,
|
||
meta: alt,
|
||
shift,
|
||
paste: false,
|
||
sequence: buffer.slice(0, m[0].length),
|
||
kittyProtocol: true,
|
||
},
|
||
length: m[0].length,
|
||
};
|
||
}
|
||
|
||
// 3) CSI-u form: ESC [ <code> ; <mods> (u|~)
|
||
// 3) CSI-u and tilde-coded functional keys: ESC [ <code> ; <mods> (u|~)
|
||
// 'u' terminator: Kitty CSI-u; '~' terminator: tilde-coded function keys.
|
||
const csiUPrefix = new RegExp(`^${ESC}\\[(\\d+)(;(\\d+))?([u~])`);
|
||
m = buffer.match(csiUPrefix);
|
||
if (m) {
|
||
const keyCode = parseInt(m[1], 10);
|
||
let modifiers = m[3] ? parseInt(m[3], 10) : KITTY_MODIFIER_BASE;
|
||
if (modifiers >= KITTY_MODIFIER_EVENT_TYPES_OFFSET) {
|
||
modifiers -= KITTY_MODIFIER_EVENT_TYPES_OFFSET;
|
||
}
|
||
const modifierBits = modifiers - KITTY_MODIFIER_BASE;
|
||
const shift =
|
||
(modifierBits & MODIFIER_SHIFT_BIT) === MODIFIER_SHIFT_BIT;
|
||
const alt = (modifierBits & MODIFIER_ALT_BIT) === MODIFIER_ALT_BIT;
|
||
const ctrl = (modifierBits & MODIFIER_CTRL_BIT) === MODIFIER_CTRL_BIT;
|
||
const terminator = m[4];
|
||
|
||
// Tilde-coded functional keys (Delete, Insert, PageUp/Down, Home/End)
|
||
if (terminator === '~') {
|
||
let name: string | null = null;
|
||
switch (keyCode) {
|
||
case 1:
|
||
name = 'home';
|
||
break;
|
||
case 2:
|
||
name = 'insert';
|
||
break;
|
||
case 3:
|
||
name = 'delete';
|
||
break;
|
||
case 4:
|
||
name = 'end';
|
||
break;
|
||
case 5:
|
||
name = 'pageup';
|
||
break;
|
||
case 6:
|
||
name = 'pagedown';
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
if (name) {
|
||
return {
|
||
key: {
|
||
name,
|
||
ctrl,
|
||
meta: alt,
|
||
shift,
|
||
paste: false,
|
||
sequence: buffer.slice(0, m[0].length),
|
||
kittyProtocol: true,
|
||
},
|
||
length: m[0].length,
|
||
};
|
||
}
|
||
}
|
||
|
||
const kittyKeyCodeToName: { [key: number]: string } = {
|
||
[CHAR_CODE_ESC]: 'escape',
|
||
[KITTY_KEYCODE_TAB]: 'tab',
|
||
[KITTY_KEYCODE_BACKSPACE]: 'backspace',
|
||
[KITTY_KEYCODE_ENTER]: 'return',
|
||
[KITTY_KEYCODE_NUMPAD_ENTER]: 'return',
|
||
};
|
||
|
||
const name = kittyKeyCodeToName[keyCode];
|
||
if (name) {
|
||
return {
|
||
key: {
|
||
name,
|
||
ctrl,
|
||
meta: alt,
|
||
shift,
|
||
paste: false,
|
||
sequence: buffer.slice(0, m[0].length),
|
||
kittyProtocol: true,
|
||
},
|
||
length: m[0].length,
|
||
};
|
||
}
|
||
|
||
// Ctrl+letters
|
||
if (
|
||
ctrl &&
|
||
keyCode >= 'a'.charCodeAt(0) &&
|
||
keyCode <= 'z'.charCodeAt(0)
|
||
) {
|
||
const letter = String.fromCharCode(keyCode);
|
||
return {
|
||
key: {
|
||
name: letter,
|
||
ctrl: true,
|
||
meta: alt,
|
||
shift,
|
||
paste: false,
|
||
sequence: buffer.slice(0, m[0].length),
|
||
kittyProtocol: true,
|
||
},
|
||
length: m[0].length,
|
||
};
|
||
}
|
||
}
|
||
|
||
// 4) Legacy function keys (no parameters): ESC [ (A|B|C|D|H|F)
|
||
// Arrows + Home/End without modifiers.
|
||
const legacyFuncKey = new RegExp(`^${ESC}\\[([ABCDHF])`);
|
||
m = buffer.match(legacyFuncKey);
|
||
if (m) {
|
||
const sym = m[1];
|
||
const nameMap: { [key: string]: string } = {
|
||
A: 'up',
|
||
B: 'down',
|
||
C: 'right',
|
||
D: 'left',
|
||
H: 'home',
|
||
F: 'end',
|
||
};
|
||
const name = nameMap[sym]!;
|
||
return {
|
||
key: {
|
||
name,
|
||
ctrl: false,
|
||
meta: false,
|
||
shift: false,
|
||
paste: false,
|
||
sequence: buffer.slice(0, m[0].length),
|
||
kittyProtocol: true,
|
||
},
|
||
length: m[0].length,
|
||
};
|
||
}
|
||
|
||
return null;
|
||
};
|
||
|
||
const broadcast = (key: Key) => {
|
||
for (const handler of subscribers) {
|
||
handler(key);
|
||
}
|
||
};
|
||
|
||
const handleKeypress = (_: unknown, key: Key) => {
|
||
if (key.name === 'paste-start') {
|
||
isPaste = true;
|
||
return;
|
||
}
|
||
if (key.name === 'paste-end') {
|
||
isPaste = false;
|
||
broadcast({
|
||
name: '',
|
||
ctrl: false,
|
||
meta: false,
|
||
shift: false,
|
||
paste: true,
|
||
sequence: pasteBuffer.toString(),
|
||
});
|
||
pasteBuffer = Buffer.alloc(0);
|
||
return;
|
||
}
|
||
|
||
if (isPaste) {
|
||
pasteBuffer = Buffer.concat([pasteBuffer, Buffer.from(key.sequence)]);
|
||
return;
|
||
}
|
||
|
||
if (
|
||
key.sequence === SINGLE_QUOTE ||
|
||
key.sequence === DOUBLE_QUOTE ||
|
||
isDraggingRef.current
|
||
) {
|
||
isDraggingRef.current = true;
|
||
dragBufferRef.current += key.sequence;
|
||
|
||
clearDraggingTimer();
|
||
draggingTimerRef.current = setTimeout(() => {
|
||
isDraggingRef.current = false;
|
||
const seq = dragBufferRef.current;
|
||
dragBufferRef.current = '';
|
||
if (seq) {
|
||
broadcast({ ...key, name: '', paste: true, sequence: seq });
|
||
}
|
||
}, DRAG_COMPLETION_TIMEOUT_MS);
|
||
|
||
return;
|
||
}
|
||
|
||
if (key.name === 'return' && waitingForEnterAfterBackslash) {
|
||
if (backslashTimeout) {
|
||
clearTimeout(backslashTimeout);
|
||
backslashTimeout = null;
|
||
}
|
||
waitingForEnterAfterBackslash = false;
|
||
broadcast({
|
||
...key,
|
||
shift: true,
|
||
sequence: '\r', // Corrected escaping for newline
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (key.sequence === '\\' && !key.name) {
|
||
// Corrected escaping for backslash
|
||
waitingForEnterAfterBackslash = true;
|
||
backslashTimeout = setTimeout(() => {
|
||
waitingForEnterAfterBackslash = false;
|
||
backslashTimeout = null;
|
||
broadcast(key);
|
||
}, BACKSLASH_ENTER_DETECTION_WINDOW_MS);
|
||
return;
|
||
}
|
||
|
||
if (waitingForEnterAfterBackslash && key.name !== 'return') {
|
||
if (backslashTimeout) {
|
||
clearTimeout(backslashTimeout);
|
||
backslashTimeout = null;
|
||
}
|
||
waitingForEnterAfterBackslash = false;
|
||
broadcast({
|
||
name: '',
|
||
sequence: '\\',
|
||
ctrl: false,
|
||
meta: false,
|
||
shift: false,
|
||
paste: false,
|
||
});
|
||
}
|
||
|
||
if (['up', 'down', 'left', 'right'].includes(key.name)) {
|
||
broadcast(key);
|
||
return;
|
||
}
|
||
|
||
if (
|
||
(key.ctrl && key.name === 'c') ||
|
||
key.sequence === `${ESC}${KITTY_CTRL_C}`
|
||
) {
|
||
if (kittySequenceBuffer && debugKeystrokeLogging) {
|
||
console.log(
|
||
'[DEBUG] Kitty buffer cleared on Ctrl+C:',
|
||
kittySequenceBuffer,
|
||
);
|
||
}
|
||
kittySequenceBuffer = '';
|
||
if (key.sequence === `${ESC}${KITTY_CTRL_C}`) {
|
||
broadcast({
|
||
name: 'c',
|
||
ctrl: true,
|
||
meta: false,
|
||
shift: false,
|
||
paste: false,
|
||
sequence: key.sequence,
|
||
kittyProtocol: true,
|
||
});
|
||
} else {
|
||
broadcast(key);
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (kittyProtocolEnabled) {
|
||
if (
|
||
kittySequenceBuffer ||
|
||
(key.sequence.startsWith(`${ESC}[`) &&
|
||
!key.sequence.startsWith(PASTE_MODE_PREFIX) &&
|
||
!key.sequence.startsWith(PASTE_MODE_SUFFIX) &&
|
||
!key.sequence.startsWith(FOCUS_IN) &&
|
||
!key.sequence.startsWith(FOCUS_OUT))
|
||
) {
|
||
kittySequenceBuffer += key.sequence;
|
||
|
||
if (debugKeystrokeLogging) {
|
||
console.log(
|
||
'[DEBUG] Kitty buffer accumulating:',
|
||
kittySequenceBuffer,
|
||
);
|
||
}
|
||
|
||
// Try to peel off as many complete sequences as are available at the
|
||
// start of the buffer. This handles batched inputs cleanly. If the
|
||
// prefix is incomplete or invalid, skip to the next CSI introducer
|
||
// (ESC[) so that a following valid sequence can still be parsed.
|
||
let parsedAny = false;
|
||
while (kittySequenceBuffer) {
|
||
const parsed = parseKittyPrefix(kittySequenceBuffer);
|
||
if (!parsed) {
|
||
// Look for the next potential CSI start beyond index 0
|
||
const nextStart = kittySequenceBuffer.indexOf(`${ESC}[`, 1);
|
||
if (nextStart > 0) {
|
||
if (debugKeystrokeLogging) {
|
||
console.log(
|
||
'[DEBUG] Skipping incomplete/invalid CSI prefix:',
|
||
kittySequenceBuffer.slice(0, nextStart),
|
||
);
|
||
}
|
||
kittySequenceBuffer = kittySequenceBuffer.slice(nextStart);
|
||
continue;
|
||
}
|
||
break;
|
||
}
|
||
if (debugKeystrokeLogging) {
|
||
const parsedSequence = kittySequenceBuffer.slice(
|
||
0,
|
||
parsed.length,
|
||
);
|
||
if (kittySequenceBuffer.length > parsed.length) {
|
||
console.log(
|
||
'[DEBUG] Kitty sequence parsed successfully (prefix):',
|
||
parsedSequence,
|
||
);
|
||
} else {
|
||
console.log(
|
||
'[DEBUG] Kitty sequence parsed successfully:',
|
||
parsedSequence,
|
||
);
|
||
}
|
||
}
|
||
// Consume the parsed prefix and broadcast it.
|
||
kittySequenceBuffer = kittySequenceBuffer.slice(parsed.length);
|
||
broadcast(parsed.key);
|
||
parsedAny = true;
|
||
}
|
||
if (parsedAny) return;
|
||
|
||
if (config?.getDebugMode() || debugKeystrokeLogging) {
|
||
const codes = Array.from(kittySequenceBuffer).map((ch) =>
|
||
ch.charCodeAt(0),
|
||
);
|
||
console.warn('Kitty sequence buffer has char codes:', codes);
|
||
}
|
||
|
||
if (kittySequenceBuffer.length > MAX_KITTY_SEQUENCE_LENGTH) {
|
||
if (debugKeystrokeLogging) {
|
||
console.log(
|
||
'[DEBUG] Kitty buffer overflow, clearing:',
|
||
kittySequenceBuffer,
|
||
);
|
||
}
|
||
if (config) {
|
||
const event = new KittySequenceOverflowEvent(
|
||
kittySequenceBuffer.length,
|
||
kittySequenceBuffer,
|
||
);
|
||
logKittySequenceOverflow(config, event);
|
||
}
|
||
kittySequenceBuffer = '';
|
||
} else {
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (key.name === 'return' && key.sequence === `${ESC}\r`) {
|
||
key.meta = true;
|
||
}
|
||
broadcast({ ...key, paste: isPaste });
|
||
};
|
||
|
||
const handleRawKeypress = (data: Buffer) => {
|
||
const pasteModePrefixBuffer = Buffer.from(PASTE_MODE_PREFIX);
|
||
const pasteModeSuffixBuffer = Buffer.from(PASTE_MODE_SUFFIX);
|
||
|
||
let pos = 0;
|
||
while (pos < data.length) {
|
||
const prefixPos = data.indexOf(pasteModePrefixBuffer, pos);
|
||
const suffixPos = data.indexOf(pasteModeSuffixBuffer, pos);
|
||
const isPrefixNext =
|
||
prefixPos !== -1 && (suffixPos === -1 || prefixPos < suffixPos);
|
||
const isSuffixNext =
|
||
suffixPos !== -1 && (prefixPos === -1 || suffixPos < prefixPos);
|
||
|
||
let nextMarkerPos = -1;
|
||
let markerLength = 0;
|
||
|
||
if (isPrefixNext) {
|
||
nextMarkerPos = prefixPos;
|
||
} else if (isSuffixNext) {
|
||
nextMarkerPos = suffixPos;
|
||
}
|
||
markerLength = pasteModeSuffixBuffer.length;
|
||
|
||
if (nextMarkerPos === -1) {
|
||
keypressStream.write(data.slice(pos));
|
||
return;
|
||
}
|
||
|
||
const nextData = data.slice(pos, nextMarkerPos);
|
||
if (nextData.length > 0) {
|
||
keypressStream.write(nextData);
|
||
}
|
||
const createPasteKeyEvent = (
|
||
name: 'paste-start' | 'paste-end',
|
||
): Key => ({
|
||
name,
|
||
ctrl: false,
|
||
meta: false,
|
||
shift: false,
|
||
paste: false,
|
||
sequence: '',
|
||
});
|
||
if (isPrefixNext) {
|
||
handleKeypress(undefined, createPasteKeyEvent('paste-start'));
|
||
} else if (isSuffixNext) {
|
||
handleKeypress(undefined, createPasteKeyEvent('paste-end'));
|
||
}
|
||
pos = nextMarkerPos + markerLength;
|
||
}
|
||
};
|
||
|
||
let rl: readline.Interface;
|
||
if (usePassthrough) {
|
||
rl = readline.createInterface({
|
||
input: keypressStream,
|
||
escapeCodeTimeout: 0,
|
||
});
|
||
readline.emitKeypressEvents(keypressStream, rl);
|
||
keypressStream.on('keypress', handleKeypress);
|
||
stdin.on('data', handleRawKeypress);
|
||
} else {
|
||
rl = readline.createInterface({ input: stdin, escapeCodeTimeout: 0 });
|
||
readline.emitKeypressEvents(stdin, rl);
|
||
stdin.on('keypress', handleKeypress);
|
||
}
|
||
|
||
return () => {
|
||
if (usePassthrough) {
|
||
keypressStream.removeListener('keypress', handleKeypress);
|
||
stdin.removeListener('data', handleRawKeypress);
|
||
} else {
|
||
stdin.removeListener('keypress', handleKeypress);
|
||
}
|
||
|
||
rl.close();
|
||
|
||
// Restore the terminal to its original state.
|
||
if (wasRaw === false) {
|
||
setRawMode(false);
|
||
}
|
||
|
||
if (backslashTimeout) {
|
||
clearTimeout(backslashTimeout);
|
||
backslashTimeout = null;
|
||
}
|
||
|
||
// Flush any pending paste data to avoid data loss on exit.
|
||
if (isPaste) {
|
||
broadcast({
|
||
name: '',
|
||
ctrl: false,
|
||
meta: false,
|
||
shift: false,
|
||
paste: true,
|
||
sequence: pasteBuffer.toString(),
|
||
});
|
||
pasteBuffer = Buffer.alloc(0);
|
||
}
|
||
|
||
if (draggingTimerRef.current) {
|
||
clearTimeout(draggingTimerRef.current);
|
||
draggingTimerRef.current = null;
|
||
}
|
||
if (isDraggingRef.current && dragBufferRef.current) {
|
||
broadcast({
|
||
name: '',
|
||
ctrl: false,
|
||
meta: false,
|
||
shift: false,
|
||
paste: true,
|
||
sequence: dragBufferRef.current,
|
||
});
|
||
isDraggingRef.current = false;
|
||
dragBufferRef.current = '';
|
||
}
|
||
};
|
||
}, [
|
||
stdin,
|
||
setRawMode,
|
||
kittyProtocolEnabled,
|
||
config,
|
||
subscribers,
|
||
debugKeystrokeLogging,
|
||
]);
|
||
|
||
return (
|
||
<KeypressContext.Provider value={{ subscribe, unsubscribe }}>
|
||
{children}
|
||
</KeypressContext.Provider>
|
||
);
|
||
}
|