From befc35bbfd6e8ce5e7c72cd1164161be06705715 Mon Sep 17 00:00:00 2001 From: Alex Emmet <111742636+Alex-Emmet@users.noreply.github.com> Date: Fri, 3 Oct 2025 12:24:42 +0200 Subject: [PATCH] UI Corners & Pingpong --- src/gui/log_panel.rs | 194 ++++++++++++++++++++---------- src/gui/mod.rs | 3 + src/gui/ratatui_interface.rs | 6 +- src/gui/widgets/betterblock.rs | 68 +++++++++++ src/main.rs | 2 - src/omikron/omikron_connection.rs | 54 ++++----- src/omikron/ping_pong_task.rs | 134 +++++++-------------- 7 files changed, 267 insertions(+), 194 deletions(-) create mode 100644 src/gui/widgets/betterblock.rs diff --git a/src/gui/log_panel.rs b/src/gui/log_panel.rs index ac03d32..5618198 100644 --- a/src/gui/log_panel.rs +++ b/src/gui/log_panel.rs @@ -1,15 +1,20 @@ +use crate::APP_STATE; +use crate::gui::widgets::betterblock::draw_block_joins; use crate::langu::language_manager::from_key; -use crate::{APP_STATE, omikron::ping_pong_task::PingPongTask}; use crossterm::{ ExecutableCommand, terminal::{LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, }; -use ratatui::widgets::canvas::{Canvas, Line}; +use ratatui::widgets::{ + BorderType, + canvas::{Canvas, Line}, +}; use ratatui::{ Terminal, backend::CrosstermBackend, layout::{Constraint, Direction, Layout}, style::Color, + symbols, widgets::{Block, Borders, List, ListItem, Paragraph}, }; use std::{collections::VecDeque, io::stdout, process::Command, thread, time::Duration}; @@ -112,15 +117,33 @@ fn smooth_data(data: &[(f64, f64)], window_size: usize) -> Vec<(f64, f64)> { fn downsample_to_fit_width(data: &[(f64, f64)], width: u16) -> Vec<(f64, f64)> { let width_usize = (width as usize) * 2; let len = data.len(); - if len == 0 { - return Vec::new(); - } - let slice = if len <= width_usize { - data.to_vec() - } else { + + if len >= width_usize { + // Trim data to fit data[len - width_usize..].to_vec() - }; - slice + } else { + let mut result = Vec::with_capacity(width_usize); + + // Define X spacing (so dummy points are properly spaced across the canvas) + let dx = 1.0; + let pad_len = width_usize - len; + + // If we have real data, use its first x position to determine where to start padding + let start_x = data + .first() + .map(|(x, _)| x - (dx * pad_len as f64)) + .unwrap_or(0.0); + let y = data.first().map(|(_, y)| *y).unwrap_or(0.0); + + // Fill padding with increasing x positions so they're visible + for i in 0..pad_len { + result.push((start_x + i as f64 * dx, -1 as f64)); + } + + // Then append the real data + result.extend_from_slice(data); + result + } } fn measure_ping_ms(host: &str) -> Option { @@ -195,10 +218,7 @@ pub fn setup() { st.push_net_down((counter, net_down)); st.push_net_up((counter, net_up)); - st.sys_info = format!( - "CPU: {:.1}% RAM: {:.1}%\nNetDown: {} NetUp: {}", - tcpu, ram, delta_received, delta_transmitted - ); + st.sys_info = format!("NetDown: {} NetUp: {}", delta_received, delta_transmitted); } counter += 1.0; @@ -216,6 +236,8 @@ pub fn setup() { loop { terminal .draw(|f| { + let sys = System::new_all(); + let size = f.size(); let chunks = Layout::default() .direction(Direction::Horizontal) @@ -240,47 +262,55 @@ pub fn setup() { let right_chunks = Layout::default() .direction(Direction::Vertical) - .constraints([Constraint::Length(5), Constraint::Min(0)]) + .constraints([Constraint::Length(3), Constraint::Min(0)]) .split(right); { let st = APP_STATE.lock().unwrap(); - let header = Paragraph::new(st.sys_info.clone()) - .block(Block::default().title("System Info").borders(Borders::ALL)); + let header = Paragraph::new(st.sys_info.clone()).block( + Block::default() + .title("System Info") + .borders(Borders::TOP.union(Borders::LEFT).union(Borders::RIGHT)), + ); f.render_widget(header, right_chunks[0]); } let grid_chunks = Layout::default() .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) + .constraints([Constraint::Percentage(49), Constraint::Percentage(51)]) .split(right_chunks[1]); let left_column = Layout::default() .direction(Direction::Vertical) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) + .constraints([Constraint::Percentage(49), Constraint::Percentage(51)]) .split(grid_chunks[0]); let right_column = Layout::default() .direction(Direction::Vertical) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) + .constraints([Constraint::Percentage(49), Constraint::Percentage(51)]) .split(grid_chunks[1]); let st = APP_STATE.lock().unwrap(); let w = grid_chunks[0].width.saturating_sub(2); - let ping_ds = downsample_to_fit_width(&smooth_data(&st.ping, 3), w); + let ping_ds = downsample_to_fit_width(&smooth_data(&st.ping, 3), w + 1); let cpu_ds = downsample_to_fit_width(&smooth_data(&st.cpu, 3), w); let ram_ds = downsample_to_fit_width(&smooth_data(&st.ram, 3), w); - let down_ds = downsample_to_fit_width(&st.net_down, w); - let up_ds = downsample_to_fit_width(&st.net_up, w); + let down_ds = downsample_to_fit_width(&st.net_down, w + 1); + let up_ds = downsample_to_fit_width(&st.net_up, w + 1); // ---- Render CPU ---- { let min_x = cpu_ds.first().map(|(x, _)| *x).unwrap_or(0.0); let max_x = cpu_ds.last().map(|(x, _)| *x).unwrap_or(100.0); - + let block = Block::default() + .title(format!( + "CPU {}%", + &st.cpu.last().unwrap_or(&(0.0 as f64, 0.0 as f64)).1 + )) + .borders(Borders::TOP.union(Borders::RIGHT).union(Borders::LEFT)); let canvas = Canvas::default() - .block(Block::default().title("CPU %").borders(Borders::ALL)) + .block(block) .x_bounds([min_x, max_x]) .y_bounds([0.0, 100.0]) .paint(|ctx| { @@ -295,14 +325,38 @@ pub fn setup() { } }); f.render_widget(canvas, left_column[0]); + draw_block_joins( + f, + left_column[0], + Borders::TOP.union(Borders::LEFT), + Borders::TOP, + ); + draw_block_joins( + f, + left_column[0], + Borders::TOP.union(Borders::RIGHT), + Borders::RIGHT, + ); } // ---- Render RAM ---- { let min_x = ram_ds.first().map(|(x, _)| *x).unwrap_or(0.0); let max_x = ram_ds.last().map(|(x, _)| *x).unwrap_or(100.0); + + let ram_used = sys.used_memory() as f64 / 1024.0 / 1024.0 / 1024.0; + let ram_total = sys.total_memory() as f64 / 1024.0 / 1024.0 / 1024.0; + let ram = (sys.used_memory() as f64 / sys.total_memory() as f64) * 100.0; + let canvas = Canvas::default() - .block(Block::default().title("RAM %").borders(Borders::ALL)) + .block( + Block::default() + .title(format!( + "RAM {:.1}% ({:.1}GiB/{:.1}GiB)", + ram, ram_used, ram_total + )) + .borders(Borders::ALL), + ) .x_bounds([min_x, max_x]) .y_bounds([0.0, 100.0]) .paint(|ctx| { @@ -316,7 +370,14 @@ pub fn setup() { }); } }); + f.render_widget(canvas, left_column[1]); + draw_block_joins( + f, + left_column[1], + Borders::ALL, + Borders::TOP.union(Borders::RIGHT), + ); } // ---- Render Ping ---- @@ -328,8 +389,8 @@ pub fn setup() { let canvas = Canvas::default() .block( Block::default() - .title(format!("Ping {}(ms)", max_y)) - .borders(Borders::ALL), + .title(format!("Ping {:.1}(ms)", max_y)) + .borders(Borders::TOP.union(Borders::RIGHT)), ) .x_bounds([min_x, max_x]) .y_bounds([0.0, max_y]) @@ -345,6 +406,12 @@ pub fn setup() { } }); f.render_widget(canvas, right_column[0]); + draw_block_joins( + f, + right_column[0], + Borders::RIGHT.union(Borders::TOP), + Borders::TOP, + ); } // ---- Render Network ---- @@ -356,42 +423,47 @@ pub fn setup() { max_sum = max_sum.max(up + down); } - let canvas = Canvas::default() - .block( - Block::default() - .title("Network Up/Down") - .borders(Borders::ALL), - ) - .x_bounds([min_x, max_x]) - .y_bounds([0.0, max_sum]) - .paint(|ctx| { - for (x, dval) in &down_ds { - ctx.draw(&Line { - x1: *x, - y1: 0.0, - x2: *x, - y2: *dval, - color: Color::Red, - }); - } - for (x, uval) in &up_ds { - let dval = down_ds - .iter() - .find(|(xx, _)| (*xx - *x).abs() < f64::EPSILON) - .map(|(_, y)| *y) - .unwrap_or(0.0); + let canvas = + Canvas::default() + .block(Block::default().title("Network Up/Down").borders( + Borders::TOP.union(Borders::RIGHT).union(Borders::BOTTOM), + )) + .x_bounds([min_x, max_x]) + .y_bounds([0.0, max_sum]) + .paint(|ctx| { + for (x, dval) in &down_ds { + ctx.draw(&Line { + x1: *x, + y1: 0.0, + x2: *x, + y2: *dval, + color: Color::Red, + }); + } + for (x, uval) in &up_ds { + let dval = down_ds + .iter() + .find(|(xx, _)| (*xx - *x).abs() < f64::EPSILON) + .map(|(_, y)| *y) + .unwrap_or(0.0); - ctx.draw(&Line { - x1: *x, - y1: dval, - x2: *x, - y2: dval + *uval, - color: Color::Green, - }); - } - }); + ctx.draw(&Line { + x1: *x, + y1: dval, + x2: *x, + y2: dval + *uval, + color: Color::Green, + }); + } + }); f.render_widget(canvas, right_column[1]); + draw_block_joins( + f, + right_column[1], + Borders::TOP.union(Borders::RIGHT).union(Borders::BOTTOM), + Borders::TOP, + ); } }) .unwrap(); diff --git a/src/gui/mod.rs b/src/gui/mod.rs index cdd1499..0f4d72d 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -1,2 +1,5 @@ pub mod log_panel; pub mod ratatui_interface; +pub mod widgets { + pub mod betterblock; +} diff --git a/src/gui/ratatui_interface.rs b/src/gui/ratatui_interface.rs index 31a7050..36abbf3 100644 --- a/src/gui/ratatui_interface.rs +++ b/src/gui/ratatui_interface.rs @@ -1,14 +1,10 @@ use color_eyre::Result; -use color_eyre::eyre::Error; -use crossterm::event::{self, Event}; +use ratatui::Frame; use ratatui::layout::{Constraint, Direction, Layout}; use ratatui::widgets::{Block, Paragraph}; -use ratatui::{DefaultTerminal, Frame}; use sys_info; use tokio::task; -use crate::omikron::omikron_connection::OmikronConnection; - pub fn launch(connection_status: bool) -> Result<()> { color_eyre::install()?; task::spawn(run(connection_status)); diff --git a/src/gui/widgets/betterblock.rs b/src/gui/widgets/betterblock.rs new file mode 100644 index 0000000..f953394 --- /dev/null +++ b/src/gui/widgets/betterblock.rs @@ -0,0 +1,68 @@ +use ratatui::layout::Rect; +use ratatui::prelude::*; +use ratatui::style::Style; +use ratatui::widgets::Borders; + +fn set_join_char(frame: &mut Frame, x: u16, y: u16, c: char) { + frame + .buffer_mut() + .set_string(x, y, c.to_string(), Style::default()); +} + +/// Draws corner join characters only if the block has borders on that side +pub fn draw_block_joins(frame: &mut Frame, area: Rect, borders: Borders, joins: Borders) { + let x0 = area.x; + let y0 = area.y; + let x1 = area.x + area.width - 1; + let y1 = area.y + area.height - 1; + + // Top-left corner + if borders.contains(Borders::TOP) && borders.contains(Borders::LEFT) { + let top_left = match (joins.contains(Borders::TOP), joins.contains(Borders::LEFT)) { + (true, true) => '┼', + (true, false) => '├', + (false, true) => '┬', + (false, false) => '┌', + }; + set_join_char(frame, x0, y0, top_left); + } + + // Top-right corner + if borders.contains(Borders::TOP) && borders.contains(Borders::RIGHT) { + let top_right = match (joins.contains(Borders::TOP), joins.contains(Borders::RIGHT)) { + (true, true) => '┼', + (true, false) => '┤', + (false, true) => '┬', + (false, false) => '┐', + }; + set_join_char(frame, x1, y0, top_right); + } + + // Bottom-left corner + if borders.contains(Borders::BOTTOM) && borders.contains(Borders::LEFT) { + let bottom_left = match ( + joins.contains(Borders::BOTTOM), + joins.contains(Borders::LEFT), + ) { + (true, true) => '┼', + (true, false) => '├', + (false, true) => '┴', + (false, false) => '└', + }; + set_join_char(frame, x0, y1, bottom_left); + } + + // Bottom-right corner + if borders.contains(Borders::BOTTOM) && borders.contains(Borders::RIGHT) { + let bottom_right = match ( + joins.contains(Borders::BOTTOM), + joins.contains(Borders::RIGHT), + ) { + (true, true) => '┼', + (true, false) => '┤', + (false, true) => '┴', + (false, false) => '┘', + }; + set_join_char(frame, x1, y1, bottom_right); + } +} diff --git a/src/main.rs b/src/main.rs index 854c988..e98d59e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,7 +19,6 @@ use crate::gui::log_panel; use crate::gui::log_panel::{AppState, log_message, log_message_trans}; use crate::langu::language_creator; use crate::omikron::omikron_connection::OmikronConnection; -use crate::omikron::ping_pong_task::PingPongTask; use crate::users::user_manager::UserManager; use crate::util::config_util::CONFIG; @@ -75,7 +74,6 @@ async fn main() { // IDENTIFICATION ON OMIKRON let omikron: OmikronConnection = OmikronConnection::new(); omikron.connect().await; - let _ping_pong_task = PingPongTask::new(Arc::new(OmikronConnection::new())); omikron .send_message( CommunicationValue::new(CommunicationType::identification) diff --git a/src/omikron/omikron_connection.rs b/src/omikron/omikron_connection.rs index e8b23be..2d4f4da 100644 --- a/src/omikron/omikron_connection.rs +++ b/src/omikron/omikron_connection.rs @@ -2,7 +2,7 @@ use crate::APP_STATE; use crate::data::communication::{CommunicationType, CommunicationValue, DataTypes}; use crate::gui::log_panel::log_message; use crate::gui::log_panel::log_message_trans; -use crate::omikron::ping_pong_task::{self, PingPongTask}; +use crate::omikron::ping_pong_task::*; use crate::users::contact::Contact; use crate::users::user_community_util::UserCommunityUtil; use crate::util::chat_files::ChatFiles; @@ -11,13 +11,13 @@ use futures_util::{SinkExt, StreamExt}; use json::JsonValue; use json::number::Number; use std::collections::HashMap; -use std::pin::Pin; +use std::ops::Deref; use std::str::FromStr; use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; use tokio::net::TcpStream; use tokio::sync::Mutex; -use tokio::time::{Duration, sleep}; +use tokio::time::{Duration, Instant, sleep}; use tokio_tungstenite::{ MaybeTlsStream, WebSocketStream, connect_async, tungstenite::protocol::Message, }; @@ -37,7 +37,8 @@ pub struct OmikronConnection { >, waiting: Arc>>>, // waiting for responses pingpong: Arc>>>, // ping-pong handler - ping_pong_task: Arc>>>, + pub message_queue: Arc>>, + pub message_send_times: Arc>>, } impl OmikronConnection { @@ -46,10 +47,15 @@ impl OmikronConnection { writer: Arc::new(Mutex::new(None)), waiting: Arc::new(Mutex::new(HashMap::new())), pingpong: Arc::new(Mutex::new(None)), - ping_pong_task: Arc::new(Mutex::new(None)), + message_queue: Arc::new(Mutex::new(Vec::new())), + message_send_times: Arc::new(Mutex::new(HashMap::new())), } } + pub async fn reconnect(&self) { + self.disconnect().await; + self.connect().await; + } /// Connect loop with retry pub async fn connect(&self) { loop { @@ -58,18 +64,19 @@ impl OmikronConnection { let (write_half, read_half) = ws_stream.split(); *self.writer.lock().await = Some(write_half); self.spawn_listener(read_half).await; - - let ppt = PingPongTask::new(Arc::new(self.clone())); - *self.ping_pong_task.lock().await = Some(Arc::new(ppt.clone())); - tokio::spawn(async move { + let cloned_self = self.clone(); + let handle = tokio::spawn(async move { loop { - ppt.send_ping(); - sleep(Duration::from_secs(5)).await; + cloned_self.send_ping().await; + sleep(Duration::from_secs(1)).await; } }); + + *self.pingpong.lock().await = Some(handle); break; } Err(e) => { + log_message("CONNECTION FAILED"); sleep(Duration::from_secs(2)).await; } } @@ -106,15 +113,10 @@ impl OmikronConnection { break; } Ok(Message::Text(text)) => { - let mut cv = CommunicationValue::from_json(&text); // needs CommunicationValue parser + let mut cv = CommunicationValue::from_json(&text); if cv.is_type(CommunicationType::pong) { - sel.ping_pong_task - .lock() - .await - .as_mut() - .unwrap() - .handle_pong(&cv, true); - return; + sel.handle_pong(&cv, true).await; + continue; } // ************************************************ // // Direct messages // @@ -342,18 +344,4 @@ impl OmikronConnection { Err(tokio_tungstenite::tungstenite::Error::ConnectionClosed) } } - - pub async fn send_ping_message(&self, uuid: Uuid) { - // Send the ping message over the connection - let ping_message = CommunicationValue::new(CommunicationType::ping) - .with_id(uuid) - .add_data_num(DataTypes::last_ping, Number::from(2)) - .to_json() - .to_string(); - self.send_message(ping_message).await; - } - pub async fn reconnect(&self) { - self.disconnect().await; - self.connect().await; - } } diff --git a/src/omikron/ping_pong_task.rs b/src/omikron/ping_pong_task.rs index 4d6a140..22bed66 100644 --- a/src/omikron/ping_pong_task.rs +++ b/src/omikron/ping_pong_task.rs @@ -1,113 +1,61 @@ use crate::APP_STATE; use crate::data::communication::{CommunicationType, CommunicationValue, DataTypes}; -use crate::gui::log_panel::AppState; +use crate::gui::log_panel::{log_message, log_message_trans}; use crate::omikron::omikron_connection::OmikronConnection; -use color_eyre::owo_colors::OwoColorize; +use crate::users::contact::Contact; +use crate::users::user_community_util::UserCommunityUtil; +use crate::util::chat_files::ChatFiles; +use crate::util::chats_util::{get_user, get_users, mod_user}; use futures_util::{SinkExt, StreamExt}; -use std::arch::x86_64::_SIDD_MASKED_NEGATIVE_POLARITY; +use json::JsonValue; +use json::number::Number; use std::collections::HashMap; +use std::ops::Deref; +use std::str::FromStr; use std::sync::Arc; +use std::time::{SystemTime, UNIX_EPOCH}; +use tokio::net::TcpStream; use tokio::sync::Mutex; use tokio::time::{Duration, Instant, sleep}; +use tokio_tungstenite::{ + MaybeTlsStream, WebSocketStream, connect_async, tungstenite::protocol::Message, +}; use uuid::Uuid; -#[derive(Clone)] -pub struct PingPongTask { - pub parent: Arc, - pub message_send_times: Arc>>, - pub no_ping_in: Arc>, - pub last_ping: Arc>>, -} -fn assert_send_sync() {} +impl OmikronConnection { + pub async fn send_ping(&self) { + let uuid = Uuid::new_v4(); + let send_time = Instant::now(); -#[test] -fn check_omikron_connection_send_sync() { - assert_send_sync::(); -} + self.message_send_times.lock().await.insert(uuid, send_time); + self.send_ping_message(uuid).await; + } -#[test] -fn check_pingpong_task_send_sync() { - assert_send_sync::(); -} -impl PingPongTask { - pub fn new(parent: Arc) -> Self { - let message_send_times = Arc::new(Mutex::new(HashMap::new())); - let no_ping_in = Arc::new(Mutex::new(-1)); - let last_ping = Arc::new(Mutex::new(None)); + pub async fn send_ping_message(&self, uuid: Uuid) { + let ping_message = CommunicationValue::new(CommunicationType::ping) + .with_id(uuid) + .add_data_num(DataTypes::last_ping, Number::from(2)) + .to_json() + .to_string(); - let task = PingPongTask { - parent: parent.clone(), - message_send_times: message_send_times.clone(), - no_ping_in: no_ping_in.clone(), - last_ping: last_ping.clone(), + self.send_message(ping_message).await; + } + + /// Handles incoming pong and calculates latency + pub async fn handle_pong(&self, cv: &CommunicationValue, log: bool) { + let id = cv.get_id(); + let send_time_opt = { + let queue = self.message_send_times.lock().await; + queue.get(&id).cloned() }; - task - } + if let Some(send_time) = send_time_opt { + let ping = Instant::now().duration_since(send_time).as_millis() as f64; + self.message_send_times.lock().await.remove(&id); - pub fn send_ping(&self) { - let sel = self.clone(); - tokio::spawn(async move { - let uuid = Uuid::new_v4(); - let send_time = Instant::now(); - let mut message_send_times = sel.message_send_times.lock().await; - message_send_times.insert(uuid, send_time); - - let no_ping_in_val = { - let no_ping_in = sel.no_ping_in.lock().await; - *no_ping_in - }; - - if no_ping_in_val != -1 { - sel.handle_slow_connection(no_ping_in_val).await; - } else { - sel.parent.send_ping_message(uuid).await; + if log { + APP_STATE.lock().unwrap().push_ping_val(ping); } - }); - } - - pub async fn handle_slow_connection(&self, no_ping_in: i32) { - if no_ping_in > 8 { - self.parent.reconnect().await; - self.reconnect().await; } } - - pub async fn reconnect(&self) { - let mut no_ping_in = self.no_ping_in.lock().await; - *no_ping_in = -1; - } - - pub fn handle_pong(&self, cv: &CommunicationValue, log: bool) { - let sel = self.clone(); - let cv = cv.clone(); - tokio::spawn(async move { - let send_time = { - let message_send_times = sel.message_send_times.lock().await; - message_send_times.get(&cv.get_id()).cloned() - }; - - if let Some(send_time) = send_time { - let receive_time = Instant::now(); - let ping = receive_time.duration_since(send_time).as_millis() as u64; - - let mut last_ping = sel.last_ping.lock().await; - *last_ping = Some(ping); - let mut no_ping_in = sel.no_ping_in.lock().await; - *no_ping_in = -1; - let mut message_send_times = sel.message_send_times.lock().await; - message_send_times.remove(&cv.get_id()); - - if log { - APP_STATE.lock().unwrap().push_log("12.0".to_string()); - APP_STATE.lock().unwrap().push_ping_val(12.0); - } - } - }); - } - - pub async fn cancel(&self) { - let mut no_ping_in = self.no_ping_in.lock().await; - *no_ping_in = -1; - } }