use ratatui::layout::Rect;
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Paragraph};
use ratatui::Frame;
use crate::ui::app::{App, PaneFocus, RightPaneMode};
use crate::ui::panels::metadata::metadata_lines;
use crate::ui::sections::Section;
pub fn render(f: &mut Frame<'_>, app: &App, area: Rect) {
let focused = app.focus() == PaneFocus::Right;
let inner_w = area.width.saturating_sub(2);
let inner_h = area.height.saturating_sub(2);
let (title, lines, scroll) = match app.right_mode() {
RightPaneMode::Metadata => (
"Metadata".to_string(),
wrap_lines_with_indent(metadata_lines(app), inner_w),
0u16,
),
RightPaneMode::Sections => {
let (title, lines, bounds) = sections_view(app, inner_w);
let scroll = scroll_offset(&bounds, app.section_index(), inner_h);
(title, lines, scroll)
}
};
let title = if focused {
format!("{title} [focus]")
} else {
title
};
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),
)
.scroll((scroll, 0));
f.render_widget(paragraph, area);
}
#[derive(Clone, Copy, Debug)]
struct SectionSpan {
start: usize,
len: usize,
}
fn sections_view(app: &App, inner_w: u16) -> (String, Vec<Line<'static>>, Vec<SectionSpan>) {
let sections = app.sections();
if sections.is_empty() {
return (
"Sections".to_string(),
vec![Line::from(Span::styled(
"No record selected. Arrow through the Records pane to explore.",
Style::default().fg(Color::DarkGray),
))],
Vec::new(),
);
}
let record_label = app
.record_selected()
.map(record_label)
.unwrap_or_else(|| "record".to_string());
let title = format!("Sections \u{2014} {record_label}");
let selected = app.section_index();
let show_education = app.education();
let mut lines = Vec::new();
let mut bounds = Vec::with_capacity(sections.len());
for (i, s) in sections.iter().enumerate() {
if i > 0 {
lines.push(separator_line(inner_w));
}
let start = lines.len();
let is_selected = selected == i;
let mut section_lines = Vec::new();
render_section(&mut section_lines, i, s, is_selected, show_education);
for line in section_lines {
wrap_line(line, inner_w, &mut lines);
}
let len = lines.len() - start;
bounds.push(SectionSpan { start, len });
lines.push(Line::from(""));
}
(title, lines, bounds)
}
fn separator_line(inner_w: u16) -> Line<'static> {
let width = inner_w.max(1) as usize;
let rule: String = "\u{2500}".repeat(width);
Line::from(Span::styled(
rule,
Style::default().fg(Color::Rgb(255, 140, 0)),
))
}
fn scroll_offset(bounds: &[SectionSpan], selected: usize, inner_h: u16) -> u16 {
if bounds.is_empty() || inner_h == 0 {
return 0;
}
let Some(span) = bounds.get(selected) else {
return 0;
};
let sel_start_row = span.start as u32;
let sel_height = span.len as u32;
let total_rows: u32 = bounds
.last()
.map(|b| (b.start + b.len) as u32)
.unwrap_or(0);
let max_scroll = total_rows.saturating_sub(u32::from(inner_h));
let peek: u32 = 3;
let desired = if sel_height + peek <= u32::from(inner_h) {
let bottom = sel_start_row + sel_height + peek;
bottom.saturating_sub(u32::from(inner_h))
} else if sel_height <= u32::from(inner_h) {
let bottom = sel_start_row + sel_height;
bottom.saturating_sub(u32::from(inner_h))
} else {
sel_start_row
};
let desired = desired.max(sel_start_row.saturating_sub(u32::from(inner_h).saturating_sub(1)));
let clamped = desired.min(max_scroll).min(sel_start_row);
u16::try_from(clamped).unwrap_or(u16::MAX)
}
fn wrap_lines_with_indent(lines: Vec<Line<'static>>, width: u16) -> Vec<Line<'static>> {
let mut out = Vec::with_capacity(lines.len());
for line in lines {
wrap_line(line, width, &mut out);
}
out
}
fn wrap_line(line: Line<'static>, width: u16, out: &mut Vec<Line<'static>>) {
let width_u = width as usize;
if width_u == 0 || line.width() <= width_u {
out.push(line);
return;
}
let mut indent: usize = 0;
'outer: for span in &line.spans {
for c in span.content.chars() {
if c == ' ' {
indent += 1;
} else {
break 'outer;
}
}
}
let indent = indent.min(width_u.saturating_sub(1));
let total_chars: usize = line.spans.iter().map(|s| s.content.chars().count()).sum();
let mut chars: Vec<(char, Style)> = Vec::with_capacity(total_chars);
for span in &line.spans {
let st = span.style;
for c in span.content.chars() {
chars.push((c, st));
}
}
let n = chars.len();
let mut i = 0;
let mut is_first = true;
while i < n {
let avail = if is_first {
width_u
} else {
width_u.saturating_sub(indent)
};
if avail == 0 {
break;
}
let hard_end = (i + avail).min(n);
let mut end = hard_end;
if end < n {
let mut k = end;
let min_content = if is_first { i + indent + 1 } else { i + 1 };
while k > min_content && chars[k - 1].0 != ' ' {
k -= 1;
}
if k > min_content {
end = k;
}
}
let mut spans: Vec<Span<'static>> = Vec::new();
if !is_first && indent > 0 {
spans.push(Span::raw(" ".repeat(indent)));
}
let mut chunk_start = i;
let mut cur_style = chars[i].1;
for j in (i + 1)..end {
if chars[j].1 != cur_style {
let s: String = chars[chunk_start..j].iter().map(|(c, _)| *c).collect();
spans.push(Span::styled(s, cur_style));
chunk_start = j;
cur_style = chars[j].1;
}
}
let s: String = chars[chunk_start..end].iter().map(|(c, _)| *c).collect();
spans.push(Span::styled(s, cur_style));
out.push(Line::from(spans));
i = end;
is_first = false;
}
}
fn record_label(r: &crate::model::record::RecordEvent) -> String {
use crate::model::record::{DecodedHandshake, RecordBody};
match &r.body {
RecordBody::Handshake(hs) => match hs {
DecodedHandshake::ClientHello(_) => "ClientHello".into(),
DecodedHandshake::ServerHello(_) => "ServerHello".into(),
DecodedHandshake::HelloRetryRequest(_) => "HelloRetryRequest".into(),
DecodedHandshake::Unknown { msg_type, .. } => {
crate::model::record::handshake_type_name(*msg_type).to_string()
}
},
RecordBody::EncryptedHandshake { inferred_label, .. } => (*inferred_label).to_string(),
RecordBody::ChangeCipherSpec => "ChangeCipherSpec".to_string(),
}
}
fn render_section(
out: &mut Vec<Line<'static>>,
_index: usize,
s: &Section,
is_selected: bool,
show_education: bool,
) {
let cursor = if is_selected { "\u{25B6} " } else { " " };
let title_style = if is_selected {
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD | Modifier::REVERSED)
} else {
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD)
};
out.push(Line::from(vec![
Span::styled(cursor.to_string(), Style::default().fg(Color::Cyan)),
Span::styled(s.title.clone(), title_style),
Span::raw(" "),
Span::styled(
format!("\u{00B7} {}", s.direction_hint),
Style::default().fg(Color::DarkGray),
),
]));
for vl in &s.value_lines {
let mut spans: Vec<Span<'static>> = vec![Span::raw(" ")];
for span in vl.spans.iter() {
spans.push(span.clone());
}
out.push(Line::from(spans));
}
if show_education {
out.push(Line::from(vec![
Span::raw(" "),
Span::styled(
s.edu_short.to_string(),
Style::default()
.fg(Color::DarkGray)
.add_modifier(Modifier::ITALIC),
),
]));
for detail in s.edu_details {
out.push(Line::from(vec![
Span::raw(" "),
Span::styled(
format!("{}: ", detail.label),
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::styled(detail.body.to_string(), Style::default().fg(Color::White)),
]));
}
}
}