use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, List, ListItem, ListState, Paragraph, Wrap};
use ratatui::Frame;
use crate::model::record::{RecordBody, RecordDirection, RecordEvent};
use crate::model::tls::TlsVersion;
use crate::ui::app::{App, PaneFocus};
pub fn render(f: &mut Frame<'_>, app: &App, area: Rect) {
let focused = app.focus() == PaneFocus::Records;
let title = if focused {
"Handshake [focus]"
} else {
"Handshake"
};
let border_style = if focused {
Style::default().fg(Color::Cyan)
} else {
Style::default()
};
let block = Block::default()
.borders(Borders::ALL)
.border_style(border_style)
.title(title);
let inner = block.inner(area);
f.render_widget(block, area);
let Some(state) = app.selected() else {
let p = Paragraph::new(vec![Line::from(Span::styled(
"No connection selected.",
Style::default().fg(Color::DarkGray),
))])
.wrap(Wrap { trim: false });
f.render_widget(p, inner);
return;
};
let header_lines = summary_header(state);
let header_h = u16::try_from(header_lines.len().min(inner.height as usize)).unwrap_or(0);
let split = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(header_h), Constraint::Min(0)])
.split(inner);
if header_h > 0 {
f.render_widget(
Paragraph::new(header_lines).wrap(Wrap { trim: false }),
split[0],
);
}
let first_ts = state.records.first().map(|r| r.timestamp_ms).unwrap_or(0);
let mut items: Vec<ListItem> = Vec::with_capacity(state.records.len() + 1);
items.push(ListItem::new(metadata_row_line()));
items.extend(
state
.records
.iter()
.map(|r| ListItem::new(row_line(r, first_ts))),
);
let mut list_state = ListState::default();
list_state.select(Some(app.records_row().min(items.len() - 1)));
let list = List::new(items)
.highlight_style(Style::default().add_modifier(Modifier::REVERSED))
.highlight_symbol("\u{25B6} ");
f.render_stateful_widget(list, split[1], &mut list_state);
}
fn summary_header(state: &crate::model::ConnectionState) -> Vec<Line<'static>> {
let hs = &state.handshake;
let mut lines = Vec::new();
let sni = hs
.sni
.clone()
.unwrap_or_else(|| state.key.server_ip.to_string());
lines.push(Line::from(vec![
Span::styled(
"\u{2192} ",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::styled(sni, Style::default().add_modifier(Modifier::BOLD)),
]));
let mut summary = String::new();
if let Some(v) = hs.tls_version {
summary.push_str(&format!("{v}"));
}
if let Some(c) = hs.cipher_suite_selected {
if !summary.is_empty() {
summary.push_str(" \u{00B7} ");
}
summary.push_str(&format!("{c}"));
}
if let Some(g) = hs.key_share_group {
if !summary.is_empty() {
summary.push_str(" \u{00B7} ");
}
summary.push_str(&format!("{g}"));
}
if !summary.is_empty() {
lines.push(Line::from(Span::styled(
summary,
Style::default().fg(Color::DarkGray),
)));
}
let legend = match hs.tls_version {
Some(TlsVersion::Tls13) => {
"flights: 1) ClientHello (clear) 2) ServerHello (clear) 3+) encrypted handshake"
}
Some(TlsVersion::Ssl30 | TlsVersion::Tls10 | TlsVersion::Tls11 | TlsVersion::Tls12) => {
"flights: 1) ClientHello 2) ServerHello / Certificate / KeyExchange 3) CCS + encrypted Finished"
}
_ => "flights: 1) ClientHello (clear) 2) ServerHello (clear) 3+) protocol-dependent",
};
lines.push(Line::from(Span::styled(
legend,
Style::default().fg(Color::DarkGray),
)));
lines.push(Line::from(""));
lines
}
fn row_line(r: &RecordEvent, first_ts: u64) -> Line<'static> {
let dt = r.timestamp_ms.saturating_sub(first_ts);
let arrow = r.direction.arrow();
let dir_color = match r.direction {
RecordDirection::ClientToServer => Color::Green,
RecordDirection::ServerToClient => Color::Magenta,
};
let dir_style = Style::default().fg(dir_color);
let (tag, _tag_color, label) = describe(r);
Line::from(vec![
Span::styled(format!("{arrow} "), dir_style.add_modifier(Modifier::BOLD)),
Span::styled(format!("{dt:>5}ms "), dir_style),
Span::styled(format!("{tag:<11}"), dir_style),
Span::styled(format!("{:>5}B ", r.outer_length), dir_style),
Span::styled(label, dir_style),
])
}
fn metadata_row_line() -> Line<'static> {
Line::from(vec![
Span::raw(" "),
Span::raw(" "),
Span::styled(
format!("{:<11}", "Metadata"),
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::styled("connection overview", Style::default().fg(Color::DarkGray)),
])
}
fn describe(r: &RecordEvent) -> (&'static str, Color, String) {
match &r.body {
RecordBody::Handshake(hs) => ("plaintext", Color::Yellow, hs.label().to_string()),
RecordBody::EncryptedHandshake { inferred_label, .. } => {
("encrypted", Color::Blue, (*inferred_label).to_string())
}
RecordBody::ChangeCipherSpec => (
"legacy",
Color::DarkGray,
"ChangeCipherSpec (cipher activation)".to_string(),
),
}
}