use std::rc::Rc;
use ratatui::Frame;
use ratatui::layout::Rect;
use ratatui::style::Color;
use ratatui::style::Style;
use ratatui::text::Line;
use ratatui::text::Span;
use ratatui::widgets::Paragraph;
use tui_pane::finder_match_bg;
use tui_pane::label_color;
mod pane;
mod selection;
pub use pane::OutputPane;
use super::pane_data;
use crate::tui::render_context::PaneRenderCtx;
fn render_output_pane_body(
frame: &mut Frame,
area: Rect,
pane: &mut OutputPane,
ctx: &PaneRenderCtx<'_>,
) {
let live = ctx.inflight.example_output();
let snapshot: Option<Rc<[String]>> = pane.selection().snapshot().map(Rc::clone);
let source: &[String] = snapshot.as_deref().unwrap_or(live);
let visible_rows = usize::from(area.height.saturating_sub(2));
let inner = Rect::new(
area.x.saturating_add(1),
area.y.saturating_add(1),
area.width.saturating_sub(2),
area.height.saturating_sub(2),
);
pane.sync_viewport(source.len(), visible_rows, inner);
let scroll_offset = u16::try_from(pane.viewport.scroll_offset()).unwrap_or(u16::MAX);
let selected_range = pane.selected_range(source);
let focused = pane.focus.is_focused;
let inner_width = usize::from(inner.width);
let block = tui_pane::default_pane_chrome()
.with_inactive_border(Style::default().fg(label_color()))
.block(output_title(pane, ctx), focused);
let lines: Vec<Line> = source
.iter()
.enumerate()
.map(|(row, raw)| {
let parsed = parse_output_line(raw);
if selected_range.is_some_and(|(lo, hi)| row >= lo && row <= hi) {
fill_row(parsed, inner_width, finder_match_bg())
} else {
parsed
}
})
.collect();
let paragraph = Paragraph::new(lines)
.block(block)
.scroll((scroll_offset, 0));
frame.render_widget(paragraph, area);
}
fn fill_row(parsed: Line<'static>, width: usize, bg: Color) -> Line<'static> {
let highlight = Style::default().bg(bg);
let mut line = Line::from(
parsed
.spans
.into_iter()
.map(|span| span.patch_style(highlight))
.collect::<Vec<_>>(),
);
let used = line.width();
if width > used {
line.spans
.push(Span::styled(" ".repeat(width - used), highlight));
}
line
}
fn parse_output_line(raw: &str) -> Line<'static> {
let padded = format!(" {raw}");
let safe = pane_data::sanitize_ansi_for_output(&padded);
ansi_to_tui::IntoText::into_text(&safe).map_or_else(
|_| Line::from(Span::raw(pane_data::strip_ansi(&safe))),
|text| {
text.lines
.into_iter()
.next()
.unwrap_or_else(|| Line::from(""))
},
)
}
fn output_title(pane: &OutputPane, ctx: &PaneRenderCtx<'_>) -> String {
let live = ctx.inflight.example_output();
let count = pane.selection_line_count(live);
let lines = if count == 1 { "line" } else { "lines" };
let focused = pane.focus.is_focused;
if pane.selection().is_visual() {
return format!(" Output — visual: {count} {lines} (y copy · Esc done) ");
}
if count > 1 {
return format!(" Output — {count} {lines} selected (y copy) ");
}
if !pane.is_following() {
return if focused {
" Output — scrolled (End follow · y copy) ".to_string()
} else {
" Output — scrolled (End to follow) ".to_string()
};
}
if let Some(name) = ctx.inflight.example_running() {
return format!(" Running: {name} (Esc to stop) ");
}
if let Some(name) = ctx.inflight.example_title() {
return if focused {
format!(" Output: {name} (y copy · Esc close) ")
} else {
format!(" Output: {name} (Esc close) ")
};
}
if focused {
" Output (y copy · Esc close) ".to_string()
} else {
" Output (Esc to close) ".to_string()
}
}