1pub fn add(left: u64, right: u64) -> u64 {
2 left + right
3}
4
5#[cfg(test)]
6mod tests {
7 use super::*;
8
9 #[test]
10 fn it_works() {
11 let result = add(2, 2);
12 assert_eq!(result, 4);
13 }
14}
15
16use std::{
17 env,
18 path::Path,
19 fs::{OpenOptions, File},
20 io::Write,
21 sync::{OnceLock, Mutex},
22 time::Instant,
23};
24use chrono::Local;
25use colored::*;
26
27static LOGGER: OnceLock<Mutex<DuckTraceLogger>> = OnceLock::new();
28
29struct DuckTraceLogger {
30 level: LogLevel,
31 log_file: Option<File>,
32 log_path: Option<String>,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd)]
36enum LogLevel {
37 Debug,
38 Info,
39 Warning,
40 Error,
41 Critical,
42}
43
44impl DuckTraceLogger {
45 fn new(file_override: Option<&str>, level_override: Option<&str>) -> Self {
46 let level = match level_override {
47 Some(l) => Self::level_from_str(l),
48 None => match env::var("DT_LOG_LEVEL")
49 .unwrap_or_else(|_| "INFO".to_string())
50 .to_uppercase()
51 .as_str()
52 {
53 "DEBUG" => LogLevel::Debug,
54 "WARNING" => LogLevel::Warning,
55 "ERROR" => LogLevel::Error,
56 "CRITICAL" => LogLevel::Critical,
57 _ => LogLevel::Info,
58 },
59 };
60
61 let (log_file, log_path) = if let Some(path_str) = file_override {
62 let path = Path::new(path_str);
63 let dir = path
64 .parent()
65 .map(|p| p.to_string_lossy().into_owned())
66 .unwrap_or_else(|| {
67 env::var("DT_LOG_PATH").unwrap_or_else(|_| {
68 let home = env::var("HOME").unwrap_or_else(|_| ".".to_string());
69 format!("{}/.config/duckTrace", home)
70 })
71 });
72 let file = path
73 .file_name()
74 .map(|f| f.to_string_lossy().into_owned())
75 .unwrap_or_else(|| "unknown-script.log".to_string());
76
77 Self::open_log_file(&dir, &file)
78 } else {
79 let dir = env::var("DT_LOG_PATH").unwrap_or_else(|_| {
80 let home = env::var("HOME").unwrap_or_else(|_| ".".to_string());
81 format!("{}/.config/duckTrace", home)
82 });
83 let file = env::var("DT_LOG_FILE").unwrap_or_else(|_| "unknown-script.log".to_string());
84
85 Self::open_log_file(&dir, &file)
86 };
87
88 let mut logger = Self {
89 level,
90 log_file,
91 log_path,
92 };
93 logger.log_config();
94 logger
95 }
96
97 fn open_log_file(dir: &str, filename: &str) -> (Option<File>, Option<String>) {
98 if std::fs::create_dir_all(dir).is_err() {
99 return (None, None);
100 }
101
102 let full_path = Path::new(dir).join(filename);
103 let path_str = full_path.to_string_lossy().into_owned();
104
105 match OpenOptions::new()
106 .create(true)
107 .append(true)
108 .open(&full_path)
109 {
110 Ok(file) => (Some(file), Some(path_str)),
111 Err(_) => (None, None),
112 }
113 }
114
115 fn level_from_str(s: &str) -> LogLevel {
116 match s.to_uppercase().as_str() {
117 "DEBUG" => LogLevel::Debug,
118 "INFO" => LogLevel::Info,
119 "WARNING" => LogLevel::Warning,
120 "ERROR" => LogLevel::Error,
121 "CRITICAL" => LogLevel::Critical,
122 _ => LogLevel::Info,
123 }
124 }
125
126 fn log_config(&mut self) {
127 let log_path_display = self
128 .log_path
129 .as_ref()
130 .map(|p| p.as_str())
131 .unwrap_or("Logging to file disabled");
132 let level_str = match self.level {
133 LogLevel::Debug => "DEBUG",
134 LogLevel::Info => "INFO",
135 LogLevel::Warning => "WARNING",
136 LogLevel::Error => "ERROR",
137 LogLevel::Critical => "CRITICAL",
138 };
139 self.log(
140 LogLevel::Debug,
141 &format!(
142 "Logger initialized: level={}, file={}",
143 level_str, log_path_display
144 ),
145 );
146 }
147
148 fn should_log(&self, msg_level: LogLevel) -> bool {
149 msg_level >= self.level
150 }
151
152 fn get_symbol(&self, level: LogLevel) -> &'static str {
153 match level {
154 LogLevel::Debug => "⁉️",
155 LogLevel::Info => "✅",
156 LogLevel::Warning => "⚠️",
157 LogLevel::Error => "❌",
158 LogLevel::Critical => "🚨",
159 }
160 }
161
162 fn format_message(&self, level: LogLevel, message: &str) -> String {
163 let timestamp = Local::now().format("%H:%M:%S");
164 let symbol = self.get_symbol(level);
165 let level_str = match level {
166 LogLevel::Debug => "DEBUG",
167 LogLevel::Info => "INFO",
168 LogLevel::Warning => "WARNING",
169 LogLevel::Error => "ERROR",
170 LogLevel::Critical => "CRITICAL",
171 };
172
173 format!(
174 "[🦆📜] [{}] {}{}{} ⮞ {}",
175 timestamp, symbol, level_str, symbol, message
176 )
177 }
178
179 fn colorize_console(&self, level: LogLevel, formatted_msg: &str) -> String {
180 match level {
181 LogLevel::Debug => formatted_msg.blue().bold().to_string(),
182 LogLevel::Info => formatted_msg.green().bold().to_string(),
183 LogLevel::Warning => formatted_msg.yellow().bold().to_string(),
184 LogLevel::Error => formatted_msg.red().bold().blink().to_string(),
185 LogLevel::Critical => formatted_msg.red().bold().blink().to_string(),
186 }
187 }
188
189 fn add_duck_say(&self, level: LogLevel, message: &str) -> String {
190 if matches!(level, LogLevel::Error | LogLevel::Critical) {
191 format!(
192 "\n\x1b[3m\x1b[38;2;0;150;150m🦆 duck say \x1b[1m\x1b[38;2;255;255;0m⮞\x1b[0m\x1b[3m\x1b[38;2;0;150;150m fuck ❌ {}\x1b[0m",
193 message
194 )
195 } else {
196 String::new()
197 }
198 }
199
200 pub fn log(&mut self, level: LogLevel, message: &str) {
201 if !self.should_log(level) {
202 return;
203 }
204
205 let formatted = self.format_message(level, message);
206 let console_output = self.colorize_console(level, &formatted);
207
208 eprintln!("{}", console_output);
209
210 if matches!(level, LogLevel::Error | LogLevel::Critical) {
211 let duck_say = self.add_duck_say(level, message);
212 eprintln!("{}", duck_say);
213 }
214
215 if let Some(file) = &mut self.log_file {
216 let timestamp = Local::now().format("%H:%M:%S");
217 let level_str = match level {
218 LogLevel::Debug => "DEBUG",
219 LogLevel::Info => "INFO",
220 LogLevel::Warning => "WARNING",
221 LogLevel::Error => "ERROR",
222 LogLevel::Critical => "CRITICAL",
223 };
224
225 let file_msg = format!("[{}] {} - {}\n", timestamp, level_str, message);
226 let _ = writeln!(file, "{}", file_msg);
227 }
228 }
229}
230
231fn with_logger<F>(level: LogLevel, msg: &str, f: F)
232where
233 F: FnOnce(&mut DuckTraceLogger, LogLevel, &str),
234{
235 let logger = LOGGER.get_or_init(|| Mutex::new(DuckTraceLogger::new(None, None)));
236 let mut guard = logger.lock().unwrap();
237 f(&mut guard, level, msg);
238}
239
240pub fn dt_debug(msg: &str) {
241 with_logger(LogLevel::Debug, msg, |logger, lvl, m| logger.log(lvl, m));
242}
243
244pub fn dt_info(msg: &str) {
245 with_logger(LogLevel::Info, msg, |logger, lvl, m| logger.log(lvl, m));
246}
247
248pub fn dt_warning(msg: &str) {
249 with_logger(LogLevel::Warning, msg, |logger, lvl, m| logger.log(lvl, m));
250}
251
252pub fn dt_error(msg: &str) {
253 with_logger(LogLevel::Error, msg, |logger, lvl, m| logger.log(lvl, m));
254}
255
256pub fn dt_critical(msg: &str) {
257 with_logger(LogLevel::Critical, msg, |logger, lvl, m| logger.log(lvl, m));
258}
259
260pub fn dt_setup(file_override: Option<&str>, level_override: Option<&str>) {
261 let _ = LOGGER.get_or_init(|| Mutex::new(DuckTraceLogger::new(file_override, level_override)));
262}
263
264pub struct DtTimer {
265 operation_name: String,
266 start_time: Instant,
267}
268
269impl DtTimer {
270 pub fn new(operation_name: &str) -> Self {
271 dt_debug(&format!("Starting {}...", operation_name));
272 Self {
273 operation_name: operation_name.to_string(),
274 start_time: Instant::now(),
275 }
276 }
277
278 pub fn lap(&self, lap_name: &str) {
279 let elapsed = self.start_time.elapsed().as_secs_f64();
280 dt_debug(&format!("{} - {}: {:.3}s", self.operation_name, lap_name, elapsed));
281 }
282
283 pub fn complete(self) {
284 let elapsed = self.start_time.elapsed().as_secs_f64();
285 dt_debug(&format!("Completed {} in {:.3}s", self.operation_name, elapsed));
286 }
287}
288
289pub fn dt_timer(name: &str) -> DtTimer {
290 DtTimer::new(name)
291}
292
293pub fn dt_duck_say(message: &str) {
294 let teal = Color::TrueColor { r: 0, g: 150, b: 150 };
295 let yellow = Color::TrueColor { r: 255, g: 255, b: 0 };
296
297 eprintln!(
298 "{}{} {}",
299 "🦆 duck say ".italic().color(teal),
300 "⮞".bold().color(yellow),
301 message.italic().color(teal)
302 );
303}
304
305#[macro_export]
306macro_rules! duck_log {
307 (debug: $($arg:tt)*) => {
308 $crate::dt_debug(&format!($($arg)*));
309 };
310 (info: $($arg:tt)*) => {
311 $crate::dt_info(&format!($($arg)*));
312 };
313 (warning: $($arg:tt)*) => {
314 $crate::dt_warning(&format!($($arg)*));
315 };
316 (error: $($arg:tt)*) => {
317 $crate::dt_error(&format!($($arg)*));
318 };
319 (critical: $($arg:tt)*) => {
320 $crate::dt_critical(&format!($($arg)*));
321 };
322}
323
324#[macro_export]
325macro_rules! dt_debug {
326 ($($arg:tt)*) => {
327 $crate::dt_debug(&format!($($arg)*));
328 };
329}
330
331#[macro_export]
332macro_rules! dt_info {
333 ($($arg:tt)*) => {
334 $crate::dt_info(&format!($($arg)*));
335 };
336}
337
338#[macro_export]
339macro_rules! dt_warning {
340 ($($arg:tt)*) => {
341 $crate::dt_warning(&format!($($arg)*));
342 };
343}
344
345#[macro_export]
346macro_rules! dt_error {
347 ($($arg:tt)*) => {
348 $crate::dt_error(&format!($($arg)*));
349 };
350}
351
352#[macro_export]
353macro_rules! dt_critical {
354 ($($arg:tt)*) => {
355 $crate::dt_critical(&format!($($arg)*));
356 };
357}
358
359#[macro_export]
360macro_rules! duck_say {
361 ($($arg:tt)*) => {
362 $crate::dt_say(&format!($($arg)*));
363 };
364}