'use strict'; // Runs the packaged Ink app (the released AppImage, extracted) against a seeded demo vault and saves 16:10 // screenshots, driving it over the Chrome DevTools protocol. Nothing is read from the app's source. // node scripts/capture/capture.cjs (env: INK_BIN, OUT, THEME, SCENES; run.sh sets these up) const { spawn } = require('node:child_process'); const path = require('node:path'); const os = require('node:os'); const fs = require('node:fs'); const BIN = process.env.INK_BIN; const OUT = process.env.OUT || path.resolve(__dirname, '../../src/assets/screens'); const ONLY = process.env.SCENES ? process.env.SCENES.split(',') : null; const theme = process.env.THEME || 'dark'; const palette = process.env.PALETTE || 'default'; const accent = process.env.ACCENT || 'blush'; const SUFFIX = process.env.SUFFIX || theme; const { seedVault } = require('./seed-vault.cjs'); const scenes = require('./scenes.cjs'); if (!BIN) throw new Error('Set INK_BIN to the extracted AppImage\'s `ink` binary (run.sh does this).'); const W = 1600, H = 1000, SCALE = 1.5, PORT = 9300 + (process.pid % 500); const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ink-shots-')); const vault = path.join(tmp, 'Notes'); fs.mkdirSync(vault); const seeded = seedVault(vault); fs.writeFileSync(path.join(tmp, 'settings.json'), JSON.stringify({ vault, lastPage: seeded.lastPage, theme, palette, accent, bounds: { width: W, height: H }, fontSize: 16, outline: false, sidebarCollapsed: false, tagsOpen: true, vaults: { [vault]: seeded.vaultSettings }, })); const KEYS = { Escape: ['Escape', 27], Return: ['Enter', 13], Enter: ['Enter', 13], Tab: ['Tab', 9], Backspace: ['Backspace', 8], Down: ['ArrowDown', 40], Up: ['ArrowUp', 38] }; const MODS = { alt: 1, control: 2, meta: 4, shift: 8 }; async function connect() { for (let i = 0; i < 150; i++) { try { const list = await (await fetch(`http://127.0.0.1:${PORT}/json`)).json(); const page = list.find((t) => t.type === 'page' && !t.url.startsWith('devtools')); if (page) { const ws = new WebSocket(page.webSocketDebuggerUrl); await new Promise((res, rej) => { ws.onopen = res; ws.onerror = rej; }); return ws; } } catch {} await sleep(200); } throw new Error('Ink did not start'); } (async () => { const proc = spawn(BIN, [ `--user-data-dir=${tmp}`, `--remote-debugging-port=${PORT}`, '--ozone-platform=headless', '--no-sandbox', '--lang=en-US', ], { stdio: 'ignore' }); const ws = await connect(); let id = 0; const pending = new Map(); ws.onmessage = (m) => { const d = JSON.parse(m.data); if (d.id && pending.has(d.id)) { const { res, rej } = pending.get(d.id); pending.delete(d.id); d.error ? rej(new Error(d.error.message)) : res(d.result); } }; const send = (method, params = {}) => new Promise((res, rej) => { pending.set(++id, { res, rej }); ws.send(JSON.stringify({ id, method, params })); }); await send('Emulation.setDeviceMetricsOverride', { width: W, height: H, deviceScaleFactor: SCALE, mobile: false }); await send('Runtime.enable'); const js = async (code) => { const r = await send('Runtime.evaluate', { expression: `(async () => { ${code} })()`, awaitPromise: true, returnByValue: true }); if (r.exceptionDetails) throw new Error(r.exceptionDetails.exception?.description || r.exceptionDetails.text); return r.result.value; }; const key = async (name, modifiers = []) => { const m = modifiers.reduce((a, k) => a | MODS[k], 0); const [k, vk] = KEYS[name] || [name, name.length === 1 ? name.toUpperCase().charCodeAt(0) : 0]; const text = !m || m === 8 ? (name.length === 1 ? name : undefined) : undefined; const code = name.length === 1 && /[a-z]/i.test(name) ? `Key${name.toUpperCase()}` : /^\d$/.test(name) ? `Digit${name}` : k; await send('Input.dispatchKeyEvent', { type: 'rawKeyDown', key: k, code, windowsVirtualKeyCode: vk, modifiers: m }); await send('Input.dispatchKeyEvent', { type: 'keyUp', key: k, code, windowsVirtualKeyCode: vk, modifiers: m }); await sleep(250); }; const type = async (t, d = 45) => { for (const ch of t) { await send('Input.dispatchKeyEvent', { type: 'keyDown', key: ch, text: ch, unmodifiedText: ch, windowsVirtualKeyCode: ch.toUpperCase().charCodeAt(0) }); await send('Input.dispatchKeyEvent', { type: 'keyUp', key: ch }); await sleep(d); } await sleep(200); }; const click = async (x, y) => { await send('Input.dispatchMouseEvent', { type: 'mouseMoved', x, y }); await send('Input.dispatchMouseEvent', { type: 'mousePressed', x, y, button: 'left', clickCount: 1 }); await send('Input.dispatchMouseEvent', { type: 'mouseReleased', x, y, button: 'left', clickCount: 1 }); await sleep(250); }; const shot = async (name) => { await sleep(500); const r = await send('Page.captureScreenshot', { format: 'png' }); const file = path.join(OUT, `${name}-${SUFFIX}.png`); fs.mkdirSync(OUT, { recursive: true }); fs.writeFileSync(file, Buffer.from(r.data, 'base64')); console.log(`saved ${path.basename(file)}`); }; const ctx = { js, key, type, click, shot, sleep, vault, W, H, send }; try { await sleep(3000); for (const [name, fn] of Object.entries(scenes)) { if (ONLY && !ONLY.includes(name)) continue; try { await fn(ctx); } catch (e) { console.log(`FAILED ${name}: ${e.message}`); } } } finally { ws.close(); proc.kill(); fs.rmSync(tmp, { recursive: true, force: true }); } })();