fast_rich/
log.rs

1//! Logging utilities similar to Rich's console.log().
2//!
3//! Provides timestamped logging with file/line information and pretty-printing.
4
5use crate::console::{Console, RenderContext};
6use crate::renderable::{Renderable, Segment};
7use crate::style::{Color, Style};
8use crate::text::Span;
9use std::time::SystemTime;
10
11/// A log message with metadata.
12#[derive(Debug)]
13pub struct LogMessage {
14    /// The message content
15    pub message: String,
16    /// File where the log was called
17    pub file: Option<&'static str>,
18    /// Line number
19    pub line: Option<u32>,
20    /// Timestamp
21    pub time: SystemTime,
22    /// Log level
23    pub level: LogLevel,
24    /// Whether to show the timestamp
25    pub show_time: bool,
26}
27
28/// Log level for messages.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
30pub enum LogLevel {
31    /// Debug level
32    Debug,
33    /// Info level (default)
34    #[default]
35    Info,
36    /// Warning level
37    Warning,
38    /// Error level
39    Error,
40}
41
42impl LogLevel {
43    /// Get the style for this log level.
44    pub fn style(&self) -> Style {
45        match self {
46            LogLevel::Debug => Style::new().foreground(Color::Magenta),
47            LogLevel::Info => Style::new().foreground(Color::Blue),
48            LogLevel::Warning => Style::new().foreground(Color::Yellow),
49            LogLevel::Error => Style::new().foreground(Color::Red).bold(),
50        }
51    }
52
53    /// Get the label for this log level.
54    pub fn label(&self) -> &'static str {
55        match self {
56            LogLevel::Debug => "DEBUG",
57            LogLevel::Info => "INFO",
58            LogLevel::Warning => "WARN",
59            LogLevel::Error => "ERROR",
60        }
61    }
62}
63
64impl LogMessage {
65    /// Create a new log message.
66    pub fn new(message: &str) -> Self {
67        LogMessage {
68            message: message.to_string(),
69            file: None,
70            line: None,
71            time: SystemTime::now(),
72            level: LogLevel::Info,
73            show_time: true,
74        }
75    }
76
77    /// Set the file and line.
78    pub fn location(mut self, file: &'static str, line: u32) -> Self {
79        self.file = Some(file);
80        self.line = Some(line);
81        self
82    }
83
84    /// Set the log level.
85    pub fn level(mut self, level: LogLevel) -> Self {
86        self.level = level;
87        self
88    }
89
90    /// Set whether to show the timestamp.
91    pub fn show_time(mut self, show: bool) -> Self {
92        self.show_time = show;
93        self
94    }
95
96    /// Format the timestamp.
97    fn format_time(&self) -> String {
98        use std::time::UNIX_EPOCH;
99
100        let duration = self.time.duration_since(UNIX_EPOCH).unwrap_or_default();
101        let secs = duration.as_secs();
102
103        let hours = (secs / 3600) % 24;
104        let minutes = (secs / 60) % 60;
105        let seconds = secs % 60;
106        let millis = duration.subsec_millis();
107
108        format!("{:02}:{:02}:{:02}.{:03}", hours, minutes, seconds, millis)
109    }
110
111    /// Format the location.
112    fn format_location(&self) -> Option<String> {
113        match (self.file, self.line) {
114            (Some(file), Some(line)) => {
115                // Get just the filename
116                let filename = file.rsplit('/').next().unwrap_or(file);
117                Some(format!("{}:{}", filename, line))
118            }
119            _ => None,
120        }
121    }
122}
123
124impl Renderable for LogMessage {
125    fn render(&self, _context: &RenderContext) -> Vec<Segment> {
126        let mut spans = Vec::new();
127
128        // Timestamp
129        if self.show_time {
130            spans.push(Span::styled(
131                format!("[{}]", self.format_time()),
132                Style::new().dim(),
133            ));
134            spans.push(Span::raw(" "));
135        }
136
137        // Level
138        spans.push(Span::styled(
139            format!("{:5}", self.level.label()),
140            self.level.style(),
141        ));
142
143        spans.push(Span::raw(" "));
144
145        // Message
146        spans.push(Span::raw(self.message.clone()));
147
148        // Location
149        if let Some(location) = self.format_location() {
150            spans.push(Span::raw(" "));
151            spans.push(Span::styled(
152                location,
153                Style::new().foreground(Color::Cyan).dim(),
154            ));
155        }
156
157        vec![Segment::line(spans)]
158    }
159}
160
161/// Extension trait for Console to add logging methods.
162pub trait ConsoleLog {
163    /// Log a message with timestamp and location.
164    fn log(&self, message: &str);
165
166    /// Log a debug message.
167    fn debug(&self, message: &str);
168
169    /// Log a warning message.
170    fn warn(&self, message: &str);
171
172    /// Log an error message.
173    fn error(&self, message: &str);
174}
175
176impl ConsoleLog for Console {
177    fn log(&self, message: &str) {
178        let log_msg = LogMessage::new(message);
179        self.print_renderable(&log_msg);
180    }
181
182    fn debug(&self, message: &str) {
183        let log_msg = LogMessage::new(message).level(LogLevel::Debug);
184        self.print_renderable(&log_msg);
185    }
186
187    fn warn(&self, message: &str) {
188        let log_msg = LogMessage::new(message).level(LogLevel::Warning);
189        self.print_renderable(&log_msg);
190    }
191
192    fn error(&self, message: &str) {
193        let log_msg = LogMessage::new(message).level(LogLevel::Error);
194        self.print_renderable(&log_msg);
195    }
196}
197
198/// Macro for logging with file/line information.
199#[macro_export]
200macro_rules! log {
201    ($console:expr, $($arg:tt)*) => {{
202        let message = format!($($arg)*);
203        let log_msg = $crate::log::LogMessage::new(&message)
204            .location(file!(), line!());
205        $console.print_renderable(&log_msg);
206    }};
207}
208
209#[cfg(feature = "logging")]
210mod log_integration {
211    //! Integration with the `log` crate.
212    use super::*;
213    use log::{Level, Log, Metadata, Record, SetLoggerError};
214    use std::sync::OnceLock;
215
216    static CONSOLE: OnceLock<Console> = OnceLock::new();
217
218    /// Configuration for the RichLogger.
219    #[derive(Clone, Debug)]
220    pub struct RichLoggerConfig {
221        /// Whether to show timestamps.
222        pub enable_time: bool,
223        /// Whether to show the file path/location.
224        pub enable_path: bool,
225    }
226
227    impl Default for RichLoggerConfig {
228        fn default() -> Self {
229            Self {
230                enable_time: true,
231                enable_path: true,
232            }
233        }
234    }
235
236    /// A log handler that outputs to a rich Console.
237    pub struct RichLogger {
238        config: RichLoggerConfig,
239    }
240
241    impl RichLogger {
242        /// Create a new builder for RichLogger.
243        pub fn builder() -> RichLoggerBuilder {
244            RichLoggerBuilder::default()
245        }
246
247        /// Initialize the logger with default settings.
248        pub fn init() -> Result<(), SetLoggerError> {
249            Self::builder().init()
250        }
251    }
252
253    /// Builder for RichLogger.
254    #[derive(Default)]
255    pub struct RichLoggerBuilder {
256        config: RichLoggerConfig,
257        level: Option<log::LevelFilter>,
258    }
259
260    impl RichLoggerBuilder {
261        /// Enable or disable timestamps.
262        pub fn enable_time(mut self, enable: bool) -> Self {
263            self.config.enable_time = enable;
264            self
265        }
266
267        /// Enable or disable file paths.
268        pub fn enable_path(mut self, enable: bool) -> Self {
269            self.config.enable_path = enable;
270            self
271        }
272
273        /// Set the max log level.
274        pub fn filter_level(mut self, level: log::LevelFilter) -> Self {
275            self.level = Some(level);
276            self
277        }
278
279        /// Initialize the logger.
280        pub fn init(self) -> Result<(), SetLoggerError> {
281            // Initialize global console if not already
282            CONSOLE.get_or_init(Console::new);
283
284            let logger = Box::new(RichLogger {
285                config: self.config,
286            });
287
288            // We need to leak the logger to satisfy 'static requirement of set_logger
289            let static_logger = Box::leak(logger);
290
291            log::set_logger(static_logger)?;
292            log::set_max_level(self.level.unwrap_or(log::LevelFilter::Trace));
293            Ok(())
294        }
295    }
296
297    impl Log for RichLogger {
298        fn enabled(&self, _metadata: &Metadata) -> bool {
299            true
300        }
301
302        fn log(&self, record: &Record) {
303            if !self.enabled(record.metadata()) {
304                return;
305            }
306
307            let console = CONSOLE.get_or_init(Console::new);
308
309            let level = match record.level() {
310                Level::Error => LogLevel::Error,
311                Level::Warn => LogLevel::Warning,
312                Level::Info => LogLevel::Info,
313                Level::Debug | Level::Trace => LogLevel::Debug,
314            };
315
316            let mut log_msg = LogMessage::new(&format!("{}", record.args()))
317                .level(level)
318                .show_time(self.config.enable_time);
319
320            if self.config.enable_path {
321                if let Some(file) = record.file_static() {
322                    if let Some(line) = record.line() {
323                        log_msg = log_msg.location(file, line);
324                    }
325                }
326            }
327
328            // Note: Timestamp is handled by LogMessage itself based on creation time,
329            // but we could suppress it in render if we passed config down.
330            // For now, let's just use what LogMessage does, but maybe we should refactor LogMessage
331            // to just hold data and let the renderer decide?
332            // Or simpler: We can't easily change LogMessage::render without changing trait signature
333            // or adding fields.
334            // Let's assume LogMessage::render always renders time if it has it,
335            // but we want to control it.
336            // Hack fix: If enable_time is false, we could modify how we construct LogMessage or
337            // implementation of Renderable for LogMessage needs to know about config.
338            // Since LogMessage is a public struct separate from RichLogger,
339            // we should probably just make LogMessage configurable or specific to this usage.
340            //
341            // For this iteration, let's keep LogMessage implementation simple and maybe update it
342            // to have public fields we can manipulate or rendering options.
343            // But LogMessage implements Renderable directly.
344
345            console.print_renderable(&log_msg);
346        }
347
348        fn flush(&self) {}
349    }
350}
351
352#[cfg(feature = "logging")]
353pub use log_integration::{RichLogger, RichLoggerBuilder};
354
355#[cfg(test)]
356mod tests {
357    use super::*;
358
359    #[test]
360    fn test_log_message_format_time() {
361        let msg = LogMessage::new("test");
362        let time = msg.format_time();
363        // Should be in HH:MM:SS.mmm format
364        assert!(time.contains(':'));
365        assert!(time.contains('.'));
366    }
367
368    #[test]
369    fn test_log_message_render() {
370        let msg = LogMessage::new("Hello").level(LogLevel::Info);
371        let context = RenderContext {
372            width: 80,
373            height: None,
374        };
375        let segments = msg.render(&context);
376
377        assert_eq!(segments.len(), 1);
378        let text = segments[0].plain_text();
379        assert!(text.contains("INFO"));
380        assert!(text.contains("Hello"));
381    }
382}