diff --git a/Cargo.lock b/Cargo.lock index c6fc1c1..24b720e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -248,6 +248,7 @@ checksum = "486d44227f71a1ef39554c0dc47e44b9f4139927c75043312690c3f476d1d788" dependencies = [ "bitflags", "crossterm_winapi", + "futures-core", "libc", "mio", "parking_lot", @@ -1126,6 +1127,7 @@ dependencies = [ "dirs-next", "futures", "log", + "parking_lot", "reqwest", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 0db0ea8..630c227 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ cargo-features = ["strip"] name = "rshub" version = "0.1.4" authors = ["Fogapod "] -edition = "2018" +edition = "2021" description = "Unofficial game launcher for UnityStation" homepage = "https://github.com/Fogapod/rshub" repository = "https://github.com/Fogapod/rshub" @@ -13,7 +13,6 @@ keywords = ["ss13", "unitystation", "ui", "cli", "terminal"] categories = ["command-line-utilities", "games"] exclude = [".github/*", ".assets/*"] - [features] default = ["rustls"] @@ -25,13 +24,20 @@ geolocation = [] [dependencies] serde_json = "1" -crossterm = "0.21" better-panic = "0.2.0" dirs-next = "2.0.0" async-trait = "0.1.51" bytesize = "1.1.0" anyhow = "1.0.43" +[dependencies.crossterm] +version = "0.21.0" +features = ["event-stream"] + +[dependencies.parking_lot] +version = "0.11.2" +features = ["send_guard"] + [dependencies.zip] version = "0.5.13" default-features = false @@ -40,7 +46,7 @@ features = ["deflate"] [dependencies.tokio] version = "1" default-features = false -features = ["rt-multi-thread", "sync", "fs", "process", "macros", "time"] +features = ["rt-multi-thread", "fs", "process", "macros", "time"] [dependencies.futures] version = "0.3.16" diff --git a/README.md b/README.md index 3818350..2e8ee37 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ RSHub requires nightly rust toolchain to build (unstable strip feature). If you do not have nightly toolchain, you can prefix cargo commands with `RUSTC_BOOTSTRAP=1 cargo ...` as a workaround. Install from crates.io (stable version): `cargo install rshub` -Or with [geolocation](#geolocation) feature: `cargo install rshub --feature geolocation` +Or with [geolocation](#geolocation) feature: `cargo install rshub --features geolocation` Run from source (latest version): `cargo run` or `cargo run --release` (slow) diff --git a/src/app.rs b/src/app.rs index a21d9e3..68f0170 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,10 +1,17 @@ use std::collections::HashMap; +use std::convert::TryFrom; use std::io; -use std::sync::atomic::AtomicBool; use std::sync::Arc; +use std::time::Duration; use tui::backend::CrosstermBackend; -use tui::terminal::Frame; +use tui::terminal::{Frame, Terminal}; + +use crossterm::event::EventStream; + +use futures::StreamExt; + +use tokio::sync::mpsc; use crate::config::AppConfig; use crate::datatypes::game_version::GameVersion; @@ -13,7 +20,13 @@ use crate::input::UserInput; use crate::states::app::AppState; #[cfg(feature = "geolocation")] use crate::views::world::World; -use crate::views::{events::EventsView, help::Help, tabs::TabView, AppView, Drawable, ViewType}; +use crate::views::{events::EventsView, help::Help, tabs::Tabs, AppView, Draw, ViewType}; + +#[derive(Debug)] +pub enum StopSignal { + UserExit, + Panic, +} #[derive(Debug)] pub enum AppAction { @@ -39,14 +52,14 @@ pub struct App { events_view: EventsView, - pub stopped: bool, - pub panicked: Arc, + pub kill_switch: mpsc::Sender, + kill_switch_recv: mpsc::Receiver, } impl App { - pub async fn new(config: AppConfig) -> Self { - let panic_bool = Arc::new(AtomicBool::new(false)); - let state = AppState::new(config, panic_bool.clone()).await; + pub fn new(config: AppConfig) -> Self { + let (kill_switch, kill_switch_recv) = mpsc::channel(1); + let state = Arc::new(AppState::new(config, kill_switch.clone())); let mut instance = Self { state, @@ -56,11 +69,11 @@ impl App { events_view: EventsView {}, - stopped: false, - panicked: panic_bool, + kill_switch, + kill_switch_recv, }; - instance.register_view(ViewType::Tab, Box::new(TabView::new())); + instance.register_view(ViewType::Tab, Box::new(Tabs::new())); #[cfg(feature = "geolocation")] instance.register_view(ViewType::World, Box::new(World {})); instance.register_view(ViewType::Help, Box::new(Help {})); @@ -72,27 +85,75 @@ impl App { self.views.insert(tp, view); } - pub async fn draw(&mut self, f: &mut Frame<'_, CrosstermBackend>) { + pub async fn run(&mut self, terminal: &mut Terminal>) { + self.state.run(Arc::clone(&self.state)).await; + + let interval = Duration::from_millis(200); + let mut delay = tokio::time::interval(interval); + let mut reader = EventStream::new(); + + loop { + tokio::select! { + _ = delay.tick() => { + terminal.draw(|f| self.draw(f)).unwrap(); + }, + maybe_event = reader.next() => { + match maybe_event { + Some(Ok(event)) => { + if let Ok(valid_input) = UserInput::try_from(&event) { + self.on_input(&valid_input).await; + } + }, + Some(Err(err)) => { + log::error!("Error reading input: {}", err); + break; + } + None => { + log::error!("Input channel closed somehow"); + break + }, + } + }, + stop = self.kill_switch_recv.recv() => { + if let Some(stop) = stop { + match stop { + StopSignal::UserExit => { + log::info!("app stopped, cleaning up"); + break; + } + StopSignal::Panic => { + log::error!("app panicked, cleaning up"); + break; + } + } + } else { + log::error!("kill_switch channel closed somehow"); + break; + } + } + } + } + } + + pub fn draw(&mut self, f: &mut Frame<'_, CrosstermBackend>) { if let Some(tp) = self.view_stack.last() { if let Some(widget) = self.views.get_mut(tp) { use tui::layout::{Constraint, Direction, Layout}; - let area = if self.state.events.read().await.current_event.is_some() { + let area = if self.state.events.read().current_event.is_some() { let chunks = Layout::default() .constraints(vec![Constraint::Min(0), Constraint::Length(1)]) .direction(Direction::Vertical) .split(f.size()); - self.events_view - .draw(f, chunks[1], Arc::clone(&self.state)) - .await; + self.events_view.draw(f, chunks[1], Arc::clone(&self.state)); chunks[0] } else { f.size() }; - widget.draw(f, area, Arc::clone(&self.state)).await; + widget.draw(f, area, Arc::clone(&self.state)); } } } @@ -101,7 +162,9 @@ impl App { log::debug!("input: {:?}", input); match input { - UserInput::Quit => self.stop(), + UserInput::Quit => { + self.kill_switch.send(StopSignal::UserExit).await.unwrap(); + } UserInput::Help => { if let Some(top_view_type) = self.view_stack.last() { if top_view_type == &ViewType::Help { @@ -134,8 +197,4 @@ impl App { } } } - - fn stop(&mut self) { - self.stopped = true; - } } diff --git a/src/datatypes/hotkey.rs b/src/datatypes/hotkey.rs new file mode 100644 index 0000000..11e75a7 --- /dev/null +++ b/src/datatypes/hotkey.rs @@ -0,0 +1,43 @@ +use std::fmt; + +use crossterm::event::{KeyCode, KeyModifiers}; + +// TODO: less code duplication by associating this with inputs.rs somehow +#[derive(Debug, Clone)] +pub struct HotKey { + pub description: &'static str, + pub key: KeyCode, + pub modifiers: Option, +} + +impl fmt::Display for HotKey { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if let Some(modifiers) = self.modifiers { + write!(f, "{:?}+", modifiers)?; + } + write!( + f, + "{}", + match self.key { + KeyCode::Backspace => "Backspace".to_owned(), + KeyCode::Enter => "Enter".to_owned(), + KeyCode::Left => "Left".to_owned(), + KeyCode::Right => "Right".to_owned(), + KeyCode::Up => "Up".to_owned(), + KeyCode::Down => "Down".to_owned(), + KeyCode::Home => "Home".to_owned(), + KeyCode::End => "End".to_owned(), + KeyCode::PageUp => "PageUp".to_owned(), + KeyCode::PageDown => "PageDown".to_owned(), + KeyCode::Tab => "Tab".to_owned(), + KeyCode::BackTab => "BackTab".to_owned(), + KeyCode::Delete => "Delete".to_owned(), + KeyCode::Insert => "Insert".to_owned(), + KeyCode::F(i) => format!("F{}", i), + KeyCode::Char(c) => c.to_uppercase().to_string(), + KeyCode::Null => "Null".to_owned(), + KeyCode::Esc => "Esc".to_owned(), + } + ) + } +} diff --git a/src/datatypes/mod.rs b/src/datatypes/mod.rs index c6306d2..7e5b45b 100644 --- a/src/datatypes/mod.rs +++ b/src/datatypes/mod.rs @@ -1,6 +1,7 @@ pub mod commit; pub mod game_version; pub mod geolocation; +pub mod hotkey; pub mod installation; pub mod server; pub mod value_sorted_map; diff --git a/src/datatypes/server.rs b/src/datatypes/server.rs index b0fae7c..0603976 100644 --- a/src/datatypes/server.rs +++ b/src/datatypes/server.rs @@ -120,8 +120,6 @@ pub struct Server { pub gamemode: String, pub time: String, pub fps: u32, - // ui update skip optimization - // pub updated: bool, pub offline: bool, } @@ -145,8 +143,7 @@ impl Server { players, fps, version, - address: address.clone(), - // updated: true, + address, offline: false, } } diff --git a/src/input.rs b/src/input.rs index f24a804..24a3596 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1,8 +1,6 @@ -use std::sync::mpsc; -use std::thread; -use std::time::Duration; +use std::convert::TryFrom; -use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers, MouseEventKind}; +use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers, MouseEventKind}; #[derive(Debug)] pub enum UserInput { @@ -29,8 +27,10 @@ pub enum UserInput { Char(char), } -impl UserInput { - fn from(event: Event) -> Option { +impl TryFrom<&Event> for UserInput { + type Error = (); + + fn try_from(event: &Event) -> Result { match event { Event::Key(key) => match key { KeyEvent { @@ -40,94 +40,63 @@ impl UserInput { | KeyEvent { code: KeyCode::Char('q' | 'Q'), .. - } => Some(Self::Quit), + } => Ok(Self::Quit), KeyEvent { code: KeyCode::Char(c), .. - } => Some(Self::Char(c)), + } => Ok(Self::Char(*c)), KeyEvent { code: KeyCode::Left, .. - } => Some(Self::Left), + } => Ok(Self::Left), KeyEvent { code: KeyCode::Right, .. - } => Some(Self::Right), + } => Ok(Self::Right), KeyEvent { code: KeyCode::Up, .. - } => Some(Self::Up), + } => Ok(Self::Up), KeyEvent { code: KeyCode::Down, .. - } => Some(Self::Down), + } => Ok(Self::Down), KeyEvent { code: KeyCode::Home, .. - } => Some(Self::Top), + } => Ok(Self::Top), KeyEvent { code: KeyCode::End, .. - } => Some(Self::Bottom), + } => Ok(Self::Bottom), KeyEvent { code: KeyCode::Esc, .. - } => Some(Self::Back), + } => Ok(Self::Back), KeyEvent { code: KeyCode::Enter, .. - } => Some(Self::Enter), + } => Ok(Self::Enter), KeyEvent { code: KeyCode::Delete | KeyCode::Backspace, .. - } => Some(Self::Delete), + } => Ok(Self::Delete), KeyEvent { code: KeyCode::Tab, .. - } => Some(Self::Tab), + } => Ok(Self::Tab), KeyEvent { code: KeyCode::F(1), .. - } => Some(Self::Help), + } => Ok(Self::Help), KeyEvent { code: KeyCode::F(5), .. - } => Some(Self::Refresh), - _ => None, + } => Ok(Self::Refresh), + _ => Err(()), }, Event::Mouse(mouse) => match mouse.kind { - MouseEventKind::ScrollUp => Some(Self::Up), - MouseEventKind::ScrollDown => Some(Self::Down), - _ => None, + MouseEventKind::ScrollUp => Ok(Self::Up), + MouseEventKind::ScrollDown => Ok(Self::Down), + _ => Err(()), }, - _ => None, + _ => Err(()), } } } - -pub(crate) enum EventOrTick { - Input(I), - Tick, -} - -pub(crate) fn spawn_input_thread(interval: Duration) -> mpsc::Receiver> { - let (tx, rx) = mpsc::channel(); - - thread::Builder::new() - .name("input".to_owned()) - .spawn(move || loop { - let event = if event::poll(interval).unwrap() { - if let Some(valid_input) = UserInput::from(event::read().unwrap()) { - EventOrTick::Input(valid_input) - } else { - EventOrTick::Tick - } - } else { - EventOrTick::Tick - }; - - if let Err(e) = tx.send(event) { - log::error!("failed to send input event, probably closed channel: {}", e); - break; - } - }) - .expect("unable to spawn input thread"); - - rx -} diff --git a/src/main.rs b/src/main.rs index cbb775a..34144f9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,24 +4,15 @@ mod constants; mod datatypes; mod input; mod states; +mod utils; mod views; use std::io; -use std::sync::atomic::Ordering; -use std::time::Duration; - -use tui::{backend::CrosstermBackend, Terminal}; - -use crossterm::{ - event::{DisableMouseCapture, EnableMouseCapture}, - execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, -}; use log::LevelFilter; use crate::config::AppConfig; -use crate::input::{spawn_input_thread, EventOrTick}; +use crate::utils::{cleanup_terminal, create_terminal, verbosity_to_log_level}; fn setup_panic_hook() { #[cfg(not(debug_assertions))] @@ -41,37 +32,6 @@ fn setup_panic_hook() { })); } -fn cleanup_terminal(terminal: Option<&mut Terminal>>) { - disable_raw_mode().unwrap(); - - let mut stdout = io::stdout(); - - // FIXME: if mouse is outside terminal, it is not released properly and garbage - // is printed after panic - execute!(stdout, LeaveAlternateScreen, DisableMouseCapture).unwrap(); - - if let Some(terminal) = terminal { - terminal.show_cursor().unwrap(); - } -} - -const fn verbosity_to_log_level(verbosity: u32) -> LevelFilter { - let mut verbosity = verbosity; - - #[cfg(debug_assertions)] - { - // jump straight to debug - verbosity += 3; - } - - match verbosity { - 0 => LevelFilter::Error, - 1 => LevelFilter::Warn, - 2 => LevelFilter::Info, - 3 => LevelFilter::Debug, - _ => LevelFilter::Trace, - } -} fn setup_logger(config: &AppConfig) -> Result<(), io::Error> { simplelog::WriteLogger::init( verbosity_to_log_level(config.verbose), @@ -100,49 +60,13 @@ fn _main() -> Result<(), Box> { .enable_all() .build()?; - let mut app = rt.block_on(app::App::new(config)); - - let mut terminal = { - enable_raw_mode()?; + let mut app = app::App::new(config); - let mut stdout = io::stdout(); - execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + let mut terminal = create_terminal()?; - let backend = CrosstermBackend::new(stdout); + rt.block_on(app.run(&mut terminal)); - Terminal::new(backend)? - }; - - { - let rx = spawn_input_thread(Duration::from_millis(200)); - - loop { - // TODO: only draw when something changed - terminal.draw(|f| { - rt.block_on(app.draw(f)); - })?; - - match rx.recv()? { - EventOrTick::Input(event) => rt.block_on(app.on_input(&event)), - EventOrTick::Tick => {} - } - - if app.stopped { - log::info!("app stopped, cleaning up"); - - cleanup_terminal(Some(&mut terminal)); - - break; - } - - if app.panicked.load(Ordering::Relaxed) { - // IMPORTANT: do not cleanup terminal, this is done in panic hook - log::error!("app panicked, cleaning up"); - - break; - } - } - } + cleanup_terminal(Some(&mut terminal)); Ok(()) } diff --git a/src/states/app.rs b/src/states/app.rs index e85d5e3..1aa83f5 100644 --- a/src/states/app.rs +++ b/src/states/app.rs @@ -1,49 +1,51 @@ -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; -use tokio::sync::RwLock; +use parking_lot::{Mutex, RwLock}; +use tokio::sync::mpsc; use tokio::task::JoinHandle; use anyhow::Result; -use crate::app::AppAction; +use crate::app::{AppAction, StopSignal}; use crate::config::AppConfig; use crate::constants::USER_AGENT; +use crate::datatypes::hotkey::HotKey; use crate::states::events::EventsState; use crate::states::help::HelpState; -use crate::states::help::HotKey; #[cfg(feature = "geolocation")] use crate::states::LocationsState; -use crate::states::{CommitState, ServersState, VersionsState}; +use crate::views::commits::Commits; +use crate::views::servers::Servers; +use crate::views::versions::Versions; pub type TaskResult = Result<()>; pub struct AppState { pub config: AppConfig, - pub commits: Arc>, - pub versions: Arc>, + pub commits: Commits, + pub versions: Versions, #[cfg(feature = "geolocation")] pub locations: Arc>, - pub servers: Arc>, + pub servers: Servers, pub events: Arc>, pub help: Mutex, pub client: reqwest::Client, - panic_bool: Arc, + kill_switch: mpsc::Sender, } impl AppState { - pub async fn new(config: AppConfig, panic_bool: Arc) -> Arc { + pub fn new(config: AppConfig, kill_switch: mpsc::Sender) -> Self { #[cfg(feature = "geolocation")] - let locations = Arc::new(RwLock::new(LocationsState::new(&config).await)); - let versions = Arc::new(RwLock::new(VersionsState::new(&config).await)); - let servers = Arc::new(RwLock::new(ServersState::new(&config).await)); - let events = Arc::new(RwLock::new(EventsState::new(&config).await)); + let locations = Arc::new(RwLock::new(LocationsState::new(&config))); + let versions = Versions::new(); + let servers = Servers::new(); + let events = Arc::new(RwLock::new(EventsState::new(&config))); - let instance = Arc::new(Self { - commits: Arc::new(RwLock::new(CommitState::new().await)), + Self { + commits: Commits::new(), versions: versions.clone(), #[cfg(feature = "geolocation")] locations: locations.clone(), @@ -57,52 +59,52 @@ impl AppState { help: Mutex::new(HelpState::new()), - panic_bool, - }); + kill_switch, + } + } - events.write().await.run(instance.clone()).await; - servers.write().await.run(instance.clone()).await; + pub async fn run(&self, instance: Arc) { + self.events.write().run(instance.clone()).await; + self.servers.run(instance.clone()).await; #[cfg(feature = "geolocation")] - locations.write().await.run(instance.clone()).await; - versions.write().await.run(instance.clone()).await; - - instance + self.locations.write().run(instance.clone()).await; + //self.versions.write().await.run(instance.clone()).await; } pub async fn on_action(&self, action: &AppAction, app: Arc) { log::debug!("action: {:?}", &action); - let f = match action { - AppAction::ConnectToServer { version, address } => Some(tokio::spawn( - VersionsState::launch(Arc::clone(&app), version.clone(), Some(address.clone())), - )), - AppAction::InstallVersion(version) => Some(tokio::spawn(VersionsState::install( - Arc::clone(&app), - version.clone(), - ))), - AppAction::LaunchVersion(version) => Some(tokio::spawn(VersionsState::launch( - Arc::clone(&app), - version.clone(), - None, - ))), - AppAction::AbortVersionInstallation(version) => Some(tokio::spawn( - VersionsState::abort_installation(Arc::clone(&app), version.clone()), - )), - AppAction::UninstallVersion(version) => Some(tokio::spawn(VersionsState::uninstall( - Arc::clone(&app), - version.clone(), - ))), - - _ => None, - }; - - if let Some(f) = f { - self.watch_task(f).await; - } + // let f = match action { + // AppAction::ConnectToServer { version, address } => Some(tokio::spawn( + // VersionsState::launch(Arc::clone(&app), version.clone(), Some(address.clone())), + // )), + // AppAction::InstallVersion(version) => Some(tokio::spawn(VersionsState::install( + // Arc::clone(&app), + // version.clone(), + // ))), + // AppAction::LaunchVersion(version) => Some(tokio::spawn(VersionsState::launch( + // Arc::clone(&app), + // version.clone(), + // None, + // ))), + // AppAction::AbortVersionInstallation(version) => Some(tokio::spawn( + // VersionsState::abort_installation(Arc::clone(&app), version.clone()), + // )), + // AppAction::UninstallVersion(version) => Some(tokio::spawn(VersionsState::uninstall( + // Arc::clone(&app), + // version.clone(), + // ))), + + // _ => None, + // }; + + // if let Some(f) = f { + // self.watch_task(f).await; + // } } pub fn display_help(&self, view_name: &str, keys: &[HotKey]) { - let mut help = self.help.lock().unwrap(); + let mut help = self.help.lock(); help.set_name(view_name); help.set_hotkeys(keys); } @@ -110,14 +112,14 @@ impl AppState { pub async fn watch_task(&self, task: JoinHandle) { tokio::spawn(Self::wrap_task( task, - self.panic_bool.clone(), + self.kill_switch.clone(), self.events.clone(), )); } async fn wrap_task( task: JoinHandle, - panic_bool: Arc, + kill_switch: mpsc::Sender, events: Arc>, ) { match task.await { @@ -126,12 +128,12 @@ impl AppState { if err.is_panic() { log::error!("error is panic, setting panic to exit on next tick"); - panic_bool.store(true, Ordering::Relaxed); + kill_switch.send(StopSignal::Panic).await.unwrap(); } } Ok(result) => { if let Err(err) = result { - events.read().await.error(err).await; + events.read().error(err).await; } } } diff --git a/src/states/events.rs b/src/states/events.rs index 3b230e8..ee82489 100644 --- a/src/states/events.rs +++ b/src/states/events.rs @@ -31,7 +31,7 @@ pub struct EventsState { } impl EventsState { - pub async fn new(_: &AppConfig) -> Self { + pub fn new(_: &AppConfig) -> Self { let (events, events_recv) = mpsc::unbounded_channel(); Self { @@ -82,7 +82,7 @@ impl EventsState { let mut lock = last_event.lock().await; *lock = Instant::now(); - app.events.write().await.current_event = Some(event); + app.events.write().current_event = Some(event); let events = app.events.clone(); let last_event = last_event.clone(); @@ -97,7 +97,7 @@ impl EventsState { return; } - events.write().await.current_event = None; + events.write().current_event = None; }); } diff --git a/src/states/help.rs b/src/states/help.rs index ae53010..c9defff 100644 --- a/src/states/help.rs +++ b/src/states/help.rs @@ -1,46 +1,6 @@ -use std::fmt; +use crossterm::event::KeyCode; -use crossterm::event::{KeyCode, KeyModifiers}; - -// TODO: less code duplication by associating this with inputs.rs somehow -#[derive(Debug, Clone)] -pub struct HotKey { - pub description: &'static str, - pub key: KeyCode, - pub modifiers: Option, -} - -impl fmt::Display for HotKey { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if let Some(modifiers) = self.modifiers { - write!(f, "{:?}+", modifiers)?; - } - write!( - f, - "{}", - match self.key { - KeyCode::Backspace => "Backspace".to_owned(), - KeyCode::Enter => "Enter".to_owned(), - KeyCode::Left => "Left".to_owned(), - KeyCode::Right => "Right".to_owned(), - KeyCode::Up => "Up".to_owned(), - KeyCode::Down => "Down".to_owned(), - KeyCode::Home => "Home".to_owned(), - KeyCode::End => "End".to_owned(), - KeyCode::PageUp => "PageUp".to_owned(), - KeyCode::PageDown => "PageDown".to_owned(), - KeyCode::Tab => "Tab".to_owned(), - KeyCode::BackTab => "BackTab".to_owned(), - KeyCode::Delete => "Delete".to_owned(), - KeyCode::Insert => "Insert".to_owned(), - KeyCode::F(i) => format!("F{}", i), - KeyCode::Char(c) => c.to_uppercase().to_string(), - KeyCode::Null => "Null".to_owned(), - KeyCode::Esc => "Esc".to_owned(), - } - ) - } -} +use crate::datatypes::hotkey::HotKey; pub struct HelpState { pub view_name: String, diff --git a/src/states/locations.rs b/src/states/locations.rs index c174f2a..73ef9db 100644 --- a/src/states/locations.rs +++ b/src/states/locations.rs @@ -17,7 +17,7 @@ pub struct LocationsState { } impl LocationsState { - pub async fn new(_: &AppConfig) -> Self { + pub fn new(_: &AppConfig) -> Self { let (tx, rx) = mpsc::unbounded_channel(); Self { @@ -88,7 +88,7 @@ impl LocationsState { log::debug!("resolved location: {:?} -> {:?}", ip, location); - app.locations.write().await.items.insert(ip, location); + app.locations.write().items.insert(ip, location); Ok(()) } diff --git a/src/states/mod.rs b/src/states/mod.rs index 3c3fc5f..7f5f945 100644 --- a/src/states/mod.rs +++ b/src/states/mod.rs @@ -1,26 +1,20 @@ pub mod app; -pub mod commits; pub mod events; pub mod help; #[cfg(feature = "geolocation")] pub mod locations; -pub mod servers; -pub mod versions; pub use app::AppState; -pub use commits::CommitState; #[cfg(feature = "geolocation")] pub use locations::LocationsState; -pub use servers::ServersState; -pub use versions::VersionsState; use crossterm::event::KeyCode; use tui::widgets::{ListState, TableState}; use crate::app::AppAction; +use crate::datatypes::hotkey::HotKey; use crate::input::UserInput; -use crate::states::help::HotKey; // tui states look same, but do not implement trait, so I made one pub trait TuiState { diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..f20e2cf --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,54 @@ +use std::io; + +use crossterm::{ + event::{DisableMouseCapture, EnableMouseCapture}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; + +use log::LevelFilter; + +use tui::{backend::CrosstermBackend, Terminal}; + +pub fn create_terminal() -> io::Result>> { + enable_raw_mode()?; + + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + + let backend = CrosstermBackend::new(stdout); + + Terminal::new(backend) +} + +pub fn cleanup_terminal(terminal: Option<&mut Terminal>>) { + disable_raw_mode().unwrap(); + + let mut stdout = io::stdout(); + + // FIXME: if mouse is outside terminal, it is not released properly and garbage + // is printed after panic + execute!(stdout, LeaveAlternateScreen, DisableMouseCapture).unwrap(); + + if let Some(terminal) = terminal { + terminal.show_cursor().unwrap(); + } +} + +pub const fn verbosity_to_log_level(verbosity: u32) -> LevelFilter { + let mut verbosity = verbosity; + + #[cfg(debug_assertions)] + { + // jump straight to debug + verbosity += 3; + } + + match verbosity { + 0 => LevelFilter::Error, + 1 => LevelFilter::Warn, + 2 => LevelFilter::Info, + 3 => LevelFilter::Debug, + _ => LevelFilter::Trace, + } +} diff --git a/src/views/commits.rs b/src/views/commits/draw.rs similarity index 56% rename from src/views/commits.rs rename to src/views/commits/draw.rs index 50da07d..e59cb3f 100644 --- a/src/views/commits.rs +++ b/src/views/commits/draw.rs @@ -6,68 +6,23 @@ use tui::{ layout::{Alignment, Constraint, Direction, Layout, Rect}, style::{Color, Style}, text::Text, - widgets::{Block, Borders, List, ListItem, ListState, Paragraph, Wrap}, + widgets::{Block, Borders, List, ListItem, Paragraph, Wrap}, Frame, }; -use crate::app::AppAction; -use crate::input::UserInput; -use crate::states::help::HotKey; -use crate::states::{AppState, CommitState, StatelessList}; -use crate::views::{Drawable, HotKeys, InputProcessor, Named}; +use crate::states::AppState; +use crate::views::Draw; -pub struct CommitView { - // TODO: - // - on 1st launch: fetch N latest commits, save latest hash - // - on 2nd launch: read latest hash and fetch newer commits - loaded: bool, +use super::Commits; - state: StatelessList, -} - -impl CommitView { - pub fn new() -> Self { - Self { - loaded: false, - state: StatelessList::new(ListState::default(), false), - } - } -} - -#[async_trait::async_trait] -impl Named for CommitView { - fn name(&self) -> String { - "Recent Commit List".to_owned() - } -} - -impl HotKeys for CommitView { - fn hotkeys(&self) -> Vec { - self.state.hotkeys() - } -} - -#[async_trait::async_trait] -impl InputProcessor for CommitView { - async fn on_input(&mut self, input: &UserInput, app: Arc) -> Option { - self.state.on_input(input, app.commits.read().await.count()) - } -} - -#[async_trait::async_trait] -impl Drawable for CommitView { - async fn draw( - &mut self, - f: &mut Frame>, - area: Rect, - app: Arc, - ) { +impl Draw for Commits { + fn draw(&self, f: &mut Frame>, area: Rect, app: Arc) { let chunks = Layout::default() .direction(Direction::Horizontal) .constraints([Constraint::Length(60), Constraint::Min(0)]) .split(area); - let commits = &app.commits.read().await.items; + let commits = &self.state.read().items; let items: Vec = commits .iter() @@ -83,9 +38,9 @@ impl Drawable for CommitView { ) .highlight_style(Style::default().bg(Color::DarkGray)); - f.render_stateful_widget(list, chunks[0], &mut self.state.state); + f.render_stateful_widget(list, chunks[0], &mut self.state.write().selection.state); - if let Some(i) = self.state.state.selected() { + if let Some(i) = self.state.read().selection.state.selected() { let chunks = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Length(3), Constraint::Min(0)]) @@ -118,13 +73,12 @@ impl Drawable for CommitView { ); } - if !self.loaded { - if !app.config.offline { - app.watch_task(tokio::spawn(CommitState::load(Arc::clone(&app)))) - .await; - } + // if !self.loaded { + // if !app.config.offline { + // self.load(app).await; + // } - self.loaded = true; - } + // self.loaded = true; + // } } } diff --git a/src/views/commits/mod.rs b/src/views/commits/mod.rs new file mode 100644 index 0000000..2eb618d --- /dev/null +++ b/src/views/commits/mod.rs @@ -0,0 +1,64 @@ +mod draw; +mod state; + +use std::sync::Arc; + +use parking_lot::RwLock; + +use crate::app::AppAction; + +use crate::datatypes::hotkey::HotKey; +use crate::input::UserInput; + +use crate::states::AppState; +use crate::views::{HotKeys, Input, Name}; + +use state::State; + +#[derive(Clone)] +pub struct Commits { + // TODO: + // - on 1st launch: fetch N latest commits, save latest hash + // - on 2nd launch: read latest hash and fetch newer commits + loaded: bool, + + state: Arc>, +} + +impl Commits { + pub fn new() -> Self { + Self { + loaded: false, + + state: Arc::new(RwLock::new(State::new())), + } + } + + pub fn count(&self) -> usize { + self.state.read().items.len() + } + + pub async fn load(&self, app: Arc) { + app.watch_task(tokio::spawn(State::load(Arc::clone(&app)))) + .await; + } +} + +impl Name for Commits { + fn name(&self) -> String { + "Recent Commit List".to_owned() + } +} + +impl HotKeys for Commits { + fn hotkeys(&self) -> Vec { + self.state.read().selection.hotkeys() + } +} + +#[async_trait::async_trait] +impl Input for Commits { + async fn on_input(&mut self, input: &UserInput, _: Arc) -> Option { + self.state.write().selection.on_input(input, self.count()) + } +} diff --git a/src/states/commits.rs b/src/views/commits/state.rs similarity index 66% rename from src/states/commits.rs rename to src/views/commits/state.rs index b72c358..4c48343 100644 --- a/src/states/commits.rs +++ b/src/views/commits/state.rs @@ -2,21 +2,24 @@ use std::sync::Arc; use anyhow::Context; +use tui::widgets::ListState; + use crate::constants::GITHUB_REPO_COMMIT_ENDPOINT_URL; use crate::datatypes::commit::{Commit, CommitsJson}; use crate::states::app::{AppState, TaskResult}; +use crate::states::StatelessList; -pub struct CommitState { +pub struct State { pub items: Vec, + pub selection: StatelessList, } -impl CommitState { - pub async fn new() -> Self { - Self { items: Vec::new() } - } - - pub fn count(&self) -> usize { - self.items.len() +impl State { + pub fn new() -> Self { + Self { + items: Vec::new(), + selection: StatelessList::new(ListState::default(), false), + } } pub async fn load(app: Arc) -> TaskResult { @@ -38,13 +41,12 @@ impl CommitState { .await .with_context(|| "parsing commits response")?; - app.commits.write().await.update(commit_range); + app.commits + .state + .write() + .items + .append(&mut commit_range.0.iter().map(Commit::from).collect()); Ok(()) } - - pub fn update(&mut self, data: CommitsJson) { - self.items - .append(&mut data.0.iter().map(Commit::from).collect()); - } } diff --git a/src/views/events.rs b/src/views/events.rs index 863fef1..7bbc399 100644 --- a/src/views/events.rs +++ b/src/views/events.rs @@ -12,19 +12,13 @@ use tui::{ use crate::states::app::AppState; use crate::states::events::AppEvent; -use crate::views::Drawable; +use crate::views::Draw; pub struct EventsView {} -#[async_trait::async_trait] -impl Drawable for EventsView { - async fn draw( - &mut self, - f: &mut Frame>, - area: Rect, - app: Arc, - ) { - if let Some(event) = &app.events.read().await.current_event { +impl Draw for EventsView { + fn draw(&self, f: &mut Frame>, area: Rect, app: Arc) { + if let Some(event) = &app.events.read().current_event { let mut style = Style::default().add_modifier(Modifier::BOLD); let mut border_style = Style::default(); diff --git a/src/views/help.rs b/src/views/help.rs index f01ecf3..68f835b 100644 --- a/src/views/help.rs +++ b/src/views/help.rs @@ -14,23 +14,21 @@ use tui::{ }; use crate::app::AppAction; +use crate::datatypes::hotkey::HotKey; use crate::input::UserInput; -use crate::states::help::HotKey; use crate::states::AppState; -use crate::views::{AppView, Drawable, HotKeys, InputProcessor, Named}; +use crate::views::{AppView, Draw, HotKeys, Input, Name}; pub struct Help {} impl AppView for Help {} -#[async_trait::async_trait] -impl Named for Help { +impl Name for Help { fn name(&self) -> String { "Help Screen".to_owned() } } -#[async_trait::async_trait] impl HotKeys for Help { fn hotkeys(&self) -> Vec { vec![HotKey { @@ -42,7 +40,7 @@ impl HotKeys for Help { } #[async_trait::async_trait] -impl InputProcessor for Help { +impl Input for Help { async fn on_input(&mut self, input: &UserInput, _: Arc) -> Option { match input { UserInput::Back => Some(AppAction::CloseView), @@ -51,15 +49,9 @@ impl InputProcessor for Help { } } -#[async_trait::async_trait] -impl Drawable for Help { - async fn draw( - &mut self, - f: &mut Frame>, - area: Rect, - app: Arc, - ) { - let help = app.help.lock().unwrap(); +impl Draw for Help { + fn draw(&self, f: &mut Frame>, area: Rect, app: Arc) { + let help = app.help.lock(); let list_length = (help.global_hotkeys.len() + help.local_hotkeys.len()) as u16 + 2 // outer border diff --git a/src/views/mod.rs b/src/views/mod.rs index d426974..58f8007 100644 --- a/src/views/mod.rs +++ b/src/views/mod.rs @@ -15,8 +15,8 @@ use tui::layout::Rect; use tui::terminal::Frame; use crate::app::AppAction; +use crate::datatypes::hotkey::HotKey; use crate::input::UserInput; -use crate::states::help::HotKey; use crate::states::AppState; #[derive(PartialEq, Eq, Hash, Debug)] @@ -27,18 +27,12 @@ pub enum ViewType { Help, } -#[async_trait::async_trait] -pub trait Drawable { - async fn draw( - &mut self, - f: &mut Frame>, - area: Rect, - app: Arc, - ); +pub trait Draw { + fn draw(&self, f: &mut Frame>, area: Rect, app: Arc); } #[async_trait::async_trait] -pub trait InputProcessor { +pub trait Input { async fn on_input(&mut self, input: &UserInput, app: Arc) -> Option; } @@ -48,8 +42,8 @@ pub trait HotKeys { } } -pub trait Named { +pub trait Name { fn name(&self) -> String; } -pub trait AppView: Drawable + InputProcessor + HotKeys + Named {} +pub trait AppView: Draw + Input + HotKeys + Name {} diff --git a/src/views/servers.rs b/src/views/servers/draw.rs similarity index 74% rename from src/views/servers.rs rename to src/views/servers/draw.rs index 44c6f72..18e6fc8 100644 --- a/src/views/servers.rs +++ b/src/views/servers/draw.rs @@ -1,113 +1,28 @@ use std::io; use std::sync::Arc; -use crossterm::event::KeyCode; - use tui::backend::CrosstermBackend; use tui::layout::Rect; use tui::terminal::Frame; -use crate::app::AppAction; -use crate::datatypes::server::Server; -use crate::input::UserInput; -use crate::states::help::HotKey; -use crate::states::{AppState, StatelessList}; -use crate::views::{Drawable, HotKeys, InputProcessor, Named, ViewType}; - use tui::{ layout::{Alignment, Constraint, Direction, Layout}, style::{Color, Modifier, Style}, symbols::DOT, text::{Span, Spans, Text}, - widgets::{Block, Borders, Paragraph, Row, Table, TableState, Wrap}, + widgets::{Block, Borders, Paragraph, Row, Table, Wrap}, }; -pub struct ServerView { - state: StatelessList, -} - -impl ServerView { - pub fn new() -> Self { - Self { - state: StatelessList::new(TableState::default(), false), - } - } -} - -impl Named for ServerView { - fn name(&self) -> String { - "Server List".to_owned() - } -} - -impl HotKeys for ServerView { - fn hotkeys(&self) -> Vec { - let mut hotkeys = vec![ - #[cfg(feature = "geolocation")] - HotKey { - description: "Show world map", - key: KeyCode::Char('m'), - modifiers: None, - }, - HotKey { - description: "Install game version for selected server", - key: KeyCode::Char('i'), - modifiers: None, - }, - HotKey { - description: "Connect to selected server (install if needed)", - key: KeyCode::Enter, - modifiers: None, - }, - ]; - - hotkeys.append(&mut self.state.hotkeys()); +use crate::datatypes::server::Server; - hotkeys - } -} +use crate::states::AppState; +use crate::views::Draw; -#[async_trait::async_trait] -impl InputProcessor for ServerView { - async fn on_input(&mut self, input: &UserInput, app: Arc) -> Option { - match input { - #[cfg(feature = "geolocation")] - UserInput::Char('m' | 'M') => Some(AppAction::OpenView(ViewType::World)), - UserInput::Char('i' | 'I') => { - if let Some(i) = self.state.selected() { - Some(AppAction::InstallVersion( - app.servers.read().await.items[i].version.clone(), - )) - } else { - None - } - } - UserInput::Enter => { - if let Some(i) = self.state.selected() { - let server = &app.servers.read().await.items[i]; - - Some(AppAction::ConnectToServer { - version: server.version.clone(), - address: server.address.clone(), - }) - } else { - None - } - } - _ => self.state.on_input(input, app.servers.read().await.count()), - } - } -} +use super::Servers; -#[async_trait::async_trait] -impl Drawable for ServerView { - async fn draw( - &mut self, - f: &mut Frame>, - area: Rect, - app: Arc, - ) { - let servers = &app.servers.read().await.items; +impl Draw for Servers { + fn draw(&self, f: &mut Frame>, area: Rect, app: Arc) { + let servers = &self.state.read().items; let mut count_online = 0; let mut count_no_players = 0; @@ -227,17 +142,17 @@ impl Drawable for ServerView { ); // draw server info - if let Some(selected) = self.state.selected().map(|s| &servers[s]) { - draw_server_info(f, chunks[1], Arc::clone(&app), selected).await; + if let Some(selected) = self.state.read().selection.selected().map(|s| &servers[s]) { + draw_server_info(f, chunks[1], Arc::clone(&app), selected); } else { draw_info(f, chunks[1], Arc::clone(&app)); } - f.render_stateful_widget(table, chunks[0], &mut self.state.state); + f.render_stateful_widget(table, chunks[0], &mut self.state.write().selection.state); } } -async fn draw_server_info( +fn draw_server_info( f: &mut Frame<'_, CrosstermBackend>, area: Rect, app: Arc, @@ -245,7 +160,7 @@ async fn draw_server_info( ) { #[cfg(feature = "geolocation")] let selected_location = - if let Some(location) = app.locations.read().await.items.get(&selected.address.ip) { + if let Some(location) = app.locations.read().items.get(&selected.address.ip) { format!("{}/{}", location.country, location.city) } else { "unknown".to_owned() diff --git a/src/views/servers/hotkeys.rs b/src/views/servers/hotkeys.rs new file mode 100644 index 0000000..582c550 --- /dev/null +++ b/src/views/servers/hotkeys.rs @@ -0,0 +1,33 @@ +use crossterm::event::KeyCode; + +use crate::datatypes::hotkey::HotKey; +use crate::views::HotKeys; + +use super::Servers; + +impl HotKeys for Servers { + fn hotkeys(&self) -> Vec { + let mut hotkeys = vec![ + #[cfg(feature = "geolocation")] + HotKey { + description: "Show world map", + key: KeyCode::Char('m'), + modifiers: None, + }, + HotKey { + description: "Install game version for selected server", + key: KeyCode::Char('i'), + modifiers: None, + }, + HotKey { + description: "Connect to selected server (install if needed)", + key: KeyCode::Enter, + modifiers: None, + }, + ]; + + hotkeys.append(&mut self.state.read().selection.hotkeys()); + + hotkeys + } +} diff --git a/src/views/servers/input.rs b/src/views/servers/input.rs new file mode 100644 index 0000000..d1254f2 --- /dev/null +++ b/src/views/servers/input.rs @@ -0,0 +1,41 @@ +use std::sync::Arc; + +use crate::app::AppAction; + +use crate::input::UserInput; +use crate::states::AppState; +use crate::views::{Input, ViewType}; + +use super::Servers; + +#[async_trait::async_trait] +impl Input for Servers { + async fn on_input(&mut self, input: &UserInput, app: Arc) -> Option { + match input { + #[cfg(feature = "geolocation")] + UserInput::Char('m' | 'M') => Some(AppAction::OpenView(ViewType::World)), + UserInput::Char('i' | 'I') => { + if let Some(i) = self.state.read().selection.selected() { + Some(AppAction::InstallVersion( + self.state.read().items[i].version.clone(), + )) + } else { + None + } + } + UserInput::Enter => { + if let Some(i) = self.state.read().selection.selected() { + let server = &self.state.read().items[i]; + + Some(AppAction::ConnectToServer { + version: server.version.clone(), + address: server.address.clone(), + }) + } else { + None + } + } + _ => self.state.write().selection.on_input(input, self.count()), + } + } +} diff --git a/src/states/servers.rs b/src/views/servers/mod.rs similarity index 52% rename from src/states/servers.rs rename to src/views/servers/mod.rs index 82a2618..6eb0e5e 100644 --- a/src/states/servers.rs +++ b/src/views/servers/mod.rs @@ -1,32 +1,38 @@ +mod draw; +mod hotkeys; +mod input; +mod state; +mod tasks; + use std::cmp::Ordering; use std::collections::HashMap; use std::sync::Arc; -use std::time::Duration; -use anyhow::Context; +use parking_lot::RwLock; -use crate::config::AppConfig; -use crate::constants::SERVER_LIST_URL; use crate::datatypes::game_version::{DownloadUrl, GameVersion}; use crate::datatypes::geolocation::IP; use crate::datatypes::server::{Address, Server, ServerListJson}; -use crate::states::app::{AppState, TaskResult}; -use crate::states::versions::VersionsState; +use crate::states::AppState; +use crate::views::Name; + +use state::State; -pub struct ServersState { - pub items: Vec, - update_interval: Duration, +use crate::views::AppView; + +#[derive(Clone)] +pub struct Servers { + pub state: Arc>, } -impl ServersState { - pub async fn new(config: &AppConfig) -> Self { +impl Servers { + pub fn new() -> Self { Self { - items: Vec::new(), - update_interval: Duration::from_secs(config.update_interval), + state: Arc::new(RwLock::new(State::new())), } } - pub async fn run(&mut self, app: Arc) { + pub async fn run(&self, app: Arc) { #[cfg(debug_assertions)] { let ip = IP::Remote("8.8.8.8".to_owned()); @@ -36,7 +42,7 @@ impl ServersState { download: DownloadUrl::new("http://evil.exploit"), }; - self.items.push(Server { + self.state.write().items.push(Server { name: "TEST SERVER PLEASE IGNORE".to_owned(), address: Address { ip: ip.clone(), @@ -52,29 +58,28 @@ impl ServersState { }); #[cfg(feature = "geolocation")] - app.locations.write().await.resolve(&ip).await; + app.locations.write().resolve(&ip).await; - let _ = VersionsState::version_discovered(Arc::clone(&app), &version).await; + // let _ = VersionsState::version_discovered(Arc::clone(&app), &version).await; } if app.config.offline { return; } - app.watch_task(tokio::task::spawn(Self::server_fetch_task(app.clone()))) + app.watch_task(tokio::task::spawn(tasks::server_fetch_task(app.clone()))) .await; } pub fn count(&self) -> usize { - self.items.len() + self.state.read().items.len() } - pub async fn update(&mut self, app: Arc, data: ServerListJson) { - let mut previously_online: HashMap = self - .items - .iter_mut() - .map(|i| (i.address.clone(), i)) - .collect(); + pub async fn update(&self, app: Arc, data: ServerListJson) { + let items = &mut self.state.write().items; + + let mut previously_online: HashMap = + items.iter_mut().map(|i| (i.address.clone(), i)).collect(); let mut created_servers: Vec = Vec::new(); @@ -87,9 +92,9 @@ impl ServersState { let version = GameVersion::from(sv.clone()); if let Some(known_server) = previously_online.remove(&address) { - // version changed (download/build/fork) + // download/build/fork changed if known_server.version != version { - VersionsState::version_discovered(Arc::clone(&app), &version).await; + // VersionsState::version_discovered(Arc::clone(&app), &version).await; known_server.version = version; } @@ -98,11 +103,11 @@ impl ServersState { known_server.offline = false; } else { #[cfg(feature = "geolocation")] - app.locations.write().await.resolve(&ip).await; + app.locations.write().resolve(&ip).await; created_servers.push(Server::new(address, version.clone(), sv)); - VersionsState::version_discovered(Arc::clone(&app), &version).await; + // VersionsState::version_discovered(Arc::clone(&app), &version).await; } } @@ -110,7 +115,7 @@ impl ServersState { sv.offline = true; } - self.items.append(&mut created_servers); + items.append(&mut created_servers); // TODO: pinned servers // TODO: custom sorts by each field @@ -120,7 +125,7 @@ impl ServersState { // - player count // - server name // https://stackoverflow.com/a/40369685 - self.items.sort_by(|a, b| match a.offline.cmp(&b.offline) { + items.sort_by(|a, b| match a.offline.cmp(&b.offline) { Ordering::Equal => match a.players.cmp(&b.players).reverse() { Ordering::Equal => a.name.cmp(&b.name), other => other, @@ -128,36 +133,18 @@ impl ServersState { other => other, }); } +} - async fn server_fetch_task(app: Arc) -> TaskResult { - let update_interval = app.servers.read().await.update_interval; - - #[cfg(feature = "geolocation")] - app.locations.write().await.resolve(&IP::Local).await; - - async fn loop_body(app: Arc) -> anyhow::Result<()> { - let data = app - .client - .get(SERVER_LIST_URL) - .send() - .await - .with_context(|| "sending server list request")? - .error_for_status()? - .json::() - .await - .with_context(|| "parsing server list response")?; - - app.servers.write().await.update(app.clone(), data).await; - - Ok(()) - } +impl Default for Servers { + fn default() -> Self { + Self::new() + } +} - loop { - if let Err(err) = loop_body(Arc::clone(&app)).await { - app.events.read().await.error(err).await; - } +impl AppView for Servers {} - tokio::time::sleep(update_interval).await; - } +impl Name for Servers { + fn name(&self) -> String { + "Server List".to_owned() } } diff --git a/src/views/servers/state.rs b/src/views/servers/state.rs new file mode 100644 index 0000000..099646e --- /dev/null +++ b/src/views/servers/state.rs @@ -0,0 +1,18 @@ +use tui::widgets::TableState; + +use crate::datatypes::server::Server; +use crate::states::StatelessList; + +pub struct State { + pub items: Vec, + pub selection: StatelessList, +} + +impl State { + pub fn new() -> Self { + Self { + items: Vec::new(), + selection: StatelessList::new(TableState::default(), false), + } + } +} diff --git a/src/views/servers/tasks.rs b/src/views/servers/tasks.rs new file mode 100644 index 0000000..a50ca1b --- /dev/null +++ b/src/views/servers/tasks.rs @@ -0,0 +1,42 @@ +use std::sync::Arc; +use std::time::Duration; + +use anyhow::Context; + +use crate::constants::SERVER_LIST_URL; +#[cfg(feature = "geolocation")] +use crate::datatypes::geolocation::IP; +use crate::datatypes::server::ServerListJson; +use crate::states::app::TaskResult; +use crate::states::AppState; + +pub async fn server_fetch_task(app: Arc) -> TaskResult { + #[cfg(feature = "geolocation")] + app.locations.write().resolve(&IP::Local).await; + + async fn loop_body(app: Arc) -> anyhow::Result<()> { + let data = app + .client + .get(SERVER_LIST_URL) + .send() + .await + .with_context(|| "sending server list request")? + .error_for_status()? + .json::() + .await + .with_context(|| "parsing server list response")?; + + app.servers.update(Arc::clone(&app), data).await; + + Ok(()) + } + + let interval = Duration::from_secs(app.config.update_interval); + loop { + if let Err(err) = loop_body(Arc::clone(&app)).await { + app.events.read().error(err).await; + } + + tokio::time::sleep(interval).await; + } +} diff --git a/src/views/tabs.rs b/src/views/tabs.rs deleted file mode 100644 index 705e3d7..0000000 --- a/src/views/tabs.rs +++ /dev/null @@ -1,229 +0,0 @@ -use std::io; -use std::sync::Arc; - -use crossterm::event::KeyCode; - -use tui::layout::Rect; -use tui::{ - backend::CrosstermBackend, - layout::{Constraint, Direction, Layout}, - style::{Color, Modifier, Style}, - symbols::DOT, - text::Spans, - widgets::BorderType, - widgets::{Block, ListState, Tabs}, - Frame, -}; - -use futures::stream::{self, StreamExt}; - -use crate::app::AppAction; - -use crate::input::UserInput; -use crate::states::help::HotKey; -use crate::states::{AppState, StatelessList}; -use crate::views::{ - commits::CommitView, servers::ServerView, versions::VersionView, AppView, Drawable, HotKeys, - InputProcessor, Named, -}; - -#[derive(Copy, Clone)] -enum Tab { - Servers, - Versions, - Commits, -} - -impl Tab { - async fn name(&self, app: Arc) -> String { - match self { - Self::Servers => { - format!("servers [{}]", app.servers.read().await.count()) - } - Self::Versions => { - format!("versions [{}]", app.versions.read().await.count()) - } - Self::Commits => format!("commits [{}]", app.commits.read().await.items.len()), - } - } - - const fn all() -> [Self; 3] { - [Self::Servers {}, Self::Versions {}, Self::Commits {}] - } - - const fn tab_count() -> usize { - Self::all().len() - } -} - -impl From for usize { - fn from(value: Tab) -> usize { - match value { - Tab::Servers => 0, - Tab::Versions => 1, - Tab::Commits => 2, - } - } -} - -pub struct TabView { - state: StatelessList, - view_servers: ServerView, - view_versions: VersionView, - view_commits: CommitView, -} - -impl TabView { - pub fn new() -> Self { - let mut state = StatelessList::new(ListState::default(), true); - - state.select_first(Tab::tab_count()); - - Self { - state, - view_servers: ServerView::new(), - view_versions: VersionView::new(), - view_commits: CommitView::new(), - } - } - - fn selected_tab(&self) -> Tab { - *Tab::all() - .get(self.state.selected().unwrap_or_default()) - .unwrap_or(&Tab::Servers) - } - - fn select_tab(&mut self, tab: Tab) { - self.state.select_index(tab.into()); - } -} - -impl AppView for TabView {} - -impl Named for TabView { - fn name(&self) -> String { - format!( - "Tab: {}", - match self.selected_tab() { - Tab::Servers => self.view_servers.name(), - Tab::Versions => self.view_versions.name(), - Tab::Commits => self.view_commits.name(), - } - ) - } -} - -impl HotKeys for TabView { - fn hotkeys(&self) -> Vec { - let mut hotkeys = vec![ - HotKey { - description: "Go to next tab", - key: KeyCode::Tab, - modifiers: None, - }, - HotKey { - description: "Go Servers tab", - key: KeyCode::Char('s'), - modifiers: None, - }, - HotKey { - description: "Go versions tab", - key: KeyCode::Char('v'), - modifiers: None, - }, - HotKey { - description: "Go Commits tab", - key: KeyCode::Char('c'), - modifiers: None, - }, - ]; - - hotkeys.append(&mut match self.selected_tab() { - Tab::Servers => self.view_servers.hotkeys(), - Tab::Versions => self.view_versions.hotkeys(), - Tab::Commits => self.view_commits.hotkeys(), - }); - - hotkeys - } -} - -#[async_trait::async_trait] -impl InputProcessor for TabView { - async fn on_input(&mut self, input: &UserInput, app: Arc) -> Option { - match input { - UserInput::Char('s' | 'S') => { - self.select_tab(Tab::Servers); - None - } - UserInput::Char('v' | 'V') => { - self.select_tab(Tab::Versions); - None - } - UserInput::Char('c' | 'C') => { - self.select_tab(Tab::Commits); - None - } - UserInput::Tab => { - self.state.select_next(Tab::tab_count()); - None - } - // cannot move this to function because of match limitation for arms - // even if they implement same trait - _ => match self.selected_tab() { - Tab::Servers => self.view_servers.on_input(input, app).await, - Tab::Versions => self.view_versions.on_input(input, app).await, - Tab::Commits => self.view_commits.on_input(input, app).await, - }, - } - } -} - -#[async_trait::async_trait] -impl Drawable for TabView { - async fn draw( - &mut self, - f: &mut Frame>, - area: Rect, - app: Arc, - ) { - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Length(1), Constraint::Min(0)].as_ref()) - .split(area); - - let header = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Min(0)]) - .split(chunks[0]); - - let tabs = stream::iter(Tab::all()) - .then(|t| { - let cloned_app = Arc::clone(&app); - async move { Spans::from(t.name(cloned_app).await) } - }) - .collect() - .await; - - f.render_widget( - Tabs::new(tabs) - .block(Block::default().border_type(BorderType::Plain)) - .highlight_style( - Style::default() - .fg(Color::Blue) - .add_modifier(Modifier::BOLD), - ) - .divider(DOT) - .select(self.state.selected().unwrap_or_default()), - header[0], - ); - - // cannot move this to function because of match limitation for arms - // even if they implement same trait - match self.selected_tab() { - Tab::Servers => self.view_servers.draw(f, chunks[1], app).await, - Tab::Versions => self.view_versions.draw(f, chunks[1], app).await, - Tab::Commits => self.view_commits.draw(f, chunks[1], app).await, - }; - } -} diff --git a/src/views/tabs/draw.rs b/src/views/tabs/draw.rs new file mode 100644 index 0000000..46ca6f7 --- /dev/null +++ b/src/views/tabs/draw.rs @@ -0,0 +1,56 @@ +use std::io; +use std::sync::Arc; + +use tui::{ + backend::CrosstermBackend, + layout::{Constraint, Direction, Layout, Rect}, + style::{Color, Modifier, Style}, + symbols::DOT, + text::Spans, + widgets::BorderType, + widgets::{Block, Tabs as TuiTabs}, + Frame, +}; + +use crate::states::AppState; +use crate::views::Draw; + +use super::tab::Tab; +use super::Tabs; + +impl Draw for Tabs { + fn draw(&self, f: &mut Frame>, area: Rect, app: Arc) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(1), Constraint::Min(0)].as_ref()) + .split(area); + + let header = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Min(0)]) + .split(chunks[0]); + + let tabs = Tab::all().map(|t| Spans::from(t.name(Arc::clone(&app)))); + + f.render_widget( + TuiTabs::new(tabs.to_vec()) + .block(Block::default().border_type(BorderType::Plain)) + .highlight_style( + Style::default() + .fg(Color::Blue) + .add_modifier(Modifier::BOLD), + ) + .divider(DOT) + .select(self.selection.selected().unwrap_or_default()), + header[0], + ); + + // cannot move this to function because of match limitation for arms + // even if they implement same trait + // match self.selected_tab() { + // Tab::Servers => self.view_servers.draw(f, chunks[1], app).await, + // Tab::Versions => self.view_versions.draw(f, chunks[1], app).await, + // Tab::Commits => self.view_commits.draw(f, chunks[1], app).await, + // }; + } +} diff --git a/src/views/tabs/hotkeys.rs b/src/views/tabs/hotkeys.rs new file mode 100644 index 0000000..35d567e --- /dev/null +++ b/src/views/tabs/hotkeys.rs @@ -0,0 +1,42 @@ +use crossterm::event::KeyCode; + +use crate::datatypes::hotkey::HotKey; +use crate::views::HotKeys; + +use super::tab::Tab; +use super::Tabs; + +impl HotKeys for Tabs { + fn hotkeys(&self) -> Vec { + let mut hotkeys = vec![ + HotKey { + description: "Go to next tab", + key: KeyCode::Tab, + modifiers: None, + }, + HotKey { + description: "Go Servers tab", + key: KeyCode::Char('s'), + modifiers: None, + }, + HotKey { + description: "Go versions tab", + key: KeyCode::Char('v'), + modifiers: None, + }, + HotKey { + description: "Go Commits tab", + key: KeyCode::Char('c'), + modifiers: None, + }, + ]; + + // hotkeys.append(&mut match self.selected_tab() { + // Tab::Servers => self.view_servers.hotkeys(), + // Tab::Versions => self.view_versions.hotkeys(), + // Tab::Commits => self.view_commits.hotkeys(), + // }); + + hotkeys + } +} diff --git a/src/views/tabs/input.rs b/src/views/tabs/input.rs new file mode 100644 index 0000000..b8071b5 --- /dev/null +++ b/src/views/tabs/input.rs @@ -0,0 +1,41 @@ +use std::sync::Arc; + +use crate::app::AppAction; +use crate::input::UserInput; +use crate::states::AppState; +use crate::views::Input; + +use super::tab::Tab; +use super::Tabs; + +#[async_trait::async_trait] +impl Input for Tabs { + async fn on_input(&mut self, input: &UserInput, app: Arc) -> Option { + match input { + UserInput::Char('s' | 'S') => { + self.select_tab(Tab::Servers); + None + } + UserInput::Char('v' | 'V') => { + self.select_tab(Tab::Versions); + None + } + UserInput::Char('c' | 'C') => { + self.select_tab(Tab::Commits); + None + } + // UserInput::Tab => { + // self.state.write().selection.select_next(Tab::tab_count()); + // None + // } + // cannot move this to function because of match limitation for arms + // even if they implement same trait + _ => match self.selected_tab() { + _ => None + // Tab::Servers => app.servers.on_input(input, app).await, + // Tab::Versions => app.versions.on_input(input, app).await, + // Tab::Commits => app.commits.on_input(input, app).await, + }, + } + } +} diff --git a/src/views/tabs/mod.rs b/src/views/tabs/mod.rs new file mode 100644 index 0000000..6c8b58c --- /dev/null +++ b/src/views/tabs/mod.rs @@ -0,0 +1,55 @@ +mod draw; +mod hotkeys; +mod input; +mod tab; + +use std::sync::Arc; + +use tui::widgets::ListState; + +use crate::states::StatelessList; +use crate::views::{AppView, Name}; + +use tab::Tab; + +#[derive(Clone)] +pub struct Tabs { + selection: Arc>, +} + +impl Tabs { + pub fn new() -> Self { + let mut selection = StatelessList::new(ListState::default(), true); + + selection.select_first(Tab::tab_count()); + + Self { + selection: Arc::new(selection), + } + } + + fn selected_tab(&self) -> Tab { + *Tab::all() + .get(self.selection.selected().unwrap_or_default()) + .unwrap_or(&Tab::Servers) + } + + fn select_tab(&mut self, tab: Tab) { + //self.selection.select_index(tab.into()); + } +} + +impl AppView for Tabs {} + +impl Name for Tabs { + fn name(&self) -> String { + format!( + "Tab: {}", + match self.selected_tab() { + Tab::Servers => "servers", + Tab::Versions => "versions", + Tab::Commits => "commits", + } + ) + } +} diff --git a/src/views/tabs/tab.rs b/src/views/tabs/tab.rs new file mode 100644 index 0000000..29c5eff --- /dev/null +++ b/src/views/tabs/tab.rs @@ -0,0 +1,42 @@ +use std::sync::Arc; + +use crate::states::AppState; + +#[derive(Copy, Clone)] +pub enum Tab { + Servers, + Versions, + Commits, +} + +impl Tab { + pub fn name(&self, app: Arc) -> String { + match self { + Self::Servers => { + format!("servers [{}]", app.servers.count()) + } + Self::Versions => { + format!("versions [{}]", app.versions.count()) + } + Self::Commits => format!("commits [{}]", app.commits.count()), + } + } + + pub const fn all() -> [Self; 3] { + [Self::Servers {}, Self::Versions {}, Self::Commits {}] + } + + pub const fn tab_count() -> usize { + Self::all().len() + } +} + +impl From for usize { + fn from(value: Tab) -> usize { + match value { + Tab::Servers => 0, + Tab::Versions => 1, + Tab::Commits => 2, + } + } +} diff --git a/src/views/versions.rs b/src/views/versions/draw.rs similarity index 62% rename from src/views/versions.rs rename to src/views/versions/draw.rs index 1b67e5f..3db0371 100644 --- a/src/views/versions.rs +++ b/src/views/versions/draw.rs @@ -1,72 +1,24 @@ use std::io; use std::sync::Arc; -use bytesize::ByteSize; - -use crossterm::event::KeyCode; - use tui::{ backend::CrosstermBackend, layout::{Alignment, Constraint, Direction, Layout, Margin, Rect}, style::{Color, Modifier, Style}, text::Span, - widgets::{Block, Borders, Gauge, Row, Table, TableState}, + widgets::{Block, Borders, Gauge, Row, Table}, Frame, }; -use crate::app::AppAction; -use crate::datatypes::{ - game_version::{DownloadUrl, GameVersion}, - installation::InstallationKind, -}; -use crate::input::UserInput; -use crate::states::help::HotKey; -use crate::states::{AppState, StatelessList}; -use crate::views::{Drawable, HotKeys, InputProcessor, Named}; - -pub struct VersionView { - state: StatelessList, -} - -impl VersionView { - pub fn new() -> Self { - Self { - state: StatelessList::new(TableState::default(), false), - } - } -} - -impl Named for VersionView { - fn name(&self) -> String { - "Version List".to_owned() - } -} +use bytesize::ByteSize; -impl HotKeys for VersionView { - fn hotkeys(&self) -> Vec { - let mut hotkeys = vec![ - HotKey { - description: "Refresh version list", - key: KeyCode::F(5), - modifiers: None, - }, - HotKey { - description: "Install selected version", - key: KeyCode::Char('i'), - modifiers: None, - }, - HotKey { - description: "Run selected version (installs if needed)", - key: KeyCode::Enter, - modifiers: None, - }, - ]; +use crate::datatypes::game_version::DownloadUrl; +use crate::datatypes::game_version::GameVersion; +use crate::datatypes::installation::InstallationKind; +use crate::states::AppState; +use crate::views::Draw; - hotkeys.append(&mut self.state.hotkeys()); - - hotkeys - } -} +use super::Versions; enum Progress { Downloading { @@ -114,75 +66,9 @@ impl Progress { } } -#[async_trait::async_trait] -impl InputProcessor for VersionView { - async fn on_input(&mut self, input: &UserInput, app: Arc) -> Option { - match input { - UserInput::Refresh => { - let mut versions = app.versions.write().await; - - versions.refresh(app.clone()).await; - - if let Some(i) = self.state.selected() { - if i >= versions.count() { - self.state.unselect(); - } - } - - None - } - UserInput::Char('i' | 'I') => { - if let Some(i) = self.state.selected() { - Some(AppAction::InstallVersion( - app.versions.read().await.items[i].version.clone(), - )) - } else { - None - } - } - UserInput::Char('d' | 'D') => { - if let Some(i) = self.state.selected() { - Some(AppAction::UninstallVersion( - app.versions.read().await.items[i].version.clone(), - )) - } else { - None - } - } - UserInput::Char('a' | 'A') => { - if let Some(i) = self.state.selected() { - Some(AppAction::AbortVersionInstallation( - app.versions.read().await.items[i].version.clone(), - )) - } else { - None - } - } - UserInput::Enter => { - if let Some(i) = self.state.selected() { - Some(AppAction::LaunchVersion( - app.versions.read().await.items[i].version.clone(), - )) - } else { - None - } - } - _ => self - .state - .on_input(input, app.versions.read().await.count()), - } - } -} - -#[async_trait::async_trait] -impl Drawable for VersionView { - async fn draw( - &mut self, - f: &mut Frame>, - area: Rect, - app: Arc, - ) { - let versions = &app.versions.read().await.items; +impl Draw for Versions { + fn draw(&self, f: &mut Frame>, area: Rect, app: Arc) { + let versions = &self.state.read().items; let mut total_size = 0; let mut in_progress = Vec::new(); @@ -272,7 +158,7 @@ impl Drawable for VersionView { .add_modifier(Modifier::BOLD), ); - f.render_stateful_widget(table, chunks[0], &mut self.state.state); + //f.render_stateful_widget(table, chunks[0], &mut self.selection.state); if !in_progress.is_empty() { let mut progress_bars_constraints = Vec::new(); diff --git a/src/views/versions/hotkeys.rs b/src/views/versions/hotkeys.rs new file mode 100644 index 0000000..f6c051d --- /dev/null +++ b/src/views/versions/hotkeys.rs @@ -0,0 +1,32 @@ +use crossterm::event::KeyCode; + +use crate::datatypes::hotkey::HotKey; +use crate::views::HotKeys; + +use super::Versions; + +impl HotKeys for Versions { + fn hotkeys(&self) -> Vec { + let mut hotkeys = vec![ + HotKey { + description: "Refresh version list", + key: KeyCode::F(5), + modifiers: None, + }, + HotKey { + description: "Install selected version", + key: KeyCode::Char('i'), + modifiers: None, + }, + HotKey { + description: "Run selected version (installs if needed)", + key: KeyCode::Enter, + modifiers: None, + }, + ]; + + //hotkeys.append(&mut self.selection.hotkeys()); + + hotkeys + } +} diff --git a/src/views/versions/input.rs b/src/views/versions/input.rs new file mode 100644 index 0000000..bcdf2ad --- /dev/null +++ b/src/views/versions/input.rs @@ -0,0 +1,66 @@ +use std::sync::Arc; + +use crate::app::AppAction; +use crate::input::UserInput; +use crate::states::AppState; +use crate::views::Input; + +use super::Versions; + +#[async_trait::async_trait] +impl Input for Versions { + async fn on_input(&mut self, input: &UserInput, app: Arc) -> Option { + match input { + // UserInput::Refresh => { + // let mut versions = self.state.write(); + + // versions.refresh(app.clone()).await; + + // if let Some(i) = self.selection.selected() { + // if i >= versions.items.len() { + // self.selection.unselect(); + // } + // } + + // None + // } + // UserInput::Char('i' | 'I') => { + // if let Some(i) = self.selection.selected() { + // Some(AppAction::InstallVersion( + // self.state.read().items[i].version.clone(), + // )) + // } else { + // None + // } + // } + // UserInput::Char('d' | 'D') => { + // if let Some(i) = self.selection.selected() { + // Some(AppAction::UninstallVersion( + // self.state.read().items[i].version.clone(), + // )) + // } else { + // None + // } + // } + // UserInput::Char('a' | 'A') => { + // if let Some(i) = self.selection.selected() { + // Some(AppAction::AbortVersionInstallation( + // self.state.read().items[i].version.clone(), + // )) + // } else { + // None + // } + // } + // UserInput::Enter => { + // if let Some(i) = self.selection.selected() { + // Some(AppAction::LaunchVersion( + // self.state.read().items[i].version.clone(), + // )) + // } else { + // None + // } + // } + _ => None, //self.selection.on_input(input, self.count()), + } + } +} diff --git a/src/views/versions/mod.rs b/src/views/versions/mod.rs new file mode 100644 index 0000000..0ac6e3b --- /dev/null +++ b/src/views/versions/mod.rs @@ -0,0 +1,35 @@ +mod draw; +mod hotkeys; +mod input; +mod state; + +use std::sync::Arc; + +use parking_lot::RwLock; + +use crate::views::Name; + +use state::State; + +#[derive(Clone)] +pub struct Versions { + pub state: Arc>, +} + +impl Versions { + pub fn new() -> Self { + Self { + state: Arc::new(RwLock::new(State::new())), + } + } + + pub fn count(&self) -> usize { + self.state.read().items.len() + } +} + +impl Name for Versions { + fn name(&self) -> String { + "Version List".to_owned() + } +} diff --git a/src/states/versions.rs b/src/views/versions/state.rs similarity index 90% rename from src/states/versions.rs rename to src/views/versions/state.rs index 0e73d5c..cd9eb7d 100644 --- a/src/states/versions.rs +++ b/src/views/versions/state.rs @@ -9,9 +9,8 @@ use futures::stream::StreamExt; use tokio::fs; use tokio::io::AsyncWriteExt; use tokio::process::Command; -use tokio::sync::RwLock; +use tui::widgets::TableState; -use crate::config::AppConfig; use crate::datatypes::{ game_version::{DownloadUrl, GameVersion}, installation::{Installation, InstallationKind}, @@ -19,17 +18,18 @@ use crate::datatypes::{ value_sorted_map::ValueSortedMap, }; use crate::states::app::{AppState, TaskResult}; +use crate::states::StatelessList; -pub struct VersionsState { +pub struct State { pub items: ValueSortedMap, - pub install_dir_error: Option, + pub selection: StatelessList, } -impl VersionsState { - pub async fn new(_: &AppConfig) -> Self { +impl State { + pub fn new() -> Self { Self { items: ValueSortedMap::new(), - install_dir_error: None, + selection: StatelessList::new(TableState::default(), false), } } @@ -37,32 +37,24 @@ impl VersionsState { self.spawn_installation_finder(app.clone()).await; } - pub fn count(&self) -> usize { - self.items.len() - } - - pub async fn spawn_installation_finder(&mut self, app: Arc) { + pub async fn spawn_installation_finder(&self, app: Arc) { app.watch_task(tokio::task::spawn(Self::fs_installation_finder_task( - app.config.clone(), - app.versions.clone(), + Arc::clone(&app), ))) .await; } - async fn fs_installation_finder_task( - app: AppConfig, - versions: Arc>, - ) -> TaskResult { + async fn fs_installation_finder_task(app: Arc) -> TaskResult { log::debug!( "installation directory: {}", - &app.dirs.installations_dir.display() + &app.config.dirs.installations_dir.display() ); - let mut dirs = fs::read_dir(app.dirs.installations_dir) + let mut dirs = fs::read_dir(&app.config.dirs.installations_dir) .await .with_context(|| "Unable to read installation directory")?; - let mut versions = versions.write().await; + let mut versions = app.versions.state.write(); while let Some(fork_dirs) = dirs .next_entry() @@ -136,7 +128,7 @@ impl VersionsState { }); // grab versions from servers state - for server in &app.servers.read().await.items { + for server in &app.servers.state.read().items { // these are the ones we filtered out if self.items.get(&server.version).is_some() { continue; @@ -154,7 +146,6 @@ impl VersionsState { self.spawn_installation_finder(app.clone()).await; app.events .read() - .await .event("Refreshed installation list (still looking in file system)") .await; } @@ -162,7 +153,7 @@ impl VersionsState { pub async fn version_discovered(app: Arc, version: &GameVersion) { log::debug!("discovered: {}", version); - let mut versions = app.versions.write().await; + let mut versions = app.versions.state.write(); if let Some(existing) = versions.items.get(version).cloned() { if !matches!(&existing.kind, InstallationKind::Discovered) { @@ -174,7 +165,6 @@ impl VersionsState { app.events .read() - .await .event(&format!("Discovered {}", version)) .await; @@ -208,11 +198,10 @@ impl VersionsState { app.events .read() - .await .event(&format!("Downloading {}", version)) .await; - match app.versions.read().await.items.get(&version) { + match app.versions.state.read().items.get(&version) { Some(Installation { kind: InstallationKind::Discovered, .. @@ -257,7 +246,7 @@ impl VersionsState { let mut progress = 0; - versions.write().await.items.insert( + versions.state.write().items.insert( version.clone(), Installation { version: version.clone(), @@ -279,7 +268,7 @@ impl VersionsState { progress += chunk.len(); - let mut versions = versions.write().await; + let mut versions = versions.state.write(); let previous = versions.items.insert( version.clone(), Installation { @@ -314,7 +303,7 @@ impl VersionsState { } } - versions.write().await.items.insert( + versions.state.write().items.insert( version.clone(), Installation { version: version.clone(), @@ -329,7 +318,6 @@ impl VersionsState { app.events .read() - .await .event(&format!("Extracting {}", version)) .await; @@ -354,7 +342,7 @@ impl VersionsState { ); } - versions.write().await.items.insert( + versions.state.write().items.insert( version.clone(), Installation { version: version.clone(), @@ -368,7 +356,6 @@ impl VersionsState { app.events .read() - .await .event(&format!("Installed version {}", version)) .await; @@ -376,7 +363,7 @@ impl VersionsState { } pub async fn abort_installation(app: Arc, version: GameVersion) -> TaskResult { - let mut versions = app.versions.write().await; + let mut versions = app.versions.state.write(); if matches!( versions.items.get(&version), @@ -395,7 +382,6 @@ impl VersionsState { app.events .read() - .await .event(&format!("Aborted installation of {}", version)) .await; } else { @@ -411,7 +397,7 @@ impl VersionsState { path.push(PathBuf::from(version.clone())); // lock in advance - let mut versions = app.versions.write().await; + let mut versions = app.versions.state.write(); match versions.items.get(&version) { Some(Installation { @@ -433,7 +419,6 @@ impl VersionsState { app.events .read() - .await .event(&format!("Uninstalled {}", version)) .await; @@ -447,14 +432,13 @@ impl VersionsState { ) -> TaskResult { app.events .read() - .await .event(&format!("Launching {}", version)) .await; if !matches!( app.versions + .state .read() - .await .items .get(&version) .ok_or_else(|| anyhow!("desync: version not in installation list"))?, diff --git a/src/views/world.rs b/src/views/world.rs index 5dffcaa..e11f748 100644 --- a/src/views/world.rs +++ b/src/views/world.rs @@ -16,15 +16,15 @@ use crossterm::event::KeyCode; use crate::app::AppAction; use crate::datatypes::geolocation::IP; +use crate::datatypes::hotkey::HotKey; use crate::input::UserInput; -use crate::states::help::HotKey; use crate::states::AppState; -use crate::views::{AppView, Drawable, HotKeys, InputProcessor, Named}; +use crate::views::{AppView, Draw, HotKeys, Input, Name}; pub struct World {} #[async_trait::async_trait] -impl InputProcessor for World { +impl Input for World { async fn on_input(&mut self, input: &UserInput, _: Arc) -> Option { match input { UserInput::Char('m' | 'M') | UserInput::Back => Some(AppAction::CloseView), @@ -35,7 +35,7 @@ impl InputProcessor for World { impl AppView for World {} -impl Named for World { +impl Name for World { fn name(&self) -> String { "World Map".to_owned() } @@ -58,18 +58,12 @@ impl HotKeys for World { } } -#[async_trait::async_trait] -impl Drawable for World { +impl Draw for World { // TODO: render selected with labels by default, all without labels // TODO: zoom and map navigation - async fn draw( - &mut self, - f: &mut Frame>, - area: Rect, - app: Arc, - ) { - let locations = &app.locations.read().await.items; - let servers = &app.servers.read().await.items; + fn draw(&self, f: &mut Frame>, area: Rect, app: Arc) { + let locations = &app.locations.read().items; + let servers = &app.servers.state.read().items; let map = Canvas::default() .block(Block::default().borders(Borders::ALL))