pinggy 0.3.4 → 0.3.6

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.
Files changed (68) hide show
  1. package/README.md +1 -1
  2. package/dist/chunk-65R2GMKQ.js +2101 -0
  3. package/dist/index.cjs +1814 -1362
  4. package/dist/index.d.cts +616 -0
  5. package/dist/index.d.ts +616 -0
  6. package/dist/index.js +38 -55
  7. package/dist/{main-CZY6GID4.js → main-2QDG7PWL.js} +229 -1726
  8. package/package.json +3 -4
  9. package/.github/workflows/npm-publish-github-packages.yml +0 -34
  10. package/.github/workflows/publish-binaries.yml +0 -223
  11. package/Makefile +0 -4
  12. package/caxa_build.js +0 -24
  13. package/dist/chunk-T5ESYDJY.js +0 -121
  14. package/ent.plist +0 -14
  15. package/jest.config.js +0 -19
  16. package/src/_tests_/build_config.test.ts +0 -91
  17. package/src/cli/buildConfig.ts +0 -475
  18. package/src/cli/defaults.ts +0 -20
  19. package/src/cli/extendedOptions.ts +0 -153
  20. package/src/cli/help.ts +0 -43
  21. package/src/cli/options.ts +0 -50
  22. package/src/cli/starCli.ts +0 -229
  23. package/src/index.ts +0 -30
  24. package/src/logger.ts +0 -138
  25. package/src/main.ts +0 -87
  26. package/src/remote_management/handler.ts +0 -244
  27. package/src/remote_management/remoteManagement.ts +0 -226
  28. package/src/remote_management/remote_schema.ts +0 -176
  29. package/src/remote_management/websocket_handlers.ts +0 -180
  30. package/src/tui/blessed/TunnelTui.ts +0 -340
  31. package/src/tui/blessed/components/DisplayUpdaters.ts +0 -189
  32. package/src/tui/blessed/components/KeyBindings.ts +0 -236
  33. package/src/tui/blessed/components/Modals.ts +0 -302
  34. package/src/tui/blessed/components/UIComponents.ts +0 -306
  35. package/src/tui/blessed/components/index.ts +0 -4
  36. package/src/tui/blessed/config.ts +0 -53
  37. package/src/tui/blessed/headerFetcher.ts +0 -42
  38. package/src/tui/blessed/index.ts +0 -2
  39. package/src/tui/blessed/qrCodeGenerator.ts +0 -20
  40. package/src/tui/blessed/webDebuggerConnection.ts +0 -128
  41. package/src/tui/ink/asciArt.ts +0 -7
  42. package/src/tui/ink/hooks/useQrCodes.ts +0 -27
  43. package/src/tui/ink/hooks/useReqResHeaders.ts +0 -27
  44. package/src/tui/ink/hooks/useTerminalSize.ts +0 -26
  45. package/src/tui/ink/hooks/useTerminalStats.ts +0 -24
  46. package/src/tui/ink/hooks/useWebDebugger.ts +0 -98
  47. package/src/tui/ink/index.tsx +0 -243
  48. package/src/tui/ink/layout/Borders.tsx +0 -15
  49. package/src/tui/ink/layout/Container.tsx +0 -15
  50. package/src/tui/ink/sections/DebuggerDetailModal.tsx +0 -53
  51. package/src/tui/ink/sections/KeyBindings.tsx +0 -58
  52. package/src/tui/ink/sections/QrCodeSection.tsx +0 -28
  53. package/src/tui/ink/sections/StatsSection.tsx +0 -20
  54. package/src/tui/ink/sections/URLsSection.tsx +0 -53
  55. package/src/tui/ink/utils/utils.ts +0 -35
  56. package/src/tui/spinner/spinner.ts +0 -64
  57. package/src/tunnel_manager/TunnelManager.ts +0 -1212
  58. package/src/types.ts +0 -255
  59. package/src/utils/FileServer.ts +0 -112
  60. package/src/utils/detect_vc_redist_on_windows.ts +0 -167
  61. package/src/utils/getFreePort.ts +0 -41
  62. package/src/utils/htmlTemplates.ts +0 -146
  63. package/src/utils/parseArgs.ts +0 -79
  64. package/src/utils/printer.ts +0 -81
  65. package/src/utils/util.ts +0 -18
  66. package/src/workers/file_serve_worker.ts +0 -33
  67. package/tsconfig.json +0 -17
  68. package/tsup.config.ts +0 -12
@@ -1,180 +0,0 @@
1
- import WebSocket from "ws";
2
- import { logger } from "../logger.js";
3
- import { ErrorCode, NewErrorResponseObject, ResponseObj, ErrorResponse, isErrorResponse, NewResponseObject } from "../types.js";
4
- import { TunnelOperations, TunnelResponse } from "./handler.js";
5
- import { GetSchema, RestartSchema, StartSchema, StopSchema, UpdateConfigSchema } from "./remote_schema.js";
6
- import z from "zod";
7
- import CLIPrinter from "../utils/printer.js";
8
-
9
- export interface ConnectionStatus {
10
- success: boolean;
11
- error_code?: number;
12
- error_msg?: string;
13
- }
14
-
15
- export interface WebSocketRequest {
16
- requestid: string;
17
- command: string;
18
- data?: string;
19
- }
20
-
21
- type CommandName = "start" | "stop" | "get" | "restart" | "updateconfig" | "list";
22
-
23
- export class WebSocketCommandHandler {
24
- private tunnelHandler = new TunnelOperations();
25
-
26
- private safeParse(text?: string): unknown {
27
- if (!text) return undefined;
28
- try {
29
- return JSON.parse(text);
30
- } catch (e) {
31
- logger.warn("Invalid JSON payload", { error: String(e), text });
32
- return undefined;
33
- }
34
- }
35
-
36
- private sendResponse(ws: WebSocket, resp: ResponseObj) {
37
- const payload = {
38
- ...resp,
39
- response: Buffer.from(resp.response || []).toString("base64"),
40
- };
41
- ws.send(JSON.stringify(payload));
42
- }
43
-
44
- private sendError(ws: WebSocket, req: Partial<WebSocketRequest>, message: string, code = ErrorCode.InternalServerError) {
45
- const resp = NewErrorResponseObject({ code, message });
46
- resp.command = req.command || "";
47
- resp.requestid = req.requestid || "";
48
- this.sendResponse(ws, resp);
49
- }
50
-
51
- private async handleStartReq(req: WebSocketRequest, raw: unknown): Promise<ResponseObj> {
52
- const dc = StartSchema.parse(raw);
53
- CLIPrinter.info("Starting tunnel with config name: " + dc.tunnelConfig.configname);
54
- const result = await this.tunnelHandler.handleStart(dc.tunnelConfig);
55
- return this.wrapResponse(result, req);
56
- }
57
-
58
- private async handleStopReq(req: WebSocketRequest, raw: unknown): Promise<ResponseObj> {
59
- const dc = StopSchema.parse(raw);
60
- CLIPrinter.info("Stopping tunnel with ID: " + dc.tunnelID);
61
- const result = await this.tunnelHandler.handleStop(dc.tunnelID);
62
- return this.wrapResponse(result, req);
63
- }
64
-
65
- private async handleGetReq(req: WebSocketRequest, raw: unknown): Promise<ResponseObj> {
66
- const dc = GetSchema.parse(raw);
67
- const result = await this.tunnelHandler.handleGet(dc.tunnelID);
68
- return this.wrapResponse(result, req);
69
- }
70
-
71
- private async handleRestartReq(req: WebSocketRequest, raw: unknown): Promise<ResponseObj> {
72
- const dc = RestartSchema.parse(raw);
73
- const result = await this.tunnelHandler.handleRestart(dc.tunnelID);
74
- return this.wrapResponse(result, req);
75
- }
76
-
77
- private async handleUpdateConfigReq(req: WebSocketRequest, raw: unknown): Promise<ResponseObj> {
78
- const dc = UpdateConfigSchema.parse(raw);
79
- const result = await this.tunnelHandler.handleUpdateConfig(dc.tunnelConfig);
80
- return this.wrapResponse(result, req);
81
- }
82
-
83
- private async handleListReq(req: WebSocketRequest): Promise<ResponseObj> {
84
- const result = await this.tunnelHandler.handleList();
85
- return this.wrapResponse(result, req);
86
- }
87
-
88
- private wrapResponse(result: ResponseObj | ErrorResponse | TunnelResponse | TunnelResponse[], req: WebSocketRequest): ResponseObj {
89
- if (isErrorResponse(result)) {
90
- const errResp = NewErrorResponseObject(result);
91
- errResp.command = req.command;
92
- errResp.requestid = req.requestid;
93
- return errResp;
94
- }
95
-
96
- // Temporary workaround to remove allowPreflight from response
97
- const finalResult = JSON.parse(JSON.stringify(result));
98
- if (Array.isArray(finalResult)) {
99
- finalResult.forEach(item => {
100
- if (item?.tunnelconfig) {
101
- delete item.tunnelconfig.allowPreflight;
102
- }
103
- });
104
- } else if (finalResult?.tunnelconfig) {
105
- delete finalResult.tunnelconfig.allowPreflight;
106
- }
107
- const respObj = NewResponseObject(finalResult);
108
- respObj.command = req.command;
109
- respObj.requestid = req.requestid;
110
- return respObj;
111
- }
112
-
113
- async handle(ws: WebSocket, req: WebSocketRequest) {
114
- const cmd = (req.command || "").toLowerCase() as CommandName | string;
115
- const raw = this.safeParse(req.data);
116
-
117
- try {
118
- let response: ResponseObj;
119
- switch (cmd as CommandName) {
120
- case "start": {
121
- response = await this.handleStartReq(req, raw);
122
- break;
123
- }
124
- case "stop": {
125
- response = await this.handleStopReq(req, raw);
126
- break;
127
- }
128
- case "get": {
129
- response = await this.handleGetReq(req, raw);
130
- break;
131
- }
132
- case "restart": {
133
- response = await this.handleRestartReq(req, raw);
134
- break;
135
- }
136
- case "updateconfig": {
137
- response = await this.handleUpdateConfigReq(req, raw);
138
- break;
139
- }
140
- case "list": {
141
- response = await this.handleListReq(req);
142
- break;
143
- }
144
- default:
145
- if (typeof req.command === 'string') {
146
- logger.warn("Unknown command", { command: req.command });
147
- }
148
- return this.sendError(ws, req, "Invalid command");
149
- }
150
- logger.debug("Sending response", { command: response.command, requestid: response.requestid });
151
- this.sendResponse(ws, response);
152
- } catch (e: any) {
153
- if (e instanceof z.ZodError) {
154
- logger.warn("Validation failed", { cmd, issues: e.issues });
155
- return this.sendError(ws, req, "Invalid request data", ErrorCode.InvalidBodyFormatError);
156
- }
157
- logger.error("Error handling command", { cmd, error: String(e) });
158
- return this.sendError(ws, req, e?.message || "Internal error");
159
- }
160
- }
161
- }
162
-
163
- // // Returns true if connection status is OK else sends logs and returns false
164
- export function handleConnectionStatusMessage(firstMessage: WebSocket.Data): boolean {
165
- try {
166
- const text = typeof firstMessage === 'string' ? firstMessage : firstMessage.toString();
167
- const cs = JSON.parse(text) as ConnectionStatus;
168
- if (!cs.success) {
169
- const msg = cs.error_msg || "Connection failed";
170
- CLIPrinter.warn(`Connection failed: ${msg}`);
171
- logger.warn("Remote management connection failed", { error_code: cs.error_code, error_msg: msg });
172
- return false;
173
- }
174
- return true;
175
- } catch (e) {
176
- logger.warn("Failed to parse connection status message", { error: String(e) });
177
- // If parsing fails, assume connection is okay
178
- return true;
179
- }
180
- }
@@ -1,340 +0,0 @@
1
- import blessed from "blessed";
2
- import { FinalConfig, ReqResPair } from "../../types.js";
3
- import { TunnelUsageType } from "@pinggy/pinggy";
4
- import { createQrCodes } from "./qrCodeGenerator.js";
5
- import { createWebDebuggerConnection, WebDebuggerConnection } from "./webDebuggerConnection.js";
6
- import { ManagedTunnel, TunnelManager } from "../../tunnel_manager/TunnelManager.js";
7
- import {
8
- createFullUI,
9
- createSimpleUI,
10
- createWarningUI,
11
- UIElements,
12
- MIN_WIDTH_WARNING,
13
- SIMPLE_LAYOUT_THRESHOLD,
14
- } from "./components/UIComponents.js";
15
- import {
16
- updateUrlsDisplay,
17
- updateStatsDisplay,
18
- updateRequestsDisplay,
19
- updateQrCodeDisplay,
20
- } from "./components/DisplayUpdaters.js";
21
- import {
22
- ModalManager,
23
- showDisconnectModal,
24
- } from "./components/Modals.js";
25
- import {
26
- setupKeyBindings,
27
- KeyBindingsState,
28
- KeyBindingsCallbacks,
29
- } from "./components/KeyBindings.js";
30
-
31
- interface TunnelAppProps {
32
- urls: string[];
33
- greet?: string;
34
- tunnelConfig?: FinalConfig;
35
- disconnectInfo?: {
36
- disconnected: boolean;
37
- error?: string;
38
- messages?: string[];
39
- } | null;
40
- tunnelInstance?:ManagedTunnel
41
- }
42
-
43
- export class TunnelTui {
44
- private screen: blessed.Widgets.Screen;
45
- private urls: string[];
46
- private greet: string;
47
- private tunnelConfig: FinalConfig | undefined;
48
- private disconnectInfo: TunnelAppProps["disconnectInfo"];
49
-
50
- // State
51
- private currentQrIndex: number = 0;
52
- private selectedIndex: number = -1; // -1 means no selection
53
- private selectedRequestKey: number | null = null; // Track selected request by key
54
- private qrCodes: string[] = [];
55
- private stats: TunnelUsageType = {
56
- elapsedTime: 0,
57
- numLiveConnections: 0,
58
- numTotalConnections: 0,
59
- numTotalReqBytes: 0,
60
- numTotalResBytes: 0,
61
- numTotalTxBytes: 0,
62
- };
63
- private pairs: ReqResPair[] = [];
64
- private webDebuggerConnection: WebDebuggerConnection | null = null;
65
-
66
- // UI Elements
67
- private uiElements!: UIElements;
68
- private modalManager: ModalManager = {
69
- detailModal: null,
70
- keyBindingsModal: null,
71
- disconnectModal: null,
72
- inDetailView: false,
73
- keyBindingView: false,
74
- inDisconnectView: false,
75
- loadingBox: null,
76
- loadingView: false,
77
- fetchAbortController: null,
78
- };
79
- private tunnelInstance?: ManagedTunnel
80
-
81
- private exitPromiseResolve: (() => void) | null = null;
82
- private exitPromise: Promise<void>;
83
-
84
- constructor(props: TunnelAppProps) {
85
- this.urls = props.urls;
86
- this.greet = props.greet || "";
87
- this.tunnelConfig = props.tunnelConfig;
88
- this.disconnectInfo = props.disconnectInfo;
89
- if(props.tunnelInstance){
90
- this.tunnelInstance=props.tunnelInstance
91
- }
92
-
93
- this.exitPromise = new Promise((resolve) => {
94
- this.exitPromiseResolve = resolve;
95
- });
96
-
97
- this.screen = blessed.screen({
98
- smartCSR: true,
99
- title: "Pinggy Tunnel",
100
- fullUnicode: true,
101
- });
102
-
103
- this.setupStatsListener();
104
- this.setupWebDebugger();
105
- this.generateQrCodes();
106
- this.createUI();
107
- this.setupKeyBindings();
108
- }
109
-
110
- private setupStatsListener() {
111
- globalThis.__PINGGY_TUNNEL_STATS__ = (newStats: TunnelUsageType) => {
112
- this.stats = { ...newStats };
113
- this.updateStatsDisplay();
114
- };
115
- }
116
-
117
- private clearSelection() {
118
- this.selectedIndex = -1;
119
- this.selectedRequestKey = null;
120
- }
121
-
122
- private setupWebDebugger() {
123
- if (this.tunnelConfig?.webDebugger) {
124
- this.webDebuggerConnection = createWebDebuggerConnection(
125
- this.tunnelConfig.webDebugger,
126
- (pairs) => {
127
- this.pairs = pairs;
128
-
129
- // If there's a selected request key, find its new index
130
- if (this.selectedRequestKey !== null) {
131
- const newIndex = pairs.findIndex(
132
- (pair) => pair.request?.key === this.selectedRequestKey
133
- );
134
-
135
- if (newIndex !== -1) {
136
- // Request still exists, update index
137
- this.selectedIndex = newIndex;
138
- } else {
139
- // Request no longer exists, clear selection
140
- this.clearSelection();
141
- }
142
- }
143
-
144
- this.updateRequestsDisplay();
145
- }
146
- );
147
- }
148
- }
149
-
150
- private async generateQrCodes() {
151
- if (this.tunnelConfig?.qrCode && this.urls.length > 0) {
152
- this.qrCodes = await createQrCodes(this.urls);
153
- this.updateQrCodeDisplay();
154
- }
155
- }
156
-
157
- // Create the UI based on terminal size
158
- private createUI() {
159
- this.buildUI();
160
-
161
- this.screen.on("resize", () => {
162
- this.handleResize();
163
- });
164
- }
165
-
166
- private buildUI() {
167
- const width = this.screen.width as number;
168
-
169
- if (width < MIN_WIDTH_WARNING) {
170
- this.uiElements = {
171
- mainContainer: createWarningUI(this.screen),
172
- urlsBox: null as any,
173
- statsBox: null as any,
174
- requestsBox: null as any,
175
- footerBox: null as any,
176
- warningBox: createWarningUI(this.screen),
177
- };
178
- this.screen.render();
179
- return;
180
- }
181
-
182
- if (width < SIMPLE_LAYOUT_THRESHOLD) {
183
- this.uiElements = createSimpleUI(this.screen, this.urls, this.greet);
184
- } else {
185
- this.uiElements = createFullUI(this.screen, this.urls, this.greet, this.tunnelConfig);
186
- }
187
-
188
- this.refreshDisplays();
189
- this.screen.render();
190
- }
191
-
192
- private refreshDisplays() {
193
- this.updateUrlsDisplay();
194
- this.updateStatsDisplay();
195
- this.updateRequestsDisplay();
196
- this.updateQrCodeDisplay();
197
- }
198
-
199
- private updateUrlsDisplay() {
200
- updateUrlsDisplay(
201
- this.uiElements?.urlsBox,
202
- this.screen,
203
- this.urls,
204
- this.currentQrIndex
205
- );
206
- }
207
-
208
- private updateStatsDisplay() {
209
- updateStatsDisplay(
210
- this.uiElements?.statsBox,
211
- this.screen,
212
- this.stats
213
- );
214
- }
215
-
216
- private updateRequestsDisplay() {
217
- const result = updateRequestsDisplay(
218
- this.uiElements?.requestsBox,
219
- this.screen,
220
- this.pairs,
221
- this.selectedIndex
222
- );
223
-
224
- // Update selectedIndex if it was adjusted due to trimming
225
- if (result.adjustedSelectedIndex !== this.selectedIndex) {
226
- if (result.adjustedSelectedIndex === -1) {
227
- // Selection was cleared due to trimming
228
- this.clearSelection();
229
- } else {
230
- // Update to new index
231
- this.selectedIndex = result.adjustedSelectedIndex;
232
- }
233
- }
234
-
235
- // Update pairs if they were trimmed (different reference means trimming occurred)
236
- if (result.trimmedPairs !== this.pairs) {
237
- this.pairs = result.trimmedPairs;
238
- }
239
- }
240
-
241
- private updateQrCodeDisplay() {
242
- updateQrCodeDisplay(
243
- this.uiElements?.qrCodeBox,
244
- this.screen,
245
- this.qrCodes,
246
- this.urls,
247
- this.currentQrIndex
248
- );
249
- }
250
-
251
- private setupKeyBindings() {
252
- const self = this;
253
-
254
-
255
- // Create a state object with getters to always get current values
256
- const state: KeyBindingsState = {
257
- get currentQrIndex() { return self.currentQrIndex; },
258
- set currentQrIndex(value: number) { self.currentQrIndex = value; },
259
- get selectedIndex() { return self.selectedIndex; },
260
- set selectedIndex(value: number) { self.selectedIndex = value; },
261
- get pairs() { return self.pairs; },
262
- get urls() { return self.urls; },
263
- };
264
-
265
- const callbacks: KeyBindingsCallbacks = {
266
- onQrIndexChange: (index: number) => {
267
- self.currentQrIndex = index;
268
- },
269
- onSelectedIndexChange: (index: number, requestKey: number | null) => {
270
- self.selectedIndex = index;
271
- self.selectedRequestKey = requestKey;
272
- },
273
- onDestroy: () => self.destroy(),
274
- updateUrlsDisplay: () => self.updateUrlsDisplay(),
275
- updateQrCodeDisplay: () => self.updateQrCodeDisplay(),
276
- updateRequestsDisplay: () => self.updateRequestsDisplay(),
277
- };
278
-
279
- setupKeyBindings(
280
- this.screen,
281
- this.modalManager,
282
- state,
283
- callbacks,
284
- this.tunnelConfig,
285
- );
286
- }
287
-
288
- private handleResize() {
289
- // Destroy current UI and recreate based on new size
290
- this.screen.children.forEach((child) => child.destroy());
291
- this.buildUI();
292
- }
293
-
294
- public updateDisconnectInfo(info: TunnelAppProps["disconnectInfo"]) {
295
- this.disconnectInfo = info;
296
- if (info?.disconnected) {
297
- const message = info.error
298
- ? `Error: ${info.error}\nTunnel will be closed.`
299
- : info.messages?.join('\n') || 'Disconnect request received. Tunnel will be closed.';
300
-
301
- showDisconnectModal(
302
- this.screen,
303
- this.modalManager,
304
- message,
305
- () => this.destroy()
306
- );
307
- }
308
- }
309
-
310
- public start() {
311
- this.screen.render();
312
- }
313
-
314
- public waitUntilExit(): Promise<void> {
315
- return this.exitPromise;
316
- }
317
-
318
- public destroy() {
319
- // Stop the tunnel first
320
- if (this.tunnelInstance?.tunnelid) {
321
- const manager = TunnelManager.getInstance();
322
- manager.stopTunnel(this.tunnelInstance.tunnelid);
323
- }
324
-
325
- // Cleanup
326
- delete globalThis.__PINGGY_TUNNEL_STATS__;
327
-
328
- if (this.webDebuggerConnection) {
329
- this.webDebuggerConnection.close();
330
- }
331
-
332
- this.screen.destroy();
333
-
334
- if (this.exitPromiseResolve) {
335
- this.exitPromiseResolve();
336
- }
337
- }
338
- }
339
-
340
- export default TunnelTui;
@@ -1,189 +0,0 @@
1
- import blessed from "blessed";
2
- import { TunnelUsageType } from "@pinggy/pinggy";
3
- import { ReqResPair } from "../../../types.js";
4
- import { getBytesInt, getStatusColor } from "../../ink/utils/utils.js";
5
- import { getTuiConfig } from "../config.js";
6
-
7
- /**
8
- * Updates the URLs display box
9
- */
10
- export function updateUrlsDisplay(
11
- urlsBox: blessed.Widgets.BoxElement | undefined,
12
- screen: blessed.Widgets.Screen,
13
- urls: string[],
14
- currentQrIndex: number
15
- ): void {
16
- if (!urlsBox) return;
17
-
18
- let content = "{green-fg}{bold}Public URLs{/bold}{/green-fg}\n";
19
- urls.forEach((url, index) => {
20
- const isSelected = index === currentQrIndex;
21
- const prefix = isSelected ? "→ " : "• ";
22
- const color = isSelected ? "yellow" : "magenta";
23
-
24
- if (isSelected) {
25
- content += `{bold}{${color}-fg}${prefix}${url}{/${color}-fg}{/bold}\n`;
26
- } else {
27
- content += `{${color}-fg}${prefix}${url}{/${color}-fg}\n`;
28
- }
29
- });
30
-
31
- urlsBox.setContent(content);
32
- screen.render();
33
- }
34
-
35
- /**
36
- * Updates the stats display box
37
- */
38
- export function updateStatsDisplay(
39
- statsBox: blessed.Widgets.BoxElement | undefined,
40
- screen: blessed.Widgets.Screen,
41
- stats: TunnelUsageType
42
- ): void {
43
- if (!statsBox) return;
44
-
45
- const content = `{green-fg}{bold}Live Stats{/bold}{/green-fg}
46
- Elapsed: ${stats.elapsedTime}s
47
- Live Connections: ${stats.numLiveConnections}
48
- Total Connections: ${stats.numTotalConnections}
49
- Request: ${getBytesInt(stats.numTotalReqBytes)}
50
- Response: ${getBytesInt(stats.numTotalResBytes)}
51
- Total Transfer: ${getBytesInt(stats.numTotalTxBytes)}`;
52
-
53
- statsBox.setContent(content);
54
- statsBox.style = { ...statsBox.style };
55
- (statsBox as any).parseContent();
56
- screen.render();
57
- }
58
-
59
- /**
60
- * Updates the requests display box
61
- * This function displays HTTP requests with reversed order (latest at top):
62
- * - Limits the total pairs to maxRequestPairs (configurable)
63
- * - Shows latest requests at top when no selection (selectedIndex = -1)
64
- * - Ensures the selected item is always visible when there is selection
65
- * - selectedIndex -1 means no selection, viewport shows top (latest requests)
66
- */
67
- export function updateRequestsDisplay(
68
- requestsBox: blessed.Widgets.BoxElement | undefined,
69
- screen: blessed.Widgets.Screen,
70
- pairs: ReqResPair[],
71
- selectedIndex: number
72
- ): { adjustedSelectedIndex: number; trimmedPairs: ReqResPair[] } {
73
- const config = getTuiConfig();
74
- const { maxRequestPairs, visibleRequestCount, viewportScrollMargin } = config;
75
-
76
- if (!requestsBox) {
77
- return { adjustedSelectedIndex: selectedIndex, trimmedPairs: pairs };
78
- }
79
-
80
- // pairs array (latest first)
81
- let allPairs = pairs;
82
- let trimmedPairs = pairs;
83
-
84
- if (allPairs.length > maxRequestPairs) {
85
- // Keep only the first maxRequestPairs (which are the latest ones)
86
- allPairs = allPairs.slice(0, maxRequestPairs);
87
- trimmedPairs = allPairs;
88
- }
89
-
90
- const totalPairs = allPairs.length;
91
-
92
- // Adjust selectedIndex if it's now out of bounds due to trimming
93
- // If the selected item was trimmed, clear the selection
94
- let adjustedSelectedIndex = selectedIndex;
95
- if (adjustedSelectedIndex >= totalPairs) {
96
- adjustedSelectedIndex = -1;
97
- }
98
-
99
- // Calculate viewport window
100
- let viewportStart: number;
101
-
102
- if (totalPairs <= visibleRequestCount) {
103
- // All pairs fit in the viewport
104
- viewportStart = 0;
105
- } else if (adjustedSelectedIndex === -1) {
106
- // No selection: show latest requests (top of the list)
107
- viewportStart = 0;
108
- } else {
109
- // Has selection: ensure selector is visible
110
- viewportStart = 0;
111
-
112
- // If selector would be below the visible area, scroll down
113
- if (adjustedSelectedIndex >= visibleRequestCount - viewportScrollMargin) {
114
- viewportStart = Math.min(
115
- totalPairs - visibleRequestCount,
116
- adjustedSelectedIndex - viewportScrollMargin
117
- );
118
- }
119
-
120
- // If selector would be above the visible area, scroll up
121
- if (adjustedSelectedIndex < viewportStart + viewportScrollMargin) {
122
- viewportStart = Math.max(0, adjustedSelectedIndex - viewportScrollMargin);
123
- }
124
- }
125
-
126
- const viewportEnd = Math.min(viewportStart + visibleRequestCount, totalPairs);
127
- const visiblePairs = allPairs.slice(viewportStart, viewportEnd);
128
-
129
- let content = "{yellow-fg}HTTP Requests:{/yellow-fg}";
130
-
131
- // Show scroll indicator if there are items above the viewport
132
- if (viewportStart > 0) {
133
- content += ` {gray-fg}↑ ${viewportStart} more{/gray-fg}`;
134
- }
135
- content += "\n";
136
-
137
- visiblePairs.forEach((pair, i) => {
138
- const globalIndex = viewportStart + i;
139
- const isSelected = adjustedSelectedIndex !== -1 && adjustedSelectedIndex === globalIndex;
140
- const prefix = isSelected ? "> " : " ";
141
- const method = pair.request?.method || "";
142
- const uri = pair.request?.uri || "";
143
- const status = pair.response?.status || "";
144
- const statusColor = getStatusColor(String(status));
145
-
146
- if (isSelected) {
147
- content += `{cyan-fg}${prefix}${method} ${status} ${uri}{/cyan-fg}\n`;
148
- } else if (pair.response) {
149
- content += `{${statusColor}-fg}${prefix}${method} ${status} ${uri}{/${statusColor}-fg}\n`;
150
- } else {
151
- content += `${prefix}${method} ...${uri}\n`;
152
- }
153
- });
154
-
155
- // Show scroll indicator if there are items below the viewport
156
- const itemsBelow = totalPairs - viewportEnd;
157
- if (itemsBelow > 0) {
158
- content += `{gray-fg} ↓ ${itemsBelow} more{/gray-fg}\n`;
159
- }
160
-
161
-
162
- requestsBox.setContent(content);
163
- screen.render();
164
-
165
- return { adjustedSelectedIndex, trimmedPairs };
166
- }
167
-
168
- /**
169
- * Updates the QR code display box
170
- */
171
- export function updateQrCodeDisplay(
172
- qrCodeBox: blessed.Widgets.BoxElement | undefined,
173
- screen: blessed.Widgets.Screen,
174
- qrCodes: string[],
175
- urls: string[],
176
- currentQrIndex: number
177
- ): void {
178
- if (!qrCodeBox || qrCodes.length === 0) return;
179
-
180
- let content = `{green-fg}{bold}QR Code ${currentQrIndex + 1}/${urls.length}{/bold}{/green-fg}\n`;
181
- if (urls.length > 1) {
182
- content += "\n{yellow-fg}← → to switch QR codes{/yellow-fg}\n";
183
- }
184
- content += qrCodes[currentQrIndex] || "";
185
- qrCodeBox.setContent(content);
186
- qrCodeBox.style = { ...qrCodeBox.style };
187
- (qrCodeBox as any).parseContent();
188
- screen.render();
189
- }