@google/gemini-cli 0.14.0-preview.0 → 0.15.0-nightly.20251110.c0b766ad

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/dist/google-gemini-cli-0.15.0-nightly.20251107.b8eeb553.tgz +0 -0
  2. package/dist/package.json +3 -3
  3. package/dist/src/config/config.d.ts +1 -8
  4. package/dist/src/config/config.js +3 -23
  5. package/dist/src/config/config.js.map +1 -1
  6. package/dist/src/config/config.test.js +11 -11
  7. package/dist/src/config/config.test.js.map +1 -1
  8. package/dist/src/config/extension-manager.js +8 -7
  9. package/dist/src/config/extension-manager.js.map +1 -1
  10. package/dist/src/config/extensions/github.d.ts +1 -1
  11. package/dist/src/config/extensions/github.js +8 -2
  12. package/dist/src/config/extensions/github.js.map +1 -1
  13. package/dist/src/config/extensions/github.test.js +8 -10
  14. package/dist/src/config/extensions/github.test.js.map +1 -1
  15. package/dist/src/gemini.js +2 -2
  16. package/dist/src/gemini.js.map +1 -1
  17. package/dist/src/generated/git-commit.d.ts +2 -2
  18. package/dist/src/generated/git-commit.js +2 -2
  19. package/dist/src/generated/git-commit.js.map +1 -1
  20. package/dist/src/services/FileCommandLoader.js +4 -2
  21. package/dist/src/services/FileCommandLoader.js.map +1 -1
  22. package/dist/src/services/FileCommandLoader.test.js +37 -0
  23. package/dist/src/services/FileCommandLoader.test.js.map +1 -1
  24. package/dist/src/test-utils/render.d.ts +1 -2
  25. package/dist/src/test-utils/render.js +2 -2
  26. package/dist/src/test-utils/render.js.map +1 -1
  27. package/dist/src/ui/AppContainer.js +14 -13
  28. package/dist/src/ui/AppContainer.js.map +1 -1
  29. package/dist/src/ui/commands/directoryCommand.js +2 -10
  30. package/dist/src/ui/commands/directoryCommand.js.map +1 -1
  31. package/dist/src/ui/commands/extensionsCommand.js +91 -2
  32. package/dist/src/ui/commands/extensionsCommand.js.map +1 -1
  33. package/dist/src/ui/commands/extensionsCommand.test.js +125 -1
  34. package/dist/src/ui/commands/extensionsCommand.test.js.map +1 -1
  35. package/dist/src/ui/commands/memoryCommand.js +2 -10
  36. package/dist/src/ui/commands/memoryCommand.js.map +1 -1
  37. package/dist/src/ui/commands/memoryCommand.test.js +11 -28
  38. package/dist/src/ui/commands/memoryCommand.test.js.map +1 -1
  39. package/dist/src/ui/commands/types.d.ts +0 -1
  40. package/dist/src/ui/commands/types.js.map +1 -1
  41. package/dist/src/ui/components/HistoryItemDisplay.js +1 -1
  42. package/dist/src/ui/components/HistoryItemDisplay.js.map +1 -1
  43. package/dist/src/ui/components/InputPrompt.test.js +35 -26
  44. package/dist/src/ui/components/InputPrompt.test.js.map +1 -1
  45. package/dist/src/ui/components/SettingsDialog.test.js +4 -4
  46. package/dist/src/ui/components/SettingsDialog.test.js.map +1 -1
  47. package/dist/src/ui/components/ThemeDialog.test.js +3 -3
  48. package/dist/src/ui/components/ThemeDialog.test.js.map +1 -1
  49. package/dist/src/ui/components/messages/InfoMessage.d.ts +2 -0
  50. package/dist/src/ui/components/messages/InfoMessage.js +4 -3
  51. package/dist/src/ui/components/messages/InfoMessage.js.map +1 -1
  52. package/dist/src/ui/components/shared/Scrollable.js +9 -4
  53. package/dist/src/ui/components/shared/Scrollable.js.map +1 -1
  54. package/dist/src/ui/components/shared/Scrollable.test.js +39 -1
  55. package/dist/src/ui/components/shared/Scrollable.test.js.map +1 -1
  56. package/dist/src/ui/components/shared/ScrollableList.test.js +1 -1
  57. package/dist/src/ui/components/shared/ScrollableList.test.js.map +1 -1
  58. package/dist/src/ui/components/shared/VirtualizedList.js +10 -3
  59. package/dist/src/ui/components/shared/VirtualizedList.js.map +1 -1
  60. package/dist/src/ui/components/shared/VirtualizedList.test.js +23 -0
  61. package/dist/src/ui/components/shared/VirtualizedList.test.js.map +1 -1
  62. package/dist/src/ui/contexts/KeypressContext.d.ts +5 -11
  63. package/dist/src/ui/contexts/KeypressContext.js +446 -755
  64. package/dist/src/ui/contexts/KeypressContext.js.map +1 -1
  65. package/dist/src/ui/contexts/KeypressContext.test.js +103 -334
  66. package/dist/src/ui/contexts/KeypressContext.test.js.map +1 -1
  67. package/dist/src/ui/contexts/ScrollProvider.js +25 -4
  68. package/dist/src/ui/contexts/ScrollProvider.js.map +1 -1
  69. package/dist/src/ui/contexts/ScrollProvider.test.d.ts +6 -0
  70. package/dist/src/ui/contexts/ScrollProvider.test.js +173 -0
  71. package/dist/src/ui/contexts/ScrollProvider.test.js.map +1 -0
  72. package/dist/src/ui/hooks/atCommandProcessor.test.js +1 -0
  73. package/dist/src/ui/hooks/atCommandProcessor.test.js.map +1 -1
  74. package/dist/src/ui/hooks/slashCommandProcessor.d.ts +1 -1
  75. package/dist/src/ui/hooks/slashCommandProcessor.js +1 -3
  76. package/dist/src/ui/hooks/slashCommandProcessor.js.map +1 -1
  77. package/dist/src/ui/hooks/slashCommandProcessor.test.js +1 -2
  78. package/dist/src/ui/hooks/slashCommandProcessor.test.js.map +1 -1
  79. package/dist/src/ui/hooks/useBatchedScroll.d.ts +14 -0
  80. package/dist/src/ui/hooks/useBatchedScroll.js +27 -0
  81. package/dist/src/ui/hooks/useBatchedScroll.js.map +1 -0
  82. package/dist/src/ui/hooks/useBatchedScroll.test.d.ts +6 -0
  83. package/dist/src/ui/hooks/useBatchedScroll.test.js +62 -0
  84. package/dist/src/ui/hooks/useBatchedScroll.test.js.map +1 -0
  85. package/dist/src/ui/hooks/useFocus.test.js +10 -10
  86. package/dist/src/ui/hooks/useFocus.test.js.map +1 -1
  87. package/dist/src/ui/hooks/useKeypress.test.js +8 -14
  88. package/dist/src/ui/hooks/useKeypress.test.js.map +1 -1
  89. package/dist/src/ui/noninteractive/nonInteractiveUi.js +0 -1
  90. package/dist/src/ui/noninteractive/nonInteractiveUi.js.map +1 -1
  91. package/dist/src/ui/state/extensions.d.ts +5 -0
  92. package/dist/src/ui/state/extensions.js +12 -0
  93. package/dist/src/ui/state/extensions.js.map +1 -1
  94. package/dist/src/ui/state/extensions.test.d.ts +6 -0
  95. package/dist/src/ui/state/extensions.test.js +62 -0
  96. package/dist/src/ui/state/extensions.test.js.map +1 -0
  97. package/dist/src/ui/types.d.ts +6 -0
  98. package/dist/src/ui/types.js +4 -0
  99. package/dist/src/ui/types.js.map +1 -1
  100. package/dist/src/ui/utils/terminalSetup.d.ts +1 -0
  101. package/dist/src/ui/utils/terminalSetup.js +1 -1
  102. package/dist/src/ui/utils/terminalSetup.js.map +1 -1
  103. package/dist/tsconfig.tsbuildinfo +1 -1
  104. package/package.json +4 -4
  105. package/dist/src/ui/utils/platformConstants.d.ts +0 -75
  106. package/dist/src/ui/utils/platformConstants.js +0 -78
  107. package/dist/src/ui/utils/platformConstants.js.map +0 -1
@@ -1,382 +1,461 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { debugLogger, KittySequenceOverflowEvent, logKittySequenceOverflow, } from '@google/gemini-cli-core';
2
+ /**
3
+ * @license
4
+ * Copyright 2025 Google LLC
5
+ * SPDX-License-Identifier: Apache-2.0
6
+ */
7
+ import { debugLogger } from '@google/gemini-cli-core';
3
8
  import { useStdin } from 'ink';
4
9
  import { createContext, useCallback, useContext, useEffect, useRef, } from 'react';
5
- import readline from 'node:readline';
6
- import { PassThrough } from 'node:stream';
7
- 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';
8
- import { ESC, couldBeMouseSequence } from '../utils/input.js';
10
+ import { ESC } from '../utils/input.js';
11
+ import { parseMouseEvent } from '../utils/mouse.js';
9
12
  import { FOCUS_IN, FOCUS_OUT } from '../hooks/useFocus.js';
10
- import { isIncompleteMouseSequence, parseMouseEvent } from '../utils/mouse.js';
11
- export const PASTE_MODE_START = `${ESC}[200~`;
12
- export const PASTE_MODE_END = `${ESC}[201~`;
13
- export const DRAG_COMPLETION_TIMEOUT_MS = 100; // Broadcast full path after 100ms if no more input
14
- export const KITTY_SEQUENCE_TIMEOUT_MS = 50; // Flush incomplete kitty sequences after 50ms
15
- export const PASTE_CODE_TIMEOUT_MS = 50; // Flush incomplete paste code after 50ms
16
- export const SINGLE_QUOTE = "'";
17
- export const DOUBLE_QUOTE = '"';
18
- // On Mac, hitting alt+char will yield funny characters.
19
- // Remap these three since we listen for them.
13
+ export const BACKSLASH_ENTER_TIMEOUT = 5;
14
+ export const ESC_TIMEOUT = 50;
15
+ export const PASTE_TIMEOUT = 50;
16
+ // Parse the key itself
17
+ const KEY_INFO_MAP = {
18
+ '[200~': { name: 'paste-start' },
19
+ '[201~': { name: 'paste-end' },
20
+ '[[A': { name: 'f1' },
21
+ '[[B': { name: 'f2' },
22
+ '[[C': { name: 'f3' },
23
+ '[[D': { name: 'f4' },
24
+ '[[E': { name: 'f5' },
25
+ '[1~': { name: 'home' },
26
+ '[2~': { name: 'insert' },
27
+ '[3~': { name: 'delete' },
28
+ '[4~': { name: 'end' },
29
+ '[5~': { name: 'pageup' },
30
+ '[6~': { name: 'pagedown' },
31
+ '[7~': { name: 'home' },
32
+ '[8~': { name: 'end' },
33
+ '[11~': { name: 'f1' },
34
+ '[12~': { name: 'f2' },
35
+ '[13~': { name: 'f3' },
36
+ '[14~': { name: 'f4' },
37
+ '[15~': { name: 'f5' },
38
+ '[17~': { name: 'f6' },
39
+ '[18~': { name: 'f7' },
40
+ '[19~': { name: 'f8' },
41
+ '[20~': { name: 'f9' },
42
+ '[21~': { name: 'f10' },
43
+ '[23~': { name: 'f11' },
44
+ '[24~': { name: 'f12' },
45
+ '[A': { name: 'up' },
46
+ '[B': { name: 'down' },
47
+ '[C': { name: 'right' },
48
+ '[D': { name: 'left' },
49
+ '[E': { name: 'clear' },
50
+ '[F': { name: 'end' },
51
+ '[H': { name: 'home' },
52
+ '[P': { name: 'f1' },
53
+ '[Q': { name: 'f2' },
54
+ '[R': { name: 'f3' },
55
+ '[S': { name: 'f4' },
56
+ OA: { name: 'up' },
57
+ OB: { name: 'down' },
58
+ OC: { name: 'right' },
59
+ OD: { name: 'left' },
60
+ OE: { name: 'clear' },
61
+ OF: { name: 'end' },
62
+ OH: { name: 'home' },
63
+ OP: { name: 'f1' },
64
+ OQ: { name: 'f2' },
65
+ OR: { name: 'f3' },
66
+ OS: { name: 'f4' },
67
+ '[[5~': { name: 'pageup' },
68
+ '[[6~': { name: 'pagedown' },
69
+ '[9u': { name: 'tab' },
70
+ '[13u': { name: 'return' },
71
+ '[27u': { name: 'escape' },
72
+ '[127u': { name: 'backspace' },
73
+ '[57414u': { name: 'return' }, // Numpad Enter
74
+ '[a': { name: 'up', shift: true },
75
+ '[b': { name: 'down', shift: true },
76
+ '[c': { name: 'right', shift: true },
77
+ '[d': { name: 'left', shift: true },
78
+ '[e': { name: 'clear', shift: true },
79
+ '[2$': { name: 'insert', shift: true },
80
+ '[3$': { name: 'delete', shift: true },
81
+ '[5$': { name: 'pageup', shift: true },
82
+ '[6$': { name: 'pagedown', shift: true },
83
+ '[7$': { name: 'home', shift: true },
84
+ '[8$': { name: 'end', shift: true },
85
+ '[Z': { name: 'tab', shift: true },
86
+ Oa: { name: 'up', ctrl: true },
87
+ Ob: { name: 'down', ctrl: true },
88
+ Oc: { name: 'right', ctrl: true },
89
+ Od: { name: 'left', ctrl: true },
90
+ Oe: { name: 'clear', ctrl: true },
91
+ '[2^': { name: 'insert', ctrl: true },
92
+ '[3^': { name: 'delete', ctrl: true },
93
+ '[5^': { name: 'pageup', ctrl: true },
94
+ '[6^': { name: 'pagedown', ctrl: true },
95
+ '[7^': { name: 'home', ctrl: true },
96
+ '[8^': { name: 'end', ctrl: true },
97
+ };
98
+ const kUTF16SurrogateThreshold = 0x10000; // 2 ** 16
99
+ function charLengthAt(str, i) {
100
+ if (str.length <= i) {
101
+ // Pretend to move to the right. This is necessary to autocomplete while
102
+ // moving to the right.
103
+ return 1;
104
+ }
105
+ const code = str.codePointAt(i);
106
+ return code !== undefined && code >= kUTF16SurrogateThreshold ? 2 : 1;
107
+ }
20
108
  const MAC_ALT_KEY_CHARACTER_MAP = {
21
109
  '\u222B': 'b', // "∫" back one word
22
110
  '\u0192': 'f', // "ƒ" forward one word
23
111
  '\u00B5': 'm', // "µ" toggle markup view
24
112
  };
25
- /**
26
- * Maps symbols from parameterized functional keys `\x1b[1;1<letter>`
27
- * to their corresponding key names (e.g., 'up', 'f1').
28
- */
29
- const LEGACY_FUNC_TO_NAME = {
30
- A: 'up',
31
- B: 'down',
32
- C: 'right',
33
- D: 'left',
34
- H: 'home',
35
- F: 'end',
36
- P: 'f1',
37
- Q: 'f2',
38
- R: 'f3',
39
- S: 'f4',
40
- };
41
- /**
42
- * Maps key codes from tilde-coded functional keys `\x1b[<code>~`
43
- * to their corresponding key names.
44
- */
45
- const TILDE_KEYCODE_TO_NAME = {
46
- 1: 'home',
47
- 2: 'insert',
48
- 3: 'delete',
49
- 4: 'end',
50
- 5: 'pageup',
51
- 6: 'pagedown',
52
- 11: 'f1',
53
- 12: 'f2',
54
- 13: 'f3',
55
- 14: 'f4',
56
- 15: 'f5',
57
- 17: 'f6', // skipping 16 is intentional
58
- 18: 'f7',
59
- 19: 'f8',
60
- 20: 'f9',
61
- 21: 'f10',
62
- 23: 'f11', // skipping 22 is intentional
63
- 24: 'f12',
64
- };
65
- /**
66
- * Check if a buffer could potentially be a valid kitty sequence or its prefix.
67
- */
68
- function couldBeKittySequence(buffer) {
69
- // Kitty sequences always start with ESC[.
70
- if (buffer.length === 0)
71
- return true;
72
- if (buffer === ESC || buffer === `${ESC}[`)
73
- return true;
74
- if (!buffer.startsWith(`${ESC}[`))
75
- return false;
76
- if (couldBeMouseSequence(buffer))
77
- return true;
78
- // Check for known kitty sequence patterns:
79
- // 1. ESC[<digit> - could be CSI-u or tilde-coded
80
- // 2. ESC[1;<digit> - parameterized functional
81
- // 3. ESC[<letter> - legacy functional keys
82
- // 4. ESC[Z - reverse tab
83
- const afterCSI = buffer.slice(2);
84
- // Check if it starts with a digit (could be CSI-u or parameterized)
85
- if (/^\d/.test(afterCSI))
86
- return true;
87
- // Check for known single-letter sequences
88
- if (/^[ABCDHFPQRSZ]/.test(afterCSI))
89
- return true;
90
- // Check for 1; pattern (parameterized sequences)
91
- if (/^1;\d/.test(afterCSI))
92
- return true;
93
- // Anything else starting with ESC[ that doesn't match our patterns
94
- // is likely not a kitty sequence we handle
95
- return false;
96
- }
97
- /**
98
- * Parses a single complete kitty/parameterized/legacy sequence from the start
99
- * of the buffer.
100
- *
101
- * This enables peel-and-continue parsing for batched input, allowing us to
102
- * "peel off" one complete event when multiple sequences arrive in a single
103
- * chunk, preventing buffer overflow and fragmentation.
104
- *
105
- * @param buffer - The input buffer string to parse.
106
- * @returns The parsed Key and the number of characters consumed, or null if
107
- * no complete sequence is found at the start of the buffer.
108
- */
109
- function parseKittyPrefix(buffer) {
110
- // In older terminals ESC [ Z was used as Cursor Backward Tabulation (CBT)
111
- // In newer terminals the same functionality of key combination for moving
112
- // backward through focusable elements is Shift+Tab, hence we will
113
- // map ESC [ Z to Shift+Tab
114
- // 0) Reverse Tab (legacy): ESC [ Z
115
- // Treat as Shift+Tab for UI purposes.
116
- // Regex parts:
117
- // ^ - start of buffer
118
- // ESC [ - CSI introducer
119
- // Z - legacy reverse tab
120
- const revTabLegacy = new RegExp(`^${ESC}\\[Z`);
121
- let m = buffer.match(revTabLegacy);
122
- if (m) {
123
- return {
124
- key: {
125
- name: 'tab',
126
- ctrl: false,
127
- meta: false,
128
- shift: true,
129
- paste: false,
130
- sequence: buffer.slice(0, m[0].length),
131
- kittyProtocol: false,
132
- },
133
- length: m[0].length,
134
- };
135
- }
136
- // 1) Reverse Tab (parameterized): ESC [ 1 ; <mods> Z
137
- // Parameterized reverse Tab: ESC [ 1 ; <mods> Z
138
- const revTabParam = new RegExp(`^${ESC}\\[1;(\\d+)Z`);
139
- m = buffer.match(revTabParam);
140
- if (m) {
141
- let mods = parseInt(m[1], 10);
142
- if (mods >= KITTY_MODIFIER_EVENT_TYPES_OFFSET) {
143
- mods -= KITTY_MODIFIER_EVENT_TYPES_OFFSET;
144
- }
145
- const bits = mods - KITTY_MODIFIER_BASE;
146
- const alt = (bits & MODIFIER_ALT_BIT) === MODIFIER_ALT_BIT;
147
- const ctrl = (bits & MODIFIER_CTRL_BIT) === MODIFIER_CTRL_BIT;
148
- return {
149
- key: {
150
- name: 'tab',
151
- ctrl,
152
- meta: alt,
153
- // Reverse tab implies Shift behavior; force shift regardless of mods
154
- shift: true,
155
- paste: false,
156
- sequence: buffer.slice(0, m[0].length),
157
- kittyProtocol: true,
158
- },
159
- length: m[0].length,
160
- };
161
- }
162
- // 2) Parameterized functional: ESC [ 1 ; <mods> (A|B|C|D|H|F|P|Q|R|S)
163
- // 2) Parameterized functional: ESC [ 1 ; <mods> (A|B|C|D|H|F|P|Q|R|S)
164
- // Arrows, Home/End, F1–F4 with modifiers encoded in <mods>.
165
- const arrowPrefix = new RegExp(`^${ESC}\\[1;(\\d+)([ABCDHFPQSR])`);
166
- m = buffer.match(arrowPrefix);
167
- if (m) {
168
- let mods = parseInt(m[1], 10);
169
- if (mods >= KITTY_MODIFIER_EVENT_TYPES_OFFSET) {
170
- mods -= KITTY_MODIFIER_EVENT_TYPES_OFFSET;
113
+ function nonKeyboardEventFilter(keypressHandler) {
114
+ return (key) => {
115
+ if (!parseMouseEvent(key.sequence) &&
116
+ key.sequence !== FOCUS_IN &&
117
+ key.sequence !== FOCUS_OUT) {
118
+ keypressHandler(key);
171
119
  }
172
- const bits = mods - KITTY_MODIFIER_BASE;
173
- const shift = (bits & MODIFIER_SHIFT_BIT) === MODIFIER_SHIFT_BIT;
174
- const alt = (bits & MODIFIER_ALT_BIT) === MODIFIER_ALT_BIT;
175
- const ctrl = (bits & MODIFIER_CTRL_BIT) === MODIFIER_CTRL_BIT;
176
- const sym = m[2];
177
- const name = LEGACY_FUNC_TO_NAME[sym] || '';
178
- if (!name)
179
- return null;
180
- return {
181
- key: {
182
- name,
183
- ctrl,
184
- meta: alt,
185
- shift,
186
- paste: false,
187
- sequence: buffer.slice(0, m[0].length),
188
- kittyProtocol: true,
189
- },
190
- length: m[0].length,
191
- };
192
- }
193
- // 3) CSI-u form: ESC [ <code> ; <mods> (u|~)
194
- // 3) CSI-u and tilde-coded functional keys: ESC [ <code> ; <mods> (u|~)
195
- // 'u' terminator: Kitty CSI-u; '~' terminator: tilde-coded function keys.
196
- const csiUPrefix = new RegExp(`^${ESC}\\[(\\d+)(;(\\d+))?([u~])`);
197
- m = buffer.match(csiUPrefix);
198
- if (m) {
199
- const keyCode = parseInt(m[1], 10);
200
- let modifiers = m[3] ? parseInt(m[3], 10) : KITTY_MODIFIER_BASE;
201
- if (modifiers >= KITTY_MODIFIER_EVENT_TYPES_OFFSET) {
202
- modifiers -= KITTY_MODIFIER_EVENT_TYPES_OFFSET;
203
- }
204
- const modifierBits = modifiers - KITTY_MODIFIER_BASE;
205
- const shift = (modifierBits & MODIFIER_SHIFT_BIT) === MODIFIER_SHIFT_BIT;
206
- const alt = (modifierBits & MODIFIER_ALT_BIT) === MODIFIER_ALT_BIT;
207
- const ctrl = (modifierBits & MODIFIER_CTRL_BIT) === MODIFIER_CTRL_BIT;
208
- const terminator = m[4];
209
- // Tilde-coded functional keys (Delete, Insert, PageUp/Down, Home/End)
210
- if (terminator === '~') {
211
- const name = TILDE_KEYCODE_TO_NAME[keyCode];
212
- if (name) {
213
- return {
214
- key: {
215
- name,
216
- ctrl,
217
- meta: alt,
218
- shift,
219
- paste: false,
220
- sequence: buffer.slice(0, m[0].length),
221
- kittyProtocol: false,
222
- },
223
- length: m[0].length,
224
- };
225
- }
226
- }
227
- const kittyKeyCodeToName = {
228
- [CHAR_CODE_ESC]: 'escape',
229
- [KITTY_KEYCODE_TAB]: 'tab',
230
- [KITTY_KEYCODE_BACKSPACE]: 'backspace',
231
- [KITTY_KEYCODE_ENTER]: 'return',
232
- [KITTY_KEYCODE_NUMPAD_ENTER]: 'return',
233
- };
234
- const name = kittyKeyCodeToName[keyCode];
235
- if (name) {
236
- return {
237
- key: {
238
- name,
239
- ctrl,
240
- meta: alt,
241
- shift,
242
- paste: false,
243
- sequence: buffer.slice(0, m[0].length),
244
- kittyProtocol: true,
245
- },
246
- length: m[0].length,
247
- };
248
- }
249
- // Ctrl+letters and Alt+letters
250
- if ((ctrl || alt) &&
251
- keyCode >= 'a'.charCodeAt(0) &&
252
- keyCode <= 'z'.charCodeAt(0)) {
253
- const letter = String.fromCharCode(keyCode);
254
- return {
255
- key: {
256
- name: letter,
257
- ctrl,
258
- meta: alt,
259
- shift,
260
- paste: false,
261
- sequence: buffer.slice(0, m[0].length),
262
- kittyProtocol: true,
263
- },
264
- length: m[0].length,
265
- };
266
- }
267
- }
268
- // 4) Legacy function keys (no parameters): ESC [ (A|B|C|D|H|F)
269
- // Arrows + Home/End without modifiers.
270
- const legacyFuncKey = new RegExp(`^${ESC}\\[([ABCDHF])`);
271
- m = buffer.match(legacyFuncKey);
272
- if (m) {
273
- const sym = m[1];
274
- const name = LEGACY_FUNC_TO_NAME[sym];
275
- return {
276
- key: {
277
- name,
278
- ctrl: false,
279
- meta: false,
280
- shift: false,
281
- paste: false,
282
- sequence: buffer.slice(0, m[0].length),
283
- kittyProtocol: false,
284
- },
285
- length: m[0].length,
286
- };
287
- }
288
- return null;
120
+ };
289
121
  }
290
122
  /**
291
- * Returns the first index before which we are certain there is no paste marker.
123
+ * Buffers "/" keys to see if they are followed return.
124
+ * Will flush the buffer if no data is received for DRAG_COMPLETION_TIMEOUT_MS
125
+ * or when a null key is received.
292
126
  */
293
- function earliestPossiblePasteMarker(data) {
294
- // Check data for full start-paste or end-paste markers.
295
- const startIndex = data.indexOf(PASTE_MODE_START);
296
- const endIndex = data.indexOf(PASTE_MODE_END);
297
- if (startIndex !== -1 && endIndex !== -1) {
298
- return Math.min(startIndex, endIndex);
299
- }
300
- else if (startIndex !== -1) {
301
- return startIndex;
302
- }
303
- else if (endIndex !== -1) {
304
- return endIndex;
305
- }
306
- // data contains no full start-paste or end-paste.
307
- // Check if data ends with a prefix of start-paste or end-paste.
308
- const codeLength = PASTE_MODE_START.length;
309
- for (let i = Math.min(data.length, codeLength - 1); i > 0; i--) {
310
- const candidate = data.slice(data.length - i);
311
- if (PASTE_MODE_START.indexOf(candidate) === 0 ||
312
- PASTE_MODE_END.indexOf(candidate) === 0) {
313
- return data.length - i;
127
+ function bufferBackslashEnter(keypressHandler) {
128
+ const bufferer = (function* () {
129
+ while (true) {
130
+ const key = yield;
131
+ if (key == null) {
132
+ continue;
133
+ }
134
+ else if (key.sequence !== '\\') {
135
+ keypressHandler(key);
136
+ continue;
137
+ }
138
+ const timeoutId = setTimeout(() => bufferer.next(null), BACKSLASH_ENTER_TIMEOUT);
139
+ const nextKey = yield;
140
+ clearTimeout(timeoutId);
141
+ if (nextKey === null) {
142
+ keypressHandler(key);
143
+ }
144
+ else if (nextKey.name === 'return') {
145
+ keypressHandler({
146
+ ...nextKey,
147
+ shift: true,
148
+ sequence: '\r', // Corrected escaping for newline
149
+ });
150
+ }
151
+ else {
152
+ keypressHandler(key);
153
+ keypressHandler(nextKey);
154
+ }
314
155
  }
315
- }
316
- return data.length;
156
+ })();
157
+ bufferer.next(); // prime the generator so it starts listening.
158
+ return (key) => bufferer.next(key);
317
159
  }
318
160
  /**
319
- * A generator that takes in data chunks and spits out paste-start and
320
- * paste-end keypresses. All non-paste marker data is passed to passthrough.
161
+ * Buffers paste events between paste-start and paste-end sequences.
162
+ * Will flush the buffer if no data is received for PASTE_TIMEOUT ms or
163
+ * when a null key is received.
321
164
  */
322
- function* pasteMarkerParser(passthrough, keypressHandler) {
323
- while (true) {
324
- let data = yield;
325
- if (data.length === 0) {
326
- continue; // we timed out
327
- }
165
+ function bufferPaste(keypressHandler) {
166
+ const bufferer = (function* () {
328
167
  while (true) {
329
- const index = earliestPossiblePasteMarker(data);
330
- if (index === data.length) {
331
- // no possible paste markers were found
332
- passthrough.write(data);
333
- break;
168
+ let key = yield;
169
+ if (key === null) {
170
+ continue;
334
171
  }
335
- if (index > 0) {
336
- // snip off and send the part that doesn't have a paste marker
337
- passthrough.write(data.slice(0, index));
338
- data = data.slice(index);
172
+ else if (key.name !== 'paste-start') {
173
+ keypressHandler(key);
174
+ continue;
339
175
  }
340
- // data starts with a possible paste marker
341
- const codeLength = PASTE_MODE_START.length;
342
- if (data.length < codeLength) {
343
- // we have a prefix. Concat the next data and try again.
344
- const newData = yield;
345
- if (newData.length === 0) {
346
- // we timed out. Just dump what we have and start over.
347
- passthrough.write(data);
176
+ let buffer = '';
177
+ while (true) {
178
+ const timeoutId = setTimeout(() => bufferer.next(null), PASTE_TIMEOUT);
179
+ key = yield;
180
+ clearTimeout(timeoutId);
181
+ if (key === null || key.name === 'paste-end') {
348
182
  break;
349
183
  }
350
- data += newData;
184
+ buffer += key.sequence;
351
185
  }
352
- else if (data.startsWith(PASTE_MODE_START)) {
353
- keypressHandler(undefined, {
354
- name: 'paste-start',
186
+ if (buffer.length > 0) {
187
+ keypressHandler({
188
+ name: '',
355
189
  ctrl: false,
356
190
  meta: false,
357
191
  shift: false,
358
- paste: false,
359
- sequence: '',
192
+ paste: true,
193
+ sequence: buffer,
360
194
  });
361
- data = data.slice(PASTE_MODE_START.length);
362
195
  }
363
- else if (data.startsWith(PASTE_MODE_END)) {
364
- keypressHandler(undefined, {
365
- name: 'paste-end',
366
- ctrl: false,
367
- meta: false,
368
- shift: false,
369
- paste: false,
370
- sequence: '',
371
- });
372
- data = data.slice(PASTE_MODE_END.length);
196
+ }
197
+ })();
198
+ bufferer.next(); // prime the generator so it starts listening.
199
+ return (key) => bufferer.next(key);
200
+ }
201
+ /**
202
+ * Turns raw data strings into keypress events sent to the provided handler.
203
+ * Buffers escape sequences until a full sequence is received or
204
+ * until a timeout occurs.
205
+ */
206
+ function createDataListener(keypressHandler) {
207
+ const parser = emitKeys(keypressHandler);
208
+ parser.next(); // prime the generator so it starts listening.
209
+ let timeoutId;
210
+ return (data) => {
211
+ clearTimeout(timeoutId);
212
+ for (const char of data) {
213
+ parser.next(char);
214
+ }
215
+ if (data.length !== 0) {
216
+ timeoutId = setTimeout(() => parser.next(''), ESC_TIMEOUT);
217
+ }
218
+ };
219
+ }
220
+ /**
221
+ * Translates raw keypress characters into key events.
222
+ * Buffers escape sequences until a full sequence is received or
223
+ * until an empty string is sent to indicate a timeout.
224
+ */
225
+ function* emitKeys(keypressHandler) {
226
+ while (true) {
227
+ let ch = yield;
228
+ let sequence = ch;
229
+ let escaped = false;
230
+ let name = undefined;
231
+ let ctrl = false;
232
+ let meta = false;
233
+ let shift = false;
234
+ let code = undefined;
235
+ if (ch === ESC) {
236
+ escaped = true;
237
+ ch = yield;
238
+ sequence += ch;
239
+ if (ch === ESC) {
240
+ ch = yield;
241
+ sequence += ch;
242
+ }
243
+ }
244
+ if (escaped && (ch === 'O' || ch === '[')) {
245
+ // ANSI escape sequence
246
+ code = ch;
247
+ let modifier = 0;
248
+ if (ch === 'O') {
249
+ // ESC O letter
250
+ // ESC O modifier letter
251
+ ch = yield;
252
+ sequence += ch;
253
+ if (ch >= '0' && ch <= '9') {
254
+ modifier = parseInt(ch, 10) - 1;
255
+ ch = yield;
256
+ sequence += ch;
257
+ }
258
+ code += ch;
259
+ }
260
+ else if (ch === '[') {
261
+ // ESC [ letter
262
+ // ESC [ modifier letter
263
+ // ESC [ [ modifier letter
264
+ // ESC [ [ num char
265
+ ch = yield;
266
+ sequence += ch;
267
+ if (ch === '[') {
268
+ // \x1b[[A
269
+ // ^--- escape codes might have a second bracket
270
+ code += ch;
271
+ ch = yield;
272
+ sequence += ch;
273
+ }
274
+ /*
275
+ * Here and later we try to buffer just enough data to get
276
+ * a complete ascii sequence.
277
+ *
278
+ * We have basically two classes of ascii characters to process:
279
+ *
280
+ *
281
+ * 1. `\x1b[24;5~` should be parsed as { code: '[24~', modifier: 5 }
282
+ *
283
+ * This particular example is featuring Ctrl+F12 in xterm.
284
+ *
285
+ * - `;5` part is optional, e.g. it could be `\x1b[24~`
286
+ * - first part can contain one or two digits
287
+ * - there is also special case when there can be 3 digits
288
+ * but without modifier. They are the case of paste bracket mode
289
+ *
290
+ * So the generic regexp is like /^(?:\d\d?(;\d)?[~^$]|\d{3}~)$/
291
+ *
292
+ *
293
+ * 2. `\x1b[1;5H` should be parsed as { code: '[H', modifier: 5 }
294
+ *
295
+ * This particular example is featuring Ctrl+Home in xterm.
296
+ *
297
+ * - `1;5` part is optional, e.g. it could be `\x1b[H`
298
+ * - `1;` part is optional, e.g. it could be `\x1b[5H`
299
+ *
300
+ * So the generic regexp is like /^((\d;)?\d)?[A-Za-z]$/
301
+ *
302
+ */
303
+ const cmdStart = sequence.length - 1;
304
+ // collect as many digits as possible
305
+ while (ch >= '0' && ch <= '9') {
306
+ ch = yield;
307
+ sequence += ch;
308
+ }
309
+ // skip modifier
310
+ if (ch === ';') {
311
+ ch = yield;
312
+ sequence += ch;
313
+ // collect as many digits as possible
314
+ while (ch >= '0' && ch <= '9') {
315
+ ch = yield;
316
+ sequence += ch;
317
+ }
318
+ }
319
+ else if (ch === '<') {
320
+ // SGR mouse mode
321
+ ch = yield;
322
+ sequence += ch;
323
+ // Don't skip on empty string here to avoid timeouts on slow events.
324
+ while (ch === '' || ch === ';' || (ch >= '0' && ch <= '9')) {
325
+ ch = yield;
326
+ sequence += ch;
327
+ }
328
+ }
329
+ else if (ch === 'M') {
330
+ // X11 mouse mode
331
+ // three characters after 'M'
332
+ ch = yield;
333
+ sequence += ch;
334
+ ch = yield;
335
+ sequence += ch;
336
+ ch = yield;
337
+ sequence += ch;
338
+ }
339
+ /*
340
+ * We buffered enough data, now trying to extract code
341
+ * and modifier from it
342
+ */
343
+ const cmd = sequence.slice(cmdStart);
344
+ let match;
345
+ if ((match = /^(\d+)(?:;(\d+))?([~^$u])$/.exec(cmd))) {
346
+ code += match[1] + match[3];
347
+ // Defaults to '1' if no modifier exists, resulting in a 0 modifier value
348
+ modifier = parseInt(match[2] ?? '1', 10) - 1;
349
+ }
350
+ else if ((match = /^((\d;)?(\d))?([A-Za-z])$/.exec(cmd))) {
351
+ code += match[4];
352
+ modifier = parseInt(match[3] ?? '1', 10) - 1;
353
+ }
354
+ else {
355
+ code += cmd;
356
+ }
357
+ }
358
+ // Parse the key modifier
359
+ ctrl = !!(modifier & 4);
360
+ meta = !!(modifier & 10); // use 10 to catch both alt (2) and meta (8).
361
+ shift = !!(modifier & 1);
362
+ const keyInfo = KEY_INFO_MAP[code];
363
+ if (keyInfo) {
364
+ name = keyInfo.name;
365
+ if (keyInfo.shift) {
366
+ shift = true;
367
+ }
368
+ if (keyInfo.ctrl) {
369
+ ctrl = true;
370
+ }
373
371
  }
374
372
  else {
375
- // This should never happen.
376
- passthrough.write(data);
377
- break;
373
+ name = 'undefined';
374
+ if ((ctrl || meta) && (code.endsWith('u') || code.endsWith('~'))) {
375
+ // CSI-u or tilde-coded functional keys: ESC [ <code> ; <mods> (u|~)
376
+ const codeNumber = parseInt(code.slice(1, -1), 10);
377
+ if (codeNumber >= 'a'.charCodeAt(0) &&
378
+ codeNumber <= 'z'.charCodeAt(0)) {
379
+ name = String.fromCharCode(codeNumber);
380
+ }
381
+ }
378
382
  }
379
383
  }
384
+ else if (ch === '\r') {
385
+ // carriage return
386
+ name = 'return';
387
+ meta = escaped;
388
+ }
389
+ else if (ch === '\n') {
390
+ // Enter, should have been called linefeed
391
+ name = 'enter';
392
+ meta = escaped;
393
+ }
394
+ else if (ch === '\t') {
395
+ // tab
396
+ name = 'tab';
397
+ meta = escaped;
398
+ }
399
+ else if (ch === '\b' || ch === '\x7f') {
400
+ // backspace or ctrl+h
401
+ name = 'backspace';
402
+ meta = escaped;
403
+ }
404
+ else if (ch === ESC) {
405
+ // escape key
406
+ name = 'escape';
407
+ meta = escaped;
408
+ }
409
+ else if (ch === ' ') {
410
+ name = 'space';
411
+ meta = escaped;
412
+ }
413
+ else if (!escaped && ch <= '\x1a') {
414
+ // ctrl+letter
415
+ name = String.fromCharCode(ch.charCodeAt(0) + 'a'.charCodeAt(0) - 1);
416
+ ctrl = true;
417
+ }
418
+ else if (/^[0-9A-Za-z]$/.exec(ch) !== null) {
419
+ // Letter, number, shift+letter
420
+ name = ch.toLowerCase();
421
+ shift = /^[A-Z]$/.exec(ch) !== null;
422
+ meta = escaped;
423
+ }
424
+ else if (MAC_ALT_KEY_CHARACTER_MAP[ch] && process.platform === 'darwin') {
425
+ name = MAC_ALT_KEY_CHARACTER_MAP[ch];
426
+ meta = true;
427
+ }
428
+ else if (sequence === `${ESC}${ESC}`) {
429
+ // Double escape
430
+ name = 'escape';
431
+ meta = true;
432
+ // Emit first escape key here, then continue processing
433
+ keypressHandler({
434
+ name: 'escape',
435
+ ctrl,
436
+ meta,
437
+ shift,
438
+ paste: false,
439
+ sequence: ESC,
440
+ });
441
+ }
442
+ else if (escaped) {
443
+ // Escape sequence timeout
444
+ name = ch.length ? undefined : 'escape';
445
+ meta = true;
446
+ }
447
+ if ((sequence.length !== 0 && (name !== undefined || escaped)) ||
448
+ charLengthAt(sequence, 0) === sequence.length) {
449
+ keypressHandler({
450
+ name: name || '',
451
+ ctrl,
452
+ meta,
453
+ shift,
454
+ paste: false,
455
+ sequence,
456
+ });
457
+ }
458
+ // Unrecognized or broken escape sequence, don't emit anything
380
459
  }
381
460
  }
382
461
  const KeypressContext = createContext(undefined);
@@ -387,10 +466,7 @@ export function useKeypressContext() {
387
466
  }
388
467
  return context;
389
468
  }
390
- function shouldUsePassthrough() {
391
- return process.env['PASTE_WORKAROUND'] !== 'false';
392
- }
393
- export function KeypressProvider({ children, kittyProtocolEnabled, config, debugKeystrokeLogging, }) {
469
+ export function KeypressProvider({ children, config, debugKeystrokeLogging, }) {
394
470
  const { stdin, setRawMode } = useStdin();
395
471
  const subscribers = useRef(new Set()).current;
396
472
  const subscribe = useCallback((handler) => subscribers.add(handler), [subscribers]);
@@ -401,419 +477,34 @@ export function KeypressProvider({ children, kittyProtocolEnabled, config, debug
401
477
  if (wasRaw === false) {
402
478
  setRawMode(true);
403
479
  }
404
- const keypressStream = shouldUsePassthrough() ? new PassThrough() : null;
405
- // If non-null that means we are in paste mode
406
- let pasteBuffer = null;
407
- // Used to turn "\" quickly followed by a "enter" into a shift enter
408
- let backslashTimeout = null;
409
- // Buffers incomplete sequences (Kitty or Mouse) and timer to flush it
410
- let inputBuffer = '';
411
- let inputTimeout = null;
412
- // Used to detect filename drag-and-drops.
413
- let dragBuffer = '';
414
- let draggingTimer = null;
415
- const clearDraggingTimer = () => {
416
- if (draggingTimer) {
417
- clearTimeout(draggingTimer);
418
- draggingTimer = null;
419
- }
420
- };
421
- const flushInputBufferOnInterrupt = (reason) => {
422
- if (inputBuffer) {
423
- if (debugKeystrokeLogging) {
424
- debugLogger.log(`[DEBUG] Input sequence flushed due to ${reason}:`, JSON.stringify(inputBuffer));
425
- }
426
- broadcast({
427
- name: '',
428
- ctrl: false,
429
- meta: false,
430
- shift: false,
431
- paste: false,
432
- sequence: inputBuffer,
433
- });
434
- inputBuffer = '';
435
- }
436
- if (inputTimeout) {
437
- clearTimeout(inputTimeout);
438
- inputTimeout = null;
439
- }
440
- };
441
- const handleKeypress = (_, key) => {
442
- if (key.sequence === FOCUS_IN || key.sequence === FOCUS_OUT) {
443
- flushInputBufferOnInterrupt('focus event');
444
- return;
445
- }
446
- if (key.name === 'paste-start') {
447
- flushInputBufferOnInterrupt('paste start');
448
- pasteBuffer = Buffer.alloc(0);
449
- return;
450
- }
451
- if (key.name === 'paste-end') {
452
- if (pasteBuffer !== null) {
453
- broadcast({
454
- name: '',
455
- ctrl: false,
456
- meta: false,
457
- shift: false,
458
- paste: true,
459
- sequence: pasteBuffer.toString(),
460
- });
461
- }
462
- pasteBuffer = null;
463
- return;
464
- }
465
- if (pasteBuffer !== null) {
466
- pasteBuffer = Buffer.concat([pasteBuffer, Buffer.from(key.sequence)]);
467
- return;
468
- }
469
- if (key.sequence === SINGLE_QUOTE ||
470
- key.sequence === DOUBLE_QUOTE ||
471
- draggingTimer !== null) {
472
- dragBuffer += key.sequence;
473
- clearDraggingTimer();
474
- draggingTimer = setTimeout(() => {
475
- draggingTimer = null;
476
- const seq = dragBuffer;
477
- dragBuffer = '';
478
- if (seq) {
479
- broadcast({ ...key, name: '', paste: true, sequence: seq });
480
- }
481
- }, DRAG_COMPLETION_TIMEOUT_MS);
482
- return;
483
- }
484
- const mappedLetter = MAC_ALT_KEY_CHARACTER_MAP[key.sequence];
485
- if (process.platform === 'darwin' && mappedLetter && !key.meta) {
486
- broadcast({
487
- name: mappedLetter,
488
- ctrl: false,
489
- meta: true,
490
- shift: false,
491
- paste: pasteBuffer !== null,
492
- sequence: key.sequence,
493
- });
494
- return;
495
- }
496
- if (key.name === 'return' && backslashTimeout !== null) {
497
- clearTimeout(backslashTimeout);
498
- backslashTimeout = null;
499
- broadcast({
500
- ...key,
501
- shift: true,
502
- sequence: '\r', // Corrected escaping for newline
503
- });
504
- return;
505
- }
506
- if (key.sequence === '\\' && !key.name) {
507
- // Corrected escaping for backslash
508
- backslashTimeout = setTimeout(() => {
509
- backslashTimeout = null;
510
- broadcast(key);
511
- }, BACKSLASH_ENTER_DETECTION_WINDOW_MS);
512
- return;
513
- }
514
- if (backslashTimeout !== null && key.name !== 'return') {
515
- clearTimeout(backslashTimeout);
516
- backslashTimeout = null;
517
- broadcast({
518
- name: '',
519
- sequence: '\\',
520
- ctrl: false,
521
- meta: false,
522
- shift: false,
523
- paste: false,
524
- });
525
- }
526
- if (['up', 'down', 'left', 'right'].includes(key.name)) {
527
- broadcast(key);
528
- return;
529
- }
530
- if ((key.ctrl && key.name === 'c') ||
531
- key.sequence === `${ESC}${KITTY_CTRL_C}`) {
532
- if (inputBuffer && debugKeystrokeLogging) {
533
- debugLogger.log('[DEBUG] Input buffer cleared on Ctrl+C:', inputBuffer);
534
- }
535
- inputBuffer = '';
536
- if (inputTimeout) {
537
- clearTimeout(inputTimeout);
538
- inputTimeout = null;
539
- }
540
- if (key.sequence === `${ESC}${KITTY_CTRL_C}`) {
541
- broadcast({
542
- name: 'c',
543
- ctrl: true,
544
- meta: false,
545
- shift: false,
546
- paste: false,
547
- sequence: key.sequence,
548
- kittyProtocol: true,
549
- });
550
- }
551
- else {
552
- broadcast(key);
553
- }
554
- return;
555
- }
556
- // Clear any pending timeout when new input arrives
557
- if (inputTimeout) {
558
- clearTimeout(inputTimeout);
559
- inputTimeout = null;
560
- }
561
- // Always check if this could start a sequence we need to buffer (Kitty or Mouse)
562
- // Other ESC sequences (like Alt+Key which is ESC+Key) should be let through if readline parsed them.
563
- const shouldBuffer = couldBeKittySequence(key.sequence);
564
- const isExcluded = [
565
- PASTE_MODE_START,
566
- PASTE_MODE_END,
567
- FOCUS_IN,
568
- FOCUS_OUT,
569
- ].some((prefix) => key.sequence.startsWith(prefix));
570
- if (inputBuffer || (shouldBuffer && !isExcluded)) {
571
- inputBuffer += key.sequence;
572
- if (debugKeystrokeLogging && !couldBeMouseSequence(inputBuffer)) {
573
- debugLogger.log('[DEBUG] Input buffer accumulating:', JSON.stringify(inputBuffer));
480
+ process.stdin.setEncoding('utf8'); // Make data events emit strings
481
+ const mouseFilterer = nonKeyboardEventFilter(broadcast);
482
+ const backslashBufferer = bufferBackslashEnter(mouseFilterer);
483
+ const pasteBufferer = bufferPaste(backslashBufferer);
484
+ let dataListener = createDataListener(pasteBufferer);
485
+ if (debugKeystrokeLogging) {
486
+ const old = dataListener;
487
+ dataListener = (data) => {
488
+ if (data.length > 0) {
489
+ debugLogger.log(`[DEBUG] Raw StdIn: ${JSON.stringify(data)}`);
574
490
  }
575
- // Try immediate parsing
576
- let remainingBuffer = inputBuffer;
577
- let parsedAny = false;
578
- while (remainingBuffer) {
579
- const parsed = parseKittyPrefix(remainingBuffer);
580
- if (parsed) {
581
- // If kitty protocol is disabled, only allow legacy/standard sequences.
582
- // parseKittyPrefix returns true for kittyProtocol if it's a modern kitty sequence.
583
- if (kittyProtocolEnabled || !parsed.key.kittyProtocol) {
584
- if (debugKeystrokeLogging) {
585
- const parsedSequence = remainingBuffer.slice(0, parsed.length);
586
- debugLogger.log('[DEBUG] Sequence parsed successfully:', JSON.stringify(parsedSequence));
587
- }
588
- broadcast(parsed.key);
589
- remainingBuffer = remainingBuffer.slice(parsed.length);
590
- parsedAny = true;
591
- continue;
592
- }
593
- }
594
- const mouseParsed = parseMouseEvent(remainingBuffer);
595
- if (mouseParsed) {
596
- // These are handled by the separate mouse sequence parser.
597
- // All we need to do is make sure we don't get confused by these
598
- // sequences.
599
- remainingBuffer = remainingBuffer.slice(mouseParsed.length);
600
- parsedAny = true;
601
- continue;
602
- }
603
- // If we can't parse a sequence at the start, check if there's
604
- // another ESC later in the buffer. If so, the data before it
605
- // is garbage/incomplete and should be dropped so we can
606
- // process the next sequence.
607
- const nextEscIndex = remainingBuffer.indexOf(ESC, 1);
608
- if (nextEscIndex !== -1) {
609
- const garbage = remainingBuffer.slice(0, nextEscIndex);
610
- // Special case: if garbage is exactly ESC, it's likely a rapid ESC press.
611
- if (garbage === ESC) {
612
- if (debugKeystrokeLogging) {
613
- debugLogger.log('[DEBUG] Flushing rapid ESC before next ESC:', JSON.stringify(garbage));
614
- }
615
- broadcast({
616
- name: 'escape',
617
- ctrl: false,
618
- meta: true,
619
- shift: false,
620
- paste: false,
621
- sequence: garbage,
622
- });
623
- }
624
- else {
625
- if (debugKeystrokeLogging) {
626
- debugLogger.log('[DEBUG] Dropping incomplete sequence before next ESC:', JSON.stringify(garbage));
627
- }
628
- }
629
- // Continue parsing from next ESC
630
- remainingBuffer = remainingBuffer.slice(nextEscIndex);
631
- // We made progress, so we can continue the loop to parse the next sequence
632
- continue;
633
- }
634
- // Check if buffer could become a valid sequence
635
- const couldBeValidKitty = kittyProtocolEnabled && couldBeKittySequence(remainingBuffer);
636
- const isMouse = isIncompleteMouseSequence(remainingBuffer);
637
- const couldBeValid = couldBeValidKitty || isMouse;
638
- if (!couldBeValid) {
639
- // Not a valid sequence - flush as regular input immediately
640
- if (debugKeystrokeLogging) {
641
- debugLogger.log('[DEBUG] Not a valid sequence, flushing:', JSON.stringify(remainingBuffer));
642
- }
643
- broadcast({
644
- name: '',
645
- ctrl: false,
646
- meta: false,
647
- shift: false,
648
- paste: false,
649
- sequence: remainingBuffer,
650
- });
651
- remainingBuffer = '';
652
- parsedAny = true;
653
- }
654
- else if (remainingBuffer.length > MAX_KITTY_SEQUENCE_LENGTH) {
655
- // Buffer overflow - log and clear
656
- if (debugKeystrokeLogging) {
657
- debugLogger.log('[DEBUG] Input buffer overflow, clearing:', JSON.stringify(remainingBuffer));
658
- }
659
- if (config && kittyProtocolEnabled) {
660
- const event = new KittySequenceOverflowEvent(remainingBuffer.length, remainingBuffer);
661
- logKittySequenceOverflow(config, event);
662
- }
663
- // Flush as regular input
664
- broadcast({
665
- name: '',
666
- ctrl: false,
667
- meta: false,
668
- shift: false,
669
- paste: false,
670
- sequence: remainingBuffer,
671
- });
672
- remainingBuffer = '';
673
- parsedAny = true;
674
- }
675
- else {
676
- if ((config?.getDebugMode() || debugKeystrokeLogging) &&
677
- !couldBeMouseSequence(inputBuffer)) {
678
- debugLogger.warn('Input sequence buffer has content:', JSON.stringify(inputBuffer));
679
- }
680
- // Could be valid but incomplete - set timeout
681
- // Only set timeout if it's NOT a mouse sequence.
682
- // Mouse sequences might be slow (e.g. over network) and we don't want to
683
- // flush them as garbage keypresses.
684
- // However, if it's just ESC or ESC[, it might be a user typing slowly,
685
- // so we should still timeout in that case.
686
- const isAmbiguousPrefix = remainingBuffer === ESC || remainingBuffer === `${ESC}[`;
687
- if (!isMouse || isAmbiguousPrefix) {
688
- inputTimeout = setTimeout(() => {
689
- if (inputBuffer) {
690
- if (debugKeystrokeLogging) {
691
- debugLogger.log('[DEBUG] Input sequence timeout, flushing:', JSON.stringify(inputBuffer));
692
- }
693
- const isEscape = inputBuffer === ESC;
694
- broadcast({
695
- name: isEscape ? 'escape' : '',
696
- ctrl: false,
697
- meta: isEscape,
698
- shift: false,
699
- paste: false,
700
- sequence: inputBuffer,
701
- });
702
- inputBuffer = '';
703
- }
704
- inputTimeout = null;
705
- }, KITTY_SEQUENCE_TIMEOUT_MS);
706
- }
707
- else {
708
- // It IS a mouse sequence and it's long enough to be unambiguously NOT just a user hitting ESC slowly.
709
- // We just wait for more data.
710
- if (inputTimeout) {
711
- clearTimeout(inputTimeout);
712
- inputTimeout = null;
713
- }
714
- }
715
- break;
716
- }
717
- }
718
- inputBuffer = remainingBuffer;
719
- if (parsedAny || inputBuffer)
720
- return;
721
- }
722
- if (key.name === 'return' && key.sequence === `${ESC}\r`) {
723
- key.meta = true;
724
- }
725
- broadcast({ ...key, paste: pasteBuffer !== null });
726
- };
727
- let cleanup = () => { };
728
- let rl;
729
- if (keypressStream !== null) {
730
- rl = readline.createInterface({
731
- input: keypressStream,
732
- escapeCodeTimeout: 0,
733
- });
734
- readline.emitKeypressEvents(keypressStream, rl);
735
- const parser = pasteMarkerParser(keypressStream, handleKeypress);
736
- parser.next(); // prime the generator so it starts listening.
737
- let timeoutId;
738
- const handleRawKeypress = (data) => {
739
- clearTimeout(timeoutId);
740
- parser.next(data);
741
- timeoutId = setTimeout(() => parser.next(''), PASTE_CODE_TIMEOUT_MS);
491
+ old(data);
742
492
  };
743
- keypressStream.on('keypress', handleKeypress);
744
- process.stdin.setEncoding('utf8'); // so handleRawKeypress gets strings
745
- stdin.on('data', handleRawKeypress);
746
- cleanup = () => {
747
- keypressStream.removeListener('keypress', handleKeypress);
748
- stdin.removeListener('data', handleRawKeypress);
749
- };
750
- }
751
- else {
752
- rl = readline.createInterface({ input: stdin, escapeCodeTimeout: 0 });
753
- readline.emitKeypressEvents(stdin, rl);
754
- stdin.on('keypress', handleKeypress);
755
- cleanup = () => stdin.removeListener('keypress', handleKeypress);
756
493
  }
494
+ stdin.on('data', dataListener);
757
495
  return () => {
758
- cleanup();
759
- rl.close();
496
+ // flush buffers by sending null key
497
+ backslashBufferer(null);
498
+ pasteBufferer(null);
499
+ // flush by sending empty string to the data listener
500
+ dataListener('');
501
+ stdin.removeListener('data', dataListener);
760
502
  // Restore the terminal to its original state.
761
503
  if (wasRaw === false) {
762
504
  setRawMode(false);
763
505
  }
764
- if (backslashTimeout) {
765
- clearTimeout(backslashTimeout);
766
- backslashTimeout = null;
767
- }
768
- if (inputTimeout) {
769
- clearTimeout(inputTimeout);
770
- inputTimeout = null;
771
- }
772
- // Flush any pending kitty sequence data to avoid data loss on exit.
773
- if (inputBuffer) {
774
- broadcast({
775
- name: '',
776
- ctrl: false,
777
- meta: false,
778
- shift: false,
779
- paste: false,
780
- sequence: inputBuffer,
781
- });
782
- inputBuffer = '';
783
- }
784
- // Flush any pending paste data to avoid data loss on exit.
785
- if (pasteBuffer !== null) {
786
- broadcast({
787
- name: '',
788
- ctrl: false,
789
- meta: false,
790
- shift: false,
791
- paste: true,
792
- sequence: pasteBuffer.toString(),
793
- });
794
- pasteBuffer = null;
795
- }
796
- clearDraggingTimer();
797
- if (dragBuffer) {
798
- broadcast({
799
- name: '',
800
- ctrl: false,
801
- meta: false,
802
- shift: false,
803
- paste: true,
804
- sequence: dragBuffer,
805
- });
806
- dragBuffer = '';
807
- }
808
506
  };
809
- }, [
810
- stdin,
811
- setRawMode,
812
- kittyProtocolEnabled,
813
- config,
814
- debugKeystrokeLogging,
815
- broadcast,
816
- ]);
507
+ }, [stdin, setRawMode, config, debugKeystrokeLogging, broadcast]);
817
508
  return (_jsx(KeypressContext.Provider, { value: { subscribe, unsubscribe }, children: children }));
818
509
  }
819
510
  //# sourceMappingURL=KeypressContext.js.map