refactor: replace vim keybindings with mouse navigation, remove mock data

- Remove all sample/mock data (artists, albums, tracks, queue, history, calendar)
- Delete input module (vim.rs, leader.rs) and related UI (which_key, cmdline)
- Add mouse support: click tabs, click list items, scroll wheel navigation
- Show real disk space via nix::statvfs instead of hardcoded value
- Simplify topbar/statusbar by removing mode display and key hints
- Hide album/track sections when no artist is selected
This commit is contained in:
Alexander
2026-05-08 22:16:38 +02:00
parent eccf0d8de5
commit 620bd374de
26 changed files with 522 additions and 1886 deletions
+60 -33
View File
@@ -1,16 +1,16 @@
#![allow(dead_code)]
use ratatui::{
Frame,
layout::{Constraint, Layout, Rect},
style::{Modifier, Style},
text::{Line, Span},
widgets::{List, ListItem, ListState, Paragraph},
Frame,
};
use crate::data::{Album, AlbumStatus, Artist, Track};
use crate::theme;
use crate::ui::pane::{section_divider, Pane};
use crate::ui::pane::{Pane, section_divider};
use crate::ui::progress_bar::progress_bar;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
@@ -53,7 +53,9 @@ impl LibraryState {
}
pub fn selected_artist(&self) -> Option<&Artist> {
self.artist_state.selected().and_then(|i| self.artists.get(i))
self.artist_state
.selected()
.and_then(|i| self.artists.get(i))
}
pub fn selected_album(&self) -> Option<&Album> {
@@ -101,7 +103,8 @@ impl LibraryState {
}
}
LibraryFocus::Albums => {
let max = self.selected_artist()
let max = self
.selected_artist()
.map(|a| a.albums.len().saturating_sub(1))
.unwrap_or(0);
if let Some(i) = self.album_state.selected() {
@@ -309,7 +312,13 @@ fn render_detail_pane(frame: &mut Frame, area: Rect, state: &mut LibraryState) {
let artist = state.selected_artist();
let meta = artist
.map(|a| format!("{} · {}", a.country, a.genres.first().map(|s| s.as_str()).unwrap_or("")))
.map(|a| {
format!(
"{} · {}",
a.country,
a.genres.first().map(|s| s.as_str()).unwrap_or("")
)
})
.unwrap_or_default();
let have_tracks: u16 = artist
@@ -319,21 +328,14 @@ fn render_detail_pane(frame: &mut Frame, area: Rect, state: &mut LibraryState) {
.map(|a| a.albums.iter().map(|al| al.total).sum())
.unwrap_or(0);
let footer = Line::from(vec![
Span::styled("[a]", Style::default().fg(theme::GRAY)),
Span::styled(" add ", Style::default().fg(theme::FG2)),
Span::styled("[m]", Style::default().fg(theme::GRAY)),
Span::styled(" monitor ", Style::default().fg(theme::FG2)),
Span::styled("[s]", Style::default().fg(theme::GRAY)),
Span::styled(" search ", Style::default().fg(theme::FG2)),
Span::styled("[r]", Style::default().fg(theme::GRAY)),
Span::styled(" refresh", Style::default().fg(theme::FG2)),
Span::raw(" "),
Span::styled(
let footer = if artist.is_some() {
Line::from(vec![Span::styled(
format!("{}/{} tracks", have_tracks, total_tracks),
Style::default().fg(theme::GRAY),
),
]);
)])
} else {
Line::from("")
};
let pane = Pane::new("Detail")
.meta(&meta)
@@ -344,6 +346,15 @@ fn render_detail_pane(frame: &mut Frame, area: Rect, state: &mut LibraryState) {
let inner = block.inner(area);
frame.render_widget(block, area);
let Some(artist) = artist else {
let msg = Paragraph::new(Span::styled(
"No artist selected",
Style::default().fg(theme::GRAY),
));
frame.render_widget(msg, inner);
return;
};
let chunks = Layout::vertical([
Constraint::Length(6),
Constraint::Length(1),
@@ -353,11 +364,9 @@ fn render_detail_pane(frame: &mut Frame, area: Rect, state: &mut LibraryState) {
])
.split(inner);
if let Some(artist) = artist {
render_artist_header(frame, chunks[0], artist);
}
render_artist_header(frame, chunks[0], artist);
let albums_count = artist.map(|a| a.albums.len()).unwrap_or(0);
let albums_count = artist.albums.len();
let albums_label = format!("{} releases", albums_count);
let album_divider = section_divider("albums", Some(&albums_label));
frame.render_widget(Paragraph::new(album_divider), chunks[1]);
@@ -407,7 +416,9 @@ fn render_artist_header(frame: &mut Frame, area: Rect, artist: &Artist) {
let lines = vec![
Line::from(Span::styled(
&artist.name,
Style::default().fg(theme::YELLOW).add_modifier(Modifier::BOLD),
Style::default()
.fg(theme::YELLOW)
.add_modifier(Modifier::BOLD),
)),
Line::from(""),
Line::from(vec![
@@ -427,7 +438,10 @@ fn render_artist_header(frame: &mut Frame, area: Rect, artist: &Artist) {
]),
Line::from(vec![
Span::styled("albums ", Style::default().fg(theme::GRAY)),
Span::styled(artist.albums.len().to_string(), Style::default().fg(theme::FG1)),
Span::styled(
artist.albums.len().to_string(),
Style::default().fg(theme::FG1),
),
Span::raw(" "),
Span::styled("tracks ", Style::default().fg(theme::GRAY)),
Span::styled(have.to_string(), Style::default().fg(theme::FG1)),
@@ -463,7 +477,8 @@ fn render_albums_list(
Style::default().fg(theme::AQUA)
};
let title_width = area.width as usize - 2 - type_str.len() - 1 - 4 - 1 - 10 - 1 - 5 - 1 - 8;
let title_width =
area.width as usize - 2 - type_str.len() - 1 - 4 - 1 - 10 - 1 - 5 - 1 - 8;
let mut title = album.title.clone();
if title.len() > title_width {
title.truncate(title_width.saturating_sub(1));
@@ -560,20 +575,32 @@ impl LibraryState {
return Vec::new();
};
if album.id == "bush" {
return crate::data::sample_tracks_bush_hall();
}
tracks_for(album)
}
}
fn tracks_for(album: &Album) -> Vec<Track> {
let titles = [
"Opening", "Curtain Call", "Half-Light", "Polaroid", "Switchback",
"Slow Dancer", "The Inheritance", "Glassworks", "Interlude", "Aftermath",
"Static", "Returner", "Dust Bowl", "Postcard", "Late Reply",
"Honeymoon", "Northern Lights", "Cold Open", "Coda", "Reprise",
"Opening",
"Curtain Call",
"Half-Light",
"Polaroid",
"Switchback",
"Slow Dancer",
"The Inheritance",
"Glassworks",
"Interlude",
"Aftermath",
"Static",
"Returner",
"Dust Bowl",
"Postcard",
"Late Reply",
"Honeymoon",
"Northern Lights",
"Cold Open",
"Coda",
"Reprise",
];
(0..album.total)