use ratatui::prelude::*;
use ratatui::widgets::{Block, Borders, Paragraph};
use super::LogCorrelationState;
use crate::theme::Theme;
pub(super) fn render(
state: &LogCorrelationState,
frame: &mut Frame,
area: Rect,
theme: &Theme,
focused: bool,
disabled: bool,
) {
if area.height < 3 || area.width < 3 {
return;
}
crate::annotation::with_registry(|reg| {
reg.register(
area,
crate::annotation::Annotation::container("log_correlation")
.with_focus(focused)
.with_disabled(disabled),
);
});
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(1), Constraint::Length(1)])
.split(area);
let streams_area = chunks[0];
let status_area = chunks[1];
render_streams(state, frame, streams_area, theme, focused, disabled);
render_status_bar(state, frame, status_area, theme, disabled);
}
fn render_streams(
state: &LogCorrelationState,
frame: &mut Frame,
area: Rect,
theme: &Theme,
focused: bool,
disabled: bool,
) {
if state.streams.is_empty() {
let border_style = if disabled {
theme.disabled_style()
} else if focused {
theme.focused_border_style()
} else {
theme.border_style()
};
let mut block = Block::default()
.borders(Borders::ALL)
.border_style(border_style);
if let Some(title) = state.title() {
block = block.title(format!(" {} ", title));
}
let inner = block.inner(area);
frame.render_widget(block, area);
let msg = Paragraph::new("No streams configured")
.style(theme.normal_style())
.alignment(Alignment::Center);
frame.render_widget(msg, inner);
return;
}
let outer_border_style = if disabled {
theme.disabled_style()
} else if focused {
theme.focused_border_style()
} else {
theme.border_style()
};
let mut outer_block = Block::default()
.borders(Borders::ALL)
.border_style(outer_border_style);
if let Some(title) = state.title() {
outer_block = outer_block.title(format!(" {} ", title));
}
let inner_area = outer_block.inner(area);
frame.render_widget(outer_block, area);
if inner_area.height == 0 || inner_area.width == 0 {
return;
}
let stream_count = state.streams.len() as u16;
let constraints: Vec<Constraint> = (0..stream_count)
.map(|i| {
if i < stream_count - 1 {
Constraint::Ratio(1, stream_count as u32)
} else {
Constraint::Min(0)
}
})
.collect();
let stream_areas = Layout::default()
.direction(Direction::Horizontal)
.constraints(constraints)
.split(inner_area);
let aligned_rows = state.aligned_rows();
let filtered: Vec<Vec<&super::CorrelationEntry>> =
state.streams.iter().map(|s| s.filtered_entries()).collect();
for (i, stream) in state.streams.iter().enumerate() {
let stream_area = stream_areas[i];
let is_active = i == state.active_stream();
render_single_stream(
state,
stream,
StreamViewState {
is_active,
focused,
disabled,
},
StreamViewData {
aligned_rows: &aligned_rows,
filtered_entries: &filtered[i],
stream_idx: i,
},
frame,
stream_area,
theme,
);
}
}
struct StreamViewState {
is_active: bool,
focused: bool,
disabled: bool,
}
struct StreamViewData<'a> {
aligned_rows: &'a [super::AlignedRow],
filtered_entries: &'a [&'a super::CorrelationEntry],
stream_idx: usize,
}
fn render_single_stream(
state: &LogCorrelationState,
stream: &super::LogStream,
view_state: StreamViewState,
data: StreamViewData<'_>,
frame: &mut Frame,
area: Rect,
theme: &Theme,
) {
let is_active = view_state.is_active;
let focused = view_state.focused;
let disabled = view_state.disabled;
if area.width < 2 || area.height < 2 {
return;
}
let border_style = if disabled {
theme.disabled_style()
} else if is_active && focused {
theme.focused_border_style()
} else {
theme.border_style()
};
let title_style = if disabled {
theme.disabled_style()
} else {
Style::default().fg(stream.color)
};
let block = Block::default()
.borders(Borders::ALL)
.border_style(border_style)
.title(Span::styled(format!(" {} ", stream.name), title_style));
let inner = block.inner(area);
frame.render_widget(block, area);
if inner.height == 0 || inner.width == 0 {
return;
}
let mut lines: Vec<Line<'_>> = Vec::new();
for row in data.aligned_rows {
let indices = &row.stream_entries[data.stream_idx];
let max_across_streams = row
.stream_entries
.iter()
.map(|idx| idx.len().max(1))
.max()
.unwrap_or(1);
if indices.is_empty() {
for _ in 0..max_across_streams {
lines.push(Line::from(""));
}
} else {
for &idx in indices {
if idx < data.filtered_entries.len() {
let entry = data.filtered_entries[idx];
let line = format_entry(entry, inner.width as usize);
let style = if disabled {
theme.disabled_style()
} else {
Style::default().fg(entry.level.color())
};
lines.push(Line::styled(line, style));
}
}
let extra = max_across_streams.saturating_sub(indices.len());
for _ in 0..extra {
lines.push(Line::from(""));
}
}
}
let total_lines = lines.len();
let viewport_height = inner.height as usize;
let offset = state.scroll_offset();
let visible_lines: Vec<Line<'_>> = lines
.into_iter()
.skip(offset)
.take(viewport_height)
.collect();
let paragraph = Paragraph::new(visible_lines);
frame.render_widget(paragraph, inner);
if total_lines > viewport_height {
let mut bar_scroll = crate::scroll::ScrollState::new(total_lines);
bar_scroll.set_viewport_height(viewport_height);
bar_scroll.set_offset(offset);
crate::scroll::render_scrollbar_inside_border(&bar_scroll, frame, area, theme);
}
}
fn format_entry(entry: &super::CorrelationEntry, max_width: usize) -> String {
let ts = format_timestamp(entry.timestamp);
let level = entry.level.label();
let prefix = format!("{} {} ", ts, level);
let remaining = max_width.saturating_sub(prefix.len());
let msg = if entry.message.len() > remaining {
&entry.message[..remaining]
} else {
&entry.message
};
format!("{}{}", prefix, msg)
}
fn format_timestamp(ts: f64) -> String {
let total_secs = ts as u64;
let hours = (total_secs / 3600) % 24;
let minutes = (total_secs % 3600) / 60;
let seconds = total_secs % 60;
format!("{:02}:{:02}:{:02}", hours, minutes, seconds)
}
fn render_status_bar(
state: &LogCorrelationState,
frame: &mut Frame,
area: Rect,
theme: &Theme,
disabled: bool,
) {
let style = if disabled {
theme.disabled_style()
} else {
theme.normal_style()
};
let active_name = if !state.streams.is_empty() {
&state.streams[state.active_stream()].name
} else {
"None"
};
let filter_text = if !state.streams.is_empty() {
let active = &state.streams[state.active_stream()];
if active.filter.is_empty() {
String::new()
} else {
format!(" [{}]", active.filter)
}
} else {
String::new()
};
let sync_label = if state.sync_scroll() { "ON" } else { "OFF" };
let status = format!(
" Active: {}{} Sync: {}",
active_name, filter_text, sync_label
);
let paragraph = Paragraph::new(status).style(style);
frame.render_widget(paragraph, area);
}