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            // TODO: Implement settings
141        }
142        OverviewComponent::Help => {
143            // Show help dialog - for now we'll add this as a state toggle
144            // In a real implementation, this might open a help dialog
145        }
146        OverviewComponent::Exit => {
147            app_events.send(AppEvent::Exit);
148        }
149    }
150}
151
152fn render_overview(
153    app_state: Res<State<AppState>>,
154    mut ratatui_context: ResMut<RatatuiContext>,
155    mut overview_state: ResMut<OverviewWidgetState>,
156) -> color_eyre::Result<()> {
157    if app_state.get() != &AppState::Overview {
158        return Ok(());
159    }
160
161    ratatui_context.draw(|frame| {
162        let area = frame.area();
163        frame.render_stateful_widget_ref(OverviewWidget, area, &mut overview_state);
164    })?;
165
166    Ok(())
167}
168
169pub struct OverviewWidget;
170
171impl StatefulWidgetRef for OverviewWidget {
172    type State = OverviewWidgetState;
173
174    fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
175        state.registered_components.clear();
176
177        if state.show_help {
178            self.render_help_overlay(area, buf, state);
179            return;
180        }
181
182        // Main layout
183        let chunks = Layout::default()
184            .direction(Direction::Vertical)
185            .constraints([
186                Constraint::Length(3), // Title
187                Constraint::Length(8), // Repository info
188                Constraint::Min(10),   // Menu buttons
189                Constraint::Length(3), // Status bar
190            ])
191            .split(area);
192
193        // Render title
194        self.render_title(chunks[0], buf);
195
196        // Render repository info
197        self.render_repo_info(chunks[1], buf, state);
198
199        // Render menu
200        self.render_menu(chunks[2], buf, state);
201
202        // Render status bar
203        self.render_status_bar(chunks[3], buf);
204    }
205}
206
207impl OverviewWidget {
208    fn render_title(&self, area: Rect, buf: &mut Buffer) {
209        let title = Paragraph::new(format!("🤖 AI Code Buddy v{APP_VERSION}"))
210            .style(THEME.title_style())
211            .alignment(Alignment::Center)
212            .block(
213                Block::default()
214                    .borders(Borders::ALL)
215                    .border_style(THEME.header_style()),
216            );
217        title.render_ref(area, buf);
218    }
219
220    fn render_repo_info(&self, area: Rect, buf: &mut Buffer, state: &OverviewWidgetState) {
221        let info_lines = vec![
222            Line::from(vec![
223                Span::styled("📂 Repository: ", THEME.info_style()),
224                Span::raw(&state.repo_info.path),
225            ]),
226            Line::from(vec![
227                Span::styled("🌿 Source Branch: ", THEME.info_style()),
228                Span::raw(&state.repo_info.source_branch),
229            ]),
230            Line::from(vec![
231                Span::styled("🎯 Target Branch: ", THEME.info_style()),
232                Span::raw(&state.repo_info.target_branch),
233            ]),
234            Line::from(vec![
235                Span::styled("📊 Files to Analyze: ", THEME.info_style()),
236                Span::raw(format!("{}", state.repo_info.files_to_analyze)),
237            ]),
238        ];
239
240        let repo_info = Paragraph::new(info_lines)
241            .block(
242                Block::default()
243                    .borders(Borders::ALL)
244                    .title("Repository Information")
245                    .title_style(THEME.header_style()),
246            )
247            .wrap(ratatui::widgets::Wrap { trim: true });
248
249        repo_info.render_ref(area, buf);
250    }
251
252    fn render_menu(&self, area: Rect, buf: &mut Buffer, state: &mut OverviewWidgetState) {
253        // Center the menu items
254        let menu_layout = Layout::default()
255            .direction(Direction::Horizontal)
256            .constraints([
257                Constraint::Percentage(20),
258                Constraint::Percentage(60),
259                Constraint::Percentage(20),
260            ])
261            .split(area);
262
263        let menu_area = menu_layout[1];
264
265        let items_layout = Layout::default()
266            .direction(Direction::Vertical)
267            .constraints([
268                Constraint::Length(3), // Start Analysis
269                Constraint::Length(1), // Spacer
270                Constraint::Length(3), // View Reports
271                Constraint::Length(1), // Spacer
272                Constraint::Length(3), // Settings
273                Constraint::Length(1), // Spacer
274                Constraint::Length(3), // Help
275                Constraint::Length(1), // Spacer
276                Constraint::Length(3), // Exit
277            ])
278            .split(menu_area);
279
280        self.render_menu_button(
281            items_layout[0],
282            buf,
283            state,
284            OverviewComponent::StartAnalysis,
285            "🚀 Start Analysis",
286        );
287
288        self.render_menu_button(
289            items_layout[2],
290            buf,
291            state,
292            OverviewComponent::ViewReports,
293            "📊 View Reports",
294        );
295
296        self.render_menu_button(
297            items_layout[4],
298            buf,
299            state,
300            OverviewComponent::Settings,
301            "⚙️  Settings",
302        );
303
304        self.render_menu_button(
305            items_layout[6],
306            buf,
307            state,
308            OverviewComponent::Help,
309            "❓ Help",
310        );
311
312        self.render_menu_button(
313            items_layout[8],
314            buf,
315            state,
316            OverviewComponent::Exit,
317            "🚪 Exit",
318        );
319    }
320
321    fn render_menu_button(
322        &self,
323        area: Rect,
324        buf: &mut Buffer,
325        state: &mut OverviewWidgetState,
326        component: OverviewComponent,
327        text: &str,
328    ) {
329        let is_selected = state.selected_component == component;
330        let is_hovered = state.hovered_component == Some(component.clone());
331
332        let style = if is_selected {
333            THEME.selected_style()
334        } else if is_hovered {
335            THEME.button_hover_style()
336        } else {
337            THEME.button_normal_style()
338        };
339
340        let border_style = if is_selected {
341            THEME.selected_style()
342        } else if is_hovered {
343            THEME.button_hover_style()
344        } else {
345            Style::default()
346        };
347
348        let button = Paragraph::new(text)
349            .style(style)
350            .alignment(Alignment::Center)
351            .block(
352                Block::default()
353                    .borders(Borders::ALL)
354                    .border_style(border_style),
355            );
356
357        button.render_ref(area, buf);
358        state.registered_components.insert(component, area);
359    }
360
361    fn render_status_bar(&self, area: Rect, buf: &mut Buffer) {
362        let status = Paragraph::new("Use ↑↓ or Tab to navigate, Enter to select, Q to quit")
363            .style(THEME.info_style())
364            .alignment(Alignment::Center)
365            .block(
366                Block::default()
367                    .borders(Borders::TOP)
368                    .border_style(THEME.info_style()),
369            );
370
371        status.render_ref(area, buf);
372    }
373
374    fn render_help_overlay(&self, area: Rect, buf: &mut Buffer, state: &mut OverviewWidgetState) {
375        // Create a centered help dialog
376        let help_area = {
377            let vertical = Layout::default()
378                .direction(Direction::Vertical)
379                .constraints([
380                    Constraint::Percentage(20),
381                    Constraint::Percentage(60),
382                    Constraint::Percentage(20),
383                ])
384                .split(area);
385
386            Layout::default()
387                .direction(Direction::Horizontal)
388                .constraints([
389                    Constraint::Percentage(15),
390                    Constraint::Percentage(70),
391                    Constraint::Percentage(15),
392                ])
393                .split(vertical[1])[1]
394        };
395
396        // Clear the background
397        for y in help_area.top()..help_area.bottom() {
398            for x in help_area.left()..help_area.right() {
399                buf.cell_mut((x, y)).unwrap().set_bg(THEME.background);
400            }
401        }
402
403        let help_content = vec![
404            Line::from("🤖 AI Code Buddy - Help"),
405            Line::from(""),
406            Line::from("🎯 What it does:"),
407            Line::from("  • Analyzes Git repositories for code quality issues"),
408            Line::from("  • Detects security vulnerabilities (OWASP Top 10)"),
409            Line::from("  • Provides performance and maintainability suggestions"),
410            Line::from("  • Compares code changes between Git branches"),
411            Line::from(""),
412            Line::from("⌨️  Keyboard Controls:"),
413            Line::from("  • ↑/↓ or Tab/Shift+Tab: Navigate menu"),
414            Line::from("  • Enter: Select menu item"),
415            Line::from("  • q: Quit application"),
416            Line::from(""),
417            Line::from("🖱️  Mouse Controls:"),
418            Line::from("  • Click: Select menu item"),
419            Line::from("  • Hover: Highlight menu item"),
420            Line::from(""),
421            Line::from("📋 Menu Options:"),
422            Line::from("  • 🚀 Start Analysis: Begin analyzing the repository"),
423            Line::from("  • 📊 View Reports: See analysis results and export"),
424            Line::from("  • ⚙️  Settings: Configure analysis options"),
425            Line::from("  • ❓ Help: Show this help screen"),
426            Line::from("  • 🚪 Exit: Quit the application"),
427            Line::from(""),
428            Line::from(Span::styled(
429                "Press any key or click anywhere to close help",
430                Style::default()
431                    .fg(THEME.accent)
432                    .add_modifier(Modifier::BOLD),
433            )),
434        ];
435
436        let help_dialog = Paragraph::new(help_content)
437            .block(
438                Block::default()
439                    .borders(Borders::ALL)
440                    .title(" Help & Controls ")
441                    .title_style(THEME.title_style())
442                    .border_style(THEME.primary_style()),
443            )
444            .wrap(ratatui::widgets::Wrap { trim: true });
445
446        help_dialog.render_ref(help_area, buf);
447
448        // Register the entire help area as clickable to close help
449        state
450            .registered_components
451            .insert(OverviewComponent::Help, help_area);
452    }
453}