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 }
143 OverviewComponent::Credits => {
144 app_events.send(AppEvent::SwitchTo(AppState::Credits));
145 }
146 OverviewComponent::Help => {
147 }
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 let chunks = Layout::default()
188 .direction(Direction::Vertical)
189 .constraints([
190 Constraint::Length(3), Constraint::Length(8), Constraint::Min(10), Constraint::Length(3), ])
195 .split(area);
196
197 self.render_title(chunks[0], buf);
199
200 self.render_repo_info(chunks[1], buf, state);
202
203 self.render_menu(chunks[2], buf, state);
205
206 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 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), Constraint::Length(1), Constraint::Length(3), Constraint::Length(1), Constraint::Length(3), Constraint::Length(1), Constraint::Length(3), Constraint::Length(1), Constraint::Length(3), ])
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 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 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 state
462 .registered_components
463 .insert(OverviewComponent::Help, help_area);
464 }
465}