From c7483a0d7601ba62d3df4663c5d2096757cfa831 Mon Sep 17 00:00:00 2001 From: Eugene Date: Fri, 10 Sep 2021 23:08:46 +0300 Subject: [PATCH 1/5] Done for today --- src/app.rs | 4 +- src/datatypes/hotkey.rs | 43 ++++ src/datatypes/mod.rs | 1 + src/datatypes/server.rs | 5 +- src/states/app.rs | 79 +++--- src/states/commits.rs | 50 ---- src/states/help.rs | 44 +--- src/states/mod.rs | 8 +- src/views/{commits.rs => commits/draw.rs} | 57 +---- src/views/commits/mod.rs | 98 ++++++++ src/views/commits/state.rs | 11 + src/views/events.rs | 4 +- src/views/help.rs | 10 +- src/views/mod.rs | 10 +- src/views/{servers.rs => servers/draw.rs} | 97 +------- src/views/servers/hotkeys.rs | 33 +++ src/views/servers/input.rs | 41 ++++ .../servers.rs => views/servers/mod.rs} | 101 ++++---- src/views/servers/state.rs | 9 + src/views/servers/tasks.rs | 42 ++++ src/views/tabs.rs | 229 ------------------ src/views/tabs/draw.rs | 70 ++++++ src/views/tabs/hotkeys.rs | 42 ++++ src/views/tabs/input.rs | 40 +++ src/views/tabs/mod.rs | 50 ++++ src/views/tabs/tab.rs | 42 ++++ src/views/{versions.rs => versions/draw.rs} | 130 +--------- src/views/versions/hotkeys.rs | 32 +++ src/views/versions/input.rs | 68 ++++++ src/views/versions/mod.rs | 37 +++ .../versions.rs => views/versions/state.rs} | 8 +- src/views/world.rs | 10 +- 32 files changed, 799 insertions(+), 706 deletions(-) create mode 100644 src/datatypes/hotkey.rs delete mode 100644 src/states/commits.rs rename src/views/{commits.rs => commits/draw.rs} (62%) create mode 100644 src/views/commits/mod.rs create mode 100644 src/views/commits/state.rs rename src/views/{servers.rs => servers/draw.rs} (76%) create mode 100644 src/views/servers/hotkeys.rs create mode 100644 src/views/servers/input.rs rename src/{states/servers.rs => views/servers/mod.rs} (57%) create mode 100644 src/views/servers/state.rs create mode 100644 src/views/servers/tasks.rs delete mode 100644 src/views/tabs.rs create mode 100644 src/views/tabs/draw.rs create mode 100644 src/views/tabs/hotkeys.rs create mode 100644 src/views/tabs/input.rs create mode 100644 src/views/tabs/mod.rs create mode 100644 src/views/tabs/tab.rs rename src/views/{versions.rs => versions/draw.rs} (64%) create mode 100644 src/views/versions/hotkeys.rs create mode 100644 src/views/versions/input.rs create mode 100644 src/views/versions/mod.rs rename src/{states/versions.rs => views/versions/state.rs} (98%) diff --git a/src/app.rs b/src/app.rs index a21d9e3..8e6ed74 100644 --- a/src/app.rs +++ b/src/app.rs @@ -13,7 +13,7 @@ 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 AppAction { @@ -60,7 +60,7 @@ impl App { panicked: panic_bool, }; - 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 {})); 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/states/app.rs b/src/states/app.rs index e85d5e3..d4e4fbc 100644 --- a/src/states/app.rs +++ b/src/states/app.rs @@ -9,22 +9,24 @@ use anyhow::Result; use crate::app::AppAction; 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: Arc>, #[cfg(feature = "geolocation")] pub locations: Arc>, - pub servers: Arc>, + pub servers: Servers, pub events: Arc>, pub help: Mutex, @@ -38,13 +40,13 @@ impl AppState { pub async fn new(config: AppConfig, panic_bool: Arc) -> Arc { #[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 versions = Versions::new(); + let servers = Servers::new(); let events = Arc::new(RwLock::new(EventsState::new(&config).await)); let instance = Arc::new(Self { - commits: Arc::new(RwLock::new(CommitState::new().await)), - versions: versions.clone(), + commits: Commits::new(), + //versions: versions.clone(), #[cfg(feature = "geolocation")] locations: locations.clone(), servers: servers.clone(), @@ -61,44 +63,43 @@ impl AppState { }); events.write().await.run(instance.clone()).await; - servers.write().await.run(instance.clone()).await; + servers.run(instance.clone()).await; #[cfg(feature = "geolocation")] locations.write().await.run(instance.clone()).await; - versions.write().await.run(instance.clone()).await; - + //versions.write().await.run(instance.clone()).await; instance } 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]) { diff --git a/src/states/commits.rs b/src/states/commits.rs deleted file mode 100644 index b72c358..0000000 --- a/src/states/commits.rs +++ /dev/null @@ -1,50 +0,0 @@ -use std::sync::Arc; - -use anyhow::Context; - -use crate::constants::GITHUB_REPO_COMMIT_ENDPOINT_URL; -use crate::datatypes::commit::{Commit, CommitsJson}; -use crate::states::app::{AppState, TaskResult}; - -pub struct CommitState { - pub items: Vec, -} - -impl CommitState { - pub async fn new() -> Self { - Self { items: Vec::new() } - } - - pub fn count(&self) -> usize { - self.items.len() - } - - pub async fn load(app: Arc) -> TaskResult { - let mut headers = reqwest::header::HeaderMap::new(); - headers.insert( - reqwest::header::ACCEPT, - "application/vnd.github.v3+json".parse().unwrap(), - ); - - let commit_range = app - .client - .get(GITHUB_REPO_COMMIT_ENDPOINT_URL) - .headers(headers) - .send() - .await - .with_context(|| "sending commits request")? - .error_for_status()? - .json::() - .await - .with_context(|| "parsing commits response")?; - - app.commits.write().await.update(commit_range); - - Ok(()) - } - - pub fn update(&mut self, data: CommitsJson) { - self.items - .append(&mut data.0.iter().map(Commit::from).collect()); - } -} 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/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/views/commits.rs b/src/views/commits/draw.rs similarity index 62% rename from src/views/commits.rs rename to src/views/commits/draw.rs index 50da07d..41fa113 100644 --- a/src/views/commits.rs +++ b/src/views/commits/draw.rs @@ -6,56 +6,17 @@ 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, - - 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()) - } -} +use super::Commits; #[async_trait::async_trait] -impl Drawable for CommitView { +impl Draw for Commits { async fn draw( &mut self, f: &mut Frame>, @@ -67,7 +28,7 @@ impl Drawable for CommitView { .constraints([Constraint::Length(60), Constraint::Min(0)]) .split(area); - let commits = &app.commits.read().await.items; + let commits = &self.state.read().await.items; let items: Vec = commits .iter() @@ -83,9 +44,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.selection.state); - if let Some(i) = self.state.state.selected() { + if let Some(i) = self.selection.state.selected() { let chunks = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Length(3), Constraint::Min(0)]) @@ -120,7 +81,7 @@ impl Drawable for CommitView { if !self.loaded { if !app.config.offline { - app.watch_task(tokio::spawn(CommitState::load(Arc::clone(&app)))) + app.watch_task(tokio::spawn(self.load(Arc::clone(&app)))) .await; } diff --git a/src/views/commits/mod.rs b/src/views/commits/mod.rs new file mode 100644 index 0000000..db99a86 --- /dev/null +++ b/src/views/commits/mod.rs @@ -0,0 +1,98 @@ +mod draw; +mod state; + +use std::sync::Arc; + +use anyhow::Context; + +use tokio::sync::RwLock; + +use tui::widgets::ListState; + +use crate::app::AppAction; +use crate::constants::GITHUB_REPO_COMMIT_ENDPOINT_URL; +use crate::datatypes::commit::{Commit, CommitsJson}; +use crate::datatypes::hotkey::HotKey; +use crate::input::UserInput; +use crate::states::app::TaskResult; +use crate::states::{AppState, StatelessList}; +use crate::views::{HotKeys, Input, Name}; + +use state::State; + +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, + + selection: StatelessList, + state: Arc>, +} + +impl Commits { + pub fn new() -> Self { + Self { + loaded: false, + selection: StatelessList::new(ListState::default(), false), + state: Arc::new(RwLock::new(State::new())), + } + } + + pub async fn count(&self) -> usize { + self.state.read().await.items.len() + } + + pub async fn load(&self, app: Arc) -> TaskResult { + let mut headers = reqwest::header::HeaderMap::new(); + headers.insert( + reqwest::header::ACCEPT, + "application/vnd.github.v3+json".parse().unwrap(), + ); + + let commit_range = app + .client + .get(GITHUB_REPO_COMMIT_ENDPOINT_URL) + .headers(headers) + .send() + .await + .with_context(|| "sending commits request")? + .error_for_status()? + .json::() + .await + .with_context(|| "parsing commits response")?; + + self.update(commit_range); + + Ok(()) + } + + pub async fn update(&self, data: CommitsJson) { + self.state + .write() + .await + .items + .append(&mut data.0.iter().map(Commit::from).collect()); + } +} + +#[async_trait::async_trait] +impl Name for Commits { + fn name(&self) -> String { + "Recent Commit List".to_owned() + } +} + +impl HotKeys for Commits { + fn hotkeys(&self) -> Vec { + self.selection.hotkeys() + } +} + +#[async_trait::async_trait] +impl Input for Commits { + async fn on_input(&mut self, input: &UserInput, app: Arc) -> Option { + self.selection + .on_input(input, self.state.read().await.count()) + } +} diff --git a/src/views/commits/state.rs b/src/views/commits/state.rs new file mode 100644 index 0000000..f3b001a --- /dev/null +++ b/src/views/commits/state.rs @@ -0,0 +1,11 @@ +use crate::datatypes::commit::Commit; + +pub struct State { + pub items: Vec, +} + +impl State { + pub fn new() -> Self { + Self { items: Vec::new() } + } +} diff --git a/src/views/events.rs b/src/views/events.rs index 863fef1..22c3627 100644 --- a/src/views/events.rs +++ b/src/views/events.rs @@ -12,12 +12,12 @@ 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 { +impl Draw for EventsView { async fn draw( &mut self, f: &mut Frame>, diff --git a/src/views/help.rs b/src/views/help.rs index f01ecf3..7170aff 100644 --- a/src/views/help.rs +++ b/src/views/help.rs @@ -14,17 +14,17 @@ 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() } @@ -42,7 +42,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), @@ -52,7 +52,7 @@ impl InputProcessor for Help { } #[async_trait::async_trait] -impl Drawable for Help { +impl Draw for Help { async fn draw( &mut self, f: &mut Frame>, diff --git a/src/views/mod.rs b/src/views/mod.rs index d426974..3d95b96 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)] @@ -28,7 +28,7 @@ pub enum ViewType { } #[async_trait::async_trait] -pub trait Drawable { +pub trait Draw { async fn draw( &mut self, f: &mut Frame>, @@ -38,7 +38,7 @@ pub trait Drawable { } #[async_trait::async_trait] -pub trait InputProcessor { +pub trait Input { async fn on_input(&mut self, input: &UserInput, app: Arc) -> Option; } @@ -48,8 +48,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 76% rename from src/views/servers.rs rename to src/views/servers/draw.rs index 44c6f72..b51098b 100644 --- a/src/views/servers.rs +++ b/src/views/servers/draw.rs @@ -1,113 +1,34 @@ 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 { +impl Draw for Servers { async fn draw( &mut self, f: &mut Frame>, area: Rect, app: Arc, ) { - let servers = &app.servers.read().await.items; + let servers = self.state.read().await.items; let mut count_online = 0; let mut count_no_players = 0; @@ -227,13 +148,13 @@ impl Drawable for ServerView { ); // draw server info - if let Some(selected) = self.state.selected().map(|s| &servers[s]) { + if let Some(selected) = self.selection.selected().map(|s| &servers[s]) { draw_server_info(f, chunks[1], Arc::clone(&app), selected).await; } 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.selection.state); } } diff --git a/src/views/servers/hotkeys.rs b/src/views/servers/hotkeys.rs new file mode 100644 index 0000000..e4e2a48 --- /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.selection.hotkeys()); + + hotkeys + } +} diff --git a/src/views/servers/input.rs b/src/views/servers/input.rs new file mode 100644 index 0000000..cec2dd4 --- /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.selection.selected() { + Some(AppAction::InstallVersion( + self.state.read().await.items[i].version.clone(), + )) + } else { + None + } + } + UserInput::Enter => { + if let Some(i) = self.selection.selected() { + let server = &self.state.read().await.items[i]; + + Some(AppAction::ConnectToServer { + version: server.version.clone(), + address: server.address.clone(), + }) + } else { + None + } + } + _ => self.selection.on_input(input, self.count().await), + } + } +} diff --git a/src/states/servers.rs b/src/views/servers/mod.rs similarity index 57% rename from src/states/servers.rs rename to src/views/servers/mod.rs index 82a2618..d7cf2fa 100644 --- a/src/states/servers.rs +++ b/src/views/servers/mod.rs @@ -1,32 +1,40 @@ +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 tokio::sync::RwLock; +use tui::widgets::TableState; -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, StatelessList}; +use crate::views::Name; + +use state::State; -pub struct ServersState { - pub items: Vec, - update_interval: Duration, +use crate::views::AppView; + +pub struct Servers { + state: Arc>, + selection: StatelessList, } -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())), + selection: StatelessList::new(TableState::default(), false), } } - 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 +44,7 @@ impl ServersState { download: DownloadUrl::new("http://evil.exploit"), }; - self.items.push(Server { + self.state.write().await.items.push(Server { name: "TEST SERVER PLEASE IGNORE".to_owned(), address: Address { ip: ip.clone(), @@ -54,27 +62,26 @@ impl ServersState { #[cfg(feature = "geolocation")] app.locations.write().await.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() + pub async fn count(&self) -> usize { + self.state.read().await.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 mut items = self.state.write().await.items; + + let mut previously_online: HashMap = + items.iter_mut().map(|i| (i.address.clone(), i)).collect(); let mut created_servers: Vec = Vec::new(); @@ -87,7 +94,7 @@ 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; known_server.version = version; @@ -110,7 +117,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 +127,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 +135,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..a397d44 --- /dev/null +++ b/src/views/servers/state.rs @@ -0,0 +1,9 @@ +pub struct State { + pub items: Vec, +} + +impl State { + pub fn new() -> Self { + Self { items: Vec::new() } + } +} diff --git a/src/views/servers/tasks.rs b/src/views/servers/tasks.rs new file mode 100644 index 0000000..67ed4b4 --- /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().await.resolve(&IP::Local).await; + + async fn loop_body(app: AppState) -> 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(()) + } + + let interval = Duration::from_secs(app.config.update_interval); + loop { + if let Err(err) = loop_body(app).await { + app.events.read().await.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..3c7d8e0 --- /dev/null +++ b/src/views/tabs/draw.rs @@ -0,0 +1,70 @@ +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 futures::stream::{self, StreamExt}; + +use crate::states::AppState; +use crate::views::Draw; + +use super::tab::Tab; +use super::Tabs; + +#[async_trait::async_trait] +impl Draw for Tabs { + 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( + TuiTabs::new(tabs) + .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..fd655d5 --- /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..453e161 --- /dev/null +++ b/src/views/tabs/input.rs @@ -0,0 +1,40 @@ +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.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() { + 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..03c9c90 --- /dev/null +++ b/src/views/tabs/mod.rs @@ -0,0 +1,50 @@ +mod draw; +mod hotkeys; +mod input; +mod tab; + +use tui::widgets::ListState; + +use crate::states::StatelessList; +use crate::views::{AppView, Name}; + +use tab::Tab; + +pub struct Tabs { + selection: StatelessList, +} + +impl Tabs { + pub fn new() -> Self { + let mut selection = StatelessList::new(ListState::default(), true); + + selection.select_first(Tab::tab_count()); + + Self { 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..83bb4ec --- /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 { + 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()), + } + } + + 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 64% rename from src/views/versions.rs rename to src/views/versions/draw.rs index 1b67e5f..7f5b477 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() - } -} - -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 bytesize::ByteSize; - hotkeys.append(&mut self.state.hotkeys()); +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 - } -} +use super::Versions; enum Progress { Downloading { @@ -115,74 +67,14 @@ 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 { +impl Draw for Versions { async fn draw( &mut self, f: &mut Frame>, area: Rect, app: Arc, ) { - let versions = &app.versions.read().await.items; + let versions = &self.state.read().await.items; let mut total_size = 0; let mut in_progress = Vec::new(); @@ -272,7 +164,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..e6542e3 --- /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..810ff62 --- /dev/null +++ b/src/views/versions/input.rs @@ -0,0 +1,68 @@ +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().await; + + versions.refresh(app.clone()).await; + + if let Some(i) = self.selection.selected() { + if i >= versions.count() { + self.selection.unselect(); + } + } + + None + } + UserInput::Char('i' | 'I') => { + if let Some(i) = self.selection.selected() { + Some(AppAction::InstallVersion( + self.state.read().await.items[i].version.clone(), + )) + } else { + None + } + } + UserInput::Char('d' | 'D') => { + if let Some(i) = self.selection.selected() { + Some(AppAction::UninstallVersion( + self.state.read().await.items[i].version.clone(), + )) + } else { + None + } + } + UserInput::Char('a' | 'A') => { + if let Some(i) = self.selection.selected() { + Some(AppAction::AbortVersionInstallation( + self.state.read().await.items[i].version.clone(), + )) + } else { + None + } + } + UserInput::Enter => { + if let Some(i) = self.selection.selected() { + Some(AppAction::LaunchVersion( + self.state.read().await.items[i].version.clone(), + )) + } else { + None + } + } + _ => self + .selection + .on_input(input, self.state.read().await.count()), + } + } +} diff --git a/src/views/versions/mod.rs b/src/views/versions/mod.rs new file mode 100644 index 0000000..9434ca1 --- /dev/null +++ b/src/views/versions/mod.rs @@ -0,0 +1,37 @@ +mod draw; +mod hotkeys; +mod input; +mod state; + +use std::sync::Arc; + +use crossterm::event::KeyCode; + +use tokio::sync::RwLock; + +use tui::widgets::TableState; + +use crate::states::StatelessList; +use crate::views::Name; + +use state::State; + +pub struct Versions { + state: Arc>, + selection: StatelessList, +} + +impl Versions { + pub fn new() -> Self { + Self { + state: Arc::new(RwLock::new(State::new())), + selection: StatelessList::new(TableState::default(), false), + } + } +} + +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 98% rename from src/states/versions.rs rename to src/views/versions/state.rs index 0e73d5c..1e6f15c 100644 --- a/src/states/versions.rs +++ b/src/views/versions/state.rs @@ -20,16 +20,14 @@ use crate::datatypes::{ }; use crate::states::app::{AppState, TaskResult}; -pub struct VersionsState { +pub struct State { pub items: ValueSortedMap, - pub install_dir_error: Option, } -impl VersionsState { - pub async fn new(_: &AppConfig) -> Self { +impl State { + pub fn new() -> Self { Self { items: ValueSortedMap::new(), - install_dir_error: None, } } diff --git a/src/views/world.rs b/src/views/world.rs index 5dffcaa..f26135c 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() } @@ -59,7 +59,7 @@ 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( From 0413b460280d8d15050c2db3d89e47e065f21669 Mon Sep 17 00:00:00 2001 From: Eugene Date: Sat, 11 Sep 2021 13:25:10 +0300 Subject: [PATCH 2/5] It compiles now --- Cargo.lock | 1 + Cargo.toml | 6 ++- src/app.rs | 8 ++- src/main.rs | 1 + src/states/app.rs | 16 +++--- src/states/events.rs | 4 +- src/states/locations.rs | 2 +- src/views/commits/draw.rs | 27 ++++------ src/views/commits/mod.rs | 62 +++++----------------- src/views/commits/state.rs | 45 +++++++++++++++- src/views/events.rs | 10 +--- src/views/help.rs | 12 +---- src/views/mod.rs | 8 +-- src/views/servers/draw.rs | 20 +++----- src/views/servers/hotkeys.rs | 2 +- src/views/servers/input.rs | 10 ++-- src/views/servers/mod.rs | 26 +++++----- src/views/servers/state.rs | 11 +++- src/views/servers/tasks.rs | 10 ++-- src/views/tabs/draw.rs | 30 +++-------- src/views/tabs/hotkeys.rs | 10 ++-- src/views/tabs/input.rs | 15 +++--- src/views/tabs/mod.rs | 11 ++-- src/views/tabs/tab.rs | 8 +-- src/views/versions/draw.rs | 12 ++--- src/views/versions/hotkeys.rs | 2 +- src/views/versions/input.rs | 96 +++++++++++++++++------------------ src/views/versions/mod.rs | 16 +++--- src/views/versions/state.rs | 53 ++++++++----------- src/views/world.rs | 12 ++--- 30 files changed, 247 insertions(+), 299 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c6fc1c1..48fca0c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1126,6 +1126,7 @@ dependencies = [ "dirs-next", "futures", "log", + "parking_lot", "reqwest", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 0db0ea8..d2ab15c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,10 @@ async-trait = "0.1.51" bytesize = "1.1.0" anyhow = "1.0.43" +[dependencies.parking_lot] +version = "0.11.2" +features = ["send_guard"] + [dependencies.zip] version = "0.5.13" default-features = false @@ -40,7 +44,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/src/app.rs b/src/app.rs index 8e6ed74..0dc64b3 100644 --- a/src/app.rs +++ b/src/app.rs @@ -77,22 +77,20 @@ impl App { 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)); } } } diff --git a/src/main.rs b/src/main.rs index cbb775a..f4ce15b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -122,6 +122,7 @@ fn _main() -> Result<(), Box> { rt.block_on(app.draw(f)); })?; + // TODO: send input events to async channel instead match rx.recv()? { EventOrTick::Input(event) => rt.block_on(app.on_input(&event)), EventOrTick::Tick => {} diff --git a/src/states/app.rs b/src/states/app.rs index d4e4fbc..85f5261 100644 --- a/src/states/app.rs +++ b/src/states/app.rs @@ -1,7 +1,7 @@ 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::task::JoinHandle; use anyhow::Result; @@ -23,7 +23,7 @@ pub type TaskResult = Result<()>; pub struct AppState { pub config: AppConfig, pub commits: Commits, - //pub versions: Arc>, + pub versions: Versions, #[cfg(feature = "geolocation")] pub locations: Arc>, pub servers: Servers, @@ -46,7 +46,7 @@ impl AppState { let instance = Arc::new(Self { commits: Commits::new(), - //versions: versions.clone(), + versions: versions.clone(), #[cfg(feature = "geolocation")] locations: locations.clone(), servers: servers.clone(), @@ -62,10 +62,10 @@ impl AppState { panic_bool, }); - events.write().await.run(instance.clone()).await; + events.write().run(instance.clone()).await; servers.run(instance.clone()).await; #[cfg(feature = "geolocation")] - locations.write().await.run(instance.clone()).await; + locations.write().run(instance.clone()).await; //versions.write().await.run(instance.clone()).await; instance } @@ -103,7 +103,7 @@ impl AppState { } 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); } @@ -132,7 +132,7 @@ impl AppState { } 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..e639578 100644 --- a/src/states/events.rs +++ b/src/states/events.rs @@ -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/locations.rs b/src/states/locations.rs index c174f2a..a8c49c4 100644 --- a/src/states/locations.rs +++ b/src/states/locations.rs @@ -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/views/commits/draw.rs b/src/views/commits/draw.rs index 41fa113..e59cb3f 100644 --- a/src/views/commits/draw.rs +++ b/src/views/commits/draw.rs @@ -15,20 +15,14 @@ use crate::views::Draw; use super::Commits; -#[async_trait::async_trait] impl Draw for Commits { - async fn draw( - &mut self, - f: &mut Frame>, - area: Rect, - app: Arc, - ) { + 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 = &self.state.read().await.items; + let commits = &self.state.read().items; let items: Vec = commits .iter() @@ -44,9 +38,9 @@ impl Draw for Commits { ) .highlight_style(Style::default().bg(Color::DarkGray)); - f.render_stateful_widget(list, chunks[0], &mut self.selection.state); + f.render_stateful_widget(list, chunks[0], &mut self.state.write().selection.state); - if let Some(i) = self.selection.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)]) @@ -79,13 +73,12 @@ impl Draw for Commits { ); } - if !self.loaded { - if !app.config.offline { - app.watch_task(tokio::spawn(self.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 index db99a86..2eb618d 100644 --- a/src/views/commits/mod.rs +++ b/src/views/commits/mod.rs @@ -3,30 +3,25 @@ mod state; use std::sync::Arc; -use anyhow::Context; - -use tokio::sync::RwLock; - -use tui::widgets::ListState; +use parking_lot::RwLock; use crate::app::AppAction; -use crate::constants::GITHUB_REPO_COMMIT_ENDPOINT_URL; -use crate::datatypes::commit::{Commit, CommitsJson}; + use crate::datatypes::hotkey::HotKey; use crate::input::UserInput; -use crate::states::app::TaskResult; -use crate::states::{AppState, StatelessList}; + +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, - selection: StatelessList, state: Arc>, } @@ -34,49 +29,21 @@ impl Commits { pub fn new() -> Self { Self { loaded: false, - selection: StatelessList::new(ListState::default(), false), + state: Arc::new(RwLock::new(State::new())), } } - pub async fn count(&self) -> usize { - self.state.read().await.items.len() + pub fn count(&self) -> usize { + self.state.read().items.len() } - pub async fn load(&self, app: Arc) -> TaskResult { - let mut headers = reqwest::header::HeaderMap::new(); - headers.insert( - reqwest::header::ACCEPT, - "application/vnd.github.v3+json".parse().unwrap(), - ); - - let commit_range = app - .client - .get(GITHUB_REPO_COMMIT_ENDPOINT_URL) - .headers(headers) - .send() - .await - .with_context(|| "sending commits request")? - .error_for_status()? - .json::() - .await - .with_context(|| "parsing commits response")?; - - self.update(commit_range); - - Ok(()) - } - - pub async fn update(&self, data: CommitsJson) { - self.state - .write() - .await - .items - .append(&mut data.0.iter().map(Commit::from).collect()); + pub async fn load(&self, app: Arc) { + app.watch_task(tokio::spawn(State::load(Arc::clone(&app)))) + .await; } } -#[async_trait::async_trait] impl Name for Commits { fn name(&self) -> String { "Recent Commit List".to_owned() @@ -85,14 +52,13 @@ impl Name for Commits { impl HotKeys for Commits { fn hotkeys(&self) -> Vec { - self.selection.hotkeys() + self.state.read().selection.hotkeys() } } #[async_trait::async_trait] impl Input for Commits { - async fn on_input(&mut self, input: &UserInput, app: Arc) -> Option { - self.selection - .on_input(input, self.state.read().await.count()) + async fn on_input(&mut self, input: &UserInput, _: Arc) -> Option { + self.state.write().selection.on_input(input, self.count()) } } diff --git a/src/views/commits/state.rs b/src/views/commits/state.rs index f3b001a..4c48343 100644 --- a/src/views/commits/state.rs +++ b/src/views/commits/state.rs @@ -1,11 +1,52 @@ -use crate::datatypes::commit::Commit; +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 State { pub items: Vec, + pub selection: StatelessList, } impl State { pub fn new() -> Self { - Self { items: Vec::new() } + Self { + items: Vec::new(), + selection: StatelessList::new(ListState::default(), false), + } + } + + pub async fn load(app: Arc) -> TaskResult { + let mut headers = reqwest::header::HeaderMap::new(); + headers.insert( + reqwest::header::ACCEPT, + "application/vnd.github.v3+json".parse().unwrap(), + ); + + let commit_range = app + .client + .get(GITHUB_REPO_COMMIT_ENDPOINT_URL) + .headers(headers) + .send() + .await + .with_context(|| "sending commits request")? + .error_for_status()? + .json::() + .await + .with_context(|| "parsing commits response")?; + + app.commits + .state + .write() + .items + .append(&mut commit_range.0.iter().map(Commit::from).collect()); + + Ok(()) } } diff --git a/src/views/events.rs b/src/views/events.rs index 22c3627..7bbc399 100644 --- a/src/views/events.rs +++ b/src/views/events.rs @@ -16,15 +16,9 @@ use crate::views::Draw; pub struct EventsView {} -#[async_trait::async_trait] impl Draw for EventsView { - async fn draw( - &mut self, - f: &mut Frame>, - area: Rect, - app: Arc, - ) { - if let Some(event) = &app.events.read().await.current_event { + 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 7170aff..68f835b 100644 --- a/src/views/help.rs +++ b/src/views/help.rs @@ -23,14 +23,12 @@ pub struct Help {} impl AppView for Help {} -#[async_trait::async_trait] 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 { @@ -51,15 +49,9 @@ impl Input for Help { } } -#[async_trait::async_trait] impl Draw for Help { - async fn draw( - &mut self, - f: &mut Frame>, - area: Rect, - app: Arc, - ) { - let help = app.help.lock().unwrap(); + 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 3d95b96..58f8007 100644 --- a/src/views/mod.rs +++ b/src/views/mod.rs @@ -27,14 +27,8 @@ pub enum ViewType { Help, } -#[async_trait::async_trait] pub trait Draw { - async fn draw( - &mut self, - f: &mut Frame>, - area: Rect, - app: Arc, - ); + fn draw(&self, f: &mut Frame>, area: Rect, app: Arc); } #[async_trait::async_trait] diff --git a/src/views/servers/draw.rs b/src/views/servers/draw.rs index b51098b..18e6fc8 100644 --- a/src/views/servers/draw.rs +++ b/src/views/servers/draw.rs @@ -20,15 +20,9 @@ use crate::views::Draw; use super::Servers; -#[async_trait::async_trait] impl Draw for Servers { - async fn draw( - &mut self, - f: &mut Frame>, - area: Rect, - app: Arc, - ) { - let servers = self.state.read().await.items; + 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; @@ -148,17 +142,17 @@ impl Draw for Servers { ); // draw server info - if let Some(selected) = self.selection.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.selection.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, @@ -166,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 index e4e2a48..582c550 100644 --- a/src/views/servers/hotkeys.rs +++ b/src/views/servers/hotkeys.rs @@ -26,7 +26,7 @@ impl HotKeys for Servers { }, ]; - hotkeys.append(&mut self.selection.hotkeys()); + hotkeys.append(&mut self.state.read().selection.hotkeys()); hotkeys } diff --git a/src/views/servers/input.rs b/src/views/servers/input.rs index cec2dd4..d1254f2 100644 --- a/src/views/servers/input.rs +++ b/src/views/servers/input.rs @@ -15,17 +15,17 @@ impl Input for Servers { #[cfg(feature = "geolocation")] UserInput::Char('m' | 'M') => Some(AppAction::OpenView(ViewType::World)), UserInput::Char('i' | 'I') => { - if let Some(i) = self.selection.selected() { + if let Some(i) = self.state.read().selection.selected() { Some(AppAction::InstallVersion( - self.state.read().await.items[i].version.clone(), + self.state.read().items[i].version.clone(), )) } else { None } } UserInput::Enter => { - if let Some(i) = self.selection.selected() { - let server = &self.state.read().await.items[i]; + if let Some(i) = self.state.read().selection.selected() { + let server = &self.state.read().items[i]; Some(AppAction::ConnectToServer { version: server.version.clone(), @@ -35,7 +35,7 @@ impl Input for Servers { None } } - _ => self.selection.on_input(input, self.count().await), + _ => self.state.write().selection.on_input(input, self.count()), } } } diff --git a/src/views/servers/mod.rs b/src/views/servers/mod.rs index d7cf2fa..6eb0e5e 100644 --- a/src/views/servers/mod.rs +++ b/src/views/servers/mod.rs @@ -8,29 +8,27 @@ use std::cmp::Ordering; use std::collections::HashMap; use std::sync::Arc; -use tokio::sync::RwLock; -use tui::widgets::TableState; +use parking_lot::RwLock; use crate::datatypes::game_version::{DownloadUrl, GameVersion}; use crate::datatypes::geolocation::IP; use crate::datatypes::server::{Address, Server, ServerListJson}; -use crate::states::{AppState, StatelessList}; +use crate::states::AppState; use crate::views::Name; use state::State; use crate::views::AppView; +#[derive(Clone)] pub struct Servers { - state: Arc>, - selection: StatelessList, + pub state: Arc>, } impl Servers { pub fn new() -> Self { Self { state: Arc::new(RwLock::new(State::new())), - selection: StatelessList::new(TableState::default(), false), } } @@ -44,7 +42,7 @@ impl Servers { download: DownloadUrl::new("http://evil.exploit"), }; - self.state.write().await.items.push(Server { + self.state.write().items.push(Server { name: "TEST SERVER PLEASE IGNORE".to_owned(), address: Address { ip: ip.clone(), @@ -60,7 +58,7 @@ impl Servers { }); #[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; } @@ -73,12 +71,12 @@ impl Servers { .await; } - pub async fn count(&self) -> usize { - self.state.read().await.items.len() + pub fn count(&self) -> usize { + self.state.read().items.len() } pub async fn update(&self, app: Arc, data: ServerListJson) { - let mut items = self.state.write().await.items; + let items = &mut self.state.write().items; let mut previously_online: HashMap = items.iter_mut().map(|i| (i.address.clone(), i)).collect(); @@ -96,7 +94,7 @@ impl Servers { if let Some(known_server) = previously_online.remove(&address) { // 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; } @@ -105,11 +103,11 @@ impl Servers { 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; } } diff --git a/src/views/servers/state.rs b/src/views/servers/state.rs index a397d44..099646e 100644 --- a/src/views/servers/state.rs +++ b/src/views/servers/state.rs @@ -1,9 +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() } + Self { + items: Vec::new(), + selection: StatelessList::new(TableState::default(), false), + } } } diff --git a/src/views/servers/tasks.rs b/src/views/servers/tasks.rs index 67ed4b4..a50ca1b 100644 --- a/src/views/servers/tasks.rs +++ b/src/views/servers/tasks.rs @@ -12,9 +12,9 @@ use crate::states::AppState; pub async fn server_fetch_task(app: Arc) -> TaskResult { #[cfg(feature = "geolocation")] - app.locations.write().await.resolve(&IP::Local).await; + app.locations.write().resolve(&IP::Local).await; - async fn loop_body(app: AppState) -> anyhow::Result<()> { + async fn loop_body(app: Arc) -> anyhow::Result<()> { let data = app .client .get(SERVER_LIST_URL) @@ -26,15 +26,15 @@ pub async fn server_fetch_task(app: Arc) -> TaskResult { .await .with_context(|| "parsing server list response")?; - app.servers.write().await.update(app.clone(), data).await; + 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(app).await { - app.events.read().await.error(err).await; + 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/draw.rs b/src/views/tabs/draw.rs index 3c7d8e0..46ca6f7 100644 --- a/src/views/tabs/draw.rs +++ b/src/views/tabs/draw.rs @@ -12,22 +12,14 @@ use tui::{ Frame, }; -use futures::stream::{self, StreamExt}; - use crate::states::AppState; use crate::views::Draw; use super::tab::Tab; use super::Tabs; -#[async_trait::async_trait] impl Draw for Tabs { - async fn draw( - &mut self, - f: &mut Frame>, - area: Rect, - app: Arc, - ) { + 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()) @@ -38,16 +30,10 @@ impl Draw for Tabs { .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; + let tabs = Tab::all().map(|t| Spans::from(t.name(Arc::clone(&app)))); f.render_widget( - TuiTabs::new(tabs) + TuiTabs::new(tabs.to_vec()) .block(Block::default().border_type(BorderType::Plain)) .highlight_style( Style::default() @@ -61,10 +47,10 @@ impl Draw for Tabs { // 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, - }; + // 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 index fd655d5..35d567e 100644 --- a/src/views/tabs/hotkeys.rs +++ b/src/views/tabs/hotkeys.rs @@ -31,11 +31,11 @@ impl HotKeys for Tabs { }, ]; - 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.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 index 453e161..b8071b5 100644 --- a/src/views/tabs/input.rs +++ b/src/views/tabs/input.rs @@ -24,16 +24,17 @@ impl Input for Tabs { self.select_tab(Tab::Commits); None } - UserInput::Tab => { - self.selection.select_next(Tab::tab_count()); - 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() { - 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, + _ => 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 index 03c9c90..6c8b58c 100644 --- a/src/views/tabs/mod.rs +++ b/src/views/tabs/mod.rs @@ -3,6 +3,8 @@ mod hotkeys; mod input; mod tab; +use std::sync::Arc; + use tui::widgets::ListState; use crate::states::StatelessList; @@ -10,8 +12,9 @@ use crate::views::{AppView, Name}; use tab::Tab; +#[derive(Clone)] pub struct Tabs { - selection: StatelessList, + selection: Arc>, } impl Tabs { @@ -20,7 +23,9 @@ impl Tabs { selection.select_first(Tab::tab_count()); - Self { selection } + Self { + selection: Arc::new(selection), + } } fn selected_tab(&self) -> Tab { @@ -30,7 +35,7 @@ impl Tabs { } fn select_tab(&mut self, tab: Tab) { - self.selection.select_index(tab.into()); + //self.selection.select_index(tab.into()); } } diff --git a/src/views/tabs/tab.rs b/src/views/tabs/tab.rs index 83bb4ec..29c5eff 100644 --- a/src/views/tabs/tab.rs +++ b/src/views/tabs/tab.rs @@ -10,15 +10,15 @@ pub enum Tab { } impl Tab { - async fn name(&self, app: Arc) -> String { + pub fn name(&self, app: Arc) -> String { match self { Self::Servers => { - format!("servers [{}]", app.servers.read().await.count()) + format!("servers [{}]", app.servers.count()) } Self::Versions => { - format!("versions [{}]", app.versions.read().await.count()) + format!("versions [{}]", app.versions.count()) } - Self::Commits => format!("commits [{}]", app.commits.read().await.items.len()), + Self::Commits => format!("commits [{}]", app.commits.count()), } } diff --git a/src/views/versions/draw.rs b/src/views/versions/draw.rs index 7f5b477..3db0371 100644 --- a/src/views/versions/draw.rs +++ b/src/views/versions/draw.rs @@ -66,15 +66,9 @@ impl Progress { } } -#[async_trait::async_trait] impl Draw for Versions { - async fn draw( - &mut self, - f: &mut Frame>, - area: Rect, - app: Arc, - ) { - let versions = &self.state.read().await.items; + 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(); @@ -164,7 +158,7 @@ impl Draw for Versions { .add_modifier(Modifier::BOLD), ); - f.render_stateful_widget(table, chunks[0], &mut self.selection.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 index e6542e3..f6c051d 100644 --- a/src/views/versions/hotkeys.rs +++ b/src/views/versions/hotkeys.rs @@ -25,7 +25,7 @@ impl HotKeys for Versions { }, ]; - hotkeys.append(&mut self.selection.hotkeys()); + //hotkeys.append(&mut self.selection.hotkeys()); hotkeys } diff --git a/src/views/versions/input.rs b/src/views/versions/input.rs index 810ff62..bcdf2ad 100644 --- a/src/views/versions/input.rs +++ b/src/views/versions/input.rs @@ -11,58 +11,56 @@ use super::Versions; 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().await; + // UserInput::Refresh => { + // let mut versions = self.state.write(); - versions.refresh(app.clone()).await; + // versions.refresh(app.clone()).await; - if let Some(i) = self.selection.selected() { - if i >= versions.count() { - self.selection.unselect(); - } - } + // 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().await.items[i].version.clone(), - )) - } else { - None - } - } - UserInput::Char('d' | 'D') => { - if let Some(i) = self.selection.selected() { - Some(AppAction::UninstallVersion( - self.state.read().await.items[i].version.clone(), - )) - } else { - None - } - } - UserInput::Char('a' | 'A') => { - if let Some(i) = self.selection.selected() { - Some(AppAction::AbortVersionInstallation( - self.state.read().await.items[i].version.clone(), - )) - } else { - None - } - } - UserInput::Enter => { - if let Some(i) = self.selection.selected() { - Some(AppAction::LaunchVersion( - self.state.read().await.items[i].version.clone(), - )) - } else { - None - } - } - _ => self - .selection - .on_input(input, self.state.read().await.count()), + // 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 index 9434ca1..0ac6e3b 100644 --- a/src/views/versions/mod.rs +++ b/src/views/versions/mod.rs @@ -5,29 +5,27 @@ mod state; use std::sync::Arc; -use crossterm::event::KeyCode; +use parking_lot::RwLock; -use tokio::sync::RwLock; - -use tui::widgets::TableState; - -use crate::states::StatelessList; use crate::views::Name; use state::State; +#[derive(Clone)] pub struct Versions { - state: Arc>, - selection: StatelessList, + pub state: Arc>, } impl Versions { pub fn new() -> Self { Self { state: Arc::new(RwLock::new(State::new())), - selection: StatelessList::new(TableState::default(), false), } } + + pub fn count(&self) -> usize { + self.state.read().items.len() + } } impl Name for Versions { diff --git a/src/views/versions/state.rs b/src/views/versions/state.rs index 1e6f15c..1580ef6 100644 --- a/src/views/versions/state.rs +++ b/src/views/versions/state.rs @@ -9,7 +9,7 @@ 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::{ @@ -19,15 +19,18 @@ use crate::datatypes::{ value_sorted_map::ValueSortedMap, }; use crate::states::app::{AppState, TaskResult}; +use crate::states::StatelessList; pub struct State { pub items: ValueSortedMap, + pub selection: StatelessList, } impl State { pub fn new() -> Self { Self { items: ValueSortedMap::new(), + selection: StatelessList::new(TableState::default(), false), } } @@ -35,32 +38,24 @@ impl State { 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() @@ -134,7 +129,7 @@ impl State { }); // 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; @@ -152,7 +147,6 @@ impl State { self.spawn_installation_finder(app.clone()).await; app.events .read() - .await .event("Refreshed installation list (still looking in file system)") .await; } @@ -160,7 +154,7 @@ impl State { 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) { @@ -172,7 +166,6 @@ impl State { app.events .read() - .await .event(&format!("Discovered {}", version)) .await; @@ -206,11 +199,10 @@ impl State { 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, .. @@ -255,7 +247,7 @@ impl State { let mut progress = 0; - versions.write().await.items.insert( + versions.state.write().items.insert( version.clone(), Installation { version: version.clone(), @@ -277,7 +269,7 @@ impl State { progress += chunk.len(); - let mut versions = versions.write().await; + let mut versions = versions.state.write(); let previous = versions.items.insert( version.clone(), Installation { @@ -312,7 +304,7 @@ impl State { } } - versions.write().await.items.insert( + versions.state.write().items.insert( version.clone(), Installation { version: version.clone(), @@ -327,7 +319,6 @@ impl State { app.events .read() - .await .event(&format!("Extracting {}", version)) .await; @@ -352,7 +343,7 @@ impl State { ); } - versions.write().await.items.insert( + versions.state.write().items.insert( version.clone(), Installation { version: version.clone(), @@ -366,7 +357,6 @@ impl State { app.events .read() - .await .event(&format!("Installed version {}", version)) .await; @@ -374,7 +364,7 @@ impl State { } 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), @@ -393,7 +383,6 @@ impl State { app.events .read() - .await .event(&format!("Aborted installation of {}", version)) .await; } else { @@ -409,7 +398,7 @@ impl State { 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 { @@ -431,7 +420,6 @@ impl State { app.events .read() - .await .event(&format!("Uninstalled {}", version)) .await; @@ -445,14 +433,13 @@ impl State { ) -> 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 f26135c..e11f748 100644 --- a/src/views/world.rs +++ b/src/views/world.rs @@ -58,18 +58,12 @@ impl HotKeys for World { } } -#[async_trait::async_trait] 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)) From a104f87f7eadf94266e11d1ae4317a6430739f6e Mon Sep 17 00:00:00 2001 From: Eugene Date: Sat, 11 Sep 2021 18:19:48 +0300 Subject: [PATCH 3/5] Rust 2021, crossterm stream --- Cargo.lock | 1 + Cargo.toml | 8 +++++--- src/views/versions/state.rs | 1 - 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 48fca0c..24b720e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -248,6 +248,7 @@ checksum = "486d44227f71a1ef39554c0dc47e44b9f4139927c75043312690c3f476d1d788" dependencies = [ "bitflags", "crossterm_winapi", + "futures-core", "libc", "mio", "parking_lot", diff --git a/Cargo.toml b/Cargo.toml index d2ab15c..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,16 @@ 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"] diff --git a/src/views/versions/state.rs b/src/views/versions/state.rs index 1580ef6..cd9eb7d 100644 --- a/src/views/versions/state.rs +++ b/src/views/versions/state.rs @@ -11,7 +11,6 @@ use tokio::io::AsyncWriteExt; use tokio::process::Command; use tui::widgets::TableState; -use crate::config::AppConfig; use crate::datatypes::{ game_version::{DownloadUrl, GameVersion}, installation::{Installation, InstallationKind}, From cc7d04d524ec9798a6886346d754733ddff88a34 Mon Sep 17 00:00:00 2001 From: Eugene Date: Thu, 16 Sep 2021 16:44:54 +0300 Subject: [PATCH 4/5] Fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) From 44724c9369fabc1545e303ecfe01f522d7506b2f Mon Sep 17 00:00:00 2001 From: Eugene Date: Thu, 16 Sep 2021 21:26:33 +0300 Subject: [PATCH 5/5] Async user input --- src/app.rs | 91 ++++++++++++++++++++++++++++++++++------- src/input.rs | 81 +++++++++++------------------------- src/main.rs | 89 +++------------------------------------- src/states/app.rs | 35 ++++++++-------- src/states/events.rs | 2 +- src/states/locations.rs | 2 +- src/utils.rs | 54 ++++++++++++++++++++++++ 7 files changed, 181 insertions(+), 173 deletions(-) create mode 100644 src/utils.rs diff --git a/src/app.rs b/src/app.rs index 0dc64b3..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; @@ -15,6 +22,12 @@ use crate::states::app::AppState; use crate::views::world::World; use crate::views::{events::EventsView, help::Help, tabs::Tabs, AppView, Draw, ViewType}; +#[derive(Debug)] +pub enum StopSignal { + UserExit, + Panic, +} + #[derive(Debug)] pub enum AppAction { // view management @@ -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,8 +69,8 @@ impl App { events_view: EventsView {}, - stopped: false, - panicked: panic_bool, + kill_switch, + kill_switch_recv, }; instance.register_view(ViewType::Tab, Box::new(Tabs::new())); @@ -72,7 +85,57 @@ 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}; @@ -99,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 { @@ -132,8 +197,4 @@ impl App { } } } - - fn stop(&mut self) { - self.stopped = true; - } } 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 f4ce15b..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,50 +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)); - })?; - - // TODO: send input events to async channel instead - 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 85f5261..1aa83f5 100644 --- a/src/states/app.rs +++ b/src/states/app.rs @@ -1,12 +1,12 @@ -use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; 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; @@ -33,18 +33,18 @@ pub struct AppState { 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 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).await)); + let events = Arc::new(RwLock::new(EventsState::new(&config))); - let instance = Arc::new(Self { + Self { commits: Commits::new(), versions: versions.clone(), #[cfg(feature = "geolocation")] @@ -59,15 +59,16 @@ impl AppState { help: Mutex::new(HelpState::new()), - panic_bool, - }); + kill_switch, + } + } - events.write().run(instance.clone()).await; - servers.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().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) { @@ -111,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 { @@ -127,7 +128,7 @@ 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) => { diff --git a/src/states/events.rs b/src/states/events.rs index e639578..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 { diff --git a/src/states/locations.rs b/src/states/locations.rs index a8c49c4..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 { 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, + } +}