opencode-mask-j0k3r-dev-rgl 2.0.23 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -0
- package/components.tsx +111 -65
- package/config.ts +31 -8
- package/package.json +1 -1
- package/tui.tsx +148 -37
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Arch Mask para OpenCode AI 🚀
|
|
2
|
+
|
|
3
|
+
Esta es una **máscara TUI (Terminal User Interface)** personalizada para [OpenCode AI](https://opencode.ai), diseñada con una estética inspirada en Arch Linux y optimizada para desarrolladores que buscan un entorno de terminal elegante, funcional y altamente personalizable.
|
|
4
|
+
|
|
5
|
+
El proyecto ha sido creado como una **base abierta**: siéntete libre de tomar el código, editarlo y adaptarlo a tus propios gustos. Todo el código fuente está **extensamente comentado en español** para resolver dudas sobre el funcionamiento de los temas, el arte ASCII y la integración con la API de OpenCode.
|
|
6
|
+
|
|
7
|
+
## ✨ Características
|
|
8
|
+
|
|
9
|
+
- 🎨 **Selector de Temas con Preview**: Cambia el estilo visual en tiempo real con `/mask` o `ctrl+m`.
|
|
10
|
+
- 🖼️ **Arte ASCII Dinámico**: Logo de Arch Linux que se adapta a los colores de cada tema.
|
|
11
|
+
- 📊 **Sidebar de Estadísticas**: Visualización reactiva de tokens, uso de contexto y costes de sesión.
|
|
12
|
+
- ☁️ **Transparencia Real**: Configurado para respetar el fondo y la opacidad de tu terminal.
|
|
13
|
+
- 📂 **Gestión de Plugins**: Configura automáticamente la barra lateral nativa de OpenCode.
|
|
14
|
+
|
|
15
|
+
## 🛠️ Instalación
|
|
16
|
+
|
|
17
|
+
Existen dos formas principales de instalar y utilizar esta máscara en tu instancia de OpenCode:
|
|
18
|
+
|
|
19
|
+
### Opción 1: Instalación vía NPM (Recomendado para usuarios)
|
|
20
|
+
|
|
21
|
+
Si el paquete está publicado en el registro de NPM, puedes agregarlo directamente a tu configuración global:
|
|
22
|
+
|
|
23
|
+
1. Instala el plugin globalmente:
|
|
24
|
+
```bash
|
|
25
|
+
opencode plugin nombre-del-paquete -g
|
|
26
|
+
```
|
|
27
|
+
2. OpenCode actualizará automáticamente tu archivo `~/.config/opencode/tui.json`.
|
|
28
|
+
|
|
29
|
+
### Opción 2: Instalación Local (Modo Desarrollador)
|
|
30
|
+
|
|
31
|
+
Si prefieres tener el código en tu PC para editarlo o probarlo localmente, sigue estos pasos:
|
|
32
|
+
|
|
33
|
+
1. Clona o descarga este repositorio en una carpeta de tu preferencia.
|
|
34
|
+
2. Entra en la carpeta y asegúrate de instalar las dependencias:
|
|
35
|
+
```bash
|
|
36
|
+
npm install
|
|
37
|
+
```
|
|
38
|
+
3. Abre tu archivo de configuración de OpenCode (`~/.config/opencode/tui.json`) y apunta el plugin directamente a la ruta de tu carpeta:
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"plugin": [
|
|
43
|
+
[
|
|
44
|
+
"/home/tu-usuario/ruta/a/opencode-mask",
|
|
45
|
+
{
|
|
46
|
+
"enabled": true,
|
|
47
|
+
"theme": "tokyo-night-dev",
|
|
48
|
+
"set_theme": true,
|
|
49
|
+
"show_sidebar": true
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## ⌨️ Atajos y Comandos
|
|
57
|
+
|
|
58
|
+
- **/mask**: Abre el selector de temas.
|
|
59
|
+
- **ctrl+m**: Atajo rápido para el selector de temas (puedes cambiarlo en `tui.tsx`).
|
|
60
|
+
- **onMove**: Navega por la lista de temas para ver una previsualización instantánea.
|
|
61
|
+
|
|
62
|
+
## 🤝 Contribuciones y Personalización
|
|
63
|
+
|
|
64
|
+
Este repositorio es una base educativa. Si quieres crear tu propio tema:
|
|
65
|
+
1. Añade un nuevo archivo `.json` en la carpeta `themes/` (usa `j0k3r-dev-rgl.json` como plantilla).
|
|
66
|
+
2. Regístralo en el bloque de instalación de `tui.tsx`.
|
|
67
|
+
3. ¡Disfruta de tu nueva interfaz!
|
|
68
|
+
|
|
69
|
+
## 📜 Créditos y Agradecimientos
|
|
70
|
+
|
|
71
|
+
Este proyecto fue creado el **4 de abril de 2026** tomando como base e inspiración el excelente trabajo de **IrrealV** en su plugin:
|
|
72
|
+
👉 [IrrealV/plugin-gentleman](https://github.com/IrrealV/plugin-gentleman)
|
|
73
|
+
|
|
74
|
+
¡Muchas gracias por compartir tu conocimiento con la comunidad!
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
Desarrollado con ❤️ por **j0k3r**
|
|
78
|
+
|
|
79
|
+
> **"El código es poesía, y el Open Source es nuestra forma de compartirla con el mundo. ¡VIVA EL OPEN SOURCE!"** 🐧✨
|
package/components.tsx
CHANGED
|
@@ -11,33 +11,54 @@ import {
|
|
|
11
11
|
import type { Cfg } from "./config";
|
|
12
12
|
import { getOSName, getProviders } from "./detection";
|
|
13
13
|
|
|
14
|
-
// ───
|
|
14
|
+
// ─── Pantalla Principal: Logo grande de Arch Linux + Leyenda ─────────────────
|
|
15
|
+
/**
|
|
16
|
+
* Componente que muestra el logo de Arch Linux a gran escala en la pantalla de inicio.
|
|
17
|
+
* Utiliza un sistema de mapeo por zonas para aplicar diferentes colores del tema actual
|
|
18
|
+
* a cada parte del arte ASCII, permitiendo que el logo se adapte visualmente a cualquier tema.
|
|
19
|
+
*
|
|
20
|
+
* @param props Contiene el tema actual de la TUI.
|
|
21
|
+
*/
|
|
15
22
|
export const HomeLogo = (props: { theme: TuiThemeCurrent }) => {
|
|
16
23
|
const t = props.theme;
|
|
17
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Mapea una zona de color definida en el frame ASCII a una propiedad del tema actual.
|
|
27
|
+
* @param zone Nombre de la zona (ej. 'hotPink', 'white', 'purple').
|
|
28
|
+
*/
|
|
29
|
+
const getZoneColor = (zone: string) => {
|
|
30
|
+
if (zone === "hotPink") return t.secondary;
|
|
31
|
+
if (zone === "white") return t.primary;
|
|
32
|
+
if (zone === "purple") return t.accent;
|
|
33
|
+
if (zone === "neonBlue") return t.primary;
|
|
34
|
+
return t.primary;
|
|
35
|
+
};
|
|
36
|
+
|
|
18
37
|
return (
|
|
19
38
|
<box flexDirection="column" alignItems="center">
|
|
39
|
+
{/* Mapeo del arte ASCII línea por línea aplicando colores según su zona */}
|
|
20
40
|
{archLogoHome.map((line, i) => {
|
|
21
41
|
const zone = homeLogoZones[i];
|
|
22
|
-
const color =
|
|
42
|
+
const color = getZoneColor(zone);
|
|
23
43
|
return <text fg={color}>{line}</text>;
|
|
24
44
|
})}
|
|
25
45
|
|
|
26
46
|
<text> </text>
|
|
47
|
+
{/* Leyenda personalizada estilo CLI debajor del logo */}
|
|
27
48
|
<box flexDirection="row" gap={0}>
|
|
28
|
-
<text fg=
|
|
49
|
+
<text fg={t.secondary} bold={true}>
|
|
29
50
|
j0k3r
|
|
30
51
|
</text>
|
|
31
|
-
<text fg=
|
|
32
|
-
<text fg=
|
|
52
|
+
<text fg={t.accent}>-</text>
|
|
53
|
+
<text fg={t.primary} bold={true}>
|
|
33
54
|
dev
|
|
34
55
|
</text>
|
|
35
|
-
<text fg=
|
|
36
|
-
<text fg=
|
|
56
|
+
<text fg={t.accent}>-</text>
|
|
57
|
+
<text fg={t.info} bold={true}>
|
|
37
58
|
rgl
|
|
38
59
|
</text>
|
|
39
|
-
<text fg=
|
|
40
|
-
<text fg=
|
|
60
|
+
<text fg={t.accent}>@</text>
|
|
61
|
+
<text fg={t.warning} bold={true}>
|
|
41
62
|
latest
|
|
42
63
|
</text>
|
|
43
64
|
</box>
|
|
@@ -61,53 +82,69 @@ export const HomeLogo = (props: { theme: TuiThemeCurrent }) => {
|
|
|
61
82
|
);
|
|
62
83
|
};
|
|
63
84
|
|
|
64
|
-
// ───
|
|
85
|
+
// ─── Utilidad de Barra de Progreso ───────────────────────────────────────────
|
|
86
|
+
/**
|
|
87
|
+
* Un componente visual que renderiza una barra de progreso estilo consola.
|
|
88
|
+
* Utiliza caracteres de bloque (█ y ░) para representar el llenado.
|
|
89
|
+
*/
|
|
65
90
|
const ProgressBar = (props: {
|
|
66
|
-
value: number;
|
|
67
|
-
width?: number;
|
|
68
|
-
fillColor: string;
|
|
69
|
-
emptyColor: string;
|
|
91
|
+
value: number; // Porcentaje (0-100)
|
|
92
|
+
width?: number; // Ancho total de la barra en caracteres
|
|
93
|
+
fillColor: string; // Color de la parte llena
|
|
94
|
+
emptyColor: string; // Color de la parte vacía
|
|
70
95
|
theme: TuiThemeCurrent;
|
|
71
96
|
}) => {
|
|
72
|
-
const width = props.width ?? 12;
|
|
73
|
-
const pct = Math.max(0, Math.min(100, props.value));
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
97
|
+
const width = () => props.width ?? 12;
|
|
98
|
+
const pct = () => Math.max(0, Math.min(100, props.value));
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Calcula cuántos bloques de '█' deben mostrarse.
|
|
102
|
+
* Garantiza que si el porcentaje es > 0, al menos se vea 1 bloque.
|
|
103
|
+
*/
|
|
104
|
+
const filled = () => {
|
|
105
|
+
const f = Math.round((pct() / 100) * width());
|
|
106
|
+
return pct() > 0 && f === 0 ? 1 : f;
|
|
107
|
+
};
|
|
108
|
+
const empty = () => width() - filled();
|
|
78
109
|
|
|
79
110
|
return (
|
|
80
111
|
<box flexDirection="row" gap={0}>
|
|
81
112
|
<text fg={props.theme.textMuted}>[</text>
|
|
82
|
-
<text fg={props.fillColor}>{
|
|
83
|
-
<text fg={props.emptyColor}>{
|
|
113
|
+
<text fg={props.fillColor}>{"█".repeat(filled())}</text>
|
|
114
|
+
<text fg={props.emptyColor}>{"░".repeat(empty())}</text>
|
|
84
115
|
<text fg={props.theme.textMuted}>]</text>
|
|
85
116
|
</box>
|
|
86
117
|
);
|
|
87
118
|
};
|
|
88
119
|
|
|
89
|
-
// ─── Sidebar: Arch
|
|
120
|
+
// ─── Sidebar: Logo de Arch + Panel de Estadísticas ───────────────────────────
|
|
121
|
+
/**
|
|
122
|
+
* Componente principal del panel lateral.
|
|
123
|
+
* Muestra una versión mini del logo de Arch Linux, la rama de Git actual y
|
|
124
|
+
* estadísticas detalladas de uso del contexto (tokens y costo estimado).
|
|
125
|
+
*/
|
|
90
126
|
export const SidebarArch = (props: {
|
|
91
127
|
theme: TuiThemeCurrent;
|
|
128
|
+
selectedTheme: string; // ID del tema activo
|
|
92
129
|
config: Cfg;
|
|
93
|
-
branch?: string;
|
|
94
|
-
getMessages?: () => any[];
|
|
95
|
-
contextLimit: number;
|
|
130
|
+
branch?: string; // Rama git actual detectada
|
|
131
|
+
getMessages?: () => any[]; // Función para obtener el historial de mensajes
|
|
132
|
+
contextLimit: number; // Límite de tokens del contexto
|
|
96
133
|
}) => {
|
|
97
134
|
if (!props.config.show_sidebar) return null;
|
|
98
135
|
|
|
99
136
|
const t = props.theme;
|
|
100
137
|
|
|
101
|
-
|
|
138
|
+
/**
|
|
139
|
+
* Calcula el total de tokens utilizados en el último mensaje del asistente.
|
|
140
|
+
*/
|
|
102
141
|
const getContextTokens = () => {
|
|
103
142
|
const messages = props.getMessages ? props.getMessages() : [];
|
|
104
|
-
// Buscar el último mensaje de asistente que tuvo output de tokens
|
|
105
143
|
const last = [...messages]
|
|
106
144
|
.reverse()
|
|
107
145
|
.find((m: any) => m.role === "assistant" && m.tokens?.output > 0);
|
|
108
146
|
if (!last) return 0;
|
|
109
147
|
|
|
110
|
-
// Suma total de todos los tokens del contexto de ese mensaje exacto
|
|
111
148
|
const tk = last.tokens;
|
|
112
149
|
return (
|
|
113
150
|
(tk.input ?? 0) +
|
|
@@ -118,6 +155,9 @@ export const SidebarArch = (props: {
|
|
|
118
155
|
);
|
|
119
156
|
};
|
|
120
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Suma el costo acumulado de todos los mensajes en la sesión actual.
|
|
160
|
+
*/
|
|
121
161
|
const getTotalCost = () => {
|
|
122
162
|
const messages = props.getMessages ? props.getMessages() : [];
|
|
123
163
|
return messages.reduce(
|
|
@@ -126,95 +166,101 @@ export const SidebarArch = (props: {
|
|
|
126
166
|
);
|
|
127
167
|
};
|
|
128
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Calcula el porcentaje de ocupación del contexto actual respecto al límite.
|
|
171
|
+
*/
|
|
129
172
|
const getContextPct = () => {
|
|
130
173
|
const limit = props.contextLimit || 1_000_000;
|
|
131
|
-
|
|
174
|
+
const pct = Math.round((getContextTokens() / limit) * 100);
|
|
175
|
+
return Math.min(100, Math.max(0, pct));
|
|
132
176
|
};
|
|
133
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Calcula el porcentaje de presupuesto gastado (asume un máximo de $1 para visualización).
|
|
180
|
+
*/
|
|
134
181
|
const getCostPct = () => Math.min(100, Math.round(getTotalCost() * 100));
|
|
135
182
|
|
|
136
183
|
const fmtTokens = (n: number) =>
|
|
137
184
|
n >= 1000 ? `${(n / 1000).toFixed(1)}k` : `${n}`;
|
|
138
185
|
const fmtCost = (n: number) => `$${n.toFixed(2)}`;
|
|
139
186
|
|
|
140
|
-
// Color: verde → amarillo → rojo según el %
|
|
141
|
-
const getCtxColor = () => {
|
|
142
|
-
const pct = getContextPct();
|
|
143
|
-
return pct < 50 ? "#00e5a0" : pct < 80 ? "#ffd166" : "#ff2d78";
|
|
144
|
-
};
|
|
145
|
-
|
|
146
187
|
return (
|
|
147
188
|
<box flexDirection="column" alignItems="center">
|
|
148
|
-
{/* Mini Arch
|
|
189
|
+
{/* Mini logo de Arch adaptado al tema */}
|
|
149
190
|
{archLogoSidebar.map((line, i) => {
|
|
150
191
|
const zone = sidebarLogoZones[i];
|
|
151
|
-
const color =
|
|
192
|
+
const color = zone === "hotPink" ? t.secondary : t.primary;
|
|
152
193
|
return <text fg={color}>{line}</text>;
|
|
153
194
|
})}
|
|
154
195
|
|
|
155
|
-
<text fg={t.textMuted}>j0k3r@latest</text>
|
|
196
|
+
<text fg={t.textMuted} scale={0.9}>j0k3r@latest</text>
|
|
156
197
|
<text> </text>
|
|
157
198
|
|
|
158
|
-
{/* Git
|
|
199
|
+
{/* Indicador de rama Git */}
|
|
159
200
|
{props.branch && (
|
|
160
201
|
<box flexDirection="row" gap={1}>
|
|
161
|
-
<text fg=
|
|
162
|
-
<text fg={t.text}>{props.branch}</text>
|
|
202
|
+
<text fg={t.accent}>⎇</text>
|
|
203
|
+
<text fg={t.text} bold={true}>{props.branch}</text>
|
|
163
204
|
</box>
|
|
164
205
|
)}
|
|
165
206
|
|
|
166
|
-
{/* ──
|
|
207
|
+
{/* ── Sección de Contexto (tokens + % usado + costo) ── */}
|
|
167
208
|
<box flexDirection="column" alignItems="center" marginTop={1}>
|
|
168
|
-
<text fg={t.textMuted} bold={true}>
|
|
169
|
-
Context
|
|
170
|
-
</text>
|
|
209
|
+
<text fg={t.textMuted} bold={true}>Contexto</text>
|
|
171
210
|
|
|
172
|
-
{/*
|
|
173
|
-
<box flexDirection="row" gap={1}>
|
|
174
|
-
<text fg={t.
|
|
211
|
+
{/* Barra de Tokens - Color Primario */}
|
|
212
|
+
<box flexDirection="row" gap={1} marginTop={0.5}>
|
|
213
|
+
<text fg={t.primary} bold={true}>{fmtTokens(getContextTokens())}</text>
|
|
175
214
|
<text fg={t.textMuted}>tokens</text>
|
|
176
215
|
</box>
|
|
177
216
|
<ProgressBar
|
|
178
217
|
value={getContextPct()}
|
|
179
218
|
width={18}
|
|
180
|
-
fillColor={
|
|
181
|
-
emptyColor=
|
|
219
|
+
fillColor={t.primary}
|
|
220
|
+
emptyColor={t.borderSubtle}
|
|
182
221
|
theme={t}
|
|
183
222
|
/>
|
|
184
223
|
|
|
185
|
-
{/*
|
|
186
|
-
<box flexDirection="row" gap={1}>
|
|
187
|
-
<text fg={
|
|
188
|
-
<text fg={t.textMuted}>
|
|
224
|
+
{/* Barra de Uso - Color Secundario */}
|
|
225
|
+
<box flexDirection="row" gap={1} marginTop={0.5}>
|
|
226
|
+
<text fg={t.secondary} bold={true}>{getContextPct()}%</text>
|
|
227
|
+
<text fg={t.textMuted}>usado</text>
|
|
189
228
|
</box>
|
|
190
229
|
<ProgressBar
|
|
191
230
|
value={getContextPct()}
|
|
192
231
|
width={18}
|
|
193
|
-
fillColor={
|
|
194
|
-
emptyColor=
|
|
232
|
+
fillColor={t.secondary}
|
|
233
|
+
emptyColor={t.borderSubtle}
|
|
195
234
|
theme={t}
|
|
196
235
|
/>
|
|
197
236
|
|
|
198
|
-
{/*
|
|
199
|
-
<box flexDirection="row" gap={1}>
|
|
200
|
-
<text fg=
|
|
201
|
-
<text fg={t.textMuted}>
|
|
237
|
+
{/* Barra de Costo - Color de Advertencia (Warning) */}
|
|
238
|
+
<box flexDirection="row" gap={1} marginTop={0.5}>
|
|
239
|
+
<text fg={t.warning} bold={true}>{fmtCost(getTotalCost())}</text>
|
|
240
|
+
<text fg={t.textMuted}>gastado</text>
|
|
202
241
|
</box>
|
|
203
242
|
<ProgressBar
|
|
204
243
|
value={getCostPct()}
|
|
205
244
|
width={18}
|
|
206
|
-
fillColor=
|
|
207
|
-
emptyColor=
|
|
245
|
+
fillColor={t.warning}
|
|
246
|
+
emptyColor={t.borderSubtle}
|
|
208
247
|
theme={t}
|
|
209
248
|
/>
|
|
210
249
|
</box>
|
|
211
250
|
|
|
212
251
|
<text> </text>
|
|
252
|
+
<text fg={t.textMuted} dimColor={true} scale={0.8}>
|
|
253
|
+
máscara: {props.selectedTheme.toUpperCase()}
|
|
254
|
+
</text>
|
|
213
255
|
</box>
|
|
214
256
|
);
|
|
215
257
|
};
|
|
216
258
|
|
|
217
|
-
// ───
|
|
259
|
+
// ─── Línea de detección de entorno ───────────────────────────────────────────
|
|
260
|
+
/**
|
|
261
|
+
* Muestra información sobre el sistema operativo y los proveedores de IA
|
|
262
|
+
* activos al final de la pantalla de inicio.
|
|
263
|
+
*/
|
|
218
264
|
export const DetectedEnv = (props: {
|
|
219
265
|
theme: TuiThemeCurrent;
|
|
220
266
|
providers: ReadonlyArray<{ id: string; name: string }> | undefined;
|
|
@@ -231,7 +277,7 @@ export const DetectedEnv = (props: {
|
|
|
231
277
|
|
|
232
278
|
return (
|
|
233
279
|
<box flexDirection="row" gap={1}>
|
|
234
|
-
<text fg={props.theme.textMuted}>
|
|
280
|
+
<text fg={props.theme.textMuted}>detectado:</text>
|
|
235
281
|
{os && <text fg={props.theme.text}>{os}</text>}
|
|
236
282
|
{os && providers && <text fg={props.theme.textMuted}>·</text>}
|
|
237
283
|
{providers && <text fg={props.theme.text}>{providers}</text>}
|
package/config.ts
CHANGED
|
@@ -1,30 +1,53 @@
|
|
|
1
|
-
// ───
|
|
1
|
+
// ─── Tipos de configuración y funciones de ayuda para el parseo ───────────────
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Define la estructura de configuración para el plugin Arch Mask.
|
|
5
|
+
* Controla qué elementos visuales se muestran y qué tema se utiliza.
|
|
6
|
+
*/
|
|
3
7
|
export type Cfg = {
|
|
4
|
-
enabled: boolean
|
|
5
|
-
theme: string
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
enabled: boolean // Indica si el plugin está activo
|
|
9
|
+
theme: string // El ID del tema visual a utilizar
|
|
10
|
+
color_preset: "current" | "cyber" | "neon" | "overclock" // Ajustes preestablecidos de color
|
|
11
|
+
set_theme: boolean // Si debe aplicar el tema automáticamente al iniciar
|
|
12
|
+
show_detected: boolean // Mostrar/ocultar la línea de entorno detectado
|
|
13
|
+
show_os: boolean // Mostrar/ocultar el nombre del sistema operativo
|
|
14
|
+
show_providers: boolean // Mostrar/ocultar los proveedores de IA detectados
|
|
15
|
+
show_sidebar: boolean // Mostrar/ocultar el panel lateral personalizado
|
|
11
16
|
}
|
|
12
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Utilidad para validar y seleccionar un valor de cadena con un respaldo (fallback).
|
|
20
|
+
* @param value El valor a evaluar
|
|
21
|
+
* @param fallback El valor por defecto si 'value' es inválido
|
|
22
|
+
*/
|
|
13
23
|
const pick = (value: unknown, fallback: string): string => {
|
|
14
24
|
if (typeof value !== "string") return fallback
|
|
15
25
|
if (!value.trim()) return fallback
|
|
16
26
|
return value
|
|
17
27
|
}
|
|
18
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Utilidad para validar valores booleanos con un respaldo (fallback).
|
|
31
|
+
* @param value El valor a evaluar
|
|
32
|
+
* @param fallback El valor por defecto si 'value' es inválido
|
|
33
|
+
*/
|
|
19
34
|
const bool = (value: unknown, fallback: boolean): boolean => {
|
|
20
35
|
if (typeof value !== "boolean") return fallback
|
|
21
36
|
return value
|
|
22
37
|
}
|
|
23
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Procesa las opciones proporcionadas y devuelve un objeto de configuración completo.
|
|
41
|
+
* Asegura que todas las propiedades tengan valores válidos mediante el uso de fallbacks.
|
|
42
|
+
*
|
|
43
|
+
* @param opts Diccionario de opciones crudas, usualmente provenientes de la configuración del plugin.
|
|
44
|
+
* @returns Un objeto de tipo Cfg con valores saneados.
|
|
45
|
+
*/
|
|
24
46
|
export const cfg = (opts: Record<string, unknown> | undefined): Cfg => {
|
|
25
47
|
return {
|
|
26
48
|
enabled: bool(opts?.enabled, true),
|
|
27
49
|
theme: pick(opts?.theme, "j0k3r-dev-rgl"),
|
|
50
|
+
color_preset: (opts?.color_preset as any) || "current",
|
|
28
51
|
set_theme: bool(opts?.set_theme, true),
|
|
29
52
|
show_detected: bool(opts?.show_detected, true),
|
|
30
53
|
show_os: bool(opts?.show_os, true),
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "opencode-mask-j0k3r-dev-rgl",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "3.0.0",
|
|
5
5
|
"description": "Arch Linux TUI mask for OpenCode — hot pink theme with prominent ASCII logo and j0k3r-dev-rgl@latest legend",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"exports": {
|
package/tui.tsx
CHANGED
|
@@ -6,25 +6,153 @@ import { cfg } from "./config";
|
|
|
6
6
|
|
|
7
7
|
const id = "j0k3r-dev-rgl";
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Función de utilidad para convertir objetos desconocidos en diccionarios planos.
|
|
11
|
+
*/
|
|
9
12
|
const rec = (value: unknown) => {
|
|
10
13
|
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
11
14
|
return Object.fromEntries(Object.entries(value));
|
|
12
15
|
};
|
|
13
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Punto de entrada principal para el plugin Arch Mask.
|
|
19
|
+
* Este plugin personaliza la interfaz de usuario (TUI) de OpenCode con una estética inspirada en Arch Linux.
|
|
20
|
+
*
|
|
21
|
+
* @param api La API de la TUI proporcionada por el sistema de plugins.
|
|
22
|
+
* @param options Opciones de configuración del usuario.
|
|
23
|
+
*/
|
|
14
24
|
const tui: TuiPlugin = async (api, options) => {
|
|
25
|
+
// Cargar y validar la configuración
|
|
15
26
|
const boot = cfg(rec(options));
|
|
16
27
|
if (!boot.enabled) return;
|
|
17
28
|
|
|
18
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Resuelve la ruta absoluta de un archivo de tema JSON.
|
|
31
|
+
*/
|
|
32
|
+
const resolveTheme = (name: string) =>
|
|
33
|
+
new URL(`./themes/${name}.json`, import.meta.url).pathname;
|
|
34
|
+
|
|
35
|
+
// ─── Instalación de Temas ────────────────────────────────────────────────
|
|
36
|
+
// Registra todos los esquemas de color disponibles en el sistema de la TUI.
|
|
19
37
|
try {
|
|
20
|
-
await api.theme.install("
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
38
|
+
await api.theme.install(resolveTheme("j0k3r-dev-rgl"));
|
|
39
|
+
await api.theme.install(resolveTheme("arch-cyber"));
|
|
40
|
+
await api.theme.install(resolveTheme("arch-neon"));
|
|
41
|
+
await api.theme.install(resolveTheme("arch-overclock"));
|
|
42
|
+
await api.theme.install(resolveTheme("mask-cyber"));
|
|
43
|
+
await api.theme.install(resolveTheme("tokyo-night-dev"));
|
|
44
|
+
await api.theme.install(resolveTheme("arch-electric"));
|
|
45
|
+
await api.theme.install(resolveTheme("j0k3r-neon"));
|
|
46
|
+
} catch (e) {
|
|
47
|
+
console.error("Fallo al instalar temas", e);
|
|
24
48
|
}
|
|
25
49
|
|
|
26
|
-
//
|
|
27
|
-
|
|
50
|
+
// ─── Comandos y Atajos ───────────────────────────────────────────────────
|
|
51
|
+
api.command.register(() => [
|
|
52
|
+
{
|
|
53
|
+
title: "Cambiar Máscara",
|
|
54
|
+
value: "mask",
|
|
55
|
+
description: "Cambiar el estilo visual de Arch Mask",
|
|
56
|
+
keybind: "ctrl+m",
|
|
57
|
+
slash: { name: "mask" },
|
|
58
|
+
onSelect: () => {
|
|
59
|
+
// Guardamos el tema actual por si el usuario cancela la selección
|
|
60
|
+
const originalTheme = api.theme.selected;
|
|
61
|
+
|
|
62
|
+
// Abrir un diálogo de selección con previsualización en tiempo real
|
|
63
|
+
api.ui.dialog.replace(() => (
|
|
64
|
+
<api.ui.DialogSelect
|
|
65
|
+
title="Arch Mask: Theme Preview"
|
|
66
|
+
options={[
|
|
67
|
+
{
|
|
68
|
+
title: "Default (j0k3r-dev-rgl)",
|
|
69
|
+
value: "j0k3r-dev-rgl",
|
|
70
|
+
description: "Estilo original con acentos púrpuras y azul Arch."
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
title: "Tokyo Night Dev",
|
|
74
|
+
value: "tokyo-night-dev",
|
|
75
|
+
description: "Lindo tema dark profesional con fondo transparente (Recomendado)."
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
title: "Arch Electric",
|
|
79
|
+
value: "arch-electric",
|
|
80
|
+
description: "Tema neón azul eléctrico que combina con tu fondo de pantalla."
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
title: "j0k3r Neon",
|
|
84
|
+
value: "j0k3r-neon",
|
|
85
|
+
description: "Inspirado en tu logo de Arch y nombre en verde neón."
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
title: "Cyber Arch",
|
|
89
|
+
value: "arch-cyber",
|
|
90
|
+
description: "Inspirado en los colores clásicos de Arch Linux (Azules y Grises)."
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
title: "Semáforo Neón",
|
|
94
|
+
value: "arch-neon",
|
|
95
|
+
description: "Contraste extremo con verdes y azules neón."
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
title: "Overclock",
|
|
99
|
+
value: "arch-overclock",
|
|
100
|
+
description: "Estilo agresivo en rosa y blanco puro."
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
title: "Cyber Mask",
|
|
104
|
+
value: "mask-cyber",
|
|
105
|
+
description: "Futurismo puro: neón azul y rosa fuerte."
|
|
106
|
+
},
|
|
107
|
+
]}
|
|
108
|
+
current={api.theme.selected}
|
|
109
|
+
/**
|
|
110
|
+
* Lógica de Previsualización:
|
|
111
|
+
* El evento 'onMove' se dispara cada vez que el usuario navega por la lista.
|
|
112
|
+
* Cambiamos el tema global instantáneamente para que el usuario vea el resultado.
|
|
113
|
+
*/
|
|
114
|
+
onMove={(opt) => {
|
|
115
|
+
try {
|
|
116
|
+
api.theme.set(opt.value);
|
|
117
|
+
} catch (e) {}
|
|
118
|
+
}}
|
|
119
|
+
/**
|
|
120
|
+
* Lógica de Confirmación:
|
|
121
|
+
* Aplica el tema definitivamente y lo guarda en el almacenamiento persistente (KV).
|
|
122
|
+
*/
|
|
123
|
+
onSelect={(opt) => {
|
|
124
|
+
try {
|
|
125
|
+
api.theme.set(opt.value);
|
|
126
|
+
api.kv.set("selected_theme", opt.value);
|
|
127
|
+
api.ui.dialog.clear();
|
|
128
|
+
api.ui.toast({
|
|
129
|
+
title: "Máscara Aplicada",
|
|
130
|
+
message: `Tema ${opt.title} configurado como predeterminado.`,
|
|
131
|
+
variant: "success",
|
|
132
|
+
});
|
|
133
|
+
} catch (e) {}
|
|
134
|
+
}}
|
|
135
|
+
/**
|
|
136
|
+
* Lógica de Cancelación:
|
|
137
|
+
* Si el usuario presiona ESC o cierra el diálogo, restauramos el tema que estaba antes.
|
|
138
|
+
*/
|
|
139
|
+
onCancel={() => {
|
|
140
|
+
try {
|
|
141
|
+
api.theme.set(originalTheme);
|
|
142
|
+
} catch (e) {}
|
|
143
|
+
api.ui.dialog.clear();
|
|
144
|
+
}}
|
|
145
|
+
/>
|
|
146
|
+
));
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
]);
|
|
150
|
+
|
|
151
|
+
// ─── Gestión de Plugins Internos ──────────────────────────────────────────
|
|
152
|
+
/**
|
|
153
|
+
* Configura qué plugins de la barra lateral deben estar activos o inactivos
|
|
154
|
+
* para mantener una interfaz limpia y coherente con el estilo 'Mask'.
|
|
155
|
+
*/
|
|
28
156
|
const enableInternal = async () => {
|
|
29
157
|
try {
|
|
30
158
|
await api.plugins.deactivate("internal:sidebar-context");
|
|
@@ -32,31 +160,21 @@ const tui: TuiPlugin = async (api, options) => {
|
|
|
32
160
|
await api.plugins.activate("internal:sidebar-lsp");
|
|
33
161
|
await api.plugins.activate("internal:sidebar-todo");
|
|
34
162
|
await api.plugins.activate("internal:sidebar-files");
|
|
35
|
-
} catch (e) {
|
|
36
|
-
// Silent fail
|
|
37
|
-
}
|
|
163
|
+
} catch (e) {}
|
|
38
164
|
};
|
|
39
165
|
enableInternal();
|
|
40
166
|
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const [providerID, modelID] = modelStr.split("/");
|
|
46
|
-
const provider = api.state.provider.find((p) => p.id === providerID);
|
|
47
|
-
const model = provider?.models?.[modelID];
|
|
48
|
-
if (model?.limit?.context) return model.limit.context;
|
|
49
|
-
} catch (_) {}
|
|
50
|
-
return 1_000_000;
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
// Slot registration
|
|
167
|
+
// ─── Registro de Slots de la UI ──────────────────────────────────────────
|
|
168
|
+
/**
|
|
169
|
+
* Define dónde y cómo se renderizan los componentes del plugin en la interfaz.
|
|
170
|
+
*/
|
|
54
171
|
api.slots.register({
|
|
55
172
|
slots: {
|
|
173
|
+
// Logo principal en la pantalla de bienvenida
|
|
56
174
|
home_logo(ctx) {
|
|
57
175
|
return <HomeLogo theme={ctx.theme.current} />;
|
|
58
176
|
},
|
|
59
|
-
|
|
177
|
+
// Barra de estado/detección en la parte inferior de la home
|
|
60
178
|
home_bottom(ctx) {
|
|
61
179
|
return (
|
|
62
180
|
<DetectedEnv
|
|
@@ -66,26 +184,23 @@ const tui: TuiPlugin = async (api, options) => {
|
|
|
66
184
|
/>
|
|
67
185
|
);
|
|
68
186
|
},
|
|
69
|
-
|
|
187
|
+
// Contenido personalizado para la barra lateral (Sidebar)
|
|
70
188
|
sidebar_content(ctx, value) {
|
|
71
189
|
const sessionID = value?.session_id;
|
|
72
|
-
|
|
73
|
-
const branch = api.state.vcs?.branch;
|
|
74
|
-
const contextLimit = getContextLimit();
|
|
75
|
-
|
|
76
190
|
return (
|
|
77
191
|
<SidebarArch
|
|
78
192
|
theme={ctx.theme.current}
|
|
193
|
+
selectedTheme={api.theme.selected}
|
|
79
194
|
config={boot}
|
|
80
|
-
branch={branch}
|
|
195
|
+
branch={api.state.vcs?.branch}
|
|
81
196
|
getMessages={() =>
|
|
82
197
|
sessionID ? api.state.session.messages(sessionID) : []
|
|
83
198
|
}
|
|
84
|
-
contextLimit={
|
|
199
|
+
contextLimit={1000000}
|
|
85
200
|
/>
|
|
86
201
|
);
|
|
87
202
|
},
|
|
88
|
-
|
|
203
|
+
// Personalización del prompt en la sesión de chat
|
|
89
204
|
session_prompt_right(ctx, value) {
|
|
90
205
|
const t = ctx.theme.current;
|
|
91
206
|
return (
|
|
@@ -100,9 +215,5 @@ const tui: TuiPlugin = async (api, options) => {
|
|
|
100
215
|
});
|
|
101
216
|
};
|
|
102
217
|
|
|
103
|
-
const plugin: TuiPluginModule & { id: string } = {
|
|
104
|
-
id,
|
|
105
|
-
tui,
|
|
106
|
-
};
|
|
107
|
-
|
|
218
|
+
const plugin: TuiPluginModule & { id: string } = { id, tui };
|
|
108
219
|
export default plugin;
|