1use bevy::prelude::*;
2use bevy_ratatui::{error::exit_on_error, terminal::RatatuiContext};
3use bevy_tokio_tasks::TokioTasksRuntime;
4use crossterm::event::{KeyCode, KeyEventKind};
5use ratatui::{
6 buffer::Buffer,
7 layout::{Alignment, Constraint, Direction, Layout, Rect},
8 style::Style,
9 text::{Line, Span},
10 widgets::{Block, Borders, Gauge, List, ListItem, Paragraph, StatefulWidgetRef, WidgetRef},
11};
12
13use crate::{
14 args::Args,
15 bevy_states::app::AppState,
16 core,
17 events::{analysis::AnalysisEvent, app::AppEvent},
18 theme::THEME,
19 widget_states::analysis::AnalysisWidgetState,
20};
21
22pub struct AnalysisPlugin;
23
24impl Plugin for AnalysisPlugin {
25 fn build(&self, app: &mut App) {
26 app.add_event::<AnalysisEvent>()
27 .init_resource::<AnalysisWidgetState>()
28 .add_systems(PreUpdate, analysis_event_handler)
29 .add_systems(Update, render_analysis.pipe(exit_on_error));
30 }
31}
32
33pub fn analysis_event_handler(
34 mut analysis_events: EventReader<AnalysisEvent>,
35 mut analysis_state: ResMut<AnalysisWidgetState>,
36 mut app_events: EventWriter<AppEvent>,
37 args: Res<Args>,
38 tokio_runtime: ResMut<TokioTasksRuntime>,
39) {
40 for event in analysis_events.read() {
41 match event {
42 AnalysisEvent::KeyEvent(key_event) => {
43 match key_event.code {
44 KeyCode::Esc => {
45 if analysis_state.is_analyzing {
48 analysis_state.is_analyzing = false;
49 }
50 app_events.send(AppEvent::SwitchTo(AppState::Overview));
51 }
52 _ => {
53 if key_event.kind == KeyEventKind::Release {
55 match key_event.code {
56 KeyCode::Enter => {
57 if !analysis_state.is_analyzing
58 && analysis_state.review.is_none()
59 {
60 start_analysis(&mut analysis_state, &args, &tokio_runtime);
61 }
62 }
63 KeyCode::Up => {
64 if !analysis_state.is_analyzing {
65 analysis_state.move_issue_selection(-1);
66 }
67 }
68 KeyCode::Down => {
69 if !analysis_state.is_analyzing {
70 analysis_state.move_issue_selection(1);
71 }
72 }
73 KeyCode::Char('r') => {
74 if !analysis_state.is_analyzing {
75 app_events.send(AppEvent::SwitchTo(AppState::Reports));
76 }
77 }
78 _ => {}
79 }
80 }
81 }
82 }
83 }
84 AnalysisEvent::MouseEvent(_mouse_event) => {
85 }
87 }
88 }
89}
90
91fn start_analysis(
92 analysis_state: &mut AnalysisWidgetState,
93 args: &Args,
94 _tokio_runtime: &TokioTasksRuntime,
95) {
96 analysis_state.start_analysis();
97
98 match core::analysis::perform_analysis(args) {
100 Ok(review) => {
101 analysis_state.complete_analysis(review);
102 }
103 Err(e) => {
104 eprintln!("AI analysis failed: {e}");
105 analysis_state.is_analyzing = false;
106 }
107 }
108}
109
110fn render_analysis(
111 app_state: Res<State<AppState>>,
112 mut ratatui_context: ResMut<RatatuiContext>,
113 mut analysis_state: ResMut<AnalysisWidgetState>,
114) -> color_eyre::Result<()> {
115 if app_state.get() != &AppState::Analysis {
116 return Ok(());
117 }
118
119 ratatui_context.draw(|frame| {
120 let area = frame.area();
121 frame.render_stateful_widget_ref(AnalysisWidget, area, &mut analysis_state);
122 })?;
123
124 Ok(())
125}
126
127pub struct AnalysisWidget;
128
129impl StatefulWidgetRef for AnalysisWidget {
130 type State = AnalysisWidgetState;
131
132 fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
133 let chunks = Layout::default()
134 .direction(Direction::Vertical)
135 .constraints([
136 Constraint::Length(3), Constraint::Min(10), Constraint::Length(3), ])
140 .split(area);
141
142 let title = Paragraph::new("đ Code Analysis")
144 .style(THEME.title_style())
145 .alignment(Alignment::Center)
146 .block(
147 Block::default()
148 .borders(Borders::ALL)
149 .border_style(THEME.header_style()),
150 );
151 title.render_ref(chunks[0], buf);
152
153 if state.is_analyzing {
155 self.render_analysis_progress(chunks[1], buf, state);
156 } else if let Some(review) = &state.review {
157 self.render_results(chunks[1], buf, state, review);
158 } else {
159 self.render_start_screen(chunks[1], buf);
160 }
161
162 self.render_status_bar(chunks[2], buf, state);
164 }
165}
166
167impl AnalysisWidget {
168 fn render_start_screen(&self, area: Rect, buf: &mut Buffer) {
169 let content = Paragraph::new(vec![
170 Line::from(""),
171 Line::from("Press Enter to start the code analysis"),
172 Line::from(""),
173 Line::from("This will analyze your Git repository for:"),
174 Line::from("âĸ Security vulnerabilities"),
175 Line::from("âĸ Performance issues"),
176 Line::from("âĸ Code quality problems"),
177 Line::from("âĸ Best practice violations"),
178 ])
179 .alignment(Alignment::Center)
180 .block(
181 Block::default()
182 .borders(Borders::ALL)
183 .title("Ready to Analyze")
184 .title_style(THEME.header_style()),
185 );
186
187 content.render_ref(area, buf);
188 }
189
190 fn render_analysis_progress(&self, area: Rect, buf: &mut Buffer, state: &AnalysisWidgetState) {
191 let chunks = Layout::default()
192 .direction(Direction::Vertical)
193 .constraints([
194 Constraint::Length(5), Constraint::Min(3), ])
197 .split(area);
198
199 let progress = Gauge::default()
201 .block(
202 Block::default()
203 .borders(Borders::ALL)
204 .title("Analysis Progress")
205 .title_style(THEME.header_style()),
206 )
207 .gauge_style(THEME.success_style())
208 .percent(state.progress as u16)
209 .label(format!("{:.1}%", state.progress));
210
211 progress.render_ref(chunks[0], buf);
212
213 let current_file = Paragraph::new(vec![
215 Line::from(""),
216 Line::from(vec![
217 Span::styled("Currently analyzing: ", THEME.info_style()),
218 Span::raw(&state.current_file),
219 ]),
220 ])
221 .alignment(Alignment::Center)
222 .block(
223 Block::default()
224 .borders(Borders::ALL)
225 .title("Status")
226 .title_style(THEME.header_style()),
227 );
228
229 current_file.render_ref(chunks[1], buf);
230 }
231
232 fn render_results(
233 &self,
234 area: Rect,
235 buf: &mut Buffer,
236 state: &AnalysisWidgetState,
237 review: &crate::core::review::Review,
238 ) {
239 let chunks = Layout::default()
240 .direction(Direction::Horizontal)
241 .constraints([
242 Constraint::Percentage(30), Constraint::Percentage(70), ])
245 .split(area);
246
247 self.render_summary(chunks[0], buf, review);
249
250 self.render_issue_list(chunks[1], buf, state, review);
252 }
253
254 fn render_summary(&self, area: Rect, buf: &mut Buffer, review: &crate::core::review::Review) {
255 let summary_lines = vec![
256 Line::from(""),
257 Line::from(vec![
258 Span::styled("đ Files: ", THEME.info_style()),
259 Span::raw(format!("{}", review.files_count)),
260 ]),
261 Line::from(""),
262 Line::from(vec![
263 Span::styled("đ Total Issues: ", THEME.info_style()),
264 Span::raw(format!("{}", review.issues_count)),
265 ]),
266 Line::from(""),
267 Line::from(vec![
268 Span::styled("đ¨ Critical: ", THEME.error_style()),
269 Span::raw(format!("{}", review.critical_issues)),
270 ]),
271 Line::from(vec![
272 Span::styled("â ī¸ High: ", THEME.warning_style()),
273 Span::raw(format!("{}", review.high_issues)),
274 ]),
275 Line::from(vec![
276 Span::styled("đļ Medium: ", THEME.warning_style()),
277 Span::raw(format!("{}", review.medium_issues)),
278 ]),
279 Line::from(vec![
280 Span::styled("âšī¸ Low: ", THEME.info_style()),
281 Span::raw(format!("{}", review.low_issues)),
282 ]),
283 ];
284
285 let summary = Paragraph::new(summary_lines).block(
286 Block::default()
287 .borders(Borders::ALL)
288 .title("Summary")
289 .title_style(THEME.header_style()),
290 );
291
292 summary.render_ref(area, buf);
293 }
294
295 fn render_issue_list(
296 &self,
297 area: Rect,
298 buf: &mut Buffer,
299 state: &AnalysisWidgetState,
300 review: &crate::core::review::Review,
301 ) {
302 if review.issues.is_empty() {
303 let no_issues = Paragraph::new(vec![
304 Line::from(""),
305 Line::from("đ No issues found!"),
306 Line::from(""),
307 Line::from("Your code looks clean. Great job!"),
308 ])
309 .alignment(Alignment::Center)
310 .block(
311 Block::default()
312 .borders(Borders::ALL)
313 .title("Issues")
314 .title_style(THEME.header_style()),
315 );
316 no_issues.render_ref(area, buf);
317 return;
318 }
319
320 let items: Vec<ListItem> = review
321 .issues
322 .iter()
323 .enumerate()
324 .map(|(i, issue)| {
325 let severity_icon = match issue.severity.as_str() {
326 "Critical" => "đ¨",
327 "High" => "â ī¸",
328 "Medium" => "đļ",
329 "Low" => "âšī¸",
330 _ => "đĄ",
331 };
332
333 let severity_style = match issue.severity.as_str() {
334 "Critical" => THEME.error_style(),
335 "High" => THEME.warning_style(),
336 "Medium" => THEME.warning_style(),
337 "Low" => THEME.info_style(),
338 _ => Style::default(),
339 };
340
341 let is_selected = i == state.selected_issue;
342
343 let lines = vec![
345 Line::from(vec![
346 Span::styled(format!("{severity_icon} "), severity_style),
347 Span::styled(issue.severity.to_string(), severity_style),
348 Span::raw(" "),
349 Span::styled(format!("{}:{}", issue.file, issue.line), THEME.info_style()),
350 ]),
351 Line::from(vec![
352 Span::raw(" "),
353 Span::styled(format!("{}: ", issue.category), THEME.header_style()),
354 Span::raw(issue.description.to_string()),
355 ]),
356 Line::from(""), ];
358
359 let style = if is_selected {
360 THEME.selected_style()
361 } else {
362 Style::default()
363 };
364
365 ListItem::new(lines).style(style)
366 })
367 .collect();
368
369 let issue_list = List::new(items)
370 .block(
371 Block::default()
372 .borders(Borders::ALL)
373 .title(format!(
374 "Issues ({}/{})",
375 state.selected_issue + 1,
376 review.issues.len().max(1)
377 ))
378 .title_style(THEME.header_style()),
379 )
380 .highlight_style(THEME.selected_style());
381
382 WidgetRef::render_ref(&issue_list, area, buf);
383 }
384
385 fn render_status_bar(&self, area: Rect, buf: &mut Buffer, state: &AnalysisWidgetState) {
386 let status_text = if state.is_analyzing {
387 "Analysis in progress... Please wait"
388 } else if state.review.is_some() {
389 "Use ââ to navigate issues, R for reports, Esc to go back"
390 } else {
391 "Enter to start analysis, Esc to go back"
392 };
393
394 let status = Paragraph::new(status_text)
395 .style(THEME.info_style())
396 .alignment(Alignment::Center)
397 .block(
398 Block::default()
399 .borders(Borders::TOP)
400 .border_style(THEME.info_style()),
401 );
402
403 status.render_ref(area, buf);
404 }
405}