code_mesh_tui/
app.rs

1use anyhow::Result;
2use ratatui::{backend::CrosstermBackend, Terminal};
3use std::io;
4use tokio::time::{Duration, Instant};
5
6use crate::{
7    chat::ChatComponent,
8    components::{CommandPalette, Dialog, StatusBar},
9    config::Config,
10    events::{AppEvent, EventHandler, InputEvent, KeybindHandler, MouseHandler},
11    file_viewer::FileViewer,
12    layout::{LayoutManager, PopupLayout},
13    renderer::Renderer,
14    theme::ThemeManager,
15};
16
17/// Main application state
18pub struct App {
19    /// Application configuration
20    config: Config,
21    /// Theme manager
22    theme_manager: ThemeManager,
23    /// Event handler
24    event_handler: EventHandler,
25    /// Keybind handler
26    keybind_handler: KeybindHandler,
27    /// Mouse handler
28    mouse_handler: MouseHandler,
29    /// Layout manager
30    layout_manager: LayoutManager,
31    /// Chat component
32    chat: ChatComponent,
33    /// File viewer component
34    file_viewer: FileViewer,
35    /// Status bar component
36    status_bar: StatusBar,
37    /// Command palette
38    command_palette: CommandPalette,
39    /// Active dialog
40    active_dialog: Option<Dialog>,
41    /// Application state
42    state: AppState,
43    /// Last render time
44    last_render: Instant,
45    /// Frame rate target
46    target_fps: u64,
47}
48
49/// Application state enumeration
50#[derive(Debug, Clone, PartialEq)]
51pub enum AppState {
52    /// Normal operation
53    Running,
54    /// Application should quit
55    Quitting,
56    /// Showing help
57    Help,
58    /// Command palette is open
59    CommandPalette,
60    /// Dialog is open
61    Dialog,
62    /// File viewer is focused
63    FileViewer,
64    /// Chat is focused
65    Chat,
66}
67
68impl App {
69    /// Create a new application instance
70    pub async fn new(config: Config) -> Result<Self> {
71        let theme_manager = ThemeManager::default();
72        let event_handler = EventHandler::new();
73        let mut keybind_handler = KeybindHandler::new();
74        
75        // Setup keybindings from config
76        keybind_handler.set_leader_key(config.keybinds.leader.clone());
77        for (action, key) in &config.keybinds.bindings {
78            keybind_handler.bind(key.clone(), action.clone());
79        }
80        
81        let mouse_handler = MouseHandler::new();
82        
83        // Initialize layout with default terminal size
84        let layout_manager = LayoutManager::new(ratatui::layout::Rect::new(0, 0, 80, 24));
85        
86        // Initialize components
87        let chat = ChatComponent::new(&config.chat, theme_manager.current_theme());
88        let file_viewer = FileViewer::new(&config.file_viewer, theme_manager.current_theme());
89        let status_bar = StatusBar::new(theme_manager.current_theme());
90        let command_palette = CommandPalette::new(theme_manager.current_theme());
91        
92        Ok(Self {
93            config,
94            theme_manager,
95            event_handler,
96            keybind_handler,
97            mouse_handler,
98            layout_manager,
99            chat,
100            file_viewer,
101            status_bar,
102            command_palette,
103            active_dialog: None,
104            state: AppState::Running,
105            last_render: Instant::now(),
106            target_fps: 60,
107        })
108    }
109    
110    /// Run the application main loop
111    pub async fn run(&mut self) -> Result<()> {
112        let mut terminal = crate::init_terminal()?;
113        
114        // Main application loop
115        while self.state != AppState::Quitting {
116            // Handle events
117            if let Some(event) = self.event_handler.try_next() {
118                self.handle_event(event).await?;
119            }
120            
121            // Render at target FPS
122            let now = Instant::now();
123            let frame_duration = Duration::from_millis(1000 / self.target_fps);
124            
125            if now.duration_since(self.last_render) >= frame_duration {
126                self.render(&mut terminal)?;
127                self.last_render = now;
128            }
129            
130            // Small sleep to prevent busy waiting
131            tokio::time::sleep(Duration::from_millis(1)).await;
132        }
133        
134        crate::restore_terminal(&mut terminal)?;
135        Ok(())
136    }
137    
138    /// Handle an application event
139    async fn handle_event(&mut self, event: AppEvent) -> Result<()> {
140        match event {
141            AppEvent::Input(input_event) => {
142                self.handle_input_event(input_event).await?;
143            }
144            AppEvent::Resize(width, height) => {
145                let new_area = ratatui::layout::Rect::new(0, 0, width, height);
146                self.layout_manager.resize(new_area);
147            }
148            AppEvent::Quit => {
149                self.state = AppState::Quitting;
150            }
151            AppEvent::Tick => {
152                // Handle periodic updates
153                self.update_components().await?;
154            }
155            AppEvent::Custom(message) => {
156                self.handle_custom_event(message).await?;
157            }
158        }
159        Ok(())
160    }
161    
162    /// Handle input events
163    async fn handle_input_event(&mut self, event: InputEvent) -> Result<()> {
164        match event {
165            InputEvent::Key(key_event) => {
166                // Check for global keybindings first
167                if let Some(action) = self.keybind_handler.handle_key(&key_event) {
168                    self.execute_action(&action).await?;
169                } else {
170                    // Route to active component
171                    self.route_key_event(key_event).await?;
172                }
173            }
174            InputEvent::Mouse(mouse_event) => {
175                let action = self.mouse_handler.handle_mouse(&mouse_event);
176                self.handle_mouse_action(action).await?;
177            }
178            InputEvent::Paste(data) => {
179                // Route paste to active component
180                if self.state == AppState::Chat {
181                    self.chat.handle_paste(data).await?;
182                }
183            }
184            InputEvent::FocusGained | InputEvent::FocusLost => {
185                // Handle focus changes if needed
186            }
187        }
188        Ok(())
189    }
190    
191    /// Execute a bound action
192    async fn execute_action(&mut self, action: &str) -> Result<()> {
193        match action {
194            "quit" => {
195                self.state = AppState::Quitting;
196            }
197            "help" => {
198                self.toggle_help();
199            }
200            "command_palette" => {
201                self.toggle_command_palette();
202            }
203            "send_message" => {
204                if self.state == AppState::Chat {
205                    self.chat.send_message().await?;
206                }
207            }
208            "new_line" => {
209                if self.state == AppState::Chat {
210                    self.chat.insert_newline();
211                }
212            }
213            "clear_input" => {
214                if self.state == AppState::Chat {
215                    self.chat.clear_input();
216                }
217            }
218            "open_file" => {
219                self.open_file_dialog();
220            }
221            "close_file" => {
222                self.file_viewer.close_file();
223                if self.state == AppState::FileViewer {
224                    self.state = AppState::Chat;
225                }
226            }
227            "toggle_diff" => {
228                self.file_viewer.toggle_diff_style();
229            }
230            "scroll_up" => {
231                self.handle_scroll(true).await?;
232            }
233            "scroll_down" => {
234                self.handle_scroll(false).await?;
235            }
236            "page_up" => {
237                self.handle_page_scroll(true).await?;
238            }
239            "page_down" => {
240                self.handle_page_scroll(false).await?;
241            }
242            _ => {
243                // Unknown action, ignore or log
244            }
245        }
246        Ok(())
247    }
248    
249    /// Route key events to the appropriate component
250    async fn route_key_event(&mut self, key_event: crossterm::event::KeyEvent) -> Result<()> {
251        match self.state {
252            AppState::Chat => {
253                self.chat.handle_key_event(key_event).await?;
254            }
255            AppState::FileViewer => {
256                self.file_viewer.handle_key_event(key_event).await?;
257            }
258            AppState::CommandPalette => {
259                if let Some(result) = self.command_palette.handle_key_event(key_event).await? {
260                    self.execute_command_palette_result(result).await?;
261                    self.state = AppState::Chat;
262                }
263            }
264            AppState::Dialog => {
265                if let Some(ref mut dialog) = self.active_dialog {
266                    if let Some(result) = dialog.handle_key_event(key_event).await? {
267                        self.handle_dialog_result(result).await?;
268                        self.active_dialog = None;
269                        self.state = AppState::Chat;
270                    }
271                }
272            }
273            _ => {}
274        }
275        Ok(())
276    }
277    
278    /// Handle mouse actions
279    async fn handle_mouse_action(&mut self, action: crate::events::MouseAction) -> Result<()> {
280        use crate::events::MouseAction;
281        
282        match action {
283            MouseAction::LeftClick(x, y) => {
284                // Determine which component was clicked
285                if self.layout_manager.main_area.intersects(ratatui::layout::Rect::new(x, y, 1, 1)) {
286                    if self.file_viewer.is_visible() {
287                        self.state = AppState::FileViewer;
288                    } else {
289                        self.state = AppState::Chat;
290                    }
291                }
292            }
293            MouseAction::ScrollUp(x, y) => {
294                if self.is_in_scrollable_area(x, y) {
295                    self.handle_scroll(true).await?;
296                }
297            }
298            MouseAction::ScrollDown(x, y) => {
299                if self.is_in_scrollable_area(x, y) {
300                    self.handle_scroll(false).await?;
301                }
302            }
303            _ => {}
304        }
305        Ok(())
306    }
307    
308    /// Update components periodically
309    async fn update_components(&mut self) -> Result<()> {
310        self.chat.update().await?;
311        self.file_viewer.update().await?;
312        self.status_bar.update(&self.state).await?;
313        Ok(())
314    }
315    
316    /// Handle custom application events
317    async fn handle_custom_event(&mut self, message: String) -> Result<()> {
318        // Parse and handle custom events
319        // This could be used for inter-component communication
320        match message.as_str() {
321            "theme_changed" => {
322                self.update_theme();
323            }
324            "file_opened" => {
325                self.state = AppState::FileViewer;
326            }
327            _ => {}
328        }
329        Ok(())
330    }
331    
332    /// Render the application
333    fn render(&mut self, terminal: &mut Terminal<CrosstermBackend<io::Stdout>>) -> Result<()> {
334        terminal.draw(|frame| {
335            let mut renderer = Renderer::new(frame, self.theme_manager.current_theme());
336            
337            // Render main layout
338            self.render_main_layout(&mut renderer);
339            
340            // Render popups/dialogs on top
341            self.render_overlays(&mut renderer);
342        })?;
343        Ok(())
344    }
345    
346    /// Render the main application layout
347    fn render_main_layout(&mut self, renderer: &mut Renderer) {
348        // Render status bar
349        self.status_bar.render(renderer, self.layout_manager.status_area);
350        
351        // Render main content area
352        if self.file_viewer.is_visible() {
353            // Show file viewer in side panel if available, or full area
354            if let Some(side_panel) = self.layout_manager.side_panel {
355                self.chat.render(renderer, self.layout_manager.main_area);
356                self.file_viewer.render(renderer, side_panel);
357            } else {
358                self.file_viewer.render(renderer, self.layout_manager.main_area);
359            }
360        } else {
361            self.chat.render(renderer, self.layout_manager.main_area);
362        }
363        
364        // Render input area
365        self.chat.render_input(renderer, self.layout_manager.input_area);
366    }
367    
368    /// Render overlay components like dialogs and command palette
369    fn render_overlays(&mut self, renderer: &mut Renderer) {
370        match self.state {
371            AppState::CommandPalette => {
372                let popup_area = PopupLayout::centered(
373                    self.layout_manager.terminal_area,
374                    60,
375                    15,
376                );
377                self.command_palette.render(renderer, popup_area);
378            }
379            AppState::Dialog => {
380                if let Some(ref mut dialog) = self.active_dialog {
381                    let popup_area = PopupLayout::centered(
382                        self.layout_manager.terminal_area,
383                        dialog.width(),
384                        dialog.height(),
385                    );
386                    dialog.render(renderer, popup_area);
387                }
388            }
389            AppState::Help => {
390                let popup_area = PopupLayout::percentage(
391                    self.layout_manager.terminal_area,
392                    80,
393                    80,
394                );
395                self.render_help(renderer, popup_area);
396            }
397            _ => {}
398        }
399    }
400    
401    /// Toggle help display
402    fn toggle_help(&mut self) {
403        self.state = if self.state == AppState::Help {
404            AppState::Chat
405        } else {
406            AppState::Help
407        };
408    }
409    
410    /// Toggle command palette
411    fn toggle_command_palette(&mut self) {
412        self.state = if self.state == AppState::CommandPalette {
413            AppState::Chat
414        } else {
415            AppState::CommandPalette
416        };
417    }
418    
419    /// Open file dialog
420    fn open_file_dialog(&mut self) {
421        // Implementation would create a file picker dialog
422        // For now, this is a placeholder
423    }
424    
425    /// Handle scrolling
426    async fn handle_scroll(&mut self, up: bool) -> Result<()> {
427        match self.state {
428            AppState::Chat => {
429                if up {
430                    self.chat.scroll_up();
431                } else {
432                    self.chat.scroll_down();
433                }
434            }
435            AppState::FileViewer => {
436                if up {
437                    self.file_viewer.scroll_up();
438                } else {
439                    self.file_viewer.scroll_down();
440                }
441            }
442            _ => {}
443        }
444        Ok(())
445    }
446    
447    /// Handle page scrolling
448    async fn handle_page_scroll(&mut self, up: bool) -> Result<()> {
449        match self.state {
450            AppState::Chat => {
451                if up {
452                    self.chat.page_up();
453                } else {
454                    self.chat.page_down();
455                }
456            }
457            AppState::FileViewer => {
458                if up {
459                    self.file_viewer.page_up();
460                } else {
461                    self.file_viewer.page_down();
462                }
463            }
464            _ => {}
465        }
466        Ok(())
467    }
468    
469    /// Check if coordinates are in a scrollable area
470    fn is_in_scrollable_area(&self, x: u16, y: u16) -> bool {
471        let point = ratatui::layout::Rect::new(x, y, 1, 1);
472        self.layout_manager.main_area.intersects(point) ||
473        self.layout_manager.side_panel.map_or(false, |area| area.intersects(point))
474    }
475    
476    /// Execute command palette result
477    async fn execute_command_palette_result(&mut self, result: String) -> Result<()> {
478        // Parse and execute command palette commands
479        match result.as_str() {
480            "open-file" => self.open_file_dialog(),
481            "toggle-theme" => self.cycle_theme(),
482            "clear-chat" => self.chat.clear().await?,
483            _ => {}
484        }
485        Ok(())
486    }
487    
488    /// Handle dialog result
489    async fn handle_dialog_result(&mut self, result: crate::components::DialogResult) -> Result<()> {
490        use crate::components::DialogResult;
491        
492        match result {
493            DialogResult::Confirmed(_data) => {
494                // Handle confirmed dialog with data
495            }
496            DialogResult::Cancelled => {
497                // Handle cancelled dialog
498            }
499        }
500        Ok(())
501    }
502    
503    /// Update theme for all components
504    fn update_theme(&mut self) {
505        let theme = self.theme_manager.current_theme();
506        self.chat.update_theme(theme);
507        self.file_viewer.update_theme(theme);
508        self.status_bar.update_theme(theme);
509        self.command_palette.update_theme(theme);
510    }
511    
512    /// Cycle through available themes
513    fn cycle_theme(&mut self) {
514        let themes = self.theme_manager.available_themes();
515        if !themes.is_empty() {
516            let current_name = self.theme_manager.current_theme().name();
517            let current_index = themes.iter().position(|name| name == current_name).unwrap_or(0);
518            let next_index = (current_index + 1) % themes.len();
519            let next_theme = &themes[next_index];
520            
521            if let Err(e) = self.theme_manager.set_theme(next_theme) {
522                eprintln!("Failed to set theme {}: {}", next_theme, e);
523            } else {
524                self.update_theme();
525            }
526        }
527    }
528    
529    /// Render help overlay
530    fn render_help(&self, renderer: &mut Renderer, area: ratatui::layout::Rect) {
531        // Implementation would render help content
532        // This is a placeholder
533    }
534}
535
536#[cfg(test)]
537mod tests {
538    use super::*;
539    use crate::config::Config;
540
541    #[tokio::test]
542    async fn test_app_creation() {
543        let config = Config::default();
544        let app = App::new(config).await;
545        assert!(app.is_ok());
546    }
547
548    #[tokio::test]
549    async fn test_app_state_transitions() {
550        let config = Config::default();
551        let mut app = App::new(config).await.unwrap();
552        
553        assert_eq!(app.state, AppState::Running);
554        
555        app.toggle_help();
556        assert_eq!(app.state, AppState::Help);
557        
558        app.toggle_help();
559        assert_eq!(app.state, AppState::Chat);
560    }
561}