@mingxy/opencode-mascot 0.2.5 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mingxy/opencode-mascot",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "OpenCode TUI mascot plugin framework - customizable ASCII mascots for your terminal",
5
5
  "author": "mingxy",
6
6
  "license": "MIT",
@@ -5,7 +5,6 @@ import type { JSX } from "@opentui/solid";
5
5
  import type { MascotPack } from "../core/types";
6
6
  import { createAnimatedRenderer } from "../core/ascii-renderer";
7
7
  import { onCelebrate } from "../core/celebration-bus";
8
- import { useDraggableMascot } from "./use-draggable-mascot";
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,17 +40,6 @@ 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
  });
@@ -55,7 +51,48 @@ export function HomeMascot(props: HomeMascotProps): JSX.Element {
55
51
  alignItems="center"
56
52
  zIndex={100}
57
53
  flexDirection="column"
58
- {...mouseProps}
54
+ onMouseDown={(e: any) => {
55
+ const now = Date.now();
56
+ if (now - lastClickTime < 300) {
57
+ switchToNext();
58
+ lastClickTime = 0;
59
+ return;
60
+ }
61
+ lastClickTime = now;
62
+
63
+ if (e.modifiers?.alt) {
64
+ dragStartX = e.x;
65
+ dragStartY = e.y;
66
+ dragAnchorX = posX();
67
+ dragAnchorY = posY();
68
+ isDragging = true;
69
+ renderers[currentName()].setDragging(true);
70
+ e.preventDefault();
71
+ e.stopPropagation();
72
+ props.api.renderer.clearSelection();
73
+ }
74
+ }}
75
+ onMouseDrag={(e: any) => {
76
+ if (e.modifiers?.alt && isDragging) {
77
+ setPosX(dragAnchorX + (e.x - dragStartX));
78
+ setPosY(dragAnchorY + (e.y - dragStartY));
79
+ e.preventDefault();
80
+ e.stopPropagation();
81
+ props.api.renderer.clearSelection();
82
+ }
83
+ }}
84
+ onMouseUp={() => {
85
+ if (isDragging) {
86
+ isDragging = false;
87
+ renderers[currentName()].setDragging(false);
88
+ }
89
+ }}
90
+ onMouseDragEnd={() => {
91
+ if (isDragging) {
92
+ isDragging = false;
93
+ renderers[currentName()].setDragging(false);
94
+ }
95
+ }}
59
96
  >
60
97
  {renderers[currentName()]?.element() ?? null}
61
98
  </box>
@@ -1,11 +1,10 @@
1
1
  /** @jsxImportSource @opentui/solid */
2
2
 
3
- import { createSignal } from "solid-js";
3
+ import { createSignal, onCleanup } 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
7
  import { onCelebrate } from "../core/celebration-bus";
8
- import { useDraggableMascot } from "./use-draggable-mascot";
9
8
 
10
9
  interface SidebarMascotProps {
11
10
  mascots: Record<string, MascotPack>;
@@ -30,6 +29,12 @@ const DEFAULT_STATE_MAP: Partial<Record<MascotState, string>> = {
30
29
 
31
30
  const MASCOT_WIDTH = 10;
32
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
+ }
33
38
 
34
39
  export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
35
40
  const names = Object.keys(props.mascots);
@@ -39,41 +44,101 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
39
44
  : names[Math.floor(Math.random() * names.length)];
40
45
 
41
46
  const [currentName, setCurrentName] = createSignal(initialName);
47
+ const [posX, setPosX] = createSignal(20);
48
+ const [posY, setPosY] = createSignal(2);
49
+ let dragStartX = 0;
50
+ let dragStartY = 0;
51
+ let dragAnchorX = 0;
52
+ let dragAnchorY = 0;
53
+ let lastClickTime = 0;
54
+ 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;
42
58
 
43
59
  const renderers: Record<string, ReturnType<typeof createAnimatedRenderer>> = {};
44
60
  for (const [name, pack] of Object.entries(props.mascots)) {
45
61
  renderers[name] = createAnimatedRenderer(pack);
46
62
  }
47
63
 
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
+
48
73
  const switchToNext = () => {
49
74
  const cur = currentName();
50
75
  const idx = names.indexOf(cur);
51
76
  setCurrentName(names[(idx + 1) % names.length]);
52
77
  };
53
78
 
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
- });
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
+ };
64
93
 
65
- const switchTo = (name: string) => {
66
- if (props.mascots[name] && name !== currentName()) {
67
- setCurrentName(name);
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();
68
123
  }
69
124
  };
70
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
+
71
138
  const setStateWithSwitch = (s: MascotState) => {
72
139
  const cur = currentName();
73
140
  renderers[cur].setState(s);
74
-
75
- const stateMap = DEFAULT_STATE_MAP;
76
- const target = stateMap[s];
141
+ const target = DEFAULT_STATE_MAP[s];
77
142
  if (target && target !== cur && props.mascots[target]) {
78
143
  setCurrentName(target);
79
144
  renderers[target].setState(s);
@@ -83,9 +148,8 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
83
148
  props.api.event.on("session.status", (data: unknown) => {
84
149
  const payload = data as { type?: string; properties?: { sessionID?: string; status?: { type?: string } } } | null;
85
150
  const statusType = payload?.properties?.status?.type;
86
-
87
151
  if (statusType === "busy" || statusType === "retry") {
88
- returnToView();
152
+ if (hideSide) returnToView();
89
153
  renderers[currentName()].setState("busy");
90
154
  } else {
91
155
  setStateWithSwitch("idle");
@@ -100,12 +164,10 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
100
164
  props.api.event.on("mascot.switch", (data: unknown) => {
101
165
  const target = data as { name?: string } | null;
102
166
  if (target?.name) {
103
- switchTo(target.name);
167
+ const name = target.name;
168
+ if (props.mascots[name] && name !== currentName()) setCurrentName(name);
104
169
  } else {
105
- const others = names.filter((n) => n !== currentName());
106
- if (others.length > 0) {
107
- switchTo(others[Math.floor(Math.random() * others.length)]);
108
- }
170
+ switchToNext();
109
171
  }
110
172
  });
111
173
 
@@ -125,7 +187,52 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
125
187
  alignItems="center"
126
188
  zIndex={100}
127
189
  flexDirection="column"
128
- {...mouseProps}
190
+ onMouseDown={(e: any) => {
191
+ if (hideSide) { returnToView(); return; }
192
+
193
+ const now = Date.now();
194
+ if (now - lastClickTime < 300) {
195
+ switchToNext();
196
+ lastClickTime = 0;
197
+ return;
198
+ }
199
+ lastClickTime = now;
200
+
201
+ if (e.modifiers?.alt) {
202
+ dragStartX = e.x;
203
+ dragStartY = e.y;
204
+ dragAnchorX = posX();
205
+ dragAnchorY = posY();
206
+ isDragging = true;
207
+ renderers[currentName()].setDragging(true);
208
+ e.preventDefault();
209
+ e.stopPropagation();
210
+ props.api.renderer.clearSelection();
211
+ }
212
+ }}
213
+ onMouseDrag={(e: any) => {
214
+ if (e.modifiers?.alt && isDragging) {
215
+ setPosX(clampX(dragAnchorX + (e.x - dragStartX)));
216
+ setPosY(clampY(dragAnchorY + (e.y - dragStartY)));
217
+ e.preventDefault();
218
+ e.stopPropagation();
219
+ props.api.renderer.clearSelection();
220
+ }
221
+ }}
222
+ onMouseUp={() => {
223
+ if (isDragging) {
224
+ isDragging = false;
225
+ renderers[currentName()].setDragging(false);
226
+ checkEdge();
227
+ }
228
+ }}
229
+ onMouseDragEnd={() => {
230
+ if (isDragging) {
231
+ isDragging = false;
232
+ renderers[currentName()].setDragging(false);
233
+ checkEdge();
234
+ }
235
+ }}
129
236
  >
130
237
  {renderers[currentName()]?.element() ?? null}
131
238
  </box>
@@ -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
- }