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 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", // line 9: `/+++ooooooooooooo/`
51
- "white", // line 10: ./ooosssso++osssssso+`
52
- "white", // line 11: .oossssso-````/ossssss+`
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, TuiSidebarMcpItem, TuiSidebarLspItem, TuiSidebarTodoItem } from "@opencode-ai/plugin/tui"
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
- archLogoHome,
8
- homeLogoZones,
9
- archLogoSidebar,
10
- sidebarLogoZones,
11
- zoneColors,
12
- } from "./ascii-frames"
13
-
14
- // ─── Home screen: Large Arch Linux logo + legend ─────────────────────────────
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
- const t = props.theme
17
-
18
- return (
19
- <box flexDirection="column" alignItems="center">
20
- {archLogoHome.map((line, i) => {
21
- const zone = homeLogoZones[i]
22
- const color = zoneColors[zone] || t.primary
23
- return <text fg={color}>{line}</text>
24
- })}
25
-
26
- <text> </text>
27
- <box flexDirection="row" gap={0}>
28
- <text fg="#ff2d78" bold={true}>j0k3r</text>
29
- <text fg="#9d4edd">-</text>
30
- <text fg="#00c8ff" bold={true}>dev</text>
31
- <text fg="#9d4edd">-</text>
32
- <text fg="#4dd8ff" bold={true}>rgl</text>
33
- <text fg="#9d4edd">@</text>
34
- <text fg="#ffd166" bold={true}>latest</text>
35
- </box>
36
-
37
- <box flexDirection="row" gap={0} marginTop={1}>
38
- <text fg={t.textMuted} dimColor={true}>╭ </text>
39
- <text fg={t.textMuted}>arch linux </text>
40
- <text fg={t.textMuted} dimColor={true}>·</text>
41
- <text fg={t.textMuted}> opencode </text>
42
- <text fg={t.textMuted} dimColor={true}>╮</text>
43
- </box>
44
-
45
- <text> </text>
46
- </box>
47
- )
48
- }
49
-
50
- // ─── Progress bar helper ──────────────────────────────────────────────────────
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
- value: number // 0100
53
- width?: number
54
- fillColor: string
55
- emptyColor: string
56
- theme: TuiThemeCurrent
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
- const width = props.width ?? 12
59
- const pct = Math.max(0, Math.min(100, props.value))
60
- const filled = Math.round((pct / 100) * width)
61
- const empty = width - filled
62
- const fill = "".repeat(filled)
63
- const trail = "░".repeat(empty)
64
-
65
- return (
66
- <box flexDirection="row" gap={0}>
67
- <text fg={props.theme.textMuted}>[</text>
68
- <text fg={props.fillColor}>{fill}</text>
69
- <text fg={props.emptyColor}>{trail}</text>
70
- <text fg={props.theme.textMuted}>]</text>
71
- </box>
72
- )
73
- }
74
-
75
- // ─── Sidebar: Arch logo + stats panel ────────────────────────────────────────
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
- theme: TuiThemeCurrent
78
- config: Cfg
79
- branch?: string
80
- mcpItems?: ReadonlyArray<TuiSidebarMcpItem>
81
- lspItems?: ReadonlyArray<TuiSidebarLspItem>
82
- todos?: ReadonlyArray<TuiSidebarTodoItem>
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
- if (!props.config.show_sidebar) return null
88
-
89
- const t = props.theme
90
-
91
- // ── Todos ─────────────────────────────────────────────────────────────────
92
- const todos = props.todos ?? []
93
- const doneTodos = todos.filter(t => t.status === "completed").length
94
- const totalTodos = todos.length
95
- const todoPct = totalTodos > 0 ? Math.round((doneTodos / totalTodos) * 100) : 0
96
-
97
- // ── MCP ───────────────────────────────────────────────────────────────────
98
- const mcpItems = props.mcpItems ?? []
99
- const mcpConnected = mcpItems.filter(m => m.status === "connected").length
100
- const mcpTotal = mcpItems.length
101
- const mcpPct = mcpTotal > 0 ? Math.round((mcpConnected / mcpTotal) * 100) : 0
102
-
103
- // ── LSP ───────────────────────────────────────────────────────────────────
104
- const lspItems = props.lspItems ?? []
105
- const lspActive = lspItems.filter(l => l.status === "idle" || l.status === "running").length
106
- const lspTotal = lspItems.length
107
-
108
- // ── Context — valores ya calculados en tui.tsx via eventos ────────────────
109
- const contextTokens = props.contextTokens
110
- const totalCost = props.contextCost
111
- const contextLimit = props.contextLimit
112
- const contextPct = Math.min(100, Math.round((contextTokens / contextLimit) * 100))
113
- const costPct = Math.min(100, Math.round(totalCost * 100))
114
-
115
- const fmtTokens = (n: number) => n >= 1000 ? `${(n / 1000).toFixed(1)}k` : `${n}`
116
- const fmtCost = (n: number) => `$${n.toFixed(2)}`
117
-
118
- // Color: verde → amarillo → rojo según el %
119
- const ctxColor = contextPct < 50 ? "#00e5a0" : contextPct < 80 ? "#ffd166" : "#ff2d78"
120
-
121
- return (
122
- <box flexDirection="column" alignItems="center">
123
-
124
- {/* Mini Arch logo */}
125
- {archLogoSidebar.map((line, i) => {
126
- const zone = sidebarLogoZones[i]
127
- const color = zoneColors[zone] || t.primary
128
- return <text fg={color}>{line}</text>
129
- })}
130
-
131
- <text fg={t.textMuted}>j0k3r@latest</text>
132
- <text> </text>
133
-
134
- {/* Git branch */}
135
- {props.branch && (
136
- <box flexDirection="row" gap={1}>
137
- <text fg="#ffd166">⎇</text>
138
- <text fg={t.text}>{props.branch}</text>
139
- </box>
140
- )}
141
-
142
- {/* ── Context (tokens + % used + cost) ── siempre visible */}
143
- <box flexDirection="column" alignItems="center" marginTop={1}>
144
- <text fg={t.textMuted} bold={true}>Context</text>
145
-
146
- {/* tokens */}
147
- <box flexDirection="row" gap={1}>
148
- <text fg={t.text}>{fmtTokens(contextTokens)}</text>
149
- <text fg={t.textMuted}>tokens</text>
150
- </box>
151
- <ProgressBar value={contextPct} width={18} fillColor={ctxColor} emptyColor="#3a3a3a" theme={t} />
152
-
153
- {/* % used */}
154
- <box flexDirection="row" gap={1}>
155
- <text fg={ctxColor}>{contextPct}%</text>
156
- <text fg={t.textMuted}>used</text>
157
- </box>
158
- <ProgressBar value={contextPct} width={18} fillColor={ctxColor} emptyColor="#3a3a3a" theme={t} />
159
-
160
- {/* $ spent */}
161
- <box flexDirection="row" gap={1}>
162
- <text fg="#ffd166">{fmtCost(totalCost)}</text>
163
- <text fg={t.textMuted}>spent</text>
164
- </box>
165
- <ProgressBar value={costPct} width={18} fillColor="#ffd166" emptyColor="#3a3a3a" theme={t} />
166
- </box>
167
-
168
- {/* ── Todos ── */}
169
- {totalTodos > 0 && (
170
- <box flexDirection="column" marginTop={1}>
171
- <box flexDirection="row" gap={1}>
172
- <text fg={t.textMuted}>todos</text>
173
- <text fg={t.text}>{doneTodos}/{totalTodos}</text>
174
- </box>
175
- <ProgressBar value={todoPct} width={12} fillColor="#9d4edd" emptyColor="#3a3a3a" theme={t} />
176
- </box>
177
- )}
178
-
179
- {/* ── MCP ── */}
180
- {mcpTotal > 0 && (
181
- <box flexDirection="column" marginTop={1}>
182
- <box flexDirection="row" gap={1}>
183
- <text fg={t.textMuted}>mcp</text>
184
- <text fg={t.text}>{mcpConnected}/{mcpTotal}</text>
185
- </box>
186
- <ProgressBar value={mcpPct} width={12} fillColor="#00c8ff" emptyColor="#3a3a3a" theme={t} />
187
- </box>
188
- )}
189
-
190
- {/* ── LSP ── */}
191
- {lspTotal > 0 && (
192
- <box flexDirection="row" gap={1} marginTop={1}>
193
- <text fg={t.textMuted}>lsp</text>
194
- <text fg={lspActive > 0 ? "#00e5a0" : "#555555"}>{lspActive}/{lspTotal}</text>
195
- </box>
196
- )}
197
-
198
- <text> </text>
199
- </box>
200
- )
201
- }
202
-
203
- // ─── Environment detection line ──────────────────────────────────────────────
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
- theme: TuiThemeCurrent
206
- providers: ReadonlyArray<{ id: string; name: string }> | undefined
207
- config: Cfg
265
+ theme: TuiThemeCurrent;
266
+ providers: ReadonlyArray<{ id: string; name: string }> | undefined;
267
+ config: Cfg;
208
268
  }) => {
209
- if (!props.config.show_detected) return null
210
-
211
- const os = props.config.show_os ? getOSName() : null
212
- const providers = props.config.show_providers ? getProviders(props.providers) : null
213
-
214
- if (!os && !providers) return null
215
-
216
- return (
217
- <box flexDirection="row" gap={1}>
218
- <text fg={props.theme.textMuted}>detected:</text>
219
- {os && <text fg={props.theme.text}>{os}</text>}
220
- {os && providers && <text fg={props.theme.textMuted}>·</text>}
221
- {providers && <text fg={props.theme.text}>{providers}</text>}
222
- </box>
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
- // ─── Configuration types and parsing helpers ──────────────────────────────────
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
- set_theme: boolean
7
- show_detected: boolean
8
- show_os: boolean
9
- show_providers: boolean
10
- show_sidebar: boolean
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": "2.0.22",
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 { createSignal } from "solid-js"
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
- if (!value || typeof value !== "object" || Array.isArray(value)) return
12
- return Object.fromEntries(Object.entries(value))
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
- const boot = cfg(rec(options))
17
- if (!boot.enabled) return
18
-
19
- // Theme setup
20
- try {
21
- await api.theme.install("./themes/j0k3r-dev-rgl.json")
22
- if (boot.set_theme) api.theme.set(boot.theme)
23
- } catch (error) {
24
- console.error("[j0k3r-dev-rgl] Theme setup failed:", error)
25
- }
26
-
27
- // ── Reactive context tracker via events ──────────────────────────────────
28
- // sessionID → { tokens, cost }
29
- type CtxStats = { tokens: number; cost: number }
30
- const ctxStore = new Map<string, CtxStats>()
31
- const msgCostTracker = new Map<string, number>() // `sid:msgId` → cost
32
-
33
- const [ctxTick, setCtxTick] = createSignal(0)
34
-
35
- let debugLogged = false
36
- api.event.on("message.updated", (event) => {
37
- // Debug: loguear estructura completa la primera vez
38
- if (!debugLogged) {
39
- debugLogged = true
40
- const propsKeys = Object.keys((event as any).properties ?? {}).join(",")
41
- const infoKeys = Object.keys(((event as any).properties?.info ?? (event as any).properties) ?? {}).join(",")
42
- console.error("[j0k3r-mask] event.properties keys:", propsKeys)
43
- console.error("[j0k3r-mask] info/msg keys:", infoKeys)
44
- console.error("[j0k3r-mask] full event:", JSON.stringify(event).slice(0, 800))
45
- api.ui.toast({ variant: "info", message: `props: ${propsKeys} | info: ${infoKeys}`, duration: 8000 })
46
- }
47
-
48
- // Intentar ambas estructuras: { properties: { info: Message } } o { properties: Message }
49
- const props = (event as any).properties
50
- const msg = props?.info ?? props
51
- if (!msg || msg.role !== "assistant") return
52
-
53
- const sid: string | undefined = msg.sessionID
54
- if (!sid) return
55
-
56
- const t = msg.tokens ?? {}
57
- const tokens = t.input ?? 0
58
- const msgId: string = msg.id ?? `${sid}-unknown`
59
- msgCostTracker.set(`${sid}:${msgId}`, msg.cost ?? 0)
60
-
61
- const cost = Array.from(msgCostTracker.entries())
62
- .filter(([k]) => k.startsWith(`${sid}:`))
63
- .reduce((s, [, v]) => s + v, 0)
64
-
65
- const prev = ctxStore.get(sid) ?? { tokens: 0, cost: 0 }
66
- ctxStore.set(sid, { tokens: Math.max(prev.tokens, tokens), cost })
67
-
68
- console.error(`[j0k3r-mask] sid=${sid} tokens=${tokens} cost=${cost}`)
69
- setCtxTick(v => v + 1)
70
- })
71
-
72
- // Resolver context window real del modelo activo
73
- const getContextLimit = (): number => {
74
- try {
75
- const modelStr = api.state.config?.model ?? ""
76
- const [providerID, modelID] = modelStr.split("/")
77
- const provider = api.state.provider.find(p => p.id === providerID)
78
- const model = provider?.models?.[modelID]
79
- if (model?.limit?.context) return model.limit.context
80
- } catch (_) {}
81
- return 1_000_000
82
- }
83
-
84
- // Slot registration
85
- api.slots.register({
86
- slots: {
87
- home_logo(ctx) {
88
- return <HomeLogo theme={ctx.theme.current} />
89
- },
90
-
91
- home_bottom(ctx) {
92
- return <DetectedEnv theme={ctx.theme.current} providers={api.state.provider} config={boot} />
93
- },
94
-
95
- sidebar_content(ctx, value) {
96
- const sessionID = value?.session_id
97
-
98
- // Registrar sesión en el tracker si es nueva
99
- if (sessionID && !ctxStore.has(sessionID)) {
100
- ctxStore.set(sessionID, { tokens: 0, cost: 0 })
101
- }
102
-
103
- // Suscribirse a la señal reactiva
104
- const _ = ctxTick()
105
- const stats = sessionID
106
- ? (ctxStore.get(sessionID) ?? { tokens: 0, cost: 0 })
107
- : { tokens: 0, cost: 0 }
108
-
109
- const branch = api.state.vcs?.branch
110
- const todos = sessionID ? api.state.session.todo(sessionID) : []
111
- const mcpItems = api.state.mcp()
112
- const lspItems = api.state.lsp()
113
-
114
- return (
115
- <SidebarArch
116
- theme={ctx.theme.current}
117
- config={boot}
118
- branch={branch}
119
- todos={todos}
120
- mcpItems={mcpItems}
121
- lspItems={lspItems}
122
- contextTokens={stats.tokens}
123
- contextCost={stats.cost}
124
- contextLimit={getContextLimit()}
125
- />
126
- )
127
- },
128
-
129
- session_prompt_right(ctx, value) {
130
- const t = ctx.theme.current
131
- return (
132
- <text fg={t.textMuted}>
133
- <span style={{ fg: "#ff2d78" }}>j0k3r</span>
134
- <span style={{ fg: "#9d4edd" }}>@</span>
135
- {(value.session_id ?? "").slice(0, 6)}
136
- </text>
137
- )
138
- },
139
- },
140
- })
141
- }
142
-
143
- const plugin: TuiPluginModule & { id: string } = {
144
- id,
145
- tui,
146
- }
147
-
148
- export default plugin
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;