netscli 0.3.2

Interactive TUI and CLI for network discovery, scanning, and diagnostics
use super::common::{fit_cell, tree_child_line};
use super::Formatter;
use ipnet::IpNet;
use netscli_core::{ArpEntry, InterfaceInfo};
use ratatui::style::{Color, Style};
use ratatui::text::{Line, Span};

impl Formatter {
    pub fn format_arp_table(entries: &[ArpEntry]) -> Vec<Line<'static>> {
        let mut lines = Vec::new();
        lines.push(Line::from(vec![
            Span::styled(
                format!("{:<16}", "IP Address"),
                Style::default().fg(Color::DarkGray),
            ),
            Span::raw(" "),
            Span::styled(
                format!("{:<18}", "MAC Address"),
                Style::default().fg(Color::DarkGray),
            ),
            Span::raw(" "),
            Span::styled(
                format!("{:<14}", "Interface"),
                Style::default().fg(Color::DarkGray),
            ),
            Span::raw(" "),
            Span::styled("Vendor", Style::default().fg(Color::DarkGray)),
        ]));

        for e in entries {
            let vendor = e.vendor.clone().unwrap_or_else(|| "-".to_string());
            let vendor_cell = fit_cell(&vendor, 28);
            let iface_cell = fit_cell(&e.interface, 14);
            lines.push(Line::from(vec![
                Span::styled(format!("{:<16}", e.ip), Style::default().fg(Color::Green)),
                Span::raw(" "),
                Span::styled(format!("{:<18}", e.mac), Style::default().fg(Color::Yellow)),
                Span::raw(" "),
                Span::styled(iface_cell, Style::default().fg(Color::Gray)),
                Span::raw(" "),
                Span::styled(vendor_cell, Style::default().fg(Color::Blue)),
            ]));
        }
        lines
    }

    pub fn format_interfaces(ifaces: &[InterfaceInfo]) -> Vec<Line<'static>> {
        let mut lines = Vec::new();
        lines.push(Line::from(vec![
            Span::styled("Interfaces ", Style::default().fg(Color::Cyan)),
            Span::styled(ifaces.len().to_string(), Style::default().fg(Color::White)),
        ]));
        lines.push(Line::default());

        if ifaces.is_empty() {
            lines.push(Line::from(Span::styled(
                "No interfaces detected.",
                Style::default().fg(Color::DarkGray),
            )));
            return lines;
        }

        for (idx, iface) in ifaces.iter().enumerate() {
            let mac = iface
                .mac
                .map(|m| m.to_string())
                .unwrap_or_else(|| "-".to_string());
            let state = if iface.is_up { "up" } else { "down" };
            let state_color = if iface.is_up {
                Color::Green
            } else {
                Color::Red
            };

            lines.push(Line::from(vec![
                Span::styled("interface ", Style::default().fg(Color::DarkGray)),
                Span::styled(iface.name.clone(), Style::default().fg(Color::Green)),
            ]));

            let mut children: Vec<(String, String, Color)> = Vec::new();
            children.push(("state".to_string(), state.to_string(), state_color));
            children.push((
                "loopback".to_string(),
                iface.is_loopback.to_string(),
                Color::Gray,
            ));
            children.push(("mac".to_string(), mac, Color::Yellow));
            for ip in &iface.ips {
                let label = match ip {
                    IpNet::V4(_) => "ipv4",
                    IpNet::V6(_) => "ipv6",
                };
                children.push((label.to_string(), ip.to_string(), Color::Gray));
            }

            let child_len = children.len();
            for (c_idx, (label, value, color)) in children.into_iter().enumerate() {
                let is_last = c_idx + 1 == child_len;
                lines.push(tree_child_line(&label, &value, is_last, color));
            }

            if idx + 1 < ifaces.len() {
                lines.push(Line::default());
            }
        }

        lines
    }
}