Add shortcut to reload database and/or library #116
@ -716,25 +716,29 @@ mod tests {
|
|||||||
let mut expected = FULL_COLLECTION.to_owned();
|
let mut expected = FULL_COLLECTION.to_owned();
|
||||||
expected.sort_unstable();
|
expected.sort_unstable();
|
||||||
|
|
||||||
let mut mh = MusicHoard::default();
|
let mut mh = MusicHoard {
|
||||||
mh.library_cache = left
|
collection: right.clone(),
|
||||||
.clone()
|
library_cache: left
|
||||||
.into_iter()
|
.clone()
|
||||||
.map(|a| (a.id.clone(), a))
|
.into_iter()
|
||||||
.collect();
|
.map(|a| (a.id.clone(), a))
|
||||||
mh.collection = right.clone();
|
.collect(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
mh.merge_collections();
|
mh.merge_collections();
|
||||||
assert_eq!(expected, mh.collection);
|
assert_eq!(expected, mh.collection);
|
||||||
|
|
||||||
// The merge is completely non-overlapping so it should be commutative.
|
// The merge is completely non-overlapping so it should be commutative.
|
||||||
let mut mh = MusicHoard::default();
|
let mut mh = MusicHoard {
|
||||||
mh.library_cache = right
|
collection: left.clone(),
|
||||||
.clone()
|
library_cache: right
|
||||||
.into_iter()
|
.clone()
|
||||||
.map(|a| (a.id.clone(), a))
|
.into_iter()
|
||||||
.collect();
|
.map(|a| (a.id.clone(), a))
|
||||||
mh.collection = left.clone();
|
.collect(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
mh.merge_collections();
|
mh.merge_collections();
|
||||||
assert_eq!(expected, mh.collection);
|
assert_eq!(expected, mh.collection);
|
||||||
@ -750,25 +754,29 @@ mod tests {
|
|||||||
let mut expected = FULL_COLLECTION.to_owned();
|
let mut expected = FULL_COLLECTION.to_owned();
|
||||||
expected.sort_unstable();
|
expected.sort_unstable();
|
||||||
|
|
||||||
let mut mh = MusicHoard::default();
|
let mut mh = MusicHoard {
|
||||||
mh.library_cache = left
|
collection: right.clone(),
|
||||||
.clone()
|
library_cache: left
|
||||||
.into_iter()
|
.clone()
|
||||||
.map(|a| (a.id.clone(), a))
|
.into_iter()
|
||||||
.collect();
|
.map(|a| (a.id.clone(), a))
|
||||||
mh.collection = right.clone();
|
.collect(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
mh.merge_collections();
|
mh.merge_collections();
|
||||||
assert_eq!(expected, mh.collection);
|
assert_eq!(expected, mh.collection);
|
||||||
|
|
||||||
// The merge does not overwrite any data so it should be commutative.
|
// The merge does not overwrite any data so it should be commutative.
|
||||||
let mut mh = MusicHoard::default();
|
let mut mh = MusicHoard {
|
||||||
mh.library_cache = right
|
collection: left.clone(),
|
||||||
.clone()
|
library_cache: right
|
||||||
.into_iter()
|
.clone()
|
||||||
.map(|a| (a.id.clone(), a))
|
.into_iter()
|
||||||
.collect();
|
.map(|a| (a.id.clone(), a))
|
||||||
mh.collection = left.clone();
|
.collect(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
mh.merge_collections();
|
mh.merge_collections();
|
||||||
assert_eq!(expected, mh.collection);
|
assert_eq!(expected, mh.collection);
|
||||||
@ -797,25 +805,29 @@ mod tests {
|
|||||||
expected.last_mut().as_mut().unwrap().sort = artist_sort.clone();
|
expected.last_mut().as_mut().unwrap().sort = artist_sort.clone();
|
||||||
expected.rotate_right(1);
|
expected.rotate_right(1);
|
||||||
|
|
||||||
let mut mh = MusicHoard::default();
|
let mut mh = MusicHoard {
|
||||||
mh.library_cache = left
|
collection: right.clone(),
|
||||||
.clone()
|
library_cache: left
|
||||||
.into_iter()
|
.clone()
|
||||||
.map(|a| (a.id.clone(), a))
|
.into_iter()
|
||||||
.collect();
|
.map(|a| (a.id.clone(), a))
|
||||||
mh.collection = right.clone();
|
.collect(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
mh.merge_collections();
|
mh.merge_collections();
|
||||||
assert_eq!(expected, mh.collection);
|
assert_eq!(expected, mh.collection);
|
||||||
|
|
||||||
// The merge overwrites the sort data, but no data is erased so it should be commutative.
|
// The merge overwrites the sort data, but no data is erased so it should be commutative.
|
||||||
let mut mh = MusicHoard::default();
|
let mut mh = MusicHoard {
|
||||||
mh.library_cache = right
|
collection: left.clone(),
|
||||||
.clone()
|
library_cache: right
|
||||||
.into_iter()
|
.clone()
|
||||||
.map(|a| (a.id.clone(), a))
|
.into_iter()
|
||||||
.collect();
|
.map(|a| (a.id.clone(), a))
|
||||||
mh.collection = left.clone();
|
.collect(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
mh.merge_collections();
|
mh.merge_collections();
|
||||||
assert_eq!(expected, mh.collection);
|
assert_eq!(expected, mh.collection);
|
||||||
|
14
src/main.rs
14
src/main.rs
@ -20,13 +20,7 @@ use musichoard::{
|
|||||||
MusicHoardBuilder, NoDatabase, NoLibrary,
|
MusicHoardBuilder, NoDatabase, NoLibrary,
|
||||||
};
|
};
|
||||||
|
|
||||||
use tui::{
|
use tui::{App, EventChannel, EventHandler, EventListener, Tui, Ui};
|
||||||
event::EventChannel,
|
|
||||||
handler::EventHandler,
|
|
||||||
listener::EventListener,
|
|
||||||
ui::{render::Renderer, Ui},
|
|
||||||
Tui,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(StructOpt)]
|
#[derive(StructOpt)]
|
||||||
struct Opt {
|
struct Opt {
|
||||||
@ -73,11 +67,11 @@ fn with<LIB: ILibrary, DB: IDatabase>(builder: MusicHoardBuilder<LIB, DB>) {
|
|||||||
let listener = EventListener::new(channel.sender());
|
let listener = EventListener::new(channel.sender());
|
||||||
let handler = EventHandler::new(channel.receiver());
|
let handler = EventHandler::new(channel.receiver());
|
||||||
|
|
||||||
let ui = Ui::new(music_hoard).expect("failed to initialise ui");
|
let app = App::new(music_hoard).expect("failed to initialise application");
|
||||||
let renderer = Renderer;
|
let ui = Ui;
|
||||||
|
|
||||||
// Run the TUI application.
|
// Run the TUI application.
|
||||||
Tui::run(terminal, ui, renderer, handler, listener).expect("failed to run tui");
|
Tui::run(terminal, app, ui, handler, listener).expect("failed to run tui");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn with_database<LIB: ILibrary>(db_opt: DbOpt, builder: MusicHoardBuilder<LIB, NoDatabase>) {
|
fn with_database<LIB: ILibrary>(db_opt: DbOpt, builder: MusicHoardBuilder<LIB, NoDatabase>) {
|
||||||
|
@ -1,21 +1,16 @@
|
|||||||
pub mod render;
|
|
||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
use musichoard::collection::{album::Album, artist::Artist, track::Track, Collection};
|
use musichoard::collection::{album::Album, artist::Artist, track::Track, Collection};
|
||||||
use ratatui::{backend::Backend, widgets::ListState, Frame};
|
use ratatui::widgets::ListState;
|
||||||
|
|
||||||
use crate::tui::{
|
use crate::tui::{lib::IMusicHoard, Error};
|
||||||
ui::render::IRender,
|
|
||||||
{lib::IMusicHoard, Error},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum UiError {
|
pub enum AppError {
|
||||||
Lib(String),
|
Lib(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for UiError {
|
impl fmt::Display for AppError {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
match *self {
|
match *self {
|
||||||
Self::Lib(ref s) => write!(f, "the musichoard library returned an error: {s}"),
|
Self::Lib(ref s) => write!(f, "the musichoard library returned an error: {s}"),
|
||||||
@ -23,54 +18,52 @@ impl fmt::Display for UiError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<musichoard::Error> for UiError {
|
impl From<musichoard::Error> for AppError {
|
||||||
fn from(err: musichoard::Error) -> UiError {
|
fn from(err: musichoard::Error) -> AppError {
|
||||||
UiError::Lib(err.to_string())
|
AppError::Lib(err.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum UiState<BS, IS, RS, ES> {
|
pub enum AppState<BS, IS, RS, ES> {
|
||||||
Browse(BS),
|
Browse(BS),
|
||||||
Info(IS),
|
Info(IS),
|
||||||
Reload(RS),
|
Reload(RS),
|
||||||
Error(ES),
|
Error(ES),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<BS, IS, RS, ES> UiState<BS, IS, RS, ES> {
|
impl<BS, IS, RS, ES> AppState<BS, IS, RS, ES> {
|
||||||
fn is_browse(&self) -> bool {
|
fn is_browse(&self) -> bool {
|
||||||
matches!(self, UiState::Browse(_))
|
matches!(self, AppState::Browse(_))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_info(&self) -> bool {
|
fn is_info(&self) -> bool {
|
||||||
matches!(self, UiState::Info(_))
|
matches!(self, AppState::Info(_))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_reload(&self) -> bool {
|
fn is_reload(&self) -> bool {
|
||||||
matches!(self, UiState::Reload(_))
|
matches!(self, AppState::Reload(_))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_error(&self) -> bool {
|
fn is_error(&self) -> bool {
|
||||||
matches!(self, UiState::Error(_))
|
matches!(self, AppState::Error(_))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait IUi {
|
pub trait IAppInteract {
|
||||||
type BS: IUiBrowse;
|
type BS: IAppInteractBrowse;
|
||||||
type IS: IUiInfo;
|
type IS: IAppInteractInfo;
|
||||||
type RS: IUiReload;
|
type RS: IAppInteractReload;
|
||||||
type ES: IUiError;
|
type ES: IAppInteractError;
|
||||||
|
|
||||||
fn is_running(&self) -> bool;
|
fn is_running(&self) -> bool;
|
||||||
fn quit(&mut self);
|
fn quit(&mut self);
|
||||||
|
|
||||||
fn save(&mut self) -> Result<(), UiError>;
|
fn save(&mut self) -> Result<(), AppError>;
|
||||||
|
|
||||||
fn state(&mut self) -> UiState<&mut Self::BS, &mut Self::IS, &mut Self::RS, &mut Self::ES>;
|
fn state(&mut self) -> AppState<&mut Self::BS, &mut Self::IS, &mut Self::RS, &mut Self::ES>;
|
||||||
|
|
||||||
fn render<R: IRender, B: Backend>(&mut self, renderer: &R, frame: &mut Frame<'_, B>);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait IUiBrowse {
|
pub trait IAppInteractBrowse {
|
||||||
fn increment_category(&mut self);
|
fn increment_category(&mut self);
|
||||||
fn decrement_category(&mut self);
|
fn decrement_category(&mut self);
|
||||||
fn increment_selection(&mut self);
|
fn increment_selection(&mut self);
|
||||||
@ -81,109 +74,46 @@ pub trait IUiBrowse {
|
|||||||
fn show_reload_menu(&mut self);
|
fn show_reload_menu(&mut self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait IUiInfo {
|
pub trait IAppInteractInfo {
|
||||||
fn hide_info_overlay(&mut self);
|
fn hide_info_overlay(&mut self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait IUiReload {
|
pub trait IAppInteractReload {
|
||||||
fn reload_library(&mut self);
|
fn reload_library(&mut self);
|
||||||
fn reload_database(&mut self);
|
fn reload_database(&mut self);
|
||||||
fn go_back(&mut self);
|
fn go_back(&mut self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait IUiError {
|
pub trait IAppInteractError {
|
||||||
fn dismiss_error(&mut self);
|
fn dismiss_error(&mut self);
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TrackSelection {
|
// It would be preferable to have a getter for each field separately. However, the selection field
|
||||||
state: ListState,
|
// needs to be mutably accessible requiring a mutable borrow of the entire struct if behind a trait.
|
||||||
|
// This in turn complicates simultaneous field access since only a single mutable borrow is allowed.
|
||||||
|
// Therefore, all fields are grouped into a single struct and returned as a batch.
|
||||||
|
pub trait IAppAccess {
|
||||||
|
fn get(&mut self) -> AppPublic;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AlbumSelection {
|
pub struct AppPublic<'app> {
|
||||||
state: ListState,
|
pub collection: &'app Collection,
|
||||||
track: TrackSelection,
|
pub selection: &'app mut Selection,
|
||||||
|
pub state: &'app AppState<(), (), (), String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ArtistSelection {
|
pub struct ArtistSelection {
|
||||||
state: ListState,
|
pub state: ListState,
|
||||||
album: AlbumSelection,
|
pub album: AlbumSelection,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TrackSelection {
|
pub struct AlbumSelection {
|
||||||
fn initialise(tracks: Option<&[Track]>) -> Self {
|
pub state: ListState,
|
||||||
let mut state = ListState::default();
|
pub track: TrackSelection,
|
||||||
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 {
|
pub struct TrackSelection {
|
||||||
fn initialise(albums: Option<&[Album]>) -> Self {
|
pub state: ListState,
|
||||||
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 {
|
impl ArtistSelection {
|
||||||
@ -245,6 +175,83 @@ impl ArtistSelection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||||
pub enum Category {
|
pub enum Category {
|
||||||
Artist,
|
Artist,
|
||||||
@ -253,19 +260,19 @@ pub enum Category {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct Selection {
|
pub struct Selection {
|
||||||
active: Category,
|
pub active: Category,
|
||||||
artist: ArtistSelection,
|
pub artist: ArtistSelection,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Selection {
|
impl Selection {
|
||||||
fn new(artists: Option<&[Artist]>) -> Self {
|
pub fn new(artists: Option<&[Artist]>) -> Self {
|
||||||
Selection {
|
Selection {
|
||||||
active: Category::Artist,
|
active: Category::Artist,
|
||||||
artist: ArtistSelection::initialise(artists),
|
artist: ArtistSelection::initialise(artists),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn increment_category(&mut self) {
|
pub fn increment_category(&mut self) {
|
||||||
self.active = match self.active {
|
self.active = match self.active {
|
||||||
Category::Artist => Category::Album,
|
Category::Artist => Category::Album,
|
||||||
Category::Album => Category::Track,
|
Category::Album => Category::Track,
|
||||||
@ -273,7 +280,7 @@ impl Selection {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn decrement_category(&mut self) {
|
pub fn decrement_category(&mut self) {
|
||||||
self.active = match self.active {
|
self.active = match self.active {
|
||||||
Category::Artist => Category::Artist,
|
Category::Artist => Category::Artist,
|
||||||
Category::Album => Category::Artist,
|
Category::Album => Category::Artist,
|
||||||
@ -281,7 +288,7 @@ impl Selection {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn increment_selection(&mut self, collection: &Collection) {
|
pub fn increment_selection(&mut self, collection: &Collection) {
|
||||||
match self.active {
|
match self.active {
|
||||||
Category::Artist => self.increment_artist(collection),
|
Category::Artist => self.increment_artist(collection),
|
||||||
Category::Album => self.increment_album(collection),
|
Category::Album => self.increment_album(collection),
|
||||||
@ -289,7 +296,7 @@ impl Selection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn decrement_selection(&mut self, collection: &Collection) {
|
pub fn decrement_selection(&mut self, collection: &Collection) {
|
||||||
match self.active {
|
match self.active {
|
||||||
Category::Artist => self.decrement_artist(collection),
|
Category::Artist => self.decrement_artist(collection),
|
||||||
Category::Album => self.decrement_album(collection),
|
Category::Album => self.decrement_album(collection),
|
||||||
@ -322,29 +329,29 @@ impl Selection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Ui<MH: IMusicHoard> {
|
pub struct App<MH: IMusicHoard> {
|
||||||
running: bool,
|
running: bool,
|
||||||
music_hoard: MH,
|
music_hoard: MH,
|
||||||
selection: Selection,
|
selection: Selection,
|
||||||
state: UiState<(), (), (), String>,
|
state: AppState<(), (), (), String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<MH: IMusicHoard> Ui<MH> {
|
impl<MH: IMusicHoard> App<MH> {
|
||||||
pub fn new(mut music_hoard: MH) -> Result<Self, Error> {
|
pub fn new(mut music_hoard: MH) -> Result<Self, Error> {
|
||||||
// FIXME: if either returns an error start in an error state
|
// FIXME: if either returns an error start in an error state
|
||||||
music_hoard.load_from_database()?;
|
music_hoard.load_from_database()?;
|
||||||
music_hoard.rescan_library()?;
|
music_hoard.rescan_library()?;
|
||||||
let selection = Selection::new(Some(music_hoard.get_collection()));
|
let selection = Selection::new(Some(music_hoard.get_collection()));
|
||||||
Ok(Ui {
|
Ok(App {
|
||||||
running: true,
|
running: true,
|
||||||
music_hoard,
|
music_hoard,
|
||||||
selection,
|
selection,
|
||||||
state: UiState::Browse(()),
|
state: AppState::Browse(()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<MH: IMusicHoard> IUi for Ui<MH> {
|
impl<MH: IMusicHoard> IAppInteract for App<MH> {
|
||||||
type BS = Self;
|
type BS = Self;
|
||||||
type IS = Self;
|
type IS = Self;
|
||||||
type RS = Self;
|
type RS = Self;
|
||||||
@ -358,37 +365,21 @@ impl<MH: IMusicHoard> IUi for Ui<MH> {
|
|||||||
self.running = false;
|
self.running = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save(&mut self) -> Result<(), UiError> {
|
fn save(&mut self) -> Result<(), AppError> {
|
||||||
Ok(self.music_hoard.save_to_database()?)
|
Ok(self.music_hoard.save_to_database()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn state(&mut self) -> UiState<&mut Self::BS, &mut Self::IS, &mut Self::RS, &mut Self::ES> {
|
fn state(&mut self) -> AppState<&mut Self::BS, &mut Self::IS, &mut Self::RS, &mut Self::ES> {
|
||||||
match self.state {
|
match self.state {
|
||||||
UiState::Browse(_) => UiState::Browse(self),
|
AppState::Browse(_) => AppState::Browse(self),
|
||||||
UiState::Info(_) => UiState::Info(self),
|
AppState::Info(_) => AppState::Info(self),
|
||||||
UiState::Reload(_) => UiState::Reload(self),
|
AppState::Reload(_) => AppState::Reload(self),
|
||||||
UiState::Error(_) => UiState::Error(self),
|
AppState::Error(_) => AppState::Error(self),
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: rendering logic should be in render.rs - reverse Renderer/Ui relationship so that TUI
|
|
||||||
// calls are renderer.render(ui, frame); Then rename ui -> app and render -> ui as it used to be
|
|
||||||
// originally.
|
|
||||||
fn render<R: IRender, B: Backend>(&mut self, _: &R, frame: &mut Frame<'_, B>) {
|
|
||||||
let collection: &Collection = &self.music_hoard.get_collection();
|
|
||||||
let selection: &mut Selection = &mut self.selection;
|
|
||||||
|
|
||||||
R::render_collection(collection, selection, frame);
|
|
||||||
match self.state {
|
|
||||||
UiState::Info(_) => R::render_info_overlay(collection, selection, frame),
|
|
||||||
UiState::Reload(_) => R::render_reload_overlay(frame),
|
|
||||||
UiState::Error(ref msg) => R::render_error_overlay(msg, frame),
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<MH: IMusicHoard> IUiBrowse for Ui<MH> {
|
impl<MH: IMusicHoard> IAppInteractBrowse for App<MH> {
|
||||||
fn increment_category(&mut self) {
|
fn increment_category(&mut self) {
|
||||||
self.selection.increment_category();
|
self.selection.increment_category();
|
||||||
}
|
}
|
||||||
@ -409,23 +400,23 @@ impl<MH: IMusicHoard> IUiBrowse for Ui<MH> {
|
|||||||
|
|
||||||
fn show_info_overlay(&mut self) {
|
fn show_info_overlay(&mut self) {
|
||||||
assert!(self.state.is_browse());
|
assert!(self.state.is_browse());
|
||||||
self.state = UiState::Info(());
|
self.state = AppState::Info(());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_reload_menu(&mut self) {
|
fn show_reload_menu(&mut self) {
|
||||||
assert!(self.state.is_browse());
|
assert!(self.state.is_browse());
|
||||||
self.state = UiState::Reload(());
|
self.state = AppState::Reload(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<MH: IMusicHoard> IUiInfo for Ui<MH> {
|
impl<MH: IMusicHoard> IAppInteractInfo for App<MH> {
|
||||||
fn hide_info_overlay(&mut self) {
|
fn hide_info_overlay(&mut self) {
|
||||||
assert!(self.state.is_info());
|
assert!(self.state.is_info());
|
||||||
self.state = UiState::Browse(());
|
self.state = AppState::Browse(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<MH: IMusicHoard> IUiReload for Ui<MH> {
|
impl<MH: IMusicHoard> IAppInteractReload for App<MH> {
|
||||||
fn reload_library(&mut self) {
|
fn reload_library(&mut self) {
|
||||||
let result = self.music_hoard.rescan_library();
|
let result = self.music_hoard.rescan_library();
|
||||||
self.refresh(result);
|
self.refresh(result);
|
||||||
@ -438,44 +429,51 @@ impl<MH: IMusicHoard> IUiReload for Ui<MH> {
|
|||||||
|
|
||||||
fn go_back(&mut self) {
|
fn go_back(&mut self) {
|
||||||
assert!(self.state.is_reload());
|
assert!(self.state.is_reload());
|
||||||
self.state = UiState::Browse(());
|
self.state = AppState::Browse(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
trait IUiReloadPrivate {
|
trait IAppInteractReloadPrivate {
|
||||||
fn refresh(&mut self, result: Result<(), musichoard::Error>);
|
fn refresh(&mut self, result: Result<(), musichoard::Error>);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<MH: IMusicHoard> IUiReloadPrivate for Ui<MH> {
|
impl<MH: IMusicHoard> IAppInteractReloadPrivate for App<MH> {
|
||||||
fn refresh(&mut self, result: Result<(), musichoard::Error>) {
|
fn refresh(&mut self, result: Result<(), musichoard::Error>) {
|
||||||
assert!(self.state.is_reload());
|
assert!(self.state.is_reload());
|
||||||
match result {
|
match result {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
self.selection = Selection::new(Some(self.music_hoard.get_collection()));
|
self.selection = Selection::new(Some(self.music_hoard.get_collection()));
|
||||||
self.state = UiState::Browse(())
|
self.state = AppState::Browse(())
|
||||||
}
|
}
|
||||||
Err(err) => self.state = UiState::Error(err.to_string()),
|
Err(err) => self.state = AppState::Error(err.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<MH: IMusicHoard> IUiError for Ui<MH> {
|
impl<MH: IMusicHoard> IAppInteractError for App<MH> {
|
||||||
fn dismiss_error(&mut self) {
|
fn dismiss_error(&mut self) {
|
||||||
assert!(self.state.is_error());
|
assert!(self.state.is_error());
|
||||||
self.state = UiState::Browse(());
|
self.state = AppState::Browse(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<MH: IMusicHoard> IAppAccess for App<MH> {
|
||||||
|
fn get(&mut self) -> AppPublic {
|
||||||
|
AppPublic {
|
||||||
|
collection: self.music_hoard.get_collection(),
|
||||||
|
selection: &mut self.selection,
|
||||||
|
state: &self.state,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::tui::lib::MockIMusicHoard;
|
use crate::tui::{lib::MockIMusicHoard, testmod::COLLECTION};
|
||||||
use crate::tui::testmod::COLLECTION;
|
|
||||||
use crate::tui::tests::{terminal, ui};
|
|
||||||
use crate::tui::ui::render::Renderer;
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
pub fn music_hoard(collection: Collection) -> MockIMusicHoard {
|
fn music_hoard(collection: Collection) -> MockIMusicHoard {
|
||||||
let mut music_hoard = MockIMusicHoard::new();
|
let mut music_hoard = MockIMusicHoard::new();
|
||||||
|
|
||||||
music_hoard
|
music_hoard
|
||||||
@ -645,11 +643,11 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn running() {
|
fn running() {
|
||||||
let mut ui = Ui::new(music_hoard(COLLECTION.to_owned())).unwrap();
|
let mut app = App::new(music_hoard(COLLECTION.to_owned())).unwrap();
|
||||||
assert!(ui.is_running());
|
assert!(app.is_running());
|
||||||
|
|
||||||
ui.quit();
|
app.quit();
|
||||||
assert!(!ui.is_running());
|
assert!(!app.is_running());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -661,99 +659,99 @@ mod tests {
|
|||||||
.times(1)
|
.times(1)
|
||||||
.return_once(|| Ok(()));
|
.return_once(|| Ok(()));
|
||||||
|
|
||||||
let mut ui = Ui::new(music_hoard).unwrap();
|
let mut app = App::new(music_hoard).unwrap();
|
||||||
|
|
||||||
let result = ui.save();
|
let result = app.save();
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn modifiers() {
|
fn modifiers() {
|
||||||
let mut ui = Ui::new(music_hoard(COLLECTION.to_owned())).unwrap();
|
let mut app = App::new(music_hoard(COLLECTION.to_owned())).unwrap();
|
||||||
assert!(ui.is_running());
|
assert!(app.is_running());
|
||||||
|
|
||||||
assert_eq!(ui.selection.active, Category::Artist);
|
assert_eq!(app.selection.active, Category::Artist);
|
||||||
assert_eq!(ui.selection.artist.state.selected(), Some(0));
|
assert_eq!(app.selection.artist.state.selected(), Some(0));
|
||||||
assert_eq!(ui.selection.artist.album.state.selected(), Some(0));
|
assert_eq!(app.selection.artist.album.state.selected(), Some(0));
|
||||||
assert_eq!(ui.selection.artist.album.track.state.selected(), Some(0));
|
assert_eq!(app.selection.artist.album.track.state.selected(), Some(0));
|
||||||
|
|
||||||
ui.increment_selection();
|
app.increment_selection();
|
||||||
assert_eq!(ui.selection.active, Category::Artist);
|
assert_eq!(app.selection.active, Category::Artist);
|
||||||
assert_eq!(ui.selection.artist.state.selected(), Some(1));
|
assert_eq!(app.selection.artist.state.selected(), Some(1));
|
||||||
assert_eq!(ui.selection.artist.album.state.selected(), Some(0));
|
assert_eq!(app.selection.artist.album.state.selected(), Some(0));
|
||||||
assert_eq!(ui.selection.artist.album.track.state.selected(), Some(0));
|
assert_eq!(app.selection.artist.album.track.state.selected(), Some(0));
|
||||||
|
|
||||||
ui.increment_category();
|
app.increment_category();
|
||||||
assert_eq!(ui.selection.active, Category::Album);
|
assert_eq!(app.selection.active, Category::Album);
|
||||||
assert_eq!(ui.selection.artist.state.selected(), Some(1));
|
assert_eq!(app.selection.artist.state.selected(), Some(1));
|
||||||
assert_eq!(ui.selection.artist.album.state.selected(), Some(0));
|
assert_eq!(app.selection.artist.album.state.selected(), Some(0));
|
||||||
assert_eq!(ui.selection.artist.album.track.state.selected(), Some(0));
|
assert_eq!(app.selection.artist.album.track.state.selected(), Some(0));
|
||||||
|
|
||||||
ui.increment_selection();
|
app.increment_selection();
|
||||||
assert_eq!(ui.selection.active, Category::Album);
|
assert_eq!(app.selection.active, Category::Album);
|
||||||
assert_eq!(ui.selection.artist.state.selected(), Some(1));
|
assert_eq!(app.selection.artist.state.selected(), Some(1));
|
||||||
assert_eq!(ui.selection.artist.album.state.selected(), Some(1));
|
assert_eq!(app.selection.artist.album.state.selected(), Some(1));
|
||||||
assert_eq!(ui.selection.artist.album.track.state.selected(), Some(0));
|
assert_eq!(app.selection.artist.album.track.state.selected(), Some(0));
|
||||||
|
|
||||||
ui.increment_category();
|
app.increment_category();
|
||||||
assert_eq!(ui.selection.active, Category::Track);
|
assert_eq!(app.selection.active, Category::Track);
|
||||||
assert_eq!(ui.selection.artist.state.selected(), Some(1));
|
assert_eq!(app.selection.artist.state.selected(), Some(1));
|
||||||
assert_eq!(ui.selection.artist.album.state.selected(), Some(1));
|
assert_eq!(app.selection.artist.album.state.selected(), Some(1));
|
||||||
assert_eq!(ui.selection.artist.album.track.state.selected(), Some(0));
|
assert_eq!(app.selection.artist.album.track.state.selected(), Some(0));
|
||||||
|
|
||||||
ui.increment_selection();
|
app.increment_selection();
|
||||||
assert_eq!(ui.selection.active, Category::Track);
|
assert_eq!(app.selection.active, Category::Track);
|
||||||
assert_eq!(ui.selection.artist.state.selected(), Some(1));
|
assert_eq!(app.selection.artist.state.selected(), Some(1));
|
||||||
assert_eq!(ui.selection.artist.album.state.selected(), Some(1));
|
assert_eq!(app.selection.artist.album.state.selected(), Some(1));
|
||||||
assert_eq!(ui.selection.artist.album.track.state.selected(), Some(1));
|
assert_eq!(app.selection.artist.album.track.state.selected(), Some(1));
|
||||||
|
|
||||||
ui.increment_category();
|
app.increment_category();
|
||||||
assert_eq!(ui.selection.active, Category::Track);
|
assert_eq!(app.selection.active, Category::Track);
|
||||||
assert_eq!(ui.selection.artist.state.selected(), Some(1));
|
assert_eq!(app.selection.artist.state.selected(), Some(1));
|
||||||
assert_eq!(ui.selection.artist.album.state.selected(), Some(1));
|
assert_eq!(app.selection.artist.album.state.selected(), Some(1));
|
||||||
assert_eq!(ui.selection.artist.album.track.state.selected(), Some(1));
|
assert_eq!(app.selection.artist.album.track.state.selected(), Some(1));
|
||||||
|
|
||||||
ui.decrement_selection();
|
app.decrement_selection();
|
||||||
assert_eq!(ui.selection.active, Category::Track);
|
assert_eq!(app.selection.active, Category::Track);
|
||||||
assert_eq!(ui.selection.artist.state.selected(), Some(1));
|
assert_eq!(app.selection.artist.state.selected(), Some(1));
|
||||||
assert_eq!(ui.selection.artist.album.state.selected(), Some(1));
|
assert_eq!(app.selection.artist.album.state.selected(), Some(1));
|
||||||
assert_eq!(ui.selection.artist.album.track.state.selected(), Some(0));
|
assert_eq!(app.selection.artist.album.track.state.selected(), Some(0));
|
||||||
|
|
||||||
ui.increment_selection();
|
app.increment_selection();
|
||||||
ui.decrement_category();
|
app.decrement_category();
|
||||||
assert_eq!(ui.selection.active, Category::Album);
|
assert_eq!(app.selection.active, Category::Album);
|
||||||
assert_eq!(ui.selection.artist.state.selected(), Some(1));
|
assert_eq!(app.selection.artist.state.selected(), Some(1));
|
||||||
assert_eq!(ui.selection.artist.album.state.selected(), Some(1));
|
assert_eq!(app.selection.artist.album.state.selected(), Some(1));
|
||||||
assert_eq!(ui.selection.artist.album.track.state.selected(), Some(1));
|
assert_eq!(app.selection.artist.album.track.state.selected(), Some(1));
|
||||||
|
|
||||||
ui.decrement_selection();
|
app.decrement_selection();
|
||||||
assert_eq!(ui.selection.active, Category::Album);
|
assert_eq!(app.selection.active, Category::Album);
|
||||||
assert_eq!(ui.selection.artist.state.selected(), Some(1));
|
assert_eq!(app.selection.artist.state.selected(), Some(1));
|
||||||
assert_eq!(ui.selection.artist.album.state.selected(), Some(0));
|
assert_eq!(app.selection.artist.album.state.selected(), Some(0));
|
||||||
assert_eq!(ui.selection.artist.album.track.state.selected(), Some(0));
|
assert_eq!(app.selection.artist.album.track.state.selected(), Some(0));
|
||||||
|
|
||||||
ui.increment_selection();
|
app.increment_selection();
|
||||||
ui.decrement_category();
|
app.decrement_category();
|
||||||
assert_eq!(ui.selection.active, Category::Artist);
|
assert_eq!(app.selection.active, Category::Artist);
|
||||||
assert_eq!(ui.selection.artist.state.selected(), Some(1));
|
assert_eq!(app.selection.artist.state.selected(), Some(1));
|
||||||
assert_eq!(ui.selection.artist.album.state.selected(), Some(1));
|
assert_eq!(app.selection.artist.album.state.selected(), Some(1));
|
||||||
assert_eq!(ui.selection.artist.album.track.state.selected(), Some(0));
|
assert_eq!(app.selection.artist.album.track.state.selected(), Some(0));
|
||||||
|
|
||||||
ui.decrement_selection();
|
app.decrement_selection();
|
||||||
assert_eq!(ui.selection.active, Category::Artist);
|
assert_eq!(app.selection.active, Category::Artist);
|
||||||
assert_eq!(ui.selection.artist.state.selected(), Some(0));
|
assert_eq!(app.selection.artist.state.selected(), Some(0));
|
||||||
assert_eq!(ui.selection.artist.album.state.selected(), Some(0));
|
assert_eq!(app.selection.artist.album.state.selected(), Some(0));
|
||||||
assert_eq!(ui.selection.artist.album.track.state.selected(), Some(0));
|
assert_eq!(app.selection.artist.album.track.state.selected(), Some(0));
|
||||||
|
|
||||||
ui.increment_category();
|
app.increment_category();
|
||||||
ui.increment_selection();
|
app.increment_selection();
|
||||||
ui.decrement_category();
|
app.decrement_category();
|
||||||
ui.decrement_selection();
|
app.decrement_selection();
|
||||||
ui.decrement_category();
|
app.decrement_category();
|
||||||
assert_eq!(ui.selection.active, Category::Artist);
|
assert_eq!(app.selection.active, Category::Artist);
|
||||||
assert_eq!(ui.selection.artist.state.selected(), Some(0));
|
assert_eq!(app.selection.artist.state.selected(), Some(0));
|
||||||
assert_eq!(ui.selection.artist.album.state.selected(), Some(1));
|
assert_eq!(app.selection.artist.album.state.selected(), Some(1));
|
||||||
assert_eq!(ui.selection.artist.album.track.state.selected(), Some(0));
|
assert_eq!(app.selection.artist.album.track.state.selected(), Some(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -761,7 +759,7 @@ mod tests {
|
|||||||
let mut collection = COLLECTION.to_owned();
|
let mut collection = COLLECTION.to_owned();
|
||||||
collection[0].albums[0].tracks = vec![];
|
collection[0].albums[0].tracks = vec![];
|
||||||
|
|
||||||
let mut app = Ui::new(music_hoard(collection)).unwrap();
|
let mut app = App::new(music_hoard(collection)).unwrap();
|
||||||
assert!(app.is_running());
|
assert!(app.is_running());
|
||||||
|
|
||||||
assert_eq!(app.selection.active, Category::Artist);
|
assert_eq!(app.selection.active, Category::Artist);
|
||||||
@ -790,7 +788,7 @@ mod tests {
|
|||||||
let mut collection = COLLECTION.to_owned();
|
let mut collection = COLLECTION.to_owned();
|
||||||
collection[0].albums = vec![];
|
collection[0].albums = vec![];
|
||||||
|
|
||||||
let mut app = Ui::new(music_hoard(collection)).unwrap();
|
let mut app = App::new(music_hoard(collection)).unwrap();
|
||||||
assert!(app.is_running());
|
assert!(app.is_running());
|
||||||
|
|
||||||
assert_eq!(app.selection.active, Category::Artist);
|
assert_eq!(app.selection.active, Category::Artist);
|
||||||
@ -829,7 +827,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn no_artists() {
|
fn no_artists() {
|
||||||
let mut app = Ui::new(music_hoard(vec![])).unwrap();
|
let mut app = App::new(music_hoard(vec![])).unwrap();
|
||||||
assert!(app.is_running());
|
assert!(app.is_running());
|
||||||
|
|
||||||
assert_eq!(app.selection.active, Category::Artist);
|
assert_eq!(app.selection.active, Category::Artist);
|
||||||
@ -882,49 +880,30 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn info_overlay() {
|
fn info_overlay() {
|
||||||
let mut terminal = terminal();
|
let mut app = App::new(music_hoard(COLLECTION.to_owned())).unwrap();
|
||||||
let renderer = Renderer;
|
assert!(app.state().is_browse());
|
||||||
|
|
||||||
let mut ui = ui(COLLECTION.to_owned());
|
app.show_info_overlay();
|
||||||
assert!(ui.state().is_browse());
|
assert!(app.state().is_info());
|
||||||
terminal.draw(|frame| ui.render(&renderer, frame)).unwrap();
|
|
||||||
|
|
||||||
ui.show_info_overlay();
|
app.hide_info_overlay();
|
||||||
assert!(ui.state().is_info());
|
assert!(app.state().is_browse());
|
||||||
terminal.draw(|frame| ui.render(&renderer, frame)).unwrap();
|
|
||||||
|
|
||||||
// Change the artist (which has a multi-link entry).
|
|
||||||
ui.increment_selection();
|
|
||||||
terminal.draw(|frame| ui.render(&renderer, frame)).unwrap();
|
|
||||||
|
|
||||||
ui.hide_info_overlay();
|
|
||||||
assert!(ui.state().is_browse());
|
|
||||||
terminal.draw(|frame| ui.render(&renderer, frame)).unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn reload_go_back() {
|
fn reload_go_back() {
|
||||||
let mut terminal = terminal();
|
let mut app = App::new(music_hoard(COLLECTION.to_owned())).unwrap();
|
||||||
let renderer = Renderer;
|
assert!(app.state().is_browse());
|
||||||
let music_hoard = music_hoard(COLLECTION.to_owned());
|
|
||||||
|
|
||||||
let mut ui = Ui::new(music_hoard).unwrap();
|
app.show_reload_menu();
|
||||||
assert!(ui.state().is_browse());
|
assert!(app.state().is_reload());
|
||||||
terminal.draw(|frame| ui.render(&renderer, frame)).unwrap();
|
|
||||||
|
|
||||||
ui.show_reload_menu();
|
app.go_back();
|
||||||
assert!(ui.state().is_reload());
|
assert!(app.state().is_browse());
|
||||||
terminal.draw(|frame| ui.render(&renderer, frame)).unwrap();
|
|
||||||
|
|
||||||
ui.go_back();
|
|
||||||
assert!(ui.state().is_browse());
|
|
||||||
terminal.draw(|frame| ui.render(&renderer, frame)).unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn reload_database() {
|
fn reload_database() {
|
||||||
let mut terminal = terminal();
|
|
||||||
let renderer = Renderer;
|
|
||||||
let mut music_hoard = music_hoard(COLLECTION.to_owned());
|
let mut music_hoard = music_hoard(COLLECTION.to_owned());
|
||||||
|
|
||||||
music_hoard
|
music_hoard
|
||||||
@ -932,23 +911,18 @@ mod tests {
|
|||||||
.times(1)
|
.times(1)
|
||||||
.return_once(|| Ok(()));
|
.return_once(|| Ok(()));
|
||||||
|
|
||||||
let mut ui = Ui::new(music_hoard).unwrap();
|
let mut app = App::new(music_hoard).unwrap();
|
||||||
assert!(ui.state().is_browse());
|
assert!(app.state().is_browse());
|
||||||
terminal.draw(|frame| ui.render(&renderer, frame)).unwrap();
|
|
||||||
|
|
||||||
ui.show_reload_menu();
|
app.show_reload_menu();
|
||||||
assert!(ui.state().is_reload());
|
assert!(app.state().is_reload());
|
||||||
terminal.draw(|frame| ui.render(&renderer, frame)).unwrap();
|
|
||||||
|
|
||||||
ui.reload_database();
|
app.reload_database();
|
||||||
assert!(ui.state().is_browse());
|
assert!(app.state().is_browse());
|
||||||
terminal.draw(|frame| ui.render(&renderer, frame)).unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn reload_library() {
|
fn reload_library() {
|
||||||
let mut terminal = terminal();
|
|
||||||
let renderer = Renderer;
|
|
||||||
let mut music_hoard = music_hoard(COLLECTION.to_owned());
|
let mut music_hoard = music_hoard(COLLECTION.to_owned());
|
||||||
|
|
||||||
music_hoard
|
music_hoard
|
||||||
@ -956,23 +930,18 @@ mod tests {
|
|||||||
.times(1)
|
.times(1)
|
||||||
.return_once(|| Ok(()));
|
.return_once(|| Ok(()));
|
||||||
|
|
||||||
let mut ui = Ui::new(music_hoard).unwrap();
|
let mut app = App::new(music_hoard).unwrap();
|
||||||
assert!(ui.state().is_browse());
|
assert!(app.state().is_browse());
|
||||||
terminal.draw(|frame| ui.render(&renderer, frame)).unwrap();
|
|
||||||
|
|
||||||
ui.show_reload_menu();
|
app.show_reload_menu();
|
||||||
assert!(ui.state().is_reload());
|
assert!(app.state().is_reload());
|
||||||
terminal.draw(|frame| ui.render(&renderer, frame)).unwrap();
|
|
||||||
|
|
||||||
ui.reload_library();
|
app.reload_library();
|
||||||
assert!(ui.state().is_browse());
|
assert!(app.state().is_browse());
|
||||||
terminal.draw(|frame| ui.render(&renderer, frame)).unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn reload_error() {
|
fn reload_error() {
|
||||||
let mut terminal = terminal();
|
|
||||||
let renderer = Renderer;
|
|
||||||
let mut music_hoard = music_hoard(COLLECTION.to_owned());
|
let mut music_hoard = music_hoard(COLLECTION.to_owned());
|
||||||
|
|
||||||
music_hoard
|
music_hoard
|
||||||
@ -980,29 +949,25 @@ mod tests {
|
|||||||
.times(1)
|
.times(1)
|
||||||
.return_once(|| Err(musichoard::Error::DatabaseError(String::from("get rekt"))));
|
.return_once(|| Err(musichoard::Error::DatabaseError(String::from("get rekt"))));
|
||||||
|
|
||||||
let mut ui = Ui::new(music_hoard).unwrap();
|
let mut app = App::new(music_hoard).unwrap();
|
||||||
assert!(ui.state().is_browse());
|
assert!(app.state().is_browse());
|
||||||
terminal.draw(|frame| ui.render(&renderer, frame)).unwrap();
|
|
||||||
|
|
||||||
ui.show_reload_menu();
|
app.show_reload_menu();
|
||||||
assert!(ui.state().is_reload());
|
assert!(app.state().is_reload());
|
||||||
terminal.draw(|frame| ui.render(&renderer, frame)).unwrap();
|
|
||||||
|
|
||||||
ui.reload_database();
|
app.reload_database();
|
||||||
assert!(ui.state().is_error());
|
assert!(app.state().is_error());
|
||||||
terminal.draw(|frame| ui.render(&renderer, frame)).unwrap();
|
|
||||||
|
|
||||||
ui.dismiss_error();
|
app.dismiss_error();
|
||||||
assert!(ui.state().is_browse());
|
assert!(app.state().is_browse());
|
||||||
terminal.draw(|frame| ui.render(&renderer, frame)).unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn errors() {
|
fn errors() {
|
||||||
let ui_err: UiError = musichoard::Error::DatabaseError(String::from("get rekt")).into();
|
let app_err: AppError = musichoard::Error::DatabaseError(String::from("get rekt")).into();
|
||||||
|
|
||||||
assert!(!ui_err.to_string().is_empty());
|
assert!(!app_err.to_string().is_empty());
|
||||||
|
|
||||||
assert!(!format!("{:?}", ui_err).is_empty());
|
assert!(!format!("{:?}", app_err).is_empty());
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -2,14 +2,14 @@ use crossterm::event::{KeyEvent, MouseEvent};
|
|||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
|
|
||||||
use crate::tui::ui::UiError;
|
use crate::tui::app::AppError;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum EventError {
|
pub enum EventError {
|
||||||
Send(Event),
|
Send(Event),
|
||||||
Recv,
|
Recv,
|
||||||
Io(std::io::Error),
|
Io(std::io::Error),
|
||||||
Ui(String),
|
App(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for EventError {
|
impl fmt::Display for EventError {
|
||||||
@ -20,8 +20,11 @@ impl fmt::Display for EventError {
|
|||||||
Self::Io(ref e) => {
|
Self::Io(ref e) => {
|
||||||
write!(f, "an I/O error was triggered during event handling: {e}")
|
write!(f, "an I/O error was triggered during event handling: {e}")
|
||||||
}
|
}
|
||||||
Self::Ui(ref s) => {
|
Self::App(ref s) => {
|
||||||
write!(f, "the UI returned an error during event handling: {s}")
|
write!(
|
||||||
|
f,
|
||||||
|
"the application returned an error during event handling: {s}"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -39,9 +42,9 @@ impl From<mpsc::RecvError> for EventError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<UiError> for EventError {
|
impl From<AppError> for EventError {
|
||||||
fn from(err: UiError) -> EventError {
|
fn from(err: AppError) -> EventError {
|
||||||
EventError::Ui(err.to_string())
|
EventError::App(err.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,8 +105,6 @@ mod tests {
|
|||||||
|
|
||||||
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers};
|
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers};
|
||||||
|
|
||||||
use crate::tui::ui::UiError;
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -147,16 +148,16 @@ mod tests {
|
|||||||
}));
|
}));
|
||||||
let recv_err = EventError::Recv;
|
let recv_err = EventError::Recv;
|
||||||
let io_err = EventError::Io(io::Error::new(io::ErrorKind::Interrupted, "interrupted"));
|
let io_err = EventError::Io(io::Error::new(io::ErrorKind::Interrupted, "interrupted"));
|
||||||
let ui_err: EventError = UiError::Lib(String::from("lib error")).into();
|
let app_err: EventError = AppError::Lib(String::from("lib error")).into();
|
||||||
|
|
||||||
assert!(!send_err.to_string().is_empty());
|
assert!(!send_err.to_string().is_empty());
|
||||||
assert!(!recv_err.to_string().is_empty());
|
assert!(!recv_err.to_string().is_empty());
|
||||||
assert!(!io_err.to_string().is_empty());
|
assert!(!io_err.to_string().is_empty());
|
||||||
assert!(!ui_err.to_string().is_empty());
|
assert!(!app_err.to_string().is_empty());
|
||||||
|
|
||||||
assert!(!format!("{:?}", send_err).is_empty());
|
assert!(!format!("{:?}", send_err).is_empty());
|
||||||
assert!(!format!("{:?}", recv_err).is_empty());
|
assert!(!format!("{:?}", recv_err).is_empty());
|
||||||
assert!(!format!("{:?}", io_err).is_empty());
|
assert!(!format!("{:?}", io_err).is_empty());
|
||||||
assert!(!format!("{:?}", ui_err).is_empty());
|
assert!(!format!("{:?}", app_err).is_empty());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,39 +1,41 @@
|
|||||||
|
// FIXME: Can code here be less verbose
|
||||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use mockall::automock;
|
use mockall::automock;
|
||||||
|
|
||||||
use crate::tui::{
|
use crate::tui::{
|
||||||
|
app::{
|
||||||
|
AppState, IAppInteract, IAppInteractBrowse, IAppInteractError, IAppInteractInfo,
|
||||||
|
IAppInteractReload,
|
||||||
|
},
|
||||||
event::{Event, EventError, EventReceiver},
|
event::{Event, EventError, EventReceiver},
|
||||||
ui::{IUi, IUiBrowse, IUiError, IUiInfo, UiState},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::ui::IUiReload;
|
|
||||||
|
|
||||||
#[cfg_attr(test, automock)]
|
#[cfg_attr(test, automock)]
|
||||||
pub trait IEventHandler<UI: IUi> {
|
pub trait IEventHandler<APP: IAppInteract> {
|
||||||
fn handle_next_event(&self, ui: &mut UI) -> Result<(), EventError>;
|
fn handle_next_event(&self, app: &mut APP) -> Result<(), EventError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
trait IEventHandlerPrivate<UI: IUi> {
|
trait IEventHandlerPrivate<APP: IAppInteract> {
|
||||||
fn handle_key_event(ui: &mut UI, key_event: KeyEvent) -> Result<(), EventError>;
|
fn handle_key_event(app: &mut APP, key_event: KeyEvent) -> Result<(), EventError>;
|
||||||
fn handle_browse_key_event(
|
fn handle_browse_key_event(
|
||||||
ui: &mut <UI as IUi>::BS,
|
app: &mut <APP as IAppInteract>::BS,
|
||||||
key_event: KeyEvent,
|
key_event: KeyEvent,
|
||||||
) -> Result<(), EventError>;
|
) -> Result<(), EventError>;
|
||||||
fn handle_info_key_event(
|
fn handle_info_key_event(
|
||||||
ui: &mut <UI as IUi>::IS,
|
app: &mut <APP as IAppInteract>::IS,
|
||||||
key_event: KeyEvent,
|
key_event: KeyEvent,
|
||||||
) -> Result<(), EventError>;
|
) -> Result<(), EventError>;
|
||||||
fn handle_reload_key_event(
|
fn handle_reload_key_event(
|
||||||
ui: &mut <UI as IUi>::RS,
|
app: &mut <APP as IAppInteract>::RS,
|
||||||
key_event: KeyEvent,
|
key_event: KeyEvent,
|
||||||
) -> Result<(), EventError>;
|
) -> Result<(), EventError>;
|
||||||
fn handle_error_key_event(
|
fn handle_error_key_event(
|
||||||
ui: &mut <UI as IUi>::ES,
|
app: &mut <APP as IAppInteract>::ES,
|
||||||
key_event: KeyEvent,
|
key_event: KeyEvent,
|
||||||
) -> Result<(), EventError>;
|
) -> Result<(), EventError>;
|
||||||
fn quit(ui: &mut UI) -> Result<(), EventError>;
|
fn quit(app: &mut APP) -> Result<(), EventError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct EventHandler {
|
pub struct EventHandler {
|
||||||
@ -47,10 +49,10 @@ impl EventHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<UI: IUi> IEventHandler<UI> for EventHandler {
|
impl<APP: IAppInteract> IEventHandler<APP> for EventHandler {
|
||||||
fn handle_next_event(&self, ui: &mut UI) -> Result<(), EventError> {
|
fn handle_next_event(&self, app: &mut APP) -> Result<(), EventError> {
|
||||||
match self.events.recv()? {
|
match self.events.recv()? {
|
||||||
Event::Key(key_event) => Self::handle_key_event(ui, key_event)?,
|
Event::Key(key_event) => Self::handle_key_event(app, key_event)?,
|
||||||
Event::Mouse(_) => {}
|
Event::Mouse(_) => {}
|
||||||
Event::Resize(_, _) => {}
|
Event::Resize(_, _) => {}
|
||||||
};
|
};
|
||||||
@ -58,31 +60,35 @@ impl<UI: IUi> IEventHandler<UI> for EventHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<UI: IUi> IEventHandlerPrivate<UI> for EventHandler {
|
impl<APP: IAppInteract> IEventHandlerPrivate<APP> for EventHandler {
|
||||||
fn handle_key_event(ui: &mut UI, key_event: KeyEvent) -> Result<(), EventError> {
|
fn handle_key_event(app: &mut APP, key_event: KeyEvent) -> Result<(), EventError> {
|
||||||
match key_event.code {
|
match key_event.code {
|
||||||
// Exit application on `ESC` or `q`.
|
// Exit application on `ESC` or `q`.
|
||||||
KeyCode::Esc | KeyCode::Char('q') => {
|
KeyCode::Esc | KeyCode::Char('q') => {
|
||||||
Self::quit(ui)?;
|
Self::quit(app)?;
|
||||||
}
|
}
|
||||||
// Exit application on `Ctrl-C`.
|
// Exit application on `Ctrl-C`.
|
||||||
KeyCode::Char('c') | KeyCode::Char('C') => {
|
KeyCode::Char('c') | KeyCode::Char('C') => {
|
||||||
if key_event.modifiers == KeyModifiers::CONTROL {
|
if key_event.modifiers == KeyModifiers::CONTROL {
|
||||||
Self::quit(ui)?;
|
Self::quit(app)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => match ui.state() {
|
_ => match app.state() {
|
||||||
UiState::Browse(browse) => {
|
AppState::Browse(browse) => {
|
||||||
<Self as IEventHandlerPrivate<UI>>::handle_browse_key_event(browse, key_event)?;
|
<Self as IEventHandlerPrivate<APP>>::handle_browse_key_event(
|
||||||
|
browse, key_event,
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
UiState::Info(info) => {
|
AppState::Info(info) => {
|
||||||
<Self as IEventHandlerPrivate<UI>>::handle_info_key_event(info, key_event)?;
|
<Self as IEventHandlerPrivate<APP>>::handle_info_key_event(info, key_event)?;
|
||||||
}
|
}
|
||||||
UiState::Reload(reload) => {
|
AppState::Reload(reload) => {
|
||||||
<Self as IEventHandlerPrivate<UI>>::handle_reload_key_event(reload, key_event)?;
|
<Self as IEventHandlerPrivate<APP>>::handle_reload_key_event(
|
||||||
|
reload, key_event,
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
UiState::Error(error) => {
|
AppState::Error(error) => {
|
||||||
<Self as IEventHandlerPrivate<UI>>::handle_error_key_event(error, key_event)?;
|
<Self as IEventHandlerPrivate<APP>>::handle_error_key_event(error, key_event)?;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -90,20 +96,20 @@ impl<UI: IUi> IEventHandlerPrivate<UI> for EventHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn handle_browse_key_event(
|
fn handle_browse_key_event(
|
||||||
ui: &mut <UI as IUi>::BS,
|
app: &mut <APP as IAppInteract>::BS,
|
||||||
key_event: KeyEvent,
|
key_event: KeyEvent,
|
||||||
) -> Result<(), EventError> {
|
) -> Result<(), EventError> {
|
||||||
match key_event.code {
|
match key_event.code {
|
||||||
// Category change.
|
// Category change.
|
||||||
KeyCode::Left => ui.decrement_category(),
|
KeyCode::Left => app.decrement_category(),
|
||||||
KeyCode::Right => ui.increment_category(),
|
KeyCode::Right => app.increment_category(),
|
||||||
// Selection change.
|
// Selection change.
|
||||||
KeyCode::Up => ui.decrement_selection(),
|
KeyCode::Up => app.decrement_selection(),
|
||||||
KeyCode::Down => ui.increment_selection(),
|
KeyCode::Down => app.increment_selection(),
|
||||||
// Toggle overlay.
|
// Toggle overlay.
|
||||||
KeyCode::Char('m') | KeyCode::Char('M') => ui.show_info_overlay(),
|
KeyCode::Char('m') | KeyCode::Char('M') => app.show_info_overlay(),
|
||||||
// Toggle Reload
|
// Toggle Reload
|
||||||
KeyCode::Char('g') | KeyCode::Char('G') => ui.show_reload_menu(),
|
KeyCode::Char('g') | KeyCode::Char('G') => app.show_reload_menu(),
|
||||||
// Othey keys.
|
// Othey keys.
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@ -112,12 +118,12 @@ impl<UI: IUi> IEventHandlerPrivate<UI> for EventHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn handle_info_key_event(
|
fn handle_info_key_event(
|
||||||
ui: &mut <UI as IUi>::IS,
|
app: &mut <APP as IAppInteract>::IS,
|
||||||
key_event: KeyEvent,
|
key_event: KeyEvent,
|
||||||
) -> Result<(), EventError> {
|
) -> Result<(), EventError> {
|
||||||
match key_event.code {
|
match key_event.code {
|
||||||
// Toggle overlay.
|
// Toggle overlay.
|
||||||
KeyCode::Char('m') | KeyCode::Char('M') => ui.hide_info_overlay(),
|
KeyCode::Char('m') | KeyCode::Char('M') => app.hide_info_overlay(),
|
||||||
// Othey keys.
|
// Othey keys.
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@ -126,15 +132,15 @@ impl<UI: IUi> IEventHandlerPrivate<UI> for EventHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn handle_reload_key_event(
|
fn handle_reload_key_event(
|
||||||
ui: &mut <UI as IUi>::RS,
|
app: &mut <APP as IAppInteract>::RS,
|
||||||
key_event: KeyEvent,
|
key_event: KeyEvent,
|
||||||
) -> Result<(), EventError> {
|
) -> Result<(), EventError> {
|
||||||
match key_event.code {
|
match key_event.code {
|
||||||
// Reload keys.
|
// Reload keys.
|
||||||
KeyCode::Char('l') | KeyCode::Char('L') => ui.reload_library(),
|
KeyCode::Char('l') | KeyCode::Char('L') => app.reload_library(),
|
||||||
KeyCode::Char('d') | KeyCode::Char('D') => ui.reload_database(),
|
KeyCode::Char('d') | KeyCode::Char('D') => app.reload_database(),
|
||||||
// Return.
|
// Return.
|
||||||
KeyCode::Char('g') | KeyCode::Char('G') => ui.go_back(),
|
KeyCode::Char('g') | KeyCode::Char('G') => app.go_back(),
|
||||||
// Othey keys.
|
// Othey keys.
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@ -143,17 +149,17 @@ impl<UI: IUi> IEventHandlerPrivate<UI> for EventHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn handle_error_key_event(
|
fn handle_error_key_event(
|
||||||
ui: &mut <UI as IUi>::ES,
|
app: &mut <APP as IAppInteract>::ES,
|
||||||
_key_event: KeyEvent,
|
_key_event: KeyEvent,
|
||||||
) -> Result<(), EventError> {
|
) -> Result<(), EventError> {
|
||||||
// Any key dismisses the error.
|
// Any key dismisses the error.
|
||||||
ui.dismiss_error();
|
app.dismiss_error();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn quit(ui: &mut UI) -> Result<(), EventError> {
|
fn quit(app: &mut APP) -> Result<(), EventError> {
|
||||||
ui.quit();
|
app.quit();
|
||||||
ui.save()?;
|
app.save()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
104
src/tui/mod.rs
104
src/tui/mod.rs
@ -1,9 +1,15 @@
|
|||||||
pub mod event;
|
mod app;
|
||||||
pub mod handler;
|
mod event;
|
||||||
pub mod listener;
|
mod handler;
|
||||||
pub mod ui;
|
|
||||||
|
|
||||||
mod lib;
|
mod lib;
|
||||||
|
mod listener;
|
||||||
|
mod ui;
|
||||||
|
|
||||||
|
pub use app::App;
|
||||||
|
pub use event::EventChannel;
|
||||||
|
pub use handler::EventHandler;
|
||||||
|
pub use listener::EventListener;
|
||||||
|
pub use ui::Ui;
|
||||||
|
|
||||||
use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
|
use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
|
||||||
use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen};
|
use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen};
|
||||||
@ -12,10 +18,13 @@ use ratatui::Terminal;
|
|||||||
use std::io;
|
use std::io;
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
use self::event::EventError;
|
use crate::tui::{
|
||||||
use self::handler::IEventHandler;
|
app::{IAppAccess, IAppInteract},
|
||||||
use self::listener::IEventListener;
|
event::EventError,
|
||||||
use self::ui::{render::IRender, IUi};
|
handler::IEventHandler,
|
||||||
|
listener::IEventListener,
|
||||||
|
ui::IUi,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
@ -43,12 +52,12 @@ impl From<EventError> for Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Tui<B: Backend, R: IRender, UI: IUi> {
|
pub struct Tui<B: Backend, UI: IUi, APP: IAppInteract + IAppAccess> {
|
||||||
terminal: Terminal<B>,
|
terminal: Terminal<B>,
|
||||||
_phantom: PhantomData<(R, UI)>,
|
_phantom: PhantomData<(UI, APP)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<B: Backend, R: IRender, UI: IUi> Tui<B, R, UI> {
|
impl<B: Backend, UI: IUi, APP: IAppInteract + IAppAccess> Tui<B, UI, APP> {
|
||||||
fn init(&mut self) -> Result<(), Error> {
|
fn init(&mut self) -> Result<(), Error> {
|
||||||
self.terminal.hide_cursor()?;
|
self.terminal.hide_cursor()?;
|
||||||
self.terminal.clear()?;
|
self.terminal.clear()?;
|
||||||
@ -67,13 +76,16 @@ impl<B: Backend, R: IRender, UI: IUi> Tui<B, R, UI> {
|
|||||||
|
|
||||||
fn main_loop(
|
fn main_loop(
|
||||||
&mut self,
|
&mut self,
|
||||||
mut ui: UI,
|
mut app: APP,
|
||||||
renderer: R,
|
_ui: UI,
|
||||||
handler: impl IEventHandler<UI>,
|
handler: impl IEventHandler<APP>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
while ui.is_running() {
|
while app.is_running() {
|
||||||
self.terminal.draw(|frame| ui.render(&renderer, frame))?;
|
// FIXME: rendering logic should be in render.rs - reverse Renderer/Ui relationship so
|
||||||
handler.handle_next_event(&mut ui)?;
|
// that TAPP calls are renderer.render(ui, frame); Then rename ui -> app and render -> ui
|
||||||
|
// as it used to be originally.
|
||||||
|
self.terminal.draw(|frame| UI::render(&mut app, frame))?;
|
||||||
|
handler.handle_next_event(&mut app)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -81,9 +93,9 @@ impl<B: Backend, R: IRender, UI: IUi> Tui<B, R, UI> {
|
|||||||
|
|
||||||
fn main(
|
fn main(
|
||||||
term: Terminal<B>,
|
term: Terminal<B>,
|
||||||
|
app: APP,
|
||||||
ui: UI,
|
ui: UI,
|
||||||
renderer: R,
|
handler: impl IEventHandler<APP>,
|
||||||
handler: impl IEventHandler<UI>,
|
|
||||||
listener: impl IEventListener,
|
listener: impl IEventListener,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let mut tui = Tui {
|
let mut tui = Tui {
|
||||||
@ -94,7 +106,7 @@ impl<B: Backend, R: IRender, UI: IUi> Tui<B, R, UI> {
|
|||||||
tui.init()?;
|
tui.init()?;
|
||||||
|
|
||||||
let listener_handle = listener.spawn();
|
let listener_handle = listener.spawn();
|
||||||
let result = tui.main_loop(ui, renderer, handler);
|
let result = tui.main_loop(app, ui, handler);
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
@ -111,7 +123,7 @@ impl<B: Backend, R: IRender, UI: IUi> Tui<B, R, UI> {
|
|||||||
Ok(err) => return Err(err.into()),
|
Ok(err) => return Err(err.into()),
|
||||||
// Calling std::panic::resume_unwind(err) as recommended by the Rust docs
|
// Calling std::panic::resume_unwind(err) as recommended by the Rust docs
|
||||||
// will not produce an error message. The panic error message is printed at
|
// will not produce an error message. The panic error message is printed at
|
||||||
// the location of the panic which at the time is hidden by the TUI.
|
// the location of the panic which at the time is hidden by the TAPP.
|
||||||
Err(_) => return Err(Error::ListenerPanic),
|
Err(_) => return Err(Error::ListenerPanic),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -141,13 +153,13 @@ impl<B: Backend, R: IRender, UI: IUi> Tui<B, R, UI> {
|
|||||||
|
|
||||||
pub fn run(
|
pub fn run(
|
||||||
term: Terminal<B>,
|
term: Terminal<B>,
|
||||||
|
app: APP,
|
||||||
ui: UI,
|
ui: UI,
|
||||||
renderer: R,
|
handler: impl IEventHandler<APP>,
|
||||||
handler: impl IEventHandler<UI>,
|
|
||||||
listener: impl IEventListener,
|
listener: impl IEventListener,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
Self::enable()?;
|
Self::enable()?;
|
||||||
let result = Self::main(term, ui, renderer, handler, listener);
|
let result = Self::main(term, app, ui, handler, listener);
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
Self::disable()?;
|
Self::disable()?;
|
||||||
@ -176,10 +188,8 @@ mod tests {
|
|||||||
use musichoard::collection::Collection;
|
use musichoard::collection::Collection;
|
||||||
|
|
||||||
use crate::tui::{
|
use crate::tui::{
|
||||||
handler::MockIEventHandler,
|
app::App, handler::MockIEventHandler, lib::MockIMusicHoard, listener::MockIEventListener,
|
||||||
lib::MockIMusicHoard,
|
ui::Ui,
|
||||||
listener::MockIEventListener,
|
|
||||||
ui::{render::Renderer, Ui},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
@ -190,7 +200,7 @@ mod tests {
|
|||||||
Terminal::new(backend).unwrap()
|
Terminal::new(backend).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn music_hoard(collection: Collection) -> MockIMusicHoard {
|
fn music_hoard(collection: Collection) -> MockIMusicHoard {
|
||||||
let mut music_hoard = MockIMusicHoard::new();
|
let mut music_hoard = MockIMusicHoard::new();
|
||||||
|
|
||||||
music_hoard.expect_load_from_database().returning(|| Ok(()));
|
music_hoard.expect_load_from_database().returning(|| Ok(()));
|
||||||
@ -200,8 +210,8 @@ mod tests {
|
|||||||
music_hoard
|
music_hoard
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ui(collection: Collection) -> Ui<MockIMusicHoard> {
|
fn app(collection: Collection) -> App<MockIMusicHoard> {
|
||||||
Ui::new(music_hoard(collection)).unwrap()
|
App::new(music_hoard(collection)).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn listener() -> MockIEventListener {
|
fn listener() -> MockIEventListener {
|
||||||
@ -215,12 +225,12 @@ mod tests {
|
|||||||
listener
|
listener
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handler() -> MockIEventHandler<Ui<MockIMusicHoard>> {
|
fn handler() -> MockIEventHandler<App<MockIMusicHoard>> {
|
||||||
let mut handler = MockIEventHandler::new();
|
let mut handler = MockIEventHandler::new();
|
||||||
handler
|
handler
|
||||||
.expect_handle_next_event()
|
.expect_handle_next_event()
|
||||||
.return_once(|ui: &mut Ui<MockIMusicHoard>| {
|
.return_once(|app: &mut App<MockIMusicHoard>| {
|
||||||
ui.quit();
|
app.quit();
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
handler
|
handler
|
||||||
@ -229,21 +239,21 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn run() {
|
fn run() {
|
||||||
let terminal = terminal();
|
let terminal = terminal();
|
||||||
let ui = ui(COLLECTION.to_owned());
|
let app = app(COLLECTION.to_owned());
|
||||||
let renderer = Renderer;
|
let ui = Ui;
|
||||||
|
|
||||||
let listener = listener();
|
let listener = listener();
|
||||||
let handler = handler();
|
let handler = handler();
|
||||||
|
|
||||||
let result = Tui::main(terminal, ui, renderer, handler, listener);
|
let result = Tui::main(terminal, app, ui, handler, listener);
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn event_error() {
|
fn event_error() {
|
||||||
let terminal = terminal();
|
let terminal = terminal();
|
||||||
let ui = ui(COLLECTION.to_owned());
|
let app = app(COLLECTION.to_owned());
|
||||||
let renderer = Renderer;
|
let ui = Ui;
|
||||||
|
|
||||||
let listener = listener();
|
let listener = listener();
|
||||||
|
|
||||||
@ -252,7 +262,7 @@ mod tests {
|
|||||||
.expect_handle_next_event()
|
.expect_handle_next_event()
|
||||||
.return_once(|_| Err(EventError::Recv));
|
.return_once(|_| Err(EventError::Recv));
|
||||||
|
|
||||||
let result = Tui::main(terminal, ui, renderer, handler, listener);
|
let result = Tui::main(terminal, app, ui, handler, listener);
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.unwrap_err(),
|
result.unwrap_err(),
|
||||||
@ -263,8 +273,8 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn listener_error() {
|
fn listener_error() {
|
||||||
let terminal = terminal();
|
let terminal = terminal();
|
||||||
let ui = ui(COLLECTION.to_owned());
|
let app = app(COLLECTION.to_owned());
|
||||||
let renderer = Renderer;
|
let ui = Ui;
|
||||||
|
|
||||||
let error = EventError::Io(io::Error::new(io::ErrorKind::Interrupted, "error"));
|
let error = EventError::Io(io::Error::new(io::ErrorKind::Interrupted, "error"));
|
||||||
let listener_handle: thread::JoinHandle<EventError> = thread::spawn(|| error);
|
let listener_handle: thread::JoinHandle<EventError> = thread::spawn(|| error);
|
||||||
@ -278,7 +288,7 @@ mod tests {
|
|||||||
.expect_handle_next_event()
|
.expect_handle_next_event()
|
||||||
.return_once(|_| Err(EventError::Recv));
|
.return_once(|_| Err(EventError::Recv));
|
||||||
|
|
||||||
let result = Tui::main(terminal, ui, renderer, handler, listener);
|
let result = Tui::main(terminal, app, ui, handler, listener);
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
|
|
||||||
let error = EventError::Io(io::Error::new(io::ErrorKind::Interrupted, "error"));
|
let error = EventError::Io(io::Error::new(io::ErrorKind::Interrupted, "error"));
|
||||||
@ -288,8 +298,8 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn listener_panic() {
|
fn listener_panic() {
|
||||||
let terminal = terminal();
|
let terminal = terminal();
|
||||||
let ui = ui(COLLECTION.to_owned());
|
let app = app(COLLECTION.to_owned());
|
||||||
let renderer = Renderer;
|
let ui = Ui;
|
||||||
|
|
||||||
let listener_handle: thread::JoinHandle<EventError> = thread::spawn(|| panic!());
|
let listener_handle: thread::JoinHandle<EventError> = thread::spawn(|| panic!());
|
||||||
while !listener_handle.is_finished() {}
|
while !listener_handle.is_finished() {}
|
||||||
@ -302,7 +312,7 @@ mod tests {
|
|||||||
.expect_handle_next_event()
|
.expect_handle_next_event()
|
||||||
.return_once(|_| Err(EventError::Recv));
|
.return_once(|_| Err(EventError::Recv));
|
||||||
|
|
||||||
let result = Tui::main(terminal, ui, renderer, handler, listener);
|
let result = Tui::main(terminal, app, ui, handler, listener);
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
assert_eq!(result.unwrap_err(), Error::ListenerPanic);
|
assert_eq!(result.unwrap_err(), Error::ListenerPanic);
|
||||||
}
|
}
|
||||||
|
@ -12,21 +12,10 @@ use ratatui::{
|
|||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::tui::ui::{Category, Selection};
|
use crate::tui::app::{AppState, Category, IAppAccess, Selection};
|
||||||
|
|
||||||
pub trait IRender {
|
pub trait IUi {
|
||||||
fn render_collection<B: Backend>(
|
fn render<APP: IAppAccess, B: Backend>(app: &mut APP, frame: &mut Frame<'_, B>);
|
||||||
artists: &Collection,
|
|
||||||
selection: &mut Selection,
|
|
||||||
frame: &mut Frame<'_, B>,
|
|
||||||
);
|
|
||||||
fn render_info_overlay<B: Backend>(
|
|
||||||
artists: &Collection,
|
|
||||||
selection: &mut Selection,
|
|
||||||
frame: &mut Frame<'_, B>,
|
|
||||||
);
|
|
||||||
fn render_reload_overlay<B: Backend>(frame: &mut Frame<'_, B>);
|
|
||||||
fn render_error_overlay<S: AsRef<str>, B: Backend>(msg: S, frame: &mut Frame<'_, B>);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ArtistArea {
|
struct ArtistArea {
|
||||||
@ -314,9 +303,9 @@ impl<'a, 'b> TrackState<'a, 'b> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Renderer;
|
pub struct Ui;
|
||||||
|
|
||||||
impl Renderer {
|
impl Ui {
|
||||||
fn style(_active: bool) -> Style {
|
fn style(_active: bool) -> Style {
|
||||||
Style::default().fg(Color::White).bg(Color::Black)
|
Style::default().fg(Color::White).bg(Color::Black)
|
||||||
}
|
}
|
||||||
@ -403,9 +392,7 @@ impl Renderer {
|
|||||||
Self::render_list_widget("Tracks", st.list, st.state, st.active, ar.list, fr);
|
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);
|
Self::render_info_widget("Track info", st.info, st.active, ar.info, fr);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl IRender for Renderer {
|
|
||||||
fn render_collection<B: Backend>(
|
fn render_collection<B: Backend>(
|
||||||
artists: &Collection,
|
artists: &Collection,
|
||||||
selection: &mut Selection,
|
selection: &mut Selection,
|
||||||
@ -496,56 +483,59 @@ impl IRender for Renderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl IUi for Ui {
|
||||||
|
fn render<APP: IAppAccess, B: Backend>(app: &mut APP, frame: &mut Frame<'_, B>) {
|
||||||
|
let app = app.get();
|
||||||
|
|
||||||
|
let collection = app.collection;
|
||||||
|
let selection = app.selection;
|
||||||
|
|
||||||
|
Self::render_collection(collection, selection, frame);
|
||||||
|
match app.state {
|
||||||
|
AppState::Info(_) => Self::render_info_overlay(collection, selection, frame),
|
||||||
|
AppState::Reload(_) => Self::render_reload_overlay(frame),
|
||||||
|
AppState::Error(ref msg) => Self::render_error_overlay(msg, frame),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::tui::{testmod::COLLECTION, tests::terminal};
|
use crate::tui::{app::AppPublic, testmod::COLLECTION, tests::terminal};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn draw_test_suite(artists: &Collection, selection: &mut Selection) {
|
// Automock does not support returning types with generic lifetimes.
|
||||||
let mut terminal = terminal();
|
impl IAppAccess for AppPublic<'_> {
|
||||||
|
fn get(&mut self) -> AppPublic {
|
||||||
terminal
|
AppPublic {
|
||||||
.draw(|frame| Renderer::render_collection(artists, selection, frame))
|
collection: self.collection,
|
||||||
.unwrap();
|
selection: self.selection,
|
||||||
|
state: self.state,
|
||||||
terminal
|
}
|
||||||
.draw(|frame| Renderer::render_info_overlay(artists, selection, frame))
|
}
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
terminal
|
|
||||||
.draw(|frame| {
|
|
||||||
Renderer::render_collection(artists, selection, frame);
|
|
||||||
Renderer::render_info_overlay(artists, selection, frame);
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
terminal
|
|
||||||
.draw(|frame| {
|
|
||||||
Renderer::render_collection(artists, selection, frame);
|
|
||||||
Renderer::render_reload_overlay(frame);
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
terminal
|
|
||||||
.draw(|frame| {
|
|
||||||
Renderer::render_collection(artists, selection, frame);
|
|
||||||
Renderer::render_error_overlay("get rekt scrub", frame);
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
fn draw_test_suite(collection: &Collection, selection: &mut Selection) {
|
||||||
fn stateless() {
|
|
||||||
let mut terminal = terminal();
|
let mut terminal = terminal();
|
||||||
|
|
||||||
terminal
|
let mut app = AppPublic {
|
||||||
.draw(|frame| Renderer::render_reload_overlay(frame))
|
collection,
|
||||||
.unwrap();
|
selection,
|
||||||
|
state: &AppState::Browse(()),
|
||||||
|
};
|
||||||
|
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
|
||||||
|
|
||||||
terminal
|
app.state = &AppState::Info(());
|
||||||
.draw(|frame| Renderer::render_error_overlay("get rekt scrub", frame))
|
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
|
||||||
.unwrap();
|
|
||||||
|
app.state = &AppState::Reload(());
|
||||||
|
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
|
||||||
|
|
||||||
|
let binding = AppState::Error(String::from("get rekt scrub"));
|
||||||
|
app.state = &binding;
|
||||||
|
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -558,16 +548,23 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn collection() {
|
fn collection() {
|
||||||
let artists: &Collection = &COLLECTION;
|
let artists = &COLLECTION;
|
||||||
let mut selection = Selection::new(Some(artists));
|
let mut selection = Selection::new(Some(artists));
|
||||||
|
|
||||||
draw_test_suite(&artists, &mut selection);
|
draw_test_suite(artists, &mut selection);
|
||||||
|
|
||||||
// Change the track (which has a different track format).
|
// Change the track (which has a different track format).
|
||||||
selection.increment_category();
|
selection.increment_category();
|
||||||
selection.increment_category();
|
selection.increment_category();
|
||||||
selection.increment_selection(artists);
|
selection.increment_selection(artists);
|
||||||
|
|
||||||
draw_test_suite(&artists, &mut selection);
|
draw_test_suite(artists, &mut selection);
|
||||||
|
|
||||||
|
// Change the artist (which has a multi-link entry).
|
||||||
|
selection.decrement_category();
|
||||||
|
selection.decrement_category();
|
||||||
|
selection.increment_selection(artists);
|
||||||
|
|
||||||
|
draw_test_suite(artists, &mut selection);
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user