@cleocode/animations 2026.5.29 → 2026.5.33

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/scripts/demo.cjs CHANGED
@@ -2,13 +2,19 @@
2
2
  /**
3
3
  * @cleocode/animations demo CLI.
4
4
  *
5
- * Cycles through every registered spinner in the terminal. CommonJS so that the
6
- * shebang works without `--experimental-loader` flags on older Node runtimes.
5
+ * Previews every primitive family in the terminal generic spinners, canon
6
+ * spinners, progress bars, and one-shot sparks. CommonJS so the shebang works
7
+ * on every Node runtime without flags.
7
8
  *
8
9
  * Usage:
9
- * cleocode-animations cycle through all spinners
10
- * cleocode-animations <name> preview one spinner
11
- * cleocode-animations --list list all spinners
10
+ * cleocode-animations cycle through every primitive family
11
+ * cleocode-animations <name> preview one spinner (generic OR canon)
12
+ * cleocode-animations spark <name> play one spark and exit
13
+ * cleocode-animations progress loop through all 3 progress styles
14
+ * cleocode-animations --list list every registered primitive
15
+ * cleocode-animations --list-canon list canon spinner aliases only
16
+ * cleocode-animations --list-sparks list sparks only
17
+ * cleocode-animations --list-progress list progress styles only
12
18
  *
13
19
  * Forked from gunnargray-dev/unicode-animations (MIT).
14
20
  */
@@ -17,57 +23,144 @@ const path = require('path');
17
23
  const fs = require('fs');
18
24
  const tty = require('tty');
19
25
 
20
- let registry;
26
+ const MODULES = {
27
+ braille: 'braille.js',
28
+ spark: 'spark.js',
29
+ progress: 'progress.js',
30
+ };
31
+
32
+ function distPath(file) {
33
+ return path.join(__dirname, '..', 'dist', 'src', file);
34
+ }
35
+
36
+ let pending;
21
37
  try {
22
- // The ESM build is loaded via dynamic import below so that this CJS shim
23
- // does not need a CJS build artifact. We resolve the dist path eagerly to
24
- // surface a clear error if the package was never built.
25
- const distPath = path.join(__dirname, '..', 'dist', 'src', 'braille.js');
26
- if (!fs.existsSync(distPath)) {
27
- console.error('@cleocode/animations: run `pnpm --filter @cleocode/animations build` first.');
28
- process.exit(1);
38
+ for (const file of Object.values(MODULES)) {
39
+ if (!fs.existsSync(distPath(file))) {
40
+ console.error('@cleocode/animations: run `pnpm --filter @cleocode/animations build` first.');
41
+ process.exit(1);
42
+ }
29
43
  }
30
- // eslint-disable-next-line no-undef
31
- registry = import(distPath);
44
+ pending = Promise.all([
45
+ import(distPath(MODULES.braille)),
46
+ import(distPath(MODULES.spark)),
47
+ import(distPath(MODULES.progress)),
48
+ ]);
32
49
  } catch (err) {
33
50
  console.error('@cleocode/animations: failed to load the built module.', err);
34
51
  process.exit(1);
35
52
  }
36
53
 
37
54
  (async () => {
38
- const mod = await registry;
39
- const S = mod.spinners || mod.default;
40
- const names = Object.keys(S);
55
+ const [brailleMod, sparkMod, progressMod] = await pending;
56
+
57
+ const SPINNERS = brailleMod.spinners;
58
+ const CANON = brailleMod.canonSpinners;
59
+ const CANON_TO_GENERIC = brailleMod.CANON_TO_GENERIC;
60
+ const SPARKS = sparkMod.sparks;
61
+ const PROGRESS_STYLES = ['tapestry', 'cascade', 'refinery'];
62
+ const renderProgressBar = progressMod.renderProgressBar;
63
+
64
+ const spinnerNames = Object.keys(SPINNERS);
65
+ const canonNames = Object.keys(CANON);
66
+ const sparkNames = Object.keys(SPARKS);
41
67
  const args = process.argv.slice(2);
42
68
 
69
+ // Color codes used by both list output (always to stdout) and the live
70
+ // animation surface (TTY-only). When the destination is not a TTY (e.g.
71
+ // piped to grep), `process.stdout.write` strips ANSI cleanly.
72
+ const bold = '\x1B[1m';
73
+ const dim = '\x1B[2m';
74
+ const magenta = '\x1B[35m';
75
+ const cyan = '\x1B[36m';
76
+ const yellow = '\x1B[33m';
77
+ const green = '\x1B[32m';
78
+ const reset = '\x1B[0m';
79
+
80
+ // ──────────────────────────────────────────────────────────────────────
81
+ // --list family — pipe-safe, writes to stdout regardless of TTY state.
82
+ // Handled BEFORE the TTY-only animation path so `cleocode-animations
83
+ // --list | grep braille` works correctly.
84
+ // ──────────────────────────────────────────────────────────────────────
85
+ if (args[0] === '--list' || args[0] === '-l') {
86
+ process.stdout.write(`\n${bold}@cleocode/animations${reset} ${dim}— primitives:${reset}\n\n`);
87
+ process.stdout.write(` ${bold}Spinners (generic) · ${spinnerNames.length}${reset}\n`);
88
+ for (const name of spinnerNames) {
89
+ const s = SPINNERS[name];
90
+ process.stdout.write(` ${magenta}${s.frames[0]}${reset} ${name} ${dim}(${s.frames.length}f, ${s.interval}ms)${reset}\n`);
91
+ }
92
+ process.stdout.write(`\n ${bold}Spinners (canon) · ${canonNames.length}${reset}\n`);
93
+ for (const name of canonNames) {
94
+ const s = CANON[name];
95
+ const generic = CANON_TO_GENERIC[name];
96
+ process.stdout.write(` ${yellow}${s.frames[0]}${reset} ${name} ${dim}→ ${generic}${reset}\n`);
97
+ }
98
+ process.stdout.write(`\n ${bold}Sparks (one-shot) · ${sparkNames.length}${reset}\n`);
99
+ for (const name of sparkNames) {
100
+ const s = SPARKS[name];
101
+ process.stdout.write(` ${green}${s.frames[Math.floor(s.frames.length / 2)]}${reset} ${name} ${dim}(${s.frames.length}f × ${s.interval}ms)${reset}\n`);
102
+ }
103
+ process.stdout.write(`\n ${bold}Progress styles · ${PROGRESS_STYLES.length}${reset}\n`);
104
+ for (const style of PROGRESS_STYLES) {
105
+ process.stdout.write(` ${cyan}${renderProgressBar(style, 0.5, 8)}${reset} ${style}\n`);
106
+ }
107
+ process.stdout.write('\n');
108
+ process.exit(0);
109
+ }
110
+
111
+ if (args[0] === '--list-canon') {
112
+ for (const name of canonNames) process.stdout.write(`${name}\n`);
113
+ process.exit(0);
114
+ }
115
+ if (args[0] === '--list-sparks') {
116
+ for (const name of sparkNames) process.stdout.write(`${name}\n`);
117
+ process.exit(0);
118
+ }
119
+ if (args[0] === '--list-progress') {
120
+ for (const name of PROGRESS_STYLES) process.stdout.write(`${name}\n`);
121
+ process.exit(0);
122
+ }
123
+
124
+ // ──────────────────────────────────────────────────────────────────────
125
+ // Below this line: animation paths that require a writable TTY for the
126
+ // live frame replacement. If stdout is piped/redirected, fall back to
127
+ // /dev/tty when available, otherwise print a one-line summary and exit
128
+ // (matching the upstream's behavior for non-TTY usage).
129
+ // ──────────────────────────────────────────────────────────────────────
43
130
  let out = process.stdout;
44
131
  if (!out.isTTY) {
45
132
  try {
46
133
  const fd = fs.openSync('/dev/tty', 'w');
47
134
  out = new tty.WriteStream(fd);
48
135
  } catch {
49
- console.log(`${names.length} spinners: ${names.join(', ')}`);
136
+ console.log(
137
+ `spinners: ${spinnerNames.length} · canon: ${canonNames.length} · sparks: ${sparkNames.length} · progress: ${PROGRESS_STYLES.length}`,
138
+ );
139
+ console.log('(no TTY — pipe to a terminal or run --list to see the registry)');
50
140
  process.exit(0);
51
141
  }
52
142
  }
53
143
 
54
144
  const hide = '\x1B[?25l';
55
145
  const show = '\x1B[?25h';
56
- const bold = '\x1B[1m';
57
- const dim = '\x1B[2m';
58
- const magenta = '\x1B[35m';
59
- const reset = '\x1B[0m';
60
-
61
146
  out.write(hide);
62
- const cleanup = () => { try { out.write(show); } catch {} };
63
- process.on('SIGINT', () => { cleanup(); out.write('\n'); process.exit(0); });
147
+ const cleanup = () => {
148
+ try {
149
+ out.write(show);
150
+ } catch {}
151
+ };
152
+ process.on('SIGINT', () => {
153
+ cleanup();
154
+ out.write('\n');
155
+ process.exit(0);
156
+ });
64
157
  process.on('exit', cleanup);
65
158
 
66
159
  if (process.stdin.isTTY) {
67
160
  process.stdin.setRawMode(true);
68
161
  process.stdin.resume();
69
162
  process.stdin.on('data', (key) => {
70
- if (key[0] === 0x71 || key[0] === 0x03 || key[0] === 0x1B) {
163
+ if (key[0] === 0x71 || key[0] === 0x03 || key[0] === 0x1b) {
71
164
  cleanup();
72
165
  out.write('\n');
73
166
  process.exit(0);
@@ -75,45 +168,109 @@ try {
75
168
  });
76
169
  }
77
170
 
78
- if (args[0] === '--list' || args[0] === '-l') {
79
- cleanup();
80
- out.write(`\n${bold}${names.length} spinners available:${reset}\n\n`);
81
- for (const name of names) {
82
- const s = S[name];
83
- out.write(` ${magenta}${s.frames[0]}${reset} ${name} ${dim}(${s.frames.length} frames, ${s.interval}ms)${reset}\n`);
171
+ // ──────────────────────────────────────────────────────────────────────
172
+ // `cleocode-animations spark <name>` — play one spark and exit
173
+ // ──────────────────────────────────────────────────────────────────────
174
+ if (args[0] === 'spark') {
175
+ const name = args[1];
176
+ if (!name || !SPARKS[name]) {
177
+ cleanup();
178
+ out.write(`Usage: cleocode-animations spark <name>\nAvailable: ${sparkNames.join(', ')}\n`);
179
+ process.exit(name ? 1 : 0);
180
+ }
181
+ const s = SPARKS[name];
182
+ for (const frame of s.frames) {
183
+ out.write(`\r\x1B[2K ${green}${frame}${reset} ${bold}${name}${reset}`);
184
+ await new Promise((r) => setTimeout(r, s.interval));
84
185
  }
85
186
  out.write('\n');
187
+ cleanup();
86
188
  process.exit(0);
87
189
  }
88
190
 
89
- if (args[0] && !names.includes(args[0])) {
90
- cleanup();
91
- out.write(`Unknown spinner: "${args[0]}"\nRun with --list to see all spinners.\n`);
92
- process.exit(1);
191
+ // ──────────────────────────────────────────────────────────────────────
192
+ // `cleocode-animations progress` — loop through every progress style
193
+ // ──────────────────────────────────────────────────────────────────────
194
+ if (args[0] === 'progress') {
195
+ let t = 0;
196
+ setInterval(() => {
197
+ const lines = PROGRESS_STYLES.map((style) => {
198
+ const ratio = (Math.sin(t * 0.05) + 1) / 2;
199
+ const bar = renderProgressBar(style, ratio, 36);
200
+ return ` ${cyan}${bar}${reset} ${dim}${style}${reset} ${Math.round(ratio * 100)}%`;
201
+ });
202
+ // Move cursor up to redraw all lines in place
203
+ if (t > 0) out.write(`\x1B[${PROGRESS_STYLES.length}A`);
204
+ out.write(`${lines.join('\n')}\n`);
205
+ t++;
206
+ }, 80);
207
+ return;
93
208
  }
94
209
 
95
- let current = args[0] ? names.indexOf(args[0]) : 0;
96
- const single = !!args[0];
97
- let i = 0;
98
- let ticksOnCurrent = 0;
210
+ // ──────────────────────────────────────────────────────────────────────
211
+ // `cleocode-animations <name>` — preview one spinner (generic OR canon)
212
+ // ──────────────────────────────────────────────────────────────────────
213
+ function resolveAny(name) {
214
+ if (SPINNERS[name]) return { spinner: SPINNERS[name], color: magenta, label: name };
215
+ if (CANON[name])
216
+ return {
217
+ spinner: CANON[name],
218
+ color: yellow,
219
+ label: `${name} ${dim}→ ${CANON_TO_GENERIC[name]}${reset}`,
220
+ };
221
+ return null;
222
+ }
99
223
 
100
- const TICKS_PER_SPINNER = 40;
224
+ if (args[0]) {
225
+ const resolved = resolveAny(args[0]);
226
+ if (!resolved) {
227
+ cleanup();
228
+ out.write(
229
+ `Unknown name: "${args[0]}"\nRun --list to see every spinner / canon alias / spark / progress style.\n`,
230
+ );
231
+ process.exit(1);
232
+ }
233
+ let i = 0;
234
+ setInterval(() => {
235
+ const f = resolved.spinner.frames[i++ % resolved.spinner.frames.length];
236
+ out.write(
237
+ `\r\x1B[2K ${resolved.color}${f}${reset} ${bold}${resolved.label}${reset} ${dim}${resolved.spinner.interval}ms${reset}`,
238
+ );
239
+ }, resolved.spinner.interval);
240
+ return;
241
+ }
101
242
 
102
- setInterval(() => {
103
- const name = names[current];
104
- const s = S[name];
105
- const frame = s.frames[i % s.frames.length];
106
- const count = single ? '' : `${dim}[${current + 1}/${names.length}]${reset}`;
243
+ // ──────────────────────────────────────────────────────────────────────
244
+ // No arg — cycle through every spinner (generic + canon, deduped on object identity)
245
+ // ──────────────────────────────────────────────────────────────────────
246
+ const tour = [];
247
+ for (const name of spinnerNames) tour.push({ kind: 'generic', name, spinner: SPINNERS[name] });
248
+ for (const name of canonNames) tour.push({ kind: 'canon', name, spinner: CANON[name] });
107
249
 
108
- out.write(`\r\x1B[2K ${magenta}${frame}${reset} ${bold}${name}${reset} ${dim}${s.interval}ms${reset} ${count}`);
250
+ let current = 0;
251
+ let i = 0;
252
+ let ticksOnCurrent = 0;
253
+ const TICKS_PER = 40;
109
254
 
255
+ setInterval(() => {
256
+ const entry = tour[current];
257
+ const f = entry.spinner.frames[i % entry.spinner.frames.length];
258
+ const tag = entry.kind === 'canon' ? `${yellow}canon${reset}` : `${magenta}generic${reset}`;
259
+ const count = `${dim}[${current + 1}/${tour.length}]${reset}`;
260
+ const label =
261
+ entry.kind === 'canon'
262
+ ? `${entry.name} ${dim}→ ${CANON_TO_GENERIC[entry.name]}${reset}`
263
+ : entry.name;
264
+ const color = entry.kind === 'canon' ? yellow : magenta;
265
+ out.write(
266
+ `\r\x1B[2K ${color}${f}${reset} ${tag} ${bold}${label}${reset} ${dim}${entry.spinner.interval}ms${reset} ${count}`,
267
+ );
110
268
  i++;
111
269
  ticksOnCurrent++;
112
-
113
- if (!single && ticksOnCurrent >= TICKS_PER_SPINNER) {
270
+ if (ticksOnCurrent >= TICKS_PER) {
114
271
  ticksOnCurrent = 0;
115
272
  i = 0;
116
- current = (current + 1) % names.length;
273
+ current = (current + 1) % tour.length;
117
274
  }
118
275
  }, 80);
119
276
  })();