Selected item is always at the bottom of list (#41)
Closes #40 Reviewed-on: https://git.wojciechkozlowski.eu/wojtek/musichoard/pulls/41
This commit is contained in:
parent
0545e5324e
commit
2ecc64437a
@ -32,7 +32,8 @@ pub struct JsonDatabase<JDB> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<JDB: JsonDatabaseBackend> JsonDatabase<JDB> {
|
impl<JDB: JsonDatabaseBackend> JsonDatabase<JDB> {
|
||||||
/// Create a new JSON database with the provided backend, e.g. [`JsonDatabaseFileBackend`].
|
/// Create a new JSON database with the provided backend, e.g.
|
||||||
|
/// [`backend::JsonDatabaseFileBackend`].
|
||||||
pub fn new(backend: JDB) -> Self {
|
pub fn new(backend: JDB) -> Self {
|
||||||
JsonDatabase { backend }
|
JsonDatabase { backend }
|
||||||
}
|
}
|
||||||
|
@ -94,7 +94,8 @@ trait LibraryPrivate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<BLE: BeetsLibraryExecutor> BeetsLibrary<BLE> {
|
impl<BLE: BeetsLibraryExecutor> BeetsLibrary<BLE> {
|
||||||
/// Create a new beets library with the provided executor, e.g. [`BeetsLibraryProcessExecutor`].
|
/// Create a new beets library with the provided executor, e.g.
|
||||||
|
/// [`executor::BeetsLibraryProcessExecutor`].
|
||||||
pub fn new(executor: BLE) -> Self {
|
pub fn new(executor: BLE) -> Self {
|
||||||
BeetsLibrary { executor }
|
BeetsLibrary { executor }
|
||||||
}
|
}
|
||||||
|
12
src/main.rs
12
src/main.rs
@ -20,10 +20,8 @@ use musichoard::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
mod tui;
|
mod tui;
|
||||||
use tui::{
|
use tui::ui::MhUi;
|
||||||
app::TuiApp, event::EventChannel, handler::TuiEventHandler, listener::TuiEventListener, ui::Ui,
|
use tui::{event::EventChannel, handler::TuiEventHandler, listener::TuiEventListener, Tui};
|
||||||
Tui,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(StructOpt)]
|
#[derive(StructOpt)]
|
||||||
struct Opt {
|
struct Opt {
|
||||||
@ -52,12 +50,10 @@ fn with<LIB: Library, DB: Database>(lib: LIB, db: DB) {
|
|||||||
let listener = TuiEventListener::new(channel.sender());
|
let listener = TuiEventListener::new(channel.sender());
|
||||||
let handler = TuiEventHandler::new(channel.receiver());
|
let handler = TuiEventHandler::new(channel.receiver());
|
||||||
|
|
||||||
let ui = Ui::new();
|
let ui = MhUi::new(collection_manager).expect("failed to initialise ui");
|
||||||
|
|
||||||
let app = TuiApp::new(collection_manager).expect("failed to initialise app");
|
|
||||||
|
|
||||||
// Run the TUI application.
|
// Run the TUI application.
|
||||||
Tui::run(terminal, app, ui, handler, listener).expect("failed to run tui");
|
Tui::run(terminal, ui, handler, listener).expect("failed to run tui");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
832
src/tui/app.rs
832
src/tui/app.rs
@ -1,832 +0,0 @@
|
|||||||
use musichoard::{collection::CollectionManager, Album, AlbumId, Artist, ArtistId, Track};
|
|
||||||
|
|
||||||
use super::Error;
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub enum Category {
|
|
||||||
Artist,
|
|
||||||
Album,
|
|
||||||
Track,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct TrackSelection {
|
|
||||||
index: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TrackSelection {
|
|
||||||
fn initialise(tracks: &[Track]) -> Option<TrackSelection> {
|
|
||||||
if !tracks.is_empty() {
|
|
||||||
Some(TrackSelection { index: 0 })
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn increment(&mut self, tracks: &[Track]) {
|
|
||||||
if let Some(result) = self.index.checked_add(1) {
|
|
||||||
if result < tracks.len() {
|
|
||||||
self.index = result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn decrement(&mut self, _tracks: &[Track]) {
|
|
||||||
if let Some(result) = self.index.checked_sub(1) {
|
|
||||||
self.index = result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct AlbumSelection {
|
|
||||||
index: usize,
|
|
||||||
track: Option<TrackSelection>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AlbumSelection {
|
|
||||||
fn initialise(albums: &[Album]) -> Option<AlbumSelection> {
|
|
||||||
if !albums.is_empty() {
|
|
||||||
Some(AlbumSelection {
|
|
||||||
index: 0,
|
|
||||||
track: TrackSelection::initialise(&albums[0].tracks),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn increment(&mut self, albums: &[Album]) {
|
|
||||||
if let Some(result) = self.index.checked_add(1) {
|
|
||||||
if result < albums.len() {
|
|
||||||
self.index = result;
|
|
||||||
self.track = TrackSelection::initialise(&albums[self.index].tracks);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn decrement(&mut self, albums: &[Album]) {
|
|
||||||
if let Some(result) = self.index.checked_sub(1) {
|
|
||||||
self.index = result;
|
|
||||||
self.track = TrackSelection::initialise(&albums[self.index].tracks);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ArtistSelection {
|
|
||||||
index: usize,
|
|
||||||
album: Option<AlbumSelection>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ArtistSelection {
|
|
||||||
fn initialise(artists: &[Artist]) -> Option<ArtistSelection> {
|
|
||||||
if !artists.is_empty() {
|
|
||||||
Some(ArtistSelection {
|
|
||||||
index: 0,
|
|
||||||
album: AlbumSelection::initialise(&artists[0].albums),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn increment(&mut self, artists: &[Artist]) {
|
|
||||||
if let Some(result) = self.index.checked_add(1) {
|
|
||||||
if result < artists.len() {
|
|
||||||
self.index = result;
|
|
||||||
self.album = AlbumSelection::initialise(&artists[self.index].albums);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn decrement(&mut self, artists: &[Artist]) {
|
|
||||||
if let Some(result) = self.index.checked_sub(1) {
|
|
||||||
self.index = result;
|
|
||||||
self.album = AlbumSelection::initialise(&artists[self.index].albums);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Selection {
|
|
||||||
active: Category,
|
|
||||||
artist: Option<ArtistSelection>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait App {
|
|
||||||
fn is_running(&self) -> bool;
|
|
||||||
fn quit(&mut self);
|
|
||||||
|
|
||||||
fn increment_category(&mut self);
|
|
||||||
fn decrement_category(&mut self);
|
|
||||||
|
|
||||||
fn increment_selection(&mut self);
|
|
||||||
fn decrement_selection(&mut self);
|
|
||||||
|
|
||||||
fn get_active_category(&self) -> Category;
|
|
||||||
|
|
||||||
fn get_artist_ids(&self) -> Vec<&ArtistId>;
|
|
||||||
fn get_album_ids(&self) -> Vec<&AlbumId>;
|
|
||||||
fn get_track_ids(&self) -> Vec<&Track>;
|
|
||||||
|
|
||||||
fn selected_artist(&self) -> Option<usize>;
|
|
||||||
fn selected_album(&self) -> Option<usize>;
|
|
||||||
fn selected_track(&self) -> Option<usize>;
|
|
||||||
}
|
|
||||||
|
|
||||||
trait AppPrivate {
|
|
||||||
fn increment_artist_selection(&mut self);
|
|
||||||
fn decrement_artist_selection(&mut self);
|
|
||||||
|
|
||||||
fn increment_album_selection(&mut self);
|
|
||||||
fn decrement_album_selection(&mut self);
|
|
||||||
|
|
||||||
fn increment_track_selection(&mut self);
|
|
||||||
|
|
||||||
fn decrement_track_selection(&mut self);
|
|
||||||
|
|
||||||
fn get_artists(&self) -> &Vec<Artist>;
|
|
||||||
fn get_albums(&self) -> Option<&Vec<Album>>;
|
|
||||||
fn get_tracks(&self) -> Option<&Vec<Track>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct TuiApp<CM> {
|
|
||||||
collection_manager: CM,
|
|
||||||
selection: Selection,
|
|
||||||
running: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<CM: CollectionManager> TuiApp<CM> {
|
|
||||||
pub fn new(mut collection_manager: CM) -> Result<Self, Error> {
|
|
||||||
collection_manager.rescan_library()?;
|
|
||||||
let selection = Selection {
|
|
||||||
active: Category::Artist,
|
|
||||||
artist: ArtistSelection::initialise(collection_manager.get_collection()),
|
|
||||||
};
|
|
||||||
Ok(TuiApp {
|
|
||||||
collection_manager,
|
|
||||||
selection,
|
|
||||||
running: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<CM: CollectionManager> App for TuiApp<CM> {
|
|
||||||
fn is_running(&self) -> bool {
|
|
||||||
self.running
|
|
||||||
}
|
|
||||||
|
|
||||||
fn quit(&mut self) {
|
|
||||||
self.running = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn increment_category(&mut self) {
|
|
||||||
self.selection.active = match self.selection.active {
|
|
||||||
Category::Artist => Category::Album,
|
|
||||||
Category::Album => Category::Track,
|
|
||||||
Category::Track => Category::Track,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn decrement_category(&mut self) {
|
|
||||||
self.selection.active = match self.selection.active {
|
|
||||||
Category::Artist => Category::Artist,
|
|
||||||
Category::Album => Category::Artist,
|
|
||||||
Category::Track => Category::Album,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn increment_selection(&mut self) {
|
|
||||||
match self.selection.active {
|
|
||||||
Category::Artist => self.increment_artist_selection(),
|
|
||||||
Category::Album => self.increment_album_selection(),
|
|
||||||
Category::Track => self.increment_track_selection(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn decrement_selection(&mut self) {
|
|
||||||
match self.selection.active {
|
|
||||||
Category::Artist => self.decrement_artist_selection(),
|
|
||||||
Category::Album => self.decrement_album_selection(),
|
|
||||||
Category::Track => self.decrement_track_selection(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_active_category(&self) -> Category {
|
|
||||||
self.selection.active
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_artist_ids(&self) -> Vec<&ArtistId> {
|
|
||||||
let artists = self.get_artists();
|
|
||||||
artists.iter().map(|a| &a.id).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_album_ids(&self) -> Vec<&AlbumId> {
|
|
||||||
if let Some(albums) = self.get_albums() {
|
|
||||||
albums.iter().map(|a| &a.id).collect()
|
|
||||||
} else {
|
|
||||||
vec![]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_track_ids(&self) -> Vec<&Track> {
|
|
||||||
if let Some(tracks) = self.get_tracks() {
|
|
||||||
tracks.iter().collect()
|
|
||||||
} else {
|
|
||||||
vec![]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn selected_artist(&self) -> Option<usize> {
|
|
||||||
self.selection.artist.as_ref().map(|s| s.index)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn selected_album(&self) -> Option<usize> {
|
|
||||||
if let Some(ref artist_selection) = self.selection.artist {
|
|
||||||
artist_selection.album.as_ref().map(|s| s.index)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn selected_track(&self) -> Option<usize> {
|
|
||||||
if let Some(ref artist_selection) = self.selection.artist {
|
|
||||||
if let Some(ref album_selection) = artist_selection.album {
|
|
||||||
album_selection.track.as_ref().map(|s| s.index)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<CM: CollectionManager> AppPrivate for TuiApp<CM> {
|
|
||||||
fn increment_artist_selection(&mut self) {
|
|
||||||
if let Some(ref mut artist_selection) = self.selection.artist {
|
|
||||||
let artists = &self.collection_manager.get_collection();
|
|
||||||
artist_selection.increment(artists);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn decrement_artist_selection(&mut self) {
|
|
||||||
if let Some(ref mut artist_selection) = self.selection.artist {
|
|
||||||
let artists = &self.collection_manager.get_collection();
|
|
||||||
artist_selection.decrement(artists);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn increment_album_selection(&mut self) {
|
|
||||||
if let Some(ref mut artist_selection) = self.selection.artist {
|
|
||||||
if let Some(ref mut album_selection) = artist_selection.album {
|
|
||||||
let artists = &self.collection_manager.get_collection();
|
|
||||||
let albums = &artists[artist_selection.index].albums;
|
|
||||||
album_selection.increment(albums);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn decrement_album_selection(&mut self) {
|
|
||||||
if let Some(ref mut artist_selection) = self.selection.artist {
|
|
||||||
if let Some(ref mut album_selection) = artist_selection.album {
|
|
||||||
let artists = &self.collection_manager.get_collection();
|
|
||||||
let albums = &artists[artist_selection.index].albums;
|
|
||||||
album_selection.decrement(albums);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn increment_track_selection(&mut self) {
|
|
||||||
if let Some(ref mut artist_selection) = self.selection.artist {
|
|
||||||
if let Some(ref mut album_selection) = artist_selection.album {
|
|
||||||
if let Some(ref mut track_selection) = album_selection.track {
|
|
||||||
let artists = &self.collection_manager.get_collection();
|
|
||||||
let albums = &artists[artist_selection.index].albums;
|
|
||||||
let tracks = &albums[album_selection.index].tracks;
|
|
||||||
track_selection.increment(tracks);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn decrement_track_selection(&mut self) {
|
|
||||||
if let Some(ref mut artist_selection) = self.selection.artist {
|
|
||||||
if let Some(ref mut album_selection) = artist_selection.album {
|
|
||||||
if let Some(ref mut track_selection) = album_selection.track {
|
|
||||||
let artists = &self.collection_manager.get_collection();
|
|
||||||
let albums = &artists[artist_selection.index].albums;
|
|
||||||
let tracks = &albums[album_selection.index].tracks;
|
|
||||||
track_selection.decrement(tracks);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_artists(&self) -> &Vec<Artist> {
|
|
||||||
self.collection_manager.get_collection()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_albums(&self) -> Option<&Vec<Album>> {
|
|
||||||
let artists = self.get_artists();
|
|
||||||
if let Some(artist_index) = self.selected_artist() {
|
|
||||||
Some(&artists[artist_index].albums)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_tracks(&self) -> Option<&Vec<Track>> {
|
|
||||||
if let Some(albums) = self.get_albums() {
|
|
||||||
if let Some(album_index) = self.selected_album() {
|
|
||||||
Some(&albums[album_index].tracks)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use crate::tests::{MockCollectionManager, COLLECTION};
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_track_selection() {
|
|
||||||
let tracks = &COLLECTION[0].albums[0].tracks;
|
|
||||||
assert!(tracks.len() > 1);
|
|
||||||
|
|
||||||
let empty = TrackSelection::initialise(&vec![]);
|
|
||||||
assert!(empty.is_none());
|
|
||||||
|
|
||||||
let sel = TrackSelection::initialise(tracks);
|
|
||||||
assert!(sel.is_some());
|
|
||||||
|
|
||||||
let mut sel = sel.unwrap();
|
|
||||||
assert_eq!(sel.index, 0);
|
|
||||||
|
|
||||||
sel.decrement(tracks);
|
|
||||||
assert_eq!(sel.index, 0);
|
|
||||||
|
|
||||||
sel.increment(tracks);
|
|
||||||
assert_eq!(sel.index, 1);
|
|
||||||
|
|
||||||
sel.decrement(tracks);
|
|
||||||
assert_eq!(sel.index, 0);
|
|
||||||
|
|
||||||
for _ in 0..(tracks.len() + 5) {
|
|
||||||
sel.increment(tracks);
|
|
||||||
}
|
|
||||||
assert_eq!(sel.index, tracks.len() - 1);
|
|
||||||
|
|
||||||
// Artifical test case to verify upper limit.
|
|
||||||
let mut sel = TrackSelection {
|
|
||||||
index: std::usize::MAX,
|
|
||||||
};
|
|
||||||
assert_eq!(sel.index, std::usize::MAX);
|
|
||||||
|
|
||||||
sel.increment(&vec![]);
|
|
||||||
assert_eq!(sel.index, std::usize::MAX);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_album_selection() {
|
|
||||||
let albums = &COLLECTION[0].albums;
|
|
||||||
assert!(albums.len() > 1);
|
|
||||||
|
|
||||||
let empty = AlbumSelection::initialise(&vec![]);
|
|
||||||
assert!(empty.is_none());
|
|
||||||
|
|
||||||
let sel = AlbumSelection::initialise(albums);
|
|
||||||
assert!(sel.is_some());
|
|
||||||
|
|
||||||
let mut sel = sel.unwrap();
|
|
||||||
assert_eq!(sel.index, 0);
|
|
||||||
assert!(sel.track.is_some());
|
|
||||||
assert_eq!(sel.track.as_ref().unwrap().index, 0);
|
|
||||||
|
|
||||||
sel.track
|
|
||||||
.as_mut()
|
|
||||||
.unwrap()
|
|
||||||
.increment(&albums[sel.index].tracks);
|
|
||||||
assert_eq!(sel.index, 0);
|
|
||||||
assert!(sel.track.is_some());
|
|
||||||
assert_eq!(sel.track.as_ref().unwrap().index, 1);
|
|
||||||
|
|
||||||
// Verify that decrement that doesn't change index does not reset track.
|
|
||||||
sel.decrement(albums);
|
|
||||||
assert_eq!(sel.index, 0);
|
|
||||||
assert!(sel.track.is_some());
|
|
||||||
assert_eq!(sel.track.as_ref().unwrap().index, 1);
|
|
||||||
|
|
||||||
sel.increment(albums);
|
|
||||||
assert_eq!(sel.index, 1);
|
|
||||||
assert!(sel.track.is_some());
|
|
||||||
assert_eq!(sel.track.as_ref().unwrap().index, 0);
|
|
||||||
|
|
||||||
sel.decrement(albums);
|
|
||||||
assert_eq!(sel.index, 0);
|
|
||||||
assert!(sel.track.is_some());
|
|
||||||
assert_eq!(sel.track.as_ref().unwrap().index, 0);
|
|
||||||
|
|
||||||
for _ in 0..(albums.len() + 5) {
|
|
||||||
sel.increment(albums);
|
|
||||||
}
|
|
||||||
assert_eq!(sel.index, albums.len() - 1);
|
|
||||||
assert!(sel.track.is_some());
|
|
||||||
assert_eq!(sel.track.as_ref().unwrap().index, 0);
|
|
||||||
|
|
||||||
sel.track
|
|
||||||
.as_mut()
|
|
||||||
.unwrap()
|
|
||||||
.increment(&albums[sel.index].tracks);
|
|
||||||
assert_eq!(sel.index, albums.len() - 1);
|
|
||||||
assert!(sel.track.is_some());
|
|
||||||
assert_eq!(sel.track.as_ref().unwrap().index, 1);
|
|
||||||
|
|
||||||
// Verify that increment that doesn't change index does not reset track.
|
|
||||||
sel.increment(albums);
|
|
||||||
assert_eq!(sel.index, albums.len() - 1);
|
|
||||||
assert!(sel.track.is_some());
|
|
||||||
assert_eq!(sel.track.as_ref().unwrap().index, 1);
|
|
||||||
|
|
||||||
// Artifical test case to verify upper limit.
|
|
||||||
let mut sel = AlbumSelection {
|
|
||||||
index: std::usize::MAX,
|
|
||||||
track: Some(TrackSelection { index: 1 }),
|
|
||||||
};
|
|
||||||
assert_eq!(sel.index, std::usize::MAX);
|
|
||||||
assert!(sel.track.is_some());
|
|
||||||
assert_eq!(sel.track.as_ref().unwrap().index, 1);
|
|
||||||
|
|
||||||
sel.increment(&vec![]);
|
|
||||||
assert_eq!(sel.index, std::usize::MAX);
|
|
||||||
assert!(sel.track.is_some());
|
|
||||||
assert_eq!(sel.track.as_ref().unwrap().index, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_artist_selection() {
|
|
||||||
let artists = &COLLECTION;
|
|
||||||
assert!(artists.len() > 1);
|
|
||||||
|
|
||||||
let empty = ArtistSelection::initialise(&vec![]);
|
|
||||||
assert!(empty.is_none());
|
|
||||||
|
|
||||||
let sel = ArtistSelection::initialise(artists);
|
|
||||||
assert!(sel.is_some());
|
|
||||||
|
|
||||||
let mut sel = sel.unwrap();
|
|
||||||
assert_eq!(sel.index, 0);
|
|
||||||
assert!(sel.album.is_some());
|
|
||||||
assert_eq!(sel.album.as_ref().unwrap().index, 0);
|
|
||||||
|
|
||||||
sel.album
|
|
||||||
.as_mut()
|
|
||||||
.unwrap()
|
|
||||||
.increment(&artists[sel.index].albums);
|
|
||||||
assert_eq!(sel.index, 0);
|
|
||||||
assert!(sel.album.is_some());
|
|
||||||
assert_eq!(sel.album.as_ref().unwrap().index, 1);
|
|
||||||
|
|
||||||
// Verify that decrement that doesn't change index does not reset album.
|
|
||||||
sel.decrement(artists);
|
|
||||||
assert_eq!(sel.index, 0);
|
|
||||||
assert!(sel.album.is_some());
|
|
||||||
assert_eq!(sel.album.as_ref().unwrap().index, 1);
|
|
||||||
|
|
||||||
sel.increment(artists);
|
|
||||||
assert_eq!(sel.index, 1);
|
|
||||||
assert!(sel.album.is_some());
|
|
||||||
assert_eq!(sel.album.as_ref().unwrap().index, 0);
|
|
||||||
|
|
||||||
sel.decrement(artists);
|
|
||||||
assert_eq!(sel.index, 0);
|
|
||||||
assert!(sel.album.is_some());
|
|
||||||
assert_eq!(sel.album.as_ref().unwrap().index, 0);
|
|
||||||
|
|
||||||
for _ in 0..(artists.len() + 5) {
|
|
||||||
sel.increment(artists);
|
|
||||||
}
|
|
||||||
assert_eq!(sel.index, artists.len() - 1);
|
|
||||||
assert!(sel.album.is_some());
|
|
||||||
assert_eq!(sel.album.as_ref().unwrap().index, 0);
|
|
||||||
|
|
||||||
sel.album
|
|
||||||
.as_mut()
|
|
||||||
.unwrap()
|
|
||||||
.increment(&artists[sel.index].albums);
|
|
||||||
assert_eq!(sel.index, artists.len() - 1);
|
|
||||||
assert!(sel.album.is_some());
|
|
||||||
assert_eq!(sel.album.as_ref().unwrap().index, 1);
|
|
||||||
|
|
||||||
// Verify that increment that doesn't change index does not reset album.
|
|
||||||
sel.increment(artists);
|
|
||||||
assert_eq!(sel.index, artists.len() - 1);
|
|
||||||
assert!(sel.album.is_some());
|
|
||||||
assert_eq!(sel.album.as_ref().unwrap().index, 1);
|
|
||||||
|
|
||||||
// Artifical test case to verify upper limit.
|
|
||||||
let mut sel = ArtistSelection {
|
|
||||||
index: std::usize::MAX,
|
|
||||||
album: Some(AlbumSelection {
|
|
||||||
index: 1,
|
|
||||||
track: None,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
assert_eq!(sel.index, std::usize::MAX);
|
|
||||||
assert!(sel.album.is_some());
|
|
||||||
assert_eq!(sel.album.as_ref().unwrap().index, 1);
|
|
||||||
|
|
||||||
sel.increment(&vec![]);
|
|
||||||
assert_eq!(sel.index, std::usize::MAX);
|
|
||||||
assert!(sel.album.is_some());
|
|
||||||
assert_eq!(sel.album.as_ref().unwrap().index, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn app_running() {
|
|
||||||
let mut collection_manager = MockCollectionManager::new();
|
|
||||||
|
|
||||||
collection_manager
|
|
||||||
.expect_rescan_library()
|
|
||||||
.times(1)
|
|
||||||
.return_once(|| Ok(()));
|
|
||||||
collection_manager
|
|
||||||
.expect_get_collection()
|
|
||||||
.return_const(COLLECTION.to_owned());
|
|
||||||
|
|
||||||
let mut app = TuiApp::new(collection_manager).unwrap();
|
|
||||||
assert!(app.is_running());
|
|
||||||
|
|
||||||
app.quit();
|
|
||||||
assert!(!app.is_running());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn app_modifiers() {
|
|
||||||
let mut collection_manager = MockCollectionManager::new();
|
|
||||||
|
|
||||||
collection_manager
|
|
||||||
.expect_rescan_library()
|
|
||||||
.times(1)
|
|
||||||
.return_once(|| Ok(()));
|
|
||||||
collection_manager
|
|
||||||
.expect_get_collection()
|
|
||||||
.return_const(COLLECTION.to_owned());
|
|
||||||
|
|
||||||
let mut app = TuiApp::new(collection_manager).unwrap();
|
|
||||||
assert!(app.is_running());
|
|
||||||
|
|
||||||
assert!(!app.get_artist_ids().is_empty());
|
|
||||||
assert!(!app.get_album_ids().is_empty());
|
|
||||||
assert!(!app.get_track_ids().is_empty());
|
|
||||||
|
|
||||||
assert_eq!(app.get_active_category(), Category::Artist);
|
|
||||||
assert_eq!(app.selected_artist(), Some(0));
|
|
||||||
assert_eq!(app.selected_album(), Some(0));
|
|
||||||
assert_eq!(app.selected_track(), Some(0));
|
|
||||||
|
|
||||||
app.increment_selection();
|
|
||||||
assert_eq!(app.get_active_category(), Category::Artist);
|
|
||||||
assert_eq!(app.selected_artist(), Some(1));
|
|
||||||
assert_eq!(app.selected_album(), Some(0));
|
|
||||||
assert_eq!(app.selected_track(), Some(0));
|
|
||||||
|
|
||||||
app.increment_category();
|
|
||||||
assert_eq!(app.get_active_category(), Category::Album);
|
|
||||||
assert_eq!(app.selected_artist(), Some(1));
|
|
||||||
assert_eq!(app.selected_album(), Some(0));
|
|
||||||
assert_eq!(app.selected_track(), Some(0));
|
|
||||||
|
|
||||||
app.increment_selection();
|
|
||||||
assert_eq!(app.get_active_category(), Category::Album);
|
|
||||||
assert_eq!(app.selected_artist(), Some(1));
|
|
||||||
assert_eq!(app.selected_album(), Some(1));
|
|
||||||
assert_eq!(app.selected_track(), Some(0));
|
|
||||||
|
|
||||||
app.increment_category();
|
|
||||||
assert_eq!(app.get_active_category(), Category::Track);
|
|
||||||
assert_eq!(app.selected_artist(), Some(1));
|
|
||||||
assert_eq!(app.selected_album(), Some(1));
|
|
||||||
assert_eq!(app.selected_track(), Some(0));
|
|
||||||
|
|
||||||
app.increment_selection();
|
|
||||||
assert_eq!(app.get_active_category(), Category::Track);
|
|
||||||
assert_eq!(app.selected_artist(), Some(1));
|
|
||||||
assert_eq!(app.selected_album(), Some(1));
|
|
||||||
assert_eq!(app.selected_track(), Some(1));
|
|
||||||
|
|
||||||
app.increment_category();
|
|
||||||
assert_eq!(app.get_active_category(), Category::Track);
|
|
||||||
assert_eq!(app.selected_artist(), Some(1));
|
|
||||||
assert_eq!(app.selected_album(), Some(1));
|
|
||||||
assert_eq!(app.selected_track(), Some(1));
|
|
||||||
|
|
||||||
app.decrement_selection();
|
|
||||||
assert_eq!(app.get_active_category(), Category::Track);
|
|
||||||
assert_eq!(app.selected_artist(), Some(1));
|
|
||||||
assert_eq!(app.selected_album(), Some(1));
|
|
||||||
assert_eq!(app.selected_track(), Some(0));
|
|
||||||
|
|
||||||
app.increment_selection();
|
|
||||||
app.decrement_category();
|
|
||||||
assert_eq!(app.get_active_category(), Category::Album);
|
|
||||||
assert_eq!(app.selected_artist(), Some(1));
|
|
||||||
assert_eq!(app.selected_album(), Some(1));
|
|
||||||
assert_eq!(app.selected_track(), Some(1));
|
|
||||||
|
|
||||||
app.decrement_selection();
|
|
||||||
assert_eq!(app.get_active_category(), Category::Album);
|
|
||||||
assert_eq!(app.selected_artist(), Some(1));
|
|
||||||
assert_eq!(app.selected_album(), Some(0));
|
|
||||||
assert_eq!(app.selected_track(), Some(0));
|
|
||||||
|
|
||||||
app.increment_selection();
|
|
||||||
app.decrement_category();
|
|
||||||
assert_eq!(app.get_active_category(), Category::Artist);
|
|
||||||
assert_eq!(app.selected_artist(), Some(1));
|
|
||||||
assert_eq!(app.selected_album(), Some(1));
|
|
||||||
assert_eq!(app.selected_track(), Some(0));
|
|
||||||
|
|
||||||
app.decrement_selection();
|
|
||||||
assert_eq!(app.get_active_category(), Category::Artist);
|
|
||||||
assert_eq!(app.selected_artist(), Some(0));
|
|
||||||
assert_eq!(app.selected_album(), Some(0));
|
|
||||||
assert_eq!(app.selected_track(), Some(0));
|
|
||||||
|
|
||||||
app.increment_category();
|
|
||||||
app.increment_selection();
|
|
||||||
app.decrement_category();
|
|
||||||
app.decrement_selection();
|
|
||||||
app.decrement_category();
|
|
||||||
assert_eq!(app.get_active_category(), Category::Artist);
|
|
||||||
assert_eq!(app.selected_artist(), Some(0));
|
|
||||||
assert_eq!(app.selected_album(), Some(1));
|
|
||||||
assert_eq!(app.selected_track(), Some(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn app_no_tracks() {
|
|
||||||
let mut collection_manager = MockCollectionManager::new();
|
|
||||||
let mut collection = COLLECTION.to_owned();
|
|
||||||
collection[0].albums[0].tracks = vec![];
|
|
||||||
|
|
||||||
collection_manager
|
|
||||||
.expect_rescan_library()
|
|
||||||
.times(1)
|
|
||||||
.return_once(|| Ok(()));
|
|
||||||
collection_manager
|
|
||||||
.expect_get_collection()
|
|
||||||
.return_const(collection);
|
|
||||||
|
|
||||||
let mut app = TuiApp::new(collection_manager).unwrap();
|
|
||||||
assert!(app.is_running());
|
|
||||||
|
|
||||||
assert!(!app.get_artist_ids().is_empty());
|
|
||||||
assert!(!app.get_album_ids().is_empty());
|
|
||||||
assert!(app.get_track_ids().is_empty());
|
|
||||||
|
|
||||||
assert_eq!(app.get_active_category(), Category::Artist);
|
|
||||||
assert_eq!(app.selected_artist(), Some(0));
|
|
||||||
assert_eq!(app.selected_album(), Some(0));
|
|
||||||
assert_eq!(app.selected_track(), None);
|
|
||||||
|
|
||||||
app.increment_category();
|
|
||||||
app.increment_category();
|
|
||||||
|
|
||||||
app.increment_selection();
|
|
||||||
assert_eq!(app.get_active_category(), Category::Track);
|
|
||||||
assert_eq!(app.selected_artist(), Some(0));
|
|
||||||
assert_eq!(app.selected_album(), Some(0));
|
|
||||||
assert_eq!(app.selected_track(), None);
|
|
||||||
|
|
||||||
app.decrement_selection();
|
|
||||||
assert_eq!(app.get_active_category(), Category::Track);
|
|
||||||
assert_eq!(app.selected_artist(), Some(0));
|
|
||||||
assert_eq!(app.selected_album(), Some(0));
|
|
||||||
assert_eq!(app.selected_track(), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn app_no_albums() {
|
|
||||||
let mut collection_manager = MockCollectionManager::new();
|
|
||||||
let mut collection = COLLECTION.to_owned();
|
|
||||||
collection[0].albums = vec![];
|
|
||||||
|
|
||||||
collection_manager
|
|
||||||
.expect_rescan_library()
|
|
||||||
.times(1)
|
|
||||||
.return_once(|| Ok(()));
|
|
||||||
collection_manager
|
|
||||||
.expect_get_collection()
|
|
||||||
.return_const(collection);
|
|
||||||
|
|
||||||
let mut app = TuiApp::new(collection_manager).unwrap();
|
|
||||||
assert!(app.is_running());
|
|
||||||
|
|
||||||
assert!(!app.get_artist_ids().is_empty());
|
|
||||||
assert!(app.get_album_ids().is_empty());
|
|
||||||
assert!(app.get_track_ids().is_empty());
|
|
||||||
|
|
||||||
assert_eq!(app.get_active_category(), Category::Artist);
|
|
||||||
assert_eq!(app.selected_artist(), Some(0));
|
|
||||||
assert_eq!(app.selected_album(), None);
|
|
||||||
assert_eq!(app.selected_track(), None);
|
|
||||||
|
|
||||||
app.increment_category();
|
|
||||||
|
|
||||||
app.increment_selection();
|
|
||||||
assert_eq!(app.get_active_category(), Category::Album);
|
|
||||||
assert_eq!(app.selected_artist(), Some(0));
|
|
||||||
assert_eq!(app.selected_album(), None);
|
|
||||||
assert_eq!(app.selected_track(), None);
|
|
||||||
|
|
||||||
app.decrement_selection();
|
|
||||||
assert_eq!(app.get_active_category(), Category::Album);
|
|
||||||
assert_eq!(app.selected_artist(), Some(0));
|
|
||||||
assert_eq!(app.selected_album(), None);
|
|
||||||
assert_eq!(app.selected_track(), None);
|
|
||||||
|
|
||||||
app.increment_category();
|
|
||||||
|
|
||||||
app.increment_selection();
|
|
||||||
assert_eq!(app.get_active_category(), Category::Track);
|
|
||||||
assert_eq!(app.selected_artist(), Some(0));
|
|
||||||
assert_eq!(app.selected_album(), None);
|
|
||||||
assert_eq!(app.selected_track(), None);
|
|
||||||
|
|
||||||
app.decrement_selection();
|
|
||||||
assert_eq!(app.get_active_category(), Category::Track);
|
|
||||||
assert_eq!(app.selected_artist(), Some(0));
|
|
||||||
assert_eq!(app.selected_album(), None);
|
|
||||||
assert_eq!(app.selected_track(), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn app_no_artists() {
|
|
||||||
let mut collection_manager = MockCollectionManager::new();
|
|
||||||
let collection = vec![];
|
|
||||||
|
|
||||||
collection_manager
|
|
||||||
.expect_rescan_library()
|
|
||||||
.times(1)
|
|
||||||
.return_once(|| Ok(()));
|
|
||||||
collection_manager
|
|
||||||
.expect_get_collection()
|
|
||||||
.return_const(collection);
|
|
||||||
|
|
||||||
let mut app = TuiApp::new(collection_manager).unwrap();
|
|
||||||
assert!(app.is_running());
|
|
||||||
|
|
||||||
assert!(app.get_artist_ids().is_empty());
|
|
||||||
assert!(app.get_album_ids().is_empty());
|
|
||||||
assert!(app.get_track_ids().is_empty());
|
|
||||||
|
|
||||||
assert_eq!(app.get_active_category(), Category::Artist);
|
|
||||||
assert_eq!(app.selected_artist(), None);
|
|
||||||
assert_eq!(app.selected_album(), None);
|
|
||||||
assert_eq!(app.selected_track(), None);
|
|
||||||
|
|
||||||
app.increment_selection();
|
|
||||||
assert_eq!(app.get_active_category(), Category::Artist);
|
|
||||||
assert_eq!(app.selected_artist(), None);
|
|
||||||
assert_eq!(app.selected_album(), None);
|
|
||||||
assert_eq!(app.selected_track(), None);
|
|
||||||
|
|
||||||
app.decrement_selection();
|
|
||||||
assert_eq!(app.get_active_category(), Category::Artist);
|
|
||||||
assert_eq!(app.selected_artist(), None);
|
|
||||||
assert_eq!(app.selected_album(), None);
|
|
||||||
assert_eq!(app.selected_track(), None);
|
|
||||||
|
|
||||||
app.increment_category();
|
|
||||||
|
|
||||||
app.increment_selection();
|
|
||||||
assert_eq!(app.get_active_category(), Category::Album);
|
|
||||||
assert_eq!(app.selected_artist(), None);
|
|
||||||
assert_eq!(app.selected_album(), None);
|
|
||||||
assert_eq!(app.selected_track(), None);
|
|
||||||
|
|
||||||
app.decrement_selection();
|
|
||||||
assert_eq!(app.get_active_category(), Category::Album);
|
|
||||||
assert_eq!(app.selected_artist(), None);
|
|
||||||
assert_eq!(app.selected_album(), None);
|
|
||||||
assert_eq!(app.selected_track(), None);
|
|
||||||
|
|
||||||
app.increment_category();
|
|
||||||
|
|
||||||
app.increment_selection();
|
|
||||||
assert_eq!(app.get_active_category(), Category::Track);
|
|
||||||
assert_eq!(app.selected_artist(), None);
|
|
||||||
assert_eq!(app.selected_album(), None);
|
|
||||||
assert_eq!(app.selected_track(), None);
|
|
||||||
|
|
||||||
app.decrement_selection();
|
|
||||||
assert_eq!(app.get_active_category(), Category::Track);
|
|
||||||
assert_eq!(app.selected_artist(), None);
|
|
||||||
assert_eq!(app.selected_album(), None);
|
|
||||||
assert_eq!(app.selected_track(), None);
|
|
||||||
}
|
|
||||||
}
|
|
@ -4,17 +4,17 @@ use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
|||||||
use mockall::automock;
|
use mockall::automock;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
app::App,
|
|
||||||
event::{Event, EventError, EventReceiver},
|
event::{Event, EventError, EventReceiver},
|
||||||
|
ui::Ui,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg_attr(test, automock)]
|
#[cfg_attr(test, automock)]
|
||||||
pub trait EventHandler<APP> {
|
pub trait EventHandler<UI> {
|
||||||
fn handle_next_event(&self, app: &mut APP) -> Result<(), EventError>;
|
fn handle_next_event(&self, ui: &mut UI) -> Result<(), EventError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
trait EventHandlerPrivate<APP> {
|
trait EventHandlerPrivate<UI> {
|
||||||
fn handle_key_event(app: &mut APP, key_event: KeyEvent);
|
fn handle_key_event(ui: &mut UI, key_event: KeyEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TuiEventHandler {
|
pub struct TuiEventHandler {
|
||||||
@ -28,10 +28,10 @@ impl TuiEventHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<APP: App> EventHandler<APP> for TuiEventHandler {
|
impl<UI: Ui> EventHandler<UI> for TuiEventHandler {
|
||||||
fn handle_next_event(&self, app: &mut APP) -> Result<(), EventError> {
|
fn handle_next_event(&self, ui: &mut UI) -> Result<(), EventError> {
|
||||||
match self.events.recv()? {
|
match self.events.recv()? {
|
||||||
Event::Key(key_event) => Self::handle_key_event(app, key_event),
|
Event::Key(key_event) => Self::handle_key_event(ui, key_event),
|
||||||
Event::Mouse(_) => {}
|
Event::Mouse(_) => {}
|
||||||
Event::Resize(_, _) => {}
|
Event::Resize(_, _) => {}
|
||||||
};
|
};
|
||||||
@ -39,32 +39,32 @@ impl<APP: App> EventHandler<APP> for TuiEventHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<APP: App> EventHandlerPrivate<APP> for TuiEventHandler {
|
impl<UI: Ui> EventHandlerPrivate<UI> for TuiEventHandler {
|
||||||
fn handle_key_event(app: &mut APP, key_event: KeyEvent) {
|
fn handle_key_event(ui: &mut UI, key_event: KeyEvent) {
|
||||||
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') => {
|
||||||
app.quit();
|
ui.quit();
|
||||||
}
|
}
|
||||||
// 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 {
|
||||||
app.quit();
|
ui.quit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Category change.
|
// Category change.
|
||||||
KeyCode::Left => {
|
KeyCode::Left => {
|
||||||
app.decrement_category();
|
ui.decrement_category();
|
||||||
}
|
}
|
||||||
KeyCode::Right => {
|
KeyCode::Right => {
|
||||||
app.increment_category();
|
ui.increment_category();
|
||||||
}
|
}
|
||||||
// Selection change.
|
// Selection change.
|
||||||
KeyCode::Up => {
|
KeyCode::Up => {
|
||||||
app.decrement_selection();
|
ui.decrement_selection();
|
||||||
}
|
}
|
||||||
KeyCode::Down => {
|
KeyCode::Down => {
|
||||||
app.increment_selection();
|
ui.increment_selection();
|
||||||
}
|
}
|
||||||
// Other keys.
|
// Other keys.
|
||||||
_ => {}
|
_ => {}
|
||||||
|
@ -6,13 +6,11 @@ use ratatui::Terminal;
|
|||||||
use std::io;
|
use std::io;
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
pub mod app;
|
|
||||||
pub mod event;
|
pub mod event;
|
||||||
pub mod handler;
|
pub mod handler;
|
||||||
pub mod listener;
|
pub mod listener;
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
|
|
||||||
use self::app::App;
|
|
||||||
use self::event::EventError;
|
use self::event::EventError;
|
||||||
use self::handler::EventHandler;
|
use self::handler::EventHandler;
|
||||||
use self::listener::EventListener;
|
use self::listener::EventListener;
|
||||||
@ -44,12 +42,12 @@ impl From<EventError> for Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Tui<B: Backend, APP> {
|
pub struct Tui<B: Backend, UI> {
|
||||||
terminal: Terminal<B>,
|
terminal: Terminal<B>,
|
||||||
_phantom: PhantomData<APP>,
|
_phantom: PhantomData<UI>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<B: Backend, APP: App> Tui<B, APP> {
|
impl<B: Backend, UI: Ui> Tui<B, UI> {
|
||||||
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()?;
|
||||||
@ -66,15 +64,10 @@ impl<B: Backend, APP: App> Tui<B, APP> {
|
|||||||
self.exit();
|
self.exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main_loop(
|
fn main_loop(&mut self, mut ui: UI, handler: impl EventHandler<UI>) -> Result<(), Error> {
|
||||||
&mut self,
|
while ui.is_running() {
|
||||||
mut app: APP,
|
self.terminal.draw(|frame| ui.render(frame))?;
|
||||||
ui: Ui<APP>,
|
handler.handle_next_event(&mut ui)?;
|
||||||
handler: impl EventHandler<APP>,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
while app.is_running() {
|
|
||||||
self.terminal.draw(|frame| ui.render(&app, frame))?;
|
|
||||||
handler.handle_next_event(&mut app)?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -82,9 +75,8 @@ impl<B: Backend, APP: App> Tui<B, APP> {
|
|||||||
|
|
||||||
fn main(
|
fn main(
|
||||||
term: Terminal<B>,
|
term: Terminal<B>,
|
||||||
app: APP,
|
ui: UI,
|
||||||
ui: Ui<APP>,
|
handler: impl EventHandler<UI>,
|
||||||
handler: impl EventHandler<APP>,
|
|
||||||
listener: impl EventListener,
|
listener: impl EventListener,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let mut tui = Tui {
|
let mut tui = Tui {
|
||||||
@ -95,7 +87,7 @@ impl<B: Backend, APP: App> Tui<B, APP> {
|
|||||||
tui.init()?;
|
tui.init()?;
|
||||||
|
|
||||||
let listener_handle = listener.spawn();
|
let listener_handle = listener.spawn();
|
||||||
let result = tui.main_loop(app, ui, handler);
|
let result = tui.main_loop(ui, handler);
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
@ -142,13 +134,12 @@ impl<B: Backend, APP: App> Tui<B, APP> {
|
|||||||
|
|
||||||
pub fn run(
|
pub fn run(
|
||||||
term: Terminal<B>,
|
term: Terminal<B>,
|
||||||
app: APP,
|
ui: UI,
|
||||||
ui: Ui<APP>,
|
handler: impl EventHandler<UI>,
|
||||||
handler: impl EventHandler<APP>,
|
|
||||||
listener: impl EventListener,
|
listener: impl EventListener,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
Self::enable()?;
|
Self::enable()?;
|
||||||
let result = Self::main(term, app, ui, handler, listener);
|
let result = Self::main(term, ui, handler, listener);
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
Self::disable()?;
|
Self::disable()?;
|
||||||
@ -175,11 +166,10 @@ mod tests {
|
|||||||
use crate::tests::{MockCollectionManager, COLLECTION};
|
use crate::tests::{MockCollectionManager, COLLECTION};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
app::{App, TuiApp},
|
|
||||||
event::EventError,
|
event::EventError,
|
||||||
handler::MockEventHandler,
|
handler::MockEventHandler,
|
||||||
listener::MockEventListener,
|
listener::MockEventListener,
|
||||||
ui::Ui,
|
ui::{MhUi, Ui},
|
||||||
Error, Tui,
|
Error, Tui,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -188,7 +178,7 @@ mod tests {
|
|||||||
Terminal::new(backend).unwrap()
|
Terminal::new(backend).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn app(collection: Collection) -> TuiApp<MockCollectionManager> {
|
pub fn ui(collection: Collection) -> MhUi<MockCollectionManager> {
|
||||||
let mut collection_manager = MockCollectionManager::new();
|
let mut collection_manager = MockCollectionManager::new();
|
||||||
|
|
||||||
collection_manager
|
collection_manager
|
||||||
@ -198,7 +188,7 @@ mod tests {
|
|||||||
.expect_get_collection()
|
.expect_get_collection()
|
||||||
.return_const(collection);
|
.return_const(collection);
|
||||||
|
|
||||||
TuiApp::new(collection_manager).unwrap()
|
MhUi::new(collection_manager).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn listener() -> MockEventListener {
|
fn listener() -> MockEventListener {
|
||||||
@ -212,35 +202,33 @@ mod tests {
|
|||||||
listener
|
listener
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handler() -> MockEventHandler<TuiApp<MockCollectionManager>> {
|
fn handler() -> MockEventHandler<MhUi<MockCollectionManager>> {
|
||||||
let mut handler = MockEventHandler::new();
|
let mut handler = MockEventHandler::new();
|
||||||
handler.expect_handle_next_event().return_once(
|
handler
|
||||||
|app: &mut TuiApp<MockCollectionManager>| {
|
.expect_handle_next_event()
|
||||||
app.quit();
|
.return_once(|ui: &mut MhUi<MockCollectionManager>| {
|
||||||
|
ui.quit();
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
});
|
||||||
);
|
|
||||||
handler
|
handler
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn run() {
|
fn run() {
|
||||||
let terminal = terminal();
|
let terminal = terminal();
|
||||||
let app = app(COLLECTION.to_owned());
|
let ui = ui(COLLECTION.to_owned());
|
||||||
let ui = Ui::new();
|
|
||||||
|
|
||||||
let listener = listener();
|
let listener = listener();
|
||||||
let handler = handler();
|
let handler = handler();
|
||||||
|
|
||||||
let result = Tui::main(terminal, app, ui, handler, listener);
|
let result = Tui::main(terminal, 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 app = app(COLLECTION.to_owned());
|
let ui = ui(COLLECTION.to_owned());
|
||||||
let ui = Ui::new();
|
|
||||||
|
|
||||||
let listener = listener();
|
let listener = listener();
|
||||||
|
|
||||||
@ -249,7 +237,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, app, ui, handler, listener);
|
let result = Tui::main(terminal, ui, handler, listener);
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.unwrap_err(),
|
result.unwrap_err(),
|
||||||
@ -260,8 +248,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn listener_error() {
|
fn listener_error() {
|
||||||
let terminal = terminal();
|
let terminal = terminal();
|
||||||
let app = app(COLLECTION.to_owned());
|
let ui = ui(COLLECTION.to_owned());
|
||||||
let ui = Ui::new();
|
|
||||||
|
|
||||||
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);
|
||||||
@ -275,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, app, ui, handler, listener);
|
let result = Tui::main(terminal, 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"));
|
||||||
@ -285,8 +272,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn listener_panic() {
|
fn listener_panic() {
|
||||||
let terminal = terminal();
|
let terminal = terminal();
|
||||||
let app = app(COLLECTION.to_owned());
|
let ui = ui(COLLECTION.to_owned());
|
||||||
let ui = Ui::new();
|
|
||||||
|
|
||||||
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() {}
|
||||||
@ -299,7 +285,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, app, ui, handler, listener);
|
let result = Tui::main(terminal, 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);
|
||||||
}
|
}
|
||||||
|
976
src/tui/ui.rs
976
src/tui/ui.rs
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user