mazer_dbg/
lib.rs

1//! A GUI-based variable inspection tool for Rust, useful for debugging without print statements.  
2//! At runtime, `inspect!(...)` opens a window displaying your variables with pretty formatting.  
3//! 
4//! This was inspired by Suneido's Inspect tool, which allows you to inspect variables in a GUI.  
5//! 
6//! This works under the hood by forking the process and using IPC channels to communicate with a GUI server.
7//! Only supported on Unix-like systems (Linux, macOS, etc.).
8//! 
9//! This also allows you to "time travel" through your debug frames, letting you go back and forth through previous variable states.
10//! NOTE: This does not change the execution of your program, only debug's inspect history.
11//! 
12//! Usage:  
13//!     ```
14//!         mazer_dbg::inspect!(var1, var2, ...);
15//!         mazer_dbg::inspect_when!(condition, var1, var2, ...);
16//!     ```
17//! 
18//! inspect!() will automatically initialize the debug server if it hasn't been done yet.
19//! 
20//! The library wraps in #[cfg(debug_assertions)] so it only compiles in debug builds. 
21//! No runtime cost in release builds, as all inspect!() calls are optimized out.
22//!
23
24#![allow(dead_code)]
25#![allow(unused_imports)]
26
27use ipc_channel::ipc;
28use nix::unistd::{ForkResult, fork};
29use serde::{Deserialize, Serialize};
30use std::collections::{BTreeMap, VecDeque};
31use std::process;
32use std::sync::{Arc, Mutex, OnceLock, Once};
33
34
35
36#[cfg(test)]
37mod tests;
38#[cfg(not(unix))]
39compile_error!("This crate is only supported on Unix-like systems (Linux, macOS, etc.)");
40
41
42#[derive(Serialize, Deserialize, Debug, Clone)]
43struct DebugMessage {
44    timestamp: u64,
45    file: String,
46    line: u32,
47    column: u32,
48    variables: BTreeMap<String, VariableDebugFrame>,
49    backtrace: String,
50}
51
52#[derive(Serialize, Deserialize, Debug, Clone)]
53struct DebugResponse {
54    continue_execution: bool,
55}
56
57/// Debug frame history manager for time traveling
58/// 
59/// Stores all debug frames without limit to allow comprehensive debugging sessions.
60/// Memory usage grows with the number of inspect!() calls, but this provides maximum
61/// flexibility for debugging complex applications.
62#[derive(Debug, Clone)]
63struct DebugFrameHistory {
64    frames: VecDeque<DebugMessage>,
65    current_index: usize,
66}
67
68impl DebugFrameHistory {
69    fn new() -> Self {
70        Self {
71            frames: VecDeque::new(),
72            current_index: 0,
73        }
74    }
75
76    fn add_frame(&mut self, frame: DebugMessage) {
77        self.frames.push_back(frame);
78        self.current_index = self.frames.len().saturating_sub(1);
79    }
80
81    fn go_backward(&mut self) -> bool {
82        if self.current_index > 0 {
83            self.current_index -= 1;
84            true
85        } else {
86            false
87        }
88    }
89
90    fn go_forward(&mut self) -> bool {
91        if self.current_index + 1 < self.frames.len() {
92            self.current_index += 1;
93            true
94        } else {
95            false
96        }
97    }
98
99    fn get_current_frame(&self) -> Option<&DebugMessage> {
100        self.frames.get(self.current_index)
101    }
102
103    fn can_go_backward(&self) -> bool {
104        self.current_index > 0
105    }
106
107    fn can_go_forward(&self) -> bool {
108        self.current_index + 1 < self.frames.len()
109    }
110
111    fn get_position_info(&self) -> (usize, usize) {
112        (self.current_index + 1, self.frames.len())
113    }
114}
115
116// Global channels for bidirectional communication
117static DEBUG_SENDER: OnceLock<Arc<Mutex<ipc::IpcSender<DebugMessage>>>> = OnceLock::new();
118static RESPONSE_RECEIVER: OnceLock<Arc<Mutex<ipc::IpcReceiver<DebugResponse>>>> = OnceLock::new();
119
120/// #[deprecated(since = "2.0.0", note = "No longer needed; `inspect!` auto-initializes, init() is still called internally!")]
121#[cfg(debug_assertions)]
122fn init() {
123    let (debug_tx, debug_rx) = match ipc::channel() {
124        Ok(channel) => channel,
125        Err(e) => {
126            panic!("Failed to create debug IPC channel: {}", e);
127        }
128    };
129
130    let (response_tx, response_rx) = match ipc::channel() {
131        Ok(channel) => channel,
132        Err(e) => {
133            panic!("Failed to create response IPC channel: {}", e);
134        }
135    };
136
137    match unsafe { fork() } {
138        Ok(ForkResult::Parent { child: _ }) => {
139            if DEBUG_SENDER.set(Arc::new(Mutex::new(debug_tx))).is_err() {
140                panic!("Failed to set debug sender");
141            }
142
143            if RESPONSE_RECEIVER
144                .set(Arc::new(Mutex::new(response_rx)))
145                .is_err()
146            {
147                panic!("Failed to set response receiver");
148            }
149        }
150        Ok(ForkResult::Child) => {
151            debug_server_process(debug_rx, response_tx);
152            unreachable!()
153        }
154        Err(e) => {
155            panic!("Fork failed: {}", e);
156        }
157    }
158}
159
160pub fn send_to_debug_server_and_wait(
161    variables: BTreeMap<String, VariableDebugFrame>,
162    file: &str,
163    line: u32,
164    column: u32,
165    backtrace: String,
166) {
167    if let (Some(sender), Some(receiver)) = (DEBUG_SENDER.get(), RESPONSE_RECEIVER.get()) {
168        if let (Ok(sender), Ok(receiver)) = (sender.lock(), receiver.lock()) {
169            let message = DebugMessage {
170                timestamp: std::time::SystemTime::now()
171                    .duration_since(std::time::UNIX_EPOCH)
172                    .unwrap_or_default()
173                    .as_millis() as u64,
174                file: file.to_string(),
175                line,
176                column,
177                variables,
178                backtrace,
179            };
180
181            if let Err(e) = sender.send(message) {
182                eprintln!("Failed to send debug message: {}", e);
183                return;
184            }
185
186            // Wait for response (GUI window closed)
187            match receiver.recv() {
188                Ok(_response) => {
189                    // continue execution
190                }
191                Err(e) => {
192                    eprintln!("Failed to receive response from debug server: {}", e);
193                }
194            }
195        }
196    }
197}
198
199/// The debug server process that receives debug messages and shows GUI
200fn debug_server_process(
201    rx: ipc::IpcReceiver<DebugMessage>,
202    response_tx: ipc::IpcSender<DebugResponse>,
203) {
204    let mut frame_history = DebugFrameHistory::new();
205    
206    loop {
207        match rx.recv() {
208            Ok(message) => {
209                frame_history.add_frame(message);
210                show_debug_gui_with_history(&mut frame_history);
211
212                // Send response to continue execution
213                let response = DebugResponse {
214                    continue_execution: true,
215                };
216
217                if let Err(e) = response_tx.send(response) {
218                    eprintln!("Failed to send response: {}", e);
219                    break;
220                }
221            }
222            Err(e) => {
223                eprintln!("Debug server: Channel error: {}", e);
224                break;
225            }
226        }
227    }
228    process::exit(0);
229}
230
231fn create_json_from_variables(variables: &BTreeMap<String, VariableDebugFrame>) -> String {
232    let mut json_parts = Vec::new();
233
234    for (name, var_frame) in variables {
235        let escaped_value = var_frame.value
236            .replace('\\', "\\\\")
237            .replace('"', "\\\"")
238            .replace('\n', "\\n")
239            .replace('\r', "\\r")
240            .replace('\t', "\\t");
241
242        // Escape the type name for JSON
243        let escaped_type = var_frame.type_name
244            .replace('\\', "\\\\")
245            .replace('"', "\\\"");
246
247        json_parts.push(format!("  \"{}\": \"{}\"", name, escaped_value));
248        json_parts.push(format!("  \"{}_type\": \"{}\"", name, escaped_type));
249        
250        let size_value = if let Some(size) = var_frame.size_hint {
251            size.to_string()
252        } else {
253            "unknown".to_string()
254        };
255
256        // size in bytes
257        json_parts.push(format!("  \"{}_size\": \"{}\"", name, size_value));
258    }
259
260    format!("{{\n{}\n}}", json_parts.join(",\n"))
261}
262
263/// Copy text to clipboard
264fn copy_to_clipboard(text: &str) -> Result<(), Box<dyn std::error::Error>> {
265    use arboard::Clipboard;
266    let mut clipboard = Clipboard::new()?;
267    clipboard.set_text(text)?;
268    Ok(())
269}
270
271/// Show GUI window with debug variables and time traveling functionality
272fn show_debug_gui_with_history(frame_history: &mut DebugFrameHistory) {
273    use eframe::egui;
274    use std::sync::{Arc, Mutex};
275
276    if let Some(current_message) = frame_history.get_current_frame() {
277        let filename = std::path::Path::new(&current_message.file)
278            .file_name()
279            .and_then(|name| name.to_str())
280            .unwrap_or(&current_message.file);
281
282        let window_title = format!("[Mazer Debug] - {}:{}", filename, current_message.line);
283
284        let options = eframe::NativeOptions {
285            viewport: egui::ViewportBuilder::default()
286                .with_inner_size([900.0, 700.0])
287                .with_title(&window_title)
288                .with_resizable(true),
289            ..Default::default()
290        };
291
292        let frame_history_arc = Arc::new(Mutex::new(frame_history.clone()));
293        let frame_history_gui = frame_history_arc.clone();
294        let show_backtrace = Arc::new(Mutex::new(false));
295        let show_backtrace_gui = show_backtrace.clone();
296        
297        let _ = eframe::run_simple_native(&window_title, options, move |ctx, _frame| {
298            let mut style = (*ctx.style()).clone();
299            style.text_styles = [
300                (egui::TextStyle::Heading, egui::FontId::proportional(20.0)),
301                (egui::TextStyle::Body, egui::FontId::proportional(24.0)),
302                (egui::TextStyle::Monospace, egui::FontId::monospace(22.0)),
303                (egui::TextStyle::Button, egui::FontId::proportional(24.0)),
304                (egui::TextStyle::Small, egui::FontId::proportional(18.0)),
305            ]
306            .into();
307            style.wrap_mode = Some(egui::TextWrapMode::Wrap);
308            if style.visuals.dark_mode {
309                style.visuals.override_text_color = Some(egui::Color32::WHITE);
310            }
311            ctx.set_style(style.clone());
312
313            egui::CentralPanel::default().show(ctx, |ui| {
314                let mut frame_history_guard = frame_history_gui.lock().unwrap();
315                
316                // Top panel for navigation and copy button
317                egui::TopBottomPanel::top("top_panel").show_inside(ui, |ui| {
318                    ui.horizontal(|ui| {
319                        // Time traveling controls on the left
320                        ui.with_layout(egui::Layout::left_to_right(egui::Align::Center), |ui| {
321                            let can_go_back = frame_history_guard.can_go_backward();
322                            let can_go_forward = frame_history_guard.can_go_forward();
323                            
324                            ui.add_enabled_ui(can_go_back, |ui| {
325                                if ui.button("⬅ Previous").clicked() {
326                                    frame_history_guard.go_backward();
327                                }
328                            });
329                            
330                            ui.add_enabled_ui(can_go_forward, |ui| {
331                                if ui.button("Next ➡").clicked() {
332                                    frame_history_guard.go_forward();
333                                }
334                            });
335
336                            let (current_pos, total_frames) = frame_history_guard.get_position_info();
337                            ui.label(format!("Frame {}/{}", current_pos, total_frames));
338                        });
339
340                        // Copy button and backtrace button on the right
341                        ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
342                            if let Some(current_frame) = frame_history_guard.get_current_frame() {
343                                if ui.button("📋").clicked() {
344                                    let json_string = create_json_from_variables(&current_frame.variables);
345                                    if let Err(e) = copy_to_clipboard(&json_string) {
346                                        eprintln!("Failed to copy to clipboard: {}", e);
347                                    }
348                                }
349                                
350                                if ui.button("📋 Backtrace").clicked() {
351                                    *show_backtrace_gui.lock().unwrap() = true;
352                                }
353                            }
354                        });
355                    });
356                });
357
358                // Main content area
359                egui::CentralPanel::default().show_inside(ui, |ui| {
360                    if let Some(current_frame) = frame_history_guard.get_current_frame() {
361                        // Show current frame info
362                        ui.horizontal(|ui| {
363                            ui.strong("File:");
364                            ui.label(&current_frame.file);
365                            ui.strong("Line:");
366                            ui.label(current_frame.line.to_string());
367                            ui.strong("Column:");
368                            ui.label(current_frame.column.to_string());
369                        });
370                        ui.separator();
371
372                        egui::ScrollArea::vertical()
373                            .max_width(f32::INFINITY)
374                            .auto_shrink([false; 2])
375                            .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysVisible)
376                            .show(ui, |ui| {
377                                ui.allocate_ui_with_layout(
378                                    ui.available_size(),
379                                    egui::Layout::left_to_right(egui::Align::Min),
380                                    |ui| {
381                                        egui::Grid::new("debug_table")
382                                            .num_columns(2)
383                                            .spacing([40.0, 4.0])
384                                            .striped(true)
385                                            .min_col_width(ui.available_width() / 2.0)
386                                            .show(ui, |ui| {
387                                                ui.strong("Name (Type, Size)");
388                                                ui.strong("Value");
389                                                ui.end_row();
390
391                                                // Table rows
392                                                use egui_extras::syntax_highlighting::{
393                                                    CodeTheme, code_view_ui,
394                                                };
395                                                let theme = CodeTheme::from_style(&style);
396                                                for (name, var_frame) in &current_frame.variables {
397                                                    // Format the name with type and size information
398                                                    let size_info = if let Some(size) = var_frame.size_hint {
399                                                        format!("{} bytes", size)
400                                                    } else {
401                                                        "unknown".to_string()
402                                                    };
403
404                                                    let name_with_info = format!("{}\n\n\t{}\n\t{}\n",
405                                                        name, 
406                                                        size_info,
407                                                        var_frame.type_name, 
408                                                    );
409
410                                                    ui.label(name_with_info);
411                                                    code_view_ui(ui, &theme, &var_frame.value, "rs");
412                                                    ui.end_row();
413                                                }
414                                            });
415                                    },
416                                );
417                            });
418                    }
419                });
420            });
421
422            // Backtrace window
423            let mut show_backtrace_state = show_backtrace_gui.lock().unwrap();
424            if *show_backtrace_state {
425                let mut open = true;
426                egui::Window::new("🔍 Call Stack / Backtrace")
427                    .open(&mut open)
428                    .default_width(800.0)
429                    .default_height(600.0)
430                    .resizable(true)
431                    .collapsible(false)
432                    .show(ctx, |ui| {
433                        let frame_history_guard = frame_history_gui.lock().unwrap();
434                        if let Some(current_frame) = frame_history_guard.get_current_frame() {
435                            ui.horizontal(|ui| {
436                                ui.strong("Backtrace for:");
437                                ui.label(format!("{}:{}", current_frame.file, current_frame.line));
438                                ui.separator();
439                                if ui.button("📋 Copy").clicked() {
440                                    if let Err(e) = copy_to_clipboard(&current_frame.backtrace) {
441                                        eprintln!("Failed to copy backtrace to clipboard: {}", e);
442                                    }
443                                }
444                            });
445                            ui.separator();
446
447                            egui::ScrollArea::vertical()
448                                .max_width(f32::INFINITY)
449                                .auto_shrink([false; 2])
450                                .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysVisible)
451                                .show(ui, |ui| {
452                                    // Format and display the backtrace nicely
453                                    let backtrace_lines: Vec<&str> = current_frame.backtrace.lines().collect();
454                                    
455                                    for line in backtrace_lines.iter() {
456                                        let line = line.trim();
457                                        if line.is_empty() {
458                                            continue;
459                                        }
460
461                                        ui.horizontal(|ui| {
462                                            // Determine line color and styling based on content
463                                            if line.contains("::") && (line.contains(".rs:") || line.contains("src/")) {
464                                                // This looks like a Rust function call with file info
465                                                ui.colored_label(egui::Color32::LIGHT_BLUE, line);
466                                            } else if line.starts_with("at ") {
467                                                // File location line
468                                                ui.colored_label(egui::Color32::LIGHT_GREEN, line);
469                                            } else if line.contains("libstd") || line.contains("libcore") || line.contains("liballoc") {
470                                                // Standard library calls
471                                                ui.colored_label(egui::Color32::GRAY, line);
472                                            } else {
473                                                // Default formatting
474                                                ui.label(line);
475                                            }
476                                        });
477                                    }
478                                    
479                                    // If backtrace is empty or doesn't contain useful info
480                                    if backtrace_lines.is_empty() || backtrace_lines.len() < 2 {
481                                        ui.colored_label(egui::Color32::YELLOW, "⚠ Backtrace information not available or limited");
482                                        ui.label("Try enabling debug symbols or running in debug mode for more detailed backtraces.");
483                                    }
484                                });
485                        }
486                    });
487                
488                if !open {
489                    *show_backtrace_state = false;
490                }
491            }
492        });
493
494        // Update the original frame_history with any navigation changes
495        if let Ok(updated_history) = frame_history_arc.lock() {
496            *frame_history = updated_history.clone();
497        }
498    }
499}
500
501#[cfg(debug_assertions)]
502pub fn ensure_init() {
503    START.call_once(|| {
504        init(); 
505    });
506}
507
508
509static START: Once = Once::new();
510
511#[derive(Serialize, Deserialize, Debug, Clone)]
512pub struct VariableDebugFrame {
513    pub name: String,
514    pub value: String,
515    pub type_name: String,
516    pub size_hint: Option<usize>,
517}
518
519#[macro_export]
520macro_rules! inspect {
521    ( $( $var:expr ),+ $(,)? ) => {{
522        #[cfg(debug_assertions)]
523        {
524            $crate::ensure_init();
525            use std::collections::BTreeMap;
526            let mut map = BTreeMap::new();
527            $(
528            let var_name = stringify!($var).to_string();
529
530            let type_name = std::any::type_name_of_val(&$var).to_string();
531            let size_hint = std::mem::size_of_val(&$var);
532
533            let vframe = $crate::VariableDebugFrame {
534                name: var_name.clone(),
535                value: format!("{:#?}", $var),
536                type_name: type_name.clone(),
537                size_hint: Some(size_hint),
538            };
539
540                map.insert(var_name, vframe);
541            )+
542
543            let bt = std::backtrace::Backtrace::force_capture();
544
545            // Send to debug server and wait for GUI to be closed (blocking)
546            $crate::send_to_debug_server_and_wait(
547                map.clone(),
548                file!(),
549                line!(),
550                column!(),
551                format!("{}", bt)
552            );
553
554            map
555        }
556    }};
557}
558
559#[macro_export]
560/// A conditional version of inspect! that only inspects if the condition is true
561/// Does have the runtime cost of evaluating the condition, but avoids inspecting 
562/// variables when not needed
563macro_rules! inspect_when {
564    ($condition:expr, $( $var:expr ),+ $(,)? ) => {{
565        #[cfg(debug_assertions)]
566        {
567            if $condition {
568                $crate::inspect!($($var),+);
569            }
570        }
571    }};
572}