opencode-system-metrics-tui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +246 -0
- package/dist/tui.d.ts +7 -0
- package/dist/tui.js +381 -0
- package/package.json +39 -0
- package/scripts/build.ts +16 -0
- package/scripts/install-hack-font.ts +90 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 HexaRevenant
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# OpenCode System Metrics TUI
|
|
2
|
+
|
|
3
|
+
OpenCode TUI plugin that adds live CPU, RAM, GPU, GPU VRAM, temperature, and network metrics to the `sidebar_content` slot.
|
|
4
|
+
|
|
5
|
+
The README is available in **Español**, **English**, and **Português**.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Español
|
|
10
|
+
|
|
11
|
+
### Características
|
|
12
|
+
|
|
13
|
+
- CPU: utilización y temperatura.
|
|
14
|
+
- RAM: memoria usada, total y porcentaje.
|
|
15
|
+
- GPU: utilización y temperatura.
|
|
16
|
+
- GPU VRAM: memoria usada, total y porcentaje.
|
|
17
|
+
- Red: velocidad de descarga y subida en tiempo real.
|
|
18
|
+
- Actualización cada 2 segundos.
|
|
19
|
+
- Colores compatibles con el tema activo de OpenCode.
|
|
20
|
+
- Iconos Hack Nerd Font con fallback Unicode.
|
|
21
|
+
- Soporte para Linux, macOS y Windows cuando el sistema expone las métricas.
|
|
22
|
+
|
|
23
|
+
### Requisitos
|
|
24
|
+
|
|
25
|
+
- OpenCode `1.18.29` o posterior con soporte para plugins TUI.
|
|
26
|
+
- Node.js 18 o superior.
|
|
27
|
+
- Bun para compilar el plugin.
|
|
28
|
+
|
|
29
|
+
### Instalación local
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install
|
|
33
|
+
npm run build
|
|
34
|
+
npm run install-font
|
|
35
|
+
opencode
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`npm run install-font` 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
|
+
|
|
40
|
+
Comandos adicionales:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm run install-font -- --check # comprobar sin descargar
|
|
44
|
+
npm run install-font -- --yes # instalar sin confirmación interactiva
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Hack Nerd Font
|
|
48
|
+
|
|
49
|
+
Se recomienda seleccionar **Hack Nerd Font Mono** en la configuración de la terminal. El instalador utiliza la distribución oficial de [Nerd Fonts](https://www.nerdfonts.com/) y descarga los archivos desde el [repositorio oficial de Hack](https://github.com/ryanoasis/nerd-fonts/tree/master/patched-fonts/Hack).
|
|
50
|
+
|
|
51
|
+
La fuente se instala solo para el usuario actual:
|
|
52
|
+
|
|
53
|
+
- Linux: `~/.local/share/fonts`
|
|
54
|
+
- macOS: `~/Library/Fonts`
|
|
55
|
+
- Windows: `%LOCALAPPDATA%\\Microsoft\\Windows\\Fonts`
|
|
56
|
+
|
|
57
|
+
Después de instalarla, selecciona **Hack Nerd Font Mono** en la terminal y reinicia la terminal y OpenCode. Si no se selecciona una Nerd Font, el plugin usa iconos Unicode de fallback.
|
|
58
|
+
|
|
59
|
+
### Publicación e instalación desde npm
|
|
60
|
+
|
|
61
|
+
Antes de publicar, cambia el nombre del paquete en `package.json` por un nombre disponible en tu cuenta u organización npm:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npm run build
|
|
65
|
+
npm publish --access public
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
En otro equipo:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
opencode plugin opencode-system-metrics-tui
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
El instalador de OpenCode configura el plugin TUI automáticamente. Reinicia OpenCode después de instalarlo.
|
|
75
|
+
|
|
76
|
+
### Verificación
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
npm test
|
|
80
|
+
npm run typecheck
|
|
81
|
+
npm run build
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## English
|
|
87
|
+
|
|
88
|
+
### Features
|
|
89
|
+
|
|
90
|
+
- CPU: utilization and temperature.
|
|
91
|
+
- RAM: used memory, total memory, and percentage.
|
|
92
|
+
- GPU: utilization and temperature.
|
|
93
|
+
- GPU VRAM: used memory, total memory, and percentage.
|
|
94
|
+
- Network: live download and upload speeds.
|
|
95
|
+
- Refreshes every 2 seconds.
|
|
96
|
+
- Uses the active OpenCode theme colors.
|
|
97
|
+
- Hack Nerd Font icons with Unicode fallback.
|
|
98
|
+
- Supports Linux, macOS, and Windows when the operating system exposes the metrics.
|
|
99
|
+
|
|
100
|
+
### Requirements
|
|
101
|
+
|
|
102
|
+
- OpenCode `1.18.29` or newer with TUI plugin support.
|
|
103
|
+
- Node.js 18 or newer.
|
|
104
|
+
- Bun to build the plugin.
|
|
105
|
+
|
|
106
|
+
### Local installation
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
npm install
|
|
110
|
+
npm run build
|
|
111
|
+
npm run install-font
|
|
112
|
+
opencode
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`npm run install-font` installs **Hack Nerd Font** for the current user only. It is not executed automatically by `npm install` and asks for confirmation before downloading the official font.
|
|
116
|
+
|
|
117
|
+
Extra commands:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
npm run install-font -- --check # check without downloading
|
|
121
|
+
npm run install-font -- --yes # install without an interactive prompt
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Hack Nerd Font
|
|
125
|
+
|
|
126
|
+
Select **Hack Nerd Font Mono** in your terminal settings. The installer uses the official [Nerd Fonts](https://www.nerdfonts.com/) distribution and downloads files from the [official Hack repository](https://github.com/ryanoasis/nerd-fonts/tree/master/patched-fonts/Hack).
|
|
127
|
+
|
|
128
|
+
The font is installed for the current user only:
|
|
129
|
+
|
|
130
|
+
- Linux: `~/.local/share/fonts`
|
|
131
|
+
- macOS: `~/Library/Fonts`
|
|
132
|
+
- Windows: `%LOCALAPPDATA%\\Microsoft\\Windows\\Fonts`
|
|
133
|
+
|
|
134
|
+
After installation, select **Hack Nerd Font Mono** in the terminal and restart the terminal and OpenCode. If a Nerd Font is not selected, the plugin uses Unicode fallback icons.
|
|
135
|
+
|
|
136
|
+
### Publishing and npm installation
|
|
137
|
+
|
|
138
|
+
Before publishing, change the package name in `package.json` to a name available in your npm account or organization:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
npm run build
|
|
142
|
+
npm publish --access public
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
On another machine:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
opencode plugin opencode-system-metrics-tui
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
The OpenCode installer configures the TUI plugin automatically. Restart OpenCode after installation.
|
|
152
|
+
|
|
153
|
+
### Verification
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
npm test
|
|
157
|
+
npm run typecheck
|
|
158
|
+
npm run build
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Português
|
|
164
|
+
|
|
165
|
+
### Funcionalidades
|
|
166
|
+
|
|
167
|
+
- CPU: utilização e temperatura.
|
|
168
|
+
- RAM: memória usada, total e porcentagem.
|
|
169
|
+
- GPU: utilização e temperatura.
|
|
170
|
+
- GPU VRAM: memória usada, total e porcentagem.
|
|
171
|
+
- Rede: velocidades de download e upload em tempo real.
|
|
172
|
+
- Atualização a cada 2 segundos.
|
|
173
|
+
- Usa as cores do tema ativo do OpenCode.
|
|
174
|
+
- Ícones Hack Nerd Font com fallback Unicode.
|
|
175
|
+
- Suporte para Linux, macOS e Windows quando o sistema disponibiliza as métricas.
|
|
176
|
+
|
|
177
|
+
### Requisitos
|
|
178
|
+
|
|
179
|
+
- OpenCode `1.18.29` ou posterior com suporte a plugins TUI.
|
|
180
|
+
- Node.js 18 ou superior.
|
|
181
|
+
- Bun para compilar o plugin.
|
|
182
|
+
|
|
183
|
+
### Instalação local
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
npm install
|
|
187
|
+
npm run build
|
|
188
|
+
npm run install-font
|
|
189
|
+
opencode
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
`npm run install-font` instala a **Hack Nerd Font** somente para o usuário atual. O comando não é executado automaticamente durante `npm install` e pede confirmação antes de baixar a fonte oficial.
|
|
193
|
+
|
|
194
|
+
Comandos adicionais:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
npm run install-font -- --check # verificar sem baixar
|
|
198
|
+
npm run install-font -- --yes # instalar sem confirmação interativa
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Hack Nerd Font
|
|
202
|
+
|
|
203
|
+
Selecione **Hack Nerd Font Mono** nas configurações do terminal. O instalador usa a distribuição oficial da [Nerd Fonts](https://www.nerdfonts.com/) e baixa os arquivos do [repositório oficial da Hack](https://github.com/ryanoasis/nerd-fonts/tree/master/patched-fonts/Hack).
|
|
204
|
+
|
|
205
|
+
A fonte é instalada somente para o usuário atual:
|
|
206
|
+
|
|
207
|
+
- Linux: `~/.local/share/fonts`
|
|
208
|
+
- macOS: `~/Library/Fonts`
|
|
209
|
+
- Windows: `%LOCALAPPDATA%\\Microsoft\\Windows\\Fonts`
|
|
210
|
+
|
|
211
|
+
Depois da instalação, selecione **Hack Nerd Font Mono** no terminal e reinicie o terminal e o OpenCode. Se uma Nerd Font não estiver selecionada, o plugin usará ícones Unicode de fallback.
|
|
212
|
+
|
|
213
|
+
### Publicação e instalação pelo npm
|
|
214
|
+
|
|
215
|
+
Antes de publicar, altere o nome do pacote em `package.json` para um nome disponível na sua conta ou organização npm:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
npm run build
|
|
219
|
+
npm publish --access public
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Em outro computador:
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
opencode plugin opencode-system-metrics-tui
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
O instalador do OpenCode configura o plugin TUI automaticamente. Reinicie o OpenCode após a instalação.
|
|
229
|
+
|
|
230
|
+
### Verificação
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
npm test
|
|
234
|
+
npm run typecheck
|
|
235
|
+
npm run build
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## License
|
|
241
|
+
|
|
242
|
+
This plugin is released under the [MIT License](LICENSE). The Hack Nerd Font files are third-party assets; see the [official Hack Nerd Fonts repository](https://github.com/ryanoasis/nerd-fonts/tree/master/patched-fonts/Hack) for their applicable license and notices.
|
|
243
|
+
|
|
244
|
+
Este plugin está publicado bajo la [licencia MIT](LICENSE). Los archivos Hack Nerd Font son recursos de terceros; consulta el [repositorio oficial](https://github.com/ryanoasis/nerd-fonts/tree/master/patched-fonts/Hack) para conocer su licencia y avisos.
|
|
245
|
+
|
|
246
|
+
Este plugin é distribuído sob a [licença MIT](LICENSE). Os arquivos Hack Nerd Font são recursos de terceiros; consulte o [repositório oficial](https://github.com/ryanoasis/nerd-fonts/tree/master/patched-fonts/Hack) para obter a licença e os avisos aplicáveis.
|
package/dist/tui.d.ts
ADDED
package/dist/tui.js
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/tui.tsx
|
|
3
|
+
import { createComponent as _$createComponent } from "@opentui/solid";
|
|
4
|
+
import { effect as _$effect } from "@opentui/solid";
|
|
5
|
+
import { createTextNode as _$createTextNode } from "@opentui/solid";
|
|
6
|
+
import { insertNode as _$insertNode } from "@opentui/solid";
|
|
7
|
+
import { insert as _$insert } from "@opentui/solid";
|
|
8
|
+
import { setProp as _$setProp } from "@opentui/solid";
|
|
9
|
+
import { createElement as _$createElement } from "@opentui/solid";
|
|
10
|
+
import { createSignal, onCleanup, onMount } from "solid-js";
|
|
11
|
+
import { TextAttributes } from "@opentui/core";
|
|
12
|
+
|
|
13
|
+
// src/format.ts
|
|
14
|
+
var BYTES_PER_GIB = 1024 ** 3;
|
|
15
|
+
function formatPercent(value) {
|
|
16
|
+
return value === null || !Number.isFinite(value) ? "\u2014" : `${Math.round(value)}%`;
|
|
17
|
+
}
|
|
18
|
+
function formatTemperature(value) {
|
|
19
|
+
return value === null || !Number.isFinite(value) ? "\u2014" : `${Math.round(value)}\xB0C`;
|
|
20
|
+
}
|
|
21
|
+
function formatGiB(bytes) {
|
|
22
|
+
if (bytes === null || !Number.isFinite(bytes))
|
|
23
|
+
return "\u2014";
|
|
24
|
+
return `${(bytes / BYTES_PER_GIB).toFixed(1)} GiB`;
|
|
25
|
+
}
|
|
26
|
+
function formatRate(bytesPerSecond) {
|
|
27
|
+
if (bytesPerSecond === null || !Number.isFinite(bytesPerSecond))
|
|
28
|
+
return "\u2014";
|
|
29
|
+
const units = ["B/s", "KiB/s", "MiB/s", "GiB/s"];
|
|
30
|
+
let value = Math.max(0, bytesPerSecond);
|
|
31
|
+
let unit = 0;
|
|
32
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
33
|
+
value /= 1024;
|
|
34
|
+
unit += 1;
|
|
35
|
+
}
|
|
36
|
+
return `${value < 10 && unit > 0 ? value.toFixed(1) : Math.round(value)} ${units[unit]}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// src/metrics.ts
|
|
40
|
+
import si from "systeminformation";
|
|
41
|
+
var previousNetwork;
|
|
42
|
+
var unavailable = {
|
|
43
|
+
cpuPercent: null,
|
|
44
|
+
memoryUsedBytes: null,
|
|
45
|
+
memoryTotalBytes: null,
|
|
46
|
+
memoryPercent: null,
|
|
47
|
+
gpuPercent: null,
|
|
48
|
+
cpuTemperatureCelsius: null,
|
|
49
|
+
gpuTemperatureCelsius: null,
|
|
50
|
+
gpuMemoryUsedBytes: null,
|
|
51
|
+
gpuMemoryTotalBytes: null,
|
|
52
|
+
gpuMemoryPercent: null,
|
|
53
|
+
downloadBytesPerSecond: null,
|
|
54
|
+
uploadBytesPerSecond: null
|
|
55
|
+
};
|
|
56
|
+
function finite(value) {
|
|
57
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
58
|
+
}
|
|
59
|
+
async function withTimeout(promise, timeoutMs) {
|
|
60
|
+
let timeout;
|
|
61
|
+
try {
|
|
62
|
+
return await Promise.race([
|
|
63
|
+
promise,
|
|
64
|
+
new Promise((_, reject) => {
|
|
65
|
+
timeout = setTimeout(() => reject(new Error("metric timeout")), timeoutMs);
|
|
66
|
+
})
|
|
67
|
+
]);
|
|
68
|
+
} finally {
|
|
69
|
+
if (timeout !== undefined)
|
|
70
|
+
clearTimeout(timeout);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function readMetrics() {
|
|
74
|
+
const [load, memory] = await Promise.allSettled([
|
|
75
|
+
si.currentLoad(),
|
|
76
|
+
si.mem()
|
|
77
|
+
]);
|
|
78
|
+
const [temperature, graphics, network] = await Promise.allSettled([
|
|
79
|
+
withTimeout(si.cpuTemperature(), 1500),
|
|
80
|
+
withTimeout(si.graphics(), 1500),
|
|
81
|
+
withTimeout(si.networkStats("*"), 1500)
|
|
82
|
+
]);
|
|
83
|
+
const cpuPercent = load.status === "fulfilled" ? finite(load.value.currentLoad) : null;
|
|
84
|
+
const memoryTotalBytes = memory.status === "fulfilled" ? finite(memory.value.total) : null;
|
|
85
|
+
const memoryAvailableBytes = memory.status === "fulfilled" ? finite(memory.value.available) : null;
|
|
86
|
+
const memoryUsedBytes = memoryTotalBytes !== null && memoryAvailableBytes !== null ? Math.max(0, memoryTotalBytes - memoryAvailableBytes) : memory.status === "fulfilled" ? finite(memory.value.used) : null;
|
|
87
|
+
const memoryPercent = memoryUsedBytes !== null && memoryTotalBytes !== null && memoryTotalBytes > 0 ? memoryUsedBytes / memoryTotalBytes * 100 : null;
|
|
88
|
+
let gpuPercent = null;
|
|
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
|
+
}
|
|
118
|
+
let downloadBytesPerSecond = null;
|
|
119
|
+
let uploadBytesPerSecond = null;
|
|
120
|
+
if (network.status === "fulfilled") {
|
|
121
|
+
const samples = network.value;
|
|
122
|
+
const timestamp = Date.now();
|
|
123
|
+
const rx = samples.reduce((total, item) => total + (finite(item.rx_bytes) ?? 0), 0);
|
|
124
|
+
const tx = samples.reduce((total, item) => total + (finite(item.tx_bytes) ?? 0), 0);
|
|
125
|
+
if (previousNetwork !== undefined) {
|
|
126
|
+
const seconds = Math.max((timestamp - previousNetwork.timestamp) / 1000, 0.001);
|
|
127
|
+
downloadBytesPerSecond = Math.max(0, rx - previousNetwork.rx) / seconds;
|
|
128
|
+
uploadBytesPerSecond = Math.max(0, tx - previousNetwork.tx) / seconds;
|
|
129
|
+
}
|
|
130
|
+
previousNetwork = { timestamp, rx, tx };
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
...unavailable,
|
|
134
|
+
cpuPercent,
|
|
135
|
+
memoryUsedBytes,
|
|
136
|
+
memoryTotalBytes,
|
|
137
|
+
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,
|
|
144
|
+
downloadBytesPerSecond,
|
|
145
|
+
uploadBytesPerSecond
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// src/font.ts
|
|
150
|
+
import { existsSync, readdirSync } from "fs";
|
|
151
|
+
import { homedir } from "os";
|
|
152
|
+
import { join as posixJoin, win32 } from "path";
|
|
153
|
+
var FONT_FILE_PATTERN = /^HackNerdFont(?:Mono|Propo|Console)?-[A-Za-z0-9-]+\.(?:ttf|otf)$/i;
|
|
154
|
+
function getFontDirectories(platform, home = homedir(), env = process.env) {
|
|
155
|
+
if (platform === "darwin")
|
|
156
|
+
return [posixJoin(home, "Library", "Fonts"), "/Library/Fonts", "/System/Library/Fonts"];
|
|
157
|
+
if (platform === "win32") {
|
|
158
|
+
const localAppData = env.LOCALAPPDATA ?? win32.join(home, "AppData", "Local");
|
|
159
|
+
const windows = env.WINDIR ?? "C:\\Windows";
|
|
160
|
+
return [win32.join(localAppData, "Microsoft", "Windows", "Fonts"), win32.join(windows, "Fonts")];
|
|
161
|
+
}
|
|
162
|
+
return [posixJoin(home, ".local", "share", "fonts"), posixJoin(home, ".fonts"), "/usr/local/share/fonts", "/usr/share/fonts"];
|
|
163
|
+
}
|
|
164
|
+
function isHackNerdFontFile(fileName) {
|
|
165
|
+
return FONT_FILE_PATTERN.test(fileName);
|
|
166
|
+
}
|
|
167
|
+
function hasHackNerdFont(platform = process.platform, home = homedir(), env = process.env) {
|
|
168
|
+
const containsFont = (directory) => {
|
|
169
|
+
if (!existsSync(directory))
|
|
170
|
+
return false;
|
|
171
|
+
try {
|
|
172
|
+
return readdirSync(directory, { withFileTypes: true }).some((entry) => {
|
|
173
|
+
const path = platform === "win32" ? win32.join(directory, entry.name) : posixJoin(directory, entry.name);
|
|
174
|
+
return entry.isFile() ? isHackNerdFontFile(entry.name) : entry.isDirectory() && containsFont(path);
|
|
175
|
+
});
|
|
176
|
+
} catch {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
return getFontDirectories(platform, home, env).some(containsFont);
|
|
181
|
+
}
|
|
182
|
+
var nerdFontIcons = {
|
|
183
|
+
title: "\uDB80\uDF79",
|
|
184
|
+
cpu: "\uDB83\uDEE0",
|
|
185
|
+
ram: "\uDB81\uDE1A",
|
|
186
|
+
gpu: "\uDB82\uDCAE",
|
|
187
|
+
vram: "\uDB80\uDF5B",
|
|
188
|
+
thermometer: "\uF2C7",
|
|
189
|
+
network: "\uDB81\uDEF3"
|
|
190
|
+
};
|
|
191
|
+
var unicodeIcons = { title: "\u25A6", cpu: "\u25A3", ram: "\u25A4", gpu: "\u25C6", vram: "\u25C8", thermometer: "\u2668", network: "\u21C5" };
|
|
192
|
+
function getMetricIcons(useNerdFont) {
|
|
193
|
+
return useNerdFont ? nerdFontIcons : unicodeIcons;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// src/tui.tsx
|
|
197
|
+
var REFRESH_INTERVAL_MS = 2000;
|
|
198
|
+
var icons = getMetricIcons(hasHackNerdFont());
|
|
199
|
+
function systemMetricsTitle() {
|
|
200
|
+
const locale = process.env.LC_ALL ?? process.env.LANGUAGE?.split(":")[0] ?? process.env.LANG ?? Intl.DateTimeFormat().resolvedOptions().locale;
|
|
201
|
+
const language = locale.split(/[-_.]/)[0].toLowerCase();
|
|
202
|
+
return {
|
|
203
|
+
de: "Systemmetriken",
|
|
204
|
+
es: "M\xE9tricas del sistema",
|
|
205
|
+
fr: "M\xE9triques syst\xE8me",
|
|
206
|
+
it: "Metriche di sistema",
|
|
207
|
+
pt: "M\xE9tricas do sistema"
|
|
208
|
+
}[language] ?? "System metrics";
|
|
209
|
+
}
|
|
210
|
+
function MetricsPanel(props) {
|
|
211
|
+
const [metrics, setMetrics] = createSignal({
|
|
212
|
+
cpuPercent: null,
|
|
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
|
+
});
|
|
245
|
+
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(` \xB7 `), _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(` \xB7 `), _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(`\u2193 `), _el$38 = _$createTextNode(` \u2191 `);
|
|
247
|
+
_$insertNode(_el$, _el$2);
|
|
248
|
+
_$insertNode(_el$, _el$4);
|
|
249
|
+
_$insertNode(_el$, _el$1);
|
|
250
|
+
_$insertNode(_el$, _el$17);
|
|
251
|
+
_$insertNode(_el$, _el$24);
|
|
252
|
+
_$insertNode(_el$, _el$32);
|
|
253
|
+
_$setProp(_el$, "flexDirection", "column");
|
|
254
|
+
_$setProp(_el$, "paddingLeft", 0);
|
|
255
|
+
_$setProp(_el$, "paddingRight", 0);
|
|
256
|
+
_$insertNode(_el$2, _el$3);
|
|
257
|
+
_$insert(_el$2, () => icons.title, _el$3);
|
|
258
|
+
_$insert(_el$2, systemMetricsTitle, null);
|
|
259
|
+
_$insertNode(_el$4, _el$5);
|
|
260
|
+
_$insertNode(_el$4, _el$6);
|
|
261
|
+
_$insertNode(_el$4, _el$8);
|
|
262
|
+
_$setProp(_el$4, "flexDirection", "row");
|
|
263
|
+
_$insert(_el$5, () => icons.cpu);
|
|
264
|
+
_$insertNode(_el$6, _$createTextNode(` CPU `));
|
|
265
|
+
_$insertNode(_el$8, _el$9);
|
|
266
|
+
_$insertNode(_el$8, _el$0);
|
|
267
|
+
_$insert(_el$8, () => formatPercent(metrics().cpuPercent), _el$9);
|
|
268
|
+
_$insert(_el$8, () => icons.thermometer, _el$0);
|
|
269
|
+
_$insert(_el$8, () => formatTemperature(metrics().cpuTemperatureCelsius), null);
|
|
270
|
+
_$insertNode(_el$1, _el$10);
|
|
271
|
+
_$insertNode(_el$1, _el$11);
|
|
272
|
+
_$insertNode(_el$1, _el$13);
|
|
273
|
+
_$setProp(_el$1, "flexDirection", "row");
|
|
274
|
+
_$insert(_el$10, () => icons.ram);
|
|
275
|
+
_$insertNode(_el$11, _$createTextNode(` RAM `));
|
|
276
|
+
_$insertNode(_el$13, _el$14);
|
|
277
|
+
_$insertNode(_el$13, _el$15);
|
|
278
|
+
_$insertNode(_el$13, _el$16);
|
|
279
|
+
_$insert(_el$13, () => formatGiB(metrics().memoryUsedBytes), _el$14);
|
|
280
|
+
_$insert(_el$13, () => formatGiB(metrics().memoryTotalBytes), _el$15);
|
|
281
|
+
_$insert(_el$13, () => formatPercent(metrics().memoryPercent), _el$16);
|
|
282
|
+
_$insertNode(_el$17, _el$18);
|
|
283
|
+
_$insertNode(_el$17, _el$19);
|
|
284
|
+
_$insertNode(_el$17, _el$21);
|
|
285
|
+
_$setProp(_el$17, "flexDirection", "row");
|
|
286
|
+
_$insert(_el$18, () => icons.gpu);
|
|
287
|
+
_$insertNode(_el$19, _$createTextNode(` GPU `));
|
|
288
|
+
_$insertNode(_el$21, _el$22);
|
|
289
|
+
_$insertNode(_el$21, _el$23);
|
|
290
|
+
_$insert(_el$21, () => formatPercent(metrics().gpuPercent), _el$22);
|
|
291
|
+
_$insert(_el$21, () => icons.thermometer, _el$23);
|
|
292
|
+
_$insert(_el$21, () => formatTemperature(metrics().gpuTemperatureCelsius), null);
|
|
293
|
+
_$insertNode(_el$24, _el$25);
|
|
294
|
+
_$insertNode(_el$24, _el$26);
|
|
295
|
+
_$insertNode(_el$24, _el$28);
|
|
296
|
+
_$setProp(_el$24, "flexDirection", "row");
|
|
297
|
+
_$insert(_el$25, () => icons.vram);
|
|
298
|
+
_$insertNode(_el$26, _$createTextNode(` GPU VRAM `));
|
|
299
|
+
_$insertNode(_el$28, _el$29);
|
|
300
|
+
_$insertNode(_el$28, _el$30);
|
|
301
|
+
_$insertNode(_el$28, _el$31);
|
|
302
|
+
_$insert(_el$28, () => formatGiB(metrics().gpuMemoryUsedBytes), _el$29);
|
|
303
|
+
_$insert(_el$28, () => formatGiB(metrics().gpuMemoryTotalBytes), _el$30);
|
|
304
|
+
_$insert(_el$28, () => formatPercent(metrics().gpuMemoryPercent), _el$31);
|
|
305
|
+
_$insertNode(_el$32, _el$33);
|
|
306
|
+
_$insertNode(_el$32, _el$34);
|
|
307
|
+
_$insertNode(_el$32, _el$36);
|
|
308
|
+
_$setProp(_el$32, "flexDirection", "row");
|
|
309
|
+
_$insert(_el$33, () => icons.network);
|
|
310
|
+
_$insertNode(_el$34, _$createTextNode(` NET `));
|
|
311
|
+
_$insertNode(_el$36, _el$37);
|
|
312
|
+
_$insertNode(_el$36, _el$38);
|
|
313
|
+
_$insert(_el$36, () => formatRate(metrics().downloadBytesPerSecond), _el$38);
|
|
314
|
+
_$insert(_el$36, () => formatRate(metrics().uploadBytesPerSecond), null);
|
|
315
|
+
_$effect((_p$) => {
|
|
316
|
+
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
|
+
_v$ !== _p$.e && (_p$.e = _$setProp(_el$2, "fg", _v$, _p$.e));
|
|
318
|
+
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$2, "attributes", _v$2, _p$.t));
|
|
319
|
+
_v$3 !== _p$.a && (_p$.a = _$setProp(_el$5, "fg", _v$3, _p$.a));
|
|
320
|
+
_v$4 !== _p$.o && (_p$.o = _$setProp(_el$6, "fg", _v$4, _p$.o));
|
|
321
|
+
_v$5 !== _p$.i && (_p$.i = _$setProp(_el$8, "fg", _v$5, _p$.i));
|
|
322
|
+
_v$6 !== _p$.n && (_p$.n = _$setProp(_el$10, "fg", _v$6, _p$.n));
|
|
323
|
+
_v$7 !== _p$.s && (_p$.s = _$setProp(_el$11, "fg", _v$7, _p$.s));
|
|
324
|
+
_v$8 !== _p$.h && (_p$.h = _$setProp(_el$13, "fg", _v$8, _p$.h));
|
|
325
|
+
_v$9 !== _p$.r && (_p$.r = _$setProp(_el$18, "fg", _v$9, _p$.r));
|
|
326
|
+
_v$0 !== _p$.d && (_p$.d = _$setProp(_el$19, "fg", _v$0, _p$.d));
|
|
327
|
+
_v$1 !== _p$.l && (_p$.l = _$setProp(_el$21, "fg", _v$1, _p$.l));
|
|
328
|
+
_v$10 !== _p$.u && (_p$.u = _$setProp(_el$25, "fg", _v$10, _p$.u));
|
|
329
|
+
_v$11 !== _p$.c && (_p$.c = _$setProp(_el$26, "fg", _v$11, _p$.c));
|
|
330
|
+
_v$12 !== _p$.w && (_p$.w = _$setProp(_el$28, "fg", _v$12, _p$.w));
|
|
331
|
+
_v$13 !== _p$.m && (_p$.m = _$setProp(_el$33, "fg", _v$13, _p$.m));
|
|
332
|
+
_v$14 !== _p$.f && (_p$.f = _$setProp(_el$34, "fg", _v$14, _p$.f));
|
|
333
|
+
_v$15 !== _p$.y && (_p$.y = _$setProp(_el$36, "fg", _v$15, _p$.y));
|
|
334
|
+
return _p$;
|
|
335
|
+
}, {
|
|
336
|
+
e: undefined,
|
|
337
|
+
t: undefined,
|
|
338
|
+
a: undefined,
|
|
339
|
+
o: undefined,
|
|
340
|
+
i: undefined,
|
|
341
|
+
n: undefined,
|
|
342
|
+
s: undefined,
|
|
343
|
+
h: undefined,
|
|
344
|
+
r: undefined,
|
|
345
|
+
d: undefined,
|
|
346
|
+
l: undefined,
|
|
347
|
+
u: undefined,
|
|
348
|
+
c: undefined,
|
|
349
|
+
w: undefined,
|
|
350
|
+
m: undefined,
|
|
351
|
+
f: undefined,
|
|
352
|
+
y: undefined
|
|
353
|
+
});
|
|
354
|
+
return _el$;
|
|
355
|
+
})();
|
|
356
|
+
}
|
|
357
|
+
var tui = async (api, options) => {
|
|
358
|
+
if (options?.enabled === false)
|
|
359
|
+
return;
|
|
360
|
+
api.slots.register({
|
|
361
|
+
order: 250,
|
|
362
|
+
slots: {
|
|
363
|
+
sidebar_content(_context, props) {
|
|
364
|
+
props.session_id;
|
|
365
|
+
return _$createComponent(MetricsPanel, {
|
|
366
|
+
get theme() {
|
|
367
|
+
return _context.theme.current;
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
};
|
|
374
|
+
var plugin = {
|
|
375
|
+
id: "opencode-monitor.system-metrics",
|
|
376
|
+
tui
|
|
377
|
+
};
|
|
378
|
+
var tui_default = plugin;
|
|
379
|
+
export {
|
|
380
|
+
tui_default as default
|
|
381
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opencode-system-metrics-tui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenCode TUI sidebar plugin for live system metrics",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
"./tui": {
|
|
9
|
+
"types": "./dist/tui.d.ts",
|
|
10
|
+
"import": "./dist/tui.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"scripts",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "bun scripts/build.ts && tsc --emitDeclarationOnly --declaration --outDir dist",
|
|
20
|
+
"test": "tsx --test test/format.test.ts",
|
|
21
|
+
"typecheck": "tsc --noEmit",
|
|
22
|
+
"install-font": "tsx scripts/install-hack-font.ts"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@opentui/core": "^0.5.10",
|
|
29
|
+
"@opentui/solid": "^0.5.10",
|
|
30
|
+
"solid-js": "^1.9.12",
|
|
31
|
+
"systeminformation": "^5.33.8"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@opencode-ai/plugin": "^1.18.29",
|
|
35
|
+
"@types/node": "^22.10.5",
|
|
36
|
+
"tsx": "^4.19.2",
|
|
37
|
+
"typescript": "^5.7.3"
|
|
38
|
+
}
|
|
39
|
+
}
|
package/scripts/build.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import solidPlugin from "@opentui/solid/bun-plugin"
|
|
2
|
+
|
|
3
|
+
const result = await Bun.build({
|
|
4
|
+
entrypoints: ["src/tui.tsx"],
|
|
5
|
+
outdir: "dist",
|
|
6
|
+
target: "bun",
|
|
7
|
+
format: "esm",
|
|
8
|
+
plugins: [solidPlugin],
|
|
9
|
+
external: ["@opencode-ai/plugin/tui", "@opentui/core", "@opentui/solid", "solid-js", "systeminformation"],
|
|
10
|
+
naming: "tui.js",
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
if (!result.success) {
|
|
14
|
+
console.error(result.logs)
|
|
15
|
+
process.exit(1)
|
|
16
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { cp, mkdir, mkdtemp, readdir, rm, writeFile } from "node:fs/promises"
|
|
2
|
+
import { homedir, tmpdir } from "node:os"
|
|
3
|
+
import { basename, join } from "node:path"
|
|
4
|
+
import { execFile as execFileCallback } from "node:child_process"
|
|
5
|
+
import { promisify } from "node:util"
|
|
6
|
+
import { getFontDirectories, hasHackNerdFont, isHackNerdFontFile, type SupportedPlatform } from "../src/font.js"
|
|
7
|
+
|
|
8
|
+
const execFile = promisify(execFileCallback)
|
|
9
|
+
const RELEASE_URL = "https://github.com/ryanoasis/nerd-fonts/releases/download/v3.4.0/Hack.zip"
|
|
10
|
+
|
|
11
|
+
function printHelp(): void {
|
|
12
|
+
console.log(`Install Hack Nerd Font for the current user.
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
npm run install-font Ask before downloading and installing
|
|
16
|
+
npm run install-font --check Detect only; never downloads or changes files
|
|
17
|
+
npm run install-font --help Show this help
|
|
18
|
+
npm run install-font --yes Skip the download confirmation`)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function confirm(): Promise<boolean> {
|
|
22
|
+
if (!process.stdin.isTTY) return false
|
|
23
|
+
process.stdout.write("Download Hack Nerd Font from the official Nerd Fonts release? [y/N] ")
|
|
24
|
+
for await (const chunk of process.stdin) return /^y(es)?$/i.test(String(chunk).trim())
|
|
25
|
+
return false
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function findFontFiles(directory: string): Promise<string[]> {
|
|
29
|
+
const files: string[] = []
|
|
30
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
31
|
+
const path = join(directory, entry.name)
|
|
32
|
+
if (entry.isDirectory()) files.push(...(await findFontFiles(path)))
|
|
33
|
+
else if (entry.isFile() && isHackNerdFontFile(entry.name)) files.push(path)
|
|
34
|
+
}
|
|
35
|
+
return files
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function extractZip(zipPath: string, destination: string, platform: SupportedPlatform): Promise<void> {
|
|
39
|
+
try {
|
|
40
|
+
if (platform === "win32") {
|
|
41
|
+
await execFile("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", "Expand-Archive -LiteralPath $args[0] -DestinationPath $args[1] -Force", zipPath, destination])
|
|
42
|
+
} else {
|
|
43
|
+
await execFile("unzip", ["-q", "-o", zipPath, "-d", destination])
|
|
44
|
+
}
|
|
45
|
+
} catch (error) {
|
|
46
|
+
throw new Error(`Could not extract the ZIP archive. Install unzip (Linux/macOS) or use PowerShell (Windows). ${error instanceof Error ? error.message : String(error)}`)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function registerWindowsFonts(fontFiles: string[]): Promise<void> {
|
|
51
|
+
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 }"
|
|
52
|
+
await execFile("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script, ...fontFiles])
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function install(): Promise<void> {
|
|
56
|
+
const platform = process.platform as SupportedPlatform
|
|
57
|
+
const supportedPlatforms: SupportedPlatform[] = ["linux", "darwin", "win32"]
|
|
58
|
+
if (!supportedPlatforms.includes(platform)) throw new Error(`Unsupported platform: ${process.platform}`)
|
|
59
|
+
if (hasHackNerdFont(platform)) return console.log("Hack Nerd Font is already installed; nothing to do.")
|
|
60
|
+
if (!(process.argv.includes("--yes") || (await confirm()))) return console.log("Installation cancelled. Use --yes to confirm non-interactively.")
|
|
61
|
+
|
|
62
|
+
const temporaryDirectory = await mkdtemp(join(tmpdir(), "opencode-monitor-font-"))
|
|
63
|
+
try {
|
|
64
|
+
console.log(`Downloading ${RELEASE_URL}`)
|
|
65
|
+
const response = await fetch(RELEASE_URL)
|
|
66
|
+
if (!response.ok) throw new Error(`Download failed with HTTP ${response.status} ${response.statusText}`)
|
|
67
|
+
const zipPath = join(temporaryDirectory, "Hack.zip")
|
|
68
|
+
await writeFile(zipPath, Buffer.from(await response.arrayBuffer()))
|
|
69
|
+
const extractedDirectory = join(temporaryDirectory, "extracted")
|
|
70
|
+
await mkdir(extractedDirectory)
|
|
71
|
+
await extractZip(zipPath, extractedDirectory, platform)
|
|
72
|
+
const fontFiles = await findFontFiles(extractedDirectory)
|
|
73
|
+
if (fontFiles.length === 0) throw new Error("The downloaded archive did not contain Hack Nerd Font files.")
|
|
74
|
+
const targetDirectory = getFontDirectories(platform)[0]
|
|
75
|
+
await mkdir(targetDirectory, { recursive: true })
|
|
76
|
+
const installedFiles = fontFiles.map((file) => join(targetDirectory, basename(file)))
|
|
77
|
+
for (let index = 0; index < fontFiles.length; index += 1) await cp(fontFiles[index], installedFiles[index], { force: true })
|
|
78
|
+
if (platform === "win32") {
|
|
79
|
+
try { await registerWindowsFonts(installedFiles) }
|
|
80
|
+
catch { console.warn("Fonts were copied, but Windows font registration failed. Select Hack Nerd Font in the terminal manually.") }
|
|
81
|
+
}
|
|
82
|
+
console.log(`Installed ${fontFiles.length} font files in ${targetDirectory}. Select Hack Nerd Font in your terminal and restart OpenCode.`)
|
|
83
|
+
} finally {
|
|
84
|
+
await rm(temporaryDirectory, { recursive: true, force: true })
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (process.argv.includes("--help")) printHelp()
|
|
89
|
+
else if (process.argv.includes("--check")) console.log(hasHackNerdFont() ? "Hack Nerd Font is installed." : "Hack Nerd Font is not installed.")
|
|
90
|
+
else install().catch((error: unknown) => { console.error(`Hack Nerd Font installation could not be completed: ${error instanceof Error ? error.message : String(error)}`); process.exitCode = 1 })
|