pi-interactive-shell 0.4.7 → 0.4.9

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.
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Terminal key encoding utilities for translating named keys and modifiers
3
+ * into terminal escape sequences.
4
+ */
5
+
6
+ // Named key sequences (without modifiers)
7
+ const NAMED_KEYS: Record<string, string> = {
8
+ // Arrow keys
9
+ up: "\x1b[A",
10
+ down: "\x1b[B",
11
+ left: "\x1b[D",
12
+ right: "\x1b[C",
13
+
14
+ // Common keys
15
+ enter: "\r",
16
+ return: "\r",
17
+ escape: "\x1b",
18
+ esc: "\x1b",
19
+ tab: "\t",
20
+ space: " ",
21
+ backspace: "\x7f",
22
+ bspace: "\x7f", // tmux-style alias
23
+
24
+ // Editing keys
25
+ delete: "\x1b[3~",
26
+ del: "\x1b[3~",
27
+ dc: "\x1b[3~", // tmux-style alias
28
+ insert: "\x1b[2~",
29
+ ic: "\x1b[2~", // tmux-style alias
30
+
31
+ // Navigation
32
+ home: "\x1b[H",
33
+ end: "\x1b[F",
34
+ pageup: "\x1b[5~",
35
+ pgup: "\x1b[5~",
36
+ ppage: "\x1b[5~", // tmux-style alias
37
+ pagedown: "\x1b[6~",
38
+ pgdn: "\x1b[6~",
39
+ npage: "\x1b[6~", // tmux-style alias
40
+
41
+ // Shift+Tab (backtab)
42
+ btab: "\x1b[Z",
43
+
44
+ // Function keys
45
+ f1: "\x1bOP",
46
+ f2: "\x1bOQ",
47
+ f3: "\x1bOR",
48
+ f4: "\x1bOS",
49
+ f5: "\x1b[15~",
50
+ f6: "\x1b[17~",
51
+ f7: "\x1b[18~",
52
+ f8: "\x1b[19~",
53
+ f9: "\x1b[20~",
54
+ f10: "\x1b[21~",
55
+ f11: "\x1b[23~",
56
+ f12: "\x1b[24~",
57
+
58
+ // Keypad keys (application mode)
59
+ kp0: "\x1bOp",
60
+ kp1: "\x1bOq",
61
+ kp2: "\x1bOr",
62
+ kp3: "\x1bOs",
63
+ kp4: "\x1bOt",
64
+ kp5: "\x1bOu",
65
+ kp6: "\x1bOv",
66
+ kp7: "\x1bOw",
67
+ kp8: "\x1bOx",
68
+ kp9: "\x1bOy",
69
+ "kp/": "\x1bOo",
70
+ "kp*": "\x1bOj",
71
+ "kp-": "\x1bOm",
72
+ "kp+": "\x1bOk",
73
+ "kp.": "\x1bOn",
74
+ kpenter: "\x1bOM",
75
+ };
76
+
77
+ // Ctrl+key combinations (ctrl+a through ctrl+z, plus some special)
78
+ const CTRL_KEYS: Record<string, string> = {};
79
+ for (let i = 0; i < 26; i++) {
80
+ const char = String.fromCharCode(97 + i); // a-z
81
+ CTRL_KEYS[`ctrl+${char}`] = String.fromCharCode(i + 1);
82
+ }
83
+ // Special ctrl combinations
84
+ CTRL_KEYS["ctrl+["] = "\x1b"; // Same as Escape
85
+ CTRL_KEYS["ctrl+\\"] = "\x1c";
86
+ CTRL_KEYS["ctrl+]"] = "\x1d";
87
+ CTRL_KEYS["ctrl+^"] = "\x1e";
88
+ CTRL_KEYS["ctrl+_"] = "\x1f";
89
+ CTRL_KEYS["ctrl+?"] = "\x7f"; // Same as Backspace
90
+
91
+ // Alt+key sends ESC followed by the key
92
+ function altKey(char: string): string {
93
+ return `\x1b${char}`;
94
+ }
95
+
96
+ // Keys that support xterm modifier encoding (CSI sequences)
97
+ const MODIFIABLE_KEYS = new Set([
98
+ "up", "down", "left", "right", "home", "end",
99
+ "pageup", "pgup", "ppage", "pagedown", "pgdn", "npage",
100
+ "insert", "ic", "delete", "del", "dc",
101
+ ]);
102
+
103
+ // Calculate xterm modifier code: 1 + (shift?1:0) + (alt?2:0) + (ctrl?4:0)
104
+ function xtermModifier(shift: boolean, alt: boolean, ctrl: boolean): number {
105
+ let mod = 1;
106
+ if (shift) mod += 1;
107
+ if (alt) mod += 2;
108
+ if (ctrl) mod += 4;
109
+ return mod;
110
+ }
111
+
112
+ // Apply xterm modifier to CSI sequence: ESC[A -> ESC[1;modA
113
+ function applyXtermModifier(sequence: string, modifier: number): string | null {
114
+ // Arrow keys: ESC[A -> ESC[1;modA
115
+ const arrowMatch = sequence.match(/^\x1b\[([A-D])$/);
116
+ if (arrowMatch) {
117
+ return `\x1b[1;${modifier}${arrowMatch[1]}`;
118
+ }
119
+ // Numbered sequences: ESC[5~ -> ESC[5;mod~
120
+ const numMatch = sequence.match(/^\x1b\[(\d+)~$/);
121
+ if (numMatch) {
122
+ return `\x1b[${numMatch[1]};${modifier}~`;
123
+ }
124
+ // Home/End: ESC[H -> ESC[1;modH, ESC[F -> ESC[1;modF
125
+ const hfMatch = sequence.match(/^\x1b\[([HF])$/);
126
+ if (hfMatch) {
127
+ return `\x1b[1;${modifier}${hfMatch[1]}`;
128
+ }
129
+ return null;
130
+ }
131
+
132
+ // Bracketed paste mode sequences
133
+ const BRACKETED_PASTE_START = "\x1b[200~";
134
+ const BRACKETED_PASTE_END = "\x1b[201~";
135
+
136
+ function encodePaste(text: string, bracketed = true): string {
137
+ if (!bracketed) return text;
138
+ return `${BRACKETED_PASTE_START}${text}${BRACKETED_PASTE_END}`;
139
+ }
140
+
141
+ /** Parse a key token and return the escape sequence */
142
+ function encodeKeyToken(token: string): string {
143
+ const normalized = token.trim().toLowerCase();
144
+ if (!normalized) return "";
145
+
146
+ // Check for direct match in named keys
147
+ if (NAMED_KEYS[normalized]) {
148
+ return NAMED_KEYS[normalized];
149
+ }
150
+
151
+ // Check for ctrl+key
152
+ if (CTRL_KEYS[normalized]) {
153
+ return CTRL_KEYS[normalized];
154
+ }
155
+
156
+ // Parse modifier prefixes: ctrl+alt+shift+key, c-m-s-key, etc.
157
+ let rest = normalized;
158
+ let ctrl = false, alt = false, shift = false;
159
+
160
+ // Support both "ctrl+alt+x" and "c-m-x" syntax
161
+ while (rest.length > 2) {
162
+ if (rest.startsWith("ctrl+") || rest.startsWith("ctrl-")) {
163
+ ctrl = true;
164
+ rest = rest.slice(5);
165
+ } else if (rest.startsWith("alt+") || rest.startsWith("alt-")) {
166
+ alt = true;
167
+ rest = rest.slice(4);
168
+ } else if (rest.startsWith("shift+") || rest.startsWith("shift-")) {
169
+ shift = true;
170
+ rest = rest.slice(6);
171
+ } else if (rest.startsWith("c-")) {
172
+ ctrl = true;
173
+ rest = rest.slice(2);
174
+ } else if (rest.startsWith("m-")) {
175
+ alt = true;
176
+ rest = rest.slice(2);
177
+ } else if (rest.startsWith("s-")) {
178
+ shift = true;
179
+ rest = rest.slice(2);
180
+ } else {
181
+ break;
182
+ }
183
+ }
184
+
185
+ // Handle shift+tab specially
186
+ if (shift && rest === "tab") {
187
+ return "\x1b[Z";
188
+ }
189
+
190
+ // Check if base key is a named key that supports modifiers
191
+ const baseSeq = NAMED_KEYS[rest];
192
+ if (baseSeq && MODIFIABLE_KEYS.has(rest) && (ctrl || alt || shift)) {
193
+ const mod = xtermModifier(shift, alt, ctrl);
194
+ if (mod > 1) {
195
+ const modified = applyXtermModifier(baseSeq, mod);
196
+ if (modified) return modified;
197
+ }
198
+ }
199
+
200
+ // For single character with modifiers
201
+ if (rest.length === 1) {
202
+ let char = rest;
203
+ if (shift && /[a-z]/.test(char)) {
204
+ char = char.toUpperCase();
205
+ }
206
+ if (ctrl) {
207
+ const ctrlChar = CTRL_KEYS[`ctrl+${char.toLowerCase()}`];
208
+ if (ctrlChar) char = ctrlChar;
209
+ }
210
+ if (alt) {
211
+ return altKey(char);
212
+ }
213
+ return char;
214
+ }
215
+
216
+ // Named key with alt modifier
217
+ if (baseSeq && alt) {
218
+ return `\x1b${baseSeq}`;
219
+ }
220
+
221
+ // Return base sequence if found
222
+ if (baseSeq) {
223
+ return baseSeq;
224
+ }
225
+
226
+ // Unknown key, return as literal
227
+ return token;
228
+ }
229
+
230
+ /** Translate input specification to terminal escape sequences */
231
+ export function translateInput(input: string | { text?: string; keys?: string[]; paste?: string; hex?: string[] }): string {
232
+ if (typeof input === "string") {
233
+ return input;
234
+ }
235
+
236
+ let result = "";
237
+
238
+ // Hex bytes (raw escape sequences)
239
+ if (input.hex?.length) {
240
+ for (const raw of input.hex) {
241
+ const trimmed = raw.trim().toLowerCase();
242
+ const normalized = trimmed.startsWith("0x") ? trimmed.slice(2) : trimmed;
243
+ if (/^[0-9a-f]{1,2}$/.test(normalized)) {
244
+ const value = Number.parseInt(normalized, 16);
245
+ if (!Number.isNaN(value) && value >= 0 && value <= 0xff) {
246
+ result += String.fromCharCode(value);
247
+ }
248
+ }
249
+ }
250
+ }
251
+
252
+ // Literal text
253
+ if (input.text) {
254
+ result += input.text;
255
+ }
256
+
257
+ // Named keys with modifier support
258
+ if (input.keys) {
259
+ for (const key of input.keys) {
260
+ result += encodeKeyToken(key);
261
+ }
262
+ }
263
+
264
+ // Bracketed paste
265
+ if (input.paste) {
266
+ result += encodePaste(input.paste);
267
+ }
268
+
269
+ return result;
270
+ }