Skip to main content

hojicha_core/
logging.rs

1//! Logging utilities for debugging TUI applications
2//!
3//! This module provides file-based logging that doesn't interfere with the terminal UI.
4//! It's essential for debugging TUI applications where stderr output would corrupt the display.
5//!
6//! # Example
7//!
8//! ```ignore
9//! use hojicha::logging;
10//!
11//! // Initialize file logger
12//! logging::init_file_logger("/tmp/app.log").unwrap();
13//!
14//! // Log messages at different levels
15//! logging::debug("Debug information");
16//! logging::info("Application started");
17//! logging::warn("Low memory");
18//! logging::error("Connection failed");
19//! ```
20
21use std::fs::OpenOptions;
22use std::io::{self, Write};
23use std::sync::{Arc, Mutex, Once};
24
25static INIT: Once = Once::new();
26static mut LOGGER: Option<Arc<Mutex<Logger>>> = None;
27
28/// Log levels for filtering messages
29#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
30pub enum LogLevel {
31    /// Debug level for verbose debugging information
32    Debug = 0,
33    /// Info level for general informational messages
34    Info = 1,
35    /// Warning level for potentially problematic situations
36    Warn = 2,
37    /// Error level for error conditions
38    Error = 3,
39}
40
41impl LogLevel {
42    fn as_str(&self) -> &'static str {
43        match self {
44            LogLevel::Debug => "DEBUG",
45            LogLevel::Info => "INFO",
46            LogLevel::Warn => "WARN",
47            LogLevel::Error => "ERROR",
48        }
49    }
50}
51
52struct Logger {
53    writer: Box<dyn Write + Send>,
54    level: LogLevel,
55}
56
57impl Logger {
58    fn log(&mut self, level: LogLevel, message: &str) -> io::Result<()> {
59        if level >= self.level {
60            let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
61            writeln!(
62                self.writer,
63                "[{}] {}: {}",
64                timestamp,
65                level.as_str(),
66                message
67            )?;
68            self.writer.flush()?;
69        }
70        Ok(())
71    }
72}
73
74/// Initialize the file logger with a given path
75///
76/// This creates or appends to the specified log file.
77/// Only the first call to any init function will take effect.
78pub fn init_file_logger(path: &str) -> io::Result<()> {
79    let file = OpenOptions::new().create(true).append(true).open(path)?;
80
81    init_with_writer(Box::new(file))
82}
83
84/// Initialize the logger with a custom writer
85///
86/// This allows logging to any destination that implements Write.
87/// Only the first call to any init function will take effect.
88pub fn init_with_writer(writer: Box<dyn Write + Send>) -> io::Result<()> {
89    init_with_writer_and_level(writer, LogLevel::Debug)
90}
91
92/// Initialize the logger with a custom writer and minimum log level
93///
94/// Messages below the specified level will be filtered out.
95/// Only the first call to any init function will take effect.
96pub fn init_with_writer_and_level(
97    writer: Box<dyn Write + Send>,
98    level: LogLevel,
99) -> io::Result<()> {
100    let result = Ok(());
101
102    INIT.call_once(|| {
103        let logger = Arc::new(Mutex::new(Logger { writer, level }));
104        unsafe {
105            LOGGER = Some(logger);
106        }
107    });
108
109    result
110}
111
112/// Log a debug message
113pub fn debug(message: &str) {
114    log(LogLevel::Debug, message);
115}
116
117/// Log an info message
118pub fn info(message: &str) {
119    log(LogLevel::Info, message);
120}
121
122/// Log a warning message
123pub fn warn(message: &str) {
124    log(LogLevel::Warn, message);
125}
126
127/// Log an error message
128pub fn error(message: &str) {
129    log(LogLevel::Error, message);
130}
131
132/// Internal logging function
133fn log(level: LogLevel, message: &str) {
134    unsafe {
135        if let Some(ref logger) = LOGGER {
136            if let Ok(mut logger) = logger.lock() {
137                let _ = logger.log(level, message);
138            }
139        }
140    }
141}
142
143/// Log command for debug messages
144///
145/// Returns a command that logs a debug message when executed.
146pub fn log_debug<M: crate::core::Message>(message: impl Into<String>) -> crate::core::Cmd<M> {
147    let msg = message.into();
148    crate::commands::custom(move || {
149        debug(&msg);
150        None
151    })
152}
153
154/// Log command for info messages
155///
156/// Returns a command that logs an info message when executed.
157pub fn log_info<M: crate::core::Message>(message: impl Into<String>) -> crate::core::Cmd<M> {
158    let msg = message.into();
159    crate::commands::custom(move || {
160        info(&msg);
161        None
162    })
163}
164
165/// Log command for warning messages
166///
167/// Returns a command that logs a warning message when executed.
168pub fn log_warn<M: crate::core::Message>(message: impl Into<String>) -> crate::core::Cmd<M> {
169    let msg = message.into();
170    crate::commands::custom(move || {
171        warn(&msg);
172        None
173    })
174}
175
176/// Log command for error messages
177///
178/// Returns a command that logs an error message when executed.
179pub fn log_error<M: crate::core::Message>(message: impl Into<String>) -> crate::core::Cmd<M> {
180    let msg = message.into();
181    crate::commands::custom(move || {
182        error(&msg);
183        None
184    })
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190    use std::sync::{Arc, Mutex};
191
192    struct TestWriter {
193        buffer: Arc<Mutex<Vec<u8>>>,
194    }
195
196    impl Write for TestWriter {
197        fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
198            self.buffer.lock().unwrap().extend_from_slice(buf);
199            Ok(buf.len())
200        }
201
202        fn flush(&mut self) -> io::Result<()> {
203            Ok(())
204        }
205    }
206
207    #[test]
208    fn test_logging_levels() {
209        let buffer = Arc::new(Mutex::new(Vec::new()));
210        let writer = TestWriter {
211            buffer: buffer.clone(),
212        };
213
214        // Note: We can't test this properly due to the Once guard
215        // In real tests, we'd need to reset the global state between tests
216        // For now, this is more of a compilation test
217        let _ = init_with_writer_and_level(Box::new(writer), LogLevel::Info);
218
219        debug("Should not appear");
220        info("Should appear");
221        warn("Should also appear");
222
223        // In a real test, we'd check the buffer contents
224        // let contents = String::from_utf8(buffer.lock().unwrap().clone()).unwrap();
225        // assert!(!contents.contains("Should not appear"));
226        // assert!(contents.contains("Should appear"));
227    }
228}