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.
- package/CHANGELOG.md +27 -0
- package/README.md +108 -102
- package/SKILL.md +49 -16
- package/config.ts +0 -2
- package/index.ts +52 -539
- package/key-encoding.ts +270 -0
- package/overlay-component.ts +82 -570
- package/package.json +6 -2
- package/pty-session.ts +32 -14
- package/reattach-overlay.ts +415 -0
- package/session-manager.ts +6 -2
- package/tool-schema.ts +254 -0
- package/types.ts +92 -0
package/key-encoding.ts
ADDED
|
@@ -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
|
+
}
|