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.
Files changed (3) hide show
  1. package/README.md +5 -3
  2. package/package.json +1 -1
  3. package/tui.tsx +146 -111
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Plugin Gentleman
2
2
 
3
- > **Mustachi** — An animated ASCII mascot bringing personality to your OpenCode terminal.
3
+ > **For the Gentleman Programming community** — Bringing Mustachi, our beloved mascot, into your OpenCode terminal.
4
4
 
5
- An OpenCode TUI plugin that adds visual flair and environment awareness to your coding sessions:
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
- An ASCII character with personality:
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",
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 - compact version for sidebar (25 chars wide)
10
- // Base structure with eyes that will be replaced dynamically
11
- const mustachiNeutralBase = [
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
- // Squinted eyes version for busy state
20
- const mustachiSquintedBase = [
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 (compact 25-char wide design)
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
- // Left pupil positions for look-around animation (progressive)
64
- // Modifies only the left eye (white sclera with dark pupil)
65
- // Right eye is monocle/glass and remains static
66
- // Pupil is on lines 2 and 3 (indices 2-3) of the 5-line eye array
67
- const leftPupilPositions = [
68
- "██░░███░░██", // center (line 2 of eyes)
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 (grayscale for clarity)
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) % leftPupilPositions.length
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 grows progressively + rotate phrases
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 || !props.isBusy) {
308
+ if (!props.config.animations) {
301
309
  setTongueFrame(0)
302
310
  setBusyPhrase("")
311
+ setExpressiveCycle(false)
303
312
  return
304
313
  }
305
314
 
306
- // Grow tongue progressively when entering busy state (2 frames: hidden -> visible)
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 = 0
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
- // Select eye base based on busy state
340
- let eyeBase = props.isBusy ? mustachiSquintedBase : mustachiNeutralBase
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() > 0 && blinkFrame() < blinkFrames.length) {
344
- eyeBase = props.isBusy
345
- ? blinkFrames[blinkFrame()].squinted
346
- : blinkFrames[blinkFrame()].left
392
+ if (blinkFrame() === 1) {
393
+ eyeFrame = eyeBlinkHalf
394
+ } else if (blinkFrame() === 2) {
395
+ eyeFrame = eyeBlinkClosed
347
396
  }
348
397
 
349
- // Add eyes with pupil position (modify line 2 for left eye pupil - index 2 in 5-line array)
350
- eyeBase.forEach((line, idx) => {
351
- if (idx === 2 && !props.isBusy && pupilIndex() >= 0) {
352
- // Replace pupil in left eye (positions 2-12 of the line for the 25-char compact design)
353
- const pupil = leftPupilPositions[pupilIndex()]
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 => lines.push(line))
406
+ mustachiMustacheSection.forEach(line => {
407
+ lines.push({ content: line, zone: "mustache" })
408
+ })
363
409
 
364
- // Add tongue if busy (progressive frames) - mark as tongue for coloring
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 => lines.push(`TONGUE:${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
- // Grayscale palette for TUI clarity
374
- const lightGray = "#C0C0C0" // Light gray for highlights
375
- const midGray = "#808080" // Mid gray for main body
376
- const darkGray = "#505050" // Dark gray for shadows
377
- const tongueColor = "#FF4466" // Pink/Red for tongue
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 grayscale gradient + pink tongue */}
382
- {buildFace().map((line, idx, arr) => {
383
- // Check if this is a tongue line
384
- const isTongue = line.startsWith("TONGUE:")
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