use std::collections::VecDeque;
#[derive(Debug, Clone)]
pub struct LogBuffer {
buf: VecDeque<u8>,
cap: usize,
total: u64,
}
impl LogBuffer {
pub fn new(cap: usize) -> Self {
LogBuffer {
buf: VecDeque::new(),
cap,
total: 0,
}
}
pub fn push(&mut self, data: &[u8]) {
self.total += data.len() as u64;
let skip = data.len().saturating_sub(self.cap);
if skip > 0 {
self.buf.clear();
self.buf.extend(&data[skip..]);
if data[skip - 1] != b'\n' {
self.drop_partial_line();
}
} else {
self.buf.extend(data);
self.trim();
}
}
pub fn set_cap(&mut self, cap: usize) {
self.cap = cap;
self.trim();
}
pub fn cap(&self) -> usize {
self.cap
}
pub fn len(&self) -> usize {
self.buf.len()
}
pub fn is_empty(&self) -> bool {
self.buf.is_empty()
}
pub fn total_written(&self) -> u64 {
self.total
}
pub fn contents(&self) -> Vec<u8> {
let (a, b) = self.buf.as_slices();
[a, b].concat()
}
pub fn tail_lines(&self, n: usize) -> Vec<String> {
let text = strip(&self.contents());
let lines: Vec<&str> = text.lines().collect();
lines[lines.len().saturating_sub(n)..]
.iter()
.map(|s| (*s).to_owned())
.collect()
}
pub fn last_lines(&self, n: usize) -> Vec<u8> {
let mut lines = 0;
let mut start = 0;
for (i, &b) in self.buf.iter().enumerate().rev() {
if b == b'\n' && i + 1 < self.buf.len() {
lines += 1;
if lines == n {
start = i + 1;
break;
}
}
}
if n == 0 {
return Vec::new();
}
self.buf.range(start..).copied().collect()
}
pub fn clear(&mut self) {
self.buf.clear();
}
fn trim(&mut self) {
if self.buf.len() <= self.cap {
return;
}
let excess = self.buf.len() - self.cap;
let at_boundary = self.buf[excess - 1] == b'\n';
self.buf.drain(..excess);
if !at_boundary {
self.drop_partial_line();
}
}
fn drop_partial_line(&mut self) {
if let Some(nl) = self.buf.iter().position(|&b| b == b'\n') {
self.buf.drain(..=nl);
}
}
}
pub fn clock(at_ms: u64) -> String {
let secs = (at_ms / 1000) as libc::time_t;
let mut tm: libc::tm = unsafe { std::mem::zeroed() };
if unsafe { libc::localtime_r(&secs, &mut tm) }.is_null() {
return "--:--:--".into();
}
format!("{:02}:{:02}:{:02}", tm.tm_hour, tm.tm_min, tm.tm_sec)
}
pub fn marker_rule(at_ms: u64, width: usize) -> String {
let time = format!(" {} ", clock(at_ms));
let rest = width.saturating_sub(time.chars().count());
let left = rest / 2;
format!("{}{time}{}", "─".repeat(left), "─".repeat(rest - left))
}
pub fn strip(data: &[u8]) -> String {
let mut stripper = Stripper::default();
let mut out = String::new();
let mut lines = stripper.feed(data);
lines.extend(stripper.close());
for line in lines {
out.push_str(&line);
out.push('\n');
}
out.push_str(&stripper.pending());
out
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
enum State {
#[default]
Ground,
Escape,
EscapeIntermediate,
Csi,
String,
StringEscape,
}
const MAX_COLUMN: usize = 4096;
#[derive(Debug, Default, Clone)]
pub struct Stripper {
state: State,
csi: Vec<u8>,
line: Vec<char>,
cursor: usize,
utf8: Vec<u8>,
row: isize,
above: Vec<Vec<char>>,
below: std::collections::VecDeque<Vec<char>>,
moved: bool,
ended: Vec<String>,
settled: Vec<Vec<char>>,
}
const ROWS_UP: isize = 24;
impl Stripper {
pub fn feed(&mut self, data: &[u8]) -> Vec<String> {
let mut lines = Vec::new();
for &b in data {
self.byte(b, &mut lines);
}
lines.append(&mut self.ended);
lines
}
pub fn settle(&mut self) -> Vec<String> {
self.settled.extend(self.above.iter().cloned());
let over = self.settled.len().saturating_sub(ROWS_UP as usize);
self.settled.drain(..over);
self.close()
}
pub fn unsettled(&self) -> bool {
!self.above.is_empty()
}
pub fn at_rest(&self) -> bool {
!self.moved
}
pub fn close(&mut self) -> Vec<String> {
self.above
.drain(..)
.map(|line| line.into_iter().collect())
.collect()
}
fn end_above(&mut self) {
let rows = self.above.drain(..).map(|line| line.into_iter().collect());
self.ended.extend(rows);
self.settled.clear();
}
fn up(&mut self, n: usize) {
self.moved = true;
let mut n = n.min(ROWS_UP as usize * 2);
if self.row == 0 {
while n > 0 {
let Some(line) = self.above.pop().or_else(|| self.settled.pop()) else {
break;
};
self.below
.push_front(std::mem::replace(&mut self.line, line));
n -= 1;
}
}
self.row = (self.row - n as isize).max(-ROWS_UP);
}
fn down(&mut self, n: usize, newline: bool) {
self.moved = !newline;
let mut n = n.min(ROWS_UP as usize);
let back = n.min(self.row.unsigned_abs());
self.row += back as isize;
n -= back;
for _ in 0..n {
let next = self.below.pop_front().unwrap_or_default();
self.above.push(std::mem::replace(&mut self.line, next));
}
let over = self.above.len().saturating_sub(ROWS_UP as usize);
if over > 0 {
let rows = self
.above
.drain(..over)
.map(|line| line.into_iter().collect());
self.ended.extend(rows);
self.settled.clear();
}
}
pub fn pending(&self) -> String {
self.line.iter().collect()
}
pub fn pending_len(&self) -> usize {
self.line.len()
}
fn byte(&mut self, b: u8, lines: &mut Vec<String>) {
match self.state {
State::Ground => self.ground(b, lines),
State::Escape => {
self.state = match b {
b'[' => {
self.csi.clear();
State::Csi
}
b']' | b'P' | b'X' | b'^' | b'_' => State::String,
0x20..=0x2f => State::EscapeIntermediate,
_ => State::Ground,
}
}
State::EscapeIntermediate => {
if !(0x20..=0x2f).contains(&b) {
self.state = State::Ground;
}
}
State::Csi => match b {
0x40..=0x7e => {
match b {
b'A' => self.up(self.count()),
b'B' => self.down(self.count(), false),
_ if self.row != 0 => {}
b'K' => self.erase_line(),
b'G' => self.cursor = self.count().saturating_sub(1).min(MAX_COLUMN),
b'C' => {
self.cursor = self
.cursor
.saturating_add(self.count())
.min(MAX_COLUMN.max(self.line.len()))
}
b'D' => self.cursor = self.cursor.saturating_sub(self.count()),
_ => {}
}
self.state = State::Ground;
}
0x20..=0x3f => self.csi.push(b),
0x1b => self.state = State::Escape,
_ => self.state = State::Ground,
},
State::String => match b {
0x07 => self.state = State::Ground,
0x1b => self.state = State::StringEscape,
_ => {}
},
State::StringEscape => {
self.state = if b == b'\\' {
State::Ground
} else {
State::String
};
}
}
}
fn ground(&mut self, b: u8, lines: &mut Vec<String>) {
if !self.utf8.is_empty() || b >= 0x80 {
self.utf8_byte(b, lines);
return;
}
match b {
0x1b => self.state = State::Escape,
b'\n' => {
if self.row == 0 {
self.cursor = 0;
}
self.down(1, true);
}
_ if self.row != 0 && matches!(b, b'\r' | 0x08) => {}
b'\r' => self.cursor = 0,
0x08 => self.cursor = self.cursor.saturating_sub(1),
b'\t' | 0x20..=0x7e => self.put(b as char),
_ => {}
}
}
fn utf8_byte(&mut self, b: u8, lines: &mut Vec<String>) {
if !self.utf8.is_empty() && b & 0xc0 != 0x80 {
self.utf8.clear();
self.put(char::REPLACEMENT_CHARACTER);
self.ground(b, lines);
return;
}
self.utf8.push(b);
let expected = match self.utf8[0] {
0xc0..=0xdf => 2,
0xe0..=0xef => 3,
0xf0..=0xf7 => 4,
_ => 1,
};
if self.utf8.len() < expected {
return;
}
let c = std::str::from_utf8(&self.utf8)
.ok()
.and_then(|s| s.chars().next())
.unwrap_or(char::REPLACEMENT_CHARACTER);
self.utf8.clear();
self.put(c);
}
fn count(&self) -> usize {
if !self.csi.is_empty() && self.csi.iter().all(u8::is_ascii_digit) {
let n = std::str::from_utf8(&self.csi)
.ok()
.and_then(|n| n.parse::<usize>().ok())
.unwrap_or(usize::MAX);
return n.max(1);
}
1
}
fn put(&mut self, c: char) {
if self.row != 0 {
return;
}
if !self.moved {
self.end_above();
}
while self.line.len() < self.cursor {
self.line.push(' ');
}
if self.cursor < self.line.len() {
self.line[self.cursor] = c;
} else {
self.line.push(c);
}
self.cursor += 1;
}
fn erase_line(&mut self) {
match self.csi.as_slice() {
b"" | b"0" => self.line.truncate(self.cursor),
b"1" => {
let end = self.cursor.min(self.line.len());
self.line[..end].fill(' ');
}
b"2" => self.line.clear(),
_ => {}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn marker_rules_centre_the_time() {
let rule = marker_rule(1_760_000_000_000, 30);
let time = clock(1_760_000_000_000);
assert_eq!(rule.chars().count(), 30, "{rule}");
assert_eq!(rule, format!("────────── {time} ──────────"));
assert_eq!(marker_rule(1_760_000_000_000, 4), format!(" {time} "));
}
#[test]
fn clock_shows_the_time_of_day() {
let time = clock(1_760_000_000_000);
assert_eq!(time.len(), 8, "{time}");
assert_eq!(time.matches(':').count(), 2, "{time}");
assert_eq!(clock(1_760_000_000_999), time);
}
#[test]
fn buffer_keeps_everything_under_the_cap() {
let mut buf = LogBuffer::new(100);
buf.push(b"one\n");
buf.push(b"two\n");
assert_eq!(buf.contents(), b"one\ntwo\n");
assert_eq!(buf.total_written(), 8);
}
#[test]
fn buffer_trims_to_a_line_boundary() {
let mut buf = LogBuffer::new(10);
buf.push(b"first line\nsecond\nthird\n");
assert_eq!(buf.contents(), b"third\n");
assert_eq!(buf.total_written(), 24);
}
#[test]
fn buffer_trims_mid_line_without_a_newline() {
let mut buf = LogBuffer::new(4);
buf.push(b"abcdefgh");
assert_eq!(buf.contents(), b"efgh");
}
#[test]
fn buffer_keeps_a_whole_line_cut_at_its_start() {
let mut buf = LogBuffer::new(10);
buf.push(b"aaaa\n");
buf.push(b"bbbb\n");
buf.push(b"cccc\n");
assert_eq!(buf.contents(), b"bbbb\ncccc\n");
}
#[test]
fn buffer_shrinks_when_the_cap_is_lowered() {
let mut buf = LogBuffer::new(100);
buf.push(b"aaaa\nbbbb\ncccc\n");
buf.set_cap(6);
assert_eq!(buf.contents(), b"cccc\n");
}
#[test]
fn last_lines_are_raw_and_count_an_unfinished_line() {
let mut buf = LogBuffer::new(100);
buf.push(b"one\r\ntwo\r\n\x1b[1m(rdbg)\x1b[0m ");
assert_eq!(buf.last_lines(2), b"two\r\n\x1b[1m(rdbg)\x1b[0m ");
assert_eq!(buf.last_lines(10), buf.contents());
assert_eq!(buf.last_lines(0), b"");
buf.push(b"\n");
assert_eq!(buf.last_lines(1), b"\x1b[1m(rdbg)\x1b[0m \n");
}
#[test]
fn tail_lines_are_stripped() {
let mut buf = LogBuffer::new(100);
buf.push(b"\x1b[32mone\x1b[0m\r\ntwo\r\nthree\r\n");
assert_eq!(buf.tail_lines(2), ["two", "three"]);
}
#[test]
fn strips_csi_colours() {
assert_eq!(strip(b"\x1b[1;31mred\x1b[0m plain"), "red plain");
}
#[test]
fn strips_osc_with_bel_and_st() {
assert_eq!(
strip(b"\x1b]0;title\x07a\x1b]8;;http://x\x1b\\link\x1b]8;;\x1b\\b"),
"alinkb"
);
}
#[test]
fn strips_charset_and_two_byte_escapes() {
assert_eq!(strip(b"\x1b(Bx\x1b=y\x1b7z"), "xyz");
}
#[test]
fn pty_line_endings_become_plain_newlines() {
assert_eq!(strip(b"one\r\ntwo\r\n"), "one\ntwo\n");
}
#[test]
fn carriage_return_overwrites() {
assert_eq!(strip(b"progress 10%\rprogress 100%\n"), "progress 100%\n");
assert_eq!(strip(b"abcdef\rXY\n"), "XYcdef\n");
}
#[test]
fn erase_line_after_carriage_return() {
assert_eq!(strip(b"long status\r\x1b[Kok\n"), "ok\n");
assert_eq!(strip(b"long status\r\x1b[2Kok\n"), "ok\n");
}
#[test]
fn cursor_movements_place_later_text() {
assert_eq!(strip(b"irb(main)> 1+\x1b[2D2 \n"), "irb(main)> 2 \n");
assert_eq!(strip(b"(rdbg) cont\x1b[1Gxx\n"), "xxdbg) cont\n");
assert_eq!(strip(b"(rdbg) \x1b[8Gc\x1b[G(\n"), "(rdbg) c\n");
assert_eq!(strip(b"ab\x1b[3Cc\n"), "ab c\n");
assert_eq!(strip(b"abc\x1b[D\x1b[KX\n"), "abX\n");
}
#[test]
fn cursor_movements_stop_at_a_column_limit() {
assert_eq!(strip(b"\x1b[999999999Gx").len(), MAX_COLUMN + 1);
assert_eq!(
strip(b"\x1b[99999999999999999999999Cx").len(),
MAX_COLUMN + 1
);
let mut s = Stripper::default();
s.feed(b"\x1b[4000C\x1b[4000C\x1b[4000Cx");
assert_eq!(s.pending().len(), MAX_COLUMN + 1);
let mut s = Stripper::default();
s.feed(&b"a".repeat(5000));
s.feed(b"\x1b[Cx");
let line = s.pending();
assert_eq!(line.len(), 5001, "{}", &line[4090..]);
assert!(line[..5000].bytes().all(|b| b == b'a'), "never moves back");
assert!(line.ends_with('x'));
}
#[test]
fn text_written_on_other_rows_leaves_the_current_line_alone() {
assert_eq!(
strip(
b"[1] pry(main)> 1\r\n\x1b[1A\x1b[0G[1] pry(main)> \x1b[1m1\x1b[0m\x1b[1B\r=> 1\r\n"
),
"[1] pry(main)> 1\n=> 1\n"
);
let mut s = Stripper::default();
s.feed(b"irb(main):001> Str\x1b[1B\x1b[1G\x1b[KString\x1b[1B\x1b[1G\x1b[KStruct\x1b[2A\x1b[18G");
assert_eq!(s.pending(), "irb(main):001> Str");
assert!(s.feed(b"\x1b[3B\rnext\n").is_empty());
assert_eq!(
s.settle(),
["irb(main):001> Str", "String", "Struct", "next"],
"writing over the rows below and settling ends them"
);
}
#[test]
fn newlines_reline_moves_back_over_dont_end_the_line() {
let mut s = Stripper::default();
s.feed(b"\x1b[?25l\x1b[1G\x1b[K\x1b[1G\x1b[0mirb(main):001> \x1b[0m\x1b[?25h\x1b[16G");
assert_eq!(s.pending(), "irb(main):001> ");
let lines = s.feed(b"\x1b[?25l\r\n\r\n\x1b[2A\x1b[16G\x1b[0mS\x1b[0m\x1b[1B\x1b[1G\x1b[K\x1b[16G\x1b[97;100mSTDERR \x1b[0m\x1b[1B\x1b[1G\x1b[K\x1b[16G\x1b[97;100mSTDIN \x1b[0m\x1b[?25h\x1b[17G\x1b[2A");
assert!(lines.is_empty(), "{lines:?}");
assert_eq!(s.pending(), "irb(main):001> S");
let lines = s.feed(b"\x1b[?25l\x1b[1G\x1b[K\x1b[1B\x1b[1G\x1b[K\x1b[1B\x1b[1G\x1b[K\x1b[?25h\x1b[1G\x1b[2Airb(main):001> S\r\r\n\x1b[1G\x1b[?2004l=> nil\r\n\x1b[?2004h\x1b[6n\x1b[?25l\x1b[1G\x1b[K\x1b[1G\x1b[0mirb(main):002> \x1b[0m\x1b[?25h\x1b[16G");
assert_eq!(lines, ["irb(main):001> S", "=> nil"]);
assert_eq!(s.pending(), "irb(main):002> ");
}
#[test]
fn huge_moves_up_and_down_count_as_a_screen() {
let mut s = Stripper::default();
s.feed(b"top\n\x1b[99999999999999999999999A\x1b[999999999Bx");
assert!(s.pending().starts_with('x'));
let lines = s.settle();
assert!(lines.len() <= ROWS_UP as usize + 1, "{}", lines.len());
}
#[test]
fn a_move_up_reaches_a_screen_at_most() {
let mut s = Stripper::default();
s.feed(b"top\n");
assert_eq!(s.settle(), ["top"]);
s.feed(b"\x1b[999A");
let mut lines = s.feed(&b"redrawn\n".repeat(30));
lines.extend(s.settle());
assert_eq!(lines.len(), 30 - 24, "{lines:?}");
let lines = s.feed(&b"\n".repeat(30));
assert_eq!(lines.len(), 30 - 24, "a screen of open rows at most");
}
#[test]
fn a_line_ends_with_text_after_it_or_when_settled() {
let mut s = Stripper::default();
assert!(s.feed(b"one\r\n").is_empty());
assert!(s.unsettled());
assert_eq!(s.settle(), ["one"]);
assert!(!s.unsettled());
assert!(s.feed(b"\x1b[1Atwo\n").is_empty());
assert_eq!(s.settle(), ["two"], "a move up opens a settled row again");
assert!(s.feed(b"three\n").is_empty());
assert_eq!(s.feed(b"four"), ["three"]);
assert!(s.feed(b"\x1b[2A\x1b[2Agone\n").is_empty());
assert!(
s.settle().is_empty(),
"rows ended by text after them stay ended"
);
}
#[test]
fn backspace_moves_back() {
assert_eq!(strip(b"abc\x08\x08X\n"), "aXc\n");
}
#[test]
fn drops_other_control_characters() {
assert_eq!(strip(b"a\x07b\x00c\td"), "abc\td");
}
#[test]
fn keeps_utf8_split_across_feeds() {
let mut s = Stripper::default();
let bytes = "blåbær\n".as_bytes();
let mut lines = s.feed(&bytes[..3]);
lines.extend(s.feed(&bytes[3..]));
lines.extend(s.settle());
assert_eq!(lines, ["blåbær"]);
}
#[test]
fn escape_split_across_feeds() {
let mut s = Stripper::default();
assert!(s.feed(b"a\x1b[3").is_empty());
assert!(s.feed(b"1mb\n").is_empty());
assert_eq!(s.settle(), ["ab"]);
}
#[test]
fn invalid_utf8_becomes_replacement() {
assert_eq!(strip(b"a\xffb\xc3c"), "a\u{fffd}b\u{fffd}c");
}
}