Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/actions/setup-native/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ inputs:
default: "0.16.0"
native-sdk-version:
description: "@native-sdk/cli version to install"
default: "0.8.4"
default: "0.9.0"
apply-patches:
description: Whether to run scripts/apply-sdk-patches.sh
default: "true"
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ zig-out/
.zig-cache/
dist/
.claude/worktrees/
.DS_Store
6 changes: 3 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

## What this is

A native desktop player (macOS + Linux + Windows) for the SUB/WAVE internet radio station, built on the **Vercel Native SDK**: declarative `.native` markup + Zig logic, rendered by the SDK's own engine — no browser, no WebView. Requires **Zig 0.16.0** and a global `@native-sdk/cli` **0.8.4+** (`npm i -g @native-sdk/cli`).
A native desktop player (macOS + Linux + Windows) for the SUB/WAVE internet radio station, built on the **Vercel Native SDK**: declarative `.native` markup + Zig logic, rendered by the SDK's own engine — no browser, no WebView. Requires **Zig 0.16.0** and a global `@native-sdk/cli` **0.9.0+** (`npm i -g @native-sdk/cli`).

## Commands

Expand Down Expand Up @@ -64,8 +64,8 @@ Data flow at runtime: timers poll `/api/now-playing`, `/api/state`, `/api/themes
- **The SDK cannot resize a live window.** Per-mode window shapes apply at next launch only.
- **A `hidden_inset_tall` masthead must pad `insets.left` AND `insets.right`.** The window-control cluster sits on a different edge per platform — macOS traffic lights lead, Windows min/max/close trail — so `onChrome` maps both into `chrome_leading` / `chrome_trailing` and `player-top.native` spacers both ends. Dropping the trailing one put the gear button under the DWM caption buttons on Windows, and fed the host's per-present caption-colour sampler (which reads the pixel 8px leading of the cluster and pushes it through `DWMWA_CAPTION_COLOR` + `USE_IMMERSIVE_DARK_MODE`) an antialiased glyph edge instead of flat header — the caption buttons flickered.
- **The FFT spectrum feed only emits while a window is visibly on screen** (occlusion gate in the SDK). A flat visualizer from an app launched in the background is not a bug — activate the app first.
- SDK 0.8.4 still has **no OS media-controls surface** (no MPNowPlayingInfoCenter / MPRemoteCommandCenter / MPRIS / hardware media keys — re-checked at the 0.8.4 upgrade). The substitutes are the tray extra, the in-window keyboard transport, and the background track toast: 0.8.4 added `fx.showNotification`, so a track change while the app is backgrounded posts a desktop notification. It is opt-in (back panel → NOTIFICATIONS) and the whole decision lives in `Model.shouldNotifyTrack` — pure, because the effect is inert and unrecorded under fake execution, so no test can observe the call itself.
- SDK 0.8.4 also has **no audio output-device API** — the platform seam is load/play/pause/stop/seek/volume, with no enumeration and no device property, so there is no in-app "play through these speakers" picker to build. Route it at the OS (`pavucontrol`/PipeWire, Windows volume mixer; macOS has nothing native). The upstream request lives in `docs/sdk-audio-device-request.md`.
- SDK 0.9.0 still has **no OS media-controls surface** (no MPNowPlayingInfoCenter / MPRemoteCommandCenter / MPRIS / hardware media keys — re-checked at the 0.9.0 upgrade). The substitutes are the tray extra, the in-window keyboard transport, and the background track toast: 0.8.4 added `fx.showNotification`, so a track change while the app is backgrounded posts a desktop notification. It is opt-in (back panel → NOTIFICATIONS) and the whole decision lives in `Model.shouldNotifyTrack` — pure, because the effect is inert and unrecorded under fake execution, so no test can observe the call itself.
- SDK 0.9.0 also has **no audio output-device API** — the platform seam is load/play/pause/stop/seek/volume, with no enumeration and no device property, so there is no in-app "play through these speakers" picker to build. Route it at the OS (`pavucontrol`/PipeWire, Windows volume mixer; macOS has nothing native). The upstream request lives in `docs/sdk-audio-device-request.md`.
- **Cover art loads through `fx.loadImage`, not `fx.fetch` + `registerImageBytes`** — the fetch and the platform decode run on a worker thread, with a content-addressed disk cache under the OS caches dir. Its ImageId **is** the effect key, which is why the counter starts at `keys.cover_image_base` (1000) clear of every other effect key. An id only reaches `model.cover_id` once the runtime reports `.loaded`; anything else leaves the initials disc standing.
- Stream format is listener-selectable (`stream_format.zig`): MP3 is the always-available floor, AAC additionally decodes on macOS (AVPlayer) and Windows (Media Foundation), and the Ogg-encapsulated Opus/FLAC mounts are Linux-only (neither AVPlayer nor Media Foundation has an Ogg demuxer). Every host asserts its **full** matrix in the `platformSupports` test — Windows fell through the defensive `else` for two releases and shipped MP3-only because cross-compiling never ran the suite for the target. Linux offers Ogg mounts optimistically and `scheduleReconnect` drops a failing non-MP3 pick back to MP3 after 3 retries. The picker **lists** every platform-decodable mount and lets you tune only the ones the station advertises via the `stream` flags on `/api/now-playing` — unserved mounts say so on their detail line and their press is absorbed in `update`, so a dead value never reaches `format_pref`. Its entry point is the pressable format chip in the transport deck's SIGNAL row (plus the always-present back-panel row).
- `native automate assert` regex does **not** support `|` alternation.
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ ones appear.

## Build & run

Requires **Zig 0.16.0** and `@native-sdk/cli` **0.8.4+**
Requires **Zig 0.16.0** and `@native-sdk/cli` **0.9.0+**
(`npm i -g @native-sdk/cli`) — **plus one local SDK patch**. After every SDK
install/upgrade:

Expand Down Expand Up @@ -204,15 +204,15 @@ serve are listed saying exactly that rather than quietly absent. The same row
is also in the back panel under SIGNAL.

**Playing through a different output device.** Not offered in-app: the SDK's
audio surface (re-checked at 0.8.4) is load/play/pause/stop/seek/volume, with
audio surface (re-checked at 0.9.0) is load/play/pause/stop/seek/volume, with
no device enumeration or output-device property on any host, so there is
nothing honest to build a picker on. Route it at the OS instead —
`pavucontrol` or any PipeWire patchbay on Linux, Settings → System → Sound →
Volume mixer on Windows. macOS has no per-app routing without a third-party
virtual audio driver. The API request is written up in
[`docs/sdk-audio-device-request.md`](docs/sdk-audio-device-request.md).

**OS media integration.** The SDK (re-checked at 0.8.4) has no system
**OS media integration.** The SDK (re-checked at 0.9.0) has no system
now-playing or media-key surface — no `MPNowPlayingInfoCenter`/
`MPRemoteCommandCenter` on macOS, no MPRIS on Linux — so hardware play/pause
keys and the OS Now Playing widget can't be wired up yet (SDK feature request).
Expand Down
Loading
Loading