@mingxy/opencode-mascot 0.2.6 → 0.2.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mingxy/opencode-mascot",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "OpenCode TUI mascot plugin framework - customizable ASCII mascots for your terminal",
5
5
  "author": "mingxy",
6
6
  "license": "MIT",
@@ -4,7 +4,7 @@ import { createSignal } from "solid-js";
4
4
  import type { JSX } from "@opentui/solid";
5
5
  import type { MascotPack } from "../core/types";
6
6
  import { createAnimatedRenderer } from "../core/ascii-renderer";
7
- import { onCelebrate } from "../core/celebration-bus";
7
+ import { onCelebrate, onVersion } from "../core/celebration-bus";
8
8
 
9
9
  interface HomeMascotProps {
10
10
  mascots: Record<string, MascotPack>;
@@ -44,6 +44,10 @@ export function HomeMascot(props: HomeMascotProps): JSX.Element {
44
44
  renderers[currentName()].celebrateUpdate(newVersion);
45
45
  });
46
46
 
47
+ onVersion((version) => {
48
+ renderers[currentName()].showVersion(version);
49
+ });
50
+
47
51
  return (
48
52
  <box
49
53
  left={posX()}
@@ -1,10 +1,10 @@
1
1
  /** @jsxImportSource @opentui/solid */
2
2
 
3
- import { createSignal, onCleanup } from "solid-js";
3
+ import { createSignal } from "solid-js";
4
4
  import type { JSX } from "@opentui/solid";
5
5
  import type { MascotPack, MascotState } from "../core/types";
6
6
  import { createAnimatedRenderer } from "../core/ascii-renderer";
7
- import { onCelebrate } from "../core/celebration-bus";
7
+ import { onCelebrate, onVersion } from "../core/celebration-bus";
8
8
 
9
9
  interface SidebarMascotProps {
10
10
  mascots: Record<string, MascotPack>;
@@ -27,15 +27,6 @@ const DEFAULT_STATE_MAP: Partial<Record<MascotState, string>> = {
27
27
  sleeping: "baozi",
28
28
  };
29
29
 
30
- const MASCOT_WIDTH = 10;
31
- const MASCOT_HEIGHT = 5;
32
- const PEEK = 2;
33
- const PEEK_INTERVAL = 1200;
34
-
35
- function termWidth(): number {
36
- return (typeof process !== "undefined" && process.stdout?.columns) || 80;
37
- }
38
-
39
30
  export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
40
31
  const names = Object.keys(props.mascots);
41
32
  const initialName =
@@ -52,89 +43,18 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
52
43
  let dragAnchorY = 0;
53
44
  let lastClickTime = 0;
54
45
  let isDragging = false;
55
- let hideSide: "left" | "right" | null = null;
56
- let peekTimer: ReturnType<typeof setInterval> | null = null;
57
- let returnTimer: ReturnType<typeof setInterval> | null = null;
58
46
 
59
47
  const renderers: Record<string, ReturnType<typeof createAnimatedRenderer>> = {};
60
48
  for (const [name, pack] of Object.entries(props.mascots)) {
61
49
  renderers[name] = createAnimatedRenderer(pack);
62
50
  }
63
51
 
64
- const stopPeek = () => {
65
- if (peekTimer) { clearInterval(peekTimer); peekTimer = null; }
66
- };
67
- const stopReturn = () => {
68
- if (returnTimer) { clearInterval(returnTimer); returnTimer = null; }
69
- };
70
-
71
- onCleanup(() => { stopPeek(); stopReturn(); });
72
-
73
52
  const switchToNext = () => {
74
53
  const cur = currentName();
75
54
  const idx = names.indexOf(cur);
76
55
  setCurrentName(names[(idx + 1) % names.length]);
77
56
  };
78
57
 
79
- const startPeek = () => {
80
- stopPeek();
81
- let phase = false;
82
- peekTimer = setInterval(() => {
83
- phase = !phase;
84
- const stretch = phase ? PEEK : 0;
85
- const w = termWidth();
86
- if (hideSide === "left") {
87
- setPosX(-(MASCOT_WIDTH - PEEK) + stretch);
88
- } else if (hideSide === "right") {
89
- setPosX(w - PEEK - stretch);
90
- }
91
- }, PEEK_INTERVAL);
92
- };
93
-
94
- const returnToView = () => {
95
- if (!hideSide) return;
96
- stopPeek();
97
- const w = termWidth();
98
- const cur = posX();
99
- const targetX = hideSide === "left" ? 0 : Math.max(0, w - MASCOT_WIDTH);
100
- const step = targetX > cur ? 2 : -2;
101
- returnTimer = setInterval(() => {
102
- const now = posX();
103
- if (Math.abs(now - targetX) <= 2) {
104
- setPosX(targetX);
105
- stopReturn();
106
- hideSide = null;
107
- renderers[currentName()].bounce();
108
- return;
109
- }
110
- setPosX(now + step);
111
- }, 16);
112
- };
113
-
114
- const checkEdge = () => {
115
- const w = termWidth();
116
- const x = posX();
117
- if (x <= -(MASCOT_WIDTH - PEEK) + 1) {
118
- hideSide = "left";
119
- startPeek();
120
- } else if (x >= w - PEEK - 1) {
121
- hideSide = "right";
122
- startPeek();
123
- }
124
- };
125
-
126
- const clampX = (rawX: number): number => {
127
- const w = termWidth();
128
- const minX = -(MASCOT_WIDTH - PEEK);
129
- const maxX = w - PEEK;
130
- return Math.max(minX, Math.min(rawX, maxX));
131
- };
132
-
133
- const clampY = (rawY: number): number => {
134
- const h = (typeof process !== "undefined" && process.stdout?.rows) || 24;
135
- return Math.max(0, Math.min(rawY, h - MASCOT_HEIGHT));
136
- };
137
-
138
58
  const setStateWithSwitch = (s: MascotState) => {
139
59
  const cur = currentName();
140
60
  renderers[cur].setState(s);
@@ -149,7 +69,6 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
149
69
  const payload = data as { type?: string; properties?: { sessionID?: string; status?: { type?: string } } } | null;
150
70
  const statusType = payload?.properties?.status?.type;
151
71
  if (statusType === "busy" || statusType === "retry") {
152
- if (hideSide) returnToView();
153
72
  renderers[currentName()].setState("busy");
154
73
  } else {
155
74
  setStateWithSwitch("idle");
@@ -179,6 +98,10 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
179
98
  renderers[currentName()].celebrateUpdate(newVersion);
180
99
  });
181
100
 
101
+ onVersion((version) => {
102
+ renderers[currentName()].showVersion(version);
103
+ });
104
+
182
105
  return (
183
106
  <box
184
107
  position="absolute"
@@ -188,8 +111,6 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
188
111
  zIndex={100}
189
112
  flexDirection="column"
190
113
  onMouseDown={(e: any) => {
191
- if (hideSide) { returnToView(); return; }
192
-
193
114
  const now = Date.now();
194
115
  if (now - lastClickTime < 300) {
195
116
  switchToNext();
@@ -212,8 +133,8 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
212
133
  }}
213
134
  onMouseDrag={(e: any) => {
214
135
  if (e.modifiers?.alt && isDragging) {
215
- setPosX(clampX(dragAnchorX + (e.x - dragStartX)));
216
- setPosY(clampY(dragAnchorY + (e.y - dragStartY)));
136
+ setPosX(dragAnchorX + (e.x - dragStartX));
137
+ setPosY(dragAnchorY + (e.y - dragStartY));
217
138
  e.preventDefault();
218
139
  e.stopPropagation();
219
140
  props.api.renderer.clearSelection();
@@ -223,14 +144,12 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
223
144
  if (isDragging) {
224
145
  isDragging = false;
225
146
  renderers[currentName()].setDragging(false);
226
- checkEdge();
227
147
  }
228
148
  }}
229
149
  onMouseDragEnd={() => {
230
150
  if (isDragging) {
231
151
  isDragging = false;
232
152
  renderers[currentName()].setDragging(false);
233
- checkEdge();
234
153
  }
235
154
  }}
236
155
  >
@@ -4,6 +4,15 @@ import { createSignal, onCleanup } from "solid-js";
4
4
  import type { JSX } from "@opentui/solid";
5
5
  import type { MascotPack, MascotState, EffectTimerCtx, EffectRenderCtx } from "./types";
6
6
 
7
+ const SUPERSCRIPT: Record<string, string> = {
8
+ "0": "⁰", "1": "¹", "2": "²", "3": "³", "4": "⁴",
9
+ "5": "⁵", "6": "⁶", "7": "⁷", "8": "⁸", "9": "⁹", ".": "·",
10
+ };
11
+
12
+ function toSuperscript(s: string): string {
13
+ return s.split("").map(c => SUPERSCRIPT[c] ?? c).join("");
14
+ }
15
+
7
16
  const STATE_TO_FRAME: Record<MascotState, string> = {
8
17
  idle: "default",
9
18
  busy: "default",
@@ -49,6 +58,7 @@ export function createAnimatedRenderer(pack: MascotPack): {
49
58
  setDragging: (v: boolean) => void;
50
59
  celebrateUpdate: (newVersion: string) => void;
51
60
  bounce: () => void;
61
+ showVersion: (version: string) => void;
52
62
  } {
53
63
  const anim = { ...DEFAULT_ANIM, ...pack.animations };
54
64
  const fg = pack.colors?.defaultFg || undefined;
@@ -345,5 +355,10 @@ export function createAnimatedRenderer(pack: MascotPack): {
345
355
  setTimeout(() => setJumpOffset(0), 450);
346
356
  };
347
357
 
348
- return { element, setState, toggleWalk, setDragging, celebrateUpdate, bounce };
358
+ const showVersion = (version: string) => {
359
+ setCelebrate({ text: `ᵛ${toSuperscript(version)}`, count: 0 });
360
+ setTimeout(() => setCelebrate(null), 3000);
361
+ };
362
+
363
+ return { element, setState, toggleWalk, setDragging, celebrateUpdate, bounce, showVersion };
349
364
  }
@@ -1,6 +1,7 @@
1
1
  const bus = new EventTarget();
2
2
 
3
3
  const CELEBRATE_EVENT = "mascot:celebrate";
4
+ const VERSION_EVENT = "mascot:version";
4
5
 
5
6
  export function emitCelebrate(newVersion: string): void {
6
7
  bus.dispatchEvent(new CustomEvent(CELEBRATE_EVENT, { detail: { newVersion } }));
@@ -14,3 +15,16 @@ export function onCelebrate(handler: (newVersion: string) => void): () => void {
14
15
  bus.addEventListener(CELEBRATE_EVENT, listener);
15
16
  return () => bus.removeEventListener(CELEBRATE_EVENT, listener);
16
17
  }
18
+
19
+ export function emitVersion(version: string): void {
20
+ bus.dispatchEvent(new CustomEvent(VERSION_EVENT, { detail: { version } }));
21
+ }
22
+
23
+ export function onVersion(handler: (version: string) => void): () => void {
24
+ const listener = (e: Event) => {
25
+ const detail = (e as CustomEvent).detail as { version: string };
26
+ handler(detail.version);
27
+ };
28
+ bus.addEventListener(VERSION_EVENT, listener);
29
+ return () => bus.removeEventListener(VERSION_EVENT, listener);
30
+ }
package/tui.tsx CHANGED
@@ -7,7 +7,7 @@ import { loadAllMascots } from "./src/core/mascot-loader"
7
7
  import { SidebarMascot } from "./src/components/sidebar-mascot"
8
8
  import { HomeMascot } from "./src/components/home-mascot"
9
9
  import { checkAndUpdate } from "./src/core/updater"
10
- import { emitCelebrate } from "./src/core/celebration-bus"
10
+ import { emitCelebrate, emitVersion } from "./src/core/celebration-bus"
11
11
 
12
12
  const __filename = fileURLToPath(import.meta.url);
13
13
  const __dirname = dirname(__filename);
@@ -37,6 +37,8 @@ const tui: TuiPlugin = async (api, _options) => {
37
37
  checkAndUpdate(pluginVersion, (newVersion) => {
38
38
  emitCelebrate(newVersion);
39
39
  }).catch(() => {});
40
+
41
+ setTimeout(() => emitVersion(pluginVersion), 1500);
40
42
  }
41
43
 
42
44
  const plugin: TuiPluginModule = {