alacritty/src/ansi.rs

1591 lines
50 KiB
Rust
Raw Normal View History

2016-06-30 03:56:12 +00:00
// Copyright 2016 Joe Wilm, The Alacritty Project Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
//! ANSI Terminal Stream Parsing
use std::io;
use std::ops::Range;
use std::str;
use vte;
use base64;
use crate::index::{Column, Line, Contains};
use crate::{MouseCursor, Rgb};
// Parse color arguments
//
// Expect that color argument looks like "rgb:xx/xx/xx" or "#xxxxxx"
fn parse_rgb_color(color: &[u8]) -> Option<Rgb> {
let mut iter = color.iter();
macro_rules! next {
() => {
iter.next().map(|v| *v as char)
}
}
macro_rules! parse_hex {
() => {{
let mut digit: u8 = 0;
let next = next!().and_then(|v| v.to_digit(16));
if let Some(value) = next {
digit = value as u8;
}
let next = next!().and_then(|v| v.to_digit(16));
if let Some(value) = next {
digit <<= 4;
digit += value as u8;
}
digit
}}
}
match next!() {
Some('r') => {
if next!() != Some('g') { return None; }
if next!() != Some('b') { return None; }
if next!() != Some(':') { return None; }
let r = parse_hex!();
let val = next!();
Display errors and warnings To make sure that all error and information reporting to the user is unified, all instances of `print!`, `eprint!`, `println!` and `eprintln!` have been removed and replaced by logging. When `RUST_LOG` is not specified, the default Alacritty logger now also prints to both the stderr and a log file. The log file is only created when a message is written to it and its name is printed to stdout the first time it is used. Whenever a warning or an error has been written to the log file/stderr, a message is now displayed in Alacritty which points to the log file where the full error is documented. The message is cleared whenever the screen is cleared using either the `clear` command or the `Ctrl+L` key binding. To make sure that log files created by root don't prevent normal users from interacting with them, the Alacritty log file is `/tmp/Alacritty-$PID.log`. Since it's still possible that the log file can't be created, the UI error/warning message now informs the user if the message was only written to stderr. The reason why it couldn't be created is then printed to stderr. To make sure the deletion of the log file at runtime doesn't create any issues, the file is re-created if a write is attempted without the file being present. To help with debugging Alacritty issues, a timestamp and the error level are printed in all log messages. All log messages now follow this format: [YYYY-MM-DD HH:MM] [LEVEL] Message Since it's not unusual to spawn a lot of different terminal emulators without restarting, Alacritty can create a ton of different log files. To combat this problem, logfiles are removed by default after Alacritty has been closed. If the user wants to persist the log of a single session, the `--persistent_logging` option can be used. For persisting all log files, the `persistent_logging` option can be set in the configuration file
2018-11-17 14:39:13 +00:00
if val != Some('/') { return None; }
let g = parse_hex!();
if next!() != Some('/') { return None; }
let b = parse_hex!();
Some(Rgb { r, g, b })
}
Some('#') => {
Some(Rgb {
r: parse_hex!(),
g: parse_hex!(),
b: parse_hex!(),
})
}
_ => None
}
}
fn parse_number(input: &[u8]) -> Option<u8> {
if input.is_empty() {
return None;
}
let mut num: u8 = 0;
for c in input {
let c = *c as char;
if let Some(digit) = c.to_digit(10) {
num = match num.checked_mul(10).and_then(|v| v.checked_add(digit as u8)) {
Some(v) => v,
None => return None,
}
} else {
return None;
}
}
Some(num)
}
/// The processor wraps a `vte::Parser` to ultimately call methods on a Handler
pub struct Processor {
state: ProcessorState,
parser: vte::Parser,
}
/// Internal state for VTE processor
2017-09-29 12:05:19 +00:00
struct ProcessorState {
preceding_char: Option<char>
}
/// Helper type that implements `vte::Perform`.
///
/// Processor creates a Performer when running advance and passes the Performer
/// to `vte::Parser`.
struct Performer<'a, H: Handler + TermInfo, W: io::Write> {
_state: &'a mut ProcessorState,
handler: &'a mut H,
writer: &'a mut W
}
impl<'a, H: Handler + TermInfo + 'a, W: io::Write> Performer<'a, H, W> {
/// Create a performer
#[inline]
pub fn new<'b>(
state: &'b mut ProcessorState,
handler: &'b mut H,
writer: &'b mut W,
) -> Performer<'b, H, W> {
Performer {
_state: state,
handler,
writer,
}
}
}
impl Default for Processor {
fn default() -> Processor {
Processor {
2017-09-29 12:05:19 +00:00
state: ProcessorState { preceding_char: None },
parser: vte::Parser::new(),
}
}
}
impl Processor {
pub fn new() -> Processor {
Default::default()
}
#[inline]
pub fn advance<H, W>(
&mut self,
handler: &mut H,
byte: u8,
writer: &mut W
)
where H: Handler + TermInfo,
W: io::Write
{
let mut performer = Performer::new(&mut self.state, handler, writer);
self.parser.advance(&mut performer, byte);
}
}
/// Trait that provides properties of terminal
pub trait TermInfo {
fn lines(&self) -> Line;
fn cols(&self) -> Column;
}
/// Type that handles actions from the parser
///
/// XXX Should probably not provide default impls for everything, but it makes
/// writing specific handler impls for tests far easier.
pub trait Handler {
/// OSC to set window title
fn set_title(&mut self, _: &str) {}
/// Set the window's mouse cursor
fn set_mouse_cursor(&mut self, _: MouseCursor) {}
/// Set the cursor style
fn set_cursor_style(&mut self, _: Option<CursorStyle>) {}
/// A character to be displayed
fn input(&mut self, _c: char) {}
/// Set cursor to position
fn goto(&mut self, _: Line, _: Column) {}
/// Set cursor to specific row
fn goto_line(&mut self, _: Line) {}
/// Set cursor to specific column
fn goto_col(&mut self, _: Column) {}
/// Insert blank characters in current line starting from cursor
fn insert_blank(&mut self, _: Column) {}
/// Move cursor up `rows`
fn move_up(&mut self, _: Line) {}
/// Move cursor down `rows`
fn move_down(&mut self, _: Line) {}
/// Identify the terminal (should write back to the pty stream)
///
/// TODO this should probably return an io::Result
fn identify_terminal<W: io::Write>(&mut self, _: &mut W) {}
2017-05-07 20:08:23 +00:00
// Report device status
fn device_status<W: io::Write>(&mut self, _: &mut W, _: usize) {}
2017-05-07 20:08:23 +00:00
/// Move cursor forward `cols`
fn move_forward(&mut self, _: Column) {}
/// Move cursor backward `cols`
fn move_backward(&mut self, _: Column) {}
/// Move cursor down `rows` and set to column 1
fn move_down_and_cr(&mut self, _: Line) {}
/// Move cursor up `rows` and set to column 1
fn move_up_and_cr(&mut self, _: Line) {}
/// Put `count` tabs
fn put_tab(&mut self, _count: i64) {}
/// Backspace `count` characters
fn backspace(&mut self) {}
/// Carriage return
fn carriage_return(&mut self) {}
/// Linefeed
fn linefeed(&mut self) {}
/// Ring the bell
///
/// Hopefully this is never implemented
fn bell(&mut self) {}
/// Substitute char under cursor
fn substitute(&mut self) {}
/// Newline
fn newline(&mut self) {}
/// Set current position as a tabstop
fn set_horizontal_tabstop(&mut self) {}
/// Scroll up `rows` rows
fn scroll_up(&mut self, _: Line) {}
/// Scroll down `rows` rows
fn scroll_down(&mut self, _: Line) {}
/// Insert `count` blank lines
fn insert_blank_lines(&mut self, _: Line) {}
/// Delete `count` lines
fn delete_lines(&mut self, _: Line) {}
/// Erase `count` chars in current line following cursor
///
2016-12-17 06:48:04 +00:00
/// Erase means resetting to the default state (default colors, no content,
/// no mode flags)
fn erase_chars(&mut self, _: Column) {}
/// Delete `count` chars
///
2016-12-17 06:48:04 +00:00
/// Deleting a character is like the delete key on the keyboard - everything
/// to the right of the deleted things is shifted left.
fn delete_chars(&mut self, _: Column) {}
/// Move backward `count` tabs
fn move_backward_tabs(&mut self, _count: i64) {}
/// Move forward `count` tabs
fn move_forward_tabs(&mut self, _count: i64) {}
/// Save current cursor position
fn save_cursor_position(&mut self) {}
/// Restore cursor position
fn restore_cursor_position(&mut self) {}
/// Clear current line
fn clear_line(&mut self, _mode: LineClearMode) {}
/// Clear screen
fn clear_screen(&mut self, _mode: ClearMode) {}
/// Clear tab stops
fn clear_tabs(&mut self, _mode: TabulationClearMode) {}
/// Reset terminal state
fn reset_state(&mut self) {}
/// Reverse Index
///
2016-12-17 06:48:04 +00:00
/// Move the active position to the same horizontal position on the
/// preceding line. If the active position is at the top margin, a scroll
/// down is performed
fn reverse_index(&mut self) {}
/// set a terminal attribute
fn terminal_attribute(&mut self, _attr: Attr) {}
/// Set mode
fn set_mode(&mut self, _mode: Mode) {}
/// Unset mode
fn unset_mode(&mut self, _: Mode) {}
/// DECSTBM - Set the terminal scrolling region
fn set_scrolling_region(&mut self, _: Range<Line>) {}
/// DECKPAM - Set keypad to applications mode (ESCape instead of digits)
fn set_keypad_application_mode(&mut self) {}
2017-10-30 15:03:58 +00:00
/// DECKPNM - Set keypad to numeric mode (digits instead of ESCape seq)
fn unset_keypad_application_mode(&mut self) {}
/// Set one of the graphic character sets, G0 to G3, as the active charset.
///
2017-10-30 15:03:58 +00:00
/// 'Invoke' one of G0 to G3 in the GL area. Also referred to as shift in,
/// shift out and locking shift depending on the set being activated
fn set_active_charset(&mut self, _: CharsetIndex) {}
/// Assign a graphic character set to G0, G1, G2 or G3
///
/// 'Designate' a graphic character set as one of G0 to G3, so that it can
/// later be 'invoked' by `set_active_charset`
fn configure_charset(&mut self, _: CharsetIndex, _: StandardCharset) {}
/// Set an indexed color value
fn set_color(&mut self, _: usize, _: Rgb) {}
/// Reset an indexed color to original value
fn reset_color(&mut self, _: usize) {}
/// Set the clipboard
fn set_clipboard(&mut self, _: &str) {}
/// Run the dectest routine
fn dectest(&mut self) {}
}
/// Describes shape of cursor
#[derive(Debug, Eq, PartialEq, Copy, Clone, Deserialize)]
pub enum CursorStyle {
/// Cursor is a block like `▒`
Block,
/// Cursor is an underscore like `_`
Underline,
/// Cursor is a vertical bar `⎸`
Beam,
/// Cursor is a box like `☐`
HollowBlock,
}
impl Default for CursorStyle {
fn default() -> CursorStyle {
CursorStyle::Block
}
}
/// Terminal modes
#[derive(Debug, Eq, PartialEq)]
pub enum Mode {
/// ?1
CursorKeys = 1,
/// Select 80 or 132 columns per page
///
/// CSI ? 3 h -> set 132 column font
/// CSI ? 3 l -> reset 80 column font
///
/// Additionally,
///
/// * set margins to default positions
/// * erases all data in page memory
/// * resets DECLRMM to unavailable
/// * clears data from the status line (if set to host-writable)
DECCOLM = 3,
2017-05-01 05:10:38 +00:00
/// IRM Insert Mode
///
/// NB should be part of non-private mode enum
///
/// * `CSI 4 h` change to insert mode
/// * `CSI 4 l` reset to replacement mode
Insert = 4,
2016-08-20 03:34:33 +00:00
/// ?6
Origin = 6,
/// ?7
LineWrap = 7,
/// ?12
BlinkingCursor = 12,
2017-04-18 17:41:11 +00:00
/// 20
///
/// NB This is actually a private mode. We should consider adding a second
/// enumeration for public/private modesets.
LineFeedNewLine = 20,
2016-08-20 03:34:33 +00:00
/// ?25
ShowCursor = 25,
/// ?1000
ReportMouseClicks = 1000,
/// ?1002
ReportCellMouseMotion = 1002,
/// ?1003
ReportAllMouseMotion = 1003,
/// ?1004
ReportFocusInOut = 1004,
/// ?1006
SgrMouse = 1006,
/// ?1049
SwapScreenAndSetRestoreCursor = 1049,
/// ?2004
BracketedPaste = 2004,
}
impl Mode {
/// Create mode from a primitive
///
/// TODO lots of unhandled values..
pub fn from_primitive(private: bool, num: i64) -> Option<Mode> {
if private {
Some(match num {
1 => Mode::CursorKeys,
3 => Mode::DECCOLM,
2016-08-20 03:34:33 +00:00
6 => Mode::Origin,
7 => Mode::LineWrap,
12 => Mode::BlinkingCursor,
25 => Mode::ShowCursor,
1000 => Mode::ReportMouseClicks,
1002 => Mode::ReportCellMouseMotion,
1003 => Mode::ReportAllMouseMotion,
1004 => Mode::ReportFocusInOut,
1006 => Mode::SgrMouse,
1049 => Mode::SwapScreenAndSetRestoreCursor,
2004 => Mode::BracketedPaste,
_ => {
trace!("[unhandled] mode={:?}", num);
return None
}
})
} else {
2017-04-18 17:41:11 +00:00
Some(match num {
2017-05-01 05:10:38 +00:00
4 => Mode::Insert,
2017-04-18 17:41:11 +00:00
20 => Mode::LineFeedNewLine,
_ => return None
})
}
}
}
/// Mode for clearing line
///
/// Relative to cursor
#[derive(Debug)]
pub enum LineClearMode {
/// Clear right of cursor
Right,
/// Clear left of cursor
Left,
/// Clear entire line
All,
}
/// Mode for clearing terminal
///
/// Relative to cursor
#[derive(Debug)]
pub enum ClearMode {
/// Clear below cursor
Below,
/// Clear above cursor
Above,
/// Clear entire terminal
All,
/// Clear 'saved' lines (scrollback)
Saved
}
/// Mode for clearing tab stops
#[derive(Debug)]
pub enum TabulationClearMode {
/// Clear stop under cursor
Current,
/// Clear all stops
All,
}
/// Standard colors
///
/// The order here matters since the enum should be castable to a `usize` for
/// indexing a color list.
#[derive(Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum NamedColor {
/// Black
Black = 0,
/// Red
Red,
/// Green
Green,
/// Yellow
Yellow,
/// Blue
Blue,
/// Magenta
Magenta,
/// Cyan
Cyan,
/// White
White,
/// Bright black
BrightBlack,
/// Bright red
BrightRed,
/// Bright green
BrightGreen,
/// Bright yellow
BrightYellow,
/// Bright blue
BrightBlue,
/// Bright magenta
BrightMagenta,
/// Bright cyan
BrightCyan,
/// Bright white
BrightWhite,
/// The foreground color
Foreground = 256,
/// The background color
Background,
/// Color for the text under the cursor
CursorText,
/// Color for the cursor itself
Cursor,
/// Dim black
DimBlack,
/// Dim red
DimRed,
/// Dim green
DimGreen,
/// Dim yellow
DimYellow,
/// Dim blue
DimBlue,
/// Dim magenta
DimMagenta,
/// Dim cyan
DimCyan,
/// Dim white
DimWhite,
/// The bright foreground color
BrightForeground,
/// Dim foreground
DimForeground,
}
impl NamedColor {
2018-07-01 16:31:46 +00:00
pub fn to_bright(self) -> Self {
match self {
NamedColor::Foreground => NamedColor::BrightForeground,
NamedColor::Black => NamedColor::BrightBlack,
NamedColor::Red => NamedColor::BrightRed,
NamedColor::Green => NamedColor::BrightGreen,
NamedColor::Yellow => NamedColor::BrightYellow,
NamedColor::Blue => NamedColor::BrightBlue,
NamedColor::Magenta => NamedColor::BrightMagenta,
NamedColor::Cyan => NamedColor::BrightCyan,
NamedColor::White => NamedColor::BrightWhite,
NamedColor::DimForeground => NamedColor::Foreground,
NamedColor::DimBlack => NamedColor::Black,
NamedColor::DimRed => NamedColor::Red,
NamedColor::DimGreen => NamedColor::Green,
NamedColor::DimYellow => NamedColor::Yellow,
NamedColor::DimBlue => NamedColor::Blue,
NamedColor::DimMagenta => NamedColor::Magenta,
NamedColor::DimCyan => NamedColor::Cyan,
NamedColor::DimWhite => NamedColor::White,
val => val
}
}
2018-07-01 16:31:46 +00:00
pub fn to_dim(self) -> Self {
match self {
NamedColor::Black => NamedColor::DimBlack,
NamedColor::Red => NamedColor::DimRed,
NamedColor::Green => NamedColor::DimGreen,
NamedColor::Yellow => NamedColor::DimYellow,
NamedColor::Blue => NamedColor::DimBlue,
NamedColor::Magenta => NamedColor::DimMagenta,
NamedColor::Cyan => NamedColor::DimCyan,
NamedColor::White => NamedColor::DimWhite,
NamedColor::Foreground => NamedColor::DimForeground,
NamedColor::BrightBlack => NamedColor::Black,
NamedColor::BrightRed => NamedColor::Red,
NamedColor::BrightGreen => NamedColor::Green,
NamedColor::BrightYellow => NamedColor::Yellow,
NamedColor::BrightBlue => NamedColor::Blue,
NamedColor::BrightMagenta => NamedColor::Magenta,
NamedColor::BrightCyan => NamedColor::Cyan,
NamedColor::BrightWhite => NamedColor::White,
NamedColor::BrightForeground => NamedColor::Foreground,
val => val
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Color {
Named(NamedColor),
Spec(Rgb),
Indexed(u8),
}
/// Terminal character attributes
#[derive(Debug, Eq, PartialEq)]
pub enum Attr {
/// Clear all special abilities
Reset,
/// Bold text
Bold,
/// Dim or secondary color
Dim,
/// Italic text
Italic,
/// Underscore text
Underscore,
/// Blink cursor slowly
BlinkSlow,
/// Blink cursor fast
BlinkFast,
/// Invert colors
Reverse,
/// Do not display characters
Hidden,
/// Strikethrough text
Strike,
/// Cancel bold
CancelBold,
/// Cancel bold and dim
CancelBoldDim,
/// Cancel italic
CancelItalic,
/// Cancel underline
CancelUnderline,
/// Cancel blink
CancelBlink,
/// Cancel inversion
CancelReverse,
/// Cancel text hiding
CancelHidden,
/// Cancel strike through
CancelStrike,
/// Set indexed foreground color
Foreground(Color),
/// Set indexed background color
Background(Color),
}
/// Identifiers which can be assigned to a graphic character set
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CharsetIndex {
/// Default set, is designated as ASCII at startup
G0,
G1,
G2,
G3,
}
impl Default for CharsetIndex {
fn default() -> Self {
CharsetIndex::G0
}
}
/// Standard or common character sets which can be designated as G0-G3
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum StandardCharset {
Ascii,
SpecialCharacterAndLineDrawing,
}
impl Default for StandardCharset {
fn default() -> Self {
StandardCharset::Ascii
}
}
impl<'a, H, W> vte::Perform for Performer<'a, H, W>
where H: Handler + TermInfo + 'a,
W: io::Write + 'a
{
#[inline]
fn print(&mut self, c: char) {
self.handler.input(c);
2017-09-29 12:05:19 +00:00
self._state.preceding_char = Some(c);
}
#[inline]
fn execute(&mut self, byte: u8) {
match byte {
C0::HT => self.handler.put_tab(1),
C0::BS => self.handler.backspace(),
C0::CR => self.handler.carriage_return(),
C0::LF | C0::VT | C0::FF => self.handler.linefeed(),
C0::BEL => self.handler.bell(),
C0::SUB => self.handler.substitute(),
C0::SI => self.handler.set_active_charset(CharsetIndex::G0),
C0::SO => self.handler.set_active_charset(CharsetIndex::G1),
C1::NEL => self.handler.newline(),
C1::HTS => self.handler.set_horizontal_tabstop(),
C1::DECID => self.handler.identify_terminal(self.writer),
_ => debug!("[unhandled] execute byte={:02x}", byte)
}
}
#[inline]
fn hook(&mut self, params: &[i64], intermediates: &[u8], ignore: bool) {
debug!("[unhandled hook] params={:?}, ints: {:?}, ignore: {:?}",
params, intermediates, ignore);
}
#[inline]
fn put(&mut self, byte: u8) {
debug!("[unhandled put] byte={:?}", byte);
}
#[inline]
fn unhook(&mut self) {
debug!("[unhandled unhook]");
}
// TODO replace OSC parsing with parser combinators
#[inline]
fn osc_dispatch(&mut self, params: &[&[u8]]) {
fn unhandled(params: &[&[u8]]) {
let mut buf = String::new();
for items in params {
buf.push_str("[");
for item in *items {
buf.push_str(&format!("{:?},", *item as char));
}
buf.push_str("],");
}
debug!("[unhandled osc_dispatch]: [{}] at line {}", &buf, line!());
}
if params.is_empty() || params[0].is_empty() {
return;
}
match params[0] {
// Set window title
b"0" | b"2" => {
if params.len() >= 2 {
if let Ok(utf8_title) = str::from_utf8(params[1]) {
self.handler.set_title(utf8_title);
return;
}
}
unhandled(params);
},
2017-03-03 23:04:09 +00:00
// Set icon name
// This is ignored, since alacritty has no concept of tabs
b"1" => return,
2017-03-03 23:04:09 +00:00
// Set color index
b"4" => {
if params.len() > 1 && params.len() % 2 != 0 {
for chunk in params[1..].chunks(2) {
let index = parse_number(chunk[0]);
let color = parse_rgb_color(chunk[1]);
if let (Some(i), Some(c)) = (index, color) {
self.handler.set_color(i as usize, c);
return;
}
}
}
unhandled(params);
}
// Set foreground color
b"10" => {
if params.len() >= 2 {
if let Some(color) = parse_rgb_color(params[1]) {
self.handler.set_color(NamedColor::Foreground as usize, color);
return;
}
}
unhandled(params);
}
// Set background color
b"11" => {
if params.len() >= 2 {
if let Some(color) = parse_rgb_color(params[1]) {
self.handler.set_color(NamedColor::Background as usize, color);
return;
}
}
unhandled(params);
}
// Set text cursor color
b"12" => {
if params.len() >= 2 {
if let Some(color) = parse_rgb_color(params[1]) {
self.handler.set_color(NamedColor::Cursor as usize, color);
return;
}
}
unhandled(params);
}
// Set cursor style
b"50" => {
if params.len() >= 2 && params[1].len() >= 13 && params[1][0..12] == *b"CursorShape=" {
let style = match params[1][12] as char {
'0' => CursorStyle::Block,
'1' => CursorStyle::Beam,
'2' => CursorStyle::Underline,
_ => return unhandled(params),
};
self.handler.set_cursor_style(Some(style));
return;
}
unhandled(params);
}
// Set clipboard
b"52" => {
if params.len() < 3 {
return unhandled(params);
}
match params[2] {
b"?" => unhandled(params),
selection => {
if let Ok(string) = base64::decode(selection) {
if let Ok(utf8_string) = str::from_utf8(&string) {
self.handler.set_clipboard(utf8_string);
}
}
}
}
}
// Reset color index
b"104" => {
// Reset all color indexes when no parameters are given
if params.len() == 1 {
for i in 0..256 {
self.handler.reset_color(i);
}
return;
}
// Reset color indexes given as parameters
for param in &params[1..] {
match parse_number(param) {
Some(index) => self.handler.reset_color(index as usize),
None => unhandled(params),
}
}
}
// Reset foreground color
b"110" => self.handler.reset_color(NamedColor::Foreground as usize),
// Reset background color
b"111" => self.handler.reset_color(NamedColor::Background as usize),
// Reset text cursor color
b"112" => self.handler.reset_color(NamedColor::Cursor as usize),
_ => unhandled(params),
}
}
#[inline]
fn csi_dispatch(
&mut self,
args: &[i64],
intermediates: &[u8],
_ignore: bool,
action: char
) {
let private = intermediates.get(0).map(|b| *b == b'?').unwrap_or(false);
let handler = &mut self.handler;
let writer = &mut self.writer;
macro_rules! unhandled {
() => {{
Display errors and warnings To make sure that all error and information reporting to the user is unified, all instances of `print!`, `eprint!`, `println!` and `eprintln!` have been removed and replaced by logging. When `RUST_LOG` is not specified, the default Alacritty logger now also prints to both the stderr and a log file. The log file is only created when a message is written to it and its name is printed to stdout the first time it is used. Whenever a warning or an error has been written to the log file/stderr, a message is now displayed in Alacritty which points to the log file where the full error is documented. The message is cleared whenever the screen is cleared using either the `clear` command or the `Ctrl+L` key binding. To make sure that log files created by root don't prevent normal users from interacting with them, the Alacritty log file is `/tmp/Alacritty-$PID.log`. Since it's still possible that the log file can't be created, the UI error/warning message now informs the user if the message was only written to stderr. The reason why it couldn't be created is then printed to stderr. To make sure the deletion of the log file at runtime doesn't create any issues, the file is re-created if a write is attempted without the file being present. To help with debugging Alacritty issues, a timestamp and the error level are printed in all log messages. All log messages now follow this format: [YYYY-MM-DD HH:MM] [LEVEL] Message Since it's not unusual to spawn a lot of different terminal emulators without restarting, Alacritty can create a ton of different log files. To combat this problem, logfiles are removed by default after Alacritty has been closed. If the user wants to persist the log of a single session, the `--persistent_logging` option can be used. For persisting all log files, the `persistent_logging` option can be set in the configuration file
2018-11-17 14:39:13 +00:00
debug!("[Unhandled CSI] action={:?}, args={:?}, intermediates={:?}",
action, args, intermediates);
return;
}}
}
macro_rules! arg_or_default {
(idx: $idx:expr, default: $default:expr) => {
args.get($idx).and_then(|v| {
if *v == 0 {
None
} else {
Some(*v)
}
}).unwrap_or($default)
}
}
match action {
'@' => handler.insert_blank(Column(arg_or_default!(idx: 0, default: 1) as usize)),
'A' => {
handler.move_up(Line(arg_or_default!(idx: 0, default: 1) as usize));
},
2017-09-29 12:05:19 +00:00
'b' => {
if let Some(c) = self._state.preceding_char {
for _ in 0..arg_or_default!(idx: 0, default: 1) {
handler.input(c);
}
}
else {
debug!("tried to repeat with no preceding char");
2017-09-29 12:05:19 +00:00
}
},
'B' | 'e' => handler.move_down(Line(arg_or_default!(idx: 0, default: 1) as usize)),
2017-05-07 20:08:23 +00:00
'c' => handler.identify_terminal(writer),
'C' | 'a' => handler.move_forward(Column(arg_or_default!(idx: 0, default: 1) as usize)),
'D' => handler.move_backward(Column(arg_or_default!(idx: 0, default: 1) as usize)),
'E' => handler.move_down_and_cr(Line(arg_or_default!(idx: 0, default: 1) as usize)),
'F' => handler.move_up_and_cr(Line(arg_or_default!(idx: 0, default: 1) as usize)),
'g' => {
let mode = match arg_or_default!(idx: 0, default: 0) {
0 => TabulationClearMode::Current,
3 => TabulationClearMode::All,
_ => unhandled!(),
};
handler.clear_tabs(mode);
},
'G' | '`' => handler.goto_col(Column(arg_or_default!(idx: 0, default: 1) as usize - 1)),
'H' | 'f' => {
let y = arg_or_default!(idx: 0, default: 1) as usize;
let x = arg_or_default!(idx: 1, default: 1) as usize;
handler.goto(Line(y - 1), Column(x - 1));
},
'I' => handler.move_forward_tabs(arg_or_default!(idx: 0, default: 1)),
'J' => {
let mode = match arg_or_default!(idx: 0, default: 0) {
0 => ClearMode::Below,
1 => ClearMode::Above,
2 => ClearMode::All,
3 => ClearMode::Saved,
_ => unhandled!(),
};
handler.clear_screen(mode);
},
'K' => {
let mode = match arg_or_default!(idx: 0, default: 0) {
0 => LineClearMode::Right,
1 => LineClearMode::Left,
2 => LineClearMode::All,
_ => unhandled!(),
};
handler.clear_line(mode);
},
'S' => handler.scroll_up(Line(arg_or_default!(idx: 0, default: 1) as usize)),
'T' => handler.scroll_down(Line(arg_or_default!(idx: 0, default: 1) as usize)),
'L' => handler.insert_blank_lines(Line(arg_or_default!(idx: 0, default: 1) as usize)),
'l' => {
for arg in args {
let mode = Mode::from_primitive(private, *arg);
match mode {
Some(mode) => handler.unset_mode(mode),
None => unhandled!(),
}
}
},
'M' => handler.delete_lines(Line(arg_or_default!(idx: 0, default: 1) as usize)),
'X' => handler.erase_chars(Column(arg_or_default!(idx: 0, default: 1) as usize)),
'P' => handler.delete_chars(Column(arg_or_default!(idx: 0, default: 1) as usize)),
'Z' => handler.move_backward_tabs(arg_or_default!(idx: 0, default: 1)),
'd' => handler.goto_line(Line(arg_or_default!(idx: 0, default: 1) as usize - 1)),
'h' => {
for arg in args {
let mode = Mode::from_primitive(private, *arg);
match mode {
Some(mode) => handler.set_mode(mode),
None => unhandled!(),
}
}
},
'm' => {
// Sometimes a C-style for loop is just what you need
let mut i = 0; // C-for initializer
if args.is_empty() {
handler.terminal_attribute(Attr::Reset);
return;
}
loop {
if i >= args.len() { // C-for condition
break;
}
let attr = match args[i] {
0 => Attr::Reset,
1 => Attr::Bold,
2 => Attr::Dim,
3 => Attr::Italic,
4 => Attr::Underscore,
5 => Attr::BlinkSlow,
6 => Attr::BlinkFast,
7 => Attr::Reverse,
8 => Attr::Hidden,
9 => Attr::Strike,
21 => Attr::CancelBold,
22 => Attr::CancelBoldDim,
23 => Attr::CancelItalic,
24 => Attr::CancelUnderline,
25 => Attr::CancelBlink,
27 => Attr::CancelReverse,
28 => Attr::CancelHidden,
29 => Attr::CancelStrike,
30 => Attr::Foreground(Color::Named(NamedColor::Black)),
31 => Attr::Foreground(Color::Named(NamedColor::Red)),
32 => Attr::Foreground(Color::Named(NamedColor::Green)),
33 => Attr::Foreground(Color::Named(NamedColor::Yellow)),
34 => Attr::Foreground(Color::Named(NamedColor::Blue)),
35 => Attr::Foreground(Color::Named(NamedColor::Magenta)),
36 => Attr::Foreground(Color::Named(NamedColor::Cyan)),
37 => Attr::Foreground(Color::Named(NamedColor::White)),
38 => {
let mut start = 0;
if let Some(color) = parse_color(&args[i..], &mut start) {
i += start;
Attr::Foreground(color)
} else {
break;
}
},
39 => Attr::Foreground(Color::Named(NamedColor::Foreground)),
40 => Attr::Background(Color::Named(NamedColor::Black)),
41 => Attr::Background(Color::Named(NamedColor::Red)),
42 => Attr::Background(Color::Named(NamedColor::Green)),
43 => Attr::Background(Color::Named(NamedColor::Yellow)),
44 => Attr::Background(Color::Named(NamedColor::Blue)),
45 => Attr::Background(Color::Named(NamedColor::Magenta)),
46 => Attr::Background(Color::Named(NamedColor::Cyan)),
47 => Attr::Background(Color::Named(NamedColor::White)),
48 => {
let mut start = 0;
if let Some(color) = parse_color(&args[i..], &mut start) {
i += start;
Attr::Background(color)
} else {
break;
}
},
49 => Attr::Background(Color::Named(NamedColor::Background)),
90 => Attr::Foreground(Color::Named(NamedColor::BrightBlack)),
91 => Attr::Foreground(Color::Named(NamedColor::BrightRed)),
92 => Attr::Foreground(Color::Named(NamedColor::BrightGreen)),
93 => Attr::Foreground(Color::Named(NamedColor::BrightYellow)),
94 => Attr::Foreground(Color::Named(NamedColor::BrightBlue)),
95 => Attr::Foreground(Color::Named(NamedColor::BrightMagenta)),
96 => Attr::Foreground(Color::Named(NamedColor::BrightCyan)),
97 => Attr::Foreground(Color::Named(NamedColor::BrightWhite)),
100 => Attr::Background(Color::Named(NamedColor::BrightBlack)),
101 => Attr::Background(Color::Named(NamedColor::BrightRed)),
102 => Attr::Background(Color::Named(NamedColor::BrightGreen)),
103 => Attr::Background(Color::Named(NamedColor::BrightYellow)),
104 => Attr::Background(Color::Named(NamedColor::BrightBlue)),
105 => Attr::Background(Color::Named(NamedColor::BrightMagenta)),
106 => Attr::Background(Color::Named(NamedColor::BrightCyan)),
107 => Attr::Background(Color::Named(NamedColor::BrightWhite)),
_ => unhandled!(),
};
handler.terminal_attribute(attr);
i += 1; // C-for expr
}
}
2017-05-08 14:50:34 +00:00
'n' => handler.device_status(writer, arg_or_default!(idx: 0, default: 0) as usize),
'r' => {
if private {
unhandled!();
}
let arg0 = arg_or_default!(idx: 0, default: 1) as usize;
let top = Line(arg0 - 1);
// Bottom should be included in the range, but range end is not
// usually included. One option would be to use an inclusive
// range, but instead we just let the open range end be 1
// higher.
let arg1 = arg_or_default!(idx: 1, default: handler.lines().0 as _) as usize;
let bottom = Line(arg1);
handler.set_scrolling_region(top..bottom);
},
's' => handler.save_cursor_position(),
'u' => handler.restore_cursor_position(),
'q' => {
let style = match arg_or_default!(idx: 0, default: 0) {
0 => None,
1 | 2 => Some(CursorStyle::Block),
3 | 4 => Some(CursorStyle::Underline),
5 | 6 => Some(CursorStyle::Beam),
_ => unhandled!()
};
handler.set_cursor_style(style);
}
_ => unhandled!(),
}
}
#[inline]
2016-12-17 06:48:04 +00:00
fn esc_dispatch(
&mut self,
params: &[i64],
intermediates: &[u8],
_ignore: bool,
byte: u8
) {
macro_rules! unhandled {
() => {{
Display errors and warnings To make sure that all error and information reporting to the user is unified, all instances of `print!`, `eprint!`, `println!` and `eprintln!` have been removed and replaced by logging. When `RUST_LOG` is not specified, the default Alacritty logger now also prints to both the stderr and a log file. The log file is only created when a message is written to it and its name is printed to stdout the first time it is used. Whenever a warning or an error has been written to the log file/stderr, a message is now displayed in Alacritty which points to the log file where the full error is documented. The message is cleared whenever the screen is cleared using either the `clear` command or the `Ctrl+L` key binding. To make sure that log files created by root don't prevent normal users from interacting with them, the Alacritty log file is `/tmp/Alacritty-$PID.log`. Since it's still possible that the log file can't be created, the UI error/warning message now informs the user if the message was only written to stderr. The reason why it couldn't be created is then printed to stderr. To make sure the deletion of the log file at runtime doesn't create any issues, the file is re-created if a write is attempted without the file being present. To help with debugging Alacritty issues, a timestamp and the error level are printed in all log messages. All log messages now follow this format: [YYYY-MM-DD HH:MM] [LEVEL] Message Since it's not unusual to spawn a lot of different terminal emulators without restarting, Alacritty can create a ton of different log files. To combat this problem, logfiles are removed by default after Alacritty has been closed. If the user wants to persist the log of a single session, the `--persistent_logging` option can be used. For persisting all log files, the `persistent_logging` option can be set in the configuration file
2018-11-17 14:39:13 +00:00
debug!("[unhandled] esc_dispatch params={:?}, ints={:?}, byte={:?} ({:02x})",
params, intermediates, byte as char, byte);
return;
}}
}
macro_rules! configure_charset {
($charset:path) => {{
let index: CharsetIndex = match intermediates.first().cloned() {
Some(b'(') => CharsetIndex::G0,
Some(b')') => CharsetIndex::G1,
Some(b'*') => CharsetIndex::G2,
Some(b'+') => CharsetIndex::G3,
_ => unhandled!(),
};
self.handler.configure_charset(index, $charset)
}}
}
match byte {
b'B' => configure_charset!(StandardCharset::Ascii),
b'D' => self.handler.linefeed(),
b'E' => {
self.handler.linefeed();
self.handler.carriage_return();
}
b'H' => self.handler.set_horizontal_tabstop(),
b'M' => self.handler.reverse_index(),
b'Z' => self.handler.identify_terminal(self.writer),
b'c' => self.handler.reset_state(),
b'0' => configure_charset!(StandardCharset::SpecialCharacterAndLineDrawing),
b'7' => self.handler.save_cursor_position(),
b'8' => {
if !intermediates.is_empty() && intermediates[0] == b'#' {
self.handler.dectest();
} else {
self.handler.restore_cursor_position();
}
}
b'=' => self.handler.set_keypad_application_mode(),
b'>' => self.handler.unset_keypad_application_mode(),
b'\\' => (), // String terminator, do nothing (parser handles as string terminator)
_ => unhandled!(),
}
}
}
/// Parse a color specifier from list of attributes
fn parse_color(attrs: &[i64], i: &mut usize) -> Option<Color> {
if attrs.len() < 2 {
return None;
}
match attrs[*i+1] {
2 => {
// RGB color spec
if attrs.len() < 5 {
debug!("Expected RGB color spec; got {:?}", attrs);
return None;
}
let r = attrs[*i+2];
let g = attrs[*i+3];
let b = attrs[*i+4];
*i += 4;
2017-01-07 00:26:31 +00:00
let range = 0..256;
2017-01-06 23:48:23 +00:00
if !range.contains_(r) || !range.contains_(g) || !range.contains_(b) {
debug!("Invalid RGB color spec: ({}, {}, {})", r, g, b);
return None;
}
Some(Color::Spec(Rgb {
r: r as u8,
g: g as u8,
b: b as u8
}))
},
5 => {
if attrs.len() < 3 {
debug!("Expected color index; got {:?}", attrs);
None
} else {
*i += 2;
let idx = attrs[*i];
match idx {
0 ..= 255 => {
Some(Color::Indexed(idx as u8))
},
_ => {
debug!("Invalid color index: {}", idx);
None
}
}
}
},
_ => {
debug!("Unexpected color attr: {}", attrs[*i+1]);
None
}
}
}
/// C0 set of 7-bit control characters (from ANSI X3.4-1977).
#[allow(non_snake_case)]
pub mod C0 {
/// Null filler, terminal should ignore this character
pub const NUL: u8 = 0x00;
/// Start of Header
pub const SOH: u8 = 0x01;
/// Start of Text, implied end of header
pub const STX: u8 = 0x02;
/// End of Text, causes some terminal to respond with ACK or NAK
pub const ETX: u8 = 0x03;
/// End of Transmission
pub const EOT: u8 = 0x04;
/// Enquiry, causes terminal to send ANSWER-BACK ID
pub const ENQ: u8 = 0x05;
/// Acknowledge, usually sent by terminal in response to ETX
pub const ACK: u8 = 0x06;
/// Bell, triggers the bell, buzzer, or beeper on the terminal
pub const BEL: u8 = 0x07;
/// Backspace, can be used to define overstruck characters
pub const BS: u8 = 0x08;
/// Horizontal Tabulation, move to next predetermined position
pub const HT: u8 = 0x09;
/// Linefeed, move to same position on next line (see also NL)
pub const LF: u8 = 0x0A;
/// Vertical Tabulation, move to next predetermined line
pub const VT: u8 = 0x0B;
/// Form Feed, move to next form or page
pub const FF: u8 = 0x0C;
/// Carriage Return, move to first character of current line
pub const CR: u8 = 0x0D;
/// Shift Out, switch to G1 (other half of character set)
pub const SO: u8 = 0x0E;
/// Shift In, switch to G0 (normal half of character set)
pub const SI: u8 = 0x0F;
/// Data Link Escape, interpret next control character specially
pub const DLE: u8 = 0x10;
/// (DC1) Terminal is allowed to resume transmitting
pub const XON: u8 = 0x11;
/// Device Control 2, causes ASR-33 to activate paper-tape reader
pub const DC2: u8 = 0x12;
/// (DC2) Terminal must pause and refrain from transmitting
pub const XOFF: u8 = 0x13;
/// Device Control 4, causes ASR-33 to deactivate paper-tape reader
pub const DC4: u8 = 0x14;
/// Negative Acknowledge, used sometimes with ETX and ACK
pub const NAK: u8 = 0x15;
/// Synchronous Idle, used to maintain timing in Sync communication
pub const SYN: u8 = 0x16;
/// End of Transmission block
pub const ETB: u8 = 0x17;
/// Cancel (makes VT100 abort current escape sequence if any)
pub const CAN: u8 = 0x18;
/// End of Medium
pub const EM: u8 = 0x19;
/// Substitute (VT100 uses this to display parity errors)
pub const SUB: u8 = 0x1A;
/// Prefix to an escape sequence
pub const ESC: u8 = 0x1B;
/// File Separator
pub const FS: u8 = 0x1C;
/// Group Separator
pub const GS: u8 = 0x1D;
/// Record Separator (sent by VT132 in block-transfer mode)
pub const RS: u8 = 0x1E;
/// Unit Separator
pub const US: u8 = 0x1F;
/// Delete, should be ignored by terminal
pub const DEL: u8 = 0x7f;
}
/// C1 set of 8-bit control characters (from ANSI X3.64-1979)
///
/// 0x80 (@), 0x81 (A), 0x82 (B), 0x83 (C) are reserved
/// 0x98 (X), 0x99 (Y) are reserved
2017-10-30 15:03:58 +00:00
/// 0x9a (Z) is 'reserved', but causes DEC terminals to respond with DA codes
#[allow(non_snake_case)]
pub mod C1 {
/// Reserved
pub const PAD: u8 = 0x80;
/// Reserved
pub const HOP: u8 = 0x81;
/// Reserved
pub const BPH: u8 = 0x82;
/// Reserved
pub const NBH: u8 = 0x83;
/// Index, moves down one line same column regardless of NL
pub const IND: u8 = 0x84;
/// New line, moves done one line and to first column (CR+LF)
pub const NEL: u8 = 0x85;
2017-10-30 15:03:58 +00:00
/// Start of Selected Area to be sent to auxiliary output device
pub const SSA: u8 = 0x86;
/// End of Selected Area to be sent to auxiliary output device
pub const ESA: u8 = 0x87;
/// Horizontal Tabulation Set at current position
pub const HTS: u8 = 0x88;
/// Hor Tab Justify, moves string to next tab position
pub const HTJ: u8 = 0x89;
/// Vertical Tabulation Set at current line
pub const VTS: u8 = 0x8A;
/// Partial Line Down (subscript)
pub const PLD: u8 = 0x8B;
/// Partial Line Up (superscript)
pub const PLU: u8 = 0x8C;
/// Reverse Index, go up one line, reverse scroll if necessary
pub const RI: u8 = 0x8D;
/// Single Shift to G2
pub const SS2: u8 = 0x8E;
/// Single Shift to G3 (VT100 uses this for sending PF keys)
pub const SS3: u8 = 0x8F;
/// Device Control String, terminated by ST (VT125 enters graphics)
pub const DCS: u8 = 0x90;
/// Private Use 1
pub const PU1: u8 = 0x91;
/// Private Use 2
pub const PU2: u8 = 0x92;
/// Set Transmit State
pub const STS: u8 = 0x93;
/// Cancel character, ignore previous character
pub const CCH: u8 = 0x94;
/// Message Waiting, turns on an indicator on the terminal
pub const MW: u8 = 0x95;
/// Start of Protected Area
pub const SPA: u8 = 0x96;
/// End of Protected Area
pub const EPA: u8 = 0x97;
/// SOS
pub const SOS: u8 = 0x98;
/// SGCI
pub const SGCI: u8 = 0x99;
/// DECID - Identify Terminal
pub const DECID: u8 = 0x9a;
2017-10-30 15:03:58 +00:00
/// Control Sequence Introducer
pub const CSI: u8 = 0x9B;
/// String Terminator (VT125 exits graphics)
pub const ST: u8 = 0x9C;
/// Operating System Command (reprograms intelligent terminal)
pub const OSC: u8 = 0x9D;
/// Privacy Message (password verification), terminated by ST
pub const PM: u8 = 0x9E;
/// Application Program Command (to word processor), term by ST
pub const APC: u8 = 0x9F;
}
// Tests for parsing escape sequences
//
// Byte sequences used in these tests are recording of pty stdout.
#[cfg(test)]
mod tests {
use std::io;
use crate::index::{Line, Column};
use super::{Processor, Handler, Attr, TermInfo, Color, StandardCharset, CharsetIndex, parse_rgb_color, parse_number};
use crate::Rgb;
/// The /dev/null of `io::Write`
struct Void;
impl io::Write for Void {
fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
Ok(bytes.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
#[derive(Default)]
struct AttrHandler {
attr: Option<Attr>,
}
impl Handler for AttrHandler {
fn terminal_attribute(&mut self, attr: Attr) {
self.attr = Some(attr);
}
}
impl TermInfo for AttrHandler {
fn lines(&self) -> Line {
Line(24)
}
fn cols(&self) -> Column {
Column(80)
}
}
#[test]
fn parse_control_attribute() {
static BYTES: &'static [u8] = &[
0x1b, 0x5b, 0x31, 0x6d
];
let mut parser = Processor::new();
let mut handler = AttrHandler::default();
for byte in &BYTES[..] {
parser.advance(&mut handler, *byte, &mut Void);
}
assert_eq!(handler.attr, Some(Attr::Bold));
}
#[test]
fn parse_truecolor_attr() {
static BYTES: &'static [u8] = &[
0x1b, 0x5b, 0x33, 0x38, 0x3b, 0x32, 0x3b, 0x31, 0x32,
0x38, 0x3b, 0x36, 0x36, 0x3b, 0x32, 0x35, 0x35, 0x6d
];
let mut parser = Processor::new();
let mut handler = AttrHandler::default();
for byte in &BYTES[..] {
parser.advance(&mut handler, *byte, &mut Void);
}
let spec = Rgb {
r: 128,
g: 66,
b: 255
};
assert_eq!(handler.attr, Some(Attr::Foreground(Color::Spec(spec))));
}
/// No exactly a test; useful for debugging
#[test]
fn parse_zsh_startup() {
static BYTES: &'static [u8] = &[
2016-12-17 06:48:04 +00:00
0x1b, 0x5b, 0x31, 0x6d, 0x1b, 0x5b, 0x37, 0x6d, 0x25, 0x1b, 0x5b,
0x32, 0x37, 0x6d, 0x1b, 0x5b, 0x31, 0x6d, 0x1b, 0x5b, 0x30, 0x6d,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x0d, 0x20, 0x0d, 0x0d, 0x1b, 0x5b, 0x30, 0x6d, 0x1b,
0x5b, 0x32, 0x37, 0x6d, 0x1b, 0x5b, 0x32, 0x34, 0x6d, 0x1b, 0x5b,
0x4a, 0x6a, 0x77, 0x69, 0x6c, 0x6d, 0x40, 0x6a, 0x77, 0x69, 0x6c,
0x6d, 0x2d, 0x64, 0x65, 0x73, 0x6b, 0x20, 0x1b, 0x5b, 0x30, 0x31,
0x3b, 0x33, 0x32, 0x6d, 0xe2, 0x9e, 0x9c, 0x20, 0x1b, 0x5b, 0x30,
0x31, 0x3b, 0x33, 0x32, 0x6d, 0x20, 0x1b, 0x5b, 0x33, 0x36, 0x6d,
0x7e, 0x2f, 0x63, 0x6f, 0x64, 0x65
];
let mut handler = AttrHandler::default();
let mut parser = Processor::new();
for byte in &BYTES[..] {
parser.advance(&mut handler, *byte, &mut Void);
}
}
struct CharsetHandler {
index: CharsetIndex,
charset: StandardCharset,
}
impl Default for CharsetHandler {
fn default() -> CharsetHandler {
CharsetHandler {
index: CharsetIndex::G0,
charset: StandardCharset::Ascii,
}
}
}
impl Handler for CharsetHandler {
fn configure_charset(&mut self, index: CharsetIndex, charset: StandardCharset) {
self.index = index;
self.charset = charset;
}
fn set_active_charset(&mut self, index: CharsetIndex) {
self.index = index;
}
}
impl TermInfo for CharsetHandler {
fn lines(&self) -> Line { Line(200) }
fn cols(&self) -> Column { Column(90) }
}
#[test]
fn parse_designate_g0_as_line_drawing() {
static BYTES: &'static [u8] = &[0x1b, b'(', b'0'];
let mut parser = Processor::new();
let mut handler = CharsetHandler::default();
for byte in &BYTES[..] {
parser.advance(&mut handler, *byte, &mut Void);
}
assert_eq!(handler.index, CharsetIndex::G0);
assert_eq!(handler.charset, StandardCharset::SpecialCharacterAndLineDrawing);
}
#[test]
fn parse_designate_g1_as_line_drawing_and_invoke() {
static BYTES: &'static [u8] = &[0x1b, 0x29, 0x30, 0x0e];
let mut parser = Processor::new();
let mut handler = CharsetHandler::default();
for byte in &BYTES[..3] {
parser.advance(&mut handler, *byte, &mut Void);
}
assert_eq!(handler.index, CharsetIndex::G1);
assert_eq!(handler.charset, StandardCharset::SpecialCharacterAndLineDrawing);
let mut handler = CharsetHandler::default();
parser.advance(&mut handler, BYTES[3], &mut Void);
assert_eq!(handler.index, CharsetIndex::G1);
}
#[test]
fn parse_valid_rgb_color() {
assert_eq!(parse_rgb_color(b"rgb:11/aa/ff"), Some(Rgb { r: 0x11, g: 0xaa, b: 0xff }));
}
#[test]
fn parse_valid_rgb_color2() {
assert_eq!(parse_rgb_color(b"#11aaff"), Some(Rgb { r: 0x11, g: 0xaa, b: 0xff }));
}
#[test]
fn parse_invalid_number() {
assert_eq!(parse_number(b"1abc"), None);
}
#[test]
fn parse_valid_number() {
assert_eq!(parse_number(b"123"), Some(123));
}
#[test]
fn parse_number_too_large() {
assert_eq!(parse_number(b"321"), None);
}
}