Skip to main content

phantomdev_tui/
lib.rs

1//! PhantomDev TUI Dashboard
2//!
3//! This crate provides a terminal user interface for visualizing
4//! stealth scores, detection results, and style profiles.
5//!
6//! Built with ❤️ by John Varghese (J0X)
7//! GitHub: https://github.com/John-Varghese-EH
8//! LinkedIn: https://linkedin.com/in/John--Varghese
9
10use 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
30/// Main TUI application
31pub struct PhantomTui {
32    /// Current tab
33    current_tab: Tab,
34    /// Stealth score
35    stealth_score: Option<StealthScore>,
36    /// Detection results
37    detection_results: Vec<DetectionResult>,
38    /// Should quit
39    should_quit: bool,
40}
41
42/// Available tabs
43#[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    /// Create a new TUI application
68    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    /// Run the TUI application
78    pub fn run(&mut self) -> Result<()> {
79        // Setup terminal
80        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        // Main loop
87        while !self.should_quit {
88            terminal.draw(|f| self.draw(f))?;
89
90            // Handle events
91            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        // Restore terminal
99        disable_raw_mode()?;
100        execute!(
101            terminal.backend_mut(),
102            LeaveAlternateScreen,
103            DisableMouseCapture
104        )?;
105
106        Ok(())
107    }
108
109    /// Handle keyboard input
110    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    /// Draw the UI
138    fn draw(&self, f: &mut Frame) {
139        let size = f.size();
140
141        // Create main layout
142        let chunks = Layout::default()
143            .direction(Direction::Vertical)
144            .margin(1)
145            .constraints([
146                Constraint::Length(3),  // Header
147                Constraint::Min(0),     // Content
148                Constraint::Length(1),  // Footer
149            ])
150            .split(size);
151
152        // Draw header
153        self.draw_header(f, chunks[0]);
154
155        // Draw content based on current tab
156        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        // Draw footer
164        self.draw_footer(f, chunks[2]);
165    }
166
167    /// Draw header with tabs
168    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    /// Draw overview tab
197    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        // Draw stealth score gauge
207        self.draw_stealth_score(f, chunks[0]);
208
209        // Draw summary
210        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    /// Draw stealth score gauge
237    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    /// Draw detection tab
268    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        // Draw pattern breakdown
278        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        // Draw recent detections
302        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    /// Draw style tab
317    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    /// Draw settings tab
353    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    /// Draw footer
384    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    /// Update stealth score
402    pub fn update_stealth_score(&mut self, score: StealthScore) {
403        self.stealth_score = Some(score);
404    }
405
406    /// Add detection result
407    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        // Simulate right key press
429        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}