use ratatui::layout::Constraint;
use ratatui::style::Style;
use ratatui::text::{Line, Span};
use ratatui::widgets::Row;
use seer_core::output::sanitize_line;
use crate::tui::action::LensData;
use crate::tui::theme::Theme;
use crate::tui::widgets::inspector::Field;
use crate::tui::widgets::or_dash;
use crate::tui::widgets::page::{Block, Detail, Page, TableBlock};
pub fn page(
theme: &Theme,
data: &LensData,
filter: &str,
focused: bool,
sel: usize,
) -> Option<Page> {
let LensData::History(entries) = data else {
return None;
};
let rows = crate::tui::filter::history_rows(entries, filter)
.map(|e| {
let when = e.timestamp.format("%Y-%m-%d %H:%M").to_string();
let source = e.result.protocol().unwrap_or("—");
let registrar = sanitize_line(&or_dash(e.result.registrar()));
Row::new(vec![
when,
sanitize_line(&e.domain),
source.to_string(),
registrar,
])
})
.collect();
let detail = focused
.then(|| crate::tui::filter::history_rows(entries, filter).nth(sel))
.flatten()
.map_or(Detail::None, |e| {
let (expires, registrar) = e.result.expiration_info();
Detail::Fields(vec![
Field::new("domain", e.domain.clone()),
Field::new(
"looked up",
e.timestamp.format("%Y-%m-%d %H:%M:%S UTC").to_string(),
),
Field::new("source", e.result.protocol().unwrap_or("—")),
Field::new("registrar", or_dash(registrar)),
Field::new(
"expires",
or_dash(expires.map(|d| d.format("%Y-%m-%d").to_string())),
),
])
});
let table = TableBlock::new(
vec!["WHEN", "DOMAIN", "SOURCE", "REGISTRAR"],
vec![
Constraint::Length(17),
Constraint::Percentage(30),
Constraint::Length(6),
Constraint::Percentage(40),
],
rows,
)
.selected(focused.then_some(sel))
.detail(detail);
Some(
Page::new("Lookup History", theme.teal)
.push(Block::Table(table))
.push(Block::Gap)
.push(Block::Lines(vec![Line::from(Span::styled(
"↵ replay · c clear all (press twice)",
Style::default().fg(theme.overlay0),
))])),
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tui::widgets::page;
fn render(
f: &mut ratatui::Frame,
area: ratatui::layout::Rect,
theme: &Theme,
data: &LensData,
filter: &str,
focused: bool,
sel: usize,
) {
if let Some(p) = page(theme, data, filter, focused, sel) {
page::draw(f, area, theme, p, focused);
}
}
use crate::tui::test_util::{render_lines, render_text};
fn entry(domain: &str) -> seer_core::HistoryEntry {
use seer_core::{HistoryEntry, LookupResult, WhoisResponse};
let whois = WhoisResponse {
domain: domain.to_string(),
registrar: Some("Example Registrar".into()),
..Default::default()
};
HistoryEntry {
domain: domain.to_string(),
timestamp: chrono::DateTime::<chrono::Utc>::from_timestamp(0, 0).unwrap(),
result: LookupResult::Whois {
data: whois,
rdap_error: None,
rdap_fallback: None,
dns_nameservers: None,
},
}
}
#[test]
fn focused_entry_shows_full_timestamp_and_registrar() {
let theme = Theme::frappe();
let data = LensData::History(vec![entry("old.com")]);
let s = render_lines(100, 20, |f| render(f, f.area(), &theme, &data, "", true, 0));
assert!(s.contains("1970-01-01 00:00:00 UTC"), "{s}");
let nav = render_lines(100, 20, |f| {
render(f, f.area(), &theme, &data, "", false, 0);
});
assert!(!nav.contains("00:00:00 UTC"), "{nav}");
}
#[test]
fn renders_empty_history_without_panic() {
let theme = Theme::frappe();
let data = LensData::History(vec![]);
let text = render_text(80, 12, |f| render(f, f.area(), &theme, &data, "", false, 0));
assert!(text.contains("History"), "panel title missing: {text:?}");
assert!(text.contains("replay"), "hint line missing: {text:?}");
}
}