1use parking_lot::Mutex;
2use std::fmt;
15use std::fs::OpenOptions;
16use std::io::Write;
17use std::sync::OnceLock;
18use std::time::{SystemTime, UNIX_EPOCH};
19
20#[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
46struct 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)]
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 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 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
135pub fn is_enabled(level: DebugLevel) -> bool {
137 let logger = get_logger().lock();
138 level <= logger.level
139}
140
141pub fn log(level: DebugLevel, category: &str, msg: &str) {
143 let mut logger = get_logger().lock();
144 logger.log(level, category, msg);
145}
146
147pub fn logf(level: DebugLevel, category: &str, args: fmt::Arguments) {
149 if is_enabled(level) {
150 log(level, category, &format!("{}", args));
151 }
152}
153
154#[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
183pub 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
209pub 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
224pub 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
240pub 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
251pub 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
262pub 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
285pub 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
297pub 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
309pub 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
320pub 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
343pub 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
363pub 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
377pub 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
388pub 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
414pub 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}