Skip to main content

par_term_emu_core_rust/
debug.rs

1use parking_lot::Mutex;
2/// Comprehensive debugging infrastructure for par-term-emu
3///
4/// Controlled by DEBUG_LEVEL environment variable:
5/// - 0 or unset: No debugging
6/// - 1: Errors only
7/// - 2: Info level (screen switches, device queries)
8/// - 3: Debug level (VT sequences, buffer changes)
9/// - 4: Trace level (every operation, buffer snapshots)
10///
11/// All output goes to /tmp/par_term_emu_core_rust_debug_rust.log on Unix/macOS,
12/// or %TEMP%\par_term_emu_core_rust_debug_rust.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            // Rust uses separate log file from Python
58            // Use /tmp on Unix/macOS for consistency with documentation
59            // Use %TEMP% on Windows
60            #[cfg(unix)]
61            let log_path = std::path::PathBuf::from("/tmp/par_term_emu_core_rust_debug_rust.log");
62            #[cfg(windows)]
63            let log_path = std::env::temp_dir().join("par_term_emu_core_rust_debug_rust.log");
64
65            match OpenOptions::new()
66                .write(true)
67                .truncate(true)
68                .create(true)
69                .open(&log_path)
70            {
71                Ok(f) => {
72                    // Write header
73                    let mut logger = DebugLogger {
74                        level,
75                        file: Some(f),
76                    };
77                    logger.write_raw(&format!(
78                        "\n{}\npar-term-emu Rust debug session started at {} (level={:?})\n{}\n",
79                        "=".repeat(80),
80                        get_timestamp(),
81                        level,
82                        "=".repeat(80)
83                    ));
84                    return logger;
85                }
86                Err(_e) => {
87                    // Silently fail if log file can't be opened
88                    // This prevents debug output from interfering with TUI applications
89                    None
90                }
91            }
92        } else {
93            None
94        };
95
96        DebugLogger { level, file }
97    }
98
99    fn write_raw(&mut self, msg: &str) {
100        if let Some(ref mut file) = self.file {
101            let _ = file.write_all(msg.as_bytes());
102            let _ = file.flush();
103        }
104    }
105
106    fn log(&mut self, level: DebugLevel, category: &str, msg: &str) {
107        if level <= self.level {
108            let timestamp = get_timestamp();
109            let level_str = match level {
110                DebugLevel::Error => "ERROR",
111                DebugLevel::Info => "INFO ",
112                DebugLevel::Debug => "DEBUG",
113                DebugLevel::Trace => "TRACE",
114                DebugLevel::Off => return,
115            };
116            self.write_raw(&format!(
117                "[{}] [{}] [{}] {}\n",
118                timestamp, level_str, category, msg
119            ));
120        }
121    }
122}
123
124static LOGGER: OnceLock<Mutex<DebugLogger>> = OnceLock::new();
125
126fn get_logger() -> &'static Mutex<DebugLogger> {
127    LOGGER.get_or_init(|| Mutex::new(DebugLogger::new()))
128}
129
130fn get_timestamp() -> String {
131    let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
132    format!("{}.{:06}", now.as_secs(), now.subsec_micros())
133}
134
135/// Check if debugging is enabled at given level
136pub fn is_enabled(level: DebugLevel) -> bool {
137    let logger = get_logger().lock();
138    level <= logger.level
139}
140
141/// Log a message at specified level
142pub fn log(level: DebugLevel, category: &str, msg: &str) {
143    let mut logger = get_logger().lock();
144    logger.log(level, category, msg);
145}
146
147/// Log formatted message
148pub fn logf(level: DebugLevel, category: &str, args: fmt::Arguments) {
149    if is_enabled(level) {
150        log(level, category, &format!("{}", args));
151    }
152}
153
154// Convenience macros for logging
155#[macro_export]
156macro_rules! debug_error {
157    ($category:expr, $($arg:tt)*) => {
158        $crate::debug::logf($crate::debug::DebugLevel::Error, $category, format_args!($($arg)*))
159    };
160}
161
162#[macro_export]
163macro_rules! debug_info {
164    ($category:expr, $($arg:tt)*) => {
165        $crate::debug::logf($crate::debug::DebugLevel::Info, $category, format_args!($($arg)*))
166    };
167}
168
169#[macro_export]
170macro_rules! debug_log {
171    ($category:expr, $($arg:tt)*) => {
172        $crate::debug::logf($crate::debug::DebugLevel::Debug, $category, format_args!($($arg)*))
173    };
174}
175
176#[macro_export]
177macro_rules! debug_trace {
178    ($category:expr, $($arg:tt)*) => {
179        $crate::debug::logf($crate::debug::DebugLevel::Trace, $category, format_args!($($arg)*))
180    };
181}
182
183/// VT sequence logging
184pub fn log_vt_input(bytes: &[u8]) {
185    if is_enabled(DebugLevel::Debug) {
186        let hex: String = bytes
187            .iter()
188            .map(|b| format!("{:02x}", b))
189            .collect::<Vec<_>>()
190            .join(" ");
191        let printable: String = bytes
192            .iter()
193            .map(|&b| {
194                if (32..127).contains(&b) {
195                    b as char
196                } else {
197                    '.'
198                }
199            })
200            .collect();
201        log(
202            DebugLevel::Debug,
203            "VT_INPUT",
204            &format!("len={} hex=[{}] ascii=[{}]", bytes.len(), hex, printable),
205        );
206    }
207}
208
209/// Screen switch logging
210pub fn log_screen_switch(to_alt: bool, reason: &str) {
211    if is_enabled(DebugLevel::Info) {
212        log(
213            DebugLevel::Info,
214            "SCREEN_SWITCH",
215            &format!(
216                "switched to {} screen ({})",
217                if to_alt { "ALTERNATE" } else { "PRIMARY" },
218                reason
219            ),
220        );
221    }
222}
223
224/// Device query logging
225pub fn log_device_query(query: &str, response: &[u8]) {
226    if is_enabled(DebugLevel::Info) {
227        let hex: String = response
228            .iter()
229            .map(|b| format!("{:02x}", b))
230            .collect::<Vec<_>>()
231            .join(" ");
232        log(
233            DebugLevel::Info,
234            "DEVICE_QUERY",
235            &format!("query='{}' response=[{}]", query, hex),
236        );
237    }
238}
239
240/// Buffer snapshot logging
241pub fn log_buffer_snapshot(label: &str, rows: usize, cols: usize, content: &str) {
242    if is_enabled(DebugLevel::Trace) {
243        let mut logger = get_logger().lock();
244        logger.write_raw(&format!(
245            "\n{:-<80}\nBUFFER SNAPSHOT: {} ({}x{})\n{:-<80}\n{}\n{:-<80}\n",
246            "", label, rows, cols, "", content, ""
247        ));
248    }
249}
250
251/// Generation counter logging
252pub fn log_generation_change(old: u64, new: u64, reason: &str) {
253    if is_enabled(DebugLevel::Debug) {
254        log(
255            DebugLevel::Debug,
256            "GENERATION",
257            &format!("counter changed: {} -> {} ({})", old, new, reason),
258        );
259    }
260}
261
262/// CSI dispatch logging
263pub fn log_csi_dispatch(params: &[i64], intermediates: &[u8], final_byte: char) {
264    if is_enabled(DebugLevel::Debug) {
265        let params_str: String = params
266            .iter()
267            .map(|p| p.to_string())
268            .collect::<Vec<_>>()
269            .join(";");
270        let inter_str: String = intermediates.iter().map(|&b| b as char).collect();
271        log(
272            DebugLevel::Debug,
273            "CSI",
274            &format!(
275                "CSI {}{}{}  (params=[{}])",
276                if inter_str.is_empty() { "" } else { &inter_str },
277                final_byte,
278                "",
279                params_str
280            ),
281        );
282    }
283}
284
285/// OSC dispatch logging
286pub fn log_osc_dispatch(params: &[&[u8]]) {
287    if is_enabled(DebugLevel::Debug) {
288        let params_str: String = params
289            .iter()
290            .map(|p| String::from_utf8_lossy(p).to_string())
291            .collect::<Vec<_>>()
292            .join(";");
293        log(DebugLevel::Debug, "OSC", &format!("OSC {}", params_str));
294    }
295}
296
297/// ESC dispatch logging
298pub fn log_esc_dispatch(intermediates: &[u8], final_byte: char) {
299    if is_enabled(DebugLevel::Debug) {
300        let inter_str: String = intermediates.iter().map(|&b| b as char).collect();
301        log(
302            DebugLevel::Debug,
303            "ESC",
304            &format!("ESC {}{}", inter_str, final_byte),
305        );
306    }
307}
308
309/// Print character logging
310pub fn log_print(c: char, col: usize, row: usize) {
311    if is_enabled(DebugLevel::Trace) {
312        log(
313            DebugLevel::Trace,
314            "PRINT",
315            &format!("char='{}' (U+{:04X}) at ({},{})", c, c as u32, col, row),
316        );
317    }
318}
319
320/// Execute control code logging
321pub fn log_execute(byte: u8) {
322    if is_enabled(DebugLevel::Debug) {
323        let name = match byte {
324            0x07 => "BEL",
325            0x08 => "BS",
326            0x09 => "HT",
327            0x0A => "LF",
328            0x0B => "VT",
329            0x0C => "FF",
330            0x0D => "CR",
331            0x0E => "SO",
332            0x0F => "SI",
333            _ => "???",
334        };
335        log(
336            DebugLevel::Debug,
337            "EXECUTE",
338            &format!("control=0x{:02X} ({})", byte, name),
339        );
340    }
341}
342
343/// Cursor movement logging
344pub fn log_cursor_move(
345    from_col: usize,
346    from_row: usize,
347    to_col: usize,
348    to_row: usize,
349    reason: &str,
350) {
351    if is_enabled(DebugLevel::Trace) {
352        log(
353            DebugLevel::Trace,
354            "CURSOR",
355            &format!(
356                "moved ({},{}) -> ({},{}) [{}]",
357                from_col, from_row, to_col, to_row, reason
358            ),
359        );
360    }
361}
362
363/// Scroll operation logging
364pub fn log_scroll(direction: &str, region_top: usize, region_bottom: usize, lines: usize) {
365    if is_enabled(DebugLevel::Debug) {
366        log(
367            DebugLevel::Debug,
368            "SCROLL",
369            &format!(
370                "{} {} lines in region [{}..{}]",
371                direction, lines, region_top, region_bottom
372            ),
373        );
374    }
375}
376
377/// Grid operation logging
378pub fn log_grid_op(operation: &str, details: &str) {
379    if is_enabled(DebugLevel::Debug) {
380        log(
381            DebugLevel::Debug,
382            "GRID_OP",
383            &format!("{}: {}", operation, details),
384        );
385    }
386}
387
388/// PTY operation logging
389pub fn log_pty_read(bytes_read: usize) {
390    if is_enabled(DebugLevel::Trace) {
391        log(
392            DebugLevel::Trace,
393            "PTY_READ",
394            &format!("read {} bytes from PTY", bytes_read),
395        );
396    }
397}
398
399pub fn log_pty_write(bytes: &[u8]) {
400    if is_enabled(DebugLevel::Debug) {
401        let hex: String = bytes
402            .iter()
403            .map(|b| format!("{:02x}", b))
404            .collect::<Vec<_>>()
405            .join(" ");
406        log(
407            DebugLevel::Debug,
408            "PTY_WRITE",
409            &format!("wrote {} bytes: [{}]", bytes.len(), hex),
410        );
411    }
412}
413
414/// Mode change logging
415pub fn log_mode_change(mode: &str, enabled: bool) {
416    if is_enabled(DebugLevel::Info) {
417        log(
418            DebugLevel::Info,
419            "MODE",
420            &format!("{} {}", mode, if enabled { "enabled" } else { "disabled" }),
421        );
422    }
423}
424
425#[cfg(test)]
426mod tests {
427    use super::*;
428
429    #[test]
430    fn test_debug_level_parsing() {
431        std::env::set_var("DEBUG_LEVEL", "3");
432        assert_eq!(DebugLevel::from_env(), DebugLevel::Debug);
433
434        std::env::set_var("DEBUG_LEVEL", "0");
435        assert_eq!(DebugLevel::from_env(), DebugLevel::Off);
436
437        std::env::remove_var("DEBUG_LEVEL");
438        assert_eq!(DebugLevel::from_env(), DebugLevel::Off);
439    }
440}