opencode-mask-j0k3r-dev-rgl 2.0.22 → 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/ascii-frames.ts +11 -11
- package/components.tsx +276 -214
- package/config.ts +31 -8
- package/package.json +1 -1
- package/tui.tsx +213 -142
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/ascii-frames.ts
CHANGED
|
@@ -47,9 +47,9 @@ export const homeLogoZones: ("hotPink" | "white")[] = [
|
|
|
47
47
|
"hotPink", // line 6: `/:-:++oooo+:
|
|
48
48
|
"hotPink", // line 7: `/++++/+++++++:
|
|
49
49
|
"hotPink", // line 8: `/+++++++++++++++:
|
|
50
|
-
"white",
|
|
51
|
-
"white",
|
|
52
|
-
"white",
|
|
50
|
+
"white", // line 9: `/+++ooooooooooooo/`
|
|
51
|
+
"white", // line 10: ./ooosssso++osssssso+`
|
|
52
|
+
"white", // line 11: .oossssso-````/ossssss+`
|
|
53
53
|
"hotPink", // line 12: -osssssso. :ssssssso.
|
|
54
54
|
"hotPink", // line 13: :osssssss/ osssso+++.
|
|
55
55
|
"hotPink", // line 14: /ossssssss/ +ssssooo/-
|
|
@@ -61,14 +61,14 @@ export const homeLogoZones: ("hotPink" | "white")[] = [
|
|
|
61
61
|
|
|
62
62
|
// ─── Sidebar logo (mini Arch) ────────────────────────────────────────────────
|
|
63
63
|
export const archLogoSidebar: string[] = [
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
64
|
+
" /\\ ",
|
|
65
|
+
" / \\ ",
|
|
66
|
+
" / \\ ",
|
|
67
|
+
" / \\ ",
|
|
68
|
+
" / ,, \\ ",
|
|
69
|
+
" / | | \\ ",
|
|
70
|
+
' / /-""-\\ \\ ',
|
|
71
|
+
"/___/ \\___\\",
|
|
72
72
|
];
|
|
73
73
|
|
|
74
74
|
export const sidebarLogoZones: ("hotPink" | "white")[] = [
|
package/components.tsx
CHANGED
|
@@ -1,224 +1,286 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
/** @jsxImportSource @opentui/solid */
|
|
3
|
-
import type { TuiThemeCurrent
|
|
4
|
-
import type { Cfg } from "./config"
|
|
5
|
-
import { getOSName, getProviders } from "./detection"
|
|
3
|
+
import type { TuiThemeCurrent } from "@opencode-ai/plugin/tui";
|
|
6
4
|
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
} from "./ascii-frames"
|
|
13
|
-
|
|
14
|
-
|
|
5
|
+
archLogoHome,
|
|
6
|
+
archLogoSidebar,
|
|
7
|
+
homeLogoZones,
|
|
8
|
+
sidebarLogoZones,
|
|
9
|
+
zoneColors,
|
|
10
|
+
} from "./ascii-frames";
|
|
11
|
+
import type { Cfg } from "./config";
|
|
12
|
+
import { getOSName, getProviders } from "./detection";
|
|
13
|
+
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
23
|
+
const t = props.theme;
|
|
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
|
+
|
|
37
|
+
return (
|
|
38
|
+
<box flexDirection="column" alignItems="center">
|
|
39
|
+
{/* Mapeo del arte ASCII línea por línea aplicando colores según su zona */}
|
|
40
|
+
{archLogoHome.map((line, i) => {
|
|
41
|
+
const zone = homeLogoZones[i];
|
|
42
|
+
const color = getZoneColor(zone);
|
|
43
|
+
return <text fg={color}>{line}</text>;
|
|
44
|
+
})}
|
|
45
|
+
|
|
46
|
+
<text> </text>
|
|
47
|
+
{/* Leyenda personalizada estilo CLI debajor del logo */}
|
|
48
|
+
<box flexDirection="row" gap={0}>
|
|
49
|
+
<text fg={t.secondary} bold={true}>
|
|
50
|
+
j0k3r
|
|
51
|
+
</text>
|
|
52
|
+
<text fg={t.accent}>-</text>
|
|
53
|
+
<text fg={t.primary} bold={true}>
|
|
54
|
+
dev
|
|
55
|
+
</text>
|
|
56
|
+
<text fg={t.accent}>-</text>
|
|
57
|
+
<text fg={t.info} bold={true}>
|
|
58
|
+
rgl
|
|
59
|
+
</text>
|
|
60
|
+
<text fg={t.accent}>@</text>
|
|
61
|
+
<text fg={t.warning} bold={true}>
|
|
62
|
+
latest
|
|
63
|
+
</text>
|
|
64
|
+
</box>
|
|
65
|
+
|
|
66
|
+
<box flexDirection="row" gap={0} marginTop={1}>
|
|
67
|
+
<text fg={t.textMuted} dimColor={true}>
|
|
68
|
+
╭{" "}
|
|
69
|
+
</text>
|
|
70
|
+
<text fg={t.textMuted}>arch linux </text>
|
|
71
|
+
<text fg={t.textMuted} dimColor={true}>
|
|
72
|
+
·
|
|
73
|
+
</text>
|
|
74
|
+
<text fg={t.textMuted}> opencode </text>
|
|
75
|
+
<text fg={t.textMuted} dimColor={true}>
|
|
76
|
+
╮
|
|
77
|
+
</text>
|
|
78
|
+
</box>
|
|
79
|
+
|
|
80
|
+
<text> </text>
|
|
81
|
+
</box>
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
|
|
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
|
+
*/
|
|
51
90
|
const ProgressBar = (props: {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
95
|
+
theme: TuiThemeCurrent;
|
|
57
96
|
}) => {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
|
|
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();
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<box flexDirection="row" gap={0}>
|
|
112
|
+
<text fg={props.theme.textMuted}>[</text>
|
|
113
|
+
<text fg={props.fillColor}>{"█".repeat(filled())}</text>
|
|
114
|
+
<text fg={props.emptyColor}>{"░".repeat(empty())}</text>
|
|
115
|
+
<text fg={props.theme.textMuted}>]</text>
|
|
116
|
+
</box>
|
|
117
|
+
);
|
|
118
|
+
};
|
|
119
|
+
|
|
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
|
+
*/
|
|
76
126
|
export const SidebarArch = (props: {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
contextTokens: number
|
|
84
|
-
contextCost: number
|
|
85
|
-
contextLimit: number
|
|
127
|
+
theme: TuiThemeCurrent;
|
|
128
|
+
selectedTheme: string; // ID del tema activo
|
|
129
|
+
config: Cfg;
|
|
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
|
|
86
133
|
}) => {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
134
|
+
if (!props.config.show_sidebar) return null;
|
|
135
|
+
|
|
136
|
+
const t = props.theme;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Calcula el total de tokens utilizados en el último mensaje del asistente.
|
|
140
|
+
*/
|
|
141
|
+
const getContextTokens = () => {
|
|
142
|
+
const messages = props.getMessages ? props.getMessages() : [];
|
|
143
|
+
const last = [...messages]
|
|
144
|
+
.reverse()
|
|
145
|
+
.find((m: any) => m.role === "assistant" && m.tokens?.output > 0);
|
|
146
|
+
if (!last) return 0;
|
|
147
|
+
|
|
148
|
+
const tk = last.tokens;
|
|
149
|
+
return (
|
|
150
|
+
(tk.input ?? 0) +
|
|
151
|
+
(tk.output ?? 0) +
|
|
152
|
+
(tk.reasoning ?? 0) +
|
|
153
|
+
(tk.cache?.read ?? 0) +
|
|
154
|
+
(tk.cache?.write ?? 0)
|
|
155
|
+
);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Suma el costo acumulado de todos los mensajes en la sesión actual.
|
|
160
|
+
*/
|
|
161
|
+
const getTotalCost = () => {
|
|
162
|
+
const messages = props.getMessages ? props.getMessages() : [];
|
|
163
|
+
return messages.reduce(
|
|
164
|
+
(sum, item) => sum + (item.role === "assistant" ? (item.cost ?? 0) : 0),
|
|
165
|
+
0,
|
|
166
|
+
);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Calcula el porcentaje de ocupación del contexto actual respecto al límite.
|
|
171
|
+
*/
|
|
172
|
+
const getContextPct = () => {
|
|
173
|
+
const limit = props.contextLimit || 1_000_000;
|
|
174
|
+
const pct = Math.round((getContextTokens() / limit) * 100);
|
|
175
|
+
return Math.min(100, Math.max(0, pct));
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Calcula el porcentaje de presupuesto gastado (asume un máximo de $1 para visualización).
|
|
180
|
+
*/
|
|
181
|
+
const getCostPct = () => Math.min(100, Math.round(getTotalCost() * 100));
|
|
182
|
+
|
|
183
|
+
const fmtTokens = (n: number) =>
|
|
184
|
+
n >= 1000 ? `${(n / 1000).toFixed(1)}k` : `${n}`;
|
|
185
|
+
const fmtCost = (n: number) => `$${n.toFixed(2)}`;
|
|
186
|
+
|
|
187
|
+
return (
|
|
188
|
+
<box flexDirection="column" alignItems="center">
|
|
189
|
+
{/* Mini logo de Arch adaptado al tema */}
|
|
190
|
+
{archLogoSidebar.map((line, i) => {
|
|
191
|
+
const zone = sidebarLogoZones[i];
|
|
192
|
+
const color = zone === "hotPink" ? t.secondary : t.primary;
|
|
193
|
+
return <text fg={color}>{line}</text>;
|
|
194
|
+
})}
|
|
195
|
+
|
|
196
|
+
<text fg={t.textMuted} scale={0.9}>j0k3r@latest</text>
|
|
197
|
+
<text> </text>
|
|
198
|
+
|
|
199
|
+
{/* Indicador de rama Git */}
|
|
200
|
+
{props.branch && (
|
|
201
|
+
<box flexDirection="row" gap={1}>
|
|
202
|
+
<text fg={t.accent}>⎇</text>
|
|
203
|
+
<text fg={t.text} bold={true}>{props.branch}</text>
|
|
204
|
+
</box>
|
|
205
|
+
)}
|
|
206
|
+
|
|
207
|
+
{/* ── Sección de Contexto (tokens + % usado + costo) ── */}
|
|
208
|
+
<box flexDirection="column" alignItems="center" marginTop={1}>
|
|
209
|
+
<text fg={t.textMuted} bold={true}>Contexto</text>
|
|
210
|
+
|
|
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>
|
|
214
|
+
<text fg={t.textMuted}>tokens</text>
|
|
215
|
+
</box>
|
|
216
|
+
<ProgressBar
|
|
217
|
+
value={getContextPct()}
|
|
218
|
+
width={18}
|
|
219
|
+
fillColor={t.primary}
|
|
220
|
+
emptyColor={t.borderSubtle}
|
|
221
|
+
theme={t}
|
|
222
|
+
/>
|
|
223
|
+
|
|
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>
|
|
228
|
+
</box>
|
|
229
|
+
<ProgressBar
|
|
230
|
+
value={getContextPct()}
|
|
231
|
+
width={18}
|
|
232
|
+
fillColor={t.secondary}
|
|
233
|
+
emptyColor={t.borderSubtle}
|
|
234
|
+
theme={t}
|
|
235
|
+
/>
|
|
236
|
+
|
|
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>
|
|
241
|
+
</box>
|
|
242
|
+
<ProgressBar
|
|
243
|
+
value={getCostPct()}
|
|
244
|
+
width={18}
|
|
245
|
+
fillColor={t.warning}
|
|
246
|
+
emptyColor={t.borderSubtle}
|
|
247
|
+
theme={t}
|
|
248
|
+
/>
|
|
249
|
+
</box>
|
|
250
|
+
|
|
251
|
+
<text> </text>
|
|
252
|
+
<text fg={t.textMuted} dimColor={true} scale={0.8}>
|
|
253
|
+
máscara: {props.selectedTheme.toUpperCase()}
|
|
254
|
+
</text>
|
|
255
|
+
</box>
|
|
256
|
+
);
|
|
257
|
+
};
|
|
258
|
+
|
|
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
|
+
*/
|
|
204
264
|
export const DetectedEnv = (props: {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
265
|
+
theme: TuiThemeCurrent;
|
|
266
|
+
providers: ReadonlyArray<{ id: string; name: string }> | undefined;
|
|
267
|
+
config: Cfg;
|
|
208
268
|
}) => {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
269
|
+
if (!props.config.show_detected) return null;
|
|
270
|
+
|
|
271
|
+
const os = props.config.show_os ? getOSName() : null;
|
|
272
|
+
const providers = props.config.show_providers
|
|
273
|
+
? getProviders(props.providers)
|
|
274
|
+
: null;
|
|
275
|
+
|
|
276
|
+
if (!os && !providers) return null;
|
|
277
|
+
|
|
278
|
+
return (
|
|
279
|
+
<box flexDirection="row" gap={1}>
|
|
280
|
+
<text fg={props.theme.textMuted}>detectado:</text>
|
|
281
|
+
{os && <text fg={props.theme.text}>{os}</text>}
|
|
282
|
+
{os && providers && <text fg={props.theme.textMuted}>·</text>}
|
|
283
|
+
{providers && <text fg={props.theme.text}>{providers}</text>}
|
|
284
|
+
</box>
|
|
285
|
+
);
|
|
286
|
+
};
|
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
|
@@ -1,148 +1,219 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
/** @jsxImportSource @opentui/solid */
|
|
3
|
-
import type { TuiPlugin, TuiPluginModule } from "@opencode-ai/plugin/tui"
|
|
4
|
-
import {
|
|
5
|
-
import { cfg } from "./config"
|
|
6
|
-
import { HomeLogo, SidebarArch, DetectedEnv } from "./components"
|
|
3
|
+
import type { TuiPlugin, TuiPluginModule } from "@opencode-ai/plugin/tui";
|
|
4
|
+
import { DetectedEnv, HomeLogo, SidebarArch } from "./components";
|
|
5
|
+
import { cfg } from "./config";
|
|
7
6
|
|
|
8
|
-
const id = "j0k3r-dev-rgl"
|
|
7
|
+
const id = "j0k3r-dev-rgl";
|
|
9
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Función de utilidad para convertir objetos desconocidos en diccionarios planos.
|
|
11
|
+
*/
|
|
10
12
|
const rec = (value: unknown) => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
|
|
13
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
14
|
+
return Object.fromEntries(Object.entries(value));
|
|
15
|
+
};
|
|
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
|
+
*/
|
|
15
24
|
const tui: TuiPlugin = async (api, options) => {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
25
|
+
// Cargar y validar la configuración
|
|
26
|
+
const boot = cfg(rec(options));
|
|
27
|
+
if (!boot.enabled) return;
|
|
28
|
+
|
|
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.
|
|
37
|
+
try {
|
|
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);
|
|
48
|
+
}
|
|
49
|
+
|
|
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
|
+
*/
|
|
156
|
+
const enableInternal = async () => {
|
|
157
|
+
try {
|
|
158
|
+
await api.plugins.deactivate("internal:sidebar-context");
|
|
159
|
+
await api.plugins.deactivate("internal:sidebar-mcp");
|
|
160
|
+
await api.plugins.activate("internal:sidebar-lsp");
|
|
161
|
+
await api.plugins.activate("internal:sidebar-todo");
|
|
162
|
+
await api.plugins.activate("internal:sidebar-files");
|
|
163
|
+
} catch (e) {}
|
|
164
|
+
};
|
|
165
|
+
enableInternal();
|
|
166
|
+
|
|
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
|
+
*/
|
|
171
|
+
api.slots.register({
|
|
172
|
+
slots: {
|
|
173
|
+
// Logo principal en la pantalla de bienvenida
|
|
174
|
+
home_logo(ctx) {
|
|
175
|
+
return <HomeLogo theme={ctx.theme.current} />;
|
|
176
|
+
},
|
|
177
|
+
// Barra de estado/detección en la parte inferior de la home
|
|
178
|
+
home_bottom(ctx) {
|
|
179
|
+
return (
|
|
180
|
+
<DetectedEnv
|
|
181
|
+
theme={ctx.theme.current}
|
|
182
|
+
providers={api.state.provider}
|
|
183
|
+
config={boot}
|
|
184
|
+
/>
|
|
185
|
+
);
|
|
186
|
+
},
|
|
187
|
+
// Contenido personalizado para la barra lateral (Sidebar)
|
|
188
|
+
sidebar_content(ctx, value) {
|
|
189
|
+
const sessionID = value?.session_id;
|
|
190
|
+
return (
|
|
191
|
+
<SidebarArch
|
|
192
|
+
theme={ctx.theme.current}
|
|
193
|
+
selectedTheme={api.theme.selected}
|
|
194
|
+
config={boot}
|
|
195
|
+
branch={api.state.vcs?.branch}
|
|
196
|
+
getMessages={() =>
|
|
197
|
+
sessionID ? api.state.session.messages(sessionID) : []
|
|
198
|
+
}
|
|
199
|
+
contextLimit={1000000}
|
|
200
|
+
/>
|
|
201
|
+
);
|
|
202
|
+
},
|
|
203
|
+
// Personalización del prompt en la sesión de chat
|
|
204
|
+
session_prompt_right(ctx, value) {
|
|
205
|
+
const t = ctx.theme.current;
|
|
206
|
+
return (
|
|
207
|
+
<text fg={t.textMuted}>
|
|
208
|
+
<span style={{ fg: "#ff2d78" }}>j0k3r</span>
|
|
209
|
+
<span style={{ fg: "#9d4edd" }}>@</span>
|
|
210
|
+
{(value.session_id ?? "").slice(0, 6)}
|
|
211
|
+
</text>
|
|
212
|
+
);
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const plugin: TuiPluginModule & { id: string } = { id, tui };
|
|
219
|
+
export default plugin;
|