plugin-gentleman 1.0.3 → 1.0.6
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 +5 -3
- package/package.json +1 -1
- package/tui.tsx +153 -127
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Plugin Gentleman
|
|
2
2
|
|
|
3
|
-
> **
|
|
3
|
+
> **For the Gentleman Programming community** — Bringing Mustachi, our beloved mascot, into your OpenCode terminal.
|
|
4
4
|
|
|
5
|
-
An OpenCode TUI plugin
|
|
5
|
+
An OpenCode TUI plugin crafted for the Gentleman Programming community. Mustachi, the official mascot of Gentleman Programming, now accompanies you through your coding sessions with visual flair and environment awareness:
|
|
6
6
|
|
|
7
7
|
- 🎭 **Prominent ASCII mustache** on the home screen
|
|
8
8
|
- 👤 **Full Mustachi face** with eyes in the sidebar
|
|
@@ -91,7 +91,9 @@ opencode plugin ./plugin-gentleman-<version>.tgz --global
|
|
|
91
91
|
|
|
92
92
|
### The Mustachi Mascot
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
**Mustachi** is the official mascot of the Gentleman Programming community — not something invented just for this plugin, but a character beloved by the community and now integrated into your coding environment.
|
|
95
|
+
|
|
96
|
+
The ASCII representation features:
|
|
95
97
|
- **Eyes** that occasionally look in different directions *(sidebar only)*
|
|
96
98
|
- **Mustache** rendered in theme colors *(both home and sidebar)*
|
|
97
99
|
- **Tongue** that appears during busy/loading states *(sidebar only)*
|
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.6",
|
|
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,69 @@ import { createSignal, onCleanup, createEffect } from "solid-js"
|
|
|
6
6
|
|
|
7
7
|
const id = "gentleman"
|
|
8
8
|
|
|
9
|
-
// Premium Mustachi ASCII art -
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"
|
|
16
|
-
"
|
|
9
|
+
// Premium Mustachi ASCII art - structured by semantic zones
|
|
10
|
+
// Each eye state is a complete frame to avoid partial replacements
|
|
11
|
+
|
|
12
|
+
// Eye frames - neutral state with different pupil positions
|
|
13
|
+
// All lines are padded to 27 chars for perfect alignment with mustache
|
|
14
|
+
const eyeNeutralCenter = [
|
|
15
|
+
" █████ █████ ", // 27 chars (was 22)
|
|
16
|
+
" ██░░░░░██ ██░░░░░██ ", // 27 chars (was 24)
|
|
17
|
+
" ██░░███░░██ ██░░░░░░░██ ", // 27 chars (was 25)
|
|
18
|
+
" ██░░███░░██ ██░░░░░░░██ ", // 27 chars (was 25)
|
|
19
|
+
"██ ██░░░░░██ ██░░░░░██ ██", // 27 chars (unchanged)
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
const eyeNeutralLeft = [
|
|
23
|
+
" █████ █████ ", // 27 chars (was 22)
|
|
24
|
+
" ██░░░░░██ ██░░░░░██ ", // 27 chars (was 24)
|
|
25
|
+
" ██████░░░██ ██░░░░░░░██ ", // 27 chars (was 25)
|
|
26
|
+
" ██████░░░██ ██░░░░░░░██ ", // 27 chars (was 25)
|
|
27
|
+
"██ ██░░░░░██ ██░░░░░██ ██", // 27 chars (unchanged)
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
const eyeNeutralRight = [
|
|
31
|
+
" █████ █████ ", // 27 chars (was 22)
|
|
32
|
+
" ██░░░░░██ ██░░░░░██ ", // 27 chars (was 24)
|
|
33
|
+
" ██░░░██████ ██░░░░░░░██ ", // 27 chars (was 25)
|
|
34
|
+
" ██░░░██████ ██░░░░░░░██ ", // 27 chars (was 25)
|
|
35
|
+
"██ ██░░░░░██ ██░░░░░██ ██", // 27 chars (unchanged)
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
// Squinted eyes version for busy/expressive state
|
|
39
|
+
const eyeSquinted = [
|
|
40
|
+
" █████ █████ ", // 27 chars (was 22)
|
|
41
|
+
" ██░░░░░██ ██░░░░░██ ", // 27 chars (was 24)
|
|
42
|
+
" ██░░███░░██ ██░░░░░░░██ ", // 27 chars (was 25)
|
|
43
|
+
" █████████ █████████ ", // 27 chars (was 24)
|
|
44
|
+
"██ █████ █████ ██", // 27 chars (unchanged)
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
// Blink frames - half closed
|
|
48
|
+
const eyeBlinkHalf = [
|
|
49
|
+
" █████ █████ ", // 27 chars (was 22)
|
|
50
|
+
" ██░░░░░██ ██░░░░░██ ", // 27 chars (was 24)
|
|
51
|
+
" ██░░███░░██ ██░░░░░░░██ ", // 27 chars (was 25)
|
|
52
|
+
" █████████ █████████ ", // 27 chars (was 24)
|
|
53
|
+
"██ █████ █████ ██", // 27 chars (unchanged)
|
|
17
54
|
]
|
|
18
55
|
|
|
19
|
-
//
|
|
20
|
-
const
|
|
21
|
-
" █████ █████",
|
|
22
|
-
" ██░░░░░██ ██░░░░░██",
|
|
23
|
-
"
|
|
24
|
-
" █████████ █████████",
|
|
25
|
-
"██ █████ █████ ██",
|
|
56
|
+
// Blink frames - fully closed
|
|
57
|
+
const eyeBlinkClosed = [
|
|
58
|
+
" █████ █████ ", // 27 chars (was 22)
|
|
59
|
+
" ██░░░░░██ ██░░░░░██ ", // 27 chars (was 24)
|
|
60
|
+
" █████████ █████████ ", // 27 chars (was 24)
|
|
61
|
+
" █████████ █████████ ", // 27 chars (was 24)
|
|
62
|
+
"██ █████ █████ ██", // 27 chars (unchanged)
|
|
26
63
|
]
|
|
27
64
|
|
|
28
|
-
// Mustache section (
|
|
65
|
+
// Mustache section (all lines padded to 27 chars for alignment)
|
|
29
66
|
const mustachiMustacheSection = [
|
|
30
|
-
"██████████ ████████",
|
|
31
|
-
"████████████ ██████████",
|
|
32
|
-
" █████████████████████████",
|
|
33
|
-
" ▓██████████ ██████████▓",
|
|
34
|
-
" ▓██████ ██████▓",
|
|
67
|
+
"██████████ ████████", // 27 chars (unchanged)
|
|
68
|
+
"████████████ ██████████", // 27 chars (unchanged)
|
|
69
|
+
" █████████████████████████ ", // 27 chars (was 26)
|
|
70
|
+
" ▓██████████ ██████████▓", // 27 chars (unchanged)
|
|
71
|
+
" ▓██████ ██████▓ ", // 27 chars (was 25)
|
|
35
72
|
]
|
|
36
73
|
|
|
37
74
|
// Tongue animation frames (progressive) - compact design
|
|
@@ -60,43 +97,12 @@ const mustachiMustacheOnly = [
|
|
|
60
97
|
"",
|
|
61
98
|
]
|
|
62
99
|
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
"██████░░░██", // looking left
|
|
70
|
-
"██░░░██████", // looking right
|
|
71
|
-
"██░░███░░██", // center again
|
|
72
|
-
]
|
|
73
|
-
|
|
74
|
-
// Blink animation frames (progressive) - affects both eyes
|
|
75
|
-
const blinkFrames = [
|
|
76
|
-
// Open eyes (default state embedded in base arrays)
|
|
77
|
-
{ left: mustachiNeutralBase, squinted: mustachiSquintedBase },
|
|
78
|
-
// Half closed
|
|
79
|
-
{
|
|
80
|
-
left: [
|
|
81
|
-
" █████ █████",
|
|
82
|
-
" ██░░░░░██ ██░░░░░██",
|
|
83
|
-
" ██░░███░░██ ██░░░░░░░██",
|
|
84
|
-
" █████████ █████████",
|
|
85
|
-
"██ █████ █████ ██",
|
|
86
|
-
],
|
|
87
|
-
squinted: mustachiSquintedBase // squinted stays squinted during blink
|
|
88
|
-
},
|
|
89
|
-
// Fully closed
|
|
90
|
-
{
|
|
91
|
-
left: [
|
|
92
|
-
" █████ █████",
|
|
93
|
-
" ██░░░░░██ ██░░░░░██",
|
|
94
|
-
" █████████ █████████",
|
|
95
|
-
" █████████ █████████",
|
|
96
|
-
"██ █████ █████ ██",
|
|
97
|
-
],
|
|
98
|
-
squinted: mustachiSquintedBase
|
|
99
|
-
},
|
|
100
|
+
// Pupil position mapping for look-around animation
|
|
101
|
+
const pupilPositionFrames = [
|
|
102
|
+
eyeNeutralCenter, // center
|
|
103
|
+
eyeNeutralLeft, // looking left
|
|
104
|
+
eyeNeutralRight, // looking right
|
|
105
|
+
eyeNeutralCenter, // back to center
|
|
100
106
|
]
|
|
101
107
|
|
|
102
108
|
// Busy/loading state with tongue and motivational phrases
|
|
@@ -227,22 +233,14 @@ const HomeLogo = (props: { theme: TuiThemeCurrent }) => {
|
|
|
227
233
|
} else if (idx >= (2 * totalLines) / 3) {
|
|
228
234
|
color = darkGray // Bottom shadow
|
|
229
235
|
}
|
|
230
|
-
return <text fg={color}>{line}</text>
|
|
236
|
+
return <text fg={color}>{line.padEnd(61, " ")}</text>
|
|
231
237
|
})}
|
|
232
238
|
|
|
233
|
-
{/* OpenCode branding
|
|
234
|
-
<box flexDirection="
|
|
235
|
-
<
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
<box flexDirection="row" gap={0}>
|
|
239
|
-
<text fg={props.theme.textMuted}>║ </text>
|
|
240
|
-
<text fg={props.theme.primary} bold={true}>OpenCode</text>
|
|
241
|
-
<text fg={props.theme.textMuted}> ║</text>
|
|
242
|
-
</box>
|
|
243
|
-
<box flexDirection="row" gap={0}>
|
|
244
|
-
<text fg={props.theme.textMuted}>╚═══════════╝</text>
|
|
245
|
-
</box>
|
|
239
|
+
{/* OpenCode branding */}
|
|
240
|
+
<box flexDirection="row" gap={0} marginTop={1}>
|
|
241
|
+
<text fg={props.theme.textMuted} dimColor={true}>╭ </text>
|
|
242
|
+
<text fg={props.theme.primary} bold={true}> O p e n C o d e </text>
|
|
243
|
+
<text fg={props.theme.textMuted} dimColor={true}> ╮</text>
|
|
246
244
|
</box>
|
|
247
245
|
|
|
248
246
|
<text> </text>
|
|
@@ -250,16 +248,17 @@ const HomeLogo = (props: { theme: TuiThemeCurrent }) => {
|
|
|
250
248
|
)
|
|
251
249
|
}
|
|
252
250
|
|
|
253
|
-
// Sidebar: Full Mustachi face with progressive animations (
|
|
251
|
+
// Sidebar: Full Mustachi face with progressive animations (semantic zone colors)
|
|
254
252
|
const SidebarMustachi = (props: { theme: TuiThemeCurrent; config: Cfg; isBusy?: boolean }) => {
|
|
255
253
|
const [pupilIndex, setPupilIndex] = createSignal(0)
|
|
256
254
|
const [blinkFrame, setBlinkFrame] = createSignal(0)
|
|
257
255
|
const [tongueFrame, setTongueFrame] = createSignal(0)
|
|
258
256
|
const [busyPhrase, setBusyPhrase] = createSignal("")
|
|
257
|
+
const [expressiveCycle, setExpressiveCycle] = createSignal(false)
|
|
259
258
|
|
|
260
259
|
// Animation: pupil movement (look around) - low frequency, progressive
|
|
261
260
|
createEffect(() => {
|
|
262
|
-
if (!props.config.animations || props.isBusy) {
|
|
261
|
+
if (!props.config.animations || props.isBusy || expressiveCycle()) {
|
|
263
262
|
setPupilIndex(0)
|
|
264
263
|
return
|
|
265
264
|
}
|
|
@@ -269,7 +268,7 @@ const SidebarMustachi = (props: { theme: TuiThemeCurrent; config: Cfg; isBusy?:
|
|
|
269
268
|
setPupilIndex((prev) => {
|
|
270
269
|
// 80% chance to stay at center, 20% to move
|
|
271
270
|
if (Math.random() < 0.8) return 0
|
|
272
|
-
return (prev + 1) %
|
|
271
|
+
return (prev + 1) % pupilPositionFrames.length
|
|
273
272
|
})
|
|
274
273
|
}, 3000)
|
|
275
274
|
|
|
@@ -303,15 +302,25 @@ const SidebarMustachi = (props: { theme: TuiThemeCurrent; config: Cfg; isBusy?:
|
|
|
303
302
|
onCleanup(() => clearInterval(interval))
|
|
304
303
|
})
|
|
305
304
|
|
|
306
|
-
// Busy state animation: tongue
|
|
305
|
+
// Busy/expressive state animation: tongue + phrases
|
|
306
|
+
// If isBusy is reliably reactive, use it; otherwise demonstrate expressiveness periodically
|
|
307
307
|
createEffect(() => {
|
|
308
|
-
if (!props.config.animations
|
|
308
|
+
if (!props.config.animations) {
|
|
309
|
+
setTongueFrame(0)
|
|
310
|
+
setBusyPhrase("")
|
|
311
|
+
setExpressiveCycle(false)
|
|
312
|
+
return
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const shouldShowExpression = props.isBusy || expressiveCycle()
|
|
316
|
+
|
|
317
|
+
if (!shouldShowExpression) {
|
|
309
318
|
setTongueFrame(0)
|
|
310
319
|
setBusyPhrase("")
|
|
311
320
|
return
|
|
312
321
|
}
|
|
313
322
|
|
|
314
|
-
//
|
|
323
|
+
// Show tongue progressively when entering expressive state
|
|
315
324
|
let currentFrame = 0
|
|
316
325
|
let tongueTimeoutId: NodeJS.Timeout | undefined
|
|
317
326
|
const growTongue = () => {
|
|
@@ -320,11 +329,10 @@ const SidebarMustachi = (props: { theme: TuiThemeCurrent; config: Cfg; isBusy?:
|
|
|
320
329
|
setTongueFrame(currentFrame)
|
|
321
330
|
}
|
|
322
331
|
}
|
|
323
|
-
// Show tongue immediately when busy
|
|
324
332
|
tongueTimeoutId = setTimeout(growTongue, 200)
|
|
325
333
|
|
|
326
334
|
// Rotate busy phrases
|
|
327
|
-
let phraseIdx =
|
|
335
|
+
let phraseIdx = Math.floor(Math.random() * busyPhrases.length)
|
|
328
336
|
setBusyPhrase(busyPhrases[phraseIdx])
|
|
329
337
|
|
|
330
338
|
const interval = setInterval(() => {
|
|
@@ -340,73 +348,91 @@ const SidebarMustachi = (props: { theme: TuiThemeCurrent; config: Cfg; isBusy?:
|
|
|
340
348
|
})
|
|
341
349
|
})
|
|
342
350
|
|
|
351
|
+
// Fallback: Periodic expressive cycle (conservative - every 45-60s for ~8s)
|
|
352
|
+
// This ensures tongue + phrases are visibly demonstrated even if runtime busy state is unreliable
|
|
353
|
+
createEffect(() => {
|
|
354
|
+
if (!props.config.animations || props.isBusy) return
|
|
355
|
+
|
|
356
|
+
const triggerExpressiveCycle = () => {
|
|
357
|
+
setExpressiveCycle(true)
|
|
358
|
+
|
|
359
|
+
// End expressive cycle after 8 seconds
|
|
360
|
+
setTimeout(() => {
|
|
361
|
+
setExpressiveCycle(false)
|
|
362
|
+
}, 8000)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// First cycle after 30-45s, then every 45-60s
|
|
366
|
+
const firstDelay = 30000 + Math.random() * 15000
|
|
367
|
+
const firstTimeout = setTimeout(triggerExpressiveCycle, firstDelay)
|
|
368
|
+
|
|
369
|
+
const interval = setInterval(() => {
|
|
370
|
+
triggerExpressiveCycle()
|
|
371
|
+
}, 45000 + Math.random() * 15000)
|
|
372
|
+
|
|
373
|
+
onCleanup(() => {
|
|
374
|
+
clearTimeout(firstTimeout)
|
|
375
|
+
clearInterval(interval)
|
|
376
|
+
})
|
|
377
|
+
})
|
|
378
|
+
|
|
343
379
|
// Build the complete Mustachi face
|
|
344
380
|
const buildFace = () => {
|
|
345
|
-
const lines: string[] = []
|
|
381
|
+
const lines: { content: string; zone: string }[] = []
|
|
346
382
|
|
|
347
|
-
// Select eye
|
|
348
|
-
let
|
|
383
|
+
// Select eye frame based on state
|
|
384
|
+
let eyeFrame = pupilPositionFrames[pupilIndex()]
|
|
385
|
+
|
|
386
|
+
// Apply squint if busy/expressive
|
|
387
|
+
if (props.isBusy || expressiveCycle()) {
|
|
388
|
+
eyeFrame = eyeSquinted
|
|
389
|
+
}
|
|
349
390
|
|
|
350
391
|
// Apply blink animation if active
|
|
351
|
-
if (blinkFrame()
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
392
|
+
if (blinkFrame() === 1) {
|
|
393
|
+
eyeFrame = eyeBlinkHalf
|
|
394
|
+
} else if (blinkFrame() === 2) {
|
|
395
|
+
eyeFrame = eyeBlinkClosed
|
|
355
396
|
}
|
|
356
397
|
|
|
357
|
-
// Add eyes with
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
const modifiedLine = line.substring(0, 2) + pupil + line.substring(13)
|
|
363
|
-
lines.push(modifiedLine)
|
|
364
|
-
} else {
|
|
365
|
-
lines.push(line)
|
|
366
|
-
}
|
|
398
|
+
// Add eyes with zone metadata
|
|
399
|
+
eyeFrame.forEach((line, idx) => {
|
|
400
|
+
// Lines 0-1 are monocle border, lines 2-4 are eye interior
|
|
401
|
+
const zone = idx < 2 ? "monocle" : "eyes"
|
|
402
|
+
lines.push({ content: line, zone })
|
|
367
403
|
})
|
|
368
404
|
|
|
369
405
|
// Add mustache section
|
|
370
|
-
mustachiMustacheSection.forEach(line =>
|
|
406
|
+
mustachiMustacheSection.forEach(line => {
|
|
407
|
+
lines.push({ content: line, zone: "mustache" })
|
|
408
|
+
})
|
|
371
409
|
|
|
372
|
-
// Add tongue if
|
|
373
|
-
if (props.isBusy && tongueFrame() > 0) {
|
|
410
|
+
// Add tongue if expressive (mark as tongue zone for pink color)
|
|
411
|
+
if ((props.isBusy || expressiveCycle()) && tongueFrame() > 0) {
|
|
374
412
|
const tongueLines = tongueFrames[tongueFrame()]
|
|
375
|
-
tongueLines.forEach(line =>
|
|
413
|
+
tongueLines.forEach(line => {
|
|
414
|
+
lines.push({ content: line, zone: "tongue" })
|
|
415
|
+
})
|
|
376
416
|
}
|
|
377
417
|
|
|
378
418
|
return lines
|
|
379
419
|
}
|
|
380
420
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
421
|
+
// Semantic zone colors for better visual hierarchy
|
|
422
|
+
const zoneColors = {
|
|
423
|
+
monocle: "#A0A0A0", // Lighter gray for monocle border
|
|
424
|
+
eyes: "#808080", // Mid gray for eyes
|
|
425
|
+
mustache: "#606060", // Darker gray for mustache
|
|
426
|
+
tongue: "#FF4466", // Pink/Red for tongue
|
|
427
|
+
}
|
|
388
428
|
|
|
389
429
|
return (
|
|
390
430
|
<box flexDirection="column" alignItems="center">
|
|
391
|
-
{/* Full Mustachi face with
|
|
392
|
-
{
|
|
393
|
-
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
if (isTongue) {
|
|
398
|
-
return <text fg={tongueColor}>{displayLine}</text>
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// Apply grayscale gradient to eyes and mustache
|
|
402
|
-
const totalLines = faceLines.length
|
|
403
|
-
let color = midGray
|
|
404
|
-
if (idx < totalLines / 3) {
|
|
405
|
-
color = lightGray // Top highlight
|
|
406
|
-
} else if (idx >= (2 * totalLines) / 3) {
|
|
407
|
-
color = darkGray // Bottom shadow
|
|
408
|
-
}
|
|
409
|
-
return <text fg={color}>{displayLine}</text>
|
|
431
|
+
{/* Full Mustachi face with semantic zone colors */}
|
|
432
|
+
{buildFace().map(({ content, zone }) => {
|
|
433
|
+
const color = zoneColors[zone as keyof typeof zoneColors] || zoneColors.mustache
|
|
434
|
+
const paddedLine = content.padEnd(27, " ")
|
|
435
|
+
return <text fg={color}>{paddedLine}</text>
|
|
410
436
|
})}
|
|
411
437
|
|
|
412
438
|
{/* Busy phrase if loading */}
|