use std::fmt;
use std::io;
use std::io::Write;
use ansi_to_tui::IntoText;
use crossterm::Command;
use crossterm::cursor::MoveDown;
use crossterm::cursor::MoveTo;
use crossterm::cursor::MoveToColumn;
use crossterm::cursor::RestorePosition;
use crossterm::cursor::SavePosition;
use crossterm::queue;
use crossterm::style::Color as CColor;
use crossterm::style::Colors;
use crossterm::style::Print;
use crossterm::style::SetAttribute;
use crossterm::style::SetBackgroundColor;
use crossterm::style::SetColors;
use crossterm::style::SetForegroundColor;
use crossterm::terminal::Clear;
use crossterm::terminal::ClearType;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::layout::Size;
use ratatui::prelude::Backend;
use ratatui::prelude::IntoCrossterm;
use ratatui::style::Color;
use ratatui::style::Modifier;
use ratatui::style::Style;
use ratatui::text::{Line, Span, Text};
use ratatui::widgets::{Paragraph, Widget, Wrap};
use super::sgr::SgrModifierChange;
pub fn scroll_strings_above_viewport<B>(
terminal: &mut crate::repl::tui::inline_terminal::Terminal<B>,
captured_lines: &[String],
) -> io::Result<()>
where
B: Backend<Error = io::Error> + Write,
{
if captured_lines.is_empty() {
return Ok(());
}
let mut joined = captured_lines.join("\n");
joined.push('\n');
let text: Text<'static> = joined
.as_bytes()
.into_text()
.unwrap_or_else(|_| Text::from(joined.clone()));
scroll_text_above_viewport(terminal, text)
}
fn scroll_text_above_viewport<B>(
terminal: &mut crate::repl::tui::inline_terminal::Terminal<B>,
text: Text<'static>,
) -> io::Result<()>
where
B: Backend<Error = io::Error> + Write,
{
let screen_size = terminal.backend().size().unwrap_or(Size::new(0, 0));
if screen_size.width == 0 || screen_size.height == 0 {
return Ok(());
}
let mut area = terminal.viewport_area;
if area.width == 0 {
return Ok(());
}
let wrap_width = area.width.max(1);
let wrapped_paragraph = Paragraph::new(text.clone()).wrap(Wrap { trim: false });
let wrapped_row_count = wrapped_paragraph.line_count(wrap_width).max(1);
let wrapped_rows = u16::try_from(wrapped_row_count).unwrap_or(u16::MAX);
let buf_rect = Rect::new(0, 0, wrap_width, wrapped_rows);
let mut buf = Buffer::empty(buf_rect);
Paragraph::new(text)
.wrap(Wrap { trim: false })
.render(buf_rect, &mut buf);
let wrapped_lines: Vec<Line<'static>> = (0..wrapped_rows)
.map(|row| buffer_row_as_line(&buf, row, wrap_width))
.collect();
let last_cursor_pos = terminal.last_known_cursor_pos;
let writer = terminal.backend_mut();
let mut should_update_area = false;
let cursor_top = if area.bottom() < screen_size.height {
let scroll_amount = wrapped_rows.min(screen_size.height - area.bottom());
let top_1based = area.top() + 1;
queue!(writer, SetScrollRegion(top_1based..screen_size.height))?;
queue!(writer, MoveTo(0, area.top()))?;
for _ in 0..scroll_amount {
queue!(writer, Print("\x1bM"))?; }
queue!(writer, ResetScrollRegion)?;
let cursor_top = area.top().saturating_sub(1);
area.y = area.y.saturating_add(scroll_amount);
should_update_area = true;
cursor_top
} else {
area.top().saturating_sub(1)
};
if area.top() > 0 {
queue!(writer, SetScrollRegion(1..area.top()))?;
queue!(writer, MoveTo(0, cursor_top))?;
for line in wrapped_lines.iter() {
queue!(writer, Print("\r\n"))?;
emit_history_line(writer, line, wrap_width as usize)?;
}
queue!(writer, ResetScrollRegion)?;
}
let _ = cursor_top;
queue!(writer, MoveTo(last_cursor_pos.x, last_cursor_pos.y))?;
std::io::Write::flush(writer)?;
if should_update_area {
terminal.set_viewport_area(area);
}
if wrapped_rows > 0 {
terminal.record_history_rows(wrapped_rows);
}
Ok(())
}
fn buffer_row_as_line(buf: &Buffer, row: u16, width: u16) -> Line<'static> {
let last_content_col = (0..width).rev().find(|&col| {
let cell = &buf[(col, row)];
cell.symbol() != " " || cell.bg != Color::Reset || cell.modifier != Modifier::empty()
});
let Some(end) = last_content_col else {
return Line::from("");
};
let mut spans: Vec<Span<'static>> = Vec::new();
let mut run_style: Option<Style> = None;
let mut run_text = String::new();
for col in 0..=end {
let cell = &buf[(col, row)];
let cell_style = Style::default()
.fg(cell.fg)
.bg(cell.bg)
.add_modifier(cell.modifier);
match run_style {
Some(style) if style == cell_style => run_text.push_str(cell.symbol()),
_ => {
if let Some(style) = run_style.take() {
spans.push(Span::styled(run_text.clone(), style));
run_text.clear();
}
run_style = Some(cell_style);
run_text.push_str(cell.symbol());
}
}
}
if let Some(style) = run_style {
spans.push(Span::styled(run_text, style));
}
Line::from(spans)
}
fn emit_history_line<W: Write>(writer: &mut W, line: &Line, wrap_width: usize) -> io::Result<()> {
queue!(writer, MoveToColumn(0))?;
let physical_rows = (line.width().max(1).div_ceil(wrap_width)) as u16;
if physical_rows > 1 {
queue!(writer, SavePosition)?;
for _ in 1..physical_rows {
queue!(writer, MoveDown(1), MoveToColumn(0))?;
queue!(writer, Clear(ClearType::UntilNewLine))?;
}
queue!(writer, RestorePosition)?;
}
queue!(
writer,
SetColors(Colors::new(
line.style
.fg
.map(IntoCrossterm::into_crossterm)
.unwrap_or(CColor::Reset),
line.style
.bg
.map(IntoCrossterm::into_crossterm)
.unwrap_or(CColor::Reset)
))
)?;
queue!(writer, Clear(ClearType::UntilNewLine))?;
let merged_spans: Vec<Span<'_>> = line
.spans
.iter()
.map(|s| Span {
style: s.style.patch(line.style),
content: s.content.clone(),
})
.collect();
emit_styled_spans(writer, merged_spans.iter())
}
fn emit_styled_spans<'a, W, I>(mut writer: W, spans: I) -> io::Result<()>
where
W: Write,
I: IntoIterator<Item = &'a Span<'a>>,
{
let mut fg = Color::Reset;
let mut bg = Color::Reset;
let mut last_modifier = Modifier::empty();
for span in spans {
let mut modifier = Modifier::empty();
modifier.insert(span.style.add_modifier);
modifier.remove(span.style.sub_modifier);
if modifier != last_modifier {
let diff = SgrModifierChange {
from: last_modifier,
to: modifier,
};
diff.queue(&mut writer)?;
last_modifier = modifier;
}
let next_fg = span.style.fg.unwrap_or(Color::Reset);
let next_bg = span.style.bg.unwrap_or(Color::Reset);
if next_fg != fg || next_bg != bg {
queue!(
writer,
SetColors(Colors::new(
next_fg.into_crossterm(),
next_bg.into_crossterm()
))
)?;
fg = next_fg;
bg = next_bg;
}
queue!(writer, Print(span.content.as_ref()))?;
}
queue!(
writer,
SetForegroundColor(CColor::Reset),
SetBackgroundColor(CColor::Reset),
SetAttribute(crossterm::style::Attribute::Reset),
)
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct SetScrollRegion(std::ops::Range<u16>);
impl Command for SetScrollRegion {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
write!(f, "\x1b[{};{}r", self.0.start, self.0.end)
}
#[cfg(windows)]
fn execute_winapi(&self) -> std::io::Result<()> {
panic!("SetScrollRegion via WinAPI unsupported — use ANSI");
}
#[cfg(windows)]
fn is_ansi_code_supported(&self) -> bool {
true
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct ResetScrollRegion;
impl Command for ResetScrollRegion {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
write!(f, "\x1b[r")
}
#[cfg(windows)]
fn execute_winapi(&self) -> std::io::Result<()> {
panic!("ResetScrollRegion via WinAPI unsupported — use ANSI");
}
#[cfg(windows)]
fn is_ansi_code_supported(&self) -> bool {
true
}
}