@tukuyomil032/broom 1.0.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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +554 -0
  3. package/dist/commands/analyze.js +371 -0
  4. package/dist/commands/backup.js +257 -0
  5. package/dist/commands/clean.js +255 -0
  6. package/dist/commands/completion.js +714 -0
  7. package/dist/commands/config.js +474 -0
  8. package/dist/commands/doctor.js +280 -0
  9. package/dist/commands/duplicates.js +325 -0
  10. package/dist/commands/help.js +34 -0
  11. package/dist/commands/index.js +22 -0
  12. package/dist/commands/installer.js +266 -0
  13. package/dist/commands/optimize.js +270 -0
  14. package/dist/commands/purge.js +271 -0
  15. package/dist/commands/remove.js +184 -0
  16. package/dist/commands/reports.js +173 -0
  17. package/dist/commands/schedule.js +249 -0
  18. package/dist/commands/status.js +468 -0
  19. package/dist/commands/touchid.js +230 -0
  20. package/dist/commands/uninstall.js +336 -0
  21. package/dist/commands/update.js +182 -0
  22. package/dist/commands/watch.js +258 -0
  23. package/dist/index.js +131 -0
  24. package/dist/scanners/base.js +21 -0
  25. package/dist/scanners/browser-cache.js +111 -0
  26. package/dist/scanners/dev-cache.js +64 -0
  27. package/dist/scanners/docker.js +96 -0
  28. package/dist/scanners/downloads.js +66 -0
  29. package/dist/scanners/homebrew.js +82 -0
  30. package/dist/scanners/index.js +126 -0
  31. package/dist/scanners/installer.js +87 -0
  32. package/dist/scanners/ios-backups.js +82 -0
  33. package/dist/scanners/node-modules.js +75 -0
  34. package/dist/scanners/temp-files.js +65 -0
  35. package/dist/scanners/trash.js +90 -0
  36. package/dist/scanners/user-cache.js +62 -0
  37. package/dist/scanners/user-logs.js +53 -0
  38. package/dist/scanners/xcode.js +124 -0
  39. package/dist/types/index.js +23 -0
  40. package/dist/ui/index.js +5 -0
  41. package/dist/ui/monitors.js +345 -0
  42. package/dist/ui/output.js +304 -0
  43. package/dist/ui/prompts.js +270 -0
  44. package/dist/utils/config.js +133 -0
  45. package/dist/utils/debug.js +119 -0
  46. package/dist/utils/fs.js +283 -0
  47. package/dist/utils/help.js +265 -0
  48. package/dist/utils/index.js +6 -0
  49. package/dist/utils/paths.js +142 -0
  50. package/dist/utils/report.js +404 -0
  51. package/package.json +87 -0
@@ -0,0 +1,62 @@
1
+ /**
2
+ * User cache scanner
3
+ */
4
+ import { readdir, stat } from 'fs/promises';
5
+ import { join } from 'path';
6
+ import { BaseScanner } from './base.js';
7
+ import { paths } from '../utils/paths.js';
8
+ import { exists, getSize, isExcludedPath } from '../utils/fs.js';
9
+ export class UserCacheScanner extends BaseScanner {
10
+ constructor() {
11
+ super(...arguments);
12
+ this.category = {
13
+ id: 'user-cache',
14
+ name: 'User Cache',
15
+ group: 'System Junk',
16
+ description: 'Application caches in ~/Library/Caches',
17
+ safetyLevel: 'safe',
18
+ };
19
+ }
20
+ async scan(_options) {
21
+ const items = [];
22
+ try {
23
+ if (!exists(paths.userCache)) {
24
+ return this.createResult([]);
25
+ }
26
+ const entries = await readdir(paths.userCache);
27
+ for (const entry of entries) {
28
+ // Skip Apple system caches
29
+ if (entry.startsWith('com.apple.')) {
30
+ continue;
31
+ }
32
+ const entryPath = join(paths.userCache, entry);
33
+ // Skip excluded paths (iCloud Drive, etc.)
34
+ if (isExcludedPath(entryPath)) {
35
+ continue;
36
+ }
37
+ try {
38
+ const stats = await stat(entryPath);
39
+ const size = await getSize(entryPath);
40
+ if (size > 0) {
41
+ items.push({
42
+ path: entryPath,
43
+ size,
44
+ name: entry,
45
+ isDirectory: stats.isDirectory(),
46
+ modifiedAt: stats.mtime,
47
+ });
48
+ }
49
+ }
50
+ catch {
51
+ // Skip if cannot access
52
+ }
53
+ }
54
+ // Sort by size descending
55
+ items.sort((a, b) => b.size - a.size);
56
+ return this.createResult(items);
57
+ }
58
+ catch (error) {
59
+ return this.createResult([], error.message);
60
+ }
61
+ }
62
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * User logs scanner
3
+ */
4
+ import { readdir, stat } from 'fs/promises';
5
+ import { join } from 'path';
6
+ import { BaseScanner } from './base.js';
7
+ import { paths } from '../utils/paths.js';
8
+ import { exists, getSize } from '../utils/fs.js';
9
+ export class UserLogsScanner extends BaseScanner {
10
+ constructor() {
11
+ super(...arguments);
12
+ this.category = {
13
+ id: 'user-logs',
14
+ name: 'User Logs',
15
+ group: 'System Junk',
16
+ description: 'Application logs in ~/Library/Logs',
17
+ safetyLevel: 'safe',
18
+ };
19
+ }
20
+ async scan(_options) {
21
+ const items = [];
22
+ try {
23
+ if (!exists(paths.userLogs)) {
24
+ return this.createResult([]);
25
+ }
26
+ const entries = await readdir(paths.userLogs);
27
+ for (const entry of entries) {
28
+ const entryPath = join(paths.userLogs, entry);
29
+ try {
30
+ const stats = await stat(entryPath);
31
+ const size = await getSize(entryPath);
32
+ if (size > 0) {
33
+ items.push({
34
+ path: entryPath,
35
+ size,
36
+ name: entry,
37
+ isDirectory: stats.isDirectory(),
38
+ modifiedAt: stats.mtime,
39
+ });
40
+ }
41
+ }
42
+ catch {
43
+ // Skip if cannot access
44
+ }
45
+ }
46
+ items.sort((a, b) => b.size - a.size);
47
+ return this.createResult(items);
48
+ }
49
+ catch (error) {
50
+ return this.createResult([], error.message);
51
+ }
52
+ }
53
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Xcode cache scanner
3
+ */
4
+ import { readdir, stat } from 'fs/promises';
5
+ import { join } from 'path';
6
+ import { BaseScanner } from './base.js';
7
+ import { paths } from '../utils/paths.js';
8
+ import { exists, getSize } from '../utils/fs.js';
9
+ export class XcodeScanner extends BaseScanner {
10
+ constructor() {
11
+ super(...arguments);
12
+ this.category = {
13
+ id: 'xcode',
14
+ name: 'Xcode Cache',
15
+ group: 'Development',
16
+ description: 'Xcode derived data, device support, and caches',
17
+ safetyLevel: 'moderate',
18
+ safetyNote: 'May need to rebuild projects',
19
+ };
20
+ this.locations = [
21
+ {
22
+ name: 'Derived Data',
23
+ path: paths.xcode.derivedData,
24
+ description: 'Build intermediates and indices',
25
+ safetyLevel: 'safe',
26
+ },
27
+ {
28
+ name: 'Archives',
29
+ path: paths.xcode.archives,
30
+ description: 'Archived builds for distribution',
31
+ safetyLevel: 'risky',
32
+ },
33
+ {
34
+ name: 'iOS Device Support',
35
+ path: paths.xcode.deviceSupport,
36
+ description: 'Debug symbols for connected devices',
37
+ safetyLevel: 'moderate',
38
+ },
39
+ {
40
+ name: 'Simulator Caches',
41
+ path: paths.xcode.simulatorCache,
42
+ description: 'Simulator cache files',
43
+ safetyLevel: 'safe',
44
+ },
45
+ {
46
+ name: 'Module Cache',
47
+ path: paths.xcode.modulesCache,
48
+ description: 'Swift/Clang module caches',
49
+ safetyLevel: 'safe',
50
+ },
51
+ {
52
+ name: 'Previews Cache',
53
+ path: paths.xcode.previewsCache,
54
+ description: 'SwiftUI preview caches',
55
+ safetyLevel: 'safe',
56
+ },
57
+ ];
58
+ }
59
+ async scan(_options) {
60
+ const items = [];
61
+ for (const location of this.locations) {
62
+ try {
63
+ if (!exists(location.path)) {
64
+ continue;
65
+ }
66
+ const stats = await stat(location.path);
67
+ const size = await getSize(location.path);
68
+ if (size > 0) {
69
+ items.push({
70
+ path: location.path,
71
+ size,
72
+ name: location.name,
73
+ isDirectory: stats.isDirectory(),
74
+ modifiedAt: stats.mtime,
75
+ });
76
+ }
77
+ }
78
+ catch {
79
+ // Skip if cannot access
80
+ }
81
+ }
82
+ // Also scan for old simulators
83
+ await this.scanOldSimulators(items);
84
+ items.sort((a, b) => b.size - a.size);
85
+ return this.createResult(items);
86
+ }
87
+ async scanOldSimulators(items) {
88
+ try {
89
+ if (!exists(paths.xcode.simulatorDevices)) {
90
+ return;
91
+ }
92
+ const entries = await readdir(paths.xcode.simulatorDevices);
93
+ for (const entry of entries) {
94
+ if (!entry.match(/^[A-F0-9-]{36}$/)) {
95
+ continue;
96
+ }
97
+ const devicePath = join(paths.xcode.simulatorDevices, entry);
98
+ const dataPath = join(devicePath, 'data');
99
+ try {
100
+ if (exists(dataPath)) {
101
+ const stats = await stat(dataPath);
102
+ const size = await getSize(dataPath);
103
+ if (size > 100 * 1024 * 1024) {
104
+ // > 100MB
105
+ items.push({
106
+ path: dataPath,
107
+ size,
108
+ name: `Simulator Data (${entry.substring(0, 8)})`,
109
+ isDirectory: true,
110
+ modifiedAt: stats.mtime,
111
+ });
112
+ }
113
+ }
114
+ }
115
+ catch {
116
+ // Skip
117
+ }
118
+ }
119
+ }
120
+ catch {
121
+ // Skip
122
+ }
123
+ }
124
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Core types for Broom CLI
3
+ */
4
+ export const DEFAULT_CONFIG = {
5
+ dryRun: false,
6
+ verbose: false,
7
+ whitelist: [],
8
+ blacklist: [],
9
+ autoConfirm: false,
10
+ safetyLevel: 'moderate',
11
+ monitorPreset: 1,
12
+ scanLocations: {
13
+ userCache: true,
14
+ systemCache: true,
15
+ systemLogs: true,
16
+ userLogs: true,
17
+ trash: true,
18
+ downloads: false,
19
+ browserCache: true,
20
+ devCache: true,
21
+ xcodeCache: true,
22
+ },
23
+ };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * UI module exports
3
+ */
4
+ export * from './output.js';
5
+ export * from './prompts.js';
@@ -0,0 +1,345 @@
1
+ import { exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+ const execAsync = promisify(exec);
4
+ /**
5
+ * Format bytes to human readable
6
+ */
7
+ export function formatBytes(bytes, decimals = 1) {
8
+ if (bytes === 0) {
9
+ return '0 B';
10
+ }
11
+ const k = 1024;
12
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
13
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
14
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i];
15
+ }
16
+ /**
17
+ * Format speed (bytes per second)
18
+ */
19
+ export function formatSpeed(bytesPerSec) {
20
+ if (bytesPerSec < 1024) {
21
+ return bytesPerSec.toFixed(2) + ' B/s';
22
+ }
23
+ if (bytesPerSec < 1024 * 1024) {
24
+ return (bytesPerSec / 1024).toFixed(2) + ' KB/s';
25
+ }
26
+ return (bytesPerSec / 1024 / 1024).toFixed(2) + ' MB/s';
27
+ }
28
+ /**
29
+ * Create colored bar for progress display
30
+ */
31
+ export function createColoredBar(percent, width) {
32
+ const filled = Math.round((percent / 100) * width);
33
+ let bar = '';
34
+ for (let i = 0; i < filled; i++) {
35
+ const ratio = i / width;
36
+ if (ratio < 0.5) {
37
+ bar += '{green-fg}█{/green-fg}';
38
+ }
39
+ else if (ratio < 0.75) {
40
+ bar += '{yellow-fg}█{/yellow-fg}';
41
+ }
42
+ else {
43
+ bar += '{red-fg}█{/red-fg}';
44
+ }
45
+ }
46
+ bar += '{black-fg}' + '░'.repeat(width - filled) + '{/black-fg}';
47
+ return bar;
48
+ }
49
+ /**
50
+ * Create simple colored bar
51
+ */
52
+ export function createSimpleBar(percent, width, color = 'green') {
53
+ const filled = Math.round((percent / 100) * width);
54
+ return `{${color}-fg}${'█'.repeat(filled)}{/${color}-fg}{black-fg}${'░'.repeat(width - filled)}{/black-fg}`;
55
+ }
56
+ /**
57
+ * Calculate system health score (0-100)
58
+ */
59
+ export function calculateHealth(cpuUsage, memUsage, diskUsage, batteryPercent) {
60
+ const cpuScore = Math.max(0, 100 - cpuUsage);
61
+ const memScore = Math.max(0, 100 - memUsage);
62
+ const diskScore = Math.max(0, 100 - diskUsage);
63
+ const batteryScore = batteryPercent;
64
+ return Math.round(cpuScore * 0.3 + memScore * 0.3 + diskScore * 0.2 + batteryScore * 0.2);
65
+ }
66
+ /**
67
+ * Monitor Preset 1: Original design (Classic Grid Layout)
68
+ */
69
+ export async function renderMonitorPreset1(screen, data, boxes) {
70
+ console.error('[DEBUG] renderMonitorPreset1 called');
71
+ const { cpuLoad, cpuInfo, cpuTemp, mem, battery, diskIO, osInfo, graphics, topProcs } = data;
72
+ const { mainDisk, activeNet, localIP } = data;
73
+ // Header
74
+ const memTotalStr = formatBytes(mem.total, 1);
75
+ const diskTotalStr = formatBytes(mainDisk?.size ?? 0, 1);
76
+ const memUsage = (mem.used / mem.total) * 100;
77
+ const diskUsage = mainDisk?.use ?? 0;
78
+ const batteryPercent = battery.hasBattery ? battery.percent : 100;
79
+ const health = calculateHealth(cpuLoad.currentLoad, memUsage, diskUsage, batteryPercent);
80
+ const gpu = graphics.controllers?.[0];
81
+ const gpuName = gpu?.model ? `(${gpu.model.replace('Apple ', '').split(' ')[0]})` : '';
82
+ let headerContent = `{bold}Broom Status{/bold} {green-fg}●{/green-fg} Health {bold}{yellow-fg}${health}{/yellow-fg}{/bold} ` +
83
+ `${osInfo.hostname.split('.')[0]} • ${cpuInfo.manufacturer} ${cpuInfo.brand} ${gpuName} • ` +
84
+ `${memTotalStr}/${diskTotalStr} • ${osInfo.distro} ${osInfo.release}`;
85
+ // CPU
86
+ const cpuCores = cpuLoad.cpus || [];
87
+ let cpuContent = '';
88
+ const coresToShow = cpuCores.slice(0, 6);
89
+ coresToShow.forEach((core, i) => {
90
+ const bar = createColoredBar(core.load, 12);
91
+ cpuContent += `Core${i + 1} ${bar} ${core.load.toFixed(1)}%\n`;
92
+ });
93
+ const temp = cpuTemp.main > 0 ? `@ ${cpuTemp.main.toFixed(1)}°C` : '';
94
+ const loadAvg = cpuLoad.avgLoad ? cpuLoad.avgLoad.toFixed(2) : '0.00';
95
+ cpuContent += `Load ${loadAvg} / ${cpuLoad.currentLoad.toFixed(2)} ${temp}`;
96
+ // Memory
97
+ const memUsedPercent = (mem.used / mem.total) * 100;
98
+ const memFreePercent = (mem.free / mem.total) * 100;
99
+ const swapUsedPercent = mem.swaptotal > 0 ? (mem.swapused / mem.swaptotal) * 100 : 0;
100
+ let memContent = '';
101
+ memContent += `Used ${createColoredBar(memUsedPercent, 12)} ${memUsedPercent.toFixed(1)}%\n`;
102
+ memContent += `Free ${createSimpleBar(memFreePercent, 12, 'green')} ${memFreePercent.toFixed(1)}%\n`;
103
+ memContent += `Swap ${createColoredBar(swapUsedPercent, 12)} ${swapUsedPercent.toFixed(1)}%\n`;
104
+ memContent += `Total ${formatBytes(mem.total)} / Avail ${formatBytes(mem.available)}`;
105
+ // Disk
106
+ const diskPercent = mainDisk?.use ?? 0;
107
+ const readSpeed = diskIO?.rIO_sec ?? 0;
108
+ const writeSpeed = diskIO?.wIO_sec ?? 0;
109
+ let diskContent = '';
110
+ diskContent += `INTR ${createColoredBar(diskPercent, 12)} ${diskPercent.toFixed(1)}%\n`;
111
+ diskContent += `Read {green-fg}█{/green-fg} ${formatSpeed(readSpeed)}\n`;
112
+ diskContent += `Write {yellow-fg}█{/yellow-fg} ${formatSpeed(writeSpeed)}`;
113
+ // Network
114
+ const rxSpeed = activeNet?.rx_sec ?? 0;
115
+ const txSpeed = activeNet?.tx_sec ?? 0;
116
+ let netContent = '';
117
+ netContent += `Down ${formatSpeed(rxSpeed)}\n`;
118
+ netContent += `Up ${formatSpeed(txSpeed)}\n\n`;
119
+ netContent += `IP: ${localIP}`;
120
+ // Processes
121
+ let procContent = '';
122
+ topProcs.slice(0, 5).forEach((proc) => {
123
+ const bar = createColoredBar(Math.min(proc.cpu, 100), 8);
124
+ procContent += `${proc.name.padEnd(14)} ${bar} ${proc.cpu.toFixed(1)}%\n`;
125
+ });
126
+ // Update box contents using boxes object
127
+ if (boxes['header']) {
128
+ boxes['header'].setContent(headerContent);
129
+ }
130
+ if (boxes['cpu']) {
131
+ boxes['cpu'].setContent(cpuContent);
132
+ }
133
+ if (boxes['mem']) {
134
+ boxes['mem'].setContent(memContent);
135
+ }
136
+ if (boxes['disk']) {
137
+ boxes['disk'].setContent(diskContent);
138
+ }
139
+ if (boxes['net']) {
140
+ boxes['net'].setContent(netContent);
141
+ }
142
+ if (boxes['proc']) {
143
+ boxes['proc'].setContent(procContent);
144
+ }
145
+ }
146
+ /**
147
+ * Monitor Preset 2: Minimal Compact Layout
148
+ */
149
+ export async function renderMonitorPreset2(screen, data, boxes) {
150
+ console.error('[DEBUG] renderMonitorPreset2 called');
151
+ console.error('[DEBUG] boxes keys:', Object.keys(boxes));
152
+ console.error('[DEBUG] boxes["main"]:', boxes['main'] ? 'exists' : 'undefined');
153
+ console.error('[DEBUG] boxes["header"]:', boxes['header'] ? 'exists' : 'undefined');
154
+ const { cpuLoad, cpuInfo, cpuTemp, mem, battery, osInfo, topProcs, mainDisk, activeNet, localIP, } = data;
155
+ const memUsage = (mem.used / mem.total) * 100;
156
+ const diskUsage = mainDisk?.use ?? 0;
157
+ const batteryPercent = battery.hasBattery ? battery.percent : 100;
158
+ const health = calculateHealth(cpuLoad.currentLoad, memUsage, diskUsage, batteryPercent);
159
+ // Compact header
160
+ let headerContent = `┌─ System ─┐ ${cpuInfo.brand} @ ${cpuTemp.main.toFixed(0)}°C │ CPU ${cpuLoad.currentLoad.toFixed(0)}% │ MEM ${memUsage.toFixed(0)}% │ DISK ${diskUsage.toFixed(0)}% │ HEALTH ${health}`;
161
+ // System stats in one line
162
+ let mainContent = '';
163
+ mainContent += `CPU ${createColoredBar(cpuLoad.currentLoad, 20)} ${cpuLoad.currentLoad.toFixed(1)}%\n`;
164
+ mainContent += `MEM ${createColoredBar(memUsage, 20)} ${memUsage.toFixed(1)}%\n`;
165
+ mainContent += `DISK ${createColoredBar(diskUsage, 20)} ${diskUsage.toFixed(1)}%\n`;
166
+ mainContent += '\n';
167
+ // Top processes
168
+ mainContent += `Top Processes:\n`;
169
+ topProcs.slice(0, 3).forEach((proc) => {
170
+ mainContent += ` ${proc.name.padEnd(12)} ${proc.cpu.toFixed(1)}%\n`;
171
+ });
172
+ mainContent += `\nNetwork: ↓ ${formatSpeed(activeNet?.rx_sec ?? 0)} ↑ ${formatSpeed(activeNet?.tx_sec ?? 0)}\n`;
173
+ mainContent += `IP: ${localIP}`;
174
+ console.error('[DEBUG] About to call setContent on boxes...');
175
+ console.error('[DEBUG] boxes["header"] value:', boxes['header']);
176
+ console.error('[DEBUG] boxes["main"] value:', boxes['main']);
177
+ if (boxes['header']) {
178
+ console.error('[DEBUG] Setting header content');
179
+ boxes['header'].setContent(headerContent);
180
+ }
181
+ else {
182
+ console.error('[DEBUG] ERROR: boxes["header"] is falsy!');
183
+ }
184
+ if (boxes['main']) {
185
+ console.error('[DEBUG] Setting main content');
186
+ boxes['main'].setContent(mainContent);
187
+ }
188
+ else {
189
+ console.error('[DEBUG] ERROR: boxes["main"] is falsy!');
190
+ }
191
+ }
192
+ /**
193
+ * Monitor Preset 3: Detailed Information
194
+ */
195
+ export async function renderMonitorPreset3(screen, data, boxes) {
196
+ console.error('[DEBUG] renderMonitorPreset3 called');
197
+ const { cpuLoad, cpuInfo, cpuTemp, mem, battery, diskIO, osInfo, graphics, topProcs, mainDisk, activeNet, localIP, } = data;
198
+ const memUsage = (mem.used / mem.total) * 100;
199
+ const diskUsage = mainDisk?.use ?? 0;
200
+ let content = '';
201
+ content += `{bold}═══ SYSTEM INFORMATION ═══{/bold}\n`;
202
+ content += `Hostname: ${osInfo.hostname}\n`;
203
+ content += `OS: ${osInfo.distro} ${osInfo.release}\n\n`;
204
+ content += `{bold}═══ HARDWARE ═══{/bold}\n`;
205
+ content += `CPU: ${cpuInfo.manufacturer} ${cpuInfo.brand}\n`;
206
+ content += `Cores: ${cpuInfo.cores} physical, ${cpuInfo.processors} logical\n`;
207
+ content += `GPU: ${graphics.controllers?.[0]?.model || 'N/A'}\n`;
208
+ content += `RAM: ${formatBytes(mem.total)}\n\n`;
209
+ content += `{bold}═══ PERFORMANCE ═══{/bold}\n`;
210
+ content += `CPU Usage: ${createColoredBar(cpuLoad.currentLoad, 15)} ${cpuLoad.currentLoad.toFixed(1)}%\n`;
211
+ content += `Temp: ${cpuTemp.main.toFixed(1)}°C\n`;
212
+ content += `Memory: ${createColoredBar(memUsage, 15)} ${memUsage.toFixed(1)}% (${formatBytes(mem.used)}/${formatBytes(mem.total)})\n`;
213
+ content += `Disk: ${createColoredBar(diskUsage, 15)} ${diskUsage.toFixed(1)}% (${formatBytes(mainDisk?.used ?? 0)}/${formatBytes(mainDisk?.size ?? 0)})\n\n`;
214
+ if (battery.hasBattery) {
215
+ content += `{bold}═══ POWER ═══{/bold}\n`;
216
+ content += `Battery: ${battery.percent.toFixed(0)}% ${battery.isCharging ? '(charging)' : '(discharging)'}\n`;
217
+ content += `Health: ${battery.maxCapacity && battery.designedCapacity ? Math.round((battery.maxCapacity / battery.designedCapacity) * 100) : 100}%\n\n`;
218
+ }
219
+ content += `{bold}═══ NETWORK ═══{/bold}\n`;
220
+ content += `IP: ${localIP}\n`;
221
+ content += `Download: ${formatSpeed(activeNet?.rx_sec ?? 0)}\n`;
222
+ content += `Upload: ${formatSpeed(activeNet?.tx_sec ?? 0)}\n\n`;
223
+ content += `{bold}═══ TOP PROCESSES ═══{/bold}\n`;
224
+ topProcs.slice(0, 5).forEach((proc) => {
225
+ content += `${proc.name.padEnd(15)} ${proc.cpu.toFixed(1)}%\n`;
226
+ });
227
+ if (boxes['main']) {
228
+ boxes['main'].setContent(content);
229
+ }
230
+ }
231
+ /**
232
+ * Monitor Preset 4: Linux-style Dashboard (like in reference image)
233
+ */
234
+ export async function renderMonitorPreset4(screen, data, boxes) {
235
+ console.error('[DEBUG] renderMonitorPreset4 called');
236
+ const { cpuLoad, cpuInfo, cpuTemp, mem, battery, diskIO, osInfo, topProcs, mainDisk, activeNet } = data;
237
+ const memUsage = (mem.used / mem.total) * 100;
238
+ const diskUsage = mainDisk?.use ?? 0;
239
+ const swapUsage = mem.swaptotal > 0 ? (mem.swapused / mem.swaptotal) * 100 : 0;
240
+ let headerContent = `{cyan-fg}┌─ Broom Status ${cpuInfo.brand} ─────────────────────┐{/cyan-fg}`;
241
+ let content = '';
242
+ // CPU section
243
+ content += `{yellow-fg}CPU Usage{/yellow-fg}\n`;
244
+ const cpuCores = cpuLoad.cpus || [];
245
+ const coresToShow = cpuCores.slice(0, 8);
246
+ coresToShow.forEach((core, i) => {
247
+ const bar = createColoredBar(core.load, 18);
248
+ content += ` CPU${i} ${bar} ${core.load.toFixed(1)}%\n`;
249
+ });
250
+ content += ` Avg ${cpuLoad.currentLoad.toFixed(1)}% Temp ${cpuTemp.main.toFixed(1)}°C\n\n`;
251
+ // Memory section
252
+ content += `{red-fg}Memory Usage{/red-fg}\n`;
253
+ content += ` Main ${createColoredBar(memUsage, 18)} ${memUsage.toFixed(1)}% ${formatBytes(mem.used)}/${formatBytes(mem.total)}\n`;
254
+ content += ` Swap ${createColoredBar(swapUsage, 18)} ${swapUsage.toFixed(1)}% ${formatBytes(mem.swapused)}/${formatBytes(mem.swaptotal)}\n\n`;
255
+ // Disk section
256
+ content += `{blue-fg}Disk Usage{/blue-fg}\n`;
257
+ const readSpeed = diskIO?.rIO_sec ?? 0;
258
+ const writeSpeed = diskIO?.wIO_sec ?? 0;
259
+ content += ` I/O ${createColoredBar(diskUsage, 18)} ${diskUsage.toFixed(1)}% ${formatBytes(mainDisk?.used ?? 0)}/${formatBytes(mainDisk?.size ?? 0)}\n`;
260
+ content += ` R/s ${formatSpeed(readSpeed)}\n`;
261
+ content += ` W/s ${formatSpeed(writeSpeed)}\n\n`;
262
+ // Network section
263
+ content += `{cyan-fg}Network{/cyan-fg}\n`;
264
+ const rxSpeed = activeNet?.rx_sec ?? 0;
265
+ const txSpeed = activeNet?.tx_sec ?? 0;
266
+ content += ` RX ${createSimpleBar(Math.min((rxSpeed / 1024 / 100) * 10, 100), 18, 'green')} ${formatSpeed(rxSpeed)}\n`;
267
+ content += ` TX ${createSimpleBar(Math.min((txSpeed / 1024 / 100) * 10, 100), 18, 'yellow')} ${formatSpeed(txSpeed)}\n\n`;
268
+ // Processes section
269
+ content += `{magenta-fg}Top Processes (by CPU){/magenta-fg}\n`;
270
+ content += ` PID Command CPU%\n`;
271
+ topProcs.slice(0, 6).forEach((proc) => {
272
+ content += ` ${proc.name.padEnd(14)} ${proc.cpu.toFixed(1)}%\n`;
273
+ });
274
+ if (boxes['header']) {
275
+ boxes['header'].setContent(headerContent);
276
+ }
277
+ if (boxes['main']) {
278
+ boxes['main'].setContent(content);
279
+ }
280
+ }
281
+ /**
282
+ * Monitor Preset 5: Modern Colorful Dashboard
283
+ */
284
+ export async function renderMonitorPreset5(screen, data, boxes) {
285
+ console.error('[DEBUG] renderMonitorPreset5 called');
286
+ const { cpuLoad, cpuInfo, cpuTemp, mem, battery, diskIO, osInfo, topProcs, mainDisk, activeNet, localIP, } = data;
287
+ const memUsage = (mem.used / mem.total) * 100;
288
+ const diskUsage = mainDisk?.use ?? 0;
289
+ const batteryPercent = battery.hasBattery ? battery.percent : 100;
290
+ const health = calculateHealth(cpuLoad.currentLoad, memUsage, diskUsage, batteryPercent);
291
+ let headerContent = `{bold}{cyan-fg}🚀 BROOM DASHBOARD {/cyan-fg}{/bold} Health: {bold}${health}{/bold} Hostname: {bold}${osInfo.hostname}{/bold}`;
292
+ let content = '';
293
+ // Quick stats row
294
+ content += `┌────────────────────────────────────────────────────────────────────┐\n`;
295
+ content += `│ {yellow-fg}◆ CPU{/yellow-fg} ${createColoredBar(cpuLoad.currentLoad, 12)} ${cpuLoad.currentLoad.toFixed(1)}% {red-fg}◆ MEM{/red-fg} ${createColoredBar(memUsage, 12)} ${memUsage.toFixed(1)}% {blue-fg}◆ DISK{/blue-fg} ${createColoredBar(diskUsage, 12)} ${diskUsage.toFixed(1)}% │\n`;
296
+ content += `└────────────────────────────────────────────────────────────────────┘\n\n`;
297
+ // Detailed sections
298
+ content += `{yellow-fg}📊 CPU{/yellow-fg} Cores: ${cpuInfo.cores} Brand: ${cpuInfo.brand} Temp: ${cpuTemp.main.toFixed(1)}°C Load: ${cpuLoad.avgLoad?.toFixed(2)}\n`;
299
+ content += `${createColoredBar(cpuLoad.currentLoad, 35)} ${cpuLoad.currentLoad.toFixed(2)}%\n\n`;
300
+ content += `{red-fg}🧠 MEMORY{/red-fg} Total: ${formatBytes(mem.total)}\n`;
301
+ content += `Used ${createColoredBar((mem.used / mem.total) * 100, 32)} ${formatBytes(mem.used)}\n`;
302
+ content += `Available ${createSimpleBar((mem.available / mem.total) * 100, 32, 'green')} ${formatBytes(mem.available)}\n\n`;
303
+ content += `{blue-fg}💾 DISK{/blue-fg} Total: ${formatBytes(mainDisk?.size ?? 0)}\n`;
304
+ content += `Used ${createColoredBar(diskUsage, 32)} ${formatBytes(mainDisk?.used ?? 0)}\n`;
305
+ content += `I/O: ↓ ${formatSpeed(diskIO?.rIO_sec ?? 0).padEnd(12)} ↑ ${formatSpeed(diskIO?.wIO_sec ?? 0)}\n\n`;
306
+ if (battery.hasBattery) {
307
+ const batteryColor = battery.percent > 20 ? 'green' : 'red';
308
+ content += `{green-fg}Power{/green-fg} Status: ${battery.isCharging ? 'Charging' : 'Battery'}\n`;
309
+ content += `${createSimpleBar(battery.percent, 35, batteryColor)} ${battery.percent.toFixed(0)}%\n\n`;
310
+ }
311
+ content += `{cyan-fg}Network{/cyan-fg} IP: ${localIP}\n`;
312
+ const rxSpeed = activeNet?.rx_sec ?? 0;
313
+ const txSpeed = activeNet?.tx_sec ?? 0;
314
+ content += `Download ${createSimpleBar(Math.min((rxSpeed / 1024 / 100) * 10, 100), 28, 'green')} ${formatSpeed(rxSpeed)}\n`;
315
+ content += `Upload ${createSimpleBar(Math.min((txSpeed / 1024 / 100) * 10, 100), 28, 'yellow')} ${formatSpeed(txSpeed)}\n\n`;
316
+ content += `{magenta-fg}⚙️ TOP PROCESSES{/magenta-fg}\n`;
317
+ topProcs.slice(0, 5).forEach((proc, i) => {
318
+ content += `${i + 1}. ${proc.name.padEnd(16)} ${proc.cpu.toFixed(1)}%\n`;
319
+ });
320
+ if (boxes['header']) {
321
+ boxes['header'].setContent(headerContent);
322
+ }
323
+ if (boxes['main']) {
324
+ boxes['main'].setContent(content);
325
+ }
326
+ }
327
+ /**
328
+ * Get render function for preset
329
+ */
330
+ export function getMonitorRenderer(preset) {
331
+ switch (preset) {
332
+ case 1:
333
+ return renderMonitorPreset1;
334
+ case 2:
335
+ return renderMonitorPreset2;
336
+ case 3:
337
+ return renderMonitorPreset3;
338
+ case 4:
339
+ return renderMonitorPreset4;
340
+ case 5:
341
+ return renderMonitorPreset5;
342
+ default:
343
+ return renderMonitorPreset1;
344
+ }
345
+ }