mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 22:21:22 -07:00
Fix Arrow Keys and make Kitty Protocol more robust (#7118)
This commit is contained in:
@@ -451,10 +451,13 @@ describe('KeypressContext - Kitty Protocol', () => {
|
||||
'[DEBUG] Kitty buffer accumulating:',
|
||||
expect.stringContaining('\x1b[27u'),
|
||||
);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
'[DEBUG] Kitty sequence parsed successfully:',
|
||||
expect.stringContaining('\x1b[27u'),
|
||||
const parsedCall = consoleLogSpy.mock.calls.find(
|
||||
(args) =>
|
||||
typeof args[0] === 'string' &&
|
||||
args[0].includes('[DEBUG] Kitty sequence parsed successfully'),
|
||||
);
|
||||
expect(parsedCall).toBeTruthy();
|
||||
expect(parsedCall?.[1]).toEqual(expect.stringContaining('\x1b[27u'));
|
||||
});
|
||||
|
||||
it('should log kitty buffer overflow when debugKeystrokeLogging is true', async () => {
|
||||
@@ -584,4 +587,137 @@ describe('KeypressContext - Kitty Protocol', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Parameterized functional keys', () => {
|
||||
it.each([
|
||||
// Parameterized
|
||||
{ sequence: `\x1b[1;2H`, expected: { name: 'home', shift: true } },
|
||||
{ sequence: `\x1b[1;5F`, expected: { name: 'end', ctrl: true } },
|
||||
{ sequence: `\x1b[1;1P`, expected: { name: 'f1' } },
|
||||
{ sequence: `\x1b[1;3Q`, expected: { name: 'f2', meta: true } },
|
||||
{ sequence: `\x1b[3~`, expected: { name: 'delete' } },
|
||||
{ sequence: `\x1b[5~`, expected: { name: 'pageup' } },
|
||||
{ sequence: `\x1b[6~`, expected: { name: 'pagedown' } },
|
||||
{ sequence: `\x1b[1~`, expected: { name: 'home' } },
|
||||
{ sequence: `\x1b[4~`, expected: { name: 'end' } },
|
||||
{ sequence: `\x1b[2~`, expected: { name: 'insert' } },
|
||||
// Legacy Arrows
|
||||
{
|
||||
sequence: `\x1b[A`,
|
||||
expected: { name: 'up', ctrl: false, meta: false, shift: false },
|
||||
},
|
||||
{
|
||||
sequence: `\x1b[B`,
|
||||
expected: { name: 'down', ctrl: false, meta: false, shift: false },
|
||||
},
|
||||
{
|
||||
sequence: `\x1b[C`,
|
||||
expected: { name: 'right', ctrl: false, meta: false, shift: false },
|
||||
},
|
||||
{
|
||||
sequence: `\x1b[D`,
|
||||
expected: { name: 'left', ctrl: false, meta: false, shift: false },
|
||||
},
|
||||
// Legacy Home/End
|
||||
{
|
||||
sequence: `\x1b[H`,
|
||||
expected: { name: 'home', ctrl: false, meta: false, shift: false },
|
||||
},
|
||||
{
|
||||
sequence: `\x1b[F`,
|
||||
expected: { name: 'end', ctrl: false, meta: false, shift: false },
|
||||
},
|
||||
])(
|
||||
'should recognize sequence "$sequence" as $expected.name',
|
||||
({ sequence, expected }) => {
|
||||
const keyHandler = vi.fn();
|
||||
const { result } = renderHook(() => useKeypressContext(), { wrapper });
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
act(() => stdin.sendKittySequence(sequence));
|
||||
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining(expected),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('Shift+Tab forms', () => {
|
||||
it.each([
|
||||
{ sequence: `\x1b[Z`, description: 'legacy reverse Tab' },
|
||||
{ sequence: `\x1b[1;2Z`, description: 'parameterized reverse Tab' },
|
||||
])(
|
||||
'should recognize $description "$sequence" as Shift+Tab',
|
||||
({ sequence }) => {
|
||||
const keyHandler = vi.fn();
|
||||
const { result } = renderHook(() => useKeypressContext(), { wrapper });
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
act(() => stdin.sendKittySequence(sequence));
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ name: 'tab', shift: true }),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('Double-tap and batching', () => {
|
||||
it('should emit two delete events for double-tap CSI[3~', async () => {
|
||||
const keyHandler = vi.fn();
|
||||
const { result } = renderHook(() => useKeypressContext(), { wrapper });
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
act(() => stdin.sendKittySequence(`\x1b[3~`));
|
||||
act(() => stdin.sendKittySequence(`\x1b[3~`));
|
||||
|
||||
expect(keyHandler).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({ name: 'delete' }),
|
||||
);
|
||||
expect(keyHandler).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({ name: 'delete' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should parse two concatenated tilde-coded sequences in one chunk', async () => {
|
||||
const keyHandler = vi.fn();
|
||||
const { result } = renderHook(() => useKeypressContext(), { wrapper });
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
act(() => stdin.sendKittySequence(`\x1b[3~\x1b[5~`));
|
||||
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ name: 'delete' }),
|
||||
);
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ name: 'pageup' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should ignore incomplete CSI then parse the next complete sequence', async () => {
|
||||
const keyHandler = vi.fn();
|
||||
const { result } = renderHook(() => useKeypressContext(), { wrapper });
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
// Incomplete ESC sequence then a complete Delete
|
||||
act(() => {
|
||||
// Provide an incomplete ESC sequence chunk with a real ESC character
|
||||
stdin.pressKey({
|
||||
name: undefined,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
sequence: '\x1b[1;',
|
||||
});
|
||||
});
|
||||
act(() => stdin.sendKittySequence(`\x1b[3~`));
|
||||
|
||||
expect(keyHandler).toHaveBeenCalledTimes(1);
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ name: 'delete' }),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,6 +29,11 @@ import {
|
||||
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';
|
||||
@@ -116,48 +121,244 @@ export function KeypressProvider({
|
||||
let backslashTimeout: NodeJS.Timeout | null = null;
|
||||
let waitingForEnterAfterBackslash = false;
|
||||
|
||||
const parseKittySequence = (sequence: string): Key | null => {
|
||||
const kittyPattern = new RegExp(`^${ESC}\\[(\\d+)(;(\\d+))?([u~])$`);
|
||||
const match = sequence.match(kittyPattern);
|
||||
if (!match) return null;
|
||||
|
||||
const keyCode = parseInt(match[1], 10);
|
||||
const modifiers = match[3] ? parseInt(match[3], 10) : 1;
|
||||
const modifierBits = modifiers - 1;
|
||||
const shift = (modifierBits & 1) === 1;
|
||||
const alt = (modifierBits & 2) === 2;
|
||||
const ctrl = (modifierBits & 4) === 4;
|
||||
|
||||
const keyNameMap: Record<number, string> = {
|
||||
[CHAR_CODE_ESC]: 'escape',
|
||||
[KITTY_KEYCODE_TAB]: 'tab',
|
||||
[KITTY_KEYCODE_BACKSPACE]: 'backspace',
|
||||
[KITTY_KEYCODE_ENTER]: 'return',
|
||||
[KITTY_KEYCODE_NUMPAD_ENTER]: 'return',
|
||||
};
|
||||
|
||||
if (keyCode in keyNameMap) {
|
||||
// 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 {
|
||||
name: keyNameMap[keyCode],
|
||||
ctrl,
|
||||
meta: alt,
|
||||
shift,
|
||||
paste: false,
|
||||
sequence,
|
||||
kittyProtocol: true,
|
||||
key: {
|
||||
name: 'tab',
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: true,
|
||||
paste: false,
|
||||
sequence: buffer.slice(0, m[0].length),
|
||||
kittyProtocol: true,
|
||||
},
|
||||
length: m[0].length,
|
||||
};
|
||||
}
|
||||
|
||||
if (keyCode >= 97 && keyCode <= 122 && ctrl) {
|
||||
const letter = String.fromCharCode(keyCode);
|
||||
// 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 {
|
||||
name: letter,
|
||||
ctrl: true,
|
||||
meta: alt,
|
||||
shift,
|
||||
paste: false,
|
||||
sequence,
|
||||
kittyProtocol: true,
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -285,18 +486,51 @@ export function KeypressProvider({
|
||||
);
|
||||
}
|
||||
|
||||
const kittyKey = parseKittySequence(kittySequenceBuffer);
|
||||
if (kittyKey) {
|
||||
if (debugKeystrokeLogging) {
|
||||
console.log(
|
||||
'[DEBUG] Kitty sequence parsed successfully:',
|
||||
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;
|
||||
}
|
||||
kittySequenceBuffer = '';
|
||||
broadcast(kittyKey);
|
||||
return;
|
||||
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) =>
|
||||
|
||||
@@ -25,6 +25,31 @@ export const KITTY_KEYCODE_NUMPAD_ENTER = 57414;
|
||||
export const KITTY_KEYCODE_TAB = 9;
|
||||
export const KITTY_KEYCODE_BACKSPACE = 127;
|
||||
|
||||
/**
|
||||
* Kitty modifier decoding constants
|
||||
*
|
||||
* In Kitty/Ghostty, the modifier parameter is encoded as (1 + bitmask).
|
||||
* Some terminals also set bit 7 (i.e., add 128) when reporting event types.
|
||||
*/
|
||||
export const KITTY_MODIFIER_BASE = 1; // Base value per spec before bitmask decode
|
||||
export const KITTY_MODIFIER_EVENT_TYPES_OFFSET = 128; // Added when event types are included
|
||||
|
||||
/**
|
||||
* Modifier bit flags for Kitty/Xterm-style parameters.
|
||||
*
|
||||
* Per spec, the modifiers parameter encodes (1 + bitmask) where:
|
||||
* - 1: no modifiers
|
||||
* - bit 0 (1): Shift
|
||||
* - bit 1 (2): Alt/Option (reported as "alt" in spec; we map to meta)
|
||||
* - bit 2 (4): Ctrl
|
||||
*
|
||||
* Some terminals add 128 to the entire modifiers field when reporting event types.
|
||||
* See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/#modifiers
|
||||
*/
|
||||
export const MODIFIER_SHIFT_BIT = 1;
|
||||
export const MODIFIER_ALT_BIT = 2;
|
||||
export const MODIFIER_CTRL_BIT = 4;
|
||||
|
||||
/**
|
||||
* Timing constants for terminal interactions
|
||||
*/
|
||||
@@ -49,7 +74,9 @@ export const BACKSLASH_ENTER_DETECTION_WINDOW_MS = 5;
|
||||
* Longest reasonable: \x1b[127;15~ = 11 chars (Del with all modifiers)
|
||||
* We use 12 to provide a small buffer.
|
||||
*/
|
||||
export const MAX_KITTY_SEQUENCE_LENGTH = 12;
|
||||
// Increased to accommodate parameterized forms and occasional colon subfields
|
||||
// while still being small enough to avoid pathological buffering.
|
||||
export const MAX_KITTY_SEQUENCE_LENGTH = 32;
|
||||
|
||||
/**
|
||||
* Character codes for common escape sequences
|
||||
|
||||
Reference in New Issue
Block a user