foundry_debugger/tui/
mod.rsuse crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use eyre::Result;
use ratatui::{
backend::{Backend, CrosstermBackend},
Terminal,
};
use std::{
io,
ops::ControlFlow,
sync::{mpsc, Arc},
thread,
time::{Duration, Instant},
};
mod context;
use crate::debugger::DebuggerContext;
use context::TUIContext;
mod draw;
type DebuggerTerminal = Terminal<CrosstermBackend<io::Stdout>>;
#[derive(Debug)]
pub enum ExitReason {
CharExit,
}
pub struct TUI<'a> {
debugger_context: &'a mut DebuggerContext,
}
impl<'a> TUI<'a> {
pub fn new(debugger_context: &'a mut DebuggerContext) -> Self {
Self { debugger_context }
}
pub fn try_run(&mut self) -> Result<ExitReason> {
let backend = CrosstermBackend::new(io::stdout());
let terminal = Terminal::new(backend)?;
TerminalGuard::with(terminal, |terminal| self.try_run_real(terminal))
}
#[instrument(target = "debugger", name = "run", skip_all, ret)]
fn try_run_real(&mut self, terminal: &mut DebuggerTerminal) -> Result<ExitReason> {
let mut cx = TUIContext::new(self.debugger_context);
cx.init();
let (tx, rx) = mpsc::channel();
thread::Builder::new()
.name("event-listener".into())
.spawn(move || Self::event_listener(tx))
.expect("failed to spawn thread");
loop {
cx.draw(terminal)?;
match cx.handle_event(rx.recv()?) {
ControlFlow::Continue(()) => {}
ControlFlow::Break(reason) => return Ok(reason),
}
}
}
fn event_listener(tx: mpsc::Sender<Event>) {
let tick_rate = Duration::from_millis(200);
let mut last_tick = Instant::now();
loop {
if event::poll(tick_rate.saturating_sub(last_tick.elapsed())).unwrap() {
let event = event::read().unwrap();
if tx.send(event).is_err() {
return;
}
}
if last_tick.elapsed() > tick_rate {
last_tick = Instant::now();
}
}
}
}
#[allow(deprecated)]
type PanicHandler = Box<dyn Fn(&std::panic::PanicInfo<'_>) + 'static + Sync + Send>;
#[must_use]
struct TerminalGuard<B: Backend + io::Write> {
terminal: Terminal<B>,
hook: Option<Arc<PanicHandler>>,
}
impl<B: Backend + io::Write> TerminalGuard<B> {
fn with<T>(terminal: Terminal<B>, mut f: impl FnMut(&mut Terminal<B>) -> T) -> T {
let mut guard = Self { terminal, hook: None };
guard.setup();
f(&mut guard.terminal)
}
fn setup(&mut self) {
let previous = Arc::new(std::panic::take_hook());
self.hook = Some(previous.clone());
std::panic::set_hook(Box::new(move |info| {
Self::half_restore(&mut std::io::stdout());
(previous)(info)
}));
let _ = enable_raw_mode();
let _ = execute!(*self.terminal.backend_mut(), EnterAlternateScreen, EnableMouseCapture);
let _ = self.terminal.hide_cursor();
let _ = self.terminal.clear();
}
fn restore(&mut self) {
if !std::thread::panicking() {
let _ = std::panic::take_hook();
let prev = self.hook.take().unwrap();
let prev = match Arc::try_unwrap(prev) {
Ok(prev) => prev,
Err(_) => unreachable!("`self.hook` is not the only reference to the panic hook"),
};
std::panic::set_hook(prev);
Self::half_restore(self.terminal.backend_mut());
}
let _ = self.terminal.show_cursor();
}
fn half_restore(w: &mut impl io::Write) {
let _ = disable_raw_mode();
let _ = execute!(*w, LeaveAlternateScreen, DisableMouseCapture);
}
}
impl<B: Backend + io::Write> Drop for TerminalGuard<B> {
#[inline]
fn drop(&mut self) {
self.restore();
}
}