plugin-gentleman 1.0.0 โ 1.0.1
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 +70 -41
- package/package.json +1 -1
- package/tui.tsx +254 -63
package/README.md
CHANGED
|
@@ -5,23 +5,43 @@ OpenCode TUI plugin featuring **Mustachi** โ an animated ASCII mascot with eye
|
|
|
5
5
|
## What This Is
|
|
6
6
|
|
|
7
7
|
A TUI plugin for OpenCode that:
|
|
8
|
-
- ๐ญ Shows **
|
|
9
|
-
-
|
|
10
|
-
-
|
|
8
|
+
- ๐ญ Shows a prominent **ASCII mustache** on the home screen
|
|
9
|
+
- ๐ค Shows the full **Mustachi face** with eyes in the sidebar
|
|
10
|
+
- ๐ Subtle **eye animations** in sidebar (optional, low-frequency)
|
|
11
|
+
- ๐ฌ **Motivational phrases** during busy/loading states in sidebar (Rioplatense Spanish style)
|
|
11
12
|
- ๐จ Installs and applies the **Gentleman theme** automatically
|
|
12
|
-
- ๐ฅ๏ธ Detects and displays your **OS and LLM providers**
|
|
13
|
+
- ๐ฅ๏ธ Detects and displays your **OS and LLM providers** below the prompt
|
|
13
14
|
- โ๏ธ Fully **configurable** via `opencode.json`
|
|
14
15
|
|
|
15
16
|
## Installation
|
|
16
17
|
|
|
17
|
-
###
|
|
18
|
+
### Recommended: Global Plugin Installation
|
|
19
|
+
|
|
20
|
+
This is the real, verified flow.
|
|
21
|
+
|
|
22
|
+
1. Install the plugin with OpenCode:
|
|
18
23
|
|
|
19
24
|
```bash
|
|
20
|
-
|
|
21
|
-
npm install -g ./plugin-gentleman-1.0.0.tgz
|
|
25
|
+
opencode plugin plugin-gentleman --global
|
|
22
26
|
```
|
|
23
27
|
|
|
24
|
-
|
|
28
|
+
2. OpenCode will download the package and update your global TUI config automatically.
|
|
29
|
+
|
|
30
|
+
3. Restart OpenCode:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
opencode
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
4. You should see:
|
|
37
|
+
- a prominent ASCII mustache on the home screen
|
|
38
|
+
- `OpenCode` below it
|
|
39
|
+
- `Detected: <OS> | <providers>` below the prompt area
|
|
40
|
+
- full Mustachi in the sidebar
|
|
41
|
+
|
|
42
|
+
### Manual Config (only if you prefer editing config yourself)
|
|
43
|
+
|
|
44
|
+
If you want to manage the config manually instead of using `opencode plugin`, add this to `~/.config/opencode/opencode.json`:
|
|
25
45
|
|
|
26
46
|
```json
|
|
27
47
|
{
|
|
@@ -30,19 +50,17 @@ Add to `~/.config/opencode/opencode.json`:
|
|
|
30
50
|
}
|
|
31
51
|
```
|
|
32
52
|
|
|
33
|
-
|
|
53
|
+
OpenCode will install npm plugins listed there automatically on startup.
|
|
34
54
|
|
|
35
|
-
|
|
36
|
-
opencode
|
|
37
|
-
```
|
|
55
|
+
### Development: Local Testing with npm link
|
|
38
56
|
|
|
39
|
-
|
|
57
|
+
For plugin development:
|
|
40
58
|
|
|
41
59
|
```bash
|
|
42
60
|
npm link
|
|
43
61
|
```
|
|
44
62
|
|
|
45
|
-
|
|
63
|
+
Then add to `~/.config/opencode/opencode.json`:
|
|
46
64
|
|
|
47
65
|
```json
|
|
48
66
|
{
|
|
@@ -51,24 +69,40 @@ Add to `~/.config/opencode/opencode.json`:
|
|
|
51
69
|
}
|
|
52
70
|
```
|
|
53
71
|
|
|
54
|
-
Restart OpenCode.
|
|
72
|
+
Restart OpenCode to see changes.
|
|
55
73
|
|
|
56
|
-
###
|
|
74
|
+
### Development: Local Testing with Tarball
|
|
75
|
+
|
|
76
|
+
For testing the packed artifact locally:
|
|
57
77
|
|
|
58
78
|
```bash
|
|
59
|
-
npm
|
|
60
|
-
opencode plugin
|
|
79
|
+
npm pack
|
|
80
|
+
opencode plugin ./plugin-gentleman-<version>.tgz --global
|
|
61
81
|
```
|
|
62
82
|
|
|
83
|
+
If your OpenCode version does not accept a tarball path there, use the recommended published-package flow instead.
|
|
84
|
+
|
|
63
85
|
## Features
|
|
64
86
|
|
|
87
|
+
### Visual Layout
|
|
88
|
+
|
|
89
|
+
**Home Screen:**
|
|
90
|
+
- Prominent ASCII mustache (no face, just the mustache)
|
|
91
|
+
- "OpenCode" branding text
|
|
92
|
+
- Environment detection line below the prompt area showing OS and providers
|
|
93
|
+
|
|
94
|
+
**Sidebar:**
|
|
95
|
+
- Full Mustachi face with eyes and mustache
|
|
96
|
+
- Animated eyes that occasionally look around (when animations enabled)
|
|
97
|
+
- Tongue and motivational phrases during busy states (when animations enabled)
|
|
98
|
+
|
|
65
99
|
### Mustachi Mascot
|
|
66
100
|
|
|
67
101
|
Mustachi is an ASCII character with:
|
|
68
|
-
- Two large eyes that occasionally look in different directions
|
|
69
|
-
- A prominent mustache rendered in theme colors
|
|
70
|
-
- A tongue that appears during busy/loading states
|
|
71
|
-
- Motivational phrases in Rioplatense Spanish style
|
|
102
|
+
- Two large eyes that occasionally look in different directions (sidebar only)
|
|
103
|
+
- A prominent mustache rendered in theme colors (both home and sidebar)
|
|
104
|
+
- A tongue that appears during busy/loading states (sidebar only)
|
|
105
|
+
- Motivational phrases in Rioplatense Spanish style (sidebar only)
|
|
72
106
|
|
|
73
107
|
Example phrases shown during busy states:
|
|
74
108
|
- "Ponete las pilas, hermano..."
|
|
@@ -204,10 +238,11 @@ Shows only Mustachi and the OpenCode branding, no OS/provider info.
|
|
|
204
238
|
|
|
205
239
|
1. **Theme Installation**: On load, installs `gentleman.json` into OpenCode themes
|
|
206
240
|
2. **Theme Activation**: If `set_theme: true`, switches to the gentleman theme
|
|
207
|
-
3. **Logo Slot**: Registers `home_logo` slot with
|
|
208
|
-
4. **Environment Detection Slot**: Registers `
|
|
209
|
-
5. **
|
|
210
|
-
6. **
|
|
241
|
+
3. **Home Logo Slot**: Registers `home_logo` slot with mustache-only ASCII art
|
|
242
|
+
4. **Environment Detection Slot**: Registers `home_bottom` slot with OS/provider info below the prompt
|
|
243
|
+
5. **Sidebar Slot**: Registers `sidebar_content` slot with full Mustachi face and animations
|
|
244
|
+
6. **Animation Loop**: If `animations: true`, starts interval timers for eye variations and busy-state detection (sidebar only)
|
|
245
|
+
7. **Busy State Detection**: Attempts to read `api.state.session.running` (best-effort; may not be exposed by all OpenCode versions)
|
|
211
246
|
|
|
212
247
|
### Technical Details
|
|
213
248
|
|
|
@@ -272,23 +307,17 @@ oc-plugin-gentleman/
|
|
|
272
307
|
- `gentleman-local.ts` โ legacy local system plugin with limited features
|
|
273
308
|
- `install-local-real.sh`, `install-local.sh` โ local installation scripts
|
|
274
309
|
- `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
310
|
|
|
285
|
-
## Caveats
|
|
311
|
+
## Caveats
|
|
286
312
|
|
|
287
|
-
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).
|
|
313
|
+
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).
|
|
288
314
|
|
|
289
315
|
2. **Animation Frequency**: Currently set to low frequency (4s for eyes, 3s for phrase rotation). If this feels too fast or too slow in real usage, adjust the intervals in `tui.tsx`.
|
|
290
316
|
|
|
291
|
-
3. **
|
|
317
|
+
3. **Slot Usage**: The plugin uses these OpenCode TUI slots:
|
|
318
|
+
- `home_logo` โ mustache-only ASCII art
|
|
319
|
+
- `home_bottom` โ environment detection (OS + providers)
|
|
320
|
+
- `sidebar_content` โ full Mustachi face with animations
|
|
292
321
|
|
|
293
322
|
4. **Theme Compatibility**: The plugin installs and optionally activates the Gentleman theme. If the user has a custom theme they prefer, they should set `set_theme: false`.
|
|
294
323
|
|
|
@@ -302,13 +331,13 @@ To modify the plugin:
|
|
|
302
331
|
4. Restart OpenCode to see changes
|
|
303
332
|
|
|
304
333
|
To add new eye variations:
|
|
305
|
-
- Edit the `eyeVariations` array in `tui.tsx`
|
|
334
|
+
- Edit the `eyeVariations` array in `tui.tsx` (affects sidebar only)
|
|
306
335
|
|
|
307
336
|
To add new busy phrases:
|
|
308
|
-
- Edit the `busyPhrases` array in `tui.tsx`
|
|
337
|
+
- Edit the `busyPhrases` array in `tui.tsx` (affects sidebar only)
|
|
309
338
|
|
|
310
339
|
To change animation timings:
|
|
311
|
-
- Adjust `setInterval` durations in the `
|
|
340
|
+
- Adjust `setInterval` durations in the `SidebarMustachi` component
|
|
312
341
|
|
|
313
342
|
## License
|
|
314
343
|
|
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.1",
|
|
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
|
}
|