Skip to content
Theme
Accent
Voice
SkillCase StudiesComponentsExamplesThemingBest PracticesPhilosophyGitHub ↗
← All case studiesTauri 2 + Rust, WebView2 renderer

lyric-viewer

Fullscreen, beat-aware synced lyric overlay for Windows.

github.com/dhruvch1244/lyric-viewer ↗

Physical separation, with one named exception

The Rust side owns SMTC (Windows media session) integration, every network/LLM call, caching, and job scheduling — none of it references the WebView2 renderer. Delete the whole UI layer and the Rust side still makes sense on its own; the renderer could be swapped for a different drawing surface without touching it. There's exactly one deliberate crossing of that boundary: a synchronous JS DSP fallback for local file playback, used only when native analysis isn't available yet. It's allowed to exist because it's narrow (one named code path, not a general escape hatch), documented at the point of use (why it's there, what keeps it from becoming the common case), and unmistakably an exception rather than a pattern to copy.

CLAUDE.md
`src-tauri/` (Rust) owns the window, OS integration (SMTC, tray, wallpaper
mode, power-state watchers), all network/LLM calls, local-file decode, and
all cached state. `src/renderer/` (plain JS, no framework/bundler) is a
WebView2 page that **draws only** — it reacts to Tauri events
(`mood`, `genre`, `lyrics`, `attribution`, `beatmap`, `track`, `tick`, ...)
via `src/renderer/tauri-shim.js` and never originates a network call, cache
write, or CPU-heavy analysis pass itself, with one narrow exception: local
file playback has a synchronous JS DSP fallback in `analyze.js` for when
native analysis is unavailable (see the async-command note below — this
fallback path is also *slow*, so keeping native analysis correctly async is
what keeps it from ever being needed on the hot path).

One scheduler, sized to what it actually contends for

The job engine (src-tauri/src/jobs/mod.rs) doesn't give every kind of background work the same treatment. I/O-bound work gets a dedicated concurrency lane sized for the actual fan-out — the resource being protected is external responsiveness, not CPU. CPU-bound analysis gets a worker pool sized to N-1 cores, leaving one core for the UI thread. A resource that can only safely support one concurrent user gets a lane of exactly 1. A dedup_key means two triggers requesting lyrics for the same track collapse into one job instead of two racing network calls. Cancellation is a CancelToken tree, so cancelling a parent (the user changed tracks) cancels every child job transitively. Priority (Now/Next/Idle) is resolved when a lane actually frees up, not frozen at submit time. Crash survival — a SQLite-backed journal — is layered on top as a separate concern, not baked into the scheduling logic itself.

src-tauri/src/jobs/mod.rs
let cpu_threads = std::thread::available_parallelism()
    .map(|n| n.get().saturating_sub(1).max(1))
    .unwrap_or(1);

let mut lanes = Vec::with_capacity(Lane::ALL.len());
for lane in Lane::ALL {
    let (threads, below_normal, limit, name) = match lane {
        // 6 concurrent network calls is enough to fan out to every
        // lyric and artwork source at once without looking like abuse
        // to any of them.
        Lane::Io => (6, false, 6, "job-io"),
        Lane::Cpu => (cpu_threads, true, cpu_threads, "job-cpu"),
        Lane::Inference => (1, true, 1, "job-infer"),
    };
    lanes.push(spawn_lane(
        Pool::new(threads, below_normal, name),
        limit,
        Arc::clone(&registry),
    ));
}
src-tauri/src/jobs/mod.rs
fn submit(&self, job: Box<dyn Runnable>, priority: Priority) -> bool {
    let key = job.dedup_key();
    let track = job.track();
    let cancel = CancelToken::default();

    // Register before queueing. Doing it the other way round would let two
    // submissions of the same key both pass the check and both queue.
    {
        let mut reg = self.registry.lock().unwrap_or_else(|e| e.into_inner());
        if reg.inflight.contains_key(&key) {
            return false;
        }
        reg.inflight.insert(key.clone(), cancel.clone());
        if let Some(track) = &track {
            reg.by_track.entry(track.clone()).or_default().insert(key.clone());
        }
    }

    let lane = self.lanes[job.lane().index()].tx[priority.index()].clone();
    if lane.send(Envelope { job, cancel }).is_err() {
        release(&self.registry, &key, track.as_deref());
        return false;
    }
    true
}

Measure, don't guess

A perf harness (scripts/perf/) exists specifically because informal benchmarking wasn't trustworthy enough to make claims from. One comment in the codebase records that results "vary 3-4x run to run on this hardware" — the kind of measured, specific number that's expensive to reconstruct once lost, so it stayed in the code as a comment instead of getting cleaned up as noise.

CLAUDE.md
Drives the real app over the Chrome DevTools Protocol — this project's
standing rule is **measure the real thing, never guess or extrapolate from
"observed" frame rate** (repeated identical runs vary 3-4x on this hardware).

```sh
npm run perf                    # steady-state scenario harness (dev build)
npm run perf:build-release       # build an instrumented release binary (own CARGO_TARGET_DIR, never bundled)
npm run perf -- --build release  # run the harness against it
npm run perf:startup             # from-launch startup-burst measurement
```

Standard library first, for the parts that actually qualify

track_key is a hand-rolled djb2 hash of lower(artist)|lower(title); the streak counter uses hand-rolled weekday math. Both pass the actual test for hand-rolling something instead of reaching for a dependency: the whole problem fits in a screen or two of code, and neither will ever need a patch for reasons outside this project's control (no security advisory, no spec update, no external edge case). A hand-rolled timezone-aware date library would fail that same test immediately — which is exactly why this project doesn't have one.

src-tauri/src/commands/lyrics_cmds.rs
/// Filename-safe cache key for a track (djb2 hash of normalised artist+title).
pub(crate) fn track_key(artist: &str, title: &str) -> String {
    let base = format!("{}|{}", artist.to_lowercase().trim(), title.to_lowercase().trim());
    let mut hash: u64 = 5381;
    for b in base.bytes() {
        hash = hash.wrapping_mul(33).wrapping_add(b as u64);
    }
    format!("{hash:016x}")
}
src-tauri/src/stats.rs
fn current_streak(days: &HashSet<i64>, today: i64) -> i64 {
    let mut cursor = if days.contains(&today) {
        today
    } else if days.contains(&(today - 1)) {
        today - 1
    } else {
        return 0;
    };
    let mut streak = 0i64;
    while days.contains(&cursor) {
        streak += 1;
        cursor -= 1;
    }
    streak
}

Honest about drift, not just about the present

The project's own docs/JOB-ENGINE.md tracks status per phase rather than describing one static architecture — including which pieces shipped differently than planned (a JS file slated for deletion that's still there on purpose, a phase whose proposed fixes were rejected once the numbers came back). Rather than silently rewrite history to match current reality, drift gets named explicitly. A doc that quietly claims to be fully current when it isn't is worse than one that admits where it moved — the first one costs the next reader (human or AI) real time re-discovering the gap themselves.

docs/JOB-ENGINE.md
**Status:** Phase 1 landed except the SQLite journal, which moved into Phase 3 and is done (§7.1, §7.7). Phase 2 landed except the local-folder `Idle` backfill, which moved to Phase 7 (§7.2). **Phase 3 is complete**... Deleting `whisper.js` was the one item dropped rather than done, deliberately: §7.10 explains why the WebView path stays as the vocal-isolation fallback. Phase 4 closed — profiled, and the fixes it proposed were rejected on the measurements (§7.9).
Accent
Voice
Corner radius

This is applied to the whole site live, not just this panel. Export it as CSS →