foundry_debugger/tui/
mod.rs

1//! The debugger TUI.
2
3use crossterm::{
4    event::{self, DisableMouseCapture, EnableMouseCapture},
5    execute,
6    terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
7};
8use eyre::Result;
9use ratatui::{
10    Terminal,
11    backend::{Backend, CrosstermBackend},
12};
13use std::{io, ops::ControlFlow, sync::Arc};
14
15mod context;
16use crate::debugger::DebuggerContext;
17use context::TUIContext;
18
19mod draw;
20
21type DebuggerTerminal = Terminal<CrosstermBackend<io::Stdout>>;
22
23/// Debugger exit reason.
24#[derive(Debug)]
25pub enum ExitReason {
26    /// Exit using 'q'.
27    CharExit,
28}
29
30/// The debugger TUI.
31pub struct TUI<'a> {
32    debugger_context: &'a mut DebuggerContext,
33}
34
35impl<'a> TUI<'a> {
36    /// Creates a new debugger.
37    pub fn new(debugger_context: &'a mut DebuggerContext) -> Self {
38        Self { debugger_context }
39    }
40
41    /// Starts the debugger TUI.
42    pub fn try_run(&mut self) -> Result<ExitReason> {
43        let backend = CrosstermBackend::new(io::stdout());
44        let terminal = Terminal::new(backend)?;
45        TerminalGuard::with(terminal, |terminal| self.run_inner(terminal))
46    }
47
48    #[instrument(target = "debugger", name = "run", skip_all, ret)]
49    fn run_inner(&mut self, terminal: &mut DebuggerTerminal) -> Result<ExitReason> {
50        let mut cx = TUIContext::new(self.debugger_context);
51        cx.init();
52        loop {
53            cx.draw(terminal)?;
54            match cx.handle_event(event::read()?) {
55                ControlFlow::Continue(()) => {}
56                ControlFlow::Break(reason) => return Ok(reason),
57            }
58        }
59    }
60}
61
62type PanicHandler = Box<dyn Fn(&std::panic::PanicHookInfo<'_>) + 'static + Sync + Send>;
63
64/// Handles terminal state.
65#[must_use]
66struct TerminalGuard<B: Backend + io::Write> {
67    terminal: Terminal<B>,
68    hook: Option<Arc<PanicHandler>>,
69}
70
71impl<B: Backend + io::Write> TerminalGuard<B> {
72    fn with<T>(terminal: Terminal<B>, mut f: impl FnMut(&mut Terminal<B>) -> T) -> T {
73        let mut guard = Self { terminal, hook: None };
74        guard.setup();
75        f(&mut guard.terminal)
76    }
77
78    fn setup(&mut self) {
79        let previous = Arc::new(std::panic::take_hook());
80        self.hook = Some(previous.clone());
81        // We need to restore the terminal state before displaying the panic message.
82        // TODO: Use `std::panic::update_hook` when it's stable
83        std::panic::set_hook(Box::new(move |info| {
84            Self::half_restore(&mut std::io::stdout());
85            (previous)(info)
86        }));
87
88        let _ = enable_raw_mode();
89        let _ = execute!(*self.terminal.backend_mut(), EnterAlternateScreen, EnableMouseCapture);
90        let _ = self.terminal.hide_cursor();
91        let _ = self.terminal.clear();
92    }
93
94    fn restore(&mut self) {
95        if !std::thread::panicking() {
96            // Drop the current hook to guarantee that `self.hook` is the only reference to it.
97            let _ = std::panic::take_hook();
98            // Restore the previous panic hook.
99            let prev = self.hook.take().unwrap();
100            let prev = match Arc::try_unwrap(prev) {
101                Ok(prev) => prev,
102                Err(_) => unreachable!("`self.hook` is not the only reference to the panic hook"),
103            };
104            std::panic::set_hook(prev);
105
106            // NOTE: Our panic handler calls this function, so we only have to call it here if we're
107            // not panicking.
108            Self::half_restore(self.terminal.backend_mut());
109        }
110
111        let _ = self.terminal.show_cursor();
112    }
113
114    fn half_restore(w: &mut impl io::Write) {
115        let _ = disable_raw_mode();
116        let _ = execute!(*w, LeaveAlternateScreen, DisableMouseCapture);
117    }
118}
119
120impl<B: Backend + io::Write> Drop for TerminalGuard<B> {
121    #[inline]
122    fn drop(&mut self) {
123        self.restore();
124    }
125}