foundry_debugger/tui/
mod.rs1use 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#[derive(Debug)]
25pub enum ExitReason {
26 CharExit,
28}
29
30pub struct TUI<'a> {
32 debugger_context: &'a mut DebuggerContext,
33}
34
35impl<'a> TUI<'a> {
36 pub fn new(debugger_context: &'a mut DebuggerContext) -> Self {
38 Self { debugger_context }
39 }
40
41 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#[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 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 let _ = std::panic::take_hook();
98 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 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}