ansimax 1.0.0 → 1.1.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.
@@ -0,0 +1,96 @@
1
+ // ─────────────────────────────────────────────
2
+ // EXAMPLE 1 — CLI installer with hierarchical tasks
3
+ //
4
+ // Demonstrates a realistic project setup flow:
5
+ // - Banner intro with theme gradient
6
+ // - Hierarchical tasks (parent + subtasks with rollup)
7
+ // - Status indicators with custom icons
8
+ // - Box for final summary
9
+ //
10
+ // Run:
11
+ // npx ts-node examples/01-cli-installer.ts
12
+ // ─────────────────────────────────────────────
13
+
14
+ import {
15
+ ascii,
16
+ themes,
17
+ loader,
18
+ components,
19
+ configure,
20
+ sleep,
21
+ } from '../dist/index.js';
22
+
23
+ // Configure global defaults
24
+ configure({
25
+ theme: 'dracula',
26
+ animationSpeed: 'fast',
27
+ });
28
+
29
+ // Simulated work
30
+ const work = (ms: number, fail = false) => async (): Promise<string> => {
31
+ await sleep(ms);
32
+ if (fail) throw new Error('Simulated failure');
33
+ return 'ok';
34
+ };
35
+
36
+ const main = async (): Promise<void> => {
37
+ // ── Intro banner ──────────────────────────────
38
+ console.log();
39
+ console.log(themes.banner('CREATE-APP', { font: 'small' }));
40
+ console.log();
41
+ console.log(components.section(themes.primary('Initializing project'), { width: 60 }));
42
+ console.log();
43
+
44
+ // ── Hierarchical task list ────────────────────
45
+ const results = await loader.tasks([
46
+ {
47
+ text: 'Setup environment',
48
+ fn: work(300),
49
+ subtasks: [
50
+ { text: 'Detect Node version', fn: work(150) },
51
+ { text: 'Verify npm registry', fn: work(200) },
52
+ { text: 'Create project dir', fn: work(100) },
53
+ ],
54
+ },
55
+ {
56
+ text: 'Install dependencies',
57
+ fn: work(400),
58
+ subtasks: [
59
+ { text: 'Resolve package tree', fn: work(250) },
60
+ { text: 'Download tarballs', fn: work(500) },
61
+ { text: 'Build native modules', fn: work(300) },
62
+ ],
63
+ },
64
+ {
65
+ text: 'Run post-install hooks',
66
+ fn: work(200),
67
+ subtasks: [
68
+ { text: 'Generate types', fn: work(150) },
69
+ { text: 'Lint workspace', fn: work(180) },
70
+ ],
71
+ },
72
+ ]);
73
+
74
+ // ── Final summary box ─────────────────────────
75
+ console.log();
76
+ const succeeded = results.filter((r) => r.success).length;
77
+ const failed = results.length - succeeded;
78
+
79
+ const summary = [
80
+ components.status('success', `${succeeded} steps completed`),
81
+ failed > 0
82
+ ? components.status('error', `${failed} steps failed`)
83
+ : components.status('info', 'No errors'),
84
+ '',
85
+ themes.muted('Next steps:'),
86
+ ' ' + themes.accent('cd my-app && npm run dev'),
87
+ ].join('\n');
88
+
89
+ console.log(components.box(summary, { borderStyle: 'rounded', padding: 1 }));
90
+ console.log();
91
+ };
92
+
93
+ main().catch((err) => {
94
+ console.error(themes.error('✗ ' + err.message));
95
+ process.exit(1);
96
+ });
@@ -0,0 +1,152 @@
1
+ // ─────────────────────────────────────────────
2
+ // EXAMPLE 2 — Real-time dashboard with live updates
3
+ //
4
+ // Demonstrates:
5
+ // - frames.live() for sticky bottom-of-screen UI
6
+ // - components.progressBar with gradient
7
+ // - components.table for data display
8
+ // - onResize listener for responsive layout
9
+ // - throttle for rate-limited updates
10
+ // - AbortSignal cleanup
11
+ //
12
+ // Run:
13
+ // npx ts-node examples/02-live-dashboard.ts
14
+ // ─────────────────────────────────────────────
15
+
16
+ import {
17
+ themes,
18
+ components,
19
+ frames,
20
+ onResize,
21
+ throttle,
22
+ termSize,
23
+ sleep,
24
+ cursor,
25
+ screen,
26
+ write,
27
+ } from '../dist/index.js';
28
+
29
+ interface Stat {
30
+ service: string;
31
+ status: 'up' | 'down' | 'pending';
32
+ latency: number;
33
+ load: number; // 0..100
34
+ }
35
+
36
+ // Simulated metrics that drift over time
37
+ const stats: Stat[] = [
38
+ { service: 'api-gateway', status: 'up', latency: 14, load: 32 },
39
+ { service: 'auth-service', status: 'up', latency: 22, load: 58 },
40
+ { service: 'database', status: 'pending', latency: 0, load: 0 },
41
+ { service: 'cdn-edge', status: 'up', latency: 8, load: 18 },
42
+ { service: 'cache-layer', status: 'up', latency: 3, load: 71 },
43
+ ];
44
+
45
+ const drift = (): void => {
46
+ for (const s of stats) {
47
+ if (s.status === 'pending') {
48
+ // Pending services come online randomly
49
+ if (Math.random() < 0.1) {
50
+ s.status = 'up';
51
+ s.latency = 10 + Math.floor(Math.random() * 30);
52
+ }
53
+ } else if (s.status === 'up') {
54
+ // Slight load + latency wander
55
+ s.load = Math.max(0, Math.min(100, s.load + (Math.random() - 0.5) * 8));
56
+ s.latency = Math.max(1, s.latency + (Math.random() - 0.5) * 3);
57
+ // Tiny chance of going down
58
+ if (Math.random() < 0.005) s.status = 'down';
59
+ }
60
+ }
61
+ };
62
+
63
+ const renderFrame = (): string => {
64
+ const { cols } = termSize();
65
+ const innerWidth = Math.max(40, Math.min(cols - 4, 80));
66
+
67
+ // ── Header with theme gradient ──
68
+ const title = themes.banner('STATUS', { font: 'small', perCharColor: false });
69
+
70
+ // ── Service rows ──
71
+ const rows: string[][] = [
72
+ ['Service', 'Status', 'Latency', 'Load'],
73
+ ...stats.map((s) => {
74
+ const statusIcon =
75
+ s.status === 'up' ? themes.accent('● up')
76
+ : s.status === 'down' ? themes.error('● down')
77
+ : themes.warning('● pending');
78
+
79
+ const latencyStr = s.status === 'up'
80
+ ? (s.latency < 20 ? themes.accent : s.latency < 50 ? themes.warning : themes.error)
81
+ (`${s.latency.toFixed(0)}ms`)
82
+ : themes.muted('—');
83
+
84
+ const loadBar = s.status === 'up'
85
+ ? components.progressBar(s.load, {
86
+ width: 20,
87
+ gradient: ['#00ff88', '#fdcb6e', '#ff6b6b'],
88
+ showPercentage: true,
89
+ })
90
+ : themes.muted('—');
91
+
92
+ return [s.service, statusIcon, latencyStr, loadBar];
93
+ }),
94
+ ];
95
+
96
+ const table = components.table(rows, { borderStyle: 'rounded', maxColWidth: innerWidth / 4 });
97
+
98
+ // ── Footer ──
99
+ const upCount = stats.filter((s) => s.status === 'up').length;
100
+ const downCount = stats.filter((s) => s.status === 'down').length;
101
+ const footer = themes.muted(
102
+ `${upCount} up · ${downCount} down · ${stats.length} total · ` +
103
+ `${cols}×${termSize().rows} terminal · Ctrl+C to exit`,
104
+ );
105
+
106
+ return [title, '', table, '', footer].join('\n');
107
+ };
108
+
109
+ const main = async (): Promise<void> => {
110
+ // Hide cursor while live UI runs
111
+ write(cursor.hide());
112
+
113
+ // Throttled resize redraw — coalesce rapid resize events into one render
114
+ const onAnyResize = throttle(() => {
115
+ // Frame engine will pick up the new size on its next tick
116
+ }, 100);
117
+ const offResize = onResize(onAnyResize);
118
+
119
+ // Start live engine at 4 fps (smooth but not CPU-hungry)
120
+ const live = frames.live({ fps: 4, autoStart: true });
121
+
122
+ // Drift loop
123
+ const ctrl = new AbortController();
124
+ process.on('SIGINT', () => {
125
+ ctrl.abort();
126
+ live.stop({ clear: false });
127
+ offResize();
128
+ write(cursor.show());
129
+ write('\n');
130
+ console.log(themes.muted('Dashboard stopped.'));
131
+ process.exit(0);
132
+ });
133
+
134
+ try {
135
+ while (!ctrl.signal.aborted) {
136
+ drift();
137
+ live.update(renderFrame());
138
+ await sleep(250, { signal: ctrl.signal });
139
+ }
140
+ } catch {
141
+ // Aborted — fall through to cleanup
142
+ } finally {
143
+ live.stop({ clear: false });
144
+ offResize();
145
+ write(cursor.show());
146
+ }
147
+ };
148
+
149
+ main().catch((err) => {
150
+ console.error(themes.error('✗ ' + (err as Error).message));
151
+ process.exit(1);
152
+ });
@@ -0,0 +1,170 @@
1
+ // ─────────────────────────────────────────────
2
+ // EXAMPLE 3 — Pixel art game loop
3
+ //
4
+ // Demonstrates:
5
+ // - Canvas with dirty-region rendering for FPS
6
+ // - Sprite drawing with alpha blending
7
+ // - Gradient backgrounds with Bayer dithering
8
+ // - Frame-rate locked game loop (drift-corrected)
9
+ // - Braille mode for high-resolution detail
10
+ //
11
+ // A bouncing rocket on a sunset gradient with a star field.
12
+ //
13
+ // Run:
14
+ // npx ts-node examples/03-pixel-art-game.ts
15
+ // ─────────────────────────────────────────────
16
+
17
+ import {
18
+ createCanvas,
19
+ images,
20
+ themes,
21
+ cursor,
22
+ screen,
23
+ write,
24
+ sleep,
25
+ termSize,
26
+ type RGBA,
27
+ } from '../dist/index.js';
28
+
29
+ const WIDTH = 60;
30
+ const HEIGHT = 30;
31
+
32
+ // ── Build static background once ───────────────
33
+ const buildBackground = (): ReturnType<typeof createCanvas> => {
34
+ const canvas = createCanvas(WIDTH, HEIGHT);
35
+
36
+ // Sunset gradient with Bayer dithering for smooth bands
37
+ const gradStr = images.gradientRect({
38
+ width: WIDTH,
39
+ height: HEIGHT,
40
+ colors: ['#1a1a2e', '#16213e', '#fd7272', '#f9ca24'],
41
+ style: 'vertical',
42
+ dither: 'bayer',
43
+ });
44
+ // gradientRect returns rendered text; instead we build pixels manually
45
+ // so we can composite sprites on top.
46
+ for (let y = 0; y < HEIGHT; y++) {
47
+ for (let x = 0; x < WIDTH; x++) {
48
+ // Simple vertical sunset
49
+ const t = y / (HEIGHT - 1);
50
+ const r = Math.round(0x1a + (0xf9 - 0x1a) * t);
51
+ const g = Math.round(0x1a + (0xca - 0x1a) * t);
52
+ const b = Math.round(0x2e + (0x24 - 0x2e) * t);
53
+ canvas.set(x, y, { r, g, b });
54
+ }
55
+ }
56
+
57
+ // Star field — random white dots in upper half
58
+ for (let i = 0; i < 30; i++) {
59
+ const x = Math.floor(Math.random() * WIDTH);
60
+ const y = Math.floor(Math.random() * (HEIGHT / 2));
61
+ const brightness = 150 + Math.floor(Math.random() * 100);
62
+ canvas.set(x, y, { r: brightness, g: brightness, b: brightness });
63
+ }
64
+
65
+ return canvas;
66
+ };
67
+
68
+ // ── Rocket sprite (5×5) with alpha edges for soft blending ──
69
+ const ROCKET = [
70
+ [null, null, { r: 255, g: 255, b: 255 }, null, null ],
71
+ [null, { r: 255, g: 100, b: 100 }, { r: 255, g: 200, b: 200 }, { r: 255, g: 100, b: 100 }, null ],
72
+ [{ r: 200, g: 50, b: 50, a: 0.7 } as RGBA, { r: 255, g: 100, b: 100 }, { r: 255, g: 150, b: 150 }, { r: 255, g: 100, b: 100 }, { r: 200, g: 50, b: 50, a: 0.7 } as RGBA],
73
+ [null, { r: 255, g: 200, b: 50 }, { r: 255, g: 240, b: 100 }, { r: 255, g: 200, b: 50 }, null ],
74
+ [null, null, { r: 255, g: 150, b: 50, a: 0.6 } as RGBA, null, null ],
75
+ ];
76
+
77
+ // ── Main loop ──────────────────────────────────
78
+ const main = async (): Promise<void> => {
79
+ const { rows } = termSize();
80
+ if (rows < HEIGHT + 5) {
81
+ console.log(themes.error(`Terminal too small. Needs at least ${HEIGHT + 5} rows.`));
82
+ process.exit(1);
83
+ }
84
+
85
+ write(screen.clear());
86
+ write(cursor.hide());
87
+ write(cursor.to(1, 1));
88
+ console.log(themes.banner('ROCKET', { font: 'small' }));
89
+
90
+ const bg = buildBackground();
91
+
92
+ let rocketX = WIDTH / 2;
93
+ let rocketY = HEIGHT - 8;
94
+ let vx = 0.4;
95
+ let vy = -0.25;
96
+
97
+ const ctrl = new AbortController();
98
+ process.on('SIGINT', () => {
99
+ ctrl.abort();
100
+ write(cursor.show());
101
+ write(cursor.to(1, HEIGHT + 8));
102
+ console.log(themes.muted('\nGame stopped.'));
103
+ process.exit(0);
104
+ });
105
+
106
+ // Frame loop — drift-corrected via wall clock
107
+ const FRAME_MS = 60; // ~16 FPS
108
+ const startTime = Date.now();
109
+ let frame = 0;
110
+
111
+ const FPS_HIST: number[] = [];
112
+ let lastFrameTime = Date.now();
113
+
114
+ while (!ctrl.signal.aborted) {
115
+ // Clone background to a frame canvas (so sprites don't mutate the bg)
116
+ const frameCanvas = createCanvas(WIDTH, HEIGHT);
117
+ for (let y = 0; y < HEIGHT; y++) {
118
+ for (let x = 0; x < WIDTH; x++) {
119
+ frameCanvas.set(x, y, bg.get(x, y));
120
+ }
121
+ }
122
+
123
+ // Update rocket physics
124
+ rocketX += vx;
125
+ rocketY += vy;
126
+ if (rocketX <= 2 || rocketX >= WIDTH - 5) vx = -vx;
127
+ if (rocketY <= 2 || rocketY >= HEIGHT - 5) vy = -vy;
128
+
129
+ // Draw the rocket with alpha blending
130
+ frameCanvas.drawSprite(Math.round(rocketX), Math.round(rocketY), ROCKET);
131
+
132
+ // FPS calc
133
+ const now = Date.now();
134
+ const delta = now - lastFrameTime;
135
+ lastFrameTime = now;
136
+ if (delta > 0) {
137
+ FPS_HIST.push(1000 / delta);
138
+ if (FPS_HIST.length > 30) FPS_HIST.shift();
139
+ }
140
+ const avgFps = FPS_HIST.reduce((s, n) => s + n, 0) / Math.max(1, FPS_HIST.length);
141
+
142
+ // Render
143
+ write(cursor.to(1, 7));
144
+ write(frameCanvas.render());
145
+ write(cursor.to(1, HEIGHT + 8));
146
+ write(themes.muted(
147
+ `Frame ${frame.toString().padStart(4)} · ` +
148
+ `${avgFps.toFixed(1)} fps · ` +
149
+ `Ctrl+C to exit`,
150
+ ));
151
+
152
+ // Drift-correct sleep
153
+ frame++;
154
+ const targetTime = startTime + frame * FRAME_MS;
155
+ const waitMs = Math.max(0, targetTime - Date.now());
156
+ try {
157
+ await sleep(waitMs, { signal: ctrl.signal });
158
+ } catch {
159
+ break;
160
+ }
161
+ }
162
+
163
+ write(cursor.show());
164
+ };
165
+
166
+ main().catch((err) => {
167
+ write(cursor.show());
168
+ console.error(themes.error('✗ ' + (err as Error).message));
169
+ process.exit(1);
170
+ });
@@ -0,0 +1,163 @@
1
+ // ─────────────────────────────────────────────
2
+ // EXAMPLE 4 — Interactive prompt with menu + multi-loader
3
+ //
4
+ // Demonstrates:
5
+ // - components.menu (interactive arrow-key navigation)
6
+ // - MENU_CANCELLED handling
7
+ // - loader.multi() — concurrent named operations
8
+ // - configure() side effects + onConfigChange
9
+ // - Theme switching at runtime
10
+ // - createTheme() for isolated theme instances
11
+ //
12
+ // Walks the user through choosing a theme, then runs parallel
13
+ // "deployment" operations under a multi-loader.
14
+ //
15
+ // Run:
16
+ // npx ts-node examples/04-interactive-deploy.ts
17
+ // ─────────────────────────────────────────────
18
+
19
+ import {
20
+ components,
21
+ loader,
22
+ themes,
23
+ createTheme,
24
+ configure,
25
+ onConfigChange,
26
+ sleep,
27
+ MENU_CANCELLED,
28
+ } from '../dist/index.js';
29
+
30
+ const main = async (): Promise<void> => {
31
+ // ── Subscribe to config changes globally ──
32
+ const offChange = onConfigChange((c) => {
33
+ // Each time the theme changes, the global `themes` follows automatically
34
+ // thanks to configure() side effects. We just log it.
35
+ console.log(themes.muted(` [config] theme=${c.theme} colorMode=${c.colorMode}`));
36
+ });
37
+
38
+ // ── Step 1: Banner ─────────────────────────────
39
+ console.log();
40
+ console.log(themes.banner('DEPLOY', { font: 'small' }));
41
+ console.log(components.section(themes.primary('Configure deployment'), { width: 60 }));
42
+ console.log();
43
+
44
+ // ── Step 2: Pick a theme via menu ─────────────
45
+ console.log(themes.text('Select a theme for the output:'));
46
+ console.log();
47
+
48
+ const themeChoices = ['dracula', 'nord', 'monokai', 'matrix', 'cyberpunk'];
49
+ const choice = await components.menu(themeChoices, {
50
+ title: ' Available themes',
51
+ pointer: '▸',
52
+ });
53
+
54
+ if (choice === MENU_CANCELLED) {
55
+ console.log();
56
+ console.log(themes.warning('⚠ Cancelled by user'));
57
+ offChange();
58
+ return;
59
+ }
60
+
61
+ const themeName = themeChoices[choice as number]!;
62
+ configure({ theme: themeName });
63
+ console.log();
64
+ console.log(themes.accent(`✓ Theme set to ${themeName}`));
65
+ console.log();
66
+
67
+ // ── Step 3: Pick deployment regions (multi-select) ──
68
+ const regionChoices = ['us-east-1', 'us-west-2', 'eu-west-1', 'ap-south-1', 'sa-east-1'];
69
+ console.log(themes.text('Select target regions (Space to toggle, Enter to confirm):'));
70
+ console.log();
71
+
72
+ const regions = await components.menu(regionChoices, {
73
+ title: ' Regions',
74
+ pointer: '▸',
75
+ multiSelect: true,
76
+ });
77
+
78
+ if (regions === MENU_CANCELLED) {
79
+ console.log();
80
+ console.log(themes.warning('⚠ Cancelled by user'));
81
+ offChange();
82
+ return;
83
+ }
84
+
85
+ const selectedRegions = (regions as number[]).map((i) => regionChoices[i]!);
86
+ if (selectedRegions.length === 0) {
87
+ console.log(themes.warning('⚠ No regions selected — aborting'));
88
+ offChange();
89
+ return;
90
+ }
91
+
92
+ console.log();
93
+ console.log(components.box(
94
+ themes.primary('Plan:') + '\n' +
95
+ selectedRegions.map((r) => ` • ${r}`).join('\n'),
96
+ { borderStyle: 'rounded', padding: 1 },
97
+ ));
98
+ console.log();
99
+
100
+ // ── Step 4: Multi-loader for parallel deployments ──
101
+ console.log(themes.text('Starting parallel deployments...'));
102
+ console.log();
103
+
104
+ const m = loader.multi();
105
+ const items = selectedRegions.map((region) => {
106
+ const item = m.add(`Deploying to ${region}`, { color: '#48dbfb' });
107
+ return { region, item };
108
+ });
109
+
110
+ // Run each region in parallel
111
+ await Promise.all(items.map(async ({ region, item }) => {
112
+ // Stage 1: build
113
+ item.update(`[${region}] Building image...`);
114
+ await sleep(400 + Math.random() * 600);
115
+
116
+ // Stage 2: upload
117
+ item.update(`[${region}] Uploading...`);
118
+ await sleep(600 + Math.random() * 800);
119
+
120
+ // Stage 3: switch
121
+ item.update(`[${region}] Switching traffic...`);
122
+ await sleep(300 + Math.random() * 400);
123
+
124
+ // Random failure chance for one region
125
+ if (Math.random() < 0.15) {
126
+ item.fail(`[${region}] Failed: health check timeout`);
127
+ } else {
128
+ item.succeed(`[${region}] Live`);
129
+ }
130
+ }));
131
+
132
+ // Wait for the multi-loader to settle (final renders)
133
+ await sleep(150);
134
+
135
+ // ── Step 5: Multi-tenant theme demo ───────────
136
+ // Create an isolated theme instance for the summary
137
+ // (does not affect global state)
138
+ const summary = createTheme('matrix');
139
+
140
+ console.log();
141
+ console.log(components.section(summary.primary('Deployment summary'), { width: 60 }));
142
+ console.log();
143
+
144
+ console.log(components.timeline(
145
+ selectedRegions.map((r) => ({
146
+ label: `Deployed to ${r}`,
147
+ time: new Date().toLocaleTimeString(),
148
+ done: true,
149
+ })),
150
+ ));
151
+
152
+ console.log();
153
+ console.log(summary.muted('Global theme remains: ' + themes.current().name));
154
+ console.log(summary.muted('Isolated summary theme: ' + summary.current().name));
155
+ console.log();
156
+
157
+ offChange();
158
+ };
159
+
160
+ main().catch((err) => {
161
+ console.error(themes.error('✗ ' + (err as Error).message));
162
+ process.exit(1);
163
+ });