sjnmusic GUI (Rust)
Pure-Rust remote control for sjnmusicd. Built on
egui + eframe.
Talks to the daemon over HTTP at 127.0.0.1:14567 by default and owns no
audio state.
This replaces the previous Electron GUI. The daemon API is unchanged.
Build & Run
Prerequisites: Rust 1.80+ (stable). On Linux you may also need:
libssl-devif you ever swapureqforreqwest(not needed today)libdbus-1-dev+ a system tray daemon (e.g.libappindicator, KDE StatusNotifierItem watcher, GNOME Shell extension) for the optional tray icon. The tray fails gracefully if one is missing.
cd gui-rs
cargo run --release
To launch with the system tray enabled the GUI runs the tray at every startup, but it falls back silently on systems without a tray service, so it's safe to leave it on.
Environment variables
| Variable | Default | Meaning |
|---|---|---|
SJNMUSIC_HOST |
127.0.0.1 |
Daemon host |
SJNMUSIC_PORT |
14567 |
Daemon port |
SJNMUSIC_THEME |
dark |
dark or light (also persisted to disk) |
RUST_LOG |
info |
Standard env_logger filter |
Features (feature parity with the Electron GUI)
- Sidebar with 8 nav routes + offline banner
- Now-playing footer with transport, seek-back, repeat, volume
- Library view with fuzzy-search filter
- Queue view with shuffle / clear / skip-to / remove
- Playlists: list view + single playlist view (add/remove/reorder/ rename/duplicate/delete/play)
- Search + Download with yt-dlp candidate picker and inline thumbnails
- Downloads: live status table auto-refresh
- History (last 100 plays)
- Stats with cards and top songs
- Settings (theme picker)
- Spacebar = pause/resume
- System tray (best effort): now-playing label, pause/resume, skip, show window, quit
Architecture
src/
main.rs entry: eframe::run_native, tray bootstrap
app.rs SJNMusicApp: eframe::App impl, panel layout, router
state.rs AppState, polling worker, Route enum, Theme
daemon.rs HTTP client + typed daemon responses
fmt.rs formatting helpers (time, html escape)
tray.rs tray-icon setup (graceful fallback)
thumbnails.rs async thumbnail download + TextureHandle cache
views/
mod.rs dispatch helper
library.rs fuzzy-filtered song list
queue.rs current queue + shuffle / clear
playlists.rs list view + create
playlist.rs single playlist (add/remove/reorder/rename/...)
search_download.rs yt-dlp picker + thumbnails
downloads.rs live status table
history.rs recent plays
stats.rs aggregate cards + top songs
settings.rs theme picker
The UI thread owns an Arc<DaemonClient> and a background worker thread
polls /now-playing, /queue, and /downloads every second, pushing
snapshots into Arc<Mutex<Snapshot>>. After each poll the worker calls
ctx.request_repaint() so the UI picks up changes.
UI actions (play / pause / skip / volume slider / ...) run as
fire-and-forget std::thread::spawns calling DaemonClient directly,
so a stalled daemon never freezes the GUI.