panex 0.9.3 → 0.9.4
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/README.md +5 -5
- package/dist/cli.js +565 -263
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +554 -261
- package/dist/index.js.map +1 -1
- package/package.json +7 -4
package/dist/index.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
// src/tui.ts
|
|
2
|
-
import
|
|
2
|
+
import { render } from "ink";
|
|
3
|
+
import { createElement } from "react";
|
|
4
|
+
|
|
5
|
+
// src/components/App.tsx
|
|
6
|
+
import { useState as useState4, useEffect as useEffect5, useRef as useRef4, useCallback as useCallback5 } from "react";
|
|
7
|
+
import { Box as Box6, useApp, useInput, useStdin as useStdin2, useStdout as useStdout4 } from "ink";
|
|
8
|
+
|
|
9
|
+
// src/hooks/useProcessManager.ts
|
|
10
|
+
import { useState, useEffect, useCallback, useRef } from "react";
|
|
3
11
|
|
|
4
12
|
// src/process-manager.ts
|
|
5
13
|
import { EventEmitter } from "events";
|
|
@@ -126,295 +134,580 @@ var ProcessManager = class extends EventEmitter {
|
|
|
126
134
|
}
|
|
127
135
|
};
|
|
128
136
|
|
|
129
|
-
// src/
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
keys: true,
|
|
175
|
-
vi: true
|
|
176
|
-
});
|
|
177
|
-
const statusBar = blessed.box({
|
|
178
|
-
parent: screen,
|
|
179
|
-
bottom: 0,
|
|
180
|
-
left: 0,
|
|
181
|
-
width: "100%",
|
|
182
|
-
height: 1,
|
|
183
|
-
style: {
|
|
184
|
-
bg: "blue",
|
|
185
|
-
fg: "white"
|
|
186
|
-
},
|
|
187
|
-
content: " [\u2191\u2193/jk] select [Enter] focus [r] restart [A] restart All [x] kill [q] quit [?] help "
|
|
188
|
-
});
|
|
189
|
-
const helpBox = blessed.box({
|
|
190
|
-
parent: screen,
|
|
191
|
-
top: "center",
|
|
192
|
-
left: "center",
|
|
193
|
-
width: "60%",
|
|
194
|
-
height: "60%",
|
|
195
|
-
label: " Help ",
|
|
196
|
-
border: { type: "line" },
|
|
197
|
-
style: {
|
|
198
|
-
border: { fg: "yellow" },
|
|
199
|
-
bg: "black"
|
|
200
|
-
},
|
|
201
|
-
hidden: true,
|
|
202
|
-
content: `
|
|
203
|
-
Keyboard Shortcuts
|
|
204
|
-
\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
137
|
+
// src/hooks/useProcessManager.ts
|
|
138
|
+
function useProcessManager(config) {
|
|
139
|
+
const [, forceUpdate] = useState({});
|
|
140
|
+
const processManagerRef = useRef(null);
|
|
141
|
+
if (!processManagerRef.current) {
|
|
142
|
+
processManagerRef.current = new ProcessManager(config.procs);
|
|
143
|
+
}
|
|
144
|
+
const pm = processManagerRef.current;
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
const update = () => forceUpdate({});
|
|
147
|
+
pm.on("output", update);
|
|
148
|
+
pm.on("started", update);
|
|
149
|
+
pm.on("exit", update);
|
|
150
|
+
pm.on("error", update);
|
|
151
|
+
pm.startAll();
|
|
152
|
+
return () => {
|
|
153
|
+
pm.removeAllListeners();
|
|
154
|
+
pm.killAll();
|
|
155
|
+
};
|
|
156
|
+
}, [pm]);
|
|
157
|
+
const getOutput = useCallback((name) => pm.getOutput(name), [pm]);
|
|
158
|
+
const getStatus = useCallback((name) => {
|
|
159
|
+
const proc = pm.getProcess(name);
|
|
160
|
+
return proc?.status ?? "stopped";
|
|
161
|
+
}, [pm]);
|
|
162
|
+
const restart = useCallback((name) => pm.restart(name), [pm]);
|
|
163
|
+
const restartAll = useCallback(() => pm.restartAll(), [pm]);
|
|
164
|
+
const kill = useCallback((name) => pm.kill(name), [pm]);
|
|
165
|
+
const killAll = useCallback(() => pm.killAll(), [pm]);
|
|
166
|
+
const write = useCallback((name, data) => pm.write(name, data), [pm]);
|
|
167
|
+
const resize = useCallback((name, cols, rows) => pm.resize(name, cols, rows), [pm]);
|
|
168
|
+
return {
|
|
169
|
+
processManager: pm,
|
|
170
|
+
processes: new Map(pm.getNames().map((n) => [n, pm.getProcess(n)])),
|
|
171
|
+
names: pm.getNames(),
|
|
172
|
+
getOutput,
|
|
173
|
+
getStatus,
|
|
174
|
+
restart,
|
|
175
|
+
restartAll,
|
|
176
|
+
kill,
|
|
177
|
+
killAll,
|
|
178
|
+
write,
|
|
179
|
+
resize
|
|
180
|
+
};
|
|
181
|
+
}
|
|
205
182
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
183
|
+
// src/hooks/useFocusMode.ts
|
|
184
|
+
import { useState as useState2, useCallback as useCallback2 } from "react";
|
|
185
|
+
function useFocusMode() {
|
|
186
|
+
const [focusMode, setFocusMode] = useState2(false);
|
|
187
|
+
const enterFocus = useCallback2(() => setFocusMode(true), []);
|
|
188
|
+
const exitFocus = useCallback2(() => setFocusMode(false), []);
|
|
189
|
+
const toggleFocus = useCallback2(() => setFocusMode((f) => !f), []);
|
|
190
|
+
return { focusMode, enterFocus, exitFocus, toggleFocus };
|
|
191
|
+
}
|
|
210
192
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
193
|
+
// src/hooks/useMouseWheel.ts
|
|
194
|
+
import { useEffect as useEffect2, useCallback as useCallback3 } from "react";
|
|
195
|
+
import { useStdin, useStdout } from "ink";
|
|
196
|
+
function useMouseWheel({ enabled = true, onWheel } = {}) {
|
|
197
|
+
const { stdin, setRawMode } = useStdin();
|
|
198
|
+
const { stdout } = useStdout();
|
|
199
|
+
const handleData = useCallback3((data) => {
|
|
200
|
+
const str = data.toString();
|
|
201
|
+
const sgrRegex = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/g;
|
|
202
|
+
let match;
|
|
203
|
+
while ((match = sgrRegex.exec(str)) !== null) {
|
|
204
|
+
const button = parseInt(match[1] ?? "0", 10);
|
|
205
|
+
const x = parseInt(match[2] ?? "0", 10);
|
|
206
|
+
const y = parseInt(match[3] ?? "0", 10);
|
|
207
|
+
const isPress = match[4] === "M";
|
|
208
|
+
if (isPress) {
|
|
209
|
+
if (button === 64) {
|
|
210
|
+
onWheel?.({ type: "wheel-up", x, y });
|
|
211
|
+
} else if (button === 65) {
|
|
212
|
+
onWheel?.({ type: "wheel-down", x, y });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}, [onWheel]);
|
|
217
|
+
useEffect2(() => {
|
|
218
|
+
if (!enabled || !stdin || !stdout) return;
|
|
219
|
+
stdout.write("\x1B[?1000h\x1B[?1006h");
|
|
220
|
+
setRawMode?.(true);
|
|
221
|
+
stdin.on("data", handleData);
|
|
222
|
+
return () => {
|
|
223
|
+
stdin.off("data", handleData);
|
|
224
|
+
stdout.write("\x1B[?1000l\x1B[?1006l");
|
|
225
|
+
};
|
|
226
|
+
}, [enabled, stdin, stdout, setRawMode, handleData]);
|
|
227
|
+
}
|
|
217
228
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
229
|
+
// src/components/ProcessList.tsx
|
|
230
|
+
import { Box, Text, useStdout as useStdout2 } from "ink";
|
|
231
|
+
import { ScrollList } from "ink-scroll-list";
|
|
232
|
+
import { forwardRef, useImperativeHandle, useRef as useRef2, useEffect as useEffect3 } from "react";
|
|
233
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
234
|
+
var ProcessList = forwardRef(
|
|
235
|
+
function ProcessList2({ names, selected, getStatus, active, height }, ref) {
|
|
236
|
+
const borderStyle = active ? "double" : "single";
|
|
237
|
+
const listRef = useRef2(null);
|
|
238
|
+
const { stdout } = useStdout2();
|
|
239
|
+
useEffect3(() => {
|
|
240
|
+
const handleResize = () => listRef.current?.remeasure();
|
|
241
|
+
stdout?.on("resize", handleResize);
|
|
242
|
+
return () => {
|
|
243
|
+
stdout?.off("resize", handleResize);
|
|
244
|
+
};
|
|
245
|
+
}, [stdout]);
|
|
246
|
+
useImperativeHandle(ref, () => ({
|
|
247
|
+
scrollBy: (delta) => listRef.current?.scrollBy(delta),
|
|
248
|
+
scrollToTop: () => listRef.current?.scrollToTop(),
|
|
249
|
+
scrollToBottom: () => listRef.current?.scrollToBottom()
|
|
250
|
+
}));
|
|
251
|
+
return /* @__PURE__ */ jsx(
|
|
252
|
+
Box,
|
|
253
|
+
{
|
|
254
|
+
flexDirection: "column",
|
|
255
|
+
borderStyle,
|
|
256
|
+
borderColor: active ? "blue" : "gray",
|
|
257
|
+
width: 20,
|
|
258
|
+
height,
|
|
259
|
+
paddingX: 1,
|
|
260
|
+
children: /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginTop: 0, height: height ? height - 2 : void 0, children: /* @__PURE__ */ jsx(
|
|
261
|
+
ScrollList,
|
|
262
|
+
{
|
|
263
|
+
ref: listRef,
|
|
264
|
+
selectedIndex: selected,
|
|
265
|
+
scrollAlignment: "auto",
|
|
266
|
+
children: names.map((name, i) => {
|
|
267
|
+
const status = getStatus(name);
|
|
268
|
+
const statusIcon = status === "running" ? "\u25CF" : status === "error" ? "\u2717" : "\u25CB";
|
|
269
|
+
const statusColor = status === "running" ? "green" : status === "error" ? "red" : "gray";
|
|
270
|
+
const isSelected = i === selected;
|
|
271
|
+
return /* @__PURE__ */ jsxs(Box, { backgroundColor: isSelected ? "blue" : void 0, children: [
|
|
272
|
+
/* @__PURE__ */ jsxs(Text, { color: isSelected ? "black" : void 0, children: [
|
|
273
|
+
name,
|
|
274
|
+
" "
|
|
275
|
+
] }),
|
|
276
|
+
/* @__PURE__ */ jsx(Text, { color: isSelected ? "black" : statusColor, children: statusIcon })
|
|
277
|
+
] }, name);
|
|
278
|
+
})
|
|
279
|
+
}
|
|
280
|
+
) })
|
|
281
|
+
}
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
);
|
|
221
285
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
screen.render();
|
|
286
|
+
// src/components/OutputPanel.tsx
|
|
287
|
+
import { Box as Box3, Text as Text3, useStdout as useStdout3 } from "ink";
|
|
288
|
+
import { ScrollView } from "ink-scroll-view";
|
|
289
|
+
import { forwardRef as forwardRef2, useImperativeHandle as useImperativeHandle2, useRef as useRef3, useEffect as useEffect4, useState as useState3, useCallback as useCallback4 } from "react";
|
|
290
|
+
|
|
291
|
+
// src/components/Scrollbar.tsx
|
|
292
|
+
import { Box as Box2, Text as Text2 } from "ink";
|
|
293
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
294
|
+
function Scrollbar({
|
|
295
|
+
scrollOffset,
|
|
296
|
+
contentHeight,
|
|
297
|
+
viewportHeight,
|
|
298
|
+
height
|
|
299
|
+
}) {
|
|
300
|
+
if (contentHeight <= viewportHeight) {
|
|
301
|
+
return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", width: 1, children: Array.from({ length: height }).map((_, i) => /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " " }, i)) });
|
|
239
302
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
303
|
+
const trackHeight = height;
|
|
304
|
+
const thumbRatio = viewportHeight / contentHeight;
|
|
305
|
+
const thumbHeight = Math.max(1, Math.round(trackHeight * thumbRatio));
|
|
306
|
+
const maxScroll = contentHeight - viewportHeight;
|
|
307
|
+
const scrollRatio = maxScroll > 0 ? scrollOffset / maxScroll : 0;
|
|
308
|
+
const thumbPosition = Math.round((trackHeight - thumbHeight) * scrollRatio);
|
|
309
|
+
const lines = [];
|
|
310
|
+
for (let i = 0; i < trackHeight; i++) {
|
|
311
|
+
if (i >= thumbPosition && i < thumbPosition + thumbHeight) {
|
|
312
|
+
lines.push("\u2588");
|
|
313
|
+
} else {
|
|
314
|
+
lines.push("\u2591");
|
|
252
315
|
}
|
|
253
|
-
screen.render();
|
|
254
316
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
317
|
+
return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", width: 1, children: lines.map((char, i) => /* @__PURE__ */ jsx2(Text2, { dimColor: char === "\u2591", children: char }, i)) });
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// src/components/OutputPanel.tsx
|
|
321
|
+
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
322
|
+
var OutputPanel = forwardRef2(
|
|
323
|
+
function OutputPanel2({ name, output, active, height, autoScroll = true, onAutoScrollChange }, ref) {
|
|
324
|
+
const borderStyle = active ? "double" : "single";
|
|
325
|
+
const lines = output.split("\n");
|
|
326
|
+
const scrollRef = useRef3(null);
|
|
327
|
+
const { stdout } = useStdout3();
|
|
328
|
+
const [scrollOffset, setScrollOffset] = useState3(0);
|
|
329
|
+
const [contentHeight, setContentHeight] = useState3(0);
|
|
330
|
+
const [viewportHeight, setViewportHeight] = useState3(0);
|
|
331
|
+
useEffect4(() => {
|
|
332
|
+
const handleResize = () => scrollRef.current?.remeasure();
|
|
333
|
+
stdout?.on("resize", handleResize);
|
|
334
|
+
return () => {
|
|
335
|
+
stdout?.off("resize", handleResize);
|
|
336
|
+
};
|
|
337
|
+
}, [stdout]);
|
|
338
|
+
const isAtBottom = useCallback4(() => {
|
|
339
|
+
if (!scrollRef.current) return true;
|
|
340
|
+
const offset = scrollRef.current.getScrollOffset();
|
|
341
|
+
const bottom = scrollRef.current.getBottomOffset();
|
|
342
|
+
return offset >= bottom - 1;
|
|
343
|
+
}, []);
|
|
344
|
+
const handleContentHeightChange = useCallback4((newHeight) => {
|
|
345
|
+
setContentHeight(newHeight);
|
|
346
|
+
if (autoScroll && scrollRef.current) {
|
|
347
|
+
setTimeout(() => {
|
|
348
|
+
scrollRef.current?.scrollToBottom();
|
|
349
|
+
}, 0);
|
|
350
|
+
}
|
|
351
|
+
}, [autoScroll]);
|
|
352
|
+
const handleScroll = useCallback4((offset) => {
|
|
353
|
+
setScrollOffset(offset);
|
|
354
|
+
if (scrollRef.current) {
|
|
355
|
+
const bottom = scrollRef.current.getBottomOffset();
|
|
356
|
+
const atBottom = offset >= bottom - 1;
|
|
357
|
+
if (!atBottom && autoScroll) {
|
|
358
|
+
onAutoScrollChange?.(false);
|
|
359
|
+
} else if (atBottom && !autoScroll) {
|
|
360
|
+
onAutoScrollChange?.(true);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}, [autoScroll, onAutoScrollChange]);
|
|
364
|
+
useImperativeHandle2(ref, () => ({
|
|
365
|
+
scrollBy: (delta) => scrollRef.current?.scrollBy(delta),
|
|
366
|
+
scrollToTop: () => scrollRef.current?.scrollToTop(),
|
|
367
|
+
scrollToBottom: () => scrollRef.current?.scrollToBottom(),
|
|
368
|
+
getScrollOffset: () => scrollRef.current?.getScrollOffset() ?? 0,
|
|
369
|
+
getContentHeight: () => scrollRef.current?.getContentHeight() ?? 0,
|
|
370
|
+
getViewportHeight: () => scrollRef.current?.getViewportHeight() ?? 0,
|
|
371
|
+
isAtBottom
|
|
372
|
+
}));
|
|
373
|
+
const scrollbarHeight = height ? height - 4 : 20;
|
|
374
|
+
const hasScroll = contentHeight > viewportHeight;
|
|
375
|
+
const pinIndicator = !autoScroll && hasScroll ? " \u2357" : "";
|
|
376
|
+
return /* @__PURE__ */ jsx3(
|
|
377
|
+
Box3,
|
|
378
|
+
{
|
|
379
|
+
flexDirection: "column",
|
|
380
|
+
borderStyle,
|
|
381
|
+
borderColor: active ? "green" : "gray",
|
|
382
|
+
flexGrow: 1,
|
|
383
|
+
height,
|
|
384
|
+
paddingLeft: 1,
|
|
385
|
+
children: /* @__PURE__ */ jsxs2(Box3, { flexDirection: "row", marginTop: 0, height: height ? height - 2 : void 0, children: [
|
|
386
|
+
/* @__PURE__ */ jsx3(Box3, { flexDirection: "column", flexGrow: 1, children: /* @__PURE__ */ jsx3(
|
|
387
|
+
ScrollView,
|
|
388
|
+
{
|
|
389
|
+
ref: scrollRef,
|
|
390
|
+
onScroll: handleScroll,
|
|
391
|
+
onContentHeightChange: handleContentHeightChange,
|
|
392
|
+
onViewportSizeChange: (layout) => setViewportHeight(layout.height),
|
|
393
|
+
children: lines.map((line, i) => /* @__PURE__ */ jsx3(Text3, { wrap: "truncate", children: line }, i))
|
|
394
|
+
}
|
|
395
|
+
) }),
|
|
396
|
+
hasScroll && /* @__PURE__ */ jsx3(
|
|
397
|
+
Scrollbar,
|
|
398
|
+
{
|
|
399
|
+
scrollOffset,
|
|
400
|
+
contentHeight,
|
|
401
|
+
viewportHeight,
|
|
402
|
+
height: scrollbarHeight
|
|
403
|
+
}
|
|
404
|
+
)
|
|
405
|
+
] })
|
|
406
|
+
}
|
|
407
|
+
);
|
|
260
408
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
// src/components/StatusBar.tsx
|
|
412
|
+
import { Box as Box4, Text as Text4 } from "ink";
|
|
413
|
+
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
414
|
+
function StatusBar({ focusMode, processName, showShiftTabHint = true }) {
|
|
415
|
+
if (focusMode && processName) {
|
|
416
|
+
const shiftTabHint = showShiftTabHint ? "Shift-Tab/" : "";
|
|
417
|
+
return /* @__PURE__ */ jsx4(Box4, { backgroundColor: "green", width: "100%", children: /* @__PURE__ */ jsxs3(Text4, { bold: true, color: "black", backgroundColor: "green", children: [
|
|
418
|
+
" ",
|
|
419
|
+
"FOCUS: ",
|
|
420
|
+
processName,
|
|
421
|
+
" - Type to interact, [",
|
|
422
|
+
shiftTabHint,
|
|
423
|
+
"Esc] to exit focus mode",
|
|
424
|
+
" "
|
|
425
|
+
] }) });
|
|
426
|
+
}
|
|
427
|
+
return /* @__PURE__ */ jsx4(Box4, { backgroundColor: "blue", width: "100%", children: /* @__PURE__ */ jsxs3(Text4, { bold: true, color: "black", backgroundColor: "blue", children: [
|
|
428
|
+
" ",
|
|
429
|
+
"[\u2191\u2193/jk] select [Tab/Enter] focus [r] restart [A] restart All [x] kill [q] quit [?] help",
|
|
430
|
+
" "
|
|
431
|
+
] }) });
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// src/components/HelpPopup.tsx
|
|
435
|
+
import { Box as Box5, Text as Text5 } from "ink";
|
|
436
|
+
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
437
|
+
function HelpPopup({ visible }) {
|
|
438
|
+
if (!visible) return null;
|
|
439
|
+
return /* @__PURE__ */ jsxs4(
|
|
440
|
+
Box5,
|
|
441
|
+
{
|
|
442
|
+
flexDirection: "column",
|
|
443
|
+
borderStyle: "single",
|
|
444
|
+
borderColor: "yellow",
|
|
445
|
+
padding: 1,
|
|
446
|
+
position: "absolute",
|
|
447
|
+
marginLeft: 10,
|
|
448
|
+
marginTop: 5,
|
|
449
|
+
children: [
|
|
450
|
+
/* @__PURE__ */ jsx5(Text5, { bold: true, color: "yellow", children: " Help " }),
|
|
451
|
+
/* @__PURE__ */ jsxs4(Text5, { children: [
|
|
452
|
+
"\n",
|
|
453
|
+
"Keyboard Shortcuts"
|
|
454
|
+
] }),
|
|
455
|
+
/* @__PURE__ */ jsx5(Text5, { children: "\u2500".repeat(18) }),
|
|
456
|
+
/* @__PURE__ */ jsxs4(Text5, { children: [
|
|
457
|
+
"\n",
|
|
458
|
+
"Navigation"
|
|
459
|
+
] }),
|
|
460
|
+
/* @__PURE__ */ jsx5(Text5, { children: " \u2191/\u2193 or j/k Navigate process list" }),
|
|
461
|
+
/* @__PURE__ */ jsx5(Text5, { children: " g/G Scroll to top/bottom of output" }),
|
|
462
|
+
/* @__PURE__ */ jsx5(Text5, { children: " PgUp/PgDn Scroll output" }),
|
|
463
|
+
/* @__PURE__ */ jsxs4(Text5, { children: [
|
|
464
|
+
"\n",
|
|
465
|
+
"Process Control"
|
|
466
|
+
] }),
|
|
467
|
+
/* @__PURE__ */ jsx5(Text5, { children: " Tab/Enter Focus process (interactive mode)" }),
|
|
468
|
+
/* @__PURE__ */ jsx5(Text5, { children: " Esc Exit focus mode" }),
|
|
469
|
+
/* @__PURE__ */ jsx5(Text5, { children: " r Restart selected process" }),
|
|
470
|
+
/* @__PURE__ */ jsx5(Text5, { children: " A Restart all processes" }),
|
|
471
|
+
/* @__PURE__ */ jsx5(Text5, { children: " x Kill selected process" }),
|
|
472
|
+
/* @__PURE__ */ jsxs4(Text5, { children: [
|
|
473
|
+
"\n",
|
|
474
|
+
"General"
|
|
475
|
+
] }),
|
|
476
|
+
/* @__PURE__ */ jsx5(Text5, { children: " ? Toggle this help" }),
|
|
477
|
+
/* @__PURE__ */ jsx5(Text5, { children: " q Quit panex" }),
|
|
478
|
+
/* @__PURE__ */ jsxs4(Text5, { children: [
|
|
479
|
+
"\n",
|
|
480
|
+
"Press any key to close this help..."
|
|
481
|
+
] })
|
|
482
|
+
]
|
|
264
483
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// src/components/App.tsx
|
|
488
|
+
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
489
|
+
function App({ config }) {
|
|
490
|
+
const { exit } = useApp();
|
|
491
|
+
const { stdout } = useStdout4();
|
|
492
|
+
const { setRawMode } = useStdin2();
|
|
493
|
+
const [selected, setSelected] = useState4(0);
|
|
494
|
+
const [showHelp, setShowHelp] = useState4(false);
|
|
495
|
+
const { focusMode, enterFocus, exitFocus } = useFocusMode();
|
|
496
|
+
const outputRef = useRef4(null);
|
|
497
|
+
const processListRef = useRef4(null);
|
|
498
|
+
const [autoScroll, setAutoScroll] = useState4({});
|
|
499
|
+
const {
|
|
500
|
+
names,
|
|
501
|
+
getOutput,
|
|
502
|
+
getStatus,
|
|
503
|
+
restart,
|
|
504
|
+
restartAll,
|
|
505
|
+
kill,
|
|
506
|
+
killAll,
|
|
507
|
+
write,
|
|
508
|
+
resize
|
|
509
|
+
} = useProcessManager(config);
|
|
510
|
+
const isShiftTabDisabled = (name) => {
|
|
511
|
+
const setting = config.settings?.noShiftTab;
|
|
512
|
+
if (setting === true) return true;
|
|
513
|
+
if (Array.isArray(setting)) return setting.includes(name);
|
|
514
|
+
return false;
|
|
515
|
+
};
|
|
516
|
+
const maxPanelHeight = stdout ? stdout.rows - 1 : void 0;
|
|
517
|
+
useEffect5(() => {
|
|
518
|
+
const name = names[selected];
|
|
519
|
+
if (name && stdout) {
|
|
520
|
+
const cols = Math.floor(stdout.columns * 0.8) - 2;
|
|
521
|
+
const rows = stdout.rows - 3;
|
|
522
|
+
resize(name, cols, rows);
|
|
276
523
|
}
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
524
|
+
}, [stdout?.columns, stdout?.rows, selected, names, resize]);
|
|
525
|
+
useEffect5(() => {
|
|
526
|
+
setAutoScroll((prev) => {
|
|
527
|
+
const next = { ...prev };
|
|
528
|
+
let changed = false;
|
|
529
|
+
for (const name of names) {
|
|
530
|
+
if (next[name] === void 0) {
|
|
531
|
+
next[name] = true;
|
|
532
|
+
changed = true;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return changed ? next : prev;
|
|
536
|
+
});
|
|
537
|
+
}, [names]);
|
|
538
|
+
const selectedName = names[selected] ?? "";
|
|
539
|
+
const output = selectedName ? getOutput(selectedName) : "";
|
|
540
|
+
const currentAutoScroll = selectedName ? autoScroll[selectedName] ?? true : true;
|
|
541
|
+
const handleAutoScrollChange = useCallback5((enabled) => {
|
|
542
|
+
if (selectedName) {
|
|
543
|
+
setAutoScroll((prev) => ({ ...prev, [selectedName]: enabled }));
|
|
291
544
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
545
|
+
}, [selectedName]);
|
|
546
|
+
const handleWheel = useCallback5((event) => {
|
|
547
|
+
const delta = event.type === "wheel-up" ? -3 : 3;
|
|
548
|
+
if (outputRef.current) {
|
|
549
|
+
outputRef.current.scrollBy(delta);
|
|
550
|
+
if (event.type === "wheel-up" && selectedName) {
|
|
551
|
+
setAutoScroll((prev) => ({ ...prev, [selectedName]: false }));
|
|
552
|
+
}
|
|
296
553
|
}
|
|
554
|
+
}, [selectedName]);
|
|
555
|
+
useMouseWheel({
|
|
556
|
+
enabled: !showHelp,
|
|
557
|
+
// Disable when help is shown
|
|
558
|
+
onWheel: handleWheel
|
|
297
559
|
});
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
screen.key(["up", "k"], () => {
|
|
303
|
-
if (focusMode || !helpBox.hidden) return;
|
|
304
|
-
if (selectedIndex > 0) {
|
|
305
|
-
saveScrollPosition();
|
|
306
|
-
selectedIndex--;
|
|
307
|
-
updateProcessList();
|
|
308
|
-
updateOutput();
|
|
560
|
+
useInput((input, key) => {
|
|
561
|
+
if (showHelp) {
|
|
562
|
+
setShowHelp(false);
|
|
563
|
+
return;
|
|
309
564
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
565
|
+
if (input === "q" || key.ctrl && input === "c") {
|
|
566
|
+
killAll();
|
|
567
|
+
setRawMode(false);
|
|
568
|
+
const rows = stdout?.rows ?? 999;
|
|
569
|
+
stdout?.write(`\x1B[${rows};1H\x1B[J\x1B[?1000l\x1B[?1006l\x1B[?25h\x1B[0m
|
|
570
|
+
`);
|
|
571
|
+
exit();
|
|
572
|
+
process.exit(0);
|
|
318
573
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
if (!helpBox.hidden) {
|
|
322
|
-
helpBox.hide();
|
|
323
|
-
screen.render();
|
|
574
|
+
if (input === "?") {
|
|
575
|
+
setShowHelp(true);
|
|
324
576
|
return;
|
|
325
577
|
}
|
|
326
|
-
if (
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
if (name) {
|
|
334
|
-
|
|
578
|
+
if (focusMode) {
|
|
579
|
+
const name = names[selected];
|
|
580
|
+
if (!name) return;
|
|
581
|
+
if (key.escape) {
|
|
582
|
+
exitFocus();
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
if (key.shift && key.tab && !isShiftTabDisabled(name)) {
|
|
586
|
+
exitFocus();
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
if (key.return) {
|
|
590
|
+
write(name, "\r");
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
if (key.upArrow) {
|
|
594
|
+
write(name, "\x1B[A");
|
|
595
|
+
return;
|
|
335
596
|
}
|
|
597
|
+
if (key.downArrow) {
|
|
598
|
+
write(name, "\x1B[B");
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
if (key.leftArrow) {
|
|
602
|
+
write(name, "\x1B[D");
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
if (key.rightArrow) {
|
|
606
|
+
write(name, "\x1B[C");
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
if (input && !key.ctrl && !key.meta) {
|
|
610
|
+
write(name, input);
|
|
611
|
+
}
|
|
612
|
+
return;
|
|
336
613
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
const name = processNames[selectedIndex];
|
|
341
|
-
if (name) {
|
|
342
|
-
processManager.restart(name);
|
|
614
|
+
if (key.upArrow || input === "k") {
|
|
615
|
+
setSelected((s) => Math.max(s - 1, 0));
|
|
616
|
+
return;
|
|
343
617
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
processManager.restartAll();
|
|
348
|
-
});
|
|
349
|
-
screen.key(["x"], () => {
|
|
350
|
-
if (focusMode || !helpBox.hidden) return;
|
|
351
|
-
const name = processNames[selectedIndex];
|
|
352
|
-
if (name) {
|
|
353
|
-
processManager.kill(name);
|
|
618
|
+
if (key.downArrow || input === "j") {
|
|
619
|
+
setSelected((s) => Math.min(s + 1, names.length - 1));
|
|
620
|
+
return;
|
|
354
621
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
outputBox.setScrollPerc(0);
|
|
359
|
-
screen.render();
|
|
360
|
-
});
|
|
361
|
-
screen.key(["S-g"], () => {
|
|
362
|
-
if (focusMode || !helpBox.hidden) return;
|
|
363
|
-
outputBox.setScrollPerc(100);
|
|
364
|
-
screen.render();
|
|
365
|
-
});
|
|
366
|
-
outputBox.on("click", () => {
|
|
367
|
-
if (!helpBox.hidden) return;
|
|
368
|
-
if (!focusMode) {
|
|
369
|
-
focusMode = true;
|
|
370
|
-
const name = processNames[selectedIndex];
|
|
371
|
-
statusBar.setContent(` FOCUS: ${name} - Type to interact, [Esc] to exit focus mode `);
|
|
372
|
-
screen.render();
|
|
622
|
+
if (key.return || key.tab) {
|
|
623
|
+
enterFocus();
|
|
624
|
+
return;
|
|
373
625
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
const clickedIndex = data.y - absTop - 1;
|
|
379
|
-
if (clickedIndex < 0 || clickedIndex >= processNames.length) return;
|
|
380
|
-
if (focusMode) {
|
|
381
|
-
focusMode = false;
|
|
382
|
-
statusBar.setContent(" [\u2191\u2193/jk] select [Enter] focus [r] restart [A] restart All [x] kill [q] quit [?] help ");
|
|
626
|
+
if (input === "r") {
|
|
627
|
+
const name = names[selected];
|
|
628
|
+
if (name) restart(name);
|
|
629
|
+
return;
|
|
383
630
|
}
|
|
384
|
-
if (
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
updateProcessList();
|
|
388
|
-
updateOutput();
|
|
631
|
+
if (input === "A") {
|
|
632
|
+
restartAll();
|
|
633
|
+
return;
|
|
389
634
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
635
|
+
if (input === "x") {
|
|
636
|
+
const name = names[selected];
|
|
637
|
+
if (name) kill(name);
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
if (input === "g") {
|
|
641
|
+
outputRef.current?.scrollToTop();
|
|
642
|
+
if (selectedName) {
|
|
643
|
+
setAutoScroll((prev) => ({ ...prev, [selectedName]: false }));
|
|
396
644
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
if (input === "G") {
|
|
648
|
+
outputRef.current?.scrollToBottom();
|
|
649
|
+
if (selectedName) {
|
|
650
|
+
setAutoScroll((prev) => ({ ...prev, [selectedName]: true }));
|
|
400
651
|
}
|
|
652
|
+
return;
|
|
401
653
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
654
|
+
if (key.pageUp) {
|
|
655
|
+
const pageSize = outputRef.current?.getViewportHeight() ?? 10;
|
|
656
|
+
outputRef.current?.scrollBy(-pageSize);
|
|
657
|
+
if (selectedName) {
|
|
658
|
+
setAutoScroll((prev) => ({ ...prev, [selectedName]: false }));
|
|
659
|
+
}
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
if (key.pageDown) {
|
|
663
|
+
const pageSize = outputRef.current?.getViewportHeight() ?? 10;
|
|
664
|
+
outputRef.current?.scrollBy(pageSize);
|
|
665
|
+
return;
|
|
409
666
|
}
|
|
410
667
|
});
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
668
|
+
const showShiftTabHint = selectedName ? !isShiftTabDisabled(selectedName) : true;
|
|
669
|
+
return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", height: maxPanelHeight, children: [
|
|
670
|
+
/* @__PURE__ */ jsxs5(Box6, { flexDirection: "row", flexGrow: 1, height: maxPanelHeight ? maxPanelHeight - 1 : void 0, children: [
|
|
671
|
+
/* @__PURE__ */ jsx6(
|
|
672
|
+
ProcessList,
|
|
673
|
+
{
|
|
674
|
+
ref: processListRef,
|
|
675
|
+
names,
|
|
676
|
+
selected,
|
|
677
|
+
getStatus,
|
|
678
|
+
active: !focusMode,
|
|
679
|
+
height: maxPanelHeight ? maxPanelHeight - 1 : void 0
|
|
680
|
+
}
|
|
681
|
+
),
|
|
682
|
+
/* @__PURE__ */ jsx6(
|
|
683
|
+
OutputPanel,
|
|
684
|
+
{
|
|
685
|
+
ref: outputRef,
|
|
686
|
+
name: selectedName,
|
|
687
|
+
output,
|
|
688
|
+
active: focusMode,
|
|
689
|
+
height: maxPanelHeight ? maxPanelHeight - 1 : void 0,
|
|
690
|
+
autoScroll: currentAutoScroll,
|
|
691
|
+
onAutoScrollChange: handleAutoScrollChange
|
|
692
|
+
}
|
|
693
|
+
)
|
|
694
|
+
] }),
|
|
695
|
+
/* @__PURE__ */ jsx6(
|
|
696
|
+
StatusBar,
|
|
697
|
+
{
|
|
698
|
+
focusMode,
|
|
699
|
+
processName: selectedName,
|
|
700
|
+
showShiftTabHint
|
|
701
|
+
}
|
|
702
|
+
),
|
|
703
|
+
/* @__PURE__ */ jsx6(HelpPopup, { visible: showHelp })
|
|
704
|
+
] });
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// src/tui.ts
|
|
708
|
+
async function createTUI(config) {
|
|
709
|
+
const { waitUntilExit } = render(createElement(App, { config }));
|
|
710
|
+
await waitUntilExit();
|
|
418
711
|
}
|
|
419
712
|
export {
|
|
420
713
|
ProcessManager,
|