[Add] Graph Cards, Some UI fixes [TODO] Manual closing after Agreements

screens is neccesarry
This commit is contained in:
Alex Emmet 2026-02-21 13:18:59 +01:00
commit 0669b8be0a
13 changed files with 206 additions and 107 deletions

View file

@ -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");
});
}

View file

@ -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 {

View file

@ -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<dyn Screen>;
}
#[allow(unused)]
pub trait InteractableElement: Send + Sync + Any {
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;

View file

@ -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<UI>,
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(),
});
}
});

View file

@ -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;

View file

@ -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::<GraphCard>())
.collect();
let graph_chunks = Layout::vertical(
graph_elements
.iter()
.map(|_| Constraint::Length(chunks[1].height / graph_elements.len() as u16))
.collect::<Vec<_>>(),
)
.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::<Vec<_>>(),
)
.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);
}
}
}

View file

@ -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<UI>,
title: String,
text: Vec<DisplayLine>,
scroll: u16,
@ -25,10 +24,6 @@ impl Screen for FileViewer {
self
}
fn get_ui(&self) -> &Arc<UI> {
&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<UI>, title: String, content: &str) -> Self {
pub fn new(title: String, content: &str) -> Self {
Self {
ui,
title,
text: parse_document(content.to_owned()),
scroll: 0,

View file

@ -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<Box<dyn Future<Output = Box<dyn Screen>> + 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<dyn Screen>
});
InteractionResult::OpenFutureScreen { screen: fut }

View file

@ -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<UI>,
sender: Option<oneshot::Sender<UserChoice>>,
eula_needed: bool,
@ -48,7 +48,6 @@ pub struct TermsUpdaterScreen {
}
impl TermsUpdaterScreen {
pub fn new(
ui: Arc<UI>,
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<dyn Screen>
Box::new(FileViewer::new(terms_type.to_string(), &content))
as Box<dyn Screen>
});
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,

View file

@ -26,7 +26,7 @@ pub static FPS: Lazy<RwLock<(f64, f64)>> = Lazy::new(|| RwLock::new((0.0, 0.0)))
pub struct UI {
pub terminal: Arc<Mutex<Terminal<CrosstermBackend<Stdout>>>>,
screen: Arc<RwLock<Option<Box<dyn Screen>>>>,
screen_stack: Arc<RwLock<Vec<Box<dyn Screen>>>>,
}
pub fn start_tui() -> Arc<UI> {
@ -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<dyn Screen>) {
*self.screen.write().await = Some(screen);
self.screen_stack.write().await.push(screen);
}
pub async fn replace_screen(&self, screen: Box<dyn Screen>) {
let mut stack = self.screen_stack.write().await;
stack.pop();
stack.push(screen);
}
pub async fn handle_input(self: Arc<Self>, 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| {

View file

@ -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;

View file

@ -59,7 +59,7 @@ async fn ensure_initial_consent(ui: Arc<UI>, 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<UI>, 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(),

View file

@ -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;