pinggy 0.4.8 → 0.5.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.
@@ -0,0 +1,1338 @@
1
+ import {
2
+ TunnelManager
3
+ } from "./chunk-DLNUDW6G.js";
4
+ import "./chunk-UB26QJ4T.js";
5
+ import {
6
+ logger
7
+ } from "./chunk-7G6SJEEA.js";
8
+ import "./chunk-GBYF2H4H.js";
9
+
10
+ // src/tui/blessed/TunnelTui.ts
11
+ import blessed3 from "blessed";
12
+
13
+ // src/tui/blessed/qrCodeGenerator.ts
14
+ import QRCode from "qrcode";
15
+ async function createQrCodes(urls) {
16
+ const codes = [];
17
+ for (const url of urls) {
18
+ const raw = await QRCode.toString(url, {
19
+ type: "utf8",
20
+ margin: 2,
21
+ errorCorrectionLevel: "L"
22
+ });
23
+ codes.push(raw);
24
+ }
25
+ return codes;
26
+ }
27
+
28
+ // src/tui/blessed/webDebuggerConnection.ts
29
+ import WebSocket from "ws";
30
+
31
+ // src/tui/blessed/config.ts
32
+ var defaultTuiConfig = {
33
+ maxRequestPairs: 100,
34
+ visibleRequestCount: 10,
35
+ visibleUrlCount: 7,
36
+ viewportScrollMargin: 2,
37
+ inactivityHttpSelectorTimeoutMs: 1e4
38
+ };
39
+ function getTuiConfig() {
40
+ return {
41
+ maxRequestPairs: defaultTuiConfig.maxRequestPairs,
42
+ visibleRequestCount: defaultTuiConfig.visibleRequestCount,
43
+ visibleUrlCount: defaultTuiConfig.visibleUrlCount,
44
+ viewportScrollMargin: defaultTuiConfig.viewportScrollMargin,
45
+ inactivityHttpSelectorTimeoutMs: defaultTuiConfig.inactivityHttpSelectorTimeoutMs
46
+ };
47
+ }
48
+
49
+ // src/tui/blessed/webDebuggerConnection.ts
50
+ function createWebDebuggerConnection(webDebuggerUrl, onUpdate) {
51
+ const pairs = /* @__PURE__ */ new Map();
52
+ const pairKeys = [];
53
+ let socket = null;
54
+ let reconnectTimeout = null;
55
+ let isStopped = false;
56
+ const config = getTuiConfig();
57
+ const maxPairs = config.maxRequestPairs;
58
+ const trimPairs = () => {
59
+ while (pairKeys.length > maxPairs) {
60
+ const oldestKey = pairKeys.shift();
61
+ if (oldestKey !== void 0) {
62
+ pairs.delete(oldestKey);
63
+ }
64
+ }
65
+ };
66
+ const upsertPair = (key, pair) => {
67
+ if (!pairs.has(key)) {
68
+ pairKeys.push(key);
69
+ }
70
+ pairs.set(key, pair);
71
+ trimPairs();
72
+ };
73
+ const connect = () => {
74
+ const ws = new WebSocket(`ws://${webDebuggerUrl}/introspec/websocket`);
75
+ socket = ws;
76
+ ws.on("open", () => {
77
+ logger.info("Web debugger connected.");
78
+ });
79
+ ws.on("message", (data) => {
80
+ try {
81
+ const raw = data.toString();
82
+ const parsed = JSON.parse(raw);
83
+ const msg = {
84
+ Req: parsed.req,
85
+ Res: parsed.res
86
+ };
87
+ if (msg.Req) {
88
+ const { key } = msg.Req;
89
+ const existing = pairs.get(key);
90
+ const merged = {
91
+ request: msg.Req,
92
+ response: existing?.response
93
+ };
94
+ upsertPair(key, merged);
95
+ }
96
+ if (msg.Res) {
97
+ const { key } = msg.Res;
98
+ const existing = pairs.get(key);
99
+ const merged = {
100
+ request: existing?.request ?? {},
101
+ response: msg.Res
102
+ };
103
+ upsertPair(key, merged);
104
+ }
105
+ const reversedPairs = [];
106
+ for (let i = pairKeys.length - 1; i >= 0; i--) {
107
+ const key = pairKeys[i];
108
+ const pair = pairs.get(key);
109
+ if (pair) {
110
+ reversedPairs.push(pair);
111
+ }
112
+ }
113
+ onUpdate(reversedPairs);
114
+ } catch (err) {
115
+ logger.error("Error parsing WebSocket message:", err instanceof Error ? err.message : err);
116
+ }
117
+ });
118
+ ws.on("close", () => {
119
+ logger.warn("Web debugger disconnected. Reconnecting in 5s...");
120
+ if (!isStopped) {
121
+ reconnectTimeout = setTimeout(connect, 5e3);
122
+ }
123
+ });
124
+ ws.on("error", (err) => {
125
+ logger.error(`WebSocket error: ${err.message}`);
126
+ });
127
+ };
128
+ connect();
129
+ return {
130
+ close: () => {
131
+ isStopped = true;
132
+ if (socket) {
133
+ socket.close();
134
+ }
135
+ if (reconnectTimeout) {
136
+ clearTimeout(reconnectTimeout);
137
+ }
138
+ }
139
+ };
140
+ }
141
+
142
+ // src/tui/blessed/components/UIComponents.ts
143
+ import blessed from "blessed";
144
+
145
+ // src/tui/ink/asciArt.ts
146
+ var asciiArtPinggyLogo = `
147
+ \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557
148
+ \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u255A\u2588\u2588\u2557 \u2588\u2588\u2554\u255D
149
+ \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2554\u255D
150
+ \u2588\u2588\u2554\u2550\u2550\u2550\u255D \u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u255A\u2588\u2588\u2554\u255D
151
+ \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2551
152
+ \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D `;
153
+
154
+ // src/tui/blessed/components/UIComponents.ts
155
+ var MIN_WIDTH_WARNING = 60;
156
+ var SIMPLE_LAYOUT_THRESHOLD = 80;
157
+ function colorizeGradient(text) {
158
+ const colors = ["red", "yellow", "green", "cyan", "blue", "magenta"];
159
+ const lines = text.split("\n");
160
+ return lines.map((line, i) => {
161
+ const color = colors[i % colors.length];
162
+ return `{${color}-fg}${line}{/${color}-fg}`;
163
+ }).join("\n");
164
+ }
165
+ function createWarningUI(screen) {
166
+ return blessed.box({
167
+ parent: screen,
168
+ top: "center",
169
+ left: "center",
170
+ width: "80%",
171
+ height: 5,
172
+ content: `{red-fg}{bold}Terminal is too narrow to show TUI (${screen.width} cols).{/bold}{/red-fg}
173
+ {yellow-fg}Please resize your terminal to at least ${MIN_WIDTH_WARNING} columns for proper display.{/yellow-fg}`,
174
+ tags: true,
175
+ align: "center",
176
+ valign: "middle",
177
+ style: {
178
+ fg: "red"
179
+ }
180
+ });
181
+ }
182
+ function createFullUI(screen, urls, greet, tunnelConfig) {
183
+ const mainContainer = blessed.box({
184
+ parent: screen,
185
+ top: 0,
186
+ left: 0,
187
+ width: "100%",
188
+ height: "100%",
189
+ padding: 1
190
+ });
191
+ const logoBox = blessed.box({
192
+ parent: mainContainer,
193
+ top: 0,
194
+ left: 0,
195
+ width: "100%",
196
+ height: 7,
197
+ content: colorizeGradient(asciiArtPinggyLogo),
198
+ tags: true
199
+ });
200
+ const contentBox = blessed.box({
201
+ parent: mainContainer,
202
+ top: 8,
203
+ left: 0,
204
+ width: "100%-2",
205
+ height: "100%-10",
206
+ padding: 0,
207
+ border: {
208
+ type: "line"
209
+ },
210
+ style: {
211
+ border: {
212
+ fg: "green"
213
+ }
214
+ }
215
+ });
216
+ let greetHeight = 0;
217
+ if (greet) {
218
+ const greetBox = blessed.box({
219
+ parent: contentBox,
220
+ top: 0,
221
+ left: "center",
222
+ width: "60%",
223
+ height: 4,
224
+ content: `{bold}${greet}{/bold}`,
225
+ tags: true,
226
+ align: "center",
227
+ style: {
228
+ fg: "green"
229
+ }
230
+ });
231
+ greetHeight = 4;
232
+ }
233
+ const upperSectionTop = greetHeight > 0 ? greetHeight : 0;
234
+ const upperSection = blessed.box({
235
+ parent: contentBox,
236
+ top: upperSectionTop,
237
+ left: 0,
238
+ width: "100%-2",
239
+ height: 10
240
+ });
241
+ const urlsBox = blessed.box({
242
+ parent: upperSection,
243
+ top: 0,
244
+ left: 0,
245
+ width: "48%",
246
+ height: "100%",
247
+ padding: { left: 1, right: 1 },
248
+ tags: true
249
+ });
250
+ const statsBox = blessed.box({
251
+ parent: upperSection,
252
+ top: 0,
253
+ right: 0,
254
+ left: "65%",
255
+ width: "35%",
256
+ height: "100%",
257
+ padding: { left: 1, right: 1 },
258
+ tags: true,
259
+ align: "left"
260
+ });
261
+ const lowerSectionTop = greetHeight + 11;
262
+ const lowerSection = blessed.box({
263
+ parent: contentBox,
264
+ top: lowerSectionTop,
265
+ left: 0,
266
+ right: 0,
267
+ bottom: 2,
268
+ width: "100%-2",
269
+ height: `100%-${lowerSectionTop + 6}`
270
+ });
271
+ const isQrCodeRequested = tunnelConfig?.isQRCode || false;
272
+ const requestsBox = blessed.box({
273
+ parent: lowerSection,
274
+ top: 0,
275
+ left: 0,
276
+ width: isQrCodeRequested ? "60%" : "80%",
277
+ height: "80%",
278
+ padding: { left: 1, right: 1 },
279
+ tags: true,
280
+ scrollable: true
281
+ });
282
+ let qrCodeBox;
283
+ if (isQrCodeRequested) {
284
+ qrCodeBox = blessed.box({
285
+ parent: lowerSection,
286
+ top: 0,
287
+ right: 0,
288
+ width: "40%",
289
+ height: "100%",
290
+ tags: true,
291
+ padding: { left: 1, right: 1 }
292
+ });
293
+ }
294
+ const footerBox = blessed.box({
295
+ parent: contentBox,
296
+ bottom: 0,
297
+ left: "center",
298
+ width: "shrink",
299
+ height: 1,
300
+ content: "Press Ctrl+C to stop the tunnel. Or press h for key bindings.",
301
+ tags: true
302
+ });
303
+ return {
304
+ mainContainer,
305
+ logoBox,
306
+ contentBox,
307
+ urlsBox,
308
+ statsBox,
309
+ requestsBox,
310
+ qrCodeBox,
311
+ footerBox
312
+ };
313
+ }
314
+ function createSimpleUI(screen, urls, greet) {
315
+ const mainContainer = blessed.box({
316
+ parent: screen,
317
+ top: 0,
318
+ left: 0,
319
+ width: "100%",
320
+ height: "100%",
321
+ padding: { left: 1, right: 1 }
322
+ });
323
+ let currentTop = 0;
324
+ if (greet) {
325
+ blessed.box({
326
+ parent: mainContainer,
327
+ top: currentTop,
328
+ left: "center",
329
+ width: "90%",
330
+ height: "shrink",
331
+ content: `{bold}${greet}{/bold}`,
332
+ tags: true,
333
+ align: "center",
334
+ style: {
335
+ fg: "green"
336
+ }
337
+ });
338
+ const lines = Math.ceil(greet.length / (screen.width * 0.9));
339
+ currentTop += Math.max(lines, 1) + 1;
340
+ }
341
+ const urlsBox = blessed.box({
342
+ parent: mainContainer,
343
+ top: currentTop,
344
+ left: 0,
345
+ width: "100%",
346
+ height: urls.length + 2,
347
+ tags: true
348
+ });
349
+ currentTop += urls.length + 3;
350
+ const statsBox = blessed.box({
351
+ parent: mainContainer,
352
+ top: currentTop,
353
+ left: 0,
354
+ width: "100%",
355
+ height: 8,
356
+ tags: true
357
+ });
358
+ currentTop += 9;
359
+ const footerBox = blessed.box({
360
+ parent: mainContainer,
361
+ bottom: 0,
362
+ left: "center",
363
+ width: "shrink",
364
+ height: 1,
365
+ content: "Press Ctrl+C to stop the tunnel.",
366
+ tags: true,
367
+ style: {
368
+ fg: "white"
369
+ }
370
+ });
371
+ return {
372
+ mainContainer,
373
+ urlsBox,
374
+ statsBox,
375
+ footerBox
376
+ };
377
+ }
378
+
379
+ // src/tui/ink/utils/utils.ts
380
+ function getStatusColor(status) {
381
+ const match = status.match(/\b(\d{3})\b/);
382
+ const statusCode = match ? parseInt(match[1], 10) : 0;
383
+ switch (true) {
384
+ case (statusCode >= 100 && statusCode < 200):
385
+ return "yellow";
386
+ case (statusCode >= 200 && statusCode < 300):
387
+ return "green";
388
+ case (statusCode >= 300 && statusCode < 400):
389
+ return "yellow";
390
+ case (statusCode >= 400 && statusCode < 500):
391
+ return "red";
392
+ case statusCode >= 500:
393
+ return "pink";
394
+ default:
395
+ return "yellow";
396
+ }
397
+ }
398
+ function getBytesInt(b) {
399
+ if (b >= 1024 * 1024 * 1024) {
400
+ return `${(b / (1024 * 1024 * 1024)).toFixed(2)} G`;
401
+ }
402
+ if (b >= 1024 * 1024) {
403
+ return `${(b / (1024 * 1024)).toFixed(2)} M`;
404
+ }
405
+ if (b >= 1024) {
406
+ return `${(b / 1024).toFixed(2)} K`;
407
+ }
408
+ return `${b.toFixed(2)} `;
409
+ }
410
+
411
+ // src/tui/blessed/components/DisplayUpdaters.ts
412
+ function updateUrlsDisplay(urlsBox, screen, urls, currentQrIndex) {
413
+ if (!urlsBox) return;
414
+ const config = getTuiConfig();
415
+ const { visibleUrlCount } = config;
416
+ let viewportStart = 0;
417
+ if (urls.length > visibleUrlCount) {
418
+ viewportStart = Math.max(0, Math.min(
419
+ currentQrIndex - Math.floor(visibleUrlCount / 2),
420
+ urls.length - visibleUrlCount
421
+ ));
422
+ }
423
+ const viewportEnd = Math.min(viewportStart + visibleUrlCount, urls.length);
424
+ const visibleUrls = urls.slice(viewportStart, viewportEnd);
425
+ let content = "{green-fg}{bold}Public URLs{/bold}{/green-fg}";
426
+ if (viewportStart > 0) {
427
+ content += ` {gray-fg}\u2191 ${viewportStart} more{/gray-fg}`;
428
+ }
429
+ content += "\n";
430
+ visibleUrls.forEach((url, i) => {
431
+ const index = viewportStart + i;
432
+ const isSelected = index === currentQrIndex;
433
+ const prefix = isSelected ? "\u2192 " : "\u2022 ";
434
+ const color = isSelected ? "yellow" : "magenta";
435
+ if (isSelected) {
436
+ content += `{bold}{${color}-fg}${prefix}${url}{/${color}-fg}{/bold}
437
+ `;
438
+ } else {
439
+ content += `{${color}-fg}${prefix}${url}{/${color}-fg}
440
+ `;
441
+ }
442
+ });
443
+ const itemsBelow = urls.length - viewportEnd;
444
+ if (itemsBelow > 0) {
445
+ content += `{gray-fg}\u2193 ${itemsBelow} more{/gray-fg}
446
+ `;
447
+ }
448
+ urlsBox.setContent(content);
449
+ screen.render();
450
+ }
451
+ function updateStatsDisplay(statsBox, screen, stats) {
452
+ if (!statsBox) return;
453
+ const content = `{green-fg}{bold}Live Stats{/bold}{/green-fg}
454
+ Elapsed: ${stats.elapsedTime}s
455
+ Live Connections: ${stats.numLiveConnections}
456
+ Total Connections: ${stats.numTotalConnections}
457
+ Request: ${getBytesInt(stats.numTotalReqBytes)}
458
+ Response: ${getBytesInt(stats.numTotalResBytes)}
459
+ Total Transfer: ${getBytesInt(stats.numTotalTxBytes)}`;
460
+ statsBox.setContent(content);
461
+ statsBox.style = { ...statsBox.style };
462
+ statsBox.parseContent();
463
+ screen.render();
464
+ }
465
+ function updateRequestsDisplay(requestsBox, screen, pairs, selectedIndex) {
466
+ const config = getTuiConfig();
467
+ const { maxRequestPairs, visibleRequestCount, viewportScrollMargin } = config;
468
+ if (!requestsBox) {
469
+ return { adjustedSelectedIndex: selectedIndex, trimmedPairs: pairs };
470
+ }
471
+ let allPairs = pairs;
472
+ let trimmedPairs = pairs;
473
+ if (allPairs.length > maxRequestPairs) {
474
+ allPairs = allPairs.slice(0, maxRequestPairs);
475
+ trimmedPairs = allPairs;
476
+ }
477
+ const totalPairs = allPairs.length;
478
+ let adjustedSelectedIndex = selectedIndex;
479
+ if (adjustedSelectedIndex >= totalPairs) {
480
+ adjustedSelectedIndex = -1;
481
+ }
482
+ let viewportStart;
483
+ if (totalPairs <= visibleRequestCount) {
484
+ viewportStart = 0;
485
+ } else if (adjustedSelectedIndex === -1) {
486
+ viewportStart = 0;
487
+ } else {
488
+ viewportStart = 0;
489
+ if (adjustedSelectedIndex >= visibleRequestCount - viewportScrollMargin) {
490
+ viewportStart = Math.min(
491
+ totalPairs - visibleRequestCount,
492
+ adjustedSelectedIndex - viewportScrollMargin
493
+ );
494
+ }
495
+ if (adjustedSelectedIndex < viewportStart + viewportScrollMargin) {
496
+ viewportStart = Math.max(0, adjustedSelectedIndex - viewportScrollMargin);
497
+ }
498
+ }
499
+ const viewportEnd = Math.min(viewportStart + visibleRequestCount, totalPairs);
500
+ const visiblePairs = allPairs.slice(viewportStart, viewportEnd);
501
+ let content = "{yellow-fg}HTTP Requests:{/yellow-fg}";
502
+ if (viewportStart > 0) {
503
+ content += ` {gray-fg}\u2191 ${viewportStart} more{/gray-fg}`;
504
+ }
505
+ content += "\n";
506
+ visiblePairs.forEach((pair, i) => {
507
+ const globalIndex = viewportStart + i;
508
+ const isSelected = adjustedSelectedIndex !== -1 && adjustedSelectedIndex === globalIndex;
509
+ const prefix = isSelected ? "> " : " ";
510
+ const method = pair.request?.method || "";
511
+ const uri = pair.request?.uri || "";
512
+ const status = pair.response?.status || "";
513
+ const statusColor = getStatusColor(String(status));
514
+ if (isSelected) {
515
+ content += `{cyan-fg}${prefix}${method} ${status} ${uri}{/cyan-fg}
516
+ `;
517
+ } else if (pair.response) {
518
+ content += `{${statusColor}-fg}${prefix}${method} ${status} ${uri}{/${statusColor}-fg}
519
+ `;
520
+ } else {
521
+ content += `${prefix}${method} ...${uri}
522
+ `;
523
+ }
524
+ });
525
+ const itemsBelow = totalPairs - viewportEnd;
526
+ if (itemsBelow > 0) {
527
+ content += `{gray-fg} \u2193 ${itemsBelow} more{/gray-fg}
528
+ `;
529
+ }
530
+ requestsBox.setContent(content);
531
+ screen.render();
532
+ return { adjustedSelectedIndex, trimmedPairs };
533
+ }
534
+ function updateQrCodeDisplay(qrCodeBox, screen, qrCodes, urls, currentQrIndex) {
535
+ if (!qrCodeBox || qrCodes.length === 0) return;
536
+ let content = `{green-fg}{bold}QR Code ${currentQrIndex + 1}/${urls.length}{/bold}{/green-fg}
537
+ `;
538
+ if (urls.length > 1) {
539
+ content += "\n{yellow-fg}\u2190 \u2192 to switch QR codes{/yellow-fg}\n";
540
+ }
541
+ content += qrCodes[currentQrIndex] || "";
542
+ qrCodeBox.setContent(content);
543
+ qrCodeBox.style = { ...qrCodeBox.style };
544
+ qrCodeBox.parseContent();
545
+ screen.render();
546
+ }
547
+
548
+ // src/tui/blessed/components/Modals.ts
549
+ import blessed2 from "blessed";
550
+ function showDetailModal(screen, manager, requestText, responseText) {
551
+ manager.inDetailView = true;
552
+ manager.detailModal = blessed2.box({
553
+ parent: screen,
554
+ top: "center",
555
+ left: "center",
556
+ width: "90%",
557
+ height: "90%",
558
+ border: {
559
+ type: "line"
560
+ },
561
+ style: {
562
+ border: {
563
+ fg: "green"
564
+ }
565
+ },
566
+ padding: { left: 2, right: 2, top: 1, bottom: 1 },
567
+ tags: true,
568
+ scrollable: true,
569
+ keys: true,
570
+ vi: true,
571
+ alwaysScroll: true,
572
+ scrollbar: {
573
+ ch: " ",
574
+ track: {
575
+ bg: "cyan"
576
+ },
577
+ style: {
578
+ inverse: true
579
+ }
580
+ }
581
+ });
582
+ const content = `{cyan-fg}{bold}Request{/bold}{/cyan-fg}
583
+ ${requestText || "(no request data)"}
584
+
585
+ {magenta-fg}{bold}Response{/bold}{/magenta-fg}
586
+ ${responseText || "(no response data)"}
587
+
588
+ {white-bg}{black-fg}Press ESC to close{/black-fg}{/white-bg}`;
589
+ manager.detailModal.setContent(content);
590
+ manager.detailModal.focus();
591
+ screen.render();
592
+ }
593
+ function closeDetailModal(screen, manager) {
594
+ if (manager.detailModal) {
595
+ manager.detailModal.destroy();
596
+ manager.detailModal = null;
597
+ }
598
+ manager.inDetailView = false;
599
+ screen.render();
600
+ }
601
+ function showKeyBindingsModal(screen, manager) {
602
+ manager.keyBindingView = true;
603
+ manager.keyBindingsModal = blessed2.box({
604
+ parent: screen,
605
+ top: "center",
606
+ left: "center",
607
+ width: "60%",
608
+ height: "80%",
609
+ border: {
610
+ type: "line"
611
+ },
612
+ style: {
613
+ border: {
614
+ fg: "green"
615
+ }
616
+ },
617
+ padding: { left: 2, right: 2, top: 1, bottom: 1 },
618
+ tags: true
619
+ });
620
+ const content = `{cyan-fg}{bold}Key Bindings{/bold}{/cyan-fg}
621
+
622
+ {bold}h{/bold} This page
623
+ {bold}c{/bold} Copy the selected URL to clipboard
624
+ {bold}Ctrl+c{/bold} Exit
625
+
626
+ Enter/Return Open selected request
627
+ Esc Return to main page (or close modals)
628
+ UP (\u2191) Scroll up the requests
629
+ Down (\u2193) Scroll down the requests
630
+ Left (\u2190) Show qr code for previous url
631
+ Right (\u2192) Show qr code for next url
632
+ Home Jump to top of requests
633
+ End Jump to bottom of requests
634
+ Ctrl+c Force Exit
635
+
636
+ {white-bg}{black-fg}Press ESC to close{/black-fg}{/white-bg}`;
637
+ manager.keyBindingsModal.setContent(content);
638
+ manager.keyBindingsModal.focus();
639
+ screen.render();
640
+ }
641
+ function closeKeyBindingsModal(screen, manager) {
642
+ if (manager.keyBindingsModal) {
643
+ manager.keyBindingsModal.destroy();
644
+ manager.keyBindingsModal = null;
645
+ }
646
+ manager.keyBindingView = false;
647
+ screen.render();
648
+ }
649
+ function showDisconnectModal(screen, manager, message, onClose) {
650
+ manager.inDisconnectView = true;
651
+ manager.disconnectModal = blessed2.box({
652
+ parent: screen,
653
+ top: "center",
654
+ left: "center",
655
+ width: "50%",
656
+ height: "20%",
657
+ border: {
658
+ type: "line"
659
+ },
660
+ style: {
661
+ border: {
662
+ fg: "red"
663
+ }
664
+ },
665
+ padding: { left: 2, right: 2, top: 1, bottom: 1 },
666
+ tags: true,
667
+ align: "center",
668
+ valign: "middle"
669
+ });
670
+ const content = `{red-fg}{bold}Tunnel Disconnected{/bold}{/red-fg}
671
+
672
+ ${message || "Disconnect request received. Tunnel will be closed."}
673
+
674
+ {white-bg}{black-fg}Closing in 3 seconds... {/black-fg}{/white-bg}`;
675
+ manager.disconnectModal.setContent(content);
676
+ manager.disconnectModal.focus();
677
+ screen.render();
678
+ const timeout = setTimeout(() => {
679
+ closeDisconnectModal(screen, manager);
680
+ if (onClose) onClose();
681
+ }, 5e3);
682
+ const keyHandler = () => {
683
+ clearTimeout(timeout);
684
+ closeDisconnectModal(screen, manager);
685
+ if (onClose) onClose();
686
+ };
687
+ manager.disconnectModal.key(["escape", "enter", "space"], keyHandler);
688
+ screen.key(["escape", "enter", "space"], keyHandler);
689
+ }
690
+ function closeDisconnectModal(screen, manager) {
691
+ if (manager.disconnectModal) {
692
+ manager.disconnectModal.destroy();
693
+ manager.disconnectModal = null;
694
+ }
695
+ manager.inDisconnectView = false;
696
+ screen.render();
697
+ }
698
+ function showReconnectingModal(screen, manager, retryCnt, message) {
699
+ if (manager.reconnectModal) {
700
+ manager.reconnectModal.destroy();
701
+ manager.reconnectModal = null;
702
+ }
703
+ manager.inReconnectView = true;
704
+ manager.reconnectModal = blessed2.box({
705
+ parent: screen,
706
+ top: "center",
707
+ left: "center",
708
+ width: "50%",
709
+ height: "20%",
710
+ border: {
711
+ type: "line"
712
+ },
713
+ style: {
714
+ border: {
715
+ fg: "yellow"
716
+ }
717
+ },
718
+ padding: { left: 2, right: 2, top: 1, bottom: 1 },
719
+ tags: true,
720
+ align: "center",
721
+ valign: "middle"
722
+ });
723
+ const content = `{yellow-fg}{bold}Reconnecting...{/bold}{/yellow-fg}
724
+
725
+ ${message || `Attempt #${retryCnt} \u2014 trying to re-establish tunnel...`}
726
+
727
+ {gray-fg}Please wait{/gray-fg}`;
728
+ manager.reconnectModal.setContent(content);
729
+ manager.reconnectModal.focus();
730
+ screen.render();
731
+ }
732
+ function closeReconnectingModal(screen, manager) {
733
+ if (manager.reconnectModal) {
734
+ manager.reconnectModal.destroy();
735
+ manager.reconnectModal = null;
736
+ }
737
+ manager.inReconnectView = false;
738
+ screen.render();
739
+ }
740
+ function showReconnectionFailedModal(screen, manager, retryCnt, onClose) {
741
+ closeReconnectingModal(screen, manager);
742
+ manager.inReconnectView = true;
743
+ manager.reconnectModal = blessed2.box({
744
+ parent: screen,
745
+ top: "center",
746
+ left: "center",
747
+ width: "50%",
748
+ height: "20%",
749
+ border: {
750
+ type: "line"
751
+ },
752
+ style: {
753
+ border: {
754
+ fg: "red"
755
+ }
756
+ },
757
+ padding: { left: 2, right: 2, top: 1, bottom: 1 },
758
+ tags: true,
759
+ align: "center",
760
+ valign: "middle"
761
+ });
762
+ const content = `{red-fg}{bold}Reconnection Failed{/bold}{/red-fg}
763
+
764
+ Failed to reconnect after ${retryCnt} attempts.
765
+ Tunnel will be closed.
766
+
767
+ {white-bg}{black-fg}Closing in 5 seconds...{/black-fg}{/white-bg}`;
768
+ manager.reconnectModal.setContent(content);
769
+ manager.reconnectModal.focus();
770
+ screen.render();
771
+ const timeout = setTimeout(() => {
772
+ closeReconnectingModal(screen, manager);
773
+ if (onClose) onClose();
774
+ }, 5e3);
775
+ const keyHandler = () => {
776
+ clearTimeout(timeout);
777
+ closeReconnectingModal(screen, manager);
778
+ if (onClose) onClose();
779
+ };
780
+ manager.reconnectModal.key(["escape", "enter", "space"], keyHandler);
781
+ screen.key(["escape", "enter", "space"], keyHandler);
782
+ }
783
+ function showLoadingModal(screen, modalManager, message = "Loading...") {
784
+ if (modalManager.loadingView) return;
785
+ modalManager.loadingBox = blessed2.box({
786
+ parent: screen,
787
+ top: "center",
788
+ left: "center",
789
+ width: "60%",
790
+ height: 8,
791
+ border: { type: "line" },
792
+ style: {
793
+ border: { fg: "yellow" }
794
+ },
795
+ tags: true,
796
+ content: `{center}{yellow-fg}{bold}${message}{/bold}{/yellow-fg}
797
+
798
+ {gray-fg}Press ESC to cancel{/gray-fg}{/center}`,
799
+ valign: "middle"
800
+ });
801
+ modalManager.loadingView = true;
802
+ screen.render();
803
+ }
804
+ function closeLoadingModal(screen, modalManager) {
805
+ if (!modalManager.loadingView || !modalManager.loadingBox) return;
806
+ modalManager.loadingBox.destroy();
807
+ modalManager.loadingBox = null;
808
+ modalManager.loadingView = false;
809
+ screen.render();
810
+ }
811
+ function showErrorModal(screen, modalManager, title = "Error", message) {
812
+ if (modalManager.loadingBox) {
813
+ modalManager.loadingBox.destroy();
814
+ modalManager.loadingBox = null;
815
+ }
816
+ modalManager.loadingBox = blessed2.box({
817
+ parent: screen,
818
+ top: "center",
819
+ left: "center",
820
+ width: "60%",
821
+ height: 9,
822
+ border: { type: "line" },
823
+ style: {
824
+ border: { fg: "red" }
825
+ },
826
+ tags: true,
827
+ content: `{center}{red-fg}{bold}${title}{/bold}{/red-fg}
828
+
829
+ {white-fg}${message}{/white-fg}
830
+
831
+ {gray-fg}Press ESC to close{/gray-fg}{/center}`,
832
+ valign: "middle"
833
+ });
834
+ modalManager.loadingView = true;
835
+ screen.render();
836
+ }
837
+
838
+ // src/tui/blessed/headerFetcher.ts
839
+ async function fetchReqResHeaders(baseUrl, key, signal) {
840
+ if (!baseUrl) {
841
+ return { req: "", res: "" };
842
+ }
843
+ try {
844
+ const [reqRes, resRes] = await Promise.all([
845
+ fetch(`http://${baseUrl}/introspec/getrawrequestheader`, {
846
+ headers: { "X-Introspec-Key": key.toString() },
847
+ signal
848
+ }),
849
+ fetch(`http://${baseUrl}/introspec/getrawresponseheader`, {
850
+ headers: { "X-Introspec-Key": key.toString() },
851
+ signal
852
+ })
853
+ ]);
854
+ const [req, res] = await Promise.all([reqRes.text(), resRes.text()]);
855
+ return { req, res };
856
+ } catch (err) {
857
+ if (err instanceof Error && err.name === "AbortError") {
858
+ throw err;
859
+ }
860
+ logger.error("Error fetching headers:", err instanceof Error ? err.message : err);
861
+ throw err;
862
+ }
863
+ }
864
+
865
+ // src/tui/blessed/components/KeyBindings.ts
866
+ function setupKeyBindings(screen, modalManager, state, callbacks, tunnelConfig) {
867
+ let inactivityTimeout = null;
868
+ const { inactivityHttpSelectorTimeoutMs } = getTuiConfig();
869
+ const INACTIVITY_TIMEOUT_MS = inactivityHttpSelectorTimeoutMs;
870
+ const resetInactivityTimer = () => {
871
+ if (inactivityTimeout) {
872
+ clearTimeout(inactivityTimeout);
873
+ }
874
+ if (state.selectedIndex !== -1) {
875
+ inactivityTimeout = setTimeout(() => {
876
+ callbacks.onSelectedIndexChange(-1, null);
877
+ callbacks.updateRequestsDisplay();
878
+ }, INACTIVITY_TIMEOUT_MS);
879
+ }
880
+ };
881
+ screen.key(["C-c"], async () => {
882
+ await callbacks.onDestroy();
883
+ process.exit(0);
884
+ });
885
+ screen.key(["escape"], () => {
886
+ if (modalManager.loadingView) {
887
+ if (modalManager.fetchAbortController) {
888
+ modalManager.fetchAbortController.abort();
889
+ modalManager.fetchAbortController = null;
890
+ }
891
+ closeLoadingModal(screen, modalManager);
892
+ return;
893
+ }
894
+ if (modalManager.inDetailView) {
895
+ closeDetailModal(screen, modalManager);
896
+ return;
897
+ }
898
+ if (modalManager.keyBindingView) {
899
+ closeKeyBindingsModal(screen, modalManager);
900
+ return;
901
+ }
902
+ if (state.selectedIndex !== -1) {
903
+ if (inactivityTimeout) {
904
+ clearTimeout(inactivityTimeout);
905
+ inactivityTimeout = null;
906
+ }
907
+ callbacks.onSelectedIndexChange(-1, null);
908
+ callbacks.updateRequestsDisplay();
909
+ }
910
+ });
911
+ screen.key(["up"], () => {
912
+ if (modalManager.inDetailView || modalManager.keyBindingView || modalManager.loadingView) return;
913
+ resetInactivityTimer();
914
+ if (state.selectedIndex === -1) {
915
+ const requestKey = state.pairs[0]?.request?.key ?? null;
916
+ callbacks.onSelectedIndexChange(0, requestKey);
917
+ callbacks.updateRequestsDisplay();
918
+ resetInactivityTimer();
919
+ } else if (state.selectedIndex > 0) {
920
+ const newIndex = state.selectedIndex - 1;
921
+ const requestKey = state.pairs[newIndex]?.request?.key ?? null;
922
+ callbacks.onSelectedIndexChange(newIndex, requestKey);
923
+ callbacks.updateRequestsDisplay();
924
+ }
925
+ });
926
+ screen.key(["down"], () => {
927
+ if (modalManager.inDetailView || modalManager.keyBindingView || modalManager.loadingView) return;
928
+ resetInactivityTimer();
929
+ const config = getTuiConfig();
930
+ const limitedLength = Math.min(state.pairs.length, config.maxRequestPairs);
931
+ if (state.selectedIndex === -1) {
932
+ if (limitedLength > 0) {
933
+ const requestKey = state.pairs[0]?.request?.key ?? null;
934
+ callbacks.onSelectedIndexChange(0, requestKey);
935
+ callbacks.updateRequestsDisplay();
936
+ resetInactivityTimer();
937
+ }
938
+ } else if (state.selectedIndex < limitedLength - 1) {
939
+ const newIndex = state.selectedIndex + 1;
940
+ const requestKey = state.pairs[newIndex]?.request?.key ?? null;
941
+ callbacks.onSelectedIndexChange(newIndex, requestKey);
942
+ callbacks.updateRequestsDisplay();
943
+ }
944
+ });
945
+ screen.key(["end"], () => {
946
+ if (modalManager.inDetailView || modalManager.keyBindingView || modalManager.loadingView) return;
947
+ resetInactivityTimer();
948
+ const config = getTuiConfig();
949
+ const limitedLength = Math.min(state.pairs.length, config.maxRequestPairs);
950
+ const lastIndex = Math.max(0, limitedLength - 1);
951
+ if (state.selectedIndex !== lastIndex) {
952
+ const requestKey = state.pairs[lastIndex]?.request?.key ?? null;
953
+ callbacks.onSelectedIndexChange(lastIndex, requestKey);
954
+ callbacks.updateRequestsDisplay();
955
+ }
956
+ });
957
+ screen.key(["enter"], async () => {
958
+ if (modalManager.inDetailView || modalManager.keyBindingView || modalManager.loadingView) return;
959
+ if (state.selectedIndex === -1) return;
960
+ resetInactivityTimer();
961
+ const pair = state.pairs[state.selectedIndex];
962
+ if (pair?.request?.key !== void 0 && pair?.request?.key !== null) {
963
+ const abortController = new AbortController();
964
+ modalManager.fetchAbortController = abortController;
965
+ showLoadingModal(screen, modalManager, "Fetching request details...");
966
+ try {
967
+ const headers = await fetchReqResHeaders(
968
+ tunnelConfig?.webDebugger || "",
969
+ pair.request.key,
970
+ abortController.signal
971
+ );
972
+ if (abortController.signal.aborted) {
973
+ return;
974
+ }
975
+ closeLoadingModal(screen, modalManager);
976
+ modalManager.fetchAbortController = null;
977
+ showDetailModal(screen, modalManager, headers.req, headers.res);
978
+ } catch (err) {
979
+ if (err instanceof Error && err.name === "AbortError" || abortController.signal.aborted) {
980
+ logger.info("Fetch request cancelled by user");
981
+ return;
982
+ }
983
+ closeLoadingModal(screen, modalManager);
984
+ modalManager.fetchAbortController = null;
985
+ const message = err instanceof Error ? err.message : String(err) || "Unknown error occurred";
986
+ logger.error("Fetch error:", err);
987
+ showErrorModal(screen, modalManager, "Failed to fetch request details", message);
988
+ }
989
+ }
990
+ });
991
+ screen.key(["h"], () => {
992
+ if (modalManager.inDetailView || modalManager.loadingView) return;
993
+ if (modalManager.keyBindingView) {
994
+ closeKeyBindingsModal(screen, modalManager);
995
+ } else {
996
+ showKeyBindingsModal(screen, modalManager);
997
+ }
998
+ });
999
+ screen.key(["c"], async () => {
1000
+ if (modalManager.inDetailView || modalManager.keyBindingView || modalManager.loadingView) return;
1001
+ if (state.urls.length > 0) {
1002
+ try {
1003
+ const clipboardy = await import("clipboardy");
1004
+ clipboardy.default.writeSync(state.urls[state.currentQrIndex]);
1005
+ } catch (err) {
1006
+ logger.error("Failed to copy to clipboard:", err);
1007
+ }
1008
+ }
1009
+ });
1010
+ screen.key(["left"], () => {
1011
+ if (modalManager.inDetailView || modalManager.keyBindingView || modalManager.loadingView) return;
1012
+ if (state.currentQrIndex > 0) {
1013
+ callbacks.onQrIndexChange(state.currentQrIndex - 1);
1014
+ callbacks.updateUrlsDisplay();
1015
+ callbacks.updateQrCodeDisplay();
1016
+ }
1017
+ });
1018
+ screen.key(["right"], () => {
1019
+ if (modalManager.inDetailView || modalManager.keyBindingView || modalManager.loadingView) return;
1020
+ if (state.currentQrIndex < state.urls.length - 1) {
1021
+ callbacks.onQrIndexChange(state.currentQrIndex + 1);
1022
+ callbacks.updateUrlsDisplay();
1023
+ callbacks.updateQrCodeDisplay();
1024
+ }
1025
+ });
1026
+ }
1027
+
1028
+ // src/tui/blessed/TunnelTui.ts
1029
+ var TunnelTui = class {
1030
+ constructor(props) {
1031
+ // State
1032
+ this.currentQrIndex = 0;
1033
+ this.selectedIndex = -1;
1034
+ // -1 means no selection
1035
+ this.selectedRequestKey = null;
1036
+ // Track selected request by key
1037
+ this.qrCodes = [];
1038
+ this.stats = {
1039
+ elapsedTime: 0,
1040
+ numLiveConnections: 0,
1041
+ numTotalConnections: 0,
1042
+ numTotalReqBytes: 0,
1043
+ numTotalResBytes: 0,
1044
+ numTotalTxBytes: 0
1045
+ };
1046
+ this.pairs = [];
1047
+ this.webDebuggerConnection = null;
1048
+ this.modalManager = {
1049
+ detailModal: null,
1050
+ keyBindingsModal: null,
1051
+ disconnectModal: null,
1052
+ reconnectModal: null,
1053
+ inDetailView: false,
1054
+ keyBindingView: false,
1055
+ inDisconnectView: false,
1056
+ inReconnectView: false,
1057
+ loadingBox: null,
1058
+ loadingView: false,
1059
+ fetchAbortController: null
1060
+ };
1061
+ this.exitPromiseResolve = null;
1062
+ this.urls = props.urls;
1063
+ this.greet = props.greet || "";
1064
+ this.tunnelConfig = props.tunnelConfig;
1065
+ this.disconnectInfo = props.disconnectInfo;
1066
+ this.onStop = props.onStop;
1067
+ if (props.tunnelInstance) {
1068
+ this.tunnelInstance = props.tunnelInstance;
1069
+ }
1070
+ this.exitPromise = new Promise((resolve) => {
1071
+ this.exitPromiseResolve = resolve;
1072
+ });
1073
+ this.screen = blessed3.screen({
1074
+ smartCSR: true,
1075
+ title: "Pinggy Tunnel",
1076
+ fullUnicode: true
1077
+ });
1078
+ this.setupStatsListener();
1079
+ this.setupWebDebugger();
1080
+ void this.generateQrCodes();
1081
+ this.createUI();
1082
+ this.setupKeyBindings();
1083
+ }
1084
+ setupStatsListener() {
1085
+ globalThis.__PINGGY_TUNNEL_STATS__ = (newStats) => {
1086
+ this.stats = { ...newStats };
1087
+ this.updateStatsDisplay();
1088
+ };
1089
+ }
1090
+ clearSelection() {
1091
+ this.selectedIndex = -1;
1092
+ this.selectedRequestKey = null;
1093
+ }
1094
+ setupWebDebugger() {
1095
+ if (this.tunnelConfig?.webDebugger) {
1096
+ this.webDebuggerConnection = createWebDebuggerConnection(
1097
+ this.tunnelConfig.webDebugger,
1098
+ (pairs) => {
1099
+ this.pairs = pairs;
1100
+ if (this.selectedRequestKey !== null) {
1101
+ const newIndex = pairs.findIndex(
1102
+ (pair) => pair.request?.key === this.selectedRequestKey
1103
+ );
1104
+ if (newIndex !== -1) {
1105
+ this.selectedIndex = newIndex;
1106
+ } else {
1107
+ this.clearSelection();
1108
+ }
1109
+ }
1110
+ this.updateRequestsDisplay();
1111
+ }
1112
+ );
1113
+ }
1114
+ }
1115
+ async generateQrCodes() {
1116
+ if (this.tunnelConfig?.isQRCode && this.urls.length > 0) {
1117
+ this.qrCodes = await createQrCodes(this.urls);
1118
+ this.updateQrCodeDisplay();
1119
+ }
1120
+ }
1121
+ // Create the UI based on terminal size
1122
+ createUI() {
1123
+ this.buildUI();
1124
+ this.screen.on("resize", () => {
1125
+ this.handleResize();
1126
+ });
1127
+ }
1128
+ buildUI() {
1129
+ const width = this.screen.width;
1130
+ if (width < MIN_WIDTH_WARNING) {
1131
+ this.uiElements = {
1132
+ mainContainer: createWarningUI(this.screen),
1133
+ warningBox: createWarningUI(this.screen)
1134
+ };
1135
+ this.screen.render();
1136
+ return;
1137
+ }
1138
+ if (width < SIMPLE_LAYOUT_THRESHOLD) {
1139
+ this.uiElements = createSimpleUI(this.screen, this.urls, this.greet);
1140
+ } else {
1141
+ this.uiElements = createFullUI(this.screen, this.urls, this.greet, this.tunnelConfig);
1142
+ }
1143
+ this.refreshDisplays();
1144
+ this.screen.render();
1145
+ }
1146
+ refreshDisplays() {
1147
+ this.updateUrlsDisplay();
1148
+ this.updateStatsDisplay();
1149
+ this.updateRequestsDisplay();
1150
+ this.updateQrCodeDisplay();
1151
+ }
1152
+ updateUrlsDisplay() {
1153
+ updateUrlsDisplay(
1154
+ this.uiElements?.urlsBox,
1155
+ this.screen,
1156
+ this.urls,
1157
+ this.currentQrIndex
1158
+ );
1159
+ }
1160
+ updateStatsDisplay() {
1161
+ updateStatsDisplay(
1162
+ this.uiElements?.statsBox,
1163
+ this.screen,
1164
+ this.stats
1165
+ );
1166
+ }
1167
+ updateRequestsDisplay() {
1168
+ const result = updateRequestsDisplay(
1169
+ this.uiElements?.requestsBox,
1170
+ this.screen,
1171
+ this.pairs,
1172
+ this.selectedIndex
1173
+ );
1174
+ if (result.adjustedSelectedIndex !== this.selectedIndex) {
1175
+ if (result.adjustedSelectedIndex === -1) {
1176
+ this.clearSelection();
1177
+ } else {
1178
+ this.selectedIndex = result.adjustedSelectedIndex;
1179
+ }
1180
+ }
1181
+ if (result.trimmedPairs !== this.pairs) {
1182
+ this.pairs = result.trimmedPairs;
1183
+ }
1184
+ }
1185
+ updateQrCodeDisplay() {
1186
+ updateQrCodeDisplay(
1187
+ this.uiElements?.qrCodeBox,
1188
+ this.screen,
1189
+ this.qrCodes,
1190
+ this.urls,
1191
+ this.currentQrIndex
1192
+ );
1193
+ }
1194
+ setupKeyBindings() {
1195
+ const self = this;
1196
+ const state = {
1197
+ get currentQrIndex() {
1198
+ return self.currentQrIndex;
1199
+ },
1200
+ set currentQrIndex(value) {
1201
+ self.currentQrIndex = value;
1202
+ },
1203
+ get selectedIndex() {
1204
+ return self.selectedIndex;
1205
+ },
1206
+ set selectedIndex(value) {
1207
+ self.selectedIndex = value;
1208
+ },
1209
+ get pairs() {
1210
+ return self.pairs;
1211
+ },
1212
+ get urls() {
1213
+ return self.urls;
1214
+ }
1215
+ };
1216
+ const callbacks = {
1217
+ onQrIndexChange: (index) => {
1218
+ self.currentQrIndex = index;
1219
+ },
1220
+ onSelectedIndexChange: (index, requestKey) => {
1221
+ self.selectedIndex = index;
1222
+ self.selectedRequestKey = requestKey;
1223
+ },
1224
+ onDestroy: () => self.destroy(),
1225
+ updateUrlsDisplay: () => self.updateUrlsDisplay(),
1226
+ updateQrCodeDisplay: () => self.updateQrCodeDisplay(),
1227
+ updateRequestsDisplay: () => self.updateRequestsDisplay()
1228
+ };
1229
+ setupKeyBindings(
1230
+ this.screen,
1231
+ this.modalManager,
1232
+ state,
1233
+ callbacks,
1234
+ this.tunnelConfig
1235
+ );
1236
+ }
1237
+ handleResize() {
1238
+ this.screen.children.forEach((child) => child.destroy());
1239
+ this.buildUI();
1240
+ }
1241
+ updateDisconnectInfo(info) {
1242
+ this.disconnectInfo = info;
1243
+ if (info?.disconnected) {
1244
+ const message = info.error ? `Error: ${info.error}
1245
+ Tunnel will be closed.` : info.messages?.join("\n") || "Disconnect request received. Tunnel will be closed.";
1246
+ showDisconnectModal(
1247
+ this.screen,
1248
+ this.modalManager,
1249
+ message,
1250
+ () => this.destroy()
1251
+ );
1252
+ }
1253
+ }
1254
+ updateReconnectingInfo(retryCnt, message) {
1255
+ showReconnectingModal(
1256
+ this.screen,
1257
+ this.modalManager,
1258
+ retryCnt,
1259
+ message
1260
+ );
1261
+ }
1262
+ closeReconnectingInfo() {
1263
+ closeReconnectingModal(this.screen, this.modalManager);
1264
+ }
1265
+ updateReconnectionFailed(retryCnt) {
1266
+ showReconnectionFailedModal(
1267
+ this.screen,
1268
+ this.modalManager,
1269
+ retryCnt,
1270
+ () => this.destroy()
1271
+ );
1272
+ }
1273
+ start() {
1274
+ this.screen.render();
1275
+ }
1276
+ waitUntilExit() {
1277
+ return this.exitPromise;
1278
+ }
1279
+ /**
1280
+ * Update stats externally (used when TUI receives data from daemon WS stream).
1281
+ */
1282
+ updateStats(newStats) {
1283
+ this.stats = { ...newStats };
1284
+ this.updateStatsDisplay();
1285
+ }
1286
+ /**
1287
+ * Update URLs externally (used on reconnect from daemon WS stream).
1288
+ */
1289
+ updateUrls(newUrls) {
1290
+ this.urls = newUrls;
1291
+ this.updateUrlsDisplay();
1292
+ void this.generateQrCodes();
1293
+ }
1294
+ /**
1295
+ * Show disconnect modal externally (from daemon WS stream).
1296
+ */
1297
+ showDisconnectModal(error, messages) {
1298
+ this.updateDisconnectInfo({
1299
+ disconnected: true,
1300
+ error,
1301
+ messages
1302
+ });
1303
+ }
1304
+ /**
1305
+ * Stop TUI without stopping tunnel (used for detach).
1306
+ */
1307
+ stop() {
1308
+ delete globalThis.__PINGGY_TUNNEL_STATS__;
1309
+ if (this.webDebuggerConnection) {
1310
+ this.webDebuggerConnection.close();
1311
+ }
1312
+ this.screen.destroy();
1313
+ if (this.exitPromiseResolve) {
1314
+ this.exitPromiseResolve();
1315
+ }
1316
+ }
1317
+ async destroy() {
1318
+ if (this.onStop) {
1319
+ await this.onStop();
1320
+ } else if (this.tunnelInstance?.tunnelid) {
1321
+ const manager = TunnelManager.getInstance();
1322
+ manager.stopTunnel(this.tunnelInstance.tunnelid);
1323
+ }
1324
+ delete globalThis.__PINGGY_TUNNEL_STATS__;
1325
+ if (this.webDebuggerConnection) {
1326
+ this.webDebuggerConnection.close();
1327
+ }
1328
+ this.screen.destroy();
1329
+ if (this.exitPromiseResolve) {
1330
+ this.exitPromiseResolve();
1331
+ }
1332
+ }
1333
+ };
1334
+ var TunnelTui_default = TunnelTui;
1335
+ export {
1336
+ TunnelTui,
1337
+ TunnelTui_default as default
1338
+ };