use crate::LineDiff;
use crate::escapes::{HIDE_CURSOR, SHOW_CURSOR, cursor_down, cursor_to, cursor_up};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CursorPos {
pub x: usize,
pub y: usize,
}
#[must_use]
fn cursor_position_changed(a: Option<CursorPos>, b: Option<CursorPos>) -> bool {
a != b
}
#[must_use]
fn build_cursor_suffix(visible_line_count: usize, cursor: Option<CursorPos>) -> String {
let Some(cursor) = cursor else {
return String::new();
};
let mut out = String::new();
let move_up = visible_line_count.saturating_sub(cursor.y);
if move_up > 0 {
out.push_str(&cursor_up(move_up));
}
out.push_str(&cursor_to(cursor.x));
out.push_str(SHOW_CURSOR);
out
}
#[must_use]
fn build_return_to_bottom(previous_line_count: usize, previous: Option<CursorPos>) -> String {
let Some(previous) = previous else {
return String::new();
};
let mut out = String::new();
let down = previous_line_count
.saturating_sub(1)
.saturating_sub(previous.y);
if down > 0 {
out.push_str(&cursor_down(down));
}
out.push_str(&cursor_to(0));
out
}
#[must_use]
fn build_return_to_bottom_prefix(
cursor_was_shown: bool,
previous_line_count: usize,
previous: Option<CursorPos>,
) -> String {
if !cursor_was_shown {
return String::new();
}
let mut out = String::from(HIDE_CURSOR);
out.push_str(&build_return_to_bottom(previous_line_count, previous));
out
}
#[must_use]
fn build_cursor_only_sequence(
cursor_was_shown: bool,
previous_line_count: usize,
previous: Option<CursorPos>,
visible_line_count: usize,
cursor: Option<CursorPos>,
) -> String {
let mut out = String::new();
if cursor_was_shown {
out.push_str(HIDE_CURSOR);
}
out.push_str(&build_return_to_bottom(previous_line_count, previous));
out.push_str(&build_cursor_suffix(visible_line_count, cursor));
out
}
#[must_use]
fn visible_line_count(line_count: usize, str_ends_with_newline: bool) -> usize {
if str_ends_with_newline {
line_count.saturating_sub(1)
} else {
line_count
}
}
#[must_use]
fn visible_lines_of(s: &str) -> u32 {
visible_line_count(s.split('\n').count(), s.ends_with('\n')) as u32
}
#[allow(non_upper_case_globals)]
pub const bsu: &str = "\u{001B}[?2026h";
#[allow(non_upper_case_globals)]
pub const esu: &str = "\u{001B}[?2026l";
const CLEAR_TERMINAL: &str = "\u{001B}[2J\u{001B}[3J\u{001B}[H";
#[must_use]
pub fn should_synchronize(is_tty: bool, interactive: Option<bool>, is_in_ci: bool) -> bool {
is_tty && interactive.unwrap_or(!is_in_ci)
}
#[must_use]
pub fn should_clear_terminal_for_frame(
is_tty: bool,
viewport_rows: usize,
prev_height: usize,
next_height: usize,
is_unmounting: bool,
) -> bool {
if !is_tty {
return false;
}
let had_previous_frame = prev_height > 0;
let was_fullscreen = prev_height >= viewport_rows;
let was_overflowing = prev_height > viewport_rows;
let is_overflowing = next_height > viewport_rows;
let is_leaving_fullscreen = was_fullscreen && next_height < viewport_rows;
let should_clear_on_unmount = is_unmounting && was_fullscreen;
was_overflowing
|| (is_overflowing && had_previous_frame)
|| is_leaving_fullscreen
|| should_clear_on_unmount
}
#[derive(Debug, Clone)]
pub struct FrameParams<'a> {
pub is_tty: bool,
pub viewport_rows: usize,
pub output: &'a str,
pub output_height: usize,
pub static_output: &'a str,
pub is_unmounting: bool,
pub cursor_dirty: bool,
pub cursor: Option<CursorPos>,
pub interactive: Option<bool>,
pub is_in_ci: bool,
pub debug: bool,
}
#[derive(Debug, Default, Clone, PartialEq)]
pub struct FrameWriter {
diff: LineDiff,
last_output: String,
last_output_height: usize,
last_output_to_render: String,
full_static_output: String,
previous_cursor_position: Option<CursorPos>,
cursor_was_shown: bool,
last_changed_lines: u32,
}
impl FrameWriter {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn write_frame(&mut self, params: &FrameParams<'_>) -> Vec<u8> {
let FrameParams {
is_tty,
viewport_rows,
output,
output_height,
static_output,
is_unmounting,
cursor_dirty: _,
cursor,
interactive,
is_in_ci,
debug,
} = *params;
let has_static_output = !static_output.is_empty() && static_output != "\n";
if debug {
let mut out = String::new();
if has_static_output {
self.full_static_output.push_str(static_output);
}
out.push_str(&self.full_static_output);
out.push_str(output);
self.record_frame(output, output, output_height);
self.last_changed_lines = visible_lines_of(output);
return out.into_bytes();
}
let is_fullscreen = is_tty && output_height >= viewport_rows;
let output_to_render = if is_fullscreen {
output.to_owned()
} else {
format!("{output}\n")
};
let ends_with_newline = output_to_render.ends_with('\n');
let output_to_render_line_count = output_to_render.split('\n').count();
if has_static_output {
self.full_static_output.push_str(static_output);
}
let sync = should_synchronize(is_tty, interactive, is_in_ci);
let should_clear = should_clear_terminal_for_frame(
is_tty,
viewport_rows,
self.last_output_height,
output_height,
is_unmounting,
);
if should_clear {
let mut body = String::new();
body.push_str(CLEAR_TERMINAL);
body.push_str(&self.full_static_output);
body.push_str(output);
self.diff.sync(&output_to_render);
body.push_str(&build_cursor_suffix(
visible_line_count(output_to_render_line_count, ends_with_newline),
cursor,
));
self.record_frame(output, &output_to_render, output_height);
self.record_cursor(cursor);
self.last_changed_lines = visible_lines_of(&output_to_render);
return wrap(body, sync);
}
if has_static_output {
let mut body = String::from_utf8(self.diff.clear()).expect("erase sequences are ascii");
body.push_str(static_output);
body.push_str(
&String::from_utf8(self.diff.diff(&output_to_render))
.expect("diff bytes are ascii/utf8"),
);
body.push_str(&build_cursor_suffix(
visible_line_count(output_to_render_line_count, ends_with_newline),
cursor,
));
self.record_frame(output, &output_to_render, output_height);
self.record_cursor(cursor);
self.last_changed_lines = self.diff.last_changed_lines();
return wrap(body, sync);
}
let cursor_changed = cursor_position_changed(cursor, self.previous_cursor_position);
if output != self.last_output || cursor_changed {
let visible = visible_line_count(output_to_render_line_count, ends_with_newline);
if output == self.last_output && cursor_changed {
let seq = build_cursor_only_sequence(
self.cursor_was_shown,
self.previous_line_count(),
self.previous_cursor_position,
visible,
cursor,
);
self.record_frame(output, &output_to_render, output_height);
self.record_cursor(cursor);
self.last_changed_lines = 0;
return wrap(seq, sync);
}
let return_prefix = build_return_to_bottom_prefix(
self.cursor_was_shown,
self.previous_line_count(),
self.previous_cursor_position,
);
let d = self.diff.diff(&output_to_render);
self.last_changed_lines = self.diff.last_changed_lines();
let suffix = build_cursor_suffix(visible, cursor);
self.record_frame(output, &output_to_render, output_height);
self.record_cursor(cursor);
if return_prefix.is_empty() && d.is_empty() && suffix.is_empty() {
return Vec::new();
}
let mut body = return_prefix.into_bytes();
body.extend_from_slice(&d);
body.extend_from_slice(suffix.as_bytes());
return wrap_bytes(body, sync);
}
self.record_frame(output, &output_to_render, output_height);
self.record_cursor(cursor);
self.last_changed_lines = 0;
Vec::new()
}
#[must_use]
pub fn last_changed_lines(&self) -> u32 {
self.last_changed_lines
}
pub fn reset_diff_state(&mut self) {
*self = Self::default();
}
fn record_frame(&mut self, output: &str, output_to_render: &str, output_height: usize) {
self.last_output.clear();
self.last_output.push_str(output);
self.last_output_to_render.clear();
self.last_output_to_render.push_str(output_to_render);
self.last_output_height = output_height;
}
fn record_cursor(&mut self, cursor: Option<CursorPos>) {
self.previous_cursor_position = cursor;
self.cursor_was_shown = cursor.is_some();
}
fn previous_line_count(&self) -> usize {
self.last_output_to_render.split('\n').count()
}
pub fn clear(&mut self) -> Vec<u8> {
let prefix = build_return_to_bottom_prefix(
self.cursor_was_shown,
self.previous_line_count(),
self.previous_cursor_position,
);
let erase = self.diff.clear();
self.previous_cursor_position = None;
self.cursor_was_shown = false;
if prefix.is_empty() {
return erase;
}
let mut out = prefix.into_bytes();
out.extend_from_slice(&erase);
out
}
pub fn sync_baseline(&mut self) {
self.diff.sync(&self.last_output_to_render);
}
pub fn restore_last_output(&mut self) -> Vec<u8> {
self.diff.diff(&self.last_output_to_render)
}
pub fn forget_last_output(&mut self) {
self.last_output.clear();
self.last_output_to_render.clear();
}
pub fn reset_static_output(&mut self) {
self.full_static_output.clear();
}
pub fn compose_console_write(&mut self, data: &[u8], sync: bool) -> Vec<u8> {
let clear = self.clear();
let restore = self.restore_last_output();
let capacity = bsu.len() + clear.len() + data.len() + restore.len() + esu.len();
let mut out = Vec::with_capacity(capacity);
if sync {
out.extend_from_slice(bsu.as_bytes());
}
out.extend_from_slice(&clear);
out.extend_from_slice(data);
out.extend_from_slice(&restore);
if sync {
out.extend_from_slice(esu.as_bytes());
}
out
}
pub fn compose_console_prefix(&mut self, sync: bool) -> Vec<u8> {
let clear = self.clear();
if !sync {
return clear;
}
let mut out = Vec::with_capacity(bsu.len() + clear.len());
out.extend_from_slice(bsu.as_bytes());
out.extend_from_slice(&clear);
out
}
pub fn compose_console_suffix(&mut self, sync: bool) -> Vec<u8> {
let restore = self.restore_last_output();
if !sync {
return restore;
}
let mut out = Vec::with_capacity(restore.len() + esu.len());
out.extend_from_slice(&restore);
out.extend_from_slice(esu.as_bytes());
out
}
}
fn wrap(body: String, sync: bool) -> Vec<u8> {
wrap_bytes(body.into_bytes(), sync)
}
fn wrap_bytes(body: Vec<u8>, sync: bool) -> Vec<u8> {
if !sync {
return body;
}
let mut out = Vec::with_capacity(body.len() + bsu.len() + esu.len());
out.extend_from_slice(bsu.as_bytes());
out.extend_from_slice(&body);
out.extend_from_slice(esu.as_bytes());
out
}
#[cfg(test)]
#[path = "frame_tests.rs"]
mod frame_tests;