par_term/
debug.rs

1use parking_lot::Mutex;
2/// Comprehensive debugging infrastructure for par-term
3///
4/// Controlled by DEBUG_LEVEL environment variable:
5/// - 0 or unset: No debugging
6/// - 1: Errors only
7/// - 2: Info level (app events, graphics operations)
8/// - 3: Debug level (rendering, calculations)
9/// - 4: Trace level (every operation, detailed info)
10///
11/// All output goes to /tmp/par_term_debug.log on Unix/macOS,
12/// or %TEMP%\par_term_debug.log on Windows.
13/// This avoids breaking TUI apps by keeping debug output separate from stdout/stderr.
14use std::fmt;
15use std::fs::OpenOptions;
16use std::io::Write;
17use std::sync::OnceLock;
18use std::time::{SystemTime, UNIX_EPOCH};
19
20/// Debug level configuration
21#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
22pub enum DebugLevel {
23    Off = 0,
24    Error = 1,
25    Info = 2,
26    Debug = 3,
27    Trace = 4,
28}
29
30impl DebugLevel {
31    fn from_env() -> Self {
32        match std::env::var("DEBUG_LEVEL") {
33            Ok(val) => match val.trim().parse::<u8>() {
34                Ok(0) => DebugLevel::Off,
35                Ok(1) => DebugLevel::Error,
36                Ok(2) => DebugLevel::Info,
37                Ok(3) => DebugLevel::Debug,
38                Ok(4) => DebugLevel::Trace,
39                _ => DebugLevel::Off,
40            },
41            Err(_) => DebugLevel::Off,
42        }
43    }
44}
45
46/// Global debug logger
47struct DebugLogger {
48    level: DebugLevel,
49    file: Option<std::fs::File>,
50}
51
52impl DebugLogger {
53    fn new() -> Self {
54        let level = DebugLevel::from_env();
55
56        let file = if level != DebugLevel::Off {
57            #[cfg(unix)]
58            let log_path = std::path::PathBuf::from("/tmp/par_term_debug.log");
59            #[cfg(windows)]
60            let log_path = std::env::temp_dir().join("par_term_debug.log");
61
62            match OpenOptions::new()
63                .write(true)
64                .truncate(true)
65                .create(true)
66                .open(&log_path)
67            {
68                Ok(f) => {
69                    // Write header
70                    let mut logger = DebugLogger {
71                        level,
72                        file: Some(f),
73                    };
74                    logger.write_raw(&format!(
75                        "\n{}\npar-term debug session started at {} (level={:?})\n{}\n",
76                        "=".repeat(80),
77                        get_timestamp(),
78                        level,
79                        "=".repeat(80)
80                    ));
81                    return logger;
82                }
83                Err(_e) => {
84                    // Silently fail if log file can't be opened
85                    // This prevents debug output from interfering with TUI applications
86                    None
87                }
88            }
89        } else {
90            None
91        };
92
93        DebugLogger { level, file }
94    }
95
96    fn write_raw(&mut self, msg: &str) {
97        if let Some(ref mut file) = self.file {
98            let _ = file.write_all(msg.as_bytes());
99            let _ = file.flush();
100        }
101    }
102
103    fn log(&mut self, level: DebugLevel, category: &str, msg: &str) {
104        if level <= self.level {
105            let timestamp = get_timestamp();
106            let level_str = match level {
107                DebugLevel::Error => "ERROR",
108                DebugLevel::Info => "INFO ",
109                DebugLevel::Debug => "DEBUG",
110                DebugLevel::Trace => "TRACE",
111                DebugLevel::Off => return,
112            };
113            self.write_raw(&format!(
114                "[{}] [{}] [{}] {}\n",
115                timestamp, level_str, category, msg
116            ));
117        }
118    }
119}
120
121static LOGGER: OnceLock<Mutex<DebugLogger>> = OnceLock::new();
122
123fn get_logger() -> &'static Mutex<DebugLogger> {
124    LOGGER.get_or_init(|| Mutex::new(DebugLogger::new()))
125}
126
127fn get_timestamp() -> String {
128    let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
129    format!("{}.{:06}", now.as_secs(), now.subsec_micros())
130}
131
132/// Check if debugging is enabled at given level
133pub fn is_enabled(level: DebugLevel) -> bool {
134    let logger = get_logger().lock();
135    level <= logger.level
136}
137
138/// Log a message at specified level
139pub fn log(level: DebugLevel, category: &str, msg: &str) {
140    let mut logger = get_logger().lock();
141    logger.log(level, category, msg);
142}
143
144/// Log formatted message
145pub fn logf(level: DebugLevel, category: &str, args: fmt::Arguments) {
146    if is_enabled(level) {
147        log(level, category, &format!("{}", args));
148    }
149}
150
151// Convenience macros for logging
152#[macro_export]
153macro_rules! debug_error {
154    ($category:expr, $($arg:tt)*) => {
155        $crate::debug::logf($crate::debug::DebugLevel::Error, $category, format_args!($($arg)*))
156    };
157}
158
159#[macro_export]
160macro_rules! debug_info {
161    ($category:expr, $($arg:tt)*) => {
162        $crate::debug::logf($crate::debug::DebugLevel::Info, $category, format_args!($($arg)*))
163    };
164}
165
166#[macro_export]
167macro_rules! debug_log {
168    ($category:expr, $($arg:tt)*) => {
169        $crate::debug::logf($crate::debug::DebugLevel::Debug, $category, format_args!($($arg)*))
170    };
171}
172
173#[macro_export]
174macro_rules! debug_trace {
175    ($category:expr, $($arg:tt)*) => {
176        $crate::debug::logf($crate::debug::DebugLevel::Trace, $category, format_args!($($arg)*))
177    };
178}