ansimax 1.0.0 → 1.1.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.
@@ -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
+ });
@@ -0,0 +1,117 @@
1
+ /**
2
+ * 04 — Trees
3
+ *
4
+ * Run: npx tsx examples/04-trees.ts
5
+ */
6
+
7
+ import {
8
+ tree,
9
+ renderTree,
10
+ walkTree,
11
+ findInTree,
12
+ countNodes,
13
+ mapTree,
14
+ filterTree,
15
+ measureTree,
16
+ color,
17
+ type TreeData,
18
+ } from '../src/index.js';
19
+
20
+ console.log();
21
+ console.log(color.bold('━━━ Builder API ━━━'));
22
+ console.log();
23
+
24
+ const project = tree({ label: 'my-app', icon: '📦', color: color.bold });
25
+ const src = project.add({ label: 'src', icon: '📁' });
26
+ src.addLeaf({ label: 'index.ts', icon: '📄' });
27
+ src.addLeaf({ label: 'app.ts', icon: '📄' });
28
+ const utils = src.add({ label: 'utils', icon: '📁' });
29
+ utils.addLeaf({ label: 'helpers.ts', icon: '📄' });
30
+ utils.addLeaf({ label: 'logger.ts', icon: '📄' });
31
+ project.addLeaf({ label: 'package.json', icon: '📦' });
32
+ project.addLeaf({ label: 'README.md', icon: '📝' });
33
+
34
+ console.log(project.render());
35
+ console.log();
36
+
37
+ console.log(color.bold('━━━ Tree styles ━━━'));
38
+ console.log();
39
+
40
+ const styles = ['rounded', 'classic', 'thick', 'double', 'ascii', 'heavy'] as const;
41
+ for (const style of styles) {
42
+ console.log(color.dim(`// style: ${style}`));
43
+ console.log(project.render({ style }));
44
+ }
45
+
46
+ console.log(color.bold('━━━ Palette colors ━━━'));
47
+ console.log();
48
+
49
+ console.log(project.render({
50
+ style: 'rounded',
51
+ palette: [color.cyan, color.green, color.magenta, color.yellow],
52
+ guideColor: color.dim,
53
+ }));
54
+ console.log();
55
+
56
+ console.log(color.bold('━━━ Plain-data API ━━━'));
57
+ console.log();
58
+
59
+ const tree2: TreeData = {
60
+ label: 'dependencies',
61
+ icon: '📦',
62
+ children: [
63
+ {
64
+ label: 'production',
65
+ icon: '🔒',
66
+ children: [
67
+ { label: 'react@18.2.0', icon: '⚛️' },
68
+ { label: 'tailwind@3.4.1', icon: '🎨' },
69
+ ],
70
+ },
71
+ {
72
+ label: 'development',
73
+ icon: '🛠️',
74
+ children: [
75
+ { label: 'typescript@5.4.0', icon: '🔷' },
76
+ { label: 'jest@29.7.0', icon: '🧪' },
77
+ { label: 'eslint@8.57.0', icon: '✅' },
78
+ ],
79
+ },
80
+ ],
81
+ };
82
+
83
+ console.log(renderTree(tree2));
84
+ console.log();
85
+
86
+ console.log(color.bold('━━━ maxDepth truncation ━━━'));
87
+ console.log();
88
+ console.log(color.dim('// maxDepth: 1'));
89
+ console.log(renderTree(tree2, { maxDepth: 1 }));
90
+ console.log();
91
+
92
+ console.log(color.bold('━━━ Algorithms ━━━'));
93
+ console.log();
94
+ console.log(' countNodes:', countNodes(tree2));
95
+
96
+ const found = findInTree(tree2, (n) => n.label.includes('react'));
97
+ console.log(' findInTree (react):', found?.label);
98
+
99
+ console.log(' walkTree paths:');
100
+ walkTree(tree2, (node, depth) => {
101
+ if (depth > 0) console.log(' ' + ' '.repeat(depth) + '→ ' + node.label);
102
+ });
103
+
104
+ const upper = mapTree(tree2, (node) => ({ ...node, label: node.label.toUpperCase() }));
105
+ console.log('\n mapTree (uppercase):');
106
+ console.log(renderTree(upper));
107
+
108
+ const onlyDev = filterTree(tree2, (n) => n.label !== 'production', { prune: true });
109
+ console.log(' filterTree (no production):');
110
+ if (onlyDev) console.log(renderTree(onlyDev));
111
+
112
+ const dim = measureTree(tree2);
113
+ console.log(` measureTree: ${dim.width} × ${dim.height}`);
114
+ console.log();
115
+
116
+ console.log(color.bold(color.green('✓ Trees test complete')));
117
+ console.log();
@@ -0,0 +1,96 @@
1
+ /**
2
+ * 05 — Components (NO interactive menu — tested separately)
3
+ *
4
+ * Run: npx tsx examples/05-components.ts
5
+ */
6
+
7
+ import { components, box, color } from '../src/index.js';
8
+
9
+ console.log();
10
+ console.log(color.bold('━━━ Tables ━━━'));
11
+ console.log();
12
+
13
+ console.log(components.table([
14
+ ['Module', 'Status', 'Coverage', 'Tests'],
15
+ ['colors', color.green('● ready'), '100%', '180'],
16
+ ['animations', color.green('● ready'), '99%', '245'],
17
+ ['loaders', color.green('● ready'), '99%', '210'],
18
+ ['frames', color.green('● ready'), '99%', '125'],
19
+ ['components', color.green('● ready'), '100%', '198'],
20
+ ['themes', color.green('● ready'), '100%', '94'],
21
+ ['trees', color.green('● ready'), '100%', '87'],
22
+ ], { borderStyle: 'rounded' }));
23
+ console.log();
24
+
25
+ console.log(color.dim('// Table with double border'));
26
+ console.log(components.table([
27
+ ['Name', 'Age', 'City'],
28
+ ['Ana', '25', 'Madrid'],
29
+ ['Carlos', '30', 'Lima'],
30
+ ], { borderStyle: 'double' }));
31
+ console.log();
32
+
33
+ console.log(color.bold('━━━ Badges ━━━'));
34
+ console.log();
35
+ console.log(' ',
36
+ components.badge('VERSION', 'v1.1.0'),
37
+ components.badge('BUILD', 'passing'),
38
+ components.badge('LICENSE', 'Apache 2.0'),
39
+ components.badge('NPM', 'ansimax'),
40
+ );
41
+ console.log();
42
+
43
+ console.log(color.bold('━━━ Status ━━━'));
44
+ console.log();
45
+ console.log(components.status('Build started', { type: 'info' }));
46
+ console.log(components.status('Linting passed', { type: 'success' }));
47
+ console.log(components.status('5 deprecation warnings', { type: 'warning' }));
48
+ console.log(components.status('Build failed', { type: 'error' }));
49
+ console.log();
50
+
51
+ console.log(color.bold('━━━ Section ━━━'));
52
+ console.log();
53
+ console.log(components.section('Installation Steps', { width: 60 }));
54
+ console.log();
55
+ console.log(components.section('Configuration', { width: 60 }));
56
+ console.log();
57
+
58
+ console.log(color.bold('━━━ Columns ━━━'));
59
+ console.log();
60
+ console.log(components.columns([
61
+ 'Lorem ipsum dolor sit amet',
62
+ 'Consectetur adipiscing elit',
63
+ 'Sed do eiusmod tempor',
64
+ ], { cols: 3, gap: 2, width: 60 }));
65
+ console.log();
66
+
67
+ console.log(color.bold('━━━ Timeline ━━━'));
68
+ console.log();
69
+ console.log(components.timeline([
70
+ { label: 'Project initialized', done: true, time: '10:00' },
71
+ { label: 'Dependencies installed', done: true, time: '10:05' },
72
+ { label: 'Build pipeline configured', done: true, time: '10:15' },
73
+ { label: 'Tests running', done: false, time: '10:32' },
74
+ { label: 'Deploy to npm', done: false },
75
+ ]));
76
+ console.log();
77
+
78
+ console.log(color.bold('━━━ Static progress bars ━━━'));
79
+ console.log();
80
+ for (const pct of [10, 25, 50, 75, 90, 100]) {
81
+ console.log(' ', `${pct.toString().padStart(3)}%`, components.progressBar(pct, { width: 30 }));
82
+ }
83
+ console.log();
84
+
85
+ console.log(color.bold('━━━ box utility ━━━'));
86
+ console.log();
87
+ console.log(box('Simple box utility', { padding: 1 }));
88
+ console.log();
89
+ console.log(box('Padded + bordered\nwith multiple lines', {
90
+ padding: 2,
91
+ borderStyle: 'rounded',
92
+ }));
93
+ console.log();
94
+
95
+ console.log(color.bold(color.green('✓ Components test complete')));
96
+ console.log();