unicode-animations 0.1.4 → 0.2.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 CHANGED
@@ -2,14 +2,15 @@
2
2
 
3
3
  Unicode spinner animations as raw frame data — no dependencies, works everywhere.
4
4
 
5
- ## Preview
5
+ ## Demo
6
6
 
7
- ```
8
- braille ⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏
9
- arc ◜ ◠ ◝ ◞ ◡ ◟
10
- halfmoon ◐
11
- blocks ▁ ▇ █ ▇ ▆ ▅ ▄ ▃ ▂
12
- line | / \
7
+ See all 22 spinners animating live:
8
+
9
+ ```bash
10
+ npx unicode-animations --web # open browser demo
11
+ npx unicode-animations # cycle through all in terminal
12
+ npx unicode-animations helix # preview a specific spinner
13
+ npx unicode-animations --list # list all spinners
13
14
  ```
14
15
 
15
16
  ## Install
@@ -30,7 +31,9 @@ const spinners = require('unicode-animations');
30
31
 
31
32
  Each spinner is a `{ frames: string[], interval: number }` object.
32
33
 
33
- ## Terminal example
34
+ ## Examples
35
+
36
+ ### CLI tool — spinner during async work
34
37
 
35
38
  ```js
36
39
  import spinners from 'unicode-animations';
@@ -38,29 +41,104 @@ import spinners from 'unicode-animations';
38
41
  const { frames, interval } = spinners.braille;
39
42
  let i = 0;
40
43
 
41
- const timer = setInterval(() => {
42
- process.stdout.write(`\r${frames[i++ % frames.length]} Loading...`);
44
+ const spinner = setInterval(() => {
45
+ process.stdout.write(`\r\x1B[2K ${frames[i++ % frames.length]} Deploying to production...`);
43
46
  }, interval);
44
47
 
45
- // Stop after 3s
46
- setTimeout(() => {
48
+ await deploy();
49
+
50
+ clearInterval(spinner);
51
+ process.stdout.write('\r\x1B[2K ✔ Deployed.\n');
52
+ ```
53
+
54
+ ### Reusable spinner helper
55
+
56
+ ```js
57
+ import spinners from 'unicode-animations';
58
+
59
+ function createSpinner(msg, name = 'braille') {
60
+ const { frames, interval } = spinners[name];
61
+ let i = 0, text = msg;
62
+ const timer = setInterval(() => {
63
+ process.stdout.write(`\r\x1B[2K ${frames[i++ % frames.length]} ${text}`);
64
+ }, interval);
65
+
66
+ return {
67
+ update(msg) { text = msg; },
68
+ stop(msg) { clearInterval(timer); process.stdout.write(`\r\x1B[2K ✔ ${msg}\n`); },
69
+ };
70
+ }
71
+
72
+ const s = createSpinner('Connecting to database...');
73
+ const db = await connect();
74
+ s.update(`Running ${migrations.length} migrations...`);
75
+ await db.migrate(migrations);
76
+ s.stop('Database ready.');
77
+ ```
78
+
79
+ ### Multi-step pipeline
80
+
81
+ ```js
82
+ import spinners from 'unicode-animations';
83
+
84
+ async function runWithSpinner(label, fn, name = 'braille') {
85
+ const { frames, interval } = spinners[name];
86
+ let i = 0;
87
+ const timer = setInterval(() => {
88
+ process.stdout.write(`\r\x1B[2K ${frames[i++ % frames.length]} ${label}`);
89
+ }, interval);
90
+ const result = await fn();
47
91
  clearInterval(timer);
48
- process.stdout.write('\rDone! \n');
49
- }, 3000);
92
+ process.stdout.write(`\r\x1B[2K ${label}\n`);
93
+ return result;
94
+ }
95
+
96
+ await runWithSpinner('Linting...', lint, 'scan');
97
+ await runWithSpinner('Running tests...', test, 'helix');
98
+ await runWithSpinner('Building...', build, 'cascade');
99
+ await runWithSpinner('Publishing...', publish, 'braille');
50
100
  ```
51
101
 
52
- ## Browser example
102
+ ### React component
103
+
104
+ ```jsx
105
+ import { useState, useEffect } from 'react';
106
+ import spinners from 'unicode-animations';
107
+
108
+ function Spinner({ name = 'braille', children }) {
109
+ const [frame, setFrame] = useState(0);
110
+ const s = spinners[name];
111
+
112
+ useEffect(() => {
113
+ const timer = setInterval(
114
+ () => setFrame(f => (f + 1) % s.frames.length),
115
+ s.interval
116
+ );
117
+ return () => clearInterval(timer);
118
+ }, [name]);
119
+
120
+ return <span style={{ fontFamily: 'monospace' }}>{s.frames[frame]} {children}</span>;
121
+ }
122
+
123
+ // Usage: <Spinner name="helix">Generating response...</Spinner>
124
+ ```
125
+
126
+ ### Browser — status indicator
53
127
 
54
128
  ```js
55
129
  import spinners from 'unicode-animations';
56
130
 
57
- const el = document.getElementById('spinner');
58
- const { frames, interval } = spinners.arc;
131
+ const el = document.getElementById('status');
132
+ const { frames, interval } = spinners.orbit;
59
133
  let i = 0;
60
134
 
61
- setInterval(() => {
62
- el.textContent = frames[i++ % frames.length];
135
+ const spinner = setInterval(() => {
136
+ el.textContent = `${frames[i++ % frames.length]} Syncing...`;
63
137
  }, interval);
138
+
139
+ await sync();
140
+ clearInterval(spinner);
141
+ el.textContent = '✔ Synced';
64
142
  ```
65
143
 
66
144
  ## All spinners
@@ -70,8 +148,8 @@ setInterval(() => {
70
148
  | Name | Preview | Interval |
71
149
  |------|---------|----------|
72
150
  | `braille` | `⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏` | 80ms |
73
- | `braillewave` | `⠁⠂⠄⡀⢀⠠⠐⠈``⠂⠄⡀⢀⠠⠐⠈⠁` | 100ms |
74
- | `dna` | `⠋⠉⠙⠚⠒⠂⠂⠒⠲⠴⠤⠄``⠙⠚⠒⠂⠂⠒⠲⠴⠤⠄⠄⠠` | 80ms |
151
+ | `braillewave` | `⠁⠂⠄⡀``⠂⠄⡀⢀` | 100ms |
152
+ | `dna` | `⠋⠉⠙⠚``⠉⠙⠚⠒` | 80ms |
75
153
 
76
154
  ### Grid animations (braille)
77
155
 
package/dist/braille.cjs CHANGED
@@ -334,31 +334,31 @@ var spinners = {
334
334
  },
335
335
  braillewave: {
336
336
  frames: [
337
- "\u2801\u2802\u2804\u2840\u2880\u2820\u2810\u2808",
338
- "\u2808\u2801\u2802\u2804\u2840\u2880\u2820\u2810",
339
- "\u2810\u2808\u2801\u2802\u2804\u2840\u2880\u2820",
340
- "\u2820\u2810\u2808\u2801\u2802\u2804\u2840\u2880",
341
- "\u2880\u2820\u2810\u2808\u2801\u2802\u2804\u2840",
342
- "\u2840\u2880\u2820\u2810\u2808\u2801\u2802\u2804",
343
- "\u2804\u2840\u2880\u2820\u2810\u2808\u2801\u2802",
344
- "\u2802\u2804\u2840\u2880\u2820\u2810\u2808\u2801"
337
+ "\u2801\u2802\u2804\u2840",
338
+ "\u2802\u2804\u2840\u2880",
339
+ "\u2804\u2840\u2880\u2820",
340
+ "\u2840\u2880\u2820\u2810",
341
+ "\u2880\u2820\u2810\u2808",
342
+ "\u2820\u2810\u2808\u2801",
343
+ "\u2810\u2808\u2801\u2802",
344
+ "\u2808\u2801\u2802\u2804"
345
345
  ],
346
346
  interval: 100
347
347
  },
348
348
  dna: {
349
349
  frames: [
350
- "\u280B\u2809\u2819\u281A\u2812\u2802\u2802\u2812\u2832\u2834\u2824\u2804",
351
- "\u2819\u281A\u2812\u2802\u2802\u2812\u2832\u2834\u2824\u2804\u2804\u2820",
352
- "\u2839\u2812\u2802\u2802\u2812\u2832\u2834\u2824\u2804\u2804\u2820\u2820",
353
- "\u2838\u2802\u2802\u2812\u2832\u2834\u2824\u2804\u2804\u2820\u2820\u2804",
354
- "\u283C\u2802\u2812\u2832\u2834\u2824\u2804\u2804\u2820\u2820\u2804\u2824",
355
- "\u2834\u2812\u2832\u2834\u2824\u2804\u2804\u2820\u2820\u2804\u2824\u2834",
356
- "\u2826\u2832\u2834\u2824\u2804\u2804\u2820\u2820\u2804\u2824\u2834\u2832",
357
- "\u2827\u2834\u2824\u2804\u2804\u2820\u2820\u2804\u2824\u2834\u2832\u2812",
358
- "\u2807\u2824\u2804\u2804\u2820\u2820\u2804\u2824\u2834\u2832\u2812\u2802",
359
- "\u280F\u2804\u2804\u2820\u2820\u2804\u2824\u2834\u2832\u2812\u2802\u2802",
360
- "\u280B\u2804\u2820\u2820\u2804\u2824\u2834\u2832\u2812\u2802\u2802\u2812",
361
- "\u2809\u2820\u2820\u2804\u2824\u2834\u2832\u2812\u2802\u2802\u2812\u2832"
350
+ "\u280B\u2809\u2819\u281A",
351
+ "\u2809\u2819\u281A\u2812",
352
+ "\u2819\u281A\u2812\u2802",
353
+ "\u281A\u2812\u2802\u2802",
354
+ "\u2812\u2802\u2802\u2812",
355
+ "\u2802\u2802\u2812\u2832",
356
+ "\u2802\u2812\u2832\u2834",
357
+ "\u2812\u2832\u2834\u2824",
358
+ "\u2832\u2834\u2824\u2804",
359
+ "\u2834\u2824\u2804\u280B",
360
+ "\u2824\u2804\u280B\u2809",
361
+ "\u2804\u280B\u2809\u2819"
362
362
  ],
363
363
  interval: 80
364
364
  },
package/dist/braille.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  gridToBraille,
4
4
  makeGrid,
5
5
  spinners
6
- } from "./chunk-MLXIK7E7.js";
6
+ } from "./chunk-JW3PMLWA.js";
7
7
  export {
8
8
  braille_default as default,
9
9
  gridToBraille,
@@ -307,31 +307,31 @@ var spinners = {
307
307
  },
308
308
  braillewave: {
309
309
  frames: [
310
- "\u2801\u2802\u2804\u2840\u2880\u2820\u2810\u2808",
311
- "\u2808\u2801\u2802\u2804\u2840\u2880\u2820\u2810",
312
- "\u2810\u2808\u2801\u2802\u2804\u2840\u2880\u2820",
313
- "\u2820\u2810\u2808\u2801\u2802\u2804\u2840\u2880",
314
- "\u2880\u2820\u2810\u2808\u2801\u2802\u2804\u2840",
315
- "\u2840\u2880\u2820\u2810\u2808\u2801\u2802\u2804",
316
- "\u2804\u2840\u2880\u2820\u2810\u2808\u2801\u2802",
317
- "\u2802\u2804\u2840\u2880\u2820\u2810\u2808\u2801"
310
+ "\u2801\u2802\u2804\u2840",
311
+ "\u2802\u2804\u2840\u2880",
312
+ "\u2804\u2840\u2880\u2820",
313
+ "\u2840\u2880\u2820\u2810",
314
+ "\u2880\u2820\u2810\u2808",
315
+ "\u2820\u2810\u2808\u2801",
316
+ "\u2810\u2808\u2801\u2802",
317
+ "\u2808\u2801\u2802\u2804"
318
318
  ],
319
319
  interval: 100
320
320
  },
321
321
  dna: {
322
322
  frames: [
323
- "\u280B\u2809\u2819\u281A\u2812\u2802\u2802\u2812\u2832\u2834\u2824\u2804",
324
- "\u2819\u281A\u2812\u2802\u2802\u2812\u2832\u2834\u2824\u2804\u2804\u2820",
325
- "\u2839\u2812\u2802\u2802\u2812\u2832\u2834\u2824\u2804\u2804\u2820\u2820",
326
- "\u2838\u2802\u2802\u2812\u2832\u2834\u2824\u2804\u2804\u2820\u2820\u2804",
327
- "\u283C\u2802\u2812\u2832\u2834\u2824\u2804\u2804\u2820\u2820\u2804\u2824",
328
- "\u2834\u2812\u2832\u2834\u2824\u2804\u2804\u2820\u2820\u2804\u2824\u2834",
329
- "\u2826\u2832\u2834\u2824\u2804\u2804\u2820\u2820\u2804\u2824\u2834\u2832",
330
- "\u2827\u2834\u2824\u2804\u2804\u2820\u2820\u2804\u2824\u2834\u2832\u2812",
331
- "\u2807\u2824\u2804\u2804\u2820\u2820\u2804\u2824\u2834\u2832\u2812\u2802",
332
- "\u280F\u2804\u2804\u2820\u2820\u2804\u2824\u2834\u2832\u2812\u2802\u2802",
333
- "\u280B\u2804\u2820\u2820\u2804\u2824\u2834\u2832\u2812\u2802\u2802\u2812",
334
- "\u2809\u2820\u2820\u2804\u2824\u2834\u2832\u2812\u2802\u2802\u2812\u2832"
323
+ "\u280B\u2809\u2819\u281A",
324
+ "\u2809\u2819\u281A\u2812",
325
+ "\u2819\u281A\u2812\u2802",
326
+ "\u281A\u2812\u2802\u2802",
327
+ "\u2812\u2802\u2802\u2812",
328
+ "\u2802\u2802\u2812\u2832",
329
+ "\u2802\u2812\u2832\u2834",
330
+ "\u2812\u2832\u2834\u2824",
331
+ "\u2832\u2834\u2824\u2804",
332
+ "\u2834\u2824\u2804\u280B",
333
+ "\u2824\u2804\u280B\u2809",
334
+ "\u2804\u280B\u2809\u2819"
335
335
  ],
336
336
  interval: 80
337
337
  },
package/dist/index.cjs CHANGED
@@ -336,31 +336,31 @@ var spinners = {
336
336
  },
337
337
  braillewave: {
338
338
  frames: [
339
- "\u2801\u2802\u2804\u2840\u2880\u2820\u2810\u2808",
340
- "\u2808\u2801\u2802\u2804\u2840\u2880\u2820\u2810",
341
- "\u2810\u2808\u2801\u2802\u2804\u2840\u2880\u2820",
342
- "\u2820\u2810\u2808\u2801\u2802\u2804\u2840\u2880",
343
- "\u2880\u2820\u2810\u2808\u2801\u2802\u2804\u2840",
344
- "\u2840\u2880\u2820\u2810\u2808\u2801\u2802\u2804",
345
- "\u2804\u2840\u2880\u2820\u2810\u2808\u2801\u2802",
346
- "\u2802\u2804\u2840\u2880\u2820\u2810\u2808\u2801"
339
+ "\u2801\u2802\u2804\u2840",
340
+ "\u2802\u2804\u2840\u2880",
341
+ "\u2804\u2840\u2880\u2820",
342
+ "\u2840\u2880\u2820\u2810",
343
+ "\u2880\u2820\u2810\u2808",
344
+ "\u2820\u2810\u2808\u2801",
345
+ "\u2810\u2808\u2801\u2802",
346
+ "\u2808\u2801\u2802\u2804"
347
347
  ],
348
348
  interval: 100
349
349
  },
350
350
  dna: {
351
351
  frames: [
352
- "\u280B\u2809\u2819\u281A\u2812\u2802\u2802\u2812\u2832\u2834\u2824\u2804",
353
- "\u2819\u281A\u2812\u2802\u2802\u2812\u2832\u2834\u2824\u2804\u2804\u2820",
354
- "\u2839\u2812\u2802\u2802\u2812\u2832\u2834\u2824\u2804\u2804\u2820\u2820",
355
- "\u2838\u2802\u2802\u2812\u2832\u2834\u2824\u2804\u2804\u2820\u2820\u2804",
356
- "\u283C\u2802\u2812\u2832\u2834\u2824\u2804\u2804\u2820\u2820\u2804\u2824",
357
- "\u2834\u2812\u2832\u2834\u2824\u2804\u2804\u2820\u2820\u2804\u2824\u2834",
358
- "\u2826\u2832\u2834\u2824\u2804\u2804\u2820\u2820\u2804\u2824\u2834\u2832",
359
- "\u2827\u2834\u2824\u2804\u2804\u2820\u2820\u2804\u2824\u2834\u2832\u2812",
360
- "\u2807\u2824\u2804\u2804\u2820\u2820\u2804\u2824\u2834\u2832\u2812\u2802",
361
- "\u280F\u2804\u2804\u2820\u2820\u2804\u2824\u2834\u2832\u2812\u2802\u2802",
362
- "\u280B\u2804\u2820\u2820\u2804\u2824\u2834\u2832\u2812\u2802\u2802\u2812",
363
- "\u2809\u2820\u2820\u2804\u2824\u2834\u2832\u2812\u2802\u2802\u2812\u2832"
352
+ "\u280B\u2809\u2819\u281A",
353
+ "\u2809\u2819\u281A\u2812",
354
+ "\u2819\u281A\u2812\u2802",
355
+ "\u281A\u2812\u2802\u2802",
356
+ "\u2812\u2802\u2802\u2812",
357
+ "\u2802\u2802\u2812\u2832",
358
+ "\u2802\u2812\u2832\u2834",
359
+ "\u2812\u2832\u2834\u2824",
360
+ "\u2832\u2834\u2824\u2804",
361
+ "\u2834\u2824\u2804\u280B",
362
+ "\u2824\u2804\u280B\u2809",
363
+ "\u2804\u280B\u2809\u2819"
364
364
  ],
365
365
  interval: 80
366
366
  },
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  gridToBraille,
4
4
  makeGrid,
5
5
  spinners
6
- } from "./chunk-MLXIK7E7.js";
6
+ } from "./chunk-JW3PMLWA.js";
7
7
  export {
8
8
  braille_default as default,
9
9
  gridToBraille,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unicode-animations",
3
- "version": "0.1.4",
3
+ "version": "0.2.1",
4
4
  "description": "Unicode spinner animations as raw frame data",
5
5
  "type": "module",
6
6
  "exports": {
@@ -13,6 +13,9 @@
13
13
  "require": { "types": "./dist/braille.d.cts", "default": "./dist/braille.cjs" }
14
14
  }
15
15
  },
16
+ "bin": {
17
+ "unicode-animations": "./scripts/demo.cjs"
18
+ },
16
19
  "main": "./dist/index.cjs",
17
20
  "module": "./dist/index.js",
18
21
  "types": "./dist/index.d.ts",
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env node
2
+
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+ const tty = require('tty');
6
+
7
+ let S;
8
+ try {
9
+ S = require(path.join(__dirname, '..', 'dist', 'index.cjs'));
10
+ S = S.spinners || S.default;
11
+ } catch {
12
+ console.error('Run `npm run build` first.');
13
+ process.exit(1);
14
+ }
15
+
16
+ const names = Object.keys(S);
17
+ const args = process.argv.slice(2);
18
+
19
+ // --web: open browser demo
20
+ if (args[0] === '--web' || args[0] === '-w') {
21
+ const { exec } = require('child_process');
22
+ const demoPath = path.join(__dirname, 'demo.html');
23
+ const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
24
+ exec(`${cmd} "${demoPath}"`);
25
+ console.log(`Opening ${demoPath}`);
26
+ process.exit(0);
27
+ }
28
+
29
+ // Get a writable TTY stream — stdout if it's a TTY, otherwise /dev/tty
30
+ let out = process.stdout;
31
+ if (!out.isTTY) {
32
+ try {
33
+ const fd = fs.openSync('/dev/tty', 'w');
34
+ out = new tty.WriteStream(fd);
35
+ } catch {
36
+ // Fallback: no TTY available, just list and exit
37
+ console.log('22 spinners: ' + names.join(', '));
38
+ process.exit(0);
39
+ }
40
+ }
41
+
42
+ const hide = '\x1B[?25l';
43
+ const show = '\x1B[?25h';
44
+ const bold = '\x1B[1m';
45
+ const dim = '\x1B[2m';
46
+ const magenta = '\x1B[35m';
47
+ const reset = '\x1B[0m';
48
+
49
+ out.write(hide);
50
+ const cleanup = () => { try { out.write(show); } catch {} };
51
+ process.on('SIGINT', () => { cleanup(); out.write('\n'); process.exit(0); });
52
+ process.on('exit', cleanup);
53
+
54
+ if (args[0] === '--list' || args[0] === '-l') {
55
+ cleanup();
56
+ out.write(`\n${bold}22 spinners available:${reset}\n\n`);
57
+ for (const name of names) {
58
+ const s = S[name];
59
+ out.write(` ${magenta}${s.frames[0]}${reset} ${name} ${dim}(${s.frames.length} frames, ${s.interval}ms)${reset}\n`);
60
+ }
61
+ out.write('\n');
62
+ process.exit(0);
63
+ }
64
+
65
+ if (args[0] && !names.includes(args[0])) {
66
+ cleanup();
67
+ out.write(`Unknown spinner: "${args[0]}"\nRun with --list to see all spinners.\n`);
68
+ process.exit(1);
69
+ }
70
+
71
+ let current = args[0] ? names.indexOf(args[0]) : 0;
72
+ const single = !!args[0];
73
+ let i = 0;
74
+ let ticksOnCurrent = 0;
75
+
76
+ const TICKS_PER_SPINNER = 40;
77
+
78
+ const timer = setInterval(() => {
79
+ const name = names[current];
80
+ const s = S[name];
81
+ const frame = s.frames[i % s.frames.length];
82
+ const count = single ? '' : `${dim}[${current + 1}/${names.length}]${reset}`;
83
+
84
+ out.write(`\r\x1B[2K ${magenta}${frame}${reset} ${bold}${name}${reset} ${dim}${s.interval}ms${reset} ${count}`);
85
+
86
+ i++;
87
+ ticksOnCurrent++;
88
+
89
+ if (!single && ticksOnCurrent >= TICKS_PER_SPINNER) {
90
+ ticksOnCurrent = 0;
91
+ i = 0;
92
+ current = (current + 1) % names.length;
93
+ }
94
+ }, 80);
@@ -0,0 +1,310 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" data-theme="dark">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>unicode-animations</title>
7
+ <style>
8
+ :root {
9
+ --bg: #131316;
10
+ --surface: #1e1e23;
11
+ --border: #2e2e36;
12
+ --text: #d8d8df;
13
+ --text-2: #a4a4b0;
14
+ --text-3: #6e6e80;
15
+ --accent: #a78bfa;
16
+ --mono: 'SF Mono', 'Cascadia Code', 'Fira Code', 'Menlo', monospace;
17
+ --sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
18
+ }
19
+ [data-theme="light"] {
20
+ --bg: #f7f7fa;
21
+ --surface: #ffffff;
22
+ --border: #e8e8ec;
23
+ --text: #2c2c3a;
24
+ --text-2: #6b6b7b;
25
+ --text-3: #9d9daa;
26
+ --accent: #7c3aed;
27
+ }
28
+
29
+ * { margin: 0; padding: 0; box-sizing: border-box; }
30
+ body {
31
+ font-family: var(--sans);
32
+ background: var(--bg);
33
+ color: var(--text);
34
+ -webkit-font-smoothing: antialiased;
35
+ transition: background 0.3s, color 0.3s;
36
+ }
37
+
38
+ .theme-toggle {
39
+ position: fixed; top: 1rem; right: 1.5rem; z-index: 100;
40
+ background: var(--surface); border: 1px solid var(--border);
41
+ border-radius: 8px; padding: 0.4rem 0.75rem;
42
+ cursor: pointer; font-size: 0.75rem; font-weight: 500;
43
+ color: var(--text-2); display: flex; align-items: center; gap: 0.4rem;
44
+ transition: border-color 0.2s;
45
+ }
46
+ .theme-toggle:hover { border-color: var(--text-2); }
47
+
48
+ header {
49
+ text-align: center;
50
+ padding: 4rem 2rem 1rem;
51
+ }
52
+ header h1 {
53
+ font-size: 2rem;
54
+ font-weight: 600;
55
+ letter-spacing: -0.03em;
56
+ }
57
+ header p {
58
+ margin-top: 0.4rem;
59
+ color: var(--text-3);
60
+ font-size: 0.95rem;
61
+ }
62
+
63
+ .install {
64
+ text-align: center;
65
+ margin: 1.5rem 0 3rem;
66
+ }
67
+ .install code {
68
+ font-family: var(--mono);
69
+ font-size: 0.85rem;
70
+ background: var(--surface);
71
+ border: 1px solid var(--border);
72
+ border-radius: 8px;
73
+ padding: 0.5rem 1.25rem;
74
+ color: var(--text-2);
75
+ }
76
+
77
+ main {
78
+ max-width: 720px;
79
+ margin: 0 auto;
80
+ padding: 0 1.5rem 6rem;
81
+ }
82
+
83
+ .section-label {
84
+ font-size: 0.65rem;
85
+ font-weight: 600;
86
+ text-transform: uppercase;
87
+ letter-spacing: 0.14em;
88
+ color: var(--text-3);
89
+ margin-bottom: 1rem;
90
+ }
91
+
92
+ .grid {
93
+ display: grid;
94
+ grid-template-columns: 1fr 1fr;
95
+ gap: 0;
96
+ }
97
+ @media (max-width: 520px) { .grid { grid-template-columns: 1fr; } }
98
+
99
+ .spinner-row {
100
+ display: flex;
101
+ align-items: center;
102
+ gap: 0.75rem;
103
+ padding: 0.6rem 1rem;
104
+ border-bottom: 1px solid var(--border);
105
+ transition: background 0.15s;
106
+ }
107
+ .spinner-row:hover { background: var(--surface); }
108
+
109
+ .spinner-frame {
110
+ font-family: var(--mono);
111
+ font-size: 1rem;
112
+ width: 5rem;
113
+ text-align: center;
114
+ flex-shrink: 0;
115
+ color: var(--accent);
116
+ white-space: nowrap;
117
+ }
118
+
119
+ .spinner-name {
120
+ font-weight: 500;
121
+ font-size: 0.85rem;
122
+ white-space: nowrap;
123
+ }
124
+
125
+ .spinner-meta {
126
+ font-size: 0.75rem;
127
+ color: var(--text-3);
128
+ margin-left: auto;
129
+ white-space: nowrap;
130
+ }
131
+
132
+ .usage {
133
+ margin-top: 3rem;
134
+ }
135
+ .usage pre {
136
+ font-family: var(--mono);
137
+ font-size: 0.8rem;
138
+ line-height: 1.6;
139
+ background: var(--surface);
140
+ border: 1px solid var(--border);
141
+ border-radius: 12px;
142
+ padding: 1.25rem 1.5rem;
143
+ overflow-x: auto;
144
+ color: var(--text-2);
145
+ }
146
+ .usage pre .kw { color: #c084fc; }
147
+ .usage pre .str { color: #86efac; }
148
+ .usage pre .cm { color: var(--text-3); }
149
+
150
+ footer {
151
+ text-align: center;
152
+ padding: 2rem;
153
+ font-size: 0.75rem;
154
+ color: var(--text-3);
155
+ }
156
+ footer a { color: var(--accent); text-decoration: none; }
157
+ footer a:hover { text-decoration: underline; }
158
+ </style>
159
+ </head>
160
+ <body>
161
+
162
+ <button class="theme-toggle" id="toggle">
163
+ <span id="toggleIcon">☀</span>
164
+ <span id="toggleLabel">Light</span>
165
+ </button>
166
+
167
+ <header>
168
+ <h1>unicode-animations</h1>
169
+ <p>18 braille spinner animations as raw frame data</p>
170
+ </header>
171
+
172
+ <div class="install">
173
+ <code>npm install unicode-animations</code>
174
+ </div>
175
+
176
+ <main>
177
+ <section>
178
+ <div class="section-label">All braille spinners</div>
179
+ <div class="grid" id="spinnerGrid"></div>
180
+ </section>
181
+
182
+ <section class="usage">
183
+ <div class="section-label" style="margin-bottom: 1rem;">Usage</div>
184
+ <pre><span class="kw">import</span> spinners <span class="kw">from</span> <span class="str">'unicode-animations'</span>
185
+
186
+ <span class="kw">const</span> { frames, interval } = spinners.braille
187
+ <span class="kw">let</span> i = 0
188
+
189
+ <span class="kw">const</span> timer = setInterval(() => {
190
+ process.stdout.write(<span class="str">`\r${frames[i++ % frames.length]} Loading...`</span>)
191
+ }, interval)
192
+
193
+ <span class="kw">await</span> doWork()
194
+ clearInterval(timer)
195
+ process.stdout.write(<span class="str">'\r✔ Done.\n'</span>)</pre>
196
+ </section>
197
+ </main>
198
+
199
+ <footer>
200
+ <a href="https://github.com/gunnargray-dev/unicode-animations">GitHub</a>
201
+ &nbsp;·&nbsp; MIT License
202
+ </footer>
203
+
204
+ <script>
205
+ // Spinner data (inlined from the package)
206
+ const BRAILLE_DOT_MAP = [[0x01,0x08],[0x02,0x10],[0x04,0x20],[0x40,0x80]];
207
+ function gridToBraille(grid) {
208
+ const rows = grid.length, cols = grid[0]?.length || 0, cc = Math.ceil(cols / 2);
209
+ let r = '';
210
+ for (let c = 0; c < cc; c++) {
211
+ let code = 0x2800;
212
+ for (let ri = 0; ri < 4 && ri < rows; ri++)
213
+ for (let d = 0; d < 2; d++) { const col = c*2+d; if (col < cols && grid[ri]?.[col]) code |= BRAILLE_DOT_MAP[ri][d]; }
214
+ r += String.fromCodePoint(code);
215
+ }
216
+ return r;
217
+ }
218
+ function makeGrid(r,c) { return Array.from({length:r},()=>Array(c).fill(false)); }
219
+
220
+ function genScan(){const W=8,H=4,f=[];for(let p=-1;p<W+1;p++){const g=makeGrid(H,W);for(let r=0;r<H;r++)for(let c=0;c<W;c++)if(c===p||c===p-1)g[r][c]=true;f.push(gridToBraille(g))}return f}
221
+ function genRain(){const W=8,H=4,n=12,f=[],o=[0,3,1,5,2,7,4,6];for(let i=0;i<n;i++){const g=makeGrid(H,W);for(let c=0;c<W;c++){const r=(i+o[c])%(H+2);if(r<H)g[r][c]=true}f.push(gridToBraille(g))}return f}
222
+ function genScanLine(){const W=6,H=4,f=[],p=[0,1,2,3,2,1];for(const row of p){const g=makeGrid(H,W);for(let c=0;c<W;c++){g[row][c]=true;if(row>0)g[row-1][c]=(c%2===0)}f.push(gridToBraille(g))}return f}
223
+ function genPulse(){const W=6,H=4,f=[],cx=W/2-0.5,cy=H/2-0.5;for(const r of[0.5,1.2,2,3,3.5]){const g=makeGrid(H,W);for(let row=0;row<H;row++)for(let col=0;col<W;col++)if(Math.abs(Math.sqrt((col-cx)**2+(row-cy)**2)-r)<0.9)g[row][col]=true;f.push(gridToBraille(g))}return f}
224
+ function genSnake(){const W=4,H=4,path=[];for(let r=0;r<H;r++)if(r%2===0)for(let c=0;c<W;c++)path.push([r,c]);else for(let c=W-1;c>=0;c--)path.push([r,c]);const f=[];for(let i=0;i<path.length;i++){const g=makeGrid(H,W);for(let t=0;t<4;t++){const idx=(i-t+path.length)%path.length;g[path[idx][0]][path[idx][1]]=true}f.push(gridToBraille(g))}return f}
225
+ function genSparkle(){const ps=[[1,0,0,1,0,0,1,0,0,0,1,0,0,1,0,0,0,1,0,0,1,0,0,1,1,0,0,0,0,1,0,0],[0,1,0,0,1,0,0,1,1,0,0,1,0,0,0,1,0,0,0,1,0,1,0,0,0,0,1,0,1,0,1,0],[0,0,1,0,0,1,0,0,0,1,0,0,0,0,1,0,1,0,1,0,0,0,0,1,0,1,0,1,0,0,0,1],[1,0,0,0,0,0,1,1,0,0,1,0,1,0,0,0,0,0,0,0,1,0,1,0,1,0,0,1,0,0,1,0],[0,0,0,1,1,0,0,0,0,1,0,0,0,1,0,1,1,0,0,1,0,0,0,0,0,1,0,0,0,1,0,1],[0,1,1,0,0,0,0,1,0,0,0,1,0,0,1,0,0,1,0,0,0,1,0,0,0,0,1,0,1,0,0,0]];const W=8,H=4,f=[];for(const p of ps){const g=makeGrid(H,W);for(let r=0;r<H;r++)for(let c=0;c<W;c++)g[r][c]=!!p[r*W+c];f.push(gridToBraille(g))}return f}
226
+ function genCascade(){const W=8,H=4,f=[];for(let o=-2;o<W+H;o++){const g=makeGrid(H,W);for(let r=0;r<H;r++)for(let c=0;c<W;c++)if(c+r===o||c+r===o-1)g[r][c]=true;f.push(gridToBraille(g))}return f}
227
+ function genColumns(){const W=6,H=4,f=[];for(let col=0;col<W;col++)for(let ft=H-1;ft>=0;ft--){const g=makeGrid(H,W);for(let pc=0;pc<col;pc++)for(let r=0;r<H;r++)g[r][pc]=true;for(let r=ft;r<H;r++)g[r][col]=true;f.push(gridToBraille(g))}const full=makeGrid(H,W);for(let r=0;r<H;r++)for(let c=0;c<W;c++)full[r][c]=true;f.push(gridToBraille(full));f.push(gridToBraille(makeGrid(H,W)));return f}
228
+ function genOrbit(){const W=2,H=4,path=[[0,0],[0,1],[1,1],[2,1],[3,1],[3,0],[2,0],[1,0]],f=[];for(let i=0;i<path.length;i++){const g=makeGrid(H,W);g[path[i][0]][path[i][1]]=true;const t=(i-1+path.length)%path.length;g[path[t][0]][path[t][1]]=true;f.push(gridToBraille(g))}return f}
229
+ function genBreathe(){const stages=[[],[[1,0]],[[0,1],[2,0]],[[0,0],[1,1],[3,0]],[[0,0],[1,1],[2,0],[3,1]],[[0,0],[0,1],[1,1],[2,0],[3,1]],[[0,0],[0,1],[1,0],[2,1],[3,0],[3,1]],[[0,0],[0,1],[1,0],[1,1],[2,0],[3,0],[3,1]],[[0,0],[0,1],[1,0],[1,1],[2,0],[2,1],[3,0],[3,1]]];const seq=[...stages,...stages.slice().reverse().slice(1)],f=[];for(const dots of seq){const g=makeGrid(4,2);for(const[r,c]of dots)g[r][c]=true;f.push(gridToBraille(g))}return f}
230
+ function genWaveRows(){const W=8,H=4,n=16,f=[];for(let i=0;i<n;i++){const g=makeGrid(H,W);for(let c=0;c<W;c++){const row=Math.round((Math.sin((i-c*0.5)*0.8)+1)/2*(H-1));g[row][c]=true;if(row>0)g[row-1][c]=(i+c)%3===0}f.push(gridToBraille(g))}return f}
231
+ function genCheckerboard(){const W=6,H=4,f=[];for(let p=0;p<4;p++){const g=makeGrid(H,W);for(let r=0;r<H;r++)for(let c=0;c<W;c++)g[r][c]=p<2?(r+c+p)%2===0:(r+c+p)%3===0;f.push(gridToBraille(g))}return f}
232
+ function genHelix(){const W=8,H=4,n=16,f=[];for(let i=0;i<n;i++){const g=makeGrid(H,W);for(let c=0;c<W;c++){const ph=(i+c)*(Math.PI/4);g[Math.round((Math.sin(ph)+1)/2*(H-1))][c]=true;g[Math.round((Math.sin(ph+Math.PI)+1)/2*(H-1))][c]=true}f.push(gridToBraille(g))}return f}
233
+ function genFillSweep(){const W=4,H=4,f=[];for(let row=H-1;row>=0;row--){const g=makeGrid(H,W);for(let r=row;r<H;r++)for(let c=0;c<W;c++)g[r][c]=true;f.push(gridToBraille(g))}const full=makeGrid(H,W);for(let r=0;r<H;r++)for(let c=0;c<W;c++)full[r][c]=true;f.push(gridToBraille(full));f.push(gridToBraille(full));for(let row=0;row<H;row++){const g=makeGrid(H,W);for(let r=row+1;r<H;r++)for(let c=0;c<W;c++)g[r][c]=true;f.push(gridToBraille(g))}f.push(gridToBraille(makeGrid(H,W)));return f}
234
+ function genDiagSwipe(){const W=4,H=4,f=[],mx=W+H-2;for(let d=0;d<=mx;d++){const g=makeGrid(H,W);for(let r=0;r<H;r++)for(let c=0;c<W;c++)if(r+c<=d)g[r][c]=true;f.push(gridToBraille(g))}const full=makeGrid(H,W);for(let r=0;r<H;r++)for(let c=0;c<W;c++)full[r][c]=true;f.push(gridToBraille(full));for(let d=0;d<=mx;d++){const g=makeGrid(H,W);for(let r=0;r<H;r++)for(let c=0;c<W;c++)if(r+c>d)g[r][c]=true;f.push(gridToBraille(g))}f.push(gridToBraille(makeGrid(H,W)));return f}
235
+
236
+ const spinners = {
237
+ braille:{frames:['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'],interval:80},
238
+ braillewave:{frames:['⠁⠂⠄⡀','⠂⠄⡀⢀','⠄⡀⢀⠠','⡀⢀⠠⠐','⢀⠠⠐⠈','⠠⠐⠈⠁','⠐⠈⠁⠂','⠈⠁⠂⠄'],interval:100},
239
+ dna:{frames:['⠋⠉⠙⠚','⠉⠙⠚⠒','⠙⠚⠒⠂','⠚⠒⠂⠂','⠒⠂⠂⠒','⠂⠂⠒⠲','⠂⠒⠲⠴','⠒⠲⠴⠤','⠲⠴⠤⠄','⠴⠤⠄⠋','⠤⠄⠋⠉','⠄⠋⠉⠙'],interval:80},
240
+ scan:{frames:genScan(),interval:70},
241
+ rain:{frames:genRain(),interval:100},
242
+ scanline:{frames:genScanLine(),interval:120},
243
+ pulse:{frames:genPulse(),interval:180},
244
+ snake:{frames:genSnake(),interval:80},
245
+ sparkle:{frames:genSparkle(),interval:150},
246
+ cascade:{frames:genCascade(),interval:60},
247
+ columns:{frames:genColumns(),interval:60},
248
+ orbit:{frames:genOrbit(),interval:100},
249
+ breathe:{frames:genBreathe(),interval:100},
250
+ waverows:{frames:genWaveRows(),interval:90},
251
+ checkerboard:{frames:genCheckerboard(),interval:250},
252
+ helix:{frames:genHelix(),interval:80},
253
+ fillsweep:{frames:genFillSweep(),interval:100},
254
+ diagswipe:{frames:genDiagSwipe(),interval:60},
255
+ };
256
+
257
+ // Build grid
258
+ const grid = document.getElementById('spinnerGrid');
259
+ const els = {};
260
+
261
+ Object.entries(spinners).forEach(([name, s]) => {
262
+ const row = document.createElement('div');
263
+ row.className = 'spinner-row';
264
+
265
+ const frame = document.createElement('span');
266
+ frame.className = 'spinner-frame';
267
+ frame.textContent = s.frames[0];
268
+
269
+ const label = document.createElement('span');
270
+ label.className = 'spinner-name';
271
+ label.textContent = name;
272
+
273
+ const meta = document.createElement('span');
274
+ meta.className = 'spinner-meta';
275
+ meta.textContent = `${s.frames.length}f · ${s.interval}ms`;
276
+
277
+ row.append(frame, label, meta);
278
+ grid.appendChild(row);
279
+ els[name] = frame;
280
+ });
281
+
282
+ // Animate — group by interval for efficiency
283
+ const byInterval = {};
284
+ Object.entries(spinners).forEach(([name, s]) => {
285
+ if (!byInterval[s.interval]) byInterval[s.interval] = [];
286
+ byInterval[s.interval].push({ name, frames: s.frames, i: 0 });
287
+ });
288
+
289
+ Object.entries(byInterval).forEach(([interval, group]) => {
290
+ setInterval(() => {
291
+ group.forEach(s => {
292
+ s.i = (s.i + 1) % s.frames.length;
293
+ els[s.name].textContent = s.frames[s.i];
294
+ });
295
+ }, Number(interval));
296
+ });
297
+
298
+ // Theme toggle
299
+ const toggle = document.getElementById('toggle');
300
+ const icon = document.getElementById('toggleIcon');
301
+ const label = document.getElementById('toggleLabel');
302
+ toggle.addEventListener('click', () => {
303
+ const dark = document.documentElement.dataset.theme === 'dark';
304
+ document.documentElement.dataset.theme = dark ? 'light' : 'dark';
305
+ icon.textContent = dark ? '☾' : '☀';
306
+ label.textContent = dark ? 'Dark' : 'Light';
307
+ });
308
+ </script>
309
+ </body>
310
+ </html>
@@ -1,12 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // Postinstall animation — showcases a few spinners side-by-side for ~1.5s
4
- // Skips gracefully in CI or non-interactive environments
5
-
6
- // npm 7+ captures ALL lifecycle script stdio (stdout + stderr) and swallows
7
- // output on success. The only way to reach the real terminal is /dev/tty.
8
3
  const fs = require('fs');
9
4
  const tty = require('tty');
5
+ const path = require('path');
10
6
 
11
7
  const ci = process.env.CI || process.env.CONTINUOUS_INTEGRATION || process.env.GITHUB_ACTIONS;
12
8
  if (ci) process.exit(0);
@@ -16,52 +12,106 @@ try {
16
12
  const fd = fs.openSync('/dev/tty', 'w');
17
13
  out = new tty.WriteStream(fd);
18
14
  } catch {
19
- // No controlling terminal (CI, Docker, piped, etc.)
20
15
  process.exit(0);
21
16
  }
22
17
 
18
+ let S;
23
19
  try {
24
- const DURATION = 1500;
25
- const INTERVAL = 80;
20
+ const mod = require(path.join(__dirname, '..', 'dist', 'index.cjs'));
21
+ S = mod.spinners || mod.default;
22
+ if (!S || !S.braille) throw new Error();
23
+ } catch {
24
+ process.exit(0);
25
+ }
26
26
 
27
- // Inline a few single-char spinners to avoid importing dist (may not exist yet)
28
- const spinners = [
29
- { frames: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'] }, // braille
30
- { frames: ['◜', '◠', '◝', '◞', '◡', '◟'] }, // arc
31
- { frames: ['◐', '◓', '◑', '◒'] }, // halfmoon
32
- { frames: ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█', '▇', '▆', '▅', '▄', '▃', '▂'] }, // blocks
33
- { frames: ['|', '/', '—', '\\'] }, // line
34
- ];
27
+ try {
28
+ const DURATION = 3000;
29
+ const INTERVAL = 80;
35
30
 
31
+ const d = '\x1B[2m';
32
+ const b = '\x1B[1m';
33
+ const r = '\x1B[0m';
34
+ const c = '\x1B[36m';
35
+ const g = '\x1B[32m';
36
+ const w = '\x1B[37m';
37
+ const m = '\x1B[35m';
36
38
  const hide = '\x1B[?25l';
37
39
  const show = '\x1B[?25h';
38
- const clearLine = '\x1B[2K\r';
39
- const bold = '\x1B[1m';
40
- const green = '\x1B[32m';
41
- const reset = '\x1B[0m';
42
40
 
43
41
  out.write(hide);
44
-
45
42
  const cleanup = () => { try { out.write(show); } catch {} };
46
43
  process.on('SIGINT', () => { cleanup(); process.exit(0); });
47
44
 
48
- let tick = 0;
45
+ // Header
46
+ out.write(`
47
+ ${b}${w} ██╗ ██╗███╗ ██╗██╗ ██████╗ ██████╗ ██████╗ ███████╗${r}
48
+ ${b}${w} ██║ ██║████╗ ██║██║██╔════╝██╔═══██╗██╔══██╗██╔════╝${r}
49
+ ${b}${w} ██║ ██║██╔██╗ ██║██║██║ ██║ ██║██║ ██║█████╗${r}
50
+ ${b}${w} ██║ ██║██║╚██╗██║██║██║ ██║ ██║██║ ██║██╔══╝${r}
51
+ ${b}${w} ╚██████╔╝██║ ╚████║██║╚██████╗╚██████╔╝██████╔╝███████╗${r}
52
+ ${d} ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝${r}
53
+ ${b}${c} b r a i l l e a n i m a t i o n s${r}
54
+
55
+ `);
56
+
57
+ // Braille spinners only, 2 columns of 9
58
+ const left = [
59
+ ['Braille', S.braille],
60
+ ['Orbit', S.orbit],
61
+ ['Breathe', S.breathe],
62
+ ['Snake', S.snake],
63
+ ['Fill Sweep', S.fillsweep],
64
+ ['Diag Swipe', S.diagswipe],
65
+ ['Pulse', S.pulse],
66
+ ['Scanline', S.scanline],
67
+ ['Columns', S.columns],
68
+ ];
69
+
70
+ const right = [
71
+ ['Checkerboard', S.checkerboard],
72
+ ['Scan', S.scan],
73
+ ['Rain', S.rain],
74
+ ['Sparkle', S.sparkle],
75
+ ['Cascade', S.cascade],
76
+ ['Wave Rows', S.waverows],
77
+ ['Helix', S.helix],
78
+ ['Braille Wave', S.braillewave],
79
+ ['DNA', S.dna],
80
+ ];
81
+
82
+ const ROWS = left.length;
83
+
84
+ function pad(str, n) { return str + ' '.repeat(Math.max(0, n - str.length)); }
85
+
86
+ function renderGrid(tick) {
87
+ let buf = '';
88
+ for (let i = 0; i < ROWS; i++) {
89
+ const [ln, ls] = left[i];
90
+ const [rn, rs] = right[i];
91
+ const lf = ls.frames[tick % ls.frames.length];
92
+ const rf = rs.frames[tick % rs.frames.length];
93
+ buf += ` ${m}${pad(lf, 3)}${r} ${d}${pad(ln, 12)}${r} ${m}${pad(rf, 12)}${r} ${d}${rn}${r}\n`;
94
+ }
95
+ return buf;
96
+ }
97
+
98
+ out.write(renderGrid(0));
99
+
100
+ let tick = 1;
49
101
  const start = Date.now();
50
102
 
51
103
  const timer = setInterval(() => {
52
104
  if (Date.now() - start >= DURATION) {
53
105
  clearInterval(timer);
54
- const done = `${clearLine} ${green}${bold}✔${reset} ${bold}unicode-animations${reset} — 22 spinners ready\n`;
55
- out.write(done);
106
+ out.write(`\n ${g}${b}✔${r} ${b}unicode-animations${r} ${d}18 braille spinners + 4 classics${r}\n\n`);
56
107
  cleanup();
57
108
  return;
58
109
  }
59
-
60
- const chars = spinners.map(s => s.frames[tick % s.frames.length]);
61
- out.write(`${clearLine} ${chars.join(' ')}`);
110
+ out.write(`\x1B[${ROWS}A`);
111
+ out.write(renderGrid(tick));
62
112
  tick++;
63
113
  }, INTERVAL);
64
114
  } catch {
65
- // Never block install
115
+ try { out.write('\x1B[?25h'); } catch {}
66
116
  process.exit(0);
67
117
  }