| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123 |
- import { parentPort, workerData } from 'worker_threads';
- import { writeSync } from 'fs';
- import { getGlyphs } from './glyphs';
- import type { ShimmerWorkerMessage } from './types';
- // Write directly to fd 1 (stdout) instead of writeStdout().
- // In Node.js worker threads, process.stdout is proxied through the main
- // thread's event loop — so if the main thread is blocked (e.g. SQLite),
- // stdout writes from the worker queue up and the animation freezes.
- // fs.writeSync(1, ...) is a direct kernel syscall that bypasses this.
- //
- // Side effect: bypasses Node's TTY-aware encoding conversion on Windows,
- // so UTF-8 bytes hit the console raw and mojibake on OEM codepages.
- // `getGlyphs()` returns ASCII fallbacks on Windows to avoid this (#168).
- function writeStdout(s: string): void {
- writeSync(1, s);
- }
- const G = getGlyphs();
- const SPINNER_GLYPHS = G.spinner;
- const ANIM_INTERVAL = 150;
- const FRAMES_PER_GLYPH = 3;
- const RST = '\x1b[0m';
- const DM = '\x1b[2m';
- const GRN = '\x1b[32m';
- const BOLD = '\x1b[1m';
- const startTime: number = workerData.startTime;
- function animFrame(): number {
- return Math.floor((Date.now() - startTime) / ANIM_INTERVAL);
- }
- function lerp(a: number, b: number, t: number): number {
- return Math.round(a + (b - a) * t);
- }
- function shimmerColor(frame: number): string {
- const t = (Math.sin(frame * 2 * Math.PI / 13) + 1) / 2;
- const r = lerp(160, 251, t);
- const g = lerp(100, 191, t);
- const b = lerp(9, 36, t);
- return `\x1b[38;2;${r};${g};${b}m${BOLD}`;
- }
- function formatNumber(n: number): string {
- return n.toLocaleString();
- }
- function renderBar(frame: number, filled: number, empty: number): string {
- if (filled === 0) return `${DM}${G.barEmpty.repeat(empty)}${RST}`;
- const cycleFrames = 24;
- const shimmerPos = ((frame % cycleFrames) / cycleFrames) * (filled + 6) - 3;
- const shimmerWidth = 3;
- let bar = '';
- for (let i = 0; i < filled; i++) {
- const dist = Math.abs(i - shimmerPos);
- const t = Math.max(0, 1 - dist / shimmerWidth);
- const r = lerp(160, 251, t);
- const g = lerp(100, 191, t);
- const b = lerp(9, 36, t);
- bar += `\x1b[38;2;${r};${g};${b}m${BOLD}${G.barFilled}`;
- }
- bar += `${RST}${DM}${G.barEmpty.repeat(empty)}${RST}`;
- return bar;
- }
- // Mutable state
- let currentMessage = '';
- let currentPercent = -1;
- let currentCount = 0;
- function render(): void {
- if (!currentMessage) return;
- const frame = animFrame();
- const glyphIdx = Math.floor(frame / FRAMES_PER_GLYPH) % SPINNER_GLYPHS.length;
- const glyph = SPINNER_GLYPHS[glyphIdx] ?? SPINNER_GLYPHS[0] ?? '.';
- const color = shimmerColor(frame);
- let line: string;
- if (currentPercent >= 0) {
- const barWidth = 25;
- const filled = Math.round(barWidth * currentPercent / 100);
- const empty = barWidth - filled;
- line = `${DM}${G.rail}${RST} ${color}${glyph}${RST} ${currentMessage} ${renderBar(frame, filled, empty)} ${currentPercent}%`;
- } else if (currentCount > 0) {
- line = `${DM}${G.rail}${RST} ${color}${glyph}${RST} ${currentMessage}... ${formatNumber(currentCount)} found`;
- } else {
- line = `${DM}${G.rail}${RST} ${color}${glyph}${RST} ${currentMessage}...`;
- }
- writeStdout(`\r\x1b[K${line}`);
- }
- function finishPhase(): void {
- if (!currentMessage) return;
- writeStdout(`\r\x1b[K`);
- let detail = '';
- if (currentPercent >= 0) detail = ` ${G.dash} done`;
- else if (currentCount > 0) detail = ` ${G.dash} ${formatNumber(currentCount)} found`;
- writeStdout(`${DM}${G.rail}${RST} ${GRN}${G.phaseDone}${RST} ${currentMessage}${detail}\n`);
- currentMessage = '';
- currentPercent = -1;
- currentCount = 0;
- }
- // Render loop — independent of main thread
- const tickInterval = setInterval(render, 50);
- parentPort!.on('message', (msg: ShimmerWorkerMessage) => {
- if (msg.type === 'update') {
- currentMessage = msg.phaseName;
- currentPercent = msg.percent;
- currentCount = msg.count;
- } else if (msg.type === 'finish-phase') {
- finishPhase();
- } else if (msg.type === 'stop') {
- clearInterval(tickInterval);
- finishPhase();
- parentPort!.postMessage({ type: 'stopped' });
- }
- });
|