qr-terminal 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.
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Live Preview Engine
3
+ *
4
+ * WYSIWYG terminal experience: QR code re-renders on every keystroke.
5
+ * Per-keystroke info badge shows QR version + char count in real-time.
6
+ * Placeholder frame holds the exact space so there's no layout jump.
7
+ */
8
+
9
+ import chalk from 'chalk';
10
+ import { createMatrix, renderHalfBlock, getQrVersion } from './engine.js';
11
+
12
+ const ESC = {
13
+ up: n => `\x1b[${n}A`,
14
+ clearDown: '\x1b[J',
15
+ hideCursor: '\x1b[?25l',
16
+ showCursor: '\x1b[?25h',
17
+ clearLine: '\x1b[2K\r',
18
+ };
19
+
20
+ const strip = s => s.replace(/\x1b\[[0-9;]*m/g, '');
21
+ const sleep = ms => new Promise(r => setTimeout(r, ms));
22
+
23
+ /**
24
+ * Launch a live-updating QR preview input.
25
+ * Returns a Promise that resolves with the final string on Enter.
26
+ */
27
+ export function liveInput({ label = 'Enter text', errorLevel = 'M', gradient = 'none' } = {}) {
28
+ return new Promise((resolve, reject) => {
29
+ let input = '';
30
+ let lastHeight = 0;
31
+ let debounce = null;
32
+ let lastVersion = 0;
33
+
34
+ // ── Frame builder ─────────────────────────────────────────────────────────
35
+
36
+ const buildLines = (text) => {
37
+ const lines = [];
38
+ const termCols = Math.min(process.stdout.columns || 80, 100);
39
+
40
+ if (!text) {
41
+ // Ghost placeholder: exact dimensions of a version-3 QR
42
+ const boxW = 33;
43
+ const boxH = 17;
44
+ const pad = 4;
45
+ const p = ' '.repeat(pad);
46
+
47
+ lines.push('');
48
+ lines.push(p + chalk.rgb(30, 40, 60)('┌' + '─'.repeat(boxW + 2) + '┐'));
49
+ for (let i = 0; i < boxH; i++) {
50
+ if (i === Math.floor(boxH / 2) - 1) {
51
+ const msg = ' Enter text to generate QR ';
52
+ const side = Math.floor((boxW + 2 - msg.length) / 2);
53
+ lines.push(
54
+ p + chalk.rgb(30,40,60)('│') +
55
+ ' '.repeat(side) +
56
+ chalk.rgb(60, 80, 120).italic(msg) +
57
+ ' '.repeat(boxW + 2 - side - msg.length) +
58
+ chalk.rgb(30,40,60)('│')
59
+ );
60
+ } else if (i === Math.floor(boxH / 2) + 1) {
61
+ const hint = ' ▀▄ half-block engine ready ';
62
+ const hside = Math.floor((boxW + 2 - hint.length) / 2);
63
+ lines.push(
64
+ p + chalk.rgb(30,40,60)('│') +
65
+ ' '.repeat(hside) +
66
+ chalk.rgb(40, 60, 90).dim(hint) +
67
+ ' '.repeat(boxW + 2 - hside - hint.length) +
68
+ chalk.rgb(30,40,60)('│')
69
+ );
70
+ } else {
71
+ // Subtle scanline pattern
72
+ const dotRow = (i % 4 === 0)
73
+ ? chalk.rgb(20, 30, 50)('·'.repeat(boxW + 2))
74
+ : ' '.repeat(boxW + 2);
75
+ lines.push(p + chalk.rgb(30,40,60)('│') + dotRow + chalk.rgb(30,40,60)('│'));
76
+ }
77
+ }
78
+ lines.push(p + chalk.rgb(30, 40, 60)('└' + '─'.repeat(boxW + 2) + '┘'));
79
+ lines.push('');
80
+ } else {
81
+ try {
82
+ const modules = createMatrix(text, { errorLevel });
83
+ const qrLines = renderHalfBlock(modules, { gradient });
84
+ const qrWidth = strip(qrLines[0] ?? '').length;
85
+ const pad = Math.max(4, Math.floor((termCols - qrWidth) / 2));
86
+ const p = ' '.repeat(pad);
87
+ const v = getQrVersion(modules.size);
88
+
89
+ // Version bump badge (flash when version increases)
90
+ const vChanged = v !== lastVersion;
91
+ lastVersion = v;
92
+
93
+ const vBadge = vChanged
94
+ ? chalk.bgCyan.bold.black(` QR v${v} `) + chalk.cyan(' ◄ upgraded')
95
+ : chalk.bgRgb(20,20,50).rgb(80,160,255)(` QR v${v} `);
96
+
97
+ const capBadge = chalk.dim(`${modules.size}×${modules.size}`);
98
+ const lenBadge = text.length > 0
99
+ ? chalk.dim(`${text.length} chars`)
100
+ : '';
101
+
102
+ lines.push('');
103
+ lines.push(
104
+ p + vBadge +
105
+ chalk.dim(' · ') + capBadge +
106
+ chalk.dim(' · ') + lenBadge
107
+ );
108
+ lines.push('');
109
+
110
+ for (const line of qrLines) lines.push(p + line);
111
+ lines.push('');
112
+ } catch (e) {
113
+ lines.push('');
114
+ lines.push(chalk.red(' ⚠ Input exceeds QR capacity — try shortening the text'));
115
+ lines.push('');
116
+ }
117
+ }
118
+
119
+ // Input field
120
+ const promptPrefix = chalk.bold.cyan(' › ') + chalk.bold.white(label + ':') + ' ';
121
+ const cursor = chalk.bgCyan.black(' ');
122
+ lines.push(promptPrefix + chalk.white(text) + cursor);
123
+ lines.push('');
124
+
125
+ return lines;
126
+ };
127
+
128
+ // ── Draw / clear ──────────────────────────────────────────────────────────
129
+
130
+ const draw = (lines) => {
131
+ for (const line of lines) process.stdout.write(line + '\n');
132
+ lastHeight = lines.length;
133
+ };
134
+
135
+ const clear = () => {
136
+ if (lastHeight > 0) {
137
+ process.stdout.write(ESC.up(lastHeight) + ESC.clearDown);
138
+ }
139
+ };
140
+
141
+ const redraw = () => {
142
+ if (debounce) clearTimeout(debounce);
143
+ debounce = setTimeout(() => {
144
+ clear();
145
+ draw(buildLines(input));
146
+ }, 16);
147
+ };
148
+
149
+ // ── Keypress handler ──────────────────────────────────────────────────────
150
+
151
+ const onData = (key) => {
152
+ if (key === '\r' || key === '\n') {
153
+ if (debounce) clearTimeout(debounce);
154
+ clear();
155
+ process.stdout.write(
156
+ chalk.bold.cyan(' › ') +
157
+ chalk.bold.white(label + ':') + ' ' +
158
+ chalk.white(input) + '\n'
159
+ );
160
+ done(null, input);
161
+
162
+ } else if (key === '\u0003') {
163
+ done(new Error('Cancelled'));
164
+
165
+ } else if (key === '\u007f' || key === '\b') {
166
+ input = input.slice(0, -1);
167
+ redraw();
168
+
169
+ } else if (key === '\u001b') {
170
+ input = '';
171
+ redraw();
172
+
173
+ } else if (key.charCodeAt(0) >= 32 && !key.startsWith('\x1b')) {
174
+ input += key;
175
+ redraw();
176
+ }
177
+ };
178
+
179
+ const done = (err, result) => {
180
+ process.stdin.removeListener('data', onData);
181
+ try {
182
+ process.stdin.setRawMode(false);
183
+ process.stdin.pause();
184
+ } catch {}
185
+ process.stdout.write(ESC.showCursor);
186
+ if (err) reject(err);
187
+ else resolve(result);
188
+ };
189
+
190
+ // ── Boot ──────────────────────────────────────────────────────────────────
191
+
192
+ process.stdout.write(ESC.hideCursor);
193
+ process.stdin.setRawMode(true);
194
+ process.stdin.resume();
195
+ process.stdin.setEncoding('utf8');
196
+ process.stdin.on('data', onData);
197
+ draw(buildLines(input));
198
+ });
199
+ }
package/src/luma.js ADDED
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Luma Detection via OSC 11
3
+ *
4
+ * Sends the OSC 11 escape sequence to query the terminal's actual background
5
+ * color, then computes its luminance. Returns { r, g, b, luminance, isLight }
6
+ * or null if the terminal doesn't support it (with a 400ms timeout).
7
+ */
8
+
9
+ export async function detectTerminalBgColor() {
10
+ if (!process.stdin.isTTY || !process.stdout.isTTY) return null;
11
+
12
+ return new Promise((resolve) => {
13
+ let buffer = '';
14
+
15
+ const timeout = setTimeout(() => {
16
+ cleanup();
17
+ resolve(null);
18
+ }, 400);
19
+
20
+ const onData = (chunk) => {
21
+ buffer += chunk;
22
+ // OSC 11 response format: \x1b]11;rgb:RRRR/GGGG/BBBB\x07
23
+ // Some terminals send \x1b\\ as terminator instead of \x07
24
+ const match = buffer.match(/\x1b\]11;rgb:([0-9a-fA-F]+)\/([0-9a-fA-F]+)\/([0-9a-fA-F]+)/);
25
+ if (match) {
26
+ cleanup();
27
+ // Values are 16-bit (0000–FFFF); take the high byte for 0–255
28
+ const r = parseInt(match[1].slice(0, 2), 16);
29
+ const g = parseInt(match[2].slice(0, 2), 16);
30
+ const b = parseInt(match[3].slice(0, 2), 16);
31
+ // WCAG relative luminance
32
+ const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
33
+ resolve({ r, g, b, luminance, isLight: luminance > 127 });
34
+ }
35
+ };
36
+
37
+ const cleanup = () => {
38
+ clearTimeout(timeout);
39
+ try {
40
+ process.stdin.removeListener('data', onData);
41
+ process.stdin.setRawMode(false);
42
+ process.stdin.pause();
43
+ } catch {}
44
+ };
45
+
46
+ try {
47
+ process.stdin.setRawMode(true);
48
+ process.stdin.resume();
49
+ process.stdin.setEncoding('latin1');
50
+ process.stdin.on('data', onData);
51
+ // Send OSC 11 query
52
+ process.stdout.write('\x1b]11;?\x07');
53
+ } catch {
54
+ cleanup();
55
+ resolve(null);
56
+ }
57
+ });
58
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Matrix Rain Animation
3
+ *
4
+ * After the QR appears, cascading green "rain droplets" sweep down each
5
+ * column before the code settles to its final gradient.
6
+ * Contrast is preserved: each droplet is bright (#00FF41), which has
7
+ * sufficient luminance against dark terminal backgrounds.
8
+ */
9
+
10
+ import { buildParticleData } from './engine.js';
11
+
12
+ const sleep = ms => new Promise(r => setTimeout(r, ms));
13
+
14
+ /**
15
+ * @param {object} modules - QR modules from createMatrix()
16
+ * @param {object} options
17
+ * @param {string} options.gradient - Final color gradient
18
+ * @param {number} options.duration - Rain duration in ms (default 2000)
19
+ * @param {number} options.fps - Frames per second (default 24)
20
+ */
21
+ export async function matrixRainQR(modules, { gradient = 'none', duration = 2000, fps = 24 } = {}) {
22
+ const { particles, displayRows, totalCols } = buildParticleData(modules, { gradient });
23
+ const frameMs = Math.floor(1000 / fps);
24
+ const frameCount = Math.floor(duration / frameMs);
25
+
26
+ // Per-column rain phase offset (0–1, cycles continuously)
27
+ const phases = Array.from({ length: totalCols }, () => Math.random());
28
+
29
+ // Build a lookup for quick per-cell access: fb[row][col] = particle data
30
+ const lookup = Array.from({ length: displayRows }, () => new Array(totalCols).fill(null));
31
+ for (const p of particles) lookup[p.targetRow][p.targetCol] = p;
32
+
33
+ process.stdout.write('\x1b[?25l');
34
+
35
+ // Print initial static frame first (so there's no blank before rain starts)
36
+ renderStatic(lookup, displayRows, totalCols, phases, 0);
37
+
38
+ for (let f = 0; f < frameCount; f++) {
39
+ const progress = f / frameCount; // 0 → 1
40
+
41
+ // Advance rain phases
42
+ for (let c = 0; c < totalCols; c++) {
43
+ phases[c] = (phases[c] + 0.04) % 1;
44
+ }
45
+
46
+ process.stdout.write(`\x1b[${displayRows}A`);
47
+
48
+ for (let row = 0; row < displayRows; row++) {
49
+ let line = '', curColor = null;
50
+ const ty = displayRows > 1 ? row / (displayRows - 1) : 0;
51
+
52
+ for (let col = 0; col < totalCols; col++) {
53
+ const cell = lookup[row][col];
54
+ if (!cell) {
55
+ if (curColor) { line += '\x1b[0m'; curColor = null; }
56
+ line += ' ';
57
+ continue;
58
+ }
59
+
60
+ // Rain droplet: bright where phase matches row position
61
+ const phase = phases[col];
62
+ const relRow = ty;
63
+ const dist = Math.abs(((phase - relRow + 1.5) % 1) - 0.5); // wrapped distance
64
+ const droplet = Math.max(0, 1 - dist * 8);
65
+
66
+ // Fade rain out as progress → 1, revealing final gradient
67
+ const rainStr = droplet * (1 - progress);
68
+
69
+ const base = cell.color;
70
+ const rain = [0, 255, 65]; // matrix green
71
+ const r = Math.round(base[0] * (1 - rainStr) + rain[0] * rainStr);
72
+ const g = Math.round(base[1] * (1 - rainStr) + rain[1] * rainStr);
73
+ const b = Math.round(base[2] * (1 - rainStr) + rain[2] * rainStr);
74
+
75
+ const key = `${r},${g},${b}`;
76
+ if (key !== curColor) {
77
+ line += `\x1b[38;2;${r};${g};${b}m`;
78
+ curColor = key;
79
+ }
80
+ line += cell.char;
81
+ }
82
+
83
+ if (curColor) line += '\x1b[0m';
84
+ process.stdout.write(' ' + line + '\n');
85
+ }
86
+
87
+ await sleep(frameMs);
88
+ }
89
+
90
+ // Final settle: reprint without rain
91
+ process.stdout.write(`\x1b[${displayRows}A`);
92
+ renderStatic(lookup, displayRows, totalCols, phases, 1);
93
+
94
+ process.stdout.write('\x1b[?25h');
95
+ }
96
+
97
+ function renderStatic(lookup, displayRows, totalCols, phases, _progress) {
98
+ for (let row = 0; row < displayRows; row++) {
99
+ let line = '', curColor = null;
100
+ for (let col = 0; col < totalCols; col++) {
101
+ const cell = lookup[row][col];
102
+ if (!cell) {
103
+ if (curColor) { line += '\x1b[0m'; curColor = null; }
104
+ line += ' ';
105
+ } else {
106
+ const [r, g, b] = cell.color;
107
+ const key = `${r},${g},${b}`;
108
+ if (key !== curColor) { line += `\x1b[38;2;${r};${g};${b}m`; curColor = key; }
109
+ line += cell.char;
110
+ }
111
+ }
112
+ if (curColor) line += '\x1b[0m';
113
+ process.stdout.write(' ' + line + '\n');
114
+ }
115
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Particle Fly-In Animation — "The Transformer Effect"
3
+ *
4
+ * Each dark QR module becomes a particle that starts at a random scatter
5
+ * position and flies into its correct place with cubic ease-out.
6
+ * A stagger delay (0–30% of duration) makes the arrival feel organic.
7
+ */
8
+
9
+ import { buildParticleData } from './engine.js';
10
+
11
+ const sleep = ms => new Promise(r => setTimeout(r, ms));
12
+
13
+ /**
14
+ * Animate QR code appearing via particle fly-in.
15
+ *
16
+ * @param {object} modules - QR modules from createMatrix()
17
+ * @param {object} options
18
+ * @param {string} options.gradient - Gradient preset name
19
+ * @param {number} options.fps - Frames per second (default 30)
20
+ * @param {number} options.duration - Total animation ms (default 1100)
21
+ */
22
+ export async function animateParticleFlyIn(modules, options = {}) {
23
+ const { gradient = 'none', fps = 30, duration = 1100 } = options;
24
+ const frameCount = Math.floor(duration / (1000 / fps));
25
+ const frameMs = Math.floor(1000 / fps);
26
+
27
+ const { particles, displayRows, totalCols } = buildParticleData(modules, { gradient });
28
+
29
+ // Assign each particle a random starting position + stagger delay
30
+ const rng = () => Math.random();
31
+ const seeded = particles.map(p => ({
32
+ ...p,
33
+ startRow: rng() * displayRows,
34
+ startCol: (rng() - 0.5) * totalCols * 2 + totalCols / 2, // wider scatter on X
35
+ delay: rng() * 0.30, // 0–30% stagger
36
+ }));
37
+
38
+ // Print blank placeholder
39
+ process.stdout.write('\x1b[?25l');
40
+ const blank = ' '.repeat(totalCols);
41
+ for (let i = 0; i < displayRows; i++) process.stdout.write(' ' + blank + '\n');
42
+
43
+ for (let f = 0; f <= frameCount; f++) {
44
+ const globalT = f / frameCount;
45
+
46
+ // Build framebuffer: 2D array of { char, color } | null
47
+ const fb = Array.from({ length: displayRows }, () => new Array(totalCols).fill(null));
48
+
49
+ for (const p of seeded) {
50
+ // Apply per-particle stagger
51
+ const localT = Math.max(0, (globalT - p.delay) / (1 - p.delay));
52
+ // Cubic ease-out
53
+ const e = 1 - Math.pow(1 - localT, 3);
54
+
55
+ const row = Math.round(p.startRow + (p.targetRow - p.startRow) * e);
56
+ const col = Math.round(p.startCol + (p.targetCol - p.startCol) * e);
57
+
58
+ if (row >= 0 && row < displayRows && col >= 0 && col < totalCols) {
59
+ fb[row][col] = { char: p.char, color: p.color };
60
+ }
61
+ }
62
+
63
+ // Move cursor back to top of animation area
64
+ process.stdout.write(`\x1b[${displayRows}A`);
65
+
66
+ // Render framebuffer
67
+ for (const row of fb) {
68
+ let line = '', curColor = null;
69
+ for (const cell of row) {
70
+ if (!cell) {
71
+ if (curColor) { line += '\x1b[0m'; curColor = null; }
72
+ line += ' ';
73
+ } else {
74
+ const [r, g, b] = cell.color;
75
+ const key = `${r},${g},${b}`;
76
+ if (key !== curColor) {
77
+ line += `\x1b[38;2;${r};${g};${b}m`;
78
+ curColor = key;
79
+ }
80
+ line += cell.char;
81
+ }
82
+ }
83
+ if (curColor) line += '\x1b[0m';
84
+ process.stdout.write(' ' + line + '\n');
85
+ }
86
+
87
+ await sleep(frameMs);
88
+ }
89
+
90
+ process.stdout.write('\x1b[?25h');
91
+ }