use ratatui::layout::Rect;
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
use ratatui::Frame;
use crate::model::tls::TlsVersion;
use crate::model::HandshakeStage;
use crate::ui::app::{App, PaneFocus};
use crate::ui::education::explain;
#[derive(Clone, Copy)]
enum FlowDir {
Right,
Left,
Both,
}
fn step_meta(stage: HandshakeStage, is_tls12: bool) -> (u8, FlowDir, bool) {
match stage {
HandshakeStage::ClientHello => (1, FlowDir::Right, false),
HandshakeStage::ServerHello => (2, FlowDir::Left, false),
HandshakeStage::Certificate => (3, FlowDir::Left, true), HandshakeStage::ServerCertificate => (3, FlowDir::Left, false), HandshakeStage::ServerKeyExchange => (4, FlowDir::Left, false), HandshakeStage::ServerHelloDone => (6, FlowDir::Left, false), HandshakeStage::ClientKeyExchange => (7, FlowDir::Right, false), HandshakeStage::ClientFinished => {
if is_tls12 {
(10, FlowDir::Right, true)
} else {
(4, FlowDir::Right, true)
}
}
HandshakeStage::ServerFinished => {
if is_tls12 {
(12, FlowDir::Left, true)
} else {
(5, FlowDir::Left, true)
}
}
HandshakeStage::ApplicationData => {
if is_tls12 {
(13, FlowDir::Both, true)
} else {
(6, FlowDir::Both, true)
}
}
HandshakeStage::Idle | HandshakeStage::Errored => (0, FlowDir::Right, false),
}
}
fn circled(n: u8) -> char {
match n {
1 => '①',
2 => '②',
3 => '③',
4 => '④',
5 => '⑤',
6 => '⑥',
7 => '⑦',
8 => '⑧',
9 => '⑨',
10 => '⑩',
11 => '⑪',
12 => '⑫',
13 => '⑬',
_ => '○',
}
}
pub fn render(f: &mut Frame<'_>, app: &App, area: Rect) {
let selected = app.selected();
let current_stage = selected.map_or(HandshakeStage::Idle, |c| c.handshake.stage);
let is_tls12 = selected
.and_then(|c| c.handshake.tls_version)
.is_some_and(|v| {
matches!(
v,
TlsVersion::Ssl30 | TlsVersion::Tls10 | TlsVersion::Tls11 | TlsVersion::Tls12
)
});
let inner_width = area.width.saturating_sub(2) as usize;
let mut lines: Vec<Line> = Vec::new();
lines.push(header_line(inner_width));
lines.push(connector_line(inner_width));
let ordered = if is_tls12 {
HandshakeStage::ordered_tls12()
} else {
HandshakeStage::ordered()
};
let cursor_stage = app.selected_record_stage();
let cursor_idx = cursor_stage.and_then(|s| ordered.iter().position(|o| *o == s));
let current_idx = ordered.iter().position(|s| *s == current_stage);
for (i, &stage) in ordered.iter().enumerate() {
let is_cursor = cursor_idx == Some(i);
let is_current = current_idx == Some(i);
let is_past = current_idx.is_some_and(|c| i < c);
lines.push(step_line(
stage,
inner_width,
is_cursor,
is_current,
is_past,
is_tls12,
));
if i + 1 < ordered.len() {
lines.push(connector_line(inner_width));
}
}
let focused_stage = cursor_stage.unwrap_or(current_stage);
if app.education() && focused_stage != HandshakeStage::Idle {
lines.push(Line::from(""));
lines.push(Line::from(Span::styled(
format!("Educational: {}", focused_stage.label()),
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)));
for chunk in explain(focused_stage).split(". ") {
if chunk.is_empty() {
continue;
}
lines.push(Line::from(chunk));
}
}
let focused = app.focus() == PaneFocus::Records;
let base_title = if is_tls12 {
"Handshake (TLS 1.2)"
} else {
"Handshake"
};
let title = if focused {
format!("{base_title} [focus]")
} else {
base_title.to_string()
};
let border_style = if focused {
Style::default().fg(Color::Cyan)
} else {
Style::default()
};
let paragraph = Paragraph::new(lines)
.block(
Block::default()
.borders(Borders::ALL)
.border_style(border_style)
.title(title),
)
.wrap(Wrap { trim: false });
f.render_widget(paragraph, area);
}
fn cw(inner_width: usize) -> usize {
inner_width.saturating_sub(8)
}
fn header_line(inner_width: usize) -> Line<'static> {
let bold_yellow = Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD);
let bar = Style::default().fg(Color::DarkGray);
let content_w = cw(inner_width);
let left_label = "CLIENT";
let right_label = "SERVER";
let gap = content_w.saturating_sub(left_label.len() + right_label.len());
Line::from(vec![
Span::raw(" "),
Span::styled("│", bar),
Span::styled(left_label, bold_yellow),
Span::raw(" ".repeat(gap)),
Span::styled(right_label, bold_yellow),
Span::styled("│", bar),
Span::raw(" "),
])
}
fn connector_line(inner_width: usize) -> Line<'static> {
let bar = Style::default().fg(Color::DarkGray);
Line::from(vec![
Span::raw(" "),
Span::styled("│", bar),
Span::raw(" ".repeat(cw(inner_width))),
Span::styled("│", bar),
Span::raw(" "),
])
}
fn step_line(
stage: HandshakeStage,
inner_width: usize,
is_cursor: bool,
is_current: bool,
is_past: bool,
is_tls12: bool,
) -> Line<'static> {
let (step_n, dir, encrypted) = step_meta(stage, is_tls12);
let arrow_style = if is_cursor {
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD | Modifier::REVERSED)
} else if is_current {
Style::default()
.fg(Color::Green)
.add_modifier(Modifier::BOLD)
} else if is_past {
Style::default().fg(Color::DarkGray)
} else {
Style::default()
};
let bar = Style::default().fg(Color::DarkGray);
let ch = circled(step_n);
let left_prefix: String = match dir {
FlowDir::Right | FlowDir::Both => format!("{ch} "),
FlowDir::Left => " ".into(),
};
let right_suffix: String = match dir {
FlowDir::Left => format!(" {ch}{}", if encrypted { 'E' } else { ' ' }),
FlowDir::Right | FlowDir::Both => {
if encrypted {
" E".into()
} else {
" ".into()
}
}
};
let content_w = cw(inner_width);
let arrow = make_arrow(dir, stage.label(), content_w);
Line::from(vec![
Span::styled(left_prefix, arrow_style),
Span::styled("│", bar),
Span::styled(arrow, arrow_style),
Span::styled("│", bar),
Span::styled(right_suffix, arrow_style),
])
}
fn make_arrow(dir: FlowDir, label: &str, width: usize) -> String {
let heads: usize = match dir {
FlowDir::Both => 2,
_ => 1,
};
if width <= label.len().saturating_add(heads) {
let max_label = width.saturating_sub(heads);
let t = &label[..max_label];
return match dir {
FlowDir::Right => format!("{t}►"),
FlowDir::Left => format!("◄{t}"),
FlowDir::Both => format!("◄{t}►"),
};
}
let dash_total = width - label.len() - heads;
let left_d = dash_total / 2;
let right_d = dash_total - left_d;
let l = "─".repeat(left_d);
let r = "─".repeat(right_d);
match dir {
FlowDir::Right => format!("{l}{label}{r}►"),
FlowDir::Left => format!("◄{l}{label}{r}"),
FlowDir::Both => format!("◄{l}{label}{r}►"),
}
}