foundry_debugger/tui/
mod.rs
1use crossterm::{
4 event::{self, DisableMouseCapture, EnableMouseCapture, Event},
5 execute,
6 terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
7};
8use eyre::Result;
9use ratatui::{
10 backend::{Backend, CrosstermBackend},
11 Terminal,
12};
13use std::{
14 io,
15 ops::ControlFlow,
16 sync::{mpsc, Arc},
17 thread,
18 time::{Duration, Instant},
19};
20
21mod context;
22use crate::debugger::DebuggerContext;
23use context::TUIContext;
24
25mod draw;
26
27type DebuggerTerminal = Terminal<CrosstermBackend<io::Stdout>>;
28
29#[derive(Debug)]
31pub enum ExitReason {
32 CharExit,
34}
35
36pub struct TUI<'a> {
38 debugger_context: &'a mut DebuggerContext,
39}
40
41impl<'a> TUI<'a> {
42 pub fn new(debugger_context: &'a mut DebuggerContext) -> Self {
44 Self { debugger_context }
45 }
46
47 pub fn try_run(&mut self) -> Result<ExitReason> {
49 let backend = CrosstermBackend::new(io::stdout());
50 let terminal = Terminal::new(backend)?;
51 TerminalGuard::with(terminal, |terminal| self.try_run_real(terminal))
52 }
53
54 #[instrument(target = "debugger", name = "run", skip_all, ret)]
55 fn try_run_real(&mut self, terminal: &mut DebuggerTerminal) -> Result<ExitReason> {
56 let mut cx = TUIContext::new(self.debugger_context);
58
59 cx.init();
60
61 let (tx, rx) = mpsc::channel();
63 thread::Builder::new()
64 .name("event-listener".into())
65 .spawn(move || Self::event_listener(tx))
66 .expect("failed to spawn thread");
67
68 loop {
70 cx.draw(terminal)?;
71 match cx.handle_event(rx.recv()?) {
72 ControlFlow::Continue(()) => {}
73 ControlFlow::Break(reason) => return Ok(reason),
74 }
75 }
76 }
77
78 fn event_listener(tx: mpsc::Sender<Event>) {
79 let tick_rate = Duration::from_millis(200);
81
82 let mut last_tick = Instant::now();
83 loop {
84 if event::poll(tick_rate.saturating_sub(last_tick.elapsed())).unwrap() {
88 let event = event::read().unwrap();
89 if tx.send(event).is_err() {
90 return;
91 }
92 }
93
94 if last_tick.elapsed() > tick_rate {
96 last_tick = Instant::now();
97 }
98 }
99 }
100}
101
102#[expect(deprecated)]
104type PanicHandler = Box<dyn Fn(&std::panic::PanicInfo<'_>) + 'static + Sync + Send>;
105
106#[must_use]
108struct TerminalGuard<B: Backend + io::Write> {
109 terminal: Terminal<B>,
110 hook: Option<Arc<PanicHandler>>,
111}
112
113impl<B: Backend + io::Write> TerminalGuard<B> {
114 fn with<T>(terminal: Terminal<B>, mut f: impl FnMut(&mut Terminal<B>) -> T) -> T {
115 let mut guard = Self { terminal, hook: None };
116 guard.setup();
117 f(&mut guard.terminal)
118 }
119
120 fn setup(&mut self) {
121 let previous = Arc::new(std::panic::take_hook());
122 self.hook = Some(previous.clone());
123 std::panic::set_hook(Box::new(move |info| {
126 Self::half_restore(&mut std::io::stdout());
127 (previous)(info)
128 }));
129
130 let _ = enable_raw_mode();
131 let _ = execute!(*self.terminal.backend_mut(), EnterAlternateScreen, EnableMouseCapture);
132 let _ = self.terminal.hide_cursor();
133 let _ = self.terminal.clear();
134 }
135
136 fn restore(&mut self) {
137 if !std::thread::panicking() {
138 let _ = std::panic::take_hook();
140 let prev = self.hook.take().unwrap();
142 let prev = match Arc::try_unwrap(prev) {
143 Ok(prev) => prev,
144 Err(_) => unreachable!("`self.hook` is not the only reference to the panic hook"),
145 };
146 std::panic::set_hook(prev);
147
148 Self::half_restore(self.terminal.backend_mut());
151 }
152
153 let _ = self.terminal.show_cursor();
154 }
155
156 fn half_restore(w: &mut impl io::Write) {
157 let _ = disable_raw_mode();
158 let _ = execute!(*w, LeaveAlternateScreen, DisableMouseCapture);
159 }
160}
161
162impl<B: Backend + io::Write> Drop for TerminalGuard<B> {
163 #[inline]
164 fn drop(&mut self) {
165 self.restore();
166 }
167}