Skip to main content

safe_migrate/report/
interactive.rs

1use 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        // Calculate available height for the list dynamically
67        let (_w, Height(h)) = terminal_size().unwrap_or((Width(80), Height(24)));
68        // Subtract lines for header (2), footer separator (2), detail text (~5), SQL (~6), and quit instructions (2)
69        // We use 18 as a safe heuristic to prevent the text from wrapping and triggering a terminal scroll.
70        let window_size = (h.saturating_sub(18) as usize).max(3);
71
72        // Render list (sliding window)
73        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            // Limit SQL context to max 5 lines to prevent pushing UI off-screen
122            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                // Important: replace all \n inside the SQL with \r\n
135                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        // Handle input
149        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}