pinggy 0.3.0 → 0.3.2
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/.github/workflows/publish-binaries.yml +50 -54
- package/Makefile +2 -2
- package/dist/index.cjs +1060 -902
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1039 -129
- package/ent.plist +14 -0
- package/package.json +31 -14
- package/scripts/pre_pkg_processing.js +74 -0
- package/src/cli/buildConfig.ts +10 -13
- package/src/cli/{starCli.tsx → starCli.ts} +51 -80
- package/src/index.ts +0 -6
- package/src/remote_management/remoteManagement.ts +0 -3
- package/src/remote_management/remote_schema.ts +2 -2
- package/src/tui/blessed/TunnelTui.ts +298 -0
- package/src/tui/blessed/components/DisplayUpdaters.ts +118 -0
- package/src/tui/blessed/components/KeyBindings.ts +134 -0
- package/src/tui/blessed/components/Modals.ts +216 -0
- package/src/tui/blessed/components/UIComponents.ts +306 -0
- package/src/tui/blessed/components/index.ts +4 -0
- package/src/tui/blessed/headerFetcher.ts +35 -0
- package/src/tui/blessed/index.ts +2 -0
- package/src/tui/blessed/qrCodeGenerator.ts +20 -0
- package/src/tui/blessed/webDebuggerConnection.ts +100 -0
- package/src/tui/{hooks → ink/hooks}/useReqResHeaders.ts +1 -1
- package/src/tui/{hooks → ink/hooks}/useWebDebugger.ts +2 -2
- package/src/tui/{index.tsx → ink/index.tsx} +12 -2
- package/src/tui/spinner/spinner.ts +64 -0
- package/src/tunnel_manager/TunnelManager.ts +9 -10
- package/src/types.ts +1 -1
- package/src/utils/printer.ts +15 -21
- package/src/utils/util.ts +5 -0
- package/tsconfig.json +1 -1
- package/dist/tui-AZUFY7T2.js +0 -584
- package/src/utils/esmOnlyPackageLoader.ts +0 -29
- /package/src/tui/{asciArt.ts → ink/asciArt.ts} +0 -0
- /package/src/tui/{hooks → ink/hooks}/useQrCodes.ts +0 -0
- /package/src/tui/{hooks → ink/hooks}/useTerminalSize.ts +0 -0
- /package/src/tui/{hooks → ink/hooks}/useTerminalStats.ts +0 -0
- /package/src/tui/{layout → ink/layout}/Borders.tsx +0 -0
- /package/src/tui/{layout → ink/layout}/Container.tsx +0 -0
- /package/src/tui/{sections → ink/sections}/DebuggerDetailModal.tsx +0 -0
- /package/src/tui/{sections → ink/sections}/KeyBindings.tsx +0 -0
- /package/src/tui/{sections → ink/sections}/QrCodeSection.tsx +0 -0
- /package/src/tui/{sections → ink/sections}/StatsSection.tsx +0 -0
- /package/src/tui/{sections → ink/sections}/URLsSection.tsx +0 -0
- /package/src/tui/{utils → ink/utils}/utils.ts +0 -0
|
@@ -0,0 +1,298 @@
|
|
|
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 = 0;
|
|
53
|
+
private qrCodes: string[] = [];
|
|
54
|
+
private stats: TunnelUsageType = {
|
|
55
|
+
elapsedTime: 0,
|
|
56
|
+
numLiveConnections: 0,
|
|
57
|
+
numTotalConnections: 0,
|
|
58
|
+
numTotalReqBytes: 0,
|
|
59
|
+
numTotalResBytes: 0,
|
|
60
|
+
numTotalTxBytes: 0,
|
|
61
|
+
};
|
|
62
|
+
private pairs: Map<number, ReqResPair> = new Map();
|
|
63
|
+
private webDebuggerConnection: WebDebuggerConnection | null = null;
|
|
64
|
+
|
|
65
|
+
// UI Elements
|
|
66
|
+
private uiElements!: UIElements;
|
|
67
|
+
private modalManager: ModalManager = {
|
|
68
|
+
detailModal: null,
|
|
69
|
+
keyBindingsModal: null,
|
|
70
|
+
disconnectModal: null,
|
|
71
|
+
inDetailView: false,
|
|
72
|
+
keyBindingView: false,
|
|
73
|
+
inDisconnectView: false,
|
|
74
|
+
};
|
|
75
|
+
private tunnelInstance?: ManagedTunnel
|
|
76
|
+
|
|
77
|
+
private exitPromiseResolve: (() => void) | null = null;
|
|
78
|
+
private exitPromise: Promise<void>;
|
|
79
|
+
|
|
80
|
+
constructor(props: TunnelAppProps) {
|
|
81
|
+
this.urls = props.urls;
|
|
82
|
+
this.greet = props.greet || "";
|
|
83
|
+
this.tunnelConfig = props.tunnelConfig;
|
|
84
|
+
this.disconnectInfo = props.disconnectInfo;
|
|
85
|
+
if(props.tunnelInstance){
|
|
86
|
+
this.tunnelInstance=props.tunnelInstance
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this.exitPromise = new Promise((resolve) => {
|
|
90
|
+
this.exitPromiseResolve = resolve;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
this.screen = blessed.screen({
|
|
94
|
+
smartCSR: true,
|
|
95
|
+
title: "Pinggy Tunnel",
|
|
96
|
+
fullUnicode: true,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
this.setupStatsListener();
|
|
100
|
+
this.setupWebDebugger();
|
|
101
|
+
this.generateQrCodes();
|
|
102
|
+
this.createUI();
|
|
103
|
+
this.setupKeyBindings();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private setupStatsListener() {
|
|
107
|
+
globalThis.__PINGGY_TUNNEL_STATS__ = (newStats: TunnelUsageType) => {
|
|
108
|
+
this.stats = { ...newStats };
|
|
109
|
+
this.updateStatsDisplay();
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private setupWebDebugger() {
|
|
114
|
+
if (this.tunnelConfig?.webDebugger) {
|
|
115
|
+
this.webDebuggerConnection = createWebDebuggerConnection(
|
|
116
|
+
this.tunnelConfig.webDebugger,
|
|
117
|
+
(pairs) => {
|
|
118
|
+
this.pairs = pairs;
|
|
119
|
+
this.updateRequestsDisplay();
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private async generateQrCodes() {
|
|
126
|
+
if (this.tunnelConfig?.qrCode && this.urls.length > 0) {
|
|
127
|
+
this.qrCodes = await createQrCodes(this.urls);
|
|
128
|
+
this.updateQrCodeDisplay();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Create the UI based on terminal size
|
|
133
|
+
private createUI() {
|
|
134
|
+
this.buildUI();
|
|
135
|
+
|
|
136
|
+
this.screen.on("resize", () => {
|
|
137
|
+
this.handleResize();
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private buildUI() {
|
|
142
|
+
const width = this.screen.width as number;
|
|
143
|
+
|
|
144
|
+
if (width < MIN_WIDTH_WARNING) {
|
|
145
|
+
this.uiElements = {
|
|
146
|
+
mainContainer: createWarningUI(this.screen),
|
|
147
|
+
urlsBox: null as any,
|
|
148
|
+
statsBox: null as any,
|
|
149
|
+
requestsBox: null as any,
|
|
150
|
+
footerBox: null as any,
|
|
151
|
+
warningBox: createWarningUI(this.screen),
|
|
152
|
+
};
|
|
153
|
+
this.screen.render();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (width < SIMPLE_LAYOUT_THRESHOLD) {
|
|
158
|
+
this.uiElements = createSimpleUI(this.screen, this.urls, this.greet);
|
|
159
|
+
} else {
|
|
160
|
+
this.uiElements = createFullUI(this.screen, this.urls, this.greet, this.tunnelConfig);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
this.refreshDisplays();
|
|
164
|
+
this.screen.render();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private refreshDisplays() {
|
|
168
|
+
this.updateUrlsDisplay();
|
|
169
|
+
this.updateStatsDisplay();
|
|
170
|
+
this.updateRequestsDisplay();
|
|
171
|
+
this.updateQrCodeDisplay();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private updateUrlsDisplay() {
|
|
175
|
+
updateUrlsDisplay(
|
|
176
|
+
this.uiElements?.urlsBox,
|
|
177
|
+
this.screen,
|
|
178
|
+
this.urls,
|
|
179
|
+
this.currentQrIndex
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private updateStatsDisplay() {
|
|
184
|
+
updateStatsDisplay(
|
|
185
|
+
this.uiElements?.statsBox,
|
|
186
|
+
this.screen,
|
|
187
|
+
this.stats
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private updateRequestsDisplay() {
|
|
192
|
+
updateRequestsDisplay(
|
|
193
|
+
this.uiElements?.requestsBox,
|
|
194
|
+
this.screen,
|
|
195
|
+
this.pairs,
|
|
196
|
+
this.selectedIndex
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private updateQrCodeDisplay() {
|
|
201
|
+
updateQrCodeDisplay(
|
|
202
|
+
this.uiElements?.qrCodeBox,
|
|
203
|
+
this.screen,
|
|
204
|
+
this.qrCodes,
|
|
205
|
+
this.urls,
|
|
206
|
+
this.currentQrIndex
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private setupKeyBindings() {
|
|
211
|
+
const self = this;
|
|
212
|
+
|
|
213
|
+
// Create a state object with getters to always get current values
|
|
214
|
+
const state: KeyBindingsState = {
|
|
215
|
+
get currentQrIndex() { return self.currentQrIndex; },
|
|
216
|
+
set currentQrIndex(value: number) { self.currentQrIndex = value; },
|
|
217
|
+
get selectedIndex() { return self.selectedIndex; },
|
|
218
|
+
set selectedIndex(value: number) { self.selectedIndex = value; },
|
|
219
|
+
get pairs() { return self.pairs; },
|
|
220
|
+
get urls() { return self.urls; },
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const callbacks: KeyBindingsCallbacks = {
|
|
224
|
+
onQrIndexChange: (index: number) => {
|
|
225
|
+
self.currentQrIndex = index;
|
|
226
|
+
},
|
|
227
|
+
onSelectedIndexChange: (index: number) => {
|
|
228
|
+
self.selectedIndex = index;
|
|
229
|
+
},
|
|
230
|
+
onDestroy: () => self.destroy(),
|
|
231
|
+
updateUrlsDisplay: () => self.updateUrlsDisplay(),
|
|
232
|
+
updateQrCodeDisplay: () => self.updateQrCodeDisplay(),
|
|
233
|
+
updateRequestsDisplay: () => self.updateRequestsDisplay(),
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
setupKeyBindings(
|
|
237
|
+
this.screen,
|
|
238
|
+
this.modalManager,
|
|
239
|
+
state,
|
|
240
|
+
callbacks,
|
|
241
|
+
this.tunnelConfig,
|
|
242
|
+
this.tunnelInstance
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private handleResize() {
|
|
247
|
+
// Destroy current UI and recreate based on new size
|
|
248
|
+
this.screen.children.forEach((child) => child.destroy());
|
|
249
|
+
this.buildUI();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
public updateDisconnectInfo(info: TunnelAppProps["disconnectInfo"]) {
|
|
253
|
+
this.disconnectInfo = info;
|
|
254
|
+
if (info?.disconnected) {
|
|
255
|
+
const message = info.error
|
|
256
|
+
? `Error: ${info.error}\nTunnel will be closed.`
|
|
257
|
+
: info.messages?.join('\n') || 'Disconnect request received. Tunnel will be closed.';
|
|
258
|
+
|
|
259
|
+
showDisconnectModal(
|
|
260
|
+
this.screen,
|
|
261
|
+
this.modalManager,
|
|
262
|
+
message,
|
|
263
|
+
() => this.destroy()
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
public start() {
|
|
269
|
+
this.screen.render();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
public waitUntilExit(): Promise<void> {
|
|
273
|
+
return this.exitPromise;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
public destroy() {
|
|
277
|
+
// Stop the tunnel first
|
|
278
|
+
if (this.tunnelInstance?.tunnelid) {
|
|
279
|
+
const manager = TunnelManager.getInstance();
|
|
280
|
+
manager.stopTunnel(this.tunnelInstance.tunnelid);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Cleanup
|
|
284
|
+
delete globalThis.__PINGGY_TUNNEL_STATS__;
|
|
285
|
+
|
|
286
|
+
if (this.webDebuggerConnection) {
|
|
287
|
+
this.webDebuggerConnection.close();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
this.screen.destroy();
|
|
291
|
+
|
|
292
|
+
if (this.exitPromiseResolve) {
|
|
293
|
+
this.exitPromiseResolve();
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export default TunnelTui;
|
|
@@ -0,0 +1,118 @@
|
|
|
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
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Updates the URLs display box
|
|
8
|
+
*/
|
|
9
|
+
export function updateUrlsDisplay(
|
|
10
|
+
urlsBox: blessed.Widgets.BoxElement | undefined,
|
|
11
|
+
screen: blessed.Widgets.Screen,
|
|
12
|
+
urls: string[],
|
|
13
|
+
currentQrIndex: number
|
|
14
|
+
): void {
|
|
15
|
+
if (!urlsBox) return;
|
|
16
|
+
|
|
17
|
+
let content = "{green-fg}{bold}Public URLs{/bold}{/green-fg}\n";
|
|
18
|
+
urls.forEach((url, index) => {
|
|
19
|
+
const isSelected = index === currentQrIndex;
|
|
20
|
+
const prefix = isSelected ? "→ " : "• ";
|
|
21
|
+
const color = isSelected ? "yellow" : "magenta";
|
|
22
|
+
|
|
23
|
+
if (isSelected) {
|
|
24
|
+
content += `{bold}{${color}-fg}${prefix}${url}{/${color}-fg}{/bold}\n`;
|
|
25
|
+
} else {
|
|
26
|
+
content += `{${color}-fg}${prefix}${url}{/${color}-fg}\n`;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
urlsBox.setContent(content);
|
|
31
|
+
screen.render();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Updates the stats display box
|
|
36
|
+
*/
|
|
37
|
+
export function updateStatsDisplay(
|
|
38
|
+
statsBox: blessed.Widgets.BoxElement | undefined,
|
|
39
|
+
screen: blessed.Widgets.Screen,
|
|
40
|
+
stats: TunnelUsageType
|
|
41
|
+
): void {
|
|
42
|
+
if (!statsBox) return;
|
|
43
|
+
|
|
44
|
+
const content = `{green-fg}{bold}Live Stats{/bold}{/green-fg}
|
|
45
|
+
Elapsed: ${stats.elapsedTime}s
|
|
46
|
+
Live Connections: ${stats.numLiveConnections}
|
|
47
|
+
Total Connections: ${stats.numTotalConnections}
|
|
48
|
+
Request: ${getBytesInt(stats.numTotalReqBytes)}
|
|
49
|
+
Response: ${getBytesInt(stats.numTotalResBytes)}
|
|
50
|
+
Total Transfer: ${getBytesInt(stats.numTotalTxBytes)}`;
|
|
51
|
+
|
|
52
|
+
statsBox.setContent(content);
|
|
53
|
+
statsBox.style = { ...statsBox.style };
|
|
54
|
+
(statsBox as any).parseContent();
|
|
55
|
+
screen.render();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Updates the requests display box
|
|
60
|
+
*/
|
|
61
|
+
export function updateRequestsDisplay(
|
|
62
|
+
requestsBox: blessed.Widgets.BoxElement | undefined,
|
|
63
|
+
screen: blessed.Widgets.Screen,
|
|
64
|
+
pairs: Map<number, ReqResPair>,
|
|
65
|
+
selectedIndex: number
|
|
66
|
+
): void {
|
|
67
|
+
if (!requestsBox) return;
|
|
68
|
+
|
|
69
|
+
const allPairs = [...pairs.values()];
|
|
70
|
+
const visiblePairs = allPairs.slice(-10);
|
|
71
|
+
const startIndex = allPairs.length - visiblePairs.length;
|
|
72
|
+
|
|
73
|
+
let content = "{yellow-fg}HTTP Requests:{/yellow-fg}\n";
|
|
74
|
+
|
|
75
|
+
visiblePairs.forEach((pair, i) => {
|
|
76
|
+
const globalIndex = startIndex + i;
|
|
77
|
+
const isSelected = selectedIndex === globalIndex;
|
|
78
|
+
const prefix = isSelected ? "> " : " ";
|
|
79
|
+
const method = pair.request?.method || "";
|
|
80
|
+
const uri = pair.request?.uri || "";
|
|
81
|
+
const status = pair.response?.status || "";
|
|
82
|
+
const statusColor = getStatusColor(String(status));
|
|
83
|
+
|
|
84
|
+
if (isSelected) {
|
|
85
|
+
content += `{cyan-fg}${prefix}${method} ${status} ${uri}{/cyan-fg}\n`;
|
|
86
|
+
} else if (pair.response) {
|
|
87
|
+
content += `{${statusColor}-fg}${prefix}${method} ${status} ${uri}{/${statusColor}-fg}\n`;
|
|
88
|
+
} else {
|
|
89
|
+
content += `${prefix}${method} ...${uri}\n`;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
requestsBox.setContent(content);
|
|
94
|
+
screen.render();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Updates the QR code display box
|
|
99
|
+
*/
|
|
100
|
+
export function updateQrCodeDisplay(
|
|
101
|
+
qrCodeBox: blessed.Widgets.BoxElement | undefined,
|
|
102
|
+
screen: blessed.Widgets.Screen,
|
|
103
|
+
qrCodes: string[],
|
|
104
|
+
urls: string[],
|
|
105
|
+
currentQrIndex: number
|
|
106
|
+
): void {
|
|
107
|
+
if (!qrCodeBox || qrCodes.length === 0) return;
|
|
108
|
+
|
|
109
|
+
let content = `{green-fg}{bold}QR Code ${currentQrIndex + 1}/${urls.length}{/bold}{/green-fg}\n`;
|
|
110
|
+
if (urls.length > 1) {
|
|
111
|
+
content += "\n{yellow-fg}← → to switch QR codes{/yellow-fg}\n";
|
|
112
|
+
}
|
|
113
|
+
content += qrCodes[currentQrIndex] || "";
|
|
114
|
+
qrCodeBox.setContent(content);
|
|
115
|
+
qrCodeBox.style = { ...qrCodeBox.style };
|
|
116
|
+
(qrCodeBox as any).parseContent();
|
|
117
|
+
screen.render();
|
|
118
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import blessed from "blessed";
|
|
2
|
+
import { ReqResPair, FinalConfig } from "../../../types.js";
|
|
3
|
+
import { ManagedTunnel, TunnelManager } from "../../../tunnel_manager/TunnelManager.js";
|
|
4
|
+
import { fetchReqResHeaders } from "../headerFetcher.js";
|
|
5
|
+
import { logger } from "../../../logger.js";
|
|
6
|
+
import { ModalManager, showDetailModal, closeDetailModal, showKeyBindingsModal, closeKeyBindingsModal } from "./Modals.js";
|
|
7
|
+
|
|
8
|
+
export interface KeyBindingsState {
|
|
9
|
+
currentQrIndex: number;
|
|
10
|
+
selectedIndex: number;
|
|
11
|
+
pairs: Map<number, ReqResPair>;
|
|
12
|
+
urls: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface KeyBindingsCallbacks {
|
|
16
|
+
onQrIndexChange: (index: number) => void;
|
|
17
|
+
onSelectedIndexChange: (index: number) => void;
|
|
18
|
+
onDestroy: () => void;
|
|
19
|
+
updateUrlsDisplay: () => void;
|
|
20
|
+
updateQrCodeDisplay: () => void;
|
|
21
|
+
updateRequestsDisplay: () => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Sets up all key bindings for the TUI
|
|
26
|
+
*/
|
|
27
|
+
export function setupKeyBindings(
|
|
28
|
+
screen: blessed.Widgets.Screen,
|
|
29
|
+
modalManager: ModalManager,
|
|
30
|
+
state: KeyBindingsState,
|
|
31
|
+
callbacks: KeyBindingsCallbacks,
|
|
32
|
+
tunnelConfig?: FinalConfig,
|
|
33
|
+
tunnelInstance?: ManagedTunnel
|
|
34
|
+
): void {
|
|
35
|
+
// Exit on Ctrl+C
|
|
36
|
+
screen.key(["C-c"], () => {
|
|
37
|
+
const manager = TunnelManager.getInstance();
|
|
38
|
+
manager.stopTunnel(tunnelInstance?.tunnelid || "");
|
|
39
|
+
callbacks.onDestroy();
|
|
40
|
+
process.exit(0);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Escape key
|
|
44
|
+
screen.key(["escape"], () => {
|
|
45
|
+
if (modalManager.inDetailView) {
|
|
46
|
+
closeDetailModal(screen, modalManager);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (modalManager.keyBindingView) {
|
|
50
|
+
closeKeyBindingsModal(screen, modalManager);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Navigation - Up
|
|
56
|
+
screen.key(["up"], () => {
|
|
57
|
+
if (modalManager.inDetailView || modalManager.keyBindingView) return;
|
|
58
|
+
if (state.selectedIndex > 0) {
|
|
59
|
+
callbacks.onSelectedIndexChange(state.selectedIndex - 1);
|
|
60
|
+
callbacks.updateRequestsDisplay();
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Navigation - Down
|
|
65
|
+
screen.key(["down"], () => {
|
|
66
|
+
if (modalManager.inDetailView || modalManager.keyBindingView) return;
|
|
67
|
+
const allPairs = [...state.pairs.values()];
|
|
68
|
+
if (state.selectedIndex < allPairs.length - 1) {
|
|
69
|
+
callbacks.onSelectedIndexChange(state.selectedIndex + 1);
|
|
70
|
+
callbacks.updateRequestsDisplay();
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Enter to view details
|
|
75
|
+
screen.key(["enter"], async () => {
|
|
76
|
+
if (modalManager.inDetailView || modalManager.keyBindingView) return;
|
|
77
|
+
const allPairs = [...state.pairs.values()];
|
|
78
|
+
const pair = allPairs[state.selectedIndex];
|
|
79
|
+
if (pair?.request?.key !== undefined && pair?.request?.key !== null) {
|
|
80
|
+
try {
|
|
81
|
+
const headers = await fetchReqResHeaders(
|
|
82
|
+
tunnelConfig?.webDebugger || "",
|
|
83
|
+
pair.request.key
|
|
84
|
+
);
|
|
85
|
+
showDetailModal(screen, modalManager, headers.req, headers.res);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
logger.error("Fetch error:", err);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Help toggle
|
|
93
|
+
screen.key(["h"], () => {
|
|
94
|
+
if (modalManager.inDetailView) return;
|
|
95
|
+
if (modalManager.keyBindingView) {
|
|
96
|
+
closeKeyBindingsModal(screen, modalManager);
|
|
97
|
+
} else {
|
|
98
|
+
showKeyBindingsModal(screen, modalManager);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Copy URL
|
|
103
|
+
screen.key(["c"], async () => {
|
|
104
|
+
if (modalManager.inDetailView || modalManager.keyBindingView) return;
|
|
105
|
+
if (state.urls.length > 0) {
|
|
106
|
+
try {
|
|
107
|
+
const clipboardy = await import("clipboardy");
|
|
108
|
+
clipboardy.default.writeSync(state.urls[state.currentQrIndex]);
|
|
109
|
+
} catch (err) {
|
|
110
|
+
logger.error("Failed to copy to clipboard:", err);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// QR code navigation - Left
|
|
116
|
+
screen.key(["left"], () => {
|
|
117
|
+
if (modalManager.inDetailView || modalManager.keyBindingView) return;
|
|
118
|
+
if (state.currentQrIndex > 0) {
|
|
119
|
+
callbacks.onQrIndexChange(state.currentQrIndex - 1);
|
|
120
|
+
callbacks.updateUrlsDisplay();
|
|
121
|
+
callbacks.updateQrCodeDisplay();
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// QR code navigation - Right
|
|
126
|
+
screen.key(["right"], () => {
|
|
127
|
+
if (modalManager.inDetailView || modalManager.keyBindingView) return;
|
|
128
|
+
if (state.currentQrIndex < state.urls.length - 1) {
|
|
129
|
+
callbacks.onQrIndexChange(state.currentQrIndex + 1);
|
|
130
|
+
callbacks.updateUrlsDisplay();
|
|
131
|
+
callbacks.updateQrCodeDisplay();
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|