Flask companion that composes dashboards in the browser, renders them through Playwright + Pillow, and pushes them to a Pimoroni Inky Impression e-ink panel over MQTT. Drop a folder under plugins/ and you have a new widget; drop another and you have a new theme. The Pi-side listener lives in dmellok/inky-dash-listener — the MQTT wire format is byte-for-byte identical to v3, so the same listener works on both.
This is v4. Architecture overview in docs/architecture.md; usage tutorial in the wiki; two reference plugins live at plugins/example_minimal/ and plugins/example_full/. The v3 source is archived at the v3-final tag.
Inky Dash is built and maintained as a personal project, aimed at people who're comfortable installing Python, running an MQTT broker, and tinkering with their own Pi. It's not a polished consumer product, and a few rough edges come with the territory:
- Single-password gate on the admin UI. Pick a password on first boot at
/setup; everyone in your house shares it. There's no concept of accounts or per-user permissions, no rate limiting, no 2FA — it's a fence for your home network, not internet-grade security. Still run it on a private network only. Don't port-forward it; use a VPN / Tailscale if you need remote access. - Single-user, single-host. There's no concept of accounts or multi-tenancy. Whoever opens the UI is the admin.
- Schema migrations are best-effort. Plugin manifests and settings are versioned, but breaking changes between releases may need a manual nudge. Back up
data/before upgrading if you've customised heavily. - Maintained as time allows. Issues + PRs are welcome and read, but response times will be variable. The bus factor is one.
If those trade-offs are fine for your use case (a panel on your wall, on your home network, that you're happy to tinker with), it works well. If you need auth, multi-user, or an internet-facing install, this isn't the right tool yet.
Clone the repo, run the install script for your OS, then start the server. Python 3.11+ is required; the script provisions everything else (venv, Playwright + Chromium, JS bundle, a seeded settings.json).
git clone https://github.com/dmellok/inky-dash && cd inky-dash
./scripts/install.sh
./scripts/run.shgit clone https://github.com/dmellok/inky-dash; cd inky-dash
.\scripts\install.ps1
.\scripts\run.ps1Then open http://localhost:5555.
Optionally pre-seed broker config so settings.json lands ready to push to a real panel:
# macOS / Linux
MQTT_HOST=192.168.1.50 \
COMPANION_BASE_URL=http://192.168.1.10:5555 \
./scripts/install.sh
# Windows
$env:MQTT_HOST = "192.168.1.50"
$env:COMPANION_BASE_URL = "http://192.168.1.10:5555"
.\scripts\install.ps1You'll also need an MQTT broker the companion + the Pi-side listener both connect to (Mosquitto is the typical pick) and the Pi-side inky-dash-listener flashed onto your Pimoroni Inky. The install script prints those next-step pointers when it finishes.
If you'd rather not run the script, the equivalent commands are:
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
python -m playwright install chromium
bun install && bun run build # or: npm install && npm run build
python -m appOnce install.sh works and run.sh looks healthy, you can install Inky Dash as a systemd service so it survives reboots:
./scripts/install-service.sh # writes /etc/systemd/system/inky-dash.service, enables + starts it
./scripts/install-service.sh --uninstall # stop + remove the unit file (repo + data/ untouched)The unit runs as the user who invoked the script (override with INKY_DASH_SERVICE_USER=<user>) so the venv, Playwright browser cache, and data/ permissions line up. View logs with journalctl -u inky-dash -f.
ruff check . && ruff format --check . && mypy && pytest- Dashboard editor — split the panel into cells from a layout picker, click a cell to configure its widget + theme + font in the sidebar, live preview rendered in an iframe. Saved pages live in
data/core/pages.json. - 29 widget plugins — clock, flip clock, weather, calendar, todo (multi-list), world clock, year-progress, sun & moon, AQI trend, HN, Reddit, news (RSS), Trakt watchlist posters, gallery, APOD, Unsplash, Wikimedia Picture of the Day, GitHub contributions heatmap, weather radar, star map, generative art, Home Assistant tile, Melbourne PTV departures, QR code (URL / WiFi / text), countdown, note, xkcd, webpage screenshot, calibration, frame aligner. (Two reference plugins —
example_minimal,example_full— ship as the canonical "how to write one" docs.) - 49 hand-curated themes — bucketed into White (6 bold-accent stark-white themes), Light (Paper / Linen / Mist / Ink / Burgundy / …), Medium, and Dark (Cyber / Embers / Reef / Flamingo / Peach + 15 monochrome + 5 neon). 7 of them put the typography itself in saturated ink rather than neutral grey. Build your own at
/themes. - Schedules — one-shot daily-at-HH:MM or every-N-minutes, with day-of-week + time-of-day-window guards. Backfill-safe (won't replay a day's worth of fires when re-enabled mid-day).
- Send page — push any image, saved dashboard, image URL, or arbitrary webpage to the panel right now. Includes fit modes (fit / fill / stretch / center / blurred-bg) for one-off images, plus a history tab with thumbnails, resend, delete, and a click-to-zoom lightbox.
- Per-cell theme overrides — each cell can paint in its own theme without affecting siblings.
- MQTT push pipeline — single-flight
PushManagerwith identical-push debounce, content-addressed render cache, LRU eviction, and a full push history.
Click any cell in the live-preview to edit it. Theme, font, and matting apply to the whole page; cells can override theme + font individually.
| Editor list | Editor |
|---|---|
![]() |
![]() |
The cell sidebar surfaces every option each plugin declares in its manifest — text fields, numbers, booleans, selects, and dynamic dropdowns (e.g. todo's list picker, gallery's folder picker). Per-cell colour overrides let you nudge individual tokens (accent, surface, divider, etc.) without forking the theme.
Five sample 1200×1600 dashboards, each leaning on a different theme to show how the design language reskins. The screenshots below are the same Playwright render the panel actually receives; the data (groceries, reading list, today, home) comes from real lists managed at /plugins/todo/.
The five pages are saved as show-paper-morning, show-cyber-desk, show-ink-study, show-embers-evening, show-cherry-pop — open any of them at /editor/<id> and remix them as a starting point for your own.
Every widget shares a baseline of design tokens defined in static/style/widget-base.css — same header strip, same flat surface tiles, same status pills — so the panel reads as one design system.
| Weather | Hacker News | News (RSS) |
|---|---|---|
![]() |
![]() |
![]() |
| Todo | Year progress | World clock |
|---|---|---|
![]() |
![]() |
![]() |
| Sun & moon | Air-quality trend | GitHub contributions |
|---|---|---|
![]() |
![]() |
![]() |
| Countdown | Note | Clock |
|---|---|---|
![]() |
![]() |
![]() |
| Star map | Calendar | QR code |
|---|---|---|
![]() |
![]() |
![]() |
![]() |
| Generative art | Wikimedia POTD |
|---|---|
![]() |
![]() |
Plus APOD, Unsplash, gallery (folder rotation with portrait/landscape/square filter), xkcd, weather radar (RainViewer + CartoDB), and webpage-screenshot widgets. All can be set full_bleed so they paint edge-to-edge with no surrounding chrome.
- Home Assistant tile — bundle up to 8 HA entities into a tile (grid, list, or hero layout). Needs a long-lived access token in
/settings. - Melbourne PTV — live train/tram/bus/V-Line departures for any PTV stop. Needs a free PTV dev ID + API key; lookup tool at
/plugins/ptv/?q=....
Schedules fire pushes automatically. Each row gets a deterministic per-schedule colour so the day-band timeline reads at a glance. The "now" cursor slides across all rows in unison; the band starts at 06:00 so each calendar day reads top-down naturally.
Two schedule types:
- Interval — every N minutes within an optional
time_of_day_start..endwindow on selected days of the week. - Daily — once a day at HH:MM on selected days. Mid-day enables don't backfill the morning's missed firings.
Push anything right now. Source tabs across the top: File / Saved dashboard / Image URL / Webpage / History.
| Send (compose) | Send (history) |
|---|---|
![]() |
![]() |
The preview pane renders at panel aspect-ratio with the Floyd–Steinberg quantizer the panel will see. Fit modes for File + Image URL: fit (letterbox) · fill (cover-crop) · stretch (distort) · center (no scaling) · blurred (server-side composite — blurred cover-fit background with the original aspect-preserved on top).
The History tab is the live PushManager history with thumbnails of every published render. Click a thumbnail for a full-screen zoom; the resend button re-publishes the exact stored render without re-rendering; delete removes the row and (when no other row references the same digest) the PNG too.
49 themes bucketed by background lightness — White (the 6 stark-white-bg bold-accent themes: Cobalt / Flame / Magenta / Kelly / Royal / Cherry), Light (Paper / Linen / Ink / Burgundy / …), Medium, and Dark (Cyber / Embers / Reef / Flamingo / Peach + 15 monochrome + 5 neon). A chip picker at the top of /themes filters down to one bucket at a time, plus a "Mine" filter for user-saved themes once any exist.
Seven themes are designed around a vibrant foreground colour — the typography itself prints in saturated ink rather than neutral grey: Ink (navy on cream), Burgundy (wine on parchment), Embers (orange on brown), Cyber (electric green on near-black), and the coral family (Reef / Flamingo / Peach). On Spectra 6 these dither into the actual coloured inks rather than collapsing to a near-black.
The theme picker shows a compact preview mock — built from the same flat-surface widget chrome the real cells use — so any palette regression in a single token is visible immediately. Build a new theme at /themes; user-created ones save alongside the built-ins and bucket into "Mine" in the picker.
Per-plugin settings (the manifest declares which fields each plugin exposes), app-level MQTT + panel + base-URL config, and theme-builder admin. Secrets are masked over the wire — they live server-side and never round-trip back to the browser.
The status page summarises everything at a glance: MQTT bridge state, last push, listener log, available pages, scheduled jobs.
app/ Flask application
state/ mypy --strict — page model + stores (pages, schedules, history)
composer.py /compose/<page_id> + /_test/render (cell hydration, theming, fonts)
admin.py editor blueprint + /api/pages + /api/send + history endpoints
push.py mypy --strict — single-flight PushManager with debounce + LRU
scheduler.py tick loop, day-of-week + time-of-day windows, first-seen guard
renderer.py mypy --strict — Playwright wrapper, screenshot at panel resolution
quantizer.py Pillow gamut projection (Spectra 6) + Floyd–Steinberg
image_ops.py Server-side composites for the send pipeline (blurred-fit)
mqtt_bridge.py paho-mqtt publisher + listener-status subscriber; NullBridge fallback
docs/
architecture.md How the pieces fit together (Flask + Lit + Playwright + MQTT)
screenshots/ Documentation screenshots (used by this README)
wiki/ Source for the GitHub wiki pages
plugins/<id>/ Drop-a-folder plugin (kind=widget|theme|font|admin)
schema/ JSON Schemas (plugin manifest, page model)
static/
components/ Lit design system (id-* web components)
lib/ Shared modules (push-state.js, vendored Chart.js, …)
pages/ Editor + send + schedules + themes + settings entry points
style/ widget-base.css (shared widget chrome), tokens.css
composer.js Mounts plugins into shadow DOMs per cell
templates/ Jinja shells: compose, editor, send, schedules, themes, settings
tests/ Top-level pytest suite (push, scheduler, history, composer)
conftest.py Root fixtures shared with plugin smoke tests
tools/ gen-page-types.mjs (schema → .d.ts)
.github/ CI: Python + Bun + Playwright + bundle build
A widget is a folder under plugins/<id>/ with:
plugin.json— manifest declaring sizes, cell options, settings, render hints (dither, full_bleed)client.js— default exportrender(host, ctx)that paints into a shadow-DOM host elementclient.css— widget styling (link/static/style/widget-base.cssfor the shared chrome)server.py(optional) — declaresfetch(options, settings, ctx)for data, plus optional Flaskblueprint()for an admin UI
See the wiki tutorial for a walkthrough and plugins/example_full/ for a worked example exercising every contract feature.
MIT.






























