macro_rules! assert_terminal {
($terminal:expr, $curs:expr, $b:expr) => {
let expected = $b;
let terminal = $terminal;
let (lines, cursor) = terminal.view();
assert_eq!(lines, expected);
assert_eq!(cursor, $curs);
};
}
pub(crate) use assert_terminal;
use regex::Regex;
#[derive(Debug)]
pub struct Terminal {
received: Vec<u8>,
}
impl Terminal {
pub fn new() -> Self {
Self { received: vec![] }
}
pub fn receive_byte(&mut self, byte: u8) {
self.received.push(byte);
}
pub fn receive_bytes(&mut self, bytes: &[u8]) {
for &byte in bytes {
self.receive_byte(byte)
}
}
pub fn view(&self) -> (Vec<String>, usize) {
let mut output = vec!["".to_string()];
let mut cursor = 0;
let mut received = std::str::from_utf8(&self.received)
.expect("Received bytes must form utf8 string")
.to_string();
let seq_re = Regex::new(r"\x1b\[([\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e])").unwrap();
while !received.is_empty() {
let (normal, seq) = if let Some(seq_match) = seq_re.find(&received) {
let seq = seq_match.as_str().to_string();
if seq_match.start() > 0 {
let normal = received[..seq_match.start()].to_string();
received = received[seq_match.end()..].to_string();
(Some(normal), Some(seq))
} else {
received = received[seq_match.end()..].to_string();
(None, Some(seq))
}
} else {
let normal = received;
received = "".to_string();
(Some(normal), None)
};
if let Some(normal) = normal {
for c in normal.chars().into_iter() {
match c {
'\r' => {
cursor = 0;
}
'\n' => {
output.push("".to_string());
}
c if c >= ' ' => {
let current = output.last_mut().unwrap();
if current.chars().count() > cursor {
current
.remove(current.char_indices().skip(cursor).next().unwrap().0);
} else {
while current.chars().count() < cursor {
current.push(' ');
}
}
current.insert(cursor, c);
cursor += 1;
}
_ => unimplemented!(),
}
}
}
if let Some(seq) = seq {
let current = output.last_mut().unwrap();
match seq.as_str() {
"\x1B[C" => {
cursor += 1;
}
"\x1B[D" => {
if cursor > 0 {
cursor -= 1;
}
}
"\x1B[P" => {
if current.chars().count() > cursor {
current.remove(current.char_indices().skip(cursor).next().unwrap().0);
}
}
"\x1B[@" => {
if current.chars().count() > cursor {
current
.insert(current.char_indices().skip(cursor).next().unwrap().0, ' ');
}
}
"\x1B[2K" => {
current.clear();
}
_ => unimplemented!(),
}
}
}
let output = output
.into_iter()
.map(|l| l.trim_end().to_string())
.collect();
(output, cursor)
}
}
#[cfg(test)]
mod tests {
use embedded_cli::codes;
use super::Terminal;
#[test]
fn simple() {
let mut terminal = Terminal::new();
assert_terminal!(&terminal, 0, vec![""]);
terminal.receive_byte(b'a');
terminal.receive_byte(b'b');
terminal.receive_byte(b'c');
assert_terminal!(terminal, 3, vec!["abc"]);
}
#[test]
fn line_feeds() {
let mut terminal = Terminal::new();
terminal.receive_byte(b'a');
terminal.receive_byte(b'b');
terminal.receive_byte(codes::LINE_FEED);
assert_terminal!(&terminal, 2, vec!["ab", ""]);
terminal.receive_byte(b'c');
assert_terminal!(&terminal, 3, vec!["ab", " c"]);
}
#[test]
fn carriage_return() {
let mut terminal = Terminal::new();
terminal.receive_byte(b'a');
terminal.receive_byte(b'b');
terminal.receive_byte(codes::CARRIAGE_RETURN);
assert_terminal!(&terminal, 0, vec!["ab"]);
terminal.receive_byte(b'c');
assert_terminal!(terminal, 1, vec!["cb"]);
}
#[test]
fn move_forward_backward() {
let mut terminal = Terminal::new();
terminal.receive_bytes(b"abc");
terminal.receive_bytes(codes::CURSOR_BACKWARD);
assert_terminal!(&terminal, 2, vec!["abc"]);
terminal.receive_bytes(codes::CURSOR_BACKWARD);
assert_terminal!(&terminal, 1, vec!["abc"]);
terminal.receive_byte(b'd');
assert_terminal!(&terminal, 2, vec!["adc"]);
terminal.receive_byte(b'e');
terminal.receive_byte(b'f');
assert_terminal!(&terminal, 4, vec!["adef"]);
terminal.receive_bytes(codes::CURSOR_BACKWARD);
terminal.receive_bytes(codes::CURSOR_BACKWARD);
terminal.receive_bytes(codes::CURSOR_BACKWARD);
assert_terminal!(&terminal, 1, vec!["adef"]);
terminal.receive_bytes(codes::CURSOR_FORWARD);
assert_terminal!(&terminal, 2, vec!["adef"]);
terminal.receive_byte(b'b');
assert_terminal!(&terminal, 3, vec!["adbf"]);
}
#[test]
fn delete_chars() {
let mut terminal = Terminal::new();
terminal.receive_bytes(b"abc");
terminal.receive_bytes(codes::CURSOR_BACKWARD);
terminal.receive_bytes(codes::DELETE_CHAR);
assert_terminal!(&terminal, 2, vec!["ab"]);
terminal.receive_bytes(b"def");
terminal.receive_bytes(codes::CURSOR_BACKWARD);
terminal.receive_bytes(codes::CURSOR_BACKWARD);
terminal.receive_bytes(codes::CURSOR_BACKWARD);
assert_terminal!(&terminal, 2, vec!["abdef"]);
terminal.receive_bytes(codes::DELETE_CHAR);
assert_terminal!(&terminal, 2, vec!["abef"]);
terminal.receive_bytes(codes::DELETE_CHAR);
assert_terminal!(&terminal, 2, vec!["abf"]);
terminal.receive_byte(b'e');
assert_terminal!(&terminal, 3, vec!["abe"]);
}
#[test]
fn insert_chars() {
let mut terminal = Terminal::new();
terminal.receive_bytes(b"abc");
terminal.receive_bytes(codes::CURSOR_BACKWARD);
terminal.receive_bytes(codes::INSERT_CHAR);
assert_terminal!(&terminal, 2, vec!["ab c"]);
terminal.receive_byte(b'd');
assert_terminal!(&terminal, 3, vec!["abdc"]);
terminal.receive_bytes(codes::CURSOR_BACKWARD);
terminal.receive_bytes(codes::CURSOR_BACKWARD);
terminal.receive_bytes(codes::INSERT_CHAR);
assert_terminal!(&terminal, 1, vec!["a bdc"]);
terminal.receive_bytes(codes::INSERT_CHAR);
assert_terminal!(&terminal, 1, vec!["a bdc"]);
}
#[test]
fn clear_line() {
let mut terminal = Terminal::new();
terminal.receive_bytes(b"abc");
terminal.receive_bytes(codes::CLEAR_LINE);
assert_terminal!(&terminal, 3, vec![""]);
terminal.receive_byte(b'd');
assert_terminal!(&terminal, 4, vec![" d"]);
}
}