panex 0.9.3 → 0.9.5

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/dist/index.js CHANGED
@@ -1,15 +1,94 @@
1
1
  // src/tui.ts
2
- import blessed from "blessed";
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";
14
+
15
+ // src/terminal-buffer.ts
16
+ import { Terminal } from "@xterm/headless";
17
+ var TerminalBuffer = class {
18
+ terminal;
19
+ rows;
20
+ cols;
21
+ constructor(cols = 200, rows = 500) {
22
+ this.rows = rows;
23
+ this.cols = cols;
24
+ this.terminal = new Terminal({
25
+ cols,
26
+ rows,
27
+ scrollback: 1e4,
28
+ allowProposedApi: true
29
+ });
30
+ }
31
+ /**
32
+ * Write data to the terminal buffer.
33
+ * The terminal will interpret all ANSI escape sequences.
34
+ */
35
+ write(data) {
36
+ this.terminal.write(data);
37
+ }
38
+ /**
39
+ * Get the current terminal buffer content as an array of lines.
40
+ * Only returns lines that have content (not all 500 rows).
41
+ */
42
+ getLines() {
43
+ const buffer = this.terminal.buffer.active;
44
+ const lines = [];
45
+ const contentLength = buffer.baseY + buffer.cursorY + 1;
46
+ for (let i = 0; i < contentLength; i++) {
47
+ const line = buffer.getLine(i);
48
+ if (line) {
49
+ lines.push(line.translateToString(true));
50
+ }
51
+ }
52
+ while (lines.length > 0 && lines[lines.length - 1] === "") {
53
+ lines.pop();
54
+ }
55
+ return lines;
56
+ }
57
+ /**
58
+ * Get the terminal content as a single string with newlines.
59
+ */
60
+ toString() {
61
+ return this.getLines().join("\n");
62
+ }
63
+ /**
64
+ * Clear the terminal buffer.
65
+ */
66
+ clear() {
67
+ this.terminal.reset();
68
+ }
69
+ /**
70
+ * Resize the terminal.
71
+ */
72
+ resize(cols, rows) {
73
+ this.cols = cols;
74
+ this.rows = rows;
75
+ this.terminal.resize(cols, rows);
76
+ }
77
+ /**
78
+ * Dispose of the terminal to free resources.
79
+ */
80
+ dispose() {
81
+ this.terminal.dispose();
82
+ }
83
+ };
84
+
85
+ // src/process-manager.ts
6
86
  var ProcessManager = class extends EventEmitter {
7
87
  constructor(procs) {
8
88
  super();
9
89
  this.procs = procs;
10
90
  }
11
91
  processes = /* @__PURE__ */ new Map();
12
- maxOutputLines = 1e4;
13
92
  async startAll() {
14
93
  for (const [name, config] of Object.entries(this.procs)) {
15
94
  await this.start(name, config);
@@ -29,7 +108,7 @@ var ProcessManager = class extends EventEmitter {
29
108
  config,
30
109
  pty: null,
31
110
  status: "running",
32
- output: [],
111
+ terminalBuffer: new TerminalBuffer(120, 30),
33
112
  exitCode: null
34
113
  };
35
114
  this.processes.set(name, managed);
@@ -42,10 +121,7 @@ var ProcessManager = class extends EventEmitter {
42
121
  rows: 30,
43
122
  data: (_terminal, data) => {
44
123
  const str = new TextDecoder().decode(data);
45
- managed.output.push(str);
46
- if (managed.output.length > this.maxOutputLines) {
47
- managed.output = managed.output.slice(-this.maxOutputLines);
48
- }
124
+ managed.terminalBuffer.write(str);
49
125
  this.emit("output", name, str);
50
126
  }
51
127
  }
@@ -67,7 +143,7 @@ var ProcessManager = class extends EventEmitter {
67
143
  this.emit("started", name);
68
144
  } catch (error) {
69
145
  managed.status = "error";
70
- managed.output = [`Error starting process: ${error}`];
146
+ managed.terminalBuffer.write(`Error starting process: ${error}`);
71
147
  managed.exitCode = -1;
72
148
  this.emit("error", name, error);
73
149
  }
@@ -78,7 +154,7 @@ var ProcessManager = class extends EventEmitter {
78
154
  if (proc.pty) {
79
155
  proc.pty.kill();
80
156
  }
81
- proc.output = [];
157
+ proc.terminalBuffer.clear();
82
158
  this.start(name, proc.config);
83
159
  }
84
160
  }
@@ -122,299 +198,614 @@ var ProcessManager = class extends EventEmitter {
122
198
  return Array.from(this.processes.keys());
123
199
  }
124
200
  getOutput(name) {
125
- return this.processes.get(name)?.output.join("") ?? "";
201
+ return this.processes.get(name)?.terminalBuffer.toString() ?? "";
126
202
  }
127
203
  };
128
204
 
129
- // src/tui.ts
130
- async function createTUI(config) {
131
- const processManager = new ProcessManager(config.procs);
132
- const screen = blessed.screen({
133
- smartCSR: true,
134
- title: "panex",
135
- fullUnicode: true
136
- });
137
- const processList = blessed.list({
138
- parent: screen,
139
- label: " PROCESSES ",
140
- top: 0,
141
- left: 0,
142
- width: "20%",
143
- height: "100%-1",
144
- border: { type: "line" },
145
- style: {
146
- border: { fg: "blue" },
147
- selected: { bg: "blue", fg: "white" },
148
- item: { fg: "white" }
149
- },
150
- mouse: config.settings?.mouse ?? true,
151
- scrollbar: {
152
- ch: "\u2502",
153
- style: { bg: "blue" }
154
- }
155
- });
156
- const outputBox = blessed.box({
157
- parent: screen,
158
- label: " OUTPUT ",
159
- top: 0,
160
- left: "20%",
161
- width: "80%",
162
- height: "100%-1",
163
- border: { type: "line" },
164
- style: {
165
- border: { fg: "green" }
166
- },
167
- scrollable: true,
168
- alwaysScroll: true,
169
- scrollbar: {
170
- ch: "\u2502",
171
- style: { bg: "green" }
172
- },
173
- mouse: config.settings?.mouse ?? true,
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
205
+ // src/hooks/useProcessManager.ts
206
+ function useProcessManager(config) {
207
+ const [, forceUpdate] = useState({});
208
+ const processManagerRef = useRef(null);
209
+ if (!processManagerRef.current) {
210
+ processManagerRef.current = new ProcessManager(config.procs);
211
+ }
212
+ const pm = processManagerRef.current;
213
+ useEffect(() => {
214
+ const update = () => forceUpdate({});
215
+ pm.on("output", update);
216
+ pm.on("started", update);
217
+ pm.on("exit", update);
218
+ pm.on("error", update);
219
+ pm.startAll();
220
+ return () => {
221
+ pm.removeAllListeners();
222
+ pm.killAll();
223
+ };
224
+ }, [pm]);
225
+ const getOutput = useCallback((name) => pm.getOutput(name), [pm]);
226
+ const getStatus = useCallback((name) => {
227
+ const proc = pm.getProcess(name);
228
+ return proc?.status ?? "stopped";
229
+ }, [pm]);
230
+ const restart = useCallback((name) => pm.restart(name), [pm]);
231
+ const restartAll = useCallback(() => pm.restartAll(), [pm]);
232
+ const kill = useCallback((name) => pm.kill(name), [pm]);
233
+ const killAll = useCallback(() => pm.killAll(), [pm]);
234
+ const write = useCallback((name, data) => pm.write(name, data), [pm]);
235
+ const resize = useCallback((name, cols, rows) => pm.resize(name, cols, rows), [pm]);
236
+ return {
237
+ processManager: pm,
238
+ processes: new Map(pm.getNames().map((n) => [n, pm.getProcess(n)])),
239
+ names: pm.getNames(),
240
+ getOutput,
241
+ getStatus,
242
+ restart,
243
+ restartAll,
244
+ kill,
245
+ killAll,
246
+ write,
247
+ resize
248
+ };
249
+ }
205
250
 
206
- Navigation
207
- \u2191/\u2193 or j/k Navigate process list
208
- g/G Scroll to top/bottom of output
209
- PgUp/PgDn Scroll output
251
+ // src/hooks/useFocusMode.ts
252
+ import { useState as useState2, useCallback as useCallback2 } from "react";
253
+ function useFocusMode() {
254
+ const [focusMode, setFocusMode] = useState2(false);
255
+ const enterFocus = useCallback2(() => setFocusMode(true), []);
256
+ const exitFocus = useCallback2(() => setFocusMode(false), []);
257
+ const toggleFocus = useCallback2(() => setFocusMode((f) => !f), []);
258
+ return { focusMode, enterFocus, exitFocus, toggleFocus };
259
+ }
210
260
 
211
- Process Control
212
- Enter Focus process (interactive mode)
213
- Esc Exit focus mode
214
- r Restart selected process
215
- A Restart all processes
216
- x Kill selected process
261
+ // src/hooks/useMouseWheel.ts
262
+ import { useEffect as useEffect2, useCallback as useCallback3 } from "react";
263
+ import { useStdin, useStdout } from "ink";
264
+ function useMouseWheel({ enabled = true, onWheel, onClick } = {}) {
265
+ const { stdin, setRawMode } = useStdin();
266
+ const { stdout } = useStdout();
267
+ const handleData = useCallback3((data) => {
268
+ const str = data.toString();
269
+ const sgrRegex = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/g;
270
+ let match;
271
+ while ((match = sgrRegex.exec(str)) !== null) {
272
+ const button = parseInt(match[1] ?? "0", 10);
273
+ const x = parseInt(match[2] ?? "0", 10);
274
+ const y = parseInt(match[3] ?? "0", 10);
275
+ const isPress = match[4] === "M";
276
+ if (isPress) {
277
+ if (button === 0) {
278
+ onClick?.({ type: "click", x, y });
279
+ } else if (button === 64) {
280
+ onWheel?.({ type: "wheel-up", x, y });
281
+ } else if (button === 65) {
282
+ onWheel?.({ type: "wheel-down", x, y });
283
+ }
284
+ }
285
+ }
286
+ }, [onWheel, onClick]);
287
+ useEffect2(() => {
288
+ if (!enabled || !stdin || !stdout) return;
289
+ stdout.write("\x1B[?1000h\x1B[?1006h");
290
+ setRawMode?.(true);
291
+ stdin.on("data", handleData);
292
+ return () => {
293
+ stdin.off("data", handleData);
294
+ stdout.write("\x1B[?1000l\x1B[?1006l");
295
+ };
296
+ }, [enabled, stdin, stdout, setRawMode, handleData]);
297
+ }
298
+
299
+ // src/components/ProcessList.tsx
300
+ import { Box, Text, useStdout as useStdout2 } from "ink";
301
+ import { ScrollList } from "ink-scroll-list";
302
+ import { forwardRef, useImperativeHandle, useRef as useRef2, useEffect as useEffect3 } from "react";
303
+ import { jsx, jsxs } from "react/jsx-runtime";
304
+ var PROCESS_LIST_WIDTH = 20;
305
+ var ProcessList = forwardRef(
306
+ function ProcessList2({ names, selected, getStatus, active, height }, ref) {
307
+ const borderStyle = active ? "double" : "single";
308
+ const listRef = useRef2(null);
309
+ const { stdout } = useStdout2();
310
+ useEffect3(() => {
311
+ const handleResize = () => listRef.current?.remeasure();
312
+ stdout?.on("resize", handleResize);
313
+ return () => {
314
+ stdout?.off("resize", handleResize);
315
+ };
316
+ }, [stdout]);
317
+ useImperativeHandle(ref, () => ({
318
+ scrollBy: (delta) => listRef.current?.scrollBy(delta),
319
+ scrollToTop: () => listRef.current?.scrollToTop(),
320
+ scrollToBottom: () => listRef.current?.scrollToBottom()
321
+ }));
322
+ return /* @__PURE__ */ jsx(
323
+ Box,
324
+ {
325
+ flexDirection: "column",
326
+ borderStyle,
327
+ borderColor: active ? "blue" : "gray",
328
+ width: PROCESS_LIST_WIDTH,
329
+ height,
330
+ paddingX: 1,
331
+ children: /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginTop: 0, height: height ? height - 2 : void 0, children: /* @__PURE__ */ jsx(
332
+ ScrollList,
333
+ {
334
+ ref: listRef,
335
+ selectedIndex: selected,
336
+ scrollAlignment: "auto",
337
+ children: names.map((name, i) => {
338
+ const status = getStatus(name);
339
+ const statusIcon = status === "running" ? "\u25CF" : status === "error" ? "\u2717" : "\u25CB";
340
+ const statusColor = status === "running" ? "green" : status === "error" ? "red" : "gray";
341
+ const isSelected = i === selected;
342
+ return /* @__PURE__ */ jsxs(Box, { backgroundColor: isSelected ? "blue" : void 0, children: [
343
+ /* @__PURE__ */ jsxs(Text, { color: isSelected ? "black" : void 0, children: [
344
+ name,
345
+ " "
346
+ ] }),
347
+ /* @__PURE__ */ jsx(Text, { color: statusColor, children: statusIcon })
348
+ ] }, name);
349
+ })
350
+ }
351
+ ) })
352
+ }
353
+ );
354
+ }
355
+ );
217
356
 
218
- General
219
- ? Toggle this help
220
- q Quit panex
357
+ // src/components/OutputPanel.tsx
358
+ import { Box as Box3, Text as Text3, useStdout as useStdout3 } from "ink";
359
+ import { ScrollView } from "ink-scroll-view";
360
+ import { forwardRef as forwardRef2, useImperativeHandle as useImperativeHandle2, useRef as useRef3, useEffect as useEffect4, useState as useState3, useCallback as useCallback4 } from "react";
221
361
 
222
- Press any key to close this help...
223
- `
224
- });
225
- let selectedIndex = 0;
226
- let focusMode = false;
227
- const processNames = Object.keys(config.procs);
228
- const scrollPositions = /* @__PURE__ */ new Map();
229
- function updateProcessList() {
230
- const items = processNames.map((name, i) => {
231
- const proc = processManager.getProcess(name);
232
- const status = proc?.status === "running" ? "\u25CF" : proc?.status === "error" ? "\u2717" : "\u25CB";
233
- const prefix = i === selectedIndex ? "\u25B6" : " ";
234
- return `${prefix} ${name} ${status}`;
235
- });
236
- processList.setItems(items);
237
- processList.select(selectedIndex);
238
- screen.render();
362
+ // src/components/Scrollbar.tsx
363
+ import { Box as Box2, Text as Text2 } from "ink";
364
+ import { jsx as jsx2 } from "react/jsx-runtime";
365
+ function Scrollbar({
366
+ scrollOffset,
367
+ contentHeight,
368
+ viewportHeight,
369
+ height
370
+ }) {
371
+ if (contentHeight <= viewportHeight) {
372
+ return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", width: 1, children: Array.from({ length: height }).map((_, i) => /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " " }, i)) });
239
373
  }
240
- function updateOutput(autoScroll = false) {
241
- const name = processNames[selectedIndex];
242
- if (name) {
243
- outputBox.setLabel(` OUTPUT: ${name} `);
244
- const output = processManager.getOutput(name);
245
- outputBox.setContent(output);
246
- if (autoScroll) {
247
- outputBox.setScrollPerc(100);
248
- } else {
249
- const savedPos = scrollPositions.get(name) ?? 100;
250
- outputBox.setScrollPerc(savedPos);
251
- }
374
+ const trackHeight = height;
375
+ const thumbRatio = viewportHeight / contentHeight;
376
+ const thumbHeight = Math.max(1, Math.round(trackHeight * thumbRatio));
377
+ const maxScroll = contentHeight - viewportHeight;
378
+ const scrollRatio = maxScroll > 0 ? scrollOffset / maxScroll : 0;
379
+ const thumbPosition = Math.round((trackHeight - thumbHeight) * scrollRatio);
380
+ const lines = [];
381
+ for (let i = 0; i < trackHeight; i++) {
382
+ if (i >= thumbPosition && i < thumbPosition + thumbHeight) {
383
+ lines.push("\u2588");
384
+ } else {
385
+ lines.push("\u2591");
252
386
  }
253
- screen.render();
254
387
  }
255
- function saveScrollPosition() {
256
- const name = processNames[selectedIndex];
257
- if (name) {
258
- scrollPositions.set(name, outputBox.getScrollPerc());
259
- }
388
+ return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", width: 1, children: lines.map((char, i) => /* @__PURE__ */ jsx2(Text2, { dimColor: char === "\u2591", children: char }, i)) });
389
+ }
390
+
391
+ // src/components/OutputPanel.tsx
392
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
393
+ var OutputPanel = forwardRef2(
394
+ function OutputPanel2({ name, output, active, height, autoScroll = true, onAutoScrollChange }, ref) {
395
+ const borderStyle = active ? "double" : "single";
396
+ const lines = output.split("\n");
397
+ const scrollRef = useRef3(null);
398
+ const { stdout } = useStdout3();
399
+ const [scrollOffset, setScrollOffset] = useState3(0);
400
+ const [contentHeight, setContentHeight] = useState3(0);
401
+ const [viewportHeight, setViewportHeight] = useState3(0);
402
+ useEffect4(() => {
403
+ const handleResize = () => scrollRef.current?.remeasure();
404
+ stdout?.on("resize", handleResize);
405
+ return () => {
406
+ stdout?.off("resize", handleResize);
407
+ };
408
+ }, [stdout]);
409
+ const isAtBottom = useCallback4(() => {
410
+ if (!scrollRef.current) return true;
411
+ const offset = scrollRef.current.getScrollOffset();
412
+ const bottom = scrollRef.current.getBottomOffset();
413
+ return offset >= bottom - 1;
414
+ }, []);
415
+ const handleContentHeightChange = useCallback4((newHeight) => {
416
+ setContentHeight(newHeight);
417
+ if (autoScroll && scrollRef.current) {
418
+ setTimeout(() => {
419
+ scrollRef.current?.scrollToBottom();
420
+ }, 0);
421
+ }
422
+ }, [autoScroll]);
423
+ const handleScroll = useCallback4((offset) => {
424
+ setScrollOffset(offset);
425
+ if (scrollRef.current) {
426
+ const bottom = scrollRef.current.getBottomOffset();
427
+ const atBottom = offset >= bottom - 1;
428
+ if (!atBottom && autoScroll) {
429
+ onAutoScrollChange?.(false);
430
+ } else if (atBottom && !autoScroll) {
431
+ onAutoScrollChange?.(true);
432
+ }
433
+ }
434
+ }, [autoScroll, onAutoScrollChange]);
435
+ useImperativeHandle2(ref, () => ({
436
+ scrollBy: (delta) => scrollRef.current?.scrollBy(delta),
437
+ scrollToTop: () => scrollRef.current?.scrollToTop(),
438
+ scrollToBottom: () => scrollRef.current?.scrollToBottom(),
439
+ getScrollOffset: () => scrollRef.current?.getScrollOffset() ?? 0,
440
+ getContentHeight: () => scrollRef.current?.getContentHeight() ?? 0,
441
+ getViewportHeight: () => scrollRef.current?.getViewportHeight() ?? 0,
442
+ isAtBottom
443
+ }));
444
+ const scrollbarHeight = height ? height - 4 : 20;
445
+ const hasScroll = contentHeight > viewportHeight;
446
+ const pinIndicator = !autoScroll && hasScroll ? " \u2357" : "";
447
+ return /* @__PURE__ */ jsx3(
448
+ Box3,
449
+ {
450
+ flexDirection: "column",
451
+ borderStyle,
452
+ borderColor: active ? "green" : "gray",
453
+ flexGrow: 1,
454
+ height,
455
+ paddingLeft: 1,
456
+ children: /* @__PURE__ */ jsxs2(Box3, { flexDirection: "row", marginTop: 0, height: height ? height - 2 : void 0, children: [
457
+ /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", flexGrow: 1, children: /* @__PURE__ */ jsx3(
458
+ ScrollView,
459
+ {
460
+ ref: scrollRef,
461
+ onScroll: handleScroll,
462
+ onContentHeightChange: handleContentHeightChange,
463
+ onViewportSizeChange: (layout) => setViewportHeight(layout.height),
464
+ children: lines.map((line, i) => /* @__PURE__ */ jsx3(Text3, { wrap: "truncate", children: line }, i))
465
+ }
466
+ ) }),
467
+ hasScroll && /* @__PURE__ */ jsx3(
468
+ Scrollbar,
469
+ {
470
+ scrollOffset,
471
+ contentHeight,
472
+ viewportHeight,
473
+ height: scrollbarHeight
474
+ }
475
+ )
476
+ ] })
477
+ }
478
+ );
260
479
  }
261
- processManager.on("output", (name) => {
262
- if (name === processNames[selectedIndex]) {
263
- updateOutput(true);
480
+ );
481
+
482
+ // src/components/StatusBar.tsx
483
+ import { Box as Box4, Text as Text4 } from "ink";
484
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
485
+ function StatusBar({ focusMode, processName, showShiftTabHint = true }) {
486
+ if (focusMode && processName) {
487
+ const shiftTabHint = showShiftTabHint ? "Shift-Tab/" : "";
488
+ return /* @__PURE__ */ jsx4(Box4, { backgroundColor: "green", width: "100%", children: /* @__PURE__ */ jsxs3(Text4, { bold: true, color: "black", backgroundColor: "green", children: [
489
+ " ",
490
+ processName,
491
+ " | [",
492
+ shiftTabHint,
493
+ "Esc] to exit focus mode",
494
+ " "
495
+ ] }) });
496
+ }
497
+ return /* @__PURE__ */ jsx4(Box4, { backgroundColor: "blue", width: "100%", children: /* @__PURE__ */ jsxs3(Text4, { bold: true, color: "black", backgroundColor: "blue", children: [
498
+ " ",
499
+ "[\u2191\u2193/jk] select [Tab/Enter] focus [r] restart [A] restart All [x] kill [q] quit [?] help",
500
+ " "
501
+ ] }) });
502
+ }
503
+
504
+ // src/components/HelpPopup.tsx
505
+ import { Box as Box5, Text as Text5 } from "ink";
506
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
507
+ function HelpPopup({ visible }) {
508
+ if (!visible) return null;
509
+ return /* @__PURE__ */ jsxs4(
510
+ Box5,
511
+ {
512
+ flexDirection: "column",
513
+ borderStyle: "single",
514
+ borderColor: "yellow",
515
+ padding: 1,
516
+ position: "absolute",
517
+ marginLeft: 10,
518
+ marginTop: 5,
519
+ children: [
520
+ /* @__PURE__ */ jsx5(Text5, { bold: true, color: "yellow", children: " Help " }),
521
+ /* @__PURE__ */ jsxs4(Text5, { children: [
522
+ "\n",
523
+ "Keyboard Shortcuts"
524
+ ] }),
525
+ /* @__PURE__ */ jsx5(Text5, { children: "\u2500".repeat(18) }),
526
+ /* @__PURE__ */ jsxs4(Text5, { children: [
527
+ "\n",
528
+ "Navigation"
529
+ ] }),
530
+ /* @__PURE__ */ jsx5(Text5, { children: " \u2191/\u2193 or j/k Navigate process list" }),
531
+ /* @__PURE__ */ jsx5(Text5, { children: " g/G Scroll to top/bottom of output" }),
532
+ /* @__PURE__ */ jsx5(Text5, { children: " PgUp/PgDn Scroll output" }),
533
+ /* @__PURE__ */ jsxs4(Text5, { children: [
534
+ "\n",
535
+ "Process Control"
536
+ ] }),
537
+ /* @__PURE__ */ jsx5(Text5, { children: " Tab/Enter Focus process (interactive mode)" }),
538
+ /* @__PURE__ */ jsx5(Text5, { children: " Esc Exit focus mode" }),
539
+ /* @__PURE__ */ jsx5(Text5, { children: " r Restart selected process" }),
540
+ /* @__PURE__ */ jsx5(Text5, { children: " A Restart all processes" }),
541
+ /* @__PURE__ */ jsx5(Text5, { children: " x Kill selected process" }),
542
+ /* @__PURE__ */ jsxs4(Text5, { children: [
543
+ "\n",
544
+ "General"
545
+ ] }),
546
+ /* @__PURE__ */ jsx5(Text5, { children: " ? Toggle this help" }),
547
+ /* @__PURE__ */ jsx5(Text5, { children: " q Quit panex" }),
548
+ /* @__PURE__ */ jsxs4(Text5, { children: [
549
+ "\n",
550
+ "Press any key to close this help..."
551
+ ] })
552
+ ]
264
553
  }
265
- });
266
- processManager.on("started", () => {
267
- updateProcessList();
268
- });
269
- processManager.on("exit", () => {
270
- updateProcessList();
271
- });
272
- processManager.on("error", (name) => {
273
- updateProcessList();
274
- if (name === processNames[selectedIndex]) {
275
- updateOutput();
554
+ );
555
+ }
556
+
557
+ // src/components/App.tsx
558
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
559
+ function App({ config }) {
560
+ const { exit } = useApp();
561
+ const { stdout } = useStdout4();
562
+ const { setRawMode } = useStdin2();
563
+ const [selected, setSelected] = useState4(0);
564
+ const [showHelp, setShowHelp] = useState4(false);
565
+ const { focusMode, enterFocus, exitFocus } = useFocusMode();
566
+ const outputRef = useRef4(null);
567
+ const processListRef = useRef4(null);
568
+ const [autoScroll, setAutoScroll] = useState4({});
569
+ const {
570
+ names,
571
+ getOutput,
572
+ getStatus,
573
+ restart,
574
+ restartAll,
575
+ kill,
576
+ killAll,
577
+ write,
578
+ resize
579
+ } = useProcessManager(config);
580
+ const isShiftTabDisabled = (name) => {
581
+ const setting = config.settings?.noShiftTab;
582
+ if (setting === true) return true;
583
+ if (Array.isArray(setting)) return setting.includes(name);
584
+ return false;
585
+ };
586
+ const maxPanelHeight = stdout ? stdout.rows - 1 : void 0;
587
+ useEffect5(() => {
588
+ const name = names[selected];
589
+ if (name && stdout) {
590
+ const cols = Math.floor(stdout.columns * 0.8) - 2;
591
+ const rows = stdout.rows - 3;
592
+ resize(name, cols, rows);
276
593
  }
594
+ }, [stdout?.columns, stdout?.rows, selected, names, resize]);
595
+ useEffect5(() => {
596
+ setAutoScroll((prev) => {
597
+ const next = { ...prev };
598
+ let changed = false;
599
+ for (const name of names) {
600
+ if (next[name] === void 0) {
601
+ next[name] = true;
602
+ changed = true;
603
+ }
604
+ }
605
+ return changed ? next : prev;
606
+ });
607
+ }, [names]);
608
+ const selectedName = names[selected] ?? "";
609
+ const output = selectedName ? getOutput(selectedName) : "";
610
+ const currentAutoScroll = selectedName ? autoScroll[selectedName] ?? true : true;
611
+ const handleAutoScrollChange = useCallback5((enabled) => {
612
+ if (selectedName) {
613
+ setAutoScroll((prev) => ({ ...prev, [selectedName]: enabled }));
614
+ }
615
+ }, [selectedName]);
616
+ const handleWheel = useCallback5((event) => {
617
+ const delta = event.type === "wheel-up" ? -3 : 3;
618
+ if (outputRef.current) {
619
+ outputRef.current.scrollBy(delta);
620
+ if (event.type === "wheel-up" && selectedName) {
621
+ setAutoScroll((prev) => ({ ...prev, [selectedName]: false }));
622
+ }
623
+ }
624
+ }, [selectedName]);
625
+ const handleClick = useCallback5((event) => {
626
+ if (event.x <= PROCESS_LIST_WIDTH) {
627
+ if (focusMode) {
628
+ exitFocus();
629
+ }
630
+ const clickedIndex = event.y - 2;
631
+ if (clickedIndex >= 0 && clickedIndex < names.length) {
632
+ setSelected(clickedIndex);
633
+ }
634
+ } else {
635
+ if (!focusMode) {
636
+ enterFocus();
637
+ }
638
+ }
639
+ }, [names.length, focusMode, enterFocus, exitFocus]);
640
+ useMouseWheel({
641
+ enabled: !showHelp,
642
+ // Disable when help is shown
643
+ onWheel: handleWheel,
644
+ onClick: handleClick
277
645
  });
278
- screen.key(["q", "C-c"], () => {
279
- processManager.killAll();
280
- process.exit(0);
281
- });
282
- screen.key(["?"], () => {
283
- helpBox.toggle();
284
- screen.render();
285
- });
286
- screen.key(["escape"], () => {
287
- if (!helpBox.hidden) {
288
- helpBox.hide();
289
- screen.render();
646
+ useInput((input, key) => {
647
+ if (showHelp) {
648
+ setShowHelp(false);
290
649
  return;
291
650
  }
651
+ if (key.ctrl && input === "c") {
652
+ killAll();
653
+ setRawMode(false);
654
+ const rows = stdout?.rows ?? 999;
655
+ stdout?.write(`\x1B[${rows};1H\x1B[J\x1B[?1000l\x1B[?1006l\x1B[?25h\x1B[0m
656
+ `);
657
+ exit();
658
+ process.exit(0);
659
+ }
292
660
  if (focusMode) {
293
- focusMode = false;
294
- statusBar.setContent(" [\u2191\u2193/jk] select [Enter] focus [r] restart [A] restart All [x] kill [q] quit [?] help ");
295
- screen.render();
661
+ const name = names[selected];
662
+ if (!name) return;
663
+ if (key.escape) {
664
+ exitFocus();
665
+ return;
666
+ }
667
+ if (key.shift && key.tab && !isShiftTabDisabled(name)) {
668
+ exitFocus();
669
+ return;
670
+ }
671
+ if (key.return) {
672
+ write(name, "\r");
673
+ return;
674
+ }
675
+ if (key.upArrow) {
676
+ write(name, "\x1B[A");
677
+ return;
678
+ }
679
+ if (key.downArrow) {
680
+ write(name, "\x1B[B");
681
+ return;
682
+ }
683
+ if (key.leftArrow) {
684
+ write(name, "\x1B[D");
685
+ return;
686
+ }
687
+ if (key.rightArrow) {
688
+ write(name, "\x1B[C");
689
+ return;
690
+ }
691
+ if (input && !key.ctrl && !key.meta) {
692
+ const filtered = input.replace(/\x1b?\[<\d+;\d+;\d+[Mm]/g, "");
693
+ if (filtered) {
694
+ write(name, filtered);
695
+ }
696
+ }
697
+ return;
296
698
  }
297
- });
298
- helpBox.key(["escape", "q", "?", "enter", "space"], () => {
299
- helpBox.hide();
300
- screen.render();
301
- });
302
- screen.key(["up", "k"], () => {
303
- if (focusMode || !helpBox.hidden) return;
304
- if (selectedIndex > 0) {
305
- saveScrollPosition();
306
- selectedIndex--;
307
- updateProcessList();
308
- updateOutput();
699
+ if (input === "q") {
700
+ killAll();
701
+ setRawMode(false);
702
+ const rows = stdout?.rows ?? 999;
703
+ stdout?.write(`\x1B[${rows};1H\x1B[J\x1B[?1000l\x1B[?1006l\x1B[?25h\x1B[0m
704
+ `);
705
+ exit();
706
+ process.exit(0);
309
707
  }
310
- });
311
- screen.key(["down", "j"], () => {
312
- if (focusMode || !helpBox.hidden) return;
313
- if (selectedIndex < processNames.length - 1) {
314
- saveScrollPosition();
315
- selectedIndex++;
316
- updateProcessList();
317
- updateOutput();
708
+ if (input === "?") {
709
+ setShowHelp(true);
710
+ return;
318
711
  }
319
- });
320
- screen.key(["enter"], () => {
321
- if (!helpBox.hidden) {
322
- helpBox.hide();
323
- screen.render();
712
+ if (key.upArrow || input === "k") {
713
+ setSelected((s) => Math.max(s - 1, 0));
324
714
  return;
325
715
  }
326
- if (!focusMode) {
327
- focusMode = true;
328
- const name = processNames[selectedIndex];
329
- statusBar.setContent(` FOCUS: ${name} - Type to interact, [Esc] to exit focus mode `);
330
- screen.render();
331
- } else {
332
- const name = processNames[selectedIndex];
333
- if (name) {
334
- processManager.write(name, "\r");
335
- }
716
+ if (key.downArrow || input === "j") {
717
+ setSelected((s) => Math.min(s + 1, names.length - 1));
718
+ return;
336
719
  }
337
- });
338
- screen.key(["r"], () => {
339
- if (focusMode || !helpBox.hidden) return;
340
- const name = processNames[selectedIndex];
341
- if (name) {
342
- processManager.restart(name);
720
+ if (key.return || key.tab) {
721
+ enterFocus();
722
+ return;
343
723
  }
344
- });
345
- screen.key(["S-a"], () => {
346
- if (focusMode || !helpBox.hidden) return;
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);
724
+ if (input === "r") {
725
+ const name = names[selected];
726
+ if (name) restart(name);
727
+ return;
354
728
  }
355
- });
356
- screen.key(["g"], () => {
357
- if (focusMode || !helpBox.hidden) return;
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();
729
+ if (input === "A") {
730
+ restartAll();
731
+ return;
373
732
  }
374
- });
375
- processList.on("element click", (_el, data) => {
376
- if (!helpBox.hidden) return;
377
- const absTop = processList.atop ?? 0;
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 ");
733
+ if (input === "x") {
734
+ const name = names[selected];
735
+ if (name) kill(name);
736
+ return;
383
737
  }
384
- if (clickedIndex !== selectedIndex) {
385
- saveScrollPosition();
386
- selectedIndex = clickedIndex;
387
- updateProcessList();
388
- updateOutput();
738
+ if (input === "g") {
739
+ outputRef.current?.scrollToTop();
740
+ if (selectedName) {
741
+ setAutoScroll((prev) => ({ ...prev, [selectedName]: false }));
742
+ }
743
+ return;
389
744
  }
390
- screen.render();
391
- });
392
- screen.on("keypress", (ch, key) => {
393
- if (focusMode && ch) {
394
- if (key.name === "escape" || key.name === "return" || key.name === "enter") {
395
- return;
745
+ if (input === "G") {
746
+ outputRef.current?.scrollToBottom();
747
+ if (selectedName) {
748
+ setAutoScroll((prev) => ({ ...prev, [selectedName]: true }));
396
749
  }
397
- const name = processNames[selectedIndex];
398
- if (name) {
399
- processManager.write(name, ch);
750
+ return;
751
+ }
752
+ if (key.pageUp) {
753
+ const pageSize = outputRef.current?.getViewportHeight() ?? 10;
754
+ outputRef.current?.scrollBy(-pageSize);
755
+ if (selectedName) {
756
+ setAutoScroll((prev) => ({ ...prev, [selectedName]: false }));
400
757
  }
758
+ return;
401
759
  }
402
- });
403
- screen.on("resize", () => {
404
- const name = processNames[selectedIndex];
405
- if (name) {
406
- const cols = Math.floor(screen.width * 0.8) - 2;
407
- const rows = screen.height - 3;
408
- processManager.resize(name, cols, rows);
760
+ if (key.pageDown) {
761
+ const pageSize = outputRef.current?.getViewportHeight() ?? 10;
762
+ outputRef.current?.scrollBy(pageSize);
763
+ return;
409
764
  }
410
765
  });
411
- updateProcessList();
412
- updateOutput();
413
- processList.focus();
414
- await processManager.startAll();
415
- updateProcessList();
416
- updateOutput();
417
- screen.render();
766
+ const showShiftTabHint = selectedName ? !isShiftTabDisabled(selectedName) : true;
767
+ return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", height: maxPanelHeight, children: [
768
+ /* @__PURE__ */ jsxs5(Box6, { flexDirection: "row", flexGrow: 1, height: maxPanelHeight ? maxPanelHeight - 1 : void 0, children: [
769
+ /* @__PURE__ */ jsx6(
770
+ ProcessList,
771
+ {
772
+ ref: processListRef,
773
+ names,
774
+ selected,
775
+ getStatus,
776
+ active: !focusMode,
777
+ height: maxPanelHeight ? maxPanelHeight - 1 : void 0
778
+ }
779
+ ),
780
+ /* @__PURE__ */ jsx6(
781
+ OutputPanel,
782
+ {
783
+ ref: outputRef,
784
+ name: selectedName,
785
+ output,
786
+ active: focusMode,
787
+ height: maxPanelHeight ? maxPanelHeight - 1 : void 0,
788
+ autoScroll: currentAutoScroll,
789
+ onAutoScrollChange: handleAutoScrollChange
790
+ }
791
+ )
792
+ ] }),
793
+ /* @__PURE__ */ jsx6(
794
+ StatusBar,
795
+ {
796
+ focusMode,
797
+ processName: selectedName,
798
+ showShiftTabHint
799
+ }
800
+ ),
801
+ /* @__PURE__ */ jsx6(HelpPopup, { visible: showHelp })
802
+ ] });
803
+ }
804
+
805
+ // src/tui.ts
806
+ async function createTUI(config) {
807
+ const { waitUntilExit } = render(createElement(App, { config }));
808
+ await waitUntilExit();
418
809
  }
419
810
  export {
420
811
  ProcessManager,