@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.
- package/LICENSE +21 -0
- package/README.md +554 -0
- package/dist/commands/analyze.js +371 -0
- package/dist/commands/backup.js +257 -0
- package/dist/commands/clean.js +255 -0
- package/dist/commands/completion.js +714 -0
- package/dist/commands/config.js +474 -0
- package/dist/commands/doctor.js +280 -0
- package/dist/commands/duplicates.js +325 -0
- package/dist/commands/help.js +34 -0
- package/dist/commands/index.js +22 -0
- package/dist/commands/installer.js +266 -0
- package/dist/commands/optimize.js +270 -0
- package/dist/commands/purge.js +271 -0
- package/dist/commands/remove.js +184 -0
- package/dist/commands/reports.js +173 -0
- package/dist/commands/schedule.js +249 -0
- package/dist/commands/status.js +468 -0
- package/dist/commands/touchid.js +230 -0
- package/dist/commands/uninstall.js +336 -0
- package/dist/commands/update.js +182 -0
- package/dist/commands/watch.js +258 -0
- package/dist/index.js +131 -0
- package/dist/scanners/base.js +21 -0
- package/dist/scanners/browser-cache.js +111 -0
- package/dist/scanners/dev-cache.js +64 -0
- package/dist/scanners/docker.js +96 -0
- package/dist/scanners/downloads.js +66 -0
- package/dist/scanners/homebrew.js +82 -0
- package/dist/scanners/index.js +126 -0
- package/dist/scanners/installer.js +87 -0
- package/dist/scanners/ios-backups.js +82 -0
- package/dist/scanners/node-modules.js +75 -0
- package/dist/scanners/temp-files.js +65 -0
- package/dist/scanners/trash.js +90 -0
- package/dist/scanners/user-cache.js +62 -0
- package/dist/scanners/user-logs.js +53 -0
- package/dist/scanners/xcode.js +124 -0
- package/dist/types/index.js +23 -0
- package/dist/ui/index.js +5 -0
- package/dist/ui/monitors.js +345 -0
- package/dist/ui/output.js +304 -0
- package/dist/ui/prompts.js +270 -0
- package/dist/utils/config.js +133 -0
- package/dist/utils/debug.js +119 -0
- package/dist/utils/fs.js +283 -0
- package/dist/utils/help.js +265 -0
- package/dist/utils/index.js +6 -0
- package/dist/utils/paths.js +142 -0
- package/dist/utils/report.js +404 -0
- 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
|
+
};
|
package/dist/ui/index.js
ADDED
|
@@ -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
|
+
}
|