Skip to main content

netrunner_cli/
ui.rs

1use colored::*;
2use console::Term;
3use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
4use rand::RngExt as _;
5use std::sync::Arc;
6use tokio::sync::RwLock;
7
8use std::io::{self, Write};
9use std::thread;
10use std::time::Duration;
11
12use netrunner_core::TestConfig;
13
14// Bandwidth monitor state for real-time graph
15#[derive(Clone)]
16pub struct BandwidthMonitor {
17    pub speed_history: Arc<RwLock<Vec<f64>>>,
18    pub current_speed: Arc<RwLock<f64>>,
19    pub peak_speed: Arc<RwLock<f64>>,
20    pub is_final: Arc<RwLock<bool>>,
21    pub throbber_frame: Arc<RwLock<usize>>,
22    #[allow(dead_code)]
23    pub title: String,
24    pub label: String,
25}
26
27impl BandwidthMonitor {
28    pub fn new(title: String, label: String) -> Self {
29        Self {
30            speed_history: Arc::new(RwLock::new(Vec::new())),
31            current_speed: Arc::new(RwLock::new(0.0)),
32            peak_speed: Arc::new(RwLock::new(0.0)),
33            is_final: Arc::new(RwLock::new(false)),
34            throbber_frame: Arc::new(RwLock::new(0)),
35            title,
36            label,
37        }
38    }
39
40    pub async fn update(&self, speed: f64) {
41        let mut history = self.speed_history.write().await;
42        let mut current = self.current_speed.write().await;
43        let mut peak = self.peak_speed.write().await;
44        let mut frame = self.throbber_frame.write().await;
45
46        *current = speed;
47        *peak = peak.max(speed);
48        history.push(speed);
49
50        // Advance throbber animation (10 frames for complete circle)
51        *frame = (*frame + 1) % 10;
52
53        // Keep only last 100 samples for graph
54        if history.len() > 100 {
55            history.remove(0);
56        }
57    }
58
59    pub async fn mark_final(&self) {
60        let mut is_final = self.is_final.write().await;
61        *is_final = true;
62    }
63
64    pub async fn render_live(&self) -> io::Result<()> {
65        let history = self.speed_history.read().await;
66        let current = self.current_speed.read().await;
67        let peak = self.peak_speed.read().await;
68        let is_final = self.is_final.read().await;
69        let frame = self.throbber_frame.read().await;
70
71        // Display speed with throbber or checkmark
72        let throbber_chars = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
73        let indicator = if *is_final {
74            "✓"
75        } else {
76            &throbber_chars[*frame].to_string()
77        };
78
79        println!(
80            "{} {}: {}",
81            indicator.bright_cyan(),
82            self.label.bright_blue().bold(),
83            format!("{:.1} Mbps", current).bright_green().bold()
84        );
85        println!();
86        println!(
87            "{} {}",
88            "Peak:".bright_cyan(),
89            format!("{:.1} Mbps", peak).bright_cyan()
90        );
91        println!();
92
93        // Create filled area graph
94        let max_val = if history.is_empty() {
95            1.0
96        } else {
97            history.iter().cloned().fold(0.0f64, f64::max).max(1.0)
98        };
99        let width = 80; // Full terminal width
100        let height = 8; // Height of graph
101
102        // Generate graph lines with filled area
103        for row in (0..height).rev() {
104            let threshold = (row as f64 / height as f64) * max_val;
105            print!("│");
106
107            if history.is_empty() {
108                // Show empty graph
109                for _ in 0..width {
110                    print!(" ");
111                }
112            } else {
113                // Take the most recent samples up to width
114                let samples_to_show = history.len().min(width);
115                let start_idx = history.len().saturating_sub(width);
116
117                for i in start_idx..history.len() {
118                    let speed = history[i];
119                    let char = if speed >= threshold { "█" } else { " " };
120                    print!("{}", char.bright_yellow());
121                }
122
123                // Fill remaining space if we have fewer samples than width
124                for _ in 0..(width - samples_to_show) {
125                    print!(" ");
126                }
127            }
128
129            println!();
130        }
131
132        // Bottom axis
133        print!("└");
134        for _ in 0..width {
135            print!("─");
136        }
137        println!();
138
139        std::io::stdout().flush()?;
140        Ok(())
141    }
142
143    pub async fn render_live_update(&self) -> io::Result<()> {
144        let history = self.speed_history.read().await;
145        let current = self.current_speed.read().await;
146        let peak = self.peak_speed.read().await;
147        let is_final = self.is_final.read().await;
148        let frame = self.throbber_frame.read().await;
149
150        // Move cursor up 13 lines and clear them
151        print!("\x1B[13A"); // Move up 13 lines
152        for _ in 0..13 {
153            print!("\x1B[2K"); // Clear line
154            print!("\x1B[1B"); // Move down 1 line
155        }
156        print!("\x1B[13A"); // Move back up to start position
157
158        // Display speed with throbber or checkmark
159        let throbber_chars = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
160        let indicator = if *is_final {
161            "✓"
162        } else {
163            &throbber_chars[*frame].to_string()
164        };
165
166        println!(
167            "{} {}: {}",
168            indicator.bright_cyan(),
169            self.label.bright_blue().bold(),
170            format!("{:.1} Mbps", current).bright_green().bold()
171        );
172        println!();
173        println!(
174            "{} {}",
175            "Peak:".bright_cyan(),
176            format!("{:.1} Mbps", peak).bright_cyan()
177        );
178        println!();
179
180        // Create filled area graph
181        let max_val = if history.is_empty() {
182            1.0
183        } else {
184            history.iter().cloned().fold(0.0f64, f64::max).max(1.0)
185        };
186        let width = 80;
187        let height = 8;
188
189        // Generate graph lines with filled area
190        for row in (0..height).rev() {
191            let threshold = (row as f64 / height as f64) * max_val;
192            print!("│");
193
194            if history.is_empty() {
195                for _ in 0..width {
196                    print!(" ");
197                }
198            } else {
199                let samples_to_show = history.len().min(width);
200                let start_idx = history.len().saturating_sub(width);
201
202                for i in start_idx..history.len() {
203                    let speed = history[i];
204                    let char = if speed >= threshold { "█" } else { " " };
205                    print!("{}", char.bright_yellow());
206                }
207
208                for _ in 0..(width - samples_to_show) {
209                    print!(" ");
210                }
211            }
212
213            println!();
214        }
215
216        // Bottom axis
217        print!("└");
218        for _ in 0..width {
219            print!("─");
220        }
221        println!();
222
223        std::io::stdout().flush()?;
224        Ok(())
225    }
226}
227
228pub struct UI {
229    term: Term,
230    multi_progress: MultiProgress,
231}
232
233impl UI {
234    pub fn new(_config: TestConfig) -> Self {
235        Self {
236            term: Term::stdout(),
237            multi_progress: MultiProgress::new(),
238        }
239    }
240
241    pub fn clear_screen(&self) -> io::Result<()> {
242        self.term.clear_screen()
243    }
244
245    pub fn show_welcome_banner(&self) -> io::Result<()> {
246        self.term.clear_screen()?;
247
248        let banner = r#"
249 _   _ ______ _______ _____  _    _ _   _ _   _ ______ _____
250| \ | |  ____|__   __|  __ \| |  | | \ | | \ | |  ____|  __ \
251|  \| | |__     | |  | |__) | |  | |  \| |  \| | |__  | |__) |
252| . ` |  __|    | |  |  _  /| |  | | . ` | . ` |  __| |  _  /
253| |\  | |____   | |  | | \ \| |__| | |\  | |\  | |____| | \ \
254|_| \_|______|  |_|  |_|  \_\\____/|_| \_|_| \_|______|_|  \_\
255
256        "#;
257
258        println!("{}", banner.bright_cyan());
259
260        println!("{}", "SYSTEM STATUS".bright_magenta().bold());
261        println!("{}", "⟨⟨⟨ NEURAL INTERFACE: ONLINE ⟩⟩⟩".bright_green());
262        println!("{}", "⟨⟨⟨ NETWORK SCANNER: INITIALIZED ⟩⟩⟩".bright_green());
263        println!("{}", "⟨⟨⟨ QUANTUM DIAGNOSTICS: READY ⟩⟩⟩".bright_green());
264        println!();
265        println!(
266            "{}",
267            ">>> JACK IN AND ANALYZE YOUR DIGITAL HIGHWAY <<<"
268                .bright_yellow()
269                .bold()
270        );
271        println!(
272            "{}",
273            ">>> DATA FLOWS | PACKET STREAMS | NEURAL PATHS <<<".bright_blue()
274        );
275        println!();
276
277        Ok(())
278    }
279
280    pub fn create_progress_bar(&self, len: u64, message: &str) -> ProgressBar {
281        let pb = self.multi_progress.add(ProgressBar::new(len));
282        pb.set_style(
283            ProgressStyle::default_bar()
284                .template(
285                    "{spinner:.green} {msg} [{bar:40.cyan/blue}] {percent}% [{elapsed_precise}]",
286                )
287                .unwrap()
288                .progress_chars("━━╸─"),
289        );
290        pb.set_message(message.to_string());
291        pb
292    }
293
294    pub fn create_speed_test_spinner(&self, message: &str) -> ProgressBar {
295        let pb = self.multi_progress.add(ProgressBar::new_spinner());
296        pb.set_style(
297            ProgressStyle::default_spinner()
298                .template("{spinner:.bright_cyan} {msg}")
299                .unwrap()
300                .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]),
301        );
302        pb.set_message(message.to_string());
303        pb.enable_steady_tick(Duration::from_millis(80));
304        pb
305    }
306
307    pub fn create_download_spinner(&self, message: &str) -> ProgressBar {
308        let pb = self.multi_progress.add(ProgressBar::new_spinner());
309        pb.set_style(
310            ProgressStyle::default_spinner()
311                .template("{spinner:.bright_green} {msg}")
312                .unwrap()
313                .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]),
314        );
315        pb.set_message(message.to_string());
316        pb.enable_steady_tick(Duration::from_millis(80));
317        pb
318    }
319
320    pub fn create_upload_spinner(&self, message: &str) -> ProgressBar {
321        let pb = self.multi_progress.add(ProgressBar::new_spinner());
322        pb.set_style(
323            ProgressStyle::default_spinner()
324                .template("{spinner:.bright_blue} {msg}")
325                .unwrap()
326                .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]),
327        );
328        pb.set_message(message.to_string());
329        pb.enable_steady_tick(Duration::from_millis(80));
330        pb
331    }
332
333    pub fn create_ping_spinner(&self, message: &str) -> ProgressBar {
334        let pb = self.multi_progress.add(ProgressBar::new_spinner());
335        pb.set_style(
336            ProgressStyle::default_spinner()
337                .template("{spinner:.bright_magenta} {msg}")
338                .unwrap()
339                .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]),
340        );
341        pb.set_message(message.to_string());
342        pb.enable_steady_tick(Duration::from_millis(80));
343        pb
344    }
345
346    pub fn create_spinner(&self, message: &str) -> ProgressBar {
347        let pb = self.multi_progress.add(ProgressBar::new_spinner());
348        pb.set_style(
349            ProgressStyle::default_spinner()
350                .template("{spinner:.bright_green} {msg}")
351                .unwrap()
352                .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]),
353        );
354        pb.set_message(message.to_string());
355        pb.enable_steady_tick(Duration::from_millis(100));
356        pb
357    }
358
359    pub fn create_cyberpunk_spinner(&self, message: &str) -> ProgressBar {
360        let pb = self.multi_progress.add(ProgressBar::new_spinner());
361        pb.set_style(
362            ProgressStyle::default_spinner()
363                .template("{spinner:.bright_cyan} {msg}")
364                .unwrap()
365                .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]),
366        );
367        pb.set_message(message.to_string());
368        pb.enable_steady_tick(Duration::from_millis(80));
369        pb
370    }
371
372    pub fn create_dna_helix_spinner(&self, message: &str) -> ProgressBar {
373        let pb = self.multi_progress.add(ProgressBar::new_spinner());
374        pb.set_style(
375            ProgressStyle::default_spinner()
376                .template("{spinner:.bright_green} {msg}")
377                .unwrap()
378                .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]),
379        );
380        pb.set_message(message.to_string());
381        pb.enable_steady_tick(Duration::from_millis(80));
382        pb
383    }
384
385    pub fn create_rocket_spinner(&self, message: &str) -> ProgressBar {
386        let pb = self.multi_progress.add(ProgressBar::new_spinner());
387        pb.set_style(
388            ProgressStyle::default_spinner()
389                .template("{spinner:.bright_yellow} {msg}")
390                .unwrap()
391                .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]),
392        );
393        pb.set_message(message.to_string());
394        pb.enable_steady_tick(Duration::from_millis(80));
395        pb
396    }
397
398    pub fn create_wave_spinner(&self, message: &str) -> ProgressBar {
399        let pb = self.multi_progress.add(ProgressBar::new_spinner());
400        pb.set_style(
401            ProgressStyle::default_spinner()
402                .template("{spinner:.bright_cyan} {msg}")
403                .unwrap()
404                .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]),
405        );
406        pb.set_message(message.to_string());
407        pb.enable_steady_tick(Duration::from_millis(80));
408        pb
409    }
410
411    pub fn create_network_scanner_bar(&self, message: &str) -> ProgressBar {
412        let pb = self.multi_progress.add(ProgressBar::new_spinner());
413        pb.set_style(
414            ProgressStyle::default_spinner()
415                .template("{spinner:.bright_yellow} {msg}")
416                .unwrap()
417                .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]),
418        );
419        pb.set_message(message.to_string());
420        pb.enable_steady_tick(Duration::from_millis(80));
421        pb
422    }
423
424    pub fn create_pacman_spinner(&self, message: &str) -> ProgressBar {
425        let pb = self.multi_progress.add(ProgressBar::new_spinner());
426        pb.set_style(
427            ProgressStyle::default_spinner()
428                .template("{spinner:.bright_yellow} {msg}")
429                .unwrap()
430                .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]),
431        );
432        pb.set_message(message.to_string());
433        pb.enable_steady_tick(Duration::from_millis(80));
434        pb
435    }
436
437    pub fn show_section_header(&self, title: &str) -> io::Result<()> {
438        println!();
439        println!(
440            "{}",
441            format!(">>> {} <<<", title.to_uppercase())
442                .bright_magenta()
443                .bold()
444        );
445        Ok(())
446    }
447
448    pub fn show_error(&self, message: &str) -> io::Result<()> {
449        println!("{} {}", "ERROR:".bright_red().bold(), message.bright_red());
450        Ok(())
451    }
452
453    pub fn show_info(&self, message: &str) -> io::Result<()> {
454        println!("{} {}", "INFO:".bright_blue().bold(), message.bright_blue());
455        Ok(())
456    }
457
458    pub fn show_typing_effect(&self, text: &str) -> io::Result<()> {
459        for char in text.chars() {
460            print!("{}", char.to_string().bright_green());
461            std::io::stdout().flush()?;
462            thread::sleep(Duration::from_millis(50));
463        }
464        println!();
465        Ok(())
466    }
467
468    pub fn show_matrix_effect(&self, lines: usize) -> io::Result<()> {
469        let matrix_chars = ["0", "1", "⠁", "⠂", "⠄", "⡀", "⢀", "⠠", "⠐", "⠈"];
470
471        for _ in 0..lines {
472            print!("{}", "█".bright_green());
473            for _ in 0..60 {
474                let idx = rand::rng().random_range(0..matrix_chars.len());
475                print!("{}", matrix_chars[idx].bright_green());
476                thread::sleep(Duration::from_millis(20));
477            }
478            println!();
479        }
480        Ok(())
481    }
482
483    pub fn show_pulse_text(&self, text: &str, pulses: usize) -> io::Result<()> {
484        for _ in 0..pulses {
485            print!("\r{}", text.bright_cyan().bold());
486            std::io::stdout().flush()?;
487            thread::sleep(Duration::from_millis(500));
488
489            print!("\r{}", text.bright_blue());
490            std::io::stdout().flush()?;
491            thread::sleep(Duration::from_millis(500));
492        }
493        println!();
494        Ok(())
495    }
496
497    pub fn show_connection_establishing(&self) -> io::Result<()> {
498        let steps = [
499            "⟨⟨⟨ INITIALIZING NEURAL INTERFACE ⟩⟩⟩",
500            "⟨⟨⟨ SCANNING NETWORK TOPOLOGY ⟩⟩⟩",
501            "⟨⟨⟨ ESTABLISHING QUANTUM TUNNEL ⟩⟩⟩",
502            "⟨⟨⟨ CALIBRATING DATA STREAMS ⟩⟩⟩",
503            "⟨⟨⟨ CONNECTION ESTABLISHED ⟩⟩⟩",
504        ];
505
506        for step in steps.iter() {
507            println!("{}", step.bright_magenta());
508            thread::sleep(Duration::from_millis(800));
509        }
510        println!();
511        Ok(())
512    }
513
514    pub fn create_bandwidth_monitor(&self, title: &str, label: &str) -> BandwidthMonitor {
515        BandwidthMonitor::new(title.to_string(), label.to_string())
516    }
517}