@graedenn/pi-loader 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Graeden
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,136 @@
1
+ # pi-loader ⠿
2
+
3
+ **Braille-dot working indicator for pi coding agent.**
4
+
5
+ Replaces the default spinner with rich 2-character braille animations. 54+ patterns including helixes, neural flickers, sweeps, snakes, and more — all in pure ASCII-compatible Unicode braille.
6
+
7
+ ![preview](https://img.shields.io/badge/patterns-54+-brightgreen)
8
+ ![license](https://img.shields.io/badge/license-MIT-blue)
9
+
10
+ ![pi-loader demo](demo/pi-loader-demo.gif)
11
+
12
+ > *Demo: preview gallery, pattern switching, speed & color controls*
13
+
14
+ ## Installation
15
+
16
+ ### Via pi (recommended)
17
+
18
+ ```bash
19
+ pi install npm:@graedenn/pi-loader
20
+ ```
21
+
22
+ Then `/reload` or restart pi.
23
+
24
+ ### Manual
25
+
26
+ ```bash
27
+ mkdir -p ~/.pi/agent/extensions/pi-loader
28
+ cp index.ts patterns.ts ~/.pi/agent/extensions/pi-loader/
29
+ /reload
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ | Command | Description |
35
+ |---|---|
36
+ | `/loader pattern <name>` | Switch animation pattern |
37
+ | `/loader color <color>` | Set color (name, `#hex`, or `0-255` ANSI) |
38
+ | `/loader speed <n>` | Set speed multiplier (0.25–10.0) |
39
+ | `/loader preview` | Interactive gallery with live preview |
40
+ | `/loader reset` | Restore defaults |
41
+ | `/loader off` / `on` | Disable / re-enable |
42
+
43
+ ### Preview mode
44
+
45
+ `/loader preview` opens an interactive picker:
46
+
47
+ | Key | Action |
48
+ |---|---|
49
+ | `←` `→` | Switch pattern |
50
+ | `↑` `↓` | Adjust speed |
51
+ | `[` `]` | Cycle color |
52
+ | `Enter` | Select and apply |
53
+ | `Esc` | Close |
54
+
55
+ Named colors: `accent`, `muted`, `dim`, `text`, `success`, `warning`, `error`, `border`, `borderAccent`
56
+
57
+ ## All Patterns
58
+
59
+ <details>
60
+ <summary>Click to expand (54+ patterns)</summary>
61
+
62
+ | Key | Name | Frames |
63
+ |---|---|---|
64
+ | `default` | Default | 10 |
65
+ | `single-dots` | Single Dots | 8 |
66
+ | `single-bounce` | Single Bounce | 14 |
67
+ | `single-fill` | Single Fill | 14 |
68
+ | `single-sweep` | Single Sweep | 8 |
69
+ | `half-helix` | Half Helix | 16 |
70
+ | `helix-core` | Helix Core | 12 |
71
+ | `helix-glow` | Helix Glow | 20 |
72
+ | `thought-helix` | Thought Helix | 19 |
73
+ | `pulse-ladder` | Pulse Ladder | 16 |
74
+ | `core-spiral` | Core Spiral | 16 |
75
+ | `twin-orbit` | Twin Orbit | 16 |
76
+ | `infinity-run` | Infinity Run | 20 |
77
+ | `radar-arc` | Radar Arc | 20 |
78
+ | `scan` | Scan | 6 |
79
+ | `sweep` | Sweep | 13 |
80
+ | `agent-sweep` | Agent Sweep | 19 |
81
+ | `sound-bars` | Sound Bars | 16 |
82
+ | `perimeter-spin-light` | Perimeter Spin Light | 12 |
83
+ | `perimeter-spin` | Perimeter Spin | 12 |
84
+ | `perimeter-spin-bold` | Perimeter Spin Bold | 12 |
85
+ | `shuffle` | Shuffle | 7 |
86
+ | `hangtime` | Hangtime | 57 |
87
+ | `line-spin` | Line Spin | 8 |
88
+ | `ray-spin` | Ray Spin | 8 |
89
+ | `rotating-x` | Rotating X | 4 |
90
+ | `texture-flip` | Texture Flip | 2 |
91
+ | `neural-flicker` | Neural Flicker | 15 |
92
+ | `neural-flicker-chaos` | Neural Flicker Chaos | 43 |
93
+ | `neural-flicker-drift` | Neural Flicker Drift | 41 |
94
+ | `neural-flicker-thought` | Neural Flicker Thought | 41 |
95
+ | `neural-scatter` | Neural Scatter | 16 |
96
+ | `neural-spike` | Neural Spike | 14 |
97
+ | `neural-random-walk` | Neural Random Walk | 16 |
98
+ | `neural-cross-spark` | Neural Cross Spark | 14 |
99
+ | `neural-offset` | Neural Offset | 14 |
100
+ | `neural-braid` | Neural Braid | 18 |
101
+ | `thinking-pulse` | Thinking Pulse | 21 |
102
+ | `soft-thinking` | Soft Thinking | 16 |
103
+ | `deep-thought` | Deep Thought | 23 |
104
+ | `signal-search` | Signal Search | 15 |
105
+ | `binary-thought` | Binary Thought | 13 |
106
+ | `center-spark` | Center Spark | 12 |
107
+ | `synapse-wave` | Synapse Wave | 20 |
108
+ | `inner-current` | Inner Current | 13 |
109
+ | `bouncing-block` | Bouncing Block | 12 |
110
+ | `face` | Smiley Face | 6 |
111
+ | `snake-crawl` | Snake Crawl | 30 |
112
+ | `snake-loop` | Snake Loop | 24 |
113
+ | `circle` | Circle | 6 |
114
+ | `square` | Square | 2 |
115
+ | `twinkle` | Twinkle | 2 |
116
+ | `corners` | Corners | 4 |
117
+ | `growth` | Growth | 10 |
118
+
119
+ </details>
120
+
121
+ ## Adding Patterns
122
+
123
+ Add new patterns in `patterns.ts` as `type: "raw"` entries. Each frame is 1–2 braille characters. Use `braille-chars.txt` as a reference for encoding.
124
+
125
+ ```ts
126
+ "my-pattern": {
127
+ type: "raw",
128
+ name: "My Pattern",
129
+ frames: ["⠋⠙", "⠹⠸", "⠼⠴"],
130
+ defaultSpeed: 1.0
131
+ }
132
+ ```
133
+
134
+ ## License
135
+
136
+ MIT
@@ -0,0 +1,75 @@
1
+ BRAILLE CHARACTER REFERENCE for pi-loader (4×4 braille dot loader)
2
+ =========================================================
3
+
4
+ Each loader frame is 2 braille characters, side by side.
5
+ The left char encodes columns 0-1, the right char encodes columns 2-3.
6
+
7
+ DOT NUMBERING (within one 2×4 cell):
8
+ ┌───┬───┐
9
+ │ 0 │ 3 │
10
+ ├───┼───┤
11
+ │ 1 │ 4 │
12
+ ├───┼───┤
13
+ │ 2 │ 5 │
14
+ ├───┼───┤
15
+ │ 6 │ 7 │
16
+ └───┴───┘
17
+
18
+ To compute a character: String.fromCodePoint(0x2800 + sum_of_dot_numbers)
19
+
20
+ Rows 0-3 = dots 0,1,2,6 (left column) and 3,4,5,7 (right column)
21
+ in the final 2-char frame.
22
+
23
+ ─────────────────────────────────────────────────────────
24
+
25
+ NO DOTS 6 or 7 (U+2800 - U+283F):
26
+
27
+ ⠀ ⠁ ⠂ ⠃ ⠄ ⠅ ⠆ ⠇ ⠈ ⠉ ⠊ ⠋ ⠌ ⠍ ⠎ ⠏
28
+ ⠐ ⠑ ⠒ ⠓ ⠔ ⠕ ⠖ ⠗ ⠘ ⠙ ⠚ ⠛ ⠜ ⠝ ⠞ ⠟
29
+ ⠠ ⠡ ⠢ ⠣ ⠤ ⠥ ⠦ ⠧ ⠨ ⠩ ⠪ ⠫ ⠬ ⠭ ⠮ ⠯
30
+ ⠰ ⠱ ⠲ ⠳ ⠴ ⠵ ⠶ ⠷ ⠸ ⠹ ⠺ ⠻ ⠼ ⠽ ⠾ ⠿
31
+
32
+ DOT 6 ONLY (U+2840 - U+287F):
33
+
34
+ ⡀ ⡁ ⡂ ⡃ ⡄ ⡅ ⡆ ⡇ ⡈ ⡉ ⡊ ⡋ ⡌ ⡍ ⡎ ⡏
35
+ ⡐ ⡑ ⡒ ⡓ ⡔ ⡕ ⡖ ⡗ ⡘ ⡙ ⡚ ⡛ ⡜ ⡝ ⡞ ⡟
36
+ ⡠ ⡡ ⡢ ⡣ ⡤ ⡥ ⡦ ⡧ ⡨ ⡩ ⡪ ⡫ ⡬ ⡭ ⡮ ⡯
37
+ ⡰ ⡱ ⡲ ⡳ ⡴ ⡵ ⡶ ⡷ ⡸ ⡹ ⡺ ⡻ ⡼ ⡽ ⡾ ⡿
38
+
39
+ DOT 7 ONLY (U+2880 - U+28BF):
40
+
41
+ ⢀ ⢁ ⢂ ⢃ ⢄ ⢅ ⢆ ⢇ ⢈ ⢉ ⢊ ⢋ ⢌ ⢍ ⢎ ⢏
42
+ ⢐ ⢑ ⢒ ⢓ ⢔ ⢕ ⢖ ⢗ ⢘ ⢙ ⢚ ⢛ ⢜ ⢝ ⢞ ⢟
43
+ ⢠ ⢡ ⢢ ⢣ ⢤ ⢥ ⢦ ⢧ ⢨ ⢩ ⢪ ⢫ ⢬ ⢭ ⢮ ⢯
44
+ ⢰ ⢱ ⢲ ⢳ ⢴ ⢵ ⢶ ⢷ ⢸ ⢹ ⢺ ⢻ ⢼ ⢽ ⢾ ⢿
45
+
46
+ DOTS 6+7 (U+28C0 - U+28FF):
47
+
48
+ ⣀ ⣁ ⣂ ⣃ ⣄ ⣅ ⣆ ⣇ ⣈ ⣉ ⣊ ⣋ ⣌ ⣍ ⣎ ⣏
49
+ ⣐ ⣑ ⣒ ⣓ ⣔ ⣕ ⣖ ⣗ ⣘ ⣙ ⣚ ⣛ ⣜ ⣝ ⣞ ⣟
50
+ ⣠ ⣡ ⣢ ⣣ ⣤ ⣥ ⣦ ⣧ ⣨ ⣩ ⣪ ⣫ ⣬ ⣭ ⣮ ⣯
51
+ ⣰ ⣱ ⣲ ⣳ ⣴ ⣵ ⣶ ⣷ ⣸ ⣹ ⣺ ⣻ ⣼ ⣽ ⣾ ⣿
52
+
53
+ ─────────────────────────────────────────────────────────
54
+
55
+ HOW TO BUILD A FRAME:
56
+
57
+ Your loader shows 2 braille chars = your 4×4 grid.
58
+
59
+ Left char = columns 0-1 of your grid
60
+ Right char = columns 2-3 of your grid
61
+
62
+ For the left char, ON dots are:
63
+ dot 0 = row0 col0 dot 3 = row0 col1
64
+ dot 1 = row1 col0 dot 4 = row1 col1
65
+ dot 2 = row2 col0 dot 5 = row2 col1
66
+ dot 6 = row3 col0 dot 7 = row3 col1
67
+
68
+ For the right char, same but for columns 2-3.
69
+
70
+ Find the char above that matches your ON dots, put them together:
71
+ frame = leftChar + rightChar (e.g. "⡱⢎")
72
+
73
+ Then register as a raw pattern:
74
+ "my-pattern": { type: "raw", name: "My Pattern",
75
+ frames: ["⡱⢎", "⢎⡱", ...], defaultSpeed: 1.0 },
Binary file
Binary file
package/index.ts ADDED
@@ -0,0 +1,417 @@
1
+ /**
2
+ * pi-loader — Braille-dot working indicator for pi coding agent
3
+ *
4
+ * Compact braille-dot working indicator inspired by dotmatrix.zzzzshawn.cloud.
5
+ * Replaces the default spinner with 2-character braille animation.
6
+ * 54+ configurable patterns.
7
+ *
8
+ * Commands (with autocomplete):
9
+ * /loader pattern <name> - Switch pattern
10
+ * /loader color <color> - Set color (name, #hex, or 0-255 ANSI)
11
+ * /loader preview [name] - Preview animation (Esc close, ←→ pattern, ↑↓ color)
12
+ * /loader speed <n> - Set speed multiplier (0.25-10.0)
13
+ * /loader reset - Reset to defaults
14
+ */
15
+
16
+ import type { AutocompleteItem } from "@earendil-works/pi-tui";
17
+ import { matchesKey } from "@earendil-works/pi-tui";
18
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
19
+
20
+ const fs = require("fs");
21
+ const path = require("path");
22
+
23
+ const cycle = <T>(arr: readonly T[], idx: number, dir: -1 | 1): T =>
24
+ arr[(idx + dir + arr.length) % arr.length]!;
25
+
26
+ const COLORS = ["accent", "muted", "dim", "text", "success", "warning", "error", "border", "borderAccent"] as const;
27
+ const PREVIEW_COLORS = [...COLORS, "16","39","48","117","123","183","193","202","213","214","228","244","255"] as string[];
28
+
29
+ function intervalMs(frameCount: number, defaultSpeed: number, speedMultiplier: number): number {
30
+ return Math.max(80, Math.min(300, 1600 / frameCount)) / (defaultSpeed * speedMultiplier);
31
+ }
32
+
33
+ function colorize(text: string, color: string, theme: { fg: (c: string, t: string) => string }): string {
34
+ if ((COLORS as readonly string[]).includes(color)) return theme.fg(color, text);
35
+ if (/^#[0-9a-fA-F]{6}$/.test(color)) {
36
+ const r = parseInt(color.slice(1, 3), 16);
37
+ const g = parseInt(color.slice(3, 5), 16);
38
+ const b = parseInt(color.slice(5, 7), 16);
39
+ return `\x1b[38;2;${r};${g};${b}m${text}\x1b[0m`;
40
+ }
41
+ if (/^\d{1,3}$/.test(color)) {
42
+ const n = parseInt(color, 10);
43
+ if (n >= 0 && n <= 255) return `\x1b[38;5;${n}m${text}\x1b[0m`;
44
+ }
45
+ return text;
46
+ }
47
+
48
+ // ─── Patterns ──────────────────────────────────────────────────────────
49
+
50
+ import { PATTERNS, PATTERN_KEYS } from "./patterns";
51
+
52
+ // ─── Preview component ────────────────────────────────────────────────
53
+
54
+ class LoaderPreviewComponent {
55
+ private animInterval: ReturnType<typeof setInterval> | null = null;
56
+ private frameIndex = 0;
57
+ private frames: string[] = [];
58
+ private theme: { fg: (c: string, t: string) => string };
59
+ private tui: { requestRender: () => void };
60
+ private done: () => void;
61
+ private patternIndex: number;
62
+ private patternKeys: string[];
63
+ private color: string;
64
+ private colorValues: string[];
65
+ private speed: number;
66
+ private onSelect: (pattern: string, color: string, speed: number) => void;
67
+
68
+ constructor(
69
+ tui: { requestRender: () => void },
70
+ theme: { fg: (c: string, t: string) => string },
71
+ done: () => void,
72
+ startIndex: number,
73
+ patternKeys: string[],
74
+ color: string,
75
+ colorValues: string[],
76
+ startSpeed: number,
77
+ onSelect: (pattern: string, color: string, speed: number) => void,
78
+ ) {
79
+ this.tui = tui;
80
+ this.theme = theme;
81
+ this.done = done;
82
+ this.patternIndex = startIndex;
83
+ this.patternKeys = patternKeys;
84
+ this.color = color;
85
+ this.colorValues = colorValues;
86
+ this.speed = startSpeed;
87
+ this.onSelect = onSelect;
88
+ this.buildFrames();
89
+ this.startAnimation();
90
+ }
91
+
92
+ private get patternKey(): string {
93
+ return this.patternKeys[this.patternIndex]!;
94
+ }
95
+
96
+ private buildFrames(): void {
97
+ const key = this.patternKey;
98
+ const entry = PATTERNS[key];
99
+ if (!entry) {
100
+ console.error("[loader] unknown pattern:", key, "keys:", Object.keys(PATTERNS).slice(0,5));
101
+ this.frames = ["⠿"];
102
+ return;
103
+ }
104
+ this.frames = entry.frames.map((f) => colorize(f, this.color, this.theme));
105
+ this.frameIndex = 0;
106
+ }
107
+
108
+ private startAnimation(): void {
109
+ this.stopAnimation();
110
+ const entry = PATTERNS[this.patternKey];
111
+ if (!entry) return;
112
+ const ms = intervalMs(this.frames.length, entry.defaultSpeed, this.speed);
113
+ this.animInterval = setInterval(() => {
114
+ this.frameIndex = (this.frameIndex + 1) % this.frames.length;
115
+ this.tui.requestRender();
116
+ }, Math.max(16, ms));
117
+ }
118
+
119
+ private stopAnimation(): void {
120
+ if (this.animInterval) { clearInterval(this.animInterval); this.animInterval = null; }
121
+ }
122
+
123
+ private close(): void {
124
+ this.stopAnimation();
125
+ this.done();
126
+ }
127
+
128
+ private switchPattern(dir: -1 | 1): void {
129
+ this.patternIndex = (this.patternIndex + dir + this.patternKeys.length) % this.patternKeys.length;
130
+ this.buildFrames();
131
+ this.startAnimation();
132
+ this.tui.requestRender();
133
+ }
134
+
135
+ private switchColor(dir: -1 | 1): void {
136
+ this.color = cycle(this.colorValues, this.colorValues.indexOf(this.color), dir);
137
+ this.buildFrames();
138
+ this.tui.requestRender();
139
+ }
140
+
141
+ private switchSpeed(dir: -1 | 1): void {
142
+ this.speed = Math.max(0.25, Math.min(10.0, this.speed + dir * 0.25));
143
+ this.startAnimation();
144
+ this.tui.requestRender();
145
+ }
146
+
147
+ render(width: number): string[] {
148
+ const frame = this.frames[this.frameIndex] ?? this.frames[0] ?? "⠿";
149
+ const entry = PATTERNS[this.patternKey];
150
+ const total = this.patternKeys.length;
151
+ const a = (s: string) => this.theme.fg("accent", s);
152
+ const d = (s: string) => this.theme.fg("dim", s);
153
+ const padded = (s: string) => s + " ".repeat(Math.max(0, width - s.length - 4));
154
+
155
+ const lines: string[] = [];
156
+ const hr = a("".padEnd(width, "─"));
157
+ lines.push(hr);
158
+ lines.push(" ".repeat(width));
159
+ lines.push(padded(" " + a("Loader Gallery")));
160
+ lines.push(" ".repeat(width));
161
+ lines.push(padded(" " + frame));
162
+ lines.push(" ".repeat(width));
163
+ lines.push(padded(" " + (entry?.name ?? this.patternKey)));
164
+ lines.push(padded(" " + d(`${this.patternIndex + 1} / ${total} · ${this.color} · ${this.speed.toFixed(1)}x`)));
165
+ lines.push(" ".repeat(width));
166
+ lines.push(padded(" " + d("[Enter] select [Esc] close")));
167
+ lines.push(padded(" " + d("[←→] pattern [↑↓] speed [[] ] color")));
168
+ lines.push(" ".repeat(width));
169
+ lines.push(hr);
170
+
171
+ return lines;
172
+ }
173
+
174
+ handleInput(data: string): void {
175
+ if (matchesKey(data, "escape")) { this.close(); return; }
176
+ if (matchesKey(data, "enter")) {
177
+ this.onSelect(this.patternKey, this.color, this.speed);
178
+ this.close();
179
+ return;
180
+ }
181
+ if (matchesKey(data, "left")) { this.switchPattern(-1); return; }
182
+ if (matchesKey(data, "right")) { this.switchPattern(1); return; }
183
+ if (matchesKey(data, "up")) { this.switchSpeed(1); return; }
184
+ if (matchesKey(data, "down")) { this.switchSpeed(-1); return; }
185
+ if (matchesKey(data, "[") || matchesKey(data, "{")) { this.switchColor(-1); return; }
186
+ if (matchesKey(data, "]") || matchesKey(data, "}")) { this.switchColor(1); return; }
187
+ }
188
+
189
+ invalidate(): void {
190
+ this.buildFrames();
191
+ }
192
+ }
193
+
194
+ // ─── Extension ─────────────────────────────────────────────────────────
195
+
196
+ interface Config {
197
+ pattern: string;
198
+ color: string;
199
+ speed: number;
200
+ }
201
+
202
+ const DEFAULTS: Config = {
203
+ pattern: "default",
204
+ color: "accent",
205
+ speed: 1.0,
206
+ };
207
+
208
+ const CONFIG_PATH = (process.env.HOME || process.env.USERPROFILE || "/tmp") +
209
+ "/.pi/agent/extensions/pi-loader/config.json";
210
+
211
+ function loadConfig(): Config {
212
+ try {
213
+ const raw = fs.readFileSync(CONFIG_PATH, "utf-8");
214
+ return { ...DEFAULTS, ...JSON.parse(raw) };
215
+ } catch (e) {
216
+ const err = e as { code?: string };
217
+ if (err.code !== "ENOENT") console.error("[loader] loadConfig error:", e);
218
+ return { ...DEFAULTS };
219
+ }
220
+ }
221
+
222
+ function saveConfig(cfg: Config): void {
223
+ try {
224
+ const dir = path.dirname(CONFIG_PATH);
225
+ fs.mkdirSync(dir, { recursive: true });
226
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2), "utf-8");
227
+ } catch (e) {
228
+ console.error("[loader] saveConfig failed:", e);
229
+ }
230
+ }
231
+
232
+ export default function (pi: ExtensionAPI) {
233
+ let config: Config = loadConfig();
234
+ let disabled = false;
235
+
236
+ const apply = (ctx: ExtensionContext) => {
237
+ if (disabled) { ctx.ui.setWorkingIndicator(); return; }
238
+ const pattern = PATTERNS[config.pattern];
239
+ if (!pattern || !pattern.frames.length) return;
240
+ ctx.ui.setWorkingIndicator({
241
+ frames: pattern.frames.map((f) => colorize(f, config.color, ctx.ui.theme)),
242
+ intervalMs: intervalMs(pattern.frames.length, pattern.defaultSpeed, config.speed),
243
+ });
244
+ };
245
+
246
+ pi.on("session_start", async (_event, ctx) => {
247
+ config = loadConfig();
248
+ apply(ctx);
249
+ });
250
+
251
+ pi.on("before_agent_start", async (_event, ctx) => {
252
+ apply(ctx);
253
+ });
254
+
255
+ pi.registerCommand("loader", {
256
+ description: "Configure pi-loader (pattern, color, speed)",
257
+ getArgumentCompletions: (prefix: string): AutocompleteItem[] | null => {
258
+ const subs: AutocompleteItem[] = [
259
+ { value: "preview ", label: "preview — pick pattern/color/speed" },
260
+ { value: "pattern ", label: "pattern — switch animation" },
261
+ { value: "color ", label: "color — set color (name/#hex/0-255)" },
262
+ { value: "speed ", label: "speed — set speed" },
263
+ { value: "off", label: "off — restore default spinner" },
264
+ { value: "on", label: "on — re-enable loader" },
265
+ { value: "reset", label: "reset — defaults" },
266
+ ];
267
+ if (!prefix) return subs;
268
+
269
+ const parts = prefix.split(/\s+/);
270
+ if (parts.length >= 2) {
271
+ const sub = parts[0]!;
272
+ const arg = parts.slice(1).join(" ");
273
+ if (sub === "pattern") {
274
+ return PATTERN_KEYS.filter((k) => k.startsWith(arg)).map((k) => ({
275
+ value: `${sub} ${k}`,
276
+ label: `${k} — ${PATTERNS[k]!.name}`,
277
+ }));
278
+ }
279
+ if (sub === "color") {
280
+ return (COLORS as readonly string[])
281
+ .filter((c) => c.startsWith(arg))
282
+ .map((c) => ({ value: `${sub} ${c}`, label: c }));
283
+ }
284
+ return null;
285
+ }
286
+ const filtered = subs.filter((s) => s.value.startsWith(prefix));
287
+ return filtered.length > 0 ? filtered : null;
288
+ },
289
+ handler: async (args, ctx) => {
290
+ const parts = args.trim().split(/\s+/);
291
+ const sub = parts[0]?.toLowerCase();
292
+ const value = parts.slice(1).join(" ");
293
+
294
+ switch (sub) {
295
+ case "pattern": {
296
+ const key = value.toLowerCase();
297
+ if (!key || !PATTERNS[key]) {
298
+ ctx.ui.notify(
299
+ PATTERN_KEYS.map((k) => ` ${k} — ${PATTERNS[k]!.name}`).join("\n"),
300
+ "info",
301
+ );
302
+ return;
303
+ }
304
+ config.pattern = key;
305
+ saveConfig(config);
306
+ apply(ctx);
307
+ ctx.ui.notify(`Pattern → ${PATTERNS[key]!.name}`, "info");
308
+ return;
309
+ }
310
+ case "color": {
311
+ if (!value) {
312
+ ctx.ui.notify(
313
+ `/loader color <name|#hex|0-255> (${config.color})\n` +
314
+ `Named: ${COLORS.join(", ")}\n` +
315
+ `Ex: accent, 196, #ff6600`,
316
+ "info",
317
+ );
318
+ return;
319
+ }
320
+ if ((COLORS as readonly string[]).includes(value)) {
321
+ config.color = value;
322
+ saveConfig(config);
323
+ apply(ctx);
324
+ return;
325
+ }
326
+ if (/^#[0-9a-fA-F]{6}$/.test(value)) {
327
+ config.color = value;
328
+ saveConfig(config);
329
+ apply(ctx);
330
+ return;
331
+ }
332
+ if (/^\d{1,3}$/.test(value)) {
333
+ const n = parseInt(value, 10);
334
+ if (n >= 0 && n <= 255) {
335
+ config.color = value;
336
+ saveConfig(config);
337
+ apply(ctx);
338
+ return;
339
+ }
340
+ }
341
+ ctx.ui.notify(
342
+ `/loader color <name|#hex|0-255> (${config.color})\n` +
343
+ `Named: ${COLORS.join(", ")}\n` +
344
+ `Ex: accent, 196, #ff6600`,
345
+ "info",
346
+ );
347
+ return;
348
+ }
349
+ case "speed": {
350
+ const n = parseFloat(value);
351
+ if (isNaN(n) || n < 0.25 || n > 10.0) {
352
+ ctx.ui.notify(`/loader speed <0.25-10.0> (${config.speed}x)`, "info");
353
+ return;
354
+ }
355
+ config.speed = n;
356
+ saveConfig(config);
357
+ apply(ctx);
358
+ return;
359
+ }
360
+ case "off": {
361
+ disabled = true;
362
+ ctx.ui.setWorkingIndicator();
363
+ ctx.ui.notify("Loader off", "info");
364
+ return;
365
+ }
366
+ case "on": {
367
+ disabled = false;
368
+ apply(ctx);
369
+ ctx.ui.notify("Loader on", "info");
370
+ return;
371
+ }
372
+ case "preview": {
373
+ const startIdx = value
374
+ ? Math.max(0, PATTERN_KEYS.indexOf(value.toLowerCase()))
375
+ : Math.max(0, PATTERN_KEYS.indexOf(config.pattern));
376
+ const previewColors = [...PREVIEW_COLORS];
377
+ if (!previewColors.includes(config.color)) previewColors.push(config.color);
378
+ ctx.ui.custom<string | null>(
379
+ (tui, theme, _kb, done) => new LoaderPreviewComponent(
380
+ tui, theme, done,
381
+ startIdx,
382
+ PATTERN_KEYS,
383
+ config.color,
384
+ previewColors,
385
+ config.speed,
386
+ (pattern, color, speed) => {
387
+ config.pattern = pattern;
388
+ config.color = color;
389
+ config.speed = speed;
390
+ saveConfig(config);
391
+ apply(ctx);
392
+ ctx.ui.notify(`Selected: ${PATTERNS[pattern]?.name ?? pattern} · ${color} · ${speed}x`, "info");
393
+ },
394
+ ),
395
+ { overlay: true },
396
+ );
397
+ return;
398
+ }
399
+ case "reset": {
400
+ config = { ...DEFAULTS };
401
+ saveConfig(config);
402
+ apply(ctx);
403
+ ctx.ui.notify("Reset → Default, accent, 1x", "info");
404
+ return;
405
+ }
406
+ default: {
407
+ const p = PATTERNS[config.pattern];
408
+ ctx.ui.notify(
409
+ `/loader [pattern|color|preview|speed|reset]\n` +
410
+ `${p?.name ?? config.pattern} · ${config.color} · ${config.speed}x`,
411
+ "info",
412
+ );
413
+ }
414
+ }
415
+ },
416
+ });
417
+ }
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@graedenn/pi-loader",
3
+ "version": "1.0.0",
4
+ "description": "Braille-dot working indicator for pi coding agent — replaces the default spinner with 54+ configurable animations",
5
+ "keywords": ["pi-package", "pi", "coding-agent", "extension", "loader", "spinner", "braille", "dot-matrix"],
6
+ "license": "MIT",
7
+ "author": "Graeden",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/graedenn/pi-loader.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/graedenn/pi-loader/issues"
14
+ },
15
+ "homepage": "https://github.com/graedenn/pi-loader#readme",
16
+ "peerDependencies": {
17
+ "@earendil-works/pi-coding-agent": "*",
18
+ "@earendil-works/pi-tui": "*"
19
+ },
20
+ "pi": {
21
+ "extensions": ["./index.ts"],
22
+ "video": "https://github.com/graedenn/pi-loader/raw/main/demo/pi-loader-demo.mp4"
23
+ }
24
+ }
package/patterns.ts ADDED
@@ -0,0 +1,112 @@
1
+ export const PATTERNS = {
2
+ "default": { type: "raw", name: "Default",
3
+ frames: ["⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏"], defaultSpeed: 1.0 },
4
+ "single-dots": { type: "raw", name: "Single Dots",
5
+ frames: ["⠁","⠂","⠄","⡀","⢀","⠠","⠐","⠈"], defaultSpeed: 1.0 },
6
+ "single-bounce": { type: "raw", name: "Single Bounce",
7
+ frames: ["⠁","⠂","⠄","⡀","⢀","⠠","⠐","⠈","⠐","⠠","⢀","⡀","⠄","⠂"], defaultSpeed: 1.0 },
8
+ "single-fill": { type: "raw", name: "Single Fill",
9
+ frames: ["⠁","⠃","⠇","⡇","⡏","⡟","⡿","⣿","⡿","⡟","⡏","⡇","⠇","⠃"], defaultSpeed: 1.0 },
10
+ "single-sweep": { type: "raw", name: "Single Sweep",
11
+ frames: ["⠁","⠉","⠘","⠰","⢠","⣀","⡄","⠆"], defaultSpeed: 1.0 },
12
+ "half-helix": { type: "raw", name: "Half Helix",
13
+ frames: ["⣨⠿","⣬⠟","⣤⠟","⣴⠛","⣴⡛","⢶⣋","⢾⣉","⠾⣍","⠿⣅","⠻⣥","⠻⣦","⢛⣦","⢙⣶","⣙⡶","⣉⡷","⣩⠷"], defaultSpeed: 1.0 },
14
+ "helix-core": { type: "raw", name: "Helix Core",
15
+ frames: ["⢠⡫","⡠⡻","⣔⠝","⢴⠋","⣮⠊","⡷⡁","⢕⡅","⢟⢄","⠫⣢","⠙⡦","⠑⣵","⢈⢾"], defaultSpeed: 1.0 },
16
+ "helix-glow": { type: "raw", name: "Helix Glow",
17
+ frames: ["⠪⡱","⢣⢱","⢃⢕","⡑⢜","⡱⢸","⡡⢪","⡌⢪","⡜⡪","⡜⡱","⢔⠱","⢆⠕","⡎⡜","⡪⡘","⡣⢊","⡇⢎","⡕⢌","⡕⢡","⢕⢣","⢎⢣","⠎⡢"], defaultSpeed: 1.0 },
18
+ "thought-helix": { type: "raw", name: "Thought Helix",
19
+ frames: ["⢸⡇","⢰⡇","⡰⡃","⡸⢇","⢸⢇","⢜⢡","⢜⢣","⢜⡣","⠸⡢","⠪⡰","⢪⡕","⢘⡕","⢑⡕","⢑⡜","⢱⡎","⢨⡎","⢨⠎","⢸⡎","⢸⡇"], defaultSpeed: 1.0 },
20
+ "pulse-ladder": { type: "raw", name: "Pulse Ladder",
21
+ frames: ["⡀⢠","⡄⢀","⡆⠀","⠇⠀","⠋⠀","⠙⠀","⠸⠀","⢰⠀","⢠⡀","⢀⡄","⠀⡆","⠀⠇","⠀⠋","⠀⠙","⠀⠸","⠀⢰"], defaultSpeed: 1.3 },
22
+ "core-spiral": { type: "raw", name: "Core Spiral",
23
+ frames: ["⠡⠄","⠩⠀","⠉⠁","⠈⠉","⠀⠙","⠀⠸","⠀⢰","⠀⣠","⢀⣀","⣀⡀","⣄⠀","⡆⠀","⠖⠀","⠒⠂","⠐⠆","⠠⠆"], defaultSpeed: 1.2 },
24
+ "twin-orbit": { type: "raw", name: "Twin Orbit",
25
+ frames: ["⠷⠀","⠧⠄","⠫⠄","⠩⠅","⠨⠍","⠈⠏","⠀⠟","⠀⠾","⠀⢶","⠐⢲","⠐⣢","⢐⣂","⣐⡂","⣰⡀","⣴⠀","⡶⠀"], defaultSpeed: 1.0 },
26
+ "infinity-run": { type: "raw", name: "Infinity Run",
27
+ frames: ["⠐⠄","⠠⠀","⠀⢀","⡀⠠","⡀⠀","⠄⠐","⠀⠈","⠂⠈","⠁⠀","⠀⠂","⠐⠄","⠠⠀","⠀⢀","⡀⠠","⡀⠀","⠄⠐","⠀⠈","⠂⠈","⠁⠀","⠀⠂"], defaultSpeed: 1.0 },
28
+ "radar-arc": { type: "raw", name: "Radar Arc",
29
+ frames: ["⠀⠰","⠀⠠","⠀⠤","⠀⡄","⠀⡀","⢀⡀","⢀⠀","⢠⠀","⠤⠀","⠄⠀","⠆⠀","⠂⠀","⠒⠀","⠘⠀","⠈⠀","⠈⠁","⠀⠁","⠀⠃","⠀⠒","⠀⠐"], defaultSpeed: 1.5 },
30
+ "scan": { type: "raw", name: "Scan",
31
+ frames: ["⡇⠀","⣿⠀","⢸⡇","⠀⣿","⠀⢸","⠀⠀"], defaultSpeed: 1.0 },
32
+ "sweep": { type: "raw", name: "Sweep",
33
+ frames: ["⡀⠀","⣀⠀","⣄⠀","⣆⠀","⣇⠀","⣿⠀","⢸⡇","⠀⣿","⠀⢸","⠀⢰","⠀⢠","⠀⣀","⠀⡀"], defaultSpeed: 1.0 },
34
+ "agent-sweep": { type: "raw", name: "Agent Sweep",
35
+ frames: ["⠁⠀","⠃⠀","⠇⠀","⡇⠀","⣿⠀","⢸⡇","⠀⣿","⠀⢸","⠀⠸","⠀⠘","⠀⠈","⠀⠐","⠀⠠","⠀⢀","⠀⡀","⢀⡀","⡀⠀","⠄⠀","⠂⠀"], defaultSpeed: 1.0 },
36
+ "sound-bars": { type: "raw", name: "Sound Bars",
37
+ frames: ["⣰⣦","⣴⣤","⣶⣦","⣶⣄","⣶⣤","⣶⣰","⣦⣴","⣆⣶","⣦⣦","⣰⣦","⣴⣦","⣶⣆","⣶⣦","⣶⣴","⣦⣶","⣤⣶","⣰⣶"], defaultSpeed: 1.0 },
38
+ "perimeter-spin-light": { type: "raw", name: "Perimeter Spin Light",
39
+ frames: ["⠃⠀","⠉⠀","⠈⠁","⠀⠉","⠀⠘","⠀⠰","⠀⢠","⠀⣀","⢀⡀","⣀⠀","⡄⠀","⠆⠀"], defaultSpeed: 1.0 },
40
+ "perimeter-spin": { type: "raw", name: "Perimeter Spin",
41
+ frames: ["⠇⠀","⠋⠀","⠉⠁","⠈⠉","⠀⠙","⠀⠸","⠀⢰","⠀⣠","⢀⣀","⣀⡀","⣄⠀","⡆⠀"], defaultSpeed: 1.0 },
42
+ "perimeter-spin-bold": { type: "raw", name: "Perimeter Spin Bold",
43
+ frames: ["⡇⠀","⠏⠀","⠋⠁","⠉⠉","⠈⠙","⠀⠹","⠀⢸","⠀⣰","⢀⣠","⣀⣀","⣄⡀","⣆⠀"], defaultSpeed: 0.95 },
44
+ "shuffle": { type: "raw", name: "Shuffle",
45
+ frames: ["⢄","⢂","⢁","⡁","⡈","⡐","⡠"], defaultSpeed: 1.0 },
46
+ "hangtime": { type: "raw", name: "Hangtime",
47
+ frames: ["⢀⠀","⡀⠀","⠄⠀","⢂⠀","⡂⠀","⠅⠀","⢃⠀","⡃⠀","⠍⠀","⢋⠀","⡋⠀","⠍⠁","⢋⠁","⡋⠁","⠍⠉","⠋⠉","⠋⠉","⠉⠙","⠉⠙","⠉⠩","⠈⢙","⠈⡙","⢈⠩","⡀⢙","⠄⡙","⢂⠩","⡂⢘","⠅⡘","⢃⠨","⡃⢐","⠍⡐","⢋⠠","⡋⢀","⠍⡁","⢋⠁","⡋⠁","⠍⠉","⠋⠉","⠋⠉","⠉⠙","⠉⠙","⠉⠩","⠈⢙","⠈⡙","⠈⠩","⠀⢙","⠀⡙","⠀⠩","⠀⢘","⠀⡘","⠀⠨","⠀⢐","⠀⡐","⠀⠠","⠀⢀","⠀⡀"], defaultSpeed: 1.0 },
48
+ "line-spin": { type: "raw", name: "Line Spin",
49
+ frames: ["⠶⠶","⠒⠤","⠑⢄","⠘⡄","⢸⡇","⢠⠃","⡠⠊","⠤⠒"], defaultSpeed: 1.0 },
50
+ "ray-spin": { type: "raw", name: "Ray Spin",
51
+ frames: ["⠀⠶","⠀⢄","⢠⡄","⡠⠀","⠶⠀","⠑⠀","⠘⠃","⠀⠊"], defaultSpeed: 1.0 },
52
+ "rotating-x": { type: "raw", name: "Rotating X",
53
+ frames: ["⠕⠪","⠚⡤","⠕⠪","⢤⠓"], defaultSpeed: 1.0 },
54
+ "texture-flip":{ type: "raw", name: "Texture Flip",
55
+ frames: ["⡪⡪","⢕⢕"], defaultSpeed: 1.0 },
56
+ "neural-flicker": { type: "raw", name: "Neural Flicker",
57
+ frames: ["⠁⠈","⠂⠐","⠄⠠","⡀⢀","⢄⡠","⢆⡰","⢇⣰","⣿⣿","⣰⢇","⡰⢆","⡠⢄","⢀⡀","⠠⠄","⠐⠂","⠈⠁"], defaultSpeed: 1.0 },
58
+ "neural-flicker-chaos": { type: "raw", name: "Neural Flicker Chaos",
59
+ frames: ["⠁⠈","⠂⠐","⠄⠠","⡀⢀","⢄⡠","⢆⡰","⢇⣰","⣿⣿","⣰⢇","⡰⢆","⡠⢄","⢀⡀","⠠⠄","⠐⠂","⠈⠁","⠈⠂","⠘⠄","⠸⡀","⢸⢀","⣸⠠","⣰⠐","⢠⠈","⡀⠘","⠄⠸","⠂⢸","⠁⣸","⠀⠰","⠈⠠","⠐⠄","⠂⠁","⠆⠈","⡆⠐","⣆⠠","⣇⢀","⢇⡀","⠇⡠","⠃⡰","⠁⣰","⠈⢠","⠐⡀","⠠⠄"], defaultSpeed: 1.0 },
60
+ "neural-flicker-drift": { type: "raw", name: "Neural Flicker Drift",
61
+ frames: ["⠁⠈","⠂⠐","⠄⠠","⡀⢀","⢄⡠","⢆⡰","⢇⣰","⣇⣸","⣰⢇","⡰⢆","⡠⢄","⢀⡀","⠠⠄","⠐⠂","⠈⠁","⠁⠂","⠃⠄","⠇⡀","⡇⢀","⣇⠠","⢇⠐","⠇⠈","⠃⠘","⠁⠸","⠂⢸","⠄⣸","⡀⢰","⢀⠠","⠠⠐","⠈⠄","⠘⡀","⠸⢀","⢸⠠","⣸⠐","⢰⠈","⠠⠘","⠐⠸","⠈⢸","⠐⣸","⠠⢰","⢀⠠"], defaultSpeed: 1.0 },
62
+ "neural-flicker-thought": { type: "raw", name: "Neural Flicker Thought",
63
+ frames: ["⠁⠈","⠂⠐","⠄⠠","⡀⢀","⢄⡠","⢆⡰","⢇⣰","⣇⣸","⢇⣰","⡰⢆","⡠⢄","⢀⡀","⠠⠄","⠐⠂","⠈⠁","⠈⠁","⠘⠂","⠸⠄","⢸⡀","⣸⢀","⢰⠠","⠠⠐","⠐⠈","⠘⠁","⠸⠂","⢸⠄","⣸⡀","⢰⢀","⠠⠠","⠂⠈","⠆⠐","⡆⠠","⣆⢀","⢆⡀","⠆⡠","⠂⡰","⠁⣰","⠈⢠","⠐⡀","⠠⠄","⢀⠂"], defaultSpeed: 1.0 },
64
+ "neural-scatter": { type: "raw", name: "Neural Scatter",
65
+ frames: ["⠁⠈","⠂⡐","⡄⠠","⠐⢀","⢄⠂","⠆⡰","⢇⠐","⠃⣰","⡠⠄","⠘⢆","⢀⡁","⠰⠂","⡂⠈","⠄⢠","⠈⡐","⠂⠠"], defaultSpeed: 1.15 },
66
+ "neural-spike": { type: "raw", name: "Neural Spike",
67
+ frames: ["⠁⠈","⠃⠐","⡂⠠","⠄⢀","⢄⠁","⠆⡠","⢂⠘","⠁⣰","⡀⠆","⠘⢄","⠠⡂","⠂⢀","⡁⠐","⠄⠈"], defaultSpeed: 1.2 },
68
+ "neural-random-walk": { type: "raw", name: "Neural Random Walk",
69
+ frames: ["⠁⠈","⠂⠐","⡄⠠","⢀⠂","⠆⡀","⠐⢄","⢂⠘","⠁⡰","⡠⠄","⠘⢂","⠄⡐","⠂⢀","⡁⠠","⠈⠆","⠐⡀","⢄⠁"], defaultSpeed: 1.1 },
70
+ "neural-cross-spark": { type: "raw", name: "Neural Cross Spark",
71
+ frames: ["⠁⠈","⠂⠐","⠄⠠","⡀⢀","⢄⡠","⠆⡰","⢂⡐","⠁⣰","⡠⠄","⠘⢆","⠐⢂","⠈⡁","⠠⠄","⠂⠐"], defaultSpeed: 1.0 },
72
+ "neural-offset": { type: "raw", name: "Neural Offset",
73
+ frames: ["⠁⠈","⡂⠐","⠄⢠","⢀⠂","⠆⡀","⠐⢄","⢂⠘","⠁⢰","⡠⠄","⠘⢂","⠄⡐","⠂⢀","⡁⠠","⠈⠆"], defaultSpeed: 1.15 },
74
+ "neural-braid": { type: "raw", name: "Neural Braid",
75
+ frames: ["⢨⠎","⢨⡎","⢠⡇","⢰⡃","⡰⡇","⡸⢅","⡜⢅","⡜⢇","⢜⢢","⢎⢢","⢎⢣","⢪⢱","⠣⡱","⠱⡸","⠑⡜","⢑⡜","⢘⡜","⢈⡎"], defaultSpeed: 1.0 },
76
+ "thinking-pulse": { type: "raw", name: "Thinking Pulse",
77
+ frames: ["⠁⠀","⠃⠀","⠇⠀","⡇⠀","⣇⠀","⣿⠀","⢸⡇","⠀⣿","⠀⣸","⠀⢸","⠀⠸","⠀⠘","⠀⠈","⠀⠐","⠀⠠","⠀⢀","⠀⡀","⢀⡀","⡀⠀","⠄⠀","⠂⠀"], defaultSpeed: 1.0 },
78
+ "soft-thinking": { type: "raw", name: "Soft Thinking",
79
+ frames: ["⠁⠀","⠂⠀","⠄⠀","⡀⠀","⢀⠀","⠀⢀","⠀⠠","⠀⠐","⠀⠈","⠀⠐","⠀⠠","⠀⢀","⢀⠀","⡀⠀","⠄⠀","⠂⠀"], defaultSpeed: 1.0 },
80
+ "deep-thought": { type: "raw", name: "Deep Thought",
81
+ frames: ["⠁⠀","⠃⠀","⠇⠀","⡇⠀","⣇⠀","⣧⠀","⣿⠀","⣿⡇","⢸⣿","⠀⣿","⠀⣼","⠀⣸","⠀⢰","⠀⠠","⠀⠐","⠀⠈","⠀⠐","⠀⠠","⠀⢀","⠀⡀","⢀⡀","⡀⠀","⠄⠀","⠂⠀"], defaultSpeed: 1.0 },
82
+ "signal-search": { type: "raw", name: "Signal Search",
83
+ frames: ["⠁⠀","⠃⠀","⠇⠀","⡇⠀","⡇⠁","⡇⠃","⡇⠇","⡇⡇","⠇⡇","⠃⡇","⠁⡇","⠀⡇","⠀⠇","⠀⠃","⠀⠁"], defaultSpeed: 1.0 },
84
+ "binary-thought": { type: "raw", name: "Binary Thought",
85
+ frames: ["⠁⠈","⠃⠘","⠇⠸","⡇⢸","⣇⣸","⡇⢸","⠇⠸","⠃⠘","⠁⠈","⠂⠐","⠆⠰","⠄⠠","⠂⠐"], defaultSpeed: 1.0 },
86
+ "center-spark": { type: "raw", name: "Center Spark",
87
+ frames: ["⠲⣆","⠲⢦","⠳⠦","⠹⠦","⠹⠶","⠸⠗","⡸⠟","⡰⠏","⣰⠎","⣴⡎","⢴⡆","⠶⣆"], defaultSpeed: 1.0 },
88
+ "synapse-wave": { type: "raw", name: "Synapse Wave",
89
+ frames: ["⣀⠝","⢄⠕","⢐⠕","⣠⠋","⡤⠊","⡢⠊","⢨⡊","⢴⠁","⢖⠁","⢕⢁","⠪⡄","⠺⡀","⠫⣀","⠑⢤","⠑⢔","⢑⢅","⠈⡦","⠈⡲","⡈⡪","⢨⠔"], defaultSpeed: 1.0 },
90
+ "inner-current": { type: "raw", name: "Inner Current",
91
+ frames: ["⢰⠇","⢰⠃","⢠⠃","⢠⠇","⠸⡇","⠸⡅","⠸⡁","⢸⡄","⢸⡅","⢸⡇","⢈⠇","⢈⡇","⢈⡆"], defaultSpeed: 1.0 },
92
+ "bouncing-block": { type: "raw", name: "Bouncing Block",
93
+ frames: ["⡇⠀","⣇⠀","⣿⠀","⢸⡇","⠀⣿","⠀⣸","⠀⢸","⠀⣸","⠀⣿","⢸⡇","⣿⠀","⣇⠀"], defaultSpeed: 1.0 },
94
+ "face": { type: "raw", name: "Smiley Face",
95
+ frames: ["⣀⣀","⣁⣈","⢅⡨","⢄⡨","⢅⡨","⣁⣈"], defaultSpeed: 1.0 },
96
+ "snake-crawl": { type: "raw", name: "Snake Crawl",
97
+ frames: ["⠉⠉","⠉⠁","⠉⠁","⠉⠉","⠈⠙","⠀⠛","⠐⠚","⠒⠒","⠖⠂","⠶⠀","⠦⠄","⠤⠤","⠠⢤","⠀⣤","⢀⣠","⣀⣀","⣀⡀","⣀⡀","⣀⣀","⢀⣠","⠀⣤","⠠⢤","⠤⠤","⠦⠄","⠶⠀","⠖⠂","⠒⠒","⠐⠚","⠀⠛","⠈⠙"], defaultSpeed: 1.0 },
98
+ "snake-loop": { type: "raw", name: "Snake Loop",
99
+ frames: ["⠉⠉","⠈⠙","⠀⠛","⠐⠚","⠒⠒","⠖⠂","⠶⠀","⠦⠄","⠤⠤","⠠⢤","⠀⣤","⢀⣠","⣀⣀","⣄⡀","⣤⠀","⡤⠄","⠤⠤","⠠⠴","⠀⠶","⠐⠲","⠒⠒","⠓⠂","⠛⠀","⠋⠁"], defaultSpeed: 1.0 },
100
+ "circle": { type: "raw", name: "Circle",
101
+ frames: [" ",".","o","O","@",""], defaultSpeed: 1.0 },
102
+ "square": { type: "raw", name: "Square",
103
+ frames: ["□","■"], defaultSpeed: 1.0 },
104
+ "twinkle": { type: "raw", name: "Twinkle",
105
+ frames: ["+","×"], defaultSpeed: 1.0 },
106
+ "corners": { type: "raw", name: "Corners",
107
+ frames: ["◢","◣","◤","◥"], defaultSpeed: 1.0 },
108
+ "growth": { type: "raw", name: "Growth",
109
+ frames: ["▁","▃","▄","▅","▆","▇","▆","▅","▄","▃"], defaultSpeed: 1.0 },
110
+ };
111
+
112
+ export const PATTERN_KEYS = Object.keys(PATTERNS);