use std::collections::VecDeque;
use chrono::Local;
#[derive(Clone, Debug)]
pub struct LineEntry {
pub text: String,
pub timestamp: chrono::DateTime<Local>,
pub raw_bytes: Vec<u8>,
pub line_ending: LineEnding,
pub is_sent: bool,
}
#[derive(Clone, Debug, PartialEq)]
pub enum LineEnding {
Lf,
CrLf,
Cr,
None,
}
impl LineEnding {
pub fn display(&self) -> &'static str {
match self {
LineEnding::Lf => "⏎",
LineEnding::CrLf => "↵",
LineEnding::Cr => "←",
LineEnding::None => "",
}
}
}
pub struct ScrollbackBuffer {
lines: VecDeque<LineEntry>,
max_lines: usize,
partial: String,
partial_raw: Vec<u8>,
}
impl ScrollbackBuffer {
pub fn new(max_lines: usize) -> Self {
Self {
lines: VecDeque::with_capacity(max_lines.min(1024)),
max_lines,
partial: String::new(),
partial_raw: Vec::new(),
}
}
pub fn push_bytes(&mut self, data: &[u8]) {
for &byte in data {
self.partial_raw.push(byte);
match byte {
b'\n' => {
let line_ending = if self.partial.ends_with('\r') {
self.partial.pop(); LineEnding::CrLf
} else {
LineEnding::Lf
};
self.commit_line(line_ending);
}
b'\r' => {
self.partial.push('\r');
}
byte => {
if self.partial.ends_with('\r') {
let cr_text: String =
self.partial[..self.partial.len() - 1].to_string();
let cr_raw = self.partial_raw[..self.partial_raw.len() - 1].to_vec();
self.partial = String::new();
self.partial_raw = vec![byte];
self.push_line(LineEntry {
text: cr_text,
timestamp: Local::now(),
raw_bytes: cr_raw,
line_ending: LineEnding::Cr,
is_sent: false,
});
self.partial.push(byte as char);
} else {
self.partial.push(byte as char);
}
}
}
}
}
fn commit_line(&mut self, line_ending: LineEnding) {
let text = std::mem::take(&mut self.partial);
let raw = std::mem::take(&mut self.partial_raw);
self.push_line(LineEntry {
text,
timestamp: Local::now(),
raw_bytes: raw,
line_ending,
is_sent: false,
});
}
fn push_line(&mut self, entry: LineEntry) {
if self.lines.len() >= self.max_lines {
self.lines.pop_front();
}
self.lines.push_back(entry);
}
pub fn push_sent_line(&mut self, text: String) {
self.push_line(LineEntry {
text,
timestamp: Local::now(),
raw_bytes: Vec::new(),
line_ending: LineEnding::None,
is_sent: true,
});
}
pub fn len(&self) -> usize {
self.lines.len()
}
pub fn is_empty(&self) -> bool {
self.lines.is_empty() && self.partial.is_empty()
}
pub fn get(&self, index: usize) -> Option<&LineEntry> {
self.lines.get(index)
}
pub fn partial_line(&self) -> Option<&str> {
if self.partial.is_empty() {
None
} else {
Some(&self.partial)
}
}
pub fn display_len(&self) -> usize {
self.lines.len() + if self.partial.is_empty() { 0 } else { 1 }
}
pub fn iter(&self) -> impl Iterator<Item = &LineEntry> {
self.lines.iter()
}
pub fn clear(&mut self) {
self.lines.clear();
self.partial.clear();
self.partial_raw.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_push_simple_lines() {
let mut buf = ScrollbackBuffer::new(100);
buf.push_bytes(b"hello\nworld\n");
assert_eq!(buf.len(), 2);
assert_eq!(buf.get(0).unwrap().text, "hello");
assert_eq!(buf.get(0).unwrap().line_ending, LineEnding::Lf);
assert_eq!(buf.get(1).unwrap().text, "world");
}
#[test]
fn test_push_crlf() {
let mut buf = ScrollbackBuffer::new(100);
buf.push_bytes(b"hello\r\nworld\r\n");
assert_eq!(buf.len(), 2);
assert_eq!(buf.get(0).unwrap().text, "hello");
assert_eq!(buf.get(0).unwrap().line_ending, LineEnding::CrLf);
assert_eq!(buf.get(1).unwrap().text, "world");
assert_eq!(buf.get(1).unwrap().line_ending, LineEnding::CrLf);
}
#[test]
fn test_partial_line() {
let mut buf = ScrollbackBuffer::new(100);
buf.push_bytes(b"hello");
assert_eq!(buf.len(), 0);
assert_eq!(buf.partial_line(), Some("hello"));
buf.push_bytes(b" world\n");
assert_eq!(buf.len(), 1);
assert_eq!(buf.get(0).unwrap().text, "hello world");
assert_eq!(buf.partial_line(), None);
}
#[test]
fn test_ring_buffer_overflow() {
let mut buf = ScrollbackBuffer::new(3);
buf.push_bytes(b"a\nb\nc\nd\ne\n");
assert_eq!(buf.len(), 3);
assert_eq!(buf.get(0).unwrap().text, "c");
assert_eq!(buf.get(1).unwrap().text, "d");
assert_eq!(buf.get(2).unwrap().text, "e");
}
#[test]
fn test_cr_only_line_ending() {
let mut buf = ScrollbackBuffer::new(100);
buf.push_bytes(b"hello\rworld");
assert_eq!(buf.len(), 1);
assert_eq!(buf.get(0).unwrap().text, "hello");
assert_eq!(buf.get(0).unwrap().line_ending, LineEnding::Cr);
assert_eq!(buf.partial_line(), Some("world"));
}
#[test]
fn test_incremental_bytes() {
let mut buf = ScrollbackBuffer::new(100);
buf.push_bytes(b"hel");
buf.push_bytes(b"lo\r");
buf.push_bytes(b"\nworld\n");
assert_eq!(buf.len(), 2);
assert_eq!(buf.get(0).unwrap().text, "hello");
assert_eq!(buf.get(0).unwrap().line_ending, LineEnding::CrLf);
assert_eq!(buf.get(1).unwrap().text, "world");
}
#[test]
fn test_display_len() {
let mut buf = ScrollbackBuffer::new(100);
assert_eq!(buf.display_len(), 0);
buf.push_bytes(b"hello");
assert_eq!(buf.display_len(), 1);
buf.push_bytes(b"\n");
assert_eq!(buf.display_len(), 1);
buf.push_bytes(b"world");
assert_eq!(buf.display_len(), 2); }
#[test]
fn test_clear() {
let mut buf = ScrollbackBuffer::new(100);
buf.push_bytes(b"hello\nworld");
buf.clear();
assert_eq!(buf.len(), 0);
assert!(buf.is_empty());
assert_eq!(buf.partial_line(), None);
}
}