use ratatui::prelude::*;
use ratatui::widgets::{Block, Borders, Paragraph};
use crate::decode::Layer;
use super::App;
pub fn render_detail_tree(frame: &mut ratatui::Frame, area: Rect, app: &App) {
let lines = match app.selected_packet() {
Some(pkt) => {
let mut all_lines = Vec::new();
for layer in &pkt.layers {
let detail = detail_lines(layer);
all_lines.extend(detail);
}
if pkt.retransmission {
all_lines.insert(
0,
Line::from(Span::styled(
"⚠ TCP Retransmission",
Style::default().fg(Color::Red).bold(),
)),
);
}
all_lines
}
None => vec![Line::from(Span::styled(
"No packet selected",
Style::default().fg(Color::DarkGray),
))],
};
let visible_height = area.height.saturating_sub(2) as usize;
let scroll = app.detail_scroll.min(lines.len().saturating_sub(visible_height));
let visible_lines: Vec<Line> = lines.into_iter().skip(scroll).take(visible_height).collect();
let paragraph = Paragraph::new(visible_lines).block(
Block::default()
.borders(Borders::ALL)
.title(" Packet Details "),
);
frame.render_widget(paragraph, area);
}
fn detail_lines(layer: &Layer) -> Vec<Line<'static>> {
let mut lines = Vec::new();
let header_style = Style::default().fg(Color::White).bold();
let field_style = Style::default().fg(Color::Gray);
match layer {
Layer::Ethernet(eth) => {
lines.push(Line::from(Span::styled(
format!(
"▸ Ethernet II, Src: {}, Dst: {}",
eth.src_mac, eth.dst_mac
),
header_style,
)));
lines.push(Line::from(Span::styled(
format!(" Source: {}", eth.src_mac),
field_style,
)));
lines.push(Line::from(Span::styled(
format!(" Destination: {}", eth.dst_mac),
field_style,
)));
lines.push(Line::from(Span::styled(
format!(" Type: 0x{:04x}", eth.ethertype),
field_style,
)));
}
Layer::Arp(arp) => {
let op_str = match arp.operation {
1 => "Request",
2 => "Reply",
_ => "Unknown",
};
lines.push(Line::from(Span::styled(
format!("▸ ARP ({})", op_str),
header_style,
)));
lines.push(Line::from(Span::styled(
format!(" Sender MAC: {}", arp.sender_mac),
field_style,
)));
lines.push(Line::from(Span::styled(
format!(" Sender IP: {}", arp.sender_ip),
field_style,
)));
lines.push(Line::from(Span::styled(
format!(" Target MAC: {}", arp.target_mac),
field_style,
)));
lines.push(Line::from(Span::styled(
format!(" Target IP: {}", arp.target_ip),
field_style,
)));
}
Layer::Ipv4(ip) => {
lines.push(Line::from(Span::styled(
format!(
"▸ Internet Protocol Version 4, Src: {}, Dst: {}",
ip.src_ip, ip.dst_ip
),
header_style,
)));
lines.push(Line::from(Span::styled(
format!(" Version: {}", ip.version),
field_style,
)));
lines.push(Line::from(Span::styled(
format!(" Header Length: {} bytes ({})", ip.ihl * 4, ip.ihl),
field_style,
)));
lines.push(Line::from(Span::styled(
format!(" Total Length: {}", ip.total_length),
field_style,
)));
lines.push(Line::from(Span::styled(
format!(" TTL: {}", ip.ttl),
field_style,
)));
lines.push(Line::from(Span::styled(
format!(" Protocol: {} ({})", protocol_name(ip.protocol), ip.protocol),
field_style,
)));
lines.push(Line::from(Span::styled(
format!(" Source: {}", ip.src_ip),
field_style,
)));
lines.push(Line::from(Span::styled(
format!(" Destination: {}", ip.dst_ip),
field_style,
)));
}
Layer::Ipv6(ip) => {
lines.push(Line::from(Span::styled(
format!(
"▸ Internet Protocol Version 6, Src: {}, Dst: {}",
ip.src_ip, ip.dst_ip
),
header_style,
)));
lines.push(Line::from(Span::styled(
format!(" Next Header: {} ({})", protocol_name(ip.next_header), ip.next_header),
field_style,
)));
lines.push(Line::from(Span::styled(
format!(" Hop Limit: {}", ip.hop_limit),
field_style,
)));
lines.push(Line::from(Span::styled(
format!(" Payload Length: {}", ip.payload_length),
field_style,
)));
}
Layer::Tcp(tcp) => {
lines.push(Line::from(Span::styled(
format!(
"▸ TCP, Src Port: {}, Dst Port: {}",
tcp.src_port, tcp.dst_port
),
header_style,
)));
lines.push(Line::from(Span::styled(
format!(" Source Port: {}", tcp.src_port),
field_style,
)));
lines.push(Line::from(Span::styled(
format!(" Destination Port: {}", tcp.dst_port),
field_style,
)));
lines.push(Line::from(Span::styled(
format!(" Sequence Number: {}", tcp.seq_num),
field_style,
)));
lines.push(Line::from(Span::styled(
format!(" Acknowledgment Number: {}", tcp.ack_num),
field_style,
)));
lines.push(Line::from(Span::styled(
format!(" Flags: {}", tcp.flags.display()),
field_style,
)));
lines.push(Line::from(Span::styled(
format!(" Window Size: {}", tcp.window_size),
field_style,
)));
lines.push(Line::from(Span::styled(
format!(" Payload Length: {}", tcp.payload_len),
field_style,
)));
}
Layer::Udp(udp) => {
lines.push(Line::from(Span::styled(
format!(
"▸ UDP, Src Port: {}, Dst Port: {}",
udp.src_port, udp.dst_port
),
header_style,
)));
lines.push(Line::from(Span::styled(
format!(" Length: {}", udp.length),
field_style,
)));
}
Layer::Icmp(icmp) => {
lines.push(Line::from(Span::styled(
format!("▸ ICMP, Type: {}, Code: {}", icmp.icmp_type, icmp.code),
header_style,
)));
}
Layer::Icmpv6(icmp) => {
lines.push(Line::from(Span::styled(
format!("▸ ICMPv6, Type: {}, Code: {}", icmp.icmp_type, icmp.code),
header_style,
)));
}
Layer::Dns(dns) => {
let dir = if dns.is_response {
"Response"
} else {
"Query"
};
lines.push(Line::from(Span::styled(
format!("▸ DNS {} (0x{:04x})", dir, dns.transaction_id),
header_style,
)));
for q in &dns.questions {
lines.push(Line::from(Span::styled(
format!(" Question: {} (type={})", q.qname, dns_type_name(q.qtype)),
field_style,
)));
}
}
Layer::TlsClientHello(tls) => {
lines.push(Line::from(Span::styled(
"▸ TLS Client Hello".to_string(),
header_style,
)));
if let Some(ref sni) = tls.sni {
lines.push(Line::from(Span::styled(
format!(" Server Name: {}", sni),
field_style,
)));
}
}
Layer::Payload { offset, len } => {
lines.push(Line::from(Span::styled(
format!("▸ Data ({} bytes at offset {})", len, offset),
field_style,
)));
}
}
lines
}
fn protocol_name(proto: u8) -> &'static str {
match proto {
1 => "ICMP",
6 => "TCP",
17 => "UDP",
58 => "ICMPv6",
_ => "Unknown",
}
}
fn dns_type_name(qtype: u16) -> &'static str {
match qtype {
1 => "A",
2 => "NS",
5 => "CNAME",
6 => "SOA",
15 => "MX",
16 => "TXT",
28 => "AAAA",
255 => "ANY",
_ => "?",
}
}