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.
Files changed (3) hide show
  1. package/README.md +70 -41
  2. package/package.json +1 -1
  3. 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 **Mustachi** (ASCII mascot) in your home logo
9
- - ๐Ÿ‘€ Subtle **eye animations** (optional, low-frequency)
10
- - ๐Ÿ’ฌ **Motivational phrases** during busy/loading states (Rioplatense Spanish style)
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
- ### Method 1: Local Testing with Tarball (Recommended First)
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
- npm pack
21
- npm install -g ./plugin-gentleman-1.0.0.tgz
25
+ opencode plugin plugin-gentleman --global
22
26
  ```
23
27
 
24
- Add to `~/.config/opencode/opencode.json`:
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
- Restart OpenCode:
53
+ OpenCode will install npm plugins listed there automatically on startup.
34
54
 
35
- ```bash
36
- opencode
37
- ```
55
+ ### Development: Local Testing with npm link
38
56
 
39
- ### Method 2: Development with npm link
57
+ For plugin development:
40
58
 
41
59
  ```bash
42
60
  npm link
43
61
  ```
44
62
 
45
- Add to `~/.config/opencode/opencode.json`:
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
- ### Method 3: npm Registry (After Publishing)
74
+ ### Development: Local Testing with Tarball
75
+
76
+ For testing the packed artifact locally:
57
77
 
58
78
  ```bash
59
- npm install -g plugin-gentleman
60
- opencode plugin install plugin-gentleman
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 Mustachi ASCII art
208
- 4. **Environment Detection Slot**: Registers `home_prompt_after` slot with OS/provider info
209
- 5. **Animation Loop**: If `animations: true`, starts interval timers for eye variations and busy-state detection
210
- 6. **Busy State Detection**: Attempts to read `api.state.session.running` (best-effort; may not be exposed by all OpenCode versions)
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 Before Publishing
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. **Sidebar Support**: OpenCode TUI may or may not support sidebar slots. Currently, Mustachi only appears in the `home_logo` slot. If sidebar slots become available, this can be added in a future version.
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 `Home` component
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.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 - inspired by mustachi examples (eyes, mustache, optional tongue)
10
- // Base state: neutral look
11
- const mustachiBase = [
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
- // Eye variations for subtle animation
29
- const eyeVariations = [
30
- { left: "โ— โ—", right: "โ—‹ โ—‹" }, // neutral
31
- { left: "โ— โ—", right: "โ—‹ โ—‹" }, // left eye looking left
32
- { left: "โ— โ—‘", right: "โ—‹ โ—‹" }, // left eye looking right
33
- { left: "โ— โ—", right: "โ— โ—‹" }, // right eye looking left
34
- { left: "โ— โ—", right: "โ—‹ โ—‘" }, // right eye looking right
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
- const Home = (props: { theme: TuiThemeCurrent; config: Cfg; isBusy?: boolean }) => {
148
- const [eyeIndex, setEyeIndex] = createSignal(0)
149
- const [showTongue, setShowTongue] = createSignal(false)
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: subtle eye variation every 3-5 seconds
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
- // Randomly change eyes occasionally (20% chance)
158
- if (Math.random() < 0.2) {
159
- setEyeIndex((Math.floor(Math.random() * eyeVariations.length)))
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) // check every 4 seconds
318
+ }, 4000)
164
319
 
165
320
  onCleanup(() => clearInterval(interval))
166
321
  })
167
322
 
168
- // Busy state animation: show tongue + rotate phrases
323
+ // Busy state animation: tongue grows progressively + rotate phrases
169
324
  createEffect(() => {
170
325
  if (!props.config.animations || !props.isBusy) {
171
- setShowTongue(false)
326
+ setTongueFrame(0)
172
327
  setBusyPhrase("")
173
328
  return
174
329
  }
175
330
 
176
- setShowTongue(true)
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) // rotate every 3 seconds
350
+ }, 3000)
185
351
 
186
- onCleanup(() => clearInterval(interval))
352
+ onCleanup(() => {
353
+ clearInterval(interval)
354
+ if (tongueTimeoutId !== undefined) {
355
+ clearTimeout(tongueTimeoutId)
356
+ }
357
+ })
187
358
  })
188
359
 
189
- // Build Mustachi with current eye state
190
- const currentEyes = eyeVariations[eyeIndex()]
191
- const mustachi = mustachiBase.map((line, idx) => {
192
- // Replace eyes in line 2
193
- if (idx === 2) {
194
- return ` โ”‚ ${currentEyes.left} โ”‚ โ”‚ ${currentEyes.right} โ”‚ `
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
- return line
197
- })
198
-
199
- // Add tongue if busy
200
- if (showTongue()) {
201
- mustachi.push(" โ•ฒ โ— โ•ฑ ")
202
- mustachi.push(" v ")
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
- {mustachi.map((line, idx) => {
213
- const totalLines = mustachi.length
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}>โ€ข</text>}
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 <Home theme={ctx.theme.current} config={value()} isBusy={isBusy()} />
479
+ return <HomeLogo theme={ctx.theme.current} />
292
480
  },
293
- home_prompt_after(ctx) {
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
  }