use super::*;
pub fn event_severity_style(severity: &str, theme: &Theme) -> Style {
match severity.to_uppercase().as_str() {
"ERROR" | "FATAL" => Style::default().fg(theme.health_red),
"WARN" => Style::default().fg(theme.health_yellow),
s if s == crate::app::EVENT_TAIL_GAP_SEVERITY => Style::default()
.fg(theme.health_yellow)
.add_modifier(Modifier::BOLD),
_ => Style::default().fg(theme.muted),
}
}
pub(crate) fn draw_events(f: &mut Frame, area: Rect, app: &mut App) {
let body_rows = area.height.saturating_sub(2) as usize;
app.event_panel.scroll = config_scroll_follow(
app.event_panel.scroll,
app.event_panel.cursor,
body_rows,
app.event_panel.events.len(),
);
let scope_suffix = match app.event_panel.for_env.as_deref() {
Some(env) => format!(" · {env}"),
None => " · all envs".to_string(),
};
let title = format!("Events {}{}", app.event_panel.events.len(), scope_suffix);
let block =
titled_block(&app.theme, &title, true, app.theme.title).padding(Padding::horizontal(1));
if app.event_panel.events.is_empty() {
let lines = vec![
Line::from(""),
Line::from(Span::styled(
format!(" {} no events yet", glyph(app.theme.icons, "◌", "o")),
Style::default()
.fg(app.theme.muted)
.add_modifier(Modifier::BOLD),
)),
Line::from(Span::styled(
" they appear on the next refresh — ^R now, or wait for the tick",
Style::default().fg(app.theme.muted),
)),
];
let p = Paragraph::new(lines).block(block);
f.render_widget(p, area);
return;
}
let now = chrono::Utc::now();
let tw = event_time_width(app.event_panel.time_format);
let lines: Vec<Line> = app
.event_panel
.events
.iter()
.enumerate()
.map(|(i, e)| {
let when = format_event_time(e.at, app.event_panel.time_format, now);
let sev_style = severity_style(&e.severity, &app.theme);
let is_cursor = app.event_panel.cursor == Some(i);
let marker = if is_cursor {
glyph(app.theme.icons, "▶ ", "> ")
} else {
" "
};
let marker_style = if is_cursor {
Style::default()
.fg(app.theme.title_alt)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(app.theme.muted)
};
Line::from(vec![
Span::styled(marker, marker_style),
Span::styled(
format!("{when:>tw$} "),
Style::default().fg(app.theme.muted),
),
Span::styled(format!("{:<5} ", e.severity), sev_style),
Span::styled(
format!("{} ", env_label(e)),
Style::default()
.fg(app.theme.text)
.add_modifier(Modifier::BOLD),
),
Span::raw(e.message.clone()),
])
})
.collect();
let para = Paragraph::new(lines)
.block(block)
.scroll((app.event_panel.scroll, 0));
f.render_widget(para, area);
}
pub(crate) fn env_label(e: &crate::aws::Event) -> String {
if e.env.is_empty() {
if e.application.is_empty() {
"—".into()
} else {
format!("[{}]", e.application)
}
} else if e.application.is_empty() {
format!("[{}]", e.env)
} else {
format!("[{}/{}]", e.application, e.env)
}
}
pub(crate) fn severity_style(s: &str, theme: &Theme) -> Style {
match s.to_uppercase().as_str() {
"ERROR" | "FATAL" => Style::default()
.fg(theme.health_red)
.add_modifier(Modifier::BOLD),
"WARN" => Style::default()
.fg(theme.health_yellow)
.add_modifier(Modifier::BOLD),
"INFO" => Style::default().fg(theme.text),
"DEBUG" | "TRACE" => Style::default().fg(theme.muted),
_ => Style::default().fg(theme.text),
}
}
pub(crate) fn humanize_age(d: chrono::Duration) -> String {
let secs = d.num_seconds().max(0);
if secs < 60 {
format!("{}s", secs)
} else if secs < 3600 {
format!("{}m", secs / 60)
} else if secs < 86_400 {
format!("{}h", secs / 3600)
} else {
format!("{}d", secs / 86_400)
}
}
pub(crate) fn format_event_time(
at: Option<chrono::DateTime<chrono::Utc>>,
mode: crate::app::EventTimeFormat,
now: chrono::DateTime<chrono::Utc>,
) -> String {
use crate::app::EventTimeFormat;
let Some(t) = at else {
return "—".into();
};
match mode {
EventTimeFormat::Utc => t.format("%Y-%m-%d %H:%M:%SZ").to_string(),
EventTimeFormat::Local => t
.with_timezone(&chrono::Local)
.format("%Y-%m-%d %H:%M:%S")
.to_string(),
EventTimeFormat::Age => humanize_age(now.signed_duration_since(t)),
}
}
pub(crate) fn event_time_width(mode: crate::app::EventTimeFormat) -> usize {
use crate::app::EventTimeFormat;
match mode {
EventTimeFormat::Utc => 20, EventTimeFormat::Local => 19, EventTimeFormat::Age => 4, }
}
pub(crate) fn age_color(
updated: Option<chrono::DateTime<chrono::Utc>>,
now: chrono::DateTime<chrono::Utc>,
theme: &Theme,
) -> Color {
let Some(u) = updated else {
return theme.muted;
};
let dur = now.signed_duration_since(u);
if dur < chrono::Duration::zero() || dur < chrono::Duration::hours(24) {
theme.title_alt
} else if dur > chrono::Duration::days(30) {
theme.muted
} else {
theme.text
}
}