pairpod-bot 0.1.0
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/LICENSE +21 -0
- package/README.md +6 -0
- package/dist/access.d.ts +2 -0
- package/dist/access.js +14 -0
- package/dist/access.js.map +1 -0
- package/dist/agents.d.ts +7 -0
- package/dist/agents.js +10 -0
- package/dist/agents.js.map +1 -0
- package/dist/attach.d.ts +2 -0
- package/dist/attach.js +65 -0
- package/dist/attach.js.map +1 -0
- package/dist/bot.d.ts +1 -0
- package/dist/bot.js +357 -0
- package/dist/bot.js.map +1 -0
- package/dist/config.d.ts +18 -0
- package/dist/config.js +39 -0
- package/dist/config.js.map +1 -0
- package/dist/db.d.ts +2 -0
- package/dist/db.js +87 -0
- package/dist/db.js.map +1 -0
- package/dist/docker.d.ts +15 -0
- package/dist/docker.js +113 -0
- package/dist/docker.js.map +1 -0
- package/dist/env.d.ts +2 -0
- package/dist/env.js +36 -0
- package/dist/env.js.map +1 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.js +25 -0
- package/dist/errors.js.map +1 -0
- package/dist/local/sessions.d.ts +5 -0
- package/dist/local/sessions.js +83 -0
- package/dist/local/sessions.js.map +1 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +8 -0
- package/dist/main.js.map +1 -0
- package/dist/naming.d.ts +3 -0
- package/dist/naming.js +19 -0
- package/dist/naming.js.map +1 -0
- package/dist/network.d.ts +1 -0
- package/dist/network.js +11 -0
- package/dist/network.js.map +1 -0
- package/dist/notifier.d.ts +4 -0
- package/dist/notifier.js +47 -0
- package/dist/notifier.js.map +1 -0
- package/dist/notify.d.ts +2 -0
- package/dist/notify.js +19 -0
- package/dist/notify.js.map +1 -0
- package/dist/paths.d.ts +9 -0
- package/dist/paths.js +18 -0
- package/dist/paths.js.map +1 -0
- package/dist/routes/attach.d.ts +13 -0
- package/dist/routes/attach.js +49 -0
- package/dist/routes/attach.js.map +1 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +51 -0
- package/dist/server.js.map +1 -0
- package/dist/ssh.d.ts +2 -0
- package/dist/ssh.js +84 -0
- package/dist/ssh.js.map +1 -0
- package/dist/store.d.ts +65 -0
- package/dist/store.js +337 -0
- package/dist/store.js.map +1 -0
- package/dist/targets/docker.d.ts +7 -0
- package/dist/targets/docker.js +14 -0
- package/dist/targets/docker.js.map +1 -0
- package/dist/targets/index.d.ts +4 -0
- package/dist/targets/index.js +33 -0
- package/dist/targets/index.js.map +1 -0
- package/dist/targets/ssh.d.ts +25 -0
- package/dist/targets/ssh.js +121 -0
- package/dist/targets/ssh.js.map +1 -0
- package/dist/targets/types.d.ts +15 -0
- package/dist/targets/types.js +2 -0
- package/dist/targets/types.js.map +1 -0
- package/dist/telegram-auth.d.ts +7 -0
- package/dist/telegram-auth.js +41 -0
- package/dist/telegram-auth.js.map +1 -0
- package/dist/vault.d.ts +4 -0
- package/dist/vault.js +57 -0
- package/dist/vault.js.map +1 -0
- package/miniapp/index.html +597 -0
- package/miniapp/ssh.html +251 -0
- package/package.json +44 -0
- package/scripts/fix-pty.cjs +15 -0
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta
|
|
6
|
+
name="viewport"
|
|
7
|
+
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"
|
|
8
|
+
/>
|
|
9
|
+
<title>pairpod terminal</title>
|
|
10
|
+
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
|
11
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css" />
|
|
12
|
+
<script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script>
|
|
13
|
+
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js"></script>
|
|
14
|
+
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-webgl@0.18.0/lib/addon-webgl.min.js"></script>
|
|
15
|
+
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-canvas@0.7.0/lib/addon-canvas.min.js"></script>
|
|
16
|
+
<style>
|
|
17
|
+
* { box-sizing: border-box; }
|
|
18
|
+
html, body {
|
|
19
|
+
margin: 0;
|
|
20
|
+
padding: 0;
|
|
21
|
+
height: 100%;
|
|
22
|
+
overflow: hidden;
|
|
23
|
+
background: #0a0a0a;
|
|
24
|
+
font-family: -apple-system, system-ui, sans-serif;
|
|
25
|
+
}
|
|
26
|
+
#app { display: flex; flex-direction: column; height: 100%; }
|
|
27
|
+
#term { flex: 1; min-height: 0; padding: 4px; }
|
|
28
|
+
#keys {
|
|
29
|
+
display: flex;
|
|
30
|
+
gap: 10px;
|
|
31
|
+
align-items: flex-end;
|
|
32
|
+
padding: 6px;
|
|
33
|
+
background: #161616;
|
|
34
|
+
border-top: 1px solid #2a2a2a;
|
|
35
|
+
}
|
|
36
|
+
#ctl {
|
|
37
|
+
display: grid;
|
|
38
|
+
grid-template-columns: repeat(4, 1fr);
|
|
39
|
+
gap: 6px;
|
|
40
|
+
flex: 1;
|
|
41
|
+
min-width: 0;
|
|
42
|
+
}
|
|
43
|
+
#arrows {
|
|
44
|
+
flex: 0 0 auto;
|
|
45
|
+
display: grid;
|
|
46
|
+
grid-template-columns: repeat(3, 38px);
|
|
47
|
+
grid-auto-rows: 32px;
|
|
48
|
+
gap: 4px;
|
|
49
|
+
}
|
|
50
|
+
#arrows .sp { visibility: hidden; }
|
|
51
|
+
.key {
|
|
52
|
+
padding: 8px 10px;
|
|
53
|
+
font-family: ui-monospace, Menlo, monospace;
|
|
54
|
+
font-size: 14px;
|
|
55
|
+
color: #e5e5e5;
|
|
56
|
+
background: #232323;
|
|
57
|
+
border: 1px solid #3a3a3a;
|
|
58
|
+
border-radius: 6px;
|
|
59
|
+
}
|
|
60
|
+
#ctl .key { min-width: 0; }
|
|
61
|
+
#arrows .key { padding: 0; font-size: 16px; }
|
|
62
|
+
.key:active { background: #3a3a3a; }
|
|
63
|
+
#nl { color: #9ad; }
|
|
64
|
+
#compose {
|
|
65
|
+
display: flex;
|
|
66
|
+
gap: 6px;
|
|
67
|
+
padding: 6px;
|
|
68
|
+
background: #161616;
|
|
69
|
+
border-top: 1px solid #2a2a2a;
|
|
70
|
+
}
|
|
71
|
+
#input {
|
|
72
|
+
flex: 1;
|
|
73
|
+
min-width: 0;
|
|
74
|
+
resize: none;
|
|
75
|
+
max-height: 120px;
|
|
76
|
+
padding: 8px 10px;
|
|
77
|
+
font-family: ui-monospace, Menlo, monospace;
|
|
78
|
+
font-size: clamp(14px, 3.4vw, 16px);
|
|
79
|
+
line-height: 1.35;
|
|
80
|
+
color: #e5e5e5;
|
|
81
|
+
background: #0a0a0a;
|
|
82
|
+
border: 1px solid #3a3a3a;
|
|
83
|
+
border-radius: 6px;
|
|
84
|
+
outline: none;
|
|
85
|
+
}
|
|
86
|
+
#input::placeholder { font-size: 13px; opacity: 0.7; }
|
|
87
|
+
#send {
|
|
88
|
+
flex: 0 0 auto;
|
|
89
|
+
padding: 8px 14px;
|
|
90
|
+
font-size: 14px;
|
|
91
|
+
color: #0a0a0a;
|
|
92
|
+
background: #e5e5e5;
|
|
93
|
+
border: none;
|
|
94
|
+
border-radius: 6px;
|
|
95
|
+
}
|
|
96
|
+
#send:active { background: #b5b5b5; }
|
|
97
|
+
#overlay {
|
|
98
|
+
position: absolute;
|
|
99
|
+
inset: 0;
|
|
100
|
+
display: none;
|
|
101
|
+
align-items: center;
|
|
102
|
+
justify-content: center;
|
|
103
|
+
background: rgba(10, 10, 10, 0.85);
|
|
104
|
+
}
|
|
105
|
+
#overlay.show { display: flex; }
|
|
106
|
+
#overlay button {
|
|
107
|
+
padding: 10px 16px;
|
|
108
|
+
color: #e5e5e5;
|
|
109
|
+
background: #232323;
|
|
110
|
+
border: 1px solid #3a3a3a;
|
|
111
|
+
border-radius: 6px;
|
|
112
|
+
font-size: 14px;
|
|
113
|
+
}
|
|
114
|
+
#toast {
|
|
115
|
+
position: absolute;
|
|
116
|
+
bottom: 14px;
|
|
117
|
+
left: 50%;
|
|
118
|
+
transform: translateX(-50%);
|
|
119
|
+
padding: 6px 12px;
|
|
120
|
+
background: rgba(40, 40, 40, 0.96);
|
|
121
|
+
color: #e5e5e5;
|
|
122
|
+
border: 1px solid #3a3a3a;
|
|
123
|
+
border-radius: 6px;
|
|
124
|
+
font-size: 13px;
|
|
125
|
+
opacity: 0;
|
|
126
|
+
transition: opacity 0.15s;
|
|
127
|
+
pointer-events: none;
|
|
128
|
+
z-index: 10;
|
|
129
|
+
}
|
|
130
|
+
#toast.show { opacity: 1; }
|
|
131
|
+
</style>
|
|
132
|
+
</head>
|
|
133
|
+
<body>
|
|
134
|
+
<div id="app">
|
|
135
|
+
<div id="term"></div>
|
|
136
|
+
<div id="compose">
|
|
137
|
+
<textarea id="input" rows="1" placeholder="Message Claude… (Enter sends, Shift+Enter = newline)"
|
|
138
|
+
autocapitalize="off" autocorrect="off" autocomplete="off" spellcheck="false"></textarea>
|
|
139
|
+
<button id="send">Send</button>
|
|
140
|
+
</div>
|
|
141
|
+
<div id="keys">
|
|
142
|
+
<div id="ctl">
|
|
143
|
+
<button class="key" data-key="esc">Esc</button>
|
|
144
|
+
<button class="key" data-key="tab">Tab</button>
|
|
145
|
+
<button class="key" data-key="stab">⇧Tab</button>
|
|
146
|
+
<button class="key" data-key="bksp">⌫</button>
|
|
147
|
+
<button class="key" data-key="ctrlc">^C</button>
|
|
148
|
+
<button class="key" data-key="clr">Clr</button>
|
|
149
|
+
<button class="key" id="nl">⇧⏎</button>
|
|
150
|
+
<button class="key" data-key="enter">⏎</button>
|
|
151
|
+
</div>
|
|
152
|
+
<div id="arrows">
|
|
153
|
+
<span class="sp"></span>
|
|
154
|
+
<button class="key" data-key="up">↑</button>
|
|
155
|
+
<span class="sp"></span>
|
|
156
|
+
<button class="key" data-key="left">←</button>
|
|
157
|
+
<button class="key" data-key="down">↓</button>
|
|
158
|
+
<button class="key" data-key="right">→</button>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
<div id="overlay"><button id="reconnect">Disconnected — tap to reconnect</button></div>
|
|
163
|
+
<div id="toast"></div>
|
|
164
|
+
|
|
165
|
+
<script>
|
|
166
|
+
const ESC = String.fromCharCode(27);
|
|
167
|
+
const KEYS = {
|
|
168
|
+
esc: ESC,
|
|
169
|
+
tab: "\t",
|
|
170
|
+
stab: ESC + "[Z",
|
|
171
|
+
up: ESC + "[A",
|
|
172
|
+
down: ESC + "[B",
|
|
173
|
+
left: ESC + "[D",
|
|
174
|
+
right: ESC + "[C",
|
|
175
|
+
enter: "\r",
|
|
176
|
+
ctrlc: String.fromCharCode(3),
|
|
177
|
+
bksp: String.fromCharCode(127),
|
|
178
|
+
clr: String.fromCharCode(21),
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const tg = window.Telegram && window.Telegram.WebApp;
|
|
182
|
+
if (tg) {
|
|
183
|
+
tg.ready();
|
|
184
|
+
tg.expand();
|
|
185
|
+
if (typeof tg.disableVerticalSwipes === "function") tg.disableVerticalSwipes();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const params = new URLSearchParams(location.search);
|
|
189
|
+
const pod = params.get("pod") || "";
|
|
190
|
+
const session = params.get("session") || "";
|
|
191
|
+
|
|
192
|
+
const term = new window.Terminal({
|
|
193
|
+
disableStdin: true,
|
|
194
|
+
cursorBlink: false,
|
|
195
|
+
fontFamily: '"JetBrains Mono", "Cascadia Code", Menlo, Monaco, monospace',
|
|
196
|
+
fontSize: 13,
|
|
197
|
+
scrollback: 1000,
|
|
198
|
+
allowProposedApi: true,
|
|
199
|
+
theme: {
|
|
200
|
+
background: "#0a0a0a",
|
|
201
|
+
foreground: "#e5e5e5",
|
|
202
|
+
cursor: "#0a0a0a",
|
|
203
|
+
cursorAccent: "#0a0a0a",
|
|
204
|
+
selectionBackground: "#444444",
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
const fit = new window.FitAddon.FitAddon();
|
|
208
|
+
term.loadAddon(fit);
|
|
209
|
+
term.open(document.getElementById("term"));
|
|
210
|
+
|
|
211
|
+
function loadRenderer() {
|
|
212
|
+
try {
|
|
213
|
+
const w = new window.WebglAddon.WebglAddon();
|
|
214
|
+
w.onContextLoss(() => w.dispose());
|
|
215
|
+
term.loadAddon(w);
|
|
216
|
+
return "webgl";
|
|
217
|
+
} catch (e) {}
|
|
218
|
+
try {
|
|
219
|
+
const c = new window.CanvasAddon.CanvasAddon();
|
|
220
|
+
term.loadAddon(c);
|
|
221
|
+
return "canvas";
|
|
222
|
+
} catch (e) {}
|
|
223
|
+
return "dom";
|
|
224
|
+
}
|
|
225
|
+
loadRenderer();
|
|
226
|
+
const savedFont = parseInt(localStorage.getItem("pp_fontsize") || "", 10);
|
|
227
|
+
if (savedFont >= 8 && savedFont <= 28) term.options.fontSize = savedFont;
|
|
228
|
+
fit.fit();
|
|
229
|
+
|
|
230
|
+
const helper = document.querySelector("#term .xterm-helper-textarea");
|
|
231
|
+
if (helper) {
|
|
232
|
+
helper.readOnly = true;
|
|
233
|
+
helper.tabIndex = -1;
|
|
234
|
+
helper.setAttribute("inputmode", "none");
|
|
235
|
+
helper.setAttribute("aria-hidden", "true");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
let ws = null;
|
|
239
|
+
|
|
240
|
+
function wsUrl() {
|
|
241
|
+
const proto = location.protocol === "https:" ? "wss" : "ws";
|
|
242
|
+
const tgData = (tg && tg.initData) || "";
|
|
243
|
+
return (
|
|
244
|
+
proto + "://" + location.host + "/attach" +
|
|
245
|
+
"?pod=" + encodeURIComponent(pod) +
|
|
246
|
+
"&session=" + encodeURIComponent(session) +
|
|
247
|
+
"&cols=" + term.cols + "&rows=" + term.rows +
|
|
248
|
+
"&tgData=" + encodeURIComponent(tgData)
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
let reconnectAttempts = 0;
|
|
253
|
+
let reconnectTimer = null;
|
|
254
|
+
|
|
255
|
+
function showOverlay(show, msg) {
|
|
256
|
+
document.getElementById("overlay").classList.toggle("show", show);
|
|
257
|
+
if (msg) document.getElementById("reconnect").textContent = msg;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function handleClose(e) {
|
|
261
|
+
const fatal = e && (e.code === 4003 || e.code === 4004);
|
|
262
|
+
if (!fatal && reconnectAttempts < 5) {
|
|
263
|
+
reconnectAttempts++;
|
|
264
|
+
showOverlay(true, "Reconnecting… (" + reconnectAttempts + ")");
|
|
265
|
+
clearTimeout(reconnectTimer);
|
|
266
|
+
reconnectTimer = setTimeout(connect, 800 * reconnectAttempts);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (e && e.code === 4004) showOverlay(true, "Session ended — start one from the bot, then reopen");
|
|
270
|
+
else if (e && e.code === 4003) showOverlay(true, "Unauthorized — reopen from the bot");
|
|
271
|
+
else showOverlay(true, "Disconnected — tap to reconnect");
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function connect() {
|
|
275
|
+
clearTimeout(reconnectTimer);
|
|
276
|
+
showOverlay(false);
|
|
277
|
+
ws = new WebSocket(wsUrl());
|
|
278
|
+
ws.binaryType = "arraybuffer";
|
|
279
|
+
ws.onopen = () => { reconnectAttempts = 0; doRefit(); };
|
|
280
|
+
ws.onmessage = (e) => {
|
|
281
|
+
if (e.data instanceof ArrayBuffer) {
|
|
282
|
+
const u8 = new Uint8Array(e.data);
|
|
283
|
+
updateModes(u8);
|
|
284
|
+
term.write(u8);
|
|
285
|
+
} else {
|
|
286
|
+
term.write(e.data);
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
ws.onclose = (e) => handleClose(e);
|
|
290
|
+
ws.onerror = () => {};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function send(data) {
|
|
294
|
+
if (ws && ws.readyState === WebSocket.OPEN) ws.send(data);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
let lastCols = 0;
|
|
298
|
+
let lastRows = 0;
|
|
299
|
+
let refitTimer = null;
|
|
300
|
+
|
|
301
|
+
function doRefit() {
|
|
302
|
+
fit.fit();
|
|
303
|
+
if (term.cols !== lastCols || term.rows !== lastRows) {
|
|
304
|
+
lastCols = term.cols;
|
|
305
|
+
lastRows = term.rows;
|
|
306
|
+
send(JSON.stringify({ type: "resize", cols: term.cols, rows: term.rows }));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function refit() {
|
|
311
|
+
clearTimeout(refitTimer);
|
|
312
|
+
refitTimer = setTimeout(doRefit, 100);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function setFontSize(px) {
|
|
316
|
+
const n = Math.max(8, Math.min(28, Math.round(px)));
|
|
317
|
+
if (n === term.options.fontSize) return;
|
|
318
|
+
term.options.fontSize = n;
|
|
319
|
+
localStorage.setItem("pp_fontsize", String(n));
|
|
320
|
+
doRefit();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
document.addEventListener("keydown", (e) => {
|
|
324
|
+
if (!(e.ctrlKey || e.metaKey)) return;
|
|
325
|
+
if (e.key === "+" || e.key === "=") { e.preventDefault(); setFontSize(term.options.fontSize + 1); }
|
|
326
|
+
else if (e.key === "-" || e.key === "_") { e.preventDefault(); setFontSize(term.options.fontSize - 1); }
|
|
327
|
+
else if (e.key === "0") { e.preventDefault(); setFontSize(13); }
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const ro = new ResizeObserver(() => refit());
|
|
331
|
+
ro.observe(document.getElementById("term"));
|
|
332
|
+
window.addEventListener("resize", refit);
|
|
333
|
+
if (tg && tg.onEvent) tg.onEvent("viewportChanged", refit);
|
|
334
|
+
setTimeout(refit, 150);
|
|
335
|
+
setTimeout(refit, 500);
|
|
336
|
+
|
|
337
|
+
for (const btn of document.querySelectorAll("[data-key]")) {
|
|
338
|
+
btn.addEventListener("click", (e) => {
|
|
339
|
+
e.preventDefault();
|
|
340
|
+
if (btn.dataset.key === "tab") {
|
|
341
|
+
sendTab();
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
const seq = KEYS[btn.dataset.key];
|
|
345
|
+
if (seq) send(seq);
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const input = document.getElementById("input");
|
|
350
|
+
input.placeholder = "Message Claude… · Enter to send";
|
|
351
|
+
|
|
352
|
+
function autoGrow() {
|
|
353
|
+
input.style.height = "auto";
|
|
354
|
+
input.style.height = Math.min(input.scrollHeight, 120) + "px";
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function submitInput() {
|
|
358
|
+
const text = input.value;
|
|
359
|
+
input.value = "";
|
|
360
|
+
autoGrow();
|
|
361
|
+
input.focus();
|
|
362
|
+
if (text.length === 0) {
|
|
363
|
+
send("\r");
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
// When the app accepts bracketed paste, wrap the text so embedded newlines stay
|
|
367
|
+
// literal (otherwise each \n submits a line). Send Enter separately to submit.
|
|
368
|
+
// The 80ms gap lets the app finish ingesting the text before the Enter registers.
|
|
369
|
+
if (bracketOn) {
|
|
370
|
+
send("\x1b[200~" + text + "\x1b[201~");
|
|
371
|
+
} else {
|
|
372
|
+
send(text);
|
|
373
|
+
}
|
|
374
|
+
setTimeout(() => send("\r"), 80);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function insertNewline() {
|
|
378
|
+
const s = input.selectionStart;
|
|
379
|
+
const end = input.selectionEnd;
|
|
380
|
+
input.value = input.value.slice(0, s) + "\n" + input.value.slice(end);
|
|
381
|
+
input.selectionStart = input.selectionEnd = s + 1;
|
|
382
|
+
autoGrow();
|
|
383
|
+
input.focus();
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// The xterm is read-only, so a partial command can only reach the shell's line via the
|
|
387
|
+
// compose box. Flush it WITHOUT Enter, then send Tab, so completion has something to act on.
|
|
388
|
+
function sendTab() {
|
|
389
|
+
if (input.value.length > 0) {
|
|
390
|
+
send(input.value);
|
|
391
|
+
input.value = "";
|
|
392
|
+
autoGrow();
|
|
393
|
+
}
|
|
394
|
+
send("\t");
|
|
395
|
+
input.focus();
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
input.addEventListener("input", autoGrow);
|
|
399
|
+
input.addEventListener("keydown", (e) => {
|
|
400
|
+
if (e.key === "Tab") {
|
|
401
|
+
e.preventDefault();
|
|
402
|
+
sendTab();
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
if (e.key !== "Enter" || e.shiftKey) return;
|
|
406
|
+
e.preventDefault();
|
|
407
|
+
submitInput();
|
|
408
|
+
});
|
|
409
|
+
document.getElementById("send").addEventListener("click", (e) => {
|
|
410
|
+
e.preventDefault();
|
|
411
|
+
submitInput();
|
|
412
|
+
});
|
|
413
|
+
document.getElementById("nl").addEventListener("click", (e) => {
|
|
414
|
+
e.preventDefault();
|
|
415
|
+
insertNewline();
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// Sniff the output stream for private-mode toggles (CSI ? <n> h/l). mouseOn drives
|
|
419
|
+
// scroll-forwarding; bracketOn tells us the app accepts bracketed paste (DECSET 2004),
|
|
420
|
+
// which we use to send multi-line input without each newline acting as a submit.
|
|
421
|
+
let mouseOn = false;
|
|
422
|
+
let bracketOn = false;
|
|
423
|
+
|
|
424
|
+
function updateModes(u8) {
|
|
425
|
+
for (let i = 0; i + 3 < u8.length; i++) {
|
|
426
|
+
if (u8[i] !== 0x1b || u8[i + 1] !== 0x5b || u8[i + 2] !== 0x3f) continue;
|
|
427
|
+
let j = i + 3;
|
|
428
|
+
const nums = [];
|
|
429
|
+
let cur = 0;
|
|
430
|
+
let has = false;
|
|
431
|
+
while (j < u8.length) {
|
|
432
|
+
const c = u8[j];
|
|
433
|
+
if (c >= 0x30 && c <= 0x39) { cur = cur * 10 + (c - 0x30); has = true; j++; }
|
|
434
|
+
else if (c === 0x3b) { nums.push(cur); cur = 0; j++; }
|
|
435
|
+
else break;
|
|
436
|
+
}
|
|
437
|
+
if (has) nums.push(cur);
|
|
438
|
+
if (j < u8.length && (u8[j] === 0x68 || u8[j] === 0x6c)) {
|
|
439
|
+
const on = u8[j] === 0x68;
|
|
440
|
+
for (const n of nums) {
|
|
441
|
+
if (n === 1000 || n === 1002 || n === 1003 || n === 1006) mouseOn = on;
|
|
442
|
+
if (n === 2004) bracketOn = on;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function sendWheel(up) {
|
|
449
|
+
if (!mouseOn) return;
|
|
450
|
+
const col = Math.max(1, Math.ceil(term.cols / 2));
|
|
451
|
+
const row = Math.max(1, Math.ceil(term.rows / 2));
|
|
452
|
+
send(ESC + "[<" + (up ? 64 : 65) + ";" + col + ";" + row + "M");
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const termEl = document.getElementById("term");
|
|
456
|
+
const NOTCH = 18;
|
|
457
|
+
let lastTouchY = null;
|
|
458
|
+
let pinchDist = null;
|
|
459
|
+
let pinchFont = 0;
|
|
460
|
+
let lpTimer = null;
|
|
461
|
+
let selectMode = false;
|
|
462
|
+
let selAnchorRow = 0;
|
|
463
|
+
let touchStartX = 0;
|
|
464
|
+
let touchStartY = 0;
|
|
465
|
+
let touchMoved = false;
|
|
466
|
+
let lastTouchEnd = 0;
|
|
467
|
+
let toastTimer = null;
|
|
468
|
+
|
|
469
|
+
function fingerDist(t) {
|
|
470
|
+
return Math.hypot(t[0].clientX - t[1].clientX, t[0].clientY - t[1].clientY);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function toast(msg) {
|
|
474
|
+
const t = document.getElementById("toast");
|
|
475
|
+
t.textContent = msg;
|
|
476
|
+
t.classList.add("show");
|
|
477
|
+
clearTimeout(toastTimer);
|
|
478
|
+
toastTimer = setTimeout(() => t.classList.remove("show"), 1300);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function rowFromY(y) {
|
|
482
|
+
const rect = termEl.getBoundingClientRect();
|
|
483
|
+
const cellH = Math.max(1, (termEl.clientHeight - 8) / term.rows);
|
|
484
|
+
const inView = Math.max(0, Math.min(term.rows - 1, Math.floor((y - rect.top - 4) / cellH)));
|
|
485
|
+
return term.buffer.active.viewportY + inView;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function copySelection() {
|
|
489
|
+
const text = term.getSelection();
|
|
490
|
+
if (!text) return;
|
|
491
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
492
|
+
navigator.clipboard.writeText(text).then(
|
|
493
|
+
() => toast("Copied " + text.length + " chars"),
|
|
494
|
+
() => toast("Copy blocked by browser")
|
|
495
|
+
);
|
|
496
|
+
} else {
|
|
497
|
+
toast("Clipboard unavailable");
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
termEl.addEventListener("touchstart", (e) => {
|
|
502
|
+
clearTimeout(lpTimer);
|
|
503
|
+
selectMode = false;
|
|
504
|
+
if (e.touches.length >= 2) {
|
|
505
|
+
pinchDist = fingerDist(e.touches);
|
|
506
|
+
pinchFont = term.options.fontSize;
|
|
507
|
+
lastTouchY = null;
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
pinchDist = null;
|
|
511
|
+
lastTouchY = e.touches[0].clientY;
|
|
512
|
+
touchStartX = e.touches[0].clientX;
|
|
513
|
+
touchStartY = e.touches[0].clientY;
|
|
514
|
+
touchMoved = false;
|
|
515
|
+
lpTimer = setTimeout(() => {
|
|
516
|
+
selectMode = true;
|
|
517
|
+
term.clearSelection();
|
|
518
|
+
selAnchorRow = rowFromY(touchStartY);
|
|
519
|
+
term.selectLines(selAnchorRow, selAnchorRow);
|
|
520
|
+
if (navigator.vibrate) navigator.vibrate(15);
|
|
521
|
+
toast("Select mode — drag, release to copy");
|
|
522
|
+
}, 400);
|
|
523
|
+
}, { passive: true });
|
|
524
|
+
|
|
525
|
+
termEl.addEventListener("touchmove", (e) => {
|
|
526
|
+
if (e.touches.length >= 2 && pinchDist) {
|
|
527
|
+
clearTimeout(lpTimer);
|
|
528
|
+
e.preventDefault();
|
|
529
|
+
setFontSize(pinchFont * (fingerDist(e.touches) / pinchDist));
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
const y = e.touches[0].clientY;
|
|
533
|
+
if (selectMode) {
|
|
534
|
+
e.preventDefault();
|
|
535
|
+
const r = rowFromY(y);
|
|
536
|
+
term.selectLines(Math.min(selAnchorRow, r), Math.max(selAnchorRow, r));
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
if (!touchMoved && Math.hypot(e.touches[0].clientX - touchStartX, y - touchStartY) > 10) {
|
|
540
|
+
touchMoved = true;
|
|
541
|
+
clearTimeout(lpTimer);
|
|
542
|
+
}
|
|
543
|
+
if (lastTouchY === null || !mouseOn) return;
|
|
544
|
+
e.preventDefault();
|
|
545
|
+
let dy = y - lastTouchY;
|
|
546
|
+
while (Math.abs(dy) >= NOTCH) {
|
|
547
|
+
sendWheel(dy > 0);
|
|
548
|
+
dy += dy > 0 ? -NOTCH : NOTCH;
|
|
549
|
+
}
|
|
550
|
+
lastTouchY = y - dy;
|
|
551
|
+
}, { passive: false });
|
|
552
|
+
|
|
553
|
+
termEl.addEventListener("touchend", (e) => {
|
|
554
|
+
clearTimeout(lpTimer);
|
|
555
|
+
if (e.touches.length < 2) pinchDist = null;
|
|
556
|
+
if (e.touches.length === 0) {
|
|
557
|
+
lastTouchEnd = Date.now();
|
|
558
|
+
if (selectMode) {
|
|
559
|
+
if (term.hasSelection()) copySelection();
|
|
560
|
+
selectMode = false;
|
|
561
|
+
} else if (!touchMoved) {
|
|
562
|
+
input.focus();
|
|
563
|
+
}
|
|
564
|
+
lastTouchY = null;
|
|
565
|
+
}
|
|
566
|
+
}, { passive: true });
|
|
567
|
+
|
|
568
|
+
termEl.addEventListener("mouseup", () => {
|
|
569
|
+
if (Date.now() - lastTouchEnd < 700) return;
|
|
570
|
+
if (term.hasSelection()) copySelection();
|
|
571
|
+
else input.focus();
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
termEl.addEventListener("wheel", (e) => {
|
|
575
|
+
if (!mouseOn) return;
|
|
576
|
+
e.preventDefault();
|
|
577
|
+
e.stopPropagation();
|
|
578
|
+
sendWheel(e.deltaY < 0);
|
|
579
|
+
}, { passive: false, capture: true });
|
|
580
|
+
|
|
581
|
+
document.getElementById("overlay").addEventListener("click", () => {
|
|
582
|
+
reconnectAttempts = 0;
|
|
583
|
+
connect();
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
document.addEventListener("visibilitychange", () => {
|
|
587
|
+
if (!document.hidden && (!ws || ws.readyState !== WebSocket.OPEN)) {
|
|
588
|
+
reconnectAttempts = 0;
|
|
589
|
+
connect();
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
connect();
|
|
594
|
+
input.focus();
|
|
595
|
+
</script>
|
|
596
|
+
</body>
|
|
597
|
+
</html>
|