use ratatui::{
style::Color,
style::Style,
text::{Line, Span},
Frame,
};
use super::state::{UiState, REDACTED_PLACEHOLDER};
use std::borrow::Cow;
pub(super) fn show_or_redact(value: Option<&str>, hide: bool) -> &str {
if hide {
REDACTED_PLACEHOLDER
} else {
value.unwrap_or("-")
}
}
pub(super) fn external_ip_for_family<'a>(
external: Option<&'a str>,
meta_ip: Option<&'a str>,
want_ipv4: bool,
) -> Option<&'a str> {
external.or_else(|| {
meta_ip.filter(|ip| {
ip.parse::<std::net::IpAddr>()
.map(|addr| addr.is_ipv4() == want_ipv4)
.unwrap_or(false)
})
})
}
pub(super) fn redact_log_line<'a>(line: &'a str, state: &UiState) -> Cow<'a, str> {
if !state.hide_network_info {
return Cow::Borrowed(line);
}
let mut needles: Vec<&str> = [
state.network.external_ipv6.as_deref(),
state.network.external_ipv4.as_deref(),
state.network.ip.as_deref(),
state.network.interface_mac.as_deref(),
state.network.as_org.as_deref(),
state.network.network_name.as_deref(),
state.network.interface_name.as_deref(),
state.network.server.as_deref(),
state.network.colo.as_deref(),
]
.into_iter()
.flatten()
.filter(|v| v.len() >= 2 && *v != "-")
.collect();
needles.sort_by_key(|v| std::cmp::Reverse(v.len()));
let mut s = line.to_string();
for needle in needles {
s = replace_token(&s, needle, REDACTED_PLACEHOLDER);
}
s = redact_ipv4_in(&s);
Cow::Owned(s)
}
pub(super) fn replace_token(haystack: &str, needle: &str, replacement: &str) -> String {
let mut out = String::with_capacity(haystack.len());
let mut last = 0;
for (i, _) in haystack.match_indices(needle) {
let before = haystack[..i].chars().next_back();
let after = haystack[i + needle.len()..].chars().next();
let is_boundary = |c: Option<char>| match c {
None => true,
Some(c) => !c.is_alphanumeric(),
};
if is_boundary(before) && is_boundary(after) {
out.push_str(&haystack[last..i]);
out.push_str(replacement);
last = i + needle.len();
}
}
out.push_str(&haystack[last..]);
out
}
pub(super) fn redact_ipv4_in(s: &str) -> String {
let bytes = s.as_bytes();
let mut out = String::with_capacity(s.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i].is_ascii_digit() {
if let Some(end) = ipv4_match_end(bytes, i) {
out.push_str(REDACTED_PLACEHOLDER);
i = end;
continue;
}
}
let cp_end = next_utf8_char_end(bytes, i);
out.push_str(std::str::from_utf8(&bytes[i..cp_end]).unwrap_or(""));
i = cp_end;
}
out
}
pub(super) fn ipv4_match_end(bytes: &[u8], start: usize) -> Option<usize> {
let mut i = start;
for octet_idx in 0..4 {
if octet_idx > 0 {
if bytes.get(i).copied() != Some(b'.') {
return None;
}
i += 1;
}
let octet_start = i;
while i < bytes.len() && (i - octet_start) < 3 && bytes[i].is_ascii_digit() {
i += 1;
}
if i == octet_start {
return None;
}
let octet: u32 = std::str::from_utf8(&bytes[octet_start..i])
.ok()?
.parse()
.ok()?;
if octet > 255 {
return None;
}
}
if let Some(&next) = bytes.get(i) {
if next.is_ascii_digit() {
return None;
}
if next == b'.' {
if let Some(&peek) = bytes.get(i + 1) {
if peek.is_ascii_digit() {
return None;
}
}
}
}
Some(i)
}
pub(super) fn next_utf8_char_end(bytes: &[u8], start: usize) -> usize {
let mut i = start + 1;
while i < bytes.len() && (bytes[i] & 0xC0) == 0x80 {
i += 1;
}
i
}
pub fn max_y(points: &[(f64, f64)]) -> f64 {
points.iter().map(|(_, y)| *y).fold(0.0, |a, b| a.max(b))
}
pub(super) fn udp_split_bar(sent: u64, received: u64, width: usize) -> Line<'static> {
let safe_sent = sent.max(1);
let safe_received = received.min(safe_sent);
let lost = safe_sent.saturating_sub(safe_received);
let lost_units = if lost > 0 {
(width as f64 * lost as f64 / safe_sent as f64)
.ceil()
.max(1.0) as usize
} else {
0
};
let ok_units = width.saturating_sub(lost_units);
let ok_part = "=".repeat(ok_units);
let lost_part = "x".repeat(lost_units);
Line::from(vec![
Span::styled("UDP split: ", Style::default().fg(Color::Gray)),
Span::raw("["),
Span::styled(ok_part, Style::default().fg(Color::Green)),
Span::styled(lost_part, Style::default().fg(Color::Red)),
Span::raw("] "),
Span::styled(
format!("ok {} lost {}", safe_received, lost),
Style::default().fg(Color::Gray),
),
])
}
pub(super) fn quality_label_color(label: &str) -> Color {
match label {
"Excellent" | "Good" => Color::Green,
"Acceptable" => Color::Yellow,
"Poor" => Color::Magenta,
"Bad" => Color::Red,
_ => Color::Gray,
}
}
mod compact;
mod full;
use ratatui::layout::Rect;
pub fn draw_dashboard(area: Rect, f: &mut Frame, state: &UiState) {
if area.height < 34 {
return compact::draw_dashboard_compact(area, f, state);
}
full::draw_dashboard_full(area, f, state);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn external_ip_prefers_probe_result() {
assert_eq!(
external_ip_for_family(Some("1.2.3.4"), Some("2001:db8::1"), true),
Some("1.2.3.4")
);
assert_eq!(
external_ip_for_family(Some("2001:db8::1"), Some("1.2.3.4"), false),
Some("2001:db8::1")
);
}
#[test]
fn external_ip_falls_back_to_meta_ip_of_same_family() {
assert_eq!(
external_ip_for_family(None, Some("1.2.3.4"), true),
Some("1.2.3.4")
);
assert_eq!(
external_ip_for_family(None, Some("2001:db8::1"), false),
Some("2001:db8::1")
);
}
#[test]
fn external_ip_ignores_meta_ip_of_other_family() {
assert_eq!(
external_ip_for_family(None, Some("2001:db8::1"), true),
None
);
assert_eq!(external_ip_for_family(None, Some("1.2.3.4"), false), None);
}
#[test]
fn external_ip_handles_missing_or_invalid_meta_ip() {
assert_eq!(external_ip_for_family(None, None, true), None);
assert_eq!(external_ip_for_family(None, Some("not-an-ip"), true), None);
}
}