1mod 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#[derive(Debug, Clone)]
56pub struct TuiConfig {
57 pub rpc_url: String,
59 pub refresh_interval: Duration,
61 pub data_fetch_interval: Duration,
63 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
78pub struct Tui {
80 app: App,
82 terminal: Terminal<CrosstermBackend<io::Stdout>>,
84 config: TuiConfig,
86}
87
88impl Tui {
89 pub async fn new(config: TuiConfig) -> Result<Self> {
91 info!("Initializing TUI with config: {:?}", config);
92
93 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 let rpc_client = Arc::new(RpcClient::new(&config.rpc_url).await?);
107
108 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 pub async fn run(mut self) -> Result<()> {
117 info!("Starting TUI event loop");
118
119 let mut data_manager = crate::data::DataManager::new(self.app.rpc_client.clone()).await?;
121
122 let exec_core = data_manager.get_execution_core();
124 let resolver_core = data_manager.get_resolver_core();
125
126 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 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 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 select! {
165 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 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 _ = ticker.tick() => {
204 if let Err(e) = self.app.update().await {
206 error!("App update error: {}", e);
207 }
208
209 data_manager.process_core_updates()?;
211
212 data_manager.update_pending_requests().await?;
214
215 data_manager.process_core_updates()?;
217 }
218 }
219
220 if self.app.should_exit() {
222 info!("App requested exit");
223 break Ok(());
224 }
225 };
226
227 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 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
250pub mod api {
252 use super::*;
253
254 pub async fn start_tui(config: TuiConfig) -> Result<()> {
256 let tui = Tui::new(config).await?;
257 tui.run().await
258 }
259
260 pub async fn start_default_tui() -> Result<()> {
262 start_tui(TuiConfig::default()).await
263 }
264
265 pub async fn start_auto_tui() -> Result<()> {
267 let mut config = TuiConfig::default();
269
270 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}