use ratatui::layout::{Constraint, Rect};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{
Block, Borders, Cell, Row, Scrollbar, ScrollbarOrientation, ScrollbarState, Table,
};
use ratatui::Frame;
use crate::app::App;
use crate::network::dns::port_service_name;
use crate::types::TcpState;
pub fn draw_connections(f: &mut Frame, area: Rect, app: &App) {
let filtered = app.filtered_connections();
let total = filtered.len();
let sort_ind = |col: usize| -> &str {
if app.sort_column == col {
if app.sort_ascending { " \u{25B2}" } else { " \u{25BC}" }
} else {
""
}
};
let hdr_style = Style::default()
.fg(Color::Rgb(160, 180, 220))
.add_modifier(Modifier::BOLD);
let header = Row::new(vec![
Cell::from(Span::styled(format!("Process{}", sort_ind(6)), hdr_style)),
Cell::from(Span::styled(format!("PID{}", sort_ind(7)), hdr_style)),
Cell::from(Span::styled(format!("Remote Host{}", sort_ind(3)), hdr_style)),
Cell::from(Span::styled("Geo", hdr_style)),
Cell::from(Span::styled(format!("Service{}", sort_ind(4)), hdr_style)),
Cell::from(Span::styled(format!("State{}", sort_ind(5)), hdr_style)),
Cell::from(Span::styled(format!("Local{}", sort_ind(2)), hdr_style)),
])
.height(1)
.style(Style::default().bg(Color::Rgb(18, 25, 42)));
let visible_height = area.height.saturating_sub(5) as usize;
let selected = if total > 0 { app.conn_scroll.min(total - 1) } else { 0 };
let viewport_start = if total <= visible_height {
0
} else {
let half = visible_height / 2;
if selected <= half {
0
} else if selected >= total.saturating_sub(half) {
total.saturating_sub(visible_height)
} else {
selected.saturating_sub(half)
}
};
let rows: Vec<Row> = filtered
.iter()
.enumerate()
.skip(viewport_start)
.take(visible_height)
.map(|(idx, conn)| {
let is_selected = idx == selected;
let proc_name = &conn.process_name;
let proc_display = {
let base = if let Some(module) = conn.module_name.as_ref() {
module.clone()
} else if proc_name.starts_with("PID:") {
format!("[{}]", &proc_name[4..])
} else {
proc_name.clone()
};
let base = if base.chars().count() > 18 {
format!("{}…", base.chars().take(17).collect::<String>())
} else {
base
};
if is_selected { format!("\u{25B8} {}", base) } else { format!(" {}", base) }
};
let proc_color = if conn.module_name.is_some() {
Color::Rgb(190, 140, 255)
} else if proc_name.starts_with("PID:") || proc_name.starts_with('[') {
Color::Rgb(90, 100, 125)
} else {
Color::Rgb(130, 200, 140)
};
let pid_str = conn.pid.to_string();
let (remote_display, remote_color) = match (&conn.dns_hostname, conn.remote_addr) {
(Some(dns), _) if dns != "localhost" => {
let host = if dns.chars().count() > 44 { format!("{}…", dns.chars().take(43).collect::<String>()) } else { dns.clone() };
(format!("\u{2192} {}", host), Color::Rgb(100, 220, 255))
}
(Some(_), Some(ip)) if ip.is_loopback() => {
("\u{2192} localhost".to_string(), Color::Rgb(75, 85, 108))
}
(None, Some(ip)) if ip.is_unspecified() => {
("*".to_string(), Color::Rgb(55, 65, 85))
}
(None, Some(ip)) => {
(format!("\u{2192} {}", ip), Color::Rgb(155, 170, 195))
}
_ => ("*".to_string(), Color::Rgb(55, 65, 85)),
};
let remote_bold = conn.dns_hostname.is_some()
&& conn.dns_hostname.as_deref() != Some("localhost");
let port = conn.remote_port.unwrap_or(conn.local_port);
let proto = conn.proto.label();
let svc_name = port_service_name(port);
let service_str = if let Some(svc) = svc_name {
format!("{}/{}", svc, proto)
} else {
format!("{}/{}", port, proto)
};
let service_color = match svc_name {
Some("HTTPS") => Color::Rgb(80, 200, 120),
Some("HTTP") => Color::Rgb(220, 180, 60),
Some("DNS") => Color::Rgb(100, 180, 255),
Some("SSH") => Color::Rgb(180, 130, 255),
Some("RDP") => Color::Rgb(255, 150, 100),
_ => Color::Rgb(180, 170, 130),
};
let state_str = conn
.state
.as_ref()
.map(|s| s.label().to_string())
.unwrap_or_else(|| "-".to_string());
let state_color = conn
.state
.as_ref()
.map(|s| s.color())
.unwrap_or(Color::Rgb(80, 100, 140));
let (geo_str, geo_color) = match conn.remote_addr {
Some(ip) if !ip.is_loopback() && !ip.is_unspecified() => {
match app.geoip.lookup(ip) {
Some(info) => (
info.code.to_string(),
Color::Rgb(170, 200, 230),
),
None => ("-".to_string(), Color::Rgb(55, 65, 85)),
}
}
_ => ("-".to_string(), Color::Rgb(55, 65, 85)),
};
let local_str = conn.local_port.to_string();
let dim = matches!(
conn.state.as_ref(),
Some(TcpState::Listen)
| Some(TcpState::Closed)
| Some(TcpState::TimeWait)
| Some(TcpState::DeleteTcb)
);
let row_bg = if is_selected {
Color::Rgb(25, 45, 85)
} else if dim {
Color::Rgb(8, 10, 18)
} else {
Color::Rgb(12, 16, 28)
};
Row::new(vec![
Cell::from(Span::styled(proc_display, Style::default().fg(proc_color))),
Cell::from(Span::styled(
pid_str,
Style::default().fg(if dim {
Color::Rgb(60, 70, 90)
} else {
Color::Rgb(120, 130, 160)
}),
)),
Cell::from(Span::styled(
remote_display,
Style::default().fg(remote_color).add_modifier(
if remote_bold && !dim {
Modifier::BOLD
} else {
Modifier::empty()
},
),
)),
Cell::from(Span::styled(
geo_str,
Style::default().fg(if dim { Color::Rgb(50, 55, 70) } else { geo_color }),
)),
Cell::from(Span::styled(
service_str,
Style::default().fg(if dim {
Color::Rgb(70, 80, 100)
} else {
service_color
}),
)),
Cell::from(Span::styled(
state_str,
Style::default()
.fg(state_color)
.add_modifier(Modifier::BOLD),
)),
Cell::from(Span::styled(
local_str,
Style::default().fg(Color::Rgb(75, 85, 108)),
)),
])
.style(Style::default().bg(row_bg))
})
.collect();
let filter_info = if app.filter_editing {
format!(" [filter: {}_]", app.filter_text)
} else if app.filter_text.is_empty() {
String::new()
} else {
format!(" [filter: {}]", app.filter_text)
};
let localhost_info = if app.hide_localhost_conn {
" \u{1F310} WAN"
} else {
" \u{1F517} ALL"
};
let mut title_spans = vec![
Span::styled(
" Connections ",
Style::default().fg(Color::Rgb(160, 180, 220)).add_modifier(Modifier::BOLD),
),
Span::styled(
format!(" {} total ", total),
Style::default().fg(Color::Rgb(100, 120, 150)),
),
Span::styled(
localhost_info.to_string(),
Style::default().fg(Color::Rgb(80, 160, 200)),
),
];
if !filter_info.is_empty() {
title_spans.push(Span::styled(
filter_info,
Style::default().fg(Color::Yellow),
));
}
let detail_line = if let Some(conn) = filtered.get(selected) {
let geo_detail = conn.remote_addr
.filter(|ip| !ip.is_loopback() && !ip.is_unspecified())
.and_then(|ip| app.geoip.lookup(ip))
.map(|g| format!("{} {}", g.code, g.name))
.unwrap_or_else(|| "Local/Private".to_string());
let remote_str = conn.dns_hostname.clone()
.or_else(|| conn.remote_addr.map(|ip| ip.to_string()))
.unwrap_or_else(|| "*".to_string());
let proc_detail = match conn.module_name.as_ref() {
Some(module) => format!("{} ({})", conn.process_name, module),
None => conn.process_name.clone(),
};
Line::from(vec![
Span::styled(" \u{25B8} ", Style::default().fg(Color::Rgb(100, 200, 255)).add_modifier(Modifier::BOLD)),
Span::styled(proc_detail, Style::default().fg(Color::Rgb(130, 200, 140)).add_modifier(Modifier::BOLD)),
Span::styled(" \u{2192} ", Style::default().fg(Color::Rgb(60, 80, 110))),
Span::styled(remote_str, Style::default().fg(Color::Rgb(100, 220, 255))),
Span::styled(" \u{2502} ", Style::default().fg(Color::Rgb(40, 55, 80))),
Span::styled(geo_detail, Style::default().fg(Color::Rgb(170, 200, 230))),
])
} else {
Line::from("")
};
let table = Table::new(
rows,
[
Constraint::Length(20), Constraint::Length(7), Constraint::Min(22), Constraint::Length(7), Constraint::Length(14), Constraint::Length(14), Constraint::Length(7), ],
)
.header(header)
.block(
Block::default()
.title(Line::from(title_spans))
.title_bottom(detail_line)
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Rgb(30, 50, 85)))
.style(Style::default().bg(Color::Rgb(12, 16, 28))),
);
f.render_widget(table, area);
if total > visible_height {
let sb_area = Rect {
x: area.x + area.width - 1,
y: area.y + 2,
width: 1,
height: area.height.saturating_sub(3),
};
let mut sb_state =
ScrollbarState::new(total.saturating_sub(visible_height)).position(viewport_start);
f.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::VerticalRight)
.style(Style::default().fg(Color::Rgb(40, 70, 120))),
sb_area,
&mut sb_state,
);
}
}