Skip to content

feat: add rustify-tui terminal music player with 3-tier feature set#2

Merged
attmous merged 26 commits intomainfrom
feat/rustify-tui
Apr 9, 2026
Merged

feat: add rustify-tui terminal music player with 3-tier feature set#2
attmous merged 26 commits intomainfrom
feat/rustify-tui

Conversation

@attmous
Copy link
Copy Markdown
Owner

@attmous attmous commented Apr 9, 2026

Summary

  • Full TUI music player (crates/rustify-tui) built on rustify-core with ratatui + crossterm
  • 3 tiers of features implemented: playback essentials, rich experience, power user
  • 24 source files, 139 tests, 3 crates in workspace
  • Design specs and implementation plans documented in docs/superpowers/

Tier 1: Playback Essentials

  • Shuffle (Fisher-Yates) and repeat (Off/All/One) modes
  • Seek keybindings (Left/Right +/-5s)
  • Gapless playback via dual-decode MixStage
  • Album art extraction (embedded tags + sidecar files)

Tier 2: Rich Experience

  • Audio spectrum visualizer (1024-pt FFT, 40 bars, sqrt scaling)
  • Waveform oscilloscope mode (Shift+V toggle)
  • Color theme system (5 presets: default/nord/dracula/gruvbox/catppuccin + custom TOML)
  • Fuzzy search via nucleo-matcher

Tier 3: Power User

  • Crossfade support (configurable duration)
  • Replay gain tag reading and volume normalization
  • Lyrics extraction (embedded tags + .lrc sidecar)
  • ListenBrainz scrobbling (50%/4min rules)
  • MPRIS stub crate (ready for Linux D-Bus)

Documentation

  • docs/TUI.md — full feature docs, keybindings, config, architecture
  • rules/DEVELOPMENT.md — code patterns and guidelines
  • rules/CLAUDE.md — session guidelines for future development

Test plan

  • cargo test --workspace passes (139 tests)
  • cargo run -p rustify-tui -- /path/to/music launches TUI, plays audio
  • Shuffle/repeat/seek keybindings work
  • Visualizer renders spectrum bars while playing
  • Theme switching via config file
  • Fuzzy search with / finds tracks

🤖 Generated with Claude Code

attmous and others added 23 commits April 8, 2026 05:18
Design spec for a rich terminal music player built on rustify-core.
Covers architecture (ratatui + crossbeam event loop), sidebar+main
layout, library browsing, playlist management, and album art rendering.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
16-task plan covering crate scaffolding, event system, app state,
UI layout (sidebar + main panel + now-playing), library indexing,
player integration, playlist/queue management, and mouse support.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds config, event system, app state, library index, UI layout
(sidebar, main panel, now-playing bar), player integration,
queue/playlist management, search, mouse support, and status bar.

44 tests passing across all modules.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Covers shuffle/repeat modes, gapless playback via dual-decode mixer,
seek keybindings, and album art extraction/rendering.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
7-task plan covering shuffle/repeat in Tracklist, Player API,
TUI keybindings, album art extraction, and gapless dual-decode mixer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Core: RepeatMode enum, Fisher-Yates shuffle in Tracklist, Player API
for set_shuffle/set_repeat with ModeChanged callbacks.

TUI: s/r keybindings for shuffle/repeat, left/right arrows for ±5s
seek, [S]/[R]/[R1] indicators in now-playing bar.

110 tests passing across workspace.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Core: new art.rs module extracts cover art from embedded tags (lofty)
with sidecar file fallback (cover.jpg, folder.jpg, etc).

TUI: background art loading on track change, art placeholder in
now-playing bar with mode indicators [S]/[R]/[R1].

114 tests passing across workspace.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds TrackEnding event when decode nears end (~3s remaining),
pre-starts next decode thread, and swaps audio channels in the
cpal callback for seamless track transitions. The MixStage
architecture supports future crossfade (Tier 3) by design.

114 tests passing across workspace.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Covers audio spectrum visualizer (FFT + waveform in now-playing bar),
color theme system (5 presets + custom TOML themes), and fuzzy search
with nucleo-matcher and match highlighting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
4-task plan: color themes, fuzzy search, core sample buffer,
and audio spectrum visualizer with FFT + waveform modes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New theme.rs module with default, nord, dracula, gruvbox, catppuccin
presets. All UI modules now use theme colors instead of hardcoded
values. Custom themes via [theme.custom] in tui.toml.

119 tests passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tcher

Library::fuzzy_search() returns ranked results with match scores
and highlighted character indices. 123 tests passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Covers crossfade (extends dual-decode mixer), MPRIS media keys
(Linux D-Bus, feature-gated), ListenBrainz scrobbling, replay gain
normalization, and lyrics display (embedded tags + .lrc sidecar).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Widen track info column, add ellipsis for long album names
- Replace chunky Gauge with thin unicode progress bar (━/━)
- Increase visualizer from 24 to 40 bars
- Add sqrt scaling for better frequency visibility

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- docs/TUI.md: complete documentation of features, keybindings,
  config, architecture, what was built, and what's next
- rules/DEVELOPMENT.md: code patterns, file organization, testing,
  commit style, Pi constraints
- rules/CLAUDE.md: session guidelines for future Claude interactions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 099613d244

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +451 to +455
let handle = thread::Builder::new()
.name("rustify-decode-pending".into())
.spawn(move || {
decode_thread(uri, pending_tx, control_rx, event_tx, crossfade_ms);
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Isolate pending decoder events from active playback

When gapless prebuffering starts, this spawns a full decode_thread on the same event_tx before the current track has ended. decode_thread emits TrackChanged immediately and can emit DecodeFailed, so the UI/scrobbler can switch to the next song several seconds early (prebuffer window) and a failure to open the next track can incorrectly force the player into Stopped while the current track is still playing. Pending decoders need separate/internal events until promotion at TrackEnded.

Useful? React with 👍 / 👎.

Comment on lines +458 to +462
self.pending_decode = Some(DecodeHandle {
control_tx,
_thread: handle,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Cancel pending gapless decoder on transport resets

A pending decoder handle is stored here, but reset paths (handle_stop, handle_next, LoadTrackUris, etc.) only stop decode_handle and never tear down pending_decode/its queued audio. If a user stops or skips during the prebuffer window, that stale pending stream can still be promoted/consumed later, leading to unexpected playback of the wrong track after a stop/queue change.

Useful? React with 👍 / 👎.

Comment on lines +81 to +83
if config.crossfade_ms > 0 {
player.set_crossfade(config.crossfade_ms);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Wire replay-gain config into playback path

The config now exposes replay_gain, and core adds replay-gain metadata parsing, but startup only applies crossfade_ms and never reads or applies replay_gain. In practice, enabling replay gain in tui.toml has no effect, so loudness normalization is silently non-functional despite the new setting.

Useful? React with 👍 / 👎.

attmous and others added 3 commits April 9, 2026 23:30
Resolve all clippy -D warnings: unused imports, dead code, manual
range contains, loop variable indexing. Apply cargo fmt formatting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add .claude/, .clone/, .superpowers/ to .gitignore.
Remove accidentally tracked worktree and brainstorm files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Gapless TrackChanged timing: pending decoders now emit
   PendingTrackReady instead of TrackChanged. Metadata is held
   until promotion at TrackEnded, preventing early UI/scrobbler
   switch. PendingDecodeFailed discards the pending decoder
   silently without affecting the current track.

2. Pending decode cleanup: added stop_pending_decode() method
   that tears down pending_decode, clears pending_track, and
   removes the pending audio slot. Called from stop_decode(),
   which is used by handle_stop, handle_next, LoadTrackUris,
   and ClearTracklist.

3. Replay gain wiring: config.replay_gain is now read at startup
   and applied on TrackChanged. Reads REPLAYGAIN_TRACK_GAIN tag,
   computes gain factor (clamped 0.1x-2.0x), adjusts volume
   relative to base_volume.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@attmous attmous merged commit dae7df5 into main Apr 9, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant