import puppeteer from 'puppeteer-core'; import { devices } from './devices'; import { createPlugin } from './plugins'; import { render } from './template'; const args = process.argv.slice(2); if (args.includes('--help') || args.includes('-h')) { console.log(`Usage: bun run src/preview.ts [options] Render a plugin to a local PNG file for testing. Options: --config Config file (default: config.json) --device Device ID from config (default: first device) --output Output PNG path (default: preview.png) --firefox Firefox executable path (or set FIREFOX env var) --dither Apply e-ink dithering via ImageMagick --html Also save the raw HTML to .html -h, --help Show this help`); process.exit(0); } function getArg(flag: string, fallback: string): string { const idx = args.indexOf(flag); return idx !== -1 && args[idx + 1] ? args[idx + 1]! : fallback; } const configPath = getArg('--config', Bun.env['CONFIG_FILE'] ?? 'config.json'); const outputPath = getArg('--output', 'preview.png'); const firefoxPath = getArg('--firefox', Bun.env['FIREFOX'] ?? 'firefox'); const dither = args.includes('--dither'); const saveHtml = args.includes('--html'); const config = JSON.parse(await Bun.file(configPath).text()); const deviceEntries = Object.entries(config.devices ?? {}); if (deviceEntries.length === 0) { console.error('No devices found in config.'); process.exit(1); } const deviceIdArg = getArg('--device', ''); const [deviceId, deviceConfig] = deviceIdArg ? [deviceIdArg, config.devices[deviceIdArg]] : (deviceEntries[0] as [string, any]); if (!deviceConfig) { console.error(`Device "${deviceIdArg}" not found. Available: ${deviceEntries.map(([k]) => k).join(', ')}`); process.exit(1); } const model = devices.find((a) => a.name === (deviceConfig.model ?? 'inkplate_10')); if (!model) { console.error(`Unknown model: ${deviceConfig.model}`); process.exit(1); } console.log(`Rendering plugin "${deviceConfig.plugin ?? 'calendar'}" for device "${deviceId}" (${model.width}x${model.height})...`); const plugin = createPlugin(deviceConfig.plugin ?? 'calendar', deviceConfig.settings ?? {}); const html = await render([[plugin, 'full']], model); if (saveHtml) { const htmlPath = outputPath.replace(/\.png$/, '.html'); await Bun.write(htmlPath, html); console.log(`HTML saved to ${htmlPath}`); } const browser = await puppeteer.launch({ browser: 'firefox', args: ['--new-instance'], executablePath: firefoxPath, }); const page = await browser.newPage(); await page.setViewport({ width: model.width, height: model.height, deviceScaleFactor: 1 }); await page.setContent(html, { waitUntil: 'networkidle2' }); await Bun.sleep(3000); const screenshot = await page.screenshot({ encoding: 'binary' }); await page.close(); await browser.close(); if (dither) { const colormap = Bun.env['COLORMAP'] ?? './colormap.png'; const processed = await Bun.spawn( ['magick', '-', '-dither', 'FloydSteinberg', '-remap', colormap, '-define', 'png:bit-depth=2', '-define', 'png:color-type=0', '-strip', 'png:-'], { stdin: screenshot } ).stdout.bytes(); await Bun.write(outputPath, processed); } else { await Bun.write(outputPath, screenshot); } console.log(`Preview saved to ${outputPath}`);