use std::ops::Range;
mod escapes;
mod frame;
pub use frame::{
CursorPos, FrameParams, FrameWriter, bsu, esu, should_clear_terminal_for_frame,
should_synchronize,
};
use escapes::{cursor_next_line, cursor_to, cursor_up, erase_end_line, erase_lines};
#[derive(Debug, Default, Clone, PartialEq)]
pub struct LineDiff {
previous_output: String,
previous_lines: Vec<Range<usize>>,
last_changed_lines: u32,
}
impl LineDiff {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn reset(&mut self) {
self.previous_output.clear();
self.previous_lines.clear();
self.last_changed_lines = 0;
}
#[must_use]
pub fn last_changed_lines(&self) -> u32 {
self.last_changed_lines
}
fn previous_line(&self, i: usize) -> Option<&str> {
self.previous_lines
.get(i)
.map(|range| &self.previous_output[range.clone()])
}
pub fn sync(&mut self, str_value: &str) {
self.previous_output.clear();
self.previous_output.push_str(str_value);
line_ranges_into(str_value, &mut self.previous_lines);
}
pub fn clear(&mut self) -> Vec<u8> {
let out = erase_lines(self.previous_lines.len());
self.previous_output.clear();
self.previous_lines.clear();
out.into_bytes()
}
pub fn diff(&mut self, next: &str) -> Vec<u8> {
if next == self.previous_output {
self.last_changed_lines = 0;
return Vec::new();
}
let next_ranges = line_ranges(next);
let next_line = |i: usize| next_ranges.get(i).map(|range| &next[range.clone()]);
let visible_count = visible_line_count(next_ranges.len(), next);
let previous_visible = visible_line_count(self.previous_lines.len(), &self.previous_output);
if next == "\n" || self.previous_output.is_empty() {
let mut out = erase_lines(self.previous_lines.len());
out.push_str(next);
self.last_changed_lines = visible_count as u32;
self.set_baseline(next, next_ranges);
return out.into_bytes();
}
let has_trailing_newline = next.ends_with('\n');
let mut buffer = String::new();
let mut changed = 0u32;
let cursor_to_col0 = cursor_to(0);
if visible_count < previous_visible {
let previous_had_trailing_newline = self.previous_output.ends_with('\n');
let extra_slot = usize::from(previous_had_trailing_newline);
buffer.push_str(&erase_lines(previous_visible - visible_count + extra_slot));
buffer.push_str(&cursor_up(visible_count));
} else {
buffer.push_str(&cursor_up(self.previous_lines.len().saturating_sub(1)));
}
for i in 0..visible_count {
let is_last_line = i == visible_count - 1;
if next_line(i) == self.previous_line(i) {
if !is_last_line || has_trailing_newline {
buffer.push_str(cursor_next_line());
}
continue;
}
changed += 1;
buffer.push_str(&cursor_to_col0);
buffer.push_str(next_line(i).unwrap_or(""));
buffer.push_str(erase_end_line());
if !is_last_line || has_trailing_newline {
buffer.push('\n');
}
}
self.last_changed_lines = changed;
self.set_baseline(next, next_ranges);
buffer.into_bytes()
}
fn set_baseline(&mut self, next: &str, next_ranges: Vec<Range<usize>>) {
self.previous_output.clear();
self.previous_output.push_str(next);
self.previous_lines = next_ranges;
}
}
fn visible_line_count(line_count: usize, str_value: &str) -> usize {
if str_value.ends_with('\n') {
line_count - 1
} else {
line_count
}
}
fn line_ranges(s: &str) -> Vec<Range<usize>> {
let mut ranges = Vec::new();
line_ranges_into(s, &mut ranges);
ranges
}
fn line_ranges_into(s: &str, ranges: &mut Vec<Range<usize>>) {
ranges.clear();
let mut start = 0;
for (idx, _) in s.match_indices('\n') {
ranges.push(start..idx);
start = idx + 1;
}
ranges.push(start..s.len());
}
#[cfg(test)]
mod tests;