pinggy 0.4.9 → 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.
- package/CLAUDE.md +112 -0
- package/README.md +214 -97
- package/dist/TunnelManager-OPUMAZFX.js +11 -0
- package/dist/TunnelTui-QZEWWH2H.js +1338 -0
- package/dist/{chunk-3RTRUYNW.js → chunk-7G6SJEEA.js} +35 -7
- package/dist/chunk-BFARGPGP.js +164 -0
- package/dist/chunk-DLNUDW6G.js +1690 -0
- package/dist/chunk-FVLXFHBL.js +2157 -0
- package/dist/chunk-GBYF2H4H.js +77 -0
- package/dist/chunk-HUP6YWH6.js +269 -0
- package/dist/chunk-MT44NAXX.js +36 -0
- package/dist/chunk-UB26QJ4T.js +10 -0
- package/dist/chunk-YJQC6LQN.js +3407 -0
- package/dist/configStore-TSGRNOE3.js +42 -0
- package/dist/daemonChild-E2CORSSB.js +24 -0
- package/dist/daemonConfig-G6S46GPJ.js +9 -0
- package/dist/index.cjs +5153 -1596
- package/dist/index.d.cts +473 -13
- package/dist/index.d.ts +473 -13
- package/dist/index.js +12 -5
- package/dist/ipcClient-LZQCCNMR.js +6 -0
- package/dist/main-F4U5R4SW.js +42 -0
- package/dist/workers/file_serve_worker.cjs +70 -21
- package/dist/workers/file_serve_worker.js +15 -9
- package/eslint.config.js +27 -0
- package/package.json +8 -4
- package/dist/chunk-YFTL44B3.js +0 -2857
- package/dist/main-4WTJG54V.js +0 -2925
|
@@ -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
|
+
};
|