@alasano/pi-mouse 0.0.1
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/README.md +26 -0
- package/assets/pi-mouse.gif +0 -0
- package/extensions/index.ts +171 -0
- package/package.json +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# pi-mouse
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
An ASCII mouse that lives above your editor in [pi](https://pi.dev). It follows your cursor as you type, scurrying left and right with an animated tail.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pi install npm:@alasano/pi-mouse
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Commands
|
|
14
|
+
|
|
15
|
+
| Command | Description |
|
|
16
|
+
| ----------------------- | ---------------------------- |
|
|
17
|
+
| `/cursor-mouse` | Toggle the mouse on or off |
|
|
18
|
+
| `/cursor-mouse on\|off` | Explicitly enable or disable |
|
|
19
|
+
|
|
20
|
+
## How it works
|
|
21
|
+
|
|
22
|
+
The mouse sits on a line above the editor. When you type, it debounces your cursor position and then walks toward it step by step. It faces the direction it's moving, and its tail wiggles as it goes. When idle, the tail still sways gently.
|
|
23
|
+
|
|
24
|
+
## Requirements
|
|
25
|
+
|
|
26
|
+
- Pi interactive mode (uses the custom editor API)
|
|
Binary file
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CustomEditor,
|
|
3
|
+
type ExtensionAPI,
|
|
4
|
+
type ExtensionContext,
|
|
5
|
+
} from '@mariozechner/pi-coding-agent';
|
|
6
|
+
|
|
7
|
+
const DEBOUNCE_MS = 420;
|
|
8
|
+
const TICK_MS = 65;
|
|
9
|
+
const CURSOR_ALIGNMENT_OFFSET = 1;
|
|
10
|
+
const MOVE_STEP = 2;
|
|
11
|
+
const IDLE_CYCLE_TICKS = 6;
|
|
12
|
+
|
|
13
|
+
const LEFT_FRAMES = ['<:3 )~~~', '<:3 )~^~', '<:3 )~~^'];
|
|
14
|
+
const RIGHT_FRAMES = ['~~~( Ɛ:>', '~^~( Ɛ:>', '^~~( Ɛ:>'];
|
|
15
|
+
|
|
16
|
+
type Facing = 'left' | 'right';
|
|
17
|
+
|
|
18
|
+
function clamp(value: number, min: number, max: number): number {
|
|
19
|
+
return Math.max(min, Math.min(value, max));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function overlayLine(width: number, sprite: string, x: number): string {
|
|
23
|
+
if (width <= 0) return '';
|
|
24
|
+
const line = Array.from({ length: width }, () => ' ');
|
|
25
|
+
for (let i = 0; i < sprite.length; i++) {
|
|
26
|
+
const col = x + i;
|
|
27
|
+
if (col < 0 || col >= width) continue;
|
|
28
|
+
line[col] = sprite[i]!;
|
|
29
|
+
}
|
|
30
|
+
return line.join('');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
class CursorMouseEditor extends CustomEditor {
|
|
34
|
+
private noseX = 2;
|
|
35
|
+
private targetNoseX = 2;
|
|
36
|
+
private facing: Facing = 'left';
|
|
37
|
+
|
|
38
|
+
private tailFrame = 0;
|
|
39
|
+
private idleTick = 0;
|
|
40
|
+
private lastRenderWidth = 80;
|
|
41
|
+
|
|
42
|
+
private debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
43
|
+
private animTimer: ReturnType<typeof setInterval> | null = null;
|
|
44
|
+
|
|
45
|
+
constructor(...args: ConstructorParameters<typeof CustomEditor>) {
|
|
46
|
+
super(...args);
|
|
47
|
+
|
|
48
|
+
this.animTimer = setInterval(() => {
|
|
49
|
+
let changed = false;
|
|
50
|
+
|
|
51
|
+
const delta = this.targetNoseX - this.noseX;
|
|
52
|
+
if (delta !== 0) {
|
|
53
|
+
this.syncFacingToTarget();
|
|
54
|
+
|
|
55
|
+
const step = Math.abs(delta) <= MOVE_STEP ? Math.abs(delta) : MOVE_STEP;
|
|
56
|
+
this.noseX += this.facing === 'right' ? step : -step;
|
|
57
|
+
this.tailFrame = (this.tailFrame + 1) % LEFT_FRAMES.length;
|
|
58
|
+
changed = true;
|
|
59
|
+
} else {
|
|
60
|
+
this.idleTick = (this.idleTick + 1) % IDLE_CYCLE_TICKS;
|
|
61
|
+
if (this.idleTick === 0) {
|
|
62
|
+
this.tailFrame = (this.tailFrame + 1) % LEFT_FRAMES.length;
|
|
63
|
+
changed = true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (changed) this.tui.requestRender();
|
|
68
|
+
}, TICK_MS);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private currentSprite(): string {
|
|
72
|
+
return this.facing === 'right' ? RIGHT_FRAMES[this.tailFrame]! : LEFT_FRAMES[this.tailFrame]!;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private contentWidth(): number {
|
|
76
|
+
return Math.max(1, this.lastRenderWidth - (this.getPaddingX() * 2 + 2));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private syncFacingToTarget(): void {
|
|
80
|
+
const delta = this.targetNoseX - this.noseX;
|
|
81
|
+
if (delta === 0) return;
|
|
82
|
+
this.facing = delta > 0 ? 'right' : 'left';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private updateTargetFromCursor(): void {
|
|
86
|
+
const cursor = this.getCursor();
|
|
87
|
+
const visualCol = cursor.col % this.contentWidth();
|
|
88
|
+
|
|
89
|
+
this.targetNoseX = clamp(
|
|
90
|
+
this.getPaddingX() + CURSOR_ALIGNMENT_OFFSET + visualCol,
|
|
91
|
+
0,
|
|
92
|
+
Math.max(0, this.lastRenderWidth - 1),
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// Flip as soon as debounce finishes, before movement starts.
|
|
96
|
+
this.syncFacingToTarget();
|
|
97
|
+
this.tui.requestRender();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private scheduleMoveToCursor(): void {
|
|
101
|
+
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
102
|
+
this.debounceTimer = setTimeout(() => this.updateTargetFromCursor(), DEBOUNCE_MS);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
override handleInput(data: string): void {
|
|
106
|
+
super.handleInput(data);
|
|
107
|
+
this.scheduleMoveToCursor();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
override render(width: number): string[] {
|
|
111
|
+
const editorLines = super.render(width);
|
|
112
|
+
if (width <= 0) return editorLines;
|
|
113
|
+
|
|
114
|
+
this.lastRenderWidth = width;
|
|
115
|
+
|
|
116
|
+
const sprite = this.currentSprite();
|
|
117
|
+
const noseAnchor = this.facing === 'left' ? 0 : sprite.length - 1;
|
|
118
|
+
|
|
119
|
+
this.noseX = clamp(this.noseX, 0, width - 1);
|
|
120
|
+
this.targetNoseX = clamp(this.targetNoseX, 0, width - 1);
|
|
121
|
+
|
|
122
|
+
const left = this.noseX - noseAnchor;
|
|
123
|
+
const mouseLine = overlayLine(width, sprite, left);
|
|
124
|
+
|
|
125
|
+
return [mouseLine, ...editorLines];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
public dispose(): void {
|
|
129
|
+
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
130
|
+
if (this.animTimer) clearInterval(this.animTimer);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export default function cursorMouseExtension(pi: ExtensionAPI) {
|
|
135
|
+
let enabled = true;
|
|
136
|
+
|
|
137
|
+
const applyEditor = (ctx: ExtensionContext) => {
|
|
138
|
+
if (!ctx.hasUI) return;
|
|
139
|
+
|
|
140
|
+
if (!enabled) {
|
|
141
|
+
ctx.ui.setEditorComponent(undefined);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
ctx.ui.setEditorComponent(
|
|
146
|
+
(tui, theme, keybindings) => new CursorMouseEditor(tui, theme, keybindings),
|
|
147
|
+
);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
pi.registerCommand('cursor-mouse', {
|
|
151
|
+
description: 'Enable or disable cursor mouse editor (usage: /cursor-mouse <on|off>)',
|
|
152
|
+
handler: async (args, ctx) => {
|
|
153
|
+
const mode = args?.trim().toLowerCase();
|
|
154
|
+
if (mode !== 'on' && mode !== 'off') {
|
|
155
|
+
ctx.ui.notify('Usage: /cursor-mouse <on|off>', 'warning');
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
enabled = mode === 'on';
|
|
160
|
+
|
|
161
|
+
applyEditor(ctx);
|
|
162
|
+
ctx.ui.notify(enabled ? 'Cursor mouse enabled' : 'Cursor mouse disabled', 'info');
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
pi.on('session_start', async (_event, ctx) => applyEditor(ctx));
|
|
167
|
+
pi.on('session_switch', async (_event, ctx) => applyEditor(ctx));
|
|
168
|
+
pi.on('session_shutdown', async (_event, ctx) => {
|
|
169
|
+
ctx.ui.setEditorComponent(undefined);
|
|
170
|
+
});
|
|
171
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@alasano/pi-mouse",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "ASCII mouse that follows your cursor above the editor in pi",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package"
|
|
7
|
+
],
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/alasano/house-of-pi",
|
|
12
|
+
"directory": "packages/pi-mouse"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"scripts": {
|
|
16
|
+
"typecheck": "tsc --noEmit"
|
|
17
|
+
},
|
|
18
|
+
"pi": {
|
|
19
|
+
"extensions": [
|
|
20
|
+
"./extensions"
|
|
21
|
+
],
|
|
22
|
+
"image": "https://raw.githubusercontent.com/alasano/house-of-pi/master/packages/pi-mouse/assets/pi-mouse.gif"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"extensions",
|
|
26
|
+
"assets",
|
|
27
|
+
"README.md"
|
|
28
|
+
],
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"@mariozechner/pi-coding-agent": "*",
|
|
31
|
+
"@mariozechner/pi-tui": "*"
|
|
32
|
+
}
|
|
33
|
+
}
|