ant_quic/
terminal_ui.rs

1// Copyright 2024 Saorsa Labs Ltd.
2//
3// This Saorsa Network Software is licensed under the General Public License (GPL), version 3.
4// Please see the file LICENSE-GPL, or visit <http://www.gnu.org/licenses/> for the full text.
5//
6// Full details available at https://saorsalabs.com/licenses
7
8//! Terminal UI formatting and display helpers for ant-quic
9//!
10//! Provides colored output, formatting, and visual elements for better UX
11
12use std::net::{IpAddr, SocketAddr};
13use tracing::Level;
14use tracing_subscriber::fmt::{FormatFields, format::Writer};
15use unicode_width::UnicodeWidthStr;
16// use four_word_networking::FourWordAdaptiveEncoder; // TODO: Add this dependency or implement locally
17
18/// ANSI color codes for terminal output
19pub mod colors {
20    /// Reset all formatting
21    pub const RESET: &str = "\x1b[0m";
22    /// Bold text
23    pub const BOLD: &str = "\x1b[1m";
24    /// Dim text
25    pub const DIM: &str = "\x1b[2m";
26
27    // Regular colors
28    /// Black foreground
29    pub const BLACK: &str = "\x1b[30m";
30    /// Red foreground
31    pub const RED: &str = "\x1b[31m";
32    /// Green foreground
33    pub const GREEN: &str = "\x1b[32m";
34    /// Yellow foreground
35    pub const YELLOW: &str = "\x1b[33m";
36    /// Blue foreground
37    pub const BLUE: &str = "\x1b[34m";
38    /// Magenta foreground
39    pub const MAGENTA: &str = "\x1b[35m";
40    /// Cyan foreground
41    pub const CYAN: &str = "\x1b[36m";
42    /// White foreground
43    pub const WHITE: &str = "\x1b[37m";
44
45    // Bright colors
46    /// Bright black foreground
47    pub const BRIGHT_BLACK: &str = "\x1b[90m";
48    /// Bright red foreground
49    pub const BRIGHT_RED: &str = "\x1b[91m";
50    /// Bright green foreground
51    pub const BRIGHT_GREEN: &str = "\x1b[92m";
52    /// Bright yellow foreground
53    pub const BRIGHT_YELLOW: &str = "\x1b[93m";
54    /// Bright blue foreground
55    pub const BRIGHT_BLUE: &str = "\x1b[94m";
56    /// Bright magenta foreground
57    pub const BRIGHT_MAGENTA: &str = "\x1b[95m";
58    /// Bright cyan foreground
59    pub const BRIGHT_CYAN: &str = "\x1b[96m";
60    /// Bright white foreground
61    pub const BRIGHT_WHITE: &str = "\x1b[97m";
62}
63
64/// Unicode symbols for visual indicators
65pub mod symbols {
66    /// Success indicator (check mark)
67    pub const CHECK: &str = "✓";
68    /// Error indicator (cross mark)
69    pub const CROSS: &str = "✗";
70    /// Information indicator (info symbol)
71    pub const INFO: &str = "ℹ";
72    /// Warning indicator (warning triangle)
73    pub const WARNING: &str = "⚠";
74    /// Right arrow glyph
75    pub const ARROW_RIGHT: &str = "→";
76    /// Bullet point glyph
77    pub const DOT: &str = "•";
78    /// Key glyph (used for authentication)
79    pub const KEY: &str = "🔑";
80    /// Network antenna glyph
81    pub const NETWORK: &str = "📡";
82    /// Globe glyph (used for public network)
83    pub const GLOBE: &str = "🌐";
84    /// Rocket glyph (used for startup)
85    pub const ROCKET: &str = "🚀";
86    /// Hourglass glyph (used for waiting)
87    pub const HOURGLASS: &str = "⏳";
88    /// Circular arrows glyph (used for retry/progress)
89    pub const CIRCULAR_ARROWS: &str = "⟳";
90}
91
92/// Box drawing characters for borders
93pub mod box_chars {
94    /// Top-left box corner
95    pub const TOP_LEFT: &str = "╭";
96    /// Top-right box corner
97    pub const TOP_RIGHT: &str = "╮";
98    /// Bottom-left box corner
99    pub const BOTTOM_LEFT: &str = "╰";
100    /// Bottom-right box corner
101    pub const BOTTOM_RIGHT: &str = "╯";
102    /// Horizontal line
103    pub const HORIZONTAL: &str = "─";
104    /// Vertical line
105    pub const VERTICAL: &str = "│";
106    /// T-junction left
107    pub const T_LEFT: &str = "├";
108    /// T-junction right
109    pub const T_RIGHT: &str = "┤";
110}
111
112/// Check if an IPv6 address is link-local (fe80::/10)
113fn is_ipv6_link_local(ip: &std::net::Ipv6Addr) -> bool {
114    let octets = ip.octets();
115    (octets[0] == 0xfe) && ((octets[1] & 0xc0) == 0x80)
116}
117
118/// Check if an IPv6 address is unique local (fc00::/7)
119fn is_ipv6_unique_local(ip: &std::net::Ipv6Addr) -> bool {
120    let octets = ip.octets();
121    (octets[0] & 0xfe) == 0xfc
122}
123
124/// Check if an IPv6 address is multicast (ff00::/8)
125fn is_ipv6_multicast(ip: &std::net::Ipv6Addr) -> bool {
126    let octets = ip.octets();
127    octets[0] == 0xff
128}
129
130/// Format a peer ID with color (shows first 8 chars)
131pub fn format_peer_id(peer_id: &[u8; 32]) -> String {
132    let hex = hex::encode(&peer_id[..4]);
133    format!("{}{}{}{}", colors::CYAN, hex, "...", colors::RESET)
134}
135
136/// Format an address with appropriate coloring
137pub fn format_address(addr: &SocketAddr) -> String {
138    let color = match addr.ip() {
139        IpAddr::V4(ip) => {
140            if ip.is_loopback() {
141                colors::DIM
142            } else if ip.is_private() {
143                colors::YELLOW
144            } else {
145                colors::GREEN
146            }
147        }
148        IpAddr::V6(ip) => {
149            if ip.is_loopback() {
150                colors::DIM
151            } else if ip.is_unspecified() {
152                colors::DIM
153            } else if is_ipv6_link_local(&ip) {
154                colors::YELLOW
155            } else if is_ipv6_unique_local(&ip) {
156                colors::CYAN
157            } else {
158                colors::BRIGHT_CYAN
159            }
160        }
161    };
162
163    format!("{}{}{}", color, addr, colors::RESET)
164}
165
166/// Format an address as four words with original address in brackets
167pub fn format_address_with_words(addr: &SocketAddr) -> String {
168    // TODO: Implement four-word encoding or add dependency
169    // For now, just return the colored address
170    format_address(addr)
171}
172
173/// Categorize and describe an IP address
174pub fn describe_address(addr: &SocketAddr) -> &'static str {
175    match addr.ip() {
176        IpAddr::V4(ip) => {
177            if ip.is_loopback() {
178                "loopback"
179            } else if ip.is_private() {
180                "private network"
181            } else if ip.is_link_local() {
182                "link-local"
183            } else {
184                "public"
185            }
186        }
187        IpAddr::V6(ip) => {
188            if ip.is_loopback() {
189                "IPv6 loopback"
190            } else if ip.is_unspecified() {
191                "IPv6 unspecified"
192            } else if is_ipv6_link_local(&ip) {
193                "IPv6 link-local"
194            } else if is_ipv6_unique_local(&ip) {
195                "IPv6 unique local"
196            } else if is_ipv6_multicast(&ip) {
197                "IPv6 multicast"
198            } else {
199                "IPv6 global"
200            }
201        }
202    }
203}
204
205/// Draw a box with title and content
206pub fn draw_box(title: &str, width: usize) -> (String, String, String) {
207    let padding = width.saturating_sub(title.width() + 4);
208    let left_pad = padding / 2;
209    let right_pad = padding - left_pad;
210
211    let top = format!(
212        "{}{} {} {}{}{}",
213        box_chars::TOP_LEFT,
214        box_chars::HORIZONTAL.repeat(left_pad),
215        title,
216        box_chars::HORIZONTAL.repeat(right_pad),
217        box_chars::HORIZONTAL,
218        box_chars::TOP_RIGHT
219    );
220
221    let middle = format!("{} {{}} {}", box_chars::VERTICAL, box_chars::VERTICAL);
222
223    let bottom = format!(
224        "{}{}{}",
225        box_chars::BOTTOM_LEFT,
226        box_chars::HORIZONTAL.repeat(width - 2),
227        box_chars::BOTTOM_RIGHT
228    );
229
230    (top, middle, bottom)
231}
232
233/// Print the startup banner
234pub fn print_banner(version: &str) {
235    let title = format!("ant-quic v{version}");
236    let (top, middle, bottom) = draw_box(&title, 60);
237
238    println!("{top}");
239    println!(
240        "{}",
241        middle.replace(
242            "{}",
243            "Starting QUIC P2P with NAT Traversal                 "
244        )
245    );
246    println!("{bottom}");
247    println!();
248}
249
250/// Print a section header
251pub fn print_section(icon: &str, title: &str) {
252    println!("{} {}{}{}", icon, colors::BOLD, title, colors::RESET);
253}
254
255/// Print an item with bullet point
256pub fn print_item(text: &str, indent: usize) {
257    let indent_str = " ".repeat(indent);
258    println!("{}{} {}", indent_str, symbols::DOT, text);
259}
260
261/// Print a status line with icon
262pub fn print_status(icon: &str, text: &str, color: &str) {
263    println!("  {} {}{}{}", icon, color, text, colors::RESET);
264}
265
266/// Format bytes into human-readable size
267pub fn format_bytes(bytes: u64) -> String {
268    const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
269    let mut size = bytes as f64;
270    let mut unit_index = 0;
271
272    while size >= 1024.0 && unit_index < UNITS.len() - 1 {
273        size /= 1024.0;
274        unit_index += 1;
275    }
276
277    if unit_index == 0 {
278        format!("{} {}", size as u64, UNITS[unit_index])
279    } else {
280        format!("{:.1} {}", size, UNITS[unit_index])
281    }
282}
283
284/// Format duration into human-readable time
285pub fn format_duration(duration: std::time::Duration) -> String {
286    let total_seconds = duration.as_secs();
287    let hours = total_seconds / 3600;
288    let minutes = (total_seconds % 3600) / 60;
289    let seconds = total_seconds % 60;
290
291    format!("{hours:02}:{minutes:02}:{seconds:02}")
292}
293
294/// Format timestamp into HH:MM:SS format
295pub fn format_timestamp(_timestamp: std::time::Instant) -> String {
296    use std::time::SystemTime;
297
298    // This is a simplified timestamp - in a real app you'd want proper time handling
299    let now = SystemTime::now();
300    let duration_since_epoch = now
301        .duration_since(SystemTime::UNIX_EPOCH)
302        .unwrap_or(std::time::Duration::ZERO);
303
304    let total_seconds = duration_since_epoch.as_secs();
305    let hours = (total_seconds % 86400) / 3600;
306    let minutes = (total_seconds % 3600) / 60;
307    let seconds = total_seconds % 60;
308
309    format!("{hours:02}:{minutes:02}:{seconds:02}")
310}
311
312/// Custom log formatter that adds colors and symbols
313pub struct ColoredLogFormatter;
314
315impl<S, N> tracing_subscriber::fmt::FormatEvent<S, N> for ColoredLogFormatter
316where
317    S: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>,
318    N: for<'a> FormatFields<'a> + 'static,
319{
320    fn format_event(
321        &self,
322        ctx: &tracing_subscriber::fmt::FmtContext<'_, S, N>,
323        mut writer: Writer<'_>,
324        event: &tracing::Event<'_>,
325    ) -> std::fmt::Result {
326        let metadata = event.metadata();
327        let level = metadata.level();
328
329        // Choose color and symbol based on level
330        let (color, symbol) = match *level {
331            Level::ERROR => (colors::RED, symbols::CROSS),
332            Level::WARN => (colors::YELLOW, symbols::WARNING),
333            Level::INFO => (colors::GREEN, symbols::CHECK),
334            Level::DEBUG => (colors::BLUE, symbols::INFO),
335            Level::TRACE => (colors::DIM, symbols::DOT),
336        };
337
338        // Write colored output
339        write!(&mut writer, "{color}{symbol} ")?;
340
341        // Write the message
342        ctx.field_format().format_fields(writer.by_ref(), event)?;
343
344        write!(&mut writer, "{}", colors::RESET)?;
345
346        writeln!(writer)
347    }
348}
349
350/// Progress indicator for operations
351pub struct ProgressIndicator {
352    message: String,
353    frames: Vec<&'static str>,
354    current_frame: usize,
355}
356
357impl ProgressIndicator {
358    /// Create a new progress indicator with a message
359    pub fn new(message: String) -> Self {
360        Self {
361            message,
362            frames: vec!["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
363            current_frame: 0,
364        }
365    }
366
367    /// Advance the spinner by one frame and redraw
368    pub fn tick(&mut self) {
369        print!(
370            "\r{} {} {} ",
371            self.frames[self.current_frame],
372            colors::BLUE,
373            self.message
374        );
375        self.current_frame = (self.current_frame + 1) % self.frames.len();
376        use std::io::{self, Write};
377        let _ = io::stdout().flush(); // Ignore flush errors in terminal UI
378    }
379
380    /// Finish the progress indicator with a success message
381    pub fn finish_success(&self, message: &str) {
382        println!(
383            "\r{} {}{}{} {}",
384            symbols::CHECK,
385            colors::GREEN,
386            self.message,
387            colors::RESET,
388            message
389        );
390    }
391
392    /// Finish the progress indicator with an error message
393    pub fn finish_error(&self, message: &str) {
394        println!(
395            "\r{} {}{}{} {}",
396            symbols::CROSS,
397            colors::RED,
398            self.message,
399            colors::RESET,
400            message
401        );
402    }
403}