plugin-gentleman 1.0.4 → 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 +146 -111
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
|
|
@@ -242,16 +248,17 @@ const HomeLogo = (props: { theme: TuiThemeCurrent }) => {
|
|
|
242
248
|
)
|
|
243
249
|
}
|
|
244
250
|
|
|
245
|
-
// Sidebar: Full Mustachi face with progressive animations (
|
|
251
|
+
// Sidebar: Full Mustachi face with progressive animations (semantic zone colors)
|
|
246
252
|
const SidebarMustachi = (props: { theme: TuiThemeCurrent; config: Cfg; isBusy?: boolean }) => {
|
|
247
253
|
const [pupilIndex, setPupilIndex] = createSignal(0)
|
|
248
254
|
const [blinkFrame, setBlinkFrame] = createSignal(0)
|
|
249
255
|
const [tongueFrame, setTongueFrame] = createSignal(0)
|
|
250
256
|
const [busyPhrase, setBusyPhrase] = createSignal("")
|
|
257
|
+
const [expressiveCycle, setExpressiveCycle] = createSignal(false)
|
|
251
258
|
|
|
252
259
|
// Animation: pupil movement (look around) - low frequency, progressive
|
|
253
260
|
createEffect(() => {
|
|
254
|
-
if (!props.config.animations || props.isBusy) {
|
|
261
|
+
if (!props.config.animations || props.isBusy || expressiveCycle()) {
|
|
255
262
|
setPupilIndex(0)
|
|
256
263
|
return
|
|
257
264
|
}
|
|
@@ -261,7 +268,7 @@ const SidebarMustachi = (props: { theme: TuiThemeCurrent; config: Cfg; isBusy?:
|
|
|
261
268
|
setPupilIndex((prev) => {
|
|
262
269
|
// 80% chance to stay at center, 20% to move
|
|
263
270
|
if (Math.random() < 0.8) return 0
|
|
264
|
-
return (prev + 1) %
|
|
271
|
+
return (prev + 1) % pupilPositionFrames.length
|
|
265
272
|
})
|
|
266
273
|
}, 3000)
|
|
267
274
|
|
|
@@ -295,15 +302,25 @@ const SidebarMustachi = (props: { theme: TuiThemeCurrent; config: Cfg; isBusy?:
|
|
|
295
302
|
onCleanup(() => clearInterval(interval))
|
|
296
303
|
})
|
|
297
304
|
|
|
298
|
-
// Busy state animation: tongue
|
|
305
|
+
// Busy/expressive state animation: tongue + phrases
|
|
306
|
+
// If isBusy is reliably reactive, use it; otherwise demonstrate expressiveness periodically
|
|
299
307
|
createEffect(() => {
|
|
300
|
-
if (!props.config.animations
|
|
308
|
+
if (!props.config.animations) {
|
|
301
309
|
setTongueFrame(0)
|
|
302
310
|
setBusyPhrase("")
|
|
311
|
+
setExpressiveCycle(false)
|
|
303
312
|
return
|
|
304
313
|
}
|
|
305
314
|
|
|
306
|
-
|
|
315
|
+
const shouldShowExpression = props.isBusy || expressiveCycle()
|
|
316
|
+
|
|
317
|
+
if (!shouldShowExpression) {
|
|
318
|
+
setTongueFrame(0)
|
|
319
|
+
setBusyPhrase("")
|
|
320
|
+
return
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Show tongue progressively when entering expressive state
|
|
307
324
|
let currentFrame = 0
|
|
308
325
|
let tongueTimeoutId: NodeJS.Timeout | undefined
|
|
309
326
|
const growTongue = () => {
|
|
@@ -312,11 +329,10 @@ const SidebarMustachi = (props: { theme: TuiThemeCurrent; config: Cfg; isBusy?:
|
|
|
312
329
|
setTongueFrame(currentFrame)
|
|
313
330
|
}
|
|
314
331
|
}
|
|
315
|
-
// Show tongue immediately when busy
|
|
316
332
|
tongueTimeoutId = setTimeout(growTongue, 200)
|
|
317
333
|
|
|
318
334
|
// Rotate busy phrases
|
|
319
|
-
let phraseIdx =
|
|
335
|
+
let phraseIdx = Math.floor(Math.random() * busyPhrases.length)
|
|
320
336
|
setBusyPhrase(busyPhrases[phraseIdx])
|
|
321
337
|
|
|
322
338
|
const interval = setInterval(() => {
|
|
@@ -332,71 +348,90 @@ const SidebarMustachi = (props: { theme: TuiThemeCurrent; config: Cfg; isBusy?:
|
|
|
332
348
|
})
|
|
333
349
|
})
|
|
334
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
|
+
|
|
335
379
|
// Build the complete Mustachi face
|
|
336
380
|
const buildFace = () => {
|
|
337
|
-
const lines: string[] = []
|
|
381
|
+
const lines: { content: string; zone: string }[] = []
|
|
382
|
+
|
|
383
|
+
// Select eye frame based on state
|
|
384
|
+
let eyeFrame = pupilPositionFrames[pupilIndex()]
|
|
338
385
|
|
|
339
|
-
//
|
|
340
|
-
|
|
386
|
+
// Apply squint if busy/expressive
|
|
387
|
+
if (props.isBusy || expressiveCycle()) {
|
|
388
|
+
eyeFrame = eyeSquinted
|
|
389
|
+
}
|
|
341
390
|
|
|
342
391
|
// Apply blink animation if active
|
|
343
|
-
if (blinkFrame()
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
392
|
+
if (blinkFrame() === 1) {
|
|
393
|
+
eyeFrame = eyeBlinkHalf
|
|
394
|
+
} else if (blinkFrame() === 2) {
|
|
395
|
+
eyeFrame = eyeBlinkClosed
|
|
347
396
|
}
|
|
348
397
|
|
|
349
|
-
// Add eyes with
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
const modifiedLine = line.substring(0, 2) + pupil + line.substring(13)
|
|
355
|
-
lines.push(modifiedLine)
|
|
356
|
-
} else {
|
|
357
|
-
lines.push(line)
|
|
358
|
-
}
|
|
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 })
|
|
359
403
|
})
|
|
360
404
|
|
|
361
405
|
// Add mustache section
|
|
362
|
-
mustachiMustacheSection.forEach(line =>
|
|
406
|
+
mustachiMustacheSection.forEach(line => {
|
|
407
|
+
lines.push({ content: line, zone: "mustache" })
|
|
408
|
+
})
|
|
363
409
|
|
|
364
|
-
// Add tongue if
|
|
365
|
-
if (props.isBusy && tongueFrame() > 0) {
|
|
410
|
+
// Add tongue if expressive (mark as tongue zone for pink color)
|
|
411
|
+
if ((props.isBusy || expressiveCycle()) && tongueFrame() > 0) {
|
|
366
412
|
const tongueLines = tongueFrames[tongueFrame()]
|
|
367
|
-
tongueLines.forEach(line =>
|
|
413
|
+
tongueLines.forEach(line => {
|
|
414
|
+
lines.push({ content: line, zone: "tongue" })
|
|
415
|
+
})
|
|
368
416
|
}
|
|
369
417
|
|
|
370
418
|
return lines
|
|
371
419
|
}
|
|
372
420
|
|
|
373
|
-
//
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
+
}
|
|
378
428
|
|
|
379
429
|
return (
|
|
380
430
|
<box flexDirection="column" alignItems="center">
|
|
381
|
-
{/* Full Mustachi face with
|
|
382
|
-
{buildFace().map((
|
|
383
|
-
|
|
384
|
-
const
|
|
385
|
-
const displayLine = isTongue ? line.substring(7) : line
|
|
386
|
-
const paddedLine = displayLine.padEnd(25, " ")
|
|
387
|
-
|
|
388
|
-
if (isTongue) {
|
|
389
|
-
return <text fg={tongueColor}>{paddedLine}</text>
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// Apply grayscale gradient to eyes and mustache
|
|
393
|
-
const totalLines = arr.length
|
|
394
|
-
let color = midGray
|
|
395
|
-
if (idx < totalLines / 3) {
|
|
396
|
-
color = lightGray // Top highlight
|
|
397
|
-
} else if (idx >= (2 * totalLines) / 3) {
|
|
398
|
-
color = darkGray // Bottom shadow
|
|
399
|
-
}
|
|
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, " ")
|
|
400
435
|
return <text fg={color}>{paddedLine}</text>
|
|
401
436
|
})}
|
|
402
437
|
|