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/dist/index.js CHANGED
@@ -1,5 +1,13 @@
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";
@@ -126,295 +134,580 @@ var ProcessManager = class extends EventEmitter {
126
134
  }
127
135
  };
128
136
 
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
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
- Navigation
207
- \u2191/\u2193 or j/k Navigate process list
208
- g/G Scroll to top/bottom of output
209
- PgUp/PgDn Scroll output
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
- 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
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
- General
219
- ? Toggle this help
220
- q Quit panex
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
- 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();
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
- 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
- }
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
- function saveScrollPosition() {
256
- const name = processNames[selectedIndex];
257
- if (name) {
258
- scrollPositions.set(name, outputBox.getScrollPerc());
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
- processManager.on("output", (name) => {
262
- if (name === processNames[selectedIndex]) {
263
- updateOutput(true);
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
- 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();
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
- 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();
290
- return;
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
- 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();
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
- 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();
560
+ useInput((input, key) => {
561
+ if (showHelp) {
562
+ setShowHelp(false);
563
+ return;
309
564
  }
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();
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
- screen.key(["enter"], () => {
321
- if (!helpBox.hidden) {
322
- helpBox.hide();
323
- screen.render();
574
+ if (input === "?") {
575
+ setShowHelp(true);
324
576
  return;
325
577
  }
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");
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
- screen.key(["r"], () => {
339
- if (focusMode || !helpBox.hidden) return;
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
- 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);
618
+ if (key.downArrow || input === "j") {
619
+ setSelected((s) => Math.min(s + 1, names.length - 1));
620
+ return;
354
621
  }
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();
622
+ if (key.return || key.tab) {
623
+ enterFocus();
624
+ return;
373
625
  }
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 ");
626
+ if (input === "r") {
627
+ const name = names[selected];
628
+ if (name) restart(name);
629
+ return;
383
630
  }
384
- if (clickedIndex !== selectedIndex) {
385
- saveScrollPosition();
386
- selectedIndex = clickedIndex;
387
- updateProcessList();
388
- updateOutput();
631
+ if (input === "A") {
632
+ restartAll();
633
+ return;
389
634
  }
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;
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
- const name = processNames[selectedIndex];
398
- if (name) {
399
- processManager.write(name, ch);
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
- 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);
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
- updateProcessList();
412
- updateOutput();
413
- processList.focus();
414
- await processManager.startAll();
415
- updateProcessList();
416
- updateOutput();
417
- screen.render();
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,