plugin-gentleman 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +158 -126
- package/package.json +1 -1
- package/tui.tsx +254 -63
package/README.md
CHANGED
|
@@ -1,27 +1,38 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Plugin Gentleman
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **Mustachi** — An animated ASCII mascot bringing personality to your OpenCode terminal.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
An OpenCode TUI plugin that adds visual flair and environment awareness to your coding sessions:
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
-
|
|
9
|
-
- 👀 Subtle
|
|
10
|
-
- 💬 **Motivational phrases** during busy
|
|
11
|
-
- 🎨
|
|
12
|
-
- 🖥️
|
|
13
|
-
- ⚙️ Fully
|
|
7
|
+
- 🎭 **Prominent ASCII mustache** on the home screen
|
|
8
|
+
- 👤 **Full Mustachi face** with eyes in the sidebar
|
|
9
|
+
- 👀 **Subtle eye animations** (optional, low-frequency)
|
|
10
|
+
- 💬 **Motivational phrases** during busy states (Rioplatense Spanish style)
|
|
11
|
+
- 🎨 **Gentleman theme** installed and applied automatically
|
|
12
|
+
- 🖥️ **Environment detection** — displays your OS and LLM providers
|
|
13
|
+
- ⚙️ **Fully configurable** via `opencode.json`
|
|
14
14
|
|
|
15
15
|
## Installation
|
|
16
16
|
|
|
17
|
-
###
|
|
17
|
+
### Quick Start
|
|
18
|
+
|
|
19
|
+
Install the plugin globally with OpenCode:
|
|
18
20
|
|
|
19
21
|
```bash
|
|
20
|
-
|
|
21
|
-
npm install -g ./plugin-gentleman-1.0.0.tgz
|
|
22
|
+
opencode plugin plugin-gentleman --global
|
|
22
23
|
```
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
OpenCode will download the package and update your TUI config automatically.
|
|
26
|
+
|
|
27
|
+
**Restart OpenCode** to see:
|
|
28
|
+
- Prominent ASCII mustache on the home screen
|
|
29
|
+
- `OpenCode` branding text
|
|
30
|
+
- `Detected: <OS> · <providers>` below the prompt area
|
|
31
|
+
- Full Mustachi face in the sidebar
|
|
32
|
+
|
|
33
|
+
### Alternative: Manual Configuration
|
|
34
|
+
|
|
35
|
+
If you prefer managing config yourself, add this to `~/.config/opencode/opencode.json`:
|
|
25
36
|
|
|
26
37
|
```json
|
|
27
38
|
{
|
|
@@ -30,19 +41,19 @@ Add to `~/.config/opencode/opencode.json`:
|
|
|
30
41
|
}
|
|
31
42
|
```
|
|
32
43
|
|
|
33
|
-
|
|
44
|
+
OpenCode will install npm plugins automatically on startup.
|
|
34
45
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
### For Developers
|
|
38
49
|
|
|
39
|
-
|
|
50
|
+
**Local Testing with npm link:**
|
|
40
51
|
|
|
41
52
|
```bash
|
|
42
53
|
npm link
|
|
43
54
|
```
|
|
44
55
|
|
|
45
|
-
|
|
56
|
+
Then add to `~/.config/opencode/opencode.json`:
|
|
46
57
|
|
|
47
58
|
```json
|
|
48
59
|
{
|
|
@@ -51,78 +62,95 @@ Add to `~/.config/opencode/opencode.json`:
|
|
|
51
62
|
}
|
|
52
63
|
```
|
|
53
64
|
|
|
54
|
-
Restart OpenCode.
|
|
65
|
+
Restart OpenCode to see changes.
|
|
55
66
|
|
|
56
|
-
|
|
67
|
+
**Local Testing with Tarball:**
|
|
57
68
|
|
|
58
69
|
```bash
|
|
59
|
-
npm
|
|
60
|
-
opencode plugin
|
|
70
|
+
npm pack
|
|
71
|
+
opencode plugin ./plugin-gentleman-<version>.tgz --global
|
|
61
72
|
```
|
|
62
73
|
|
|
74
|
+
*If your OpenCode version doesn't accept a tarball path, use the published package flow instead.*
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
63
78
|
## Features
|
|
64
79
|
|
|
65
|
-
###
|
|
80
|
+
### Visual Components
|
|
81
|
+
|
|
82
|
+
**Home Screen:**
|
|
83
|
+
- Prominent ASCII mustache (no face, just the mustache)
|
|
84
|
+
- "OpenCode" branding text
|
|
85
|
+
- Environment detection showing `OS · Providers` below the prompt area
|
|
66
86
|
|
|
67
|
-
|
|
68
|
-
-
|
|
69
|
-
-
|
|
70
|
-
-
|
|
71
|
-
- Motivational phrases in Rioplatense Spanish style
|
|
87
|
+
**Sidebar:**
|
|
88
|
+
- Full Mustachi face with eyes and mustache
|
|
89
|
+
- Animated eyes that occasionally look around *(when animations enabled)*
|
|
90
|
+
- Tongue and motivational phrases during busy states *(when animations enabled)*
|
|
72
91
|
|
|
73
|
-
|
|
74
|
-
- "Ponete las pilas, hermano..."
|
|
75
|
-
- "Dale que va, dale que va..."
|
|
76
|
-
- "Ya casi, ya casi..."
|
|
77
|
-
- "Ahí vamos, loco..."
|
|
78
|
-
- "Un toque más y listo..."
|
|
79
|
-
- "Aguantá que estoy pensando..."
|
|
92
|
+
### The Mustachi Mascot
|
|
80
93
|
|
|
81
|
-
|
|
94
|
+
An ASCII character with personality:
|
|
95
|
+
- **Eyes** that occasionally look in different directions *(sidebar only)*
|
|
96
|
+
- **Mustache** rendered in theme colors *(both home and sidebar)*
|
|
97
|
+
- **Tongue** that appears during busy/loading states *(sidebar only)*
|
|
98
|
+
- **Motivational phrases** in Rioplatense Spanish style *(sidebar only)*
|
|
82
99
|
|
|
83
|
-
**
|
|
100
|
+
**Example phrases during busy states:**
|
|
101
|
+
- *"Ponete las pilas, hermano..."*
|
|
102
|
+
- *"Dale que va, dale que va..."*
|
|
103
|
+
- *"Ya casi, ya casi..."*
|
|
104
|
+
- *"Ahí vamos, loco..."*
|
|
105
|
+
- *"Un toque más y listo..."*
|
|
106
|
+
- *"Aguantá que estoy pensando..."*
|
|
84
107
|
|
|
85
|
-
|
|
86
|
-
- Every ~4 seconds, Mustachi's eyes may look in a random direction
|
|
87
|
-
- 20% chance of eye movement per interval
|
|
88
|
-
- Returns to neutral position most of the time
|
|
108
|
+
### Animations
|
|
89
109
|
|
|
90
|
-
|
|
91
|
-
- Tongue appears when OpenCode is processing
|
|
92
|
-
- Motivational phrase rotates every 3 seconds
|
|
93
|
-
- Only active during detected busy states
|
|
110
|
+
**Low complexity, low frequency** — subtle and non-intrusive:
|
|
94
111
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
112
|
+
**Eye Direction** *(when `animations: true`)*
|
|
113
|
+
- Every ~4 seconds, eyes may look in a random direction
|
|
114
|
+
- 20% chance of movement per interval
|
|
115
|
+
- Returns to neutral position most of the time
|
|
99
116
|
|
|
100
|
-
|
|
117
|
+
**Busy State** *(when `animations: true`)*
|
|
118
|
+
- Tongue appears when OpenCode is processing
|
|
119
|
+
- Motivational phrase rotates every 3 seconds
|
|
120
|
+
- Only active during detected busy states
|
|
101
121
|
|
|
102
|
-
|
|
103
|
-
-
|
|
104
|
-
-
|
|
105
|
-
- Accent: Warm gold (`#E0C15A`)
|
|
106
|
-
- Text: Clean white (`#F3F6F9`)
|
|
122
|
+
**Disabled** *(when `animations: false`)*
|
|
123
|
+
- Mustachi stays in neutral position
|
|
124
|
+
- No eye movement, tongue, or phrases
|
|
107
125
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
- **
|
|
126
|
+
### Gentleman Theme
|
|
127
|
+
|
|
128
|
+
A refined dark color palette:
|
|
129
|
+
- **Background:** Deep navy (`#06080f`)
|
|
130
|
+
- **Primary:** Soft blue (`#7FB4CA`)
|
|
131
|
+
- **Accent:** Warm gold (`#E0C15A`)
|
|
132
|
+
- **Text:** Clean white (`#F3F6F9`)
|
|
133
|
+
|
|
134
|
+
**Mustachi gradient** (3-tone):
|
|
135
|
+
- **Top:** Accent gold
|
|
136
|
+
- **Middle:** Primary blue
|
|
137
|
+
- **Bottom:** Error pink (`#CB7C94`)
|
|
112
138
|
|
|
113
139
|
### Environment Detection
|
|
114
140
|
|
|
115
|
-
|
|
116
|
-
- **OS Detection
|
|
117
|
-
- **Provider Detection
|
|
141
|
+
Displays a "Detected" line with:
|
|
142
|
+
- **OS Detection:** Reads distro name on Linux, shows "macOS" or "Windows" on other platforms
|
|
143
|
+
- **Provider Detection:** Lists active LLM providers (OpenAI, Copilot, Google, etc.)
|
|
118
144
|
|
|
119
145
|
Both are fully configurable and can be hidden.
|
|
120
146
|
|
|
147
|
+
---
|
|
148
|
+
|
|
121
149
|
## Configuration
|
|
122
150
|
|
|
123
|
-
All options are
|
|
151
|
+
All options are configured via plugin tuple syntax in `opencode.json`.
|
|
124
152
|
|
|
125
|
-
### Default
|
|
153
|
+
### Default Settings
|
|
126
154
|
|
|
127
155
|
```json
|
|
128
156
|
{
|
|
@@ -144,7 +172,7 @@ All options are set via plugin tuple syntax in `opencode.json`:
|
|
|
144
172
|
}
|
|
145
173
|
```
|
|
146
174
|
|
|
147
|
-
###
|
|
175
|
+
### Available Options
|
|
148
176
|
|
|
149
177
|
| Option | Type | Default | Description |
|
|
150
178
|
|--------|------|---------|-------------|
|
|
@@ -156,7 +184,9 @@ All options are set via plugin tuple syntax in `opencode.json`:
|
|
|
156
184
|
| `show_providers` | boolean | `true` | Show detected LLM providers |
|
|
157
185
|
| `animations` | boolean | `true` | Enable Mustachi animations (eyes, busy state) |
|
|
158
186
|
|
|
159
|
-
###
|
|
187
|
+
### Examples
|
|
188
|
+
|
|
189
|
+
**Disable Animations:**
|
|
160
190
|
|
|
161
191
|
```json
|
|
162
192
|
{
|
|
@@ -169,7 +199,7 @@ All options are set via plugin tuple syntax in `opencode.json`:
|
|
|
169
199
|
|
|
170
200
|
Mustachi will remain static with neutral eyes and no busy-state expressions.
|
|
171
201
|
|
|
172
|
-
|
|
202
|
+
**Logo Only (No Detection):**
|
|
173
203
|
|
|
174
204
|
```json
|
|
175
205
|
{
|
|
@@ -180,9 +210,9 @@ Mustachi will remain static with neutral eyes and no busy-state expressions.
|
|
|
180
210
|
}
|
|
181
211
|
```
|
|
182
212
|
|
|
183
|
-
Shows only Mustachi and
|
|
213
|
+
Shows only Mustachi and OpenCode branding, no OS/provider info.
|
|
184
214
|
|
|
185
|
-
|
|
215
|
+
**Show Only OS:**
|
|
186
216
|
|
|
187
217
|
```json
|
|
188
218
|
{
|
|
@@ -200,25 +230,32 @@ Shows only Mustachi and the OpenCode branding, no OS/provider info.
|
|
|
200
230
|
}
|
|
201
231
|
```
|
|
202
232
|
|
|
233
|
+
---
|
|
234
|
+
|
|
203
235
|
## How It Works
|
|
204
236
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
237
|
+
The plugin integrates with OpenCode's TUI system through slot registration:
|
|
238
|
+
|
|
239
|
+
1. **Theme Installation:** Installs `gentleman.json` into OpenCode themes on load
|
|
240
|
+
2. **Theme Activation:** Switches to the gentleman theme *(if `set_theme: true`)*
|
|
241
|
+
3. **Home Logo Slot:** Registers `home_logo` with mustache-only ASCII art
|
|
242
|
+
4. **Environment Detection Slot:** Registers `home_bottom` with OS/provider info below the prompt
|
|
243
|
+
5. **Sidebar Slot:** Registers `sidebar_content` with full Mustachi face and animations
|
|
244
|
+
6. **Animation Loop:** Starts interval timers for eye variations and busy-state detection *(if `animations: true`, sidebar only)*
|
|
245
|
+
7. **Busy State Detection:** Attempts to read `api.state.session.running` *(best-effort; may not be exposed by all OpenCode versions)*
|
|
246
|
+
|
|
247
|
+
### Technical Stack
|
|
211
248
|
|
|
212
|
-
|
|
249
|
+
- **No Build Step:** Plain TSX transpiled at runtime by OpenCode
|
|
250
|
+
- **Solid.js Reactivity:** Uses `createSignal` and `createEffect` for animations
|
|
251
|
+
- **Safe Detection:** All OS/provider detection wrapped in try-catch blocks
|
|
252
|
+
- **Cleanup:** Uses `onCleanup` to clear intervals when component unmounts
|
|
213
253
|
|
|
214
|
-
|
|
215
|
-
- **Solid.js Reactivity**: Uses `createSignal` and `createEffect` for animations
|
|
216
|
-
- **Safe Detection**: All OS/provider detection wrapped in try-catch blocks
|
|
217
|
-
- **Cleanup**: Uses `onCleanup` to clear intervals when component unmounts
|
|
254
|
+
---
|
|
218
255
|
|
|
219
256
|
## Supported Providers
|
|
220
257
|
|
|
221
|
-
|
|
258
|
+
Friendly name mapping for LLM providers:
|
|
222
259
|
|
|
223
260
|
| Provider ID | Display Name |
|
|
224
261
|
|-------------|--------------|
|
|
@@ -235,80 +272,75 @@ The plugin maps these provider IDs to friendly names:
|
|
|
235
272
|
| `together` | Together |
|
|
236
273
|
| `perplexity` | Perplexity |
|
|
237
274
|
|
|
238
|
-
Unknown provider IDs display the configured name or raw ID
|
|
275
|
+
*Unknown provider IDs display the configured name or raw ID.*
|
|
239
276
|
|
|
240
|
-
|
|
277
|
+
---
|
|
241
278
|
|
|
242
|
-
|
|
279
|
+
## Important Notes
|
|
243
280
|
|
|
244
|
-
|
|
281
|
+
### TUI Plugin vs System Plugin
|
|
282
|
+
|
|
283
|
+
**This is a TUI plugin for npm installation.**
|
|
284
|
+
|
|
285
|
+
If you copy `.ts` files to `~/.config/opencode/plugins/` (system plugin):
|
|
245
286
|
- ❌ **NO visual changes** — system plugins cannot modify the TUI
|
|
246
|
-
- ❌ **NO Mustachi** — only TUI plugins can register slot components
|
|
287
|
+
- ❌ **NO Mustachi** — only TUI plugins can register slot components
|
|
247
288
|
- ❌ **NO animations** — JSX/Solid.js components only work in TUI plugins
|
|
248
289
|
|
|
249
|
-
**For full features, use npm installation** (
|
|
290
|
+
**For full features, use npm installation** (recommended global method above).
|
|
250
291
|
|
|
251
|
-
|
|
292
|
+
### Package Contents
|
|
252
293
|
|
|
253
|
-
|
|
254
|
-
oc-plugin-gentleman/
|
|
255
|
-
├── tui.tsx # TUI plugin entry point (main implementation) ✅ npm
|
|
256
|
-
├── gentleman.json # Gentleman theme definition ✅ npm
|
|
257
|
-
├── package.json # npm package manifest with exports ✅ npm
|
|
258
|
-
├── README.md # This file ✅ npm (auto-included)
|
|
259
|
-
├── gentleman-local.ts # Legacy local system plugin (repo-only, not in npm package)
|
|
260
|
-
├── install-local-real.sh # Install script for local plugin (repo-only)
|
|
261
|
-
├── install-local.sh # Install script for local plugin (repo-only)
|
|
262
|
-
└── mustachi examples/ # PNG reference images (repo-only)
|
|
263
|
-
```
|
|
264
|
-
|
|
265
|
-
**Files included in npm package** (via `files` field in package.json):
|
|
294
|
+
**Files included in npm package** *(via `files` field in `package.json`)*:
|
|
266
295
|
- `tui.tsx` — main plugin implementation
|
|
267
296
|
- `gentleman.json` — theme definition
|
|
268
297
|
- `package.json` — auto-included by npm
|
|
269
298
|
- `README.md` — auto-included by npm
|
|
270
299
|
|
|
271
|
-
**
|
|
300
|
+
**Repository-only files** *(excluded from npm package)*:
|
|
272
301
|
- `gentleman-local.ts` — legacy local system plugin with limited features
|
|
273
302
|
- `install-local-real.sh`, `install-local.sh` — local installation scripts
|
|
274
|
-
- `mustachi examples/` — reference images
|
|
275
|
-
oc-plugin-gentleman/
|
|
276
|
-
├── tui.tsx # TUI plugin entry point (main implementation)
|
|
277
|
-
├── gentleman.json # Gentleman theme definition
|
|
278
|
-
├── package.json # npm package manifest with exports
|
|
279
|
-
├── gentleman-local.ts # Legacy local system plugin (limited features)
|
|
280
|
-
├── install-local-real.sh # Install script for local system plugin
|
|
281
|
-
├── mustachi examples/ # PNG reference images (not used in final plugin)
|
|
282
|
-
└── README.md # This file
|
|
283
|
-
```
|
|
284
303
|
|
|
285
|
-
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## Known Limitations
|
|
307
|
+
|
|
308
|
+
1. **Busy State Detection:** The plugin attempts to detect busy states via `api.state.session.running`, but this may not be exposed in all OpenCode versions. If unavailable, busy-state animations won't trigger *(eye animations still work in sidebar)*.
|
|
286
309
|
|
|
287
|
-
|
|
310
|
+
2. **Animation Frequency:** Currently set to low frequency (4s for eyes, 3s for phrase rotation). Adjust the intervals in `tui.tsx` if needed.
|
|
288
311
|
|
|
289
|
-
|
|
312
|
+
3. **Slot Usage:** The plugin uses these OpenCode TUI slots:
|
|
313
|
+
- `home_logo` — mustache-only ASCII art
|
|
314
|
+
- `home_bottom` — environment detection (OS + providers)
|
|
315
|
+
- `sidebar_content` — full Mustachi face with animations
|
|
290
316
|
|
|
291
|
-
|
|
317
|
+
4. **Theme Compatibility:** The plugin installs and optionally activates the Gentleman theme. If you prefer a custom theme, set `set_theme: false`.
|
|
292
318
|
|
|
293
|
-
|
|
319
|
+
---
|
|
294
320
|
|
|
295
321
|
## Development
|
|
296
322
|
|
|
297
|
-
|
|
323
|
+
### Modifying the Plugin
|
|
324
|
+
|
|
325
|
+
To work on the plugin:
|
|
298
326
|
|
|
299
327
|
1. Edit `tui.tsx` (main implementation)
|
|
300
328
|
2. Test locally with `npm link` or `npm pack`
|
|
301
329
|
3. No build step needed — OpenCode transpiles TSX at runtime
|
|
302
330
|
4. Restart OpenCode to see changes
|
|
303
331
|
|
|
304
|
-
|
|
305
|
-
|
|
332
|
+
### Adding New Content
|
|
333
|
+
|
|
334
|
+
**Eye variations:**
|
|
335
|
+
- Edit the `eyeVariations` array in `tui.tsx` *(affects sidebar only)*
|
|
336
|
+
|
|
337
|
+
**Busy phrases:**
|
|
338
|
+
- Edit the `busyPhrases` array in `tui.tsx` *(affects sidebar only)*
|
|
306
339
|
|
|
307
|
-
|
|
308
|
-
-
|
|
340
|
+
**Animation timings:**
|
|
341
|
+
- Adjust `setInterval` durations in the `SidebarMustachi` component
|
|
309
342
|
|
|
310
|
-
|
|
311
|
-
- Adjust `setInterval` durations in the `Home` component
|
|
343
|
+
---
|
|
312
344
|
|
|
313
345
|
## License
|
|
314
346
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "plugin-gentleman",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.2",
|
|
5
5
|
"description": "OpenCode TUI plugin featuring Mustachi - an animated ASCII mascot with eyes, mustache, and optional motivational phrases during busy states",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"exports": {
|
package/tui.tsx
CHANGED
|
@@ -6,32 +6,123 @@ import { createSignal, onCleanup, createEffect } from "solid-js"
|
|
|
6
6
|
|
|
7
7
|
const id = "gentleman"
|
|
8
8
|
|
|
9
|
-
// Mustachi ASCII art -
|
|
10
|
-
// Base
|
|
11
|
-
const
|
|
9
|
+
// Premium Mustachi ASCII art - full version for sidebar
|
|
10
|
+
// Base structure with eyes that will be replaced dynamically
|
|
11
|
+
const mustachiNeutralBase = [
|
|
12
|
+
" ████████████ ████████████",
|
|
13
|
+
" ██░░░░░░░░░░░░██ ██░░░░░░░░░░░░██",
|
|
14
|
+
" ██░░░░░░░░░░░░░░██ ██░░░░░░░░░░░░░░██",
|
|
15
|
+
" ██░░░░████████░░░░██ ██░░░░░░░░░░░░░░░░██",
|
|
16
|
+
" ██░░░░████████░░░░██ ██░░░░░░░░░░░░░░░░██",
|
|
17
|
+
" ██░░░░████████░░░░██ ██░░░░░░░░░░░░░░░░██",
|
|
18
|
+
" ██░░░░░░░░░░░░░░██ ██░░░░░░░░░░░░░░██",
|
|
19
|
+
" ██░░░░░░░░░░░░██ ██░░░░░░░░░░░░██",
|
|
20
|
+
" ████████████ ████████████",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
// Squinted eyes version for busy state
|
|
24
|
+
const mustachiSquintedBase = [
|
|
25
|
+
" ████████████ ████████████",
|
|
26
|
+
" ██░░░░░░░░░░░░██ ██░░░░░░░░░░░░██",
|
|
27
|
+
" ██░░░████████░░░██ ██░░░░░░░░░░░░░░██",
|
|
28
|
+
" ██░░████░░░░████░░██ ██░░░░░░░░░░░░░░░░██",
|
|
29
|
+
" ██░░██░░░░░░░░██░░██ ██░░░░░░░░░░░░░░░░██",
|
|
30
|
+
" ██░░░░░░░░░░░░░░░░██ ██░░░░░░░░░░░░░░░░██",
|
|
31
|
+
" ██░░░░░░░░░░░░░░██ ██░░░░░░░░░░░░░░██",
|
|
32
|
+
" ██░░░░░░░░░░░░██ ██░░░░░░░░░░░░██",
|
|
33
|
+
" ████████████ ████████████",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
// Mustache section (shared by all states)
|
|
37
|
+
const mustachiMustacheSection = [
|
|
38
|
+
" ████████ ████████",
|
|
39
|
+
" ████████████ ████████████",
|
|
40
|
+
" ██ ████████████████ ████████████████ ██",
|
|
41
|
+
" ████ ████████████████████ ████████████████████ ████",
|
|
42
|
+
" ██████ ███████████████████████████████████████████ ██████",
|
|
43
|
+
" ███████████████████████████████████████████████████████████",
|
|
44
|
+
" ███████████████████████████████████████████████████████████",
|
|
45
|
+
" ███████████████████████████████████████████████████████████",
|
|
46
|
+
" █████████████████████████████████████████████████████████",
|
|
47
|
+
" ███████████████████████████████████████████████████████",
|
|
48
|
+
" ▓▓█████████████████████ █████████████████████▓▓",
|
|
49
|
+
" ▓▓▓███████████████ ███████████████▓▓▓",
|
|
50
|
+
" ▓▓▓█████████ █████████▓▓▓",
|
|
51
|
+
" ▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
// Tongue animation frames (progressive)
|
|
55
|
+
const tongueFrames = [
|
|
56
|
+
[], // no tongue
|
|
57
|
+
[" ███████"], // small tongue
|
|
58
|
+
[" ███████", " █████"], // medium tongue
|
|
59
|
+
[" ███████", " █████", " ███"], // full tongue
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
// Mustache-only ASCII art for home logo (prominent and simple)
|
|
63
|
+
const mustachiMustacheOnly = [
|
|
12
64
|
"",
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
65
|
+
" ████████ ████████",
|
|
66
|
+
" ████████████ ████████████",
|
|
67
|
+
" ██ ████████████████ ████████████████ ██",
|
|
68
|
+
" ████ ████████████████████ ████████████████████ ████",
|
|
69
|
+
" ██████ ███████████████████████████████████████████ ██████",
|
|
70
|
+
" ███████████████████████████████████████████████████████████",
|
|
71
|
+
" ███████████████████████████████████████████████████████████",
|
|
72
|
+
" ███████████████████████████████████████████████████████████",
|
|
73
|
+
" █████████████████████████████████████████████████████████",
|
|
74
|
+
" ███████████████████████████████████████████████████████",
|
|
75
|
+
" ▓▓█████████████████████ █████████████████████▓▓",
|
|
76
|
+
" ▓▓▓███████████████ ███████████████▓▓▓",
|
|
77
|
+
" ▓▓▓█████████ █████████▓▓▓",
|
|
78
|
+
" ▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓",
|
|
25
79
|
"",
|
|
26
80
|
]
|
|
27
81
|
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
82
|
+
// Left pupil positions for look-around animation (progressive)
|
|
83
|
+
// Modifies only the left eye (white sclera with dark pupil)
|
|
84
|
+
// Right eye is monocle/glass and remains static
|
|
85
|
+
const leftPupilPositions = [
|
|
86
|
+
"████████", // center (line 3 of eyes)
|
|
87
|
+
"██████ ", // looking left
|
|
88
|
+
" ██████", // looking right
|
|
89
|
+
"████████", // center again
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
// Blink animation frames (progressive) - affects both eyes
|
|
93
|
+
const blinkFrames = [
|
|
94
|
+
// Open eyes (default state embedded in base arrays)
|
|
95
|
+
{ left: mustachiNeutralBase, squinted: mustachiSquintedBase },
|
|
96
|
+
// Half closed
|
|
97
|
+
{
|
|
98
|
+
left: [
|
|
99
|
+
" ████████████ ████████████",
|
|
100
|
+
" ██░░░░░░░░░░░░██ ██░░░░░░░░░░░░██",
|
|
101
|
+
" ██░░░░████░░░░░░██ ██░░░░░░░░░░░░░░██",
|
|
102
|
+
" ██░░░░████████░░░░██ ██░░░░░░░░░░░░░░░░██",
|
|
103
|
+
" ██░░░░████████░░░░██ ██░░░░░░░░░░░░░░░░██",
|
|
104
|
+
" ██░░░░████████░░░░██ ██░░░░░░░░░░░░░░░░██",
|
|
105
|
+
" ██░░░░░░░░░░░░░░██ ██░░░░░░░░░░░░░░██",
|
|
106
|
+
" ██░░░░░░░░░░░░██ ██░░░░░░░░░░░░██",
|
|
107
|
+
" ████████████ ████████████",
|
|
108
|
+
],
|
|
109
|
+
squinted: mustachiSquintedBase // squinted stays squinted during blink
|
|
110
|
+
},
|
|
111
|
+
// Fully closed
|
|
112
|
+
{
|
|
113
|
+
left: [
|
|
114
|
+
" ████████████ ████████████",
|
|
115
|
+
" ██░░░░░░░░░░░░██ ██░░░░░░░░░░░░██",
|
|
116
|
+
" ██░░████████░░░░██ ██░░░░░░░░░░░░░░██",
|
|
117
|
+
" ██░░████████████░░██ ██░░░░░░░░░░░░░░░░██",
|
|
118
|
+
" ██░░████████████░░██ ██░░░░░░░░░░░░░░░░██",
|
|
119
|
+
" ██░░████████████░░██ ██░░░░░░░░░░░░░░░░██",
|
|
120
|
+
" ██░░░░░░░░░░░░░░██ ██░░░░░░░░░░░░░░██",
|
|
121
|
+
" ██░░░░░░░░░░░░██ ██░░░░░░░░░░░░██",
|
|
122
|
+
" ████████████ ████████████",
|
|
123
|
+
],
|
|
124
|
+
squinted: mustachiSquintedBase
|
|
125
|
+
},
|
|
35
126
|
]
|
|
36
127
|
|
|
37
128
|
// Busy/loading state with tongue and motivational phrases
|
|
@@ -144,73 +235,177 @@ const getProviders = (providers: ReadonlyArray<{ id: string; name: string }> | u
|
|
|
144
235
|
return Array.from(names).sort().join(", ")
|
|
145
236
|
}
|
|
146
237
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const
|
|
238
|
+
// Home logo: Mustache-only (simple and prominent)
|
|
239
|
+
const HomeLogo = (props: { theme: TuiThemeCurrent }) => {
|
|
240
|
+
const topColor = props.theme.accent || "#E0C15A"
|
|
241
|
+
const midColor = props.theme.primary || "#7FB4CA"
|
|
242
|
+
const bottomColor = props.theme.error || "#CB7C94"
|
|
243
|
+
|
|
244
|
+
return (
|
|
245
|
+
<box flexDirection="column" alignItems="center">
|
|
246
|
+
{/* Mustache-only with 3-tone gradient */}
|
|
247
|
+
{mustachiMustacheOnly.map((line, idx) => {
|
|
248
|
+
const totalLines = mustachiMustacheOnly.length
|
|
249
|
+
let color = midColor
|
|
250
|
+
if (idx < totalLines / 3) {
|
|
251
|
+
color = topColor
|
|
252
|
+
} else if (idx >= (2 * totalLines) / 3) {
|
|
253
|
+
color = bottomColor
|
|
254
|
+
}
|
|
255
|
+
return <text fg={color}>{line}</text>
|
|
256
|
+
})}
|
|
257
|
+
|
|
258
|
+
{/* OpenCode branding */}
|
|
259
|
+
<box flexDirection="row" gap={0}>
|
|
260
|
+
<text fg={props.theme.textMuted} dimColor={true}>╭</text>
|
|
261
|
+
<text fg={props.theme.primary} dimColor={false}> OpenCode </text>
|
|
262
|
+
<text fg={props.theme.textMuted} dimColor={true}>╮</text>
|
|
263
|
+
</box>
|
|
264
|
+
|
|
265
|
+
<text> </text>
|
|
266
|
+
</box>
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Sidebar: Full Mustachi face with progressive animations
|
|
271
|
+
const SidebarMustachi = (props: { theme: TuiThemeCurrent; config: Cfg; isBusy?: boolean }) => {
|
|
272
|
+
const [pupilIndex, setPupilIndex] = createSignal(0)
|
|
273
|
+
const [blinkFrame, setBlinkFrame] = createSignal(0)
|
|
274
|
+
const [tongueFrame, setTongueFrame] = createSignal(0)
|
|
150
275
|
const [busyPhrase, setBusyPhrase] = createSignal("")
|
|
151
276
|
|
|
152
|
-
// Animation:
|
|
277
|
+
// Animation: pupil movement (look around) - low frequency, progressive
|
|
278
|
+
createEffect(() => {
|
|
279
|
+
if (!props.config.animations || props.isBusy) {
|
|
280
|
+
setPupilIndex(0)
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const interval = setInterval(() => {
|
|
285
|
+
// Cycle through pupil positions progressively
|
|
286
|
+
setPupilIndex((prev) => {
|
|
287
|
+
// 80% chance to stay at center, 20% to move
|
|
288
|
+
if (Math.random() < 0.8) return 0
|
|
289
|
+
return (prev + 1) % leftPupilPositions.length
|
|
290
|
+
})
|
|
291
|
+
}, 3000)
|
|
292
|
+
|
|
293
|
+
onCleanup(() => clearInterval(interval))
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
// Animation: blink - occasional, progressive
|
|
153
297
|
createEffect(() => {
|
|
154
298
|
if (!props.config.animations) return
|
|
155
299
|
|
|
300
|
+
const blinkSequence = async () => {
|
|
301
|
+
// Open -> half -> closed -> half -> open
|
|
302
|
+
setBlinkFrame(0)
|
|
303
|
+
await new Promise(r => setTimeout(r, 150))
|
|
304
|
+
setBlinkFrame(1)
|
|
305
|
+
await new Promise(r => setTimeout(r, 100))
|
|
306
|
+
setBlinkFrame(2)
|
|
307
|
+
await new Promise(r => setTimeout(r, 100))
|
|
308
|
+
setBlinkFrame(1)
|
|
309
|
+
await new Promise(r => setTimeout(r, 100))
|
|
310
|
+
setBlinkFrame(0)
|
|
311
|
+
}
|
|
312
|
+
|
|
156
313
|
const interval = setInterval(() => {
|
|
157
|
-
//
|
|
158
|
-
if (Math.random() < 0.
|
|
159
|
-
|
|
160
|
-
} else {
|
|
161
|
-
setEyeIndex(0) // back to neutral
|
|
314
|
+
// Blink occasionally (15% chance every 4s)
|
|
315
|
+
if (Math.random() < 0.15) {
|
|
316
|
+
blinkSequence()
|
|
162
317
|
}
|
|
163
|
-
}, 4000)
|
|
318
|
+
}, 4000)
|
|
164
319
|
|
|
165
320
|
onCleanup(() => clearInterval(interval))
|
|
166
321
|
})
|
|
167
322
|
|
|
168
|
-
// Busy state animation:
|
|
323
|
+
// Busy state animation: tongue grows progressively + rotate phrases
|
|
169
324
|
createEffect(() => {
|
|
170
325
|
if (!props.config.animations || !props.isBusy) {
|
|
171
|
-
|
|
326
|
+
setTongueFrame(0)
|
|
172
327
|
setBusyPhrase("")
|
|
173
328
|
return
|
|
174
329
|
}
|
|
175
330
|
|
|
176
|
-
|
|
331
|
+
// Grow tongue progressively when entering busy state
|
|
332
|
+
let currentFrame = 0
|
|
333
|
+
let tongueTimeoutId: NodeJS.Timeout | undefined
|
|
334
|
+
const growTongue = () => {
|
|
335
|
+
if (currentFrame < tongueFrames.length - 1) {
|
|
336
|
+
currentFrame++
|
|
337
|
+
setTongueFrame(currentFrame)
|
|
338
|
+
tongueTimeoutId = setTimeout(growTongue, 200)
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
growTongue()
|
|
177
342
|
|
|
343
|
+
// Rotate busy phrases
|
|
178
344
|
let phraseIdx = 0
|
|
179
345
|
setBusyPhrase(busyPhrases[phraseIdx])
|
|
180
346
|
|
|
181
347
|
const interval = setInterval(() => {
|
|
182
348
|
phraseIdx = (phraseIdx + 1) % busyPhrases.length
|
|
183
349
|
setBusyPhrase(busyPhrases[phraseIdx])
|
|
184
|
-
}, 3000)
|
|
350
|
+
}, 3000)
|
|
185
351
|
|
|
186
|
-
onCleanup(() =>
|
|
352
|
+
onCleanup(() => {
|
|
353
|
+
clearInterval(interval)
|
|
354
|
+
if (tongueTimeoutId !== undefined) {
|
|
355
|
+
clearTimeout(tongueTimeoutId)
|
|
356
|
+
}
|
|
357
|
+
})
|
|
187
358
|
})
|
|
188
359
|
|
|
189
|
-
// Build
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
360
|
+
// Build the complete Mustachi face
|
|
361
|
+
const buildFace = () => {
|
|
362
|
+
const lines: string[] = []
|
|
363
|
+
|
|
364
|
+
// Select eye base based on busy state
|
|
365
|
+
let eyeBase = props.isBusy ? mustachiSquintedBase : mustachiNeutralBase
|
|
366
|
+
|
|
367
|
+
// Apply blink animation if active
|
|
368
|
+
if (blinkFrame() > 0 && blinkFrame() < blinkFrames.length) {
|
|
369
|
+
eyeBase = props.isBusy
|
|
370
|
+
? blinkFrames[blinkFrame()].squinted
|
|
371
|
+
: blinkFrames[blinkFrame()].left
|
|
195
372
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
373
|
+
|
|
374
|
+
// Add eyes with pupil position (modify line 3 for left eye pupil)
|
|
375
|
+
eyeBase.forEach((line, idx) => {
|
|
376
|
+
if (idx === 3 && !props.isBusy && pupilIndex() >= 0) {
|
|
377
|
+
// Replace pupil in left eye (center of line 3)
|
|
378
|
+
const pupil = leftPupilPositions[pupilIndex()]
|
|
379
|
+
const modifiedLine = line.substring(0, 14) + pupil + line.substring(22)
|
|
380
|
+
lines.push(modifiedLine)
|
|
381
|
+
} else {
|
|
382
|
+
lines.push(line)
|
|
383
|
+
}
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
// Add mustache section
|
|
387
|
+
mustachiMustacheSection.forEach(line => lines.push(line))
|
|
388
|
+
|
|
389
|
+
// Add tongue if busy (progressive frames)
|
|
390
|
+
if (props.isBusy && tongueFrame() > 0) {
|
|
391
|
+
const tongueLines = tongueFrames[tongueFrame()]
|
|
392
|
+
tongueLines.forEach(line => lines.push(line))
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return lines
|
|
203
396
|
}
|
|
204
397
|
|
|
398
|
+
const faceLines = buildFace()
|
|
399
|
+
|
|
205
400
|
const topColor = props.theme.accent || "#E0C15A"
|
|
206
401
|
const midColor = props.theme.primary || "#7FB4CA"
|
|
207
402
|
const bottomColor = props.theme.error || "#CB7C94"
|
|
208
403
|
|
|
209
404
|
return (
|
|
210
405
|
<box flexDirection="column" alignItems="center">
|
|
211
|
-
{/* Mustachi with 3-tone gradient */}
|
|
212
|
-
{
|
|
213
|
-
const totalLines =
|
|
406
|
+
{/* Full Mustachi face with 3-tone gradient */}
|
|
407
|
+
{faceLines.map((line, idx) => {
|
|
408
|
+
const totalLines = faceLines.length
|
|
214
409
|
let color = midColor
|
|
215
410
|
if (idx < totalLines / 3) {
|
|
216
411
|
color = topColor
|
|
@@ -220,13 +415,6 @@ const Home = (props: { theme: TuiThemeCurrent; config: Cfg; isBusy?: boolean })
|
|
|
220
415
|
return <text fg={color}>{line}</text>
|
|
221
416
|
})}
|
|
222
417
|
|
|
223
|
-
{/* OpenCode branding */}
|
|
224
|
-
<box flexDirection="row" gap={0}>
|
|
225
|
-
<text fg={props.theme.textMuted} dimColor={true}>╭</text>
|
|
226
|
-
<text fg={props.theme.primary} dimColor={false}> OpenCode </text>
|
|
227
|
-
<text fg={props.theme.textMuted} dimColor={true}>╮</text>
|
|
228
|
-
</box>
|
|
229
|
-
|
|
230
418
|
{/* Busy phrase if loading */}
|
|
231
419
|
{busyPhrase() && (
|
|
232
420
|
<text fg={props.theme.warning}>{busyPhrase()}</text>
|
|
@@ -254,7 +442,7 @@ const DetectedEnv = (props: {
|
|
|
254
442
|
<box flexDirection="row" gap={1}>
|
|
255
443
|
<text fg={props.theme.textMuted}>Detected:</text>
|
|
256
444
|
{os && <text fg={props.theme.text}>{os}</text>}
|
|
257
|
-
{os && providers && <text fg={props.theme.textMuted}
|
|
445
|
+
{os && providers && <text fg={props.theme.textMuted}>·</text>}
|
|
258
446
|
{providers && <text fg={props.theme.text}>{providers}</text>}
|
|
259
447
|
</box>
|
|
260
448
|
)
|
|
@@ -288,11 +476,14 @@ const tui: TuiPlugin = async (api, options) => {
|
|
|
288
476
|
api.slots.register({
|
|
289
477
|
slots: {
|
|
290
478
|
home_logo(ctx) {
|
|
291
|
-
return <
|
|
479
|
+
return <HomeLogo theme={ctx.theme.current} />
|
|
292
480
|
},
|
|
293
|
-
|
|
481
|
+
home_bottom(ctx) {
|
|
294
482
|
return <DetectedEnv theme={ctx.theme.current} providers={api.state.provider} config={value()} />
|
|
295
483
|
},
|
|
484
|
+
sidebar_content(ctx) {
|
|
485
|
+
return <SidebarMustachi theme={ctx.theme.current} config={value()} isBusy={isBusy()} />
|
|
486
|
+
},
|
|
296
487
|
},
|
|
297
488
|
})
|
|
298
489
|
}
|