safe_migrate/report/
interactive.rs1use crate::analysis::state::Confidence;
2use crate::report::violations::Violation;
3use anyhow::Result;
4use crossterm::{
5 cursor,
6 event::{self, Event, KeyCode, KeyEventKind},
7 execute, queue,
8 style::{Color, Print, ResetColor, SetForegroundColor},
9 terminal::{
10 Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode,
11 enable_raw_mode,
12 },
13};
14use std::io::{Write, stdout};
15use terminal_size::{Height, Width, terminal_size};
16
17struct TerminalGuard;
18
19impl Drop for TerminalGuard {
20 fn drop(&mut self) {
21 let _ = disable_raw_mode();
22 let mut stdout = stdout();
23 let _ = execute!(
24 stdout,
25 cursor::Show,
26 Clear(ClearType::All),
27 LeaveAlternateScreen,
28 cursor::MoveTo(0, 0)
29 );
30 }
31}
32
33pub fn run_interactive(violations: &[Violation], confidence: &Confidence) -> Result<()> {
34 if violations.is_empty() {
35 println!("No violations found!");
36 return Ok(());
37 }
38
39 enable_raw_mode()?;
40 let _guard = TerminalGuard;
41
42 let mut stdout = stdout();
43 execute!(
44 stdout,
45 EnterAlternateScreen,
46 cursor::Hide,
47 Clear(ClearType::All),
48 cursor::MoveTo(0, 0)
49 )?;
50
51 let mut selected: usize = 0;
52
53 loop {
54 queue!(stdout, Clear(ClearType::All), cursor::MoveTo(0, 0))?;
55 queue!(
56 stdout,
57 SetForegroundColor(Color::Cyan),
58 Print(format!(
59 "Safe-Migrate Interactive Viewer ({} violations) [Confidence: {:?}]\r\n\r\n",
60 violations.len(),
61 confidence
62 )),
63 ResetColor,
64 )?;
65
66 let (_w, Height(h)) = terminal_size().unwrap_or((Width(80), Height(24)));
68 let window_size = (h.saturating_sub(18) as usize).max(3);
71
72 let start = selected.saturating_sub(window_size / 2);
74 let end = std::cmp::min(start + window_size, violations.len());
75
76 if start > 0 {
77 queue!(stdout, Print(" ...\r\n"))?;
78 }
79
80 for (i, v) in violations.iter().enumerate().take(end).skip(start) {
81 let prefix = if i == selected { " > " } else { " " };
82 let color = match v.tier {
83 crate::report::violations::ViolationTier::Tier1 => Color::Red,
84 crate::report::violations::ViolationTier::Tier2 => Color::Yellow,
85 crate::report::violations::ViolationTier::Tier3 => Color::Green,
86 };
87
88 queue!(
89 stdout,
90 Print(prefix),
91 SetForegroundColor(color),
92 Print(format!("[{:?}] ", v.tier)),
93 ResetColor,
94 Print(format!("{} (rule: {})\r\n", v.operation_kind, v.rule_id))
95 )?;
96 }
97
98 if end < violations.len() {
99 queue!(stdout, Print(" ...\r\n"))?;
100 }
101
102 queue!(
103 stdout,
104 Print("\r\n------------------------------------------------------------\r\n")
105 )?;
106
107 let active = &violations[selected];
108 queue!(
109 stdout,
110 SetForegroundColor(Color::White),
111 Print("Reason: "),
112 ResetColor,
113 Print(format!("{}\r\n", active.reason)),
114 SetForegroundColor(Color::White),
115 Print("Recipe: "),
116 ResetColor,
117 Print(format!("{}\r\n", active.recipe)),
118 )?;
119
120 if let Some(sql) = &active.sql {
121 let mut sql_lines: Vec<&str> = sql.lines().collect();
123 let mut truncated = false;
124 if sql_lines.len() > 5 {
125 sql_lines.truncate(5);
126 truncated = true;
127 }
128
129 queue!(
130 stdout,
131 SetForegroundColor(Color::White),
132 Print("\r\nSQL Context:\r\n"),
133 SetForegroundColor(Color::DarkGrey),
134 Print(format!("{}\r\n", sql_lines.join("\r\n"))),
136 )?;
137
138 if truncated {
139 queue!(stdout, Print("... (truncated)\r\n"))?;
140 }
141 queue!(stdout, ResetColor)?;
142 }
143
144 queue!(stdout, Print("\r\n[Up/Down] Navigate | [q/Esc] Quit\r\n"))?;
145
146 stdout.flush()?;
147
148 if let Event::Key(key) = event::read()?
150 && key.kind == KeyEventKind::Press
151 {
152 match key.code {
153 KeyCode::Char('q') | KeyCode::Esc => break,
154 KeyCode::Up if selected > 0 => selected -= 1,
155 KeyCode::Down if selected < violations.len() - 1 => selected += 1,
156 _ => {}
157 }
158 }
159 }
160
161 drop(_guard);
162 println!("Exited interactive mode.");
163
164 Ok(())
165}