Files
gemini-cli/packages/cli/src/ui/contexts/KeypressContext.tsx

801 lines
22 KiB
TypeScript
Raw Normal View History

/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
2025-11-09 08:45:04 -08:00
import { debugLogger, type Config } from '@google/gemini-cli-core';
import { useStdin } from 'ink';
import { MultiMap } from 'mnemonist';
import type React from 'react';
import {
createContext,
useCallback,
useContext,
useEffect,
useRef,
} from 'react';
2025-11-09 08:45:04 -08:00
import { ESC } from '../utils/input.js';
import { parseMouseEvent } from '../utils/mouse.js';
import { FOCUS_IN, FOCUS_OUT } from '../hooks/useFocus.js';
import { appEvents, AppEvent } from '../../utils/events.js';
import { terminalCapabilityManager } from '../utils/terminalCapabilityManager.js';
2025-11-09 08:45:04 -08:00
export const BACKSLASH_ENTER_TIMEOUT = 5;
export const ESC_TIMEOUT = 50;
export const PASTE_TIMEOUT = 30_000;
export const FAST_RETURN_TIMEOUT = 30;
2025-11-09 08:45:04 -08:00
export enum KeypressPriority {
Low = -100,
Normal = 0,
High = 100,
Critical = 200,
}
2025-11-09 14:36:13 -08:00
// Parse the key itself
const KEY_INFO_MAP: Record<
string,
{ name: string; shift?: boolean; ctrl?: boolean }
> = {
'[200~': { name: 'paste-start' },
'[201~': { name: 'paste-end' },
'[[A': { name: 'f1' },
'[[B': { name: 'f2' },
'[[C': { name: 'f3' },
'[[D': { name: 'f4' },
'[[E': { name: 'f5' },
'[1~': { name: 'home' },
'[2~': { name: 'insert' },
'[3~': { name: 'delete' },
'[4~': { name: 'end' },
'[5~': { name: 'pageup' },
'[6~': { name: 'pagedown' },
'[7~': { name: 'home' },
'[8~': { name: 'end' },
'[11~': { name: 'f1' },
'[12~': { name: 'f2' },
'[13~': { name: 'f3' },
'[14~': { name: 'f4' },
'[15~': { name: 'f5' },
'[17~': { name: 'f6' },
'[18~': { name: 'f7' },
'[19~': { name: 'f8' },
'[20~': { name: 'f9' },
'[21~': { name: 'f10' },
'[23~': { name: 'f11' },
'[24~': { name: 'f12' },
'[A': { name: 'up' },
'[B': { name: 'down' },
'[C': { name: 'right' },
'[D': { name: 'left' },
'[E': { name: 'clear' },
'[F': { name: 'end' },
'[H': { name: 'home' },
'[P': { name: 'f1' },
'[Q': { name: 'f2' },
'[R': { name: 'f3' },
'[S': { name: 'f4' },
OA: { name: 'up' },
OB: { name: 'down' },
OC: { name: 'right' },
OD: { name: 'left' },
OE: { name: 'clear' },
OF: { name: 'end' },
OH: { name: 'home' },
OP: { name: 'f1' },
OQ: { name: 'f2' },
OR: { name: 'f3' },
OS: { name: 'f4' },
OZ: { name: 'tab', shift: true }, // SS3 Shift+Tab variant for Windows terminals
2025-11-09 14:36:13 -08:00
'[[5~': { name: 'pageup' },
'[[6~': { name: 'pagedown' },
'[9u': { name: 'tab' },
'[13u': { name: 'return' },
'[27u': { name: 'escape' },
'[32u': { name: 'space' },
2025-11-09 14:36:13 -08:00
'[127u': { name: 'backspace' },
'[57414u': { name: 'return' }, // Numpad Enter
'[a': { name: 'up', shift: true },
'[b': { name: 'down', shift: true },
'[c': { name: 'right', shift: true },
'[d': { name: 'left', shift: true },
'[e': { name: 'clear', shift: true },
'[2$': { name: 'insert', shift: true },
'[3$': { name: 'delete', shift: true },
'[5$': { name: 'pageup', shift: true },
'[6$': { name: 'pagedown', shift: true },
'[7$': { name: 'home', shift: true },
'[8$': { name: 'end', shift: true },
'[Z': { name: 'tab', shift: true },
Oa: { name: 'up', ctrl: true },
Ob: { name: 'down', ctrl: true },
Oc: { name: 'right', ctrl: true },
Od: { name: 'left', ctrl: true },
Oe: { name: 'clear', ctrl: true },
'[2^': { name: 'insert', ctrl: true },
'[3^': { name: 'delete', ctrl: true },
'[5^': { name: 'pageup', ctrl: true },
'[6^': { name: 'pagedown', ctrl: true },
'[7^': { name: 'home', ctrl: true },
'[8^': { name: 'end', ctrl: true },
};
2025-11-09 08:45:04 -08:00
const kUTF16SurrogateThreshold = 0x10000; // 2 ** 16
function charLengthAt(str: string, i: number): number {
if (str.length <= i) {
// Pretend to move to the right. This is necessary to autocomplete while
// moving to the right.
return 1;
}
const code = str.codePointAt(i);
return code !== undefined && code >= kUTF16SurrogateThreshold ? 2 : 1;
}
// Note: we do not convert alt+z, alt+shift+z, or alt+v here
// because mac users have alternative hotkeys.
2025-10-29 14:32:02 -07:00
const MAC_ALT_KEY_CHARACTER_MAP: Record<string, string> = {
'\u222B': 'b', // "∫" back one word
'\u0192': 'f', // "ƒ" forward one word
'\u00B5': 'm', // "µ" toggle markup view
2026-02-05 10:54:46 -08:00
'\u03A9': 'z', // "Ω" Option+z
'\u00B8': 'Z', // "¸" Option+Shift+z
'\u2202': 'd', // "∂" delete word forward
};
2025-11-09 08:45:04 -08:00
function nonKeyboardEventFilter(
keypressHandler: KeypressHandler,
): KeypressHandler {
return (key: Key) => {
if (
!parseMouseEvent(key.sequence) &&
key.sequence !== FOCUS_IN &&
key.sequence !== FOCUS_OUT
) {
keypressHandler(key);
}
};
}
/**
* Converts return keys pressed quickly after other keys into plain
* insertable return characters.
*
* This is to accommodate older terminals that paste text without bracketing.
*/
function bufferFastReturn(keypressHandler: KeypressHandler): KeypressHandler {
let lastKeyTime = 0;
return (key: Key) => {
const now = Date.now();
if (key.name === 'return' && now - lastKeyTime <= FAST_RETURN_TIMEOUT) {
keypressHandler({
...key,
2026-01-14 16:30:07 -08:00
name: 'return',
shift: true, // to make it a newline, not a submission
alt: false,
ctrl: false,
cmd: false,
sequence: '\r',
insertable: true,
});
} else {
keypressHandler(key);
}
lastKeyTime = now;
};
}
2025-10-22 17:51:03 -07:00
/**
2025-11-09 08:45:04 -08:00
* Buffers "/" keys to see if they are followed return.
* Will flush the buffer if no data is received for DRAG_COMPLETION_TIMEOUT_MS
* or when a null key is received.
2025-10-22 17:51:03 -07:00
*/
2025-11-09 08:45:04 -08:00
function bufferBackslashEnter(
keypressHandler: KeypressHandler,
): KeypressHandler {
2025-11-09 08:45:04 -08:00
const bufferer = (function* (): Generator<void, void, Key | null> {
while (true) {
const key = yield;
2025-10-22 17:51:03 -07:00
2025-11-09 08:45:04 -08:00
if (key == null) {
continue;
2025-11-09 14:08:29 -08:00
} else if (key.sequence !== '\\') {
2025-11-09 08:45:04 -08:00
keypressHandler(key);
continue;
}
2025-10-22 17:51:03 -07:00
2025-11-09 08:45:04 -08:00
const timeoutId = setTimeout(
() => bufferer.next(null),
BACKSLASH_ENTER_TIMEOUT,
);
const nextKey = yield;
clearTimeout(timeoutId);
if (nextKey === null) {
keypressHandler(key);
} else if (nextKey.name === 'return') {
keypressHandler({
2025-11-09 14:08:29 -08:00
...nextKey,
2025-11-09 08:45:04 -08:00
shift: true,
sequence: '\r', // Corrected escaping for newline
});
} else {
keypressHandler(key);
keypressHandler(nextKey);
}
}
})();
2025-10-22 17:51:03 -07:00
2025-11-09 08:45:04 -08:00
bufferer.next(); // prime the generator so it starts listening.
2025-10-22 17:51:03 -07:00
return (key: Key) => {
bufferer.next(key);
};
}
2025-10-22 17:51:03 -07:00
/**
2025-11-09 08:45:04 -08:00
* Buffers paste events between paste-start and paste-end sequences.
* Will flush the buffer if no data is received for PASTE_TIMEOUT ms or
* when a null key is received.
2025-10-22 17:51:03 -07:00
*/
function bufferPaste(keypressHandler: KeypressHandler): KeypressHandler {
2025-11-09 08:45:04 -08:00
const bufferer = (function* (): Generator<void, void, Key | null> {
while (true) {
let key = yield;
2025-10-22 17:51:03 -07:00
2025-11-09 08:45:04 -08:00
if (key === null) {
continue;
} else if (key.name !== 'paste-start') {
keypressHandler(key);
continue;
2025-10-22 17:51:03 -07:00
}
2025-11-09 08:45:04 -08:00
let buffer = '';
while (true) {
const timeoutId = setTimeout(() => bufferer.next(null), PASTE_TIMEOUT);
key = yield;
clearTimeout(timeoutId);
2025-10-22 17:51:03 -07:00
if (key === null) {
appEvents.emit(AppEvent.PasteTimeout);
break;
}
if (key.name === 'paste-end') {
2025-11-09 08:45:04 -08:00
break;
}
buffer += key.sequence;
}
2025-10-22 17:51:03 -07:00
2025-11-09 08:45:04 -08:00
if (buffer.length > 0) {
keypressHandler({
2026-01-14 13:27:36 -08:00
name: 'paste',
2025-11-09 08:45:04 -08:00
shift: false,
alt: false,
ctrl: false,
cmd: false,
insertable: true,
2025-11-09 08:45:04 -08:00
sequence: buffer,
});
}
2025-10-22 17:51:03 -07:00
}
2025-11-09 08:45:04 -08:00
})();
bufferer.next(); // prime the generator so it starts listening.
2025-10-22 17:51:03 -07:00
return (key: Key) => {
bufferer.next(key);
};
2025-10-22 17:51:03 -07:00
}
/**
2025-11-09 08:45:04 -08:00
* Turns raw data strings into keypress events sent to the provided handler.
* Buffers escape sequences until a full sequence is received or
* until a timeout occurs.
*/
2025-11-09 08:45:04 -08:00
function createDataListener(keypressHandler: KeypressHandler) {
const parser = emitKeys(keypressHandler);
parser.next(); // prime the generator so it starts listening.
let timeoutId: NodeJS.Timeout;
return (data: string) => {
clearTimeout(timeoutId);
for (const char of data) {
parser.next(char);
}
2025-11-09 08:45:04 -08:00
if (data.length !== 0) {
timeoutId = setTimeout(() => parser.next(''), ESC_TIMEOUT);
}
};
}
/**
2025-11-09 08:45:04 -08:00
* Translates raw keypress characters into key events.
* Buffers escape sequences until a full sequence is received or
* until an empty string is sent to indicate a timeout.
*/
2025-11-09 08:45:04 -08:00
function* emitKeys(
keypressHandler: KeypressHandler,
): Generator<void, void, string> {
2026-02-05 10:54:46 -08:00
const lang = process.env['LANG'] || '';
const lcAll = process.env['LC_ALL'] || '';
const isGreek = lang.startsWith('el') || lcAll.startsWith('el');
while (true) {
2025-11-09 08:45:04 -08:00
let ch = yield;
let sequence = ch;
let escaped = false;
let name = undefined;
let shift = false;
let alt = false;
let ctrl = false;
let cmd = false;
2025-11-09 08:45:04 -08:00
let code = undefined;
let insertable = false;
2025-11-09 08:45:04 -08:00
if (ch === ESC) {
escaped = true;
ch = yield;
sequence += ch;
if (ch === ESC) {
ch = yield;
sequence += ch;
}
}
if (escaped && (ch === 'O' || ch === '[' || ch === ']')) {
2025-11-09 08:45:04 -08:00
// ANSI escape sequence
code = ch;
let modifier = 0;
if (ch === ']') {
// OSC sequence
// ESC ] <params> ; <data> BEL
// ESC ] <params> ; <data> ESC \
let buffer = '';
// Read until BEL, `ESC \`, or timeout (empty string)
while (true) {
const next = yield;
if (next === '' || next === '\u0007') {
break;
} else if (next === ESC) {
const afterEsc = yield;
if (afterEsc === '' || afterEsc === '\\') {
break;
}
buffer += next + afterEsc;
continue;
}
buffer += next;
}
// Check for OSC 52 (Clipboard) response
// Format: 52;c;<base64> or 52;p;<base64>
const match = /^52;[cp];(.*)$/.exec(buffer);
if (match) {
try {
const base64Data = match[1];
const decoded = Buffer.from(base64Data, 'base64').toString('utf-8');
keypressHandler({
name: 'paste',
shift: false,
alt: false,
ctrl: false,
cmd: false,
insertable: true,
sequence: decoded,
});
} catch (_e) {
debugLogger.log('Failed to decode OSC 52 clipboard data');
}
}
continue; // resume main loop
} else if (ch === 'O') {
2025-11-09 08:45:04 -08:00
// ESC O letter
// ESC O modifier letter
ch = yield;
sequence += ch;
if (ch >= '0' && ch <= '9') {
modifier = parseInt(ch, 10) - 1;
ch = yield;
sequence += ch;
}
code += ch;
} else if (ch === '[') {
// ESC [ letter
// ESC [ modifier letter
// ESC [ [ modifier letter
// ESC [ [ num char
ch = yield;
sequence += ch;
if (ch === '[') {
// \x1b[[A
// ^--- escape codes might have a second bracket
code += ch;
ch = yield;
sequence += ch;
}
/*
* Here and later we try to buffer just enough data to get
* a complete ascii sequence.
*
* We have basically two classes of ascii characters to process:
*
*
* 1. `\x1b[24;5~` should be parsed as { code: '[24~', modifier: 5 }
*
* This particular example is featuring Ctrl+F12 in xterm.
*
* - `;5` part is optional, e.g. it could be `\x1b[24~`
* - first part can contain one or two digits
* - there is also special case when there can be 3 digits
* but without modifier. They are the case of paste bracket mode
*
* So the generic regexp is like /^(?:\d\d?(;\d)?[~^$]|\d{3}~)$/
*
*
* 2. `\x1b[1;5H` should be parsed as { code: '[H', modifier: 5 }
*
* This particular example is featuring Ctrl+Home in xterm.
*
* - `1;5` part is optional, e.g. it could be `\x1b[H`
* - `1;` part is optional, e.g. it could be `\x1b[5H`
*
* So the generic regexp is like /^((\d;)?\d)?[A-Za-z]$/
*
*/
const cmdStart = sequence.length - 1;
// collect as many digits as possible
while (ch >= '0' && ch <= '9') {
ch = yield;
sequence += ch;
}
// skip modifier
if (ch === ';') {
while (ch === ';') {
2025-11-09 08:45:04 -08:00
ch = yield;
sequence += ch;
// collect as many digits as possible
while (ch >= '0' && ch <= '9') {
ch = yield;
sequence += ch;
}
2025-11-09 08:45:04 -08:00
}
} else if (ch === '<') {
// SGR mouse mode
ch = yield;
sequence += ch;
// Don't skip on empty string here to avoid timeouts on slow events.
while (ch === '' || ch === ';' || (ch >= '0' && ch <= '9')) {
ch = yield;
sequence += ch;
}
} else if (ch === 'M') {
// X11 mouse mode
// three characters after 'M'
ch = yield;
sequence += ch;
ch = yield;
sequence += ch;
ch = yield;
sequence += ch;
}
/*
* We buffered enough data, now trying to extract code
* and modifier from it
*/
const cmd = sequence.slice(cmdStart);
let match;
if ((match = /^(\d+)(?:;(\d+))?(?:;(\d+))?([~^$u])$/.exec(cmd))) {
if (match[1] === '27' && match[3] && match[4] === '~') {
// modifyOtherKeys format: CSI 27 ; modifier ; key ~
// Treat as CSI u: key + 'u'
code += match[3] + 'u';
modifier = parseInt(match[2] ?? '1', 10) - 1;
} else {
code += match[1] + match[4];
// Defaults to '1' if no modifier exists, resulting in a 0 modifier value
modifier = parseInt(match[2] ?? '1', 10) - 1;
}
2025-11-18 11:49:08 -08:00
} else if ((match = /^(\d+)?(?:;(\d+))?([A-Za-z])$/.exec(cmd))) {
code += match[3];
modifier = parseInt(match[2] ?? match[1] ?? '1', 10) - 1;
2025-11-09 08:45:04 -08:00
} else {
code += cmd;
}
}
2025-11-09 08:45:04 -08:00
// Parse the key modifier
shift = !!(modifier & 1);
alt = !!(modifier & 2);
ctrl = !!(modifier & 4);
cmd = !!(modifier & 8);
2025-11-09 08:45:04 -08:00
2025-11-09 14:36:13 -08:00
const keyInfo = KEY_INFO_MAP[code];
if (keyInfo) {
name = keyInfo.name;
if (keyInfo.shift) {
2025-11-09 08:45:04 -08:00
shift = true;
2025-11-09 14:36:13 -08:00
}
if (keyInfo.ctrl) {
2025-11-09 08:45:04 -08:00
ctrl = true;
2025-11-09 14:36:13 -08:00
}
if (name === 'space' && !ctrl && !cmd && !alt) {
sequence = ' ';
insertable = true;
}
2025-11-09 14:36:13 -08:00
} else {
name = 'undefined';
if (
(ctrl || cmd || alt) &&
(code.endsWith('u') || code.endsWith('~'))
) {
2025-11-09 14:36:13 -08:00
// CSI-u or tilde-coded functional keys: ESC [ <code> ; <mods> (u|~)
const codeNumber = parseInt(code.slice(1, -1), 10);
if (
codeNumber >= 'a'.charCodeAt(0) &&
codeNumber <= 'z'.charCodeAt(0)
) {
name = String.fromCharCode(codeNumber);
2025-11-09 08:45:04 -08:00
}
2025-11-09 14:36:13 -08:00
}
}
2025-11-09 08:45:04 -08:00
} else if (ch === '\r') {
// carriage return
name = 'return';
alt = escaped;
} else if (escaped && ch === '\n') {
// Alt+Enter (linefeed), should be consistent with carriage return
name = 'return';
alt = escaped;
2025-11-09 08:45:04 -08:00
} else if (ch === '\t') {
// tab
name = 'tab';
alt = escaped;
2025-11-09 08:45:04 -08:00
} else if (ch === '\b' || ch === '\x7f') {
// backspace or ctrl+h
name = 'backspace';
alt = escaped;
2025-11-09 08:45:04 -08:00
} else if (ch === ESC) {
// escape key
name = 'escape';
alt = escaped;
2025-11-09 08:45:04 -08:00
} else if (ch === ' ') {
name = 'space';
alt = escaped;
insertable = true;
2025-11-09 08:45:04 -08:00
} else if (!escaped && ch <= '\x1a') {
// ctrl+letter
name = String.fromCharCode(ch.charCodeAt(0) + 'a'.charCodeAt(0) - 1);
ctrl = true;
} else if (/^[0-9A-Za-z]$/.exec(ch) !== null) {
// Letter, number, shift+letter
name = ch.toLowerCase();
shift = /^[A-Z]$/.exec(ch) !== null;
alt = escaped;
insertable = true;
} else if (MAC_ALT_KEY_CHARACTER_MAP[ch]) {
// Note: we do this even if we are not on Mac, because mac users may
// remotely connect to non-Mac systems.
2026-02-05 10:54:46 -08:00
// We skip this mapping for Greek users to avoid blocking the Omega character.
if (isGreek && ch === '\u03A9') {
insertable = true;
} else {
const mapped = MAC_ALT_KEY_CHARACTER_MAP[ch];
name = mapped.toLowerCase();
shift = mapped !== name;
alt = true;
}
2025-11-09 08:45:04 -08:00
} else if (sequence === `${ESC}${ESC}`) {
// Double escape
name = 'escape';
alt = true;
2025-11-09 08:45:04 -08:00
// Emit first escape key here, then continue processing
keypressHandler({
name: 'escape',
shift,
alt,
ctrl,
cmd,
insertable: false,
2025-11-09 08:45:04 -08:00
sequence: ESC,
});
} else if (escaped) {
// Escape sequence timeout
name = ch.length ? undefined : 'escape';
alt = true;
} else {
// Any other character is considered printable.
insertable = true;
}
2025-11-09 08:45:04 -08:00
if (
(sequence.length !== 0 && (name !== undefined || escaped)) ||
charLengthAt(sequence, 0) === sequence.length
) {
keypressHandler({
name: name || '',
shift,
alt,
ctrl,
cmd,
insertable,
2025-11-09 08:45:04 -08:00
sequence,
});
}
// Unrecognized or broken escape sequence, don't emit anything
}
}
export interface Key {
name: string;
shift: boolean;
alt: boolean;
ctrl: boolean;
cmd: boolean; // Command/Windows/Super key
insertable: boolean;
sequence: string;
}
export type KeypressHandler = (key: Key) => boolean | void;
interface KeypressContextValue {
subscribe: (
handler: KeypressHandler,
priority?: KeypressPriority | boolean,
) => 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,
config,
debugKeystrokeLogging,
}: {
children: React.ReactNode;
config?: Config;
debugKeystrokeLogging?: boolean;
}) {
const { stdin, setRawMode } = useStdin();
const subscribersToPriority = useRef<Map<KeypressHandler, number>>(
new Map(),
).current;
const subscribers = useRef(
new MultiMap<number, KeypressHandler>(Set),
).current;
const sortedPriorities = useRef<number[]>([]);
const subscribe = useCallback(
(
handler: KeypressHandler,
priority: KeypressPriority | boolean = KeypressPriority.Normal,
) => {
const p =
typeof priority === 'boolean'
? priority
? KeypressPriority.High
: KeypressPriority.Normal
: priority;
subscribersToPriority.set(handler, p);
const hadPriority = subscribers.has(p);
subscribers.set(p, handler);
if (!hadPriority) {
// Cache sorted priorities only when a new priority level is added
sortedPriorities.current = Array.from(subscribers.keys()).sort(
(a, b) => b - a,
);
}
},
[subscribers, subscribersToPriority],
);
const unsubscribe = useCallback(
(handler: KeypressHandler) => {
const p = subscribersToPriority.get(handler);
if (p !== undefined) {
subscribers.remove(p, handler);
subscribersToPriority.delete(handler);
if (!subscribers.has(p)) {
// Cache sorted priorities only when a priority level is completely removed
sortedPriorities.current = Array.from(subscribers.keys()).sort(
(a, b) => b - a,
);
}
}
},
[subscribers, subscribersToPriority],
2025-10-22 17:51:03 -07:00
);
2025-10-22 17:51:03 -07:00
const broadcast = useCallback(
(key: Key) => {
// Use cached sorted priorities to avoid sorting on every keypress
for (const p of sortedPriorities.current) {
const set = subscribers.get(p);
if (!set) continue;
// Within a priority level, use stack behavior (last subscribed is first to handle)
const handlers = Array.from(set).reverse();
for (const handler of handlers) {
if (handler(key) === true) {
return;
}
}
}
},
[subscribers],
);
useEffect(() => {
const wasRaw = stdin.isRaw;
if (wasRaw === false) {
setRawMode(true);
}
2025-11-09 08:45:04 -08:00
process.stdin.setEncoding('utf8'); // Make data events emit strings
let processor = nonKeyboardEventFilter(broadcast);
if (!terminalCapabilityManager.isKittyProtocolEnabled()) {
processor = bufferFastReturn(processor);
}
processor = bufferBackslashEnter(processor);
processor = bufferPaste(processor);
let dataListener = createDataListener(processor);
2025-11-09 08:45:04 -08:00
if (debugKeystrokeLogging) {
const old = dataListener;
dataListener = (data: string) => {
if (data.length > 0) {
debugLogger.log(`[DEBUG] Raw StdIn: ${JSON.stringify(data)}`);
2025-11-03 13:41:58 -08:00
}
2025-11-09 08:45:04 -08:00
old(data);
};
}
2025-11-09 08:45:04 -08:00
stdin.on('data', dataListener);
return () => {
2025-11-09 08:45:04 -08:00
stdin.removeListener('data', dataListener);
if (wasRaw === false) {
setRawMode(false);
}
};
2025-11-09 08:45:04 -08:00
}, [stdin, setRawMode, config, debugKeystrokeLogging, broadcast]);
return (
<KeypressContext.Provider value={{ subscribe, unsubscribe }}>
{children}
</KeypressContext.Provider>
);
}