- Swift 79.3%
- JavaScript 11.1%
- Python 4.8%
- Shell 4%
- Makefile 0.8%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .agents/skills/agent-wiki | ||
| .claude/skills/agent-wiki | ||
| .wiki | ||
| docs | ||
| Protos | ||
| Resources | ||
| scripts | ||
| Sources/TVDashboard | ||
| Tests | ||
| .gitignore | ||
| AGENTS.md | ||
| CLAUDE.md | ||
| Makefile | ||
| Package.resolved | ||
| Package.swift | ||
| README.md | ||
TV Dashboard
A small macOS app that turns a TV into a read-only "kiosk" dashboard built from
widgets — a big digital clock and one or more of your Apple Notes — arranged on a
grid you configure from a control window on your Mac. The default layout shows the clock
above two notes side by side (Day 0 and Day 1). The TV just displays; you
drive everything from the Mac.
It's designed for a TV you reach over AirPlay (in this setup: a Hisense TV that
appears on the network as Living Room), used in Extend mode so the TV is a
second display rather than a mirror of your Mac.
What you need (prerequisites)
- A Mac running macOS 14 (Sonoma) or newer. (Built and tested on macOS 26 with Xcode 26 / Swift 6.3.)
- Xcode or the Xcode Command Line Tools — this is what provides the
swiftcompiler andmake. If you don't have them:xcode-select --install - Apple Notes with a note titled
Day 0(or whatever note you want to show). - The Mac and the TV on the same Wi-Fi / LAN.
No external packages — it builds entirely with the Swift toolchain.
Build & run
From the project folder, the one-liner is:
make run
That compiles the app, assembles TV Dashboard.app, and launches it. The control
window opens on your Mac.
Other targets:
| Command | What it does |
|---|---|
make build |
Just compile (no bundle). |
make app |
Build and assemble .build/TV Dashboard.app. |
make run |
Build, bundle, and open the app. |
make install |
Build, write a dist/*.dmg, install into ~/Applications, and relaunch the kiosk (only if the running instance is a debug build). |
make release |
Build a signed, distributable .dmg under dist/ (see Releases). |
make clean |
Delete build artifacts. |
Run it as the bundled app (
make run), not viaswift run. macOS attaches the "control Notes" permission to the app bundle and its identity, so launching the real.appis what gives you a clean permission prompt that sticks.
You can also open the folder in Xcode directly (File → Open → Package.swift) and
press Run, if you prefer.
Releases: install from a .dmg
If you were handed a TV Dashboard-<version>.dmg (rather than building from source):
- Double-click the
.dmgto mount it, then dragTV Dashboard.apponto the Applications alias in the window. - The app is self-signed, not notarized by Apple, so the first time you open it macOS
warns that it's from an unidentified developer. Get past it once, either way:
- Right-click the app → Open, then click Open in the dialog, or
- run
xattr -dr com.apple.quarantine "/Applications/TV Dashboard.app"in Terminal first.
- After that first launch it opens normally. Continue with the Apple Notes permission prompt below.
On your own machine you can skip the manual steps:
make installbuilds the app, writes the.dmgtodist/, copies the app into~/Applicationswith quarantine already cleared, and relaunches the kiosk if the running instance is a debug build. The admin panel's bottom-right footer shows the build identity (e.g.v1.1.0+a1b2c3d · build 94) so you can tell a dev build from a clean release at a glance.Maintainers: the release
.dmgis produced withmake release— seedocs/releasing.md.
First run: the Apple Notes permission prompt
The first time the app tries to read your notes, macOS shows:
"TV Dashboard" wants access to control "Notes".
Click OK (Allow). That's what lets the dashboard read the Day 0 note.
- If you dismissed it or clicked Don't Allow, either press "Grant Notes Access…" in the control window, or go to System Settings → Privacy & Security → Automation → TV Dashboard and turn on Notes.
- The control window's Status panel shows Apple Notes access: Granted once it's working.
Connect the TV over AirPlay (Extend)
AirPlay is connected manually:
- Open Control Center (menu-bar icon, top-right).
- Open Screen Mirroring.
- Select
Living Room. - Choose "Use As Separate Display" (Extend) — not Mirror — if macOS asks.
- Back in the app, press "Start Kiosk on External Display."
The dashboard appears full-screen on the TV. The button only enables once macOS sees a second display, so connect AirPlay first.
The control window's Status panel also tells you whether Living Room is
currently reachable on the network (Bonjour discovery) and whether an external
display is present, so you can sanity-check both before starting the kiosk.
Configure the dashboard (widgets & layout)
Everything is driven from the Dashboard Layout panel in the control window — the TV itself is read-only.
The dashboard is a vertical stack of rows; each row holds one or more widgets side by side. Two widget types ship today:
- Clock — 12/24-hour, optional seconds/date, and optional photo-backed sets. While a photo clock is running, its control-panel Next photo button advances that clock immediately; the action is runtime-only and does not change the saved layout.
- Note — shows an Apple Note. Pick it by typing its exact name, or press "Refresh Note List" and choose from the per-widget menu. Optional title header and a font-size scale.
Use the row/widget controls to add, remove, reorder, and resize: the height/width steppers set each row's / widget's share of the screen; "Add Row" adds a band, the + on a row adds a widget to it, and the trash buttons remove. "Load default (clock + 2 notes)" restores the starting layout.
Edits apply to the TV automatically (a moment after you stop changing things) and are saved
to ~/Library/Application Support/TV Dashboard/config.json, so the layout persists across
launches.
Verify live updates
- With the kiosk running on the TV, open
Day 0in Apple Notes on your Mac. - Type something.
- It appears on the TV within about 1–2 seconds (the app re-reads the note once a second).
- The "Last note update" time in the control window also advances.
How it renders
The whole dashboard is one CSS grid sized to exactly the TV screen (100vh × 100vw): nothing scrolls and nothing spills past the screen edge. Each widget lives in its own cell; if a note is taller than its cell it's clipped with a soft fade at the bottom as a "there's more" cue rather than scrolling. Widgets update independently — a note edit patches only that note's cell, so the clock never flickers and the layout never jumps.
Inside a note cell it renders the structure used in notes like Day 0:
- Bold text (section titles like
TO DO,House TODO). - Blank-line spacing.
- Numbered lists (
Rishav Todo). - Checklist rows with hollow circles (the unordered lists).
The TV surface is strictly read-only: no text selection, no menus, no links, and the mouse pointer is bounced back to your Mac's display if it wanders onto the TV.
Not in V1 (see docs/handoff-v1.md): images, attachments, links, tables, and
reliable checklist checked/unchecked state — Apple's public scripting interface
doesn't expose whether a checklist item is ticked, so V1 shows every checklist item the
same way. Recovering checked-state is planned for a later version.
Troubleshooting
The TV isn't discovered ("Living Room: Not found")
- Make sure the TV is on and on the same Wi-Fi as the Mac.
- Some networks block Bonjour/mDNS between devices ("AP/client isolation"). Try the same Wi-Fi band, or a network where the Mac can see the TV.
- You can still connect: open Control Center → Screen Mirroring and look for
Living Roomthere. The dashboard only needs the second display to run the kiosk; Bonjour status is a convenience check.
Notes permission denied
- System Settings → Privacy & Security → Automation → TV Dashboard → enable Notes.
- If TV Dashboard isn't listed, press "Grant Notes Access…" in the app to trigger the prompt, then allow it.
- Builds are signed with a stable self-signed identity (the
TVD Localcert), so the grant normally sticks across rebuilds. If that cert isn't installed the build falls back to ad-hoc signing, whose identity changes with the code — so macOS may re-prompt after a rebuild; just allow it again. (Distribution.dmgs use the same self-signed identity and are not notarized — seedocs/releasing.md.)
No second display after connecting AirPlay
- Make sure you chose Extend ("Use As Separate Display"), not Mirror.
- Give it a few seconds — the "Start Kiosk…" button enables automatically when the display appears (the app watches for display changes).
- If the TV connected as a mirror, open System Settings → Displays and set the TV to "Use as separate display" / extend.
- If the external display goes away while the kiosk is up, the app tears the kiosk down cleanly on its own.
The dashboard stopped updating after I edited the note
A note's name in Apple Notes is its first line. The app finds each note widget's note
by name the first time (e.g. Day 0), but then tracks it by a stable internal id, so
after that you can rename the heading and the dashboard keeps following it. The only thing
that breaks the link is the very first lookup: if no note has that exact first line
(e.g. you'd already renamed it), that cell shows a "Loading …" placeholder. Fix: set the
widget's note name to the note's current first-line text in the Dashboard Layout panel —
from then on, edits (including future renames) refresh live. If updates still don't appear,
check the "Last note update" time in the control window: if it's advancing, the app is
reading fine; if it's frozen, see the error line and the Notes-permission steps above.
macOS notification suppression / screen-sharing indicators (observe & document)
When you AirPlay/extend to a TV, macOS may change notification behavior and show screen-sharing-style indicators. For V1, observe and note what your Mac does:
- Whether a Focus / Do Not Disturb turns on automatically while mirroring/extending (System Settings → Focus, and the Control Center Focus tile).
- Whether a purple/orange screen-recording or AirPlay indicator appears in the menu bar.
- Whether notification banners still appear on your Mac (primary) display.
V1 uses Extend (a second display), not Mirror, which generally avoids the "mirroring to TV" Focus trigger — but confirm on your setup and jot down what you see. Actively preventing DND/notification suppression is planned for a later version; V1 only documents it.
Stopping / quitting
- Press "Stop Kiosk" to take the dashboard off the TV (keeps the control window).
- ⌘Q quits the app (this also stops the kiosk and releases the keep-awake power assertion).
- Closing the control window quits the app when the kiosk isn't running.
While the kiosk is active, the Mac is kept from sleeping its display (a power assertion), so the TV doesn't go dark mid-display.
Developer notes
- Architecture: native Swift / AppKit + SwiftUI. Notes are read in-process via
NSAppleScript(confined to a serial queue) behind aNotesSourceprotocol, so a deeper Notes reader can be swapped in later. - Widget rendering: the TV is a single locked-down
WKWebViewthat loads a static CSS-grid shell once (DashboardPageBuilder). Swift then drives it throughKioskBridge, which pushes the layout (applyConfig) and per-cell data (applyData) as base64-encoded JSON overevaluateJavaScript— so updates patch only the changed cell, with no reload. Adding a widget type = a case inWidgetProps, aWidgetRendererbranch, and a JS branch. - Config: the layout is a
CodableDashboardConfig(rows → widgets withfrweights- per-widget props), edited in the admin window and persisted by
ConfigStoreto~/Library/Application Support/TV Dashboard/config.json.
- per-widget props), edited in the admin window and persisted by
- Headless checks:
make testruns both suites (no GUI).make selftest(swift run TVDashboard --selftest) covers the Swift rendering/config/bridge-encoding logic;make jstestextracts the real shell JavaScript fromDashboardPageBuilderand runs it (against a stubbed DOM, fed the real--dump-configoutput via Node) to guard the Swift↔JS contract —applyConfig/applyDatalogic, not real CSS/layout. Needs Node. - Layout/structure:
Sources/TVDashboard/ main.swift app entry AppDelegate.swift windows AppState.swift state + multi-note polling + bridge push Config/ DashboardConfig (layout model) + ConfigStore (JSON persistence) Notes/ NotesSource protocol + AppleScript reader Render/ DashboardPageBuilder (web shell), KioskBridge, WidgetPayload, NoteHTMLRenderer (note sanitizer), KioskWebView Views/ KioskView (TV surface), AdminView (control panel + layout editor) Kiosk/ DisplayManager, KioskWindow, PowerManager Net/ BonjourBrowser