use regex::Regex;
use std::sync::LazyLock;
#[derive(Clone, Debug, PartialEq)]
pub struct Sequence {
pub command: SequenceCommand,
pub text: String,
}
#[derive(Clone, Debug, PartialEq)]
pub enum SequenceCommand {
CursorMoveHome,
CursorMoveToLineAndColumn((u32, u32)),
CursorMoveLinesUp(u32),
CursorMoveLinesDown(u32),
CursorMoveColumnsRight(u32),
CursorMoveColumnsLeft(u32),
CursorMoveBeginningLinesDown(u32),
CursorMoveBeginningLinesUp(u32),
CursorMoveToColumn(u32),
CursorRequestPosition,
CursorMoveUpOne,
CursorSavePosition,
CursorRestorePosition,
EraseFromCursorToEndOfScreen,
EraseFromBeginningOfScreenToCursor,
EraseEntireScreen,
EraseSavedLines,
EraseFromCursorToEndOfLine,
EraseFromStartOfLineToCursor,
EraseEntireLine,
Unhandled,
}
pub const ESC: char = '\x1b';
static SEQUENCE_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(
format!(
r"(?x)
^ # Must match from start of string
{ESC} # Escape character
(?:
([A-Za-z0-9]) # If sequence without '[', capture a single character
|
\[ # If sequence with '[':
([0-9]+(?:;[0-9]+)*)? # Capture numbers separated by ';'
([A-Za-z]) # And a single following character
)
$ # Must match to end of string"
)
.as_str(),
)
.unwrap()
});
impl Sequence {
pub fn from(buffer: &str) -> Option<Self> {
SequenceCommand::from(buffer).map(|command| Self {
command,
text: buffer.to_string(),
})
}
}
impl SequenceCommand {
fn from(buffer: &str) -> Option<Self> {
let captures = (*SEQUENCE_REGEX).captures(buffer)?;
assert_eq!(4, captures.len());
Some(if let Some(cap1) = captures.get(1) {
assert_eq!(None, captures.get(2));
assert_eq!(1, cap1.len());
Self::without_bracket(cap1.as_str().chars().nth(0).unwrap())
} else {
let numbers = if let Some(numbers) = captures.get(2) {
numbers
.as_str()
.split(';')
.map(|s| s.parse::<u32>().unwrap())
.collect::<Vec<u32>>()
} else {
vec![]
};
let cap3 = captures.get(3).expect("Regex should find end character");
assert_eq!(1, cap3.len());
let c = cap3.as_str().chars().nth(0).unwrap();
Self::with_bracket(&numbers, c)
})
}
fn without_bracket(c: char) -> Self {
match c {
'M' => Self::CursorMoveUpOne,
'7' => Self::CursorSavePosition,
'8' => Self::CursorRestorePosition,
_ => Self::Unhandled,
}
}
fn with_bracket(numbers: &[u32], c: char) -> Self {
match numbers.len() {
0 => match c {
'H' => Self::CursorMoveHome,
'J' => Self::EraseFromCursorToEndOfScreen,
'K' => Self::EraseFromCursorToEndOfLine,
's' => Self::CursorSavePosition,
'u' => Self::CursorRestorePosition,
_ => Self::Unhandled,
},
1 => {
let number = numbers[0];
match c {
'A' => Self::CursorMoveLinesUp(number),
'B' => Self::CursorMoveLinesDown(number),
'C' => Self::CursorMoveColumnsRight(number),
'D' => Self::CursorMoveColumnsLeft(number),
'E' => Self::CursorMoveBeginningLinesUp(number),
'F' => Self::CursorMoveBeginningLinesDown(number),
'G' => Self::CursorMoveToColumn(number),
'J' => match number {
0 => Self::EraseFromCursorToEndOfScreen,
1 => Self::EraseFromBeginningOfScreenToCursor,
2 => Self::EraseEntireScreen,
3 => Self::EraseSavedLines,
_ => Self::Unhandled,
},
'K' => match number {
0 => Self::EraseFromCursorToEndOfLine,
1 => Self::EraseFromStartOfLineToCursor,
2 => Self::EraseEntireLine,
_ => Self::Unhandled,
},
'n' => {
if number == 6 {
Self::CursorRequestPosition
} else {
Self::Unhandled
}
}
_ => Self::Unhandled,
}
}
2 => match c {
'f' | 'H' => Self::CursorMoveToLineAndColumn((numbers[0], numbers[1])),
_ => Self::Unhandled,
},
_ => Self::Unhandled,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! esc {
($chars:expr) => {
format!("{}{}", ESC, $chars).as_str()
};
}
macro_rules! assert_esc {
($command:expr, $text:expr) => {
assert_eq!(
Sequence {
command: $command,
text: $text.to_string()
},
Sequence::from($text).unwrap()
)
};
}
macro_rules! assert_incomplete_esc {
($text:expr) => {
assert_eq!(None, Sequence::from($text))
};
}
#[test]
fn match_escape_returns_none_for_incomplete_escape_sequences() {
assert_incomplete_esc!(esc!(""));
assert_incomplete_esc!(esc!("["));
assert_incomplete_esc!(esc!("[1"));
assert_incomplete_esc!(esc!("[12"));
assert_incomplete_esc!(esc!("[12;1"));
assert_incomplete_esc!(esc!("[12;13"));
}
#[test]
fn match_escape_returns_correct_escape_sequences() {
assert_esc!(SequenceCommand::CursorMoveUpOne, esc!("M"));
assert_esc!(SequenceCommand::CursorSavePosition, esc!("7"));
assert_esc!(SequenceCommand::CursorRestorePosition, esc!("8"));
assert_esc!(SequenceCommand::CursorMoveHome, esc!("[H"));
assert_esc!(SequenceCommand::CursorSavePosition, esc!("[s"));
assert_esc!(SequenceCommand::CursorRestorePosition, esc!("[u"));
assert_esc!(SequenceCommand::CursorMoveLinesUp(17), esc!("[17A"));
assert_esc!(SequenceCommand::CursorMoveLinesDown(18), esc!("[18B"));
assert_esc!(SequenceCommand::CursorMoveColumnsRight(19), esc!("[19C"));
assert_esc!(SequenceCommand::CursorMoveColumnsLeft(20), esc!("[20D"));
assert_esc!(
SequenceCommand::CursorMoveBeginningLinesUp(21),
esc!("[21E")
);
assert_esc!(
SequenceCommand::CursorMoveBeginningLinesDown(22),
esc!("[22F")
);
assert_esc!(SequenceCommand::CursorMoveToColumn(23), esc!("[23G"));
assert_esc!(SequenceCommand::CursorRequestPosition, esc!("[6n"));
assert_esc!(SequenceCommand::EraseFromCursorToEndOfScreen, esc!("[J"));
assert_esc!(SequenceCommand::EraseFromCursorToEndOfScreen, esc!("[0J"));
assert_esc!(
SequenceCommand::EraseFromBeginningOfScreenToCursor,
esc!("[1J")
);
assert_esc!(SequenceCommand::EraseEntireScreen, esc!("[2J"));
assert_esc!(SequenceCommand::EraseSavedLines, esc!("[3J"));
assert_esc!(SequenceCommand::EraseFromCursorToEndOfLine, esc!("[K"));
assert_esc!(SequenceCommand::EraseFromCursorToEndOfLine, esc!("[0K"));
assert_esc!(SequenceCommand::EraseFromStartOfLineToCursor, esc!("[1K"));
assert_esc!(SequenceCommand::EraseEntireLine, esc!("[2K"));
assert_esc!(
SequenceCommand::CursorMoveToLineAndColumn((17, 42)),
esc!("[17;42H")
);
assert_esc!(
SequenceCommand::CursorMoveToLineAndColumn((17, 42)),
esc!("[17;42f")
);
}
#[test]
fn match_escape_returns_unhandled_for_other_escape_sequences() {
assert_esc!(SequenceCommand::Unhandled, esc!("9"));
assert_esc!(SequenceCommand::Unhandled, esc!("Q"));
assert_esc!(SequenceCommand::Unhandled, esc!("[Q"));
assert_esc!(SequenceCommand::Unhandled, esc!("[17Q"));
assert_esc!(SequenceCommand::Unhandled, esc!("[17;18Q"));
}
}