1use anyhow::Result;
11use ratatui::{
12 backend::CrosstermBackend,
13 layout::{Alignment, Constraint, Direction, Layout, Rect},
14 style::{Color, Modifier, Style},
15 text::{Span, Line},
16 widgets::{
17 Block, Borders, Gauge, Paragraph, Wrap, BarChart, List, ListItem, Tabs,
18 },
19 Frame, Terminal,
20};
21use crossterm::{
22 event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
23 execute,
24 terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
25};
26use std::io;
27use std::time::Duration;
28use phantomdev_core::{StealthScore, DetectionResult};
29
30pub struct PhantomTui {
32 current_tab: Tab,
34 stealth_score: Option<StealthScore>,
36 detection_results: Vec<DetectionResult>,
38 should_quit: bool,
40}
41
42#[derive(Clone, Copy, PartialEq, Eq, Debug)]
44enum Tab {
45 Overview,
46 Detection,
47 Style,
48 Settings,
49}
50
51impl Tab {
52 fn title(&self) -> &'static str {
53 match self {
54 Tab::Overview => "Overview",
55 Tab::Detection => "Detection",
56 Tab::Style => "Style",
57 Tab::Settings => "Settings",
58 }
59 }
60
61 fn all() -> Vec<Tab> {
62 vec![Tab::Overview, Tab::Detection, Tab::Style, Tab::Settings]
63 }
64}
65
66impl PhantomTui {
67 pub fn new() -> Self {
69 Self {
70 current_tab: Tab::Overview,
71 stealth_score: None,
72 detection_results: Vec::new(),
73 should_quit: false,
74 }
75 }
76
77 pub fn run(&mut self) -> Result<()> {
79 enable_raw_mode()?;
81 let mut stdout = io::stdout();
82 execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
83 let backend = CrosstermBackend::new(stdout);
84 let mut terminal = Terminal::new(backend)?;
85
86 while !self.should_quit {
88 terminal.draw(|f| self.draw(f))?;
89
90 if event::poll(Duration::from_millis(100))? {
92 if let Event::Key(key) = event::read()? {
93 self.handle_key(key);
94 }
95 }
96 }
97
98 disable_raw_mode()?;
100 execute!(
101 terminal.backend_mut(),
102 LeaveAlternateScreen,
103 DisableMouseCapture
104 )?;
105
106 Ok(())
107 }
108
109 fn handle_key(&mut self, key: event::KeyEvent) {
111 match key.code {
112 KeyCode::Char('q') | KeyCode::Esc => {
113 self.should_quit = true;
114 }
115 KeyCode::Left => {
116 let tabs = Tab::all();
117 let current = tabs.iter().position(|&t| t == self.current_tab).unwrap_or(0);
118 if current > 0 {
119 self.current_tab = tabs[current - 1];
120 }
121 }
122 KeyCode::Right => {
123 let tabs = Tab::all();
124 let current = tabs.iter().position(|&t| t == self.current_tab).unwrap_or(0);
125 if current < tabs.len() - 1 {
126 self.current_tab = tabs[current + 1];
127 }
128 }
129 KeyCode::Char('1') => self.current_tab = Tab::Overview,
130 KeyCode::Char('2') => self.current_tab = Tab::Detection,
131 KeyCode::Char('3') => self.current_tab = Tab::Style,
132 KeyCode::Char('4') => self.current_tab = Tab::Settings,
133 _ => {}
134 }
135 }
136
137 fn draw(&self, f: &mut Frame) {
139 let size = f.size();
140
141 let chunks = Layout::default()
143 .direction(Direction::Vertical)
144 .margin(1)
145 .constraints([
146 Constraint::Length(3), Constraint::Min(0), Constraint::Length(1), ])
150 .split(size);
151
152 self.draw_header(f, chunks[0]);
154
155 match self.current_tab {
157 Tab::Overview => self.draw_overview(f, chunks[1]),
158 Tab::Detection => self.draw_detection(f, chunks[1]),
159 Tab::Style => self.draw_style(f, chunks[1]),
160 Tab::Settings => self.draw_settings(f, chunks[1]),
161 }
162
163 self.draw_footer(f, chunks[2]);
165 }
166
167 fn draw_header(&self, f: &mut Frame, area: Rect) {
169 let tabs = Tab::all();
170 let titles: Vec<Line> = tabs
171 .iter()
172 .map(|t| {
173 let style = if *t == self.current_tab {
174 Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)
175 } else {
176 Style::default().fg(Color::Gray)
177 };
178 Line::from(Span::styled(t.title(), style))
179 })
180 .collect();
181
182 let tabs_widget = Tabs::new(titles)
183 .block(
184 Block::default()
185 .borders(Borders::ALL)
186 .title("PhantomDev Dashboard")
187 .title_style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD))
188 )
189 .style(Style::default().fg(Color::White))
190 .highlight_style(Style::default().add_modifier(Modifier::UNDERLINED))
191 .divider(Span::raw(" | "));
192
193 f.render_widget(tabs_widget, area);
194 }
195
196 fn draw_overview(&self, f: &mut Frame, area: Rect) {
198 let chunks = Layout::default()
199 .direction(Direction::Vertical)
200 .constraints([
201 Constraint::Length(10),
202 Constraint::Min(0),
203 ])
204 .split(area);
205
206 self.draw_stealth_score(f, chunks[0]);
208
209 let summary = vec![
211 Line::from("PhantomDev Status:"),
212 Line::from(""),
213 Line::from(vec![
214 Span::raw(" • Detection Engine: "),
215 Span::styled("Active", Style::default().fg(Color::Green)),
216 ]),
217 Line::from(vec![
218 Span::raw(" • Humanizer: "),
219 Span::styled("Ready", Style::default().fg(Color::Green)),
220 ]),
221 Line::from(vec![
222 Span::raw(" • Jitter Engine: "),
223 Span::styled("Disabled", Style::default().fg(Color::Yellow)),
224 ]),
225 Line::from(""),
226 Line::from("Press 'q' to quit, '1-4' to switch tabs"),
227 ];
228
229 let paragraph = Paragraph::new(summary)
230 .block(Block::default().borders(Borders::ALL).title("Summary"))
231 .wrap(Wrap { trim: true });
232
233 f.render_widget(paragraph, chunks[1]);
234 }
235
236 fn draw_stealth_score(&self, f: &mut Frame, area: Rect) {
238 let score = self.stealth_score.as_ref().unwrap_or(&StealthScore {
239 overall: 0.75,
240 ai_probability: 0.25,
241 pattern_score: 0.3,
242 style_score: 0.8,
243 });
244
245 let gauge = Gauge::default()
246 .block(Block::default().borders(Borders::ALL).title("Stealth Score"))
247 .gauge_style(
248 Style::default()
249 .fg(if score.overall > 0.7 {
250 Color::Green
251 } else if score.overall > 0.4 {
252 Color::Yellow
253 } else {
254 Color::Red
255 })
256 .bg(Color::DarkGray)
257 )
258 .label(Span::styled(
259 format!("{:.0}%", score.overall * 100.0),
260 Style::default().fg(Color::White).add_modifier(Modifier::BOLD),
261 ))
262 .ratio(score.overall as f64);
263
264 f.render_widget(gauge, area);
265 }
266
267 fn draw_detection(&self, f: &mut Frame, area: Rect) {
269 let chunks = Layout::default()
270 .direction(Direction::Vertical)
271 .constraints([
272 Constraint::Length(8),
273 Constraint::Min(0),
274 ])
275 .split(area);
276
277 let patterns = vec![
279 ("Watermarks", 0.2),
280 ("Emojis", 0.1),
281 ("Comments", 0.3),
282 ("Naming", 0.15),
283 ("Entropy", 0.25),
284 ];
285
286 let bars: Vec<(&str, u64)> = patterns
287 .iter()
288 .map(|(name, value)| (*name, (*value * 100.0) as u64))
289 .collect();
290
291 let barchart = BarChart::default()
292 .block(Block::default().borders(Borders::ALL).title("Pattern Detection"))
293 .bar_width(8)
294 .bar_gap(2)
295 .data(&bars)
296 .style(Style::default().fg(Color::Cyan))
297 .value_style(Style::default().fg(Color::White).add_modifier(Modifier::BOLD));
298
299 f.render_widget(barchart, chunks[0]);
300
301 let items: Vec<ListItem> = vec![
303 ListItem::new("src/main.rs - 85% stealth"),
304 ListItem::new("src/lib.rs - 92% stealth"),
305 ListItem::new("tests/integration.rs - 78% stealth"),
306 ListItem::new("README.md - 95% stealth"),
307 ];
308
309 let list = List::new(items)
310 .block(Block::default().borders(Borders::ALL).title("Recent Scans"))
311 .style(Style::default().fg(Color::White));
312
313 f.render_widget(list, chunks[1]);
314 }
315
316 fn draw_style(&self, f: &mut Frame, area: Rect) {
318 let style_info = vec![
319 Line::from("Style Profile:"),
320 Line::from(""),
321 Line::from(vec![
322 Span::raw(" Naming Convention: "),
323 Span::styled("snake_case", Style::default().fg(Color::Cyan)),
324 ]),
325 Line::from(vec![
326 Span::raw(" Comment Style: "),
327 Span::styled("Inline Only", Style::default().fg(Color::Cyan)),
328 ]),
329 Line::from(vec![
330 Span::raw(" Indentation: "),
331 Span::styled("4 spaces", Style::default().fg(Color::Cyan)),
332 ]),
333 Line::from(vec![
334 Span::raw(" Max Line Length: "),
335 Span::styled("100 chars", Style::default().fg(Color::Cyan)),
336 ]),
337 Line::from(vec![
338 Span::raw(" Commit Format: "),
339 Span::styled("Conventional", Style::default().fg(Color::Cyan)),
340 ]),
341 Line::from(""),
342 Line::from("Style learned from 127 commits and 45 files"),
343 ];
344
345 let paragraph = Paragraph::new(style_info)
346 .block(Block::default().borders(Borders::ALL).title("Current Style Profile"))
347 .wrap(Wrap { trim: true });
348
349 f.render_widget(paragraph, area);
350 }
351
352 fn draw_settings(&self, f: &mut Frame, area: Rect) {
354 let settings = vec![
355 Line::from("Configuration:"),
356 Line::from(""),
357 Line::from(vec![
358 Span::raw(" [x] Use Local Models"),
359 ]),
360 Line::from(vec![
361 Span::raw(" [x] Cloud Fallback"),
362 ]),
363 Line::from(vec![
364 Span::raw(" [ ] Auto Humanize"),
365 ]),
366 Line::from(vec![
367 Span::raw(" [ ] Jitter Enabled"),
368 ]),
369 Line::from(""),
370 Line::from("Detection Threshold: 0.15"),
371 Line::from("Entropy Level: 0.50"),
372 Line::from(""),
373 Line::from("Press 'q' to quit"),
374 ];
375
376 let paragraph = Paragraph::new(settings)
377 .block(Block::default().borders(Borders::ALL).title("Settings"))
378 .wrap(Wrap { trim: true });
379
380 f.render_widget(paragraph, area);
381 }
382
383 fn draw_footer(&self, f: &mut Frame, area: Rect) {
385 let footer = Line::from(vec![
386 Span::raw(" "),
387 Span::styled("PhantomDev", Style::default().fg(Color::Cyan)),
388 Span::raw(" | "),
389 Span::raw("Press "),
390 Span::styled("q", Style::default().add_modifier(Modifier::BOLD)),
391 Span::raw(" to quit"),
392 ]);
393
394 let paragraph = Paragraph::new(footer)
395 .style(Style::default().fg(Color::Gray))
396 .alignment(Alignment::Center);
397
398 f.render_widget(paragraph, area);
399 }
400
401 pub fn update_stealth_score(&mut self, score: StealthScore) {
403 self.stealth_score = Some(score);
404 }
405
406 pub fn add_detection_result(&mut self, result: DetectionResult) {
408 self.detection_results.push(result);
409 }
410}
411
412#[cfg(test)]
413mod tests {
414 use super::*;
415
416 #[test]
417 fn test_tui_creation() {
418 let tui = PhantomTui::new();
419 assert_eq!(tui.current_tab, Tab::Overview);
420 assert!(!tui.should_quit);
421 }
422
423 #[test]
424 fn test_tab_navigation() {
425 let mut tui = PhantomTui::new();
426 assert_eq!(tui.current_tab, Tab::Overview);
427
428 tui.handle_key(event::KeyEvent {
430 code: KeyCode::Right,
431 modifiers: event::KeyModifiers::empty(),
432 kind: event::KeyEventKind::Press,
433 state: event::KeyEventState::NONE,
434 });
435 assert_eq!(tui.current_tab, Tab::Detection);
436 }
437
438 #[test]
439 fn test_quit() {
440 let mut tui = PhantomTui::new();
441 assert!(!tui.should_quit);
442
443 tui.handle_key(event::KeyEvent {
444 code: KeyCode::Char('q'),
445 modifiers: event::KeyModifiers::empty(),
446 kind: event::KeyEventKind::Press,
447 state: event::KeyEventState::NONE,
448 });
449 assert!(tui.should_quit);
450 }
451}