Add today-view plugin with Tana, ICS, and Donetick data sources
Implements an e-ink daily dashboard plugin ("today") with four sections:
date/focus/success header, agenda timeline, chores checklist, and
due/overdue task lists.
Data sources:
- Focus & success text: Tana daily note (src/sources/tana.ts)
- Due/overdue tasks: Tana task search (src/sources/tana.ts)
- Agenda events: ICS calendar feeds (src/sources/ics.ts)
- Chores: Donetick API (src/sources/donetick.ts)
All sources fetch in parallel and fall back gracefully on error.
Tests use mock HTTP servers with synthetic data — no real services needed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ddcb03d3dd
commit
2e34246d14
12 changed files with 1665 additions and 11 deletions
96
src/preview.ts
Normal file
96
src/preview.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
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 <path> Config file (default: config.json)
|
||||
--device <id> Device ID from config (default: first device)
|
||||
--output <path> Output PNG path (default: preview.png)
|
||||
--firefox <path> Firefox executable path (or set FIREFOX env var)
|
||||
--dither Apply e-ink dithering via ImageMagick
|
||||
--html Also save the raw HTML to <output>.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}`);
|
||||
Loading…
Add table
Add a link
Reference in a new issue