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 overview_state.repo_info.files_to_analyze = 42; }
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 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 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 }
142 OverviewComponent::Help => {
143 }
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 let chunks = Layout::default()
184 .direction(Direction::Vertical)
185 .constraints([
186 Constraint::Length(3), Constraint::Length(8), Constraint::Min(10), Constraint::Length(3), ])
191 .split(area);
192
193 self.render_title(chunks[0], buf);
195
196 self.render_repo_info(chunks[1], buf, state);
198
199 self.render_menu(chunks[2], buf, state);
201
202 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 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), Constraint::Length(1), Constraint::Length(3), Constraint::Length(1), Constraint::Length(3), Constraint::Length(1), Constraint::Length(3), Constraint::Length(1), Constraint::Length(3), ])
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 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 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 state
450 .registered_components
451 .insert(OverviewComponent::Help, help_area);
452 }
453}