ai_code_buddy/widgets/
overview.rs

1use bevy::prelude::*;
2use bevy_ratatui::{error::exit_on_error, terminal::RatatuiContext};
3use crossterm::event::{KeyCode, KeyEventKind, MouseEventKind};
4use ratatui::{
5    buffer::Buffer,
6    layout::{Alignment, Constraint, Direction, Layout, Rect},
7    style::{Modifier, Style},
8    text::{Line, Span},
9    widgets::{Block, Borders, Paragraph, StatefulWidgetRef, WidgetRef},
10};
11
12use crate::{
13    args::Args,
14    bevy_states::app::AppState,
15    events::{app::AppEvent, overview::OverviewEvent},
16    theme::THEME,
17    version::APP_VERSION,
18    widget_states::overview::{OverviewComponent, OverviewWidgetState, SelectionDirection},
19};
20
21pub struct OverviewPlugin;
22
23impl Plugin for OverviewPlugin {
24    fn build(&self, app: &mut App) {
25        app.add_event::<OverviewEvent>()
26            .init_resource::<OverviewWidgetState>()
27            .add_systems(Startup, initialize_overview_state)
28            .add_systems(PreUpdate, overview_event_handler)
29            .add_systems(Update, render_overview.pipe(exit_on_error));
30    }
31}
32
33pub fn initialize_overview_state(mut overview_state: ResMut<OverviewWidgetState>, args: Res<Args>) {
34    overview_state.repo_info.path = args.repo_path.clone();
35    overview_state.repo_info.source_branch = args.source_branch.clone();
36    overview_state.repo_info.target_branch = args.target_branch.clone();
37
38    // TODO: Calculate files to analyze
39    overview_state.repo_info.files_to_analyze = 42; // Placeholder
40}
41
42pub fn overview_event_handler(
43    mut overview_events: EventReader<OverviewEvent>,
44    mut overview_state: ResMut<OverviewWidgetState>,
45    mut app_events: EventWriter<AppEvent>,
46) {
47    for event in overview_events.read() {
48        match event {
49            OverviewEvent::KeyEvent(key_event) => {
50                if key_event.kind == KeyEventKind::Release {
51                    // If help is showing, any key closes it
52                    if overview_state.show_help {
53                        overview_state.show_help = false;
54                        return;
55                    }
56
57                    match key_event.code {
58                        KeyCode::Tab => {
59                            overview_state.move_selection(SelectionDirection::Next);
60                        }
61                        KeyCode::BackTab => {
62                            overview_state.move_selection(SelectionDirection::Previous);
63                        }
64                        KeyCode::Up => {
65                            overview_state.move_selection(SelectionDirection::Previous);
66                        }
67                        KeyCode::Down => {
68                            overview_state.move_selection(SelectionDirection::Next);
69                        }
70                        KeyCode::Enter => match overview_state.selected_component {
71                            OverviewComponent::Help => {
72                                overview_state.show_help = !overview_state.show_help;
73                            }
74                            _ => {
75                                handle_selection(
76                                    &overview_state.selected_component,
77                                    &mut app_events,
78                                );
79                            }
80                        },
81                        _ => {}
82                    }
83                }
84            }
85            OverviewEvent::MouseEvent(mouse_event) => {
86                // If help is showing, any click closes it
87                if overview_state.show_help {
88                    if let MouseEventKind::Up(_) = mouse_event.kind {
89                        overview_state.show_help = false;
90                    }
91                    return;
92                }
93
94                match mouse_event.kind {
95                    MouseEventKind::Up(_) => {
96                        let x = mouse_event.column;
97                        let y = mouse_event.row;
98
99                        let components: Vec<_> = overview_state
100                            .registered_components
101                            .clone()
102                            .into_iter()
103                            .collect();
104                        for (component, _rect) in components {
105                            if overview_state.is_over(component.clone(), x, y) {
106                                overview_state.selected_component = component.clone();
107                                match component {
108                                    OverviewComponent::Help => {
109                                        overview_state.show_help = !overview_state.show_help;
110                                    }
111                                    _ => {
112                                        handle_selection(&component, &mut app_events);
113                                    }
114                                }
115                                break;
116                            }
117                        }
118                    }
119                    MouseEventKind::Moved => {
120                        let x = mouse_event.column;
121                        let y = mouse_event.row;
122                        overview_state.update_hover(x, y);
123                    }
124                    _ => {}
125                }
126            }
127        }
128    }
129}
130
131fn handle_selection(component: &OverviewComponent, app_events: &mut EventWriter<AppEvent>) {
132    match component {
133        OverviewComponent::StartAnalysis => {
134            app_events.send(AppEvent::SwitchTo(AppState::Analysis));
135        }
136        OverviewComponent::ViewReports => {
137            app_events.send(AppEvent::SwitchTo(AppState::Reports));
138        }
139        OverviewComponent::Settings => {
140            // Settings functionality - for now we'll add this as a placeholder
141            // In a real implementation, this might open a settings dialog
142        }
143        OverviewComponent::Credits => {
144            app_events.send(AppEvent::SwitchTo(AppState::Credits));
145        }
146        OverviewComponent::Help => {
147            // Show help dialog - for now we'll add this as a state toggle
148            // In a real implementation, this might open a help dialog
149        }
150        OverviewComponent::Exit => {
151            app_events.send(AppEvent::Exit);
152        }
153    }
154}
155
156fn render_overview(
157    app_state: Res<State<AppState>>,
158    mut ratatui_context: ResMut<RatatuiContext>,
159    mut overview_state: ResMut<OverviewWidgetState>,
160) -> color_eyre::Result<()> {
161    if app_state.get() != &AppState::Overview {
162        return Ok(());
163    }
164
165    ratatui_context.draw(|frame| {
166        let area = frame.area();
167        frame.render_stateful_widget_ref(OverviewWidget, area, &mut overview_state);
168    })?;
169
170    Ok(())
171}
172
173pub struct OverviewWidget;
174
175impl StatefulWidgetRef for OverviewWidget {
176    type State = OverviewWidgetState;
177
178    fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
179        state.registered_components.clear();
180
181        if state.show_help {
182            self.render_help_overlay(area, buf, state);
183            return;
184        }
185
186        // Main layout
187        let chunks = Layout::default()
188            .direction(Direction::Vertical)
189            .constraints([
190                Constraint::Length(3), // Title
191                Constraint::Length(8), // Repository info
192                Constraint::Min(10),   // Menu buttons
193                Constraint::Length(3), // Status bar
194            ])
195            .split(area);
196
197        // Render title
198        self.render_title(chunks[0], buf);
199
200        // Render repository info
201        self.render_repo_info(chunks[1], buf, state);
202
203        // Render menu
204        self.render_menu(chunks[2], buf, state);
205
206        // Render status bar
207        self.render_status_bar(chunks[3], buf);
208    }
209}
210
211impl OverviewWidget {
212    fn render_title(&self, area: Rect, buf: &mut Buffer) {
213        let title = Paragraph::new(format!("🤖 AI Code Buddy v{APP_VERSION}"))
214            .style(THEME.title_style())
215            .alignment(Alignment::Center)
216            .block(
217                Block::default()
218                    .borders(Borders::ALL)
219                    .border_style(THEME.header_style()),
220            );
221        title.render_ref(area, buf);
222    }
223
224    fn render_repo_info(&self, area: Rect, buf: &mut Buffer, state: &OverviewWidgetState) {
225        let info_lines = vec![
226            Line::from(vec![
227                Span::styled("📂 Repository: ", THEME.info_style()),
228                Span::raw(&state.repo_info.path),
229            ]),
230            Line::from(vec![
231                Span::styled("🌿 Source Branch: ", THEME.info_style()),
232                Span::raw(&state.repo_info.source_branch),
233            ]),
234            Line::from(vec![
235                Span::styled("🎯 Target Branch: ", THEME.info_style()),
236                Span::raw(&state.repo_info.target_branch),
237            ]),
238            Line::from(vec![
239                Span::styled("📊 Files to Analyze: ", THEME.info_style()),
240                Span::raw(format!("{}", state.repo_info.files_to_analyze)),
241            ]),
242        ];
243
244        let repo_info = Paragraph::new(info_lines)
245            .block(
246                Block::default()
247                    .borders(Borders::ALL)
248                    .title("Repository Information")
249                    .title_style(THEME.header_style()),
250            )
251            .wrap(ratatui::widgets::Wrap { trim: true });
252
253        repo_info.render_ref(area, buf);
254    }
255
256    fn render_menu(&self, area: Rect, buf: &mut Buffer, state: &mut OverviewWidgetState) {
257        // Center the menu items
258        let menu_layout = Layout::default()
259            .direction(Direction::Horizontal)
260            .constraints([
261                Constraint::Percentage(20),
262                Constraint::Percentage(60),
263                Constraint::Percentage(20),
264            ])
265            .split(area);
266
267        let menu_area = menu_layout[1];
268
269        let items_layout = Layout::default()
270            .direction(Direction::Vertical)
271            .constraints([
272                Constraint::Length(3), // Start Analysis
273                Constraint::Length(1), // Spacer
274                Constraint::Length(3), // View Reports
275                Constraint::Length(1), // Spacer
276                Constraint::Length(3), // Credits
277                Constraint::Length(1), // Spacer
278                Constraint::Length(3), // Help
279                Constraint::Length(1), // Spacer
280                Constraint::Length(3), // Exit
281            ])
282            .split(menu_area);
283
284        self.render_menu_button(
285            items_layout[0],
286            buf,
287            state,
288            OverviewComponent::StartAnalysis,
289            "🚀 Start Analysis",
290        );
291
292        self.render_menu_button(
293            items_layout[2],
294            buf,
295            state,
296            OverviewComponent::ViewReports,
297            "📊 View Reports",
298        );
299
300        self.render_menu_button(
301            items_layout[4],
302            buf,
303            state,
304            OverviewComponent::Settings,
305            "⚙️  Settings",
306        );
307
308        self.render_menu_button(
309            items_layout[6],
310            buf,
311            state,
312            OverviewComponent::Credits,
313            "🎉 Credits",
314        );
315
316        self.render_menu_button(
317            items_layout[6],
318            buf,
319            state,
320            OverviewComponent::Help,
321            "❓ Help",
322        );
323
324        self.render_menu_button(
325            items_layout[8],
326            buf,
327            state,
328            OverviewComponent::Exit,
329            "🚪 Exit",
330        );
331    }
332
333    fn render_menu_button(
334        &self,
335        area: Rect,
336        buf: &mut Buffer,
337        state: &mut OverviewWidgetState,
338        component: OverviewComponent,
339        text: &str,
340    ) {
341        let is_selected = state.selected_component == component;
342        let is_hovered = state.hovered_component == Some(component.clone());
343
344        let style = if is_selected {
345            THEME.selected_style()
346        } else if is_hovered {
347            THEME.button_hover_style()
348        } else {
349            THEME.button_normal_style()
350        };
351
352        let border_style = if is_selected {
353            THEME.selected_style()
354        } else if is_hovered {
355            THEME.button_hover_style()
356        } else {
357            Style::default()
358        };
359
360        let button = Paragraph::new(text)
361            .style(style)
362            .alignment(Alignment::Center)
363            .block(
364                Block::default()
365                    .borders(Borders::ALL)
366                    .border_style(border_style),
367            );
368
369        button.render_ref(area, buf);
370        state.registered_components.insert(component, area);
371    }
372
373    fn render_status_bar(&self, area: Rect, buf: &mut Buffer) {
374        let status = Paragraph::new("Use ↑↓ or Tab to navigate, Enter to select, Q to quit")
375            .style(THEME.info_style())
376            .alignment(Alignment::Center)
377            .block(
378                Block::default()
379                    .borders(Borders::TOP)
380                    .border_style(THEME.info_style()),
381            );
382
383        status.render_ref(area, buf);
384    }
385
386    fn render_help_overlay(&self, area: Rect, buf: &mut Buffer, state: &mut OverviewWidgetState) {
387        // Create a centered help dialog
388        let help_area = {
389            let vertical = Layout::default()
390                .direction(Direction::Vertical)
391                .constraints([
392                    Constraint::Percentage(20),
393                    Constraint::Percentage(60),
394                    Constraint::Percentage(20),
395                ])
396                .split(area);
397
398            Layout::default()
399                .direction(Direction::Horizontal)
400                .constraints([
401                    Constraint::Percentage(15),
402                    Constraint::Percentage(70),
403                    Constraint::Percentage(15),
404                ])
405                .split(vertical[1])[1]
406        };
407
408        // Clear the background
409        for y in help_area.top()..help_area.bottom() {
410            for x in help_area.left()..help_area.right() {
411                buf.cell_mut((x, y)).unwrap().set_bg(THEME.background);
412            }
413        }
414
415        let help_content = vec![
416            Line::from("🤖 AI Code Buddy - Help"),
417            Line::from(""),
418            Line::from("🎯 What it does:"),
419            Line::from("  • Analyzes Git repositories for code quality issues"),
420            Line::from("  • Detects security vulnerabilities (OWASP Top 10)"),
421            Line::from("  • Provides performance and maintainability suggestions"),
422            Line::from("  • Compares code changes between Git branches"),
423            Line::from(""),
424            Line::from("⌨️  Keyboard Controls:"),
425            Line::from("  • ↑/↓ or Tab/Shift+Tab: Navigate menu"),
426            Line::from("  • Enter: Select menu item"),
427            Line::from("  • q: Quit application"),
428            Line::from(""),
429            Line::from("🖱️  Mouse Controls:"),
430            Line::from("  • Click: Select menu item"),
431            Line::from("  • Hover: Highlight menu item"),
432            Line::from(""),
433            Line::from("📋 Menu Options:"),
434            Line::from("  • 🚀 Start Analysis: Begin analyzing the repository"),
435            Line::from("  • 📊 View Reports: See analysis results and export"),
436            Line::from("  • 🎉 Credits: View project contributors and acknowledgments"),
437            Line::from("  • ❓ Help: Show this help screen"),
438            Line::from("  • 🚪 Exit: Quit the application"),
439            Line::from(""),
440            Line::from(Span::styled(
441                "Press any key or click anywhere to close help",
442                Style::default()
443                    .fg(THEME.accent)
444                    .add_modifier(Modifier::BOLD),
445            )),
446        ];
447
448        let help_dialog = Paragraph::new(help_content)
449            .block(
450                Block::default()
451                    .borders(Borders::ALL)
452                    .title(" Help & Controls ")
453                    .title_style(THEME.title_style())
454                    .border_style(THEME.primary_style()),
455            )
456            .wrap(ratatui::widgets::Wrap { trim: true });
457
458        help_dialog.render_ref(help_area, buf);
459
460        // Register the entire help area as clickable to close help
461        state
462            .registered_components
463            .insert(OverviewComponent::Help, help_area);
464    }
465}