diff --git a/src/app/handler.rs b/src/app/handler.rs
@@ -221,11 +221,6 @@ pub fn handle_key_event(key: KeyEvent, app: &App, ui: &mut Ui) -> Vec<AppAction>
return actions;
}
- // In search input mode, ignore (search handled by modal now)
- if app.search_mode {
- return actions;
- }
-
// Chord: Ctrl+E pressed, waiting for next key
if ui.chord_pending {
ui.chord_pending = false;
diff --git a/src/app/mod.rs b/src/app/mod.rs
@@ -62,9 +62,6 @@ pub struct App {
pub queue: QueueState,
pub library: Library,
pub music_dir: PathBuf,
- pub search_query: String,
- pub search_mode: bool,
- pub search_results: Vec<usize>,
pub playlists: Vec<state::Playlist>,
pub sync_state: SyncState,
pub initial_scan_complete: bool,
@@ -87,9 +84,6 @@ impl App {
queue: QueueState::default(),
library: Library::new(),
music_dir,
- search_query: String::new(),
- search_mode: false,
- search_results: Vec::new(),
playlists: vec![state::Playlist::new("Bookmarks")],
sync_state: SyncState::Idle,
initial_scan_complete: false,
@@ -207,14 +201,12 @@ impl App {
self.queue.current_index =
if self.queue.tracks.is_empty() { None } else { Some(0) };
self.queue.selected_index = 0;
- self.queue.scroll_offset = 0;
self.resync_shuffle_order(false);
}
AppAction::ClearQueue => {
self.queue.tracks.clear();
self.queue.current_index = None;
self.queue.selected_index = 0;
- self.queue.scroll_offset = 0;
// Without this the engine keeps playing a track the queue no
// longer contains, and its position updates keep the clock running.
self.stop_playback();
@@ -372,9 +364,6 @@ impl App {
self.queue.selected_index = self.queue.selected_index.min(
self.queue.tracks.len().saturating_sub(1)
);
- self.queue.scroll_offset = self.queue.scroll_offset.min(
- self.queue.tracks.len().saturating_sub(1)
- );
// Remap playlists. Entries the new library lacks are parked in `missing`
// rather than dropped, and anything parked earlier is restored if it came
@@ -401,11 +390,6 @@ impl App {
pl.missing = missing;
}
- // Remap search results
- if !self.search_query.is_empty() {
- self.search_results = new_lib.search(&self.search_query);
- }
-
self.library = new_lib;
self.sync_state = SyncState::Idle;
self.resync_shuffle_order(true);
@@ -622,7 +606,6 @@ mod tests {
sample_rate: None,
bit_depth: None,
channels: None,
- lyrics: None,
})
.collect();
app.handle_action(AppAction::ReplaceQueue((0..len).collect()));
diff --git a/src/app/state.rs b/src/app/state.rs
@@ -148,7 +148,6 @@ pub struct QueueState {
pub tracks: Vec<usize>,
pub current_index: Option<usize>,
pub selected_index: usize,
- pub scroll_offset: usize,
/// Positions into `tracks`, in shuffled play order — a real permutation, so
/// a shuffled pass reaches every track exactly once and can end. Empty while
/// shuffle is off.
diff --git a/src/audio/player.rs b/src/audio/player.rs
@@ -96,10 +96,10 @@ fn player_thread(cmd_rx: Receiver<PlayerCommand>, event_tx: Sender<Event>) {
play_start = Some(Instant::now());
let _ = event_tx.send(Event::Audio(AudioEvent::Playing));
}
- Err(e) => {
+ Err(_) => {
duration = 0.0;
play_start = None;
- let _ = event_tx.send(Event::Audio(AudioEvent::TrackError(e)));
+ let _ = event_tx.send(Event::Audio(AudioEvent::TrackError));
}
}
}
diff --git a/src/event/input.rs b/src/event/input.rs
@@ -21,8 +21,8 @@ pub fn spawn_input_thread(tx: Sender<Event>) -> std::thread::JoinHandle<()> {
break;
}
}
- Ok(CtEvent::Resize(w, h))
- if tx.send(Event::Resize(w, h)).is_err() => {
+ Ok(CtEvent::Resize(_, _))
+ if tx.send(Event::Resize).is_err() => {
break;
}
_ => {}
diff --git a/src/event/mod.rs b/src/event/mod.rs
@@ -1,22 +1,22 @@
pub mod input;
#[derive(Debug)]
-#[allow(dead_code)]
pub enum Event {
Key(crossterm::event::KeyEvent),
Mouse(crossterm::event::MouseEvent),
- Resize(u16, u16),
+ /// Dimensions are not carried: the next draw re-queries the terminal.
+ Resize,
Tick,
Audio(AudioEvent),
LibraryReady(crate::library::Library),
}
#[derive(Debug, Clone)]
-#[allow(dead_code)]
pub enum AudioEvent {
PositionUpdate { position_secs: f64, duration_secs: f64 },
TrackFinished,
- TrackError(String),
+ /// The current track could not be decoded.
+ TrackError,
/// The output device could not be opened. The player thread exits after
/// sending this, so every later command is dropped — the UI has to say so
/// rather than show a progress bar advancing over silence.
diff --git a/src/library/mod.rs b/src/library/mod.rs
@@ -40,17 +40,6 @@ impl Library {
result
}
- #[allow(dead_code)]
- pub fn get_album_artists(&self) -> Vec<String> {
- let mut set = BTreeSet::new();
- for t in &self.tracks {
- if !t.album_artist.is_empty() {
- set.insert(t.album_artist.clone());
- }
- }
- set.into_iter().collect()
- }
-
pub fn get_genres(&self) -> Vec<String> {
let mut set = BTreeSet::new();
for t in &self.tracks {
@@ -91,16 +80,6 @@ impl Library {
.collect()
}
- #[allow(dead_code)]
- pub fn get_tracks_by_album_artist(&self, album_artist: &str) -> Vec<usize> {
- self.tracks
- .iter()
- .enumerate()
- .filter(|(_, t)| t.album_artist == album_artist)
- .map(|(i, _)| i)
- .collect()
- }
-
pub fn get_tracks_by_album(&self, album: &str) -> Vec<usize> {
self.tracks
.iter()
@@ -119,17 +98,6 @@ impl Library {
.collect()
}
- #[allow(dead_code)]
- pub fn get_albums_by_album_artist(&self, album_artist: &str) -> Vec<String> {
- let mut set = BTreeSet::new();
- for t in &self.tracks {
- if t.album_artist == album_artist && !t.album.is_empty() {
- set.insert(t.album.clone());
- }
- }
- set.into_iter().collect()
- }
-
pub fn get_formats(&self) -> Vec<String> {
let mut set = BTreeSet::new();
for t in &self.tracks {
diff --git a/src/library/track.rs b/src/library/track.rs
@@ -22,8 +22,6 @@ pub struct Track {
pub sample_rate: Option<u32>,
pub bit_depth: Option<u8>,
pub channels: Option<u8>,
- #[allow(dead_code)]
- pub lyrics: Option<String>,
}
impl Track {
@@ -71,9 +69,6 @@ impl Track {
sample_rate: properties.sample_rate(),
bit_depth: properties.bit_depth(),
channels: properties.channels(),
- lyrics: tag
- .and_then(|t| t.get_string(&ItemKey::Lyrics))
- .map(|s| s.to_string()),
})
}
@@ -155,7 +150,6 @@ mod tests {
sample_rate,
bit_depth,
channels,
- lyrics: None,
}
}
diff --git a/src/main.rs b/src/main.rs
@@ -208,7 +208,6 @@ fn run_app(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>) -> Result<()>
// Handle queue selection directly for playlist focus
// Skip when any modal is open
if app.focus == FocusedPane::Playlist
- && !app.search_mode
&& !ui.show_search_modal
&& !ui.show_help_modal
&& !ui.show_playlist_modal
@@ -233,7 +232,7 @@ fn run_app(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>) -> Result<()>
handler::handle_mouse_event(mouse, &app, &mut ui, area)
}
}
- Event::Resize(_, _) => {
+ Event::Resize => {
vec![] // Will re-render on next loop
}
Event::Tick => {
@@ -272,7 +271,7 @@ fn run_app(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>) -> Result<()>
duration_secs,
}],
AudioEvent::TrackFinished => vec![app::AppAction::TrackFinished],
- AudioEvent::TrackError(_) => vec![app::AppAction::TrackFailed],
+ AudioEvent::TrackError => vec![app::AppAction::TrackFailed],
AudioEvent::DeviceError(msg) => {
app.audio_error = Some(msg);
app.playback.state = app::state::PlayState::Stopped;
diff --git a/src/ui/panes/album_artists_pane.rs b/src/ui/panes/album_artists_pane.rs
@@ -1,186 +0,0 @@
-use crossterm::event::{KeyCode, KeyEvent, MouseButton, MouseEvent, MouseEventKind};
-use ratatui::layout::Rect;
-use ratatui::style::{Color, Modifier, Style};
-use ratatui::text::{Line, Span};
-use ratatui::widgets::{Block, Borders, List, ListItem, Scrollbar, ScrollbarOrientation, ScrollbarState};
-use ratatui::Frame;
-
-use crate::app::{App, AppAction};
-use crate::ui::pane::Pane;
-use crate::ui::theme::Theme;
-
-const HOVER_BG: Color = Color::Indexed(238);
-
-pub struct AlbumArtistsPane {
- pub selected: usize,
- pub scroll_offset: usize,
- pub hover_row: Option<usize>,
-}
-
-impl AlbumArtistsPane {
- pub fn new() -> Self {
- Self {
- selected: 0,
- scroll_offset: 0,
- hover_row: None,
- }
- }
-}
-
-impl Pane for AlbumArtistsPane {
- fn render(&mut self, frame: &mut Frame, area: Rect, focused: bool, app: &App, theme: &Theme) {
- let album_artists = app.library.get_album_artists();
- let count = album_artists.len();
- let border_color = if focused {
- theme.border_focused
- } else {
- theme.border_unfocused
- };
-
- let block = Block::default()
- .borders(Borders::ALL)
- .border_style(Style::default().fg(border_color))
- .title_style(Style::default().fg(if focused {
- theme.border_focused
- } else {
- theme.fg
- }));
-
- let inner = block.inner(area);
- let inner_height = inner.height as usize;
-
- // Auto-scroll
- if count > 0 {
- if self.selected < self.scroll_offset {
- self.scroll_offset = self.selected;
- }
- if inner_height > 0 && self.selected >= self.scroll_offset + inner_height {
- self.scroll_offset = self.selected - inner_height + 1;
- }
- }
-
- let has_scrollbar = count > inner_height;
- let highlight = Style::default()
- .bg(theme.highlight_bg)
- .fg(theme.highlight_fg)
- .add_modifier(Modifier::BOLD);
-
- let items: Vec<ListItem> = album_artists
- .iter()
- .enumerate()
- .skip(self.scroll_offset)
- .take(inner_height)
- .map(|(i, artist)| {
- let is_selected = i == self.selected;
- let is_hovered = self.hover_row == Some(i);
- let style = if is_selected && focused {
- highlight
- } else if is_hovered {
- Style::default().fg(theme.fg).bg(HOVER_BG)
- } else {
- Style::default().fg(theme.fg)
- };
- ListItem::new(Line::from(Span::styled(format!(" {}", artist), style)))
- })
- .collect();
-
- let list = List::new(items).block(block);
- frame.render_widget(list, area);
-
- if has_scrollbar {
- let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
- .begin_symbol(None)
- .end_symbol(None);
- let mut scrollbar_state = ScrollbarState::new(count)
- .position(self.scroll_offset);
- frame.render_stateful_widget(
- scrollbar,
- area.inner(ratatui::layout::Margin { vertical: 1, horizontal: 0 }),
- &mut scrollbar_state,
- );
- }
- }
-
- fn handle_key(&mut self, key: KeyEvent, app: &App) -> Option<AppAction> {
- let album_artists = app.library.get_album_artists();
- let count = album_artists.len();
- if count == 0 {
- return None;
- }
-
- match key.code {
- KeyCode::Char('j') | KeyCode::Down => {
- if self.selected < count - 1 {
- self.selected += 1;
- }
- None
- }
- KeyCode::Char('k') | KeyCode::Up => {
- if self.selected > 0 {
- self.selected -= 1;
- }
- None
- }
- KeyCode::Enter => {
- if self.selected < count {
- let tracks =
- app.library.get_tracks_by_album_artist(&album_artists[self.selected]);
- if !tracks.is_empty() {
- return Some(AppAction::AddToQueue(tracks));
- }
- }
- None
- }
- KeyCode::Home | KeyCode::Char('g') => {
- self.selected = 0;
- self.scroll_offset = 0;
- None
- }
- KeyCode::End | KeyCode::Char('G') => {
- self.selected = count.saturating_sub(1);
- None
- }
- _ => None,
- }
- }
-
- fn handle_mouse(&mut self, event: MouseEvent, area: Rect, app: &App) -> Option<AppAction> {
- let block = Block::default().borders(Borders::ALL);
- let inner = block.inner(area);
- let count = app.library.get_album_artists().len();
-
- match event.kind {
- MouseEventKind::Down(MouseButton::Left) => {
- if event.column >= inner.x
- && event.column < inner.x + inner.width
- && event.row >= inner.y
- && event.row < inner.y + inner.height
- {
- let clicked = self.scroll_offset + (event.row - inner.y) as usize;
- if clicked < count {
- self.selected = clicked;
- }
- }
- None
- }
- MouseEventKind::ScrollDown => self.handle_scroll(false, app),
- MouseEventKind::ScrollUp => self.handle_scroll(true, app),
- _ => None,
- }
- }
-
- fn handle_scroll(&mut self, up: bool, app: &App) -> Option<AppAction> {
- let count = app.library.get_album_artists().len();
- if count == 0 {
- return None;
- }
- if up {
- self.scroll_offset = self.scroll_offset.saturating_sub(3);
- self.selected = self.selected.saturating_sub(3);
- } else {
- self.scroll_offset = (self.scroll_offset + 3).min(count.saturating_sub(1));
- self.selected = (self.selected + 3).min(count.saturating_sub(1));
- }
- None
- }
-}
diff --git a/src/ui/panes/queue_pane.rs b/src/ui/panes/queue_pane.rs
@@ -1,4 +1,4 @@
-use crossterm::event::{KeyCode, KeyEvent, MouseButton, MouseEvent, MouseEventKind};
+use crossterm::event::{KeyCode, KeyEvent, MouseEvent, MouseEventKind};
use ratatui::layout::Rect;
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
@@ -253,25 +253,9 @@ impl Pane for QueuePane {
}
}
- fn handle_mouse(&mut self, event: MouseEvent, area: Rect, app: &App) -> Option<AppAction> {
- let block = Block::default().borders(Borders::ALL);
- let inner = block.inner(area);
- let count = app.queue.tracks.len();
-
+ fn handle_mouse(&mut self, event: MouseEvent, _area: Rect, app: &App) -> Option<AppAction> {
+ // Clicks are handled in handler.rs, which owns queue selection.
match event.kind {
- MouseEventKind::Down(MouseButton::Left) => {
- if event.column >= inner.x
- && event.column < inner.x + inner.width
- && event.row >= inner.y
- && event.row < inner.y + inner.height
- {
- let clicked = self.scroll_offset + (event.row - inner.y) as usize;
- if clicked < count {
- // Selection handled by handler.rs
- }
- }
- None
- }
MouseEventKind::ScrollDown => self.handle_scroll(false, app),
MouseEventKind::ScrollUp => self.handle_scroll(true, app),
_ => None,
diff --git a/src/ui/panes/search_pane.rs b/src/ui/panes/search_pane.rs
@@ -1,207 +0,0 @@
-use crossterm::event::{KeyCode, KeyEvent, MouseButton, MouseEvent, MouseEventKind};
-use ratatui::layout::{Constraint, Direction, Layout, Rect};
-use ratatui::style::{Color, Modifier, Style};
-use ratatui::text::{Line, Span};
-use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph};
-use ratatui::Frame;
-
-use crate::app::{App, AppAction};
-use crate::ui::pane::Pane;
-use crate::ui::theme::Theme;
-
-pub struct SearchPane {
- pub selected: usize,
- pub scroll_offset: usize,
- pub input_mode: bool,
- pub hover_row: Option<usize>,
-}
-
-impl SearchPane {
- pub fn new() -> Self {
- Self {
- selected: 0,
- scroll_offset: 0,
- input_mode: false,
- hover_row: None,
- }
- }
-}
-
-impl Pane for SearchPane {
- fn render(&mut self, frame: &mut Frame, area: Rect, focused: bool, app: &App, theme: &Theme) {
- let border_color = if focused {
- theme.border_focused
- } else {
- theme.border_unfocused
- };
-
- let block = Block::default()
- .borders(Borders::ALL)
- .border_style(Style::default().fg(border_color))
- .title_style(Style::default().fg(if focused {
- theme.border_focused
- } else {
- theme.fg
- }));
-
- let inner = block.inner(area);
- frame.render_widget(block, area);
-
- let chunks = Layout::default()
- .direction(Direction::Vertical)
- .constraints([Constraint::Length(1), Constraint::Min(1)])
- .split(inner);
-
- // Search input line
- let input_style = if self.input_mode {
- Style::default().fg(Color::White).bg(Color::DarkGray)
- } else {
- Style::default().fg(Color::Gray)
- };
-
- let cursor = if self.input_mode { "▎" } else { "" };
- let input = Paragraph::new(Line::from(vec![
- Span::styled(" / ", Style::default().fg(Color::Cyan)),
- Span::styled(&app.search_query, input_style),
- Span::styled(cursor, Style::default().fg(Color::White)),
- ]));
- frame.render_widget(input, chunks[0]);
-
- // Results
- let inner_height = chunks[1].height as usize;
- let items: Vec<ListItem> = app
- .search_results
- .iter()
- .enumerate()
- .skip(self.scroll_offset)
- .take(inner_height)
- .map(|(i, &track_idx)| {
- let track = &app.library.tracks[track_idx];
- let is_selected = i == self.selected;
- let style = if is_selected && focused {
- Style::default()
- .bg(theme.highlight_bg)
- .fg(theme.highlight_fg)
- .add_modifier(Modifier::BOLD)
- } else {
- Style::default().fg(theme.fg)
- };
-
- let artist_style = if is_selected && focused {
- style
- } else {
- Style::default().fg(Color::Gray)
- };
-
- ListItem::new(Line::from(vec![
- Span::styled(&track.title, style),
- Span::styled(format!(" - {}", track.display_artist()), artist_style),
- ]))
- })
- .collect();
-
- let list = List::new(items);
- frame.render_widget(list, chunks[1]);
- }
-
- fn handle_key(&mut self, key: KeyEvent, app: &App) -> Option<AppAction> {
- if self.input_mode || app.search_mode {
- match key.code {
- KeyCode::Esc => {
- self.input_mode = false;
- return Some(AppAction::ExitSearchMode);
- }
- KeyCode::Enter => {
- self.input_mode = false;
- // Add selected search result to queue
- if self.selected < app.search_results.len() {
- let track_idx = app.search_results[self.selected];
- return Some(AppAction::AddToQueue(vec![track_idx]));
- }
- return Some(AppAction::ExitSearchMode);
- }
- KeyCode::Backspace => {
- let mut q = app.search_query.clone();
- q.pop();
- return Some(AppAction::SearchQuery(q));
- }
- KeyCode::Char(c) => {
- let mut q = app.search_query.clone();
- q.push(c);
- return Some(AppAction::SearchQuery(q));
- }
- _ => {}
- }
- return None;
- }
-
- match key.code {
- KeyCode::Char('/') | KeyCode::Char('i') => {
- self.input_mode = true;
- Some(AppAction::EnterSearchMode)
- }
- KeyCode::Char('j') | KeyCode::Down => {
- if self.selected < app.search_results.len().saturating_sub(1) {
- self.selected += 1;
- }
- None
- }
- KeyCode::Char('k') | KeyCode::Up => {
- if self.selected > 0 {
- self.selected -= 1;
- }
- None
- }
- KeyCode::Enter => {
- if self.selected < app.search_results.len() {
- let track_idx = app.search_results[self.selected];
- Some(AppAction::AddToQueue(vec![track_idx]))
- } else {
- None
- }
- }
- _ => None,
- }
- }
-
- fn handle_mouse(&mut self, event: MouseEvent, area: Rect, app: &App) -> Option<AppAction> {
- let block = Block::default().borders(Borders::ALL);
- let inner = block.inner(area);
-
- match event.kind {
- MouseEventKind::Down(MouseButton::Left) => {
- if event.row == inner.y {
- // Click on search input
- self.input_mode = true;
- return Some(AppAction::EnterSearchMode);
- }
- let results_y = inner.y + 1;
- if event.row >= results_y && event.row < inner.y + inner.height {
- let clicked = self.scroll_offset + (event.row - results_y) as usize;
- if clicked < app.search_results.len() {
- self.selected = clicked;
- }
- }
- None
- }
- MouseEventKind::ScrollDown => self.handle_scroll(false, app),
- MouseEventKind::ScrollUp => self.handle_scroll(true, app),
- _ => None,
- }
- }
-
- fn handle_scroll(&mut self, up: bool, app: &App) -> Option<AppAction> {
- let count = app.search_results.len();
- if count == 0 {
- return None;
- }
- if up {
- self.scroll_offset = self.scroll_offset.saturating_sub(3);
- self.selected = self.selected.saturating_sub(3);
- } else {
- self.scroll_offset = (self.scroll_offset + 3).min(count.saturating_sub(1));
- self.selected = (self.selected + 3).min(count.saturating_sub(1));
- }
- None
- }
-}
diff --git a/src/ui/theme.rs b/src/ui/theme.rs
@@ -1,21 +1,14 @@
use ratatui::style::{Color, Modifier, Style};
-#[allow(dead_code)]
pub struct Theme {
- pub bg: Color,
pub fg: Color,
pub border_focused: Color,
pub border_unfocused: Color,
pub highlight_bg: Color,
pub highlight_fg: Color,
- pub tab_active: Style,
pub tab_inactive: Style,
- pub status_bar_bg: Color,
pub progress_filled: Color,
pub progress_empty: Color,
- pub playing_indicator: Color,
- pub title_style: Style,
- pub artist_style: Style,
pub dim_style: Style,
pub current_track_style: Style,
}
@@ -23,7 +16,6 @@ pub struct Theme {
impl Default for Theme {
fn default() -> Self {
Self {
- bg: Color::Reset,
fg: Color::White,
// These were both Cyan, which made the focus indicator a no-op:
// every pane picked a border colour from `focused` and got the same
@@ -32,18 +24,9 @@ impl Default for Theme {
border_unfocused: Color::Rgb(70, 80, 95),
highlight_bg: Color::Cyan,
highlight_fg: Color::Black,
- tab_active: Style::default()
- .fg(Color::Cyan)
- .add_modifier(Modifier::BOLD),
tab_inactive: Style::default().fg(Color::DarkGray),
- status_bar_bg: Color::DarkGray,
progress_filled: Color::Rgb(200, 80, 255),
progress_empty: Color::Indexed(236),
- playing_indicator: Color::Rgb(80, 255, 120),
- title_style: Style::default()
- .fg(Color::White)
- .add_modifier(Modifier::BOLD),
- artist_style: Style::default().fg(Color::Gray),
dim_style: Style::default().fg(Color::DarkGray),
current_track_style: Style::default()
.fg(Color::Rgb(100, 220, 255))
|