pub mod escape;
mod read_char;
use crate::token::read_char::read_char;
use std::io::Read;
#[derive(Clone, Debug, PartialEq)]
pub enum Token {
Char(char),
CarriageReturn,
LineFeed,
EscapeSequence(escape::Sequence),
EndOfFile,
}
impl Token {
fn from_single_char(c: char) -> Self {
match c {
'\r' => Self::CarriageReturn,
'\n' => Self::LineFeed,
_ => Self::Char(c),
}
}
}
pub struct SerialTokenizer<'a> {
stream: &'a mut (dyn Read + Send),
escape_buf: String,
}
impl<'a> SerialTokenizer<'a> {
pub fn new(stream: &'a mut (dyn Read + Send)) -> Self {
Self {
stream,
escape_buf: String::with_capacity(32),
}
}
pub fn next(&mut self) -> Result<Token, std::io::Error> {
if self.escape_buf.is_empty() {
if let Some(c) = read_char(self.stream)? {
if c == escape::ESC {
self.escape_buf.push(c);
self.detect_and_get_escape()
} else {
Ok(Token::from_single_char(c))
}
} else {
Ok(Token::EndOfFile)
}
} else if self.escape_buf.chars().next().unwrap() == escape::ESC {
self.detect_and_get_escape()
} else {
Ok(self.take_char_from_buffer())
}
}
fn detect_and_get_escape(&mut self) -> Result<Token, std::io::Error> {
assert!(!self.escape_buf.is_empty());
while let Some(c) = read_char(self.stream)? {
self.escape_buf.push(c);
if c.is_control() {
break;
}
if let Some(sequence) = escape::Sequence::from(self.escape_buf.as_str()) {
self.escape_buf.clear();
return Ok(Token::EscapeSequence(sequence));
}
}
Ok(self.take_char_from_buffer())
}
fn take_char_from_buffer(&mut self) -> Token {
assert!(!self.escape_buf.is_empty());
let c = self.escape_buf.chars().next().unwrap();
self.escape_buf.remove(0);
Token::from_single_char(c)
}
}
#[cfg(test)]
mod tests {
use super::*;
type Command = escape::SequenceCommand;
macro_rules! stream {
($str:expr) => {
std::io::Cursor::new($str.as_bytes())
};
}
macro_rules! assert_next {
($tokenizer:ident, $token:expr) => {
assert_eq!($token, $tokenizer.next().unwrap())
};
}
macro_rules! esc_token {
($command:expr, $text: expr) => {
Token::EscapeSequence(escape::Sequence {
command: $command,
text: $text.to_string(),
})
};
}
#[test]
fn text_is_tokenized_as_chars() {
let mut stream = stream!("text");
let mut tokenizer = SerialTokenizer::new(&mut stream);
assert_next!(tokenizer, Token::Char('t'));
assert_next!(tokenizer, Token::Char('e'));
assert_next!(tokenizer, Token::Char('x'));
assert_next!(tokenizer, Token::Char('t'));
assert_next!(tokenizer, Token::EndOfFile);
}
#[test]
fn eof_is_repeatedly_returned() {
let mut stream = stream!("t");
let mut tokenizer = SerialTokenizer::new(&mut stream);
assert_next!(tokenizer, Token::Char('t'));
assert_next!(tokenizer, Token::EndOfFile);
assert_next!(tokenizer, Token::EndOfFile);
assert_next!(tokenizer, Token::EndOfFile);
}
#[test]
fn line_breaks_are_tokenized_as_cr_and_lf() {
let mut stream = stream!("1\n2\r\n");
let mut tokenizer = SerialTokenizer::new(&mut stream);
assert_next!(tokenizer, Token::Char('1'));
assert_next!(tokenizer, Token::LineFeed);
assert_next!(tokenizer, Token::Char('2'));
assert_next!(tokenizer, Token::CarriageReturn);
assert_next!(tokenizer, Token::LineFeed);
assert_next!(tokenizer, Token::EndOfFile);
}
#[test]
fn sole_escape_sequence_is_tokenized_as_such() {
let mut stream = stream!("\x1b[H");
let mut tokenizer = SerialTokenizer::new(&mut stream);
assert_next!(tokenizer, esc_token!(Command::CursorMoveHome, "\x1b[H"));
assert_next!(tokenizer, Token::EndOfFile);
}
#[test]
fn consecutive_escape_sequences_are_tokenized_as_such() {
let mut stream = stream!("\x1b[H\x1bM");
let mut tokenizer = SerialTokenizer::new(&mut stream);
assert_next!(tokenizer, esc_token!(Command::CursorMoveHome, "\x1b[H"));
assert_next!(tokenizer, esc_token!(Command::CursorMoveUpOne, "\x1bM"));
assert_next!(tokenizer, Token::EndOfFile);
}
#[test]
fn escape_sequence_in_text_is_tokenized_as_such() {
let mut stream = stream!("1\x1b[17;42f2");
let mut tokenizer = SerialTokenizer::new(&mut stream);
assert_next!(tokenizer, Token::Char('1'));
assert_next!(
tokenizer,
esc_token!(Command::CursorMoveToLineAndColumn((17, 42)), "\x1b[17;42f")
);
assert_next!(tokenizer, Token::Char('2'));
assert_next!(tokenizer, Token::EndOfFile);
}
#[test]
fn escape_that_is_not_sequence_is_tokenized_as_char() {
let mut stream = stream!("\x1b[1");
let mut tokenizer = SerialTokenizer::new(&mut stream);
assert_next!(tokenizer, Token::Char(escape::ESC));
assert_next!(tokenizer, Token::Char('['));
assert_next!(tokenizer, Token::Char('1'));
assert_next!(tokenizer, Token::EndOfFile);
}
#[test]
fn escape_that_is_not_sequence_near_newline_is_tokenized_as_char() {
let mut stream = stream!("\x1b[\n");
let mut tokenizer = SerialTokenizer::new(&mut stream);
assert_next!(tokenizer, Token::Char(escape::ESC));
assert_next!(tokenizer, Token::Char('['));
assert_next!(tokenizer, Token::LineFeed);
assert_next!(tokenizer, Token::EndOfFile);
}
#[test]
fn other_special_characters_are_tokenized_as_char() {
let mut stream = stream!("\t\0\\");
let mut tokenizer = SerialTokenizer::new(&mut stream);
assert_next!(tokenizer, Token::Char('\t'));
assert_next!(tokenizer, Token::Char('\0'));
assert_next!(tokenizer, Token::Char('\\'));
assert_next!(tokenizer, Token::EndOfFile);
}
}