pyre-cli 9.0.0 → 9.1.0
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/{anomaliesCmd-TO52YY2B.js → anomaliesCmd-25OFKYPK.js} +1 -1
- package/dist/{chunk-EQZCK2VC.js → chunk-QOLZ7T32.js} +9 -0
- package/dist/{collectors-KNEN4YJD.js → collectors-VACLPHRP.js} +1 -1
- package/dist/index.js +418 -80
- package/package.json +1 -1
- package/src/anomaliesCmd.ts +1 -1
- package/src/formatters/render.ts +127 -38
- package/src/formatters/types.ts +6 -0
- package/src/live/input.ts +149 -5
- package/src/live/render.ts +145 -3
- package/src/live/state.ts +7 -0
- package/src/live/types.ts +1 -1
- package/src/monitors/collectors.ts +11 -0
package/package.json
CHANGED
package/src/anomaliesCmd.ts
CHANGED
|
@@ -20,7 +20,7 @@ export function runAnomaliesDigest(opts: AnomaliesCmdOptions = {}): void {
|
|
|
20
20
|
return;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
const files = fs.readdirSync(dir).filter(f => f.startsWith('pyre-
|
|
23
|
+
const files = fs.readdirSync(dir).filter(f => f.startsWith('pyre-') && f.endsWith('.csv'));
|
|
24
24
|
if (files.length === 0) {
|
|
25
25
|
console.log(chalk.yellow(` No CSV logs found for range '${sinceStr}' in ${dir}.`));
|
|
26
26
|
console.log(chalk.dim(' Enable background logging with `pyre --log` to record history.\n'));
|
package/src/formatters/render.ts
CHANGED
|
@@ -474,7 +474,8 @@ function pctColor(v: number): (s: string) => string {
|
|
|
474
474
|
function processTableLines(
|
|
475
475
|
processes: { pid: number; ppid: number; user: string; cpu: number; mem: number; command: string; state: string; threads: number; runtime: number }[],
|
|
476
476
|
contentWidth: number,
|
|
477
|
-
borderAccent = THEMES.default.border
|
|
477
|
+
borderAccent = THEMES.default.border,
|
|
478
|
+
selectedIdx: number = -1
|
|
478
479
|
): string[] {
|
|
479
480
|
const pidW = 8, ppidW = 8, userW = 12, cpuW = 8, memW = 8, stateW = 8, thrW = 6, rtW = 10;
|
|
480
481
|
|
|
@@ -490,13 +491,14 @@ function processTableLines(
|
|
|
490
491
|
chalk.bold.hex('#f8f8f2').bgHex('#44475a')(' RUNTIME ') +
|
|
491
492
|
chalk.bold.hex('#f8f8f2').bgHex('#44475a')(' COMMAND'.padEnd(cmdW + 1));
|
|
492
493
|
|
|
493
|
-
const rows = processes.map(p => {
|
|
494
|
+
const rows = processes.map((p, idx) => {
|
|
495
|
+
const isSelected = idx === selectedIdx;
|
|
494
496
|
const cpuStr = pctColor(p.cpu)(`${p.cpu.toFixed(1)}`.padEnd(cpuW));
|
|
495
497
|
const memStr = pctColor(p.mem)(`${p.mem.toFixed(1)}`.padEnd(memW));
|
|
496
498
|
const stateStr = chalk.dim(p.state.padEnd(stateW));
|
|
497
499
|
const thrStr = String(p.threads).padEnd(thrW);
|
|
498
500
|
const rtStr = formatRuntime(p.runtime).padEnd(rtW);
|
|
499
|
-
|
|
501
|
+
const line = (
|
|
500
502
|
String(p.pid).padEnd(pidW) +
|
|
501
503
|
String(p.ppid).padEnd(ppidW) +
|
|
502
504
|
truncatePlain(p.user, userW - 1).padEnd(userW) +
|
|
@@ -507,6 +509,7 @@ function processTableLines(
|
|
|
507
509
|
rtStr +
|
|
508
510
|
truncatePlain(p.command, cmdW)
|
|
509
511
|
);
|
|
512
|
+
return isSelected ? chalk.bgCyan.black(line) : line;
|
|
510
513
|
});
|
|
511
514
|
return [head, borderAccent('─'.repeat(contentWidth)), ...rows];
|
|
512
515
|
}
|
|
@@ -605,7 +608,7 @@ function diskTableLines(
|
|
|
605
608
|
|
|
606
609
|
// --- main dashboard ------------------------------------------------------------
|
|
607
610
|
|
|
608
|
-
function detailPanelTitle(panel: string): string {
|
|
611
|
+
function detailPanelTitle(panel: string, trackedPid?: number | null): string {
|
|
609
612
|
const map: Record<string, string> = {
|
|
610
613
|
cpu: 'CPU',
|
|
611
614
|
mem: 'Memory',
|
|
@@ -617,7 +620,7 @@ function detailPanelTitle(panel: string): string {
|
|
|
617
620
|
packets: 'Packets',
|
|
618
621
|
tasks: 'Tasks',
|
|
619
622
|
disk: 'Disk',
|
|
620
|
-
process: 'Processes',
|
|
623
|
+
process: trackedPid !== undefined && trackedPid !== null ? `Processes (Following PID ${trackedPid})` : 'Processes',
|
|
621
624
|
};
|
|
622
625
|
return map[panel] || panel;
|
|
623
626
|
}
|
|
@@ -681,11 +684,51 @@ function activeDetailLines(data: StatsData, activePanel: string, width: number,
|
|
|
681
684
|
? data.processes.filter(p => p.command.toLowerCase().includes(opts.filter!.toLowerCase()))
|
|
682
685
|
: data.processes;
|
|
683
686
|
const sorted = sortProcesses(filtered, opts.sortBy ?? 'cpu');
|
|
684
|
-
|
|
687
|
+
let selectedIdx = opts.selectedProcessIndex ?? -1;
|
|
688
|
+
const isTracking = opts.trackedPid !== undefined && opts.trackedPid !== null;
|
|
689
|
+
if (isTracking) {
|
|
690
|
+
const trackedIdx = sorted.findIndex(p => p.pid === opts.trackedPid);
|
|
691
|
+
if (trackedIdx >= 0) {
|
|
692
|
+
selectedIdx = trackedIdx;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Build tracking info header lines if we're following a process
|
|
697
|
+
const trackingLines: string[] = [];
|
|
698
|
+
if (isTracking) {
|
|
699
|
+
const tp = sorted.find(p => p.pid === opts.trackedPid);
|
|
700
|
+
if (tp) {
|
|
701
|
+
const cpuColor = tp.cpu > 50 ? chalk.red.bold : tp.cpu > 20 ? chalk.yellow : chalk.green;
|
|
702
|
+
const memColor = tp.mem > 50 ? chalk.red.bold : tp.mem > 20 ? chalk.yellow : chalk.green;
|
|
703
|
+
trackingLines.push(chalk.bgHex('#1a1a2e').hex('#e94560').bold(` ◉ FOLLOWING PID ${tp.pid} — ${truncatePlain(tp.command, Math.max(10, contentWidth - 30))} `));
|
|
704
|
+
trackingLines.push(
|
|
705
|
+
` ${chalk.bold('CPU:')} ${cpuColor(tp.cpu.toFixed(1) + '%')} ` +
|
|
706
|
+
`${chalk.bold('MEM:')} ${memColor(tp.mem.toFixed(1) + '%')} ` +
|
|
707
|
+
`${chalk.bold('State:')} ${chalk.dim(tp.state)} ` +
|
|
708
|
+
`${chalk.bold('Threads:')} ${tp.threads} ` +
|
|
709
|
+
`${chalk.bold('Runtime:')} ${formatRuntime(tp.runtime)} ` +
|
|
710
|
+
`${chalk.bold('User:')} ${tp.user}`
|
|
711
|
+
);
|
|
712
|
+
trackingLines.push(chalk.dim(` Full: ${tp.command}`));
|
|
713
|
+
trackingLines.push(theme.border('─'.repeat(contentWidth)));
|
|
714
|
+
} else {
|
|
715
|
+
trackingLines.push(chalk.yellow(` ◉ PID ${opts.trackedPid} exited — no longer in process list`));
|
|
716
|
+
trackingLines.push(theme.border('─'.repeat(contentWidth)));
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const availableRows = Math.max(1, (opts.processLimit || sorted.length) - trackingLines.length);
|
|
721
|
+
let offset = 0;
|
|
722
|
+
if (selectedIdx >= 0) {
|
|
723
|
+
if (selectedIdx >= availableRows) {
|
|
724
|
+
offset = selectedIdx - availableRows + 1;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
const visible = sorted.slice(offset, offset + availableRows);
|
|
685
728
|
if (opts.treeView) {
|
|
686
|
-
return buildProcessTree(
|
|
729
|
+
return [...trackingLines, ...buildProcessTree(visible)];
|
|
687
730
|
}
|
|
688
|
-
return processTableLines(
|
|
731
|
+
return [...trackingLines, ...processTableLines(visible, width - 4, theme.border, selectedIdx - offset)];
|
|
689
732
|
}
|
|
690
733
|
case 'anomalies': {
|
|
691
734
|
if (!opts.anomalyHistory || opts.anomalyHistory.length === 0) return ['No anomalies detected yet.'];
|
|
@@ -854,9 +897,48 @@ function btopProcBox(data: StatsData, width: number, targetHeight: number, theme
|
|
|
854
897
|
|
|
855
898
|
const headerRowOverhead = 2; // top & bottom border
|
|
856
899
|
const tableHeaderOverhead = 2; // header + separator line
|
|
857
|
-
const availableRows = Math.max(1, targetHeight - headerRowOverhead - tableHeaderOverhead);
|
|
858
900
|
|
|
859
|
-
const
|
|
901
|
+
const isTracking = opts.trackedPid !== undefined && opts.trackedPid !== null;
|
|
902
|
+
|
|
903
|
+
// Build compact tracking info lines
|
|
904
|
+
const trackingLines: string[] = [];
|
|
905
|
+
if (isTracking) {
|
|
906
|
+
const tp = sorted.find(p => p.pid === opts.trackedPid);
|
|
907
|
+
if (tp) {
|
|
908
|
+
const cpuColor = tp.cpu > 50 ? chalk.red.bold : tp.cpu > 20 ? chalk.yellow : chalk.green;
|
|
909
|
+
const memColor = tp.mem > 50 ? chalk.red.bold : tp.mem > 20 ? chalk.yellow : chalk.green;
|
|
910
|
+
trackingLines.push(chalk.bgHex('#1a1a2e').hex('#e94560').bold(` ◉ PID ${tp.pid} ${truncatePlain(tp.command, Math.max(6, contentWidth - 20))} `));
|
|
911
|
+
trackingLines.push(
|
|
912
|
+
` ${cpuColor('CPU ' + tp.cpu.toFixed(1) + '%')} ` +
|
|
913
|
+
`${memColor('MEM ' + tp.mem.toFixed(1) + '%')} ` +
|
|
914
|
+
`${chalk.dim(tp.state)} ` +
|
|
915
|
+
`THR:${tp.threads} ` +
|
|
916
|
+
`${formatRuntime(tp.runtime)}`
|
|
917
|
+
);
|
|
918
|
+
trackingLines.push(theme.border('─'.repeat(contentWidth)));
|
|
919
|
+
} else {
|
|
920
|
+
trackingLines.push(chalk.yellow(` ◉ PID ${opts.trackedPid} exited`));
|
|
921
|
+
trackingLines.push(theme.border('─'.repeat(contentWidth)));
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
const availableRows = Math.max(1, targetHeight - headerRowOverhead - tableHeaderOverhead - trackingLines.length);
|
|
926
|
+
|
|
927
|
+
let selectedIdx = opts.selectedProcessIndex ?? -1;
|
|
928
|
+
if (isTracking) {
|
|
929
|
+
const trackedIdx = sorted.findIndex(p => p.pid === opts.trackedPid);
|
|
930
|
+
if (trackedIdx >= 0) {
|
|
931
|
+
selectedIdx = trackedIdx;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
let offset = 0;
|
|
936
|
+
if (selectedIdx >= 0) {
|
|
937
|
+
if (selectedIdx >= availableRows) {
|
|
938
|
+
offset = selectedIdx - availableRows + 1;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
const visible = sorted.slice(offset, offset + availableRows);
|
|
860
942
|
|
|
861
943
|
const pidW = 6, userW = 7, thrW = 3, memW = 6, cpuW = 6;
|
|
862
944
|
const fixedW = pidW + userW + thrW + memW + cpuW + 5;
|
|
@@ -871,12 +953,14 @@ function btopProcBox(data: StatsData, width: number, targetHeight: number, theme
|
|
|
871
953
|
chalk.bold('CPU%'.padStart(cpuW));
|
|
872
954
|
|
|
873
955
|
if (opts.treeView) {
|
|
874
|
-
const treeLines = buildProcessTree(
|
|
875
|
-
|
|
876
|
-
|
|
956
|
+
const treeLines = buildProcessTree(visible);
|
|
957
|
+
let rightTitle = opts.filter ? `filter: "${opts.filter}"` : `tree · sort: ${opts.sortBy || 'cpu'}`;
|
|
958
|
+
if (isTracking) rightTitle += ` · following PID ${opts.trackedPid}`;
|
|
959
|
+
return panel('P proc', [...trackingLines, head, theme.border('─'.repeat(contentWidth)), ...treeLines], width, theme.process, theme.border, targetHeight - 2, rightTitle);
|
|
877
960
|
}
|
|
878
961
|
|
|
879
|
-
const rows =
|
|
962
|
+
const rows = visible.map((p, idx) => {
|
|
963
|
+
const isSelected = (offset + idx) === selectedIdx;
|
|
880
964
|
const cpuVal = p.cpu.toFixed(1) + '%';
|
|
881
965
|
const cpuStr = p.cpu > 50 ? chalk.red.bold(cpuVal.padStart(cpuW)) : p.cpu > 20 ? chalk.yellow(cpuVal.padStart(cpuW)) : chalk.green(cpuVal.padStart(cpuW));
|
|
882
966
|
const memStr = (p.mem.toFixed(1) + '%').padStart(memW);
|
|
@@ -885,15 +969,19 @@ function btopProcBox(data: StatsData, width: number, targetHeight: number, theme
|
|
|
885
969
|
const pidStr = String(p.pid).padEnd(pidW);
|
|
886
970
|
const cmdStr = truncatePlain(p.command, cmdW).padEnd(cmdW);
|
|
887
971
|
|
|
888
|
-
|
|
972
|
+
const line = `${pidStr} ${cmdStr} ${userStr} ${thrStr} ${memStr} ${cpuStr}`;
|
|
973
|
+
return isSelected ? chalk.bgCyan.black(line) : line;
|
|
889
974
|
});
|
|
890
975
|
|
|
891
|
-
const lines = [head, theme.border('─'.repeat(contentWidth)), ...rows];
|
|
976
|
+
const lines = [...trackingLines, head, theme.border('─'.repeat(contentWidth)), ...rows];
|
|
892
977
|
if (rows.length === 0) {
|
|
893
978
|
lines.push(chalk.dim(`No processes match "${opts.filter || ''}"`));
|
|
894
979
|
}
|
|
895
980
|
|
|
896
|
-
|
|
981
|
+
let rightTitle = opts.filter ? `filter: "${opts.filter}"` : `sort: ${opts.sortBy || 'cpu'}`;
|
|
982
|
+
if (isTracking) {
|
|
983
|
+
rightTitle += ` · following PID ${opts.trackedPid}`;
|
|
984
|
+
}
|
|
897
985
|
return panel('P proc', lines, width, theme.process, theme.border, targetHeight - 2, rightTitle);
|
|
898
986
|
}
|
|
899
987
|
|
|
@@ -920,33 +1008,34 @@ export function formatTable(data: StatsData, opts: TableOptions = {}): string {
|
|
|
920
1008
|
if (activePanel !== 'grid') {
|
|
921
1009
|
const lines = activeDetailLines(data, activePanel, width, opts);
|
|
922
1010
|
if (lines) {
|
|
923
|
-
out.push(...panel(detailPanelTitle(activePanel), lines, width, theme[activePanel as keyof ThemeColors] ?? theme.cpu, theme.border));
|
|
1011
|
+
out.push(...panel(detailPanelTitle(activePanel, opts.trackedPid), lines, width, theme[activePanel as keyof ThemeColors] ?? theme.cpu, theme.border));
|
|
924
1012
|
}
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
out.push(...btopCpuBox(data, width, theme, opts));
|
|
931
|
-
out.push('');
|
|
1013
|
+
} else {
|
|
1014
|
+
// --- btop Dashboard Grid ---
|
|
1015
|
+
// 1. Top CPU & System Overview Box
|
|
1016
|
+
out.push(...btopCpuBox(data, width, theme, opts));
|
|
1017
|
+
out.push('');
|
|
932
1018
|
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
1019
|
+
// 2. Bottom Split Section
|
|
1020
|
+
if (width >= 85) {
|
|
1021
|
+
const leftColW = Math.max(32, Math.floor(width * 0.38));
|
|
1022
|
+
const rightColW = width - leftColW - 2;
|
|
937
1023
|
|
|
938
|
-
|
|
939
|
-
|
|
1024
|
+
const leftLines = btopLeftColumn(data, leftColW, theme, opts);
|
|
1025
|
+
const procLines = btopProcBox(data, rightColW, leftLines.length, theme, opts);
|
|
940
1026
|
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
1027
|
+
out.push(...hstack([leftLines, procLines], 2, width));
|
|
1028
|
+
} else {
|
|
1029
|
+
const leftLines = btopLeftColumn(data, width, theme, opts);
|
|
1030
|
+
out.push(...leftLines);
|
|
1031
|
+
out.push('');
|
|
1032
|
+
const procLines = btopProcBox(data, width, 12, theme, opts);
|
|
1033
|
+
out.push(...procLines);
|
|
1034
|
+
}
|
|
948
1035
|
}
|
|
949
1036
|
|
|
1037
|
+
// Inspect process modal has been replaced by inline tracking headers
|
|
1038
|
+
|
|
950
1039
|
return out.join('\n');
|
|
951
1040
|
}
|
|
952
1041
|
|
package/src/formatters/types.ts
CHANGED
|
@@ -80,4 +80,10 @@ export interface TableOptions {
|
|
|
80
80
|
graphMode?: 'spark' | 'bar';
|
|
81
81
|
/** Panel layout order. */
|
|
82
82
|
panelLayout?: string[];
|
|
83
|
+
/** Currently selected process index for highlight. */
|
|
84
|
+
selectedProcessIndex?: number;
|
|
85
|
+
/** Currently tracked PID. */
|
|
86
|
+
trackedPid?: number | null;
|
|
87
|
+
/** Currently inspected process details for modal overlay. */
|
|
88
|
+
inspectProcess?: any;
|
|
83
89
|
}
|
package/src/live/input.ts
CHANGED
|
@@ -10,7 +10,7 @@ import readline from 'node:readline';
|
|
|
10
10
|
import chalk from 'chalk';
|
|
11
11
|
import { collectAll, StatsData } from '../monitors/index.js';
|
|
12
12
|
import { THEMES, type ThemeName, type VisibleItems, getTabHitboxes, TAB_BAR_ROW } from '../formatters/index.js';
|
|
13
|
-
import { state, setStatus, getToggleKey, type SplashColorScheme, type SplashAnimation } from './state.js';
|
|
13
|
+
import { state, setStatus, getToggleKey, type SplashColorScheme, type SplashAnimation, MENU_OPTIONS } from './state.js';
|
|
14
14
|
import { exportSnapshot, startLogging, stopLogging, toggleLogging, writeLogRow } from './export.js';
|
|
15
15
|
import { render, footerLine, checkAlerts, invalidateTableCache, invalidateFrame } from './render.js';
|
|
16
16
|
import type { LiveOptions, ExportFormat, InputMode, SortMode, GraphMode, ActivePanel } from './types.js';
|
|
@@ -115,6 +115,70 @@ function handleInputModeKey(str: string, key: readline.Key) {
|
|
|
115
115
|
return;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
if (state.inputMode === 'menu') {
|
|
119
|
+
if (isEscKey(key, str)) {
|
|
120
|
+
state.inputMode = null;
|
|
121
|
+
render();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (isUpKey(key, str)) {
|
|
125
|
+
state.menuSelectionIndex = (state.menuSelectionIndex - 1 + MENU_OPTIONS.length) % MENU_OPTIONS.length;
|
|
126
|
+
render();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (isDownKey(key, str)) {
|
|
130
|
+
state.menuSelectionIndex = (state.menuSelectionIndex + 1) % MENU_OPTIONS.length;
|
|
131
|
+
render();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (isEnterKey(key, str) || str === ' ') {
|
|
135
|
+
const option = MENU_OPTIONS[state.menuSelectionIndex];
|
|
136
|
+
switch (option) {
|
|
137
|
+
case 'Resume Dashboard':
|
|
138
|
+
state.inputMode = null;
|
|
139
|
+
break;
|
|
140
|
+
case 'Settings (Customizer)':
|
|
141
|
+
state.inputMode = 'customizer';
|
|
142
|
+
state.customizerIndex = 0;
|
|
143
|
+
break;
|
|
144
|
+
case 'Readme':
|
|
145
|
+
state.inputMode = 'readme';
|
|
146
|
+
break;
|
|
147
|
+
case 'Credits':
|
|
148
|
+
state.inputMode = 'credits';
|
|
149
|
+
break;
|
|
150
|
+
case 'Quit pyre':
|
|
151
|
+
process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
|
|
152
|
+
process.exit(0);
|
|
153
|
+
}
|
|
154
|
+
render();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
render();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (state.inputMode === 'readme' || state.inputMode === 'credits') {
|
|
162
|
+
if (isEscKey(key, str) || isEnterKey(key, str)) {
|
|
163
|
+
state.inputMode = 'menu';
|
|
164
|
+
render();
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (state.inputMode === 'readme') {
|
|
168
|
+
if (isUpKey(key, str)) {
|
|
169
|
+
state.readmeScrollOffset = Math.max(0, state.readmeScrollOffset - 1);
|
|
170
|
+
render();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (isDownKey(key, str)) {
|
|
174
|
+
state.readmeScrollOffset += 1;
|
|
175
|
+
render();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
118
182
|
if (state.inputMode === 'signal') {
|
|
119
183
|
if (isUpKey(key, str)) {
|
|
120
184
|
const idx = state.SIGNAL_OPTIONS.indexOf(state.inputBuffer as typeof state.SIGNAL_OPTIONS[number]);
|
|
@@ -569,11 +633,72 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
|
|
|
569
633
|
case 'tab':
|
|
570
634
|
cycleTab(1);
|
|
571
635
|
break;
|
|
636
|
+
case 'up':
|
|
637
|
+
if (state.lastData?.processes?.length) {
|
|
638
|
+
state.trackedPid = null;
|
|
639
|
+
state.processSelectionIndex = Math.max(0, state.processSelectionIndex - 1);
|
|
640
|
+
render();
|
|
641
|
+
}
|
|
642
|
+
break;
|
|
643
|
+
case 'down':
|
|
644
|
+
if (state.lastData?.processes?.length) {
|
|
645
|
+
state.trackedPid = null;
|
|
646
|
+
const count = state.lastData.processes.length;
|
|
647
|
+
state.processSelectionIndex = Math.min(count - 1, state.processSelectionIndex + 1);
|
|
648
|
+
render();
|
|
649
|
+
}
|
|
650
|
+
break;
|
|
651
|
+
case 'space':
|
|
652
|
+
case ' ':
|
|
653
|
+
if (state.lastData?.processes?.length && state.processSelectionIndex >= 0) {
|
|
654
|
+
const sorted = state.lastData.processes;
|
|
655
|
+
const proc = sorted[state.processSelectionIndex];
|
|
656
|
+
if (proc) {
|
|
657
|
+
if (state.trackedPid === proc.pid) {
|
|
658
|
+
state.trackedPid = null;
|
|
659
|
+
setStatus(`Stopped following PID ${proc.pid}`);
|
|
660
|
+
} else {
|
|
661
|
+
state.trackedPid = proc.pid;
|
|
662
|
+
setStatus(`Following PID ${proc.pid} (${proc.command})`);
|
|
663
|
+
}
|
|
664
|
+
render();
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
break;
|
|
668
|
+
case 'return':
|
|
669
|
+
case 'enter':
|
|
670
|
+
if (state.inspectingProcess) {
|
|
671
|
+
state.inspectingProcess = null;
|
|
672
|
+
render();
|
|
673
|
+
} else if (state.lastData?.processes?.length && state.processSelectionIndex >= 0) {
|
|
674
|
+
const sorted = state.lastData.processes;
|
|
675
|
+
const proc = sorted[state.processSelectionIndex];
|
|
676
|
+
if (proc) {
|
|
677
|
+
state.inspectingProcess = proc;
|
|
678
|
+
state.trackedPid = proc.pid;
|
|
679
|
+
setStatus(`Inspecting & following PID ${proc.pid}`);
|
|
680
|
+
render();
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
break;
|
|
572
684
|
case 'escape':
|
|
573
685
|
case 'esc':
|
|
574
|
-
state.
|
|
575
|
-
|
|
576
|
-
|
|
686
|
+
if (state.inspectingProcess) {
|
|
687
|
+
state.inspectingProcess = null;
|
|
688
|
+
render();
|
|
689
|
+
} else if (state.trackedPid !== null) {
|
|
690
|
+
state.trackedPid = null;
|
|
691
|
+
setStatus('Stopped following process');
|
|
692
|
+
render();
|
|
693
|
+
} else if (state.activePanel !== 'grid') {
|
|
694
|
+
state.activePanel = 'grid';
|
|
695
|
+
setStatus('Grid view');
|
|
696
|
+
render();
|
|
697
|
+
} else {
|
|
698
|
+
state.inputMode = 'menu';
|
|
699
|
+
state.menuSelectionIndex = 0;
|
|
700
|
+
render();
|
|
701
|
+
}
|
|
577
702
|
break;
|
|
578
703
|
default:
|
|
579
704
|
if (str && str.length === 1) {
|
|
@@ -592,7 +717,7 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
|
|
|
592
717
|
}
|
|
593
718
|
|
|
594
719
|
function tabKeyToId(str: string): string | null {
|
|
595
|
-
const map: Record<string, string> = { '1': 'cpu', '2': 'mem', '3': 'gpu', '4': 'power', '5': 'battery', '6': 'thermal', '7': 'network', '8': 'packets', '9': 'tasks', '0': 'disk', 'p': 'process', 'r': 'p2p' };
|
|
720
|
+
const map: Record<string, string> = { '1': 'cpu', '2': 'mem', '3': 'gpu', '4': 'power', '5': 'battery', '6': 'thermal', '7': 'network', '8': 'packets', '9': 'tasks', '0': 'disk', 'p': 'process', 'r': 'p2p', 'A': 'anomalies' };
|
|
596
721
|
return map[str] ?? null;
|
|
597
722
|
}
|
|
598
723
|
|
|
@@ -612,6 +737,12 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
|
|
|
612
737
|
const cy = seq.charCodeAt(5) - 32;
|
|
613
738
|
if (cb === 0 || cb === 1) {
|
|
614
739
|
handleMouseClick(cy, cx);
|
|
740
|
+
} else if (cb === 64) {
|
|
741
|
+
// scroll up
|
|
742
|
+
scrollProcessSelection(-1);
|
|
743
|
+
} else if (cb === 65) {
|
|
744
|
+
// scroll down
|
|
745
|
+
scrollProcessSelection(1);
|
|
615
746
|
}
|
|
616
747
|
}
|
|
617
748
|
|
|
@@ -623,6 +754,19 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
|
|
|
623
754
|
const cy = parseInt(match[3]) - 1;
|
|
624
755
|
if (button === 0 || button === 1) {
|
|
625
756
|
handleMouseClick(cy, cx);
|
|
757
|
+
} else if (button === 64) {
|
|
758
|
+
scrollProcessSelection(-1);
|
|
759
|
+
} else if (button === 65) {
|
|
760
|
+
scrollProcessSelection(1);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function scrollProcessSelection(dir: number) {
|
|
765
|
+
if (state.lastData?.processes?.length) {
|
|
766
|
+
state.trackedPid = null;
|
|
767
|
+
const count = state.lastData.processes.length;
|
|
768
|
+
state.processSelectionIndex = Math.max(0, Math.min(count - 1, state.processSelectionIndex + dir));
|
|
769
|
+
render();
|
|
626
770
|
}
|
|
627
771
|
}
|
|
628
772
|
|
package/src/live/render.ts
CHANGED
|
@@ -7,10 +7,13 @@
|
|
|
7
7
|
* `state.ts`.
|
|
8
8
|
*/
|
|
9
9
|
import { execFile, exec } from 'node:child_process';
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
10
13
|
import chalk from 'chalk';
|
|
11
|
-
import { formatTable, formatGraphs, gridColumns, THEMES, type ThemeName, panel, fitVisible } from '../formatters/index.js';
|
|
14
|
+
import { formatTable, formatGraphs, gridColumns, THEMES, type ThemeName, panel, fitVisible, visLen } from '../formatters/index.js';
|
|
12
15
|
import { detectAnomalies } from '../anomalies.js';
|
|
13
|
-
import { state, setStatus, SIGNAL_OPTIONS, getToggleKey } from './state.js';
|
|
16
|
+
import { state, setStatus, SIGNAL_OPTIONS, getToggleKey, MENU_OPTIONS } from './state.js';
|
|
14
17
|
import type { StatsData } from '../monitors/index.js';
|
|
15
18
|
|
|
16
19
|
function formatTempForUnit(c: number): string {
|
|
@@ -48,6 +51,9 @@ function tableCacheParams(): string {
|
|
|
48
51
|
return [
|
|
49
52
|
state.sortMode,
|
|
50
53
|
state.processFilter || '',
|
|
54
|
+
state.processSelectionIndex,
|
|
55
|
+
state.trackedPid || '',
|
|
56
|
+
state.inspectingProcess ? state.inspectingProcess.pid : '',
|
|
51
57
|
limit,
|
|
52
58
|
state.currentTheme,
|
|
53
59
|
state.activePanel,
|
|
@@ -197,13 +203,41 @@ function render() {
|
|
|
197
203
|
writeFrame(out);
|
|
198
204
|
return;
|
|
199
205
|
}
|
|
206
|
+
|
|
207
|
+
if (state.inputMode === 'menu') {
|
|
208
|
+
writeFrame(renderSplashMenu(cols, rows));
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (state.inputMode === 'readme') {
|
|
212
|
+
writeFrame(renderReadme(cols, rows));
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (state.inputMode === 'credits') {
|
|
216
|
+
writeFrame(renderCredits(cols, rows));
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
200
219
|
const lines: string[] = [];
|
|
201
220
|
const theme = THEMES[state.currentTheme] || THEMES.default;
|
|
202
221
|
|
|
203
222
|
const params = tableCacheParams();
|
|
204
223
|
const anomalies = detectAnomalies(state.lastData, state.history);
|
|
224
|
+
if (anomalies.length > 0) {
|
|
225
|
+
for (const a of anomalies) {
|
|
226
|
+
const now = new Date();
|
|
227
|
+
const alertWithTime = { ...a, timestamp: now };
|
|
228
|
+
const exists = state.anomalyHistory.some(
|
|
229
|
+
h => h.metric === a.metric && Math.abs(h.zScore - a.zScore) < 0.1 && (now.getTime() - (h.timestamp?.getTime() || 0)) < 10000
|
|
230
|
+
);
|
|
231
|
+
if (!exists) {
|
|
232
|
+
state.anomalyHistory.unshift(alertWithTime);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (state.anomalyHistory.length > 100) {
|
|
236
|
+
state.anomalyHistory.length = 100;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
205
239
|
const anomalyCacheKey = anomalies.map(a => `${a.metric}:${a.zScore.toFixed(2)}`).join(',');
|
|
206
|
-
const cacheParams = params + '|' + anomalyCacheKey;
|
|
240
|
+
const cacheParams = params + '|' + anomalyCacheKey + '|' + state.anomalyHistory.length;
|
|
207
241
|
const tableStr = _tableDataDirty || state.lastData !== _cachedTableData || cacheParams !== _cachedTableParams
|
|
208
242
|
? (() => {
|
|
209
243
|
const out = formatTable(state.lastData, {
|
|
@@ -221,6 +255,9 @@ function render() {
|
|
|
221
255
|
history: state.history,
|
|
222
256
|
graphMode: state.graphMode,
|
|
223
257
|
panelLayout: state.panelLayout,
|
|
258
|
+
selectedProcessIndex: state.processSelectionIndex,
|
|
259
|
+
trackedPid: state.trackedPid,
|
|
260
|
+
inspectProcess: state.inspectingProcess,
|
|
224
261
|
});
|
|
225
262
|
_cachedTableData = state.lastData;
|
|
226
263
|
_cachedTableParams = cacheParams;
|
|
@@ -422,4 +459,109 @@ function checkAlerts(data: StatsData) {
|
|
|
422
459
|
}
|
|
423
460
|
}
|
|
424
461
|
|
|
462
|
+
function renderSplashMenu(cols: number, rows: number): string[] {
|
|
463
|
+
const out: string[] = [];
|
|
464
|
+
const contentLines: string[] = [];
|
|
465
|
+
const logo = [
|
|
466
|
+
chalk.hex('#ff6004').bold(' ██████╗ ██╗ ██╗██████╗ ███████╗'),
|
|
467
|
+
chalk.hex('#ff8004').bold(' ██╔══██╗╚██╗ ██╔╝██╔══██╗██╔════╝'),
|
|
468
|
+
chalk.hex('#ffa004').bold(' ██████╔╝ ╚████╔╝ ██████╔╝█████╗ '),
|
|
469
|
+
chalk.hex('#ffc004').bold(' ██╔═══╝ ╚██╔╝ ██╔══██╗██╔══╝ '),
|
|
470
|
+
chalk.hex('#ffe004').bold(' ██║ ██║ ██║ ██║███████╗'),
|
|
471
|
+
chalk.hex('#ffff40').bold(' ╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝')
|
|
472
|
+
];
|
|
473
|
+
|
|
474
|
+
const menuTop = Math.max(0, Math.floor(rows / 2) - 8);
|
|
475
|
+
for (let i = 0; i < menuTop; i++) out.push('');
|
|
476
|
+
|
|
477
|
+
const strip = (s: string) => s.replace(/^(\x1b\[[0-9;]*m)+( +)/, '$1');
|
|
478
|
+
for (const line of logo) contentLines.push(strip(line));
|
|
479
|
+
contentLines.push('');
|
|
480
|
+
contentLines.push(strip(chalk.dim(' system monitor v9.0.0')));
|
|
481
|
+
contentLines.push('');
|
|
482
|
+
contentLines.push('');
|
|
483
|
+
|
|
484
|
+
for (let i = 0; i < MENU_OPTIONS.length; i++) {
|
|
485
|
+
const isSelected = i === state.menuSelectionIndex;
|
|
486
|
+
const opt = MENU_OPTIONS[i];
|
|
487
|
+
const prefix = isSelected ? chalk.cyan.bold(' ▶ ') : ' ';
|
|
488
|
+
const text = isSelected ? chalk.bgCyan.black(` ${opt} `) : chalk.dim(` ${opt} `);
|
|
489
|
+
contentLines.push(prefix + text);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
contentLines.push('');
|
|
493
|
+
contentLines.push('');
|
|
494
|
+
contentLines.push(strip(chalk.dim(' Use ↑/↓ to navigate, Enter to select')));
|
|
495
|
+
|
|
496
|
+
while (contentLines.length < rows) contentLines.push('');
|
|
497
|
+
const maxWidth = Math.max(...contentLines.map(visLen));
|
|
498
|
+
const padding = Math.max(0, Math.floor((cols - maxWidth) / 2));
|
|
499
|
+
const padStr = ' '.repeat(padding);
|
|
500
|
+
for (const line of contentLines) out.push(padStr + fitVisible(line, cols));
|
|
501
|
+
return out;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
let cachedReadmeLines: string[] | null = null;
|
|
505
|
+
|
|
506
|
+
function getReadmeLines(): string[] {
|
|
507
|
+
if (cachedReadmeLines) return cachedReadmeLines;
|
|
508
|
+
try {
|
|
509
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
510
|
+
const __dirname = path.dirname(__filename);
|
|
511
|
+
const rootDir = path.resolve(__dirname, '..', '..');
|
|
512
|
+
const readmePath = path.join(rootDir, 'README.md');
|
|
513
|
+
const content = fs.readFileSync(readmePath, 'utf8');
|
|
514
|
+
cachedReadmeLines = content.split('\n');
|
|
515
|
+
return cachedReadmeLines;
|
|
516
|
+
} catch (err) {
|
|
517
|
+
return [chalk.red('Failed to load README.md')];
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function renderReadme(cols: number, rows: number): string[] {
|
|
522
|
+
const lines = getReadmeLines();
|
|
523
|
+
const availableLines = Math.max(1, rows - 2); // Body height of the panel
|
|
524
|
+
|
|
525
|
+
const startIdx = Math.min(state.readmeScrollOffset, Math.max(0, lines.length - availableLines));
|
|
526
|
+
state.readmeScrollOffset = startIdx; // Clamp the state value
|
|
527
|
+
|
|
528
|
+
const content = lines.slice(startIdx, startIdx + availableLines).map(l => {
|
|
529
|
+
let out = l;
|
|
530
|
+
if (out.startsWith('#')) {
|
|
531
|
+
out = chalk.cyan.bold(out);
|
|
532
|
+
} else if (out.startsWith('```')) {
|
|
533
|
+
out = chalk.dim(out);
|
|
534
|
+
} else if (out.startsWith('- ') || out.startsWith('* ')) {
|
|
535
|
+
out = ' ' + chalk.yellow(out.substring(0, 2)) + out.substring(2);
|
|
536
|
+
}
|
|
537
|
+
return out;
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
const out: string[] = [];
|
|
541
|
+
const pct = Math.round((startIdx / Math.max(1, lines.length - availableLines)) * 100);
|
|
542
|
+
const title = lines.length > availableLines
|
|
543
|
+
? `README (${pct}%)`
|
|
544
|
+
: 'README';
|
|
545
|
+
out.push(...panel(title, content, cols, chalk.cyan, chalk.dim, availableLines));
|
|
546
|
+
return out;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function renderCredits(cols: number, rows: number): string[] {
|
|
550
|
+
const out: string[] = [];
|
|
551
|
+
const content = [
|
|
552
|
+
chalk.bold.hex('#ff6004')('Pyre - The Terminal System Monitor'),
|
|
553
|
+
'',
|
|
554
|
+
'Created with ❤️ using Node.js',
|
|
555
|
+
'',
|
|
556
|
+
chalk.cyan('Libraries used:'),
|
|
557
|
+
' - chalk (Terminal styling)',
|
|
558
|
+
' - systeminformation (System metrics)',
|
|
559
|
+
'',
|
|
560
|
+
chalk.dim('Press Esc or Enter to return to menu.')
|
|
561
|
+
];
|
|
562
|
+
|
|
563
|
+
out.push(...panel('CREDITS', content, cols, chalk.magenta, chalk.dim, rows - 2));
|
|
564
|
+
return out;
|
|
565
|
+
}
|
|
566
|
+
|
|
425
567
|
export { render, footerLine, renderCustomizerOverlay, checkAlerts, processRowBudget, invalidateTableCache, invalidateFrame };
|