diff --git a/logs/log_1771405721.txt b/logs/log_1771405721.txt new file mode 100644 index 0000000..19beaef --- /dev/null +++ b/logs/log_1771405721.txt @@ -0,0 +1,13 @@ +1771405721036 IOTA ID: 0 +1771405721036 User IDS: +1771405721046 Community IDS: +1771405721047 TLS certificate 'certs/cert.pem' not found. +1771405721047 HTTP Server running on 0.0.0.0:1984 +1771405721048 Communities active on ws://10.209.226.34:1984/community/... +1771405721048 Downloading ZIP file... +1771405721507 Failed to download file: Status 404 Not Found +1771405721507 Error downloading file: Failed to download file: Status 404 Not Found +1771405721507 IOTA_REGISTER_NEW +1771405721512 OMIKRON_CONNECTING +1771405721987 OMIKRON_CONNECTION_SUCCESS +1771405731879 Web Server shutdown complete. diff --git a/src/communities/community_connection.rs b/src/communities/community_connection.rs index 1ec919f..a08f01f 100644 --- a/src/communities/community_connection.rs +++ b/src/communities/community_connection.rs @@ -160,7 +160,7 @@ impl CommunityConnection { let user_pub_key: PublicKey = match PublicKey::from_bytes(&user_public_key_bytes) { Some(key) => key, - None => { + __ => { self.send_error_response(&cv.get_id(), CommunicationType::error_invalid_user_id) .await; return; @@ -178,7 +178,7 @@ impl CommunityConnection { let shared_secret = match community_private_key.to_diffie_hellman(&user_pub_key) { Some(secret) => secret, - None => { + _ => { self.send_error_response(&cv.get_id(), CommunicationType::error) .await; return; @@ -223,7 +223,7 @@ impl CommunityConnection { async fn handle_challenge_response(self: Arc, cv: CommunicationValue) { let client_challenge_response_b64 = match cv.get_data(DataTypes::challenge) { Some(data) => data.to_string(), - None => { + _ => { self.send_error_response(&cv.get_id(), CommunicationType::error) .await; return; @@ -273,7 +273,7 @@ impl CommunityConnection { let shared_secret = match community_private_key.to_diffie_hellman(&user_pub_key) { Some(secret) => secret, - None => { + _ => { self.send_error_response(&cv.get_id(), CommunicationType::error) .await; return; diff --git a/src/communities/community_manager.rs b/src/communities/community_manager.rs index 573913d..8369f5a 100644 --- a/src/communities/community_manager.rs +++ b/src/communities/community_manager.rs @@ -1,5 +1,5 @@ use crate::communities::community::{self, Community}; -use crate::gui::log_panel; +use crate::log; use crate::util::file_util; use once_cell::sync::Lazy; use std::collections::HashMap; @@ -35,7 +35,7 @@ pub async fn load_communities() { if let Some(community) = community::load(&name).await { add_community(community).await; } else { - log_panel::log_message(format!("failed to load the {} community", &name)); + log!("failed to load the {} community", &name); } } } diff --git a/src/communities/interactables/text_chat.rs b/src/communities/interactables/text_chat.rs index 8200afd..2cb7585 100644 --- a/src/communities/interactables/text_chat.rs +++ b/src/communities/interactables/text_chat.rs @@ -4,7 +4,7 @@ use crate::{ interactables::interactable::Interactable, }, data::communication::{CommunicationType, CommunicationValue, DataTypes}, - gui::log_panel::log_message, + log, util::file_util::{get_children, load_file, save_file}, }; use async_trait::async_trait; @@ -37,7 +37,7 @@ impl TextChat { ); if let Err(e) = fs::create_dir_all(user_dir) { - log_message(format!("Failed to create chat directory: {}", e)); + log!("Failed to create chat directory: {}", e); return; } @@ -56,16 +56,15 @@ impl TextChat { break; } } else { - log_message(format!("Failed to parse existing JSON file: {}", file_name)); + log!("Failed to parse existing JSON file: {}", file_name); } } else { - // New file, use empty array break; } chunk_index += 1; if chunk_index > 1000 { - log_message(format!("Too many message chunks. Aborting add.")); + log!("Too many message chunks. Aborting add."); return; } } @@ -77,12 +76,12 @@ impl TextChat { }; if let Err(e) = message_chunk.push(json_obj) { - log_message(format!("Failed to push new message into JSON array: {}", e)); + log!("Failed to push new message into JSON array: {}", e); return; } let file_name = format!("msgs_{}.json", chunk_index); - log_message(format!("Saving message to {}/{}", user_dir, file_name)); + log!("Saving message to {}/{}", user_dir, file_name); save_file(&user_dir, &file_name, &message_chunk.dump()); } pub fn get_messages(&self, loaded_messages: i64, amount: i64) -> JsonValue { diff --git a/src/gui/app_state.rs b/src/gui/app_state.rs index 23997cb..925d89a 100644 --- a/src/gui/app_state.rs +++ b/src/gui/app_state.rs @@ -1,10 +1,10 @@ -use std::collections::VecDeque; - +use crate::gui::elements::log_card::UiLogEntry; use json::{JsonValue, object}; +use std::collections::VecDeque; #[derive(Clone)] pub struct AppState { - pub logs: VecDeque, + pub logs: VecDeque, pub cpu: Vec<(f64, f64)>, pub ram: Vec<(f64, f64)>, pub ping: Vec<(f64, f64)>, @@ -28,13 +28,17 @@ impl AppState { } } - pub fn push_log(&mut self, msg: String) { + pub fn push_log(&mut self, msg: UiLogEntry) { if self.logs.len() >= MAX_LOGS { self.logs.pop_front(); } self.logs.push_back(msg); } + pub fn get_logs(&self) -> &VecDeque { + &self.logs + } + pub fn push_cpu(&mut self, pt: (f64, f64)) { self.cpu.push(pt); if self.cpu.len() > MAX_POINTS { @@ -112,28 +116,23 @@ impl AppState { let len = data.len(); if len >= width_usize { - // Trim data to fit data[len - width_usize..].to_vec() } 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 _ = 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 } diff --git a/src/gui/elements/elements.rs b/src/gui/elements/elements.rs new file mode 100644 index 0000000..bc1a3ec --- /dev/null +++ b/src/gui/elements/elements.rs @@ -0,0 +1,35 @@ +use std::any::Any; + +use crossterm::event::KeyEvent; +use ratatui::{Frame, layout::Rect}; + +use crate::gui::{interaction_result::InteractionResult, screens::screens::Screen}; + +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); +} + +pub trait InfoElement: Send + Sync + Any { + fn as_any(&self) -> &dyn Any; + fn as_any_mut(&mut self) -> &mut dyn Any; + fn as_element(&self) -> &dyn Element; + fn as_element_mut(&mut self) -> &mut dyn Element; + + fn get_info_screen(&self) -> Box; +} + +pub trait InteractableElement: Send + Sync + Any { + fn as_any(&self) -> &dyn Any; + fn as_any_mut(&mut self) -> &mut dyn Any; + fn as_element(&self) -> &dyn Element; + fn as_element_mut(&mut self) -> &mut dyn Element; + + fn interact(&mut self, key: KeyEvent) -> InteractionResult; + + fn can_focus(&self) -> bool; + fn is_focused(&self) -> bool; + fn focus(&mut self, f: bool); +} diff --git a/src/gui/elements/log_card.rs b/src/gui/elements/log_card.rs new file mode 100644 index 0000000..85d0fa8 --- /dev/null +++ b/src/gui/elements/log_card.rs @@ -0,0 +1,113 @@ +use crate::gui::elements::elements::InteractableElement; +use crate::gui::interaction_result::InteractionResult; +use crate::{APP_STATE, gui::elements::elements::Element}; +use crossterm::event::{KeyCode, KeyEvent}; +use ratatui::{ + Frame, + layout::Rect, + style::{Color, Style}, + text::{Line, Span}, + widgets::{Block, Borders, Paragraph, Wrap}, +}; +use std::any::Any; +use std::sync::Arc; + +#[derive(Clone)] +pub struct UiLogEntry { + pub line: String, + pub color: Color, +} + +pub struct LogCard { + focused: bool, + scroll: u16, +} + +impl LogCard { + pub fn new() -> Self { + Self { + focused: false, + scroll: 0, + } + } +} +impl Element for LogCard { + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn render(&self, f: &mut Frame, area: Rect) { + let state = APP_STATE.lock().unwrap(); + let logs = state.get_logs(); + + let lines: Vec = logs + .iter() + .map(|log| Line::from(Span::raw(log.line.clone())).style(log.color)) + .collect(); + + let block = Block::default() + .title("Logs") + .borders(Borders::ALL) + .border_style(if self.focused { + Style::default().fg(Color::Yellow) + } else { + Style::default() + }); + + let paragraph = Paragraph::new(lines) + .block(block) + .wrap(Wrap { trim: false }) + .scroll((self.scroll, 0)); + + f.render_widget(paragraph, area); + } +} +impl InteractableElement for LogCard { + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn as_element(&self) -> &dyn Element { + self + } + + fn as_element_mut(&mut self) -> &mut dyn Element { + self + } + + fn interact(&mut self, key: KeyEvent) -> InteractionResult { + match key.code { + KeyCode::Up => { + if self.scroll > 0 { + self.scroll -= 1; + } + InteractionResult::Handeled + } + KeyCode::Down => { + self.scroll += 1; + InteractionResult::Handeled + } + _ => InteractionResult::Unhandeled, + } + } + + fn can_focus(&self) -> bool { + true + } + + fn is_focused(&self) -> bool { + self.focused + } + + fn focus(&mut self, f: bool) { + self.focused = f; + } +} diff --git a/src/gui/input_handler.rs b/src/gui/input_handler.rs deleted file mode 100644 index c46a19f..0000000 --- a/src/gui/input_handler.rs +++ /dev/null @@ -1,112 +0,0 @@ -use std::time::Duration; - -use crate::ACTIVE_TASKS; -use crate::{RELOAD, SHUTDOWN, gui::tui::UNIQUE, util::config_util::CONFIG}; -use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers, poll, read}; - -use json::JsonValue; - -pub fn setup_input_handler() { - tokio::spawn(async move { - { - let mut tasks = ACTIVE_TASKS.lock().unwrap(); - tasks.push("Input Handler".to_string()); - } - - loop { - { - let should_shutdown = *SHUTDOWN.read().await; - if should_shutdown { - break; - } - } - - let has_event = match poll(Duration::from_millis(100)) { - Ok(true) => true, - Ok(false) => false, - Err(_) => false, - }; - - if has_event { - match read() { - Ok(event) => { - if let Event::Key(key_event) = event { - handle_input(key_event).await; - } - } - Err(_) => (), - } - } - } - { - let mut tasks = ACTIVE_TASKS.lock().unwrap(); - tasks.retain(|t| t != "Input Handler"); - } - }); -} - -pub async fn handle_input(key: KeyEvent) { - match (key.code, key.modifiers) { - (KeyCode::Char('q'), KeyModifiers::CONTROL) => { - *SHUTDOWN.write().await = true; - } - (KeyCode::Char('c'), KeyModifiers::CONTROL) => { - *SHUTDOWN.write().await = true; - } - (KeyCode::Char('r'), KeyModifiers::CONTROL) => { - { - *RELOAD.write().await = true; - } - { - *SHUTDOWN.write().await = true; - } - } - (KeyCode::Backspace, KeyModifiers::NONE) => { - let password: String = { - let cfg = CONFIG.read().await; - cfg.get("password").as_str().unwrap_or("").to_string() - }; - let password: &str = &password; - let password = match password.char_indices().next_back() { - Some((i, _)) => &password[..i], - _ => password, - }; - - CONFIG - .write() - .await - .change("password", JsonValue::String(password.to_string())); - CONFIG.write().await.update(); - *UNIQUE.write().await = true; - } - (KeyCode::Char(c), KeyModifiers::NONE) => { - let password: String = { - let cfg = CONFIG.read().await; - cfg.get("password").as_str().unwrap_or("").to_string() - }; - let password = &format!("{}{}", password, c); - - CONFIG - .write() - .await - .change("password", JsonValue::String(password.to_string())); - CONFIG.write().await.update(); - *UNIQUE.write().await = true; - } - (KeyCode::Char(c), KeyModifiers::SHIFT) => { - let password: String = { - let cfg = CONFIG.read().await; - cfg.get("password").as_str().unwrap_or("").to_string() - }; - let password = &format!("{}{}", password, c); - - CONFIG - .write() - .await - .change("password", JsonValue::String(password.to_string())); - CONFIG.write().await.update(); - *UNIQUE.write().await = true; - } - _ => {} - } -} diff --git a/src/gui/interaction_result.rs b/src/gui/interaction_result.rs new file mode 100644 index 0000000..34baef2 --- /dev/null +++ b/src/gui/interaction_result.rs @@ -0,0 +1,27 @@ +use std::fmt::Debug; +use std::future::Future; +use std::pin::Pin; + +use crate::gui::screens::screens::Screen; + +pub enum InteractionResult { + OpenScreen { + screen: Box, + }, + OpenFutureScreen { + screen: Pin> + Send>>, + }, + Handeled, + Unhandeled, +} + +impl Debug for InteractionResult { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + InteractionResult::OpenScreen { screen: _ } => write!(f, "OpenScreen"), + InteractionResult::OpenFutureScreen { screen: _ } => write!(f, "OpenFutureScreen"), + InteractionResult::Handeled => write!(f, "Handeled"), + InteractionResult::Unhandeled => write!(f, "Unhandeled"), + } + } +} diff --git a/src/gui/log_panel.rs b/src/gui/log_panel.rs deleted file mode 100644 index 5a3e753..0000000 --- a/src/gui/log_panel.rs +++ /dev/null @@ -1,108 +0,0 @@ -use crate::ACTIVE_TASKS; -use crate::APP_STATE; -use crate::SHUTDOWN; -use crate::gui::tui::UNIQUE; -use crate::langu::language_manager::format; -use crate::langu::language_manager::from_key; - -use crate::data::communication::{CommunicationType, CommunicationValue, DataTypes}; - -use std::{thread, time::Duration}; -use sysinfo::{RefreshKind, System}; - -pub fn log_cv(cv: &CommunicationValue) { - if cv.is_type(CommunicationType::identification_response) { - let args = [cv.get_data(DataTypes::accepted).unwrap().as_str().unwrap()]; - log_message(format(&"identification_response", &args)); - } else { - log_message_trans(format!("{:?}", &cv.get_type())); - } - tokio::spawn(async move { - *UNIQUE.write().await = true; - }); -} -pub fn log_message_trans(key: impl Into) { - APP_STATE.lock().unwrap().push_log(from_key(&key.into())); - tokio::spawn(async move { - *UNIQUE.write().await = true; - }); -} -pub fn log_message(msg: impl Into) { - APP_STATE.lock().unwrap().push_log(msg.into()); - tokio::spawn(async move { - *UNIQUE.write().await = true; - }); -} -pub fn log_message_format(msg: impl Into, args: &[&str]) { - APP_STATE - .lock() - .unwrap() - .push_log(format(&msg.into(), args)); - tokio::spawn(async move { - *UNIQUE.write().await = true; - }); -} - -pub fn setup() { - tokio::spawn(async move { - { - ACTIVE_TASKS.lock().unwrap().push("metrics".to_string()); - } - 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; - *UNIQUE.write().await = true; - thread::sleep(Duration::from_millis(1000)); - } - { - ACTIVE_TASKS - .lock() - .unwrap() - .retain(|t| !t.eq(&"metrics".to_string())); - } - }); -} diff --git a/src/gui/mod.rs b/src/gui/mod.rs index e1c7598..d94ecdf 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -1,8 +1,10 @@ -pub mod app_state; -pub mod log_panel; -pub mod settings_panel; -pub mod widgets { - pub mod betterblock; +pub mod elements { + pub mod elements; + pub mod log_card; } -pub mod input_handler; -pub mod tui; +pub mod screens { + pub mod screens; +} +pub mod app_state; +pub mod interaction_result; +pub mod ui; diff --git a/src/gui/screens/screens.rs b/src/gui/screens/screens.rs new file mode 100644 index 0000000..78f3bee --- /dev/null +++ b/src/gui/screens/screens.rs @@ -0,0 +1,27 @@ +use std::{any::Any, sync::Arc}; + +use crossterm::event::KeyEvent; +use ratatui::{Frame, layout::Rect}; + +use crate::gui::{interaction_result::InteractionResult, ui::UI}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NavDirection { + Up, + Down, + Left, + Right, + + Next, + Prev, +} + +pub trait Screen: Send + Sync + Any { + fn as_any(&self) -> &dyn Any; + fn as_any_mut(&mut self) -> &mut dyn Any; + + fn get_ui(&self) -> &Arc; + + fn render(&self, f: &mut Frame, rect: Rect); + fn handle_input(&mut self, event: KeyEvent) -> InteractionResult; +} diff --git a/src/gui/settings_panel.rs b/src/gui/settings_panel.rs deleted file mode 100644 index 2838adc..0000000 --- a/src/gui/settings_panel.rs +++ /dev/null @@ -1,43 +0,0 @@ -use ratatui::{ - Frame, - layout::{Constraint, Direction, Layout, Margin, Rect}, - text::Span, - widgets::{Block, Paragraph}, -}; - -use crate::gui::app_state::AppState; - -pub fn draw(frame: &mut Frame, area: Rect, block: Block, password: String, _state: AppState) { - frame.render_widget(block, area); - - let padded = area.inner(Margin { - horizontal: 1, - vertical: 1, - }); - - let info = vec![ - "This is the technical end of your Iota", - "Press Ctrl+Q to quit", - "Press Ctrl+R to reload", - "Select a password for the WebUI", - ]; - let mut constraints: Vec = Vec::new(); - - for _ in 0..info.len() { - constraints.push(Constraint::Length(1)); - } - - constraints.push(Constraint::Length(3)); - - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints(constraints) - .split(padded); - - for i in 0..info.len() { - frame.render_widget(Paragraph::new(info[i]), chunks[i]); - } - - let prefix = Span::raw(format!("select password : {}", password)); - frame.render_widget(Paragraph::new(prefix), chunks[info.len()]); -} diff --git a/src/gui/tui.rs b/src/gui/tui.rs deleted file mode 100644 index 3e4b0d6..0000000 --- a/src/gui/tui.rs +++ /dev/null @@ -1,230 +0,0 @@ -use std::{io::Stdout, sync::Arc, time::Duration}; - -use crate::{ - APP_STATE, SHUTDOWN, - gui::{settings_panel, widgets::betterblock::draw_block_joins}, - util::config_util::CONFIG, -}; -use once_cell::sync::Lazy; -use ratatui::{ - Frame, Terminal, - layout::{Constraint, Direction, Layout, Rect}, - prelude::CrosstermBackend, - style::Color, - widgets::{ - Block, Borders, List, ListItem, - canvas::{Canvas, Line}, - }, -}; -use tokio::{ - self, - sync::{Mutex, RwLock}, -}; - -pub static UNIQUE: Lazy> = Lazy::new(|| RwLock::new(true)); -pub static TERMINAL: Lazy>>>> = - Lazy::new(|| Arc::new(Mutex::new(ratatui::init()))); - -pub fn start_tui() { - tokio::spawn(async move { - loop { - if *SHUTDOWN.read().await { - break; - } - - if *UNIQUE.read().await { - render_tui().await; - } else { - tokio::time::sleep(Duration::from_millis(50)).await; - } - } - ratatui::restore(); - }); -} - -pub async fn render_tui() { - let password = CONFIG - .read() - .await - .get("password") - .as_str() - .unwrap_or("") - .to_string(); - - let mut terminal = TERMINAL.lock().await; - let _ = terminal.draw(|f| { - let area = f.area(); - - let is_horizontal = area.width >= 120; - let chunks = if is_horizontal { - Layout::default() - .direction(Direction::Horizontal) - .constraints([ - Constraint::Percentage(40), - Constraint::Percentage(60), - Constraint::Min(40), - ]) - .split(area) - } else { - Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Percentage(40), Constraint::Percentage(60)]) - .split(area) - }; - - let state = APP_STATE.lock().unwrap().clone(); - - // LOGS PANEL - { - let borders = if is_horizontal { - Borders::LEFT.union(Borders::TOP).union(Borders::BOTTOM) - } else { - Borders::LEFT.union(Borders::RIGHT).union(Borders::TOP) - }; - let items: Vec = state - .logs - .iter() - .rev() - .map(|s| ListItem::new(s.clone())) - .collect(); - let list = List::new(items).block(Block::default().title("Logs").borders(borders)); - f.render_widget(list, chunks[0]); - } - // SETTINGS PANEL - { - let borders = if is_horizontal { - Borders::LEFT.union(Borders::TOP).union(Borders::BOTTOM) - } else { - Borders::ALL - }; - settings_panel::draw( - f, - chunks[1], - Block::default().title("Settings").borders(borders), - password, - state.clone(), - ); - if is_horizontal { - draw_block_joins( - f, - chunks[1], - Borders::TOP.union(Borders::LEFT).union(Borders::BOTTOM), - Borders::LEFT, - ); - } else { - draw_block_joins(f, chunks[1], Borders::ALL, Borders::TOP); - } - } - - // PERFORMANCE GRAPHS - if is_horizontal { - let stack = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Ratio(1, 3), - Constraint::Ratio(1, 3), - Constraint::Ratio(1, 3), - ]) - .split(chunks[2]); - - render_graphs( - f, - stack[0], - "CPU".into(), - "%".into(), - state.with_width(38).cpu, - Borders::TOP.union(Borders::LEFT).union(Borders::RIGHT), - Color::Cyan, - ); - draw_block_joins( - f, - stack[0], - Borders::TOP.union(Borders::LEFT), - Borders::LEFT, - ); - - render_graphs( - f, - stack[1], - "RAM".into(), - "%".into(), - state.with_width(38).ram, - Borders::TOP.union(Borders::LEFT).union(Borders::RIGHT), - Color::Green, - ); - draw_block_joins( - f, - stack[1], - Borders::TOP.union(Borders::LEFT).union(Borders::RIGHT), - Borders::TOP, - ); - - render_graphs( - f, - stack[2], - "PING".into(), - "ms".into(), - state.with_width(38).ping, - Borders::ALL, - Color::Magenta, - ); - draw_block_joins(f, stack[2], Borders::ALL, Borders::TOP); - draw_block_joins( - f, - stack[2], - Borders::BOTTOM.union(Borders::LEFT), - Borders::LEFT, - ); - } - }); - - *UNIQUE.write().await = false; -} - -pub fn render_graphs( - f: &mut Frame<'_>, - area: Rect, - title: String, - unit: String, - graph: Vec<(f64, f64)>, - borders: Borders, - color: Color, -) { - let min_x = graph.first().map(|(x, _)| *x).unwrap_or(0.0); - let max_x = graph.last().map(|(x, _)| *x).unwrap_or(100.0); - let min_y = graph - .iter() - .map(|(_, y)| *y) - .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 block = Block::default() - .title(format!( - "─{}:─{}{}──{}/{}─MIN/MAX", - title, - graph.last().unwrap_or(&(0.0, 0.0)).1 as i64, - unit, - min_y as i64, - max_y as i64 - )) - .borders(borders); - - let canvas = Canvas::default() - .block(block) - .x_bounds([min_x, max_x]) - .y_bounds([0.0, 100.0]) - .paint(|ctx| { - for (x, y) in &graph { - ctx.draw(&Line { - x1: *x, - y1: 0.0, - x2: *x, - y2: *y, - color, - }); - } - }); - f.render_widget(canvas, area); -} diff --git a/src/gui/ui.rs b/src/gui/ui.rs new file mode 100644 index 0000000..810d68a --- /dev/null +++ b/src/gui/ui.rs @@ -0,0 +1,197 @@ +use crossterm::{ + cursor::MoveTo, + execute, + terminal::{Clear, ClearType, EnterAlternateScreen}, +}; +use ratatui::{ + Terminal, + backend::{Backend, CrosstermBackend}, + layout::Size, +}; +use std::{ + io::{self, Error}, + sync::{Arc, Mutex}, +}; + +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; + +use crate::gui::{interaction_result::InteractionResult, screens::screens::Screen}; + +struct ResizableBackend<'a> { + backend: CrosstermBackend<&'a mut Vec>, + size: Size, +} + +impl<'a> ResizableBackend<'a> { + fn new(buf: &'a mut Vec, width: u16, height: u16) -> Self { + Self { + backend: CrosstermBackend::new(buf), + size: Size::new(width, height), + } + } +} + +impl<'a> Backend for ResizableBackend<'a> { + type Error = Error; + + fn draw<'b, I>(&mut self, content: I) -> io::Result<()> + where + I: Iterator, + { + self.backend.draw(content) + } + + fn hide_cursor(&mut self) -> io::Result<()> { + self.backend.hide_cursor() + } + + fn show_cursor(&mut self) -> io::Result<()> { + self.backend.show_cursor() + } + + #[allow(deprecated)] + fn get_cursor(&mut self) -> io::Result<(u16, u16)> { + self.backend.get_cursor() + } + + #[allow(deprecated)] + fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> { + self.backend.set_cursor(x, y) + } + + fn clear(&mut self) -> io::Result<()> { + self.backend.clear() + } + + fn clear_region(&mut self, region: ratatui::backend::ClearType) -> io::Result<()> { + self.backend.clear_region(region) + } + + fn size(&self) -> Result { + Ok(self.size) + } + + fn flush(&mut self) -> io::Result<()> { + self.backend.flush() + } + + fn get_cursor_position(&mut self) -> Result { + todo!() + } + + fn set_cursor_position>( + &mut self, + _position: P, + ) -> Result<(), Self::Error> { + todo!() + } + + fn window_size(&mut self) -> Result { + todo!() + } +} + +/// UI state and rendering + +pub struct UI { + pub cols: Arc>, + pub rows: Arc>, + screen: Arc>>>, + + cached_render: Arc>>, +} + +impl UI { + pub fn new(cols: u16, rows: u16) -> Self { + Self { + cols: Arc::new(Mutex::new(cols)), + rows: Arc::new(Mutex::new(rows)), + screen: Arc::new(Mutex::new(None)), + cached_render: Arc::new(Mutex::new(Vec::new())), + } + } + + pub fn set_screen(&self, screen: Box) { + *self.screen.lock().unwrap() = Some(screen); + } + + pub fn resize(&self, cols: u32, rows: u32) { + *self.cols.lock().unwrap() = cols as u16; + *self.rows.lock().unwrap() = rows as u16; + } + + pub async fn handle_input(&self, input: &[u8]) { + let key_event = if input.len() == 1 { + let c = input[0] as char; + if c.is_ascii() { + Some(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE)) + } else { + None + } + } else { + None + }; + + let event = match input { + b"\x1b[A" => Some(KeyEvent::new(KeyCode::Up, KeyModifiers::NONE)), + b"\x1b[B" => Some(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)), + b"\x1b[C" => Some(KeyEvent::new(KeyCode::Right, KeyModifiers::NONE)), + b"\x1b[D" => Some(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE)), + b"\r" | b"\n" => Some(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)), + b"\x7f" => Some(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE)), + _ => key_event, + }; + + if let Some(event) = event { + let result = { + let mut guard = self.screen.lock().unwrap(); + if let Some(screen) = guard.as_mut() { + screen.handle_input(event) + } else { + return; + } + }; + match result { + InteractionResult::OpenScreen { screen } => { + self.set_screen(screen); + } + InteractionResult::OpenFutureScreen { screen: fut } => { + let ui = self.clone(); + let screen = fut.await; + ui.set_screen(screen); + } + _ => {} + } + } + } + + pub fn render(&self) -> Vec { + let mut buf = Vec::new(); + + execute!( + &mut buf, + EnterAlternateScreen, + Clear(ClearType::All), + MoveTo(0, 0) + ) + .unwrap(); + + { + let backend = ResizableBackend::new( + &mut buf, + *self.cols.lock().unwrap(), + *self.rows.lock().unwrap(), + ); + + let mut terminal = Terminal::new(backend).unwrap(); + + if let Some(screen) = self.screen.lock().unwrap().as_ref() { + let _ = terminal.draw(|f| screen.render(f, f.area())); + } + } + + *self.cached_render.lock().unwrap() = buf.clone(); + + buf + } +} diff --git a/src/gui/widgets/betterblock.rs b/src/gui/widgets/betterblock.rs deleted file mode 100644 index f953394..0000000 --- a/src/gui/widgets/betterblock.rs +++ /dev/null @@ -1,68 +0,0 @@ -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 b90cfbe..0c14d7c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,12 +20,8 @@ mod util; use crate::communities::community_manager; use crate::communities::interactables::registry; use crate::gui::app_state::AppState; -use crate::gui::input_handler; -use crate::gui::log_panel; -use crate::gui::log_panel::{log_message, log_message_trans}; -use crate::gui::tui; +use crate::gui::ui; use crate::langu::language_creator; -use crate::langu::language_manager::format; use crate::omikron::omikron_connection::{OMIKRON_CONNECTION, OmikronConnection}; use crate::server::server::start; use crate::terms::consent_state; @@ -33,6 +29,7 @@ use crate::users::user_manager; use crate::util::config_util::CONFIG; use crate::util::file_util::download_and_extract_zip; use crate::util::file_util::has_dir; +use crate::util::logger; pub static APP_STATE: LazyLock>> = LazyLock::new(|| Arc::new(Mutex::new(AppState::new()))); @@ -48,6 +45,9 @@ async fn main() { *RELOAD.write().await = false; *SHUTDOWN.write().await = false; + let tui = ui::UI::new(100, 100); + tokio::spawn(async move { tui.render() }); + // EULA let (tos, pp) = consent_state::ConsentManager::check().await; if !tos { @@ -71,16 +71,14 @@ async fn main() { } // UI - log_panel::setup(); - tui::start_tui(); - input_handler::setup_input_handler(); + logger::startup(); // BASIC CONFIGURATION &CONFIG.write().await.load(); // USER MANAGEMENT if let Err(_) = user_manager::load_users().await { - log_message_trans("user_load_failed"); + log_t!("user_load_failed"); } let mut sb = "".to_string(); @@ -94,11 +92,11 @@ async fn main() { sb.remove(0); sb = sb + ","; } - log_message(format!( + log!( "IOTA ID: {}", CONFIG.read().await.get_iota_id().to_string() - )); - log_message(format!("User IDS: {}", sb)); + ); + log!("User IDS: {}", sb); // COMMUNITY MANAGEMENT registry::load_interactables().await; @@ -113,7 +111,7 @@ async fn main() { sb1.remove(0); sb1 = sb1 + ","; } - log_message(format!("Community IDS: {}", sb1)); + log!("Community IDS: {}", sb1); let port = CONFIG.read().await.get_port(); let mut ip = "0.0.0.0".to_string(); for iface in pnet::datalink::interfaces() { @@ -127,12 +125,12 @@ async fn main() { } } if start(port).await { - log_message(format("community_active", &[&ip, &port.to_string()])); + log_t!("community_active", ip, port.to_string()); } else { if port < 1024 { - log_message(format("community_start_error_admin", &[&port.to_string()])); + log_t!("community_start_error_admin", port.to_string()); } else { - log_message(format("community_start_error", &[&port.to_string()])); + log_t!("community_start_error", port.to_string()); } } if !has_dir("web") { @@ -150,7 +148,7 @@ async fn main() { omikron.connect().await; let mut omikron_connection = OMIKRON_CONNECTION.write().await; *omikron_connection = Some(omikron.clone()); - log_message_trans("setup_completed"); + log_t!("setup_completed"); loop { if *SHUTDOWN.read().await { break; diff --git a/src/omikron/omikron_connection.rs b/src/omikron/omikron_connection.rs index 586a863..8d90920 100644 --- a/src/omikron/omikron_connection.rs +++ b/src/omikron/omikron_connection.rs @@ -1,15 +1,14 @@ -use crate::gui::log_panel::{log_cv, log_message_format}; use crate::users::contact::Contact; use crate::users::user_community_util::UserCommunityUtil; use crate::util::chat_files::{MessageState, change_message_state}; use crate::util::chats_util::{get_user, mod_user}; use crate::util::crypto_util::{DataFormat, SecurePayload}; use crate::util::file_util::{get_children, load_file, save_file}; +use crate::util::logger::PrintType; use crate::util::{chat_files, chats_util}; -use crate::{ACTIVE_TASKS, SHUTDOWN}; +use crate::{ACTIVE_TASKS, SHUTDOWN, log, log_cv, log_t}; use crate::{ data::communication::{CommunicationType, CommunicationValue, DataTypes}, - gui::log_panel::{log_message, log_message_trans}, util::{config_util::CONFIG, crypto_helper}, }; use dashmap::DashMap; @@ -26,6 +25,7 @@ use tokio::time::{Duration, Instant, sleep}; use tokio_tungstenite::{connect_async, tungstenite::protocol::Message}; use tungstenite::Utf8Bytes; use uuid::Uuid; +use warp::filters::log::log; pub static OMIKRON_CONNECTION: LazyLock>>>> = LazyLock::new(|| Arc::new(RwLock::new(None))); @@ -66,7 +66,7 @@ impl OmikronConnection { drop(conf); if iota_id == 0 || public_key.is_none() || private_key.is_none() { - log_message_trans("iota_register_new"); + log_t!("iota_register_new"); let key_pair = crypto_helper::generate_keypair(); let public_key_base64 = crypto_helper::public_key_to_base64(&key_pair.public); let private_key_base64 = crypto_helper::secret_key_to_base64(&key_pair.secret); @@ -97,10 +97,10 @@ impl OmikronConnection { conf_write.change("iota_id", iota_json.clone()); conf_write.update(); drop(conf_write); - log_message(format!("Registered with Iota-ID: {}", iota_id)); + log!("Registered with Iota-ID: {}", iota_id); } Err(timeout) => { - log_message(timeout); + log!("{}", timeout); } } } @@ -121,7 +121,7 @@ impl OmikronConnection { if self.is_connected().await { return true; } - log_message_trans("omikron_connecting"); + log_t!("omikron_connecting"); let conf = CONFIG.read().await; let addr = conf @@ -130,11 +130,11 @@ impl OmikronConnection { .unwrap_or("wss://app.tensamin.net/ws/iota/"); let stream_res = connect_async(addr).await; if let Err(e) = stream_res { - log_message(format!("con error {}", e.to_string())); + log!("con error {}", e.to_string()); return false; } let (stream, _) = stream_res.unwrap(); - log_message_trans("omikron_connection_success"); + log_t!("omikron_connection_success"); let (write_half, read_half) = stream.split(); @@ -196,7 +196,7 @@ impl OmikronConnection { ); } *is_connected_out.lock().await = false; - log_message("Connection closed."); + log!("Connection closed."); }); { ACTIVE_TASKS @@ -219,7 +219,7 @@ impl OmikronConnection { tokio::spawn(async move { match msg { Ok(Message::Close(Some(frame))) => { - log_message(format!("[Omikron] Closed: {:?}", frame)); + log!("[Omikron] Closed: {:?}", frame); *is_connected.lock().await = false; return; } @@ -271,7 +271,7 @@ impl OmikronConnection { self.send_message(&response).await; } else { - log_message("Failed to decrypt challenge"); + log!("Failed to decrypt challenge"); } return; @@ -286,9 +286,8 @@ impl OmikronConnection { let mut conf = CONFIG.write().await; conf.change("iota_id", JsonValue::Number(iota_id.into())); conf.update(); - log_message(format!("Iota registered with ID: {}", iota_id)); + log!("Iota registered with ID: {}", iota_id); - // Now, proceed to login let login_message = CommunicationValue::new(CommunicationType::identification) .add_data( @@ -301,13 +300,13 @@ impl OmikronConnection { self_clone.send_message(&login_message).await; }); } else { - log_message("Iota registration failed."); + log("Iota registration failed."); } return; } if cv.is_type(CommunicationType::identification_response) { if let Some(accepted) = cv.get_data(DataTypes::accepted) { - log_message(format!("Omikron connected: {}", accepted.to_string())); + log!("Omikron connected: {}", accepted.to_string()); } return; } @@ -315,7 +314,7 @@ impl OmikronConnection { // ************************************************ // // Direct messages // // ************************************************ // - log_cv(&cv); + log_cv!(PrintType::General, &cv); if let Some((_, y)) = waiting.remove(&cv.get_id()) { y(cv); return; @@ -699,7 +698,7 @@ impl OmikronConnection { } } Err(e) => { - log_message(format!("[Omikron] Error: {}", e)); + log!("Omikron] Error: {}", e); *is_connected.lock().await = false; return; } @@ -721,17 +720,17 @@ impl OmikronConnection { Ok(_) => match writer.flush().await { Ok(_) => return, Err(e) => { - log_message_format("send_message_failed", &[&e.to_string()]); + log_t!("send_message_failed", e.to_string()); *connected.lock().await = false; } }, Err(e) => { - log_message_format("send_message_failed", &[&e.to_string()]); + log_t!("send_message_failed", e.to_string()); *connected.lock().await = false; } } } else { - log_message_format("send_message_failed", &["Immutable Writer"]); + log_t!("send_message_failed", "Immutable Writer".to_string()); *connected.lock().await = false; } } @@ -750,7 +749,7 @@ impl OmikronConnection { let inner_tx = task_tx.clone(); tokio::spawn(async move { if let Err(e) = inner_tx.send(response_cv).await { - log_message(format!("Failed to send response back to awaiter: {}", &e)); + log_t!("Failed to send response back to awaiter: {}", e.to_string()); } }); }), diff --git a/src/server/api.rs b/src/server/api.rs index 060de02..f9a87a4 100755 --- a/src/server/api.rs +++ b/src/server/api.rs @@ -133,7 +133,7 @@ async fn users_add( let username = match payload.get("username").and_then(|v| v.as_str()) { Some(u) => u, - None => return error(), + _ => return error(), }; if let (Some(user), Some(_)) = crate::users::user_manager::create_user(username).await { diff --git a/src/server/server.rs b/src/server/server.rs index 51c15fc..57b7462 100644 --- a/src/server/server.rs +++ b/src/server/server.rs @@ -1,13 +1,11 @@ -use crate::gui::log_panel::log_message; +use crate::log; use crate::server::api::api_config; use crate::server::socket::WsSession; use crate::server::web_path_parser; use crate::util::file_util::load_file_buf; use crate::{ACTIVE_TASKS, SHUTDOWN}; - use actix_web::{App, Error, HttpRequest, HttpServer, Responder, dev::ServerHandle, web}; use actix_web_actors::ws; - use rustls::ServerConfig; use rustls::pki_types::{CertificateDer, PrivateKeyDer}; use std::{ @@ -20,7 +18,7 @@ use std::{ async fn ws_handler(req: HttpRequest, stream: web::Payload) -> Result { let path = req.path().to_string(); - log_message(format!("WS connection from {:?}", req.peer_addr())); + log!("WS connection from {:?}", req.peer_addr()); let session = WsSession::new(path); ws::start(session, &req, stream) } @@ -33,7 +31,7 @@ pub async fn start(port: u16) -> bool { let _ = tokio::spawn(async move { let server = match load_tls_config() { Ok(Some(tls_config)) => { - log_message(format!("HTTPS (HTTP/2) Server running on 0.0.0.0:{}", port)); + log!("HTTPS (HTTP/2) Server running on 0.0.0.0:{}", port); let _config = (*tls_config).clone(); HttpServer::new(move || { App::new() @@ -46,8 +44,8 @@ pub async fn start(port: u16) -> bool { .unwrap() .run() } - Ok(None) => { - log_message(format!("HTTP Server running on 0.0.0.0:{}", port)); + Ok(_) => { + log!("HTTP Server running on 0.0.0.0:{}", port); HttpServer::new(move || { App::new() .app_data(web::Data::new(false)) @@ -60,7 +58,7 @@ pub async fn start(port: u16) -> bool { .run() } Err(e) => { - log_message(format!("TLS config error: {}", e)); + log!("TLS config error: {}", e); return; } }; @@ -71,7 +69,7 @@ pub async fn start(port: u16) -> bool { ACTIVE_TASKS.lock().unwrap().push("WebServer".into()); server.await.unwrap(); ACTIVE_TASKS.lock().unwrap().retain(|t| t != "WebServer"); - log_message("Web Server shutdown complete."); + log!("Web Server shutdown complete."); }); if let Ok(server_handle) = rx.await { @@ -87,7 +85,7 @@ pub async fn start(port: u16) -> bool { async fn wait_for_shutdown(server_handle: ServerHandle) { loop { if *SHUTDOWN.read().await { - log_message("Shutdown signal received."); + log!("Shutdown signal received."); server_handle.stop(true).await; break; } @@ -102,7 +100,7 @@ fn load_tls_config() -> Result>, Box> { let cert_file_buf = match cert_file_res { Ok(b) => b, Err(e) if e.kind() == ErrorKind::NotFound => { - log_message("TLS certificate 'certs/cert.pem' not found."); + log!("TLS certificate 'certs/cert.pem' not found."); return Ok(None); } Err(e) => return Err(e.into()), // Other IO error @@ -111,7 +109,7 @@ fn load_tls_config() -> Result>, Box> { let key_file_buf = match key_file_res { Ok(b) => b, Err(e) if e.kind() == ErrorKind::NotFound => { - log_message("TLS key 'certs/cert.key' not found."); + log!("TLS key 'certs/cert.key' not found."); return Ok(None); } Err(e) => return Err(e.into()), // Other IO error diff --git a/src/server/socket.rs b/src/server/socket.rs index 9e9df57..79a4e4d 100644 --- a/src/server/socket.rs +++ b/src/server/socket.rs @@ -1,6 +1,6 @@ use crate::{ data::communication::{CommunicationType, CommunicationValue}, - gui::log_panel::log_message, + log, }; use actix::{Actor, ActorContext, AsyncContext, StreamHandler}; use actix_web_actors::ws; @@ -57,21 +57,21 @@ impl Actor for WsSession { fn started(&mut self, ctx: &mut Self::Context) { self.start_heartbeat(ctx); - log_message(format!("WebSocket session started for path: {}", self.path)); + log!("WebSocket session started for path: {}", self.path); if self.path.starts_with("/ws/users/") { - log_message(format!("UserConnection handling is not yet implemented.",)); + log!("UserConnection handling is not yet implemented."); } else if self.path.starts_with("/ws/community/") { let community_id = self.path.split('/').nth(3).unwrap_or_default(); - log_message(format!( + log!( "CommunityConnection handling for {} is not yet implemented.", community_id - )); + ); } } fn stopped(&mut self, _ctx: &mut Self::Context) { - log_message(format!("WebSocket session stopped for path: {}", self.path)); + log!("WebSocket session stopped for path: {}", self.path); } } diff --git a/src/users/user_manager.rs b/src/users/user_manager.rs index 8242dee..e42d428 100644 --- a/src/users/user_manager.rs +++ b/src/users/user_manager.rs @@ -1,5 +1,5 @@ use crate::data::communication::{CommunicationType, CommunicationValue, DataTypes}; -use crate::gui::log_panel::log_message; +use crate::log; use crate::omikron::omikron_connection::{OMIKRON_CONNECTION, OmikronConnection}; use crate::users::user_profile::UserProfile; use crate::util::crypto_helper::{self, public_key_to_base64}; @@ -113,7 +113,7 @@ pub async fn create_user(username: &str) -> (Option, Option } *SHUTDOWN.write().await = true; *RELOAD.write().await = true; - log_message("Created User"); + log!("Created User"); save_file( "", &format!("{}.tu", username), diff --git a/src/util/chat_files.rs b/src/util/chat_files.rs index 5b0b4d8..347a518 100644 --- a/src/util/chat_files.rs +++ b/src/util/chat_files.rs @@ -3,7 +3,7 @@ use json::{self, JsonValue, array, object}; use std::fs::{self}; use std::path::Path; -use crate::gui::log_panel::log_message; +use crate::log; #[derive(PartialEq, Debug, Clone)] pub enum MessageState { @@ -58,7 +58,7 @@ pub fn add_message( ); if let Err(e) = fs::create_dir_all(&user_dir) { - log_message(format!("Failed to create chat directory: {}", e)); + log!("Failed to create chat directory: {}", e); return; } @@ -77,7 +77,7 @@ pub fn add_message( break; } } else { - log_message(format!("Failed to parse existing JSON file: {}", file_name)); + log!("Failed to parse existing JSON file: {}", file_name); } } else { break; @@ -85,7 +85,7 @@ pub fn add_message( chunk_index += 1; if chunk_index > 1000 { - log_message(format!("Too many message chunks. Aborting add.")); + log!("Too many message chunks. Aborting add."); return; } } @@ -98,7 +98,7 @@ pub fn add_message( }; if let Err(e) = message_chunk.push(json_obj) { - log_message(format!("Failed to push new message into JSON array: {}", e)); + log!("Failed to push new message into JSON array: {}", e); return; } diff --git a/src/util/file_util.rs b/src/util/file_util.rs index f5b177c..f51c863 100755 --- a/src/util/file_util.rs +++ b/src/util/file_util.rs @@ -9,7 +9,7 @@ use uuid::Uuid; use walkdir::WalkDir; use zip::ZipArchive; -use crate::gui::log_panel::log_message; +use crate::log; #[allow(dead_code)] pub fn delete_directory(path: &str) -> bool { @@ -23,11 +23,11 @@ fn delete_dir_recursive(directory: &Path) -> bool { return false; } if let Err(e) = fs::remove_dir_all(directory) { - log_message(format!( + log!( "[IMPORTANT] Couldn't delete directory {}: {}", directory.display(), e, - )); + ); return false; } true @@ -97,7 +97,7 @@ pub fn load_file(path: &str, name: &str) -> String { if !dir.exists() { if let Err(e) = fs::create_dir_all(&dir) { - log_message(format!("[IMPORTANT] Couldn't create directories: {}", e)); + log!("[IMPORTANT] Couldn't create directories: {}", e); return String::new(); } return String::new(); @@ -105,7 +105,7 @@ pub fn load_file(path: &str, name: &str) -> String { if !file_path.exists() { if let Err(e) = File::create(&file_path) { - log_message(format!("[IMPORTANT] Couldn't create file: {}", e)); + log!("[IMPORTANT] Couldn't create file: {}", e); } return String::new(); } @@ -130,17 +130,17 @@ pub fn save_file(path: &str, name: &str, value: &str) { if !dir.exists() { if let Err(e) = fs::create_dir_all(&dir) { - log_message(format!("[IMPORTANT] Couldn't create directories: {}", e)); + log!("[IMPORTANT] Couldn't create directories: {}", e); return; } } if let Err(e) = fs::write(&file_path, value) { - log_message(format!( + log!( "[IMPORTANT] Couldn't write file {}: {}", file_path.display(), e - )); + ); } } @@ -231,7 +231,7 @@ pub async fn download_zip(url: &str, zip_path: &Path) -> Result<(), Box true, Err(e) => { - log_message(format!("Error during ZIP extraction: {}", e)); + log!("Error during ZIP extraction: {}", e); false } }; if let Err(e) = tokio::fs::remove_file(&zip_path).await { - log_message(format!( - "Error cleaning up ZIP file {}: {}", - zip_path.display(), - e - )); + log!("Error cleaning up ZIP file {}: {}", zip_path.display(), e); } else if successful { - log_message("Downloaded and extracted ZIP file successfully."); + log!("Downloaded and extracted ZIP file successfully."); } } diff --git a/src/util/logger.rs b/src/util/logger.rs new file mode 100644 index 0000000..665bdf7 --- /dev/null +++ b/src/util/logger.rs @@ -0,0 +1,298 @@ +use std::{ + fs::{self, OpenOptions}, + io::Write, + path::Path, + sync::{OnceLock, mpsc}, + thread, + time::{SystemTime, UNIX_EPOCH}, +}; + +use ratatui::style::Color; + +use crate::{APP_STATE, gui::elements::log_card::UiLogEntry, langu::language_manager}; + +static LOGGER: OnceLock> = OnceLock::new(); + +#[derive(Clone, Copy)] +#[allow(unused)] +pub enum PrintType { + Call, + Client, + Iota, + Omikron, + Omega, + General, +} + +struct LogMessage { + timestamp_ms: u128, + prefix: &'static str, + kind: PrintType, + is_error: bool, + + translation_key: Option, + format_args: Vec, + + message: Option, +} + +pub fn startup() { + let (tx, rx) = mpsc::channel::(); + LOGGER.set(tx).expect("Logger already initialized"); + + thread::spawn(move || { + let log_dir = Path::new("logs"); + fs::create_dir_all(log_dir).expect("Failed to create log directory"); + + let start_ts = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + + let path = log_dir.join(format!("log_{}.txt", start_ts)); + let mut file = OpenOptions::new() + .create(true) + .append(true) + .open(path) + .expect("Failed to open log file"); + + for msg in rx { + let resolved_message = if let Some(key) = msg.translation_key { + let args: Vec<&str> = msg.format_args.iter().map(|s| s.as_str()).collect(); + language_manager::format(&key, &args) + } else { + msg.message.unwrap_or_default() + }; + + let ts = fixed_box(&msg.timestamp_ms.to_string(), 13); + + let line = format!("{} {}", msg.prefix, resolved_message); + + let _ = writeln!(file, "{} {}", ts, line); + + let color = colorize(msg.kind, msg.is_error); + + let ui_entry = UiLogEntry { + line: line.clone(), + color, + }; + + { + let mut state = APP_STATE.lock().unwrap(); + state.push_log(ui_entry); + } + } + }); +} + +fn colorize(kind: PrintType, is_error: bool) -> Color { + if is_error { + return Color::Red; + } + + match kind { + PrintType::Call => Color::Magenta, + PrintType::Client => Color::Green, + PrintType::Iota => Color::Yellow, + PrintType::Omikron => Color::Blue, + PrintType::Omega => Color::Cyan, + PrintType::General => Color::White, + } +} + +fn fixed_box(content: &str, width: usize) -> String { + let s: String = content.chars().take(width).collect(); + let len = s.chars().count(); + if len < width { + format!("[{}{}]", " ".repeat(width - len), s) + } else { + s + } +} + +pub fn log_internal_translated( + kind: PrintType, + prefix: &'static str, + is_error: bool, + key: &str, + args: Vec, +) { + if let Some(tx) = LOGGER.get() { + let _ = tx.send(LogMessage { + timestamp_ms: SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis(), + prefix, + kind, + is_error, + translation_key: Some(key.to_string()), + format_args: args, + message: None, + }); + } +} + +pub fn log_internal(kind: PrintType, prefix: &'static str, is_error: bool, message: String) { + if let Some(tx) = LOGGER.get() { + let _ = tx.send(LogMessage { + timestamp_ms: SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis(), + prefix, + kind, + is_error, + translation_key: None, + format_args: Vec::new(), + message: Some(message), + }); + } +} + +use crate::data::communication::CommunicationValue; +use json::JsonValue; +pub fn log_cv_internal(cv: &CommunicationValue, print_type: Option) { + let formatted = format_cv(cv); + + log_internal( + print_type.unwrap_or(PrintType::General), + "", + false, + formatted, + ); +} + +pub fn format_cv(cv: &CommunicationValue) -> String { + let mut parts = Vec::new(); + + let sender = cv.get_sender(); + let receiver = cv.get_receiver(); + + if sender > 0 && receiver > 0 { + parts.push(format!("{} > {}", sender, receiver)); + } else if sender > 0 { + parts.push(format!("{}", sender)); + } else if receiver > 0 { + parts.push(format!("> {}", receiver)); + } + + let comm_type = cv.get_type().to_string(); + parts.push(format!("{}", comm_type)); + + let mut data_parts = Vec::new(); + if let JsonValue::Object(data) = &cv.clone().to_json()["data"] { + for (key, value) in data.iter() { + let val_string = match value { + JsonValue::String(s) => s.clone(), + _ => value.dump(), + }; + + data_parts.push(format!("{} {}", key, val_string)); + } + } + + if !data_parts.is_empty() { + parts.push(format!("{}", data_parts.join(", "))); + } + + parts.join(": ") +} +#[macro_export] +macro_rules! log_cv { + ($kind:expr, $cv:expr) => { + $crate::util::logger::log_cv_internal(&$cv, Some($kind)) + }; + ($cv:expr) => { + $crate::util::logger::log_cv_internal(&$cv, None) + }; +} + +#[macro_export] +macro_rules! log_t { + ($key:expr) => { + $crate::util::logger::log_internal_translated( + $crate::util::logger::PrintType::General, + "", + false, + $key, + vec![] + ) + }; + + ($key:expr, $($arg:expr),+) => { + $crate::util::logger::log_internal_translated( + $crate::util::logger::PrintType::General, + "", + false, + $key, + vec![$($arg),+] + ) + }; +} +#[macro_export] +macro_rules! log_t_err { + ($key:expr) => { + $crate::util::logger::log_internal_translated( + $crate::util::logger::PrintType::General, + ">>", + true, + $key, + vec![] + ) + }; + + ($key:expr, $($arg:expr),+) => { + $crate::util::logger::log_internal_translated( + $crate::util::logger::PrintType::General, + ">>", + true, + $key, + vec![$($arg.to_string()),+] + ) + }; +} + +/// Log a general informational message. +#[macro_export] +macro_rules! log { + ($($arg:tt)*) => { + $crate::util::logger::log_internal($crate::util::logger::PrintType::General, "", false, format!($($arg)*)) + }; +} +/// Log an inbound message (`>`). +#[macro_export] +macro_rules! log_in { + ($($arg:tt)*) => { + $crate::util::logger::log_internal( + $crate::util::logger::PrintType::General, + ">", + false, + format!($($arg)*) + ) + }; +} +/// Log an outbound message (`<`). +#[macro_export] +macro_rules! log_out { + ($($arg:tt)*) => { + $crate::util::logger::log_internal( + $crate::util::logger::PrintType::General, + "<", + false, + format!($($arg)*) + ) + }; +} +/// Log an error message (`>>`). +#[macro_export] +macro_rules! log_err { + ($($arg:tt)*) => { + $crate::util::logger::log_internal( + $crate::util::logger::PrintType::General, + ">>", + true, + format!($($arg)*) + ) + }; +} diff --git a/src/util/mod.rs b/src/util/mod.rs index 0a0d8db..8a1382b 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -4,3 +4,4 @@ pub mod config_util; pub mod crypto_helper; pub mod crypto_util; pub mod file_util; +pub mod logger;