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.
Files changed (3) hide show
  1. package/README.md +5 -3
  2. package/package.json +1 -1
  3. package/tui.tsx +153 -127
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.3",
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
@@ -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 — enlarged and prominent */}
234
- <box flexDirection="column" alignItems="center" marginTop={1}>
235
- <box flexDirection="row" gap={0}>
236
- <text fg={props.theme.textMuted}>╔═══════════╗</text>
237
- </box>
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 (grayscale for clarity)
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) % leftPupilPositions.length
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 grows progressively + rotate phrases
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 || !props.isBusy) {
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
- // Grow tongue progressively when entering busy state (2 frames: hidden -> visible)
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 = 0
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 base based on busy state
348
- let eyeBase = props.isBusy ? mustachiSquintedBase : mustachiNeutralBase
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() > 0 && blinkFrame() < blinkFrames.length) {
352
- eyeBase = props.isBusy
353
- ? blinkFrames[blinkFrame()].squinted
354
- : blinkFrames[blinkFrame()].left
392
+ if (blinkFrame() === 1) {
393
+ eyeFrame = eyeBlinkHalf
394
+ } else if (blinkFrame() === 2) {
395
+ eyeFrame = eyeBlinkClosed
355
396
  }
356
397
 
357
- // Add eyes with pupil position (modify line 2 for left eye pupil - index 2 in 5-line array)
358
- eyeBase.forEach((line, idx) => {
359
- if (idx === 2 && !props.isBusy && pupilIndex() >= 0) {
360
- // Replace pupil in left eye (positions 2-12 of the line for the 25-char compact design)
361
- const pupil = leftPupilPositions[pupilIndex()]
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 => lines.push(line))
406
+ mustachiMustacheSection.forEach(line => {
407
+ lines.push({ content: line, zone: "mustache" })
408
+ })
371
409
 
372
- // Add tongue if busy (progressive frames) - mark as tongue for coloring
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 => lines.push(`TONGUE:${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
- const faceLines = buildFace()
382
-
383
- // Grayscale palette for TUI clarity
384
- const lightGray = "#C0C0C0" // Light gray for highlights
385
- const midGray = "#808080" // Mid gray for main body
386
- const darkGray = "#505050" // Dark gray for shadows
387
- 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
+ }
388
428
 
389
429
  return (
390
430
  <box flexDirection="column" alignItems="center">
391
- {/* Full Mustachi face with grayscale gradient + pink tongue */}
392
- {faceLines.map((line, idx) => {
393
- // Check if this is a tongue line
394
- const isTongue = line.startsWith("TONGUE:")
395
- const displayLine = isTongue ? line.substring(7) : line
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 */}