diff --git a/src/gui/app_state.rs b/src/gui/app_state.rs index 925d89a..3cf1f06 100644 --- a/src/gui/app_state.rs +++ b/src/gui/app_state.rs @@ -1,6 +1,7 @@ -use crate::gui::elements::log_card::UiLogEntry; +use crate::{ACTIVE_TASKS, APP_STATE, SHUTDOWN, gui::elements::log_card::UiLogEntry}; use json::{JsonValue, object}; -use std::collections::VecDeque; +use std::{collections::VecDeque, thread, time::Duration}; +use sysinfo::{RefreshKind, System}; #[derive(Clone)] pub struct AppState { @@ -138,3 +139,63 @@ impl AppState { } } } + +pub fn setup() { + ACTIVE_TASKS.insert("System info loader".to_string()); + tokio::spawn(async move { + let mut sys = System::new_with_specifics(RefreshKind::new()); + let mut last_total_received = 0u64; + let mut last_total_transmitted = 0u64; + let mut counter = 0.0; + loop { + if *SHUTDOWN.read().await { + break; + } + sys.refresh_all(); + + let mut tcpu = 0; + for cpu in sys.cpus() { + tcpu += cpu.cpu_usage() as i64; + tcpu /= 2; + } + let ram = (sys.used_memory() as f64 / sys.total_memory() as f64) * 100.0; + + let total_received = 0u64; + let total_transmitted = 0u64; + + let delta_received = if last_total_received == 0 { + 0 + } else { + total_received.saturating_sub(last_total_received) + }; + let delta_transmitted = if last_total_transmitted == 0 { + 0 + } else { + total_transmitted.saturating_sub(last_total_transmitted) + }; + last_total_received = total_received; + last_total_transmitted = total_transmitted; + + let net_down = delta_received as f64; + let net_up = delta_transmitted as f64; + + { + let mut st = APP_STATE.lock().unwrap(); + st.push_cpu((counter, tcpu as f64)); + st.push_ram((counter, ram)); + st.push_net_down((counter, net_down)); + st.push_net_up((counter, net_up)); + + st.sys_info = format!("NetDown: {} NetUp: {}", delta_received, delta_transmitted); + } + + counter += 1.0; + if counter > 30.0 { + thread::sleep(Duration::from_millis(500)); + } else { + thread::sleep(Duration::from_millis(5)); + } + } + ACTIVE_TASKS.remove("System info loader"); + }); +} diff --git a/src/gui/elements/console_card.rs b/src/gui/elements/console_card.rs index 7ab1d6c..889e4e7 100644 --- a/src/gui/elements/console_card.rs +++ b/src/gui/elements/console_card.rs @@ -124,8 +124,14 @@ impl InteractableElement for ConsoleCard { fn interact(&mut self, key: KeyEvent) -> InteractionResult { match key.code { KeyCode::Enter => { + if self.content.is_empty() { + log!(""); + return InteractionResult::Handled; + } let command = self.content.clone(); let id = Uuid::new_v4(); + let id = id.to_string(); + let id = id.split_at(8).0; let task_id = format!("command_{}_{}", command, id); ACTIVE_TASKS.insert(task_id.clone()); tokio::spawn(async move { diff --git a/src/gui/elements/elements.rs b/src/gui/elements/elements.rs index c5441f1..3429e52 100644 --- a/src/gui/elements/elements.rs +++ b/src/gui/elements/elements.rs @@ -5,12 +5,15 @@ use ratatui::{Frame, layout::Rect, widgets::Borders}; use crate::gui::{interaction_result::InteractionResult, screens::screens::Screen}; +#[allow(unused)] pub trait Element: Send + Sync + Any { fn as_any(&self) -> &dyn Any; fn as_any_mut(&mut self) -> &mut dyn Any; fn render(&self, f: &mut Frame, r: Rect); } + +#[allow(unused)] pub trait JoinableElement: Send + Sync + Any { fn as_any(&self) -> &dyn Any; fn as_any_mut(&mut self) -> &mut dyn Any; @@ -21,6 +24,7 @@ pub trait JoinableElement: Send + Sync + Any { fn set_joins(&mut self, joins: Borders); } +#[allow(unused)] pub trait InfoElement: Send + Sync + Any { fn as_any(&self) -> &dyn Any; fn as_any_mut(&mut self) -> &mut dyn Any; @@ -30,6 +34,7 @@ pub trait InfoElement: Send + Sync + Any { fn get_info_screen(&self) -> Box; } +#[allow(unused)] pub trait InteractableElement: Send + Sync + Any { fn as_any(&self) -> &dyn Any; fn as_any_mut(&mut self) -> &mut dyn Any; diff --git a/src/gui/elements/graph_card.rs b/src/gui/elements/graph_card.rs index a0a7562..d95f937 100644 --- a/src/gui/elements/graph_card.rs +++ b/src/gui/elements/graph_card.rs @@ -28,11 +28,19 @@ pub enum GRAPHS { } impl GRAPHS { + pub fn get_color(&self) -> Color { + match self { + GRAPHS::Ram => Color::Blue, + GRAPHS::Cpu => Color::Red, + GRAPHS::Ping => Color::Green, + } + } + pub fn get_graph(&self) -> Vec<(f64, f64)> { match self { - GRAPHS::Ram => APP_STATE.lock().unwrap().ram.clone(), - GRAPHS::Cpu => APP_STATE.lock().unwrap().cpu.clone(), - GRAPHS::Ping => APP_STATE.lock().unwrap().ping.clone(), + GRAPHS::Ram => APP_STATE.lock().unwrap().with_width(28).ram.clone(), + GRAPHS::Cpu => APP_STATE.lock().unwrap().with_width(28).cpu.clone(), + GRAPHS::Ping => APP_STATE.lock().unwrap().with_width(28).ping.clone(), } } @@ -45,13 +53,13 @@ impl GRAPHS { } } +#[allow(unused)] pub struct GraphCard { ui: Arc, graph_type: GRAPHS, focused: bool, pub title: String, - color: Color, borders: Borders, joins: Borders, @@ -66,10 +74,9 @@ impl GraphCard { graph_type, focused: false, title, - color: Color::White, borders: Borders::ALL, joins: Borders::NONE, - open: false, + open: true, } } @@ -98,11 +105,11 @@ impl Element for GraphCard { .filter(|y| *y > 0.0) .min_by(|a, b| a.total_cmp(b)) .unwrap_or(0.0); - let max_y = graph.iter().map(|(_, y)| *y).fold(f64::MIN, f64::max); + let max_y = graph.iter().map(|(_, y)| *y).fold(-1.0, f64::max); let block = Block::default() .title(format!( - "─{}:─{}{}──{}/{}─MIN/MAX", + "{}:─{}{}─{}min/{}max", self.title, graph.last().unwrap_or(&(0.0, 0.0)).1 as i64, unit, @@ -127,7 +134,7 @@ impl Element for GraphCard { y1: 0.0, x2: *x, y2: *y, - color: self.color, + color: self.graph_type.get_color(), }); } }); diff --git a/src/gui/mod.rs b/src/gui/mod.rs index 0dadd8f..b355046 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -6,6 +6,7 @@ pub mod elements { } pub mod screens { pub mod main_screen; + pub mod md_viewer; pub mod screens; pub mod terms_checker; pub mod terms_updater; diff --git a/src/gui/screens/main_screen.rs b/src/gui/screens/main_screen.rs index f0ffe41..85dbbaa 100644 --- a/src/gui/screens/main_screen.rs +++ b/src/gui/screens/main_screen.rs @@ -32,8 +32,8 @@ impl MainScreen { let nav_grid = vec![ vec![Some(0), Some(2)], - vec![Some(1), Some(3)], - vec![None, Some(4)], + vec![Some(0), Some(3)], + vec![Some(1), Some(4)], ]; let mut log_card = LogCard::new(); @@ -44,10 +44,10 @@ impl MainScreen { elements.push(Box::new(log_card)); elements.push(Box::new(console_card)); - let mut ram_graph = GraphCard::new(ui.clone(), GRAPHS::Ram, "RAM Usage".into()); + let mut ram_graph = GraphCard::new(ui.clone(), GRAPHS::Ram, "RAM".into()); ram_graph.set_borders(Borders::TOP.union(Borders::LEFT).union(Borders::RIGHT)); elements.push(Box::new(ram_graph)); - let mut cpu_graph = GraphCard::new(ui.clone(), GRAPHS::Cpu, "CPU Usage".into()); + let mut cpu_graph = GraphCard::new(ui.clone(), GRAPHS::Cpu, "CPU".into()); cpu_graph.set_borders(Borders::TOP.union(Borders::LEFT).union(Borders::RIGHT)); cpu_graph.set_joins(Borders::TOP); elements.push(Box::new(cpu_graph)); @@ -87,51 +87,64 @@ impl MainScreen { } fn navigate(&mut self, direction: NavDirection) { - let (mut y, mut x) = self.selected_coords; + let (current_row, current_col) = self.selected_coords; + let current_element = self.nav_grid[current_row][current_col]; - self.unfocus_current(y, x); + self.unfocus_current(current_row, current_col); - match direction { - NavDirection::Up => { - if y > 0 { - y -= 1; - } + let (delta_row, delta_col) = match direction { + NavDirection::Up => (-1isize, 0), + NavDirection::Down => (1, 0), + NavDirection::Left => (0, -1), + NavDirection::Right => (0, 1), + _ => (0, 0), + }; + + let mut next_row = current_row as isize; + let mut next_col = current_col as isize; + + loop { + next_row += delta_row; + next_col += delta_col; + + if next_row < 0 || next_col < 0 { + self.selected_coords = ( + (next_row - delta_row) as usize, + (next_col - delta_col) as usize, + ); + break; } - NavDirection::Down => { - if y < self.nav_grid.len() - 1 { - y += 1; - } + let next_row_u = next_row as usize; + let next_col_u = next_col as usize; + + if next_row_u >= self.nav_grid.len() { + self.selected_coords = ( + (next_row - delta_row) as usize, + (next_col - delta_col) as usize, + ); + break; } - NavDirection::Left => { - if x > 0 { - x -= 1; + + if let Some(row) = self.nav_grid.get(next_row_u) { + if next_col_u >= row.len() { + self.selected_coords = ( + (next_row - delta_row) as usize, + (next_col - delta_col) as usize, + ); + break; } - } - NavDirection::Right => { - if let Some(row) = self.nav_grid.get(y) { - if x < row.len() - 1 { - x += 1; + + if let Some(next_element) = row[next_col_u] { + if Some(next_element) != current_element { + self.selected_coords = (next_row_u, next_col_u); + self.focus_current(); + return; } } } - _ => {} } - if let Some(row) = self.nav_grid.get(y) { - if x >= row.len() { - x = row.len() - 1; - } - } - - if self - .nav_grid - .get(y) - .and_then(|r| r.get(x)) - .map_or(false, |e| e.is_some()) - { - self.selected_coords = (y, x); - self.focus_current(); - } + self.focus_current(); } } @@ -156,21 +169,26 @@ impl Screen for MainScreen { let graphs_width = if self.graphs_open { 30 } else { 2 }; let main_width = inner.width.saturating_sub(graphs_width); - let chunks = Layout::default() + let horizontal_chunks = Layout::default() .direction(ratatui::layout::Direction::Horizontal) .constraints([ Constraint::Length(main_width), Constraint::Length(graphs_width), ]) .split(inner); - let left_chunks = - Layout::vertical([Constraint::Min(0), Constraint::Length(3)]).split(chunks[0]); - if let Some(Some(index)) = self.nav_grid.get(0).and_then(|r| r.get(0)) { - self.elements[*index].as_element().render(f, left_chunks[0]); + let left_area = horizontal_chunks[0]; + let right_area = horizontal_chunks[1]; + + let left_rows = + Layout::vertical([Constraint::Min(0), Constraint::Length(3)]).split(left_area); + + if let Some(log) = self.elements.get(0) { + log.as_element().render(f, left_rows[0]); } - if let Some(Some(index)) = self.nav_grid.get(1).and_then(|r| r.get(0)) { - self.elements[*index].as_element().render(f, left_chunks[1]); + + if let Some(console) = self.elements.get(1) { + console.as_element().render(f, left_rows[1]); } let graph_elements: Vec<_> = self @@ -179,16 +197,18 @@ impl Screen for MainScreen { .filter(|el| el.as_any().is::()) .collect(); - let graph_chunks = Layout::vertical( - graph_elements - .iter() - .map(|_| Constraint::Length(chunks[1].height / graph_elements.len() as u16)) - .collect::>(), - ) - .split(chunks[1]); + if !graph_elements.is_empty() { + let graph_chunks = Layout::vertical( + graph_elements + .iter() + .map(|_| Constraint::Ratio(1, graph_elements.len() as u32)) + .collect::>(), + ) + .split(right_area); - for (el, area) in graph_elements.iter().zip(graph_chunks.iter()) { - el.as_element().render(f, *area); + for (el, area) in graph_elements.iter().zip(graph_chunks.iter()) { + el.as_element().render(f, *area); + } } } diff --git a/src/terms/md_viewer.rs b/src/gui/screens/md_viewer.rs similarity index 98% rename from src/terms/md_viewer.rs rename to src/gui/screens/md_viewer.rs index 5a4adf8..10eabce 100644 --- a/src/terms/md_viewer.rs +++ b/src/gui/screens/md_viewer.rs @@ -5,12 +5,11 @@ use ratatui::{ text::{Line, Span}, widgets::{Block, Borders, Paragraph, Wrap}, }; -use std::{any::Any, sync::Arc, time::Duration}; +use std::{any::Any, time::Duration}; -use crate::gui::{interaction_result::InteractionResult, screens::screens::Screen, ui::UI}; +use crate::gui::{interaction_result::InteractionResult, screens::screens::Screen}; pub struct FileViewer { - ui: Arc, title: String, text: Vec, scroll: u16, @@ -25,10 +24,6 @@ impl Screen for FileViewer { self } - fn get_ui(&self) -> &Arc { - &self.ui - } - fn render(&self, f: &mut Frame, rect: Rect) { self.draw(f, rect); } @@ -54,9 +49,8 @@ impl Screen for FileViewer { } impl FileViewer { - pub fn new(ui: Arc, title: String, content: &str) -> Self { + pub fn new(title: String, content: &str) -> Self { Self { - ui, title, text: parse_document(content.to_owned()), scroll: 0, diff --git a/src/gui/screens/terms_checker.rs b/src/gui/screens/terms_checker.rs index 7aaa1c0..26bb7c6 100644 --- a/src/gui/screens/terms_checker.rs +++ b/src/gui/screens/terms_checker.rs @@ -1,10 +1,13 @@ use crate::{ - gui::{interaction_result::InteractionResult, screens::screens::Screen, ui::UI}, + gui::{ + interaction_result::InteractionResult, + screens::{md_viewer::FileViewer, screens::Screen}, + ui::UI, + }, terms::{ buttons::{checkbox, draw_buttons}, consent_state::UserChoice, focus::Focus, - md_viewer::FileViewer, terms_getter::{Type, get_link, get_terms}, }, }; @@ -279,12 +282,11 @@ impl Screen for TermsCheckerScreen { _ => None, }; if let Some(terms_type) = terms_type { - let ui = self.ui.clone(); let fut: Pin> + Send>> = Box::pin(async move { let content = get_terms(terms_type.clone()).await.unwrap(); let screen: FileViewer = - FileViewer::new(ui.clone(), terms_type.to_string(), &content); + FileViewer::new(terms_type.to_string(), &content); Box::new(screen) as Box }); InteractionResult::OpenFutureScreen { screen: fut } diff --git a/src/gui/screens/terms_updater.rs b/src/gui/screens/terms_updater.rs index 7fa95ed..35da364 100644 --- a/src/gui/screens/terms_updater.rs +++ b/src/gui/screens/terms_updater.rs @@ -1,12 +1,14 @@ use crate::{ - gui::{interaction_result::InteractionResult, screens::screens::Screen, ui::UI}, + gui::{ + interaction_result::InteractionResult, + screens::{md_viewer::FileViewer, screens::Screen}, + }, terms::{ buttons::{checkbox, draw_buttons}, consent_state::{UpdateDecision, UserChoice}, doc::Doc, focus::Focus, - md_viewer::FileViewer, - terms_getter::{Type, get_link, get_terms}, + terms_getter::{Type, get_newest_link, get_terms}, }, }; use chrono::{Local, TimeZone, Utc}; @@ -19,11 +21,9 @@ use ratatui::{ widgets::{Block, Borders, Paragraph}, }; use std::any::Any; -use std::sync::Arc; use tokio::sync::oneshot; pub struct TermsUpdaterScreen { - ui: Arc, sender: Option>, eula_needed: bool, @@ -48,7 +48,6 @@ pub struct TermsUpdaterScreen { } impl TermsUpdaterScreen { pub fn new( - ui: Arc, consent_eula: UpdateDecision, consent_tos: UpdateDecision, consent_pp: UpdateDecision, @@ -87,7 +86,6 @@ impl TermsUpdaterScreen { || (pp_needed && !pp_future); Self { - ui, sender, eula_needed, @@ -653,14 +651,10 @@ impl Screen for TermsUpdaterScreen { }; if let Some(terms_type) = terms_type { - let ui = self.ui.clone(); let fut = Box::pin(async move { let content = get_terms(terms_type.clone()).await.unwrap(); - Box::new(FileViewer::new( - ui.clone(), - terms_type.to_string(), - &content, - )) as Box + Box::new(FileViewer::new(terms_type.to_string(), &content)) + as Box }); return InteractionResult::OpenFutureScreen { screen: fut }; @@ -670,15 +664,15 @@ impl Screen for TermsUpdaterScreen { } KeyCode::Char('l') | KeyCode::Char('L') => match self.focus { Focus::Eula => { - let _ = open::that(get_link(Type::EULA)); + let _ = open::that(get_newest_link(Type::EULA)); InteractionResult::Handled } Focus::Tos => { - let _ = open::that(get_link(Type::TOS)); + let _ = open::that(get_newest_link(Type::TOS)); InteractionResult::Handled } Focus::Pp => { - let _ = open::that(get_link(Type::PP)); + let _ = open::that(get_newest_link(Type::PP)); InteractionResult::Handled } _ => InteractionResult::Unhandled, diff --git a/src/gui/ui.rs b/src/gui/ui.rs index 9052ea7..5903a4b 100644 --- a/src/gui/ui.rs +++ b/src/gui/ui.rs @@ -26,7 +26,7 @@ pub static FPS: Lazy> = Lazy::new(|| RwLock::new((0.0, 0.0))) pub struct UI { pub terminal: Arc>>>, - screen: Arc>>>, + screen_stack: Arc>>>, } pub fn start_tui() -> Arc { @@ -111,18 +111,22 @@ impl UI { let terminal = init(); Self { terminal: Arc::new(Mutex::new(terminal)), - screen: Arc::new(RwLock::new(None)), + screen_stack: Arc::new(RwLock::new(Vec::new())), } } pub async fn set_screen(&self, screen: Box) { - *self.screen.write().await = Some(screen); + self.screen_stack.write().await.push(screen); + } + pub async fn replace_screen(&self, screen: Box) { + let mut stack = self.screen_stack.write().await; + stack.pop(); + stack.push(screen); } - pub async fn handle_input(self: Arc, key_event: KeyEvent) { let result = { - let mut guard = self.screen.write().await; - if let Some(screen) = guard.as_mut() { + let mut stack = self.screen_stack.write().await; + if let Some(screen) = stack.last_mut() { screen.handle_input(key_event) } else { return; @@ -138,7 +142,12 @@ impl UI { ui.set_screen(screen).await; } InteractionResult::CloseScreen => { - *self.screen.write().await = None; + let mut stack = self.screen_stack.write().await; + stack.pop(); + + if stack.is_empty() { + *SHUTDOWN.write().await = true; + } } InteractionResult::Handled => {} InteractionResult::Unhandled => {} @@ -146,7 +155,7 @@ impl UI { } pub async fn render(&self) { - if let Some(screen) = self.screen.read().await.as_ref() { + if let Some(screen) = self.screen_stack.read().await.last() { let mut terminal = self.terminal.lock().unwrap(); terminal .draw(|f| { diff --git a/src/main.rs b/src/main.rs index 800414e..a72ce87 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,6 +20,7 @@ mod util; use crate::communities::community_manager; use crate::communities::interactables::registry; +use crate::gui::app_state; use crate::gui::app_state::AppState; use crate::gui::screens::main_screen::MainScreen; use crate::gui::ui::start_tui; @@ -78,6 +79,7 @@ async fn main() { println!("You can find this at 'agreements'!"); return; } + app_state::setup(); let main_screen = MainScreen::new(ui.clone()).await; ui.set_screen(Box::new(main_screen)).await; diff --git a/src/terms/consent_state.rs b/src/terms/consent_state.rs index 861b374..1cc6e0a 100644 --- a/src/terms/consent_state.rs +++ b/src/terms/consent_state.rs @@ -59,7 +59,7 @@ async fn ensure_initial_consent(ui: Arc, state: &mut ConsentState) -> Result } } - &state.save_state(); + let _ = &state.save_state(); Ok(()) } UserChoice::Deny => Err(()), @@ -77,7 +77,6 @@ async fn ensure_updates(ui: Arc, state: &mut ConsentState) -> Result<(), ()> let (tx, rx) = oneshot::channel(); ui.set_screen(Box::new(TermsUpdaterScreen::new( - ui.clone(), eula_update.clone(), tos_update.clone(), privacy_update.clone(), diff --git a/src/terms/mod.rs b/src/terms/mod.rs index b65db75..4a11008 100644 --- a/src/terms/mod.rs +++ b/src/terms/mod.rs @@ -2,5 +2,4 @@ pub mod buttons; pub mod consent_state; pub mod doc; pub mod focus; -pub mod md_viewer; pub mod terms_getter;