1use crate::console::{Console, RenderContext};
6use crate::renderable::{Renderable, Segment};
7use crate::style::{Color, Style};
8use crate::text::Span;
9use std::time::SystemTime;
10
11#[derive(Debug)]
13pub struct LogMessage {
14 pub message: String,
16 pub file: Option<&'static str>,
18 pub line: Option<u32>,
20 pub time: SystemTime,
22 pub level: LogLevel,
24 pub show_time: bool,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
30pub enum LogLevel {
31 Debug,
33 #[default]
35 Info,
36 Warning,
38 Error,
40}
41
42impl LogLevel {
43 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 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 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 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 pub fn level(mut self, level: LogLevel) -> Self {
86 self.level = level;
87 self
88 }
89
90 pub fn show_time(mut self, show: bool) -> Self {
92 self.show_time = show;
93 self
94 }
95
96 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 fn format_location(&self) -> Option<String> {
113 match (self.file, self.line) {
114 (Some(file), Some(line)) => {
115 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 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 spans.push(Span::styled(
139 format!("{:5}", self.level.label()),
140 self.level.style(),
141 ));
142
143 spans.push(Span::raw(" "));
144
145 spans.push(Span::raw(self.message.clone()));
147
148 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
161pub trait ConsoleLog {
163 fn log(&self, message: &str);
165
166 fn debug(&self, message: &str);
168
169 fn warn(&self, message: &str);
171
172 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_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 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 #[derive(Clone, Debug)]
220 pub struct RichLoggerConfig {
221 pub enable_time: bool,
223 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 pub struct RichLogger {
238 config: RichLoggerConfig,
239 }
240
241 impl RichLogger {
242 pub fn builder() -> RichLoggerBuilder {
244 RichLoggerBuilder::default()
245 }
246
247 pub fn init() -> Result<(), SetLoggerError> {
249 Self::builder().init()
250 }
251 }
252
253 #[derive(Default)]
255 pub struct RichLoggerBuilder {
256 config: RichLoggerConfig,
257 level: Option<log::LevelFilter>,
258 }
259
260 impl RichLoggerBuilder {
261 pub fn enable_time(mut self, enable: bool) -> Self {
263 self.config.enable_time = enable;
264 self
265 }
266
267 pub fn enable_path(mut self, enable: bool) -> Self {
269 self.config.enable_path = enable;
270 self
271 }
272
273 pub fn filter_level(mut self, level: log::LevelFilter) -> Self {
275 self.level = Some(level);
276 self
277 }
278
279 pub fn init(self) -> Result<(), SetLoggerError> {
281 CONSOLE.get_or_init(Console::new);
283
284 let logger = Box::new(RichLogger {
285 config: self.config,
286 });
287
288 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 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 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}