From de8b03bc846decf56629f953acbab596ec437cff Mon Sep 17 00:00:00 2001 From: Alex Emmet <111742636+Alex-Emmet@users.noreply.github.com> Date: Mon, 30 Mar 2026 21:52:45 +0200 Subject: [PATCH] [Add] Propper logging design --- src/gui/elements/console_card.rs | 25 +-- src/gui/elements/log_card.rs | 258 ++++++++++++++++--------------- src/gui/input_handler.rs | 60 ++++--- src/util/logger.rs | 111 +++++++------ 4 files changed, 229 insertions(+), 225 deletions(-) diff --git a/src/gui/elements/console_card.rs b/src/gui/elements/console_card.rs index ca697a9..b189bd4 100644 --- a/src/gui/elements/console_card.rs +++ b/src/gui/elements/console_card.rs @@ -17,7 +17,7 @@ use crate::{ ui::FPS, util::borders::draw_block_joins, }, - log, log_cv, + log, log_command, log_cv, omikron::omikron_connection::OMIKRON_CONNECTION, users::{user_manager, user_profile::UserProfile}, util::file_util, @@ -91,14 +91,6 @@ impl ConsoleCard { } } - fn get_placeholder_text(&self) -> Option { - if self.content.is_empty() { - Some(" send message (start commands with /)".to_string()) - } else { - None - } - } - fn cursor_spans(&self) -> Vec> { let cursor_visible = self.cursor_visible(); let cursor_style = Style::default().fg(Color::White).bg(Color::DarkGray); @@ -112,12 +104,12 @@ impl ConsoleCard { spans.push(Span::styled(" ", Style::default().fg(Color::White))); } spans.push(Span::styled( - "send message (start commands with /)", + "send command ( for info)", Style::default().fg(Color::DarkGray), )); } else { spans.push(Span::styled( - " send message (start commands with /)", + " send command ( for info)", Style::default().fg(Color::DarkGray), )); } @@ -177,13 +169,6 @@ impl ConsoleCard { Style::default().fg(Color::White) } - fn split_before_cursor(&self) -> (String, String) { - let byte_index = self.byte_index(); - let before = self.content[..byte_index].to_string(); - let after = self.content[byte_index..].to_string(); - (before, after) - } - fn render_cursor_spans(&self) -> Vec> { self.cursor_spans() } @@ -316,6 +301,8 @@ impl InteractableElement for ConsoleCard { let task_id = format!("command_{}_{}", command, id); ACTIVE_TASKS.insert(task_id.clone()); + log_command!("{}", command); + tokio::spawn(async move { run_command(&command).await; ACTIVE_TASKS.remove(&task_id); @@ -393,8 +380,6 @@ impl InteractableElement for ConsoleCard { } pub async fn run_command(command: &str) { - log!(":{}", command); - let parts = command.split(" ").collect::>(); match parts.as_slice() { diff --git a/src/gui/elements/log_card.rs b/src/gui/elements/log_card.rs index 51e2b35..e2d4ba1 100755 --- a/src/gui/elements/log_card.rs +++ b/src/gui/elements/log_card.rs @@ -2,6 +2,7 @@ use crate::APP_STATE; use crate::gui::elements::elements::{Element, InteractableElement, JoinableElement}; use crate::gui::interaction_result::InteractionResult; use crate::gui::util::borders::draw_block_joins; +use crate::util::logger::PrintType; use crossterm::event::{KeyCode, KeyEvent}; use ratatui::{ Frame, @@ -11,52 +12,17 @@ use ratatui::{ widgets::{Block, Borders, Paragraph}, }; use std::any::Any; - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Sender { - User, - System, -} - -impl Sender { - pub fn prefix_color(self) -> Color { - match self { - Sender::User => Color::Magenta, - Sender::System => Color::Blue, - } - } -} - -impl Sender { - pub fn color(self) -> Color { - match self { - Sender::User => Color::Magenta, - Sender::System => Color::Blue, - } - } -} +use std::time::{SystemTime, UNIX_EPOCH}; #[derive(Clone, Debug)] -pub struct LogEntry { +pub struct UiLogEntry { pub timestamp_ms: u128, - pub sender: Sender, + pub sender: PrintType, pub message: String, pub is_error: bool, } -impl LogEntry { - pub fn new(sender: Sender, message: String, is_error: bool) -> Self { - Self { - timestamp_ms: std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_millis(), - sender, - message, - is_error, - } - } - +impl UiLogEntry { pub fn format_timestamp(&self) -> String { let secs = (self.timestamp_ms / 1000) as i64; let hours = (secs / 3600) % 24; @@ -66,13 +32,37 @@ impl LogEntry { } } -#[derive(Clone)] -pub struct UiLogEntry { - pub sender: Sender, - pub message: String, +impl From for UiLogEntry { + fn from(entry: LogEntry) -> Self { + Self { + timestamp_ms: entry.timestamp_ms, + sender: entry.sender, + message: entry.message, + is_error: entry.is_error, + } + } +} + +#[derive(Clone, Debug)] +pub struct LogEntry { pub timestamp_ms: u128, + pub sender: PrintType, + pub message: String, pub is_error: bool, - pub color: Color, +} + +impl LogEntry { + pub fn new(sender: PrintType, message: String, is_error: bool) -> Self { + Self { + timestamp_ms: SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis(), + sender, + message, + is_error, + } + } } pub struct LogCard { @@ -85,15 +75,6 @@ pub struct LogCard { pub joins: Borders, } -struct RenderedLine { - prefix: String, - sender: Sender, - timestamp: String, - content: String, - is_error: bool, - is_first: bool, -} - impl LogCard { pub fn new() -> Self { Self { @@ -135,78 +116,85 @@ impl LogCard { s.len() } - fn wrap_entry(entry: &UiLogEntry, available_width: usize) -> Vec<(bool, String)> { + fn wrap_entry(entry: &UiLogEntry, available_width: usize) -> Vec<(String, Color, bool)> { let mut result = Vec::new(); - let continuation_prefix_width = 2; - let first_line_width = available_width.saturating_sub(continuation_prefix_width); - let continuation_content_width = available_width.saturating_sub(continuation_prefix_width); - let paragraphs: Vec<&str> = entry.message.split('\n').collect(); + let timestamp = entry.format_timestamp(); + let first_prefix = "┌ "; + let default_prefix = "│ "; + let last_prefix = "└ "; - for (para_idx, paragraph) in paragraphs.iter().enumerate() { - if paragraph.is_empty() { - if para_idx == 0 { - result.push((true, String::new())); - } else { - result.push((false, String::new())); - } + let prefix_width = 2; + let first_line_width = available_width.saturating_sub(prefix_width + 1); + let continuation_width = available_width.saturating_sub(prefix_width); + + let segments: Vec<&str> = entry.message.split('\n').collect(); + + let mut raw_lines = Vec::new(); + + for segment in segments { + let mut remaining = segment; + if remaining.is_empty() { + raw_lines.push(String::new()); continue; } - let mut remaining = *paragraph; - let mut is_first_line = para_idx == 0; - + let mut is_first_part = true; while !remaining.is_empty() { - let current_width = if is_first_line { + let current_width = if is_first_part { first_line_width } else { - continuation_content_width + continuation_width }; let split_point = Self::find_split_point(remaining, current_width); - let line_content = &remaining[..split_point]; - - result.push((is_first_line, line_content.to_string())); - + let line_content = remaining[..split_point].to_string(); + raw_lines.push(line_content); remaining = &remaining[split_point..]; - is_first_line = false; + is_first_part = false; } } - if result.is_empty() { - result.push((true, String::new())); + if raw_lines.is_empty() { + raw_lines.push(String::new()); + } + + for (idx, content) in raw_lines.iter().enumerate() { + let is_last = idx + 1 == raw_lines.len(); + let is_single = raw_lines.len() == 1; + let is_first = idx == 0; + let prefix = if is_first { + first_prefix + } else if is_last && !is_single { + last_prefix + } else { + default_prefix + }; + + let mut line = String::from(prefix); + line.push_str(content); + + if is_last && !timestamp.is_empty() { + line.push(' '); + line.push_str(×tamp); + } + + result.push((line, entry.sender.prefix_color(), entry.is_error)); } result } - fn build_all_lines(&self, entries: Vec, width: usize) -> Vec { + fn build_all_lines( + &self, + entries: Vec, + width: usize, + ) -> Vec<(String, Color, bool)> { let mut lines = Vec::new(); for entry in entries { let wrapped = Self::wrap_entry(&entry, width); - let timestamp = { - let secs = (entry.timestamp_ms / 1000) as i64; - let hours = (secs / 3600) % 24; - let minutes = (secs / 60) % 60; - let seconds = secs % 60; - format!("{:02}:{:02}:{:02}", hours, minutes, seconds) - }; - - for (is_first, content) in wrapped { - lines.push(RenderedLine { - prefix: "│ ".to_string(), - sender: entry.sender, - timestamp: if is_first { - timestamp.clone() - } else { - String::new() - }, - content, - is_error: entry.is_error, - is_first, - }); - } + lines.extend(wrapped); } lines @@ -270,6 +258,32 @@ impl LogCard { fn scroll_down(&mut self) { self.scroll_offset = self.scroll_offset.saturating_sub(1); } + + fn split_line_prefix(line: &str) -> (&str, &str) { + if let Some(rest) = line.strip_prefix("└ ") { + ("└ ", rest) + } else if let Some(rest) = line.strip_prefix("│ ") { + ("│ ", rest) + } else if let Some(rest) = line.strip_prefix("┌ ") { + ("┌ ", rest) + } else { + ("", line) + } + } + + fn split_timestamp_suffix(line: &str) -> (&str, &str) { + if let Some(idx) = line.rfind(' ') { + let possible_timestamp = &line[idx + 1..]; + if possible_timestamp.len() == 8 + && possible_timestamp.as_bytes()[2] == b':' + && possible_timestamp.as_bytes()[5] == b':' + { + let (content, timestamp_with_space) = line.split_at(idx); + return (content.trim_end(), timestamp_with_space); + } + } + (line, "") + } } impl Element for LogCard { @@ -308,31 +322,33 @@ impl Element for LogCard { let (start, end) = self.calculate_view_window(total_lines, visible_height); let visible_lines = &all_lines[start..end]; - let lines: Vec = visible_lines + let rendered_lines: Vec = visible_lines .iter() - .map(|rl| { + .map(|(line, prefix_color, is_error)| { let mut spans = Vec::new(); - spans.push(Span::styled( - &rl.prefix, - Style::default().fg(rl.sender.prefix_color()), - )); + let (prefix, rest) = Self::split_line_prefix(line); - if !rl.content.is_empty() { - let content_color = if rl.is_error { - Color::Red - } else { - Color::White - }; + if !prefix.is_empty() { spans.push(Span::styled( - &rl.content, - Style::default().fg(content_color), + prefix.to_string(), + Style::default().fg(*prefix_color), )); } - if rl.is_first { + let (content, timestamp) = Self::split_timestamp_suffix(rest); + let text_color = if *is_error { Color::Red } else { Color::White }; + + if !content.is_empty() { spans.push(Span::styled( - format!(" {}", rl.timestamp), + content.to_string(), + Style::default().fg(text_color), + )); + } + + if !timestamp.is_empty() { + spans.push(Span::styled( + timestamp.to_string(), Style::default().fg(Color::DarkGray), )); } @@ -341,7 +357,7 @@ impl Element for LogCard { }) .collect(); - for (idx, line) in lines.iter().enumerate() { + for (idx, line) in rendered_lines.iter().enumerate() { let line_area = Rect { x: inner_area.x, y: inner_area.y + idx as u16, @@ -365,11 +381,11 @@ impl JoinableElement for LogCard { } fn as_element(&self) -> &(dyn Element + 'static) { - self + &*self } fn as_element_mut(&mut self) -> &mut (dyn Element + 'static) { - self + &mut *self } fn set_borders(&mut self, borders: Borders) { @@ -391,11 +407,11 @@ impl InteractableElement for LogCard { } fn as_element(&self) -> &(dyn Element + 'static) { - self + &*self } fn as_element_mut(&mut self) -> &mut (dyn Element + 'static) { - self + &mut *self } fn interact(&mut self, key: KeyEvent) -> InteractionResult { diff --git a/src/gui/input_handler.rs b/src/gui/input_handler.rs index d4e0d45..b2557c5 100644 --- a/src/gui/input_handler.rs +++ b/src/gui/input_handler.rs @@ -1,59 +1,51 @@ -use crate::ACTIVE_TASKS; use crate::gui::ui::{UI, UNIQUE}; use crate::{RELOAD, SHUTDOWN}; -use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, poll, read}; +use crossterm::event::{Event, KeyEvent, KeyEventKind, KeyModifiers, poll, read}; use std::sync::Arc; use std::sync::atomic::Ordering; use std::time::Duration; pub fn setup_input_handler(ui: Arc) { tokio::spawn(async move { - ACTIVE_TASKS.insert("Input Handler".to_string()); - loop { - { - let should_shutdown = *SHUTDOWN.read().await; - if should_shutdown { - break; - } + if *SHUTDOWN.read().await { + break; } - let has_event = match poll(Duration::from_millis(100)) { - Ok(true) => true, - Ok(false) => false, - Err(_) => false, - }; + let event_result = tokio::task::spawn_blocking(|| { + if let Ok(true) = poll(Duration::from_millis(100)) { + read().ok().and_then(|ev| match ev { + Event::Key(key) if key.kind == KeyEventKind::Press => Some(key), + _ => None, + }) + } else { + None + } + }) + .await; - if has_event { - match read() { - Ok(event) => { - if let Event::Key(key_event) = event { - if key_event.kind == KeyEventKind::Press { - let uic = ui.clone(); - handle_input(key_event, uic).await; - UNIQUE.store(true, Ordering::Relaxed); - } - } - } - Err(_) => (), + match event_result { + Ok(Some(key_event)) => { + handle_input(key_event, ui.clone()).await; + UNIQUE.store(true, Ordering::Relaxed); + } + Ok(_) => {} + Err(e) => { + eprintln!("Input task error: {}", e); + tokio::time::sleep(Duration::from_millis(10)).await; } } } - { - ACTIVE_TASKS.remove("Input Handler"); - } }); } pub async fn handle_input(key: KeyEvent, ui: Arc) { match (key.code, key.modifiers) { - (KeyCode::Char('q'), KeyModifiers::CONTROL) => { + (crossterm::event::KeyCode::Char('q'), KeyModifiers::CONTROL) + | (crossterm::event::KeyCode::Char('c'), KeyModifiers::CONTROL) => { *SHUTDOWN.write().await = true; } - (KeyCode::Char('c'), KeyModifiers::CONTROL) => { - *SHUTDOWN.write().await = true; - } - (KeyCode::Char('r'), KeyModifiers::CONTROL) => { + (crossterm::event::KeyCode::Char('r'), KeyModifiers::CONTROL) => { *RELOAD.write().await = true; *SHUTDOWN.write().await = true; } diff --git a/src/util/logger.rs b/src/util/logger.rs index 5ec3bfd..a5feb02 100755 --- a/src/util/logger.rs +++ b/src/util/logger.rs @@ -13,16 +13,13 @@ use ttp_core::{CommunicationValue, DataTypes, DataValue}; use crate::{ APP_STATE, - gui::{ - elements::log_card::{LogEntry, Sender, UiLogEntry}, - ui::UNIQUE, - }, + gui::{elements::log_card::LogEntry, ui::UNIQUE}, langu::language_manager, }; static LOGGER: OnceLock> = OnceLock::new(); -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] #[allow(unused)] pub enum PrintType { Call, @@ -31,6 +28,20 @@ pub enum PrintType { Omikron, Omega, General, + Command, +} +impl PrintType { + pub fn prefix_color(self) -> Color { + match self { + PrintType::Call => Color::Magenta, + PrintType::Client => Color::Green, + PrintType::Iota => Color::Yellow, + PrintType::Omikron => Color::Blue, + PrintType::Omega => Color::Cyan, + PrintType::General => Color::LightCyan, + PrintType::Command => Color::LightGreen, + } + } } struct LogMessage { @@ -38,10 +49,8 @@ struct LogMessage { prefix: String, kind: PrintType, is_error: bool, - translation_key: Option, format_args: Vec, - message: Option, } @@ -73,18 +82,8 @@ pub fn startup() { msg.message.unwrap_or_default() }; - let ts = fixed_box(&msg.timestamp_ms.to_string(), 13); - - let sender = match msg.kind { - PrintType::Client => Sender::User, - PrintType::Call - | PrintType::Iota - | PrintType::Omikron - | PrintType::Omega - | PrintType::General => Sender::System, - }; - - let entry = LogEntry::new(sender, resolved_message, msg.is_error); + let timestamp = format_timestamp_inline(msg.timestamp_ms); + let entry = LogEntry::new(msg.kind, resolved_message, msg.is_error); let prefix = if msg.prefix.is_empty() { String::new() @@ -92,30 +91,18 @@ pub fn startup() { format!("{} ", msg.prefix) }; - let line = format!("{}| {}", prefix, entry.message,); - let _ = writeln!( file, - "{} {} {}", - ts, - line, - format_timestamp_inline(entry.timestamp_ms) + "{} {}{}", + fixed_box(&msg.timestamp_ms.to_string(), 13), + prefix, + entry.message ); - let color = colorize(msg.kind, msg.is_error); + let _ = writeln!(file, " {}", timestamp); - let ui_entry = UiLogEntry { - sender, - message: entry.message.clone(), - timestamp_ms: entry.timestamp_ms, - is_error: msg.is_error, - color, - }; - - { - let mut state = APP_STATE.lock().unwrap(); - state.push_log(ui_entry); - } + let mut state = APP_STATE.lock().unwrap(); + state.push_log(entry.into()); } }); } @@ -128,6 +115,16 @@ fn format_timestamp_inline(timestamp_ms: u128) -> String { format!("[{:02}:{:02}:{:02}]", hours, minutes, seconds) } +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 + } +} + fn colorize(kind: PrintType, is_error: bool) -> Color { if is_error { return Color::Red; @@ -139,17 +136,8 @@ fn colorize(kind: PrintType, is_error: bool) -> Color { 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 + PrintType::General => Color::LightCyan, + PrintType::Command => Color::LightGreen, } } @@ -217,6 +205,7 @@ macro_rules! log_t { ) }; } + #[macro_export] macro_rules! log_t_err { ($key:expr) => { @@ -240,13 +229,32 @@ macro_rules! log_t_err { }; } +/// Log a command message. +#[macro_export] +macro_rules! log_command { + ($($arg:tt)*) => { + $crate::util::logger::log_internal( + $crate::util::logger::PrintType::Command, + "".to_string(), + false, + format!($($arg)*) + ) + }; +} + /// Log a general informational message. #[macro_export] macro_rules! log { ($($arg:tt)*) => { - $crate::util::logger::log_internal($crate::util::logger::PrintType::General, "".to_string(), false, format!($($arg)*)) + $crate::util::logger::log_internal( + $crate::util::logger::PrintType::General, + "".to_string(), + false, + format!($($arg)*) + ) }; } + /// Log an inbound message (`>`). #[macro_export] macro_rules! log_in { @@ -259,6 +267,7 @@ macro_rules! log_in { ) }; } + /// Log an outbound message (`<`). #[macro_export] macro_rules! log_out { @@ -271,6 +280,7 @@ macro_rules! log_out { ) }; } + /// Log an error message (`>>`). #[macro_export] macro_rules! log_err { @@ -410,6 +420,7 @@ macro_rules! log_cv_in { $crate::util::logger::log_cv_internal("> ", &$cv, None) }; } + #[macro_export] macro_rules! log_cv_out { ($kind:expr, $cv:expr) => {