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