opencode-system-metrics-tui 0.1.1 → 0.1.2
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/README.md +27 -12
- package/dist/install-hack-font.js +156 -0
- package/dist/tui.js +246 -110
- package/package.json +7 -3
- package/scripts/build.ts +3 -3
package/README.md
CHANGED
|
@@ -15,11 +15,14 @@ The README is available in **Español**, **English**, and **Português**.
|
|
|
15
15
|
- GPU: utilización y temperatura.
|
|
16
16
|
- GPU VRAM: memoria usada, total y porcentaje.
|
|
17
17
|
- Red: velocidad de descarga y subida en tiempo real.
|
|
18
|
-
-
|
|
18
|
+
- CPU, RAM y red se consultan con una cadencia general de 2 segundos; GPU, VRAM y temperaturas usan una caché expirable de 10 segundos para reducir el coste sin congelar los sensores.
|
|
19
19
|
- Colores compatibles con el tema activo de OpenCode.
|
|
20
|
-
- Iconos
|
|
20
|
+
- Iconos Unicode por defecto en Windows (la detección de fuentes instaladas no cambia la selección); Linux/macOS usan Nerd Font solo si está instalada y mantienen fallback Unicode.
|
|
21
21
|
- En Windows usa iconos Unicode visibles de forma predeterminada, porque OpenCode no puede detectar la fuente activa de la terminal.
|
|
22
22
|
- Soporte para Linux, macOS y Windows cuando el sistema expone las métricas.
|
|
23
|
+
- En Windows, la temperatura de CPU requiere LibreHardwareMonitor ejecutándose como administrador con **Remote Web Server** activo en `http://127.0.0.1:8085`.
|
|
24
|
+
- En Windows, la velocidad de red usa PowerShell y `Get-NetAdapterStatistics`; no agrega una dependencia npm.
|
|
25
|
+
- Las lecturas opcionales de Windows se hacen bajo demanda, con timeout y caché; no hay timers globales de PowerShell.
|
|
23
26
|
|
|
24
27
|
### Requisitos
|
|
25
28
|
|
|
@@ -36,7 +39,7 @@ npm run install-font
|
|
|
36
39
|
opencode
|
|
37
40
|
```
|
|
38
41
|
|
|
39
|
-
`npm run install-font` instala **Hack Nerd Font** únicamente para el usuario actual
|
|
42
|
+
`npm run install-font` ejecuta el instalador compilado incluido en `dist/`, instala **Hack Nerd Font** únicamente para el usuario actual, no se ejecuta automáticamente durante `npm install` y solicita confirmación antes de descargar la fuente oficial.
|
|
40
43
|
|
|
41
44
|
Comandos adicionales:
|
|
42
45
|
|
|
@@ -55,7 +58,7 @@ La fuente se instala solo para el usuario actual:
|
|
|
55
58
|
- macOS: `~/Library/Fonts`
|
|
56
59
|
- Windows: `%LOCALAPPDATA%\\Microsoft\\Windows\\Fonts`
|
|
57
60
|
|
|
58
|
-
Después de instalarla, selecciona **Hack Nerd Font Mono** en la terminal y reinicia la terminal y OpenCode.
|
|
61
|
+
Después de instalarla, selecciona **Hack Nerd Font Mono** en la terminal y reinicia la terminal y OpenCode. En Windows, para optar explícitamente por iconos Nerd Font define `OPENCODE_MONITOR_NERD_FONT=1`; no se activa solo por detectar archivos instalados.
|
|
59
62
|
|
|
60
63
|
### Publicación e instalación desde npm
|
|
61
64
|
|
|
@@ -72,6 +75,8 @@ En otro equipo:
|
|
|
72
75
|
opencode plugin opencode-system-metrics-tui
|
|
73
76
|
```
|
|
74
77
|
|
|
78
|
+
El paquete exporta tanto la raíz (`opencode-system-metrics-tui`) como `./tui`; ambas rutas cargan el mismo bundle TUI.
|
|
79
|
+
|
|
75
80
|
El instalador de OpenCode configura el plugin TUI automáticamente. Reinicia OpenCode después de instalarlo.
|
|
76
81
|
|
|
77
82
|
Publicar en npm e instalar en OpenCode son pasos distintos:
|
|
@@ -99,11 +104,14 @@ npm run build
|
|
|
99
104
|
- GPU: utilization and temperature.
|
|
100
105
|
- GPU VRAM: used memory, total memory, and percentage.
|
|
101
106
|
- Network: live download and upload speeds.
|
|
102
|
-
-
|
|
107
|
+
- CPU, RAM, and network use the 2-second general cadence; GPU, VRAM, and temperatures use an expiring 10-second cache to reduce cost without freezing sensors.
|
|
103
108
|
- Uses the active OpenCode theme colors.
|
|
104
|
-
-
|
|
109
|
+
- Windows uses Unicode icons by default (installed-font detection does not change selection); Linux/macOS use Nerd Font only when installed and retain Unicode fallback.
|
|
105
110
|
- On Windows, visible Unicode icons are used by default because OpenCode cannot detect the terminal's active font.
|
|
106
111
|
- Supports Linux, macOS, and Windows when the operating system exposes the metrics.
|
|
112
|
+
- On Windows, CPU temperature requires LibreHardwareMonitor running as administrator with **Remote Web Server** enabled at `http://127.0.0.1:8085`.
|
|
113
|
+
- On Windows, network speed uses PowerShell and `Get-NetAdapterStatistics`; it adds no npm dependency.
|
|
114
|
+
- Optional Windows reads are on demand, timeout-bounded, and cached; no global PowerShell timers are used.
|
|
107
115
|
|
|
108
116
|
### Requirements
|
|
109
117
|
|
|
@@ -120,7 +128,7 @@ npm run install-font
|
|
|
120
128
|
opencode
|
|
121
129
|
```
|
|
122
130
|
|
|
123
|
-
`npm run install-font` installs **Hack Nerd Font** for the current user only
|
|
131
|
+
`npm run install-font` runs the compiled installer included in `dist/`, installs **Hack Nerd Font** for the current user only, is not executed automatically by `npm install`, and asks for confirmation before downloading the official font.
|
|
124
132
|
|
|
125
133
|
Extra commands:
|
|
126
134
|
|
|
@@ -139,7 +147,7 @@ The font is installed for the current user only:
|
|
|
139
147
|
- macOS: `~/Library/Fonts`
|
|
140
148
|
- Windows: `%LOCALAPPDATA%\\Microsoft\\Windows\\Fonts`
|
|
141
149
|
|
|
142
|
-
After installation, select **Hack Nerd Font Mono** in the terminal and restart the terminal and OpenCode.
|
|
150
|
+
After installation, select **Hack Nerd Font Mono** in the terminal and restart the terminal and OpenCode. On Windows, explicitly opt into Nerd Font icons with `OPENCODE_MONITOR_NERD_FONT=1`; installed-font detection alone never enables them.
|
|
143
151
|
|
|
144
152
|
### Publishing and npm installation
|
|
145
153
|
|
|
@@ -156,6 +164,8 @@ On another machine:
|
|
|
156
164
|
opencode plugin opencode-system-metrics-tui
|
|
157
165
|
```
|
|
158
166
|
|
|
167
|
+
The package exports both the root (`opencode-system-metrics-tui`) and `./tui`; both paths load the same TUI bundle.
|
|
168
|
+
|
|
159
169
|
The OpenCode installer configures the TUI plugin automatically. Restart OpenCode after installation.
|
|
160
170
|
|
|
161
171
|
Publishing to npm and installing in OpenCode are separate steps:
|
|
@@ -183,11 +193,14 @@ npm run build
|
|
|
183
193
|
- GPU: utilização e temperatura.
|
|
184
194
|
- GPU VRAM: memória usada, total e porcentagem.
|
|
185
195
|
- Rede: velocidades de download e upload em tempo real.
|
|
186
|
-
-
|
|
196
|
+
- CPU, RAM e rede usam a cadência geral de 2 segundos; GPU, VRAM e temperaturas usam uma cache expirável de 10 segundos para reduzir o custo sem congelar os sensores.
|
|
187
197
|
- Usa as cores do tema ativo do OpenCode.
|
|
188
|
-
-
|
|
198
|
+
- No Windows, os ícones Unicode são usados por padrão (detectar fontes instaladas não muda a seleção); Linux/macOS usam Nerd Font apenas quando instalada e mantêm fallback Unicode.
|
|
189
199
|
- No Windows, ícones Unicode visíveis são usados por padrão porque o OpenCode não consegue detectar a fonte ativa do terminal.
|
|
190
200
|
- Suporte para Linux, macOS e Windows quando o sistema disponibiliza as métricas.
|
|
201
|
+
- No Windows, a temperatura da CPU requer o LibreHardwareMonitor executado como administrador com o **Remote Web Server** ativo em `http://127.0.0.1:8085`.
|
|
202
|
+
- No Windows, a velocidade da rede usa o PowerShell e `Get-NetAdapterStatistics`; nenhuma dependência npm é adicionada.
|
|
203
|
+
- As leituras opcionais do Windows são sob demanda, têm timeout e cache; não há timers globais do PowerShell.
|
|
191
204
|
|
|
192
205
|
### Requisitos
|
|
193
206
|
|
|
@@ -204,7 +217,7 @@ npm run install-font
|
|
|
204
217
|
opencode
|
|
205
218
|
```
|
|
206
219
|
|
|
207
|
-
`npm run install-font` instala a **Hack Nerd Font** somente para o usuário atual
|
|
220
|
+
`npm run install-font` executa o instalador compilado incluído em `dist/`, instala a **Hack Nerd Font** somente para o usuário atual, não é executado automaticamente durante `npm install` e pede confirmação antes de baixar a fonte oficial.
|
|
208
221
|
|
|
209
222
|
Comandos adicionais:
|
|
210
223
|
|
|
@@ -223,7 +236,7 @@ A fonte é instalada somente para o usuário atual:
|
|
|
223
236
|
- macOS: `~/Library/Fonts`
|
|
224
237
|
- Windows: `%LOCALAPPDATA%\\Microsoft\\Windows\\Fonts`
|
|
225
238
|
|
|
226
|
-
Depois da instalação, selecione **Hack Nerd Font Mono** no terminal e reinicie o terminal e o OpenCode.
|
|
239
|
+
Depois da instalação, selecione **Hack Nerd Font Mono** no terminal e reinicie o terminal e o OpenCode. No Windows, opte explicitamente pelos ícones Nerd Font com `OPENCODE_MONITOR_NERD_FONT=1`; detectar arquivos instalados sozinho nunca os ativa.
|
|
227
240
|
|
|
228
241
|
### Publicação e instalação pelo npm
|
|
229
242
|
|
|
@@ -240,6 +253,8 @@ Em outro computador:
|
|
|
240
253
|
opencode plugin opencode-system-metrics-tui
|
|
241
254
|
```
|
|
242
255
|
|
|
256
|
+
O pacote exporta tanto a raiz (`opencode-system-metrics-tui`) quanto `./tui`; os dois caminhos carregam o mesmo bundle TUI.
|
|
257
|
+
|
|
243
258
|
O instalador do OpenCode configura o plugin TUI automaticamente. Reinicie o OpenCode após a instalação.
|
|
244
259
|
|
|
245
260
|
Publicar no npm e instalar no OpenCode são etapas diferentes:
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// scripts/install-hack-font.ts
|
|
2
|
+
import { cp, mkdir, mkdtemp, readdir, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { basename, join } from "node:path";
|
|
5
|
+
import { execFile as execFileCallback } from "node:child_process";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
|
|
8
|
+
// src/font.ts
|
|
9
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { join as posixJoin, win32 } from "node:path";
|
|
12
|
+
var NERD_FONT_FILE_PATTERN = /NerdFont[A-Za-z0-9-]*[-_][A-Za-z0-9-]+\.(?:ttf|otf)$/i;
|
|
13
|
+
function getFontDirectories(platform, home = homedir(), env = process.env) {
|
|
14
|
+
if (platform === "darwin")
|
|
15
|
+
return [posixJoin(home, "Library", "Fonts"), "/Library/Fonts", "/System/Library/Fonts"];
|
|
16
|
+
if (platform === "win32") {
|
|
17
|
+
const localAppData = env.LOCALAPPDATA ?? win32.join(home, "AppData", "Local");
|
|
18
|
+
const windows = env.WINDIR ?? "C:\\Windows";
|
|
19
|
+
return [win32.join(localAppData, "Microsoft", "Windows", "Fonts"), win32.join(windows, "Fonts")];
|
|
20
|
+
}
|
|
21
|
+
return [posixJoin(home, ".local", "share", "fonts"), posixJoin(home, ".fonts"), "/usr/local/share/fonts", "/usr/share/fonts"];
|
|
22
|
+
}
|
|
23
|
+
function isNerdFontFile(fileName) {
|
|
24
|
+
return NERD_FONT_FILE_PATTERN.test(fileName);
|
|
25
|
+
}
|
|
26
|
+
var isHackNerdFontFile = isNerdFontFile;
|
|
27
|
+
function hasNerdFont(platform = process.platform, home = homedir(), env = process.env) {
|
|
28
|
+
const containsFont = (directory) => {
|
|
29
|
+
if (!existsSync(directory))
|
|
30
|
+
return false;
|
|
31
|
+
try {
|
|
32
|
+
return readdirSync(directory, { withFileTypes: true }).some((entry) => {
|
|
33
|
+
const path = platform === "win32" ? win32.join(directory, entry.name) : posixJoin(directory, entry.name);
|
|
34
|
+
return entry.isFile() ? isNerdFontFile(entry.name) : entry.isDirectory() && containsFont(path);
|
|
35
|
+
});
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
return getFontDirectories(platform, home, env).some(containsFont);
|
|
41
|
+
}
|
|
42
|
+
var hasHackNerdFont = hasNerdFont;
|
|
43
|
+
var nerdFontIcons = {
|
|
44
|
+
title: "\uDB80\uDF79",
|
|
45
|
+
cpu: "\uDB83\uDEE0",
|
|
46
|
+
ram: "\uDB81\uDE1A",
|
|
47
|
+
gpu: "\uDB82\uDCAE",
|
|
48
|
+
vram: "\uDB80\uDF5B",
|
|
49
|
+
thermometer: "",
|
|
50
|
+
network: "\uDB81\uDEF3"
|
|
51
|
+
};
|
|
52
|
+
var unicodeIcons = { title: "▦", cpu: "▣", ram: "▤", gpu: "◆", vram: "◈", thermometer: "♨", network: "⇅" };
|
|
53
|
+
function getMetricIcons(useNerdFont) {
|
|
54
|
+
return useNerdFont ? nerdFontIcons : unicodeIcons;
|
|
55
|
+
}
|
|
56
|
+
function shouldUseNerdFont(platform, detected, env = process.env) {
|
|
57
|
+
if (platform === "win32")
|
|
58
|
+
return env.OPENCODE_MONITOR_NERD_FONT === "1";
|
|
59
|
+
return detected;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// scripts/install-hack-font.ts
|
|
63
|
+
var execFile = promisify(execFileCallback);
|
|
64
|
+
var RELEASE_URL = "https://github.com/ryanoasis/nerd-fonts/releases/download/v3.4.0/Hack.zip";
|
|
65
|
+
function printHelp() {
|
|
66
|
+
console.log(`Install Hack Nerd Font for the current user.
|
|
67
|
+
|
|
68
|
+
Usage:
|
|
69
|
+
npm run install-font Ask before downloading and installing
|
|
70
|
+
npm run install-font --check Detect only; never downloads or changes files
|
|
71
|
+
npm run install-font --help Show this help
|
|
72
|
+
npm run install-font --yes Skip the download confirmation`);
|
|
73
|
+
}
|
|
74
|
+
async function confirm() {
|
|
75
|
+
if (!process.stdin.isTTY)
|
|
76
|
+
return false;
|
|
77
|
+
process.stdout.write("Download Hack Nerd Font from the official Nerd Fonts release? [y/N] ");
|
|
78
|
+
for await (const chunk of process.stdin)
|
|
79
|
+
return /^y(es)?$/i.test(String(chunk).trim());
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
async function findFontFiles(directory) {
|
|
83
|
+
const files = [];
|
|
84
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
85
|
+
const path = join(directory, entry.name);
|
|
86
|
+
if (entry.isDirectory())
|
|
87
|
+
files.push(...await findFontFiles(path));
|
|
88
|
+
else if (entry.isFile() && isHackNerdFontFile(entry.name))
|
|
89
|
+
files.push(path);
|
|
90
|
+
}
|
|
91
|
+
return files;
|
|
92
|
+
}
|
|
93
|
+
async function extractZip(zipPath, destination, platform) {
|
|
94
|
+
try {
|
|
95
|
+
if (platform === "win32") {
|
|
96
|
+
await execFile("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", "Expand-Archive -LiteralPath $args[0] -DestinationPath $args[1] -Force", zipPath, destination]);
|
|
97
|
+
} else {
|
|
98
|
+
await execFile("unzip", ["-q", "-o", zipPath, "-d", destination]);
|
|
99
|
+
}
|
|
100
|
+
} catch (error) {
|
|
101
|
+
throw new Error(`Could not extract the ZIP archive. Install unzip (Linux/macOS) or use PowerShell (Windows). ${error instanceof Error ? error.message : String(error)}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async function registerWindowsFonts(fontFiles) {
|
|
105
|
+
const script = "$key='HKCU:\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Fonts'; New-Item -Path $key -Force | Out-Null; foreach ($font in $args) { New-ItemProperty -Path $key -Name ([IO.Path]::GetFileNameWithoutExtension($font)) -Value $font -PropertyType String -Force | Out-Null }";
|
|
106
|
+
await execFile("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script, ...fontFiles]);
|
|
107
|
+
}
|
|
108
|
+
async function install() {
|
|
109
|
+
const platform = process.platform;
|
|
110
|
+
const supportedPlatforms = ["linux", "darwin", "win32"];
|
|
111
|
+
if (!supportedPlatforms.includes(platform))
|
|
112
|
+
throw new Error(`Unsupported platform: ${process.platform}`);
|
|
113
|
+
if (hasHackNerdFont(platform))
|
|
114
|
+
return console.log("Hack Nerd Font is already installed; nothing to do.");
|
|
115
|
+
if (!(process.argv.includes("--yes") || await confirm()))
|
|
116
|
+
return console.log("Installation cancelled. Use --yes to confirm non-interactively.");
|
|
117
|
+
const temporaryDirectory = await mkdtemp(join(tmpdir(), "opencode-monitor-font-"));
|
|
118
|
+
try {
|
|
119
|
+
console.log(`Downloading ${RELEASE_URL}`);
|
|
120
|
+
const response = await fetch(RELEASE_URL);
|
|
121
|
+
if (!response.ok)
|
|
122
|
+
throw new Error(`Download failed with HTTP ${response.status} ${response.statusText}`);
|
|
123
|
+
const zipPath = join(temporaryDirectory, "Hack.zip");
|
|
124
|
+
await writeFile(zipPath, Buffer.from(await response.arrayBuffer()));
|
|
125
|
+
const extractedDirectory = join(temporaryDirectory, "extracted");
|
|
126
|
+
await mkdir(extractedDirectory);
|
|
127
|
+
await extractZip(zipPath, extractedDirectory, platform);
|
|
128
|
+
const fontFiles = await findFontFiles(extractedDirectory);
|
|
129
|
+
if (fontFiles.length === 0)
|
|
130
|
+
throw new Error("The downloaded archive did not contain Hack Nerd Font files.");
|
|
131
|
+
const targetDirectory = getFontDirectories(platform)[0];
|
|
132
|
+
await mkdir(targetDirectory, { recursive: true });
|
|
133
|
+
const installedFiles = fontFiles.map((file) => join(targetDirectory, basename(file)));
|
|
134
|
+
for (let index = 0;index < fontFiles.length; index += 1)
|
|
135
|
+
await cp(fontFiles[index], installedFiles[index], { force: true });
|
|
136
|
+
if (platform === "win32") {
|
|
137
|
+
try {
|
|
138
|
+
await registerWindowsFonts(installedFiles);
|
|
139
|
+
} catch {
|
|
140
|
+
console.warn("Fonts were copied, but Windows font registration failed. Select Hack Nerd Font in the terminal manually.");
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
console.log(`Installed ${fontFiles.length} font files in ${targetDirectory}. Select Hack Nerd Font in your terminal and restart OpenCode.`);
|
|
144
|
+
} finally {
|
|
145
|
+
await rm(temporaryDirectory, { recursive: true, force: true });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (process.argv.includes("--help"))
|
|
149
|
+
printHelp();
|
|
150
|
+
else if (process.argv.includes("--check"))
|
|
151
|
+
console.log(hasHackNerdFont() ? "Hack Nerd Font is installed." : "Hack Nerd Font is not installed.");
|
|
152
|
+
else
|
|
153
|
+
install().catch((error) => {
|
|
154
|
+
console.error(`Hack Nerd Font installation could not be completed: ${error instanceof Error ? error.message : String(error)}`);
|
|
155
|
+
process.exitCode = 1;
|
|
156
|
+
});
|
package/dist/tui.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// @bun
|
|
2
1
|
// src/tui.tsx
|
|
3
2
|
import { createComponent as _$createComponent } from "@opentui/solid";
|
|
4
3
|
import { effect as _$effect } from "@opentui/solid";
|
|
@@ -13,19 +12,19 @@ import { TextAttributes } from "@opentui/core";
|
|
|
13
12
|
// src/format.ts
|
|
14
13
|
var BYTES_PER_GIB = 1024 ** 3;
|
|
15
14
|
function formatPercent(value) {
|
|
16
|
-
return value === null || !Number.isFinite(value) ? "
|
|
15
|
+
return value === null || !Number.isFinite(value) ? "—" : `${Math.round(value)}%`;
|
|
17
16
|
}
|
|
18
17
|
function formatTemperature(value) {
|
|
19
|
-
return value === null || !Number.isFinite(value) ? "
|
|
18
|
+
return value === null || !Number.isFinite(value) ? "—" : `${Math.round(value)}°C`;
|
|
20
19
|
}
|
|
21
20
|
function formatGiB(bytes) {
|
|
22
21
|
if (bytes === null || !Number.isFinite(bytes))
|
|
23
|
-
return "
|
|
22
|
+
return "—";
|
|
24
23
|
return `${(bytes / BYTES_PER_GIB).toFixed(1)} GiB`;
|
|
25
24
|
}
|
|
26
25
|
function formatRate(bytesPerSecond) {
|
|
27
26
|
if (bytesPerSecond === null || !Number.isFinite(bytesPerSecond))
|
|
28
|
-
return "
|
|
27
|
+
return "—";
|
|
29
28
|
const units = ["B/s", "KiB/s", "MiB/s", "GiB/s"];
|
|
30
29
|
let value = Math.max(0, bytesPerSecond);
|
|
31
30
|
let unit = 0;
|
|
@@ -37,7 +36,50 @@ function formatRate(bytesPerSecond) {
|
|
|
37
36
|
}
|
|
38
37
|
|
|
39
38
|
// src/metrics.ts
|
|
39
|
+
import { execFile } from "node:child_process";
|
|
40
|
+
import { promisify } from "node:util";
|
|
40
41
|
import si from "systeminformation";
|
|
42
|
+
var execFileAsync = promisify(execFile);
|
|
43
|
+
function parseMonitorTemperature(value) {
|
|
44
|
+
if (value === undefined)
|
|
45
|
+
return;
|
|
46
|
+
const parsed = Number(value.replace(",", ".").replace(/[^\d.-]/g, ""));
|
|
47
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
48
|
+
}
|
|
49
|
+
async function readLibreHardwareMonitorTemperature() {
|
|
50
|
+
const response = await fetch("http://127.0.0.1:8085/data.json", { signal: AbortSignal.timeout(2000) });
|
|
51
|
+
if (!response.ok)
|
|
52
|
+
return;
|
|
53
|
+
const root = await response.json();
|
|
54
|
+
const sensors = [];
|
|
55
|
+
const visit = (node) => {
|
|
56
|
+
if (node.Type === "Temperature" && node.Text !== undefined)
|
|
57
|
+
sensors.push(node);
|
|
58
|
+
for (const child of node.Children ?? [])
|
|
59
|
+
visit(child);
|
|
60
|
+
};
|
|
61
|
+
visit(root);
|
|
62
|
+
const cpuSensor = sensors.find((sensor) => sensor.Text === "CPU Package") ?? sensors.find((sensor) => sensor.Text === "Core Average") ?? sensors.find((sensor) => sensor.Text === "Core Max");
|
|
63
|
+
return parseMonitorTemperature(cpuSensor?.Value);
|
|
64
|
+
}
|
|
65
|
+
function parseWindowsNetworkOutput(stdout) {
|
|
66
|
+
const parsed = JSON.parse(stdout.trim());
|
|
67
|
+
const rows = Array.isArray(parsed) ? parsed : [parsed];
|
|
68
|
+
return rows.map((item) => ({
|
|
69
|
+
iface: item.Name,
|
|
70
|
+
rx_bytes: finite(item.ReceivedBytes) ?? 0,
|
|
71
|
+
tx_bytes: finite(item.SentBytes) ?? 0
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
async function readWindowsNetworkSample() {
|
|
75
|
+
const { stdout } = await execFileAsync("powershell.exe", [
|
|
76
|
+
"-NoProfile",
|
|
77
|
+
"-NonInteractive",
|
|
78
|
+
"-Command",
|
|
79
|
+
"Get-NetAdapterStatistics | Select-Object Name,ReceivedBytes,SentBytes | ConvertTo-Json -Compress"
|
|
80
|
+
], { windowsHide: true, timeout: 15000, maxBuffer: 1024 * 1024 });
|
|
81
|
+
return parseWindowsNetworkOutput(stdout);
|
|
82
|
+
}
|
|
41
83
|
var previousNetwork;
|
|
42
84
|
var unavailable = {
|
|
43
85
|
cpuPercent: null,
|
|
@@ -70,62 +112,139 @@ async function withTimeout(promise, timeoutMs) {
|
|
|
70
112
|
clearTimeout(timeout);
|
|
71
113
|
}
|
|
72
114
|
}
|
|
115
|
+
var SLOW_METRIC_CACHE_MS = 1e4;
|
|
116
|
+
var TEMPERATURE_CACHE_MS = 1e4;
|
|
117
|
+
var isWindows = process.platform === "win32";
|
|
118
|
+
var NETWORK_CACHE_MS = 2000;
|
|
119
|
+
function selectGpuMetrics(graphics) {
|
|
120
|
+
const empty = {
|
|
121
|
+
gpuPercent: null,
|
|
122
|
+
gpuTemperatureCelsius: null,
|
|
123
|
+
gpuMemoryUsedBytes: null,
|
|
124
|
+
gpuMemoryTotalBytes: null,
|
|
125
|
+
gpuMemoryPercent: null
|
|
126
|
+
};
|
|
127
|
+
if (graphics === undefined)
|
|
128
|
+
return empty;
|
|
129
|
+
const controller = graphics.controllers.map((item) => {
|
|
130
|
+
const utilization = finite(item.utilizationGpu);
|
|
131
|
+
const temperature = finite(item.temperatureGpu);
|
|
132
|
+
const memoryTotal = finite(item.memoryTotal);
|
|
133
|
+
const memoryFree = finite(item.memoryFree);
|
|
134
|
+
const memoryUsed = finite(item.memoryUsed) ?? (memoryTotal !== null && memoryFree !== null ? Math.max(0, memoryTotal - memoryFree) : null);
|
|
135
|
+
const hasMemory = memoryUsed !== null && memoryTotal !== null && memoryTotal > 0;
|
|
136
|
+
return { utilization, temperature, memoryUsed, memoryTotal, hasMemory };
|
|
137
|
+
}).filter(({ utilization, temperature, hasMemory }) => utilization !== null || temperature !== null || hasMemory).sort((left, right) => {
|
|
138
|
+
const score = (value) => (value.hasMemory ? 2 : 0) + (value.utilization !== null ? 1 : 0);
|
|
139
|
+
return score(right) - score(left) || (right.utilization ?? -Infinity) - (left.utilization ?? -Infinity);
|
|
140
|
+
})[0];
|
|
141
|
+
if (controller === undefined)
|
|
142
|
+
return empty;
|
|
143
|
+
const result = {
|
|
144
|
+
gpuPercent: controller.utilization ?? 0,
|
|
145
|
+
gpuTemperatureCelsius: controller.temperature,
|
|
146
|
+
gpuMemoryUsedBytes: null,
|
|
147
|
+
gpuMemoryTotalBytes: null,
|
|
148
|
+
gpuMemoryPercent: null
|
|
149
|
+
};
|
|
150
|
+
if (controller.hasMemory) {
|
|
151
|
+
result.gpuMemoryUsedBytes = controller.memoryUsed * 1024 ** 2;
|
|
152
|
+
result.gpuMemoryTotalBytes = controller.memoryTotal * 1024 ** 2;
|
|
153
|
+
result.gpuMemoryPercent = controller.memoryUsed / controller.memoryTotal * 100;
|
|
154
|
+
}
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
function readCachedMetric(state, reader, cacheMs = SLOW_METRIC_CACHE_MS, now = Date.now()) {
|
|
158
|
+
if (state.inFlight !== undefined)
|
|
159
|
+
return state.inFlight;
|
|
160
|
+
if (now - state.lastAttemptAt < cacheMs)
|
|
161
|
+
return Promise.resolve(state.value);
|
|
162
|
+
state.lastAttemptAt = now;
|
|
163
|
+
state.inFlight = reader().then((value) => {
|
|
164
|
+
state.value = value;
|
|
165
|
+
return value;
|
|
166
|
+
}).catch(() => state.value).finally(() => {
|
|
167
|
+
state.inFlight = undefined;
|
|
168
|
+
});
|
|
169
|
+
return state.inFlight;
|
|
170
|
+
}
|
|
171
|
+
var cachedCpuTemperature = {
|
|
172
|
+
value: undefined,
|
|
173
|
+
lastAttemptAt: 0,
|
|
174
|
+
inFlight: undefined
|
|
175
|
+
};
|
|
176
|
+
var cachedGraphics = {
|
|
177
|
+
value: undefined,
|
|
178
|
+
lastAttemptAt: 0,
|
|
179
|
+
inFlight: undefined
|
|
180
|
+
};
|
|
181
|
+
var cachedNetwork = {
|
|
182
|
+
value: undefined,
|
|
183
|
+
lastAttemptAt: 0,
|
|
184
|
+
inFlight: undefined
|
|
185
|
+
};
|
|
186
|
+
var previousWindowsNetwork;
|
|
187
|
+
async function readWindowsNetworkMetrics() {
|
|
188
|
+
const samples = await readWindowsNetworkSample();
|
|
189
|
+
const timestamp = Date.now();
|
|
190
|
+
const rx = samples.reduce((total, item) => total + (finite(item.rx_bytes) ?? 0), 0);
|
|
191
|
+
const tx = samples.reduce((total, item) => total + (finite(item.tx_bytes) ?? 0), 0);
|
|
192
|
+
const seconds = previousWindowsNetwork === undefined ? undefined : Math.max((timestamp - previousWindowsNetwork.timestamp) / 1000, 0.001);
|
|
193
|
+
const result = [{
|
|
194
|
+
rx_bytes: rx,
|
|
195
|
+
tx_bytes: tx,
|
|
196
|
+
rx_sec: seconds === undefined ? undefined : Math.max(0, rx - previousWindowsNetwork.rx) / seconds,
|
|
197
|
+
tx_sec: seconds === undefined ? undefined : Math.max(0, tx - previousWindowsNetwork.tx) / seconds
|
|
198
|
+
}];
|
|
199
|
+
previousWindowsNetwork = { timestamp, rx, tx };
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
async function readWindowsCpuTemperature() {
|
|
203
|
+
let value;
|
|
204
|
+
try {
|
|
205
|
+
value = await readLibreHardwareMonitorTemperature();
|
|
206
|
+
} catch {}
|
|
207
|
+
if (value === undefined) {
|
|
208
|
+
try {
|
|
209
|
+
const fallback = await si.cpuTemperature();
|
|
210
|
+
value = finite(fallback.main) ?? finite(fallback.max) ?? undefined;
|
|
211
|
+
} catch {}
|
|
212
|
+
}
|
|
213
|
+
return value === undefined ? undefined : { main: value };
|
|
214
|
+
}
|
|
73
215
|
async function readMetrics() {
|
|
74
216
|
const [load, memory] = await Promise.allSettled([
|
|
75
|
-
si.currentLoad(),
|
|
76
|
-
si.mem()
|
|
217
|
+
withTimeout(si.currentLoad(), 1500),
|
|
218
|
+
withTimeout(si.mem(), 1500)
|
|
77
219
|
]);
|
|
78
220
|
const [temperature, graphics, network] = await Promise.allSettled([
|
|
79
|
-
withTimeout(si.cpuTemperature(),
|
|
80
|
-
withTimeout(si.graphics(),
|
|
81
|
-
withTimeout(si.networkStats("*"), 1500)
|
|
221
|
+
withTimeout(readCachedMetric(cachedCpuTemperature, isWindows ? readWindowsCpuTemperature : () => si.cpuTemperature(), TEMPERATURE_CACHE_MS), 2500),
|
|
222
|
+
withTimeout(readCachedMetric(cachedGraphics, () => si.graphics(), SLOW_METRIC_CACHE_MS), 2500),
|
|
223
|
+
withTimeout(readCachedMetric(cachedNetwork, isWindows ? readWindowsNetworkMetrics : () => si.networkStats("*"), isWindows ? NETWORK_CACHE_MS : 2000), 1500)
|
|
82
224
|
]);
|
|
83
225
|
const cpuPercent = load.status === "fulfilled" ? finite(load.value.currentLoad) : null;
|
|
84
226
|
const memoryTotalBytes = memory.status === "fulfilled" ? finite(memory.value.total) : null;
|
|
85
227
|
const memoryAvailableBytes = memory.status === "fulfilled" ? finite(memory.value.available) : null;
|
|
86
228
|
const memoryUsedBytes = memoryTotalBytes !== null && memoryAvailableBytes !== null ? Math.max(0, memoryTotalBytes - memoryAvailableBytes) : memory.status === "fulfilled" ? finite(memory.value.used) : null;
|
|
87
229
|
const memoryPercent = memoryUsedBytes !== null && memoryTotalBytes !== null && memoryTotalBytes > 0 ? memoryUsedBytes / memoryTotalBytes * 100 : null;
|
|
88
|
-
|
|
89
|
-
let gpuTemperatureCelsius = null;
|
|
90
|
-
let gpuMemoryUsedBytes = null;
|
|
91
|
-
let gpuMemoryTotalBytes = null;
|
|
92
|
-
let gpuMemoryPercent = null;
|
|
93
|
-
if (graphics.status === "fulfilled") {
|
|
94
|
-
const controller = graphics.value.controllers.map((item) => {
|
|
95
|
-
const utilization = finite(item.utilizationGpu);
|
|
96
|
-
const temperature = finite(item.temperatureGpu);
|
|
97
|
-
const memoryUsed = finite(item.memoryUsed);
|
|
98
|
-
const memoryTotal = finite(item.memoryTotal);
|
|
99
|
-
const hasMemory = memoryUsed !== null && memoryTotal !== null && memoryTotal > 0;
|
|
100
|
-
return { item, utilization, temperature, memoryUsed, memoryTotal, hasMemory };
|
|
101
|
-
}).filter(({ utilization, temperature, hasMemory }) => utilization !== null || temperature !== null || hasMemory).sort((left, right) => {
|
|
102
|
-
const score = (value) => (value.hasMemory ? 2 : 0) + (value.utilization !== null ? 1 : 0);
|
|
103
|
-
const scoreDifference = score(right) - score(left);
|
|
104
|
-
if (scoreDifference !== 0)
|
|
105
|
-
return scoreDifference;
|
|
106
|
-
return (right.utilization ?? -Infinity) - (left.utilization ?? -Infinity);
|
|
107
|
-
})[0];
|
|
108
|
-
if (controller !== undefined) {
|
|
109
|
-
gpuPercent = controller.utilization ?? (controller.hasMemory ? 0 : null);
|
|
110
|
-
gpuTemperatureCelsius = controller.temperature;
|
|
111
|
-
if (controller.hasMemory) {
|
|
112
|
-
gpuMemoryUsedBytes = controller.memoryUsed * 1024 ** 2;
|
|
113
|
-
gpuMemoryTotalBytes = controller.memoryTotal * 1024 ** 2;
|
|
114
|
-
gpuMemoryPercent = controller.memoryUsed / controller.memoryTotal * 100;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
230
|
+
const gpu = selectGpuMetrics(graphics.status === "fulfilled" ? graphics.value : undefined);
|
|
118
231
|
let downloadBytesPerSecond = null;
|
|
119
232
|
let uploadBytesPerSecond = null;
|
|
120
|
-
if (network.status === "fulfilled") {
|
|
233
|
+
if (network.status === "fulfilled" && network.value !== undefined) {
|
|
121
234
|
const samples = network.value;
|
|
122
235
|
const timestamp = Date.now();
|
|
123
236
|
const rx = samples.reduce((total, item) => total + (finite(item.rx_bytes) ?? 0), 0);
|
|
124
237
|
const tx = samples.reduce((total, item) => total + (finite(item.tx_bytes) ?? 0), 0);
|
|
125
|
-
if (
|
|
238
|
+
if (isWindows) {
|
|
239
|
+
downloadBytesPerSecond = finite(samples[0]?.rx_sec) ?? null;
|
|
240
|
+
uploadBytesPerSecond = finite(samples[0]?.tx_sec) ?? null;
|
|
241
|
+
} else if (previousNetwork !== undefined) {
|
|
126
242
|
const seconds = Math.max((timestamp - previousNetwork.timestamp) / 1000, 0.001);
|
|
127
243
|
downloadBytesPerSecond = Math.max(0, rx - previousNetwork.rx) / seconds;
|
|
128
244
|
uploadBytesPerSecond = Math.max(0, tx - previousNetwork.tx) / seconds;
|
|
245
|
+
} else {
|
|
246
|
+
downloadBytesPerSecond = finite(samples[0]?.rx_sec) ?? null;
|
|
247
|
+
uploadBytesPerSecond = finite(samples[0]?.tx_sec) ?? null;
|
|
129
248
|
}
|
|
130
249
|
previousNetwork = { timestamp, rx, tx };
|
|
131
250
|
}
|
|
@@ -135,22 +254,22 @@ async function readMetrics() {
|
|
|
135
254
|
memoryUsedBytes,
|
|
136
255
|
memoryTotalBytes,
|
|
137
256
|
memoryPercent,
|
|
138
|
-
gpuPercent,
|
|
139
|
-
cpuTemperatureCelsius: temperature.status === "fulfilled" ? finite(temperature.value.main) ?? finite(temperature.value.max) : null,
|
|
140
|
-
gpuTemperatureCelsius,
|
|
141
|
-
gpuMemoryUsedBytes,
|
|
142
|
-
gpuMemoryTotalBytes,
|
|
143
|
-
gpuMemoryPercent,
|
|
257
|
+
gpuPercent: gpu.gpuPercent,
|
|
258
|
+
cpuTemperatureCelsius: temperature.status === "fulfilled" && temperature.value !== undefined ? finite(temperature.value.main) ?? ("max" in temperature.value ? finite(temperature.value.max) : null) : null,
|
|
259
|
+
gpuTemperatureCelsius: gpu.gpuTemperatureCelsius,
|
|
260
|
+
gpuMemoryUsedBytes: gpu.gpuMemoryUsedBytes,
|
|
261
|
+
gpuMemoryTotalBytes: gpu.gpuMemoryTotalBytes,
|
|
262
|
+
gpuMemoryPercent: gpu.gpuMemoryPercent,
|
|
144
263
|
downloadBytesPerSecond,
|
|
145
264
|
uploadBytesPerSecond
|
|
146
265
|
};
|
|
147
266
|
}
|
|
148
267
|
|
|
149
268
|
// src/font.ts
|
|
150
|
-
import { existsSync, readdirSync } from "fs";
|
|
151
|
-
import { homedir } from "os";
|
|
152
|
-
import { join as posixJoin, win32 } from "path";
|
|
153
|
-
var
|
|
269
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
270
|
+
import { homedir } from "node:os";
|
|
271
|
+
import { join as posixJoin, win32 } from "node:path";
|
|
272
|
+
var NERD_FONT_FILE_PATTERN = /NerdFont[A-Za-z0-9-]*[-_][A-Za-z0-9-]+\.(?:ttf|otf)$/i;
|
|
154
273
|
function getFontDirectories(platform, home = homedir(), env = process.env) {
|
|
155
274
|
if (platform === "darwin")
|
|
156
275
|
return [posixJoin(home, "Library", "Fonts"), "/Library/Fonts", "/System/Library/Fonts"];
|
|
@@ -161,17 +280,18 @@ function getFontDirectories(platform, home = homedir(), env = process.env) {
|
|
|
161
280
|
}
|
|
162
281
|
return [posixJoin(home, ".local", "share", "fonts"), posixJoin(home, ".fonts"), "/usr/local/share/fonts", "/usr/share/fonts"];
|
|
163
282
|
}
|
|
164
|
-
function
|
|
165
|
-
return
|
|
283
|
+
function isNerdFontFile(fileName) {
|
|
284
|
+
return NERD_FONT_FILE_PATTERN.test(fileName);
|
|
166
285
|
}
|
|
167
|
-
|
|
286
|
+
var isHackNerdFontFile = isNerdFontFile;
|
|
287
|
+
function hasNerdFont(platform = process.platform, home = homedir(), env = process.env) {
|
|
168
288
|
const containsFont = (directory) => {
|
|
169
289
|
if (!existsSync(directory))
|
|
170
290
|
return false;
|
|
171
291
|
try {
|
|
172
292
|
return readdirSync(directory, { withFileTypes: true }).some((entry) => {
|
|
173
293
|
const path = platform === "win32" ? win32.join(directory, entry.name) : posixJoin(directory, entry.name);
|
|
174
|
-
return entry.isFile() ?
|
|
294
|
+
return entry.isFile() ? isNerdFontFile(entry.name) : entry.isDirectory() && containsFont(path);
|
|
175
295
|
});
|
|
176
296
|
} catch {
|
|
177
297
|
return false;
|
|
@@ -179,71 +299,87 @@ function hasHackNerdFont(platform = process.platform, home = homedir(), env = pr
|
|
|
179
299
|
};
|
|
180
300
|
return getFontDirectories(platform, home, env).some(containsFont);
|
|
181
301
|
}
|
|
302
|
+
var hasHackNerdFont = hasNerdFont;
|
|
182
303
|
var nerdFontIcons = {
|
|
183
304
|
title: "\uDB80\uDF79",
|
|
184
305
|
cpu: "\uDB83\uDEE0",
|
|
185
306
|
ram: "\uDB81\uDE1A",
|
|
186
307
|
gpu: "\uDB82\uDCAE",
|
|
187
308
|
vram: "\uDB80\uDF5B",
|
|
188
|
-
thermometer: "
|
|
309
|
+
thermometer: "",
|
|
189
310
|
network: "\uDB81\uDEF3"
|
|
190
311
|
};
|
|
191
|
-
var unicodeIcons = { title: "
|
|
312
|
+
var unicodeIcons = { title: "▦", cpu: "▣", ram: "▤", gpu: "◆", vram: "◈", thermometer: "♨", network: "⇅" };
|
|
192
313
|
function getMetricIcons(useNerdFont) {
|
|
193
314
|
return useNerdFont ? nerdFontIcons : unicodeIcons;
|
|
194
315
|
}
|
|
316
|
+
function shouldUseNerdFont(platform, detected, env = process.env) {
|
|
317
|
+
if (platform === "win32")
|
|
318
|
+
return env.OPENCODE_MONITOR_NERD_FONT === "1";
|
|
319
|
+
return detected;
|
|
320
|
+
}
|
|
195
321
|
|
|
196
322
|
// src/tui.tsx
|
|
197
|
-
var REFRESH_INTERVAL_MS =
|
|
198
|
-
var icons = getMetricIcons(process.platform
|
|
323
|
+
var REFRESH_INTERVAL_MS = 2000;
|
|
324
|
+
var icons = getMetricIcons(shouldUseNerdFont(process.platform, hasNerdFont()));
|
|
325
|
+
var initialMetrics = {
|
|
326
|
+
cpuPercent: null,
|
|
327
|
+
memoryUsedBytes: null,
|
|
328
|
+
memoryTotalBytes: null,
|
|
329
|
+
memoryPercent: null,
|
|
330
|
+
gpuPercent: null,
|
|
331
|
+
cpuTemperatureCelsius: null,
|
|
332
|
+
gpuTemperatureCelsius: null,
|
|
333
|
+
gpuMemoryUsedBytes: null,
|
|
334
|
+
gpuMemoryTotalBytes: null,
|
|
335
|
+
gpuMemoryPercent: null,
|
|
336
|
+
downloadBytesPerSecond: null,
|
|
337
|
+
uploadBytesPerSecond: null
|
|
338
|
+
};
|
|
339
|
+
var [sharedMetrics, setSharedMetrics] = createSignal(initialMetrics);
|
|
340
|
+
var metricsTimer;
|
|
341
|
+
var metricsConsumers = 0;
|
|
342
|
+
var refreshing = false;
|
|
343
|
+
var refreshMetrics = async () => {
|
|
344
|
+
if (refreshing)
|
|
345
|
+
return;
|
|
346
|
+
refreshing = true;
|
|
347
|
+
try {
|
|
348
|
+
setSharedMetrics(await readMetrics());
|
|
349
|
+
} finally {
|
|
350
|
+
refreshing = false;
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
function startMetricsPolling() {
|
|
354
|
+
metricsConsumers += 1;
|
|
355
|
+
if (metricsConsumers !== 1)
|
|
356
|
+
return;
|
|
357
|
+
refreshMetrics();
|
|
358
|
+
metricsTimer = setInterval(() => void refreshMetrics(), REFRESH_INTERVAL_MS);
|
|
359
|
+
}
|
|
360
|
+
function stopMetricsPolling() {
|
|
361
|
+
metricsConsumers = Math.max(0, metricsConsumers - 1);
|
|
362
|
+
if (metricsConsumers !== 0 || metricsTimer === undefined)
|
|
363
|
+
return;
|
|
364
|
+
clearInterval(metricsTimer);
|
|
365
|
+
metricsTimer = undefined;
|
|
366
|
+
}
|
|
199
367
|
function systemMetricsTitle() {
|
|
200
368
|
const locale = process.env.LC_ALL ?? process.env.LANGUAGE?.split(":")[0] ?? process.env.LANG ?? Intl.DateTimeFormat().resolvedOptions().locale;
|
|
201
369
|
const language = locale.split(/[-_.]/)[0].toLowerCase();
|
|
202
370
|
return {
|
|
203
371
|
de: "Systemmetriken",
|
|
204
|
-
es: "
|
|
205
|
-
fr: "
|
|
372
|
+
es: "Métricas del sistema",
|
|
373
|
+
fr: "Métriques système",
|
|
206
374
|
it: "Metriche di sistema",
|
|
207
|
-
pt: "
|
|
375
|
+
pt: "Métricas do sistema"
|
|
208
376
|
}[language] ?? "System metrics";
|
|
209
377
|
}
|
|
210
378
|
function MetricsPanel(props) {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
memoryUsedBytes: null,
|
|
214
|
-
memoryTotalBytes: null,
|
|
215
|
-
memoryPercent: null,
|
|
216
|
-
gpuPercent: null,
|
|
217
|
-
cpuTemperatureCelsius: null,
|
|
218
|
-
gpuTemperatureCelsius: null,
|
|
219
|
-
gpuMemoryUsedBytes: null,
|
|
220
|
-
gpuMemoryTotalBytes: null,
|
|
221
|
-
gpuMemoryPercent: null,
|
|
222
|
-
downloadBytesPerSecond: null,
|
|
223
|
-
uploadBytesPerSecond: null
|
|
224
|
-
});
|
|
225
|
-
let refreshing = false;
|
|
226
|
-
const refresh = async () => {
|
|
227
|
-
if (refreshing)
|
|
228
|
-
return;
|
|
229
|
-
refreshing = true;
|
|
230
|
-
try {
|
|
231
|
-
setMetrics(await readMetrics());
|
|
232
|
-
} finally {
|
|
233
|
-
refreshing = false;
|
|
234
|
-
}
|
|
235
|
-
};
|
|
236
|
-
let timer;
|
|
237
|
-
onMount(() => {
|
|
238
|
-
refresh();
|
|
239
|
-
timer = setInterval(() => void refresh(), REFRESH_INTERVAL_MS);
|
|
240
|
-
});
|
|
241
|
-
onCleanup(() => {
|
|
242
|
-
if (timer !== undefined)
|
|
243
|
-
clearInterval(timer);
|
|
244
|
-
});
|
|
379
|
+
onMount(startMetricsPolling);
|
|
380
|
+
onCleanup(stopMetricsPolling);
|
|
245
381
|
return (() => {
|
|
246
|
-
var _el$ = _$createElement("box"), _el$2 = _$createElement("text"), _el$3 = _$createTextNode(` `), _el$4 = _$createElement("box"), _el$5 = _$createElement("text"), _el$6 = _$createElement("text"), _el$8 = _$createElement("text"), _el$9 = _$createTextNode(`
|
|
382
|
+
var _el$ = _$createElement("box"), _el$2 = _$createElement("text"), _el$3 = _$createTextNode(` `), _el$4 = _$createElement("box"), _el$5 = _$createElement("text"), _el$6 = _$createElement("text"), _el$8 = _$createElement("text"), _el$9 = _$createTextNode(` · `), _el$0 = _$createTextNode(` `), _el$1 = _$createElement("box"), _el$10 = _$createElement("text"), _el$11 = _$createElement("text"), _el$13 = _$createElement("text"), _el$14 = _$createTextNode(` / `), _el$15 = _$createTextNode(` (`), _el$16 = _$createTextNode(`)`), _el$17 = _$createElement("box"), _el$18 = _$createElement("text"), _el$19 = _$createElement("text"), _el$21 = _$createElement("text"), _el$22 = _$createTextNode(` · `), _el$23 = _$createTextNode(` `), _el$24 = _$createElement("box"), _el$25 = _$createElement("text"), _el$26 = _$createElement("text"), _el$28 = _$createElement("text"), _el$29 = _$createTextNode(` / `), _el$30 = _$createTextNode(` (`), _el$31 = _$createTextNode(`)`), _el$32 = _$createElement("box"), _el$33 = _$createElement("text"), _el$34 = _$createElement("text"), _el$36 = _$createElement("text"), _el$37 = _$createTextNode(`↓ `), _el$38 = _$createTextNode(` ↑ `);
|
|
247
383
|
_$insertNode(_el$, _el$2);
|
|
248
384
|
_$insertNode(_el$, _el$4);
|
|
249
385
|
_$insertNode(_el$, _el$1);
|
|
@@ -264,9 +400,9 @@ function MetricsPanel(props) {
|
|
|
264
400
|
_$insertNode(_el$6, _$createTextNode(` CPU `));
|
|
265
401
|
_$insertNode(_el$8, _el$9);
|
|
266
402
|
_$insertNode(_el$8, _el$0);
|
|
267
|
-
_$insert(_el$8, () => formatPercent(
|
|
403
|
+
_$insert(_el$8, () => formatPercent(sharedMetrics().cpuPercent), _el$9);
|
|
268
404
|
_$insert(_el$8, () => icons.thermometer, _el$0);
|
|
269
|
-
_$insert(_el$8, () => formatTemperature(
|
|
405
|
+
_$insert(_el$8, () => formatTemperature(sharedMetrics().cpuTemperatureCelsius), null);
|
|
270
406
|
_$insertNode(_el$1, _el$10);
|
|
271
407
|
_$insertNode(_el$1, _el$11);
|
|
272
408
|
_$insertNode(_el$1, _el$13);
|
|
@@ -276,9 +412,9 @@ function MetricsPanel(props) {
|
|
|
276
412
|
_$insertNode(_el$13, _el$14);
|
|
277
413
|
_$insertNode(_el$13, _el$15);
|
|
278
414
|
_$insertNode(_el$13, _el$16);
|
|
279
|
-
_$insert(_el$13, () => formatGiB(
|
|
280
|
-
_$insert(_el$13, () => formatGiB(
|
|
281
|
-
_$insert(_el$13, () => formatPercent(
|
|
415
|
+
_$insert(_el$13, () => formatGiB(sharedMetrics().memoryUsedBytes), _el$14);
|
|
416
|
+
_$insert(_el$13, () => formatGiB(sharedMetrics().memoryTotalBytes), _el$15);
|
|
417
|
+
_$insert(_el$13, () => formatPercent(sharedMetrics().memoryPercent), _el$16);
|
|
282
418
|
_$insertNode(_el$17, _el$18);
|
|
283
419
|
_$insertNode(_el$17, _el$19);
|
|
284
420
|
_$insertNode(_el$17, _el$21);
|
|
@@ -287,9 +423,9 @@ function MetricsPanel(props) {
|
|
|
287
423
|
_$insertNode(_el$19, _$createTextNode(` GPU `));
|
|
288
424
|
_$insertNode(_el$21, _el$22);
|
|
289
425
|
_$insertNode(_el$21, _el$23);
|
|
290
|
-
_$insert(_el$21, () => formatPercent(
|
|
426
|
+
_$insert(_el$21, () => formatPercent(sharedMetrics().gpuPercent), _el$22);
|
|
291
427
|
_$insert(_el$21, () => icons.thermometer, _el$23);
|
|
292
|
-
_$insert(_el$21, () => formatTemperature(
|
|
428
|
+
_$insert(_el$21, () => formatTemperature(sharedMetrics().gpuTemperatureCelsius), null);
|
|
293
429
|
_$insertNode(_el$24, _el$25);
|
|
294
430
|
_$insertNode(_el$24, _el$26);
|
|
295
431
|
_$insertNode(_el$24, _el$28);
|
|
@@ -299,9 +435,9 @@ function MetricsPanel(props) {
|
|
|
299
435
|
_$insertNode(_el$28, _el$29);
|
|
300
436
|
_$insertNode(_el$28, _el$30);
|
|
301
437
|
_$insertNode(_el$28, _el$31);
|
|
302
|
-
_$insert(_el$28, () => formatGiB(
|
|
303
|
-
_$insert(_el$28, () => formatGiB(
|
|
304
|
-
_$insert(_el$28, () => formatPercent(
|
|
438
|
+
_$insert(_el$28, () => formatGiB(sharedMetrics().gpuMemoryUsedBytes), _el$29);
|
|
439
|
+
_$insert(_el$28, () => formatGiB(sharedMetrics().gpuMemoryTotalBytes), _el$30);
|
|
440
|
+
_$insert(_el$28, () => formatPercent(sharedMetrics().gpuMemoryPercent), _el$31);
|
|
305
441
|
_$insertNode(_el$32, _el$33);
|
|
306
442
|
_$insertNode(_el$32, _el$34);
|
|
307
443
|
_$insertNode(_el$32, _el$36);
|
|
@@ -310,8 +446,8 @@ function MetricsPanel(props) {
|
|
|
310
446
|
_$insertNode(_el$34, _$createTextNode(` NET `));
|
|
311
447
|
_$insertNode(_el$36, _el$37);
|
|
312
448
|
_$insertNode(_el$36, _el$38);
|
|
313
|
-
_$insert(_el$36, () => formatRate(
|
|
314
|
-
_$insert(_el$36, () => formatRate(
|
|
449
|
+
_$insert(_el$36, () => formatRate(sharedMetrics().downloadBytesPerSecond), _el$38);
|
|
450
|
+
_$insert(_el$36, () => formatRate(sharedMetrics().uploadBytesPerSecond), null);
|
|
315
451
|
_$effect((_p$) => {
|
|
316
452
|
var _v$ = props.theme.text, _v$2 = TextAttributes.BOLD, _v$3 = props.theme.success, _v$4 = props.theme.text, _v$5 = props.theme.textMuted, _v$6 = props.theme.success, _v$7 = props.theme.text, _v$8 = props.theme.textMuted, _v$9 = props.theme.success, _v$0 = props.theme.text, _v$1 = props.theme.textMuted, _v$10 = props.theme.success, _v$11 = props.theme.text, _v$12 = props.theme.textMuted, _v$13 = props.theme.success, _v$14 = props.theme.text, _v$15 = props.theme.textMuted;
|
|
317
453
|
_v$ !== _p$.e && (_p$.e = _$setProp(_el$2, "fg", _v$, _p$.e));
|
package/package.json
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-system-metrics-tui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "OpenCode TUI sidebar plugin for live system metrics",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/tui.d.ts",
|
|
10
|
+
"import": "./dist/tui.js"
|
|
11
|
+
},
|
|
8
12
|
"./tui": {
|
|
9
13
|
"types": "./dist/tui.d.ts",
|
|
10
14
|
"import": "./dist/tui.js"
|
|
@@ -17,9 +21,9 @@
|
|
|
17
21
|
],
|
|
18
22
|
"scripts": {
|
|
19
23
|
"build": "bun scripts/build.ts && tsc --emitDeclarationOnly --declaration --outDir dist",
|
|
20
|
-
"test": "tsx --test test
|
|
24
|
+
"test": "tsx --test test/*.test.ts",
|
|
21
25
|
"typecheck": "tsc --noEmit",
|
|
22
|
-
"install-font": "
|
|
26
|
+
"install-font": "node dist/install-hack-font.js"
|
|
23
27
|
},
|
|
24
28
|
"engines": {
|
|
25
29
|
"node": ">=18"
|
package/scripts/build.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import solidPlugin from "@opentui/solid/bun-plugin"
|
|
2
2
|
|
|
3
3
|
const result = await Bun.build({
|
|
4
|
-
entrypoints: ["src/tui.tsx"],
|
|
4
|
+
entrypoints: ["src/tui.tsx", "scripts/install-hack-font.ts"],
|
|
5
5
|
outdir: "dist",
|
|
6
|
-
target: "
|
|
6
|
+
target: "node",
|
|
7
7
|
format: "esm",
|
|
8
8
|
plugins: [solidPlugin],
|
|
9
9
|
external: ["@opencode-ai/plugin/tui", "@opentui/core", "@opentui/solid", "solid-js", "systeminformation"],
|
|
10
|
-
naming: "
|
|
10
|
+
naming: "[name].js",
|
|
11
11
|
})
|
|
12
12
|
|
|
13
13
|
if (!result.success) {
|