Skip to main content

edb_tui/
lib.rs

1// EDB - Ethereum Debugger
2// Copyright (C) 2024 Zhuo Zhang and Wuqi Zhang
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU Affero General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU Affero General Public License for more details.
13//
14// You should have received a copy of the GNU Affero General Public License
15// along with this program. If not, see <https://www.gnu.org/licenses/>.
16
17// Copyright (C) 2024 Zhuo Zhang and Wuqi Zhang
18// SPDX-License-Identifier: AGPL-3.0
19//! Terminal User Interface for EDB
20//!
21//! This crate provides a terminal-based interface for interacting with the EDB engine.
22
23mod app;
24mod config;
25mod data;
26mod layout;
27mod panels;
28mod rpc;
29mod ui;
30
31pub use app::App;
32pub use config::Config;
33pub use layout::{LayoutConfig, LayoutManager, LayoutType};
34pub use panels::EventResponse;
35pub use rpc::RpcClient;
36pub use ui::{
37    BorderPresets, BreakpointStatus, ColorScheme, ConnectionStatus, EnhancedBorder,
38    ExecutionStatus, FileStatus, Icons, PanelStatus, RpcStatus, Spinner, SpinnerAnimation,
39    SpinnerStyles, StatusBar, Theme,
40};
41
42use crossterm::{
43    event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream},
44    execute,
45    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
46};
47use eyre::Result;
48use futures::StreamExt;
49use ratatui::{backend::CrosstermBackend, Terminal};
50use std::{io, sync::Arc, time::Duration};
51use tokio::{select, time::interval};
52use tracing::{debug, error, info, warn};
53
54/// Configuration for the TUI
55#[derive(Debug, Clone)]
56pub struct TuiConfig {
57    /// RPC endpoint URL
58    pub rpc_url: String,
59    /// Terminal refresh interval
60    pub refresh_interval: Duration,
61    /// Data fetch interval
62    pub data_fetch_interval: Duration,
63    /// Enable mouse support
64    pub enable_mouse: bool,
65}
66
67impl Default for TuiConfig {
68    fn default() -> Self {
69        Self {
70            rpc_url: "http://localhost:3030".to_string(),
71            refresh_interval: Duration::from_millis(50),
72            data_fetch_interval: Duration::from_millis(200),
73            enable_mouse: false,
74        }
75    }
76}
77
78/// Main TUI runner that manages the terminal interface and event loop
79pub struct Tui {
80    /// The main application state and panel management
81    app: App,
82    /// Terminal backend for rendering and input handling
83    terminal: Terminal<CrosstermBackend<io::Stdout>>,
84    /// Configuration settings for the TUI behavior
85    config: TuiConfig,
86}
87
88impl Tui {
89    /// Create a new TUI instance
90    pub async fn new(config: TuiConfig) -> Result<Self> {
91        info!("Initializing TUI with config: {:?}", config);
92
93        // Setup terminal
94        enable_raw_mode()?;
95        let mut stdout = io::stdout();
96        if config.enable_mouse {
97            execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
98        } else {
99            execute!(stdout, EnterAlternateScreen)?;
100        }
101
102        let backend = CrosstermBackend::new(stdout);
103        let terminal = Terminal::new(backend)?;
104
105        // Create RPC client
106        let rpc_client = Arc::new(RpcClient::new(&config.rpc_url).await?);
107
108        // Create app with layout manager
109        let layout_config = LayoutConfig { enable_mouse: config.enable_mouse };
110        let app = App::new(rpc_client, layout_config).await?;
111
112        Ok(Self { app, terminal, config })
113    }
114
115    /// Run the main TUI event loop
116    pub async fn run(mut self) -> Result<()> {
117        info!("Starting TUI event loop");
118
119        // Create DataManager
120        let mut data_manager = crate::data::DataManager::new(self.app.rpc_client.clone()).await?;
121
122        // Get cores for background processing
123        let exec_core = data_manager.get_execution_core();
124        let resolver_core = data_manager.get_resolver_core();
125
126        // Spawn background task for execution core processing
127        let exec_handle = tokio::spawn(async move {
128            let mut interval = tokio::time::interval(self.config.data_fetch_interval);
129            loop {
130                interval.tick().await;
131                let mut core = exec_core.write().await;
132                if let Err(e) = core.process_pending_requests().await {
133                    error!("Error processing execution requests: {}", e);
134                }
135            }
136        });
137
138        // Spawn background task for resolver core processing
139        let resolver_handle = tokio::spawn(async move {
140            let mut interval = tokio::time::interval(self.config.data_fetch_interval);
141            loop {
142                interval.tick().await;
143                let mut core = resolver_core.write().await;
144                if let Err(e) = core.process_pending_requests().await {
145                    error!("Error processing resolver requests: {}", e);
146                }
147            }
148        });
149
150        let mut event_stream = EventStream::new();
151        let mut ticker = interval(self.config.refresh_interval);
152
153        let result = loop {
154            // Render current state
155            let render_result = self.terminal.draw(|frame| {
156                self.app.render(frame, &mut data_manager);
157            });
158
159            if let Err(e) = render_result {
160                break Err(e.into());
161            }
162
163            // Handle events
164            select! {
165                // Handle terminal events (keyboard, mouse, resize)
166                event_result = event_stream.next() => {
167                    if let Some(Ok(event)) = event_result {
168                        debug!("Received event: {:?}", event);
169
170                        match event {
171                            Event::Key(key_event) => {
172                                match self.app.handle_key_event(key_event, &mut data_manager).await? {
173                                    EventResponse::Exit => {
174                                        info!("Exit requested");
175                                        break Ok(());
176                                    }
177                                    EventResponse::Handled => {},
178                                    EventResponse::NotHandled => {
179                                        warn!("Unhandled key event: {:?}", key_event);
180                                    }
181                                    EventResponse::ChangeFocus(panel_type) => {
182                                        // Handle panel focus changes
183                                        debug!("Focus change requested to {:?}", panel_type);
184                                        self.app.change_focus(panel_type);
185                                    }
186                                }
187                            }
188                            Event::Mouse(mouse_event) if self.config.enable_mouse => {
189                                if let Err(e) = self.app.handle_mouse_event(mouse_event, &mut data_manager).await {
190                                    error!("Mouse event error: {}", e);
191                                }
192                            }
193                            Event::Resize(width, height) => {
194                                debug!("Terminal resized: {}x{}", width, height);
195                                self.app.handle_resize(width, height);
196                            }
197                            _ => {}
198                        }
199                    }
200                }
201
202                // Periodic refresh tick
203                _ = ticker.tick() => {
204                    // Update app state periodically
205                    if let Err(e) = self.app.update().await {
206                        error!("App update error: {}", e);
207                    }
208
209                    // Pull updates from cores (the first time we try to get more cached data)
210                    data_manager.process_core_updates()?;
211
212                    // Push pending requests to cores
213                    data_manager.update_pending_requests().await?;
214
215                    // Pull updates from cores
216                    data_manager.process_core_updates()?;
217                }
218            }
219
220            // Check if app wants to exit
221            if self.app.should_exit() {
222                info!("App requested exit");
223                break Ok(());
224            }
225        };
226
227        // Abort background tasks
228        exec_handle.abort();
229        resolver_handle.abort();
230
231        info!("TUI event loop ended");
232        result
233    }
234}
235
236impl Drop for Tui {
237    fn drop(&mut self) {
238        // Restore terminal state
239        let _ = disable_raw_mode();
240        if self.config.enable_mouse {
241            let _ =
242                execute!(self.terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture);
243        } else {
244            let _ = execute!(self.terminal.backend_mut(), LeaveAlternateScreen);
245        }
246        let _ = self.terminal.show_cursor();
247    }
248}
249
250/// Public API for the TUI module
251pub mod api {
252    use super::*;
253
254    /// Start the TUI with the given configuration
255    pub async fn start_tui(config: TuiConfig) -> Result<()> {
256        let tui = Tui::new(config).await?;
257        tui.run().await
258    }
259
260    /// Start the TUI with default configuration
261    pub async fn start_default_tui() -> Result<()> {
262        start_tui(TuiConfig::default()).await
263    }
264
265    /// Start the TUI with auto-detected RPC port
266    pub async fn start_auto_tui() -> Result<()> {
267        // Try to detect RPC server port
268        let mut config = TuiConfig::default();
269
270        // Try common ports
271        for port in [3030, 8545, 8546, 9944] {
272            let url = format!("http://localhost:{port}");
273            if RpcClient::test_connection(&url).await.is_ok() {
274                config.rpc_url = url;
275                break;
276            }
277        }
278
279        start_tui(config).await
280    }
281}