musichoard/src/tui/ui.rs

1253 lines
39 KiB
Rust
Raw Normal View History

use std::fmt;
use musichoard::collection::{
album::Album,
artist::Artist,
track::{Format, Track},
Collection,
};
use ratatui::{
backend::Backend,
layout::{Alignment, Rect},
style::{Color, Style},
widgets::{Block, BorderType, Borders, Clear, List, ListItem, ListState, Paragraph},
Frame,
};
use crate::tui::{lib::IMusicHoard, Error};
#[derive(Debug)]
pub enum UiError {
Lib(String),
}
impl fmt::Display for UiError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
Self::Lib(ref s) => write!(f, "the musichoard library returned an error: {s}"),
}
}
}
impl From<musichoard::Error> for UiError {
fn from(err: musichoard::Error) -> UiError {
UiError::Lib(err.to_string())
}
}
pub enum UiState<BS, IS> {
2024-01-28 11:49:41 +01:00
Browse(BS),
Info(IS),
}
impl<BS, IS> UiState<BS, IS> {
fn is_browse(&self) -> bool {
matches!(self, UiState::Browse(_))
2024-01-28 11:49:41 +01:00
}
fn is_info(&self) -> bool {
matches!(self, UiState::Info(_))
2024-01-28 11:49:41 +01:00
}
}
pub trait IUi {
type BS: IUiBrowse;
type IS: IUiInfo;
fn is_running(&self) -> bool;
fn quit(&mut self);
fn save(&mut self) -> Result<(), UiError>;
fn render<B: Backend>(&mut self, frame: &mut Frame<'_, B>);
fn state(&mut self) -> UiState<&mut Self::BS, &mut Self::IS>;
2024-01-28 11:49:41 +01:00
}
pub trait IUiBrowse {
fn increment_category(&mut self);
fn decrement_category(&mut self);
fn increment_selection(&mut self);
fn decrement_selection(&mut self);
fn show_info_overlay(&mut self);
2024-01-28 11:49:41 +01:00
}
pub trait IUiInfo {
fn hide_info_overlay(&mut self);
}
struct TrackSelection {
state: ListState,
}
struct AlbumSelection {
state: ListState,
track: TrackSelection,
}
struct ArtistSelection {
state: ListState,
album: AlbumSelection,
}
impl TrackSelection {
fn initialise(tracks: Option<&[Track]>) -> Self {
let mut state = ListState::default();
if let Some(tracks) = tracks {
state.select(if !tracks.is_empty() { Some(0) } else { None });
} else {
state.select(None);
};
TrackSelection { state }
}
fn increment(&mut self, tracks: &[Track]) {
if let Some(index) = self.state.selected() {
if let Some(result) = index.checked_add(1) {
if result < tracks.len() {
self.state.select(Some(result));
}
}
}
}
fn decrement(&mut self, _tracks: &[Track]) {
if let Some(index) = self.state.selected() {
if let Some(result) = index.checked_sub(1) {
self.state.select(Some(result));
}
}
}
}
impl AlbumSelection {
fn initialise(albums: Option<&[Album]>) -> Self {
let mut state = ListState::default();
let track: TrackSelection;
if let Some(albums) = albums {
state.select(if !albums.is_empty() { Some(0) } else { None });
track = TrackSelection::initialise(albums.first().map(|a| a.tracks.as_slice()));
} else {
state.select(None);
track = TrackSelection::initialise(None);
}
AlbumSelection { state, track }
}
fn increment(&mut self, albums: &[Album]) {
if let Some(index) = self.state.selected() {
if let Some(result) = index.checked_add(1) {
if result < albums.len() {
self.state.select(Some(result));
self.track = TrackSelection::initialise(Some(&albums[result].tracks));
}
}
}
}
fn increment_track(&mut self, albums: &[Album]) {
if let Some(index) = self.state.selected() {
self.track.increment(&albums[index].tracks);
}
}
fn decrement(&mut self, albums: &[Album]) {
if let Some(index) = self.state.selected() {
if let Some(result) = index.checked_sub(1) {
self.state.select(Some(result));
self.track = TrackSelection::initialise(Some(&albums[result].tracks));
}
}
}
fn decrement_track(&mut self, albums: &[Album]) {
if let Some(index) = self.state.selected() {
self.track.decrement(&albums[index].tracks);
}
}
}
impl ArtistSelection {
fn initialise(artists: Option<&[Artist]>) -> Self {
let mut state = ListState::default();
let album: AlbumSelection;
if let Some(artists) = artists {
state.select(if !artists.is_empty() { Some(0) } else { None });
album = AlbumSelection::initialise(artists.first().map(|a| a.albums.as_slice()));
} else {
state.select(None);
album = AlbumSelection::initialise(None);
}
ArtistSelection { state, album }
}
fn increment(&mut self, artists: &[Artist]) {
if let Some(index) = self.state.selected() {
if let Some(result) = index.checked_add(1) {
if result < artists.len() {
self.state.select(Some(result));
self.album = AlbumSelection::initialise(Some(&artists[result].albums));
}
}
}
}
fn increment_album(&mut self, artists: &[Artist]) {
if let Some(index) = self.state.selected() {
self.album.increment(&artists[index].albums);
}
}
fn increment_track(&mut self, artists: &[Artist]) {
if let Some(index) = self.state.selected() {
self.album.increment_track(&artists[index].albums);
}
}
fn decrement(&mut self, artists: &[Artist]) {
if let Some(index) = self.state.selected() {
if let Some(result) = index.checked_sub(1) {
self.state.select(Some(result));
self.album = AlbumSelection::initialise(Some(&artists[result].albums));
}
}
}
fn decrement_album(&mut self, artists: &[Artist]) {
if let Some(index) = self.state.selected() {
self.album.decrement(&artists[index].albums);
}
}
fn decrement_track(&mut self, artists: &[Artist]) {
if let Some(index) = self.state.selected() {
self.album.decrement_track(&artists[index].albums);
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Category {
Artist,
Album,
Track,
}
struct Selection {
active: Category,
artist: ArtistSelection,
}
impl Selection {
fn new(artists: Option<&[Artist]>) -> Self {
Selection {
active: Category::Artist,
artist: ArtistSelection::initialise(artists),
}
}
fn increment_category(&mut self) {
self.active = match self.active {
Category::Artist => Category::Album,
Category::Album => Category::Track,
Category::Track => Category::Track,
};
}
fn decrement_category(&mut self) {
self.active = match self.active {
Category::Artist => Category::Artist,
Category::Album => Category::Artist,
Category::Track => Category::Album,
};
}
fn increment_selection(&mut self, collection: &Collection) {
match self.active {
Category::Artist => self.increment_artist(collection),
Category::Album => self.increment_album(collection),
Category::Track => self.increment_track(collection),
}
}
fn decrement_selection(&mut self, collection: &Collection) {
match self.active {
Category::Artist => self.decrement_artist(collection),
Category::Album => self.decrement_album(collection),
Category::Track => self.decrement_track(collection),
}
}
fn increment_artist(&mut self, artists: &[Artist]) {
self.artist.increment(artists);
}
fn decrement_artist(&mut self, artists: &[Artist]) {
self.artist.decrement(artists);
}
fn increment_album(&mut self, artists: &[Artist]) {
self.artist.increment_album(artists);
}
fn decrement_album(&mut self, artists: &[Artist]) {
self.artist.decrement_album(artists);
}
fn increment_track(&mut self, artists: &[Artist]) {
self.artist.increment_track(artists);
}
fn decrement_track(&mut self, artists: &[Artist]) {
self.artist.decrement_track(artists);
}
}
struct ArtistArea {
list: Rect,
}
struct AlbumArea {
list: Rect,
info: Rect,
}
struct TrackArea {
list: Rect,
info: Rect,
}
struct FrameArea {
artist: ArtistArea,
album: AlbumArea,
track: TrackArea,
}
impl FrameArea {
fn new(frame: Rect) -> Self {
let width_one_third = frame.width / 3;
let height_one_third = frame.height / 3;
let panel_width = width_one_third;
let panel_width_last = frame.width - 2 * panel_width;
let panel_height_top = frame.height - height_one_third;
let panel_height_bottom = height_one_third;
let artist_list = Rect {
x: frame.x,
y: frame.y,
width: panel_width,
height: frame.height,
};
let album_list = Rect {
x: artist_list.x + artist_list.width,
y: frame.y,
width: panel_width,
height: panel_height_top,
};
let album_info = Rect {
x: album_list.x,
y: album_list.y + album_list.height,
width: album_list.width,
height: panel_height_bottom,
};
let track_list = Rect {
x: album_list.x + album_list.width,
y: frame.y,
width: panel_width_last,
height: panel_height_top,
};
let track_info = Rect {
x: track_list.x,
y: track_list.y + track_list.height,
width: track_list.width,
height: panel_height_bottom,
};
FrameArea {
artist: ArtistArea { list: artist_list },
album: AlbumArea {
list: album_list,
info: album_info,
},
track: TrackArea {
list: track_list,
info: track_info,
},
}
}
}
struct OverlayArea {
artist: Rect,
}
impl OverlayArea {
fn new(frame: Rect) -> Self {
let margin_factor = 8;
let width_margin = frame.width / margin_factor;
let height_margin = frame.height / margin_factor;
let artist = Rect {
x: width_margin,
y: height_margin,
width: frame.width - (2 * width_margin),
height: frame.height - (2 * height_margin),
};
OverlayArea { artist }
}
}
struct ArtistState<'a, 'b> {
active: bool,
list: List<'a>,
state: &'b mut ListState,
}
impl<'a, 'b> ArtistState<'a, 'b> {
fn new(active: bool, artists: &'a [Artist], state: &'b mut ListState) -> ArtistState<'a, 'b> {
let list = List::new(
artists
.iter()
.map(|a| ListItem::new(a.id.name.as_str()))
.collect::<Vec<ListItem>>(),
);
ArtistState {
active,
list,
state,
}
}
}
struct ArtistOverlay<'a> {
properties: Paragraph<'a>,
}
impl<'a> ArtistOverlay<'a> {
fn opt_opt_to_str<S: AsRef<str>>(opt: Option<Option<&S>>) -> &str {
opt.flatten().map(|item| item.as_ref()).unwrap_or("")
}
fn opt_vec_to_string<S: AsRef<str>>(opt_vec: Option<&Vec<S>>, indent: &str) -> String {
opt_vec
.map(|vec| {
if vec.len() < 2 {
vec.first()
.map(|item| item.as_ref())
.unwrap_or("")
.to_string()
} else {
let indent = format!("\n{indent}");
let list = vec
.iter()
.map(|item| item.as_ref())
.collect::<Vec<&str>>()
.join(&indent);
format!("{indent}{list}")
}
})
.unwrap_or_else(|| String::from(""))
}
fn new(artists: &'a [Artist], state: &ListState) -> ArtistOverlay<'a> {
let artist = state.selected().map(|i| &artists[i]);
let item_indent = " ";
let list_indent = " - ";
let properties = Paragraph::new(format!(
"Artist: {}\n\n{item_indent}\
MusicBrainz: {}\n{item_indent}\
MusicButler: {}\n{item_indent}\
Bandcamp: {}\n{item_indent}\
Qobuz: {}",
artist.map(|a| a.id.name.as_str()).unwrap_or(""),
Self::opt_opt_to_str(artist.map(|a| a.properties.musicbrainz.as_ref())),
Self::opt_vec_to_string(artist.map(|a| &a.properties.musicbutler), list_indent),
Self::opt_vec_to_string(artist.map(|a| &a.properties.bandcamp), list_indent),
Self::opt_opt_to_str(artist.map(|a| a.properties.qobuz.as_ref())),
));
ArtistOverlay { properties }
}
}
struct AlbumState<'a, 'b> {
active: bool,
list: List<'a>,
state: &'b mut ListState,
info: Paragraph<'a>,
}
impl<'a, 'b> AlbumState<'a, 'b> {
fn new(active: bool, albums: &'a [Album], state: &'b mut ListState) -> AlbumState<'a, 'b> {
let list = List::new(
albums
.iter()
.map(|a| ListItem::new(a.id.title.as_str()))
.collect::<Vec<ListItem>>(),
);
let album = state.selected().map(|i| &albums[i]);
let info = Paragraph::new(format!(
"Title: {}\n\
Year: {}",
album.map(|a| a.id.title.as_str()).unwrap_or(""),
album.map(|a| a.id.year.to_string()).unwrap_or_default(),
));
AlbumState {
active,
list,
state,
info,
}
}
}
struct TrackState<'a, 'b> {
active: bool,
list: List<'a>,
state: &'b mut ListState,
info: Paragraph<'a>,
}
impl<'a, 'b> TrackState<'a, 'b> {
fn new(active: bool, tracks: &'a [Track], state: &'b mut ListState) -> TrackState<'a, 'b> {
let list = List::new(
tracks
.iter()
.map(|tr| ListItem::new(tr.id.title.as_str()))
.collect::<Vec<ListItem>>(),
);
let track = state.selected().map(|i| &tracks[i]);
let info = Paragraph::new(format!(
"Track: {}\n\
Title: {}\n\
Artist: {}\n\
Quality: {}",
track.map(|t| t.id.number.to_string()).unwrap_or_default(),
track.map(|t| t.id.title.as_str()).unwrap_or(""),
track.map(|t| t.artist.join("; ")).unwrap_or_default(),
track
.map(|t| match t.quality.format {
Format::Flac => "FLAC".to_string(),
Format::Mp3 => format!("MP3 {}kbps", t.quality.bitrate),
})
.unwrap_or_default(),
));
TrackState {
active,
list,
state,
info,
}
}
}
pub struct Ui<MH> {
running: bool,
music_hoard: MH,
selection: Selection,
state: UiState<(), ()>,
2024-01-28 11:49:41 +01:00
}
impl<MH: IMusicHoard> Ui<MH> {
pub fn new(mut music_hoard: MH) -> Result<Self, Error> {
music_hoard.load_from_database()?;
music_hoard.rescan_library()?;
let selection = Selection::new(Some(music_hoard.get_collection()));
Ok(Ui {
running: true,
music_hoard,
selection,
state: UiState::Browse(()),
})
}
fn style(_active: bool) -> Style {
Style::default().fg(Color::White).bg(Color::Black)
}
fn block_style(active: bool) -> Style {
Self::style(active)
}
fn highlight_style(active: bool) -> Style {
if active {
Style::default().fg(Color::White).bg(Color::DarkGray)
} else {
Self::style(false)
}
}
fn block<'a>(title: &str, active: bool) -> Block<'a> {
Block::default()
.title_alignment(Alignment::Center)
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.style(Self::block_style(active))
.title(format!(" {title} "))
}
fn render_list_widget<B: Backend>(
title: &str,
list: List,
list_state: &mut ListState,
active: bool,
area: Rect,
frame: &mut Frame<'_, B>,
) {
frame.render_stateful_widget(
list.highlight_style(Self::highlight_style(active))
.highlight_symbol(">> ")
.style(Self::style(active))
.block(Self::block(title, active)),
area,
list_state,
);
}
fn render_info_widget<B: Backend>(
title: &str,
paragraph: Paragraph,
active: bool,
area: Rect,
frame: &mut Frame<'_, B>,
) {
frame.render_widget(
paragraph
.style(Self::style(active))
.block(Self::block(title, active)),
area,
);
}
fn render_overlay_widget<B: Backend>(
title: &str,
paragraph: Paragraph,
area: Rect,
frame: &mut Frame<'_, B>,
) {
frame.render_widget(Clear, area);
frame.render_widget(
paragraph
.style(Self::style(true))
.block(Self::block(title, true)),
area,
);
}
fn render_artist_column<B: Backend>(st: ArtistState, ar: ArtistArea, fr: &mut Frame<'_, B>) {
Self::render_list_widget("Artists", st.list, st.state, st.active, ar.list, fr);
}
fn render_album_column<B: Backend>(st: AlbumState, ar: AlbumArea, fr: &mut Frame<'_, B>) {
Self::render_list_widget("Albums", st.list, st.state, st.active, ar.list, fr);
Self::render_info_widget("Album info", st.info, st.active, ar.info, fr);
}
fn render_track_column<B: Backend>(st: TrackState, ar: TrackArea, fr: &mut Frame<'_, B>) {
Self::render_list_widget("Tracks", st.list, st.state, st.active, ar.list, fr);
Self::render_info_widget("Track info", st.info, st.active, ar.info, fr);
}
fn render_collection<B: Backend>(&mut self, frame: &mut Frame<'_, B>) {
let active = self.selection.active;
let areas = FrameArea::new(frame.size());
let artists = self.music_hoard.get_collection();
let artist_selection = &mut self.selection.artist;
let artist_state = ArtistState::new(
active == Category::Artist,
artists,
&mut artist_selection.state,
);
Self::render_artist_column(artist_state, areas.artist, frame);
let no_albums: Vec<Album> = vec![];
let albums = artist_selection
.state
.selected()
.map(|i| &artists[i].albums)
.unwrap_or_else(|| &no_albums);
let album_selection = &mut artist_selection.album;
let album_state = AlbumState::new(
active == Category::Album,
albums,
&mut album_selection.state,
);
Self::render_album_column(album_state, areas.album, frame);
let no_tracks: Vec<Track> = vec![];
let tracks = album_selection
.state
.selected()
.map(|i| &albums[i].tracks)
.unwrap_or_else(|| &no_tracks);
let track_selection = &mut album_selection.track;
let track_state = TrackState::new(
active == Category::Track,
tracks,
&mut track_selection.state,
);
Self::render_track_column(track_state, areas.track, frame);
}
fn render_overlay<B: Backend>(&mut self, frame: &mut Frame<'_, B>) {
let areas = OverlayArea::new(frame.size());
let artists = self.music_hoard.get_collection();
let artist_selection = &mut self.selection.artist;
let artist_overlay = ArtistOverlay::new(artists, &artist_selection.state);
Self::render_overlay_widget("Artist", artist_overlay.properties, areas.artist, frame);
}
}
impl<MH: IMusicHoard> IUi for Ui<MH> {
type BS = Self;
type IS = Self;
fn is_running(&self) -> bool {
self.running
}
fn quit(&mut self) {
self.running = false;
}
fn save(&mut self) -> Result<(), UiError> {
Ok(self.music_hoard.save_to_database()?)
}
fn state(&mut self) -> UiState<&mut Self::BS, &mut Self::IS> {
match self.state {
UiState::Browse(_) => UiState::Browse(self),
UiState::Info(_) => UiState::Info(self),
}
}
fn render<B: Backend>(&mut self, frame: &mut Frame<'_, B>) {
self.render_collection(frame);
if self.state.is_info() {
self.render_overlay(frame);
}
}
2024-01-28 11:49:41 +01:00
}
impl<MH: IMusicHoard> IUiBrowse for Ui<MH> {
fn increment_category(&mut self) {
self.selection.increment_category();
}
fn decrement_category(&mut self) {
self.selection.decrement_category();
}
fn increment_selection(&mut self) {
self.selection
.increment_selection(self.music_hoard.get_collection());
}
fn decrement_selection(&mut self) {
self.selection
.decrement_selection(self.music_hoard.get_collection());
}
fn show_info_overlay(&mut self) {
assert!(self.state.is_browse());
self.state = UiState::Info(());
2024-01-28 11:49:41 +01:00
}
}
impl<MH: IMusicHoard> IUiInfo for Ui<MH> {
fn hide_info_overlay(&mut self) {
assert!(self.state.is_info());
self.state = UiState::Browse(());
}
}
#[cfg(test)]
mod tests {
use crate::tui::lib::MockIMusicHoard;
use crate::tui::testmod::COLLECTION;
use crate::tui::tests::{terminal, ui};
use super::*;
pub fn music_hoard(collection: Collection) -> MockIMusicHoard {
let mut music_hoard = MockIMusicHoard::new();
music_hoard
.expect_load_from_database()
.times(1)
.return_once(|| Ok(()));
music_hoard
.expect_rescan_library()
.times(1)
.return_once(|| Ok(()));
music_hoard.expect_get_collection().return_const(collection);
music_hoard
}
#[test]
fn test_track_selection() {
let tracks = &COLLECTION[0].albums[0].tracks;
assert!(tracks.len() > 1);
let empty = TrackSelection::initialise(None);
assert_eq!(empty.state.selected(), None);
let empty = TrackSelection::initialise(Some(&[]));
assert_eq!(empty.state.selected(), None);
let mut sel = TrackSelection::initialise(Some(tracks));
assert_eq!(sel.state.selected(), Some(0));
sel.decrement(tracks);
assert_eq!(sel.state.selected(), Some(0));
sel.increment(tracks);
assert_eq!(sel.state.selected(), Some(1));
sel.decrement(tracks);
assert_eq!(sel.state.selected(), Some(0));
for _ in 0..(tracks.len() + 5) {
sel.increment(tracks);
}
assert_eq!(sel.state.selected(), Some(tracks.len() - 1));
// Artifical test case to verify upper limit.
sel.state.select(Some(std::usize::MAX));
assert_eq!(sel.state.selected(), Some(std::usize::MAX));
sel.increment(&[]);
assert_eq!(sel.state.selected(), Some(std::usize::MAX));
}
#[test]
fn test_album_selection() {
let albums = &COLLECTION[0].albums;
assert!(albums.len() > 1);
let empty = AlbumSelection::initialise(None);
assert_eq!(empty.state.selected(), None);
let empty = AlbumSelection::initialise(Some(&[]));
assert_eq!(empty.state.selected(), None);
let mut sel = AlbumSelection::initialise(Some(albums));
assert_eq!(sel.state.selected(), Some(0));
assert_eq!(sel.track.state.selected(), Some(0));
sel.increment_track(albums);
assert_eq!(sel.state.selected(), Some(0));
assert_eq!(sel.track.state.selected(), Some(1));
// Verify that decrement that doesn't change index does not reset track.
sel.decrement(albums);
assert_eq!(sel.state.selected(), Some(0));
assert_eq!(sel.track.state.selected(), Some(1));
sel.increment(albums);
assert_eq!(sel.state.selected(), Some(1));
assert_eq!(sel.track.state.selected(), Some(0));
sel.decrement(albums);
assert_eq!(sel.state.selected(), Some(0));
assert_eq!(sel.track.state.selected(), Some(0));
for _ in 0..(albums.len() + 5) {
sel.increment(albums);
}
assert_eq!(sel.state.selected(), Some(albums.len() - 1));
assert_eq!(sel.track.state.selected(), Some(0));
sel.increment_track(albums);
assert_eq!(sel.state.selected(), Some(albums.len() - 1));
assert_eq!(sel.track.state.selected(), Some(1));
// Verify that increment that doesn't change index does not reset track.
sel.increment(albums);
assert_eq!(sel.state.selected(), Some(albums.len() - 1));
assert_eq!(sel.track.state.selected(), Some(1));
// Artifical test case to verify upper limit.
sel.state.select(Some(std::usize::MAX));
sel.track.state.select(Some(1));
assert_eq!(sel.state.selected(), Some(std::usize::MAX));
assert_eq!(sel.track.state.selected(), Some(1));
sel.increment(&[]);
assert_eq!(sel.state.selected(), Some(std::usize::MAX));
assert_eq!(sel.track.state.selected(), Some(1));
}
#[test]
fn test_artist_selection() {
let artists = &COLLECTION;
assert!(artists.len() > 1);
let empty = ArtistSelection::initialise(None);
assert_eq!(empty.state.selected(), None);
let empty = ArtistSelection::initialise(Some(&[]));
assert_eq!(empty.state.selected(), None);
let mut sel = ArtistSelection::initialise(Some(artists));
assert_eq!(sel.state.selected(), Some(0));
assert_eq!(sel.album.state.selected(), Some(0));
sel.increment_album(artists);
assert_eq!(sel.state.selected(), Some(0));
assert_eq!(sel.album.state.selected(), Some(1));
// Verify that decrement that doesn't change index does not reset album.
sel.decrement(artists);
assert_eq!(sel.state.selected(), Some(0));
assert_eq!(sel.album.state.selected(), Some(1));
sel.increment(artists);
assert_eq!(sel.state.selected(), Some(1));
assert_eq!(sel.album.state.selected(), Some(0));
sel.decrement(artists);
assert_eq!(sel.state.selected(), Some(0));
assert_eq!(sel.album.state.selected(), Some(0));
for _ in 0..(artists.len() + 5) {
sel.increment(artists);
}
assert_eq!(sel.state.selected(), Some(artists.len() - 1));
assert_eq!(sel.album.state.selected(), Some(0));
sel.increment_album(artists);
assert_eq!(sel.state.selected(), Some(artists.len() - 1));
assert_eq!(sel.album.state.selected(), Some(1));
// Verify that increment that doesn't change index does not reset album.
sel.increment(artists);
assert_eq!(sel.state.selected(), Some(artists.len() - 1));
assert_eq!(sel.album.state.selected(), Some(1));
// Artifical test case to verify upper limit.
sel.state.select(Some(std::usize::MAX));
sel.album.state.select(Some(1));
assert_eq!(sel.state.selected(), Some(std::usize::MAX));
assert_eq!(sel.album.state.selected(), Some(1));
sel.increment(&[]);
assert_eq!(sel.state.selected(), Some(std::usize::MAX));
assert_eq!(sel.album.state.selected(), Some(1));
}
#[test]
fn ui_running() {
let mut ui = Ui::new(music_hoard(COLLECTION.to_owned())).unwrap();
assert!(ui.is_running());
ui.quit();
assert!(!ui.is_running());
}
#[test]
fn ui_save() {
let mut music_hoard = music_hoard(COLLECTION.to_owned());
music_hoard
.expect_save_to_database()
.times(1)
.return_once(|| Ok(()));
let mut ui = Ui::new(music_hoard).unwrap();
let result = ui.save();
assert!(result.is_ok());
}
#[test]
fn ui_modifiers() {
let mut ui = Ui::new(music_hoard(COLLECTION.to_owned())).unwrap();
assert!(ui.is_running());
assert_eq!(ui.selection.active, Category::Artist);
assert_eq!(ui.selection.artist.state.selected(), Some(0));
assert_eq!(ui.selection.artist.album.state.selected(), Some(0));
assert_eq!(ui.selection.artist.album.track.state.selected(), Some(0));
ui.increment_selection();
assert_eq!(ui.selection.active, Category::Artist);
assert_eq!(ui.selection.artist.state.selected(), Some(1));
assert_eq!(ui.selection.artist.album.state.selected(), Some(0));
assert_eq!(ui.selection.artist.album.track.state.selected(), Some(0));
ui.increment_category();
assert_eq!(ui.selection.active, Category::Album);
assert_eq!(ui.selection.artist.state.selected(), Some(1));
assert_eq!(ui.selection.artist.album.state.selected(), Some(0));
assert_eq!(ui.selection.artist.album.track.state.selected(), Some(0));
ui.increment_selection();
assert_eq!(ui.selection.active, Category::Album);
assert_eq!(ui.selection.artist.state.selected(), Some(1));
assert_eq!(ui.selection.artist.album.state.selected(), Some(1));
assert_eq!(ui.selection.artist.album.track.state.selected(), Some(0));
ui.increment_category();
assert_eq!(ui.selection.active, Category::Track);
assert_eq!(ui.selection.artist.state.selected(), Some(1));
assert_eq!(ui.selection.artist.album.state.selected(), Some(1));
assert_eq!(ui.selection.artist.album.track.state.selected(), Some(0));
ui.increment_selection();
assert_eq!(ui.selection.active, Category::Track);
assert_eq!(ui.selection.artist.state.selected(), Some(1));
assert_eq!(ui.selection.artist.album.state.selected(), Some(1));
assert_eq!(ui.selection.artist.album.track.state.selected(), Some(1));
ui.increment_category();
assert_eq!(ui.selection.active, Category::Track);
assert_eq!(ui.selection.artist.state.selected(), Some(1));
assert_eq!(ui.selection.artist.album.state.selected(), Some(1));
assert_eq!(ui.selection.artist.album.track.state.selected(), Some(1));
ui.decrement_selection();
assert_eq!(ui.selection.active, Category::Track);
assert_eq!(ui.selection.artist.state.selected(), Some(1));
assert_eq!(ui.selection.artist.album.state.selected(), Some(1));
assert_eq!(ui.selection.artist.album.track.state.selected(), Some(0));
ui.increment_selection();
ui.decrement_category();
assert_eq!(ui.selection.active, Category::Album);
assert_eq!(ui.selection.artist.state.selected(), Some(1));
assert_eq!(ui.selection.artist.album.state.selected(), Some(1));
assert_eq!(ui.selection.artist.album.track.state.selected(), Some(1));
ui.decrement_selection();
assert_eq!(ui.selection.active, Category::Album);
assert_eq!(ui.selection.artist.state.selected(), Some(1));
assert_eq!(ui.selection.artist.album.state.selected(), Some(0));
assert_eq!(ui.selection.artist.album.track.state.selected(), Some(0));
ui.increment_selection();
ui.decrement_category();
assert_eq!(ui.selection.active, Category::Artist);
assert_eq!(ui.selection.artist.state.selected(), Some(1));
assert_eq!(ui.selection.artist.album.state.selected(), Some(1));
assert_eq!(ui.selection.artist.album.track.state.selected(), Some(0));
ui.decrement_selection();
assert_eq!(ui.selection.active, Category::Artist);
assert_eq!(ui.selection.artist.state.selected(), Some(0));
assert_eq!(ui.selection.artist.album.state.selected(), Some(0));
assert_eq!(ui.selection.artist.album.track.state.selected(), Some(0));
ui.increment_category();
ui.increment_selection();
ui.decrement_category();
ui.decrement_selection();
ui.decrement_category();
assert_eq!(ui.selection.active, Category::Artist);
assert_eq!(ui.selection.artist.state.selected(), Some(0));
assert_eq!(ui.selection.artist.album.state.selected(), Some(1));
assert_eq!(ui.selection.artist.album.track.state.selected(), Some(0));
}
#[test]
fn app_no_tracks() {
let mut collection = COLLECTION.to_owned();
collection[0].albums[0].tracks = vec![];
let mut app = Ui::new(music_hoard(collection)).unwrap();
assert!(app.is_running());
assert_eq!(app.selection.active, Category::Artist);
assert_eq!(app.selection.artist.state.selected(), Some(0));
assert_eq!(app.selection.artist.album.state.selected(), Some(0));
assert_eq!(app.selection.artist.album.track.state.selected(), None);
app.increment_category();
app.increment_category();
app.increment_selection();
assert_eq!(app.selection.active, Category::Track);
assert_eq!(app.selection.artist.state.selected(), Some(0));
assert_eq!(app.selection.artist.album.state.selected(), Some(0));
assert_eq!(app.selection.artist.album.track.state.selected(), None);
app.decrement_selection();
assert_eq!(app.selection.active, Category::Track);
assert_eq!(app.selection.artist.state.selected(), Some(0));
assert_eq!(app.selection.artist.album.state.selected(), Some(0));
assert_eq!(app.selection.artist.album.track.state.selected(), None);
}
#[test]
fn app_no_albums() {
let mut collection = COLLECTION.to_owned();
collection[0].albums = vec![];
let mut app = Ui::new(music_hoard(collection)).unwrap();
assert!(app.is_running());
assert_eq!(app.selection.active, Category::Artist);
assert_eq!(app.selection.artist.state.selected(), Some(0));
assert_eq!(app.selection.artist.album.state.selected(), None);
assert_eq!(app.selection.artist.album.track.state.selected(), None);
app.increment_category();
app.increment_selection();
assert_eq!(app.selection.active, Category::Album);
assert_eq!(app.selection.artist.state.selected(), Some(0));
assert_eq!(app.selection.artist.album.state.selected(), None);
assert_eq!(app.selection.artist.album.track.state.selected(), None);
app.decrement_selection();
assert_eq!(app.selection.active, Category::Album);
assert_eq!(app.selection.artist.state.selected(), Some(0));
assert_eq!(app.selection.artist.album.state.selected(), None);
assert_eq!(app.selection.artist.album.track.state.selected(), None);
app.increment_category();
app.increment_selection();
assert_eq!(app.selection.active, Category::Track);
assert_eq!(app.selection.artist.state.selected(), Some(0));
assert_eq!(app.selection.artist.album.state.selected(), None);
assert_eq!(app.selection.artist.album.track.state.selected(), None);
app.decrement_selection();
assert_eq!(app.selection.active, Category::Track);
assert_eq!(app.selection.artist.state.selected(), Some(0));
assert_eq!(app.selection.artist.album.state.selected(), None);
assert_eq!(app.selection.artist.album.track.state.selected(), None);
}
#[test]
fn app_no_artists() {
let mut app = Ui::new(music_hoard(vec![])).unwrap();
assert!(app.is_running());
assert_eq!(app.selection.active, Category::Artist);
assert_eq!(app.selection.artist.state.selected(), None);
assert_eq!(app.selection.artist.album.state.selected(), None);
assert_eq!(app.selection.artist.album.track.state.selected(), None);
app.increment_selection();
assert_eq!(app.selection.active, Category::Artist);
assert_eq!(app.selection.artist.state.selected(), None);
assert_eq!(app.selection.artist.album.state.selected(), None);
assert_eq!(app.selection.artist.album.track.state.selected(), None);
app.decrement_selection();
assert_eq!(app.selection.active, Category::Artist);
assert_eq!(app.selection.artist.state.selected(), None);
assert_eq!(app.selection.artist.album.state.selected(), None);
assert_eq!(app.selection.artist.album.track.state.selected(), None);
app.increment_category();
app.increment_selection();
assert_eq!(app.selection.active, Category::Album);
assert_eq!(app.selection.artist.state.selected(), None);
assert_eq!(app.selection.artist.album.state.selected(), None);
assert_eq!(app.selection.artist.album.track.state.selected(), None);
app.decrement_selection();
assert_eq!(app.selection.active, Category::Album);
assert_eq!(app.selection.artist.state.selected(), None);
assert_eq!(app.selection.artist.album.state.selected(), None);
assert_eq!(app.selection.artist.album.track.state.selected(), None);
app.increment_category();
app.increment_selection();
assert_eq!(app.selection.active, Category::Track);
assert_eq!(app.selection.artist.state.selected(), None);
assert_eq!(app.selection.artist.album.state.selected(), None);
assert_eq!(app.selection.artist.album.track.state.selected(), None);
app.decrement_selection();
assert_eq!(app.selection.active, Category::Track);
assert_eq!(app.selection.artist.state.selected(), None);
assert_eq!(app.selection.artist.album.state.selected(), None);
assert_eq!(app.selection.artist.album.track.state.selected(), None);
}
// This is UI so the only sensible unit test is to run the code through various app states.
#[test]
fn empty() {
let mut terminal = terminal();
let mut ui = ui(vec![]);
terminal.draw(|frame| ui.render(frame)).unwrap();
}
#[test]
fn collection() {
let mut terminal = terminal();
let mut ui = ui(COLLECTION.to_owned());
terminal.draw(|frame| ui.render(frame)).unwrap();
// Change the track (which has a different track format).
ui.increment_category();
ui.increment_category();
ui.increment_selection();
terminal.draw(|frame| ui.render(frame)).unwrap();
}
#[test]
fn overlay() {
let mut terminal = terminal();
let mut ui = ui(COLLECTION.to_owned());
assert!(ui.state().is_browse());
terminal.draw(|frame| ui.render(frame)).unwrap();
ui.show_info_overlay();
assert!(ui.state().is_info());
terminal.draw(|frame| ui.render(frame)).unwrap();
// Change the artist (which has a multi-link entry).
ui.increment_selection();
terminal.draw(|frame| ui.render(frame)).unwrap();
ui.hide_info_overlay();
assert!(ui.state().is_browse());
terminal.draw(|frame| ui.render(frame)).unwrap();
}
#[test]
fn errors() {
let ui_err: UiError = musichoard::Error::DatabaseError(String::from("get rekt")).into();
assert!(!ui_err.to_string().is_empty());
assert!(!format!("{:?}", ui_err).is_empty());
}
}