Version 1 of solution
This commit is contained in:
parent
0545e5324e
commit
e9981a4bc1
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() {
|
||||||
|
1316
src/tui/app.rs
1316
src/tui/app.rs
File diff suppressed because it is too large
Load Diff
@ -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.
|
||||||
_ => {}
|
_ => {}
|
||||||
|
277
src/tui/mod.rs
277
src/tui/mod.rs
@ -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()?;
|
||||||
@ -165,155 +156,151 @@ impl<B: Backend, APP: App> Tui<B, APP> {
|
|||||||
// GRCOV_EXCL_STOP
|
// GRCOV_EXCL_STOP
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
// #[cfg(test)]
|
||||||
mod tests {
|
// mod tests {
|
||||||
use std::{io, thread};
|
// use std::{io, thread};
|
||||||
|
|
||||||
use musichoard::collection::{self, Collection};
|
// use musichoard::collection::{self, Collection};
|
||||||
use ratatui::{backend::TestBackend, Terminal};
|
// use ratatui::{backend::TestBackend, Terminal};
|
||||||
|
|
||||||
use crate::tests::{MockCollectionManager, COLLECTION};
|
// use crate::tests::{MockCollectionManager, COLLECTION};
|
||||||
|
|
||||||
use super::{
|
// use super::{
|
||||||
app::{App, TuiApp},
|
// app::TuiApp, event::EventError, handler::MockEventHandler, listener::MockEventListener,
|
||||||
event::EventError,
|
// ui::Ui, Error, Tui,
|
||||||
handler::MockEventHandler,
|
// };
|
||||||
listener::MockEventListener,
|
|
||||||
ui::Ui,
|
|
||||||
Error, Tui,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn terminal() -> Terminal<TestBackend> {
|
// pub fn terminal() -> Terminal<TestBackend> {
|
||||||
let backend = TestBackend::new(150, 30);
|
// let backend = TestBackend::new(150, 30);
|
||||||
Terminal::new(backend).unwrap()
|
// Terminal::new(backend).unwrap()
|
||||||
}
|
// }
|
||||||
|
|
||||||
pub fn app(collection: Collection) -> TuiApp<MockCollectionManager> {
|
// pub fn app(collection: Collection) -> TuiApp<MockCollectionManager> {
|
||||||
let mut collection_manager = MockCollectionManager::new();
|
// let mut collection_manager = MockCollectionManager::new();
|
||||||
|
|
||||||
collection_manager
|
// collection_manager
|
||||||
.expect_rescan_library()
|
// .expect_rescan_library()
|
||||||
.returning(|| Ok(()));
|
// .returning(|| Ok(()));
|
||||||
collection_manager
|
// collection_manager
|
||||||
.expect_get_collection()
|
// .expect_get_collection()
|
||||||
.return_const(collection);
|
// .return_const(collection);
|
||||||
|
|
||||||
TuiApp::new(collection_manager).unwrap()
|
// TuiApp::new(collection_manager).unwrap()
|
||||||
}
|
// }
|
||||||
|
|
||||||
fn listener() -> MockEventListener {
|
// fn listener() -> MockEventListener {
|
||||||
let mut listener = MockEventListener::new();
|
// let mut listener = MockEventListener::new();
|
||||||
listener.expect_spawn().return_once(|| {
|
// listener.expect_spawn().return_once(|| {
|
||||||
thread::spawn(|| {
|
// thread::spawn(|| {
|
||||||
thread::park();
|
// thread::park();
|
||||||
return EventError::Io(io::Error::new(io::ErrorKind::Interrupted, "unparked"));
|
// return EventError::Io(io::Error::new(io::ErrorKind::Interrupted, "unparked"));
|
||||||
})
|
// })
|
||||||
});
|
// });
|
||||||
listener
|
// listener
|
||||||
}
|
// }
|
||||||
|
|
||||||
fn handler() -> MockEventHandler<TuiApp<MockCollectionManager>> {
|
// fn handler() -> MockEventHandler<TuiApp<MockCollectionManager>> {
|
||||||
let mut handler = MockEventHandler::new();
|
// let mut handler = MockEventHandler::new();
|
||||||
handler.expect_handle_next_event().return_once(
|
// handler.expect_handle_next_event().return_once(
|
||||||
|app: &mut TuiApp<MockCollectionManager>| {
|
// |app: &mut TuiApp<MockCollectionManager>| {
|
||||||
app.quit();
|
// app.quit();
|
||||||
Ok(())
|
// Ok(())
|
||||||
},
|
// },
|
||||||
);
|
// );
|
||||||
handler
|
// handler
|
||||||
}
|
// }
|
||||||
|
|
||||||
#[test]
|
// #[test]
|
||||||
fn run() {
|
// fn run() {
|
||||||
let terminal = terminal();
|
// let terminal = terminal();
|
||||||
let app = app(COLLECTION.to_owned());
|
// let app = app(COLLECTION.to_owned());
|
||||||
let ui = Ui::new();
|
// 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, 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 app = app(COLLECTION.to_owned());
|
// let app = app(COLLECTION.to_owned());
|
||||||
let ui = Ui::new();
|
// let ui = Ui::new();
|
||||||
|
|
||||||
let listener = listener();
|
// let listener = listener();
|
||||||
|
|
||||||
let mut handler = MockEventHandler::new();
|
// let mut handler = MockEventHandler::new();
|
||||||
handler
|
// handler
|
||||||
.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, app, ui, handler, listener);
|
||||||
assert!(result.is_err());
|
// assert!(result.is_err());
|
||||||
assert_eq!(
|
// assert_eq!(
|
||||||
result.unwrap_err(),
|
// result.unwrap_err(),
|
||||||
Error::Event(EventError::Recv.to_string())
|
// Error::Event(EventError::Recv.to_string())
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
#[test]
|
// #[test]
|
||||||
fn listener_error() {
|
// fn listener_error() {
|
||||||
let terminal = terminal();
|
// let terminal = terminal();
|
||||||
let app = app(COLLECTION.to_owned());
|
// let app = app(COLLECTION.to_owned());
|
||||||
let ui = Ui::new();
|
// 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);
|
||||||
while !listener_handle.is_finished() {}
|
// while !listener_handle.is_finished() {}
|
||||||
|
|
||||||
let mut listener = MockEventListener::new();
|
// let mut listener = MockEventListener::new();
|
||||||
listener.expect_spawn().return_once(|| listener_handle);
|
// listener.expect_spawn().return_once(|| listener_handle);
|
||||||
|
|
||||||
let mut handler = MockEventHandler::new();
|
// let mut handler = MockEventHandler::new();
|
||||||
handler
|
// handler
|
||||||
.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, 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"));
|
||||||
assert_eq!(result.unwrap_err(), Error::Event(error.to_string()));
|
// assert_eq!(result.unwrap_err(), Error::Event(error.to_string()));
|
||||||
}
|
// }
|
||||||
|
|
||||||
#[test]
|
// #[test]
|
||||||
fn listener_panic() {
|
// fn listener_panic() {
|
||||||
let terminal = terminal();
|
// let terminal = terminal();
|
||||||
let app = app(COLLECTION.to_owned());
|
// let app = app(COLLECTION.to_owned());
|
||||||
let ui = Ui::new();
|
// 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() {}
|
||||||
|
|
||||||
let mut listener = MockEventListener::new();
|
// let mut listener = MockEventListener::new();
|
||||||
listener.expect_spawn().return_once(|| listener_handle);
|
// listener.expect_spawn().return_once(|| listener_handle);
|
||||||
|
|
||||||
let mut handler = MockEventHandler::new();
|
// let mut handler = MockEventHandler::new();
|
||||||
handler
|
// handler
|
||||||
.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, 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);
|
||||||
}
|
// }
|
||||||
|
|
||||||
#[test]
|
// #[test]
|
||||||
fn errors() {
|
// fn errors() {
|
||||||
let collection_err: Error = collection::Error::DatabaseError(String::from("")).into();
|
// let collection_err: Error = collection::Error::DatabaseError(String::from("")).into();
|
||||||
let io_err: Error = io::Error::new(io::ErrorKind::Interrupted, "error").into();
|
// let io_err: Error = io::Error::new(io::ErrorKind::Interrupted, "error").into();
|
||||||
let event_err: Error = EventError::Recv.into();
|
// let event_err: Error = EventError::Recv.into();
|
||||||
let listener_err = Error::ListenerPanic;
|
// let listener_err = Error::ListenerPanic;
|
||||||
|
|
||||||
assert!(!format!("{:?}", collection_err).is_empty());
|
// assert!(!format!("{:?}", collection_err).is_empty());
|
||||||
assert!(!format!("{:?}", io_err).is_empty());
|
// assert!(!format!("{:?}", io_err).is_empty());
|
||||||
assert!(!format!("{:?}", event_err).is_empty());
|
// assert!(!format!("{:?}", event_err).is_empty());
|
||||||
assert!(!format!("{:?}", listener_err).is_empty());
|
// assert!(!format!("{:?}", listener_err).is_empty());
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
510
src/tui/ui.rs
510
src/tui/ui.rs
@ -1,6 +1,7 @@
|
|||||||
use std::marker::PhantomData;
|
use musichoard::{
|
||||||
|
collection::{Collection, CollectionManager},
|
||||||
use musichoard::TrackFormat;
|
Album, AlbumId, Artist, Track, TrackFormat,
|
||||||
|
};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::Backend,
|
backend::Backend,
|
||||||
layout::{Alignment, Rect},
|
layout::{Alignment, Rect},
|
||||||
@ -9,7 +10,276 @@ use ratatui::{
|
|||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::app::{App, Category};
|
use super::Error;
|
||||||
|
|
||||||
|
struct TrackSelection {
|
||||||
|
selection: ListState,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AlbumSelection {
|
||||||
|
selection: ListState,
|
||||||
|
track: TrackSelection,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ArtistSelection {
|
||||||
|
selection: ListState,
|
||||||
|
album: AlbumSelection,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TrackSelection {
|
||||||
|
fn initialise(tracks: Option<&[Track]>) -> Self {
|
||||||
|
let mut selection = ListState::default();
|
||||||
|
if let Some(tracks) = tracks {
|
||||||
|
selection.select(if !tracks.is_empty() { Some(0) } else { None });
|
||||||
|
} else {
|
||||||
|
selection.select(None);
|
||||||
|
};
|
||||||
|
TrackSelection { selection }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selection(&mut self) -> &mut ListState {
|
||||||
|
&mut self.selection
|
||||||
|
}
|
||||||
|
|
||||||
|
fn increment(&mut self, tracks: &[Track]) {
|
||||||
|
if let Some(index) = self.selection.selected() {
|
||||||
|
if let Some(result) = index.checked_add(1) {
|
||||||
|
if result < tracks.len() {
|
||||||
|
self.selection.select(Some(result));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrement(&mut self, _tracks: &[Track]) {
|
||||||
|
if let Some(index) = self.selection.selected() {
|
||||||
|
if let Some(result) = index.checked_sub(1) {
|
||||||
|
self.selection.select(Some(result));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AlbumSelection {
|
||||||
|
fn initialise(albums: Option<&[Album]>) -> Self {
|
||||||
|
let mut selection = ListState::default();
|
||||||
|
let track: TrackSelection;
|
||||||
|
if let Some(albums) = albums {
|
||||||
|
selection.select(if !albums.is_empty() { Some(0) } else { None });
|
||||||
|
track = TrackSelection::initialise(albums.get(0).map(|a| a.tracks.as_slice()));
|
||||||
|
} else {
|
||||||
|
selection.select(None);
|
||||||
|
track = TrackSelection::initialise(None);
|
||||||
|
}
|
||||||
|
AlbumSelection { selection, track }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selection(&mut self) -> &mut ListState {
|
||||||
|
&mut self.selection
|
||||||
|
}
|
||||||
|
|
||||||
|
fn track_selection(&mut self) -> &mut ListState {
|
||||||
|
self.track.selection()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn increment(&mut self, albums: &[Album]) {
|
||||||
|
if let Some(index) = self.selection.selected() {
|
||||||
|
if let Some(result) = index.checked_add(1) {
|
||||||
|
if result < albums.len() {
|
||||||
|
self.selection.select(Some(result));
|
||||||
|
self.track = TrackSelection::initialise(Some(&albums[result].tracks));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn increment_track(&mut self, albums: &[Album]) {
|
||||||
|
if let Some(index) = self.selection.selected() {
|
||||||
|
self.track.increment(&albums[index].tracks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrement(&mut self, albums: &[Album]) {
|
||||||
|
if let Some(index) = self.selection.selected() {
|
||||||
|
if let Some(result) = index.checked_sub(1) {
|
||||||
|
self.selection.select(Some(result));
|
||||||
|
self.track = TrackSelection::initialise(Some(&albums[result].tracks));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrement_track(&mut self, albums: &[Album]) {
|
||||||
|
if let Some(index) = self.selection.selected() {
|
||||||
|
self.track.decrement(&albums[index].tracks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ArtistSelection {
|
||||||
|
fn initialise(artists: Option<&[Artist]>) -> Self {
|
||||||
|
let mut selection = ListState::default();
|
||||||
|
let album: AlbumSelection;
|
||||||
|
if let Some(artists) = artists {
|
||||||
|
selection.select(if !artists.is_empty() { Some(0) } else { None });
|
||||||
|
album = AlbumSelection::initialise(artists.get(0).map(|a| a.albums.as_slice()));
|
||||||
|
} else {
|
||||||
|
selection.select(None);
|
||||||
|
album = AlbumSelection::initialise(None);
|
||||||
|
}
|
||||||
|
ArtistSelection { selection, album }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selection(&mut self) -> &mut ListState {
|
||||||
|
&mut self.selection
|
||||||
|
}
|
||||||
|
|
||||||
|
fn album_selection(&mut self) -> &mut ListState {
|
||||||
|
self.album.selection()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn track_selection(&mut self) -> &mut ListState {
|
||||||
|
self.album.track_selection()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn increment(&mut self, artists: &[Artist]) {
|
||||||
|
if let Some(index) = self.selection.selected() {
|
||||||
|
if let Some(result) = index.checked_add(1) {
|
||||||
|
if result < artists.len() {
|
||||||
|
self.selection.select(Some(result));
|
||||||
|
self.album = AlbumSelection::initialise(Some(&artists[result].albums));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn increment_album(&mut self, artists: &[Artist]) {
|
||||||
|
if let Some(index) = self.selection.selected() {
|
||||||
|
self.album.increment(&artists[index].albums);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn increment_track(&mut self, artists: &[Artist]) {
|
||||||
|
if let Some(index) = self.selection.selected() {
|
||||||
|
self.album.increment_track(&artists[index].albums);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrement(&mut self, artists: &[Artist]) {
|
||||||
|
if let Some(index) = self.selection.selected() {
|
||||||
|
if let Some(result) = index.checked_sub(1) {
|
||||||
|
self.selection.select(Some(result));
|
||||||
|
self.album = AlbumSelection::initialise(Some(&artists[result].albums));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrement_album(&mut self, artists: &[Artist]) {
|
||||||
|
if let Some(index) = self.selection.selected() {
|
||||||
|
self.album.decrement(&artists[index].albums);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrement_track(&mut self, artists: &[Artist]) {
|
||||||
|
if let Some(index) = self.selection.selected() {
|
||||||
|
self.album.decrement_track(&artists[index].albums);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum Category {
|
||||||
|
Artist,
|
||||||
|
Album,
|
||||||
|
Track,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Selection {
|
||||||
|
active: Category,
|
||||||
|
artist: ArtistSelection,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Selection {
|
||||||
|
fn new(artists: Option<&[Artist]>) -> Self {
|
||||||
|
Selection {
|
||||||
|
active: Category::Artist,
|
||||||
|
artist: ArtistSelection::initialise(artists),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn artist_selection(&mut self) -> &mut ListState {
|
||||||
|
self.artist.selection()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn album_selection(&mut self) -> &mut ListState {
|
||||||
|
self.artist.album_selection()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn track_selection(&mut self) -> &mut ListState {
|
||||||
|
self.artist.track_selection()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn increment_category(&mut self) {
|
||||||
|
self.active = match self.active {
|
||||||
|
Category::Artist => Category::Album,
|
||||||
|
Category::Album => Category::Track,
|
||||||
|
Category::Track => Category::Track,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrement_category(&mut self) {
|
||||||
|
self.active = match self.active {
|
||||||
|
Category::Artist => Category::Artist,
|
||||||
|
Category::Album => Category::Artist,
|
||||||
|
Category::Track => Category::Album,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn increment_selection(&mut self, collection: &Collection) {
|
||||||
|
match self.active {
|
||||||
|
Category::Artist => self.increment_artist(collection),
|
||||||
|
Category::Album => self.increment_album(collection),
|
||||||
|
Category::Track => self.increment_track(collection),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrement_selection(&mut self, collection: &Collection) {
|
||||||
|
match self.active {
|
||||||
|
Category::Artist => self.decrement_artist(collection),
|
||||||
|
Category::Album => self.decrement_album(collection),
|
||||||
|
Category::Track => self.decrement_track(collection),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn increment_artist(&mut self, artists: &[Artist]) {
|
||||||
|
self.artist.increment(artists);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrement_artist(&mut self, artists: &[Artist]) {
|
||||||
|
self.artist.decrement(artists);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn increment_album(&mut self, artists: &[Artist]) {
|
||||||
|
self.artist.increment_album(artists);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrement_album(&mut self, artists: &[Artist]) {
|
||||||
|
self.artist.decrement_album(artists);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn increment_track(&mut self, artists: &[Artist]) {
|
||||||
|
self.artist.increment_track(artists);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrement_track(&mut self, artists: &[Artist]) {
|
||||||
|
self.artist.decrement_track(artists);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MhUi<CM> {
|
||||||
|
collection_manager: CM,
|
||||||
|
selection: Selection,
|
||||||
|
running: bool,
|
||||||
|
}
|
||||||
|
|
||||||
struct ArtistArea {
|
struct ArtistArea {
|
||||||
list: Rect,
|
list: Rect,
|
||||||
@ -33,7 +303,7 @@ struct FrameAreas {
|
|||||||
|
|
||||||
struct SelectionList<'a> {
|
struct SelectionList<'a> {
|
||||||
list: List<'a>,
|
list: List<'a>,
|
||||||
state: ListState,
|
state: &'a mut ListState,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ArtistState<'a> {
|
struct ArtistState<'a> {
|
||||||
@ -53,21 +323,15 @@ struct TrackState<'a> {
|
|||||||
active: bool,
|
active: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AppState<'a> {
|
impl<CM: CollectionManager> MhUi<CM> {
|
||||||
artists: ArtistState<'a>,
|
pub fn new(mut collection_manager: CM) -> Result<Self, Error> {
|
||||||
albums: AlbumState<'a>,
|
collection_manager.rescan_library()?;
|
||||||
tracks: TrackState<'a>,
|
let selection = Selection::new(Some(collection_manager.get_collection()));
|
||||||
}
|
Ok(MhUi {
|
||||||
|
collection_manager,
|
||||||
pub struct Ui<APP> {
|
selection,
|
||||||
_phantom: PhantomData<APP>,
|
running: true,
|
||||||
}
|
})
|
||||||
|
|
||||||
impl<APP: App> Ui<APP> {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Ui {
|
|
||||||
_phantom: PhantomData,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn construct_areas(frame: Rect) -> FrameAreas {
|
fn construct_areas(frame: Rect) -> FrameAreas {
|
||||||
@ -127,21 +391,17 @@ impl<APP: App> Ui<APP> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn construct_artist_list(app: &APP) -> ArtistState {
|
fn construct_artist_state(&mut self) -> ArtistState {
|
||||||
let artists = app.get_artist_ids();
|
let artists = self.collection_manager.get_collection();
|
||||||
let list = List::new(
|
let list = List::new(
|
||||||
artists
|
artists
|
||||||
.iter()
|
.iter()
|
||||||
.map(|id| ListItem::new(id.name.as_str()))
|
.map(|a| ListItem::new(a.id.name.as_str()))
|
||||||
.collect::<Vec<ListItem>>(),
|
.collect::<Vec<ListItem>>(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let selected_artist = app.selected_artist();
|
let active = self.selection.active == Category::Artist;
|
||||||
|
let state = self.selection.artist_selection();
|
||||||
let mut state = ListState::default();
|
|
||||||
state.select(selected_artist);
|
|
||||||
|
|
||||||
let active = app.get_active_category() == Category::Artist;
|
|
||||||
|
|
||||||
ArtistState {
|
ArtistState {
|
||||||
list: SelectionList { list, state },
|
list: SelectionList { list, state },
|
||||||
@ -149,8 +409,18 @@ impl<APP: App> Ui<APP> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn construct_album_list(app: &APP) -> AlbumState {
|
fn construct_album_state(&mut self) -> AlbumState {
|
||||||
let albums = app.get_album_ids();
|
let albums: Vec<&AlbumId> =
|
||||||
|
if let Some(artist_index) = self.selection.artist.selection.selected() {
|
||||||
|
self.collection_manager.get_collection()[artist_index]
|
||||||
|
.albums
|
||||||
|
.iter()
|
||||||
|
.map(|a| &a.id)
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
|
||||||
let list = List::new(
|
let list = List::new(
|
||||||
albums
|
albums
|
||||||
.iter()
|
.iter()
|
||||||
@ -158,14 +428,10 @@ impl<APP: App> Ui<APP> {
|
|||||||
.collect::<Vec<ListItem>>(),
|
.collect::<Vec<ListItem>>(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let selected_album = app.selected_album();
|
let active = self.selection.active == Category::Album;
|
||||||
|
let state = self.selection.album_selection();
|
||||||
|
|
||||||
let mut state = ListState::default();
|
let album = state.selected().map(|i| albums[i]);
|
||||||
state.select(selected_album);
|
|
||||||
|
|
||||||
let active = app.get_active_category() == Category::Album;
|
|
||||||
|
|
||||||
let album = selected_album.map(|i| albums[i]);
|
|
||||||
let info = Paragraph::new(format!(
|
let info = Paragraph::new(format!(
|
||||||
"Title: {}\n\
|
"Title: {}\n\
|
||||||
Year: {}",
|
Year: {}",
|
||||||
@ -182,8 +448,21 @@ impl<APP: App> Ui<APP> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn construct_track_list(app: &APP) -> TrackState {
|
fn construct_track_state(&mut self) -> TrackState {
|
||||||
let tracks = app.get_track_ids();
|
let tracks: Vec<&Track> =
|
||||||
|
if let Some(artist_index) = self.selection.artist.selection.selected() {
|
||||||
|
if let Some(album_index) = self.selection.artist.album.selection.selected() {
|
||||||
|
self.collection_manager.get_collection()[artist_index].albums[album_index]
|
||||||
|
.tracks
|
||||||
|
.iter()
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
|
||||||
let list = List::new(
|
let list = List::new(
|
||||||
tracks
|
tracks
|
||||||
.iter()
|
.iter()
|
||||||
@ -191,14 +470,10 @@ impl<APP: App> Ui<APP> {
|
|||||||
.collect::<Vec<ListItem>>(),
|
.collect::<Vec<ListItem>>(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let selected_track = app.selected_track();
|
let active = self.selection.active == Category::Track;
|
||||||
|
let state = self.selection.track_selection();
|
||||||
|
|
||||||
let mut state = ListState::default();
|
let track = state.selected().map(|i| tracks[i]);
|
||||||
state.select(selected_track);
|
|
||||||
|
|
||||||
let active = app.get_active_category() == Category::Track;
|
|
||||||
|
|
||||||
let track = selected_track.map(|i| tracks[i]);
|
|
||||||
let info = Paragraph::new(format!(
|
let info = Paragraph::new(format!(
|
||||||
"Track: {}\n\
|
"Track: {}\n\
|
||||||
Title: {}\n\
|
Title: {}\n\
|
||||||
@ -226,14 +501,6 @@ impl<APP: App> Ui<APP> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn construct_app_state(app: &APP) -> AppState {
|
|
||||||
AppState {
|
|
||||||
artists: Self::construct_artist_list(app),
|
|
||||||
albums: Self::construct_album_list(app),
|
|
||||||
tracks: Self::construct_track_list(app),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
@ -292,78 +559,109 @@ impl<APP: App> Ui<APP> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_artist_column<B: Backend>(
|
fn render_artist_column<B: Backend>(&mut self, area: ArtistArea, frame: &mut Frame<'_, B>) {
|
||||||
state: ArtistState,
|
let state = self.construct_artist_state();
|
||||||
area: ArtistArea,
|
|
||||||
frame: &mut Frame<'_, B>,
|
|
||||||
) {
|
|
||||||
Self::render_list_widget("Artists", state.list, state.active, area.list, frame);
|
Self::render_list_widget("Artists", state.list, state.active, area.list, frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_album_column<B: Backend>(
|
fn render_album_column<B: Backend>(&mut self, area: AlbumArea, frame: &mut Frame<'_, B>) {
|
||||||
state: AlbumState,
|
let state = self.construct_album_state();
|
||||||
area: AlbumArea,
|
|
||||||
frame: &mut Frame<'_, B>,
|
|
||||||
) {
|
|
||||||
Self::render_list_widget("Albums", state.list, state.active, area.list, frame);
|
Self::render_list_widget("Albums", state.list, state.active, area.list, frame);
|
||||||
Self::render_info_widget("Album info", state.info, state.active, area.info, frame);
|
Self::render_info_widget("Album info", state.info, state.active, area.info, frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_track_column<B: Backend>(
|
fn render_track_column<B: Backend>(&mut self, area: TrackArea, frame: &mut Frame<'_, B>) {
|
||||||
state: TrackState,
|
let state = self.construct_track_state();
|
||||||
area: TrackArea,
|
|
||||||
frame: &mut Frame<'_, B>,
|
|
||||||
) {
|
|
||||||
Self::render_list_widget("Tracks", state.list, state.active, area.list, frame);
|
Self::render_list_widget("Tracks", state.list, state.active, area.list, frame);
|
||||||
Self::render_info_widget("Track info", state.info, state.active, area.info, frame);
|
Self::render_info_widget("Track info", state.info, state.active, area.info, frame);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn render<B: Backend>(&self, app: &APP, frame: &mut Frame<'_, B>) {
|
pub trait Ui {
|
||||||
|
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 render<B: Backend>(&mut self, frame: &mut Frame<'_, B>);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<CM: CollectionManager> Ui for MhUi<CM> {
|
||||||
|
fn is_running(&self) -> bool {
|
||||||
|
self.running
|
||||||
|
}
|
||||||
|
|
||||||
|
fn quit(&mut self) {
|
||||||
|
self.running = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn increment_category(&mut self) {
|
||||||
|
self.selection.increment_category();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrement_category(&mut self) {
|
||||||
|
self.selection.decrement_category();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn increment_selection(&mut self) {
|
||||||
|
self.selection
|
||||||
|
.increment_selection(self.collection_manager.get_collection());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrement_selection(&mut self) {
|
||||||
|
self.selection
|
||||||
|
.decrement_selection(self.collection_manager.get_collection());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render<B: Backend>(&mut self, frame: &mut Frame<'_, B>) {
|
||||||
let areas = Self::construct_areas(frame.size());
|
let areas = Self::construct_areas(frame.size());
|
||||||
let app_state = Self::construct_app_state(app);
|
|
||||||
|
|
||||||
Self::render_artist_column(app_state.artists, areas.artists, frame);
|
self.render_artist_column(areas.artists, frame);
|
||||||
Self::render_album_column(app_state.albums, areas.albums, frame);
|
self.render_album_column(areas.albums, frame);
|
||||||
Self::render_track_column(app_state.tracks, areas.tracks, frame);
|
self.render_track_column(areas.tracks, frame);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
// #[cfg(test)]
|
||||||
mod tests {
|
// mod tests {
|
||||||
// This is UI so the only sensible unit test is to run the code through various app states.
|
// // This is UI so the only sensible unit test is to run the code through various app states.
|
||||||
|
|
||||||
use crate::{
|
// use crate::{
|
||||||
tests::COLLECTION,
|
// tests::COLLECTION,
|
||||||
tui::{
|
// tui::{
|
||||||
app::App,
|
// app::App,
|
||||||
tests::{app, terminal},
|
// tests::{app, terminal},
|
||||||
},
|
// },
|
||||||
};
|
// };
|
||||||
|
|
||||||
use super::Ui;
|
// use super::Ui;
|
||||||
|
|
||||||
#[test]
|
// #[test]
|
||||||
fn empty() {
|
// fn empty() {
|
||||||
let mut terminal = terminal();
|
// let mut terminal = terminal();
|
||||||
let app = app(vec![]);
|
// let app = app(vec![]);
|
||||||
let ui = Ui::new();
|
// let ui = Ui::new();
|
||||||
|
|
||||||
terminal.draw(|frame| ui.render(&app, frame)).unwrap();
|
// terminal.draw(|frame| ui.render(&app, frame)).unwrap();
|
||||||
}
|
// }
|
||||||
|
|
||||||
#[test]
|
// #[test]
|
||||||
fn collection() {
|
// fn collection() {
|
||||||
let mut terminal = terminal();
|
// let mut terminal = terminal();
|
||||||
let mut app = app(COLLECTION.to_owned());
|
// let mut app = app(COLLECTION.to_owned());
|
||||||
let ui = Ui::new();
|
// let ui = Ui::new();
|
||||||
|
|
||||||
terminal.draw(|frame| ui.render(&app, frame)).unwrap();
|
// terminal.draw(|frame| ui.render(&app, frame)).unwrap();
|
||||||
|
|
||||||
// Change the track (which has a different track format).
|
// // Change the track (which has a different track format).
|
||||||
app.increment_category();
|
// app.increment_category();
|
||||||
app.increment_category();
|
// app.increment_category();
|
||||||
app.increment_selection();
|
// app.increment_selection();
|
||||||
|
|
||||||
terminal.draw(|frame| ui.render(&app, frame)).unwrap();
|
// terminal.draw(|frame| ui.render(&app, frame)).unwrap();
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
Loading…
Reference in New Issue
Block a user