opencode-system-metrics-tui 0.1.0 → 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 +48 -12
- package/dist/install-hack-font.js +156 -0
- package/dist/tui.js +245 -109
- package/package.json +7 -3
- package/scripts/build.ts +3 -3
package/README.md
CHANGED
|
@@ -15,10 +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
|
+
- En Windows usa iconos Unicode visibles de forma predeterminada, porque OpenCode no puede detectar la fuente activa de la terminal.
|
|
21
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.
|
|
22
26
|
|
|
23
27
|
### Requisitos
|
|
24
28
|
|
|
@@ -35,7 +39,7 @@ npm run install-font
|
|
|
35
39
|
opencode
|
|
36
40
|
```
|
|
37
41
|
|
|
38
|
-
`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.
|
|
39
43
|
|
|
40
44
|
Comandos adicionales:
|
|
41
45
|
|
|
@@ -54,7 +58,7 @@ La fuente se instala solo para el usuario actual:
|
|
|
54
58
|
- macOS: `~/Library/Fonts`
|
|
55
59
|
- Windows: `%LOCALAPPDATA%\\Microsoft\\Windows\\Fonts`
|
|
56
60
|
|
|
57
|
-
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.
|
|
58
62
|
|
|
59
63
|
### Publicación e instalación desde npm
|
|
60
64
|
|
|
@@ -71,8 +75,16 @@ En otro equipo:
|
|
|
71
75
|
opencode plugin opencode-system-metrics-tui
|
|
72
76
|
```
|
|
73
77
|
|
|
78
|
+
El paquete exporta tanto la raíz (`opencode-system-metrics-tui`) como `./tui`; ambas rutas cargan el mismo bundle TUI.
|
|
79
|
+
|
|
74
80
|
El instalador de OpenCode configura el plugin TUI automáticamente. Reinicia OpenCode después de instalarlo.
|
|
75
81
|
|
|
82
|
+
Publicar en npm e instalar en OpenCode son pasos distintos:
|
|
83
|
+
|
|
84
|
+
- `npm publish --access public` publica el paquete en npm.
|
|
85
|
+
- `opencode plugin opencode-system-metrics-tui` lo instala solo en el proyecto actual.
|
|
86
|
+
- `opencode plugin -g opencode-system-metrics-tui` lo instala globalmente para todos los proyectos.
|
|
87
|
+
|
|
76
88
|
### Verificación
|
|
77
89
|
|
|
78
90
|
```bash
|
|
@@ -92,10 +104,14 @@ npm run build
|
|
|
92
104
|
- GPU: utilization and temperature.
|
|
93
105
|
- GPU VRAM: used memory, total memory, and percentage.
|
|
94
106
|
- Network: live download and upload speeds.
|
|
95
|
-
-
|
|
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.
|
|
96
108
|
- Uses the active OpenCode theme colors.
|
|
97
|
-
-
|
|
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.
|
|
110
|
+
- On Windows, visible Unicode icons are used by default because OpenCode cannot detect the terminal's active font.
|
|
98
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.
|
|
99
115
|
|
|
100
116
|
### Requirements
|
|
101
117
|
|
|
@@ -112,7 +128,7 @@ npm run install-font
|
|
|
112
128
|
opencode
|
|
113
129
|
```
|
|
114
130
|
|
|
115
|
-
`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.
|
|
116
132
|
|
|
117
133
|
Extra commands:
|
|
118
134
|
|
|
@@ -131,7 +147,7 @@ The font is installed for the current user only:
|
|
|
131
147
|
- macOS: `~/Library/Fonts`
|
|
132
148
|
- Windows: `%LOCALAPPDATA%\\Microsoft\\Windows\\Fonts`
|
|
133
149
|
|
|
134
|
-
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.
|
|
135
151
|
|
|
136
152
|
### Publishing and npm installation
|
|
137
153
|
|
|
@@ -148,8 +164,16 @@ On another machine:
|
|
|
148
164
|
opencode plugin opencode-system-metrics-tui
|
|
149
165
|
```
|
|
150
166
|
|
|
167
|
+
The package exports both the root (`opencode-system-metrics-tui`) and `./tui`; both paths load the same TUI bundle.
|
|
168
|
+
|
|
151
169
|
The OpenCode installer configures the TUI plugin automatically. Restart OpenCode after installation.
|
|
152
170
|
|
|
171
|
+
Publishing to npm and installing in OpenCode are separate steps:
|
|
172
|
+
|
|
173
|
+
- `npm publish --access public` publishes the package to npm.
|
|
174
|
+
- `opencode plugin opencode-system-metrics-tui` installs it for the current project only.
|
|
175
|
+
- `opencode plugin -g opencode-system-metrics-tui` installs it globally for all projects.
|
|
176
|
+
|
|
153
177
|
### Verification
|
|
154
178
|
|
|
155
179
|
```bash
|
|
@@ -169,10 +193,14 @@ npm run build
|
|
|
169
193
|
- GPU: utilização e temperatura.
|
|
170
194
|
- GPU VRAM: memória usada, total e porcentagem.
|
|
171
195
|
- Rede: velocidades de download e upload em tempo real.
|
|
172
|
-
-
|
|
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.
|
|
173
197
|
- Usa as cores do tema ativo do OpenCode.
|
|
174
|
-
-
|
|
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.
|
|
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.
|
|
175
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.
|
|
176
204
|
|
|
177
205
|
### Requisitos
|
|
178
206
|
|
|
@@ -189,7 +217,7 @@ npm run install-font
|
|
|
189
217
|
opencode
|
|
190
218
|
```
|
|
191
219
|
|
|
192
|
-
`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.
|
|
193
221
|
|
|
194
222
|
Comandos adicionais:
|
|
195
223
|
|
|
@@ -208,7 +236,7 @@ A fonte é instalada somente para o usuário atual:
|
|
|
208
236
|
- macOS: `~/Library/Fonts`
|
|
209
237
|
- Windows: `%LOCALAPPDATA%\\Microsoft\\Windows\\Fonts`
|
|
210
238
|
|
|
211
|
-
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.
|
|
212
240
|
|
|
213
241
|
### Publicação e instalação pelo npm
|
|
214
242
|
|
|
@@ -225,8 +253,16 @@ Em outro computador:
|
|
|
225
253
|
opencode plugin opencode-system-metrics-tui
|
|
226
254
|
```
|
|
227
255
|
|
|
256
|
+
O pacote exporta tanto a raiz (`opencode-system-metrics-tui`) quanto `./tui`; os dois caminhos carregam o mesmo bundle TUI.
|
|
257
|
+
|
|
228
258
|
O instalador do OpenCode configura o plugin TUI automaticamente. Reinicie o OpenCode após a instalação.
|
|
229
259
|
|
|
260
|
+
Publicar no npm e instalar no OpenCode são etapas diferentes:
|
|
261
|
+
|
|
262
|
+
- `npm publish --access public` publica o pacote no npm.
|
|
263
|
+
- `opencode plugin opencode-system-metrics-tui` instala o plugin apenas no projeto atual.
|
|
264
|
+
- `opencode plugin -g opencode-system-metrics-tui` instala o plugin globalmente para todos os projetos.
|
|
265
|
+
|
|
230
266
|
### Verificação
|
|
231
267
|
|
|
232
268
|
```bash
|
|
@@ -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
323
|
var REFRESH_INTERVAL_MS = 2000;
|
|
198
|
-
var icons = getMetricIcons(
|
|
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) {
|