Skip to main content

foundry_debugger/tui/
mod.rs

1//! The debugger TUI.
2
3use eyre::Result;
4use foundry_tui::{TuiFallbackReason, TuiMode, run_app_if_interactive, tui_mode};
5
6mod context;
7use crate::debugger::DebuggerContext;
8use context::TUIContext;
9
10mod draw;
11
12/// Debugger exit reason.
13#[derive(Debug)]
14pub enum ExitReason {
15    /// Exit using 'q'.
16    CharExit,
17}
18
19/// The debugger TUI.
20pub struct TUI<'a> {
21    debugger_context: &'a mut DebuggerContext,
22}
23
24impl<'a> TUI<'a> {
25    /// Creates a new debugger.
26    pub const fn new(debugger_context: &'a mut DebuggerContext) -> Self {
27        Self { debugger_context }
28    }
29
30    /// Starts the debugger TUI.
31    pub fn try_run(&mut self) -> Result<ExitReason> {
32        self.run_inner()
33    }
34
35    #[instrument(target = "debugger", name = "run", skip_all, ret)]
36    fn run_inner(&mut self) -> Result<ExitReason> {
37        let mut cx = TUIContext::new(self.debugger_context);
38        cx.init();
39        match run_app_if_interactive(&mut cx)? {
40            Some(exit_reason) => Ok(exit_reason),
41            None => {
42                let message = match tui_mode() {
43                    TuiMode::Fallback(reason) => non_interactive_debugger_message(reason),
44                    TuiMode::Interactive => String::from(
45                        "Cannot open the debugger TUI in this environment. Re-run in an \
46                         interactive terminal.",
47                    ),
48                };
49                eyre::bail!("{message} {}", debugger_dump_hint());
50            }
51        }
52    }
53}
54
55fn non_interactive_debugger_message(reason: TuiFallbackReason) -> String {
56    format!(
57        "Cannot open the debugger TUI because {}. Re-run in an interactive terminal.",
58        reason.as_str()
59    )
60}
61
62const fn debugger_dump_hint() -> &'static str {
63    "Pass `--dump <PATH>` to export debugger steps."
64}
65
66#[cfg(test)]
67mod tests {
68    use super::{TuiFallbackReason, debugger_dump_hint, non_interactive_debugger_message};
69    use crate::{DebugNode, Debugger};
70    use std::{env, ffi::OsString};
71
72    struct EnvVarGuard {
73        key: &'static str,
74        previous: Option<OsString>,
75    }
76
77    impl EnvVarGuard {
78        fn set(key: &'static str, value: &str) -> Self {
79            let previous = env::var_os(key);
80            unsafe { env::set_var(key, value) };
81            Self { key, previous }
82        }
83    }
84
85    impl Drop for EnvVarGuard {
86        fn drop(&mut self) {
87            unsafe {
88                match &self.previous {
89                    Some(value) => env::set_var(self.key, value),
90                    None => env::remove_var(self.key),
91                }
92            }
93        }
94    }
95
96    #[test]
97    fn fallback_message_includes_reason() {
98        let msg = non_interactive_debugger_message(TuiFallbackReason::Ci);
99        assert!(msg.contains("running in CI"));
100        assert!(!msg.contains("--dump <PATH>"));
101    }
102
103    #[test]
104    fn dump_hint_includes_dump_flag() {
105        assert!(debugger_dump_hint().contains("--dump <PATH>"));
106    }
107
108    #[test]
109    fn debugger_tui_falls_back_in_ci_with_dump_hint() {
110        let _ci = EnvVarGuard::set("CI", "1");
111        let mut debugger = Debugger::new(
112            vec![DebugNode::default()],
113            Default::default(),
114            Default::default(),
115            Default::default(),
116        );
117
118        let message = debugger.try_run_tui().unwrap_err().to_string();
119
120        assert!(message.contains("running in CI"));
121        assert!(message.contains("--dump <PATH>"));
122    }
123}