use std::fs::File;
use std::io;
use std::io::Write;
use crossterm::queue;
use crossterm::terminal::{BeginSynchronizedUpdate, EndSynchronizedUpdate};
use ratatui::backend::Backend;
use ratatui::backend::CrosstermBackend;
use ratatui::layout::Rect;
use super::inline_terminal;
use super::scrollback;
pub type TuiBackend = CrosstermBackend<File>;
pub struct InlineTui {
terminal: inline_terminal::Terminal<TuiBackend>,
pending_history_lines: Vec<String>,
history_log: Vec<String>,
replay_full_history: bool,
}
const HISTORY_LOG_CAPACITY: usize = 10_000;
impl InlineTui {
pub fn new(terminal: inline_terminal::Terminal<TuiBackend>) -> Self {
Self {
terminal,
pending_history_lines: Vec::new(),
history_log: Vec::new(),
replay_full_history: false,
}
}
pub fn queue_history_lines(&mut self, lines: Vec<String>) {
self.pending_history_lines.extend(lines.iter().cloned());
self.history_log.extend(lines);
if self.history_log.len() > HISTORY_LOG_CAPACITY {
let excess = self.history_log.len() - HISTORY_LOG_CAPACITY;
self.history_log.drain(..excess);
}
}
pub fn draw<F>(&mut self, desired_height: u16, render_callback: F) -> io::Result<()>
where
F: FnOnce(&mut inline_terminal::Frame),
{
let screen_size_changed = {
let live = self.terminal.backend().size()?;
live != self.terminal.last_known_screen_size
};
queue!(self.terminal.backend_mut(), BeginSynchronizedUpdate)?;
let frame_result = self.compose_frame(screen_size_changed, desired_height, render_callback);
queue!(self.terminal.backend_mut(), EndSynchronizedUpdate)?;
Write::flush(self.terminal.backend_mut())?;
frame_result
}
fn compose_frame<F>(
&mut self,
screen_size_changed: bool,
desired_height: u16,
render_callback: F,
) -> io::Result<()>
where
F: FnOnce(&mut inline_terminal::Frame),
{
if screen_size_changed {
self.terminal.clear_visible_screen()?;
let live = self.terminal.backend().size()?;
self.terminal
.set_viewport_area(Rect::new(0, 0, live.width, 0));
self.terminal.invalidate_viewport();
self.pending_history_lines.clear();
self.replay_full_history = true;
}
Self::fit_viewport_height(&mut self.terminal, desired_height)?;
if self.replay_full_history {
scrollback::scroll_strings_above_viewport(&mut self.terminal, &self.history_log)?;
self.pending_history_lines.clear();
self.replay_full_history = false;
} else if !self.pending_history_lines.is_empty() {
let batch = std::mem::take(&mut self.pending_history_lines);
scrollback::scroll_strings_above_viewport(&mut self.terminal, &batch)?;
}
self.terminal.draw(render_callback)
}
fn fit_viewport_height(
terminal: &mut inline_terminal::Terminal<TuiBackend>,
desired_height: u16,
) -> io::Result<()> {
let screen = terminal.size()?;
let mut area = terminal.viewport_area;
area.height = desired_height.min(screen.height);
area.width = screen.width;
if area.bottom() > screen.height {
let scroll_by = area.bottom() - screen.height;
if area.top() > 0 {
terminal
.backend_mut()
.scroll_region_up(0..area.top(), scroll_by)?;
}
area.y = screen.height - area.height;
}
if area != terminal.viewport_area {
terminal.clear()?;
terminal.set_viewport_area(area);
}
Ok(())
}
}