@mingxy/opencode-mascot 0.2.5 → 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.5",
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,8 +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";
8
- import { useDraggableMascot } from "./use-draggable-mascot";
7
+ import { onCelebrate, onVersion } from "../core/celebration-bus";
9
8
 
10
9
  interface HomeMascotProps {
11
10
  mascots: Record<string, MascotPack>;
@@ -21,6 +20,14 @@ export function HomeMascot(props: HomeMascotProps): JSX.Element {
21
20
  const initialName = names[Math.floor(Math.random() * names.length)];
22
21
 
23
22
  const [currentName, setCurrentName] = createSignal(initialName);
23
+ const [posX, setPosX] = createSignal(0);
24
+ const [posY, setPosY] = createSignal(0);
25
+ let dragStartX = 0;
26
+ let dragStartY = 0;
27
+ let dragAnchorX = 0;
28
+ let dragAnchorY = 0;
29
+ let lastClickTime = 0;
30
+ let isDragging = false;
24
31
 
25
32
  const renderers: Record<string, ReturnType<typeof createAnimatedRenderer>> = {};
26
33
  for (const [name, pack] of Object.entries(props.mascots)) {
@@ -33,21 +40,14 @@ export function HomeMascot(props: HomeMascotProps): JSX.Element {
33
40
  setCurrentName(names[(idx + 1) % names.length]);
34
41
  };
35
42
 
36
- const { posX, posY, mouseProps } = useDraggableMascot({
37
- initialX: 0,
38
- initialY: 0,
39
- mascotWidth: 10,
40
- mascotHeight: 5,
41
- onSwitch: switchToNext,
42
- clearSelection: () => props.api.renderer.clearSelection(),
43
- setDragging: (v) => renderers[currentName()].setDragging(v),
44
- enableEdge: false,
45
- });
46
-
47
43
  onCelebrate((newVersion) => {
48
44
  renderers[currentName()].celebrateUpdate(newVersion);
49
45
  });
50
46
 
47
+ onVersion((version) => {
48
+ renderers[currentName()].showVersion(version);
49
+ });
50
+
51
51
  return (
52
52
  <box
53
53
  left={posX()}
@@ -55,7 +55,48 @@ export function HomeMascot(props: HomeMascotProps): JSX.Element {
55
55
  alignItems="center"
56
56
  zIndex={100}
57
57
  flexDirection="column"
58
- {...mouseProps}
58
+ onMouseDown={(e: any) => {
59
+ const now = Date.now();
60
+ if (now - lastClickTime < 300) {
61
+ switchToNext();
62
+ lastClickTime = 0;
63
+ return;
64
+ }
65
+ lastClickTime = now;
66
+
67
+ if (e.modifiers?.alt) {
68
+ dragStartX = e.x;
69
+ dragStartY = e.y;
70
+ dragAnchorX = posX();
71
+ dragAnchorY = posY();
72
+ isDragging = true;
73
+ renderers[currentName()].setDragging(true);
74
+ e.preventDefault();
75
+ e.stopPropagation();
76
+ props.api.renderer.clearSelection();
77
+ }
78
+ }}
79
+ onMouseDrag={(e: any) => {
80
+ if (e.modifiers?.alt && isDragging) {
81
+ setPosX(dragAnchorX + (e.x - dragStartX));
82
+ setPosY(dragAnchorY + (e.y - dragStartY));
83
+ e.preventDefault();
84
+ e.stopPropagation();
85
+ props.api.renderer.clearSelection();
86
+ }
87
+ }}
88
+ onMouseUp={() => {
89
+ if (isDragging) {
90
+ isDragging = false;
91
+ renderers[currentName()].setDragging(false);
92
+ }
93
+ }}
94
+ onMouseDragEnd={() => {
95
+ if (isDragging) {
96
+ isDragging = false;
97
+ renderers[currentName()].setDragging(false);
98
+ }
99
+ }}
59
100
  >
60
101
  {renderers[currentName()]?.element() ?? null}
61
102
  </box>
@@ -4,8 +4,7 @@ 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";
8
- import { useDraggableMascot } from "./use-draggable-mascot";
7
+ import { onCelebrate, onVersion } from "../core/celebration-bus";
9
8
 
10
9
  interface SidebarMascotProps {
11
10
  mascots: Record<string, MascotPack>;
@@ -28,9 +27,6 @@ const DEFAULT_STATE_MAP: Partial<Record<MascotState, string>> = {
28
27
  sleeping: "baozi",
29
28
  };
30
29
 
31
- const MASCOT_WIDTH = 10;
32
- const MASCOT_HEIGHT = 5;
33
-
34
30
  export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
35
31
  const names = Object.keys(props.mascots);
36
32
  const initialName =
@@ -39,6 +35,14 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
39
35
  : names[Math.floor(Math.random() * names.length)];
40
36
 
41
37
  const [currentName, setCurrentName] = createSignal(initialName);
38
+ const [posX, setPosX] = createSignal(20);
39
+ const [posY, setPosY] = createSignal(2);
40
+ let dragStartX = 0;
41
+ let dragStartY = 0;
42
+ let dragAnchorX = 0;
43
+ let dragAnchorY = 0;
44
+ let lastClickTime = 0;
45
+ let isDragging = false;
42
46
 
43
47
  const renderers: Record<string, ReturnType<typeof createAnimatedRenderer>> = {};
44
48
  for (const [name, pack] of Object.entries(props.mascots)) {
@@ -51,29 +55,10 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
51
55
  setCurrentName(names[(idx + 1) % names.length]);
52
56
  };
53
57
 
54
- const { posX, posY, mouseProps, returnToView } = useDraggableMascot({
55
- initialX: 20,
56
- initialY: 2,
57
- mascotWidth: MASCOT_WIDTH,
58
- mascotHeight: MASCOT_HEIGHT,
59
- onSwitch: switchToNext,
60
- clearSelection: () => props.api.renderer.clearSelection(),
61
- setDragging: (v) => renderers[currentName()].setDragging(v),
62
- onReturnComplete: () => renderers[currentName()].bounce(),
63
- });
64
-
65
- const switchTo = (name: string) => {
66
- if (props.mascots[name] && name !== currentName()) {
67
- setCurrentName(name);
68
- }
69
- };
70
-
71
58
  const setStateWithSwitch = (s: MascotState) => {
72
59
  const cur = currentName();
73
60
  renderers[cur].setState(s);
74
-
75
- const stateMap = DEFAULT_STATE_MAP;
76
- const target = stateMap[s];
61
+ const target = DEFAULT_STATE_MAP[s];
77
62
  if (target && target !== cur && props.mascots[target]) {
78
63
  setCurrentName(target);
79
64
  renderers[target].setState(s);
@@ -83,9 +68,7 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
83
68
  props.api.event.on("session.status", (data: unknown) => {
84
69
  const payload = data as { type?: string; properties?: { sessionID?: string; status?: { type?: string } } } | null;
85
70
  const statusType = payload?.properties?.status?.type;
86
-
87
71
  if (statusType === "busy" || statusType === "retry") {
88
- returnToView();
89
72
  renderers[currentName()].setState("busy");
90
73
  } else {
91
74
  setStateWithSwitch("idle");
@@ -100,12 +83,10 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
100
83
  props.api.event.on("mascot.switch", (data: unknown) => {
101
84
  const target = data as { name?: string } | null;
102
85
  if (target?.name) {
103
- switchTo(target.name);
86
+ const name = target.name;
87
+ if (props.mascots[name] && name !== currentName()) setCurrentName(name);
104
88
  } else {
105
- const others = names.filter((n) => n !== currentName());
106
- if (others.length > 0) {
107
- switchTo(others[Math.floor(Math.random() * others.length)]);
108
- }
89
+ switchToNext();
109
90
  }
110
91
  });
111
92
 
@@ -117,6 +98,10 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
117
98
  renderers[currentName()].celebrateUpdate(newVersion);
118
99
  });
119
100
 
101
+ onVersion((version) => {
102
+ renderers[currentName()].showVersion(version);
103
+ });
104
+
120
105
  return (
121
106
  <box
122
107
  position="absolute"
@@ -125,7 +110,48 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
125
110
  alignItems="center"
126
111
  zIndex={100}
127
112
  flexDirection="column"
128
- {...mouseProps}
113
+ onMouseDown={(e: any) => {
114
+ const now = Date.now();
115
+ if (now - lastClickTime < 300) {
116
+ switchToNext();
117
+ lastClickTime = 0;
118
+ return;
119
+ }
120
+ lastClickTime = now;
121
+
122
+ if (e.modifiers?.alt) {
123
+ dragStartX = e.x;
124
+ dragStartY = e.y;
125
+ dragAnchorX = posX();
126
+ dragAnchorY = posY();
127
+ isDragging = true;
128
+ renderers[currentName()].setDragging(true);
129
+ e.preventDefault();
130
+ e.stopPropagation();
131
+ props.api.renderer.clearSelection();
132
+ }
133
+ }}
134
+ onMouseDrag={(e: any) => {
135
+ if (e.modifiers?.alt && isDragging) {
136
+ setPosX(dragAnchorX + (e.x - dragStartX));
137
+ setPosY(dragAnchorY + (e.y - dragStartY));
138
+ e.preventDefault();
139
+ e.stopPropagation();
140
+ props.api.renderer.clearSelection();
141
+ }
142
+ }}
143
+ onMouseUp={() => {
144
+ if (isDragging) {
145
+ isDragging = false;
146
+ renderers[currentName()].setDragging(false);
147
+ }
148
+ }}
149
+ onMouseDragEnd={() => {
150
+ if (isDragging) {
151
+ isDragging = false;
152
+ renderers[currentName()].setDragging(false);
153
+ }
154
+ }}
129
155
  >
130
156
  {renderers[currentName()]?.element() ?? null}
131
157
  </box>
@@ -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 = {
@@ -1,187 +0,0 @@
1
- /** @jsxImportSource @opentui/solid */
2
-
3
- import { createSignal, onCleanup, type Accessor } from "solid-js";
4
-
5
- type DragState = "normal" | "edge_hidden" | "returning";
6
- type HideSide = "left" | "right" | null;
7
-
8
- export interface MouseProps {
9
- onMouseDown: (e: any) => void;
10
- onMouseDrag: (e: any) => void;
11
- onMouseUp: (e: any) => void;
12
- onMouseDragEnd: (e: any) => void;
13
- }
14
-
15
- interface UseDraggableOpts {
16
- initialX: number;
17
- initialY: number;
18
- mascotWidth: number;
19
- mascotHeight: number;
20
- onSwitch: () => void;
21
- clearSelection: () => void;
22
- setDragging: (v: boolean) => void;
23
- onReturnComplete?: () => void;
24
- peekVisible?: number;
25
- enableEdge?: boolean;
26
- }
27
-
28
- function getTermSize(): { width: number; height: number } {
29
- const width = (typeof process !== "undefined" && process.stdout?.columns) || 80;
30
- const height = (typeof process !== "undefined" && process.stdout?.rows) || 24;
31
- return { width, height };
32
- }
33
-
34
- export function useDraggableMascot(opts: UseDraggableOpts): {
35
- posX: Accessor<number>;
36
- posY: Accessor<number>;
37
- mouseProps: MouseProps;
38
- returnToView: () => void;
39
- isHidden: () => boolean;
40
- } {
41
- const peek = opts.peekVisible ?? 2;
42
- const enableEdge = opts.enableEdge ?? true;
43
-
44
- const [posX, setPosX] = createSignal(opts.initialX);
45
- const [posY, setPosY] = createSignal(opts.initialY);
46
-
47
- let dragStartX = 0;
48
- let dragStartY = 0;
49
- let dragAnchorX = 0;
50
- let dragAnchorY = 0;
51
- let lastClickTime = 0;
52
- let isDragging = false;
53
- let state: DragState = "normal";
54
- let hideSide: HideSide = null;
55
- let peekTimer: ReturnType<typeof setInterval> | null = null;
56
- let returnTimer: ReturnType<typeof setInterval> | null = null;
57
-
58
- const stopPeek = () => {
59
- if (peekTimer) { clearInterval(peekTimer); peekTimer = null; }
60
- };
61
- const stopReturn = () => {
62
- if (returnTimer) { clearInterval(returnTimer); returnTimer = null; }
63
- };
64
-
65
- onCleanup(() => { stopPeek(); stopReturn(); });
66
-
67
- const clampX = (rawX: number): number => {
68
- if (!enableEdge) return rawX;
69
- const { width } = getTermSize();
70
- const minX = -(opts.mascotWidth - peek);
71
- const maxX = width - peek;
72
- return Math.max(minX, Math.min(rawX, maxX));
73
- };
74
-
75
- const clampY = (rawY: number): number => {
76
- if (!enableEdge) return rawY;
77
- const { height } = getTermSize();
78
- return Math.max(0, Math.min(rawY, height - opts.mascotHeight));
79
- };
80
-
81
- const startPeek = () => {
82
- stopPeek();
83
- state = "edge_hidden";
84
- let phase = false;
85
- peekTimer = setInterval(() => {
86
- phase = !phase;
87
- const stretch = phase ? 2 : 0;
88
- const { width } = getTermSize();
89
- if (hideSide === "left") {
90
- setPosX(-(opts.mascotWidth - peek) + stretch);
91
- } else if (hideSide === "right") {
92
- setPosX(width - peek - stretch);
93
- }
94
- }, 1200);
95
- };
96
-
97
- const returnToView = () => {
98
- if (state !== "edge_hidden") return;
99
- stopPeek();
100
- state = "returning";
101
- const { width } = getTermSize();
102
- const cur = posX();
103
- const targetX = hideSide === "left" ? 0 : Math.max(0, width - opts.mascotWidth);
104
- const step = targetX > cur ? 2 : -2;
105
-
106
- returnTimer = setInterval(() => {
107
- const now = posX();
108
- if (Math.abs(now - targetX) <= 2) {
109
- setPosX(targetX);
110
- stopReturn();
111
- state = "normal";
112
- hideSide = null;
113
- opts.onReturnComplete?.();
114
- return;
115
- }
116
- setPosX(now + step);
117
- }, 16);
118
- };
119
-
120
- const checkEdgeOnRelease = () => {
121
- if (!enableEdge) return;
122
- const { width } = getTermSize();
123
- const x = posX();
124
- if (x <= -(opts.mascotWidth - peek) + 1) {
125
- hideSide = "left";
126
- startPeek();
127
- } else if (x >= width - peek - 1) {
128
- hideSide = "right";
129
- startPeek();
130
- }
131
- };
132
-
133
- const mouseProps: MouseProps = {
134
- onMouseDown(e: any) {
135
- if (state === "edge_hidden") {
136
- returnToView();
137
- return;
138
- }
139
- if (state === "returning") return;
140
-
141
- const now = Date.now();
142
- if (now - lastClickTime < 300) {
143
- opts.onSwitch();
144
- lastClickTime = 0;
145
- return;
146
- }
147
- lastClickTime = now;
148
-
149
- if (e.modifiers?.alt) {
150
- dragStartX = e.x;
151
- dragStartY = e.y;
152
- dragAnchorX = posX();
153
- dragAnchorY = posY();
154
- isDragging = true;
155
- opts.setDragging(true);
156
- e.preventDefault();
157
- e.stopPropagation();
158
- opts.clearSelection();
159
- }
160
- },
161
- onMouseDrag(e: any) {
162
- if (e.modifiers?.alt && isDragging) {
163
- setPosX(clampX(dragAnchorX + (e.x - dragStartX)));
164
- setPosY(clampY(dragAnchorY + (e.y - dragStartY)));
165
- e.preventDefault();
166
- e.stopPropagation();
167
- opts.clearSelection();
168
- }
169
- },
170
- onMouseUp() {
171
- if (isDragging) {
172
- isDragging = false;
173
- opts.setDragging(false);
174
- checkEdgeOnRelease();
175
- }
176
- },
177
- onMouseDragEnd() {
178
- if (isDragging) {
179
- isDragging = false;
180
- opts.setDragging(false);
181
- checkEdgeOnRelease();
182
- }
183
- },
184
- };
185
-
186
- return { posX, posY, mouseProps, returnToView, isHidden: () => state === "edge_hidden" };
187
- }