use crossterm::{
self as ct,
cursor::{MoveDown, MoveLeft, MoveRight, MoveUp},
style::Print,
terminal::{Clear, ClearType},
};
use std::cmp::Ordering;
use std::io::{self, Write as _W};
const MAX_PATCH_LINES: usize = 3;
pub struct TermBuffer {
state: State,
flushed: State,
stdout: io::Stderr,
}
impl Drop for TermBuffer {
fn drop(&mut self) {
if !std::thread::panicking() {
self.state = Default::default();
self.render_frame();
}
self.cursor_to_end();
ct::queue!(self.stdout, Print("".to_string())).unwrap();
self.flush();
}
}
impl Default for TermBuffer {
fn default() -> Self {
Self::new()
}
}
impl TermBuffer {
pub fn new() -> Self {
TermBuffer {
state: Default::default(),
flushed: Default::default(),
stdout: io::stderr(),
}
}
pub fn push_line(&mut self, row: impl Into<String>) {
self.state.push(row);
}
pub fn lines(&self) -> u16 {
self.state.len() as u16
}
pub fn set_next_cursor(&mut self, cursor: (u16, u16)) {
self.state.set_cursor(cursor);
}
pub fn forget(&mut self) -> usize {
let lines = self.flushed.len();
self.cursor_to_end();
self.state = Default::default();
self.flushed = Default::default();
lines
}
pub fn render_frame(&mut self) {
let same_line_count = self.state.len() == self.flushed.len();
if !same_line_count {
return self.render_full();
}
let changed_lines: Vec<_> = self
.state
.iter()
.zip(self.flushed.iter())
.enumerate()
.filter_map(|(i, (a, b))| if a == b { None } else { Some(i) })
.collect();
let changed_cursor = self.state.cursor != self.flushed.cursor;
if changed_lines.is_empty() && !changed_cursor {
self.flushed = self.state.reset();
} else if changed_lines.is_empty() && changed_cursor {
match self.state.cursor.1 == self.flushed.cursor.1 {
true => self.render_one_line(self.state.cursor.1 as usize),
false => {
self.render_one_line(self.flushed.cursor.1 as usize);
self.render_one_line(self.state.cursor.1 as usize);
}
}
self.flushed = self.state.reset();
} else if !changed_lines.is_empty() && changed_lines.len() <= MAX_PATCH_LINES {
for line_num in changed_lines {
self.render_one_line(line_num);
}
self.flushed = self.state.reset();
} else {
self.render_full();
}
}
fn queue_move_cursor_y(&mut self, down: isize) {
match down.cmp(&0) {
Ordering::Greater => {
let down = down as u16;
ct::queue!(self.stdout, MoveDown(down), MoveLeft(1000)).unwrap();
}
Ordering::Less => {
let up = (-down) as u16;
ct::queue!(self.stdout, MoveUp(up), MoveLeft(1000)).unwrap();
}
_ => ct::queue!(self.stdout, MoveLeft(1000)).unwrap(),
}
}
pub fn render_one_line(&mut self, line_index: usize) {
let down = line_index as isize - self.flushed.cursor.1 as isize;
let state = self.state.clone();
self.queue_move_cursor_y(down);
let new_y = (self.flushed.cursor.1 as isize + down) as u16;
let (dx, dy) = state.cursor;
ct::queue!(self.stdout, Clear(ClearType::UntilNewLine)).unwrap();
ct::queue!(self.stdout, Print(state.rows[line_index].to_string())).unwrap();
ct::queue!(self.stdout, MoveLeft(1000)).unwrap();
self.queue_move_cursor_y(dy as isize - new_y as isize);
if dx > 0 {
ct::queue!(self.stdout, MoveRight(dx)).unwrap();
}
self.flushed.cursor = (dx, dy);
}
pub fn render_full(&mut self) {
self.cursor_to_start();
self.queue_clear();
let state = self.state.reset();
for item in state.rows.iter() {
ct::queue!(
self.stdout,
Print(item.to_string()),
Print("\n".to_string()),
MoveLeft(1000)
)
.unwrap();
}
let (cx, cy) = (0, state.len() as u16);
let (dx, dy) = state.get_cursor();
match dy.cmp(&cy) {
Ordering::Less => ct::queue!(self.stdout, MoveUp(cy - dy)).unwrap(),
Ordering::Greater => ct::queue!(self.stdout, MoveDown(dy - cy)).unwrap(),
_ => {}
}
match dx.cmp(&cx) {
Ordering::Less => ct::queue!(self.stdout, MoveLeft(cx - dx)).unwrap(),
Ordering::Greater => ct::queue!(self.stdout, MoveRight(dx - cx)).unwrap(),
_ => {}
}
ct::queue!(self.stdout, crate::color::reset_item()).unwrap();
self.flushed = state;
}
pub fn flush(&mut self) {
self.stdout.flush().expect("flush failed");
}
fn cursor_to_end(&mut self) {
let (cursor_x, cursor_y) = self.flushed.get_cursor();
let height = self.flushed.len() as u16;
let down = height.saturating_sub(cursor_y);
let move_down = down > 0;
let move_left = cursor_x > 0;
if move_down {
ct::queue!(self.stdout, MoveDown(down)).unwrap();
}
if move_left {
ct::queue!(self.stdout, MoveLeft(cursor_x)).unwrap();
}
if move_down || move_left {
self.flush();
}
}
fn queue_clear(&mut self) {
ct::queue!(self.stdout, Clear(ClearType::FromCursorDown)).unwrap();
}
fn cursor_to_start(&mut self) {
let (_, y) = self.flushed.cursor;
ct::queue!(self.stdout, MoveLeft(1000)).unwrap();
if y > 0 {
ct::queue!(self.stdout, MoveUp(y)).unwrap();
}
}
}
#[derive(Clone, Debug)]
struct State {
cursor: (u16, u16),
rows: Vec<String>,
first_row: u16,
}
impl PartialEq for State {
fn eq(&self, other: &Self) -> bool {
self.cursor == other.cursor && self.rows == other.rows
}
}
impl Default for State {
fn default() -> Self {
State {
cursor: (0, 0),
rows: vec![],
first_row: 0,
}
}
}
impl State {
pub fn len(&self) -> usize {
self.rows.len()
}
pub fn push(&mut self, row: impl Into<String>) {
self.rows.push(row.into());
}
pub fn set_cursor(&mut self, cursor: (u16, u16)) {
self.cursor = cursor;
}
pub fn get_cursor(&self) -> (u16, u16) {
self.cursor
}
pub fn reset(&mut self) -> Self {
std::mem::take(self)
}
pub fn iter(&self) -> impl Iterator<Item = &str> {
self.rows.iter().map(|s| s.as_str())
}
}