use std::{ fs::{self, OpenOptions}, io::Write, path::Path, sync::{OnceLock, atomic::Ordering, mpsc}, thread, time::{SystemTime, UNIX_EPOCH}, }; use mtp::codec::{CommunicationValue, DataTypeId, DataValue, Version}; use ratatui::style::Color; use iota_state::{UNIQUE, UiLogEntry}; use tokio::sync::broadcast; pub mod language_creator; pub mod language_manager; static LOGGER: OnceLock> = OnceLock::new(); static LOG_BROADCASTER: OnceLock> = OnceLock::new(); #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] #[allow(unused)] pub enum PrintType { Call, Client, Iota, 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 { timestamp_ms: u128, prefix: String, kind: PrintType, is_error: bool, translation_key: Option, format_args: Vec, message: Option, } /* The logger owns file persistence while consumers receive rendered entries * through a process-local broadcast subscription. */ pub fn startup() { let (tx, rx) = mpsc::channel::(); if LOGGER.set(tx).is_err() { return; } let (broadcast_tx, _) = broadcast::channel(512); let _ = LOG_BROADCASTER.set(broadcast_tx.clone()); thread::spawn(move || { let working_dir = iota_util::file_util::get_directory(); let base_dir = Path::new(&working_dir); let log_dir = base_dir.join("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 timestamp = format_timestamp_inline(msg.timestamp_ms); let prefix = if msg.prefix.is_empty() { String::new() } else { format!("{} ", msg.prefix) }; let _ = writeln!( file, "{} {}{}", fixed_box(&msg.timestamp_ms.to_string(), 13), prefix, resolved_message ); let _ = writeln!(file, " {}", timestamp); let entry = UiLogEntry { timestamp_ms: msg.timestamp_ms, sender: format!("{:?}", msg.kind), message: resolved_message, is_error: msg.is_error, }; let _ = broadcast_tx.send(entry); } }); } pub fn subscribe() -> Option> { LOG_BROADCASTER.get().map(broadcast::Sender::subscribe) } fn format_timestamp_inline(timestamp_ms: u128) -> String { let secs = (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) } 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: String, is_error: bool, key: &str, args: Vec, ) { if let Some(tx) = LOGGER.get() { UNIQUE.store(true, Ordering::Relaxed); 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: String, is_error: bool, message: String) { if let Some(tx) = LOGGER.get() { UNIQUE.store(true, Ordering::Relaxed); 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), }); } } #[macro_export] macro_rules! log_t { ($key:expr) => { $crate::log_internal_translated( $crate::PrintType::General, "".to_string(), false, $key, vec![] ) }; ($key:expr, $($arg:expr),+) => { $crate::log_internal_translated( $crate::PrintType::General, "".to_string(), false, $key, vec![$($arg),+] ) }; } #[macro_export] macro_rules! log_t_err { ($key:expr) => { $crate::log_internal_translated( $crate::PrintType::General, "".to_string(), true, $key, vec![] ) }; ($key:expr, $($arg:expr),+) => { $crate::log_internal_translated( $crate::PrintType::General, "".to_string(), true, $key, vec![$($arg.to_string()),+] ) }; } /// Log a command message. #[macro_export] macro_rules! log_command { ($($arg:tt)*) => { $crate::log_internal( $crate::PrintType::Command, "".to_string(), false, format!($($arg)*) ) }; } /// Log a general informational message. #[macro_export] macro_rules! log { ($($arg:tt)*) => { $crate::log_internal( $crate::PrintType::General, "".to_string(), false, format!($($arg)*) ) }; } /// Log an inbound message (`>`). #[macro_export] macro_rules! log_in { ($($arg:tt)*) => { $crate::log_internal( $crate::PrintType::General, ">".to_string(), false, format!($($arg)*) ) }; } /// Log an outbound message (`<`). #[macro_export] macro_rules! log_out { ($($arg:tt)*) => { $crate::log_internal( $crate::PrintType::General, "<".to_string(), false, format!($($arg)*) ) }; } /// Log an error message (`>>`). #[macro_export] macro_rules! log_err { ($($arg:tt)*) => { $crate::log_internal( $crate::PrintType::General, ">>".to_string(), true, format!($($arg)*) ) }; } // ******** COMMUNICATION VALUES ******** pub fn log_cv_internal( prefix: &'static str, cv: &CommunicationValue, print_type: Option, ) { let formatted = format_cv(cv); log_internal( print_type.unwrap_or(PrintType::General), prefix.to_string(), 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 data = cv.data(); let formated_data = format_data_container( data.iter().map(|(k, v)| (*k, v.clone())).collect(), Version(1, 0), ); parts.push(format!("{}", formated_data)); parts.join(": ") } fn format_data_container(data: Vec<(DataTypeId, DataValue)>, version: Version) -> String { let parts: Vec = data .into_iter() .map(|(key, value)| { let key_str = key.to_string(); match value { DataValue::Str(s) => format!("{}=\"{}\"", key_str, s), DataValue::Container(inner) => { let inner_formatted = format_data_container(inner, version.clone()); format!("{}={{ {} }}", key_str, inner_formatted) } DataValue::Array(arr) => { let arr_formatted = format_array(arr, version.clone()); format!("{}=[{}]", key_str, arr_formatted) } DataValue::Bool(b) => format!("{}={}", key_str, b), DataValue::BoolTrue => format!("{}=true", key_str), DataValue::BoolFalse => format!("{}=false", key_str), DataValue::SignedNumber(num) => format!("{}={}", key_str, num), _ => "".to_string(), } }) .collect(); parts.join(", ") } fn format_array(arr: Vec, version: Version) -> String { let parts: Vec = arr .into_iter() .map(|value| match value { DataValue::Str(s) => format!("\"{}\"", s), DataValue::Container(inner) => { let inner_formatted = format_data_container(inner, version.clone()); format!("{{ {} }}", inner_formatted) } DataValue::Array(inner_arr) => { let formatted = format_array(inner_arr, version.clone()); format!("[{}]", formatted) } DataValue::Bool(b) => b.to_string(), DataValue::BoolTrue => "true".to_string(), DataValue::BoolFalse => "false".to_string(), DataValue::SignedNumber(num) => num.to_string(), _ => String::new(), }) .collect(); parts.join(", ") } #[macro_export] macro_rules! log_cv { ($kind:expr, $cv:expr) => { $crate::log_cv_internal("", &$cv, Some($kind)) }; ($cv:expr) => { $crate::log_cv_internal("", &$cv, None) }; } #[macro_export] macro_rules! log_cv_in { ($kind:expr, $cv:expr) => { $crate::log_cv_internal("> ", &$cv, Some($kind)) }; ($cv:expr) => { $crate::log_cv_internal("> ", &$cv, None) }; } #[macro_export] macro_rules! log_cv_out { ($kind:expr, $cv:expr) => { $crate::log_cv_internal("< ", &$cv, Some($kind)) }; ($cv:expr) => { $crate::log_cv_internal("< ", &$cv, None) }; }