1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
30pub enum LogLevel {
31 Debug = 0,
33 Info = 1,
35 Warn = 2,
37 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
74pub 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
84pub fn init_with_writer(writer: Box<dyn Write + Send>) -> io::Result<()> {
89 init_with_writer_and_level(writer, LogLevel::Debug)
90}
91
92pub 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
112pub fn debug(message: &str) {
114 log(LogLevel::Debug, message);
115}
116
117pub fn info(message: &str) {
119 log(LogLevel::Info, message);
120}
121
122pub fn warn(message: &str) {
124 log(LogLevel::Warn, message);
125}
126
127pub fn error(message: &str) {
129 log(LogLevel::Error, message);
130}
131
132fn 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
143pub 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
154pub 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
165pub 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
176pub 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 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 }
228}