metwatch 0.1.0 → 0.2.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.
@@ -1,332 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // Ambient declarations for blessed and blessed-contrib
3
- //
4
- // We use a local declaration file instead of @types/blessed because:
5
- // 1. @types/blessed (0.1.20, 2019) predates many blessed APIs and conflicts
6
- // with Bun's bundler module resolution.
7
- // 2. We only need the subset of the API that MetWatch actually uses.
8
- // 3. This file is authoritative and won't drift when npm packages change.
9
- //
10
- // Add declarations here as new blessed features are consumed.
11
- // ---------------------------------------------------------------------------
12
-
13
- declare module 'blessed' {
14
- import { EventEmitter } from 'events';
15
-
16
- // ── Style objects ─────────────────────────────────────────────────────────
17
-
18
- interface BorderStyle {
19
- type?: 'line' | 'bg';
20
- fg?: string;
21
- bg?: string;
22
- }
23
-
24
- interface ScrollbarStyle {
25
- ch?: string;
26
- fg?: string;
27
- bg?: string;
28
- track?: { bg?: string; fg?: string };
29
- }
30
-
31
- interface ElementStyle {
32
- fg?: string;
33
- bg?: string;
34
- bold?: boolean;
35
- underline?: boolean;
36
- border?: BorderStyle;
37
- scrollbar?: ScrollbarStyle;
38
- label?: Partial<ElementStyle>;
39
- focus?: Partial<ElementStyle>;
40
- hover?: Partial<ElementStyle>;
41
- selected?: Partial<ElementStyle>;
42
- item?: Partial<ElementStyle>;
43
- header?: Partial<ElementStyle>;
44
- cell?: Partial<ElementStyle>;
45
- bar?: Partial<ElementStyle>;
46
- }
47
-
48
- // ── Options ───────────────────────────────────────────────────────────────
49
-
50
- interface ScreenOptions {
51
- smartCSR?: boolean;
52
- fastCSR?: boolean;
53
- fullUnicode?: boolean;
54
- dockBorders?: boolean;
55
- title?: string;
56
- debug?: boolean;
57
- ignoreLocked?: string[];
58
- autoPadding?: boolean;
59
- }
60
-
61
- interface ElementOptions {
62
- parent?: BlessedElement | BlessedScreen;
63
- top?: number | string;
64
- left?: number | string;
65
- width?: number | string;
66
- height?: number | string;
67
- right?: number | string;
68
- bottom?: number | string;
69
- content?: string;
70
- label?: string;
71
- tags?: boolean;
72
- border?: BorderStyle | { type: string };
73
- style?: ElementStyle;
74
- scrollable?: boolean;
75
- alwaysScroll?: boolean;
76
- scrollbar?: ScrollbarStyle | boolean;
77
- keys?: boolean;
78
- vi?: boolean;
79
- mouse?: boolean;
80
- hidden?: boolean;
81
- padding?: number | { top?: number; right?: number; bottom?: number; left?: number };
82
- shrink?: boolean;
83
- focusable?: boolean;
84
- input?: boolean;
85
- clickable?: boolean;
86
- interactive?: boolean;
87
- }
88
-
89
- interface BoxOptions extends ElementOptions {}
90
-
91
- interface ListOptions extends ElementOptions {
92
- items?: string[];
93
- invertSelected?: boolean;
94
- }
95
-
96
- interface ListTableOptions extends ElementOptions {
97
- data?: string[][];
98
- rows?: string[][];
99
- pad?: number;
100
- noCellBorders?: boolean;
101
- fillCellBorders?: boolean;
102
- }
103
-
104
- interface LogOptions extends ElementOptions {
105
- scrollback?: number;
106
- scrollOnInput?: boolean;
107
- }
108
-
109
- interface ProgressBarOptions extends ElementOptions {
110
- orientation?: 'horizontal' | 'vertical';
111
- pch?: string;
112
- filled?: number;
113
- value?: number;
114
- }
115
-
116
- interface QuestionOptions extends ElementOptions {}
117
- interface MessageOptions extends ElementOptions {}
118
-
119
- // ── Elements ──────────────────────────────────────────────────────────────
120
-
121
- class BlessedElement extends EventEmitter {
122
- width: number;
123
- height: number;
124
- top: number;
125
- left: number;
126
- content: string;
127
- hidden: boolean;
128
-
129
- setContent(text: string): void;
130
- getContent(): string;
131
- setText(text: string): void;
132
- setLabel(text: string): void;
133
- show(): void;
134
- hide(): void;
135
- toggle(): void;
136
- focus(): void;
137
- render(): void;
138
- destroy(): void;
139
- key(keys: string | string[], handler: (ch: string, key: KeyEvent) => void): void;
140
- onceKey(keys: string | string[], handler: (ch: string, key: KeyEvent) => void): void;
141
- unkey(keys: string | string[], handler: (ch: string, key: KeyEvent) => void): void;
142
- on(event: string, handler: (...args: unknown[]) => void): this;
143
- append(child: BlessedElement): void;
144
- remove(child: BlessedElement): void;
145
- }
146
-
147
- class BlessedScreen extends BlessedElement {
148
- cols: number;
149
- rows: number;
150
- focused: BlessedElement;
151
-
152
- render(): void;
153
- destroy(): void;
154
- key(keys: string | string[], handler: (ch: string, key: KeyEvent) => void): void;
155
- append(child: BlessedElement): void;
156
- remove(child: BlessedElement): void;
157
- }
158
-
159
- class Box extends BlessedElement {
160
- constructor(options?: BoxOptions);
161
- }
162
-
163
- class Log extends BlessedElement {
164
- constructor(options?: LogOptions);
165
- log(text: string): void;
166
- add(text: string): void;
167
- }
168
-
169
- class ListTable extends BlessedElement {
170
- constructor(options?: ListTableOptions);
171
- setData(data: string[][]): void;
172
- setRows(rows: string[][]): void;
173
- }
174
-
175
- class ProgressBar extends BlessedElement {
176
- constructor(options?: ProgressBarOptions);
177
- setProgress(amount: number): void;
178
- progress(amount: number): void;
179
- reset(): void;
180
- }
181
-
182
- class Question extends BlessedElement {
183
- constructor(options?: QuestionOptions);
184
- ask(question: string, callback: (err: Error | null, value: boolean) => void): void;
185
- }
186
-
187
- class Message extends BlessedElement {
188
- constructor(options?: MessageOptions);
189
- display(text: string, time: number, callback?: () => void): void;
190
- error(text: string, time: number, callback?: () => void): void;
191
- }
192
-
193
- // ── Keyboard event ────────────────────────────────────────────────────────
194
-
195
- interface KeyEvent {
196
- name: string;
197
- ctrl: boolean;
198
- meta: boolean;
199
- shift: boolean;
200
- sequence: string;
201
- full: string;
202
- }
203
-
204
- // ── Factory functions ─────────────────────────────────────────────────────
205
-
206
- function screen(options?: ScreenOptions): BlessedScreen;
207
- function box(options?: BoxOptions): Box;
208
- function log(options?: LogOptions): Log;
209
- function listtable(options?: ListTableOptions): ListTable;
210
- function progressbar(options?: ProgressBarOptions): ProgressBar;
211
- function question(options?: QuestionOptions): Question;
212
- function message(options?: MessageOptions): Message;
213
- }
214
-
215
- declare module 'blessed-contrib' {
216
- import type { BlessedElement, BlessedScreen, ElementOptions } from 'blessed';
217
-
218
- // ── Grid ──────────────────────────────────────────────────────────────────
219
-
220
- interface GridOptions {
221
- rows: number;
222
- cols: number;
223
- screen: BlessedScreen;
224
- hideBorder?: boolean;
225
- color?: string;
226
- }
227
-
228
- type WidgetConstructor<T> = new (options: ElementOptions) => T;
229
-
230
- class grid {
231
- constructor(options: GridOptions);
232
- set<T extends BlessedElement>(
233
- row: number,
234
- col: number,
235
- rowSpan: number,
236
- colSpan: number,
237
- widget: WidgetConstructor<T> | ((options: ElementOptions) => T),
238
- options: Record<string, unknown>
239
- ): T;
240
- }
241
-
242
- // ── Gauge ─────────────────────────────────────────────────────────────────
243
-
244
- interface GaugeOptions extends ElementOptions {
245
- label?: string;
246
- stroke?: string;
247
- fill?: string;
248
- percent?: number;
249
- showLabel?: boolean;
250
- }
251
-
252
- class gauge extends BlessedElement {
253
- constructor(options?: GaugeOptions);
254
- setPercent(percent: number): void;
255
- setData(data: { percent: number; label?: string }): void;
256
- }
257
-
258
- // ── GaugeList (stacked gauges) ────────────────────────────────────────────
259
-
260
- interface GaugeListOptions extends ElementOptions {
261
- label?: string;
262
- gauges?: Array<{ percent: number; stroke?: string; label?: string }>;
263
- }
264
-
265
- class gaugeList extends BlessedElement {
266
- constructor(options?: GaugeListOptions);
267
- setGauges(gauges: Array<{ percent: number; stroke?: string; label?: string }>): void;
268
- }
269
-
270
- // ── Table ─────────────────────────────────────────────────────────────────
271
-
272
- interface TableOptions extends ElementOptions {
273
- keys?: boolean;
274
- vi?: boolean;
275
- mouse?: boolean;
276
- interactive?: boolean;
277
- label?: string;
278
- columnSpacing?: number;
279
- columnWidth?: number[];
280
- }
281
-
282
- class table extends BlessedElement {
283
- constructor(options?: TableOptions);
284
- setData(data: { headers: string[]; data: string[][] }): void;
285
- }
286
-
287
- // ── Log ───────────────────────────────────────────────────────────────────
288
-
289
- interface ContribLogOptions extends ElementOptions {
290
- label?: string;
291
- bufferLength?: number;
292
- scrollbar?: { ch?: string };
293
- }
294
-
295
- class log extends BlessedElement {
296
- constructor(options?: ContribLogOptions);
297
- log(text: string): void;
298
- add(text: string): void;
299
- }
300
-
301
- // ── Line chart ────────────────────────────────────────────────────────────
302
-
303
- interface LineSeries {
304
- title?: string;
305
- x: string[];
306
- y: number[];
307
- style?: { line?: string };
308
- }
309
-
310
- interface LineOptions extends ElementOptions {
311
- label?: string;
312
- showLegend?: boolean;
313
- legend?: { width?: number };
314
- xLabelPadding?: number;
315
- xPadding?: number;
316
- numYLabels?: number;
317
- showNthLabel?: number;
318
- wholeNumbersOnly?: boolean;
319
- minY?: number;
320
- maxY?: number;
321
- style?: {
322
- line?: string | string[];
323
- text?: string;
324
- baseline?: string;
325
- };
326
- }
327
-
328
- class line extends BlessedElement {
329
- constructor(options?: LineOptions);
330
- setData(data: LineSeries | LineSeries[]): void;
331
- }
332
- }
package/src/ui/layout.ts DELETED
@@ -1,318 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // Layout — Dynamic Collapsible Grid
3
- //
4
- // btop-inspired layout: all panels live on one screen, user can toggle each
5
- // panel on/off with a key. Hidden panels collapse; remaining panels expand.
6
- //
7
- // Default grid (all panels visible, ~50 row terminal assumed):
8
- //
9
- // ┌────────────┬────────────┬────────────┐ ← row A height ~20%
10
- // │ CPU │ Memory │ Disk │
11
- // ├────────────┴────────────┴────────────┤ ← row B height ~18%
12
- // │ Network │ Runtime │
13
- // ├─────────────────────────────────────┤ ← row C height ~32%
14
- // │ Processes │
15
- // ├─────────────────────────────────────┤ ← row D height ~30%
16
- // │ Logs │
17
- // └─────────────────────────────────────┘
18
- //
19
- // Panel toggle keys:
20
- // d → Disk n → Network
21
- // R → Runtime p → Processes
22
- // l → Logs (focus/toggle)
23
- // (CPU and Memory are always visible)
24
- //
25
- // Heights are recalculated every time a panel is toggled. The layout
26
- // re-builds widget positions using blessed's hide()/show() — no widget
27
- // teardown needed (positions are updated via .top/.height attributes).
28
- // ---------------------------------------------------------------------------
29
-
30
- import blessed from 'blessed';
31
- import type { BlessedScreen, BlessedElement } from 'blessed';
32
- import type { ResolvedConfig } from '../types/config.types.ts';
33
- import type { LauncherHandle } from '../core/launcher.ts';
34
- import type { LogManagerHandle } from '../core/log-manager.ts';
35
- import { createCpuWidget } from './widgets/cpu.widget.ts';
36
- import { createMemoryWidget } from './widgets/memory.widget.ts';
37
- import { createDiskWidget } from './widgets/disk.widget.ts';
38
- import { createNetworkWidget } from './widgets/network.widget.ts';
39
- import { createRuntimeWidget } from './widgets/runtime.widget.ts';
40
- import { createProcessTableWidget } from './widgets/process-table.widget.ts';
41
- import { createLogsWidget } from './widgets/logs.widget.ts';
42
-
43
- interface LayoutOptions {
44
- screen: BlessedScreen;
45
- config: ResolvedConfig;
46
- launcher: LauncherHandle | null;
47
- logManager: LogManagerHandle | null;
48
- }
49
-
50
- interface LayoutHandles {
51
- destroy: () => void;
52
- }
53
-
54
- // ── Height constants (percentages, must sum to 100 when all panels visible) ──
55
-
56
- const H = {
57
- ROW_A: 20, // CPU + Memory + Disk
58
- ROW_B: 24, // Network + Runtime (extra height for line graph)
59
- ROW_C: 30, // Processes
60
- ROW_D: 26, // Logs
61
- } as const;
62
-
63
- // ── Utility ───────────────────────────────────────────────────────────────────
64
-
65
- type Pct = `${number}%`;
66
- function pct(n: number): Pct { return `${n}%`; }
67
-
68
- /** Given a set of visible row heights (% units), compute their cumulative tops. */
69
- function tops(heights: number[]): number[] {
70
- const result: number[] = [];
71
- let acc = 0;
72
- for (const h of heights) {
73
- result.push(acc);
74
- acc += h;
75
- }
76
- return result;
77
- }
78
-
79
- // ── Factory ───────────────────────────────────────────────────────────────────
80
-
81
- export function buildLayout(opts: LayoutOptions): LayoutHandles {
82
- const { screen, config, launcher, logManager } = opts;
83
-
84
- const panels = config.panels;
85
- const managedNames = new Set<string>(
86
- launcher ? launcher.getAll().map(p => p.name) : []
87
- );
88
-
89
- // Track which optional panels are currently visible
90
- const visible = {
91
- disk: panels.disk !== false,
92
- network: panels.network !== false,
93
- runtime: panels.runtime !== false,
94
- processes: panels.processes !== false,
95
- logs: panels.logs !== false,
96
- };
97
-
98
- // ── Create all widgets ─────────────────────────────────────────────────────
99
- // Initial positions are placeholders; recalc() sets real values.
100
-
101
- const cpu = createCpuWidget({
102
- screen, top: 0, left: 0, width: '34%', height: pct(H.ROW_A),
103
- });
104
-
105
- const memory = createMemoryWidget({
106
- screen, top: 0, left: '34%', width: '33%', height: pct(H.ROW_A),
107
- });
108
-
109
- const disk = createDiskWidget({
110
- screen, top: 0, left: '67%', width: '33%', height: pct(H.ROW_A),
111
- });
112
-
113
- const network = createNetworkWidget({
114
- screen, top: pct(H.ROW_A), left: 0, width: '55%', height: pct(H.ROW_B),
115
- });
116
-
117
- const runtime = createRuntimeWidget({
118
- screen, top: pct(H.ROW_A), left: '55%', width: '45%', height: pct(H.ROW_B),
119
- });
120
-
121
- const processes = createProcessTableWidget({
122
- screen,
123
- config,
124
- getManagedById: (id) => launcher?.get(id),
125
- managedNames,
126
- top: pct(H.ROW_A + H.ROW_B),
127
- left: 0,
128
- width: '100%',
129
- height: pct(H.ROW_C),
130
- });
131
-
132
- const logs = createLogsWidget({
133
- screen,
134
- logManager: logManager ?? {
135
- getLines: () => [],
136
- getAllLines: () => [],
137
- clearLines: () => undefined,
138
- destroy: () => undefined,
139
- },
140
- processCount: managedNames.size,
141
- top: pct(H.ROW_A + H.ROW_B + H.ROW_C),
142
- left: 0,
143
- width: '100%',
144
- height: pct(H.ROW_D),
145
- });
146
-
147
- // ── Apply initial visibility from config ───────────────────────────────────
148
-
149
- function applyVisibility(): void {
150
- // Row A: CPU + Memory always shown; Disk optional.
151
- // When Disk is hidden, CPU and Memory each take 50%.
152
- const cpuBox = cpu.box as unknown as { top: string; left: string; width: string; height: string };
153
- const memBox = memory.box as unknown as { top: string; left: string; width: string; height: string };
154
- const diskBox = disk.box as unknown as { top: string; left: string; width: string; height: string; hide: () => void; show: () => void };
155
- const netBox = network.box as unknown as { top: string; left: string; width: string; height: string; hide: () => void; show: () => void };
156
- const runtimeBox = runtime.box as unknown as { top: string; left: string; width: string; height: string; hide: () => void; show: () => void };
157
- const procBox = processes.box as unknown as { top: string; height: string; hide: () => void; show: () => void };
158
- const logsBox = logs.box as unknown as { top: string; height: string; hide: () => void; show: () => void };
159
-
160
- // Row A widths
161
- if (visible.disk) {
162
- cpuBox.width = '34%'; cpuBox.left = '0%';
163
- memBox.width = '33%'; memBox.left = '34%';
164
- diskBox.width = '33%'; diskBox.left = '67%';
165
- diskBox.show();
166
- } else {
167
- cpuBox.width = '50%'; cpuBox.left = '0%';
168
- memBox.width = '50%'; memBox.left = '50%';
169
- diskBox.hide();
170
- }
171
-
172
- // Row A height
173
- const rowAH = H.ROW_A;
174
- cpuBox.height = pct(rowAH); memBox.height = pct(rowAH);
175
-
176
- // Row B: Network + Runtime. If both hidden, row B has height 0.
177
- let rowBTop = rowAH;
178
- let rowBH = 0;
179
-
180
- if (visible.network || visible.runtime) {
181
- rowBH = H.ROW_B;
182
- if (visible.network && visible.runtime) {
183
- netBox.width = '55%'; netBox.left = '0%';
184
- runtimeBox.width = '45%'; runtimeBox.left = '55%';
185
- } else if (visible.network) {
186
- netBox.width = '100%'; netBox.left = '0%';
187
- } else {
188
- runtimeBox.width = '100%'; runtimeBox.left = '0%';
189
- }
190
- if (visible.network) { netBox.top = pct(rowBTop); netBox.height = pct(rowBH); netBox.show(); }
191
- else netBox.hide();
192
- if (visible.runtime) { runtimeBox.top = pct(rowBTop); runtimeBox.height = pct(rowBH); runtimeBox.show(); }
193
- else runtimeBox.hide();
194
- } else {
195
- netBox.hide();
196
- runtimeBox.hide();
197
- }
198
-
199
- // Remaining height after rows A and B
200
- const remaining = 100 - rowAH - rowBH;
201
-
202
- // Split remaining between Processes and Logs
203
- const showProc = visible.processes;
204
- const showLogs = visible.logs;
205
-
206
- let procH = 0;
207
- let logsH = 0;
208
-
209
- if (showProc && showLogs) {
210
- procH = Math.round(remaining * 0.55);
211
- logsH = remaining - procH;
212
- } else if (showProc) {
213
- procH = remaining;
214
- } else if (showLogs) {
215
- logsH = remaining;
216
- }
217
-
218
- const rowCTop = rowAH + rowBH;
219
- const rowDTop = rowCTop + procH;
220
-
221
- if (showProc) {
222
- procBox.top = pct(rowCTop); procBox.height = pct(procH); procBox.show();
223
- } else {
224
- procBox.hide();
225
- }
226
-
227
- if (showLogs) {
228
- logsBox.top = pct(rowDTop); logsBox.height = pct(logsH); logsBox.show();
229
- } else {
230
- logsBox.hide();
231
- }
232
-
233
- screen.render();
234
- }
235
-
236
- applyVisibility();
237
-
238
- // ── Toggle keybindings ─────────────────────────────────────────────────────
239
-
240
- screen.key(['d'], () => {
241
- visible.disk = !visible.disk;
242
- applyVisibility();
243
- });
244
-
245
- screen.key(['n'], () => {
246
- visible.network = !visible.network;
247
- applyVisibility();
248
- });
249
-
250
- screen.key(['R'], () => {
251
- visible.runtime = !visible.runtime;
252
- applyVisibility();
253
- });
254
-
255
- screen.key(['p'], () => {
256
- visible.processes = !visible.processes;
257
- applyVisibility();
258
- });
259
-
260
- // ── Help overlay ───────────────────────────────────────────────────────────
261
- // Press ? to show keybindings overlay
262
-
263
- const helpBox = blessed.box({
264
- parent: screen as unknown as BlessedElement,
265
- top: 'center', left: 'center',
266
- width: 52, height: 18,
267
- tags: true,
268
- border: { type: 'line' },
269
- hidden: true,
270
- style: { border: { fg: 'white' }, bg: 'black' },
271
- label: ' MetWatch — Keybindings ',
272
- content: [
273
- '',
274
- ' {bold}Navigation{/bold}',
275
- ' ↑ / k Move process selection up',
276
- ' ↓ / j Move process selection down',
277
- '',
278
- ' {bold}View{/bold}',
279
- ' a Process table: All mode',
280
- ' f Process table: Watched mode',
281
- ' c / m Sort by CPU / Memory',
282
- '',
283
- ' {bold}Panel Toggles{/bold}',
284
- ' d Toggle Disk panel',
285
- ' n Toggle Network panel',
286
- ' R Toggle Runtime panel',
287
- ' p Toggle Process panel',
288
- '',
289
- ' {bold}Actions{/bold}',
290
- ' K Kill selected process',
291
- ' r Restart managed process',
292
- ' s Stop managed process',
293
- ' q / Ctrl+C Quit',
294
- ' ? Close this help',
295
- ].join('\n'),
296
- });
297
-
298
- screen.key(['?'], () => {
299
- const box = helpBox as unknown as { hidden: boolean; show: () => void; hide: () => void };
300
- if (box.hidden) { box.show(); } else { box.hide(); }
301
- screen.render();
302
- });
303
-
304
- // ── Destroy ────────────────────────────────────────────────────────────────
305
-
306
- function destroy(): void {
307
- cpu.destroy();
308
- memory.destroy();
309
- disk.destroy();
310
- network.destroy();
311
- runtime.destroy();
312
- processes.destroy();
313
- logs.destroy();
314
- helpBox.destroy();
315
- }
316
-
317
- return { destroy };
318
- }