diff options
author | Theodore Dubois <tblodt@icloud.com> | 2019-04-28 06:24:58 -0700 |
---|---|---|
committer | Christian Duerr <chrisduerr@users.noreply.github.com> | 2019-04-28 13:24:58 +0000 |
commit | dbd8538762ef8968a493e1bf996e8693479ca783 (patch) | |
tree | 32ac2a6a5e01238a272d4ba534551d2e42903c7a /alacritty_terminal/src | |
parent | 9c6d12ea2c863ba76015bdedc00db13b7307725a (diff) | |
download | alacritty-dbd8538762ef8968a493e1bf996e8693479ca783.tar.gz alacritty-dbd8538762ef8968a493e1bf996e8693479ca783.zip |
Split alacritty into a separate crates
The crate containing the entry point is called alacritty, and the crate
containing everything else is called alacritty_terminal.
Diffstat (limited to 'alacritty_terminal/src')
35 files changed, 18839 insertions, 0 deletions
diff --git a/alacritty_terminal/src/ansi.rs b/alacritty_terminal/src/ansi.rs new file mode 100644 index 00000000..c0ebb79c --- /dev/null +++ b/alacritty_terminal/src/ansi.rs @@ -0,0 +1,1568 @@ +// 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 crate::index::{Column, Contains, Line}; +use base64; +use glutin::MouseCursor; +use vte; + +use crate::term::color::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!(); + 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 +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 { 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) {} + + // Report device status + fn device_status<W: io::Write>(&mut self, _: &mut W, _: usize) {} + + /// 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 + /// + /// Erase means resetting to the default state (default colors, no content, + /// no mode flags) + fn erase_chars(&mut self, _: Column) {} + + /// Delete `count` chars + /// + /// 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 + /// + /// 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) {} + + /// 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. + /// + /// '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, Hash, 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, + + /// Invisible cursor + Hidden, +} + +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, + /// 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, + /// ?6 + Origin = 6, + /// ?7 + LineWrap = 7, + /// ?12 + BlinkingCursor = 12, + /// 20 + /// + /// NB This is actually a private mode. We should consider adding a second + /// enumeration for public/private modesets. + LineFeedNewLine = 20, + /// ?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, + 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!("[unimplemented] primitive mode: {}", num); + return None; + }, + }) + } else { + Some(match num { + 4 => Mode::Insert, + 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 { + 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, + } + } + + 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, + /// Strikeout 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 strikeout + 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); + 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); + }, + + // Set icon name + // This is ignored, since alacritty has no concept of tabs + b"1" => return, + + // 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 ¶ms[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 { + () => {{ + 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)); + }, + '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"); + } + }, + 'B' | 'e' => handler.move_down(Line(arg_or_default!(idx: 0, default: 1) as usize)), + '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 + } + }, + '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] + fn esc_dispatch(&mut self, params: &[i64], intermediates: &[u8], _ignore: bool, byte: u8) { + macro_rules! unhandled { + () => {{ + 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; + + let range = 0..256; + 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 +/// 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; + /// 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; + /// 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 super::{ + parse_number, parse_rgb_color, Attr, CharsetIndex, Color, Handler, Processor, + StandardCharset, TermInfo, + }; + use crate::index::{Column, Line}; + use crate::term::color::Rgb; + use std::io; + + /// 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] = &[ + 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); + } +} diff --git a/alacritty_terminal/src/cli.rs b/alacritty_terminal/src/cli.rs new file mode 100644 index 00000000..2cddbc82 --- /dev/null +++ b/alacritty_terminal/src/cli.rs @@ -0,0 +1,243 @@ +// 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. +use ::log; +use clap::{crate_authors, crate_description, crate_name, crate_version, App, Arg}; + +use crate::config::{Delta, Dimensions, Shell}; +use crate::index::{Column, Line}; +use crate::window::DEFAULT_NAME; +use std::borrow::Cow; +use std::path::{Path, PathBuf}; + +/// Options specified on the command line +pub struct Options { + pub live_config_reload: Option<bool>, + pub print_events: bool, + pub ref_test: bool, + pub dimensions: Option<Dimensions>, + pub position: Option<Delta<i32>>, + pub title: Option<String>, + pub class: Option<String>, + pub log_level: log::LevelFilter, + pub command: Option<Shell<'static>>, + pub working_dir: Option<PathBuf>, + pub config: Option<PathBuf>, + pub persistent_logging: bool, +} + +impl Default for Options { + fn default() -> Options { + Options { + live_config_reload: None, + print_events: false, + ref_test: false, + dimensions: None, + position: None, + title: None, + class: None, + log_level: log::LevelFilter::Warn, + command: None, + working_dir: None, + config: None, + persistent_logging: false, + } + } +} + +impl Options { + /// Build `Options` from command line arguments + pub fn load() -> Options { + let mut options = Options::default(); + + let matches = App::new(crate_name!()) + .version(crate_version!()) + .author(crate_authors!("\n")) + .about(crate_description!()) + .arg(Arg::with_name("ref-test").long("ref-test").help("Generates ref test")) + .arg( + Arg::with_name("live-config-reload") + .long("live-config-reload") + .help("Enable automatic config reloading"), + ) + .arg( + Arg::with_name("no-live-config-reload") + .long("no-live-config-reload") + .help("Disable automatic config reloading") + .conflicts_with("live-config-reload"), + ) + .arg( + Arg::with_name("print-events") + .long("print-events") + .help("Print all events to stdout"), + ) + .arg( + Arg::with_name("persistent-logging") + .long("persistent-logging") + .help("Keep the log file after quitting Alacritty"), + ) + .arg( + Arg::with_name("dimensions") + .long("dimensions") + .short("d") + .value_names(&["columns", "lines"]) + .help( + "Defines the window dimensions. Falls back to size specified by window \ + manager if set to 0x0 [default: 0x0]", + ), + ) + .arg( + Arg::with_name("position") + .long("position") + .allow_hyphen_values(true) + .value_names(&["x-pos", "y-pos"]) + .help( + "Defines the window position. Falls back to position specified by window \ + manager if unset [default: unset]", + ), + ) + .arg( + Arg::with_name("title") + .long("title") + .short("t") + .takes_value(true) + .help(&format!("Defines the window title [default: {}]", DEFAULT_NAME)), + ) + .arg( + Arg::with_name("class") + .long("class") + .takes_value(true) + .help(&format!("Defines window class on Linux [default: {}]", DEFAULT_NAME)), + ) + .arg( + Arg::with_name("q") + .short("q") + .multiple(true) + .conflicts_with("v") + .help("Reduces the level of verbosity (the min level is -qq)"), + ) + .arg( + Arg::with_name("v") + .short("v") + .multiple(true) + .conflicts_with("q") + .help("Increases the level of verbosity (the max level is -vvv)"), + ) + .arg( + Arg::with_name("working-directory") + .long("working-directory") + .takes_value(true) + .help("Start the shell in the specified working directory"), + ) + .arg(Arg::with_name("config-file").long("config-file").takes_value(true).help( + "Specify alternative configuration file [default: \ + $XDG_CONFIG_HOME/alacritty/alacritty.yml]", + )) + .arg( + Arg::with_name("command") + .long("command") + .short("e") + .multiple(true) + .takes_value(true) + .min_values(1) + .allow_hyphen_values(true) + .help("Command and args to execute (must be last argument)"), + ) + .get_matches(); + + if matches.is_present("ref-test") { + options.ref_test = true; + } + + if matches.is_present("print-events") { + options.print_events = true; + } + + if matches.is_present("live-config-reload") { + options.live_config_reload = Some(true); + } else if matches.is_present("no-live-config-reload") { + options.live_config_reload = Some(false); + } + + if matches.is_present("persistent-logging") { + options.persistent_logging = true; + } + + if let Some(mut dimensions) = matches.values_of("dimensions") { + let width = dimensions.next().map(|w| w.parse().map(Column)); + let height = dimensions.next().map(|h| h.parse().map(Line)); + if let (Some(Ok(width)), Some(Ok(height))) = (width, height) { + options.dimensions = Some(Dimensions::new(width, height)); + } + } + + if let Some(mut position) = matches.values_of("position") { + let x = position.next().map(str::parse); + let y = position.next().map(str::parse); + if let (Some(Ok(x)), Some(Ok(y))) = (x, y) { + options.position = Some(Delta { x, y }); + } + } + + options.class = matches.value_of("class").map(ToOwned::to_owned); + options.title = matches.value_of("title").map(ToOwned::to_owned); + + match matches.occurrences_of("q") { + 0 => {}, + 1 => options.log_level = log::LevelFilter::Error, + 2 | _ => options.log_level = log::LevelFilter::Off, + } + + match matches.occurrences_of("v") { + 0 if !options.print_events => {}, + 0 | 1 => options.log_level = log::LevelFilter::Info, + 2 => options.log_level = log::LevelFilter::Debug, + 3 | _ => options.log_level = log::LevelFilter::Trace, + } + + if let Some(dir) = matches.value_of("working-directory") { + options.working_dir = Some(PathBuf::from(dir.to_string())); + } + + if let Some(path) = matches.value_of("config-file") { + options.config = Some(PathBuf::from(path.to_string())); + } + + if let Some(mut args) = matches.values_of("command") { + // The following unwrap is guaranteed to succeed. + // If 'command' exists it must also have a first item since + // Arg::min_values(1) is set. + let command = String::from(args.next().unwrap()); + let args = args.map(String::from).collect(); + options.command = Some(Shell::new_with_args(command, args)); + } + + options + } + + pub fn dimensions(&self) -> Option<Dimensions> { + self.dimensions + } + + pub fn position(&self) -> Option<Delta<i32>> { + self.position + } + + pub fn command(&self) -> Option<&Shell<'_>> { + self.command.as_ref() + } + + pub fn config_path(&self) -> Option<Cow<'_, Path>> { + self.config.as_ref().map(|p| Cow::Borrowed(p.as_path())) + } +} diff --git a/alacritty_terminal/src/config/bindings.rs b/alacritty_terminal/src/config/bindings.rs new file mode 100644 index 00000000..7e69182b --- /dev/null +++ b/alacritty_terminal/src/config/bindings.rs @@ -0,0 +1,233 @@ +// 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. +use glutin::{ModifiersState, MouseButton}; + +use super::Key; +use crate::input::{Action, KeyBinding, MouseBinding}; +use crate::term::TermMode; + +macro_rules! bindings { + ( + $ty:ident; + $( + $key:path + $(,[$($mod:ident: $enabled:expr),*])* + $(,+$mode:expr)* + $(,~$notmode:expr)* + ;$action:expr + );* + $(;)* + ) => {{ + let mut v = Vec::new(); + + $( + let mut _mods = ModifiersState { + $($($mod: $enabled),*,)* + ..Default::default() + }; + let mut _mode = TermMode::empty(); + $(_mode = $mode;)* + let mut _notmode = TermMode::empty(); + $(_notmode = $notmode;)* + + v.push($ty { + trigger: $key, + mods: _mods, + mode: _mode, + notmode: _notmode, + action: $action, + }); + )* + + v + }} +} + +pub fn default_mouse_bindings() -> Vec<MouseBinding> { + bindings!( + MouseBinding; + MouseButton::Middle; Action::PasteSelection; + ) +} + +pub fn default_key_bindings() -> Vec<KeyBinding> { + let mut bindings = bindings!( + KeyBinding; + Key::Paste; Action::Paste; + Key::Copy; Action::Copy; + Key::L, [ctrl: true]; Action::ClearLogNotice; + Key::L, [ctrl: true]; Action::Esc("\x0c".into()); + Key::Home, [alt: true]; Action::Esc("\x1b[1;3H".into()); + Key::Home, +TermMode::APP_CURSOR; Action::Esc("\x1bOH".into()); + Key::Home, ~TermMode::APP_CURSOR; Action::Esc("\x1b[H".into()); + Key::End, [alt: true]; Action::Esc("\x1b[1;3F".into()); + Key::End, +TermMode::APP_CURSOR; Action::Esc("\x1bOF".into()); + Key::End, ~TermMode::APP_CURSOR; Action::Esc("\x1b[F".into()); + Key::PageUp, [shift: true], ~TermMode::ALT_SCREEN; Action::ScrollPageUp; + Key::PageUp, [shift: true], +TermMode::ALT_SCREEN; Action::Esc("\x1b[5;2~".into()); + Key::PageUp, [ctrl: true]; Action::Esc("\x1b[5;5~".into()); + Key::PageUp, [alt: true]; Action::Esc("\x1b[5;3~".into()); + Key::PageUp; Action::Esc("\x1b[5~".into()); + Key::PageDown, [shift: true], ~TermMode::ALT_SCREEN; Action::ScrollPageDown; + Key::PageDown, [shift: true], +TermMode::ALT_SCREEN; Action::Esc("\x1b[6;2~".into()); + Key::PageDown, [ctrl: true]; Action::Esc("\x1b[6;5~".into()); + Key::PageDown, [alt: true]; Action::Esc("\x1b[6;3~".into()); + Key::PageDown; Action::Esc("\x1b[6~".into()); + Key::Tab, [shift: true]; Action::Esc("\x1b[Z".into()); + Key::Back; Action::Esc("\x7f".into()); + Key::Back, [alt: true]; Action::Esc("\x1b\x7f".into()); + Key::Insert; Action::Esc("\x1b[2~".into()); + Key::Delete; Action::Esc("\x1b[3~".into()); + Key::Left, [shift: true]; Action::Esc("\x1b[1;2D".into()); + Key::Left, [ctrl: true]; Action::Esc("\x1b[1;5D".into()); + Key::Left, [alt: true]; Action::Esc("\x1b[1;3D".into()); + Key::Left, ~TermMode::APP_CURSOR; Action::Esc("\x1b[D".into()); + Key::Left, +TermMode::APP_CURSOR; Action::Esc("\x1bOD".into()); + Key::Right, [shift: true]; Action::Esc("\x1b[1;2C".into()); + Key::Right, [ctrl: true]; Action::Esc("\x1b[1;5C".into()); + Key::Right, [alt: true]; Action::Esc("\x1b[1;3C".into()); + Key::Right, ~TermMode::APP_CURSOR; Action::Esc("\x1b[C".into()); + Key::Right, +TermMode::APP_CURSOR; Action::Esc("\x1bOC".into()); + Key::Up, [shift: true]; Action::Esc("\x1b[1;2A".into()); + Key::Up, [ctrl: true]; Action::Esc("\x1b[1;5A".into()); + Key::Up, [alt: true]; Action::Esc("\x1b[1;3A".into()); + Key::Up, ~TermMode::APP_CURSOR; Action::Esc("\x1b[A".into()); + Key::Up, +TermMode::APP_CURSOR; Action::Esc("\x1bOA".into()); + Key::Down, [shift: true]; Action::Esc("\x1b[1;2B".into()); + Key::Down, [ctrl: true]; Action::Esc("\x1b[1;5B".into()); + Key::Down, [alt: true]; Action::Esc("\x1b[1;3B".into()); + Key::Down, ~TermMode::APP_CURSOR; Action::Esc("\x1b[B".into()); + Key::Down, +TermMode::APP_CURSOR; Action::Esc("\x1bOB".into()); + Key::F1; Action::Esc("\x1bOP".into()); + Key::F2; Action::Esc("\x1bOQ".into()); + Key::F3; Action::Esc("\x1bOR".into()); + Key::F4; Action::Esc("\x1bOS".into()); + Key::F5; Action::Esc("\x1b[15~".into()); + Key::F6; Action::Esc("\x1b[17~".into()); + Key::F7; Action::Esc("\x1b[18~".into()); + Key::F8; Action::Esc("\x1b[19~".into()); + Key::F9; Action::Esc("\x1b[20~".into()); + Key::F10; Action::Esc("\x1b[21~".into()); + Key::F11; Action::Esc("\x1b[23~".into()); + Key::F12; Action::Esc("\x1b[24~".into()); + Key::F1, [shift: true]; Action::Esc("\x1b[1;2P".into()); + Key::F2, [shift: true]; Action::Esc("\x1b[1;2Q".into()); + Key::F3, [shift: true]; Action::Esc("\x1b[1;2R".into()); + Key::F4, [shift: true]; Action::Esc("\x1b[1;2S".into()); + Key::F5, [shift: true]; Action::Esc("\x1b[15;2~".into()); + Key::F6, [shift: true]; Action::Esc("\x1b[17;2~".into()); + Key::F7, [shift: true]; Action::Esc("\x1b[18;2~".into()); + Key::F8, [shift: true]; Action::Esc("\x1b[19;2~".into()); + Key::F9, [shift: true]; Action::Esc("\x1b[20;2~".into()); + Key::F10, [shift: true]; Action::Esc("\x1b[21;2~".into()); + Key::F11, [shift: true]; Action::Esc("\x1b[23;2~".into()); + Key::F12, [shift: true]; Action::Esc("\x1b[24;2~".into()); + Key::F1, [ctrl: true]; Action::Esc("\x1b[1;5P".into()); + Key::F2, [ctrl: true]; Action::Esc("\x1b[1;5Q".into()); + Key::F3, [ctrl: true]; Action::Esc("\x1b[1;5R".into()); + Key::F4, [ctrl: true]; Action::Esc("\x1b[1;5S".into()); + Key::F5, [ctrl: true]; Action::Esc("\x1b[15;5~".into()); + Key::F6, [ctrl: true]; Action::Esc("\x1b[17;5~".into()); + Key::F7, [ctrl: true]; Action::Esc("\x1b[18;5~".into()); + Key::F8, [ctrl: true]; Action::Esc("\x1b[19;5~".into()); + Key::F9, [ctrl: true]; Action::Esc("\x1b[20;5~".into()); + Key::F10, [ctrl: true]; Action::Esc("\x1b[21;5~".into()); + Key::F11, [ctrl: true]; Action::Esc("\x1b[23;5~".into()); + Key::F12, [ctrl: true]; Action::Esc("\x1b[24;5~".into()); + Key::F1, [alt: true]; Action::Esc("\x1b[1;6P".into()); + Key::F2, [alt: true]; Action::Esc("\x1b[1;6Q".into()); + Key::F3, [alt: true]; Action::Esc("\x1b[1;6R".into()); + Key::F4, [alt: true]; Action::Esc("\x1b[1;6S".into()); + Key::F5, [alt: true]; Action::Esc("\x1b[15;6~".into()); + Key::F6, [alt: true]; Action::Esc("\x1b[17;6~".into()); + Key::F7, [alt: true]; Action::Esc("\x1b[18;6~".into()); + Key::F8, [alt: true]; Action::Esc("\x1b[19;6~".into()); + Key::F9, [alt: true]; Action::Esc("\x1b[20;6~".into()); + Key::F10, [alt: true]; Action::Esc("\x1b[21;6~".into()); + Key::F11, [alt: true]; Action::Esc("\x1b[23;6~".into()); + Key::F12, [alt: true]; Action::Esc("\x1b[24;6~".into()); + Key::F1, [logo: true]; Action::Esc("\x1b[1;3P".into()); + Key::F2, [logo: true]; Action::Esc("\x1b[1;3Q".into()); + Key::F3, [logo: true]; Action::Esc("\x1b[1;3R".into()); + Key::F4, [logo: true]; Action::Esc("\x1b[1;3S".into()); + Key::F5, [logo: true]; Action::Esc("\x1b[15;3~".into()); + Key::F6, [logo: true]; Action::Esc("\x1b[17;3~".into()); + Key::F7, [logo: true]; Action::Esc("\x1b[18;3~".into()); + Key::F8, [logo: true]; Action::Esc("\x1b[19;3~".into()); + Key::F9, [logo: true]; Action::Esc("\x1b[20;3~".into()); + Key::F10, [logo: true]; Action::Esc("\x1b[21;3~".into()); + Key::F11, [logo: true]; Action::Esc("\x1b[23;3~".into()); + Key::F12, [logo: true]; Action::Esc("\x1b[24;3~".into()); + Key::NumpadEnter; Action::Esc("\n".into()); + ); + + bindings.extend(platform_key_bindings()); + + bindings +} + +#[cfg(not(any(target_os = "macos", test)))] +fn common_keybindings() -> Vec<KeyBinding> { + bindings!( + KeyBinding; + Key::V, [ctrl: true, shift: true]; Action::Paste; + Key::C, [ctrl: true, shift: true]; Action::Copy; + Key::Insert, [shift: true]; Action::PasteSelection; + Key::Key0, [ctrl: true]; Action::ResetFontSize; + Key::Equals, [ctrl: true]; Action::IncreaseFontSize; + Key::Add, [ctrl: true]; Action::IncreaseFontSize; + Key::Subtract, [ctrl: true]; Action::DecreaseFontSize; + Key::Minus, [ctrl: true]; Action::DecreaseFontSize; + ) +} + +#[cfg(not(any(target_os = "macos", target_os = "windows", test)))] +pub fn platform_key_bindings() -> Vec<KeyBinding> { + common_keybindings() +} + +#[cfg(all(target_os = "windows", not(test)))] +pub fn platform_key_bindings() -> Vec<KeyBinding> { + let mut bindings = bindings!( + KeyBinding; + Key::Return, [alt: true]; Action::ToggleFullscreen; + ); + bindings.extend(common_keybindings()); + bindings +} + +#[cfg(all(target_os = "macos", not(test)))] +pub fn platform_key_bindings() -> Vec<KeyBinding> { + bindings!( + KeyBinding; + Key::Key0, [logo: true]; Action::ResetFontSize; + Key::Equals, [logo: true]; Action::IncreaseFontSize; + Key::Add, [logo: true]; Action::IncreaseFontSize; + Key::Minus, [logo: true]; Action::DecreaseFontSize; + Key::F, [ctrl: true, logo: true]; Action::ToggleFullscreen; + Key::K, [logo: true]; Action::ClearHistory; + Key::K, [logo: true]; Action::Esc("\x0c".into()); + Key::V, [logo: true]; Action::Paste; + Key::C, [logo: true]; Action::Copy; + Key::H, [logo: true]; Action::Hide; + Key::Q, [logo: true]; Action::Quit; + Key::W, [logo: true]; Action::Quit; + ) +} + +// Don't return any bindings for tests since they are commented-out by default +#[cfg(test)] +pub fn platform_key_bindings() -> Vec<KeyBinding> { + vec![] +} diff --git a/alacritty_terminal/src/config/mod.rs b/alacritty_terminal/src/config/mod.rs new file mode 100644 index 00000000..9502e3fd --- /dev/null +++ b/alacritty_terminal/src/config/mod.rs @@ -0,0 +1,2749 @@ +//! Configuration definitions and file loading +//! +//! Alacritty reads from a config file at startup to determine various runtime +//! parameters including font family and style, font size, etc. In the future, +//! the config file will also hold user and platform specific keybindings. +use std::borrow::Cow; +use std::collections::HashMap; +use std::fs::File; +use std::io::{self, Read, Write}; +use std::path::{Path, PathBuf}; +use std::str::FromStr; +use std::sync::mpsc; +use std::time::Duration; +use std::{env, fmt}; + +use font::Size; +use glutin::ModifiersState; +use notify::{watcher, DebouncedEvent, RecursiveMode, Watcher}; +use serde::de::Error as SerdeError; +use serde::de::{MapAccess, Unexpected, Visitor}; +use serde::{self, de, Deserialize}; +use serde_yaml; + +use crate::ansi::CursorStyle; +use crate::cli::Options; +use crate::index::{Column, Line}; +use crate::input::{Action, Binding, KeyBinding, MouseBinding}; +use crate::term::color::Rgb; + +mod bindings; + +pub const SOURCE_FILE_PATH: &str = file!(); +const MAX_SCROLLBACK_LINES: u32 = 100_000; +static DEFAULT_ALACRITTY_CONFIG: &'static str = + include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../alacritty.yml")); + +#[serde(default)] +#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] +pub struct Selection { + #[serde(deserialize_with = "deserialize_escape_chars")] + pub semantic_escape_chars: String, + #[serde(deserialize_with = "failure_default")] + pub save_to_clipboard: bool, +} + +impl Default for Selection { + fn default() -> Selection { + Selection { + semantic_escape_chars: default_escape_chars(), + save_to_clipboard: Default::default(), + } + } +} + +fn deserialize_escape_chars<'a, D>(deserializer: D) -> ::std::result::Result<String, D::Error> +where + D: de::Deserializer<'a>, +{ + match String::deserialize(deserializer) { + Ok(escape_chars) => Ok(escape_chars), + Err(err) => { + error!("Problem with config: {}; using default value", err); + Ok(default_escape_chars()) + }, + } +} + +fn default_escape_chars() -> String { + String::from(",│`|:\"' ()[]{}<>") +} + +#[serde(default)] +#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] +pub struct ClickHandler { + #[serde(deserialize_with = "deserialize_duration_ms")] + pub threshold: Duration, +} + +impl Default for ClickHandler { + fn default() -> Self { + ClickHandler { threshold: default_threshold_ms() } + } +} + +fn default_threshold_ms() -> Duration { + Duration::from_millis(300) +} + +fn deserialize_duration_ms<'a, D>(deserializer: D) -> ::std::result::Result<Duration, D::Error> +where + D: de::Deserializer<'a>, +{ + match u64::deserialize(deserializer) { + Ok(threshold_ms) => Ok(Duration::from_millis(threshold_ms)), + Err(err) => { + error!("Problem with config: {}; using default value", err); + Ok(default_threshold_ms()) + }, + } +} + +#[serde(default)] +#[derive(Default, Clone, Debug, Deserialize, PartialEq, Eq)] +pub struct Mouse { + #[serde(deserialize_with = "failure_default")] + pub double_click: ClickHandler, + #[serde(deserialize_with = "failure_default")] + pub triple_click: ClickHandler, + #[serde(deserialize_with = "failure_default")] + pub hide_when_typing: bool, + #[serde(deserialize_with = "failure_default")] + pub url: Url, + + // TODO: DEPRECATED + pub faux_scrollback_lines: Option<usize>, +} + +#[serde(default)] +#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] +pub struct Url { + // Program for opening links + #[serde(deserialize_with = "deserialize_launcher")] + pub launcher: Option<CommandWrapper>, + + // Modifier used to open links + #[serde(deserialize_with = "deserialize_modifiers")] + pub modifiers: ModifiersState, +} + +fn deserialize_launcher<'a, D>( + deserializer: D, +) -> ::std::result::Result<Option<CommandWrapper>, D::Error> +where + D: de::Deserializer<'a>, +{ + let default = Url::default().launcher; + + // Deserialize to generic value + let val = match serde_yaml::Value::deserialize(deserializer) { + Ok(val) => val, + Err(err) => { + error!("Problem with config: {}; using {}", err, default.clone().unwrap().program()); + return Ok(default); + }, + }; + + // Accept `None` to disable the launcher + if val.as_str().filter(|v| v.to_lowercase() == "none").is_some() { + return Ok(None); + } + + match <Option<CommandWrapper>>::deserialize(val) { + Ok(launcher) => Ok(launcher), + Err(err) => { + error!("Problem with config: {}; using {}", err, default.clone().unwrap().program()); + Ok(default) + }, + } +} + +impl Default for Url { + fn default() -> Url { + Url { + #[cfg(not(any(target_os = "macos", windows)))] + launcher: Some(CommandWrapper::Just(String::from("xdg-open"))), + #[cfg(target_os = "macos")] + launcher: Some(CommandWrapper::Just(String::from("open"))), + #[cfg(windows)] + launcher: Some(CommandWrapper::Just(String::from("explorer"))), + modifiers: Default::default(), + } + } +} + +fn deserialize_modifiers<'a, D>(deserializer: D) -> ::std::result::Result<ModifiersState, D::Error> +where + D: de::Deserializer<'a>, +{ + ModsWrapper::deserialize(deserializer).map(ModsWrapper::into_inner) +} + +/// `VisualBellAnimations` are modeled after a subset of CSS transitions and Robert +/// Penner's Easing Functions. +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)] +pub enum VisualBellAnimation { + Ease, // CSS + EaseOut, // CSS + EaseOutSine, // Penner + EaseOutQuad, // Penner + EaseOutCubic, // Penner + EaseOutQuart, // Penner + EaseOutQuint, // Penner + EaseOutExpo, // Penner + EaseOutCirc, // Penner + Linear, +} + +impl Default for VisualBellAnimation { + fn default() -> Self { + VisualBellAnimation::EaseOutExpo + } +} + +#[serde(default)] +#[derive(Debug, Deserialize, PartialEq, Eq)] +pub struct VisualBellConfig { + /// Visual bell animation function + #[serde(deserialize_with = "failure_default")] + animation: VisualBellAnimation, + + /// Visual bell duration in milliseconds + #[serde(deserialize_with = "failure_default")] + duration: u16, + + /// Visual bell flash color + #[serde(deserialize_with = "rgb_from_hex")] + color: Rgb, +} + +impl Default for VisualBellConfig { + fn default() -> VisualBellConfig { + VisualBellConfig { + animation: Default::default(), + duration: Default::default(), + color: default_visual_bell_color(), + } + } +} + +fn default_visual_bell_color() -> Rgb { + Rgb { r: 255, g: 255, b: 255 } +} + +impl VisualBellConfig { + /// Visual bell animation + #[inline] + pub fn animation(&self) -> VisualBellAnimation { + self.animation + } + + /// Visual bell duration in milliseconds + #[inline] + pub fn duration(&self) -> Duration { + Duration::from_millis(u64::from(self.duration)) + } + + /// Visual bell flash color + #[inline] + pub fn color(&self) -> Rgb { + self.color + } +} + +#[derive(Debug, Deserialize, PartialEq, Eq)] +pub struct Shell<'a> { + program: Cow<'a, str>, + + #[serde(default, deserialize_with = "failure_default")] + args: Vec<String>, +} + +impl<'a> Shell<'a> { + pub fn new<S>(program: S) -> Shell<'a> + where + S: Into<Cow<'a, str>>, + { + Shell { program: program.into(), args: Vec::new() } + } + + pub fn new_with_args<S>(program: S, args: Vec<String>) -> Shell<'a> + where + S: Into<Cow<'a, str>>, + { + Shell { program: program.into(), args } + } + + pub fn program(&self) -> &str { + &*self.program + } + + pub fn args(&self) -> &[String] { + self.args.as_slice() + } +} + +/// Wrapper around f32 that represents an alpha value between 0.0 and 1.0 +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Alpha(f32); + +impl Alpha { + pub fn new(value: f32) -> Self { + Alpha(Self::clamp_to_valid_range(value)) + } + + pub fn set(&mut self, value: f32) { + self.0 = Self::clamp_to_valid_range(value); + } + + #[inline] + pub fn get(self) -> f32 { + self.0 + } + + fn clamp_to_valid_range(value: f32) -> f32 { + if value < 0.0 { + 0.0 + } else if value > 1.0 { + 1.0 + } else { + value + } + } +} + +impl Default for Alpha { + fn default() -> Self { + Alpha(1.0) + } +} + +#[derive(Debug, Deserialize, Copy, Clone, PartialEq, Eq)] +pub enum StartupMode { + Windowed, + Maximized, + Fullscreen, + #[cfg(target_os = "macos")] + SimpleFullscreen, +} + +impl Default for StartupMode { + fn default() -> StartupMode { + StartupMode::Windowed + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum Decorations { + Full, + Transparent, + Buttonless, + None, +} + +impl Default for Decorations { + fn default() -> Decorations { + Decorations::Full + } +} + +impl<'de> Deserialize<'de> for Decorations { + fn deserialize<D>(deserializer: D) -> ::std::result::Result<Decorations, D::Error> + where + D: de::Deserializer<'de>, + { + struct DecorationsVisitor; + + impl<'de> Visitor<'de> for DecorationsVisitor { + type Value = Decorations; + + fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("Some subset of full|transparent|buttonless|none") + } + + #[cfg(target_os = "macos")] + fn visit_str<E>(self, value: &str) -> ::std::result::Result<Decorations, E> + where + E: de::Error, + { + match value.to_lowercase().as_str() { + "transparent" => Ok(Decorations::Transparent), + "buttonless" => Ok(Decorations::Buttonless), + "none" => Ok(Decorations::None), + "full" => Ok(Decorations::Full), + "true" => { + error!( + "Deprecated decorations boolean value, use one of \ + transparent|buttonless|none|full instead; falling back to \"full\"" + ); + Ok(Decorations::Full) + }, + "false" => { + error!( + "Deprecated decorations boolean value, use one of \ + transparent|buttonless|none|full instead; falling back to \"none\"" + ); + Ok(Decorations::None) + }, + _ => { + error!("Invalid decorations value: {}; using default value", value); + Ok(Decorations::Full) + }, + } + } + + #[cfg(not(target_os = "macos"))] + fn visit_str<E>(self, value: &str) -> ::std::result::Result<Decorations, E> + where + E: de::Error, + { + match value.to_lowercase().as_str() { + "none" => Ok(Decorations::None), + "full" => Ok(Decorations::Full), + "true" => { + error!( + "Deprecated decorations boolean value, use one of none|full instead; \ + falling back to \"full\"" + ); + Ok(Decorations::Full) + }, + "false" => { + error!( + "Deprecated decorations boolean value, use one of none|full instead; \ + falling back to \"none\"" + ); + Ok(Decorations::None) + }, + "transparent" | "buttonless" => { + error!("macOS-only decorations value: {}; using default value", value); + Ok(Decorations::Full) + }, + _ => { + error!("Invalid decorations value: {}; using default value", value); + Ok(Decorations::Full) + }, + } + } + } + + deserializer.deserialize_str(DecorationsVisitor) + } +} + +#[serde(default)] +#[derive(Debug, Copy, Clone, Deserialize, PartialEq, Eq)] +pub struct WindowConfig { + /// Initial dimensions + #[serde(default, deserialize_with = "failure_default")] + dimensions: Dimensions, + + /// Initial position + #[serde(default, deserialize_with = "failure_default")] + position: Option<Delta<i32>>, + + /// Pixel padding + #[serde(deserialize_with = "deserialize_padding")] + padding: Delta<u8>, + + /// Draw the window with title bar / borders + #[serde(deserialize_with = "failure_default")] + decorations: Decorations, + + /// Spread out additional padding evenly + #[serde(deserialize_with = "failure_default")] + dynamic_padding: bool, + + /// Startup mode + #[serde(deserialize_with = "failure_default")] + startup_mode: StartupMode, + + /// TODO: DEPRECATED + #[serde(deserialize_with = "failure_default")] + start_maximized: Option<bool>, +} + +impl Default for WindowConfig { + fn default() -> Self { + WindowConfig { + dimensions: Default::default(), + position: Default::default(), + padding: default_padding(), + decorations: Default::default(), + dynamic_padding: Default::default(), + start_maximized: Default::default(), + startup_mode: Default::default(), + } + } +} + +fn default_padding() -> Delta<u8> { + Delta { x: 2, y: 2 } +} + +fn deserialize_padding<'a, D>(deserializer: D) -> ::std::result::Result<Delta<u8>, D::Error> +where + D: de::Deserializer<'a>, +{ + match Delta::deserialize(deserializer) { + Ok(delta) => Ok(delta), + Err(err) => { + error!("Problem with config: {}; using default value", err); + Ok(default_padding()) + }, + } +} + +impl WindowConfig { + pub fn decorations(&self) -> Decorations { + self.decorations + } + + pub fn dynamic_padding(&self) -> bool { + self.dynamic_padding + } + + pub fn startup_mode(&self) -> StartupMode { + self.startup_mode + } + + pub fn position(&self) -> Option<Delta<i32>> { + self.position + } +} + +/// Top-level config type +#[derive(Debug, PartialEq, Deserialize)] +pub struct Config { + /// Pixel padding + #[serde(default, deserialize_with = "failure_default")] + padding: Option<Delta<u8>>, + + /// TERM env variable + #[serde(default, deserialize_with = "failure_default")] + env: HashMap<String, String>, + + /// Font configuration + #[serde(default, deserialize_with = "failure_default")] + font: Font, + + /// Should show render timer + #[serde(default, deserialize_with = "failure_default")] + render_timer: bool, + + /// Should draw bold text with brighter colors instead of bold font + #[serde(default = "default_true_bool", deserialize_with = "deserialize_true_bool")] + draw_bold_text_with_bright_colors: bool, + + #[serde(default, deserialize_with = "failure_default")] + colors: Colors, + + /// Background opacity from 0.0 to 1.0 + #[serde(default, deserialize_with = "failure_default")] + background_opacity: Alpha, + + /// Window configuration + #[serde(default, deserialize_with = "failure_default")] + window: WindowConfig, + + /// Keybindings + #[serde(default = "default_key_bindings", deserialize_with = "deserialize_key_bindings")] + key_bindings: Vec<KeyBinding>, + + /// Bindings for the mouse + #[serde(default = "default_mouse_bindings", deserialize_with = "deserialize_mouse_bindings")] + mouse_bindings: Vec<MouseBinding>, + + #[serde(default, deserialize_with = "failure_default")] + selection: Selection, + + #[serde(default, deserialize_with = "failure_default")] + mouse: Mouse, + + /// Path to a shell program to run on startup + #[serde(default, deserialize_with = "failure_default")] + shell: Option<Shell<'static>>, + + /// Path where config was loaded from + #[serde(default, deserialize_with = "failure_default")] + config_path: Option<PathBuf>, + + /// Visual bell configuration + #[serde(default, deserialize_with = "failure_default")] + visual_bell: VisualBellConfig, + + /// Use dynamic title + #[serde(default = "default_true_bool", deserialize_with = "deserialize_true_bool")] + dynamic_title: bool, + + /// Live config reload + #[serde(default = "default_true_bool", deserialize_with = "deserialize_true_bool")] + live_config_reload: bool, + + /// Number of spaces in one tab + #[serde(default = "default_tabspaces", deserialize_with = "deserialize_tabspaces")] + tabspaces: usize, + + /// How much scrolling history to keep + #[serde(default, deserialize_with = "failure_default")] + scrolling: Scrolling, + + /// Cursor configuration + #[serde(default, deserialize_with = "failure_default")] + cursor: Cursor, + + /// Keep the log file after quitting + #[serde(default, deserialize_with = "failure_default")] + persistent_logging: bool, + + /// Enable experimental conpty backend instead of using winpty. + /// Will only take effect on Windows 10 Oct 2018 and later. + #[cfg(windows)] + #[serde(default, deserialize_with = "failure_default")] + enable_experimental_conpty_backend: bool, + + /// Send escape sequences using the alt key. + #[serde(default = "default_true_bool", deserialize_with = "deserialize_true_bool")] + alt_send_esc: bool, + + // TODO: DEPRECATED + custom_cursor_colors: Option<bool>, + + // TODO: DEPRECATED + hide_cursor_when_typing: Option<bool>, + + // TODO: DEPRECATED + cursor_style: Option<CursorStyle>, + + // TODO: DEPRECATED + unfocused_hollow_cursor: Option<bool>, + + // TODO: DEPRECATED + dimensions: Option<Dimensions>, +} + +impl Default for Config { + fn default() -> Self { + serde_yaml::from_str(DEFAULT_ALACRITTY_CONFIG).expect("default config is invalid") + } +} + +fn default_key_bindings() -> Vec<KeyBinding> { + bindings::default_key_bindings() +} + +fn default_mouse_bindings() -> Vec<MouseBinding> { + bindings::default_mouse_bindings() +} + +fn deserialize_key_bindings<'a, D>( + deserializer: D, +) -> ::std::result::Result<Vec<KeyBinding>, D::Error> +where + D: de::Deserializer<'a>, +{ + deserialize_bindings(deserializer, bindings::default_key_bindings()) +} + +fn deserialize_mouse_bindings<'a, D>( + deserializer: D, +) -> ::std::result::Result<Vec<MouseBinding>, D::Error> +where + D: de::Deserializer<'a>, +{ + deserialize_bindings(deserializer, bindings::default_mouse_bindings()) +} + +fn deserialize_bindings<'a, D, T>( + deserializer: D, + mut default: Vec<Binding<T>>, +) -> ::std::result::Result<Vec<Binding<T>>, D::Error> +where + D: de::Deserializer<'a>, + T: Copy + Eq + std::hash::Hash + std::fmt::Debug, + Binding<T>: de::Deserialize<'a>, +{ + let mut bindings: Vec<Binding<T>> = failure_default_vec(deserializer)?; + + for binding in bindings.iter() { + default.retain(|b| !b.triggers_match(binding)); + } + + bindings.extend(default); + + Ok(bindings) +} + +fn failure_default_vec<'a, D, T>(deserializer: D) -> ::std::result::Result<Vec<T>, D::Error> +where + D: de::Deserializer<'a>, + T: Deserialize<'a>, +{ + // Deserialize as generic vector + let vec = match Vec::<serde_yaml::Value>::deserialize(deserializer) { + Ok(vec) => vec, + Err(err) => { + error!("Problem with config: {}; using empty vector", err); + return Ok(Vec::new()); + }, + }; + + // Move to lossy vector + let mut bindings: Vec<T> = Vec::new(); + for value in vec { + match T::deserialize(value) { + Ok(binding) => bindings.push(binding), + Err(err) => { + error!("Problem with config: {}; skipping value", err); + }, + } + } + + Ok(bindings) +} + +fn default_tabspaces() -> usize { + 8 +} + +fn deserialize_tabspaces<'a, D>(deserializer: D) -> ::std::result::Result<usize, D::Error> +where + D: de::Deserializer<'a>, +{ + match usize::deserialize(deserializer) { + Ok(value) => Ok(value), + Err(err) => { + error!("Problem with config: {}; using 8", err); + Ok(default_tabspaces()) + }, + } +} + +fn deserialize_true_bool<'a, D>(deserializer: D) -> ::std::result::Result<bool, D::Error> +where + D: de::Deserializer<'a>, +{ + match bool::deserialize(deserializer) { + Ok(value) => Ok(value), + Err(err) => { + error!("Problem with config: {}; using true", err); + Ok(true) + }, + } +} + +fn default_true_bool() -> bool { + true +} + +fn failure_default<'a, D, T>(deserializer: D) -> ::std::result::Result<T, D::Error> +where + D: de::Deserializer<'a>, + T: Deserialize<'a> + Default, +{ + match T::deserialize(deserializer) { + Ok(value) => Ok(value), + Err(err) => { + error!("Problem with config: {}; using default value", err); + Ok(T::default()) + }, + } +} + +/// Struct for scrolling related settings +#[serde(default)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)] +pub struct Scrolling { + #[serde(deserialize_with = "deserialize_scrolling_history")] + pub history: u32, + #[serde(deserialize_with = "deserialize_scrolling_multiplier")] + pub multiplier: u8, + #[serde(deserialize_with = "deserialize_scrolling_multiplier")] + pub faux_multiplier: u8, + #[serde(deserialize_with = "failure_default")] + pub auto_scroll: bool, +} + +impl Default for Scrolling { + fn default() -> Self { + Self { + history: default_scrolling_history(), + multiplier: default_scrolling_multiplier(), + faux_multiplier: default_scrolling_multiplier(), + auto_scroll: Default::default(), + } + } +} + +fn default_scrolling_history() -> u32 { + 10_000 +} + +// Default for normal and faux scrolling +fn default_scrolling_multiplier() -> u8 { + 3 +} + +fn deserialize_scrolling_history<'a, D>(deserializer: D) -> ::std::result::Result<u32, D::Error> +where + D: de::Deserializer<'a>, +{ + match u32::deserialize(deserializer) { + Ok(lines) => { + if lines > MAX_SCROLLBACK_LINES { + error!( + "Problem with config: scrollback size is {}, but expected a maximum of {}; \ + using {1} instead", + lines, MAX_SCROLLBACK_LINES, + ); + Ok(MAX_SCROLLBACK_LINES) + } else { + Ok(lines) + } + }, + Err(err) => { + error!("Problem with config: {}; using default value", err); + Ok(default_scrolling_history()) + }, + } +} + +fn deserialize_scrolling_multiplier<'a, D>(deserializer: D) -> ::std::result::Result<u8, D::Error> +where + D: de::Deserializer<'a>, +{ + match u8::deserialize(deserializer) { + Ok(lines) => Ok(lines), + Err(err) => { + error!("Problem with config: {}; using default value", err); + Ok(default_scrolling_multiplier()) + }, + } +} + +/// Newtype for implementing deserialize on glutin Mods +/// +/// Our deserialize impl wouldn't be covered by a derive(Deserialize); see the +/// impl below. +#[derive(Debug, Copy, Clone, Hash, Default, Eq, PartialEq)] +struct ModsWrapper(ModifiersState); + +impl ModsWrapper { + fn into_inner(self) -> ModifiersState { + self.0 + } +} + +impl<'a> de::Deserialize<'a> for ModsWrapper { + fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error> + where + D: de::Deserializer<'a>, + { + struct ModsVisitor; + + impl<'a> Visitor<'a> for ModsVisitor { + type Value = ModsWrapper; + + fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("Some subset of Command|Shift|Super|Alt|Option|Control") + } + + fn visit_str<E>(self, value: &str) -> ::std::result::Result<ModsWrapper, E> + where + E: de::Error, + { + let mut res = ModifiersState::default(); + for modifier in value.split('|') { + match modifier.trim() { + "Command" | "Super" => res.logo = true, + "Shift" => res.shift = true, + "Alt" | "Option" => res.alt = true, + "Control" => res.ctrl = true, + "None" => (), + _ => error!("Unknown modifier {:?}", modifier), + } + } + + Ok(ModsWrapper(res)) + } + } + + deserializer.deserialize_str(ModsVisitor) + } +} + +struct ActionWrapper(crate::input::Action); + +impl ActionWrapper { + fn into_inner(self) -> crate::input::Action { + self.0 + } +} + +impl<'a> de::Deserialize<'a> for ActionWrapper { + fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error> + where + D: de::Deserializer<'a>, + { + struct ActionVisitor; + + impl<'a> Visitor<'a> for ActionVisitor { + type Value = ActionWrapper; + + fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str( + "Paste, Copy, PasteSelection, IncreaseFontSize, DecreaseFontSize, \ + ResetFontSize, ScrollPageUp, ScrollPageDown, ScrollLineUp, ScrollLineDown, \ + ScrollToTop, ScrollToBottom, ClearHistory, Hide, ClearLogNotice, \ + SpawnNewInstance, ToggleFullscreen, ToggleSimpleFullscreen, None or Quit", + ) + } + + fn visit_str<E>(self, value: &str) -> ::std::result::Result<ActionWrapper, E> + where + E: de::Error, + { + Ok(ActionWrapper(match value { + "Paste" => Action::Paste, + "Copy" => Action::Copy, + "PasteSelection" => Action::PasteSelection, + "IncreaseFontSize" => Action::IncreaseFontSize, + "DecreaseFontSize" => Action::DecreaseFontSize, + "ResetFontSize" => Action::ResetFontSize, + "ScrollPageUp" => Action::ScrollPageUp, + "ScrollPageDown" => Action::ScrollPageDown, + "ScrollLineUp" => Action::ScrollLineUp, + "ScrollLineDown" => Action::ScrollLineDown, + "ScrollToTop" => Action::ScrollToTop, + "ScrollToBottom" => Action::ScrollToBottom, + "ClearHistory" => Action::ClearHistory, + "Hide" => Action::Hide, + "Quit" => Action::Quit, + "ClearLogNotice" => Action::ClearLogNotice, + "SpawnNewInstance" => Action::SpawnNewInstance, + "ToggleFullscreen" => Action::ToggleFullscreen, + #[cfg(target_os = "macos")] + "ToggleSimpleFullscreen" => Action::ToggleSimpleFullscreen, + "None" => Action::None, + _ => return Err(E::invalid_value(Unexpected::Str(value), &self)), + })) + } + } + deserializer.deserialize_str(ActionVisitor) + } +} + +#[serde(untagged)] +#[derive(Debug, Deserialize, Clone, PartialEq, Eq)] +pub enum CommandWrapper { + Just(String), + WithArgs { + program: String, + #[serde(default)] + args: Vec<String>, + }, +} + +impl CommandWrapper { + pub fn program(&self) -> &str { + match self { + CommandWrapper::Just(program) => program, + CommandWrapper::WithArgs { program, .. } => program, + } + } + + pub fn args(&self) -> &[String] { + match self { + CommandWrapper::Just(_) => &[], + CommandWrapper::WithArgs { args, .. } => args, + } + } +} + +use crate::term::{mode, TermMode}; + +struct ModeWrapper { + pub mode: TermMode, + pub not_mode: TermMode, +} + +impl<'a> de::Deserialize<'a> for ModeWrapper { + fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error> + where + D: de::Deserializer<'a>, + { + struct ModeVisitor; + + impl<'a> Visitor<'a> for ModeVisitor { + type Value = ModeWrapper; + + fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("Combination of AppCursor | AppKeypad, possibly with negation (~)") + } + + fn visit_str<E>(self, value: &str) -> ::std::result::Result<ModeWrapper, E> + where + E: de::Error, + { + let mut res = ModeWrapper { mode: TermMode::empty(), not_mode: TermMode::empty() }; + + for modifier in value.split('|') { + match modifier.trim() { + "AppCursor" => res.mode |= mode::TermMode::APP_CURSOR, + "~AppCursor" => res.not_mode |= mode::TermMode::APP_CURSOR, + "AppKeypad" => res.mode |= mode::TermMode::APP_KEYPAD, + "~AppKeypad" => res.not_mode |= mode::TermMode::APP_KEYPAD, + "~Alt" => res.not_mode |= mode::TermMode::ALT_SCREEN, + "Alt" => res.mode |= mode::TermMode::ALT_SCREEN, + _ => error!("Unknown mode {:?}", modifier), + } + } + + Ok(res) + } + } + deserializer.deserialize_str(ModeVisitor) + } +} + +struct MouseButton(::glutin::MouseButton); + +impl MouseButton { + fn into_inner(self) -> ::glutin::MouseButton { + self.0 + } +} + +impl<'a> de::Deserialize<'a> for MouseButton { + fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error> + where + D: de::Deserializer<'a>, + { + struct MouseButtonVisitor; + + impl<'a> Visitor<'a> for MouseButtonVisitor { + type Value = MouseButton; + + fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("Left, Right, Middle, or a number") + } + + fn visit_str<E>(self, value: &str) -> ::std::result::Result<MouseButton, E> + where + E: de::Error, + { + match value { + "Left" => Ok(MouseButton(::glutin::MouseButton::Left)), + "Right" => Ok(MouseButton(::glutin::MouseButton::Right)), + "Middle" => Ok(MouseButton(::glutin::MouseButton::Middle)), + _ => { + if let Ok(index) = u8::from_str(value) { + Ok(MouseButton(::glutin::MouseButton::Other(index))) + } else { + Err(E::invalid_value(Unexpected::Str(value), &self)) + } + }, + } + } + } + + deserializer.deserialize_str(MouseButtonVisitor) + } +} + +/// Bindings are deserialized into a `RawBinding` before being parsed as a +/// `KeyBinding` or `MouseBinding`. +#[derive(PartialEq, Eq)] +struct RawBinding { + key: Option<Key>, + mouse: Option<::glutin::MouseButton>, + mods: ModifiersState, + mode: TermMode, + notmode: TermMode, + action: Action, +} + +impl RawBinding { + fn into_mouse_binding(self) -> ::std::result::Result<MouseBinding, Self> { + if let Some(mouse) = self.mouse { + Ok(Binding { + trigger: mouse, + mods: self.mods, + action: self.action, + mode: self.mode, + notmode: self.notmode, + }) + } else { + Err(self) + } + } + + fn into_key_binding(self) -> ::std::result::Result<KeyBinding, Self> { + if let Some(key) = self.key { + Ok(KeyBinding { + trigger: key, + mods: self.mods, + action: self.action, + mode: self.mode, + notmode: self.notmode, + }) + } else { + Err(self) + } + } +} + +impl<'a> de::Deserialize<'a> for RawBinding { + fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error> + where + D: de::Deserializer<'a>, + { + enum Field { + Key, + Mods, + Mode, + Action, + Chars, + Mouse, + Command, + } + + impl<'a> de::Deserialize<'a> for Field { + fn deserialize<D>(deserializer: D) -> ::std::result::Result<Field, D::Error> + where + D: de::Deserializer<'a>, + { + struct FieldVisitor; + + static FIELDS: &'static [&'static str] = + &["key", "mods", "mode", "action", "chars", "mouse", "command"]; + + impl<'a> Visitor<'a> for FieldVisitor { + type Value = Field; + + fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("binding fields") + } + + fn visit_str<E>(self, value: &str) -> ::std::result::Result<Field, E> + where + E: de::Error, + { + match value { + "key" => Ok(Field::Key), + "mods" => Ok(Field::Mods), + "mode" => Ok(Field::Mode), + "action" => Ok(Field::Action), + "chars" => Ok(Field::Chars), + "mouse" => Ok(Field::Mouse), + "command" => Ok(Field::Command), + _ => Err(E::unknown_field(value, FIELDS)), + } + } + } + + deserializer.deserialize_str(FieldVisitor) + } + } + + struct RawBindingVisitor; + impl<'a> Visitor<'a> for RawBindingVisitor { + type Value = RawBinding; + + fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("binding specification") + } + + fn visit_map<V>(self, mut map: V) -> ::std::result::Result<RawBinding, V::Error> + where + V: MapAccess<'a>, + { + let mut mods: Option<ModifiersState> = None; + let mut key: Option<Key> = None; + let mut chars: Option<String> = None; + let mut action: Option<crate::input::Action> = None; + let mut mode: Option<TermMode> = None; + let mut not_mode: Option<TermMode> = None; + let mut mouse: Option<::glutin::MouseButton> = None; + let mut command: Option<CommandWrapper> = None; + + use ::serde::de::Error; + + while let Some(struct_key) = map.next_key::<Field>()? { + match struct_key { + Field::Key => { + if key.is_some() { + return Err(<V::Error as Error>::duplicate_field("key")); + } + + let val = map.next_value::<serde_yaml::Value>()?; + if val.is_u64() { + let scancode = val.as_u64().unwrap(); + if scancode > u64::from(::std::u32::MAX) { + return Err(<V::Error as Error>::custom(format!( + "Invalid key binding, scancode too big: {}", + scancode + ))); + } + key = Some(Key::Scancode(scancode as u32)); + } else { + let k = Key::deserialize(val).map_err(V::Error::custom)?; + key = Some(k); + } + }, + Field::Mods => { + if mods.is_some() { + return Err(<V::Error as Error>::duplicate_field("mods")); + } + + mods = Some(map.next_value::<ModsWrapper>()?.into_inner()); + }, + Field::Mode => { + if mode.is_some() { + return Err(<V::Error as Error>::duplicate_field("mode")); + } + + let mode_deserializer = map.next_value::<ModeWrapper>()?; + mode = Some(mode_deserializer.mode); + not_mode = Some(mode_deserializer.not_mode); + }, + Field::Action => { + if action.is_some() { + return Err(<V::Error as Error>::duplicate_field("action")); + } + + action = Some(map.next_value::<ActionWrapper>()?.into_inner()); + }, + Field::Chars => { + if chars.is_some() { + return Err(<V::Error as Error>::duplicate_field("chars")); + } + + chars = Some(map.next_value()?); + }, + Field::Mouse => { + if chars.is_some() { + return Err(<V::Error as Error>::duplicate_field("mouse")); + } + + mouse = Some(map.next_value::<MouseButton>()?.into_inner()); + }, + Field::Command => { + if command.is_some() { + return Err(<V::Error as Error>::duplicate_field("command")); + } + + command = Some(map.next_value::<CommandWrapper>()?); + }, + } + } + + let action = match (action, chars, command) { + (Some(action), None, None) => action, + (None, Some(chars), None) => Action::Esc(chars), + (None, None, Some(cmd)) => match cmd { + CommandWrapper::Just(program) => Action::Command(program, vec![]), + CommandWrapper::WithArgs { program, args } => { + Action::Command(program, args) + }, + }, + (None, None, None) => { + return Err(V::Error::custom("must specify chars, action or command")); + }, + _ => { + return Err(V::Error::custom("must specify only chars, action or command")) + }, + }; + + let mode = mode.unwrap_or_else(TermMode::empty); + let not_mode = not_mode.unwrap_or_else(TermMode::empty); + let mods = mods.unwrap_or_else(ModifiersState::default); + + if mouse.is_none() && key.is_none() { + return Err(V::Error::custom("bindings require mouse button or key")); + } + + Ok(RawBinding { mode, notmode: not_mode, action, key, mouse, mods }) + } + } + + const FIELDS: &[&str] = &["key", "mods", "mode", "action", "chars", "mouse", "command"]; + + deserializer.deserialize_struct("RawBinding", FIELDS, RawBindingVisitor) + } +} + +impl<'a> de::Deserialize<'a> for Alpha { + fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error> + where + D: de::Deserializer<'a>, + { + let value = f32::deserialize(deserializer)?; + Ok(Alpha::new(value)) + } +} + +impl<'a> de::Deserialize<'a> for MouseBinding { + fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error> + where + D: de::Deserializer<'a>, + { + let raw = RawBinding::deserialize(deserializer)?; + raw.into_mouse_binding().map_err(|_| D::Error::custom("expected mouse binding")) + } +} + +impl<'a> de::Deserialize<'a> for KeyBinding { + fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error> + where + D: de::Deserializer<'a>, + { + let raw = RawBinding::deserialize(deserializer)?; + raw.into_key_binding().map_err(|_| D::Error::custom("expected key binding")) + } +} + +/// Errors occurring during config loading +#[derive(Debug)] +pub enum Error { + /// Config file not found + NotFound, + + /// Config file empty + Empty, + + /// Couldn't read $HOME environment variable + ReadingEnvHome(env::VarError), + + /// io error reading file + Io(io::Error), + + /// Not valid yaml or missing parameters + Yaml(serde_yaml::Error), +} + +#[serde(default)] +#[derive(Debug, Deserialize, PartialEq, Eq)] +pub struct Colors { + #[serde(deserialize_with = "failure_default")] + pub primary: PrimaryColors, + #[serde(deserialize_with = "failure_default")] + pub cursor: CursorColors, + #[serde(deserialize_with = "failure_default")] + pub selection: SelectionColors, + #[serde(deserialize_with = "deserialize_normal_colors")] + pub normal: AnsiColors, + #[serde(deserialize_with = "deserialize_bright_colors")] + pub bright: AnsiColors, + #[serde(deserialize_with = "failure_default")] + pub dim: Option<AnsiColors>, + #[serde(deserialize_with = "failure_default_vec")] + pub indexed_colors: Vec<IndexedColor>, +} + +impl Default for Colors { + fn default() -> Colors { + Colors { + primary: Default::default(), + cursor: Default::default(), + selection: Default::default(), + normal: default_normal_colors(), + bright: default_bright_colors(), + dim: Default::default(), + indexed_colors: Default::default(), + } + } +} + +fn default_normal_colors() -> AnsiColors { + AnsiColors { + black: Rgb { r: 0x00, g: 0x00, b: 0x00 }, + red: Rgb { r: 0xd5, g: 0x4e, b: 0x53 }, + green: Rgb { r: 0xb9, g: 0xca, b: 0x4a }, + yellow: Rgb { r: 0xe6, g: 0xc5, b: 0x47 }, + blue: Rgb { r: 0x7a, g: 0xa6, b: 0xda }, + magenta: Rgb { r: 0xc3, g: 0x97, b: 0xd8 }, + cyan: Rgb { r: 0x70, g: 0xc0, b: 0xba }, + white: Rgb { r: 0xea, g: 0xea, b: 0xea }, + } +} + +fn default_bright_colors() -> AnsiColors { + AnsiColors { + black: Rgb { r: 0x66, g: 0x66, b: 0x66 }, + red: Rgb { r: 0xff, g: 0x33, b: 0x34 }, + green: Rgb { r: 0x9e, g: 0xc4, b: 0x00 }, + yellow: Rgb { r: 0xe7, g: 0xc5, b: 0x47 }, + blue: Rgb { r: 0x7a, g: 0xa6, b: 0xda }, + magenta: Rgb { r: 0xb7, g: 0x7e, b: 0xe0 }, + cyan: Rgb { r: 0x54, g: 0xce, b: 0xd6 }, + white: Rgb { r: 0xff, g: 0xff, b: 0xff }, + } +} + +fn deserialize_normal_colors<'a, D>(deserializer: D) -> ::std::result::Result<AnsiColors, D::Error> +where + D: de::Deserializer<'a>, +{ + match AnsiColors::deserialize(deserializer) { + Ok(escape_chars) => Ok(escape_chars), + Err(err) => { + error!("Problem with config: {}; using default value", err); + Ok(default_normal_colors()) + }, + } +} + +fn deserialize_bright_colors<'a, D>(deserializer: D) -> ::std::result::Result<AnsiColors, D::Error> +where + D: de::Deserializer<'a>, +{ + match AnsiColors::deserialize(deserializer) { + Ok(escape_chars) => Ok(escape_chars), + Err(err) => { + error!("Problem with config: {}; using default value", err); + Ok(default_bright_colors()) + }, + } +} + +#[derive(Debug, Deserialize, PartialEq, Eq)] +pub struct IndexedColor { + #[serde(deserialize_with = "deserialize_color_index")] + pub index: u8, + #[serde(deserialize_with = "rgb_from_hex")] + pub color: Rgb, +} + +fn deserialize_color_index<'a, D>(deserializer: D) -> ::std::result::Result<u8, D::Error> +where + D: de::Deserializer<'a>, +{ + match u8::deserialize(deserializer) { + Ok(index) => { + if index < 16 { + error!( + "Problem with config: indexed_color's index is {}, but a value bigger than 15 \ + was expected; ignoring setting", + index + ); + + // Return value out of range to ignore this color + Ok(0) + } else { + Ok(index) + } + }, + Err(err) => { + error!("Problem with config: {}; ignoring setting", err); + + // Return value out of range to ignore this color + Ok(0) + }, + } +} + +#[serde(default)] +#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq)] +pub struct Cursor { + #[serde(deserialize_with = "failure_default")] + pub style: CursorStyle, + #[serde(deserialize_with = "deserialize_true_bool")] + pub unfocused_hollow: bool, +} + +impl Default for Cursor { + fn default() -> Self { + Self { style: Default::default(), unfocused_hollow: true } + } +} + +#[serde(default)] +#[derive(Debug, Copy, Clone, Default, Deserialize, PartialEq, Eq)] +pub struct CursorColors { + #[serde(deserialize_with = "deserialize_optional_color")] + pub text: Option<Rgb>, + #[serde(deserialize_with = "deserialize_optional_color")] + pub cursor: Option<Rgb>, +} + +#[serde(default)] +#[derive(Debug, Copy, Clone, Default, Deserialize, PartialEq, Eq)] +pub struct SelectionColors { + #[serde(deserialize_with = "deserialize_optional_color")] + pub text: Option<Rgb>, + #[serde(deserialize_with = "deserialize_optional_color")] + pub background: Option<Rgb>, +} + +#[serde(default)] +#[derive(Debug, Deserialize, PartialEq, Eq)] +pub struct PrimaryColors { + #[serde(deserialize_with = "rgb_from_hex")] + pub background: Rgb, + #[serde(deserialize_with = "rgb_from_hex")] + pub foreground: Rgb, + #[serde(deserialize_with = "deserialize_optional_color")] + pub bright_foreground: Option<Rgb>, + #[serde(deserialize_with = "deserialize_optional_color")] + pub dim_foreground: Option<Rgb>, +} + +impl Default for PrimaryColors { + fn default() -> Self { + PrimaryColors { + background: default_background(), + foreground: default_foreground(), + bright_foreground: Default::default(), + dim_foreground: Default::default(), + } + } +} + +fn deserialize_optional_color<'a, D>( + deserializer: D, +) -> ::std::result::Result<Option<Rgb>, D::Error> +where + D: de::Deserializer<'a>, +{ + match Option::deserialize(deserializer) { + Ok(Some(color)) => { + let color: serde_yaml::Value = color; + Ok(Some(rgb_from_hex(color).unwrap())) + }, + Ok(None) => Ok(None), + Err(err) => { + error!("Problem with config: {}; using standard foreground color", err); + Ok(None) + }, + } +} + +fn default_background() -> Rgb { + Rgb { r: 0, g: 0, b: 0 } +} + +fn default_foreground() -> Rgb { + Rgb { r: 0xea, g: 0xea, b: 0xea } +} + +/// The 8-colors sections of config +#[derive(Debug, Deserialize, PartialEq, Eq)] +pub struct AnsiColors { + #[serde(deserialize_with = "rgb_from_hex")] + pub black: Rgb, + #[serde(deserialize_with = "rgb_from_hex")] + pub red: Rgb, + #[serde(deserialize_with = "rgb_from_hex")] + pub green: Rgb, + #[serde(deserialize_with = "rgb_from_hex")] + pub yellow: Rgb, + #[serde(deserialize_with = "rgb_from_hex")] + pub blue: Rgb, + #[serde(deserialize_with = "rgb_from_hex")] + pub magenta: Rgb, + #[serde(deserialize_with = "rgb_from_hex")] + pub cyan: Rgb, + #[serde(deserialize_with = "rgb_from_hex")] + pub white: Rgb, +} + +/// Deserialize an Rgb from a hex string +/// +/// This is *not* the deserialize impl for Rgb since we want a symmetric +/// serialize/deserialize impl for ref tests. +fn rgb_from_hex<'a, D>(deserializer: D) -> ::std::result::Result<Rgb, D::Error> +where + D: de::Deserializer<'a>, +{ + struct RgbVisitor; + + impl<'a> Visitor<'a> for RgbVisitor { + type Value = Rgb; + + fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("hex color like 0xff00ff") + } + + fn visit_str<E>(self, value: &str) -> ::std::result::Result<Rgb, E> + where + E: ::serde::de::Error, + { + Rgb::from_str(&value[..]) + .map_err(|_| E::custom("failed to parse rgb; expected hex color like 0xff00ff")) + } + } + + let rgb = deserializer.deserialize_str(RgbVisitor); + + // Use #ff00ff as fallback color + match rgb { + Ok(rgb) => Ok(rgb), + Err(err) => { + error!("Problem with config: {}; using color #ff00ff", err); + Ok(Rgb { r: 255, g: 0, b: 255 }) + }, + } +} + +impl FromStr for Rgb { + type Err = (); + + fn from_str(s: &str) -> ::std::result::Result<Rgb, ()> { + let mut chars = s.chars(); + let mut rgb = Rgb::default(); + + macro_rules! component { + ($($c:ident),*) => { + $( + match chars.next().and_then(|c| c.to_digit(16)) { + Some(val) => rgb.$c = (val as u8) << 4, + None => return Err(()) + } + + match chars.next().and_then(|c| c.to_digit(16)) { + Some(val) => rgb.$c |= val as u8, + None => return Err(()) + } + )* + } + } + + match chars.next() { + Some('0') => { + if chars.next() != Some('x') { + return Err(()); + } + }, + Some('#') => (), + _ => return Err(()), + } + + component!(r, g, b); + + Ok(rgb) + } +} + +impl ::std::error::Error for Error { + fn cause(&self) -> Option<&dyn (::std::error::Error)> { + match *self { + Error::NotFound | Error::Empty => None, + Error::ReadingEnvHome(ref err) => Some(err), + Error::Io(ref err) => Some(err), + Error::Yaml(ref err) => Some(err), + } + } + + fn description(&self) -> &str { + match *self { + Error::NotFound => "Couldn't locate config file", + Error::Empty => "Empty config file", + Error::ReadingEnvHome(ref err) => err.description(), + Error::Io(ref err) => err.description(), + Error::Yaml(ref err) => err.description(), + } + } +} + +impl ::std::fmt::Display for Error { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + match *self { + Error::NotFound | Error::Empty => { + write!(f, "{}", ::std::error::Error::description(self)) + }, + Error::ReadingEnvHome(ref err) => { + write!(f, "Couldn't read $HOME environment variable: {}", err) + }, + Error::Io(ref err) => write!(f, "Error reading config file: {}", err), + Error::Yaml(ref err) => write!(f, "Problem with config: {}", err), + } + } +} + +impl From<env::VarError> for Error { + fn from(val: env::VarError) -> Error { + Error::ReadingEnvHome(val) + } +} + +impl From<io::Error> for Error { + fn from(val: io::Error) -> Error { + if val.kind() == io::ErrorKind::NotFound { + Error::NotFound + } else { + Error::Io(val) + } + } +} + +impl From<serde_yaml::Error> for Error { + fn from(val: serde_yaml::Error) -> Error { + Error::Yaml(val) + } +} + +/// Result from config loading +pub type Result<T> = ::std::result::Result<T, Error>; + +impl Config { + /// Get the location of the first found default config file paths + /// according to the following order: + /// + /// 1. $XDG_CONFIG_HOME/alacritty/alacritty.yml + /// 2. $XDG_CONFIG_HOME/alacritty.yml + /// 3. $HOME/.config/alacritty/alacritty.yml + /// 4. $HOME/.alacritty.yml + #[cfg(not(windows))] + pub fn installed_config<'a>() -> Option<Cow<'a, Path>> { + // Try using XDG location by default + ::xdg::BaseDirectories::with_prefix("alacritty") + .ok() + .and_then(|xdg| xdg.find_config_file("alacritty.yml")) + .or_else(|| { + ::xdg::BaseDirectories::new() + .ok() + .and_then(|fallback| fallback.find_config_file("alacritty.yml")) + }) + .or_else(|| { + if let Ok(home) = env::var("HOME") { + // Fallback path: $HOME/.config/alacritty/alacritty.yml + let fallback = PathBuf::from(&home).join(".config/alacritty/alacritty.yml"); + if fallback.exists() { + return Some(fallback); + } + // Fallback path: $HOME/.alacritty.yml + let fallback = PathBuf::from(&home).join(".alacritty.yml"); + if fallback.exists() { + return Some(fallback); + } + } + None + }) + .map(Into::into) + } + + // TODO: Remove old configuration location warning (Deprecated 03/12/2018) + #[cfg(windows)] + pub fn installed_config<'a>() -> Option<Cow<'a, Path>> { + let old = dirs::home_dir().map(|path| path.join("alacritty.yml")); + let new = dirs::config_dir().map(|path| path.join("alacritty\\alacritty.yml")); + + if let Some(old_path) = old.as_ref().filter(|old| old.exists()) { + warn!( + "Found configuration at: {}; this file should be moved to the new location: {}", + old_path.to_string_lossy(), + new.as_ref().map(|new| new.to_string_lossy()).unwrap(), + ); + + old.map(Cow::from) + } else { + new.filter(|new| new.exists()).map(Cow::from) + } + } + + #[cfg(not(windows))] + pub fn write_defaults() -> io::Result<Cow<'static, Path>> { + let path = xdg::BaseDirectories::with_prefix("alacritty") + .map_err(|err| io::Error::new(io::ErrorKind::NotFound, err.to_string().as_str())) + .and_then(|p| p.place_config_file("alacritty.yml"))?; + + File::create(&path)?.write_all(DEFAULT_ALACRITTY_CONFIG.as_bytes())?; + + Ok(path.into()) + } + + #[cfg(windows)] + pub fn write_defaults() -> io::Result<Cow<'static, Path>> { + let mut path = dirs::config_dir().ok_or_else(|| { + io::Error::new(io::ErrorKind::NotFound, "Couldn't find profile directory") + })?; + + path = path.join("alacritty/alacritty.yml"); + + std::fs::create_dir_all(path.parent().unwrap())?; + + File::create(&path)?.write_all(DEFAULT_ALACRITTY_CONFIG.as_bytes())?; + + Ok(path.into()) + } + + /// Get list of colors + /// + /// The ordering returned here is expected by the terminal. Colors are simply indexed in this + /// array for performance. + pub fn colors(&self) -> &Colors { + &self.colors + } + + #[inline] + pub fn background_opacity(&self) -> Alpha { + self.background_opacity + } + + pub fn key_bindings(&self) -> &[KeyBinding] { + &self.key_bindings[..] + } + + pub fn mouse_bindings(&self) -> &[MouseBinding] { + &self.mouse_bindings[..] + } + + pub fn mouse(&self) -> &Mouse { + &self.mouse + } + + pub fn selection(&self) -> &Selection { + &self.selection + } + + pub fn tabspaces(&self) -> usize { + self.tabspaces + } + + pub fn padding(&self) -> &Delta<u8> { + self.padding.as_ref().unwrap_or(&self.window.padding) + } + + #[inline] + pub fn draw_bold_text_with_bright_colors(&self) -> bool { + self.draw_bold_text_with_bright_colors + } + + /// Get font config + #[inline] + pub fn font(&self) -> &Font { + &self.font + } + + /// Get window dimensions + #[inline] + pub fn dimensions(&self) -> Dimensions { + self.dimensions.unwrap_or(self.window.dimensions) + } + + /// Get window config + #[inline] + pub fn window(&self) -> &WindowConfig { + &self.window + } + + /// Get visual bell config + #[inline] + pub fn visual_bell(&self) -> &VisualBellConfig { + &self.visual_bell + } + + /// Should show render timer + #[inline] + pub fn render_timer(&self) -> bool { + self.render_timer + } + + #[cfg(target_os = "macos")] + #[inline] + pub fn use_thin_strokes(&self) -> bool { + self.font.use_thin_strokes + } + + #[cfg(not(target_os = "macos"))] + #[inline] + pub fn use_thin_strokes(&self) -> bool { + false + } + + pub fn path(&self) -> Option<&Path> { + self.config_path.as_ref().map(PathBuf::as_path) + } + + pub fn shell(&self) -> Option<&Shell<'_>> { + self.shell.as_ref() + } + + pub fn env(&self) -> &HashMap<String, String> { + &self.env + } + + /// Should hide mouse cursor when typing + #[inline] + pub fn hide_mouse_when_typing(&self) -> bool { + self.hide_cursor_when_typing.unwrap_or(self.mouse.hide_when_typing) + } + + /// Style of the cursor + #[inline] + pub fn cursor_style(&self) -> CursorStyle { + self.cursor_style.unwrap_or(self.cursor.style) + } + + /// Use hollow block cursor when unfocused + #[inline] + pub fn unfocused_hollow_cursor(&self) -> bool { + self.unfocused_hollow_cursor.unwrap_or(self.cursor.unfocused_hollow) + } + + /// Live config reload + #[inline] + pub fn live_config_reload(&self) -> bool { + self.live_config_reload + } + + #[inline] + pub fn dynamic_title(&self) -> bool { + self.dynamic_title + } + + /// Scrolling settings + #[inline] + pub fn scrolling(&self) -> Scrolling { + self.scrolling + } + + /// Cursor foreground color + #[inline] + pub fn cursor_text_color(&self) -> Option<Rgb> { + self.colors.cursor.text + } + + /// Cursor background color + #[inline] + pub fn cursor_cursor_color(&self) -> Option<Rgb> { + self.colors.cursor.cursor + } + + /// Enable experimental conpty backend (Windows only) + #[cfg(windows)] + #[inline] + pub fn enable_experimental_conpty_backend(&self) -> bool { + self.enable_experimental_conpty_backend + } + + /// Send escape sequences using the alt key + #[inline] + pub fn alt_send_esc(&self) -> bool { + self.alt_send_esc + } + + // Update the history size, used in ref tests + pub fn set_history(&mut self, history: u32) { + self.scrolling.history = history; + } + + /// Keep the log file after quitting Alacritty + #[inline] + pub fn persistent_logging(&self) -> bool { + self.persistent_logging + } + + /// Overrides the `dynamic_title` configuration based on `--title`. + pub fn update_dynamic_title(mut self, options: &Options) -> Self { + if options.title.is_some() { + self.dynamic_title = false; + } + self + } + + pub fn load_from(path: PathBuf) -> Config { + let mut config = Config::reload_from(&path).unwrap_or_else(|_| Config::default()); + config.config_path = Some(path); + config + } + + pub fn reload_from(path: &PathBuf) -> Result<Config> { + match Config::read_config(path) { + Ok(config) => Ok(config), + Err(err) => { + error!("Unable to load config {:?}: {}", path, err); + Err(err) + }, + } + } + + fn read_config(path: &PathBuf) -> Result<Config> { + let mut contents = String::new(); + File::open(path)?.read_to_string(&mut contents)?; + + // Prevent parsing error with empty string + if contents.is_empty() { + return Ok(Config::default()); + } + + let mut config: Config = serde_yaml::from_str(&contents)?; + config.print_deprecation_warnings(); + + Ok(config) + } + + fn print_deprecation_warnings(&mut self) { + if self.dimensions.is_some() { + warn!("Config dimensions is deprecated; please use window.dimensions instead"); + } + + if self.padding.is_some() { + warn!("Config padding is deprecated; please use window.padding instead"); + } + + if self.mouse.faux_scrollback_lines.is_some() { + warn!( + "Config mouse.faux_scrollback_lines is deprecated; please use \ + mouse.faux_scrolling_lines instead" + ); + } + + if let Some(custom_cursor_colors) = self.custom_cursor_colors { + warn!("Config custom_cursor_colors is deprecated"); + + if !custom_cursor_colors { + self.colors.cursor.cursor = None; + self.colors.cursor.text = None; + } + } + + if self.cursor_style.is_some() { + warn!("Config cursor_style is deprecated; please use cursor.style instead"); + } + + if self.hide_cursor_when_typing.is_some() { + warn!( + "Config hide_cursor_when_typing is deprecated; please use mouse.hide_when_typing \ + instead" + ); + } + + if self.unfocused_hollow_cursor.is_some() { + warn!( + "Config unfocused_hollow_cursor is deprecated; please use cursor.unfocused_hollow \ + instead" + ); + } + + if let Some(start_maximized) = self.window.start_maximized { + warn!( + "Config window.start_maximized is deprecated; please use window.startup_mode \ + instead" + ); + + // While `start_maximized` is deprecated its setting takes precedence. + if start_maximized { + self.window.startup_mode = StartupMode::Maximized; + } + } + } +} + +/// Window Dimensions +/// +/// Newtype to avoid passing values incorrectly +#[serde(default)] +#[derive(Default, Debug, Copy, Clone, Deserialize, PartialEq, Eq)] +pub struct Dimensions { + /// Window width in character columns + #[serde(deserialize_with = "failure_default")] + columns: Column, + + /// Window Height in character lines + #[serde(deserialize_with = "failure_default")] + lines: Line, +} + +impl Dimensions { + pub fn new(columns: Column, lines: Line) -> Self { + Dimensions { columns, lines } + } + + /// Get lines + #[inline] + pub fn lines_u32(&self) -> u32 { + self.lines.0 as u32 + } + + /// Get columns + #[inline] + pub fn columns_u32(&self) -> u32 { + self.columns.0 as u32 + } +} + +/// A delta for a point in a 2 dimensional plane +#[serde(default, bound(deserialize = "T: Deserialize<'de> + Default"))] +#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)] +pub struct Delta<T: Default + PartialEq + Eq> { + /// Horizontal change + #[serde(deserialize_with = "failure_default")] + pub x: T, + /// Vertical change + #[serde(deserialize_with = "failure_default")] + pub y: T, +} + +trait DeserializeSize: Sized { + fn deserialize<'a, D>(_: D) -> ::std::result::Result<Self, D::Error> + where + D: serde::de::Deserializer<'a>; +} + +impl DeserializeSize for Size { + fn deserialize<'a, D>(deserializer: D) -> ::std::result::Result<Self, D::Error> + where + D: serde::de::Deserializer<'a>, + { + use std::marker::PhantomData; + + struct NumVisitor<__D> { + _marker: PhantomData<__D>, + } + + impl<'a, __D> Visitor<'a> for NumVisitor<__D> + where + __D: serde::de::Deserializer<'a>, + { + type Value = f64; + + fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("f64 or u64") + } + + fn visit_f64<E>(self, value: f64) -> ::std::result::Result<Self::Value, E> + where + E: ::serde::de::Error, + { + Ok(value) + } + + fn visit_u64<E>(self, value: u64) -> ::std::result::Result<Self::Value, E> + where + E: ::serde::de::Error, + { + Ok(value as f64) + } + } + + let size = deserializer + .deserialize_any(NumVisitor::<D> { _marker: PhantomData }) + .map(|v| Size::new(v as _)); + + // Use default font size as fallback + match size { + Ok(size) => Ok(size), + Err(err) => { + let size = default_font_size(); + error!("Problem with config: {}; using size {}", err, size.as_f32_pts()); + Ok(size) + }, + } + } +} + +/// Font config +/// +/// Defaults are provided at the level of this struct per platform, but not per +/// field in this struct. It might be nice in the future to have defaults for +/// each value independently. Alternatively, maybe erroring when the user +/// doesn't provide complete config is Ok. +#[serde(default)] +#[derive(Debug, Deserialize, Clone, PartialEq, Eq)] +pub struct Font { + /// Normal font face + #[serde(deserialize_with = "failure_default")] + normal: FontDescription, + + /// Bold font face + #[serde(deserialize_with = "failure_default")] + italic: SecondaryFontDescription, + + /// Italic font face + #[serde(deserialize_with = "failure_default")] + bold: SecondaryFontDescription, + + /// Font size in points + #[serde(deserialize_with = "DeserializeSize::deserialize")] + pub size: Size, + + /// Extra spacing per character + #[serde(deserialize_with = "failure_default")] + offset: Delta<i8>, + + /// Glyph offset within character cell + #[serde(deserialize_with = "failure_default")] + glyph_offset: Delta<i8>, + + #[cfg(target_os = "macos")] + #[serde(deserialize_with = "deserialize_true_bool")] + use_thin_strokes: bool, + + // TODO: Deprecated + #[serde(deserialize_with = "deserialize_scale_with_dpi")] + scale_with_dpi: Option<()>, +} + +impl Default for Font { + fn default() -> Font { + Font { + #[cfg(target_os = "macos")] + use_thin_strokes: true, + size: default_font_size(), + normal: Default::default(), + bold: Default::default(), + italic: Default::default(), + scale_with_dpi: Default::default(), + glyph_offset: Default::default(), + offset: Default::default(), + } + } +} + +impl Font { + /// Get the font size in points + #[inline] + pub fn size(&self) -> Size { + self.size + } + + /// Get offsets to font metrics + #[inline] + pub fn offset(&self) -> &Delta<i8> { + &self.offset + } + + /// Get cell offsets for glyphs + #[inline] + pub fn glyph_offset(&self) -> &Delta<i8> { + &self.glyph_offset + } + + /// Get a font clone with a size modification + pub fn with_size(self, size: Size) -> Font { + Font { size, ..self } + } + + // Get normal font description + pub fn normal(&self) -> &FontDescription { + &self.normal + } + + // Get italic font description + pub fn italic(&self) -> FontDescription { + self.italic.desc(&self.normal) + } + + // Get bold font description + pub fn bold(&self) -> FontDescription { + self.bold.desc(&self.normal) + } +} + +fn default_font_size() -> Size { + Size::new(11.) +} + +fn deserialize_scale_with_dpi<'a, D>(deserializer: D) -> ::std::result::Result<Option<()>, D::Error> +where + D: de::Deserializer<'a>, +{ + // This is necessary in order to get serde to complete deserialization of the configuration + let _ignored = bool::deserialize(deserializer); + error!( + "The scale_with_dpi setting has been removed, on X11 the WINIT_HIDPI_FACTOR environment \ + variable can be used instead." + ); + Ok(None) +} + +/// Description of the normal font +#[serde(default)] +#[derive(Debug, Deserialize, Clone, PartialEq, Eq)] +pub struct FontDescription { + #[serde(deserialize_with = "failure_default")] + pub family: String, + #[serde(deserialize_with = "failure_default")] + pub style: Option<String>, +} + +impl Default for FontDescription { + fn default() -> FontDescription { + FontDescription { + #[cfg(not(any(target_os = "macos", windows)))] + family: "monospace".into(), + #[cfg(target_os = "macos")] + family: "Menlo".into(), + #[cfg(windows)] + family: "Consolas".into(), + style: None, + } + } +} + +/// Description of the italic and bold font +#[serde(default)] +#[derive(Debug, Default, Deserialize, Clone, PartialEq, Eq)] +pub struct SecondaryFontDescription { + #[serde(deserialize_with = "failure_default")] + family: Option<String>, + #[serde(deserialize_with = "failure_default")] + style: Option<String>, +} + +impl SecondaryFontDescription { + pub fn desc(&self, fallback: &FontDescription) -> FontDescription { + FontDescription { + family: self.family.clone().unwrap_or_else(|| fallback.family.clone()), + style: self.style.clone(), + } + } +} + +pub struct Monitor { + _thread: ::std::thread::JoinHandle<()>, + rx: mpsc::Receiver<PathBuf>, +} + +pub trait OnConfigReload { + fn on_config_reload(&mut self); +} + +impl OnConfigReload for crate::display::Notifier { + fn on_config_reload(&mut self) { + self.notify(); + } +} + +impl Monitor { + /// Get pending config changes + pub fn pending(&self) -> Option<PathBuf> { + let mut config = None; + while let Ok(new) = self.rx.try_recv() { + config = Some(new); + } + + config + } + + pub fn new<H, P>(path: P, mut handler: H) -> Monitor + where + H: OnConfigReload + Send + 'static, + P: Into<PathBuf>, + { + let path = path.into(); + + let (config_tx, config_rx) = mpsc::channel(); + + Monitor { + _thread: crate::util::thread::spawn_named("config watcher", move || { + let (tx, rx) = mpsc::channel(); + // The Duration argument is a debouncing period. + let mut watcher = + watcher(tx, Duration::from_millis(10)).expect("Unable to spawn file watcher"); + let config_path = ::std::fs::canonicalize(path).expect("canonicalize config path"); + + // Get directory of config + let mut parent = config_path.clone(); + parent.pop(); + + // Watch directory + watcher + .watch(&parent, RecursiveMode::NonRecursive) + .expect("watch alacritty.yml dir"); + + loop { + match rx.recv().expect("watcher event") { + DebouncedEvent::Rename(..) => continue, + DebouncedEvent::Write(path) + | DebouncedEvent::Create(path) + | DebouncedEvent::Chmod(path) => { + if path != config_path { + continue; + } + + let _ = config_tx.send(path); + handler.on_config_reload(); + }, + _ => {}, + } + } + }), + rx: config_rx, + } + } +} + +#[derive(Deserialize, Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub enum Key { + Scancode(u32), + Key1, + Key2, + Key3, + Key4, + Key5, + Key6, + Key7, + Key8, + Key9, + Key0, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N, + O, + P, + Q, + R, + S, + T, + U, + V, + W, + X, + Y, + Z, + Escape, + F1, + F2, + F3, + F4, + F5, + F6, + F7, + F8, + F9, + F10, + F11, + F12, + F13, + F14, + F15, + F16, + F17, + F18, + F19, + F20, + F21, + F22, + F23, + F24, + Snapshot, + Scroll, + Pause, + Insert, + Home, + Delete, + End, + PageDown, + PageUp, + Left, + Up, + Right, + Down, + Back, + Return, + Space, + Compose, + Numlock, + Numpad0, + Numpad1, + Numpad2, + Numpad3, + Numpad4, + Numpad5, + Numpad6, + Numpad7, + Numpad8, + Numpad9, + AbntC1, + AbntC2, + Add, + Apostrophe, + Apps, + At, + Ax, + Backslash, + Calculator, + Capital, + Colon, + Comma, + Convert, + Decimal, + Divide, + Equals, + Grave, + Kana, + Kanji, + LAlt, + LBracket, + LControl, + LShift, + LWin, + Mail, + MediaSelect, + MediaStop, + Minus, + Multiply, + Mute, + MyComputer, + NavigateForward, + NavigateBackward, + NextTrack, + NoConvert, + NumpadComma, + NumpadEnter, + NumpadEquals, + OEM102, + Period, + PlayPause, + Power, + PrevTrack, + RAlt, + RBracket, + RControl, + RShift, + RWin, + Semicolon, + Slash, + Sleep, + Stop, + Subtract, + Sysrq, + Tab, + Underline, + Unlabeled, + VolumeDown, + VolumeUp, + Wake, + WebBack, + WebFavorites, + WebForward, + WebHome, + WebRefresh, + WebSearch, + WebStop, + Yen, + Caret, + Copy, + Paste, + Cut, +} + +impl Key { + pub fn from_glutin_input(key: ::glutin::VirtualKeyCode) -> Self { + use glutin::VirtualKeyCode::*; + // Thank you, vim macros and regex! + match key { + Key1 => Key::Key1, + Key2 => Key::Key2, + Key3 => Key::Key3, + Key4 => Key::Key4, + Key5 => Key::Key5, + Key6 => Key::Key6, + Key7 => Key::Key7, + Key8 => Key::Key8, + Key9 => Key::Key9, + Key0 => Key::Key0, + A => Key::A, + B => Key::B, + C => Key::C, + D => Key::D, + E => Key::E, + F => Key::F, + G => Key::G, + H => Key::H, + I => Key::I, + J => Key::J, + K => Key::K, + L => Key::L, + M => Key::M, + N => Key::N, + O => Key::O, + P => Key::P, + Q => Key::Q, + R => Key::R, + S => Key::S, + T => Key::T, + U => Key::U, + V => Key::V, + W => Key::W, + X => Key::X, + Y => Key::Y, + Z => Key::Z, + Escape => Key::Escape, + F1 => Key::F1, + F2 => Key::F2, + F3 => Key::F3, + F4 => Key::F4, + F5 => Key::F5, + F6 => Key::F6, + F7 => Key::F7, + F8 => Key::F8, + F9 => Key::F9, + F10 => Key::F10, + F11 => Key::F11, + F12 => Key::F12, + F13 => Key::F13, + F14 => Key::F14, + F15 => Key::F15, + F16 => Key::F16, + F17 => Key::F17, + F18 => Key::F18, + F19 => Key::F19, + F20 => Key::F20, + F21 => Key::F21, + F22 => Key::F22, + F23 => Key::F23, + F24 => Key::F24, + Snapshot => Key::Snapshot, + Scroll => Key::Scroll, + Pause => Key::Pause, + Insert => Key::Insert, + Home => Key::Home, + Delete => Key::Delete, + End => Key::End, + PageDown => Key::PageDown, + PageUp => Key::PageUp, + Left => Key::Left, + Up => Key::Up, + Right => Key::Right, + Down => Key::Down, + Back => Key::Back, + Return => Key::Return, + Space => Key::Space, + Compose => Key::Compose, + Numlock => Key::Numlock, + Numpad0 => Key::Numpad0, + Numpad1 => Key::Numpad1, + Numpad2 => Key::Numpad2, + Numpad3 => Key::Numpad3, + Numpad4 => Key::Numpad4, + Numpad5 => Key::Numpad5, + Numpad6 => Key::Numpad6, + Numpad7 => Key::Numpad7, + Numpad8 => Key::Numpad8, + Numpad9 => Key::Numpad9, + AbntC1 => Key::AbntC1, + AbntC2 => Key::AbntC2, + Add => Key::Add, + Apostrophe => Key::Apostrophe, + Apps => Key::Apps, + At => Key::At, + Ax => Key::Ax, + Backslash => Key::Backslash, + Calculator => Key::Calculator, + Capital => Key::Capital, + Colon => Key::Colon, + Comma => Key::Comma, + Convert => Key::Convert, + Decimal => Key::Decimal, + Divide => Key::Divide, + Equals => Key::Equals, + Grave => Key::Grave, + Kana => Key::Kana, + Kanji => Key::Kanji, + LAlt => Key::LAlt, + LBracket => Key::LBracket, + LControl => Key::LControl, + LShift => Key::LShift, + LWin => Key::LWin, + Mail => Key::Mail, + MediaSelect => Key::MediaSelect, + MediaStop => Key::MediaStop, + Minus => Key::Minus, + Multiply => Key::Multiply, + Mute => Key::Mute, + MyComputer => Key::MyComputer, + NavigateForward => Key::NavigateForward, + NavigateBackward => Key::NavigateBackward, + NextTrack => Key::NextTrack, + NoConvert => Key::NoConvert, + NumpadComma => Key::NumpadComma, + NumpadEnter => Key::NumpadEnter, + NumpadEquals => Key::NumpadEquals, + OEM102 => Key::OEM102, + Period => Key::Period, + PlayPause => Key::PlayPause, + Power => Key::Power, + PrevTrack => Key::PrevTrack, + RAlt => Key::RAlt, + RBracket => Key::RBracket, + RControl => Key::RControl, + RShift => Key::RShift, + RWin => Key::RWin, + Semicolon => Key::Semicolon, + Slash => Key::Slash, + Sleep => Key::Sleep, + Stop => Key::Stop, + Subtract => Key::Subtract, + Sysrq => Key::Sysrq, + Tab => Key::Tab, + Underline => Key::Underline, + Unlabeled => Key::Unlabeled, + VolumeDown => Key::VolumeDown, + VolumeUp => Key::VolumeUp, + Wake => Key::Wake, + WebBack => Key::WebBack, + WebFavorites => Key::WebFavorites, + WebForward => Key::WebForward, + WebHome => Key::WebHome, + WebRefresh => Key::WebRefresh, + WebSearch => Key::WebSearch, + WebStop => Key::WebStop, + Yen => Key::Yen, + Caret => Key::Caret, + Copy => Key::Copy, + Paste => Key::Paste, + Cut => Key::Cut, + } + } +} + +#[cfg(test)] +mod tests { + use super::{Config, DEFAULT_ALACRITTY_CONFIG}; + use crate::cli::Options; + + #[test] + fn parse_config() { + let config: Config = + ::serde_yaml::from_str(DEFAULT_ALACRITTY_CONFIG).expect("deserialize config"); + + // Sanity check that mouse bindings are being parsed + assert!(!config.mouse_bindings.is_empty()); + + // Sanity check that key bindings are being parsed + assert!(!config.key_bindings.is_empty()); + } + + #[test] + fn dynamic_title_ignoring_options_by_default() { + let config: Config = + ::serde_yaml::from_str(DEFAULT_ALACRITTY_CONFIG).expect("deserialize config"); + let old_dynamic_title = config.dynamic_title; + let options = Options::default(); + let config = config.update_dynamic_title(&options); + assert_eq!(old_dynamic_title, config.dynamic_title); + } + + #[test] + fn dynamic_title_overridden_by_options() { + let config: Config = + ::serde_yaml::from_str(DEFAULT_ALACRITTY_CONFIG).expect("deserialize config"); + let mut options = Options::default(); + options.title = Some("foo".to_owned()); + let config = config.update_dynamic_title(&options); + assert!(!config.dynamic_title); + } + + #[test] + fn default_match_empty() { + let default = Config::default(); + + let empty = serde_yaml::from_str("key: val\n").unwrap(); + + assert_eq!(default, empty); + } +} diff --git a/alacritty_terminal/src/cursor.rs b/alacritty_terminal/src/cursor.rs new file mode 100644 index 00000000..196241a0 --- /dev/null +++ b/alacritty_terminal/src/cursor.rs @@ -0,0 +1,99 @@ +// 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. + +//! Helpers for creating different cursor glyphs from font metrics + +use std::cmp; + +use font::{Metrics, RasterizedGlyph}; + +use crate::ansi::CursorStyle; + +/// Width/Height of the cursor relative to the font width +pub const CURSOR_WIDTH_PERCENTAGE: i32 = 15; + +pub fn get_cursor_glyph( + cursor: CursorStyle, + metrics: Metrics, + offset_x: i8, + offset_y: i8, + is_wide: bool, +) -> RasterizedGlyph { + // Calculate the cell metrics + let height = metrics.line_height as i32 + i32::from(offset_y); + let mut width = metrics.average_advance as i32 + i32::from(offset_x); + let line_width = cmp::max(width * CURSOR_WIDTH_PERCENTAGE / 100, 1); + + // Double the cursor width if it's above a double-width glyph + if is_wide { + width *= 2; + } + + match cursor { + CursorStyle::HollowBlock => get_box_cursor_glyph(height, width, line_width), + CursorStyle::Underline => get_underline_cursor_glyph(width, line_width), + CursorStyle::Beam => get_beam_cursor_glyph(height, line_width), + CursorStyle::Block => get_block_cursor_glyph(height, width), + CursorStyle::Hidden => RasterizedGlyph::default(), + } +} + +// Returns a custom underline cursor character +pub fn get_underline_cursor_glyph(width: i32, line_width: i32) -> RasterizedGlyph { + // Create a new rectangle, the height is relative to the font width + let buf = vec![255u8; (width * line_width * 3) as usize]; + + // Create a custom glyph with the rectangle data attached to it + RasterizedGlyph { c: ' ', top: line_width, left: 0, height: line_width, width, buf } +} + +// Returns a custom beam cursor character +pub fn get_beam_cursor_glyph(height: i32, line_width: i32) -> RasterizedGlyph { + // Create a new rectangle that is at least one pixel wide + let buf = vec![255u8; (line_width * height * 3) as usize]; + + // Create a custom glyph with the rectangle data attached to it + RasterizedGlyph { c: ' ', top: height, left: 0, height, width: line_width, buf } +} + +// Returns a custom box cursor character +pub fn get_box_cursor_glyph(height: i32, width: i32, line_width: i32) -> RasterizedGlyph { + // Create a new box outline rectangle + let mut buf = Vec::with_capacity((width * height * 3) as usize); + for y in 0..height { + for x in 0..width { + if y < line_width + || y >= height - line_width + || x < line_width + || x >= width - line_width + { + buf.append(&mut vec![255u8; 3]); + } else { + buf.append(&mut vec![0u8; 3]); + } + } + } + + // Create a custom glyph with the rectangle data attached to it + RasterizedGlyph { c: ' ', top: height, left: 0, height, width, buf } +} + +// Returns a custom block cursor character +pub fn get_block_cursor_glyph(height: i32, width: i32) -> RasterizedGlyph { + // Create a completely filled glyph + let buf = vec![255u8; (width * height * 3) as usize]; + + // Create a custom glyph with the rectangle data attached to it + RasterizedGlyph { c: ' ', top: height, left: 0, height, width, buf } +} diff --git a/alacritty_terminal/src/display.rs b/alacritty_terminal/src/display.rs new file mode 100644 index 00000000..1d5799f6 --- /dev/null +++ b/alacritty_terminal/src/display.rs @@ -0,0 +1,560 @@ +// 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. + +//! The display subsystem including window management, font rasterization, and +//! GPU drawing. +use std::f64; +use std::sync::mpsc; + +use glutin::dpi::{PhysicalPosition, PhysicalSize}; +use glutin::EventsLoop; +use parking_lot::MutexGuard; + +use crate::cli; +use crate::config::{Config, StartupMode}; +use crate::index::Line; +use crate::message_bar::Message; +use crate::meter::Meter; +use crate::renderer::rects::{Rect, Rects}; +use crate::renderer::{self, GlyphCache, QuadRenderer}; +use crate::sync::FairMutex; +use crate::term::color::Rgb; +use crate::term::{RenderableCell, SizeInfo, Term}; +use crate::window::{self, Window}; +use font::{self, Rasterize}; + +#[derive(Debug)] +pub enum Error { + /// Error with window management + Window(window::Error), + + /// Error dealing with fonts + Font(font::Error), + + /// Error in renderer + Render(renderer::Error), +} + +impl ::std::error::Error for Error { + fn cause(&self) -> Option<&dyn (::std::error::Error)> { + match *self { + Error::Window(ref err) => Some(err), + Error::Font(ref err) => Some(err), + Error::Render(ref err) => Some(err), + } + } + + fn description(&self) -> &str { + match *self { + Error::Window(ref err) => err.description(), + Error::Font(ref err) => err.description(), + Error::Render(ref err) => err.description(), + } + } +} + +impl ::std::fmt::Display for Error { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + match *self { + Error::Window(ref err) => err.fmt(f), + Error::Font(ref err) => err.fmt(f), + Error::Render(ref err) => err.fmt(f), + } + } +} + +impl From<window::Error> for Error { + fn from(val: window::Error) -> Error { + Error::Window(val) + } +} + +impl From<font::Error> for Error { + fn from(val: font::Error) -> Error { + Error::Font(val) + } +} + +impl From<renderer::Error> for Error { + fn from(val: renderer::Error) -> Error { + Error::Render(val) + } +} + +/// The display wraps a window, font rasterizer, and GPU renderer +pub struct Display { + window: Window, + renderer: QuadRenderer, + glyph_cache: GlyphCache, + render_timer: bool, + rx: mpsc::Receiver<PhysicalSize>, + tx: mpsc::Sender<PhysicalSize>, + meter: Meter, + font_size: font::Size, + size_info: SizeInfo, + last_message: Option<Message>, +} + +/// Can wakeup the render loop from other threads +pub struct Notifier(window::Proxy); + +/// Types that are interested in when the display is resized +pub trait OnResize { + fn on_resize(&mut self, size: &SizeInfo); +} + +impl Notifier { + pub fn notify(&self) { + self.0.wakeup_event_loop(); + } +} + +impl Display { + pub fn notifier(&self) -> Notifier { + Notifier(self.window.create_window_proxy()) + } + + pub fn update_config(&mut self, config: &Config) { + self.render_timer = config.render_timer(); + } + + /// Get size info about the display + pub fn size(&self) -> &SizeInfo { + &self.size_info + } + + pub fn new(config: &Config, options: &cli::Options) -> Result<Display, Error> { + // Extract some properties from config + let render_timer = config.render_timer(); + + // Guess DPR based on first monitor + let event_loop = EventsLoop::new(); + let estimated_dpr = + event_loop.get_available_monitors().next().map(|m| m.get_hidpi_factor()).unwrap_or(1.); + + // Guess the target window dimensions + let metrics = GlyphCache::static_metrics(config, estimated_dpr as f32)?; + let (cell_width, cell_height) = Self::compute_cell_size(config, &metrics); + let dimensions = + Self::calculate_dimensions(config, options, estimated_dpr, cell_width, cell_height); + + debug!("Estimated DPR: {}", estimated_dpr); + debug!("Estimated Cell Size: {} x {}", cell_width, cell_height); + debug!("Estimated Dimensions: {:?}", dimensions); + + // Create the window where Alacritty will be displayed + let logical = dimensions.map(|d| PhysicalSize::new(d.0, d.1).to_logical(estimated_dpr)); + let mut window = Window::new(event_loop, &options, config.window(), logical)?; + + let dpr = window.hidpi_factor(); + info!("Device pixel ratio: {}", dpr); + + // get window properties for initializing the other subsystems + let mut viewport_size = + window.inner_size_pixels().expect("glutin returns window size").to_physical(dpr); + + // Create renderer + let mut renderer = QuadRenderer::new()?; + + let (glyph_cache, cell_width, cell_height) = + Self::new_glyph_cache(dpr, &mut renderer, config)?; + + let mut padding_x = f64::from(config.padding().x) * dpr; + let mut padding_y = f64::from(config.padding().y) * dpr; + + if let Some((width, height)) = + Self::calculate_dimensions(config, options, dpr, cell_width, cell_height) + { + if dimensions == Some((width, height)) { + info!("Estimated DPR correctly, skipping resize"); + } else { + viewport_size = PhysicalSize::new(width, height); + window.set_inner_size(viewport_size.to_logical(dpr)); + } + } else if config.window().dynamic_padding() { + // Make sure additional padding is spread evenly + let cw = f64::from(cell_width); + let ch = f64::from(cell_height); + padding_x = padding_x + (viewport_size.width - 2. * padding_x) % cw / 2.; + padding_y = padding_y + (viewport_size.height - 2. * padding_y) % ch / 2.; + } + + padding_x = padding_x.floor(); + padding_y = padding_y.floor(); + + // Update OpenGL projection + renderer.resize(viewport_size, padding_x as f32, padding_y as f32); + + info!("Cell Size: {} x {}", cell_width, cell_height); + info!("Padding: {} x {}", padding_x, padding_y); + + let size_info = SizeInfo { + dpr, + width: viewport_size.width as f32, + height: viewport_size.height as f32, + cell_width: cell_width as f32, + cell_height: cell_height as f32, + padding_x: padding_x as f32, + padding_y: padding_y as f32, + }; + + // Channel for resize events + // + // macOS has a callback for getting resize events, the channel is used + // to queue resize events until the next draw call. Unfortunately, it + // seems that the event loop is blocked until the window is done + // resizing. If any drawing were to happen during a resize, it would + // need to be in the callback. + let (tx, rx) = mpsc::channel(); + + // Clear screen + let background_color = config.colors().primary.background; + renderer.with_api(config, &size_info, |api| { + api.clear(background_color); + }); + + Ok(Display { + window, + renderer, + glyph_cache, + render_timer, + tx, + rx, + meter: Meter::new(), + font_size: config.font().size(), + size_info, + last_message: None, + }) + } + + fn calculate_dimensions( + config: &Config, + options: &cli::Options, + dpr: f64, + cell_width: f32, + cell_height: f32, + ) -> Option<(f64, f64)> { + let dimensions = options.dimensions().unwrap_or_else(|| config.dimensions()); + + if dimensions.columns_u32() == 0 + || dimensions.lines_u32() == 0 + || config.window().startup_mode() != StartupMode::Windowed + { + return None; + } + + let padding_x = f64::from(config.padding().x) * dpr; + let padding_y = f64::from(config.padding().y) * dpr; + + // Calculate new size based on cols/lines specified in config + let grid_width = cell_width as u32 * dimensions.columns_u32(); + let grid_height = cell_height as u32 * dimensions.lines_u32(); + + let width = (f64::from(grid_width) + 2. * padding_x).floor(); + let height = (f64::from(grid_height) + 2. * padding_y).floor(); + + Some((width, height)) + } + + fn new_glyph_cache( + dpr: f64, + renderer: &mut QuadRenderer, + config: &Config, + ) -> Result<(GlyphCache, f32, f32), Error> { + let font = config.font().clone(); + let rasterizer = font::Rasterizer::new(dpr as f32, config.use_thin_strokes())?; + + // Initialize glyph cache + let glyph_cache = { + info!("Initializing glyph cache..."); + let init_start = ::std::time::Instant::now(); + + let cache = + renderer.with_loader(|mut api| GlyphCache::new(rasterizer, &font, &mut api))?; + + let stop = init_start.elapsed(); + let stop_f = stop.as_secs() as f64 + f64::from(stop.subsec_nanos()) / 1_000_000_000f64; + info!("... finished initializing glyph cache in {}s", stop_f); + + cache + }; + + // Need font metrics to resize the window properly. This suggests to me the + // font metrics should be computed before creating the window in the first + // place so that a resize is not needed. + let (cw, ch) = Self::compute_cell_size(config, &glyph_cache.font_metrics()); + + Ok((glyph_cache, cw, ch)) + } + + pub fn update_glyph_cache(&mut self, config: &Config) { + let cache = &mut self.glyph_cache; + let dpr = self.size_info.dpr; + let size = self.font_size; + + self.renderer.with_loader(|mut api| { + let _ = cache.update_font_size(config.font(), size, dpr, &mut api); + }); + + let (cw, ch) = Self::compute_cell_size(config, &cache.font_metrics()); + self.size_info.cell_width = cw; + self.size_info.cell_height = ch; + } + + fn compute_cell_size(config: &Config, metrics: &font::Metrics) -> (f32, f32) { + let offset_x = f64::from(config.font().offset().x); + let offset_y = f64::from(config.font().offset().y); + ( + f32::max(1., ((metrics.average_advance + offset_x) as f32).floor()), + f32::max(1., ((metrics.line_height + offset_y) as f32).floor()), + ) + } + + #[inline] + pub fn resize_channel(&self) -> mpsc::Sender<PhysicalSize> { + self.tx.clone() + } + + pub fn window(&mut self) -> &mut Window { + &mut self.window + } + + /// Process pending resize events + pub fn handle_resize( + &mut self, + terminal: &mut MutexGuard<'_, Term>, + config: &Config, + pty_resize_handle: &mut dyn OnResize, + processor_resize_handle: &mut dyn OnResize, + ) { + let previous_cols = self.size_info.cols(); + let previous_lines = self.size_info.lines(); + + // Resize events new_size and are handled outside the poll_events + // iterator. This has the effect of coalescing multiple resize + // events into one. + let mut new_size = None; + + // Take most recent resize event, if any + while let Ok(size) = self.rx.try_recv() { + new_size = Some(size); + } + + // Update the DPR + let dpr = self.window.hidpi_factor(); + + // Font size/DPI factor modification detected + let font_changed = + terminal.font_size != self.font_size || (dpr - self.size_info.dpr).abs() > f64::EPSILON; + + // Skip resize if nothing changed + if let Some(new_size) = new_size { + if !font_changed + && (new_size.width - f64::from(self.size_info.width)).abs() < f64::EPSILON + && (new_size.height - f64::from(self.size_info.height)).abs() < f64::EPSILON + { + return; + } + } + + if font_changed || self.last_message != terminal.message_buffer_mut().message() { + if new_size == None { + // Force a resize to refresh things + new_size = Some(PhysicalSize::new( + f64::from(self.size_info.width) / self.size_info.dpr * dpr, + f64::from(self.size_info.height) / self.size_info.dpr * dpr, + )); + } + + self.font_size = terminal.font_size; + self.last_message = terminal.message_buffer_mut().message(); + self.size_info.dpr = dpr; + } + + if font_changed { + self.update_glyph_cache(config); + } + + if let Some(psize) = new_size.take() { + let width = psize.width as f32; + let height = psize.height as f32; + let cell_width = self.size_info.cell_width; + let cell_height = self.size_info.cell_height; + + self.size_info.width = width; + self.size_info.height = height; + + let mut padding_x = f32::from(config.padding().x) * dpr as f32; + let mut padding_y = f32::from(config.padding().y) * dpr as f32; + + if config.window().dynamic_padding() { + padding_x = padding_x + ((width - 2. * padding_x) % cell_width) / 2.; + padding_y = padding_y + ((height - 2. * padding_y) % cell_height) / 2.; + } + + self.size_info.padding_x = padding_x.floor(); + self.size_info.padding_y = padding_y.floor(); + + let size = &self.size_info; + terminal.resize(size); + processor_resize_handle.on_resize(size); + + // Subtract message bar lines for pty size + let mut pty_size = *size; + if let Some(message) = terminal.message_buffer_mut().message() { + pty_size.height -= pty_size.cell_height * message.text(&size).len() as f32; + } + + if previous_cols != pty_size.cols() || previous_lines != pty_size.lines() { + pty_resize_handle.on_resize(&pty_size); + } + + self.window.resize(psize); + self.renderer.resize(psize, self.size_info.padding_x, self.size_info.padding_y); + } + } + + /// Draw the screen + /// + /// A reference to Term whose state is being drawn must be provided. + /// + /// This call may block if vsync is enabled + pub fn draw(&mut self, terminal: &FairMutex<Term>, config: &Config) { + let mut terminal = terminal.lock(); + let size_info = *terminal.size_info(); + let visual_bell_intensity = terminal.visual_bell.intensity(); + let background_color = terminal.background_color(); + let metrics = self.glyph_cache.font_metrics(); + + let window_focused = self.window.is_focused; + let grid_cells: Vec<RenderableCell> = + terminal.renderable_cells(config, window_focused, metrics).collect(); + + // Get message from terminal to ignore modifications after lock is dropped + let message_buffer = terminal.message_buffer_mut().message(); + + // Clear dirty flag + terminal.dirty = !terminal.visual_bell.completed(); + + if let Some(title) = terminal.get_next_title() { + self.window.set_title(&title); + } + + if let Some(mouse_cursor) = terminal.get_next_mouse_cursor() { + self.window.set_mouse_cursor(mouse_cursor); + } + + if let Some(is_urgent) = terminal.next_is_urgent.take() { + // We don't need to set the urgent flag if we already have the + // user's attention. + if !is_urgent || !self.window.is_focused { + self.window.set_urgent(is_urgent); + } + } + + // Clear when terminal mutex isn't held. Mesa for + // some reason takes a long time to call glClear(). The driver descends + // into xcb_connect_to_fd() which ends up calling __poll_nocancel() + // which blocks for a while. + // + // By keeping this outside of the critical region, the Mesa bug is + // worked around to some extent. Since this doesn't actually address the + // issue of glClear being slow, less time is available for input + // handling and rendering. + drop(terminal); + + self.renderer.with_api(config, &size_info, |api| { + api.clear(background_color); + }); + + { + let glyph_cache = &mut self.glyph_cache; + let mut rects = Rects::new(&metrics, &size_info); + + // Draw grid + { + let _sampler = self.meter.sampler(); + + self.renderer.with_api(config, &size_info, |mut api| { + // Iterate over all non-empty cells in the grid + for cell in grid_cells { + // Update underline/strikeout + rects.update_lines(&size_info, &cell); + + // Draw the cell + api.render_cell(cell, glyph_cache); + } + }); + } + + if let Some(message) = message_buffer { + let text = message.text(&size_info); + + // Create a new rectangle for the background + let start_line = size_info.lines().0 - text.len(); + let y = size_info.padding_y + size_info.cell_height * start_line as f32; + let rect = Rect::new(0., y, size_info.width, size_info.height - y); + rects.push(rect, message.color()); + + // Draw rectangles including the new background + self.renderer.draw_rects(config, &size_info, visual_bell_intensity, rects); + + // Relay messages to the user + let mut offset = 1; + for message_text in text.iter().rev() { + self.renderer.with_api(config, &size_info, |mut api| { + api.render_string( + &message_text, + Line(size_info.lines().saturating_sub(offset)), + glyph_cache, + None, + ); + }); + offset += 1; + } + } else { + // Draw rectangles + self.renderer.draw_rects(config, &size_info, visual_bell_intensity, rects); + } + + // Draw render timer + if self.render_timer { + let timing = format!("{:.3} usec", self.meter.average()); + let color = Rgb { r: 0xd5, g: 0x4e, b: 0x53 }; + self.renderer.with_api(config, &size_info, |mut api| { + api.render_string(&timing[..], size_info.lines() - 2, glyph_cache, Some(color)); + }); + } + } + + self.window.swap_buffers().expect("swap buffers"); + } + + pub fn get_window_id(&self) -> Option<usize> { + self.window.get_window_id() + } + + /// Adjust the IME editor position according to the new location of the cursor + pub fn update_ime_position(&mut self, terminal: &Term) { + let point = terminal.cursor().point; + let SizeInfo { cell_width: cw, cell_height: ch, padding_x: px, padding_y: py, .. } = + *terminal.size_info(); + + let dpr = self.window().hidpi_factor(); + let nspot_x = f64::from(px + point.col.0 as f32 * cw); + let nspot_y = f64::from(py + (point.line.0 + 1) as f32 * ch); + + self.window().set_ime_spot(PhysicalPosition::from((nspot_x, nspot_y)).to_logical(dpr)); + } +} diff --git a/alacritty_terminal/src/event.rs b/alacritty_terminal/src/event.rs new file mode 100644 index 00000000..1f3e9ca5 --- /dev/null +++ b/alacritty_terminal/src/event.rs @@ -0,0 +1,595 @@ +//! Process window events +use std::borrow::Cow; +use std::env; +#[cfg(unix)] +use std::fs; +use std::fs::File; +use std::io::Write; +use std::sync::mpsc; +use std::time::Instant; + +use copypasta::{Buffer as ClipboardBuffer, Clipboard, Load, Store}; +use glutin::dpi::PhysicalSize; +use glutin::{self, ElementState, Event, ModifiersState, MouseButton}; +use parking_lot::MutexGuard; +use serde_json as json; + +use crate::cli::Options; +use crate::config::{self, Config}; +use crate::display::OnResize; +use crate::grid::Scroll; +use crate::index::{Column, Line, Point, Side}; +use crate::input::{self, KeyBinding, MouseBinding}; +use crate::selection::Selection; +use crate::sync::FairMutex; +use crate::term::cell::Cell; +use crate::term::{SizeInfo, Term}; +#[cfg(unix)] +use crate::tty; +use crate::util::fmt::Red; +use crate::util::{limit, start_daemon}; +use crate::window::Window; + +/// Byte sequences are sent to a `Notify` in response to some events +pub trait Notify { + /// Notify that an escape sequence should be written to the pty + /// + /// TODO this needs to be able to error somehow + fn notify<B: Into<Cow<'static, [u8]>>>(&mut self, _: B); +} + +pub struct ActionContext<'a, N> { + pub notifier: &'a mut N, + pub terminal: &'a mut Term, + pub size_info: &'a mut SizeInfo, + pub mouse: &'a mut Mouse, + pub received_count: &'a mut usize, + pub suppress_chars: &'a mut bool, + pub last_modifiers: &'a mut ModifiersState, + pub window_changes: &'a mut WindowChanges, +} + +impl<'a, N: Notify + 'a> input::ActionContext for ActionContext<'a, N> { + fn write_to_pty<B: Into<Cow<'static, [u8]>>>(&mut self, val: B) { + self.notifier.notify(val); + } + + fn size_info(&self) -> SizeInfo { + *self.size_info + } + + fn scroll(&mut self, scroll: Scroll) { + self.terminal.scroll_display(scroll); + + if let ElementState::Pressed = self.mouse().left_button_state { + let (x, y) = (self.mouse().x, self.mouse().y); + let size_info = self.size_info(); + let point = size_info.pixels_to_coords(x, y); + let cell_side = self.mouse().cell_side; + self.update_selection(Point { line: point.line, col: point.col }, cell_side); + } + } + + fn copy_selection(&self, buffer: ClipboardBuffer) { + if let Some(selected) = self.terminal.selection_to_string() { + if !selected.is_empty() { + Clipboard::new() + .and_then(|mut clipboard| clipboard.store(selected, buffer)) + .unwrap_or_else(|err| { + warn!("Error storing selection to clipboard. {}", Red(err)); + }); + } + } + } + + fn selection_is_empty(&self) -> bool { + self.terminal.selection().as_ref().map(Selection::is_empty).unwrap_or(true) + } + + fn clear_selection(&mut self) { + *self.terminal.selection_mut() = None; + self.terminal.dirty = true; + } + + fn update_selection(&mut self, point: Point, side: Side) { + let point = self.terminal.visible_to_buffer(point); + + // Update selection if one exists + if let Some(ref mut selection) = self.terminal.selection_mut() { + selection.update(point, side); + } + + self.terminal.dirty = true; + } + + fn simple_selection(&mut self, point: Point, side: Side) { + let point = self.terminal.visible_to_buffer(point); + *self.terminal.selection_mut() = Some(Selection::simple(point, side)); + self.terminal.dirty = true; + } + + fn semantic_selection(&mut self, point: Point) { + let point = self.terminal.visible_to_buffer(point); + *self.terminal.selection_mut() = Some(Selection::semantic(point)); + self.terminal.dirty = true; + } + + fn line_selection(&mut self, point: Point) { + let point = self.terminal.visible_to_buffer(point); + *self.terminal.selection_mut() = Some(Selection::lines(point)); + self.terminal.dirty = true; + } + + fn mouse_coords(&self) -> Option<Point> { + self.terminal.pixels_to_coords(self.mouse.x as usize, self.mouse.y as usize) + } + + #[inline] + fn mouse_mut(&mut self) -> &mut Mouse { + self.mouse + } + + #[inline] + fn mouse(&self) -> &Mouse { + self.mouse + } + + #[inline] + fn received_count(&mut self) -> &mut usize { + &mut self.received_count + } + + #[inline] + fn suppress_chars(&mut self) -> &mut bool { + &mut self.suppress_chars + } + + #[inline] + fn last_modifiers(&mut self) -> &mut ModifiersState { + &mut self.last_modifiers + } + + #[inline] + fn hide_window(&mut self) { + self.window_changes.hide = true; + } + + #[inline] + fn terminal(&self) -> &Term { + self.terminal + } + + #[inline] + fn terminal_mut(&mut self) -> &mut Term { + self.terminal + } + + fn spawn_new_instance(&mut self) { + let alacritty = env::args().next().unwrap(); + + #[cfg(unix)] + let args = { + #[cfg(not(target_os = "freebsd"))] + let proc_prefix = ""; + #[cfg(target_os = "freebsd")] + let proc_prefix = "/compat/linux"; + let link_path = format!("{}/proc/{}/cwd", proc_prefix, tty::child_pid()); + if let Ok(path) = fs::read_link(link_path) { + vec!["--working-directory".into(), path] + } else { + Vec::new() + } + }; + #[cfg(not(unix))] + let args: Vec<String> = Vec::new(); + + match start_daemon(&alacritty, &args) { + Ok(_) => debug!("Started new Alacritty process: {} {:?}", alacritty, args), + Err(_) => warn!("Unable to start new Alacritty process: {} {:?}", alacritty, args), + } + } + + fn toggle_fullscreen(&mut self) { + self.window_changes.toggle_fullscreen(); + } + + #[cfg(target_os = "macos")] + fn toggle_simple_fullscreen(&mut self) { + self.window_changes.toggle_simple_fullscreen() + } +} + +/// The ActionContext can't really have direct access to the Window +/// with the current design. Event handlers that want to change the +/// window must set these flags instead. The processor will trigger +/// the actual changes. +#[derive(Default)] +pub struct WindowChanges { + pub hide: bool, + pub toggle_fullscreen: bool, + #[cfg(target_os = "macos")] + pub toggle_simple_fullscreen: bool, +} + +impl WindowChanges { + fn clear(&mut self) { + *self = WindowChanges::default(); + } + + fn toggle_fullscreen(&mut self) { + self.toggle_fullscreen = !self.toggle_fullscreen; + } + + #[cfg(target_os = "macos")] + fn toggle_simple_fullscreen(&mut self) { + self.toggle_simple_fullscreen = !self.toggle_simple_fullscreen; + } +} + +pub enum ClickState { + None, + Click, + DoubleClick, + TripleClick, +} + +/// State of the mouse +pub struct Mouse { + pub x: usize, + pub y: usize, + pub left_button_state: ElementState, + pub middle_button_state: ElementState, + pub right_button_state: ElementState, + pub last_click_timestamp: Instant, + pub click_state: ClickState, + pub scroll_px: i32, + pub line: Line, + pub column: Column, + pub cell_side: Side, + pub lines_scrolled: f32, + pub block_url_launcher: bool, + pub last_button: MouseButton, +} + +impl Default for Mouse { + fn default() -> Mouse { + Mouse { + x: 0, + y: 0, + last_click_timestamp: Instant::now(), + left_button_state: ElementState::Released, + middle_button_state: ElementState::Released, + right_button_state: ElementState::Released, + click_state: ClickState::None, + scroll_px: 0, + line: Line(0), + column: Column(0), + cell_side: Side::Left, + lines_scrolled: 0.0, + block_url_launcher: false, + last_button: MouseButton::Other(0), + } + } +} + +/// The event processor +/// +/// Stores some state from received events and dispatches actions when they are +/// triggered. +pub struct Processor<N> { + key_bindings: Vec<KeyBinding>, + mouse_bindings: Vec<MouseBinding>, + mouse_config: config::Mouse, + scrolling_config: config::Scrolling, + print_events: bool, + wait_for_event: bool, + notifier: N, + mouse: Mouse, + resize_tx: mpsc::Sender<PhysicalSize>, + ref_test: bool, + size_info: SizeInfo, + hide_mouse_when_typing: bool, + hide_mouse: bool, + received_count: usize, + suppress_chars: bool, + last_modifiers: ModifiersState, + pending_events: Vec<Event>, + window_changes: WindowChanges, + save_to_clipboard: bool, + alt_send_esc: bool, + is_fullscreen: bool, + is_simple_fullscreen: bool, +} + +/// Notify that the terminal was resized +/// +/// Currently this just forwards the notice to the input processor. +impl<N> OnResize for Processor<N> { + fn on_resize(&mut self, size: &SizeInfo) { + self.size_info = size.to_owned(); + } +} + +impl<N: Notify> Processor<N> { + /// Create a new event processor + /// + /// Takes a writer which is expected to be hooked up to the write end of a + /// pty. + pub fn new( + notifier: N, + resize_tx: mpsc::Sender<PhysicalSize>, + options: &Options, + config: &Config, + ref_test: bool, + size_info: SizeInfo, + ) -> Processor<N> { + Processor { + key_bindings: config.key_bindings().to_vec(), + mouse_bindings: config.mouse_bindings().to_vec(), + mouse_config: config.mouse().to_owned(), + scrolling_config: config.scrolling(), + print_events: options.print_events, + wait_for_event: true, + notifier, + resize_tx, + ref_test, + mouse: Default::default(), + size_info, + hide_mouse_when_typing: config.hide_mouse_when_typing(), + hide_mouse: false, + received_count: 0, + suppress_chars: false, + last_modifiers: Default::default(), + pending_events: Vec::with_capacity(4), + window_changes: Default::default(), + save_to_clipboard: config.selection().save_to_clipboard, + alt_send_esc: config.alt_send_esc(), + is_fullscreen: false, + is_simple_fullscreen: false, + } + } + + /// Handle events from glutin + /// + /// Doesn't take self mutably due to borrow checking. Kinda uggo but w/e. + fn handle_event<'a>( + processor: &mut input::Processor<'a, ActionContext<'a, N>>, + event: Event, + ref_test: bool, + resize_tx: &mpsc::Sender<PhysicalSize>, + hide_mouse: &mut bool, + window_is_focused: &mut bool, + ) { + match event { + // Pass on device events + Event::DeviceEvent { .. } | Event::Suspended { .. } => (), + Event::WindowEvent { event, .. } => { + use glutin::WindowEvent::*; + match event { + CloseRequested => { + if ref_test { + // dump grid state + let mut grid = processor.ctx.terminal.grid().clone(); + grid.initialize_all(&Cell::default()); + grid.truncate(); + + let serialized_grid = json::to_string(&grid).expect("serialize grid"); + + let serialized_size = + json::to_string(processor.ctx.terminal.size_info()) + .expect("serialize size"); + + let serialized_config = + format!("{{\"history_size\":{}}}", grid.history_size()); + + File::create("./grid.json") + .and_then(|mut f| f.write_all(serialized_grid.as_bytes())) + .expect("write grid.json"); + + File::create("./size.json") + .and_then(|mut f| f.write_all(serialized_size.as_bytes())) + .expect("write size.json"); + + File::create("./config.json") + .and_then(|mut f| f.write_all(serialized_config.as_bytes())) + .expect("write config.json"); + } + + processor.ctx.terminal.exit(); + }, + Resized(lsize) => { + // Resize events are emitted via glutin/winit with logical sizes + // However the terminal, window and renderer use physical sizes + // so a conversion must be done here + resize_tx + .send(lsize.to_physical(processor.ctx.size_info.dpr)) + .expect("send new size"); + processor.ctx.terminal.dirty = true; + }, + KeyboardInput { input, .. } => { + processor.process_key(input); + if input.state == ElementState::Pressed { + // Hide cursor while typing + *hide_mouse = true; + } + }, + ReceivedCharacter(c) => { + processor.received_char(c); + }, + MouseInput { state, button, modifiers, .. } => { + if !cfg!(target_os = "macos") || *window_is_focused { + *hide_mouse = false; + processor.mouse_input(state, button, modifiers); + processor.ctx.terminal.dirty = true; + } + }, + CursorMoved { position: lpos, modifiers, .. } => { + let (x, y) = lpos.to_physical(processor.ctx.size_info.dpr).into(); + let x: i32 = limit(x, 0, processor.ctx.size_info.width as i32); + let y: i32 = limit(y, 0, processor.ctx.size_info.height as i32); + + *hide_mouse = false; + processor.mouse_moved(x as usize, y as usize, modifiers); + }, + MouseWheel { delta, phase, modifiers, .. } => { + *hide_mouse = false; + processor.on_mouse_wheel(delta, phase, modifiers); + }, + Refresh => { + processor.ctx.terminal.dirty = true; + }, + Focused(is_focused) => { + *window_is_focused = is_focused; + + if is_focused { + processor.ctx.terminal.dirty = true; + processor.ctx.terminal.next_is_urgent = Some(false); + } else { + processor.ctx.terminal.reset_url_highlight(); + processor.ctx.terminal.dirty = true; + *hide_mouse = false; + } + + processor.on_focus_change(is_focused); + }, + DroppedFile(path) => { + use crate::input::ActionContext; + let path: String = path.to_string_lossy().into(); + processor.ctx.write_to_pty(path.into_bytes()); + }, + HiDpiFactorChanged(new_dpr) => { + processor.ctx.size_info.dpr = new_dpr; + processor.ctx.terminal.dirty = true; + }, + _ => (), + } + }, + Event::Awakened => { + processor.ctx.terminal.dirty = true; + }, + } + } + + /// Process events. When `wait_for_event` is set, this method is guaranteed + /// to process at least one event. + pub fn process_events<'a>( + &mut self, + term: &'a FairMutex<Term>, + window: &mut Window, + ) -> MutexGuard<'a, Term> { + // Terminal is lazily initialized the first time an event is returned + // from the blocking WaitEventsIterator. Otherwise, the pty reader would + // be blocked the entire time we wait for input! + let mut terminal; + + self.pending_events.clear(); + + { + // Ditto on lazy initialization for context and processor. + let context; + let mut processor: input::Processor<'_, ActionContext<'_, N>>; + + let print_events = self.print_events; + + let ref_test = self.ref_test; + let resize_tx = &self.resize_tx; + + if self.wait_for_event { + // A Vec is used here since wait_events can potentially yield + // multiple events before the interrupt is handled. For example, + // Resize and Moved events. + let pending_events = &mut self.pending_events; + window.wait_events(|e| { + pending_events.push(e); + glutin::ControlFlow::Break + }); + } + + terminal = term.lock(); + + context = ActionContext { + terminal: &mut terminal, + notifier: &mut self.notifier, + mouse: &mut self.mouse, + size_info: &mut self.size_info, + received_count: &mut self.received_count, + suppress_chars: &mut self.suppress_chars, + last_modifiers: &mut self.last_modifiers, + window_changes: &mut self.window_changes, + }; + + processor = input::Processor { + ctx: context, + scrolling_config: &self.scrolling_config, + mouse_config: &self.mouse_config, + key_bindings: &self.key_bindings[..], + mouse_bindings: &self.mouse_bindings[..], + save_to_clipboard: self.save_to_clipboard, + alt_send_esc: self.alt_send_esc, + }; + + let mut window_is_focused = window.is_focused; + + // Scope needed to that hide_mouse isn't borrowed after the scope + // ends. + { + let hide_mouse = &mut self.hide_mouse; + let mut process = |event| { + if print_events { + info!("glutin event: {:?}", event); + } + Processor::handle_event( + &mut processor, + event, + ref_test, + resize_tx, + hide_mouse, + &mut window_is_focused, + ); + }; + + for event in self.pending_events.drain(..) { + process(event); + } + + window.poll_events(process); + } + + if self.hide_mouse_when_typing { + window.set_mouse_visible(!self.hide_mouse); + } + + window.is_focused = window_is_focused; + } + + if self.window_changes.hide { + window.hide(); + } + + #[cfg(target_os = "macos")] + { + if self.window_changes.toggle_simple_fullscreen && !self.is_fullscreen { + window.set_simple_fullscreen(!self.is_simple_fullscreen); + self.is_simple_fullscreen = !self.is_simple_fullscreen; + } + } + + if self.window_changes.toggle_fullscreen && !self.is_simple_fullscreen { + window.set_fullscreen(!self.is_fullscreen); + self.is_fullscreen = !self.is_fullscreen; + } + + self.window_changes.clear(); + self.wait_for_event = !terminal.dirty; + + terminal + } + + pub fn update_config(&mut self, config: &Config) { + self.key_bindings = config.key_bindings().to_vec(); + self.mouse_bindings = config.mouse_bindings().to_vec(); + self.mouse_config = config.mouse().to_owned(); + self.save_to_clipboard = config.selection().save_to_clipboard; + self.alt_send_esc = config.alt_send_esc(); + } +} diff --git a/alacritty_terminal/src/event_loop.rs b/alacritty_terminal/src/event_loop.rs new file mode 100644 index 00000000..4941b479 --- /dev/null +++ b/alacritty_terminal/src/event_loop.rs @@ -0,0 +1,436 @@ +//! The main event loop which performs I/O on the pseudoterminal +use std::borrow::Cow; +use std::collections::VecDeque; +use std::fs::File; +use std::io::{self, ErrorKind, Read, Write}; +use std::marker::Send; +use std::sync::Arc; + +use mio::{self, Events, PollOpt, Ready}; +use mio_extras::channel::{self, Receiver, Sender}; + +#[cfg(not(windows))] +use mio::unix::UnixReady; + +use crate::ansi; +use crate::display; +use crate::event; +use crate::sync::FairMutex; +use crate::term::Term; +use crate::tty; +use crate::util::thread; + +/// Messages that may be sent to the `EventLoop` +#[derive(Debug)] +pub enum Msg { + /// Data that should be written to the pty + Input(Cow<'static, [u8]>), + + /// Indicates that the `EventLoop` should shut down, as Alacritty is shutting down + Shutdown, +} + +/// The main event!.. loop. +/// +/// Handles all the pty I/O and runs the pty parser which updates terminal +/// state. +pub struct EventLoop<T: tty::EventedPty> { + poll: mio::Poll, + pty: T, + rx: Receiver<Msg>, + tx: Sender<Msg>, + terminal: Arc<FairMutex<Term>>, + display: display::Notifier, + ref_test: bool, +} + +/// Helper type which tracks how much of a buffer has been written. +struct Writing { + source: Cow<'static, [u8]>, + written: usize, +} + +/// Indicates the result of draining the mio channel +#[derive(Debug)] +enum DrainResult { + /// At least one new item was received + ReceivedItem, + /// Nothing was available to receive + Empty, + /// A shutdown message was received + Shutdown, +} + +impl DrainResult { + pub fn is_shutdown(&self) -> bool { + match *self { + DrainResult::Shutdown => true, + _ => false, + } + } +} + +/// All of the mutable state needed to run the event loop +/// +/// Contains list of items to write, current write state, etc. Anything that +/// would otherwise be mutated on the `EventLoop` goes here. +pub struct State { + write_list: VecDeque<Cow<'static, [u8]>>, + writing: Option<Writing>, + parser: ansi::Processor, +} + +pub struct Notifier(pub Sender<Msg>); + +impl event::Notify for Notifier { + fn notify<B>(&mut self, bytes: B) + where + B: Into<Cow<'static, [u8]>>, + { + let bytes = bytes.into(); + // terminal hangs if we send 0 bytes through. + if bytes.len() == 0 { + return; + } + if self.0.send(Msg::Input(bytes)).is_err() { + panic!("expected send event loop msg"); + } + } +} + +impl Default for State { + fn default() -> State { + State { write_list: VecDeque::new(), parser: ansi::Processor::new(), writing: None } + } +} + +impl State { + #[inline] + fn ensure_next(&mut self) { + if self.writing.is_none() { + self.goto_next(); + } + } + + #[inline] + fn goto_next(&mut self) { + self.writing = self.write_list.pop_front().map(Writing::new); + } + + #[inline] + fn take_current(&mut self) -> Option<Writing> { + self.writing.take() + } + + #[inline] + fn needs_write(&self) -> bool { + self.writing.is_some() || !self.write_list.is_empty() + } + + #[inline] + fn set_current(&mut self, new: Option<Writing>) { + self.writing = new; + } +} + +impl Writing { + #[inline] + fn new(c: Cow<'static, [u8]>) -> Writing { + Writing { source: c, written: 0 } + } + + #[inline] + fn advance(&mut self, n: usize) { + self.written += n; + } + + #[inline] + fn remaining_bytes(&self) -> &[u8] { + &self.source[self.written..] + } + + #[inline] + fn finished(&self) -> bool { + self.written >= self.source.len() + } +} + +impl<T> EventLoop<T> +where + T: tty::EventedPty + Send + 'static, +{ + /// Create a new event loop + pub fn new( + terminal: Arc<FairMutex<Term>>, + display: display::Notifier, + pty: T, + ref_test: bool, + ) -> EventLoop<T> { + let (tx, rx) = channel::channel(); + EventLoop { + poll: mio::Poll::new().expect("create mio Poll"), + pty, + tx, + rx, + terminal, + display, + ref_test, + } + } + + pub fn channel(&self) -> Sender<Msg> { + self.tx.clone() + } + + // Drain the channel + // + // Returns a `DrainResult` indicating the result of receiving from the channel + // + fn drain_recv_channel(&self, state: &mut State) -> DrainResult { + let mut received_item = false; + while let Ok(msg) = self.rx.try_recv() { + received_item = true; + match msg { + Msg::Input(input) => { + state.write_list.push_back(input); + }, + Msg::Shutdown => { + return DrainResult::Shutdown; + }, + } + } + + if received_item { + DrainResult::ReceivedItem + } else { + DrainResult::Empty + } + } + + // Returns a `bool` indicating whether or not the event loop should continue running + #[inline] + fn channel_event(&mut self, token: mio::Token, state: &mut State) -> bool { + if self.drain_recv_channel(state).is_shutdown() { + return false; + } + + self.poll + .reregister(&self.rx, token, Ready::readable(), PollOpt::edge() | PollOpt::oneshot()) + .unwrap(); + + true + } + + #[inline] + fn pty_read<X>( + &mut self, + state: &mut State, + buf: &mut [u8], + mut writer: Option<&mut X>, + ) -> io::Result<()> + where + X: Write, + { + const MAX_READ: usize = 0x1_0000; + let mut processed = 0; + let mut terminal = None; + + // Flag to keep track if wakeup has already been sent + let mut send_wakeup = false; + + loop { + match self.pty.reader().read(&mut buf[..]) { + Ok(0) => break, + Ok(got) => { + // Record bytes read; used to limit time spent in pty_read. + processed += got; + + // Send a copy of bytes read to a subscriber. Used for + // example with ref test recording. + writer = writer.map(|w| { + w.write_all(&buf[..got]).unwrap(); + w + }); + + // Get reference to terminal. Lock is acquired on initial + // iteration and held until there's no bytes left to parse + // or we've reached MAX_READ. + let terminal = if terminal.is_none() { + terminal = Some(self.terminal.lock()); + let terminal = terminal.as_mut().unwrap(); + send_wakeup = !terminal.dirty; + terminal + } else { + terminal.as_mut().unwrap() + }; + + // Run the parser + for byte in &buf[..got] { + state.parser.advance(&mut **terminal, *byte, &mut self.pty.writer()); + } + + // Exit if we've processed enough bytes + if processed > MAX_READ { + break; + } + }, + Err(err) => match err.kind() { + ErrorKind::Interrupted | ErrorKind::WouldBlock => { + break; + }, + _ => return Err(err), + }, + } + } + + // Only request a draw if one hasn't already been requested. + if let Some(mut terminal) = terminal { + if send_wakeup { + self.display.notify(); + terminal.dirty = true; + } + } + + Ok(()) + } + + #[inline] + fn pty_write(&mut self, state: &mut State) -> io::Result<()> { + state.ensure_next(); + + 'write_many: while let Some(mut current) = state.take_current() { + 'write_one: loop { + match self.pty.writer().write(current.remaining_bytes()) { + Ok(0) => { + state.set_current(Some(current)); + break 'write_many; + }, + Ok(n) => { + current.advance(n); + if current.finished() { + state.goto_next(); + break 'write_one; + } + }, + Err(err) => { + state.set_current(Some(current)); + match err.kind() { + ErrorKind::Interrupted | ErrorKind::WouldBlock => break 'write_many, + _ => return Err(err), + } + }, + } + } + } + + Ok(()) + } + + pub fn spawn(mut self, state: Option<State>) -> thread::JoinHandle<(Self, State)> { + thread::spawn_named("pty reader", move || { + let mut state = state.unwrap_or_else(Default::default); + let mut buf = [0u8; 0x1000]; + + let mut tokens = (0..).map(Into::into); + + let poll_opts = PollOpt::edge() | PollOpt::oneshot(); + + let channel_token = tokens.next().unwrap(); + self.poll.register(&self.rx, channel_token, Ready::readable(), poll_opts).unwrap(); + + // Register TTY through EventedRW interface + self.pty.register(&self.poll, &mut tokens, Ready::readable(), poll_opts).unwrap(); + + let mut events = Events::with_capacity(1024); + + let mut pipe = if self.ref_test { + Some(File::create("./alacritty.recording").expect("create alacritty recording")) + } else { + None + }; + + 'event_loop: loop { + if let Err(err) = self.poll.poll(&mut events, None) { + match err.kind() { + ErrorKind::Interrupted => continue, + _ => panic!("EventLoop polling error: {:?}", err), + } + } + + for event in events.iter() { + match event.token() { + token if token == channel_token => { + if !self.channel_event(channel_token, &mut state) { + break 'event_loop; + } + }, + + #[cfg(unix)] + token if token == self.pty.child_event_token() => { + if let Some(tty::ChildEvent::Exited) = self.pty.next_child_event() { + self.terminal.lock().exit(); + self.display.notify(); + break 'event_loop; + } + }, + + token + if token == self.pty.read_token() + || token == self.pty.write_token() => + { + #[cfg(unix)] + { + if UnixReady::from(event.readiness()).is_hup() { + // don't try to do I/O on a dead PTY + continue; + } + } + + if event.readiness().is_readable() { + if let Err(e) = self.pty_read(&mut state, &mut buf, pipe.as_mut()) { + #[cfg(target_os = "linux")] + { + // On Linux, a `read` on the master side of a PTY can fail + // with `EIO` if the client side hangs up. In that case, + // just loop back round for the inevitable `Exited` event. + // This sucks, but checking the process is either racy or + // blocking. + if e.kind() == ErrorKind::Other { + continue; + } + } + + error!("Error reading from PTY in event loop: {}", e); + break 'event_loop; + } + } + + if event.readiness().is_writable() { + if let Err(e) = self.pty_write(&mut state) { + error!("Error writing to PTY in event loop: {}", e); + break 'event_loop; + } + } + } + _ => (), + } + } + + // Register write interest if necessary + let mut interest = Ready::readable(); + if state.needs_write() { + interest.insert(Ready::writable()); + } + // Reregister with new interest + self.pty.reregister(&self.poll, interest, poll_opts).unwrap(); + } + + // The evented instances are not dropped here so deregister them explicitly + // TODO: Is this still necessary? + let _ = self.poll.deregister(&self.rx); + let _ = self.pty.deregister(&self.poll); + + (self, state) + }) + } +} diff --git a/alacritty_terminal/src/grid/mod.rs b/alacritty_terminal/src/grid/mod.rs new file mode 100644 index 00000000..3a6bacf8 --- /dev/null +++ b/alacritty_terminal/src/grid/mod.rs @@ -0,0 +1,886 @@ +// 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. + +//! A specialized 2d grid implementation optimized for use in a terminal. + +use std::cmp::{max, min, Ordering}; +use std::ops::{Deref, Index, IndexMut, Range, RangeFrom, RangeFull, RangeInclusive, RangeTo}; + +use crate::index::{self, Column, IndexRange, Line, Point}; +use crate::selection::Selection; + +mod row; +pub use self::row::Row; + +#[cfg(test)] +mod tests; + +mod storage; +use self::storage::Storage; + +const MIN_INIT_SIZE: usize = 1_000; + +/// Bidirection iterator +pub trait BidirectionalIterator: Iterator { + fn prev(&mut self) -> Option<Self::Item>; +} + +/// An item in the grid along with its Line and Column. +pub struct Indexed<T> { + pub inner: T, + pub line: Line, + pub column: Column, +} + +impl<T> Deref for Indexed<T> { + type Target = T; + + #[inline] + fn deref(&self) -> &T { + &self.inner + } +} + +impl<T: PartialEq> ::std::cmp::PartialEq for Grid<T> { + fn eq(&self, other: &Self) -> bool { + // Compare struct fields and check result of grid comparison + self.raw.eq(&other.raw) + && self.cols.eq(&other.cols) + && self.lines.eq(&other.lines) + && self.display_offset.eq(&other.display_offset) + && self.scroll_limit.eq(&other.scroll_limit) + && self.selection.eq(&other.selection) + && self.url_highlight.eq(&other.url_highlight) + } +} + +pub trait GridCell { + fn is_empty(&self) -> bool; + fn is_wrap(&self) -> bool; + fn set_wrap(&mut self, wrap: bool); +} + +/// Represents the terminal display contents +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Grid<T> { + /// Lines in the grid. Each row holds a list of cells corresponding to the + /// columns in that row. + raw: Storage<T>, + + /// Number of columns + cols: index::Column, + + /// Number of lines. + /// + /// Invariant: lines is equivalent to raw.len() + lines: index::Line, + + /// Offset of displayed area + /// + /// If the displayed region isn't at the bottom of the screen, it stays + /// stationary while more text is emitted. The scrolling implementation + /// updates this offset accordingly. + #[serde(default)] + display_offset: usize, + + /// An limit on how far back it's possible to scroll + #[serde(default)] + scroll_limit: usize, + + /// Selected region + #[serde(skip)] + pub selection: Option<Selection>, + + #[serde(default)] + max_scroll_limit: usize, + + /// Range for URL hover highlights + #[serde(default)] + pub url_highlight: Option<RangeInclusive<index::Linear>>, +} + +#[derive(Copy, Clone)] +pub enum Scroll { + Lines(isize), + PageUp, + PageDown, + Top, + Bottom, +} + +#[derive(Copy, Clone)] +pub enum ViewportPosition { + Visible(Line), + Above, + Below, +} + +impl<T: GridCell + Copy + Clone> Grid<T> { + pub fn new(lines: index::Line, cols: index::Column, scrollback: usize, template: T) -> Grid<T> { + let raw = Storage::with_capacity(lines, Row::new(cols, &template)); + Grid { + raw, + cols, + lines, + display_offset: 0, + scroll_limit: 0, + selection: None, + max_scroll_limit: scrollback, + url_highlight: None, + } + } + + pub fn visible_to_buffer(&self, point: Point) -> Point<usize> { + Point { line: self.visible_line_to_buffer(point.line), col: point.col } + } + + pub fn buffer_line_to_visible(&self, line: usize) -> ViewportPosition { + let offset = line.saturating_sub(self.display_offset); + if line < self.display_offset { + ViewportPosition::Below + } else if offset >= *self.num_lines() { + ViewportPosition::Above + } else { + ViewportPosition::Visible(self.lines - offset - 1) + } + } + + pub fn visible_line_to_buffer(&self, line: Line) -> usize { + self.line_to_offset(line) + self.display_offset + } + + /// Update the size of the scrollback history + pub fn update_history(&mut self, history_size: usize, template: &T) { + self.raw.update_history(history_size, Row::new(self.cols, &template)); + self.max_scroll_limit = history_size; + self.scroll_limit = min(self.scroll_limit, history_size); + self.display_offset = min(self.display_offset, self.scroll_limit); + } + + pub fn scroll_display(&mut self, scroll: Scroll) { + match scroll { + Scroll::Lines(count) => { + self.display_offset = min( + max((self.display_offset as isize) + count, 0isize) as usize, + self.scroll_limit, + ); + }, + Scroll::PageUp => { + self.display_offset = min(self.display_offset + self.lines.0, self.scroll_limit); + }, + Scroll::PageDown => { + self.display_offset -= min(self.display_offset, self.lines.0); + }, + Scroll::Top => self.display_offset = self.scroll_limit, + Scroll::Bottom => self.display_offset = 0, + } + } + + pub fn resize( + &mut self, + lines: index::Line, + cols: index::Column, + cursor_pos: &mut Point, + template: &T, + ) { + // Check that there's actually work to do and return early if not + if lines == self.lines && cols == self.cols { + return; + } + + match self.lines.cmp(&lines) { + Ordering::Less => self.grow_lines(lines, template), + Ordering::Greater => self.shrink_lines(lines), + Ordering::Equal => (), + } + + match self.cols.cmp(&cols) { + Ordering::Less => self.grow_cols(cols, cursor_pos, template), + Ordering::Greater => self.shrink_cols(cols, template), + Ordering::Equal => (), + } + } + + fn increase_scroll_limit(&mut self, count: usize, template: &T) { + self.scroll_limit = min(self.scroll_limit + count, self.max_scroll_limit); + + // Initialize new lines when the history buffer is smaller than the scroll limit + let history_size = self.raw.len().saturating_sub(*self.lines); + if history_size < self.scroll_limit { + let new = min( + max(self.scroll_limit - history_size, MIN_INIT_SIZE), + self.max_scroll_limit - history_size, + ); + self.raw.initialize(new, Row::new(self.cols, template)); + } + } + + fn decrease_scroll_limit(&mut self, count: usize) { + self.scroll_limit = self.scroll_limit.saturating_sub(count); + } + + /// Add lines to the visible area + /// + /// Alacritty keeps the cursor at the bottom of the terminal as long as there + /// is scrollback available. Once scrollback is exhausted, new lines are + /// simply added to the bottom of the screen. + fn grow_lines(&mut self, new_line_count: index::Line, template: &T) { + let lines_added = new_line_count - self.lines; + + // Need to "resize" before updating buffer + self.raw.grow_visible_lines(new_line_count, Row::new(self.cols, template)); + self.lines = new_line_count; + + // Move existing lines up if there is no scrollback to fill new lines + if lines_added.0 > self.scroll_limit { + let scroll_lines = lines_added - self.scroll_limit; + self.scroll_up(&(Line(0)..new_line_count), scroll_lines, template); + } + + self.scroll_limit = self.scroll_limit.saturating_sub(*lines_added); + self.display_offset = self.display_offset.saturating_sub(*lines_added); + } + + fn grow_cols(&mut self, cols: index::Column, cursor_pos: &mut Point, template: &T) { + // Truncate all buffered lines + self.raw.grow_hidden(cols, template); + + let max_lines = self.lines.0 + self.max_scroll_limit; + + // Iterate backwards with indices for mutation during iteration + let mut i = self.raw.len(); + while i > 0 { + i -= 1; + + // Grow the current line if there's wrapped content available + while i >= 1 + && self.raw[i].len() < cols.0 + && self.raw[i].last().map(GridCell::is_wrap) == Some(true) + { + // Remove wrap flag before appending additional cells + if let Some(cell) = self.raw[i].last_mut() { + cell.set_wrap(false); + } + + // Append as many cells from the next line as possible + let len = min(self.raw[i - 1].len(), cols.0 - self.raw[i].len()); + let mut cells = self.raw[i - 1].front_split_off(len); + self.raw[i].append(&mut cells); + + if self.raw[i - 1].is_empty() { + // Remove following line if all cells have been drained + self.raw.remove(i - 1); + + if self.raw.len() < self.lines.0 || self.scroll_limit == 0 { + // Add new line and move lines up if we can't pull from history + self.raw.insert(0, Row::new(cols, template), max_lines); + cursor_pos.line = Line(cursor_pos.line.saturating_sub(1)); + } else { + // Make sure viewport doesn't move if line is outside of the visible area + if i < self.display_offset { + self.display_offset = self.display_offset.saturating_sub(1); + } + + // Remove one line from scrollback, since we just moved it to the viewport + self.scroll_limit = self.scroll_limit.saturating_sub(1); + self.display_offset = min(self.display_offset, self.scroll_limit); + i -= 1; + } + } else if let Some(cell) = self.raw[i].last_mut() { + // Set wrap flag if next line still has cells + cell.set_wrap(true); + } + } + + // Fill remaining cells + if self.raw[i].len() < cols.0 { + self.raw[i].grow(cols, template); + } + } + + self.cols = cols; + } + + fn shrink_cols(&mut self, cols: index::Column, template: &T) { + // Truncate all buffered lines + self.raw.shrink_hidden(cols); + + let max_lines = self.lines.0 + self.max_scroll_limit; + + // Iterate backwards with indices for mutation during iteration + let mut i = self.raw.len(); + while i > 0 { + i -= 1; + + if let Some(mut new_row) = self.raw[i].shrink(cols) { + // Set line as wrapped if cells got removed + if let Some(cell) = self.raw[i].last_mut() { + cell.set_wrap(true); + } + + if Some(true) == new_row.last().map(|c| c.is_wrap() && i >= 1) + && new_row.len() < cols.0 + { + // Make sure previous wrap flag doesn't linger around + if let Some(cell) = new_row.last_mut() { + cell.set_wrap(false); + } + + // Add removed cells to start of next row + self.raw[i - 1].append_front(new_row); + } else { + // Make sure viewport doesn't move if line is outside of the visible area + if i < self.display_offset { + self.display_offset = min(self.display_offset + 1, self.max_scroll_limit); + } + + // Make sure new row is at least as long as new width + let occ = new_row.len(); + if occ < cols.0 { + new_row.append(&mut vec![*template; cols.0 - occ]); + } + let row = Row::from_vec(new_row, occ); + + // Add new row with all removed cells + self.raw.insert(i, row, max_lines); + + // Increase scrollback history + self.scroll_limit = min(self.scroll_limit + 1, self.max_scroll_limit); + + // Since inserted might exceed cols, we need to check the same line again + i += 1; + } + } + } + + self.cols = cols; + } + + /// Remove lines from the visible area + /// + /// The behavior in Terminal.app and iTerm.app is to keep the cursor at the + /// bottom of the screen. This is achieved by pushing history "out the top" + /// of the terminal window. + /// + /// Alacritty takes the same approach. + fn shrink_lines(&mut self, target: index::Line) { + let prev = self.lines; + + self.selection = None; + self.url_highlight = None; + self.raw.rotate(*prev as isize - *target as isize); + self.raw.shrink_visible_lines(target); + self.lines = target; + } + + /// Convert a Line index (active region) to a buffer offset + /// + /// # Panics + /// + /// This method will panic if `Line` is larger than the grid dimensions + pub fn line_to_offset(&self, line: index::Line) -> usize { + assert!(line < self.num_lines()); + + *(self.num_lines() - line - 1) + } + + #[inline] + pub fn scroll_down( + &mut self, + region: &Range<index::Line>, + positions: index::Line, + template: &T, + ) { + // Whether or not there is a scrolling region active, as long as it + // starts at the top, we can do a full rotation which just involves + // changing the start index. + // + // To accomodate scroll regions, rows are reordered at the end. + if region.start == Line(0) { + // Rotate the entire line buffer. If there's a scrolling region + // active, the bottom lines are restored in the next step. + self.raw.rotate_up(*positions); + if let Some(ref mut selection) = self.selection { + selection.rotate(-(*positions as isize)); + } + self.url_highlight = None; + + self.decrease_scroll_limit(*positions); + + // Now, restore any scroll region lines + let lines = self.lines; + for i in IndexRange(region.end..lines) { + self.raw.swap_lines(i, i + positions); + } + + // Finally, reset recycled lines + for i in IndexRange(Line(0)..positions) { + self.raw[i].reset(&template); + } + } else { + // Subregion rotation + for line in IndexRange((region.start + positions)..region.end).rev() { + self.raw.swap_lines(line, line - positions); + } + + for line in IndexRange(region.start..(region.start + positions)) { + self.raw[line].reset(&template); + } + } + } + + /// scroll_up moves lines at the bottom towards the top + /// + /// This is the performance-sensitive part of scrolling. + pub fn scroll_up(&mut self, region: &Range<index::Line>, positions: index::Line, template: &T) { + if region.start == Line(0) { + // Update display offset when not pinned to active area + if self.display_offset != 0 { + self.display_offset = + min(self.display_offset + *positions, self.len() - self.num_lines().0); + } + + self.increase_scroll_limit(*positions, template); + + // Rotate the entire line buffer. If there's a scrolling region + // active, the bottom lines are restored in the next step. + self.raw.rotate(-(*positions as isize)); + if let Some(ref mut selection) = self.selection { + selection.rotate(*positions as isize); + } + self.url_highlight = None; + + // // This next loop swaps "fixed" lines outside of a scroll region + // // back into place after the rotation. The work is done in buffer- + // // space rather than terminal-space to avoid redundant + // // transformations. + let fixed_lines = *self.num_lines() - *region.end; + + for i in 0..fixed_lines { + self.raw.swap(i, i + *positions); + } + + // Finally, reset recycled lines + // + // Recycled lines are just above the end of the scrolling region. + for i in 0..*positions { + self.raw[i + fixed_lines].reset(&template); + } + } else { + // Subregion rotation + for line in IndexRange(region.start..(region.end - positions)) { + self.raw.swap_lines(line, line + positions); + } + + // Clear reused lines + for line in IndexRange((region.end - positions)..region.end) { + self.raw[line].reset(&template); + } + } + } + + // Completely reset the grid state + pub fn reset(&mut self, template: &T) { + // Explicitly purge all lines from history + let shrinkage = self.raw.len() - self.lines.0; + self.raw.shrink_lines(shrinkage); + self.clear_history(); + + // Reset all visible lines + for row in 0..self.raw.len() { + self.raw[row].reset(template); + } + + self.display_offset = 0; + self.selection = None; + self.url_highlight = None; + } +} + +#[allow(clippy::len_without_is_empty)] +impl<T> Grid<T> { + #[inline] + pub fn num_lines(&self) -> index::Line { + self.lines + } + + pub fn display_iter(&self) -> DisplayIter<'_, T> { + DisplayIter::new(self) + } + + #[inline] + pub fn num_cols(&self) -> index::Column { + self.cols + } + + pub fn clear_history(&mut self) { + self.scroll_limit = 0; + } + + #[inline] + pub fn scroll_limit(&self) -> usize { + self.scroll_limit + } + + /// Total number of lines in the buffer, this includes scrollback + visible lines + #[inline] + pub fn len(&self) -> usize { + self.raw.len() + } + + #[inline] + pub fn history_size(&self) -> usize { + self.raw.len().saturating_sub(*self.lines) + } + + /// This is used only for initializing after loading ref-tests + pub fn initialize_all(&mut self, template: &T) + where + T: Copy, + { + let history_size = self.raw.len().saturating_sub(*self.lines); + self.raw.initialize(self.max_scroll_limit - history_size, Row::new(self.cols, template)); + } + + /// This is used only for truncating before saving ref-tests + pub fn truncate(&mut self) { + self.raw.truncate(); + } + + pub fn iter_from(&self, point: Point<usize>) -> GridIterator<'_, T> { + GridIterator { grid: self, cur: point } + } + + #[inline] + pub fn contains(&self, point: &Point) -> bool { + self.lines > point.line && self.cols > point.col + } + + #[inline] + pub fn display_offset(&self) -> usize { + self.display_offset + } +} + +pub struct GridIterator<'a, T> { + /// Immutable grid reference + grid: &'a Grid<T>, + + /// Current position of the iterator within the grid. + pub cur: Point<usize>, +} + +impl<'a, T> Iterator for GridIterator<'a, T> { + type Item = &'a T; + + fn next(&mut self) -> Option<Self::Item> { + let last_col = self.grid.num_cols() - Column(1); + match self.cur { + Point { line, col } if line == 0 && col == last_col => None, + Point { col, .. } if (col == last_col) => { + self.cur.line -= 1; + self.cur.col = Column(0); + Some(&self.grid[self.cur.line][self.cur.col]) + }, + _ => { + self.cur.col += Column(1); + Some(&self.grid[self.cur.line][self.cur.col]) + }, + } + } +} + +impl<'a, T> BidirectionalIterator for GridIterator<'a, T> { + fn prev(&mut self) -> Option<Self::Item> { + let num_cols = self.grid.num_cols(); + + match self.cur { + Point { line, col: Column(0) } if line == self.grid.len() - 1 => None, + Point { col: Column(0), .. } => { + self.cur.line += 1; + self.cur.col = num_cols - Column(1); + Some(&self.grid[self.cur.line][self.cur.col]) + }, + _ => { + self.cur.col -= Column(1); + Some(&self.grid[self.cur.line][self.cur.col]) + }, + } + } +} + +/// Index active region by line +impl<T> Index<index::Line> for Grid<T> { + type Output = Row<T>; + + #[inline] + fn index(&self, index: index::Line) -> &Row<T> { + &self.raw[index] + } +} + +/// Index with buffer offset +impl<T> Index<usize> for Grid<T> { + type Output = Row<T>; + + #[inline] + fn index(&self, index: usize) -> &Row<T> { + &self.raw[index] + } +} + +impl<T> IndexMut<index::Line> for Grid<T> { + #[inline] + fn index_mut(&mut self, index: index::Line) -> &mut Row<T> { + &mut self.raw[index] + } +} + +impl<T> IndexMut<usize> for Grid<T> { + #[inline] + fn index_mut(&mut self, index: usize) -> &mut Row<T> { + &mut self.raw[index] + } +} + +impl<'point, T> Index<&'point Point> for Grid<T> { + type Output = T; + + #[inline] + fn index<'a>(&'a self, point: &Point) -> &'a T { + &self[point.line][point.col] + } +} + +impl<'point, T> IndexMut<&'point Point> for Grid<T> { + #[inline] + fn index_mut<'a, 'b>(&'a mut self, point: &'b Point) -> &'a mut T { + &mut self[point.line][point.col] + } +} + +// ------------------------------------------------------------------------------------------------- +// REGIONS +// ------------------------------------------------------------------------------------------------- + +/// A subset of lines in the grid +/// +/// May be constructed using Grid::region(..) +pub struct Region<'a, T> { + start: Line, + end: Line, + raw: &'a Storage<T>, +} + +/// A mutable subset of lines in the grid +/// +/// May be constructed using Grid::region_mut(..) +pub struct RegionMut<'a, T> { + start: Line, + end: Line, + raw: &'a mut Storage<T>, +} + +impl<'a, T> RegionMut<'a, T> { + /// Call the provided function for every item in this region + pub fn each<F: Fn(&mut T)>(self, func: F) { + for row in self { + for item in row { + func(item) + } + } + } +} + +pub trait IndexRegion<I, T> { + /// Get an immutable region of Self + fn region(&self, _: I) -> Region<'_, T>; + + /// Get a mutable region of Self + fn region_mut(&mut self, _: I) -> RegionMut<'_, T>; +} + +impl<T> IndexRegion<Range<Line>, T> for Grid<T> { + fn region(&self, index: Range<Line>) -> Region<'_, T> { + assert!(index.start < self.num_lines()); + assert!(index.end <= self.num_lines()); + assert!(index.start <= index.end); + Region { start: index.start, end: index.end, raw: &self.raw } + } + + fn region_mut(&mut self, index: Range<Line>) -> RegionMut<'_, T> { + assert!(index.start < self.num_lines()); + assert!(index.end <= self.num_lines()); + assert!(index.start <= index.end); + RegionMut { start: index.start, end: index.end, raw: &mut self.raw } + } +} + +impl<T> IndexRegion<RangeTo<Line>, T> for Grid<T> { + fn region(&self, index: RangeTo<Line>) -> Region<'_, T> { + assert!(index.end <= self.num_lines()); + Region { start: Line(0), end: index.end, raw: &self.raw } + } + + fn region_mut(&mut self, index: RangeTo<Line>) -> RegionMut<'_, T> { + assert!(index.end <= self.num_lines()); + RegionMut { start: Line(0), end: index.end, raw: &mut self.raw } + } +} + +impl<T> IndexRegion<RangeFrom<Line>, T> for Grid<T> { + fn region(&self, index: RangeFrom<Line>) -> Region<'_, T> { + assert!(index.start < self.num_lines()); + Region { start: index.start, end: self.num_lines(), raw: &self.raw } + } + + fn region_mut(&mut self, index: RangeFrom<Line>) -> RegionMut<'_, T> { + assert!(index.start < self.num_lines()); + RegionMut { start: index.start, end: self.num_lines(), raw: &mut self.raw } + } +} + +impl<T> IndexRegion<RangeFull, T> for Grid<T> { + fn region(&self, _: RangeFull) -> Region<'_, T> { + Region { start: Line(0), end: self.num_lines(), raw: &self.raw } + } + + fn region_mut(&mut self, _: RangeFull) -> RegionMut<'_, T> { + RegionMut { start: Line(0), end: self.num_lines(), raw: &mut self.raw } + } +} + +pub struct RegionIter<'a, T> { + end: Line, + cur: Line, + raw: &'a Storage<T>, +} + +pub struct RegionIterMut<'a, T> { + end: Line, + cur: Line, + raw: &'a mut Storage<T>, +} + +impl<'a, T> IntoIterator for Region<'a, T> { + type IntoIter = RegionIter<'a, T>; + type Item = &'a Row<T>; + + fn into_iter(self) -> Self::IntoIter { + RegionIter { end: self.end, cur: self.start, raw: self.raw } + } +} + +impl<'a, T> IntoIterator for RegionMut<'a, T> { + type IntoIter = RegionIterMut<'a, T>; + type Item = &'a mut Row<T>; + + fn into_iter(self) -> Self::IntoIter { + RegionIterMut { end: self.end, cur: self.start, raw: self.raw } + } +} + +impl<'a, T> Iterator for RegionIter<'a, T> { + type Item = &'a Row<T>; + + fn next(&mut self) -> Option<Self::Item> { + if self.cur < self.end { + let index = self.cur; + self.cur += 1; + Some(&self.raw[index]) + } else { + None + } + } +} + +impl<'a, T> Iterator for RegionIterMut<'a, T> { + type Item = &'a mut Row<T>; + + fn next(&mut self) -> Option<Self::Item> { + if self.cur < self.end { + let index = self.cur; + self.cur += 1; + unsafe { Some(&mut *(&mut self.raw[index] as *mut _)) } + } else { + None + } + } +} + +// ------------------------------------------------------------------------------------------------- +// DISPLAY ITERATOR +// ------------------------------------------------------------------------------------------------- + +/// Iterates over the visible area accounting for buffer transform +pub struct DisplayIter<'a, T> { + grid: &'a Grid<T>, + offset: usize, + limit: usize, + col: Column, + line: Line, +} + +impl<'a, T: 'a> DisplayIter<'a, T> { + pub fn new(grid: &'a Grid<T>) -> DisplayIter<'a, T> { + let offset = grid.display_offset + *grid.num_lines() - 1; + let limit = grid.display_offset; + let col = Column(0); + let line = Line(0); + + DisplayIter { grid, offset, col, limit, line } + } + + pub fn offset(&self) -> usize { + self.offset + } + + pub fn column(&self) -> Column { + self.col + } + + pub fn line(&self) -> Line { + self.line + } +} + +impl<'a, T: Copy + 'a> Iterator for DisplayIter<'a, T> { + type Item = Indexed<T>; + + #[inline] + fn next(&mut self) -> Option<Self::Item> { + // Return None if we've reached the end. + if self.offset == self.limit && self.grid.num_cols() == self.col { + return None; + } + + // Get the next item. + let item = Some(Indexed { + inner: self.grid.raw[self.offset][self.col], + line: self.line, + column: self.col, + }); + + // Update line/col to point to next item + self.col += 1; + if self.col == self.grid.num_cols() && self.offset != self.limit { + self.offset -= 1; + + self.col = Column(0); + self.line = Line(*self.grid.lines - 1 - (self.offset - self.limit)); + } + + item + } +} diff --git a/alacritty_terminal/src/grid/row.rs b/alacritty_terminal/src/grid/row.rs new file mode 100644 index 00000000..88a23871 --- /dev/null +++ b/alacritty_terminal/src/grid/row.rs @@ -0,0 +1,264 @@ +// 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. + +//! Defines the Row type which makes up lines in the grid + +use std::cmp::{max, min}; +use std::ops::{Index, IndexMut}; +use std::ops::{Range, RangeFrom, RangeFull, RangeTo, RangeToInclusive}; +use std::slice; + +use crate::grid::GridCell; +use crate::index::Column; + +/// A row in the grid +#[derive(Default, Clone, Debug, Serialize, Deserialize)] +pub struct Row<T> { + inner: Vec<T>, + + /// occupied entries + /// + /// Semantically, this value can be understood as the **end** of an + /// Exclusive Range. Thus, + /// + /// - Zero means there are no occupied entries + /// - 1 means there is a value at index zero, but nowhere else + /// - `occ == inner.len` means every value is occupied + pub(crate) occ: usize, +} + +impl<T: PartialEq> PartialEq for Row<T> { + fn eq(&self, other: &Self) -> bool { + self.inner == other.inner + } +} + +impl<T: Copy> Row<T> { + pub fn new(columns: Column, template: &T) -> Row<T> { + Row { inner: vec![*template; *columns], occ: 0 } + } + + pub fn grow(&mut self, cols: Column, template: &T) { + if self.inner.len() >= cols.0 { + return; + } + + self.inner.append(&mut vec![*template; cols.0 - self.len()]); + } + + pub fn shrink(&mut self, cols: Column) -> Option<Vec<T>> + where + T: GridCell, + { + if self.inner.len() <= cols.0 { + return None; + } + + // Split off cells for a new row + let mut new_row = self.inner.split_off(cols.0); + let index = new_row.iter().rposition(|c| !c.is_empty()).map(|i| i + 1).unwrap_or(0); + new_row.truncate(index); + + self.occ = min(self.occ, *cols); + + if new_row.is_empty() { + None + } else { + Some(new_row) + } + } + + /// Resets contents to the contents of `other` + #[inline(never)] + pub fn reset(&mut self, other: &T) { + for item in &mut self.inner[..self.occ] { + *item = *other; + } + self.occ = 0; + } +} + +#[allow(clippy::len_without_is_empty)] +impl<T> Row<T> { + #[inline] + pub fn from_vec(vec: Vec<T>, occ: usize) -> Row<T> { + Row { inner: vec, occ } + } + + #[inline] + pub fn len(&self) -> usize { + self.inner.len() + } + + #[inline] + pub fn last(&self) -> Option<&T> { + self.inner.last() + } + + #[inline] + pub fn last_mut(&mut self) -> Option<&mut T> { + self.occ = self.inner.len(); + self.inner.last_mut() + } + + #[inline] + pub fn append(&mut self, vec: &mut Vec<T>) + where + T: GridCell, + { + self.inner.append(vec); + self.occ = self.inner.iter().rposition(|c| !c.is_empty()).map(|i| i + 1).unwrap_or(0); + } + + #[inline] + pub fn append_front(&mut self, mut vec: Vec<T>) { + self.occ += vec.len(); + vec.append(&mut self.inner); + self.inner = vec; + } + + #[inline] + pub fn is_empty(&self) -> bool + where + T: GridCell, + { + self.inner.iter().all(GridCell::is_empty) + } + + #[inline] + pub fn front_split_off(&mut self, at: usize) -> Vec<T> { + self.occ = self.occ.saturating_sub(at); + + let mut split = self.inner.split_off(at); + std::mem::swap(&mut split, &mut self.inner); + split + } +} + +impl<'a, T> IntoIterator for &'a mut Row<T> { + type IntoIter = slice::IterMut<'a, T>; + type Item = &'a mut T; + + #[inline] + fn into_iter(self) -> slice::IterMut<'a, T> { + self.occ = self.len(); + self.inner.iter_mut() + } +} + +impl<T> Index<Column> for Row<T> { + type Output = T; + + #[inline] + fn index(&self, index: Column) -> &T { + &self.inner[index.0] + } +} + +impl<T> IndexMut<Column> for Row<T> { + #[inline] + fn index_mut(&mut self, index: Column) -> &mut T { + self.occ = max(self.occ, *index + 1); + &mut self.inner[index.0] + } +} + +// ----------------------------------------------------------------------------- +// Index ranges of columns +// ----------------------------------------------------------------------------- + +impl<T> Index<Range<Column>> for Row<T> { + type Output = [T]; + + #[inline] + fn index(&self, index: Range<Column>) -> &[T] { + &self.inner[(index.start.0)..(index.end.0)] + } +} + +impl<T> IndexMut<Range<Column>> for Row<T> { + #[inline] + fn index_mut(&mut self, index: Range<Column>) -> &mut [T] { + self.occ = max(self.occ, *index.end); + &mut self.inner[(index.start.0)..(index.end.0)] + } +} + +impl<T> Index<RangeTo<Column>> for Row<T> { + type Output = [T]; + + #[inline] + fn index(&self, index: RangeTo<Column>) -> &[T] { + &self.inner[..(index.end.0)] + } +} + +impl<T> IndexMut<RangeTo<Column>> for Row<T> { + #[inline] + fn index_mut(&mut self, index: RangeTo<Column>) -> &mut [T] { + self.occ = max(self.occ, *index.end); + &mut self.inner[..(index.end.0)] + } +} + +impl<T> Index<RangeFrom<Column>> for Row<T> { + type Output = [T]; + + #[inline] + fn index(&self, index: RangeFrom<Column>) -> &[T] { + &self.inner[(index.start.0)..] + } +} + +impl<T> IndexMut<RangeFrom<Column>> for Row<T> { + #[inline] + fn index_mut(&mut self, index: RangeFrom<Column>) -> &mut [T] { + self.occ = self.len(); + &mut self.inner[(index.start.0)..] + } +} + +impl<T> Index<RangeFull> for Row<T> { + type Output = [T]; + + #[inline] + fn index(&self, _: RangeFull) -> &[T] { + &self.inner[..] + } +} + +impl<T> IndexMut<RangeFull> for Row<T> { + #[inline] + fn index_mut(&mut self, _: RangeFull) -> &mut [T] { + self.occ = self.len(); + &mut self.inner[..] + } +} + +impl<T> Index<RangeToInclusive<Column>> for Row<T> { + type Output = [T]; + + #[inline] + fn index(&self, index: RangeToInclusive<Column>) -> &[T] { + &self.inner[..=(index.end.0)] + } +} + +impl<T> IndexMut<RangeToInclusive<Column>> for Row<T> { + #[inline] + fn index_mut(&mut self, index: RangeToInclusive<Column>) -> &mut [T] { + self.occ = max(self.occ, *index.end); + &mut self.inner[..=(index.end.0)] + } +} diff --git a/alacritty_terminal/src/grid/storage.rs b/alacritty_terminal/src/grid/storage.rs new file mode 100644 index 00000000..32260426 --- /dev/null +++ b/alacritty_terminal/src/grid/storage.rs @@ -0,0 +1,922 @@ +/// Wrapper around Vec which supports fast indexing and rotation +/// +/// The rotation implemented by grid::Storage is a simple integer addition. +/// Compare with standard library rotation which requires rearranging items in +/// memory. +/// +/// As a consequence, the indexing operators need to be reimplemented for this +/// type to account for the 0th element not always being at the start of the +/// allocation. +/// +/// Because certain Vec operations are no longer valid on this type, no Deref +/// implementation is provided. Anything from Vec that should be exposed must be +/// done so manually. +use std::ops::{Index, IndexMut}; + +use static_assertions::assert_eq_size; + +use super::Row; +use crate::grid::GridCell; +use crate::index::{Column, Line}; + +/// Maximum number of invisible lines before buffer is resized +const TRUNCATE_STEP: usize = 100; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Storage<T> { + inner: Vec<Row<T>>, + zero: usize, + visible_lines: Line, + + /// Total number of lines currently active in the terminal (scrollback + visible) + /// + /// Shrinking this length allows reducing the number of lines in the scrollback buffer without + /// having to truncate the raw `inner` buffer. + /// As long as `len` is bigger than `inner`, it is also possible to grow the scrollback buffer + /// without any additional insertions. + #[serde(default)] + len: usize, +} + +impl<T: PartialEq> ::std::cmp::PartialEq for Storage<T> { + fn eq(&self, other: &Self) -> bool { + // Make sure length is equal + if self.inner.len() != other.inner.len() { + return false; + } + + // Check which vec has the bigger zero + let (ref bigger, ref smaller) = + if self.zero >= other.zero { (self, other) } else { (other, self) }; + + // Calculate the actual zero offset + let len = self.inner.len(); + let bigger_zero = bigger.zero % len; + let smaller_zero = smaller.zero % len; + + // Compare the slices in chunks + // Chunks: + // - Bigger zero to the end + // - Remaining lines in smaller zero vec + // - Beginning of smaller zero vec + // + // Example: + // Bigger Zero (6): + // 4 5 6 | 7 8 9 | 0 1 2 3 + // C2 C2 C2 | C3 C3 C3 | C1 C1 C1 C1 + // Smaller Zero (3): + // 7 8 9 | 0 1 2 3 | 4 5 6 + // C3 C3 C3 | C1 C1 C1 C1 | C2 C2 C2 + bigger.inner[bigger_zero..] + == smaller.inner[smaller_zero..smaller_zero + (len - bigger_zero)] + && bigger.inner[..bigger_zero - smaller_zero] + == smaller.inner[smaller_zero + (len - bigger_zero)..] + && bigger.inner[bigger_zero - smaller_zero..bigger_zero] + == smaller.inner[..smaller_zero] + } +} + +impl<T> Storage<T> { + #[inline] + pub fn with_capacity(lines: Line, template: Row<T>) -> Storage<T> + where + T: Clone, + { + // Initialize visible lines, the scrollback buffer is initialized dynamically + let inner = vec![template; lines.0]; + + Storage { inner, zero: 0, visible_lines: lines - 1, len: lines.0 } + } + + /// Update the size of the scrollback history + pub fn update_history(&mut self, history_size: usize, template_row: Row<T>) + where + T: Clone, + { + let current_history = self.len - (self.visible_lines.0 + 1); + if history_size > current_history { + self.grow_lines(history_size - current_history, template_row); + } else if history_size < current_history { + self.shrink_lines(current_history - history_size); + } + } + + /// Increase the number of lines in the buffer + pub fn grow_visible_lines(&mut self, next: Line, template_row: Row<T>) + where + T: Clone, + { + // Number of lines the buffer needs to grow + let growage = (next - (self.visible_lines + 1)).0; + self.grow_lines(growage, template_row); + + // Update visible lines + self.visible_lines = next - 1; + } + + /// Grow the number of lines in the buffer, filling new lines with the template + fn grow_lines(&mut self, growage: usize, template_row: Row<T>) + where + T: Clone, + { + // Only grow if there are not enough lines still hidden + let mut new_growage = 0; + if growage > (self.inner.len() - self.len) { + // Lines to grow additionally to invisible lines + new_growage = growage - (self.inner.len() - self.len); + + // Split off the beginning of the raw inner buffer + let mut start_buffer = self.inner.split_off(self.zero); + + // Insert new template rows at the end of the raw inner buffer + let mut new_lines = vec![template_row; new_growage]; + self.inner.append(&mut new_lines); + + // Add the start to the raw inner buffer again + self.inner.append(&mut start_buffer); + } + + // Update raw buffer length and zero offset + self.zero = (self.zero + new_growage) % self.inner.len(); + self.len += growage; + } + + /// Decrease the number of lines in the buffer + pub fn shrink_visible_lines(&mut self, next: Line) { + // Shrink the size without removing any lines + let shrinkage = (self.visible_lines - (next - 1)).0; + self.shrink_lines(shrinkage); + + // Update visible lines + self.visible_lines = next - 1; + } + + // Shrink the number of lines in the buffer + pub fn shrink_lines(&mut self, shrinkage: usize) { + self.len -= shrinkage; + + // Free memory + if self.inner.len() > self.len() + TRUNCATE_STEP { + self.truncate(); + } + } + + /// Truncate the invisible elements from the raw buffer + pub fn truncate(&mut self) { + self.inner.rotate_left(self.zero); + self.inner.truncate(self.len); + + self.zero = 0; + } + + /// Dynamically grow the storage buffer at runtime + pub fn initialize(&mut self, num_rows: usize, template_row: Row<T>) + where + T: Clone, + { + let mut new = vec![template_row; num_rows]; + + let mut split = self.inner.split_off(self.zero); + self.inner.append(&mut new); + self.inner.append(&mut split); + + self.zero += num_rows; + self.len += num_rows; + } + + #[inline] + pub fn len(&self) -> usize { + self.len + } + + #[inline] + /// Compute actual index in underlying storage given the requested index. + fn compute_index(&self, requested: usize) -> usize { + debug_assert!(requested < self.len); + let zeroed = requested + self.zero; + + // This part is critical for performance, + // so an if/else is used here instead of a moludo operation + if zeroed >= self.inner.len() { + zeroed - self.inner.len() + } else { + zeroed + } + } + + pub fn swap_lines(&mut self, a: Line, b: Line) { + let offset = self.inner.len() + self.zero + *self.visible_lines; + let a = (offset - *a) % self.inner.len(); + let b = (offset - *b) % self.inner.len(); + self.inner.swap(a, b); + } + + /// Swap implementation for Row<T>. + /// + /// Exploits the known size of Row<T> to produce a slightly more efficient + /// swap than going through slice::swap. + /// + /// The default implementation from swap generates 8 movups and 4 movaps + /// instructions. This implementation achieves the swap in only 8 movups + /// instructions. + pub fn swap(&mut self, a: usize, b: usize) { + assert_eq_size!(Row<T>, [usize; 4]); + + let a = self.compute_index(a); + let b = self.compute_index(b); + + unsafe { + // Cast to a qword array to opt out of copy restrictions and avoid + // drop hazards. Byte array is no good here since for whatever + // reason LLVM won't optimized it. + let a_ptr = self.inner.as_mut_ptr().add(a) as *mut usize; + let b_ptr = self.inner.as_mut_ptr().add(b) as *mut usize; + + // Copy 1 qword at a time + // + // The optimizer unrolls this loop and vectorizes it. + let mut tmp: usize; + for i in 0..4 { + tmp = *a_ptr.offset(i); + *a_ptr.offset(i) = *b_ptr.offset(i); + *b_ptr.offset(i) = tmp; + } + } + } + + #[inline] + pub fn rotate(&mut self, count: isize) { + debug_assert!(count.abs() as usize <= self.inner.len()); + + let len = self.inner.len(); + self.zero = (self.zero as isize + count + len as isize) as usize % len; + } + + // Fast path + #[inline] + pub fn rotate_up(&mut self, count: usize) { + self.zero = (self.zero + count) % self.inner.len(); + } + + #[inline] + pub fn insert(&mut self, index: usize, row: Row<T>, max_lines: usize) { + let index = self.compute_index(index); + self.inner.insert(index, row); + + if index < self.zero { + self.zero += 1; + } + + if self.len < max_lines { + self.len += 1; + } + } + + #[inline] + pub fn remove(&mut self, index: usize) -> Row<T> { + let index = self.compute_index(index); + if index < self.zero { + self.zero -= 1; + } + self.len -= 1; + + self.inner.remove(index) + } + + /// Shrink columns of hidden buffered lines. + /// + /// XXX This suggests that Storage is a leaky abstraction. Ultimately, this + /// is needed because of the grow/shrink lines functionality. + #[inline] + pub fn shrink_hidden(&mut self, cols: Column) + where + T: GridCell + Copy, + { + let start = self.zero + self.len; + let end = self.zero + self.inner.len(); + for mut i in start..end { + if i >= self.inner.len() { + i -= self.inner.len(); + } + + self.inner[i].shrink(cols); + } + } + + /// Grow columns of hidden buffered lines. + /// + /// XXX This suggests that Storage is a leaky abstraction. Ultimately, this + /// is needed because of the grow/shrink lines functionality. + #[inline] + pub fn grow_hidden(&mut self, cols: Column, template: &T) + where + T: Copy + Clone, + { + let start = self.zero + self.len; + let end = self.zero + self.inner.len(); + for mut i in start..end { + if i >= self.inner.len() { + i -= self.inner.len(); + } + + self.inner[i].grow(cols, template); + } + } +} + +impl<T> Index<usize> for Storage<T> { + type Output = Row<T>; + + #[inline] + fn index(&self, index: usize) -> &Self::Output { + &self.inner[self.compute_index(index)] + } +} + +impl<T> IndexMut<usize> for Storage<T> { + #[inline] + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + let index = self.compute_index(index); // borrowck + &mut self.inner[index] + } +} + +impl<T> Index<Line> for Storage<T> { + type Output = Row<T>; + + #[inline] + fn index(&self, index: Line) -> &Self::Output { + let index = self.visible_lines - index; + &self[*index] + } +} + +impl<T> IndexMut<Line> for Storage<T> { + #[inline] + fn index_mut(&mut self, index: Line) -> &mut Self::Output { + let index = self.visible_lines - index; + &mut self[*index] + } +} + +/// Grow the buffer one line at the end of the buffer +/// +/// Before: +/// 0: 0 <- Zero +/// 1: 1 +/// 2: - +/// After: +/// 0: - +/// 1: 0 <- Zero +/// 2: 1 +/// 3: - +#[test] +fn grow_after_zero() { + // Setup storage area + let mut storage = Storage { + inner: vec![ + Row::new(Column(1), &'0'), + Row::new(Column(1), &'1'), + Row::new(Column(1), &'-'), + ], + zero: 0, + visible_lines: Line(2), + len: 3, + }; + + // Grow buffer + storage.grow_visible_lines(Line(4), Row::new(Column(1), &'-')); + + // Make sure the result is correct + let expected = Storage { + inner: vec![ + Row::new(Column(1), &'-'), + Row::new(Column(1), &'0'), + Row::new(Column(1), &'1'), + Row::new(Column(1), &'-'), + ], + zero: 1, + visible_lines: Line(0), + len: 4, + }; + assert_eq!(storage.inner, expected.inner); + assert_eq!(storage.zero, expected.zero); + assert_eq!(storage.len, expected.len); +} + +/// Grow the buffer one line at the start of the buffer +/// +/// Before: +/// 0: - +/// 1: 0 <- Zero +/// 2: 1 +/// After: +/// 0: - +/// 1: - +/// 2: 0 <- Zero +/// 3: 1 +#[test] +fn grow_before_zero() { + // Setup storage area + let mut storage = Storage { + inner: vec![ + Row::new(Column(1), &'-'), + Row::new(Column(1), &'0'), + Row::new(Column(1), &'1'), + ], + zero: 1, + visible_lines: Line(2), + len: 3, + }; + + // Grow buffer + storage.grow_visible_lines(Line(4), Row::new(Column(1), &'-')); + + // Make sure the result is correct + let expected = Storage { + inner: vec![ + Row::new(Column(1), &'-'), + Row::new(Column(1), &'-'), + Row::new(Column(1), &'0'), + Row::new(Column(1), &'1'), + ], + zero: 2, + visible_lines: Line(0), + len: 4, + }; + assert_eq!(storage.inner, expected.inner); + assert_eq!(storage.zero, expected.zero); + assert_eq!(storage.len, expected.len); +} + +/// Shrink the buffer one line at the start of the buffer +/// +/// Before: +/// 0: 2 +/// 1: 0 <- Zero +/// 2: 1 +/// After: +/// 0: 2 <- Hidden +/// 0: 0 <- Zero +/// 1: 1 +#[test] +fn shrink_before_zero() { + // Setup storage area + let mut storage = Storage { + inner: vec![ + Row::new(Column(1), &'2'), + Row::new(Column(1), &'0'), + Row::new(Column(1), &'1'), + ], + zero: 1, + visible_lines: Line(2), + len: 3, + }; + + // Shrink buffer + storage.shrink_visible_lines(Line(2)); + + // Make sure the result is correct + let expected = Storage { + inner: vec![ + Row::new(Column(1), &'2'), + Row::new(Column(1), &'0'), + Row::new(Column(1), &'1'), + ], + zero: 1, + visible_lines: Line(0), + len: 2, + }; + assert_eq!(storage.inner, expected.inner); + assert_eq!(storage.zero, expected.zero); + assert_eq!(storage.len, expected.len); +} + +/// Shrink the buffer one line at the end of the buffer +/// +/// Before: +/// 0: 0 <- Zero +/// 1: 1 +/// 2: 2 +/// After: +/// 0: 0 <- Zero +/// 1: 1 +/// 2: 2 <- Hidden +#[test] +fn shrink_after_zero() { + // Setup storage area + let mut storage = Storage { + inner: vec![ + Row::new(Column(1), &'0'), + Row::new(Column(1), &'1'), + Row::new(Column(1), &'2'), + ], + zero: 0, + visible_lines: Line(2), + len: 3, + }; + + // Shrink buffer + storage.shrink_visible_lines(Line(2)); + + // Make sure the result is correct + let expected = Storage { + inner: vec![ + Row::new(Column(1), &'0'), + Row::new(Column(1), &'1'), + Row::new(Column(1), &'2'), + ], + zero: 0, + visible_lines: Line(0), + len: 2, + }; + assert_eq!(storage.inner, expected.inner); + assert_eq!(storage.zero, expected.zero); + assert_eq!(storage.len, expected.len); +} + +/// Shrink the buffer at the start and end of the buffer +/// +/// Before: +/// 0: 4 +/// 1: 5 +/// 2: 0 <- Zero +/// 3: 1 +/// 4: 2 +/// 5: 3 +/// After: +/// 0: 4 <- Hidden +/// 1: 5 <- Hidden +/// 2: 0 <- Zero +/// 3: 1 +/// 4: 2 <- Hidden +/// 5: 3 <- Hidden +#[test] +fn shrink_before_and_after_zero() { + // Setup storage area + let mut storage = Storage { + inner: vec![ + Row::new(Column(1), &'4'), + Row::new(Column(1), &'5'), + Row::new(Column(1), &'0'), + Row::new(Column(1), &'1'), + Row::new(Column(1), &'2'), + Row::new(Column(1), &'3'), + ], + zero: 2, + visible_lines: Line(5), + len: 6, + }; + + // Shrink buffer + storage.shrink_visible_lines(Line(2)); + + // Make sure the result is correct + let expected = Storage { + inner: vec![ + Row::new(Column(1), &'4'), + Row::new(Column(1), &'5'), + Row::new(Column(1), &'0'), + Row::new(Column(1), &'1'), + Row::new(Column(1), &'2'), + Row::new(Column(1), &'3'), + ], + zero: 2, + visible_lines: Line(0), + len: 2, + }; + assert_eq!(storage.inner, expected.inner); + assert_eq!(storage.zero, expected.zero); + assert_eq!(storage.len, expected.len); +} + +/// Check that when truncating all hidden lines are removed from the raw buffer +/// +/// Before: +/// 0: 4 <- Hidden +/// 1: 5 <- Hidden +/// 2: 0 <- Zero +/// 3: 1 +/// 4: 2 <- Hidden +/// 5: 3 <- Hidden +/// After: +/// 0: 0 <- Zero +/// 1: 1 +#[test] +fn truncate_invisible_lines() { + // Setup storage area + let mut storage = Storage { + inner: vec![ + Row::new(Column(1), &'4'), + Row::new(Column(1), &'5'), + Row::new(Column(1), &'0'), + Row::new(Column(1), &'1'), + Row::new(Column(1), &'2'), + Row::new(Column(1), &'3'), + ], + zero: 2, + visible_lines: Line(1), + len: 2, + }; + + // Truncate buffer + storage.truncate(); + + // Make sure the result is correct + let expected = Storage { + inner: vec![Row::new(Column(1), &'0'), Row::new(Column(1), &'1')], + zero: 0, + visible_lines: Line(1), + len: 2, + }; + assert_eq!(storage.visible_lines, expected.visible_lines); + assert_eq!(storage.inner, expected.inner); + assert_eq!(storage.zero, expected.zero); + assert_eq!(storage.len, expected.len); +} + +/// Truncate buffer only at the beginning +/// +/// Before: +/// 0: 1 +/// 1: 2 <- Hidden +/// 2: 0 <- Zero +/// After: +/// 0: 1 +/// 0: 0 <- Zero +#[test] +fn truncate_invisible_lines_beginning() { + // Setup storage area + let mut storage = Storage { + inner: vec![ + Row::new(Column(1), &'1'), + Row::new(Column(1), &'2'), + Row::new(Column(1), &'0'), + ], + zero: 2, + visible_lines: Line(1), + len: 2, + }; + + // Truncate buffer + storage.truncate(); + + // Make sure the result is correct + let expected = Storage { + inner: vec![Row::new(Column(1), &'0'), Row::new(Column(1), &'1')], + zero: 0, + visible_lines: Line(1), + len: 2, + }; + assert_eq!(storage.visible_lines, expected.visible_lines); + assert_eq!(storage.inner, expected.inner); + assert_eq!(storage.zero, expected.zero); + assert_eq!(storage.len, expected.len); +} + +/// First shrink the buffer and then grow it again +/// +/// Before: +/// 0: 4 +/// 1: 5 +/// 2: 0 <- Zero +/// 3: 1 +/// 4: 2 +/// 5: 3 +/// After Shrinking: +/// 0: 4 <- Hidden +/// 1: 5 <- Hidden +/// 2: 0 <- Zero +/// 3: 1 +/// 4: 2 +/// 5: 3 <- Hidden +/// After Growing: +/// 0: 4 +/// 1: 5 +/// 2: - +/// 3: 0 <- Zero +/// 4: 1 +/// 5: 2 +/// 6: 3 +#[test] +fn shrink_then_grow() { + // Setup storage area + let mut storage = Storage { + inner: vec![ + Row::new(Column(1), &'4'), + Row::new(Column(1), &'5'), + Row::new(Column(1), &'0'), + Row::new(Column(1), &'1'), + Row::new(Column(1), &'2'), + Row::new(Column(1), &'3'), + ], + zero: 2, + visible_lines: Line(0), + len: 6, + }; + + // Shrink buffer + storage.shrink_lines(3); + + // Make sure the result after shrinking is correct + let shrinking_expected = Storage { + inner: vec![ + Row::new(Column(1), &'4'), + Row::new(Column(1), &'5'), + Row::new(Column(1), &'0'), + Row::new(Column(1), &'1'), + Row::new(Column(1), &'2'), + Row::new(Column(1), &'3'), + ], + zero: 2, + visible_lines: Line(0), + len: 3, + }; + assert_eq!(storage.inner, shrinking_expected.inner); + assert_eq!(storage.zero, shrinking_expected.zero); + assert_eq!(storage.len, shrinking_expected.len); + + // Grow buffer + storage.grow_lines(4, Row::new(Column(1), &'-')); + + // Make sure the result after shrinking is correct + let growing_expected = Storage { + inner: vec![ + Row::new(Column(1), &'4'), + Row::new(Column(1), &'5'), + Row::new(Column(1), &'-'), + Row::new(Column(1), &'0'), + Row::new(Column(1), &'1'), + Row::new(Column(1), &'2'), + Row::new(Column(1), &'3'), + ], + zero: 3, + visible_lines: Line(0), + len: 7, + }; + assert_eq!(storage.inner, growing_expected.inner); + assert_eq!(storage.zero, growing_expected.zero); + assert_eq!(storage.len, growing_expected.len); +} + +#[test] +fn initialize() { + // Setup storage area + let mut storage = Storage { + inner: vec![ + Row::new(Column(1), &'4'), + Row::new(Column(1), &'5'), + Row::new(Column(1), &'0'), + Row::new(Column(1), &'1'), + Row::new(Column(1), &'2'), + Row::new(Column(1), &'3'), + ], + zero: 2, + visible_lines: Line(0), + len: 6, + }; + + // Initialize additional lines + storage.initialize(3, Row::new(Column(1), &'-')); + + // Make sure the lines are present and at the right location + let shrinking_expected = Storage { + inner: vec![ + Row::new(Column(1), &'4'), + Row::new(Column(1), &'5'), + Row::new(Column(1), &'-'), + Row::new(Column(1), &'-'), + Row::new(Column(1), &'-'), + Row::new(Column(1), &'0'), + Row::new(Column(1), &'1'), + Row::new(Column(1), &'2'), + Row::new(Column(1), &'3'), + ], + zero: 5, + visible_lines: Line(0), + len: 9, + }; + assert_eq!(storage.inner, shrinking_expected.inner); + assert_eq!(storage.zero, shrinking_expected.zero); + assert_eq!(storage.len, shrinking_expected.len); +} + +#[test] +fn insert() { + // Setup storage area + let mut storage = Storage { + inner: vec![ + Row::new(Column(1), &'4'), + Row::new(Column(1), &'5'), + Row::new(Column(1), &'0'), + Row::new(Column(1), &'1'), + Row::new(Column(1), &'2'), + Row::new(Column(1), &'3'), + ], + zero: 2, + visible_lines: Line(0), + len: 6, + }; + + // Initialize additional lines + storage.insert(2, Row::new(Column(1), &'-'), 100); + + // Make sure the lines are present and at the right location + let shrinking_expected = Storage { + inner: vec![ + Row::new(Column(1), &'4'), + Row::new(Column(1), &'5'), + Row::new(Column(1), &'0'), + Row::new(Column(1), &'1'), + Row::new(Column(1), &'-'), + Row::new(Column(1), &'2'), + Row::new(Column(1), &'3'), + ], + zero: 2, + visible_lines: Line(0), + len: 7, + }; + assert_eq!(storage.inner, shrinking_expected.inner); + assert_eq!(storage.zero, shrinking_expected.zero); + assert_eq!(storage.len, shrinking_expected.len); +} + +#[test] +fn insert_truncate_max() { + // Setup storage area + let mut storage = Storage { + inner: vec![ + Row::new(Column(1), &'4'), + Row::new(Column(1), &'5'), + Row::new(Column(1), &'0'), + Row::new(Column(1), &'1'), + Row::new(Column(1), &'2'), + Row::new(Column(1), &'3'), + ], + zero: 2, + visible_lines: Line(0), + len: 6, + }; + + // Initialize additional lines + storage.insert(2, Row::new(Column(1), &'-'), 6); + + // Make sure the lines are present and at the right location + let shrinking_expected = Storage { + inner: vec![ + Row::new(Column(1), &'4'), + Row::new(Column(1), &'5'), + Row::new(Column(1), &'0'), + Row::new(Column(1), &'1'), + Row::new(Column(1), &'-'), + Row::new(Column(1), &'2'), + Row::new(Column(1), &'3'), + ], + zero: 2, + visible_lines: Line(0), + len: 6, + }; + assert_eq!(storage.inner, shrinking_expected.inner); + assert_eq!(storage.zero, shrinking_expected.zero); + assert_eq!(storage.len, shrinking_expected.len); +} + +#[test] +fn insert_at_zero() { + // Setup storage area + let mut storage = Storage { + inner: vec![ + Row::new(Column(1), &'4'), + Row::new(Column(1), &'5'), + Row::new(Column(1), &'0'), + Row::new(Column(1), &'1'), + Row::new(Column(1), &'2'), + Row::new(Column(1), &'3'), + ], + zero: 2, + visible_lines: Line(0), + len: 6, + }; + + // Initialize additional lines + storage.insert(0, Row::new(Column(1), &'-'), 6); + + // Make sure the lines are present and at the right location + let shrinking_expected = Storage { + inner: vec![ + Row::new(Column(1), &'4'), + Row::new(Column(1), &'5'), + Row::new(Column(1), &'-'), + Row::new(Column(1), &'0'), + Row::new(Column(1), &'1'), + Row::new(Column(1), &'2'), + Row::new(Column(1), &'3'), + ], + zero: 2, + visible_lines: Line(0), + len: 6, + }; + assert_eq!(storage.inner, shrinking_expected.inner); + assert_eq!(storage.zero, shrinking_expected.zero); + assert_eq!(storage.len, shrinking_expected.len); +} diff --git a/alacritty_terminal/src/grid/tests.rs b/alacritty_terminal/src/grid/tests.rs new file mode 100644 index 00000000..fc41fdc6 --- /dev/null +++ b/alacritty_terminal/src/grid/tests.rs @@ -0,0 +1,292 @@ +// 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. + +//! Tests for the Gird + +use super::{BidirectionalIterator, Grid}; +use crate::grid::GridCell; +use crate::index::{Column, Line, Point}; +use crate::term::cell::{Cell, Flags}; + +impl GridCell for usize { + fn is_empty(&self) -> bool { + false + } + + fn is_wrap(&self) -> bool { + false + } + + fn set_wrap(&mut self, _wrap: bool) {} +} + +// Scroll up moves lines upwards +#[test] +fn scroll_up() { + let mut grid = Grid::new(Line(10), Column(1), 0, 0); + for i in 0..10 { + grid[Line(i)][Column(0)] = i; + } + + grid.scroll_up(&(Line(0)..Line(10)), Line(2), &0); + + assert_eq!(grid[Line(0)][Column(0)], 2); + assert_eq!(grid[Line(0)].occ, 1); + assert_eq!(grid[Line(1)][Column(0)], 3); + assert_eq!(grid[Line(1)].occ, 1); + assert_eq!(grid[Line(2)][Column(0)], 4); + assert_eq!(grid[Line(2)].occ, 1); + assert_eq!(grid[Line(3)][Column(0)], 5); + assert_eq!(grid[Line(3)].occ, 1); + assert_eq!(grid[Line(4)][Column(0)], 6); + assert_eq!(grid[Line(4)].occ, 1); + assert_eq!(grid[Line(5)][Column(0)], 7); + assert_eq!(grid[Line(5)].occ, 1); + assert_eq!(grid[Line(6)][Column(0)], 8); + assert_eq!(grid[Line(6)].occ, 1); + assert_eq!(grid[Line(7)][Column(0)], 9); + assert_eq!(grid[Line(7)].occ, 1); + assert_eq!(grid[Line(8)][Column(0)], 0); // was 0 + assert_eq!(grid[Line(8)].occ, 0); + assert_eq!(grid[Line(9)][Column(0)], 0); // was 1 + assert_eq!(grid[Line(9)].occ, 0); +} + +// Scroll down moves lines downwards +#[test] +fn scroll_down() { + let mut grid = Grid::new(Line(10), Column(1), 0, 0); + for i in 0..10 { + grid[Line(i)][Column(0)] = i; + } + + grid.scroll_down(&(Line(0)..Line(10)), Line(2), &0); + + assert_eq!(grid[Line(0)][Column(0)], 0); // was 8 + assert_eq!(grid[Line(0)].occ, 0); + assert_eq!(grid[Line(1)][Column(0)], 0); // was 9 + assert_eq!(grid[Line(1)].occ, 0); + assert_eq!(grid[Line(2)][Column(0)], 0); + assert_eq!(grid[Line(2)].occ, 1); + assert_eq!(grid[Line(3)][Column(0)], 1); + assert_eq!(grid[Line(3)].occ, 1); + assert_eq!(grid[Line(4)][Column(0)], 2); + assert_eq!(grid[Line(4)].occ, 1); + assert_eq!(grid[Line(5)][Column(0)], 3); + assert_eq!(grid[Line(5)].occ, 1); + assert_eq!(grid[Line(6)][Column(0)], 4); + assert_eq!(grid[Line(6)].occ, 1); + assert_eq!(grid[Line(7)][Column(0)], 5); + assert_eq!(grid[Line(7)].occ, 1); + assert_eq!(grid[Line(8)][Column(0)], 6); + assert_eq!(grid[Line(8)].occ, 1); + assert_eq!(grid[Line(9)][Column(0)], 7); + assert_eq!(grid[Line(9)].occ, 1); +} + +// Test that GridIterator works +#[test] +fn test_iter() { + let mut grid = Grid::new(Line(5), Column(5), 0, 0); + for i in 0..5 { + for j in 0..5 { + grid[Line(i)][Column(j)] = i * 5 + j; + } + } + + let mut iter = grid.iter_from(Point { line: 4, col: Column(0) }); + + assert_eq!(None, iter.prev()); + assert_eq!(Some(&1), iter.next()); + assert_eq!(Column(1), iter.cur.col); + assert_eq!(4, iter.cur.line); + + assert_eq!(Some(&2), iter.next()); + assert_eq!(Some(&3), iter.next()); + assert_eq!(Some(&4), iter.next()); + + // test linewrapping + assert_eq!(Some(&5), iter.next()); + assert_eq!(Column(0), iter.cur.col); + assert_eq!(3, iter.cur.line); + + assert_eq!(Some(&4), iter.prev()); + assert_eq!(Column(4), iter.cur.col); + assert_eq!(4, iter.cur.line); + + // test that iter ends at end of grid + let mut final_iter = grid.iter_from(Point { line: 0, col: Column(4) }); + assert_eq!(None, final_iter.next()); + assert_eq!(Some(&23), final_iter.prev()); +} + +#[test] +fn shrink_reflow() { + let mut grid = Grid::new(Line(1), Column(5), 2, cell('x')); + grid[Line(0)][Column(0)] = cell('1'); + grid[Line(0)][Column(1)] = cell('2'); + grid[Line(0)][Column(2)] = cell('3'); + grid[Line(0)][Column(3)] = cell('4'); + grid[Line(0)][Column(4)] = cell('5'); + + grid.resize(Line(1), Column(2), &mut Point::new(Line(0), Column(0)), &Cell::default()); + + assert_eq!(grid.len(), 3); + + assert_eq!(grid[2].len(), 2); + assert_eq!(grid[2][Column(0)], cell('1')); + assert_eq!(grid[2][Column(1)], wrap_cell('2')); + + assert_eq!(grid[1].len(), 2); + assert_eq!(grid[1][Column(0)], cell('3')); + assert_eq!(grid[1][Column(1)], wrap_cell('4')); + + assert_eq!(grid[0].len(), 2); + assert_eq!(grid[0][Column(0)], cell('5')); + assert_eq!(grid[0][Column(1)], Cell::default()); +} + +#[test] +fn shrink_reflow_twice() { + let mut grid = Grid::new(Line(1), Column(5), 2, cell('x')); + grid[Line(0)][Column(0)] = cell('1'); + grid[Line(0)][Column(1)] = cell('2'); + grid[Line(0)][Column(2)] = cell('3'); + grid[Line(0)][Column(3)] = cell('4'); + grid[Line(0)][Column(4)] = cell('5'); + + grid.resize(Line(1), Column(4), &mut Point::new(Line(0), Column(0)), &Cell::default()); + grid.resize(Line(1), Column(2), &mut Point::new(Line(0), Column(0)), &Cell::default()); + + assert_eq!(grid.len(), 3); + + assert_eq!(grid[2].len(), 2); + assert_eq!(grid[2][Column(0)], cell('1')); + assert_eq!(grid[2][Column(1)], wrap_cell('2')); + + assert_eq!(grid[1].len(), 2); + assert_eq!(grid[1][Column(0)], cell('3')); + assert_eq!(grid[1][Column(1)], wrap_cell('4')); + + assert_eq!(grid[0].len(), 2); + assert_eq!(grid[0][Column(0)], cell('5')); + assert_eq!(grid[0][Column(1)], Cell::default()); +} + +#[test] +fn shrink_reflow_empty_cell_inside_line() { + let mut grid = Grid::new(Line(1), Column(5), 3, cell('x')); + grid[Line(0)][Column(0)] = cell('1'); + grid[Line(0)][Column(1)] = Cell::default(); + grid[Line(0)][Column(2)] = cell('3'); + grid[Line(0)][Column(3)] = cell('4'); + grid[Line(0)][Column(4)] = Cell::default(); + + grid.resize(Line(1), Column(2), &mut Point::new(Line(0), Column(0)), &Cell::default()); + + assert_eq!(grid.len(), 2); + + assert_eq!(grid[1].len(), 2); + assert_eq!(grid[1][Column(0)], cell('1')); + assert_eq!(grid[1][Column(1)], wrap_cell(' ')); + + assert_eq!(grid[0].len(), 2); + assert_eq!(grid[0][Column(0)], cell('3')); + assert_eq!(grid[0][Column(1)], cell('4')); + + grid.resize(Line(1), Column(1), &mut Point::new(Line(0), Column(0)), &Cell::default()); + + assert_eq!(grid.len(), 4); + + assert_eq!(grid[3].len(), 1); + assert_eq!(grid[3][Column(0)], wrap_cell('1')); + + assert_eq!(grid[2].len(), 1); + assert_eq!(grid[2][Column(0)], wrap_cell(' ')); + + assert_eq!(grid[1].len(), 1); + assert_eq!(grid[1][Column(0)], wrap_cell('3')); + + assert_eq!(grid[0].len(), 1); + assert_eq!(grid[0][Column(0)], cell('4')); +} + +#[test] +fn grow_reflow() { + let mut grid = Grid::new(Line(2), Column(2), 0, cell('x')); + grid[Line(0)][Column(0)] = cell('1'); + grid[Line(0)][Column(1)] = wrap_cell('2'); + grid[Line(1)][Column(0)] = cell('3'); + grid[Line(1)][Column(1)] = Cell::default(); + + grid.resize(Line(2), Column(3), &mut Point::new(Line(0), Column(0)), &Cell::default()); + + assert_eq!(grid.len(), 2); + + assert_eq!(grid[1].len(), 3); + assert_eq!(grid[1][Column(0)], cell('1')); + assert_eq!(grid[1][Column(1)], cell('2')); + assert_eq!(grid[1][Column(2)], cell('3')); + + // Make sure rest of grid is empty + assert_eq!(grid[0].len(), 3); + assert_eq!(grid[0][Column(0)], Cell::default()); + assert_eq!(grid[0][Column(1)], Cell::default()); + assert_eq!(grid[0][Column(2)], Cell::default()); +} + +#[test] +fn grow_reflow_multiline() { + let mut grid = Grid::new(Line(3), Column(2), 0, cell('x')); + grid[Line(0)][Column(0)] = cell('1'); + grid[Line(0)][Column(1)] = wrap_cell('2'); + grid[Line(1)][Column(0)] = cell('3'); + grid[Line(1)][Column(1)] = wrap_cell('4'); + grid[Line(2)][Column(0)] = cell('5'); + grid[Line(2)][Column(1)] = cell('6'); + + grid.resize(Line(3), Column(6), &mut Point::new(Line(0), Column(0)), &Cell::default()); + + assert_eq!(grid.len(), 3); + + assert_eq!(grid[2].len(), 6); + assert_eq!(grid[2][Column(0)], cell('1')); + assert_eq!(grid[2][Column(1)], cell('2')); + assert_eq!(grid[2][Column(2)], cell('3')); + assert_eq!(grid[2][Column(3)], cell('4')); + assert_eq!(grid[2][Column(4)], cell('5')); + assert_eq!(grid[2][Column(5)], cell('6')); + + // Make sure rest of grid is empty + // https://github.com/rust-lang/rust-clippy/issues/3788 + #[allow(clippy::needless_range_loop)] + for r in 0..2 { + assert_eq!(grid[r].len(), 6); + for c in 0..6 { + assert_eq!(grid[r][Column(c)], Cell::default()); + } + } +} + +fn cell(c: char) -> Cell { + let mut cell = Cell::default(); + cell.c = c; + cell +} + +fn wrap_cell(c: char) -> Cell { + let mut cell = cell(c); + cell.flags.insert(Flags::WRAPLINE); + cell +} diff --git a/alacritty_terminal/src/index.rs b/alacritty_terminal/src/index.rs new file mode 100644 index 00000000..f6ea4ad3 --- /dev/null +++ b/alacritty_terminal/src/index.rs @@ -0,0 +1,406 @@ +// 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. + +//! Line and Column newtypes for strongly typed tty/grid/terminal APIs + +/// Indexing types and implementations for Grid and Line +use std::cmp::{Ord, Ordering}; +use std::fmt; +use std::ops::{self, Add, AddAssign, Deref, Range, RangeInclusive, Sub, SubAssign}; + +use crate::term::RenderableCell; + +/// The side of a cell +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum Side { + Left, + Right, +} + +/// Index in the grid using row, column notation +#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Serialize, Deserialize, PartialOrd)] +pub struct Point<L = Line> { + pub line: L, + pub col: Column, +} + +impl<L> Point<L> { + pub fn new(line: L, col: Column) -> Point<L> { + Point { line, col } + } +} + +impl Ord for Point { + fn cmp(&self, other: &Point) -> Ordering { + use std::cmp::Ordering::*; + match (self.line.cmp(&other.line), self.col.cmp(&other.col)) { + (Equal, Equal) => Equal, + (Equal, ord) | (ord, Equal) => ord, + (Less, _) => Less, + (Greater, _) => Greater, + } + } +} + +impl From<Point<usize>> for Point<isize> { + fn from(point: Point<usize>) -> Self { + Point::new(point.line as isize, point.col) + } +} + +impl From<Point<isize>> for Point<usize> { + fn from(point: Point<isize>) -> Self { + Point::new(point.line as usize, point.col) + } +} + +impl From<Point> for Point<usize> { + fn from(point: Point) -> Self { + Point::new(point.line.0, point.col) + } +} + +impl From<&RenderableCell> for Point<Line> { + fn from(cell: &RenderableCell) -> Self { + Point::new(cell.line, cell.column) + } +} + +/// A line +/// +/// Newtype to avoid passing values incorrectly +#[derive(Debug, Copy, Clone, Eq, PartialEq, Default, Ord, PartialOrd, Serialize, Deserialize)] +pub struct Line(pub usize); + +impl fmt::Display for Line { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +/// A column +/// +/// Newtype to avoid passing values incorrectly +#[derive(Debug, Copy, Clone, Eq, PartialEq, Default, Ord, PartialOrd, Serialize, Deserialize)] +pub struct Column(pub usize); + +impl fmt::Display for Column { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +/// A linear index +/// +/// Newtype to avoid passing values incorrectly +#[derive(Debug, Copy, Clone, Eq, PartialEq, Default, Ord, PartialOrd, Serialize, Deserialize)] +pub struct Linear(pub usize); + +impl Linear { + pub fn new(columns: Column, column: Column, line: Line) -> Self { + Linear(line.0 * columns.0 + column.0) + } + + pub fn from_point(columns: Column, point: Point<usize>) -> Self { + Linear(point.line * columns.0 + point.col.0) + } +} + +impl fmt::Display for Linear { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Linear({})", self.0) + } +} + +// Copyright 2015 The Rust Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. +// +// implements binary operators "&T op U", "T op &U", "&T op &U" +// based on "T op U" where T and U are expected to be `Copy`able +macro_rules! forward_ref_binop { + (impl $imp:ident, $method:ident for $t:ty, $u:ty) => { + impl<'a> $imp<$u> for &'a $t { + type Output = <$t as $imp<$u>>::Output; + + #[inline] + fn $method(self, other: $u) -> <$t as $imp<$u>>::Output { + $imp::$method(*self, other) + } + } + + impl<'a> $imp<&'a $u> for $t { + type Output = <$t as $imp<$u>>::Output; + + #[inline] + fn $method(self, other: &'a $u) -> <$t as $imp<$u>>::Output { + $imp::$method(self, *other) + } + } + + impl<'a, 'b> $imp<&'a $u> for &'b $t { + type Output = <$t as $imp<$u>>::Output; + + #[inline] + fn $method(self, other: &'a $u) -> <$t as $imp<$u>>::Output { + $imp::$method(*self, *other) + } + } + }; +} + +/// Macro for deriving deref +macro_rules! deref { + ($ty:ty, $target:ty) => { + impl Deref for $ty { + type Target = $target; + + #[inline] + fn deref(&self) -> &$target { + &self.0 + } + } + }; +} + +macro_rules! add { + ($ty:ty, $construct:expr) => { + impl ops::Add<$ty> for $ty { + type Output = $ty; + + #[inline] + fn add(self, rhs: $ty) -> $ty { + $construct(self.0 + rhs.0) + } + } + }; +} + +macro_rules! sub { + ($ty:ty, $construct:expr) => { + impl ops::Sub<$ty> for $ty { + type Output = $ty; + + #[inline] + fn sub(self, rhs: $ty) -> $ty { + $construct(self.0 - rhs.0) + } + } + + impl<'a> ops::Sub<$ty> for &'a $ty { + type Output = $ty; + + #[inline] + fn sub(self, rhs: $ty) -> $ty { + $construct(self.0 - rhs.0) + } + } + + impl<'a> ops::Sub<&'a $ty> for $ty { + type Output = $ty; + + #[inline] + fn sub(self, rhs: &'a $ty) -> $ty { + $construct(self.0 - rhs.0) + } + } + + impl<'a, 'b> ops::Sub<&'a $ty> for &'b $ty { + type Output = $ty; + + #[inline] + fn sub(self, rhs: &'a $ty) -> $ty { + $construct(self.0 - rhs.0) + } + } + }; +} + +/// This exists because we can't implement Iterator on Range +/// and the existing impl needs the unstable Step trait +/// This should be removed and replaced with a Step impl +/// in the ops macro when `step_by` is stabilized +pub struct IndexRange<T>(pub Range<T>); + +impl<T> From<Range<T>> for IndexRange<T> { + fn from(from: Range<T>) -> Self { + IndexRange(from) + } +} + +// can be removed if range_contains is stabilized +pub trait Contains { + type Content; + fn contains_(&self, item: Self::Content) -> bool; +} + +impl<T: PartialOrd<T>> Contains for Range<T> { + type Content = T; + + fn contains_(&self, item: Self::Content) -> bool { + (self.start <= item) && (item < self.end) + } +} + +impl<T: PartialOrd<T>> Contains for RangeInclusive<T> { + type Content = T; + + fn contains_(&self, item: Self::Content) -> bool { + (self.start() <= &item) && (&item <= self.end()) + } +} + +macro_rules! ops { + ($ty:ty, $construct:expr) => { + add!($ty, $construct); + sub!($ty, $construct); + deref!($ty, usize); + forward_ref_binop!(impl Add, add for $ty, $ty); + + impl $ty { + #[inline] + fn steps_between(start: $ty, end: $ty, by: $ty) -> Option<usize> { + if by == $construct(0) { return None; } + if start < end { + // Note: We assume $t <= usize here + let diff = (end - start).0; + let by = by.0; + if diff % by > 0 { + Some(diff / by + 1) + } else { + Some(diff / by) + } + } else { + Some(0) + } + } + + #[inline] + fn steps_between_by_one(start: $ty, end: $ty) -> Option<usize> { + Self::steps_between(start, end, $construct(1)) + } + } + + impl Iterator for IndexRange<$ty> { + type Item = $ty; + #[inline] + fn next(&mut self) -> Option<$ty> { + if self.0.start < self.0.end { + let old = self.0.start; + self.0.start = old + 1; + Some(old) + } else { + None + } + } + #[inline] + fn size_hint(&self) -> (usize, Option<usize>) { + match Self::Item::steps_between_by_one(self.0.start, self.0.end) { + Some(hint) => (hint, Some(hint)), + None => (0, None) + } + } + } + + impl DoubleEndedIterator for IndexRange<$ty> { + #[inline] + fn next_back(&mut self) -> Option<$ty> { + if self.0.start < self.0.end { + let new = self.0.end - 1; + self.0.end = new; + Some(new) + } else { + None + } + } + } + impl AddAssign<$ty> for $ty { + #[inline] + fn add_assign(&mut self, rhs: $ty) { + self.0 += rhs.0 + } + } + + impl SubAssign<$ty> for $ty { + #[inline] + fn sub_assign(&mut self, rhs: $ty) { + self.0 -= rhs.0 + } + } + + impl AddAssign<usize> for $ty { + #[inline] + fn add_assign(&mut self, rhs: usize) { + self.0 += rhs + } + } + + impl SubAssign<usize> for $ty { + #[inline] + fn sub_assign(&mut self, rhs: usize) { + self.0 -= rhs + } + } + + impl From<usize> for $ty { + #[inline] + fn from(val: usize) -> $ty { + $construct(val) + } + } + + impl Add<usize> for $ty { + type Output = $ty; + + #[inline] + fn add(self, rhs: usize) -> $ty { + $construct(self.0 + rhs) + } + } + + impl Sub<usize> for $ty { + type Output = $ty; + + #[inline] + fn sub(self, rhs: usize) -> $ty { + $construct(self.0 - rhs) + } + } + } +} + +ops!(Line, Line); +ops!(Column, Column); +ops!(Linear, Linear); + +#[cfg(test)] +mod tests { + use super::{Column, Line, Point}; + + #[test] + fn location_ordering() { + assert!(Point::new(Line(0), Column(0)) == Point::new(Line(0), Column(0))); + assert!(Point::new(Line(1), Column(0)) > Point::new(Line(0), Column(0))); + assert!(Point::new(Line(0), Column(1)) > Point::new(Line(0), Column(0))); + assert!(Point::new(Line(1), Column(1)) > Point::new(Line(0), Column(0))); + assert!(Point::new(Line(1), Column(1)) > Point::new(Line(0), Column(1))); + assert!(Point::new(Line(1), Column(1)) > Point::new(Line(1), Column(0))); + } +} diff --git a/alacritty_terminal/src/input.rs b/alacritty_terminal/src/input.rs new file mode 100644 index 00000000..fc79b398 --- /dev/null +++ b/alacritty_terminal/src/input.rs @@ -0,0 +1,1300 @@ +// 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. +// +//! Handle input from glutin +//! +//! Certain key combinations should send some escape sequence back to the pty. +//! In order to figure that out, state about which modifier keys are pressed +//! needs to be tracked. Additionally, we need a bit of a state machine to +//! determine what to do when a non-modifier key is pressed. +use std::borrow::Cow; +use std::mem; +use std::ops::RangeInclusive; +use std::time::Instant; + +use copypasta::{Buffer as ClipboardBuffer, Clipboard, Load}; +use glutin::{ + ElementState, KeyboardInput, ModifiersState, MouseButton, MouseCursor, MouseScrollDelta, + TouchPhase, +}; +use unicode_width::UnicodeWidthStr; + +use crate::ansi::{ClearMode, Handler}; +use crate::config::{self, Key}; +use crate::event::{ClickState, Mouse}; +use crate::grid::Scroll; +use crate::index::{Column, Line, Linear, Point, Side}; +use crate::message_bar::{self, Message}; +use crate::term::mode::TermMode; +use crate::term::{Search, SizeInfo, Term}; +use crate::url::Url; +use crate::util::fmt::Red; +use crate::util::start_daemon; + +pub const FONT_SIZE_STEP: f32 = 0.5; + +/// Processes input from glutin. +/// +/// An escape sequence may be emitted in case specific keys or key combinations +/// are activated. +/// +/// TODO also need terminal state when processing input +pub struct Processor<'a, A: 'a> { + pub key_bindings: &'a [KeyBinding], + pub mouse_bindings: &'a [MouseBinding], + pub mouse_config: &'a config::Mouse, + pub scrolling_config: &'a config::Scrolling, + pub ctx: A, + pub save_to_clipboard: bool, + pub alt_send_esc: bool, +} + +pub trait ActionContext { + fn write_to_pty<B: Into<Cow<'static, [u8]>>>(&mut self, _: B); + fn size_info(&self) -> SizeInfo; + fn copy_selection(&self, _: ClipboardBuffer); + fn clear_selection(&mut self); + fn update_selection(&mut self, point: Point, side: Side); + fn simple_selection(&mut self, point: Point, side: Side); + fn semantic_selection(&mut self, point: Point); + fn line_selection(&mut self, point: Point); + fn selection_is_empty(&self) -> bool; + fn mouse_mut(&mut self) -> &mut Mouse; + fn mouse(&self) -> &Mouse; + fn mouse_coords(&self) -> Option<Point>; + fn received_count(&mut self) -> &mut usize; + fn suppress_chars(&mut self) -> &mut bool; + fn last_modifiers(&mut self) -> &mut ModifiersState; + fn scroll(&mut self, scroll: Scroll); + fn hide_window(&mut self); + fn terminal(&self) -> &Term; + fn terminal_mut(&mut self) -> &mut Term; + fn spawn_new_instance(&mut self); + fn toggle_fullscreen(&mut self); + #[cfg(target_os = "macos")] + fn toggle_simple_fullscreen(&mut self); +} + +/// Describes a state and action to take in that state +/// +/// This is the shared component of `MouseBinding` and `KeyBinding` +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Binding<T> { + /// Modifier keys required to activate binding + pub mods: ModifiersState, + + /// String to send to pty if mods and mode match + pub action: Action, + + /// Terminal mode required to activate binding + pub mode: TermMode, + + /// excluded terminal modes where the binding won't be activated + pub notmode: TermMode, + + /// This property is used as part of the trigger detection code. + /// + /// For example, this might be a key like "G", or a mouse button. + pub trigger: T, +} + +/// Bindings that are triggered by a keyboard key +pub type KeyBinding = Binding<Key>; + +/// Bindings that are triggered by a mouse button +pub type MouseBinding = Binding<MouseButton>; + +impl Default for KeyBinding { + fn default() -> KeyBinding { + KeyBinding { + mods: Default::default(), + action: Action::Esc(String::new()), + mode: TermMode::NONE, + notmode: TermMode::NONE, + trigger: Key::A, + } + } +} + +impl Default for MouseBinding { + fn default() -> MouseBinding { + MouseBinding { + mods: Default::default(), + action: Action::Esc(String::new()), + mode: TermMode::NONE, + notmode: TermMode::NONE, + trigger: MouseButton::Left, + } + } +} + +impl<T: Eq> Binding<T> { + #[inline] + fn is_triggered_by( + &self, + mode: TermMode, + mods: ModifiersState, + input: &T, + relaxed: bool, + ) -> bool { + // Check input first since bindings are stored in one big list. This is + // the most likely item to fail so prioritizing it here allows more + // checks to be short circuited. + self.trigger == *input + && self.mode_matches(mode) + && self.not_mode_matches(mode) + && self.mods_match(mods, relaxed) + } + + #[inline] + pub fn triggers_match(&self, binding: &Binding<T>) -> bool { + self.trigger == binding.trigger + && self.mode == binding.mode + && self.notmode == binding.notmode + && self.mods == binding.mods + } +} + +impl<T> Binding<T> { + /// Execute the action associate with this binding + #[inline] + fn execute<A: ActionContext>(&self, ctx: &mut A, mouse_mode: bool) { + self.action.execute(ctx, mouse_mode) + } + + #[inline] + fn mode_matches(&self, mode: TermMode) -> bool { + self.mode.is_empty() || mode.intersects(self.mode) + } + + #[inline] + fn not_mode_matches(&self, mode: TermMode) -> bool { + self.notmode.is_empty() || !mode.intersects(self.notmode) + } + + /// Check that two mods descriptions for equivalence + #[inline] + fn mods_match(&self, mods: ModifiersState, relaxed: bool) -> bool { + if relaxed { + self.mods.relaxed_eq(mods) + } else { + self.mods == mods + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Action { + /// Write an escape sequence + Esc(String), + + /// Paste contents of system clipboard + Paste, + + // Store current selection into clipboard + Copy, + + /// Paste contents of selection buffer + PasteSelection, + + /// Increase font size + IncreaseFontSize, + + /// Decrease font size + DecreaseFontSize, + + /// Reset font size to the config value + ResetFontSize, + + /// Scroll exactly one page up + ScrollPageUp, + + /// Scroll exactly one page down + ScrollPageDown, + + /// Scroll one line up + ScrollLineUp, + + /// Scroll one line down + ScrollLineDown, + + /// Scroll all the way to the top + ScrollToTop, + + /// Scroll all the way to the bottom + ScrollToBottom, + + /// Clear the display buffer(s) to remove history + ClearHistory, + + /// Run given command + Command(String, Vec<String>), + + /// Hides the Alacritty window + Hide, + + /// Quits Alacritty. + Quit, + + /// Clears warning and error notices. + ClearLogNotice, + + /// Spawn a new instance of Alacritty. + SpawnNewInstance, + + /// Toggle fullscreen. + ToggleFullscreen, + + /// Toggle simple fullscreen on macos. + #[cfg(target_os = "macos")] + ToggleSimpleFullscreen, + + /// No action. + None, +} + +impl Default for Action { + fn default() -> Action { + Action::None + } +} + +impl Action { + #[inline] + fn execute<A: ActionContext>(&self, ctx: &mut A, mouse_mode: bool) { + match *self { + Action::Esc(ref s) => { + ctx.scroll(Scroll::Bottom); + ctx.write_to_pty(s.clone().into_bytes()) + }, + Action::Copy => { + ctx.copy_selection(ClipboardBuffer::Primary); + }, + Action::Paste => { + Clipboard::new() + .and_then(|clipboard| clipboard.load_primary()) + .map(|contents| self.paste(ctx, &contents)) + .unwrap_or_else(|err| { + error!("Error loading data from clipboard: {}", Red(err)); + }); + }, + Action::PasteSelection => { + // Only paste if mouse events are not captured by an application + if !mouse_mode { + Clipboard::new() + .and_then(|clipboard| clipboard.load_selection()) + .map(|contents| self.paste(ctx, &contents)) + .unwrap_or_else(|err| { + error!("Error loading data from clipboard: {}", Red(err)); + }); + } + }, + Action::Command(ref program, ref args) => { + trace!("Running command {} with args {:?}", program, args); + + match start_daemon(program, args) { + Ok(_) => { + debug!("Spawned new proc"); + }, + Err(err) => { + warn!("Couldn't run command {}", err); + }, + } + }, + Action::ToggleFullscreen => { + ctx.toggle_fullscreen(); + }, + #[cfg(target_os = "macos")] + Action::ToggleSimpleFullscreen => { + ctx.toggle_simple_fullscreen(); + }, + Action::Hide => { + ctx.hide_window(); + }, + Action::Quit => { + ctx.terminal_mut().exit(); + }, + Action::IncreaseFontSize => { + ctx.terminal_mut().change_font_size(FONT_SIZE_STEP); + }, + Action::DecreaseFontSize => { + ctx.terminal_mut().change_font_size(-FONT_SIZE_STEP); + }, + Action::ResetFontSize => { + ctx.terminal_mut().reset_font_size(); + }, + Action::ScrollPageUp => { + ctx.scroll(Scroll::PageUp); + }, + Action::ScrollPageDown => { + ctx.scroll(Scroll::PageDown); + }, + Action::ScrollLineUp => { + ctx.scroll(Scroll::Lines(1)); + }, + Action::ScrollLineDown => { + ctx.scroll(Scroll::Lines(-1)); + }, + Action::ScrollToTop => { + ctx.scroll(Scroll::Top); + }, + Action::ScrollToBottom => { + ctx.scroll(Scroll::Bottom); + }, + Action::ClearHistory => { + ctx.terminal_mut().clear_screen(ClearMode::Saved); + }, + Action::ClearLogNotice => { + ctx.terminal_mut().message_buffer_mut().pop(); + }, + Action::SpawnNewInstance => { + ctx.spawn_new_instance(); + }, + Action::None => (), + } + } + + fn paste<A: ActionContext>(&self, ctx: &mut A, contents: &str) { + if ctx.terminal().mode().contains(TermMode::BRACKETED_PASTE) { + ctx.write_to_pty(&b"\x1b[200~"[..]); + ctx.write_to_pty(contents.replace("\x1b", "").into_bytes()); + ctx.write_to_pty(&b"\x1b[201~"[..]); + } else { + // In non-bracketed (ie: normal) mode, terminal applications cannot distinguish + // pasted data from keystrokes. + // In theory, we should construct the keystrokes needed to produce the data we are + // pasting... since that's neither practical nor sensible (and probably an impossible + // task to solve in a general way), we'll just replace line breaks (windows and unix + // style) with a singe carriage return (\r, which is what the Enter key produces). + ctx.write_to_pty(contents.replace("\r\n", "\r").replace("\n", "\r").into_bytes()); + } + } +} + +trait RelaxedEq<T: ?Sized = Self> { + fn relaxed_eq(&self, other: T) -> bool; +} + +impl RelaxedEq for ModifiersState { + // Make sure that modifiers in the config are always present, + // but ignore surplus modifiers. + fn relaxed_eq(&self, other: Self) -> bool { + (!self.logo || other.logo) + && (!self.alt || other.alt) + && (!self.ctrl || other.ctrl) + && (!self.shift || other.shift) + } +} + +impl From<&'static str> for Action { + fn from(s: &'static str) -> Action { + Action::Esc(s.into()) + } +} + +impl<'a, A: ActionContext + 'a> Processor<'a, A> { + #[inline] + pub fn mouse_moved(&mut self, x: usize, y: usize, modifiers: ModifiersState) { + self.ctx.mouse_mut().x = x; + self.ctx.mouse_mut().y = y; + + let size_info = self.ctx.size_info(); + let point = size_info.pixels_to_coords(x, y); + + let cell_side = self.get_mouse_side(); + let prev_side = mem::replace(&mut self.ctx.mouse_mut().cell_side, cell_side); + let prev_line = mem::replace(&mut self.ctx.mouse_mut().line, point.line); + let prev_col = mem::replace(&mut self.ctx.mouse_mut().column, point.col); + + let motion_mode = TermMode::MOUSE_MOTION | TermMode::MOUSE_DRAG; + let report_mode = TermMode::MOUSE_REPORT_CLICK | motion_mode; + + let mouse_moved = prev_line != self.ctx.mouse().line + || prev_col != self.ctx.mouse().column + || prev_side != cell_side; + + // Don't launch URLs if mouse has moved + if mouse_moved { + self.ctx.mouse_mut().block_url_launcher = true; + } + + // Only report motions when cell changed and mouse is not over the message bar + if self.message_at_point(Some(point)).is_some() || !mouse_moved { + return; + } + + // Underline URLs and change cursor on hover + self.update_url_highlight(point, modifiers); + + if self.ctx.mouse().left_button_state == ElementState::Pressed + && (modifiers.shift || !self.ctx.terminal().mode().intersects(report_mode)) + { + self.ctx.update_selection(Point { line: point.line, col: point.col }, cell_side); + } else if self.ctx.terminal().mode().intersects(motion_mode) + && size_info.contains_point(x, y, false) + { + if self.ctx.mouse().left_button_state == ElementState::Pressed { + self.mouse_report(32, ElementState::Pressed, modifiers); + } else if self.ctx.mouse().middle_button_state == ElementState::Pressed { + self.mouse_report(33, ElementState::Pressed, modifiers); + } else if self.ctx.mouse().right_button_state == ElementState::Pressed { + self.mouse_report(34, ElementState::Pressed, modifiers); + } else if self.ctx.terminal().mode().contains(TermMode::MOUSE_MOTION) { + self.mouse_report(35, ElementState::Pressed, modifiers); + } + } + } + + /// Underline URLs and change the mouse cursor when URL hover state changes. + fn update_url_highlight(&mut self, point: Point, modifiers: ModifiersState) { + let mouse_mode = + TermMode::MOUSE_MOTION | TermMode::MOUSE_DRAG | TermMode::MOUSE_REPORT_CLICK; + + // Only show URLs as launchable when all required modifiers are pressed + let url = if self.mouse_config.url.modifiers.relaxed_eq(modifiers) + && (!self.ctx.terminal().mode().intersects(mouse_mode) || modifiers.shift) + && self.mouse_config.url.launcher.is_some() + { + self.ctx.terminal().url_search(point.into()) + } else { + None + }; + + if let Some(Url { origin, text }) = url { + let cols = self.ctx.size_info().cols().0; + + // Calculate the URL's start position + let lines_before = (origin + cols - point.col.0 - 1) / cols; + let (start_col, start_line) = if lines_before > point.line.0 { + (0, 0) + } else { + let start_col = (cols + point.col.0 - origin % cols) % cols; + let start_line = point.line.0 - lines_before; + (start_col, start_line) + }; + let start = Point::new(start_line, Column(start_col)); + + // Calculate the URL's end position + let len = text.width(); + let end_col = (point.col.0 + len - origin) % cols - 1; + let end_line = point.line.0 + (point.col.0 + len - origin) / cols; + let end = Point::new(end_line, Column(end_col)); + + let start = Linear::from_point(Column(cols), start); + let end = Linear::from_point(Column(cols), end); + + self.ctx.terminal_mut().set_url_highlight(RangeInclusive::new(start, end)); + self.ctx.terminal_mut().set_mouse_cursor(MouseCursor::Hand); + self.ctx.terminal_mut().dirty = true; + } else { + self.ctx.terminal_mut().reset_url_highlight(); + } + } + + fn get_mouse_side(&self) -> Side { + let size_info = self.ctx.size_info(); + let x = self.ctx.mouse().x; + + let cell_x = x.saturating_sub(size_info.padding_x as usize) % size_info.cell_width as usize; + let half_cell_width = (size_info.cell_width / 2.0) as usize; + + let additional_padding = + (size_info.width - size_info.padding_x * 2.) % size_info.cell_width; + let end_of_grid = size_info.width - size_info.padding_x - additional_padding; + + if cell_x > half_cell_width + // Edge case when mouse leaves the window + || x as f32 >= end_of_grid + { + Side::Right + } else { + Side::Left + } + } + + pub fn normal_mouse_report(&mut self, button: u8) { + let (line, column) = (self.ctx.mouse().line, self.ctx.mouse().column); + + if line < Line(223) && column < Column(223) { + let msg = vec![ + b'\x1b', + b'[', + b'M', + 32 + button, + 32 + 1 + column.0 as u8, + 32 + 1 + line.0 as u8, + ]; + + self.ctx.write_to_pty(msg); + } + } + + pub fn sgr_mouse_report(&mut self, button: u8, state: ElementState) { + let (line, column) = (self.ctx.mouse().line, self.ctx.mouse().column); + let c = match state { + ElementState::Pressed => 'M', + ElementState::Released => 'm', + }; + + let msg = format!("\x1b[<{};{};{}{}", button, column + 1, line + 1, c); + self.ctx.write_to_pty(msg.into_bytes()); + } + + pub fn mouse_report(&mut self, button: u8, state: ElementState, modifiers: ModifiersState) { + // Calculate modifiers value + let mut mods = 0; + if modifiers.shift { + mods += 4; + } + if modifiers.alt { + mods += 8; + } + if modifiers.ctrl { + mods += 16; + } + + // Report mouse events + if self.ctx.terminal().mode().contains(TermMode::SGR_MOUSE) { + self.sgr_mouse_report(button + mods, state); + } else if let ElementState::Released = state { + self.normal_mouse_report(3 + mods); + } else { + self.normal_mouse_report(button + mods); + } + } + + pub fn on_mouse_double_click(&mut self, button: MouseButton, point: Option<Point>) { + if let (Some(point), true) = (point, button == MouseButton::Left) { + self.ctx.semantic_selection(point); + } + } + + pub fn on_mouse_triple_click(&mut self, button: MouseButton, point: Option<Point>) { + if let (Some(point), true) = (point, button == MouseButton::Left) { + self.ctx.line_selection(point); + } + } + + pub fn on_mouse_press( + &mut self, + button: MouseButton, + modifiers: ModifiersState, + point: Option<Point>, + ) { + let now = Instant::now(); + let elapsed = self.ctx.mouse().last_click_timestamp.elapsed(); + self.ctx.mouse_mut().last_click_timestamp = now; + + let button_changed = self.ctx.mouse().last_button != button; + + self.ctx.mouse_mut().click_state = match self.ctx.mouse().click_state { + ClickState::Click + if !button_changed && elapsed < self.mouse_config.double_click.threshold => + { + self.ctx.mouse_mut().block_url_launcher = true; + self.on_mouse_double_click(button, point); + ClickState::DoubleClick + } + ClickState::DoubleClick + if !button_changed && elapsed < self.mouse_config.triple_click.threshold => + { + self.ctx.mouse_mut().block_url_launcher = true; + self.on_mouse_triple_click(button, point); + ClickState::TripleClick + } + _ => { + // Don't launch URLs if this click cleared the selection + self.ctx.mouse_mut().block_url_launcher = !self.ctx.selection_is_empty(); + + self.ctx.clear_selection(); + + // Start new empty selection + let side = self.ctx.mouse().cell_side; + if let Some(point) = point { + self.ctx.simple_selection(point, side); + } + + let report_modes = + TermMode::MOUSE_REPORT_CLICK | TermMode::MOUSE_DRAG | TermMode::MOUSE_MOTION; + if !modifiers.shift && self.ctx.terminal().mode().intersects(report_modes) { + let code = match button { + MouseButton::Left => 0, + MouseButton::Middle => 1, + MouseButton::Right => 2, + // Can't properly report more than three buttons. + MouseButton::Other(_) => return, + }; + self.mouse_report(code, ElementState::Pressed, modifiers); + return; + } + + ClickState::Click + }, + }; + } + + pub fn on_mouse_release( + &mut self, + button: MouseButton, + modifiers: ModifiersState, + point: Option<Point>, + ) { + let report_modes = + TermMode::MOUSE_REPORT_CLICK | TermMode::MOUSE_DRAG | TermMode::MOUSE_MOTION; + if !modifiers.shift && self.ctx.terminal().mode().intersects(report_modes) { + let code = match button { + MouseButton::Left => 0, + MouseButton::Middle => 1, + MouseButton::Right => 2, + // Can't properly report more than three buttons. + MouseButton::Other(_) => return, + }; + self.mouse_report(code, ElementState::Released, modifiers); + return; + } else if let (Some(point), true) = (point, button == MouseButton::Left) { + self.launch_url(modifiers, point); + } + + self.copy_selection(); + } + + // Spawn URL launcher when clicking on URLs + fn launch_url(&self, modifiers: ModifiersState, point: Point) -> Option<()> { + if !self.mouse_config.url.modifiers.relaxed_eq(modifiers) + || self.ctx.mouse().block_url_launcher + { + return None; + } + + let text = self.ctx.terminal().url_search(point.into())?.text; + + let launcher = self.mouse_config.url.launcher.as_ref()?; + let mut args = launcher.args().to_vec(); + args.push(text); + + match start_daemon(launcher.program(), &args) { + Ok(_) => debug!("Launched {} with args {:?}", launcher.program(), args), + Err(_) => warn!("Unable to launch {} with args {:?}", launcher.program(), args), + } + + Some(()) + } + + pub fn on_mouse_wheel( + &mut self, + delta: MouseScrollDelta, + phase: TouchPhase, + modifiers: ModifiersState, + ) { + match delta { + MouseScrollDelta::LineDelta(_columns, lines) => { + let new_scroll_px = lines * self.ctx.size_info().cell_height; + self.scroll_terminal(modifiers, new_scroll_px as i32); + }, + MouseScrollDelta::PixelDelta(lpos) => { + match phase { + TouchPhase::Started => { + // Reset offset to zero + self.ctx.mouse_mut().scroll_px = 0; + }, + TouchPhase::Moved => { + self.scroll_terminal(modifiers, lpos.y as i32); + }, + _ => (), + } + }, + } + } + + fn scroll_terminal(&mut self, modifiers: ModifiersState, new_scroll_px: i32) { + let mouse_modes = + TermMode::MOUSE_REPORT_CLICK | TermMode::MOUSE_DRAG | TermMode::MOUSE_MOTION; + let height = self.ctx.size_info().cell_height as i32; + + // Make sure the new and deprecated setting are both allowed + let faux_multiplier = self + .mouse_config + .faux_scrollback_lines + .unwrap_or(self.scrolling_config.faux_multiplier as usize); + + if self.ctx.terminal().mode().intersects(mouse_modes) { + self.ctx.mouse_mut().scroll_px += new_scroll_px; + + let code = if new_scroll_px > 0 { 64 } else { 65 }; + let lines = (self.ctx.mouse().scroll_px / height).abs(); + + for _ in 0..lines { + self.mouse_report(code, ElementState::Pressed, modifiers); + } + } else if self.ctx.terminal().mode().contains(TermMode::ALT_SCREEN) + && faux_multiplier > 0 + && !modifiers.shift + { + self.ctx.mouse_mut().scroll_px += new_scroll_px * faux_multiplier as i32; + + let cmd = if new_scroll_px > 0 { b'A' } else { b'B' }; + let lines = (self.ctx.mouse().scroll_px / height).abs(); + + let mut content = Vec::with_capacity(lines as usize * 3); + for _ in 0..lines { + content.push(0x1b); + content.push(b'O'); + content.push(cmd); + } + self.ctx.write_to_pty(content); + } else { + let multiplier = i32::from(self.scrolling_config.multiplier); + self.ctx.mouse_mut().scroll_px += new_scroll_px * multiplier; + + let lines = self.ctx.mouse().scroll_px / height; + + self.ctx.scroll(Scroll::Lines(lines as isize)); + } + + self.ctx.mouse_mut().scroll_px %= height; + } + + pub fn on_focus_change(&mut self, is_focused: bool) { + if self.ctx.terminal().mode().contains(TermMode::FOCUS_IN_OUT) { + let chr = if is_focused { "I" } else { "O" }; + + let msg = format!("\x1b[{}", chr); + self.ctx.write_to_pty(msg.into_bytes()); + } + } + + pub fn mouse_input( + &mut self, + state: ElementState, + button: MouseButton, + modifiers: ModifiersState, + ) { + match button { + MouseButton::Left => self.ctx.mouse_mut().left_button_state = state, + MouseButton::Middle => self.ctx.mouse_mut().middle_button_state = state, + MouseButton::Right => self.ctx.mouse_mut().right_button_state = state, + _ => (), + } + + let point = self.ctx.mouse_coords(); + + // Skip normal mouse events if the message bar has been clicked + if let Some(message) = self.message_at_point(point) { + // Message should never be `Some` if point is `None` + debug_assert!(point.is_some()); + self.on_message_bar_click(state, point.unwrap(), message); + } else { + match state { + ElementState::Pressed => { + self.process_mouse_bindings(modifiers, button); + self.on_mouse_press(button, modifiers, point); + }, + ElementState::Released => self.on_mouse_release(button, modifiers, point), + } + } + + self.ctx.mouse_mut().last_button = button; + } + + /// Process key input + pub fn process_key(&mut self, input: KeyboardInput) { + match input.state { + ElementState::Pressed => { + *self.ctx.last_modifiers() = input.modifiers; + *self.ctx.received_count() = 0; + *self.ctx.suppress_chars() = false; + + if self.process_key_bindings(input) { + *self.ctx.suppress_chars() = true; + } + }, + ElementState::Released => *self.ctx.suppress_chars() = false, + } + } + + /// Process a received character + pub fn received_char(&mut self, c: char) { + if *self.ctx.suppress_chars() { + return; + } + + self.ctx.scroll(Scroll::Bottom); + self.ctx.clear_selection(); + + let utf8_len = c.len_utf8(); + let mut bytes = Vec::with_capacity(utf8_len); + unsafe { + bytes.set_len(utf8_len); + c.encode_utf8(&mut bytes[..]); + } + + if self.alt_send_esc + && *self.ctx.received_count() == 0 + && self.ctx.last_modifiers().alt + && utf8_len == 1 + { + bytes.insert(0, b'\x1b'); + } + + self.ctx.write_to_pty(bytes); + + *self.ctx.received_count() += 1; + } + + /// Attempts to find a binding and execute its action + /// + /// The provided mode, mods, and key must match what is allowed by a binding + /// for its action to be executed. + /// + /// Returns true if an action is executed. + fn process_key_bindings(&mut self, input: KeyboardInput) -> bool { + let mut has_binding = false; + for binding in self.key_bindings { + let is_triggered = match binding.trigger { + Key::Scancode(_) => binding.is_triggered_by( + *self.ctx.terminal().mode(), + input.modifiers, + &Key::Scancode(input.scancode), + false, + ), + _ => { + if let Some(key) = input.virtual_keycode { + let key = Key::from_glutin_input(key); + binding.is_triggered_by( + *self.ctx.terminal().mode(), + input.modifiers, + &key, + false, + ) + } else { + false + } + }, + }; + + if is_triggered { + // binding was triggered; run the action + binding.execute(&mut self.ctx, false); + has_binding = true; + } + } + + has_binding + } + + /// Attempts to find a binding and execute its action + /// + /// The provided mode, mods, and key must match what is allowed by a binding + /// for its action to be executed. + /// + /// Returns true if an action is executed. + fn process_mouse_bindings(&mut self, mods: ModifiersState, button: MouseButton) -> bool { + let mut has_binding = false; + for binding in self.mouse_bindings { + if binding.is_triggered_by(*self.ctx.terminal().mode(), mods, &button, true) { + // binding was triggered; run the action + let mouse_mode = !mods.shift + && self.ctx.terminal().mode().intersects( + TermMode::MOUSE_REPORT_CLICK + | TermMode::MOUSE_DRAG + | TermMode::MOUSE_MOTION, + ); + binding.execute(&mut self.ctx, mouse_mode); + has_binding = true; + } + } + + has_binding + } + + /// Return the message bar's message if there is some at the specified point + fn message_at_point(&mut self, point: Option<Point>) -> Option<Message> { + if let (Some(point), Some(message)) = + (point, self.ctx.terminal_mut().message_buffer_mut().message()) + { + let size = self.ctx.size_info(); + if point.line.0 >= size.lines().saturating_sub(message.text(&size).len()) { + return Some(message); + } + } + + None + } + + /// Handle clicks on the message bar. + fn on_message_bar_click(&mut self, button_state: ElementState, point: Point, message: Message) { + match button_state { + ElementState::Released => self.copy_selection(), + ElementState::Pressed => { + let size = self.ctx.size_info(); + if point.col + message_bar::CLOSE_BUTTON_TEXT.len() >= size.cols() + && point.line == size.lines() - message.text(&size).len() + { + self.ctx.terminal_mut().message_buffer_mut().pop(); + } + + self.ctx.clear_selection(); + }, + } + } + + /// Copy text selection. + fn copy_selection(&mut self) { + if self.save_to_clipboard { + self.ctx.copy_selection(ClipboardBuffer::Primary); + } + self.ctx.copy_selection(ClipboardBuffer::Selection); + } +} + +#[cfg(test)] +mod tests { + use std::borrow::Cow; + use std::time::Duration; + + use glutin::{ElementState, Event, ModifiersState, MouseButton, VirtualKeyCode, WindowEvent}; + + use crate::config::{self, ClickHandler, Config}; + use crate::event::{ClickState, Mouse, WindowChanges}; + use crate::grid::Scroll; + use crate::index::{Point, Side}; + use crate::message_bar::MessageBuffer; + use crate::selection::Selection; + use crate::term::{SizeInfo, Term, TermMode}; + + use super::{Action, Binding, Processor}; + use copypasta::Buffer as ClipboardBuffer; + + const KEY: VirtualKeyCode = VirtualKeyCode::Key0; + + #[derive(PartialEq)] + enum MultiClick { + DoubleClick, + TripleClick, + None, + } + + struct ActionContext<'a> { + pub terminal: &'a mut Term, + pub selection: &'a mut Option<Selection>, + pub size_info: &'a SizeInfo, + pub mouse: &'a mut Mouse, + pub last_action: MultiClick, + pub received_count: usize, + pub suppress_chars: bool, + pub last_modifiers: ModifiersState, + pub window_changes: &'a mut WindowChanges, + } + + impl<'a> super::ActionContext for ActionContext<'a> { + fn write_to_pty<B: Into<Cow<'static, [u8]>>>(&mut self, _val: B) {} + + fn update_selection(&mut self, _point: Point, _side: Side) {} + + fn simple_selection(&mut self, _point: Point, _side: Side) {} + + fn copy_selection(&self, _buffer: ClipboardBuffer) {} + + fn clear_selection(&mut self) {} + + fn hide_window(&mut self) {} + + fn spawn_new_instance(&mut self) {} + + fn toggle_fullscreen(&mut self) {} + + #[cfg(target_os = "macos")] + fn toggle_simple_fullscreen(&mut self) {} + + fn terminal(&self) -> &Term { + &self.terminal + } + + fn terminal_mut(&mut self) -> &mut Term { + &mut self.terminal + } + + fn size_info(&self) -> SizeInfo { + *self.size_info + } + + fn semantic_selection(&mut self, _point: Point) { + // set something that we can check for here + self.last_action = MultiClick::DoubleClick; + } + + fn line_selection(&mut self, _point: Point) { + self.last_action = MultiClick::TripleClick; + } + + fn selection_is_empty(&self) -> bool { + true + } + + fn scroll(&mut self, scroll: Scroll) { + self.terminal.scroll_display(scroll); + } + + fn mouse_coords(&self) -> Option<Point> { + self.terminal.pixels_to_coords(self.mouse.x as usize, self.mouse.y as usize) + } + + #[inline] + fn mouse_mut(&mut self) -> &mut Mouse { + self.mouse + } + + #[inline] + fn mouse(&self) -> &Mouse { + self.mouse + } + + fn received_count(&mut self) -> &mut usize { + &mut self.received_count + } + + fn suppress_chars(&mut self) -> &mut bool { + &mut self.suppress_chars + } + + fn last_modifiers(&mut self) -> &mut ModifiersState { + &mut self.last_modifiers + } + } + + macro_rules! test_clickstate { + { + name: $name:ident, + initial_state: $initial_state:expr, + initial_button: $initial_button:expr, + input: $input:expr, + end_state: $end_state:pat, + last_action: $last_action:expr + } => { + #[test] + fn $name() { + let config = Config::default(); + let size = SizeInfo { + width: 21.0, + height: 51.0, + cell_width: 3.0, + cell_height: 3.0, + padding_x: 0.0, + padding_y: 0.0, + dpr: 1.0, + }; + + let mut terminal = Term::new(&config, size, MessageBuffer::new()); + + let mut mouse = Mouse::default(); + mouse.click_state = $initial_state; + mouse.last_button = $initial_button; + + let mut selection = None; + + let context = ActionContext { + terminal: &mut terminal, + selection: &mut selection, + mouse: &mut mouse, + size_info: &size, + last_action: MultiClick::None, + received_count: 0, + suppress_chars: false, + last_modifiers: ModifiersState::default(), + window_changes: &mut WindowChanges::default(), + }; + + let mut processor = Processor { + ctx: context, + mouse_config: &config::Mouse { + double_click: ClickHandler { + threshold: Duration::from_millis(1000), + }, + triple_click: ClickHandler { + threshold: Duration::from_millis(1000), + }, + hide_when_typing: false, + faux_scrollback_lines: None, + url: Default::default(), + }, + scrolling_config: &config::Scrolling::default(), + key_bindings: &config.key_bindings()[..], + mouse_bindings: &config.mouse_bindings()[..], + save_to_clipboard: config.selection().save_to_clipboard, + alt_send_esc: config.alt_send_esc(), + }; + + if let Event::WindowEvent { event: WindowEvent::MouseInput { state, button, modifiers, .. }, .. } = $input { + processor.mouse_input(state, button, modifiers); + }; + + assert!(match processor.ctx.mouse.click_state { + $end_state => processor.ctx.last_action == $last_action, + _ => false + }); + } + } + } + + macro_rules! test_process_binding { + { + name: $name:ident, + binding: $binding:expr, + triggers: $triggers:expr, + mode: $mode:expr, + mods: $mods:expr + } => { + #[test] + fn $name() { + if $triggers { + assert!($binding.is_triggered_by($mode, $mods, &KEY, false)); + } else { + assert!(!$binding.is_triggered_by($mode, $mods, &KEY, false)); + } + } + } + } + + test_clickstate! { + name: single_click, + initial_state: ClickState::None, + initial_button: MouseButton::Other(0), + input: Event::WindowEvent { + event: WindowEvent::MouseInput { + state: ElementState::Pressed, + button: MouseButton::Left, + device_id: unsafe { ::std::mem::transmute_copy(&0) }, + modifiers: ModifiersState::default(), + }, + window_id: unsafe { ::std::mem::transmute_copy(&0) }, + }, + end_state: ClickState::Click, + last_action: MultiClick::None + } + + test_clickstate! { + name: double_click, + initial_state: ClickState::Click, + initial_button: MouseButton::Left, + input: Event::WindowEvent { + event: WindowEvent::MouseInput { + state: ElementState::Pressed, + button: MouseButton::Left, + device_id: unsafe { ::std::mem::transmute_copy(&0) }, + modifiers: ModifiersState::default(), + }, + window_id: unsafe { ::std::mem::transmute_copy(&0) }, + }, + end_state: ClickState::DoubleClick, + last_action: MultiClick::DoubleClick + } + + test_clickstate! { + name: triple_click, + initial_state: ClickState::DoubleClick, + initial_button: MouseButton::Left, + input: Event::WindowEvent { + event: WindowEvent::MouseInput { + state: ElementState::Pressed, + button: MouseButton::Left, + device_id: unsafe { ::std::mem::transmute_copy(&0) }, + modifiers: ModifiersState::default(), + }, + window_id: unsafe { ::std::mem::transmute_copy(&0) }, + }, + end_state: ClickState::TripleClick, + last_action: MultiClick::TripleClick + } + + test_clickstate! { + name: multi_click_separate_buttons, + initial_state: ClickState::DoubleClick, + initial_button: MouseButton::Left, + input: Event::WindowEvent { + event: WindowEvent::MouseInput { + state: ElementState::Pressed, + button: MouseButton::Right, + device_id: unsafe { ::std::mem::transmute_copy(&0) }, + modifiers: ModifiersState::default(), + }, + window_id: unsafe { ::std::mem::transmute_copy(&0) }, + }, + end_state: ClickState::Click, + last_action: MultiClick::None + } + + test_process_binding! { + name: process_binding_nomode_shiftmod_require_shift, + binding: Binding { trigger: KEY, mods: ModifiersState { shift: true, ctrl: false, alt: false, logo: false }, action: Action::from("\x1b[1;2D"), mode: TermMode::NONE, notmode: TermMode::NONE }, + triggers: true, + mode: TermMode::NONE, + mods: ModifiersState { shift: true, ctrl: false, alt: false, logo: false } + } + + test_process_binding! { + name: process_binding_nomode_nomod_require_shift, + binding: Binding { trigger: KEY, mods: ModifiersState { shift: true, ctrl: false, alt: false, logo: false }, action: Action::from("\x1b[1;2D"), mode: TermMode::NONE, notmode: TermMode::NONE }, + triggers: false, + mode: TermMode::NONE, + mods: ModifiersState { shift: false, ctrl: false, alt: false, logo: false } + } + + test_process_binding! { + name: process_binding_nomode_controlmod, + binding: Binding { trigger: KEY, mods: ModifiersState { ctrl: true, shift: false, alt: false, logo: false }, action: Action::from("\x1b[1;5D"), mode: TermMode::NONE, notmode: TermMode::NONE }, + triggers: true, + mode: TermMode::NONE, + mods: ModifiersState { ctrl: true, shift: false, alt: false, logo: false } + } + + test_process_binding! { + name: process_binding_nomode_nomod_require_not_appcursor, + binding: Binding { trigger: KEY, mods: ModifiersState { shift: false, ctrl: false, alt: false, logo: false }, action: Action::from("\x1b[D"), mode: TermMode::NONE, notmode: TermMode::APP_CURSOR }, + triggers: true, + mode: TermMode::NONE, + mods: ModifiersState { shift: false, ctrl: false, alt: false, logo: false } + } + + test_process_binding! { + name: process_binding_appcursormode_nomod_require_appcursor, + binding: Binding { trigger: KEY, mods: ModifiersState { shift: false, ctrl: false, alt: false, logo: false }, action: Action::from("\x1bOD"), mode: TermMode::APP_CURSOR, notmode: TermMode::NONE }, + triggers: true, + mode: TermMode::APP_CURSOR, + mods: ModifiersState { shift: false, ctrl: false, alt: false, logo: false } + } + + test_process_binding! { + name: process_binding_nomode_nomod_require_appcursor, + binding: Binding { trigger: KEY, mods: ModifiersState { shift: false, ctrl: false, alt: false, logo: false }, action: Action::from("\x1bOD"), mode: TermMode::APP_CURSOR, notmode: TermMode::NONE }, + triggers: false, + mode: TermMode::NONE, + mods: ModifiersState { shift: false, ctrl: false, alt: false, logo: false } + } + + test_process_binding! { + name: process_binding_appcursormode_appkeypadmode_nomod_require_appcursor, + binding: Binding { trigger: KEY, mods: ModifiersState { shift: false, ctrl: false, alt: false, logo: false }, action: Action::from("\x1bOD"), mode: TermMode::APP_CURSOR, notmode: TermMode::NONE }, + triggers: true, + mode: TermMode::APP_CURSOR | TermMode::APP_KEYPAD, + mods: ModifiersState { shift: false, ctrl: false, alt: false, logo: false } + } + + test_process_binding! { + name: process_binding_fail_with_extra_mods, + binding: Binding { trigger: KEY, mods: ModifiersState { shift: false, ctrl: false, alt: false, logo: true }, action: Action::from("arst"), mode: TermMode::NONE, notmode: TermMode::NONE }, + triggers: false, + mode: TermMode::NONE, + mods: ModifiersState { shift: false, ctrl: false, alt: true, logo: true } + } +} diff --git a/alacritty_terminal/src/lib.rs b/alacritty_terminal/src/lib.rs new file mode 100644 index 00000000..ab1ba35e --- /dev/null +++ b/alacritty_terminal/src/lib.rs @@ -0,0 +1,60 @@ +// 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. +// +//! Alacritty - The GPU Enhanced Terminal +#![deny(clippy::all, clippy::if_not_else, clippy::enum_glob_use, clippy::wrong_pub_self_convention)] +#![cfg_attr(feature = "nightly", feature(core_intrinsics))] +#![cfg_attr(all(test, feature = "bench"), feature(test))] + +#[macro_use] +extern crate log; +#[macro_use] +extern crate serde_derive; + +#[cfg(target_os = "macos")] +#[macro_use] +extern crate objc; + +#[macro_use] +pub mod macros; +pub mod ansi; +pub mod cli; +pub mod config; +mod cursor; +pub mod display; +pub mod event; +pub mod event_loop; +pub mod grid; +pub mod index; +pub mod input; +pub mod locale; +pub mod message_bar; +pub mod meter; +pub mod panic; +pub mod renderer; +pub mod selection; +pub mod sync; +pub mod term; +pub mod tty; +mod url; +pub mod util; +pub mod window; + +pub use crate::grid::Grid; +pub use crate::term::Term; + +pub mod gl { + #![allow(clippy::all)] + include!(concat!(env!("OUT_DIR"), "/gl_bindings.rs")); +} diff --git a/alacritty_terminal/src/locale.rs b/alacritty_terminal/src/locale.rs new file mode 100644 index 00000000..40c915b5 --- /dev/null +++ b/alacritty_terminal/src/locale.rs @@ -0,0 +1,89 @@ +// 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. +#![allow(clippy::let_unit_value)] +#![cfg(target_os = "macos")] +use libc::{setlocale, LC_CTYPE}; +use std::env; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; +use std::ptr::null; +use std::slice; +use std::str; + +use objc::runtime::{Class, Object}; + +pub fn set_locale_environment() { + let locale_id = unsafe { + let locale_class = Class::get("NSLocale").unwrap(); + let locale: *const Object = msg_send![locale_class, currentLocale]; + let _: () = msg_send![locale_class, release]; + // `localeIdentifier` returns extra metadata with the locale (including currency and + // collator) on newer versions of macOS. This is not a valid locale, so we use + // `languageCode` and `countryCode`, if they're available (macOS 10.12+): + // https://developer.apple.com/documentation/foundation/nslocale/1416263-localeidentifier?language=objc + // https://developer.apple.com/documentation/foundation/nslocale/1643060-countrycode?language=objc + // https://developer.apple.com/documentation/foundation/nslocale/1643026-languagecode?language=objc + let is_language_code_supported: bool = + msg_send![locale, respondsToSelector: sel!(languageCode)]; + let is_country_code_supported: bool = + msg_send![locale, respondsToSelector: sel!(countryCode)]; + let locale_id = if is_language_code_supported && is_country_code_supported { + let language_code: *const Object = msg_send![locale, languageCode]; + let country_code: *const Object = msg_send![locale, countryCode]; + let language_code_str = nsstring_as_str(language_code).to_owned(); + let _: () = msg_send![language_code, release]; + let country_code_str = nsstring_as_str(country_code).to_owned(); + let _: () = msg_send![country_code, release]; + format!("{}_{}.UTF-8", &language_code_str, &country_code_str) + } else { + let identifier: *const Object = msg_send![locale, localeIdentifier]; + let identifier_str = nsstring_as_str(identifier).to_owned(); + let _: () = msg_send![identifier, release]; + identifier_str + ".UTF-8" + }; + let _: () = msg_send![locale, release]; + locale_id + }; + // check if locale_id is valid + let locale_c_str = CString::new(locale_id.to_owned()).unwrap(); + let locale_ptr = locale_c_str.as_ptr(); + let locale_id = unsafe { + // save a copy of original setting + let original = setlocale(LC_CTYPE, null()); + let saved_original = if original.is_null() { + CString::new("").unwrap() + } else { + CStr::from_ptr(original).to_owned() + }; + // try setting `locale_id` + let modified = setlocale(LC_CTYPE, locale_ptr); + let result = if modified.is_null() { String::new() } else { locale_id }; + // restore original setting + setlocale(LC_CTYPE, saved_original.as_ptr()); + result + }; + + env::set_var("LANG", &locale_id); +} + +const UTF8_ENCODING: usize = 4; + +unsafe fn nsstring_as_str<'a>(nsstring: *const Object) -> &'a str { + let cstr: *const c_char = msg_send![nsstring, UTF8String]; + let len: usize = msg_send![nsstring, lengthOfBytesUsingEncoding: UTF8_ENCODING]; + str::from_utf8(slice::from_raw_parts(cstr as *const u8, len)).unwrap() +} + +#[cfg(not(target_os = "macos"))] +pub fn set_locale_environment() {} diff --git a/alacritty_terminal/src/macros.rs b/alacritty_terminal/src/macros.rs new file mode 100644 index 00000000..519f8b6a --- /dev/null +++ b/alacritty_terminal/src/macros.rs @@ -0,0 +1,21 @@ +// 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. + +#[macro_export] +macro_rules! die { + ($($arg:tt)*) => {{ + error!($($arg)*); + ::std::process::exit(1); + }} +} diff --git a/alacritty_terminal/src/message_bar.rs b/alacritty_terminal/src/message_bar.rs new file mode 100644 index 00000000..8883dcb0 --- /dev/null +++ b/alacritty_terminal/src/message_bar.rs @@ -0,0 +1,473 @@ +// 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. + +use crossbeam_channel::{Receiver, Sender}; + +use crate::term::color::Rgb; +use crate::term::SizeInfo; + +pub const CLOSE_BUTTON_TEXT: &str = "[X]"; +const CLOSE_BUTTON_PADDING: usize = 1; +const MIN_FREE_LINES: usize = 3; +const TRUNCATED_MESSAGE: &str = "[MESSAGE TRUNCATED]"; + +/// Message for display in the MessageBuffer +#[derive(Debug, Eq, PartialEq, Clone)] +pub struct Message { + text: String, + color: Rgb, + topic: Option<String>, +} + +impl Message { + /// Create a new message + pub fn new(text: String, color: Rgb) -> Message { + Message { text, color, topic: None } + } + + /// Formatted message text lines + pub fn text(&self, size_info: &SizeInfo) -> Vec<String> { + let num_cols = size_info.cols().0; + let max_lines = size_info.lines().saturating_sub(MIN_FREE_LINES); + let button_len = CLOSE_BUTTON_TEXT.len(); + + // Split line to fit the screen + let mut lines = Vec::new(); + let mut line = String::new(); + for c in self.text.trim().chars() { + if c == '\n' + || line.len() == num_cols + // Keep space in first line for button + || (lines.is_empty() + && num_cols >= button_len + && line.len() == num_cols.saturating_sub(button_len + CLOSE_BUTTON_PADDING)) + { + // Attempt to wrap on word boundaries + if let (Some(index), true) = (line.rfind(char::is_whitespace), c != '\n') { + let split = line.split_off(index + 1); + line.pop(); + lines.push(Self::pad_text(line, num_cols)); + line = split + } else { + lines.push(Self::pad_text(line, num_cols)); + line = String::new(); + } + } + + if c != '\n' { + line.push(c); + } + } + lines.push(Self::pad_text(line, num_cols)); + + // Truncate output if it's too long + if lines.len() > max_lines { + lines.truncate(max_lines); + if TRUNCATED_MESSAGE.len() <= num_cols { + if let Some(line) = lines.iter_mut().last() { + *line = Self::pad_text(TRUNCATED_MESSAGE.into(), num_cols); + } + } + } + + // Append close button to first line + if button_len <= num_cols { + if let Some(line) = lines.get_mut(0) { + line.truncate(num_cols - button_len); + line.push_str(CLOSE_BUTTON_TEXT); + } + } + + lines + } + + /// Message color + #[inline] + pub fn color(&self) -> Rgb { + self.color + } + + /// Message topic + #[inline] + pub fn topic(&self) -> Option<&String> { + self.topic.as_ref() + } + + /// Update the message topic + #[inline] + pub fn set_topic(&mut self, topic: String) { + self.topic = Some(topic); + } + + /// Right-pad text to fit a specific number of columns + #[inline] + fn pad_text(mut text: String, num_cols: usize) -> String { + let padding_len = num_cols.saturating_sub(text.len()); + text.extend(vec![' '; padding_len]); + text + } +} + +/// Storage for message bar +#[derive(Debug)] +pub struct MessageBuffer { + current: Option<Message>, + messages: Receiver<Message>, + tx: Sender<Message>, +} + +impl MessageBuffer { + /// Create new message buffer + pub fn new() -> MessageBuffer { + let (tx, messages) = crossbeam_channel::unbounded(); + MessageBuffer { current: None, messages, tx } + } + + /// Check if there are any messages queued + #[inline] + pub fn is_empty(&self) -> bool { + self.current.is_none() + } + + /// Current message + #[inline] + pub fn message(&mut self) -> Option<Message> { + if let Some(current) = &self.current { + Some(current.clone()) + } else { + self.current = self.messages.try_recv().ok(); + self.current.clone() + } + } + + /// Channel for adding new messages + #[inline] + pub fn tx(&self) -> Sender<Message> { + self.tx.clone() + } + + /// Remove the currently visible message + #[inline] + pub fn pop(&mut self) { + // Remove all duplicates + for msg in self + .messages + .try_iter() + .take(self.messages.len()) + .filter(|m| Some(m) != self.current.as_ref()) + { + let _ = self.tx.send(msg); + } + + // Remove the message itself + self.current = self.messages.try_recv().ok(); + } + + /// Remove all messages with a specific topic + #[inline] + pub fn remove_topic(&mut self, topic: &str) { + // Filter messages currently pending + for msg in self + .messages + .try_iter() + .take(self.messages.len()) + .filter(|m| m.topic().map(String::as_str) != Some(topic)) + { + let _ = self.tx.send(msg); + } + + // Remove the currently active message + self.current = self.messages.try_recv().ok(); + } +} + +impl Default for MessageBuffer { + fn default() -> MessageBuffer { + MessageBuffer::new() + } +} + +#[cfg(test)] +mod test { + use super::{Message, MessageBuffer, MIN_FREE_LINES}; + use crate::term::{color, SizeInfo}; + + #[test] + fn appends_close_button() { + let input = "a"; + let mut message_buffer = MessageBuffer::new(); + message_buffer.tx().send(Message::new(input.into(), color::RED)).unwrap(); + let size = SizeInfo { + width: 7., + height: 10., + cell_width: 1., + cell_height: 1., + padding_x: 0., + padding_y: 0., + dpr: 0., + }; + + let lines = message_buffer.message().unwrap().text(&size); + + assert_eq!(lines, vec![String::from("a [X]")]); + } + + #[test] + fn multiline_close_button_first_line() { + let input = "fo\nbar"; + let mut message_buffer = MessageBuffer::new(); + message_buffer.tx().send(Message::new(input.into(), color::RED)).unwrap(); + let size = SizeInfo { + width: 6., + height: 10., + cell_width: 1., + cell_height: 1., + padding_x: 0., + padding_y: 0., + dpr: 0., + }; + + let lines = message_buffer.message().unwrap().text(&size); + + assert_eq!(lines, vec![String::from("fo [X]"), String::from("bar ")]); + } + + #[test] + fn splits_on_newline() { + let input = "a\nb"; + let mut message_buffer = MessageBuffer::new(); + message_buffer.tx().send(Message::new(input.into(), color::RED)).unwrap(); + let size = SizeInfo { + width: 6., + height: 10., + cell_width: 1., + cell_height: 1., + padding_x: 0., + padding_y: 0., + dpr: 0., + }; + + let lines = message_buffer.message().unwrap().text(&size); + + assert_eq!(lines.len(), 2); + } + + #[test] + fn splits_on_length() { + let input = "foobar1"; + let mut message_buffer = MessageBuffer::new(); + message_buffer.tx().send(Message::new(input.into(), color::RED)).unwrap(); + let size = SizeInfo { + width: 6., + height: 10., + cell_width: 1., + cell_height: 1., + padding_x: 0., + padding_y: 0., + dpr: 0., + }; + + let lines = message_buffer.message().unwrap().text(&size); + + assert_eq!(lines.len(), 2); + } + + #[test] + fn empty_with_shortterm() { + let input = "foobar"; + let mut message_buffer = MessageBuffer::new(); + message_buffer.tx().send(Message::new(input.into(), color::RED)).unwrap(); + let size = SizeInfo { + width: 6., + height: 0., + cell_width: 1., + cell_height: 1., + padding_x: 0., + padding_y: 0., + dpr: 0., + }; + + let lines = message_buffer.message().unwrap().text(&size); + + assert_eq!(lines.len(), 0); + } + + #[test] + fn truncates_long_messages() { + let input = "hahahahahahahahahahaha truncate this because it's too long for the term"; + let mut message_buffer = MessageBuffer::new(); + message_buffer.tx().send(Message::new(input.into(), color::RED)).unwrap(); + let size = SizeInfo { + width: 22., + height: (MIN_FREE_LINES + 2) as f32, + cell_width: 1., + cell_height: 1., + padding_x: 0., + padding_y: 0., + dpr: 0., + }; + + let lines = message_buffer.message().unwrap().text(&size); + + assert_eq!(lines, vec![ + String::from("hahahahahahahahaha [X]"), + String::from("[MESSAGE TRUNCATED] ") + ]); + } + + #[test] + fn hide_button_when_too_narrow() { + let input = "ha"; + let mut message_buffer = MessageBuffer::new(); + message_buffer.tx().send(Message::new(input.into(), color::RED)).unwrap(); + let size = SizeInfo { + width: 2., + height: 10., + cell_width: 1., + cell_height: 1., + padding_x: 0., + padding_y: 0., + dpr: 0., + }; + + let lines = message_buffer.message().unwrap().text(&size); + + assert_eq!(lines, vec![String::from("ha")]); + } + + #[test] + fn hide_truncated_when_too_narrow() { + let input = "hahahahahahahahaha"; + let mut message_buffer = MessageBuffer::new(); + message_buffer.tx().send(Message::new(input.into(), color::RED)).unwrap(); + let size = SizeInfo { + width: 2., + height: (MIN_FREE_LINES + 2) as f32, + cell_width: 1., + cell_height: 1., + padding_x: 0., + padding_y: 0., + dpr: 0., + }; + + let lines = message_buffer.message().unwrap().text(&size); + + assert_eq!(lines, vec![String::from("ha"), String::from("ha")]); + } + + #[test] + fn add_newline_for_button() { + let input = "test"; + let mut message_buffer = MessageBuffer::new(); + message_buffer.tx().send(Message::new(input.into(), color::RED)).unwrap(); + let size = SizeInfo { + width: 5., + height: 10., + cell_width: 1., + cell_height: 1., + padding_x: 0., + padding_y: 0., + dpr: 0., + }; + + let lines = message_buffer.message().unwrap().text(&size); + + assert_eq!(lines, vec![String::from("t [X]"), String::from("est ")]); + } + + #[test] + fn remove_topic() { + let mut message_buffer = MessageBuffer::new(); + for i in 0..10 { + let mut msg = Message::new(i.to_string(), color::RED); + if i % 2 == 0 && i < 5 { + msg.set_topic("topic".into()); + } + message_buffer.tx().send(msg).unwrap(); + } + + message_buffer.remove_topic("topic"); + + // Count number of messages + let mut num_messages = 0; + while message_buffer.message().is_some() { + num_messages += 1; + message_buffer.pop(); + } + + assert_eq!(num_messages, 7); + } + + #[test] + fn pop() { + let mut message_buffer = MessageBuffer::new(); + let one = Message::new(String::from("one"), color::RED); + message_buffer.tx().send(one.clone()).unwrap(); + let two = Message::new(String::from("two"), color::YELLOW); + message_buffer.tx().send(two.clone()).unwrap(); + + assert_eq!(message_buffer.message(), Some(one)); + + message_buffer.pop(); + + assert_eq!(message_buffer.message(), Some(two)); + } + + #[test] + fn wrap_on_words() { + let input = "a\nbc defg"; + let mut message_buffer = MessageBuffer::new(); + message_buffer.tx().send(Message::new(input.into(), color::RED)).unwrap(); + let size = SizeInfo { + width: 5., + height: 10., + cell_width: 1., + cell_height: 1., + padding_x: 0., + padding_y: 0., + dpr: 0., + }; + + let lines = message_buffer.message().unwrap().text(&size); + + assert_eq!(lines, vec![ + String::from("a [X]"), + String::from("bc "), + String::from("defg ") + ]); + } + + #[test] + fn remove_duplicates() { + let mut message_buffer = MessageBuffer::new(); + for _ in 0..10 { + let msg = Message::new(String::from("test"), color::RED); + message_buffer.tx().send(msg).unwrap(); + } + message_buffer.tx().send(Message::new(String::from("other"), color::RED)).unwrap(); + message_buffer.tx().send(Message::new(String::from("test"), color::YELLOW)).unwrap(); + let _ = message_buffer.message(); + + message_buffer.pop(); + + // Count number of messages + let mut num_messages = 0; + while message_buffer.message().is_some() { + num_messages += 1; + message_buffer.pop(); + } + + assert_eq!(num_messages, 2); + } +} diff --git a/alacritty_terminal/src/meter.rs b/alacritty_terminal/src/meter.rs new file mode 100644 index 00000000..19d7fe70 --- /dev/null +++ b/alacritty_terminal/src/meter.rs @@ -0,0 +1,110 @@ +// 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. +// +//! Rendering time meter +//! +//! Used to track rendering times and provide moving averages. +//! +//! # Examples +//! +//! ```rust +//! // create a meter +//! let mut meter = alacritty_terminal::meter::Meter::new(); +//! +//! // Sample something. +//! { +//! let _sampler = meter.sampler(); +//! } +//! +//! // Get the moving average. The meter tracks a fixed number of samples, and +//! // the average won't mean much until it's filled up at least once. +//! println!("Average time: {}", meter.average()); + +use std::time::{Duration, Instant}; + +const NUM_SAMPLES: usize = 10; + +/// The meter +#[derive(Default)] +pub struct Meter { + /// Track last 60 timestamps + times: [f64; NUM_SAMPLES], + + /// Average sample time in microseconds + avg: f64, + + /// Index of next time to update. + index: usize, +} + +/// Sampler +/// +/// Samplers record how long they are "alive" for and update the meter on drop. +pub struct Sampler<'a> { + /// Reference to meter that created the sampler + meter: &'a mut Meter, + + // When the sampler was created + created_at: Instant, +} + +impl<'a> Sampler<'a> { + fn new(meter: &'a mut Meter) -> Sampler<'a> { + Sampler { meter, created_at: Instant::now() } + } + + #[inline] + fn alive_duration(&self) -> Duration { + self.created_at.elapsed() + } +} + +impl<'a> Drop for Sampler<'a> { + fn drop(&mut self) { + self.meter.add_sample(self.alive_duration()); + } +} + +impl Meter { + /// Create a meter + pub fn new() -> Meter { + Default::default() + } + + /// Get a sampler + pub fn sampler(&mut self) -> Sampler<'_> { + Sampler::new(self) + } + + /// Get the current average sample duration in microseconds + pub fn average(&self) -> f64 { + self.avg + } + + /// Add a sample + /// + /// Used by Sampler::drop. + fn add_sample(&mut self, sample: Duration) { + let mut usec = 0f64; + + usec += f64::from(sample.subsec_nanos()) / 1e3; + usec += (sample.as_secs() as f64) * 1e6; + + let prev = self.times[self.index]; + self.times[self.index] = usec; + self.avg -= prev / NUM_SAMPLES as f64; + self.avg += usec / NUM_SAMPLES as f64; + self.index = (self.index + 1) % NUM_SAMPLES; + } +} diff --git a/alacritty_terminal/src/panic.rs b/alacritty_terminal/src/panic.rs new file mode 100644 index 00000000..4d3524ed --- /dev/null +++ b/alacritty_terminal/src/panic.rs @@ -0,0 +1,53 @@ +// 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 the default behavior of the other platforms. +#[cfg(not(windows))] +pub fn attach_handler() {} + +// Install a panic handler that renders the panic in a classical Windows error +// dialog box as well as writes the panic to STDERR. +#[cfg(windows)] +pub fn attach_handler() { + use std::{io, io::Write, panic, ptr}; + use winapi::um::winuser; + + panic::set_hook(Box::new(|panic_info| { + let _ = writeln!(io::stderr(), "{}", panic_info); + let msg = format!("{}\n\nPress Ctrl-C to Copy", panic_info); + unsafe { + winuser::MessageBoxW( + ptr::null_mut(), + win32_string(&msg).as_ptr(), + win32_string("Alacritty: Runtime Error").as_ptr(), + winuser::MB_ICONERROR + | winuser::MB_OK + | winuser::MB_SETFOREGROUND + | winuser::MB_TASKMODAL, + ); + } + })); +} + +// Converts the string slice into a Windows-standard representation for "W"- +// suffixed function variants, which accept UTF-16 encoded string values. +#[cfg(windows)] +fn win32_string(value: &str) -> Vec<u16> { + use std::ffi::OsStr; + use std::iter::once; + use std::os::windows::ffi::OsStrExt; + OsStr::new(value).encode_wide().chain(once(0)).collect() +} diff --git a/alacritty_terminal/src/renderer/mod.rs b/alacritty_terminal/src/renderer/mod.rs new file mode 100644 index 00000000..82c6c2df --- /dev/null +++ b/alacritty_terminal/src/renderer/mod.rs @@ -0,0 +1,1629 @@ +// 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. +use std::collections::HashMap; +use std::fs::File; +use std::hash::BuildHasherDefault; +use std::io::{self, Read}; +use std::mem::size_of; +use std::path::PathBuf; +use std::ptr; +use std::sync::mpsc; +use std::time::Duration; + +use fnv::FnvHasher; +use font::{self, FontDesc, FontKey, GlyphKey, Rasterize, RasterizedGlyph, Rasterizer}; +use glutin::dpi::PhysicalSize; +use notify::{watcher, DebouncedEvent, RecursiveMode, Watcher}; + +use crate::ansi::CursorStyle; +use crate::config::{self, Config, Delta}; +use crate::gl; +use crate::gl::types::*; +use crate::index::{Column, Line}; +use crate::renderer::rects::{Rect, Rects}; +use crate::term::color::Rgb; +use crate::term::{self, cell, RenderableCell, RenderableCellContent}; + +pub mod rects; + +// Shader paths for live reload +static TEXT_SHADER_F_PATH: &'static str = + concat!(env!("CARGO_MANIFEST_DIR"), "/../res/text.f.glsl"); +static TEXT_SHADER_V_PATH: &'static str = + concat!(env!("CARGO_MANIFEST_DIR"), "/../res/text.v.glsl"); +static RECT_SHADER_F_PATH: &'static str = + concat!(env!("CARGO_MANIFEST_DIR"), "/../res/rect.f.glsl"); +static RECT_SHADER_V_PATH: &'static str = + concat!(env!("CARGO_MANIFEST_DIR"), "/../res/rect.v.glsl"); + +// Shader source which is used when live-shader-reload feature is disable +static TEXT_SHADER_F: &'static str = + include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../res/text.f.glsl")); +static TEXT_SHADER_V: &'static str = + include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../res/text.v.glsl")); +static RECT_SHADER_F: &'static str = + include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../res/rect.f.glsl")); +static RECT_SHADER_V: &'static str = + include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../res/rect.v.glsl")); + +/// `LoadGlyph` allows for copying a rasterized glyph into graphics memory +pub trait LoadGlyph { + /// Load the rasterized glyph into GPU memory + fn load_glyph(&mut self, rasterized: &RasterizedGlyph) -> Glyph; + + /// Clear any state accumulated from previous loaded glyphs + /// + /// This can, for instance, be used to reset the texture Atlas. + fn clear(&mut self); +} + +enum Msg { + ShaderReload, +} + +#[derive(Debug)] +pub enum Error { + ShaderCreation(ShaderCreationError), +} + +impl ::std::error::Error for Error { + fn cause(&self) -> Option<&dyn (::std::error::Error)> { + match *self { + Error::ShaderCreation(ref err) => Some(err), + } + } + + fn description(&self) -> &str { + match *self { + Error::ShaderCreation(ref err) => err.description(), + } + } +} + +impl ::std::fmt::Display for Error { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + match *self { + Error::ShaderCreation(ref err) => { + write!(f, "There was an error initializing the shaders: {}", err) + }, + } + } +} + +impl From<ShaderCreationError> for Error { + fn from(val: ShaderCreationError) -> Error { + Error::ShaderCreation(val) + } +} + +/// Text drawing program +/// +/// Uniforms are prefixed with "u", and vertex attributes are prefixed with "a". +#[derive(Debug)] +pub struct TextShaderProgram { + // Program id + id: GLuint, + + /// projection scale and offset uniform + u_projection: GLint, + + /// Cell dimensions (pixels) + u_cell_dim: GLint, + + /// Background pass flag + /// + /// Rendering is split into two passes; 1 for backgrounds, and one for text + u_background: GLint, +} + +/// Rectangle drawing program +/// +/// Uniforms are prefixed with "u" +#[derive(Debug)] +pub struct RectShaderProgram { + // Program id + id: GLuint, + /// Rectangle color + u_color: GLint, +} + +#[derive(Copy, Debug, Clone)] +pub struct Glyph { + tex_id: GLuint, + top: f32, + left: f32, + width: f32, + height: f32, + uv_bot: f32, + uv_left: f32, + uv_width: f32, + uv_height: f32, +} + +/// Naïve glyph cache +/// +/// Currently only keyed by `char`, and thus not possible to hold different +/// representations of the same code point. +pub struct GlyphCache { + /// Cache of buffered glyphs + cache: HashMap<GlyphKey, Glyph, BuildHasherDefault<FnvHasher>>, + + /// Cache of buffered cursor glyphs + cursor_cache: HashMap<CursorStyle, Glyph, BuildHasherDefault<FnvHasher>>, + + /// Rasterizer for loading new glyphs + rasterizer: Rasterizer, + + /// regular font + font_key: FontKey, + + /// italic font + italic_key: FontKey, + + /// bold font + bold_key: FontKey, + + /// font size + font_size: font::Size, + + /// glyph offset + glyph_offset: Delta<i8>, + + metrics: ::font::Metrics, +} + +impl GlyphCache { + pub fn new<L>( + mut rasterizer: Rasterizer, + font: &config::Font, + loader: &mut L, + ) -> Result<GlyphCache, font::Error> + where + L: LoadGlyph, + { + let (regular, bold, italic) = Self::compute_font_keys(font, &mut rasterizer)?; + + // Need to load at least one glyph for the face before calling metrics. + // The glyph requested here ('m' at the time of writing) has no special + // meaning. + rasterizer.get_glyph(GlyphKey { font_key: regular, c: 'm', size: font.size() })?; + + let metrics = rasterizer.metrics(regular, font.size())?; + + let mut cache = GlyphCache { + cache: HashMap::default(), + cursor_cache: HashMap::default(), + rasterizer, + font_size: font.size(), + font_key: regular, + bold_key: bold, + italic_key: italic, + glyph_offset: *font.glyph_offset(), + metrics, + }; + + cache.load_glyphs_for_font(regular, loader); + cache.load_glyphs_for_font(bold, loader); + cache.load_glyphs_for_font(italic, loader); + + Ok(cache) + } + + fn load_glyphs_for_font<L: LoadGlyph>(&mut self, font: FontKey, loader: &mut L) { + let size = self.font_size; + for i in 32u8..=128u8 { + self.get(GlyphKey { font_key: font, c: i as char, size }, loader); + } + } + + /// Computes font keys for (Regular, Bold, Italic) + fn compute_font_keys( + font: &config::Font, + rasterizer: &mut Rasterizer, + ) -> Result<(FontKey, FontKey, FontKey), font::Error> { + let size = font.size(); + + // Load regular font + let regular_desc = + Self::make_desc(&font.normal(), font::Slant::Normal, font::Weight::Normal); + + let regular = rasterizer.load_font(®ular_desc, size)?; + + // helper to load a description if it is not the regular_desc + let mut load_or_regular = |desc: FontDesc| { + if desc == regular_desc { + regular + } else { + rasterizer.load_font(&desc, size).unwrap_or_else(|_| regular) + } + }; + + // Load bold font + let bold_desc = Self::make_desc(&font.bold(), font::Slant::Normal, font::Weight::Bold); + + let bold = load_or_regular(bold_desc); + + // Load italic font + let italic_desc = + Self::make_desc(&font.italic(), font::Slant::Italic, font::Weight::Normal); + + let italic = load_or_regular(italic_desc); + + Ok((regular, bold, italic)) + } + + fn make_desc( + desc: &config::FontDescription, + slant: font::Slant, + weight: font::Weight, + ) -> FontDesc { + let style = if let Some(ref spec) = desc.style { + font::Style::Specific(spec.to_owned()) + } else { + font::Style::Description { slant, weight } + }; + FontDesc::new(desc.family.clone(), style) + } + + pub fn font_metrics(&self) -> font::Metrics { + self.rasterizer + .metrics(self.font_key, self.font_size) + .expect("metrics load since font is loaded at glyph cache creation") + } + + pub fn get<'a, L>(&'a mut self, glyph_key: GlyphKey, loader: &mut L) -> &'a Glyph + where + L: LoadGlyph, + { + let glyph_offset = self.glyph_offset; + let rasterizer = &mut self.rasterizer; + let metrics = &self.metrics; + self.cache.entry(glyph_key).or_insert_with(|| { + let mut rasterized = + rasterizer.get_glyph(glyph_key).unwrap_or_else(|_| Default::default()); + + rasterized.left += i32::from(glyph_offset.x); + rasterized.top += i32::from(glyph_offset.y); + rasterized.top -= metrics.descent as i32; + + loader.load_glyph(&rasterized) + }) + } + + pub fn update_font_size<L: LoadGlyph>( + &mut self, + font: &config::Font, + size: font::Size, + dpr: f64, + loader: &mut L, + ) -> Result<(), font::Error> { + // Clear currently cached data in both GL and the registry + loader.clear(); + self.cache = HashMap::default(); + self.cursor_cache = HashMap::default(); + + // Update dpi scaling + self.rasterizer.update_dpr(dpr as f32); + + // Recompute font keys + let font = font.to_owned().with_size(size); + let (regular, bold, italic) = Self::compute_font_keys(&font, &mut self.rasterizer)?; + + self.rasterizer.get_glyph(GlyphKey { font_key: regular, c: 'm', size: font.size() })?; + let metrics = self.rasterizer.metrics(regular, size)?; + + info!("Font size changed to {:?} with DPR of {}", font.size, dpr); + + self.font_size = font.size; + self.font_key = regular; + self.bold_key = bold; + self.italic_key = italic; + self.metrics = metrics; + + self.load_glyphs_for_font(regular, loader); + self.load_glyphs_for_font(bold, loader); + self.load_glyphs_for_font(italic, loader); + + Ok(()) + } + + // Calculate font metrics without access to a glyph cache + // + // This should only be used *before* OpenGL is initialized and the glyph cache can be filled. + pub fn static_metrics(config: &Config, dpr: f32) -> Result<font::Metrics, font::Error> { + let font = config.font().clone(); + + let mut rasterizer = font::Rasterizer::new(dpr, config.use_thin_strokes())?; + let regular_desc = + GlyphCache::make_desc(&font.normal(), font::Slant::Normal, font::Weight::Normal); + let regular = rasterizer.load_font(®ular_desc, font.size())?; + rasterizer.get_glyph(GlyphKey { font_key: regular, c: 'm', size: font.size() })?; + + rasterizer.metrics(regular, font.size()) + } +} + +#[derive(Debug)] +#[repr(C)] +struct InstanceData { + // coords + col: f32, + row: f32, + // glyph offset + left: f32, + top: f32, + // glyph scale + width: f32, + height: f32, + // uv offset + uv_left: f32, + uv_bot: f32, + // uv scale + uv_width: f32, + uv_height: f32, + // color + r: f32, + g: f32, + b: f32, + // background color + bg_r: f32, + bg_g: f32, + bg_b: f32, + bg_a: f32, +} + +#[derive(Debug)] +pub struct QuadRenderer { + program: TextShaderProgram, + rect_program: RectShaderProgram, + vao: GLuint, + ebo: GLuint, + vbo_instance: GLuint, + rect_vao: GLuint, + rect_vbo: GLuint, + atlas: Vec<Atlas>, + current_atlas: usize, + active_tex: GLuint, + batch: Batch, + rx: mpsc::Receiver<Msg>, +} + +#[derive(Debug)] +pub struct RenderApi<'a> { + active_tex: &'a mut GLuint, + batch: &'a mut Batch, + atlas: &'a mut Vec<Atlas>, + current_atlas: &'a mut usize, + program: &'a mut TextShaderProgram, + config: &'a Config, +} + +#[derive(Debug)] +pub struct LoaderApi<'a> { + active_tex: &'a mut GLuint, + atlas: &'a mut Vec<Atlas>, + current_atlas: &'a mut usize, +} + +#[derive(Debug)] +pub struct PackedVertex { + x: f32, + y: f32, +} + +#[derive(Debug, Default)] +pub struct Batch { + tex: GLuint, + instances: Vec<InstanceData>, +} + +impl Batch { + #[inline] + pub fn new() -> Batch { + Batch { tex: 0, instances: Vec::with_capacity(BATCH_MAX) } + } + + pub fn add_item(&mut self, cell: &RenderableCell, glyph: &Glyph) { + if self.is_empty() { + self.tex = glyph.tex_id; + } + + self.instances.push(InstanceData { + col: cell.column.0 as f32, + row: cell.line.0 as f32, + + top: glyph.top, + left: glyph.left, + width: glyph.width, + height: glyph.height, + + uv_bot: glyph.uv_bot, + uv_left: glyph.uv_left, + uv_width: glyph.uv_width, + uv_height: glyph.uv_height, + + r: f32::from(cell.fg.r), + g: f32::from(cell.fg.g), + b: f32::from(cell.fg.b), + + bg_r: f32::from(cell.bg.r), + bg_g: f32::from(cell.bg.g), + bg_b: f32::from(cell.bg.b), + bg_a: cell.bg_alpha, + }); + } + + #[inline] + pub fn full(&self) -> bool { + self.capacity() == self.len() + } + + #[inline] + pub fn len(&self) -> usize { + self.instances.len() + } + + #[inline] + pub fn capacity(&self) -> usize { + BATCH_MAX + } + + #[inline] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + #[inline] + pub fn size(&self) -> usize { + self.len() * size_of::<InstanceData>() + } + + pub fn clear(&mut self) { + self.tex = 0; + self.instances.clear(); + } +} + +/// Maximum items to be drawn in a batch. +const BATCH_MAX: usize = 0x1_0000; +const ATLAS_SIZE: i32 = 1024; + +impl QuadRenderer { + pub fn new() -> Result<QuadRenderer, Error> { + let program = TextShaderProgram::new()?; + let rect_program = RectShaderProgram::new()?; + + let mut vao: GLuint = 0; + let mut ebo: GLuint = 0; + + let mut vbo_instance: GLuint = 0; + + let mut rect_vao: GLuint = 0; + let mut rect_vbo: GLuint = 0; + let mut rect_ebo: GLuint = 0; + + unsafe { + gl::Enable(gl::BLEND); + gl::BlendFunc(gl::SRC1_COLOR, gl::ONE_MINUS_SRC1_COLOR); + gl::Enable(gl::MULTISAMPLE); + + // Disable depth mask, as the renderer never uses depth tests + gl::DepthMask(gl::FALSE); + + gl::GenVertexArrays(1, &mut vao); + gl::GenBuffers(1, &mut ebo); + gl::GenBuffers(1, &mut vbo_instance); + gl::BindVertexArray(vao); + + // --------------------- + // Set up element buffer + // --------------------- + let indices: [u32; 6] = [0, 1, 3, 1, 2, 3]; + + gl::BindBuffer(gl::ELEMENT_ARRAY_BUFFER, ebo); + gl::BufferData( + gl::ELEMENT_ARRAY_BUFFER, + (6 * size_of::<u32>()) as isize, + indices.as_ptr() as *const _, + gl::STATIC_DRAW, + ); + + // ---------------------------- + // Setup vertex instance buffer + // ---------------------------- + gl::BindBuffer(gl::ARRAY_BUFFER, vbo_instance); + gl::BufferData( + gl::ARRAY_BUFFER, + (BATCH_MAX * size_of::<InstanceData>()) as isize, + ptr::null(), + gl::STREAM_DRAW, + ); + // coords + gl::VertexAttribPointer( + 0, + 2, + gl::FLOAT, + gl::FALSE, + size_of::<InstanceData>() as i32, + ptr::null(), + ); + gl::EnableVertexAttribArray(0); + gl::VertexAttribDivisor(0, 1); + // glyphoffset + gl::VertexAttribPointer( + 1, + 4, + gl::FLOAT, + gl::FALSE, + size_of::<InstanceData>() as i32, + (2 * size_of::<f32>()) as *const _, + ); + gl::EnableVertexAttribArray(1); + gl::VertexAttribDivisor(1, 1); + // uv + gl::VertexAttribPointer( + 2, + 4, + gl::FLOAT, + gl::FALSE, + size_of::<InstanceData>() as i32, + (6 * size_of::<f32>()) as *const _, + ); + gl::EnableVertexAttribArray(2); + gl::VertexAttribDivisor(2, 1); + // color + gl::VertexAttribPointer( + 3, + 3, + gl::FLOAT, + gl::FALSE, + size_of::<InstanceData>() as i32, + (10 * size_of::<f32>()) as *const _, + ); + gl::EnableVertexAttribArray(3); + gl::VertexAttribDivisor(3, 1); + // color + gl::VertexAttribPointer( + 4, + 4, + gl::FLOAT, + gl::FALSE, + size_of::<InstanceData>() as i32, + (13 * size_of::<f32>()) as *const _, + ); + gl::EnableVertexAttribArray(4); + gl::VertexAttribDivisor(4, 1); + + // Rectangle setup + gl::GenVertexArrays(1, &mut rect_vao); + gl::GenBuffers(1, &mut rect_vbo); + gl::GenBuffers(1, &mut rect_ebo); + gl::BindVertexArray(rect_vao); + let indices: [i32; 6] = [0, 1, 3, 1, 2, 3]; + gl::BindBuffer(gl::ELEMENT_ARRAY_BUFFER, rect_ebo); + gl::BufferData( + gl::ELEMENT_ARRAY_BUFFER, + (size_of::<i32>() * indices.len()) as _, + indices.as_ptr() as *const _, + gl::STATIC_DRAW, + ); + + // Cleanup + gl::BindVertexArray(0); + gl::BindBuffer(gl::ARRAY_BUFFER, 0); + gl::BindBuffer(gl::ELEMENT_ARRAY_BUFFER, 0); + } + + let (msg_tx, msg_rx) = mpsc::channel(); + + if cfg!(feature = "live-shader-reload") { + ::std::thread::spawn(move || { + let (tx, rx) = ::std::sync::mpsc::channel(); + // The Duration argument is a debouncing period. + let mut watcher = + watcher(tx, Duration::from_millis(10)).expect("create file watcher"); + watcher + .watch(TEXT_SHADER_F_PATH, RecursiveMode::NonRecursive) + .expect("watch fragment shader"); + watcher + .watch(TEXT_SHADER_V_PATH, RecursiveMode::NonRecursive) + .expect("watch vertex shader"); + + loop { + let event = rx.recv().expect("watcher event"); + + match event { + DebouncedEvent::Rename(..) => continue, + DebouncedEvent::Create(_) + | DebouncedEvent::Write(_) + | DebouncedEvent::Chmod(_) => { + msg_tx.send(Msg::ShaderReload).expect("msg send ok"); + }, + _ => {}, + } + } + }); + } + + let mut renderer = QuadRenderer { + program, + rect_program, + vao, + ebo, + vbo_instance, + rect_vao, + rect_vbo, + atlas: Vec::new(), + current_atlas: 0, + active_tex: 0, + batch: Batch::new(), + rx: msg_rx, + }; + + let atlas = Atlas::new(ATLAS_SIZE); + renderer.atlas.push(atlas); + + Ok(renderer) + } + + // Draw all rectangles simultaneously to prevent excessive program swaps + pub fn draw_rects( + &mut self, + config: &Config, + props: &term::SizeInfo, + visual_bell_intensity: f64, + cell_line_rects: Rects, + ) { + // Swap to rectangle rendering program + unsafe { + // Swap program + gl::UseProgram(self.rect_program.id); + + // Remove padding from viewport + gl::Viewport(0, 0, props.width as i32, props.height as i32); + + // Change blending strategy + gl::BlendFunc(gl::SRC_ALPHA, gl::ONE_MINUS_SRC_ALPHA); + + // Setup data and buffers + gl::BindVertexArray(self.rect_vao); + gl::BindBuffer(gl::ARRAY_BUFFER, self.rect_vbo); + + // Position + gl::VertexAttribPointer( + 0, + 2, + gl::FLOAT, + gl::FALSE, + (size_of::<f32>() * 2) as _, + ptr::null(), + ); + gl::EnableVertexAttribArray(0); + } + + // Draw visual bell + let color = config.visual_bell().color(); + let rect = Rect::new(0., 0., props.width, props.height); + self.render_rect(&rect, color, visual_bell_intensity as f32, props); + + // Draw underlines and strikeouts + for cell_line_rect in cell_line_rects.rects() { + self.render_rect(&cell_line_rect.0, cell_line_rect.1, 255., props); + } + + // Deactivate rectangle program again + unsafe { + // Reset blending strategy + gl::BlendFunc(gl::SRC1_COLOR, gl::ONE_MINUS_SRC1_COLOR); + + // Reset data and buffers + gl::BindBuffer(gl::ARRAY_BUFFER, 0); + gl::BindVertexArray(0); + + let padding_x = props.padding_x as i32; + let padding_y = props.padding_y as i32; + let width = props.width as i32; + let height = props.height as i32; + gl::Viewport(padding_x, padding_y, width - 2 * padding_x, height - 2 * padding_y); + + // Disable program + gl::UseProgram(0); + } + } + + pub fn with_api<F, T>(&mut self, config: &Config, props: &term::SizeInfo, func: F) -> T + where + F: FnOnce(RenderApi<'_>) -> T, + { + // Flush message queue + if let Ok(Msg::ShaderReload) = self.rx.try_recv() { + self.reload_shaders(props); + } + while let Ok(_) = self.rx.try_recv() {} + + unsafe { + gl::UseProgram(self.program.id); + self.program.set_term_uniforms(props); + + gl::BindVertexArray(self.vao); + gl::BindBuffer(gl::ELEMENT_ARRAY_BUFFER, self.ebo); + gl::BindBuffer(gl::ARRAY_BUFFER, self.vbo_instance); + gl::ActiveTexture(gl::TEXTURE0); + } + + let res = func(RenderApi { + active_tex: &mut self.active_tex, + batch: &mut self.batch, + atlas: &mut self.atlas, + current_atlas: &mut self.current_atlas, + program: &mut self.program, + config, + }); + + unsafe { + gl::BindBuffer(gl::ELEMENT_ARRAY_BUFFER, 0); + gl::BindBuffer(gl::ARRAY_BUFFER, 0); + gl::BindVertexArray(0); + + gl::UseProgram(0); + } + + res + } + + pub fn with_loader<F, T>(&mut self, func: F) -> T + where + F: FnOnce(LoaderApi<'_>) -> T, + { + unsafe { + gl::ActiveTexture(gl::TEXTURE0); + } + + func(LoaderApi { + active_tex: &mut self.active_tex, + atlas: &mut self.atlas, + current_atlas: &mut self.current_atlas, + }) + } + + pub fn reload_shaders(&mut self, props: &term::SizeInfo) { + info!("Reloading shaders..."); + let result = (TextShaderProgram::new(), RectShaderProgram::new()); + let (program, rect_program) = match result { + (Ok(program), Ok(rect_program)) => { + unsafe { + gl::UseProgram(program.id); + program.update_projection( + props.width, + props.height, + props.padding_x, + props.padding_y, + ); + gl::UseProgram(0); + } + + info!("... successfully reloaded shaders"); + (program, rect_program) + }, + (Err(err), _) | (_, Err(err)) => { + error!("{}", err); + return; + }, + }; + + self.active_tex = 0; + self.program = program; + self.rect_program = rect_program; + } + + pub fn resize(&mut self, size: PhysicalSize, padding_x: f32, padding_y: f32) { + let (width, height): (u32, u32) = size.into(); + + // viewport + unsafe { + let width = width as i32; + let height = height as i32; + let padding_x = padding_x as i32; + let padding_y = padding_y as i32; + gl::Viewport(padding_x, padding_y, width - 2 * padding_x, height - 2 * padding_y); + + // update projection + gl::UseProgram(self.program.id); + self.program.update_projection( + width as f32, + height as f32, + padding_x as f32, + padding_y as f32, + ); + gl::UseProgram(0); + } + } + + // Render a rectangle + // + // This requires the rectangle program to be activated + fn render_rect(&mut self, rect: &Rect<f32>, color: Rgb, alpha: f32, size: &term::SizeInfo) { + // Do nothing when alpha is fully transparent + if alpha == 0. { + return; + } + + // Calculate rectangle position + let center_x = size.width / 2.; + let center_y = size.height / 2.; + let x = (rect.x - center_x) / center_x; + let y = -(rect.y - center_y) / center_y; + let width = rect.width / center_x; + let height = rect.height / center_y; + + unsafe { + // Setup vertices + let vertices: [f32; 8] = [x + width, y, x + width, y - height, x, y - height, x, y]; + + // Load vertex data into array buffer + gl::BufferData( + gl::ARRAY_BUFFER, + (size_of::<f32>() * vertices.len()) as _, + vertices.as_ptr() as *const _, + gl::STATIC_DRAW, + ); + + // Color + self.rect_program.set_color(color, alpha); + + // Draw the rectangle + gl::DrawElements(gl::TRIANGLES, 6, gl::UNSIGNED_INT, ptr::null()); + } + } +} + +impl<'a> RenderApi<'a> { + pub fn clear(&self, color: Rgb) { + let alpha = self.config.background_opacity().get(); + unsafe { + gl::ClearColor( + (f32::from(color.r) / 255.0).min(1.0) * alpha, + (f32::from(color.g) / 255.0).min(1.0) * alpha, + (f32::from(color.b) / 255.0).min(1.0) * alpha, + alpha, + ); + gl::Clear(gl::COLOR_BUFFER_BIT); + } + } + + fn render_batch(&mut self) { + unsafe { + gl::BufferSubData( + gl::ARRAY_BUFFER, + 0, + self.batch.size() as isize, + self.batch.instances.as_ptr() as *const _, + ); + } + + // Bind texture if necessary + if *self.active_tex != self.batch.tex { + unsafe { + gl::BindTexture(gl::TEXTURE_2D, self.batch.tex); + } + *self.active_tex = self.batch.tex; + } + + unsafe { + self.program.set_background_pass(true); + gl::DrawElementsInstanced( + gl::TRIANGLES, + 6, + gl::UNSIGNED_INT, + ptr::null(), + self.batch.len() as GLsizei, + ); + self.program.set_background_pass(false); + gl::DrawElementsInstanced( + gl::TRIANGLES, + 6, + gl::UNSIGNED_INT, + ptr::null(), + self.batch.len() as GLsizei, + ); + } + + self.batch.clear(); + } + + /// Render a string in a variable location. Used for printing the render timer, warnings and + /// errors. + pub fn render_string( + &mut self, + string: &str, + line: Line, + glyph_cache: &mut GlyphCache, + color: Option<Rgb>, + ) { + let bg_alpha = color.map(|_| 1.0).unwrap_or(0.0); + let col = Column(0); + + let cells = string + .chars() + .enumerate() + .map(|(i, c)| RenderableCell { + line, + column: col + i, + inner: RenderableCellContent::Chars({ + let mut chars = [' '; cell::MAX_ZEROWIDTH_CHARS + 1]; + chars[0] = c; + chars + }), + bg: color.unwrap_or(Rgb { r: 0, g: 0, b: 0 }), + fg: Rgb { r: 0, g: 0, b: 0 }, + flags: cell::Flags::empty(), + bg_alpha, + }) + .collect::<Vec<_>>(); + + for cell in cells { + self.render_cell(cell, glyph_cache); + } + } + + #[inline] + fn add_render_item(&mut self, cell: &RenderableCell, glyph: &Glyph) { + // Flush batch if tex changing + if !self.batch.is_empty() && self.batch.tex != glyph.tex_id { + self.render_batch(); + } + + self.batch.add_item(cell, glyph); + + // Render batch and clear if it's full + if self.batch.full() { + self.render_batch(); + } + } + + pub fn render_cell(&mut self, cell: RenderableCell, glyph_cache: &mut GlyphCache) { + let chars = match cell.inner { + RenderableCellContent::Cursor((cursor_style, ref raw)) => { + // Raw cell pixel buffers like cursors don't need to go through font lookup + let glyph = glyph_cache + .cursor_cache + .entry(cursor_style) + .or_insert_with(|| self.load_glyph(raw)); + self.add_render_item(&cell, &glyph); + return; + }, + RenderableCellContent::Chars(chars) => chars, + }; + + // Get font key for cell + // FIXME this is super inefficient. + let font_key = if cell.flags.contains(cell::Flags::BOLD) { + glyph_cache.bold_key + } else if cell.flags.contains(cell::Flags::ITALIC) { + glyph_cache.italic_key + } else { + glyph_cache.font_key + }; + + // Don't render text of HIDDEN cells + let mut chars = if cell.flags.contains(cell::Flags::HIDDEN) { + [' '; cell::MAX_ZEROWIDTH_CHARS + 1] + } else { + chars + }; + + // Render tabs as spaces in case the font doesn't support it + if chars[0] == '\t' { + chars[0] = ' '; + } + + let mut glyph_key = GlyphKey { font_key, size: glyph_cache.font_size, c: chars[0] }; + + // Add cell to batch + let glyph = glyph_cache.get(glyph_key, self); + self.add_render_item(&cell, glyph); + + // Render zero-width characters + for c in (&chars[1..]).iter().filter(|c| **c != ' ') { + glyph_key.c = *c; + let mut glyph = *glyph_cache.get(glyph_key, self); + + // The metrics of zero-width characters are based on rendering + // the character after the current cell, with the anchor at the + // right side of the preceding character. Since we render the + // zero-width characters inside the preceding character, the + // anchor has been moved to the right by one cell. + glyph.left += glyph_cache.metrics.average_advance as f32; + + self.add_render_item(&cell, &glyph); + } + } +} + +/// Load a glyph into a texture atlas +/// +/// If the current atlas is full, a new one will be created. +#[inline] +fn load_glyph( + active_tex: &mut GLuint, + atlas: &mut Vec<Atlas>, + current_atlas: &mut usize, + rasterized: &RasterizedGlyph, +) -> Glyph { + // At least one atlas is guaranteed to be in the `self.atlas` list; thus + // the unwrap. + match atlas[*current_atlas].insert(rasterized, active_tex) { + Ok(glyph) => glyph, + Err(AtlasInsertError::Full) => { + *current_atlas += 1; + if *current_atlas == atlas.len() { + let new = Atlas::new(ATLAS_SIZE); + *active_tex = 0; // Atlas::new binds a texture. Ugh this is sloppy. + atlas.push(new); + } + load_glyph(active_tex, atlas, current_atlas, rasterized) + }, + Err(AtlasInsertError::GlyphTooLarge) => Glyph { + tex_id: atlas[*current_atlas].id, + top: 0.0, + left: 0.0, + width: 0.0, + height: 0.0, + uv_bot: 0.0, + uv_left: 0.0, + uv_width: 0.0, + uv_height: 0.0, + }, + } +} + +#[inline] +fn clear_atlas(atlas: &mut Vec<Atlas>, current_atlas: &mut usize) { + for atlas in atlas.iter_mut() { + atlas.clear(); + } + *current_atlas = 0; +} + +impl<'a> LoadGlyph for LoaderApi<'a> { + fn load_glyph(&mut self, rasterized: &RasterizedGlyph) -> Glyph { + load_glyph(self.active_tex, self.atlas, self.current_atlas, rasterized) + } + + fn clear(&mut self) { + clear_atlas(self.atlas, self.current_atlas) + } +} + +impl<'a> LoadGlyph for RenderApi<'a> { + fn load_glyph(&mut self, rasterized: &RasterizedGlyph) -> Glyph { + load_glyph(self.active_tex, self.atlas, self.current_atlas, rasterized) + } + + fn clear(&mut self) { + clear_atlas(self.atlas, self.current_atlas) + } +} + +impl<'a> Drop for RenderApi<'a> { + fn drop(&mut self) { + if !self.batch.is_empty() { + self.render_batch(); + } + } +} + +impl TextShaderProgram { + pub fn new() -> Result<TextShaderProgram, ShaderCreationError> { + let (vertex_src, fragment_src) = if cfg!(feature = "live-shader-reload") { + (None, None) + } else { + (Some(TEXT_SHADER_V), Some(TEXT_SHADER_F)) + }; + let vertex_shader = create_shader(TEXT_SHADER_V_PATH, gl::VERTEX_SHADER, vertex_src)?; + let fragment_shader = create_shader(TEXT_SHADER_F_PATH, gl::FRAGMENT_SHADER, fragment_src)?; + let program = create_program(vertex_shader, fragment_shader)?; + + unsafe { + gl::DeleteShader(fragment_shader); + gl::DeleteShader(vertex_shader); + gl::UseProgram(program); + } + + macro_rules! cptr { + ($thing:expr) => { + $thing.as_ptr() as *const _ + }; + } + + macro_rules! assert_uniform_valid { + ($uniform:expr) => { + assert!($uniform != gl::INVALID_VALUE as i32); + assert!($uniform != gl::INVALID_OPERATION as i32); + }; + ( $( $uniform:expr ),* ) => { + $( assert_uniform_valid!($uniform); )* + }; + } + + // get uniform locations + let (projection, cell_dim, background) = unsafe { + ( + gl::GetUniformLocation(program, cptr!(b"projection\0")), + gl::GetUniformLocation(program, cptr!(b"cellDim\0")), + gl::GetUniformLocation(program, cptr!(b"backgroundPass\0")), + ) + }; + + assert_uniform_valid!(projection, cell_dim, background); + + let shader = TextShaderProgram { + id: program, + u_projection: projection, + u_cell_dim: cell_dim, + u_background: background, + }; + + unsafe { + gl::UseProgram(0); + } + + Ok(shader) + } + + fn update_projection(&self, width: f32, height: f32, padding_x: f32, padding_y: f32) { + // Bounds check + if (width as u32) < (2 * padding_x as u32) || (height as u32) < (2 * padding_y as u32) { + return; + } + + // Compute scale and offset factors, from pixel to ndc space. Y is inverted + // [0, width - 2 * padding_x] to [-1, 1] + // [height - 2 * padding_y, 0] to [-1, 1] + let scale_x = 2. / (width - 2. * padding_x); + let scale_y = -2. / (height - 2. * padding_y); + let offset_x = -1.; + let offset_y = 1.; + + info!("Width: {}, Height: {}", width, height); + + unsafe { + gl::Uniform4f(self.u_projection, offset_x, offset_y, scale_x, scale_y); + } + } + + fn set_term_uniforms(&self, props: &term::SizeInfo) { + unsafe { + gl::Uniform2f(self.u_cell_dim, props.cell_width, props.cell_height); + } + } + + fn set_background_pass(&self, background_pass: bool) { + let value = if background_pass { 1 } else { 0 }; + + unsafe { + gl::Uniform1i(self.u_background, value); + } + } +} + +impl Drop for TextShaderProgram { + fn drop(&mut self) { + unsafe { + gl::DeleteProgram(self.id); + } + } +} + +impl RectShaderProgram { + pub fn new() -> Result<Self, ShaderCreationError> { + let (vertex_src, fragment_src) = if cfg!(feature = "live-shader-reload") { + (None, None) + } else { + (Some(RECT_SHADER_V), Some(RECT_SHADER_F)) + }; + let vertex_shader = create_shader(RECT_SHADER_V_PATH, gl::VERTEX_SHADER, vertex_src)?; + let fragment_shader = create_shader(RECT_SHADER_F_PATH, gl::FRAGMENT_SHADER, fragment_src)?; + let program = create_program(vertex_shader, fragment_shader)?; + + unsafe { + gl::DeleteShader(fragment_shader); + gl::DeleteShader(vertex_shader); + gl::UseProgram(program); + } + + // get uniform locations + let u_color = unsafe { gl::GetUniformLocation(program, b"color\0".as_ptr() as *const _) }; + + let shader = RectShaderProgram { id: program, u_color }; + + unsafe { gl::UseProgram(0) } + + Ok(shader) + } + + fn set_color(&self, color: Rgb, alpha: f32) { + unsafe { + gl::Uniform4f( + self.u_color, + f32::from(color.r) / 255., + f32::from(color.g) / 255., + f32::from(color.b) / 255., + alpha, + ); + } + } +} + +impl Drop for RectShaderProgram { + fn drop(&mut self) { + unsafe { + gl::DeleteProgram(self.id); + } + } +} + +fn create_program(vertex: GLuint, fragment: GLuint) -> Result<GLuint, ShaderCreationError> { + unsafe { + let program = gl::CreateProgram(); + gl::AttachShader(program, vertex); + gl::AttachShader(program, fragment); + gl::LinkProgram(program); + + let mut success: GLint = 0; + gl::GetProgramiv(program, gl::LINK_STATUS, &mut success); + + if success == i32::from(gl::TRUE) { + Ok(program) + } else { + Err(ShaderCreationError::Link(get_program_info_log(program))) + } + } +} + +fn create_shader( + path: &str, + kind: GLenum, + source: Option<&'static str>, +) -> Result<GLuint, ShaderCreationError> { + let from_disk; + let source = if let Some(src) = source { + src + } else { + from_disk = read_file(path)?; + &from_disk[..] + }; + + let len: [GLint; 1] = [source.len() as GLint]; + + let shader = unsafe { + let shader = gl::CreateShader(kind); + gl::ShaderSource(shader, 1, &(source.as_ptr() as *const _), len.as_ptr()); + gl::CompileShader(shader); + shader + }; + + let mut success: GLint = 0; + unsafe { + gl::GetShaderiv(shader, gl::COMPILE_STATUS, &mut success); + } + + if success == GLint::from(gl::TRUE) { + Ok(shader) + } else { + // Read log + let log = get_shader_info_log(shader); + + // Cleanup + unsafe { + gl::DeleteShader(shader); + } + + Err(ShaderCreationError::Compile(PathBuf::from(path), log)) + } +} + +fn get_program_info_log(program: GLuint) -> String { + // Get expected log length + let mut max_length: GLint = 0; + unsafe { + gl::GetProgramiv(program, gl::INFO_LOG_LENGTH, &mut max_length); + } + + // Read the info log + let mut actual_length: GLint = 0; + let mut buf: Vec<u8> = Vec::with_capacity(max_length as usize); + unsafe { + gl::GetProgramInfoLog(program, max_length, &mut actual_length, buf.as_mut_ptr() as *mut _); + } + + // Build a string + unsafe { + buf.set_len(actual_length as usize); + } + + // XXX should we expect opengl to return garbage? + String::from_utf8(buf).unwrap() +} + +fn get_shader_info_log(shader: GLuint) -> String { + // Get expected log length + let mut max_length: GLint = 0; + unsafe { + gl::GetShaderiv(shader, gl::INFO_LOG_LENGTH, &mut max_length); + } + + // Read the info log + let mut actual_length: GLint = 0; + let mut buf: Vec<u8> = Vec::with_capacity(max_length as usize); + unsafe { + gl::GetShaderInfoLog(shader, max_length, &mut actual_length, buf.as_mut_ptr() as *mut _); + } + + // Build a string + unsafe { + buf.set_len(actual_length as usize); + } + + // XXX should we expect opengl to return garbage? + String::from_utf8(buf).unwrap() +} + +fn read_file(path: &str) -> Result<String, io::Error> { + let mut f = File::open(path)?; + let mut buf = String::new(); + f.read_to_string(&mut buf)?; + + Ok(buf) +} + +#[derive(Debug)] +pub enum ShaderCreationError { + /// Error reading file + Io(io::Error), + + /// Error compiling shader + Compile(PathBuf, String), + + /// Problem linking + Link(String), +} + +impl ::std::error::Error for ShaderCreationError { + fn cause(&self) -> Option<&dyn (::std::error::Error)> { + match *self { + ShaderCreationError::Io(ref err) => Some(err), + _ => None, + } + } + + fn description(&self) -> &str { + match *self { + ShaderCreationError::Io(ref err) => err.description(), + ShaderCreationError::Compile(ref _path, ref s) => s.as_str(), + ShaderCreationError::Link(ref s) => s.as_str(), + } + } +} + +impl ::std::fmt::Display for ShaderCreationError { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + match *self { + ShaderCreationError::Io(ref err) => write!(f, "Couldn't read shader: {}", err), + ShaderCreationError::Compile(ref path, ref log) => { + write!(f, "Failed compiling shader at {}: {}", path.display(), log) + }, + ShaderCreationError::Link(ref log) => write!(f, "Failed linking shader: {}", log), + } + } +} + +impl From<io::Error> for ShaderCreationError { + fn from(val: io::Error) -> ShaderCreationError { + ShaderCreationError::Io(val) + } +} + +/// Manages a single texture atlas +/// +/// The strategy for filling an atlas looks roughly like this: +/// +/// ```ignore +/// (width, height) +/// ┌─────┬─────┬─────┬─────┬─────┐ +/// │ 10 │ │ │ │ │ <- Empty spaces; can be filled while +/// │ │ │ │ │ │ glyph_height < height - row_baseline +/// ├⎼⎼⎼⎼⎼┼⎼⎼⎼⎼⎼┼⎼⎼⎼⎼⎼┼⎼⎼⎼⎼⎼┼⎼⎼⎼⎼⎼┤ +/// │ 5 │ 6 │ 7 │ 8 │ 9 │ +/// │ │ │ │ │ │ +/// ├⎼⎼⎼⎼⎼┼⎼⎼⎼⎼⎼┼⎼⎼⎼⎼⎼┼⎼⎼⎼⎼⎼┴⎼⎼⎼⎼⎼┤ <- Row height is tallest glyph in row; this is +/// │ 1 │ 2 │ 3 │ 4 │ used as the baseline for the following row. +/// │ │ │ │ │ <- Row considered full when next glyph doesn't +/// └─────┴─────┴─────┴───────────┘ fit in the row. +/// (0, 0) x-> +/// ``` +#[derive(Debug)] +struct Atlas { + /// Texture id for this atlas + id: GLuint, + + /// Width of atlas + width: i32, + + /// Height of atlas + height: i32, + + /// Left-most free pixel in a row. + /// + /// This is called the extent because it is the upper bound of used pixels + /// in a row. + row_extent: i32, + + /// Baseline for glyphs in the current row + row_baseline: i32, + + /// Tallest glyph in current row + /// + /// This is used as the advance when end of row is reached + row_tallest: i32, +} + +/// Error that can happen when inserting a texture to the Atlas +enum AtlasInsertError { + /// Texture atlas is full + Full, + + /// The glyph cannot fit within a single texture + GlyphTooLarge, +} + +impl Atlas { + fn new(size: i32) -> Atlas { + let mut id: GLuint = 0; + unsafe { + gl::PixelStorei(gl::UNPACK_ALIGNMENT, 1); + gl::GenTextures(1, &mut id); + gl::BindTexture(gl::TEXTURE_2D, id); + gl::TexImage2D( + gl::TEXTURE_2D, + 0, + gl::RGB as i32, + size, + size, + 0, + gl::RGB, + gl::UNSIGNED_BYTE, + ptr::null(), + ); + + gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_S, gl::CLAMP_TO_EDGE as i32); + gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_T, gl::CLAMP_TO_EDGE as i32); + gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MIN_FILTER, gl::LINEAR as i32); + gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MAG_FILTER, gl::LINEAR as i32); + + gl::BindTexture(gl::TEXTURE_2D, 0); + } + + Atlas { id, width: size, height: size, row_extent: 0, row_baseline: 0, row_tallest: 0 } + } + + pub fn clear(&mut self) { + self.row_extent = 0; + self.row_baseline = 0; + self.row_tallest = 0; + } + + /// Insert a RasterizedGlyph into the texture atlas + pub fn insert( + &mut self, + glyph: &RasterizedGlyph, + active_tex: &mut u32, + ) -> Result<Glyph, AtlasInsertError> { + if glyph.width > self.width || glyph.height > self.height { + return Err(AtlasInsertError::GlyphTooLarge); + } + + // If there's not enough room in current row, go onto next one + if !self.room_in_row(glyph) { + self.advance_row()?; + } + + // If there's still not room, there's nothing that can be done here. + if !self.room_in_row(glyph) { + return Err(AtlasInsertError::Full); + } + + // There appears to be room; load the glyph. + Ok(self.insert_inner(glyph, active_tex)) + } + + /// Insert the glyph without checking for room + /// + /// Internal function for use once atlas has been checked for space. GL + /// errors could still occur at this point if we were checking for them; + /// hence, the Result. + fn insert_inner(&mut self, glyph: &RasterizedGlyph, active_tex: &mut u32) -> Glyph { + let offset_y = self.row_baseline; + let offset_x = self.row_extent; + let height = glyph.height as i32; + let width = glyph.width as i32; + + unsafe { + gl::BindTexture(gl::TEXTURE_2D, self.id); + + // Load data into OpenGL + gl::TexSubImage2D( + gl::TEXTURE_2D, + 0, + offset_x, + offset_y, + width, + height, + gl::RGB, + gl::UNSIGNED_BYTE, + glyph.buf.as_ptr() as *const _, + ); + + gl::BindTexture(gl::TEXTURE_2D, 0); + *active_tex = 0; + } + + // Update Atlas state + self.row_extent = offset_x + width; + if height > self.row_tallest { + self.row_tallest = height; + } + + // Generate UV coordinates + let uv_bot = offset_y as f32 / self.height as f32; + let uv_left = offset_x as f32 / self.width as f32; + let uv_height = height as f32 / self.height as f32; + let uv_width = width as f32 / self.width as f32; + + Glyph { + tex_id: self.id, + top: glyph.top as f32, + width: width as f32, + height: height as f32, + left: glyph.left as f32, + uv_bot, + uv_left, + uv_width, + uv_height, + } + } + + /// Check if there's room in the current row for given glyph + fn room_in_row(&self, raw: &RasterizedGlyph) -> bool { + let next_extent = self.row_extent + raw.width as i32; + let enough_width = next_extent <= self.width; + let enough_height = (raw.height as i32) < (self.height - self.row_baseline); + + enough_width && enough_height + } + + /// Mark current row as finished and prepare to insert into the next row + fn advance_row(&mut self) -> Result<(), AtlasInsertError> { + let advance_to = self.row_baseline + self.row_tallest; + if self.height - advance_to <= 0 { + return Err(AtlasInsertError::Full); + } + + self.row_baseline = advance_to; + self.row_extent = 0; + self.row_tallest = 0; + + Ok(()) + } +} diff --git a/alacritty_terminal/src/renderer/rects.rs b/alacritty_terminal/src/renderer/rects.rs new file mode 100644 index 00000000..b4f8a012 --- /dev/null +++ b/alacritty_terminal/src/renderer/rects.rs @@ -0,0 +1,156 @@ +// 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. +use font::Metrics; + +use crate::index::Point; +use crate::term::cell::Flags; +use crate::term::color::Rgb; +use crate::term::{RenderableCell, SizeInfo}; + +#[derive(Debug, Copy, Clone)] +pub struct Rect<T> { + pub x: T, + pub y: T, + pub width: T, + pub height: T, +} + +impl<T> Rect<T> { + pub fn new(x: T, y: T, width: T, height: T) -> Self { + Rect { x, y, width, height } + } +} + +#[derive(Debug)] +struct Line { + flag: Flags, + range: Option<(RenderableCell, Point)>, +} + +impl Line { + fn new(flag: Flags) -> Self { + Self { flag, range: None } + } +} + +/// Rects for underline, strikeout and more. +pub struct Rects<'a> { + inner: Vec<(Rect<f32>, Rgb)>, + active_lines: Vec<Line>, + metrics: &'a Metrics, + size: &'a SizeInfo, +} + +impl<'a> Rects<'a> { + pub fn new(metrics: &'a Metrics, size: &'a SizeInfo) -> Self { + let active_lines = vec![Line::new(Flags::UNDERLINE), Line::new(Flags::STRIKEOUT)]; + Self { inner: Vec::new(), active_lines, metrics, size } + } + + /// Convert the stored rects to rectangles for the renderer. + pub fn rects(&self) -> &Vec<(Rect<f32>, Rgb)> { + &self.inner + } + + /// Update the stored lines with the next cell info. + pub fn update_lines(&mut self, size_info: &SizeInfo, cell: &RenderableCell) { + for line in self.active_lines.iter_mut() { + match line.range { + // Check for end if line is present + Some((ref mut start, ref mut end)) => { + // No change in line + if cell.line == start.line + && cell.flags.contains(line.flag) + && cell.fg == start.fg + && cell.column == end.col + 1 + { + if size_info.cols() == cell.column && size_info.lines() == cell.line { + // Add the last rect if we've reached the end of the terminal + self.inner.push(create_rect( + &start, + cell.into(), + line.flag, + &self.metrics, + &self.size, + )); + } else { + // Update the length of the line + *end = cell.into(); + } + + continue; + } + + self.inner.push(create_rect(start, *end, line.flag, &self.metrics, &self.size)); + + // Start a new line if the flag is present + if cell.flags.contains(line.flag) { + *start = cell.clone(); + *end = cell.into(); + } else { + line.range = None; + } + }, + // Check for new start of line + None => { + if cell.flags.contains(line.flag) { + line.range = Some((cell.clone(), cell.into())); + } + }, + }; + } + } + + // Add a rectangle + pub fn push(&mut self, rect: Rect<f32>, color: Rgb) { + self.inner.push((rect, color)); + } +} + +/// Create a rectangle that starts on the left of `start` and ends on the right +/// of `end`, based on the given flag and size metrics. +fn create_rect( + start: &RenderableCell, + end: Point, + flag: Flags, + metrics: &Metrics, + size: &SizeInfo, +) -> (Rect<f32>, Rgb) { + let start_x = start.column.0 as f32 * size.cell_width; + let end_x = (end.col.0 + 1) as f32 * size.cell_width; + let width = end_x - start_x; + + let (position, mut height) = match flag { + Flags::UNDERLINE => (metrics.underline_position, metrics.underline_thickness), + Flags::STRIKEOUT => (metrics.strikeout_position, metrics.strikeout_thickness), + _ => unimplemented!("Invalid flag for cell line drawing specified"), + }; + + // Make sure lines are always visible + height = height.max(1.); + + let cell_bottom = (start.line.0 as f32 + 1.) * size.cell_height; + let baseline = cell_bottom + metrics.descent; + + let mut y = baseline - position - height / 2.; + let max_y = cell_bottom - height; + if y > max_y { + y = max_y; + } + + let rect = + Rect::new(start_x + size.padding_x, y.round() + size.padding_y, width, height.round()); + + (rect, start.fg) +} diff --git a/alacritty_terminal/src/selection.rs b/alacritty_terminal/src/selection.rs new file mode 100644 index 00000000..d2009586 --- /dev/null +++ b/alacritty_terminal/src/selection.rs @@ -0,0 +1,571 @@ +// 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. + +//! State management for a selection in the grid +//! +//! A selection should start when the mouse is clicked, and it should be +//! finalized when the button is released. The selection should be cleared +//! when text is added/removed/scrolled on the screen. The selection should +//! also be cleared if the user clicks off of the selection. +use std::cmp::{max, min}; +use std::ops::Range; + +use crate::index::{Column, Point, Side}; +use crate::term::Search; + +/// Describes a region of a 2-dimensional area +/// +/// Used to track a text selection. There are three supported modes, each with its own constructor: +/// [`simple`], [`semantic`], and [`lines`]. The [`simple`] mode precisely tracks which cells are +/// selected without any expansion. [`semantic`] mode expands the initial selection to the nearest +/// semantic escape char in either direction. [`lines`] will always select entire lines. +/// +/// Calls to [`update`] operate different based on the selection kind. The [`simple`] mode does +/// nothing special, simply tracks points and sides. [`semantic`] will continue to expand out to +/// semantic boundaries as the selection point changes. Similarly, [`lines`] will always expand the +/// new point to encompass entire lines. +/// +/// [`simple`]: enum.Selection.html#method.simple +/// [`semantic`]: enum.Selection.html#method.semantic +/// [`lines`]: enum.Selection.html#method.lines +#[derive(Debug, Clone, PartialEq)] +pub enum Selection { + Simple { + /// The region representing start and end of cursor movement + region: Range<Anchor>, + }, + Semantic { + /// The region representing start and end of cursor movement + region: Range<Point<isize>>, + }, + Lines { + /// The region representing start and end of cursor movement + region: Range<Point<isize>>, + + /// The line under the initial point. This is always selected regardless + /// of which way the cursor is moved. + initial_line: isize, + }, +} + +/// A Point and side within that point. +#[derive(Debug, Clone, PartialEq)] +pub struct Anchor { + point: Point<isize>, + side: Side, +} + +impl Anchor { + fn new(point: Point<isize>, side: Side) -> Anchor { + Anchor { point, side } + } +} + +/// A type that has 2-dimensional boundaries +pub trait Dimensions { + /// Get the size of the area + fn dimensions(&self) -> Point; +} + +impl Selection { + pub fn simple(location: Point<usize>, side: Side) -> Selection { + Selection::Simple { + region: Range { + start: Anchor::new(location.into(), side), + end: Anchor::new(location.into(), side), + }, + } + } + + pub fn rotate(&mut self, offset: isize) { + match *self { + Selection::Simple { ref mut region } => { + region.start.point.line += offset; + region.end.point.line += offset; + }, + Selection::Semantic { ref mut region } => { + region.start.line += offset; + region.end.line += offset; + }, + Selection::Lines { ref mut region, ref mut initial_line } => { + region.start.line += offset; + region.end.line += offset; + *initial_line += offset; + }, + } + } + + pub fn semantic(point: Point<usize>) -> Selection { + Selection::Semantic { region: Range { start: point.into(), end: point.into() } } + } + + pub fn lines(point: Point<usize>) -> Selection { + Selection::Lines { + region: Range { start: point.into(), end: point.into() }, + initial_line: point.line as isize, + } + } + + pub fn update(&mut self, location: Point<usize>, side: Side) { + // Always update the `end`; can normalize later during span generation. + match *self { + Selection::Simple { ref mut region } => { + region.end = Anchor::new(location.into(), side); + }, + Selection::Semantic { ref mut region } | Selection::Lines { ref mut region, .. } => { + region.end = location.into(); + }, + } + } + + pub fn to_span<G>(&self, grid: &G, alt_screen: bool) -> Option<Span> + where + G: Search + Dimensions, + { + match *self { + Selection::Simple { ref region } => Selection::span_simple(grid, region, alt_screen), + Selection::Semantic { ref region } => { + Selection::span_semantic(grid, region, alt_screen) + }, + Selection::Lines { ref region, initial_line } => { + Selection::span_lines(grid, region, initial_line, alt_screen) + }, + } + } + + pub fn is_empty(&self) -> bool { + match *self { + Selection::Simple { ref region } => { + region.start == region.end && region.start.side == region.end.side + }, + Selection::Semantic { .. } | Selection::Lines { .. } => false, + } + } + + fn span_semantic<G>(grid: &G, region: &Range<Point<isize>>, alt_screen: bool) -> Option<Span> + where + G: Search + Dimensions, + { + let cols = grid.dimensions().col; + let lines = grid.dimensions().line.0 as isize; + + // Normalize ordering of selected cells + let (mut front, mut tail) = if region.start < region.end { + (region.start, region.end) + } else { + (region.end, region.start) + }; + + if alt_screen { + Selection::alt_screen_clamp(&mut front, &mut tail, lines, cols)?; + } + + let (mut start, mut end) = if front < tail && front.line == tail.line { + (grid.semantic_search_left(front.into()), grid.semantic_search_right(tail.into())) + } else { + (grid.semantic_search_right(front.into()), grid.semantic_search_left(tail.into())) + }; + + if start > end { + ::std::mem::swap(&mut start, &mut end); + } + + Some(Span { cols, front: start, tail: end, ty: SpanType::Inclusive }) + } + + fn span_lines<G>( + grid: &G, + region: &Range<Point<isize>>, + initial_line: isize, + alt_screen: bool, + ) -> Option<Span> + where + G: Dimensions, + { + let cols = grid.dimensions().col; + let lines = grid.dimensions().line.0 as isize; + + // First, create start and end points based on initial line and the grid + // dimensions. + let mut start = Point { col: cols - 1, line: initial_line }; + let mut end = Point { col: Column(0), line: initial_line }; + + // Now, expand lines based on where cursor started and ended. + if region.start.line < region.end.line { + // Start is below end + start.line = min(start.line, region.start.line); + end.line = max(end.line, region.end.line); + } else { + // Start is above end + start.line = min(start.line, region.end.line); + end.line = max(end.line, region.start.line); + } + + if alt_screen { + Selection::alt_screen_clamp(&mut start, &mut end, lines, cols)?; + } + + Some(Span { cols, front: start.into(), tail: end.into(), ty: SpanType::Inclusive }) + } + + fn span_simple<G>(grid: &G, region: &Range<Anchor>, alt_screen: bool) -> Option<Span> + where + G: Dimensions, + { + let start = region.start.point; + let start_side = region.start.side; + let end = region.end.point; + let end_side = region.end.side; + let cols = grid.dimensions().col; + let lines = grid.dimensions().line.0 as isize; + + // Make sure front is always the "bottom" and tail is always the "top" + let (mut front, mut tail, front_side, tail_side) = + if start.line > end.line || start.line == end.line && start.col <= end.col { + // Selected upward; start/end are swapped + (end, start, end_side, start_side) + } else { + // Selected downward; no swapping + (start, end, start_side, end_side) + }; + + // No selection for single cell with identical sides or two cell with right+left sides + if (front == tail && front_side == tail_side) + || (tail_side == Side::Right + && front_side == Side::Left + && front.line == tail.line + && front.col == tail.col + 1) + { + return None; + } + + // Remove last cell if selection ends to the left of a cell + if front_side == Side::Left && start != end { + // Special case when selection starts to left of first cell + if front.col == Column(0) { + front.col = cols - 1; + front.line += 1; + } else { + front.col -= 1; + } + } + + // Remove first cell if selection starts at the right of a cell + if tail_side == Side::Right && front != tail { + tail.col += 1; + } + + if alt_screen { + Selection::alt_screen_clamp(&mut front, &mut tail, lines, cols)?; + } + + // Return the selection with all cells inclusive + Some(Span { cols, front: front.into(), tail: tail.into(), ty: SpanType::Inclusive }) + } + + // Clamp selection in the alternate screen to the visible region + fn alt_screen_clamp( + front: &mut Point<isize>, + tail: &mut Point<isize>, + lines: isize, + cols: Column, + ) -> Option<()> { + if tail.line >= lines { + // Don't show selection above visible region + if front.line >= lines { + return None; + } + + // Clamp selection above viewport to visible region + tail.line = lines - 1; + tail.col = Column(0); + } + + if front.line < 0 { + // Don't show selection below visible region + if tail.line < 0 { + return None; + } + + // Clamp selection below viewport to visible region + front.line = 0; + front.col = cols - 1; + } + + Some(()) + } +} + +/// How to interpret the locations of a Span. +#[derive(Debug, Eq, PartialEq)] +pub enum SpanType { + /// Includes the beginning and end locations + Inclusive, + + /// Exclude both beginning and end + Exclusive, + + /// Excludes last cell of selection + ExcludeTail, + + /// Excludes first cell of selection + ExcludeFront, +} + +/// Represents a span of selected cells +#[derive(Debug, Eq, PartialEq)] +pub struct Span { + front: Point<usize>, + tail: Point<usize>, + cols: Column, + + /// The type says whether ends are included or not. + ty: SpanType, +} + +#[derive(Debug)] +pub struct Locations { + /// Start point from bottom of buffer + pub start: Point<usize>, + /// End point towards top of buffer + pub end: Point<usize>, +} + +impl Span { + pub fn to_locations(&self) -> Locations { + let (start, end) = match self.ty { + SpanType::Inclusive => (self.front, self.tail), + SpanType::Exclusive => { + (Span::wrap_start(self.front, self.cols), Span::wrap_end(self.tail, self.cols)) + }, + SpanType::ExcludeFront => (Span::wrap_start(self.front, self.cols), self.tail), + SpanType::ExcludeTail => (self.front, Span::wrap_end(self.tail, self.cols)), + }; + + Locations { start, end } + } + + fn wrap_start(mut start: Point<usize>, cols: Column) -> Point<usize> { + if start.col == cols - 1 { + Point { line: start.line + 1, col: Column(0) } + } else { + start.col += 1; + start + } + } + + fn wrap_end(end: Point<usize>, cols: Column) -> Point<usize> { + if end.col == Column(0) && end.line != 0 { + Point { line: end.line - 1, col: cols } + } else { + Point { line: end.line, col: end.col - 1 } + } + } +} + +/// Tests for selection +/// +/// There are comments on all of the tests describing the selection. Pictograms +/// are used to avoid ambiguity. Grid cells are represented by a [ ]. Only +/// cells that are completely covered are counted in a selection. Ends are +/// represented by `B` and `E` for begin and end, respectively. A selected cell +/// looks like [XX], [BX] (at the start), [XB] (at the end), [XE] (at the end), +/// and [EX] (at the start), or [BE] for a single cell. Partially selected cells +/// look like [ B] and [E ]. +#[cfg(test)] +mod test { + use super::{Selection, Span, SpanType}; + use crate::index::{Column, Line, Point, Side}; + use crate::url::Url; + + struct Dimensions(Point); + impl super::Dimensions for Dimensions { + fn dimensions(&self) -> Point { + self.0 + } + } + + impl Dimensions { + pub fn new(line: usize, col: usize) -> Self { + Dimensions(Point { line: Line(line), col: Column(col) }) + } + } + + impl super::Search for Dimensions { + fn semantic_search_left(&self, point: Point<usize>) -> Point<usize> { + point + } + + fn semantic_search_right(&self, point: Point<usize>) -> Point<usize> { + point + } + + fn url_search(&self, _: Point<usize>) -> Option<Url> { + None + } + } + + /// Test case of single cell selection + /// + /// 1. [ ] + /// 2. [B ] + /// 3. [BE] + #[test] + fn single_cell_left_to_right() { + let location = Point { line: 0, col: Column(0) }; + let mut selection = Selection::simple(location, Side::Left); + selection.update(location, Side::Right); + + assert_eq!(selection.to_span(&Dimensions::new(1, 1), false).unwrap(), Span { + cols: Column(1), + ty: SpanType::Inclusive, + front: location, + tail: location + }); + } + + /// Test case of single cell selection + /// + /// 1. [ ] + /// 2. [ B] + /// 3. [EB] + #[test] + fn single_cell_right_to_left() { + let location = Point { line: 0, col: Column(0) }; + let mut selection = Selection::simple(location, Side::Right); + selection.update(location, Side::Left); + + assert_eq!(selection.to_span(&Dimensions::new(1, 1), false).unwrap(), Span { + cols: Column(1), + ty: SpanType::Inclusive, + front: location, + tail: location + }); + } + + /// Test adjacent cell selection from left to right + /// + /// 1. [ ][ ] + /// 2. [ B][ ] + /// 3. [ B][E ] + #[test] + fn between_adjacent_cells_left_to_right() { + let mut selection = Selection::simple(Point::new(0, Column(0)), Side::Right); + selection.update(Point::new(0, Column(1)), Side::Left); + + assert_eq!(selection.to_span(&Dimensions::new(1, 2), false), None); + } + + /// Test adjacent cell selection from right to left + /// + /// 1. [ ][ ] + /// 2. [ ][B ] + /// 3. [ E][B ] + #[test] + fn between_adjacent_cells_right_to_left() { + let mut selection = Selection::simple(Point::new(0, Column(1)), Side::Left); + selection.update(Point::new(0, Column(0)), Side::Right); + + assert_eq!(selection.to_span(&Dimensions::new(1, 2), false), None); + } + + /// Test selection across adjacent lines + /// + /// + /// 1. [ ][ ][ ][ ][ ] + /// [ ][ ][ ][ ][ ] + /// 2. [ ][ B][ ][ ][ ] + /// [ ][ ][ ][ ][ ] + /// 3. [ ][ B][XX][XX][XX] + /// [XX][XE][ ][ ][ ] + #[test] + fn across_adjacent_lines_upward_final_cell_exclusive() { + let mut selection = Selection::simple(Point::new(1, Column(1)), Side::Right); + selection.update(Point::new(0, Column(1)), Side::Right); + + assert_eq!(selection.to_span(&Dimensions::new(2, 5), false).unwrap(), Span { + cols: Column(5), + front: Point::new(0, Column(1)), + tail: Point::new(1, Column(2)), + ty: SpanType::Inclusive, + }); + } + + /// Test selection across adjacent lines + /// + /// + /// 1. [ ][ ][ ][ ][ ] + /// [ ][ ][ ][ ][ ] + /// 2. [ ][ ][ ][ ][ ] + /// [ ][ B][ ][ ][ ] + /// 3. [ ][ E][XX][XX][XX] + /// [XX][XB][ ][ ][ ] + /// 4. [ E][XX][XX][XX][XX] + /// [XX][XB][ ][ ][ ] + #[test] + fn selection_bigger_then_smaller() { + let mut selection = Selection::simple(Point::new(0, Column(1)), Side::Right); + selection.update(Point::new(1, Column(1)), Side::Right); + selection.update(Point::new(1, Column(0)), Side::Right); + + assert_eq!(selection.to_span(&Dimensions::new(2, 5), false).unwrap(), Span { + cols: Column(5), + front: Point::new(0, Column(1)), + tail: Point::new(1, Column(1)), + ty: SpanType::Inclusive, + }); + } + + #[test] + fn alt_scren_lines() { + let mut selection = Selection::lines(Point::new(0, Column(0))); + selection.update(Point::new(5, Column(3)), Side::Right); + selection.rotate(-3); + + assert_eq!(selection.to_span(&Dimensions::new(10, 5), true).unwrap(), Span { + cols: Column(5), + front: Point::new(0, Column(4)), + tail: Point::new(2, Column(0)), + ty: SpanType::Inclusive, + }); + } + + #[test] + fn alt_screen_semantic() { + let mut selection = Selection::semantic(Point::new(0, Column(0))); + selection.update(Point::new(5, Column(3)), Side::Right); + selection.rotate(-3); + + assert_eq!(selection.to_span(&Dimensions::new(10, 5), true).unwrap(), Span { + cols: Column(5), + front: Point::new(0, Column(4)), + tail: Point::new(2, Column(3)), + ty: SpanType::Inclusive, + }); + } + + #[test] + fn alt_screen_simple() { + let mut selection = Selection::simple(Point::new(0, Column(0)), Side::Right); + selection.update(Point::new(5, Column(3)), Side::Right); + selection.rotate(-3); + + assert_eq!(selection.to_span(&Dimensions::new(10, 5), true).unwrap(), Span { + cols: Column(5), + front: Point::new(0, Column(4)), + tail: Point::new(2, Column(4)), + ty: SpanType::Inclusive, + }); + } +} diff --git a/alacritty_terminal/src/sync.rs b/alacritty_terminal/src/sync.rs new file mode 100644 index 00000000..0fcd0862 --- /dev/null +++ b/alacritty_terminal/src/sync.rs @@ -0,0 +1,44 @@ +// 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. + +//! Synchronization types +//! +//! Most importantly, a fair mutex is included +use parking_lot::{Mutex, MutexGuard}; + +/// A fair mutex +/// +/// Uses an extra lock to ensure that if one thread is waiting that it will get +/// the lock before a single thread can re-lock it. +pub struct FairMutex<T> { + /// Data + data: Mutex<T>, + /// Next-to-access + next: Mutex<()>, +} + +impl<T> FairMutex<T> { + /// Create a new fair mutex + pub fn new(data: T) -> FairMutex<T> { + FairMutex { data: Mutex::new(data), next: Mutex::new(()) } + } + + /// Lock the mutex + pub fn lock(&self) -> MutexGuard<'_, T> { + // Must bind to a temporary or the lock will be freed before going + // into data.lock() + let _next = self.next.lock(); + self.data.lock() + } +} diff --git a/alacritty_terminal/src/term/cell.rs b/alacritty_terminal/src/term/cell.rs new file mode 100644 index 00000000..4d2f4c1c --- /dev/null +++ b/alacritty_terminal/src/term/cell.rs @@ -0,0 +1,205 @@ +// 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. +use bitflags::bitflags; + +use crate::ansi::{Color, NamedColor}; +use crate::grid::{self, GridCell}; +use crate::index::Column; + +// Maximum number of zerowidth characters which will be stored per cell. +pub const MAX_ZEROWIDTH_CHARS: usize = 5; + +bitflags! { + #[derive(Serialize, Deserialize)] + pub struct Flags: u16 { + const INVERSE = 0b00_0000_0001; + const BOLD = 0b00_0000_0010; + const ITALIC = 0b00_0000_0100; + const UNDERLINE = 0b00_0000_1000; + const WRAPLINE = 0b00_0001_0000; + const WIDE_CHAR = 0b00_0010_0000; + const WIDE_CHAR_SPACER = 0b00_0100_0000; + const DIM = 0b00_1000_0000; + const DIM_BOLD = 0b00_1000_0010; + const HIDDEN = 0b01_0000_0000; + const STRIKEOUT = 0b10_0000_0000; + } +} + +const fn default_extra() -> [char; MAX_ZEROWIDTH_CHARS] { + [' '; MAX_ZEROWIDTH_CHARS] +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] +pub struct Cell { + pub c: char, + pub fg: Color, + pub bg: Color, + pub flags: Flags, + #[serde(default = "default_extra")] + pub extra: [char; MAX_ZEROWIDTH_CHARS], +} + +impl Default for Cell { + fn default() -> Cell { + Cell::new(' ', Color::Named(NamedColor::Foreground), Color::Named(NamedColor::Background)) + } +} + +impl GridCell for Cell { + #[inline] + fn is_empty(&self) -> bool { + (self.c == ' ' || self.c == '\t') + && self.extra[0] == ' ' + && self.bg == Color::Named(NamedColor::Background) + && !self + .flags + .intersects(Flags::INVERSE | Flags::UNDERLINE | Flags::STRIKEOUT | Flags::WRAPLINE) + } + + #[inline] + fn is_wrap(&self) -> bool { + self.flags.contains(Flags::WRAPLINE) + } + + #[inline] + fn set_wrap(&mut self, wrap: bool) { + if wrap { + self.flags.insert(Flags::WRAPLINE); + } else { + self.flags.remove(Flags::WRAPLINE); + } + } +} + +/// Get the length of occupied cells in a line +pub trait LineLength { + /// Calculate the occupied line length + fn line_length(&self) -> Column; +} + +impl LineLength for grid::Row<Cell> { + fn line_length(&self) -> Column { + let mut length = Column(0); + + if self[Column(self.len() - 1)].flags.contains(Flags::WRAPLINE) { + return Column(self.len()); + } + + for (index, cell) in self[..].iter().rev().enumerate() { + if cell.c != ' ' || cell.extra[0] != ' ' { + length = Column(self.len() - index); + break; + } + } + + length + } +} + +impl Cell { + #[inline] + pub fn bold(&self) -> bool { + self.flags.contains(Flags::BOLD) + } + + #[inline] + pub fn inverse(&self) -> bool { + self.flags.contains(Flags::INVERSE) + } + + #[inline] + pub fn dim(&self) -> bool { + self.flags.contains(Flags::DIM) + } + + pub fn new(c: char, fg: Color, bg: Color) -> Cell { + Cell { extra: [' '; MAX_ZEROWIDTH_CHARS], c, bg, fg, flags: Flags::empty() } + } + + #[inline] + pub fn reset(&mut self, template: &Cell) { + // memcpy template to self + *self = *template; + } + + #[inline] + pub fn chars(&self) -> [char; MAX_ZEROWIDTH_CHARS + 1] { + unsafe { + let mut chars = [std::mem::uninitialized(); MAX_ZEROWIDTH_CHARS + 1]; + std::ptr::write(&mut chars[0], self.c); + std::ptr::copy_nonoverlapping( + self.extra.as_ptr(), + chars.as_mut_ptr().offset(1), + self.extra.len(), + ); + chars + } + } + + #[inline] + pub fn push_extra(&mut self, c: char) { + for elem in self.extra.iter_mut() { + if elem == &' ' { + *elem = c; + break; + } + } + } +} + +#[cfg(test)] +mod tests { + use super::{Cell, LineLength}; + + use crate::grid::Row; + use crate::index::Column; + + #[test] + fn line_length_works() { + let template = Cell::default(); + let mut row = Row::new(Column(10), &template); + row[Column(5)].c = 'a'; + + assert_eq!(row.line_length(), Column(6)); + } + + #[test] + fn line_length_works_with_wrapline() { + let template = Cell::default(); + let mut row = Row::new(Column(10), &template); + row[Column(9)].flags.insert(super::Flags::WRAPLINE); + + assert_eq!(row.line_length(), Column(10)); + } +} + +#[cfg(all(test, feature = "bench"))] +mod benches { + extern crate test; + use super::Cell; + + #[bench] + fn cell_reset(b: &mut test::Bencher) { + b.iter(|| { + let mut cell = Cell::default(); + + for _ in 0..100 { + cell.reset(test::black_box(&Cell::default())); + } + + test::black_box(cell); + }); + } +} diff --git a/alacritty_terminal/src/term/color.rs b/alacritty_terminal/src/term/color.rs new file mode 100644 index 00000000..39def612 --- /dev/null +++ b/alacritty_terminal/src/term/color.rs @@ -0,0 +1,224 @@ +use std::fmt; +use std::ops::{Index, IndexMut, Mul}; + +use crate::ansi; +use crate::config::Colors; + +pub const COUNT: usize = 270; + +pub const RED: Rgb = Rgb { r: 0xff, g: 0x0, b: 0x0 }; +pub const YELLOW: Rgb = Rgb { r: 0xff, g: 0xff, b: 0x0 }; + +#[derive(Debug, Eq, PartialEq, Copy, Clone, Default, Serialize, Deserialize)] +pub struct Rgb { + pub r: u8, + pub g: u8, + pub b: u8, +} + +// a multiply function for Rgb, as the default dim is just *2/3 +impl Mul<f32> for Rgb { + type Output = Rgb; + + fn mul(self, rhs: f32) -> Rgb { + let result = Rgb { + r: (f32::from(self.r) * rhs).max(0.0).min(255.0) as u8, + g: (f32::from(self.g) * rhs).max(0.0).min(255.0) as u8, + b: (f32::from(self.b) * rhs).max(0.0).min(255.0) as u8, + }; + + trace!("Scaling RGB by {} from {:?} to {:?}", rhs, self, result); + + result + } +} + +/// List of indexed colors +/// +/// The first 16 entries are the standard ansi named colors. Items 16..232 are +/// the color cube. Items 233..256 are the grayscale ramp. Item 256 is +/// the configured foreground color, item 257 is the configured background +/// color, item 258 is the cursor foreground color, item 259 is the cursor +/// background color. Following that are 8 positions for dim colors. +/// Item 268 is the bright foreground color, 269 the dim foreground. +#[derive(Copy, Clone)] +pub struct List([Rgb; COUNT]); + +impl<'a> From<&'a Colors> for List { + fn from(colors: &Colors) -> List { + // Type inference fails without this annotation + let mut list: List = unsafe { ::std::mem::uninitialized() }; + + list.fill_named(colors); + list.fill_cube(colors); + list.fill_gray_ramp(colors); + + list + } +} + +impl List { + pub fn fill_named(&mut self, colors: &Colors) { + // Normals + self[ansi::NamedColor::Black] = colors.normal.black; + self[ansi::NamedColor::Red] = colors.normal.red; + self[ansi::NamedColor::Green] = colors.normal.green; + self[ansi::NamedColor::Yellow] = colors.normal.yellow; + self[ansi::NamedColor::Blue] = colors.normal.blue; + self[ansi::NamedColor::Magenta] = colors.normal.magenta; + self[ansi::NamedColor::Cyan] = colors.normal.cyan; + self[ansi::NamedColor::White] = colors.normal.white; + + // Brights + self[ansi::NamedColor::BrightBlack] = colors.bright.black; + self[ansi::NamedColor::BrightRed] = colors.bright.red; + self[ansi::NamedColor::BrightGreen] = colors.bright.green; + self[ansi::NamedColor::BrightYellow] = colors.bright.yellow; + self[ansi::NamedColor::BrightBlue] = colors.bright.blue; + self[ansi::NamedColor::BrightMagenta] = colors.bright.magenta; + self[ansi::NamedColor::BrightCyan] = colors.bright.cyan; + self[ansi::NamedColor::BrightWhite] = colors.bright.white; + self[ansi::NamedColor::BrightForeground] = + colors.primary.bright_foreground.unwrap_or(colors.primary.foreground); + + // Foreground and background + self[ansi::NamedColor::Foreground] = colors.primary.foreground; + self[ansi::NamedColor::Background] = colors.primary.background; + + // Foreground and background for custom cursor colors + self[ansi::NamedColor::CursorText] = colors.cursor.text.unwrap_or_else(Rgb::default); + self[ansi::NamedColor::Cursor] = colors.cursor.cursor.unwrap_or_else(Rgb::default); + + // Dims + self[ansi::NamedColor::DimForeground] = + colors.primary.dim_foreground.unwrap_or(colors.primary.foreground * 0.66); + match colors.dim { + Some(ref dim) => { + trace!("Using config-provided dim colors"); + self[ansi::NamedColor::DimBlack] = dim.black; + self[ansi::NamedColor::DimRed] = dim.red; + self[ansi::NamedColor::DimGreen] = dim.green; + self[ansi::NamedColor::DimYellow] = dim.yellow; + self[ansi::NamedColor::DimBlue] = dim.blue; + self[ansi::NamedColor::DimMagenta] = dim.magenta; + self[ansi::NamedColor::DimCyan] = dim.cyan; + self[ansi::NamedColor::DimWhite] = dim.white; + }, + None => { + trace!("Deriving dim colors from normal colors"); + self[ansi::NamedColor::DimBlack] = colors.normal.black * 0.66; + self[ansi::NamedColor::DimRed] = colors.normal.red * 0.66; + self[ansi::NamedColor::DimGreen] = colors.normal.green * 0.66; + self[ansi::NamedColor::DimYellow] = colors.normal.yellow * 0.66; + self[ansi::NamedColor::DimBlue] = colors.normal.blue * 0.66; + self[ansi::NamedColor::DimMagenta] = colors.normal.magenta * 0.66; + self[ansi::NamedColor::DimCyan] = colors.normal.cyan * 0.66; + self[ansi::NamedColor::DimWhite] = colors.normal.white * 0.66; + }, + } + } + + pub fn fill_cube(&mut self, colors: &Colors) { + let mut index: usize = 16; + // Build colors + for r in 0..6 { + for g in 0..6 { + for b in 0..6 { + // Override colors 16..232 with the config (if present) + if let Some(indexed_color) = + colors.indexed_colors.iter().find(|ic| ic.index == index as u8) + { + self[index] = indexed_color.color; + } else { + self[index] = Rgb { + r: if r == 0 { 0 } else { r * 40 + 55 }, + b: if b == 0 { 0 } else { b * 40 + 55 }, + g: if g == 0 { 0 } else { g * 40 + 55 }, + }; + } + index += 1; + } + } + } + + debug_assert!(index == 232); + } + + pub fn fill_gray_ramp(&mut self, colors: &Colors) { + let mut index: usize = 232; + + for i in 0..24 { + // Index of the color is number of named colors + number of cube colors + i + let color_index = 16 + 216 + i; + + // Override colors 232..256 with the config (if present) + if let Some(indexed_color) = + colors.indexed_colors.iter().find(|ic| ic.index == color_index) + { + self[index] = indexed_color.color; + index += 1; + continue; + } + + let value = i * 10 + 8; + self[index] = Rgb { r: value, g: value, b: value }; + index += 1; + } + + debug_assert!(index == 256); + } +} + +impl fmt::Debug for List { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("List[..]") + } +} + +impl Index<ansi::NamedColor> for List { + type Output = Rgb; + + #[inline] + fn index(&self, idx: ansi::NamedColor) -> &Self::Output { + &self.0[idx as usize] + } +} + +impl IndexMut<ansi::NamedColor> for List { + #[inline] + fn index_mut(&mut self, idx: ansi::NamedColor) -> &mut Self::Output { + &mut self.0[idx as usize] + } +} + +impl Index<usize> for List { + type Output = Rgb; + + #[inline] + fn index(&self, idx: usize) -> &Self::Output { + &self.0[idx] + } +} + +impl IndexMut<usize> for List { + #[inline] + fn index_mut(&mut self, idx: usize) -> &mut Self::Output { + &mut self.0[idx] + } +} + +impl Index<u8> for List { + type Output = Rgb; + + #[inline] + fn index(&self, idx: u8) -> &Self::Output { + &self.0[idx as usize] + } +} + +impl IndexMut<u8> for List { + #[inline] + fn index_mut(&mut self, idx: u8) -> &mut Self::Output { + &mut self.0[idx as usize] + } +} diff --git a/alacritty_terminal/src/term/mod.rs b/alacritty_terminal/src/term/mod.rs new file mode 100644 index 00000000..94b2ade2 --- /dev/null +++ b/alacritty_terminal/src/term/mod.rs @@ -0,0 +1,2442 @@ +// 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. +// +//! Exports the `Term` type which is a high-level API for the Grid +use std::cmp::{max, min}; +use std::ops::{Index, IndexMut, Range, RangeInclusive}; +use std::time::{Duration, Instant}; +use std::{io, mem, ptr}; + +use copypasta::{Clipboard, Load, Store}; +use font::{self, RasterizedGlyph, Size}; +use glutin::MouseCursor; +use unicode_width::UnicodeWidthChar; + +use crate::ansi::{ + self, Attr, CharsetIndex, Color, CursorStyle, Handler, NamedColor, StandardCharset, +}; +use crate::config::{Config, VisualBellAnimation}; +use crate::cursor; +use crate::grid::{ + BidirectionalIterator, DisplayIter, Grid, GridCell, IndexRegion, Indexed, Scroll, + ViewportPosition, +}; +use crate::index::{self, Column, Contains, IndexRange, Line, Linear, Point}; +use crate::input::FONT_SIZE_STEP; +use crate::message_bar::MessageBuffer; +use crate::selection::{self, Locations, Selection}; +use crate::term::cell::{Cell, Flags, LineLength}; +use crate::term::color::Rgb; +use crate::url::{Url, UrlParser}; + +#[cfg(windows)] +use crate::tty; + +pub mod cell; +pub mod color; + +/// A type that can expand a given point to a region +/// +/// Usually this is implemented for some 2-D array type since +/// points are two dimensional indices. +pub trait Search { + /// Find the nearest semantic boundary _to the left_ of provided point. + fn semantic_search_left(&self, _: Point<usize>) -> Point<usize>; + /// Find the nearest semantic boundary _to the point_ of provided point. + fn semantic_search_right(&self, _: Point<usize>) -> Point<usize>; + /// Find the nearest URL boundary in both directions. + fn url_search(&self, _: Point<usize>) -> Option<Url>; +} + +impl Search for Term { + fn semantic_search_left(&self, mut point: Point<usize>) -> Point<usize> { + // Limit the starting point to the last line in the history + point.line = min(point.line, self.grid.len() - 1); + + let mut iter = self.grid.iter_from(point); + let last_col = self.grid.num_cols() - Column(1); + + while let Some(cell) = iter.prev() { + if self.semantic_escape_chars.contains(cell.c) { + break; + } + + if iter.cur.col == last_col && !cell.flags.contains(cell::Flags::WRAPLINE) { + break; // cut off if on new line or hit escape char + } + + point = iter.cur; + } + + point + } + + fn semantic_search_right(&self, mut point: Point<usize>) -> Point<usize> { + // Limit the starting point to the last line in the history + point.line = min(point.line, self.grid.len() - 1); + + let mut iter = self.grid.iter_from(point); + let last_col = self.grid.num_cols() - 1; + + while let Some(cell) = iter.next() { + if self.semantic_escape_chars.contains(cell.c) { + break; + } + + point = iter.cur; + + if point.col == last_col && !cell.flags.contains(cell::Flags::WRAPLINE) { + break; // cut off if on new line or hit escape char + } + } + + point + } + + fn url_search(&self, mut point: Point<usize>) -> Option<Url> { + let last_col = self.grid.num_cols() - 1; + + // Switch first line from top to bottom + point.line = self.grid.num_lines().0 - point.line - 1; + + // Remove viewport scroll offset + point.line += self.grid.display_offset(); + + // Create forwards and backwards iterators + let mut iterf = self.grid.iter_from(point); + point.col += 1; + let mut iterb = self.grid.iter_from(point); + + // Find URLs + let mut url_parser = UrlParser::new(); + while let Some(cell) = iterb.prev() { + if (iterb.cur.col == last_col && !cell.flags.contains(cell::Flags::WRAPLINE)) + || url_parser.advance_left(cell) + { + break; + } + } + + while let Some(cell) = iterf.next() { + if url_parser.advance_right(cell) + || (iterf.cur.col == last_col && !cell.flags.contains(cell::Flags::WRAPLINE)) + { + break; + } + } + url_parser.url() + } +} + +impl selection::Dimensions for Term { + fn dimensions(&self) -> Point { + Point { col: self.grid.num_cols(), line: self.grid.num_lines() } + } +} + +/// Iterator that yields cells needing render +/// +/// Yields cells that require work to be displayed (that is, not a an empty +/// background cell). Additionally, this manages some state of the grid only +/// relevant for rendering like temporarily changing the cell with the cursor. +/// +/// This manages the cursor during a render. The cursor location is inverted to +/// draw it, and reverted after drawing to maintain state. +pub struct RenderableCellsIter<'a> { + inner: DisplayIter<'a, Cell>, + grid: &'a Grid<Cell>, + cursor: &'a Point, + cursor_offset: usize, + cursor_cell: Option<RasterizedGlyph>, + cursor_style: CursorStyle, + config: &'a Config, + colors: &'a color::List, + selection: Option<RangeInclusive<index::Linear>>, + url_highlight: &'a Option<RangeInclusive<index::Linear>>, +} + +impl<'a> RenderableCellsIter<'a> { + /// Create the renderable cells iterator + /// + /// The cursor and terminal mode are required for properly displaying the + /// cursor. + fn new<'b>( + term: &'b Term, + config: &'b Config, + selection: Option<Locations>, + mut cursor_style: CursorStyle, + metrics: font::Metrics, + ) -> RenderableCellsIter<'b> { + let grid = &term.grid; + + let cursor_offset = grid.line_to_offset(term.cursor.point.line); + let inner = grid.display_iter(); + + let mut selection_range = None; + if let Some(loc) = selection { + // Get on-screen lines of the selection's locations + let start_line = grid.buffer_line_to_visible(loc.start.line); + let end_line = grid.buffer_line_to_visible(loc.end.line); + + // Get start/end locations based on what part of selection is on screen + let locations = match (start_line, end_line) { + (ViewportPosition::Visible(start_line), ViewportPosition::Visible(end_line)) => { + Some((start_line, loc.start.col, end_line, loc.end.col)) + }, + (ViewportPosition::Visible(start_line), ViewportPosition::Above) => { + Some((start_line, loc.start.col, Line(0), Column(0))) + }, + (ViewportPosition::Below, ViewportPosition::Visible(end_line)) => { + Some((grid.num_lines(), Column(0), end_line, loc.end.col)) + }, + (ViewportPosition::Below, ViewportPosition::Above) => { + Some((grid.num_lines(), Column(0), Line(0), Column(0))) + }, + _ => None, + }; + + if let Some((start_line, start_col, end_line, end_col)) = locations { + // start and end *lines* are swapped as we switch from buffer to + // Line coordinates. + let mut end = Point { line: start_line, col: start_col }; + let mut start = Point { line: end_line, col: end_col }; + + if start > end { + ::std::mem::swap(&mut start, &mut end); + } + + let cols = grid.num_cols(); + let start = Linear::from_point(cols, start.into()); + let end = Linear::from_point(cols, end.into()); + + // Update the selection + selection_range = Some(RangeInclusive::new(start, end)); + } + } + + // Load cursor glyph + let cursor = &term.cursor.point; + let cursor_visible = term.mode.contains(TermMode::SHOW_CURSOR) && grid.contains(cursor); + let cursor_cell = if cursor_visible { + let offset_x = config.font().offset().x; + let offset_y = config.font().offset().y; + + let is_wide = grid[cursor].flags.contains(cell::Flags::WIDE_CHAR) + && (cursor.col + 1) < grid.num_cols(); + Some(cursor::get_cursor_glyph(cursor_style, metrics, offset_x, offset_y, is_wide)) + } else { + // Use hidden cursor so text will not get inverted + cursor_style = CursorStyle::Hidden; + None + }; + + RenderableCellsIter { + cursor, + cursor_offset, + grid, + inner, + selection: selection_range, + url_highlight: &grid.url_highlight, + config, + colors: &term.colors, + cursor_cell, + cursor_style, + } + } +} + +#[derive(Clone, Debug)] +pub enum RenderableCellContent { + Chars([char; cell::MAX_ZEROWIDTH_CHARS + 1]), + Cursor((CursorStyle, RasterizedGlyph)), +} + +#[derive(Clone, Debug)] +pub struct RenderableCell { + /// A _Display_ line (not necessarily an _Active_ line) + pub line: Line, + pub column: Column, + pub inner: RenderableCellContent, + pub fg: Rgb, + pub bg: Rgb, + pub bg_alpha: f32, + pub flags: cell::Flags, +} + +impl RenderableCell { + fn new(config: &Config, colors: &color::List, cell: Indexed<Cell>, selected: bool) -> Self { + // Lookup RGB values + let mut fg_rgb = Self::compute_fg_rgb(config, colors, cell.fg, cell.flags); + let mut bg_rgb = Self::compute_bg_rgb(colors, cell.bg); + + let selection_background = config.colors().selection.background; + if let (true, Some(col)) = (selected, selection_background) { + // Override selection background with config colors + bg_rgb = col; + } else if selected ^ cell.inverse() { + if fg_rgb == bg_rgb && !cell.flags.contains(Flags::HIDDEN) { + // Reveal inversed text when fg/bg is the same + fg_rgb = colors[NamedColor::Background]; + bg_rgb = colors[NamedColor::Foreground]; + } else { + // Invert cell fg and bg colors + mem::swap(&mut fg_rgb, &mut bg_rgb); + } + } + + // Override selection text with config colors + if let (true, Some(col)) = (selected, config.colors().selection.text) { + fg_rgb = col; + } + + RenderableCell { + line: cell.line, + column: cell.column, + inner: RenderableCellContent::Chars(cell.chars()), + fg: fg_rgb, + bg: bg_rgb, + bg_alpha: Self::compute_bg_alpha(colors, bg_rgb), + flags: cell.flags, + } + } + + fn compute_fg_rgb(config: &Config, colors: &color::List, fg: Color, flags: cell::Flags) -> Rgb { + match fg { + Color::Spec(rgb) => rgb, + Color::Named(ansi) => { + match (config.draw_bold_text_with_bright_colors(), flags & Flags::DIM_BOLD) { + // If no bright foreground is set, treat it like the BOLD flag doesn't exist + (_, cell::Flags::DIM_BOLD) + if ansi == NamedColor::Foreground + && config.colors().primary.bright_foreground.is_none() => + { + colors[NamedColor::DimForeground] + }, + // Draw bold text in bright colors *and* contains bold flag. + (true, cell::Flags::BOLD) => colors[ansi.to_bright()], + // Cell is marked as dim and not bold + (_, cell::Flags::DIM) | (false, cell::Flags::DIM_BOLD) => colors[ansi.to_dim()], + // None of the above, keep original color. + _ => colors[ansi], + } + }, + Color::Indexed(idx) => { + let idx = match ( + config.draw_bold_text_with_bright_colors(), + flags & Flags::DIM_BOLD, + idx, + ) { + (true, cell::Flags::BOLD, 0..=7) => idx as usize + 8, + (false, cell::Flags::DIM, 8..=15) => idx as usize - 8, + (false, cell::Flags::DIM, 0..=7) => idx as usize + 260, + _ => idx as usize, + }; + + colors[idx] + }, + } + } + + #[inline] + fn compute_bg_alpha(colors: &color::List, bg: Rgb) -> f32 { + if colors[NamedColor::Background] == bg { + 0. + } else { + 1. + } + } + + #[inline] + fn compute_bg_rgb(colors: &color::List, bg: Color) -> Rgb { + match bg { + Color::Spec(rgb) => rgb, + Color::Named(ansi) => colors[ansi], + Color::Indexed(idx) => colors[idx], + } + } +} + +impl<'a> Iterator for RenderableCellsIter<'a> { + type Item = RenderableCell; + + /// Gets the next renderable cell + /// + /// Skips empty (background) cells and applies any flags to the cell state + /// (eg. invert fg and bg colors). + #[inline] + fn next(&mut self) -> Option<Self::Item> { + loop { + if self.cursor_offset == self.inner.offset() && self.inner.column() == self.cursor.col { + // Handle cursor + if let Some(cursor_cell) = self.cursor_cell.take() { + let cell = Indexed { + inner: self.grid[self.cursor], + column: self.cursor.col, + line: self.cursor.line, + }; + let mut renderable_cell = + RenderableCell::new(self.config, self.colors, cell, false); + + renderable_cell.inner = + RenderableCellContent::Cursor((self.cursor_style, cursor_cell)); + + if let Some(color) = self.config.cursor_cursor_color() { + renderable_cell.fg = color; + } + + return Some(renderable_cell); + } else { + let mut cell = + RenderableCell::new(self.config, self.colors, self.inner.next()?, false); + + if self.cursor_style == CursorStyle::Block { + std::mem::swap(&mut cell.bg, &mut cell.fg); + + if let Some(color) = self.config.cursor_text_color() { + cell.fg = color; + } + } + + return Some(cell); + } + } else { + let mut cell = self.inner.next()?; + + let index = Linear::new(self.grid.num_cols(), cell.column, cell.line); + + let selected = + self.selection.as_ref().map(|range| range.contains_(index)).unwrap_or(false); + + // Skip empty cells + if cell.is_empty() && !selected { + continue; + } + + // Underline URL highlights + if self.url_highlight.as_ref().map(|range| range.contains_(index)).unwrap_or(false) + { + cell.inner.flags.insert(Flags::UNDERLINE); + } + + return Some(RenderableCell::new(self.config, self.colors, cell, selected)); + } + } + } +} + +pub mod mode { + use bitflags::bitflags; + + bitflags! { + pub struct TermMode: u16 { + const SHOW_CURSOR = 0b00_0000_0000_0001; + const APP_CURSOR = 0b00_0000_0000_0010; + const APP_KEYPAD = 0b00_0000_0000_0100; + const MOUSE_REPORT_CLICK = 0b00_0000_0000_1000; + const BRACKETED_PASTE = 0b00_0000_0001_0000; + const SGR_MOUSE = 0b00_0000_0010_0000; + const MOUSE_MOTION = 0b00_0000_0100_0000; + const LINE_WRAP = 0b00_0000_1000_0000; + const LINE_FEED_NEW_LINE = 0b00_0001_0000_0000; + const ORIGIN = 0b00_0010_0000_0000; + const INSERT = 0b00_0100_0000_0000; + const FOCUS_IN_OUT = 0b00_1000_0000_0000; + const ALT_SCREEN = 0b01_0000_0000_0000; + const MOUSE_DRAG = 0b10_0000_0000_0000; + const ANY = 0b11_1111_1111_1111; + const NONE = 0; + } + } + + impl Default for TermMode { + fn default() -> TermMode { + TermMode::SHOW_CURSOR | TermMode::LINE_WRAP + } + } +} + +pub use crate::term::mode::TermMode; + +trait CharsetMapping { + fn map(&self, c: char) -> char { + c + } +} + +impl CharsetMapping for StandardCharset { + /// Switch/Map character to the active charset. Ascii is the common case and + /// for that we want to do as little as possible. + #[inline] + fn map(&self, c: char) -> char { + match *self { + StandardCharset::Ascii => c, + StandardCharset::SpecialCharacterAndLineDrawing => match c { + '`' => '◆', + 'a' => '▒', + 'b' => '\t', + 'c' => '\u{000c}', + 'd' => '\r', + 'e' => '\n', + 'f' => '°', + 'g' => '±', + 'h' => '\u{2424}', + 'i' => '\u{000b}', + 'j' => '┘', + 'k' => '┐', + 'l' => '┌', + 'm' => '└', + 'n' => '┼', + 'o' => '⎺', + 'p' => '⎻', + 'q' => '─', + 'r' => '⎼', + 's' => '⎽', + 't' => '├', + 'u' => '┤', + 'v' => '┴', + 'w' => '┬', + 'x' => '│', + 'y' => '≤', + 'z' => '≥', + '{' => 'π', + '|' => '≠', + '}' => '£', + '~' => '·', + _ => c, + }, + } + } +} + +#[derive(Default, Copy, Clone)] +struct Charsets([StandardCharset; 4]); + +impl Index<CharsetIndex> for Charsets { + type Output = StandardCharset; + + fn index(&self, index: CharsetIndex) -> &StandardCharset { + &self.0[index as usize] + } +} + +impl IndexMut<CharsetIndex> for Charsets { + fn index_mut(&mut self, index: CharsetIndex) -> &mut StandardCharset { + &mut self.0[index as usize] + } +} + +#[derive(Default, Copy, Clone)] +pub struct Cursor { + /// The location of this cursor + pub point: Point, + + /// Template cell when using this cursor + template: Cell, + + /// Currently configured graphic character sets + charsets: Charsets, +} + +pub struct VisualBell { + /// Visual bell animation + animation: VisualBellAnimation, + + /// Visual bell duration + duration: Duration, + + /// The last time the visual bell rang, if at all + start_time: Option<Instant>, +} + +fn cubic_bezier(p0: f64, p1: f64, p2: f64, p3: f64, x: f64) -> f64 { + (1.0 - x).powi(3) * p0 + + 3.0 * (1.0 - x).powi(2) * x * p1 + + 3.0 * (1.0 - x) * x.powi(2) * p2 + + x.powi(3) * p3 +} + +impl VisualBell { + pub fn new(config: &Config) -> VisualBell { + let visual_bell_config = config.visual_bell(); + VisualBell { + animation: visual_bell_config.animation(), + duration: visual_bell_config.duration(), + start_time: None, + } + } + + /// Ring the visual bell, and return its intensity. + pub fn ring(&mut self) -> f64 { + let now = Instant::now(); + self.start_time = Some(now); + self.intensity_at_instant(now) + } + + /// Get the currently intensity of the visual bell. The bell's intensity + /// ramps down from 1.0 to 0.0 at a rate determined by the bell's duration. + pub fn intensity(&self) -> f64 { + self.intensity_at_instant(Instant::now()) + } + + /// Check whether or not the visual bell has completed "ringing". + pub fn completed(&mut self) -> bool { + match self.start_time { + Some(earlier) => { + if Instant::now().duration_since(earlier) >= self.duration { + self.start_time = None; + } + false + }, + None => true, + } + } + + /// Get the intensity of the visual bell at a particular instant. The bell's + /// intensity ramps down from 1.0 to 0.0 at a rate determined by the bell's + /// duration. + pub fn intensity_at_instant(&self, instant: Instant) -> f64 { + // If `duration` is zero, then the VisualBell is disabled; therefore, + // its `intensity` is zero. + if self.duration == Duration::from_secs(0) { + return 0.0; + } + + match self.start_time { + // Similarly, if `start_time` is `None`, then the VisualBell has not + // been "rung"; therefore, its `intensity` is zero. + None => 0.0, + + Some(earlier) => { + // Finally, if the `instant` at which we wish to compute the + // VisualBell's `intensity` occurred before the VisualBell was + // "rung", then its `intensity` is also zero. + if instant < earlier { + return 0.0; + } + + let elapsed = instant.duration_since(earlier); + let elapsed_f = + elapsed.as_secs() as f64 + f64::from(elapsed.subsec_nanos()) / 1e9f64; + let duration_f = self.duration.as_secs() as f64 + + f64::from(self.duration.subsec_nanos()) / 1e9f64; + + // Otherwise, we compute a value `time` from 0.0 to 1.0 + // inclusive that represents the ratio of `elapsed` time to the + // `duration` of the VisualBell. + let time = (elapsed_f / duration_f).min(1.0); + + // We use this to compute the inverse `intensity` of the + // VisualBell. When `time` is 0.0, `inverse_intensity` is 0.0, + // and when `time` is 1.0, `inverse_intensity` is 1.0. + let inverse_intensity = match self.animation { + VisualBellAnimation::Ease | VisualBellAnimation::EaseOut => { + cubic_bezier(0.25, 0.1, 0.25, 1.0, time) + }, + VisualBellAnimation::EaseOutSine => cubic_bezier(0.39, 0.575, 0.565, 1.0, time), + VisualBellAnimation::EaseOutQuad => cubic_bezier(0.25, 0.46, 0.45, 0.94, time), + VisualBellAnimation::EaseOutCubic => { + cubic_bezier(0.215, 0.61, 0.355, 1.0, time) + }, + VisualBellAnimation::EaseOutQuart => cubic_bezier(0.165, 0.84, 0.44, 1.0, time), + VisualBellAnimation::EaseOutQuint => cubic_bezier(0.23, 1.0, 0.32, 1.0, time), + VisualBellAnimation::EaseOutExpo => cubic_bezier(0.19, 1.0, 0.22, 1.0, time), + VisualBellAnimation::EaseOutCirc => cubic_bezier(0.075, 0.82, 0.165, 1.0, time), + VisualBellAnimation::Linear => time, + }; + + // Since we want the `intensity` of the VisualBell to decay over + // `time`, we subtract the `inverse_intensity` from 1.0. + 1.0 - inverse_intensity + }, + } + } + + pub fn update_config(&mut self, config: &Config) { + let visual_bell_config = config.visual_bell(); + self.animation = visual_bell_config.animation(); + self.duration = visual_bell_config.duration(); + } +} + +pub struct Term { + /// The grid + grid: Grid<Cell>, + + /// Tracks if the next call to input will need to first handle wrapping. + /// This is true after the last column is set with the input function. Any function that + /// implicitly sets the line or column needs to set this to false to avoid wrapping twice. + /// input_needs_wrap ensures that cursor.col is always valid for use into indexing into + /// arrays. Without it we would have to sanitize cursor.col every time we used it. + input_needs_wrap: bool, + + /// Got a request to set title; it's buffered here until next draw. + /// + /// Would be nice to avoid the allocation... + next_title: Option<String>, + + /// Got a request to set the mouse cursor; it's buffered here until the next draw + next_mouse_cursor: Option<MouseCursor>, + + /// Alternate grid + alt_grid: Grid<Cell>, + + /// Alt is active + alt: bool, + + /// The cursor + cursor: Cursor, + + /// The graphic character set, out of `charsets`, which ASCII is currently + /// being mapped to + active_charset: CharsetIndex, + + /// Tabstops + tabs: TabStops, + + /// Mode flags + mode: TermMode, + + /// Scroll region + scroll_region: Range<Line>, + + /// Font size + pub font_size: Size, + original_font_size: Size, + + /// Size + size_info: SizeInfo, + + pub dirty: bool, + + pub visual_bell: VisualBell, + pub next_is_urgent: Option<bool>, + + /// Saved cursor from main grid + cursor_save: Cursor, + + /// Saved cursor from alt grid + cursor_save_alt: Cursor, + + semantic_escape_chars: String, + + /// Colors used for rendering + colors: color::List, + + /// Is color in `colors` modified or not + color_modified: [bool; color::COUNT], + + /// Original colors from config + original_colors: color::List, + + /// Current style of the cursor + cursor_style: Option<CursorStyle>, + + /// Default style for resetting the cursor + default_cursor_style: CursorStyle, + + /// Whether to permit updating the terminal title + dynamic_title: bool, + + /// Number of spaces in one tab + tabspaces: usize, + + /// Automatically scroll to bottom when new lines are added + auto_scroll: bool, + + /// Buffer to store messages for the message bar + message_buffer: MessageBuffer, + + /// Hint that Alacritty should be closed + should_exit: bool, +} + +/// Terminal size info +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +pub struct SizeInfo { + /// Terminal window width + pub width: f32, + + /// Terminal window height + pub height: f32, + + /// Width of individual cell + pub cell_width: f32, + + /// Height of individual cell + pub cell_height: f32, + + /// Horizontal window padding + pub padding_x: f32, + + /// Horizontal window padding + pub padding_y: f32, + + /// DPI factor of the current window + #[serde(default)] + pub dpr: f64, +} + +impl SizeInfo { + #[inline] + pub fn lines(&self) -> Line { + Line(((self.height - 2. * self.padding_y) / self.cell_height) as usize) + } + + #[inline] + pub fn cols(&self) -> Column { + Column(((self.width - 2. * self.padding_x) / self.cell_width) as usize) + } + + pub fn contains_point(&self, x: usize, y: usize, include_padding: bool) -> bool { + if include_padding { + x < self.width as usize && y < self.height as usize + } else { + x < (self.width - self.padding_x) as usize + && x >= self.padding_x as usize + && y < (self.height - self.padding_y) as usize + && y >= self.padding_y as usize + } + } + + pub fn pixels_to_coords(&self, x: usize, y: usize) -> Point { + let col = Column(x.saturating_sub(self.padding_x as usize) / (self.cell_width as usize)); + let line = Line(y.saturating_sub(self.padding_y as usize) / (self.cell_height as usize)); + + Point { + line: min(line, Line(self.lines().saturating_sub(1))), + col: min(col, Column(self.cols().saturating_sub(1))), + } + } +} + +impl Term { + pub fn selection(&self) -> &Option<Selection> { + &self.grid.selection + } + + pub fn selection_mut(&mut self) -> &mut Option<Selection> { + &mut self.grid.selection + } + + #[inline] + pub fn get_next_title(&mut self) -> Option<String> { + self.next_title.take() + } + + #[inline] + pub fn scroll_display(&mut self, scroll: Scroll) { + self.grid.scroll_display(scroll); + self.reset_url_highlight(); + self.dirty = true; + } + + #[inline] + pub fn get_next_mouse_cursor(&mut self) -> Option<MouseCursor> { + self.next_mouse_cursor.take() + } + + pub fn new(config: &Config, size: SizeInfo, message_buffer: MessageBuffer) -> Term { + let num_cols = size.cols(); + let num_lines = size.lines(); + + let history_size = config.scrolling().history as usize; + let grid = Grid::new(num_lines, num_cols, history_size, Cell::default()); + let alt = Grid::new(num_lines, num_cols, 0 /* scroll history */, Cell::default()); + + let tabspaces = config.tabspaces(); + let tabs = TabStops::new(grid.num_cols(), tabspaces); + + let scroll_region = Line(0)..grid.num_lines(); + + let colors = color::List::from(config.colors()); + + Term { + next_title: None, + next_mouse_cursor: None, + dirty: false, + visual_bell: VisualBell::new(config), + next_is_urgent: None, + input_needs_wrap: false, + grid, + alt_grid: alt, + alt: false, + font_size: config.font().size(), + original_font_size: config.font().size(), + active_charset: Default::default(), + cursor: Default::default(), + cursor_save: Default::default(), + cursor_save_alt: Default::default(), + tabs, + mode: Default::default(), + scroll_region, + size_info: size, + colors, + color_modified: [false; color::COUNT], + original_colors: colors, + semantic_escape_chars: config.selection().semantic_escape_chars.clone(), + cursor_style: None, + default_cursor_style: config.cursor_style(), + dynamic_title: config.dynamic_title(), + tabspaces, + auto_scroll: config.scrolling().auto_scroll, + message_buffer, + should_exit: false, + } + } + + pub fn change_font_size(&mut self, delta: f32) { + // Saturating addition with minimum font size FONT_SIZE_STEP + let new_size = self.font_size + Size::new(delta); + self.font_size = max(new_size, Size::new(FONT_SIZE_STEP)); + self.dirty = true; + } + + pub fn reset_font_size(&mut self) { + self.font_size = self.original_font_size; + self.dirty = true; + } + + pub fn update_config(&mut self, config: &Config) { + self.semantic_escape_chars = config.selection().semantic_escape_chars.clone(); + self.original_colors.fill_named(config.colors()); + self.original_colors.fill_cube(config.colors()); + self.original_colors.fill_gray_ramp(config.colors()); + for i in 0..color::COUNT { + if !self.color_modified[i] { + self.colors[i] = self.original_colors[i]; + } + } + self.visual_bell.update_config(config); + self.default_cursor_style = config.cursor_style(); + self.dynamic_title = config.dynamic_title(); + self.auto_scroll = config.scrolling().auto_scroll; + self.grid.update_history(config.scrolling().history as usize, &self.cursor.template); + } + + #[inline] + pub fn needs_draw(&self) -> bool { + self.dirty + } + + pub fn selection_to_string(&self) -> Option<String> { + /// Need a generic push() for the Append trait + trait PushChar { + fn push_char(&mut self, c: char); + fn maybe_newline(&mut self, grid: &Grid<Cell>, line: usize, ending: Column) { + if ending != Column(0) + && !grid[line][ending - 1].flags.contains(cell::Flags::WRAPLINE) + { + self.push_char('\n'); + } + } + } + + impl PushChar for String { + #[inline] + fn push_char(&mut self, c: char) { + self.push(c); + } + } + + trait Append: PushChar { + fn append( + &mut self, + grid: &Grid<Cell>, + tabs: &TabStops, + line: usize, + cols: Range<Column>, + ); + } + + impl Append for String { + fn append( + &mut self, + grid: &Grid<Cell>, + tabs: &TabStops, + mut line: usize, + cols: Range<Column>, + ) { + // Select until last line still within the buffer + line = min(line, grid.len() - 1); + + let grid_line = &grid[line]; + let line_length = grid_line.line_length(); + let line_end = min(line_length, cols.end + 1); + + if line_end.0 == 0 && cols.end >= grid.num_cols() - 1 { + self.push('\n'); + } else if cols.start < line_end { + let mut tab_mode = false; + + for col in IndexRange::from(cols.start..line_end) { + let cell = grid_line[col]; + + if tab_mode { + // Skip over whitespace until next tab-stop once a tab was found + if tabs[col] { + tab_mode = false; + } else if cell.c == ' ' { + continue; + } + } + + if !cell.flags.contains(cell::Flags::WIDE_CHAR_SPACER) { + self.push(cell.c); + for c in (&cell.chars()[1..]).iter().filter(|c| **c != ' ') { + self.push(*c); + } + } + + if cell.c == '\t' { + tab_mode = true; + } + } + + if cols.end >= grid.num_cols() - 1 { + self.maybe_newline(grid, line, line_end); + } + } + } + } + + let alt_screen = self.mode.contains(TermMode::ALT_SCREEN); + let selection = self.grid.selection.clone()?; + let span = selection.to_span(self, alt_screen)?; + + let mut res = String::new(); + + let Locations { mut start, mut end } = span.to_locations(); + + if start > end { + ::std::mem::swap(&mut start, &mut end); + } + + let line_count = end.line - start.line; + let max_col = Column(usize::max_value() - 1); + + match line_count { + // Selection within single line + 0 => { + res.append(&self.grid, &self.tabs, start.line, start.col..end.col); + }, + + // Selection ends on line following start + 1 => { + // Ending line + res.append(&self.grid, &self.tabs, end.line, end.col..max_col); + + // Starting line + res.append(&self.grid, &self.tabs, start.line, Column(0)..start.col); + }, + + // Multi line selection + _ => { + // Ending line + res.append(&self.grid, &self.tabs, end.line, end.col..max_col); + + let middle_range = (start.line + 1)..(end.line); + for line in middle_range.rev() { + res.append(&self.grid, &self.tabs, line, Column(0)..max_col); + } + + // Starting line + res.append(&self.grid, &self.tabs, start.line, Column(0)..start.col); + }, + } + + Some(res) + } + + pub(crate) fn visible_to_buffer(&self, point: Point) -> Point<usize> { + self.grid.visible_to_buffer(point) + } + + /// Convert the given pixel values to a grid coordinate + /// + /// The mouse coordinates are expected to be relative to the top left. The + /// line and column returned are also relative to the top left. + /// + /// Returns None if the coordinates are outside the window, + /// padding pixels are considered inside the window + pub fn pixels_to_coords(&self, x: usize, y: usize) -> Option<Point> { + if self.size_info.contains_point(x, y, true) { + Some(self.size_info.pixels_to_coords(x, y)) + } else { + None + } + } + + /// Access to the raw grid data structure + /// + /// This is a bit of a hack; when the window is closed, the event processor + /// serializes the grid state to a file. + pub fn grid(&self) -> &Grid<Cell> { + &self.grid + } + + /// Mutable access for swapping out the grid during tests + #[cfg(test)] + pub fn grid_mut(&mut self) -> &mut Grid<Cell> { + &mut self.grid + } + + /// Iterate over the *renderable* cells in the terminal + /// + /// A renderable cell is any cell which has content other than the default + /// background color. Cells with an alternate background color are + /// considered renderable as are cells with any text content. + pub fn renderable_cells<'b>( + &'b self, + config: &'b Config, + window_focused: bool, + metrics: font::Metrics, + ) -> RenderableCellsIter<'_> { + let alt_screen = self.mode.contains(TermMode::ALT_SCREEN); + let selection = self + .grid + .selection + .as_ref() + .and_then(|s| s.to_span(self, alt_screen)) + .map(|span| span.to_locations()); + + let cursor = if window_focused || !config.unfocused_hollow_cursor() { + self.cursor_style.unwrap_or(self.default_cursor_style) + } else { + CursorStyle::HollowBlock + }; + + RenderableCellsIter::new(&self, config, selection, cursor, metrics) + } + + /// Resize terminal to new dimensions + pub fn resize(&mut self, size: &SizeInfo) { + debug!("Resizing terminal"); + + // Bounds check; lots of math assumes width and height are > 0 + if size.width as usize <= 2 * self.size_info.padding_x as usize + || size.height as usize <= 2 * self.size_info.padding_y as usize + { + return; + } + + let old_cols = self.grid.num_cols(); + let old_lines = self.grid.num_lines(); + let mut num_cols = size.cols(); + let mut num_lines = size.lines(); + + if let Some(message) = self.message_buffer.message() { + num_lines -= message.text(size).len(); + } + + self.size_info = *size; + + if old_cols == num_cols && old_lines == num_lines { + debug!("Term::resize dimensions unchanged"); + return; + } + + self.grid.selection = None; + self.alt_grid.selection = None; + self.grid.url_highlight = None; + + // Should not allow less than 1 col, causes all sorts of checks to be required. + if num_cols <= Column(1) { + num_cols = Column(2); + } + + // Should not allow less than 1 line, causes all sorts of checks to be required. + if num_lines <= Line(1) { + num_lines = Line(2); + } + + // Scroll up to keep cursor in terminal + if self.cursor.point.line >= num_lines { + let lines = self.cursor.point.line - num_lines + 1; + self.grid.scroll_up(&(Line(0)..old_lines), lines, &self.cursor.template); + } + + // Scroll up alt grid as well + if self.cursor_save_alt.point.line >= num_lines { + let lines = self.cursor_save_alt.point.line - num_lines + 1; + self.alt_grid.scroll_up(&(Line(0)..old_lines), lines, &self.cursor_save_alt.template); + } + + // Move prompt down when growing if scrollback lines are available + if num_lines > old_lines { + if self.mode.contains(TermMode::ALT_SCREEN) { + let growage = min(num_lines - old_lines, Line(self.alt_grid.scroll_limit())); + self.cursor_save.point.line += growage; + } else { + let growage = min(num_lines - old_lines, Line(self.grid.scroll_limit())); + self.cursor.point.line += growage; + } + } + + debug!("New num_cols is {} and num_lines is {}", num_cols, num_lines); + + // Resize grids to new size + let alt_cursor_point = if self.mode.contains(TermMode::ALT_SCREEN) { + &mut self.cursor_save.point + } else { + &mut self.cursor_save_alt.point + }; + self.grid.resize(num_lines, num_cols, &mut self.cursor.point, &Cell::default()); + self.alt_grid.resize(num_lines, num_cols, alt_cursor_point, &Cell::default()); + + // Reset scrolling region to new size + self.scroll_region = Line(0)..self.grid.num_lines(); + + // Ensure cursors are in-bounds. + self.cursor.point.col = min(self.cursor.point.col, num_cols - 1); + self.cursor.point.line = min(self.cursor.point.line, num_lines - 1); + self.cursor_save.point.col = min(self.cursor_save.point.col, num_cols - 1); + self.cursor_save.point.line = min(self.cursor_save.point.line, num_lines - 1); + self.cursor_save_alt.point.col = min(self.cursor_save_alt.point.col, num_cols - 1); + self.cursor_save_alt.point.line = min(self.cursor_save_alt.point.line, num_lines - 1); + + // Recreate tabs list + self.tabs = TabStops::new(self.grid.num_cols(), self.tabspaces); + } + + #[inline] + pub fn size_info(&self) -> &SizeInfo { + &self.size_info + } + + #[inline] + pub fn mode(&self) -> &TermMode { + &self.mode + } + + #[inline] + pub fn cursor(&self) -> &Cursor { + &self.cursor + } + + pub fn swap_alt(&mut self) { + if self.alt { + let template = &self.cursor.template; + self.grid.region_mut(..).each(|c| c.reset(template)); + } + + self.alt = !self.alt; + ::std::mem::swap(&mut self.grid, &mut self.alt_grid); + } + + /// Scroll screen down + /// + /// Text moves down; clear at bottom + /// Expects origin to be in scroll range. + #[inline] + fn scroll_down_relative(&mut self, origin: Line, mut lines: Line) { + trace!("Scrolling down relative: origin={}, lines={}", origin, lines); + lines = min(lines, self.scroll_region.end - self.scroll_region.start); + lines = min(lines, self.scroll_region.end - origin); + + // Scroll between origin and bottom + let mut template = self.cursor.template; + template.flags = Flags::empty(); + self.grid.scroll_down(&(origin..self.scroll_region.end), lines, &template); + } + + /// Scroll screen up + /// + /// Text moves up; clear at top + /// Expects origin to be in scroll range. + #[inline] + fn scroll_up_relative(&mut self, origin: Line, lines: Line) { + trace!("Scrolling up relative: origin={}, lines={}", origin, lines); + let lines = min(lines, self.scroll_region.end - self.scroll_region.start); + + // Scroll from origin to bottom less number of lines + let mut template = self.cursor.template; + template.flags = Flags::empty(); + self.grid.scroll_up(&(origin..self.scroll_region.end), lines, &template); + } + + fn deccolm(&mut self) { + // Setting 132 column font makes no sense, but run the other side effects + // Clear scrolling region + let scroll_region = Line(0)..self.grid.num_lines(); + self.set_scrolling_region(scroll_region); + + // Clear grid + let template = self.cursor.template; + self.grid.region_mut(..).each(|c| c.reset(&template)); + } + + #[inline] + pub fn background_color(&self) -> Rgb { + self.colors[NamedColor::Background] + } + + #[inline] + pub fn message_buffer_mut(&mut self) -> &mut MessageBuffer { + &mut self.message_buffer + } + + #[inline] + pub fn message_buffer(&self) -> &MessageBuffer { + &self.message_buffer + } + + #[inline] + pub fn exit(&mut self) { + self.should_exit = true; + } + + #[inline] + pub fn should_exit(&self) -> bool { + self.should_exit + } + + #[inline] + pub fn set_url_highlight(&mut self, hl: RangeInclusive<index::Linear>) { + self.grid.url_highlight = Some(hl); + } + + #[inline] + pub fn reset_url_highlight(&mut self) { + let mouse_mode = + TermMode::MOUSE_MOTION | TermMode::MOUSE_DRAG | TermMode::MOUSE_REPORT_CLICK; + let mouse_cursor = if self.mode().intersects(mouse_mode) { + MouseCursor::Default + } else { + MouseCursor::Text + }; + self.set_mouse_cursor(mouse_cursor); + + self.grid.url_highlight = None; + self.dirty = true; + } +} + +impl ansi::TermInfo for Term { + #[inline] + fn lines(&self) -> Line { + self.grid.num_lines() + } + + #[inline] + fn cols(&self) -> Column { + self.grid.num_cols() + } +} + +impl ansi::Handler for Term { + /// Set the window title + #[inline] + fn set_title(&mut self, title: &str) { + if self.dynamic_title { + self.next_title = Some(title.to_owned()); + + #[cfg(windows)] + { + // cmd.exe in winpty: winpty incorrectly sets the title to ' ' instead of + // 'Alacritty' - thus we have to substitute this back to get equivalent + // behaviour as conpty. + // + // The starts_with check is necessary because other shells e.g. bash set a + // different title and don't need Alacritty prepended. + if !tty::is_conpty() && title.starts_with(' ') { + self.next_title = Some(format!("Alacritty {}", title.trim())); + } + } + } + } + + /// Set the mouse cursor + #[inline] + fn set_mouse_cursor(&mut self, cursor: MouseCursor) { + self.next_mouse_cursor = Some(cursor); + } + + /// A character to be displayed + #[inline] + fn input(&mut self, c: char) { + // If enabled, scroll to bottom when character is received + if self.auto_scroll { + self.scroll_display(Scroll::Bottom); + } + + if self.input_needs_wrap { + if !self.mode.contains(TermMode::LINE_WRAP) { + return; + } + + trace!("Wrapping input"); + + { + let location = Point { line: self.cursor.point.line, col: self.cursor.point.col }; + + let cell = &mut self.grid[&location]; + cell.flags.insert(cell::Flags::WRAPLINE); + } + + if (self.cursor.point.line + 1) >= self.scroll_region.end { + self.linefeed(); + } else { + self.cursor.point.line += 1; + } + + self.cursor.point.col = Column(0); + self.input_needs_wrap = false; + } + + // Number of cells the char will occupy + if let Some(width) = c.width() { + let num_cols = self.grid.num_cols(); + + // If in insert mode, first shift cells to the right. + if self.mode.contains(TermMode::INSERT) && self.cursor.point.col + width < num_cols { + let line = self.cursor.point.line; + let col = self.cursor.point.col; + let line = &mut self.grid[line]; + + let src = line[col..].as_ptr(); + let dst = line[(col + width)..].as_mut_ptr(); + unsafe { + // memmove + ptr::copy(src, dst, (num_cols - col - width).0); + } + } + + // Handle zero-width characters + if width == 0 { + let mut col = self.cursor.point.col.0.saturating_sub(1); + let line = self.cursor.point.line; + if self.grid[line][Column(col)].flags.contains(cell::Flags::WIDE_CHAR_SPACER) { + col = col.saturating_sub(1); + } + self.grid[line][Column(col)].push_extra(c); + return; + } + + let cell = &mut self.grid[&self.cursor.point]; + *cell = self.cursor.template; + cell.c = self.cursor.charsets[self.active_charset].map(c); + + // Handle wide chars + if width == 2 { + cell.flags.insert(cell::Flags::WIDE_CHAR); + + if self.cursor.point.col + 1 < num_cols { + self.cursor.point.col += 1; + let spacer = &mut self.grid[&self.cursor.point]; + *spacer = self.cursor.template; + spacer.flags.insert(cell::Flags::WIDE_CHAR_SPACER); + } + } + } + + if (self.cursor.point.col + 1) < self.grid.num_cols() { + self.cursor.point.col += 1; + } else { + self.input_needs_wrap = true; + } + } + + #[inline] + fn dectest(&mut self) { + trace!("Dectesting"); + let mut template = self.cursor.template; + template.c = 'E'; + + self.grid.region_mut(..).each(|c| c.reset(&template)); + } + + #[inline] + fn goto(&mut self, line: Line, col: Column) { + trace!("Going to: line={}, col={}", line, col); + let (y_offset, max_y) = if self.mode.contains(TermMode::ORIGIN) { + (self.scroll_region.start, self.scroll_region.end - 1) + } else { + (Line(0), self.grid.num_lines() - 1) + }; + + self.cursor.point.line = min(line + y_offset, max_y); + self.cursor.point.col = min(col, self.grid.num_cols() - 1); + self.input_needs_wrap = false; + } + + #[inline] + fn goto_line(&mut self, line: Line) { + trace!("Going to line: {}", line); + self.goto(line, self.cursor.point.col) + } + + #[inline] + fn goto_col(&mut self, col: Column) { + trace!("Going to column: {}", col); + self.goto(self.cursor.point.line, col) + } + + #[inline] + fn insert_blank(&mut self, count: Column) { + // Ensure inserting within terminal bounds + + let count = min(count, self.size_info.cols() - self.cursor.point.col); + + let source = self.cursor.point.col; + let destination = self.cursor.point.col + count; + let num_cells = (self.size_info.cols() - destination).0; + + let line = &mut self.grid[self.cursor.point.line]; + + unsafe { + let src = line[source..].as_ptr(); + let dst = line[destination..].as_mut_ptr(); + + ptr::copy(src, dst, num_cells); + } + + // Cells were just moved out towards the end of the line; fill in + // between source and dest with blanks. + let template = self.cursor.template; + for c in &mut line[source..destination] { + c.reset(&template); + } + } + + #[inline] + fn move_up(&mut self, lines: Line) { + trace!("Moving up: {}", lines); + let move_to = Line(self.cursor.point.line.0.saturating_sub(lines.0)); + self.goto(move_to, self.cursor.point.col) + } + + #[inline] + fn move_down(&mut self, lines: Line) { + trace!("Moving down: {}", lines); + let move_to = self.cursor.point.line + lines; + self.goto(move_to, self.cursor.point.col) + } + + #[inline] + fn move_forward(&mut self, cols: Column) { + trace!("Moving forward: {}", cols); + self.cursor.point.col = min(self.cursor.point.col + cols, self.grid.num_cols() - 1); + self.input_needs_wrap = false; + } + + #[inline] + fn move_backward(&mut self, cols: Column) { + trace!("Moving backward: {}", cols); + self.cursor.point.col -= min(self.cursor.point.col, cols); + self.input_needs_wrap = false; + } + + #[inline] + fn identify_terminal<W: io::Write>(&mut self, writer: &mut W) { + let _ = writer.write_all(b"\x1b[?6c"); + } + + #[inline] + fn device_status<W: io::Write>(&mut self, writer: &mut W, arg: usize) { + trace!("Reporting device status: {}", arg); + match arg { + 5 => { + let _ = writer.write_all(b"\x1b[0n"); + }, + 6 => { + let pos = self.cursor.point; + let _ = write!(writer, "\x1b[{};{}R", pos.line + 1, pos.col + 1); + }, + _ => debug!("unknown device status query: {}", arg), + }; + } + + #[inline] + fn move_down_and_cr(&mut self, lines: Line) { + trace!("Moving down and cr: {}", lines); + let move_to = self.cursor.point.line + lines; + self.goto(move_to, Column(0)) + } + + #[inline] + fn move_up_and_cr(&mut self, lines: Line) { + trace!("Moving up and cr: {}", lines); + let move_to = Line(self.cursor.point.line.0.saturating_sub(lines.0)); + self.goto(move_to, Column(0)) + } + + #[inline] + fn put_tab(&mut self, mut count: i64) { + trace!("Putting tab: {}", count); + + while self.cursor.point.col < self.grid.num_cols() && count != 0 { + count -= 1; + + let cell = &mut self.grid[&self.cursor.point]; + if cell.c == ' ' { + cell.c = self.cursor.charsets[self.active_charset].map('\t'); + } + + loop { + if (self.cursor.point.col + 1) == self.grid.num_cols() { + break; + } + + self.cursor.point.col += 1; + + if self.tabs[self.cursor.point.col] { + break; + } + } + } + + self.input_needs_wrap = false; + } + + /// Backspace `count` characters + #[inline] + fn backspace(&mut self) { + trace!("Backspace"); + if self.cursor.point.col > Column(0) { + self.cursor.point.col -= 1; + self.input_needs_wrap = false; + } + } + + /// Carriage return + #[inline] + fn carriage_return(&mut self) { + trace!("Carriage return"); + self.cursor.point.col = Column(0); + self.input_needs_wrap = false; + } + + /// Linefeed + #[inline] + fn linefeed(&mut self) { + trace!("Linefeed"); + let next = self.cursor.point.line + 1; + if next == self.scroll_region.end { + self.scroll_up(Line(1)); + } else if next < self.grid.num_lines() { + self.cursor.point.line += 1; + } + } + + /// Set current position as a tabstop + #[inline] + fn bell(&mut self) { + trace!("Bell"); + self.visual_bell.ring(); + self.next_is_urgent = Some(true); + } + + #[inline] + fn substitute(&mut self) { + trace!("[unimplemented] Substitute"); + } + + /// Run LF/NL + /// + /// LF/NL mode has some interesting history. According to ECMA-48 4th + /// edition, in LINE FEED mode, + /// + /// > The execution of the formatter functions LINE FEED (LF), FORM FEED + /// (FF), LINE TABULATION (VT) cause only movement of the active position in + /// the direction of the line progression. + /// + /// In NEW LINE mode, + /// + /// > The execution of the formatter functions LINE FEED (LF), FORM FEED + /// (FF), LINE TABULATION (VT) cause movement to the line home position on + /// the following line, the following form, etc. In the case of LF this is + /// referred to as the New Line (NL) option. + /// + /// Additionally, ECMA-48 4th edition says that this option is deprecated. + /// ECMA-48 5th edition only mentions this option (without explanation) + /// saying that it's been removed. + /// + /// As an emulator, we need to support it since applications may still rely + /// on it. + #[inline] + fn newline(&mut self) { + self.linefeed(); + + if self.mode.contains(TermMode::LINE_FEED_NEW_LINE) { + self.carriage_return(); + } + } + + #[inline] + fn set_horizontal_tabstop(&mut self) { + trace!("Setting horizontal tabstop"); + let column = self.cursor.point.col; + self.tabs[column] = true; + } + + #[inline] + fn scroll_up(&mut self, lines: Line) { + let origin = self.scroll_region.start; + self.scroll_up_relative(origin, lines); + } + + #[inline] + fn scroll_down(&mut self, lines: Line) { + let origin = self.scroll_region.start; + self.scroll_down_relative(origin, lines); + } + + #[inline] + fn insert_blank_lines(&mut self, lines: Line) { + trace!("Inserting blank {} lines", lines); + if self.scroll_region.contains_(self.cursor.point.line) { + let origin = self.cursor.point.line; + self.scroll_down_relative(origin, lines); + } + } + + #[inline] + fn delete_lines(&mut self, lines: Line) { + trace!("Deleting {} lines", lines); + if self.scroll_region.contains_(self.cursor.point.line) { + let origin = self.cursor.point.line; + self.scroll_up_relative(origin, lines); + } + } + + #[inline] + fn erase_chars(&mut self, count: Column) { + trace!("Erasing chars: count={}, col={}", count, self.cursor.point.col); + let start = self.cursor.point.col; + let end = min(start + count, self.grid.num_cols()); + + let row = &mut self.grid[self.cursor.point.line]; + let template = self.cursor.template; // Cleared cells have current background color set + for c in &mut row[start..end] { + c.reset(&template); + } + } + + #[inline] + fn delete_chars(&mut self, count: Column) { + // Ensure deleting within terminal bounds + let count = min(count, self.size_info.cols()); + + let start = self.cursor.point.col; + let end = min(start + count, self.grid.num_cols() - 1); + let n = (self.size_info.cols() - end).0; + + let line = &mut self.grid[self.cursor.point.line]; + + unsafe { + let src = line[end..].as_ptr(); + let dst = line[start..].as_mut_ptr(); + + ptr::copy(src, dst, n); + } + + // Clear last `count` cells in line. If deleting 1 char, need to delete + // 1 cell. + let template = self.cursor.template; + let end = self.size_info.cols() - count; + for c in &mut line[end..] { + c.reset(&template); + } + } + + #[inline] + fn move_backward_tabs(&mut self, count: i64) { + trace!("Moving backward {} tabs", count); + + for _ in 0..count { + let mut col = self.cursor.point.col; + for i in (0..(col.0)).rev() { + if self.tabs[index::Column(i)] { + col = index::Column(i); + break; + } + } + self.cursor.point.col = col; + } + } + + #[inline] + fn move_forward_tabs(&mut self, count: i64) { + trace!("[unimplemented] Moving forward {} tabs", count); + } + + #[inline] + fn save_cursor_position(&mut self) { + trace!("Saving cursor position"); + let cursor = if self.alt { &mut self.cursor_save_alt } else { &mut self.cursor_save }; + + *cursor = self.cursor; + } + + #[inline] + fn restore_cursor_position(&mut self) { + trace!("Restoring cursor position"); + let source = if self.alt { &self.cursor_save_alt } else { &self.cursor_save }; + + self.cursor = *source; + self.cursor.point.line = min(self.cursor.point.line, self.grid.num_lines() - 1); + self.cursor.point.col = min(self.cursor.point.col, self.grid.num_cols() - 1); + } + + #[inline] + fn clear_line(&mut self, mode: ansi::LineClearMode) { + trace!("Clearing line: {:?}", mode); + let mut template = self.cursor.template; + template.flags ^= template.flags; + + let col = self.cursor.point.col; + + match mode { + ansi::LineClearMode::Right => { + let row = &mut self.grid[self.cursor.point.line]; + for cell in &mut row[col..] { + cell.reset(&template); + } + }, + ansi::LineClearMode::Left => { + let row = &mut self.grid[self.cursor.point.line]; + for cell in &mut row[..=col] { + cell.reset(&template); + } + }, + ansi::LineClearMode::All => { + let row = &mut self.grid[self.cursor.point.line]; + for cell in &mut row[..] { + cell.reset(&template); + } + }, + } + } + + /// Set the indexed color value + #[inline] + fn set_color(&mut self, index: usize, color: Rgb) { + trace!("Setting color[{}] = {:?}", index, color); + self.colors[index] = color; + self.color_modified[index] = true; + } + + /// Reset the indexed color to original value + #[inline] + fn reset_color(&mut self, index: usize) { + trace!("Reseting color[{}]", index); + self.colors[index] = self.original_colors[index]; + self.color_modified[index] = false; + } + + /// Set the clipboard + #[inline] + fn set_clipboard(&mut self, string: &str) { + Clipboard::new().and_then(|mut clipboard| clipboard.store_primary(string)).unwrap_or_else( + |err| { + warn!("Error storing selection to clipboard: {}", err); + }, + ); + } + + #[inline] + fn clear_screen(&mut self, mode: ansi::ClearMode) { + trace!("Clearing screen: {:?}", mode); + let mut template = self.cursor.template; + template.flags ^= template.flags; + + // Remove active selections and URL highlights + self.grid.selection = None; + self.grid.url_highlight = None; + + match mode { + ansi::ClearMode::Below => { + for cell in &mut self.grid[self.cursor.point.line][self.cursor.point.col..] { + cell.reset(&template); + } + if self.cursor.point.line < self.grid.num_lines() - 1 { + self.grid + .region_mut((self.cursor.point.line + 1)..) + .each(|cell| cell.reset(&template)); + } + }, + ansi::ClearMode::All => self.grid.region_mut(..).each(|c| c.reset(&template)), + ansi::ClearMode::Above => { + // If clearing more than one line + if self.cursor.point.line > Line(1) { + // Fully clear all lines before the current line + self.grid + .region_mut(..self.cursor.point.line) + .each(|cell| cell.reset(&template)); + } + // Clear up to the current column in the current line + let end = min(self.cursor.point.col + 1, self.grid.num_cols()); + for cell in &mut self.grid[self.cursor.point.line][..end] { + cell.reset(&template); + } + }, + ansi::ClearMode::Saved => self.grid.clear_history(), + } + } + + #[inline] + fn clear_tabs(&mut self, mode: ansi::TabulationClearMode) { + trace!("Clearing tabs: {:?}", mode); + match mode { + ansi::TabulationClearMode::Current => { + let column = self.cursor.point.col; + self.tabs[column] = false; + }, + ansi::TabulationClearMode::All => { + self.tabs.clear_all(); + }, + } + } + + // Reset all important fields in the term struct + #[inline] + fn reset_state(&mut self) { + if self.alt { + self.swap_alt(); + } + self.input_needs_wrap = false; + self.next_title = None; + self.next_mouse_cursor = None; + self.cursor = Default::default(); + self.active_charset = Default::default(); + self.mode = Default::default(); + self.font_size = self.original_font_size; + self.next_is_urgent = None; + self.cursor_save = Default::default(); + self.cursor_save_alt = Default::default(); + self.colors = self.original_colors; + self.color_modified = [false; color::COUNT]; + self.cursor_style = None; + self.grid.reset(&Cell::default()); + self.alt_grid.reset(&Cell::default()); + self.scroll_region = Line(0)..self.grid.num_lines(); + } + + #[inline] + fn reverse_index(&mut self) { + trace!("Reversing index"); + // if cursor is at the top + if self.cursor.point.line == self.scroll_region.start { + self.scroll_down(Line(1)); + } else { + self.cursor.point.line -= min(self.cursor.point.line, Line(1)); + } + } + + /// set a terminal attribute + #[inline] + fn terminal_attribute(&mut self, attr: Attr) { + trace!("Setting attribute: {:?}", attr); + match attr { + Attr::Foreground(color) => self.cursor.template.fg = color, + Attr::Background(color) => self.cursor.template.bg = color, + Attr::Reset => { + self.cursor.template.fg = Color::Named(NamedColor::Foreground); + self.cursor.template.bg = Color::Named(NamedColor::Background); + self.cursor.template.flags = cell::Flags::empty(); + }, + Attr::Reverse => self.cursor.template.flags.insert(cell::Flags::INVERSE), + Attr::CancelReverse => self.cursor.template.flags.remove(cell::Flags::INVERSE), + Attr::Bold => self.cursor.template.flags.insert(cell::Flags::BOLD), + Attr::CancelBold => self.cursor.template.flags.remove(cell::Flags::BOLD), + Attr::Dim => self.cursor.template.flags.insert(cell::Flags::DIM), + Attr::CancelBoldDim => { + self.cursor.template.flags.remove(cell::Flags::BOLD | cell::Flags::DIM) + }, + Attr::Italic => self.cursor.template.flags.insert(cell::Flags::ITALIC), + Attr::CancelItalic => self.cursor.template.flags.remove(cell::Flags::ITALIC), + Attr::Underscore => self.cursor.template.flags.insert(cell::Flags::UNDERLINE), + Attr::CancelUnderline => self.cursor.template.flags.remove(cell::Flags::UNDERLINE), + Attr::Hidden => self.cursor.template.flags.insert(cell::Flags::HIDDEN), + Attr::CancelHidden => self.cursor.template.flags.remove(cell::Flags::HIDDEN), + Attr::Strike => self.cursor.template.flags.insert(cell::Flags::STRIKEOUT), + Attr::CancelStrike => self.cursor.template.flags.remove(cell::Flags::STRIKEOUT), + _ => { + debug!("Term got unhandled attr: {:?}", attr); + }, + } + } + + #[inline] + fn set_mode(&mut self, mode: ansi::Mode) { + trace!("Setting mode: {:?}", mode); + match mode { + ansi::Mode::SwapScreenAndSetRestoreCursor => { + if !self.alt { + self.mode.insert(TermMode::ALT_SCREEN); + self.save_cursor_position(); + self.swap_alt(); + self.save_cursor_position(); + } + }, + ansi::Mode::ShowCursor => self.mode.insert(TermMode::SHOW_CURSOR), + ansi::Mode::CursorKeys => self.mode.insert(TermMode::APP_CURSOR), + ansi::Mode::ReportMouseClicks => { + self.mode.insert(TermMode::MOUSE_REPORT_CLICK); + self.set_mouse_cursor(MouseCursor::Default); + }, + ansi::Mode::ReportCellMouseMotion => { + self.mode.insert(TermMode::MOUSE_DRAG); + self.set_mouse_cursor(MouseCursor::Default); + }, + ansi::Mode::ReportAllMouseMotion => { + self.mode.insert(TermMode::MOUSE_MOTION); + self.set_mouse_cursor(MouseCursor::Default); + }, + ansi::Mode::ReportFocusInOut => self.mode.insert(TermMode::FOCUS_IN_OUT), + ansi::Mode::BracketedPaste => self.mode.insert(TermMode::BRACKETED_PASTE), + ansi::Mode::SgrMouse => self.mode.insert(TermMode::SGR_MOUSE), + ansi::Mode::LineWrap => self.mode.insert(TermMode::LINE_WRAP), + ansi::Mode::LineFeedNewLine => self.mode.insert(TermMode::LINE_FEED_NEW_LINE), + ansi::Mode::Origin => self.mode.insert(TermMode::ORIGIN), + ansi::Mode::DECCOLM => self.deccolm(), + ansi::Mode::Insert => self.mode.insert(TermMode::INSERT), // heh + ansi::Mode::BlinkingCursor => { + trace!("... unimplemented mode"); + }, + } + } + + #[inline] + fn unset_mode(&mut self, mode: ansi::Mode) { + trace!("Unsetting mode: {:?}", mode); + match mode { + ansi::Mode::SwapScreenAndSetRestoreCursor => { + if self.alt { + self.mode.remove(TermMode::ALT_SCREEN); + self.restore_cursor_position(); + self.swap_alt(); + self.restore_cursor_position(); + } + }, + ansi::Mode::ShowCursor => self.mode.remove(TermMode::SHOW_CURSOR), + ansi::Mode::CursorKeys => self.mode.remove(TermMode::APP_CURSOR), + ansi::Mode::ReportMouseClicks => { + self.mode.remove(TermMode::MOUSE_REPORT_CLICK); + self.set_mouse_cursor(MouseCursor::Text); + }, + ansi::Mode::ReportCellMouseMotion => { + self.mode.remove(TermMode::MOUSE_DRAG); + self.set_mouse_cursor(MouseCursor::Text); + }, + ansi::Mode::ReportAllMouseMotion => { + self.mode.remove(TermMode::MOUSE_MOTION); + self.set_mouse_cursor(MouseCursor::Text); + }, + ansi::Mode::ReportFocusInOut => self.mode.remove(TermMode::FOCUS_IN_OUT), + ansi::Mode::BracketedPaste => self.mode.remove(TermMode::BRACKETED_PASTE), + ansi::Mode::SgrMouse => self.mode.remove(TermMode::SGR_MOUSE), + ansi::Mode::LineWrap => self.mode.remove(TermMode::LINE_WRAP), + ansi::Mode::LineFeedNewLine => self.mode.remove(TermMode::LINE_FEED_NEW_LINE), + ansi::Mode::Origin => self.mode.remove(TermMode::ORIGIN), + ansi::Mode::DECCOLM => self.deccolm(), + ansi::Mode::Insert => self.mode.remove(TermMode::INSERT), + ansi::Mode::BlinkingCursor => { + trace!("... unimplemented mode"); + }, + } + } + + #[inline] + fn set_scrolling_region(&mut self, region: Range<Line>) { + trace!("Setting scrolling region: {:?}", region); + self.scroll_region.start = min(region.start, self.grid.num_lines()); + self.scroll_region.end = min(region.end, self.grid.num_lines()); + self.goto(Line(0), Column(0)); + } + + #[inline] + fn set_keypad_application_mode(&mut self) { + trace!("Setting keypad application mode"); + self.mode.insert(TermMode::APP_KEYPAD); + } + + #[inline] + fn unset_keypad_application_mode(&mut self) { + trace!("Unsetting keypad application mode"); + self.mode.remove(TermMode::APP_KEYPAD); + } + + #[inline] + fn configure_charset(&mut self, index: CharsetIndex, charset: StandardCharset) { + trace!("Configuring charset {:?} as {:?}", index, charset); + self.cursor.charsets[index] = charset; + } + + #[inline] + fn set_active_charset(&mut self, index: CharsetIndex) { + trace!("Setting active charset {:?}", index); + self.active_charset = index; + } + + #[inline] + fn set_cursor_style(&mut self, style: Option<CursorStyle>) { + trace!("Setting cursor style {:?}", style); + self.cursor_style = style; + } +} + +struct TabStops { + tabs: Vec<bool>, +} + +impl TabStops { + fn new(num_cols: Column, tabspaces: usize) -> TabStops { + TabStops { + tabs: IndexRange::from(Column(0)..num_cols) + .map(|i| (*i as usize) % tabspaces == 0) + .collect::<Vec<bool>>(), + } + } + + fn clear_all(&mut self) { + unsafe { + ptr::write_bytes(self.tabs.as_mut_ptr(), 0, self.tabs.len()); + } + } +} + +impl Index<Column> for TabStops { + type Output = bool; + + fn index(&self, index: Column) -> &bool { + &self.tabs[index.0] + } +} + +impl IndexMut<Column> for TabStops { + fn index_mut(&mut self, index: Column) -> &mut bool { + self.tabs.index_mut(index.0) + } +} + +#[cfg(test)] +mod tests { + use serde_json; + + use super::{Cell, SizeInfo, Term}; + use crate::term::cell; + + use crate::ansi::{self, CharsetIndex, Handler, StandardCharset}; + use crate::config::Config; + use crate::grid::{Grid, Scroll}; + use crate::index::{Column, Line, Point, Side}; + use crate::input::FONT_SIZE_STEP; + use crate::message_bar::MessageBuffer; + use crate::selection::Selection; + use font::Size; + use std::mem; + + #[test] + fn semantic_selection_works() { + let size = SizeInfo { + width: 21.0, + height: 51.0, + cell_width: 3.0, + cell_height: 3.0, + padding_x: 0.0, + padding_y: 0.0, + dpr: 1.0, + }; + let mut term = Term::new(&Default::default(), size, MessageBuffer::new()); + let mut grid: Grid<Cell> = Grid::new(Line(3), Column(5), 0, Cell::default()); + for i in 0..5 { + for j in 0..2 { + grid[Line(j)][Column(i)].c = 'a'; + } + } + grid[Line(0)][Column(0)].c = '"'; + grid[Line(0)][Column(3)].c = '"'; + grid[Line(1)][Column(2)].c = '"'; + grid[Line(0)][Column(4)].flags.insert(cell::Flags::WRAPLINE); + + let mut escape_chars = String::from("\""); + + mem::swap(&mut term.grid, &mut grid); + mem::swap(&mut term.semantic_escape_chars, &mut escape_chars); + + { + *term.selection_mut() = Some(Selection::semantic(Point { line: 2, col: Column(1) })); + assert_eq!(term.selection_to_string(), Some(String::from("aa"))); + } + + { + *term.selection_mut() = Some(Selection::semantic(Point { line: 2, col: Column(4) })); + assert_eq!(term.selection_to_string(), Some(String::from("aaa"))); + } + + { + *term.selection_mut() = Some(Selection::semantic(Point { line: 1, col: Column(1) })); + assert_eq!(term.selection_to_string(), Some(String::from("aaa"))); + } + } + + #[test] + fn line_selection_works() { + let size = SizeInfo { + width: 21.0, + height: 51.0, + cell_width: 3.0, + cell_height: 3.0, + padding_x: 0.0, + padding_y: 0.0, + dpr: 1.0, + }; + let mut term = Term::new(&Default::default(), size, MessageBuffer::new()); + let mut grid: Grid<Cell> = Grid::new(Line(1), Column(5), 0, Cell::default()); + for i in 0..5 { + grid[Line(0)][Column(i)].c = 'a'; + } + grid[Line(0)][Column(0)].c = '"'; + grid[Line(0)][Column(3)].c = '"'; + + mem::swap(&mut term.grid, &mut grid); + + *term.selection_mut() = Some(Selection::lines(Point { line: 0, col: Column(3) })); + assert_eq!(term.selection_to_string(), Some(String::from("\"aa\"a\n"))); + } + + #[test] + fn selecting_empty_line() { + let size = SizeInfo { + width: 21.0, + height: 51.0, + cell_width: 3.0, + cell_height: 3.0, + padding_x: 0.0, + padding_y: 0.0, + dpr: 1.0, + }; + let mut term = Term::new(&Default::default(), size, MessageBuffer::new()); + let mut grid: Grid<Cell> = Grid::new(Line(3), Column(3), 0, Cell::default()); + for l in 0..3 { + if l != 1 { + for c in 0..3 { + grid[Line(l)][Column(c)].c = 'a'; + } + } + } + + mem::swap(&mut term.grid, &mut grid); + + let mut selection = Selection::simple(Point { line: 2, col: Column(0) }, Side::Left); + selection.update(Point { line: 0, col: Column(2) }, Side::Right); + *term.selection_mut() = Some(selection); + assert_eq!(term.selection_to_string(), Some("aaa\n\naaa\n".into())); + } + + /// Check that the grid can be serialized back and forth losslessly + /// + /// This test is in the term module as opposed to the grid since we want to + /// test this property with a T=Cell. + #[test] + fn grid_serde() { + let template = Cell::default(); + + let grid: Grid<Cell> = Grid::new(Line(24), Column(80), 0, template); + let serialized = serde_json::to_string(&grid).expect("ser"); + let deserialized = serde_json::from_str::<Grid<Cell>>(&serialized).expect("de"); + + assert_eq!(deserialized, grid); + } + + #[test] + fn input_line_drawing_character() { + let size = SizeInfo { + width: 21.0, + height: 51.0, + cell_width: 3.0, + cell_height: 3.0, + padding_x: 0.0, + padding_y: 0.0, + dpr: 1.0, + }; + let mut term = Term::new(&Default::default(), size, MessageBuffer::new()); + let cursor = Point::new(Line(0), Column(0)); + term.configure_charset(CharsetIndex::G0, StandardCharset::SpecialCharacterAndLineDrawing); + term.input('a'); + + assert_eq!(term.grid()[&cursor].c, '▒'); + } + + fn change_font_size_works(font_size: f32) { + let size = SizeInfo { + width: 21.0, + height: 51.0, + cell_width: 3.0, + cell_height: 3.0, + padding_x: 0.0, + padding_y: 0.0, + dpr: 1.0, + }; + let config: Config = Default::default(); + let mut term: Term = Term::new(&config, size, MessageBuffer::new()); + term.change_font_size(font_size); + + let expected_font_size: Size = config.font().size() + Size::new(font_size); + assert_eq!(term.font_size, expected_font_size); + } + + #[test] + fn increase_font_size_works() { + change_font_size_works(10.0); + } + + #[test] + fn decrease_font_size_works() { + change_font_size_works(-10.0); + } + + #[test] + fn prevent_font_below_threshold_works() { + let size = SizeInfo { + width: 21.0, + height: 51.0, + cell_width: 3.0, + cell_height: 3.0, + padding_x: 0.0, + padding_y: 0.0, + dpr: 1.0, + }; + let config: Config = Default::default(); + let mut term: Term = Term::new(&config, size, MessageBuffer::new()); + + term.change_font_size(-100.0); + + let expected_font_size: Size = Size::new(FONT_SIZE_STEP); + assert_eq!(term.font_size, expected_font_size); + } + + #[test] + fn reset_font_size_works() { + let size = SizeInfo { + width: 21.0, + height: 51.0, + cell_width: 3.0, + cell_height: 3.0, + padding_x: 0.0, + padding_y: 0.0, + dpr: 1.0, + }; + let config: Config = Default::default(); + let mut term: Term = Term::new(&config, size, MessageBuffer::new()); + + term.change_font_size(10.0); + term.reset_font_size(); + + let expected_font_size: Size = config.font().size(); + assert_eq!(term.font_size, expected_font_size); + } + + #[test] + fn clear_saved_lines() { + let size = SizeInfo { + width: 21.0, + height: 51.0, + cell_width: 3.0, + cell_height: 3.0, + padding_x: 0.0, + padding_y: 0.0, + dpr: 1.0, + }; + let config: Config = Default::default(); + let mut term: Term = Term::new(&config, size, MessageBuffer::new()); + + // Add one line of scrollback + term.grid.scroll_up(&(Line(0)..Line(1)), Line(1), &Cell::default()); + + // Clear the history + term.clear_screen(ansi::ClearMode::Saved); + + // Make sure that scrolling does not change the grid + let mut scrolled_grid = term.grid.clone(); + scrolled_grid.scroll_display(Scroll::Top); + assert_eq!(term.grid, scrolled_grid); + } +} + +#[cfg(all(test, feature = "bench"))] +mod benches { + extern crate serde_json as json; + extern crate test; + + use std::fs::File; + use std::io::Read; + use std::mem; + use std::path::Path; + + use crate::config::Config; + use crate::grid::Grid; + use crate::message_bar::MessageBuffer; + + use super::cell::Cell; + use super::{SizeInfo, Term}; + + fn read_string<P>(path: P) -> String + where + P: AsRef<Path>, + { + let mut res = String::new(); + File::open(path.as_ref()).unwrap().read_to_string(&mut res).unwrap(); + + res + } + + /// Benchmark for the renderable cells iterator + /// + /// The renderable cells iterator yields cells that require work to be + /// displayed (that is, not a an empty background cell). This benchmark + /// measures how long it takes to process the whole iterator. + /// + /// When this benchmark was first added, it averaged ~78usec on my macbook + /// pro. The total render time for this grid is anywhere between ~1500 and + /// ~2000usec (measured imprecisely with the visual meter). + #[bench] + fn render_iter(b: &mut test::Bencher) { + // Need some realistic grid state; using one of the ref files. + let serialized_grid = read_string(concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/ref/vim_large_window_scroll/grid.json" + )); + let serialized_size = read_string(concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/ref/vim_large_window_scroll/size.json" + )); + + let mut grid: Grid<Cell> = json::from_str(&serialized_grid).unwrap(); + let size: SizeInfo = json::from_str(&serialized_size).unwrap(); + + let config = Config::default(); + + let mut terminal = Term::new(&config, size, MessageBuffer::new()); + mem::swap(&mut terminal.grid, &mut grid); + + let metrics = font::Metrics { + descent: 0., + line_height: 0., + average_advance: 0., + underline_position: 0., + underline_thickness: 0., + strikeout_position: 0., + strikeout_thickness: 0., + }; + + b.iter(|| { + let iter = terminal.renderable_cells(&config, false, metrics); + for cell in iter { + test::black_box(cell); + } + }) + } +} diff --git a/alacritty_terminal/src/tty/mod.rs b/alacritty_terminal/src/tty/mod.rs new file mode 100644 index 00000000..ec175ee6 --- /dev/null +++ b/alacritty_terminal/src/tty/mod.rs @@ -0,0 +1,96 @@ +// 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. +// +//! tty related functionality +use mio; +use std::{env, io}; + +use terminfo::Database; + +use crate::config::Config; + +#[cfg(not(windows))] +mod unix; +#[cfg(not(windows))] +pub use self::unix::*; + +#[cfg(windows)] +mod windows; +#[cfg(windows)] +pub use self::windows::*; + +/// This trait defines the behaviour needed to read and/or write to a stream. +/// It defines an abstraction over mio's interface in order to allow either one +/// read/write object or a seperate read and write object. +pub trait EventedReadWrite { + type Reader: io::Read; + type Writer: io::Write; + + fn register( + &mut self, + _: &mio::Poll, + _: &mut dyn Iterator<Item = mio::Token>, + _: mio::Ready, + _: mio::PollOpt, + ) -> io::Result<()>; + fn reregister(&mut self, _: &mio::Poll, _: mio::Ready, _: mio::PollOpt) -> io::Result<()>; + fn deregister(&mut self, _: &mio::Poll) -> io::Result<()>; + + fn reader(&mut self) -> &mut Self::Reader; + fn read_token(&self) -> mio::Token; + fn writer(&mut self) -> &mut Self::Writer; + fn write_token(&self) -> mio::Token; +} + +/// Events concerning TTY child processes +#[derive(PartialEq)] +pub enum ChildEvent { + /// Indicates the child has exited + Exited, +} + +/// A pseudoterminal (or PTY) +/// +/// This is a refinement of EventedReadWrite that also provides a channel through which we can be +/// notified if the PTY child process does something we care about (other than writing to the TTY). +/// In particular, this allows for race-free child exit notification on UNIX (cf. `SIGCHLD`). +pub trait EventedPty: EventedReadWrite { + #[cfg(unix)] + fn child_event_token(&self) -> mio::Token; + + /// Tries to retrieve an event + /// + /// Returns `Some(event)` on success, or `None` if there are no events to retrieve. + #[cfg(unix)] + fn next_child_event(&mut self) -> Option<ChildEvent>; +} + +// Setup environment variables +pub fn setup_env(config: &Config) { + // Default to 'alacritty' terminfo if it is available, otherwise + // default to 'xterm-256color'. May be overridden by user's config + // below. + env::set_var( + "TERM", + if Database::from_name("alacritty").is_ok() { "alacritty" } else { "xterm-256color" }, + ); + + // Advertise 24-bit color support + env::set_var("COLORTERM", "truecolor"); + + // Set env vars from config + for (key, value) in config.env().iter() { + env::set_var(key, value); + } +} diff --git a/alacritty_terminal/src/tty/unix.rs b/alacritty_terminal/src/tty/unix.rs new file mode 100644 index 00000000..0e3dc2fd --- /dev/null +++ b/alacritty_terminal/src/tty/unix.rs @@ -0,0 +1,405 @@ +// 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. +// +//! tty related functionality +//! + +use crate::cli::Options; +use crate::config::{Config, Shell}; +use crate::display::OnResize; +use crate::term::SizeInfo; +use crate::tty::{ChildEvent, EventedPty, EventedReadWrite}; +use mio; + +use libc::{self, c_int, pid_t, winsize, TIOCSCTTY}; +use nix::pty::openpty; +use signal_hook::{self as sighook, iterator::Signals}; + +use mio::unix::EventedFd; +use std::ffi::CStr; +use std::fs::File; +use std::io; +use std::os::unix::{ + io::{AsRawFd, FromRawFd, RawFd}, + process::CommandExt, +}; +use std::process::{Child, Command, Stdio}; +use std::ptr; +use std::sync::atomic::{AtomicUsize, Ordering}; + +/// Process ID of child process +/// +/// Necessary to put this in static storage for `sigchld` to have access +static PID: AtomicUsize = AtomicUsize::new(0); + +pub fn child_pid() -> pid_t { + PID.load(Ordering::Relaxed) as pid_t +} + +/// Get the current value of errno +fn errno() -> c_int { + ::errno::errno().0 +} + +/// Get raw fds for master/slave ends of a new pty +fn make_pty(size: winsize) -> (RawFd, RawFd) { + let mut win_size = size; + win_size.ws_xpixel = 0; + win_size.ws_ypixel = 0; + + let ends = openpty(Some(&win_size), None).expect("openpty failed"); + + (ends.master, ends.slave) +} + +/// Really only needed on BSD, but should be fine elsewhere +fn set_controlling_terminal(fd: c_int) { + let res = unsafe { + // TIOSCTTY changes based on platform and the `ioctl` call is different + // based on architecture (32/64). So a generic cast is used to make sure + // there are no issues. To allow such a generic cast the clippy warning + // is disabled. + #[allow(clippy::cast_lossless)] + libc::ioctl(fd, TIOCSCTTY as _, 0) + }; + + if res < 0 { + die!("ioctl TIOCSCTTY failed: {}", errno()); + } +} + +#[derive(Debug)] +struct Passwd<'a> { + name: &'a str, + passwd: &'a str, + uid: libc::uid_t, + gid: libc::gid_t, + gecos: &'a str, + dir: &'a str, + shell: &'a str, +} + +/// Return a Passwd struct with pointers into the provided buf +/// +/// # Unsafety +/// +/// If `buf` is changed while `Passwd` is alive, bad thing will almost certainly happen. +fn get_pw_entry(buf: &mut [i8; 1024]) -> Passwd<'_> { + // Create zeroed passwd struct + let mut entry: libc::passwd = unsafe { ::std::mem::uninitialized() }; + + let mut res: *mut libc::passwd = ptr::null_mut(); + + // Try and read the pw file. + let uid = unsafe { libc::getuid() }; + let status = unsafe { + libc::getpwuid_r(uid, &mut entry, buf.as_mut_ptr() as *mut _, buf.len(), &mut res) + }; + + if status < 0 { + die!("getpwuid_r failed"); + } + + if res.is_null() { + die!("pw not found"); + } + + // sanity check + assert_eq!(entry.pw_uid, uid); + + // Build a borrowed Passwd struct + Passwd { + name: unsafe { CStr::from_ptr(entry.pw_name).to_str().unwrap() }, + passwd: unsafe { CStr::from_ptr(entry.pw_passwd).to_str().unwrap() }, + uid: entry.pw_uid, + gid: entry.pw_gid, + gecos: unsafe { CStr::from_ptr(entry.pw_gecos).to_str().unwrap() }, + dir: unsafe { CStr::from_ptr(entry.pw_dir).to_str().unwrap() }, + shell: unsafe { CStr::from_ptr(entry.pw_shell).to_str().unwrap() }, + } +} + +pub struct Pty { + child: Child, + pub fd: File, + token: mio::Token, + signals: Signals, + signals_token: mio::Token, +} + +impl Pty { + /// Resize the pty + /// + /// Tells the kernel that the window size changed with the new pixel + /// dimensions and line/column counts. + pub fn resize<T: ToWinsize>(&self, size: &T) { + let win = size.to_winsize(); + + let res = unsafe { libc::ioctl(self.fd.as_raw_fd(), libc::TIOCSWINSZ, &win as *const _) }; + + if res < 0 { + die!("ioctl TIOCSWINSZ failed: {}", errno()); + } + } +} + +/// Create a new tty and return a handle to interact with it. +pub fn new<T: ToWinsize>( + config: &Config, + options: &Options, + size: &T, + window_id: Option<usize>, +) -> Pty { + let win_size = size.to_winsize(); + let mut buf = [0; 1024]; + let pw = get_pw_entry(&mut buf); + + let (master, slave) = make_pty(win_size); + + let default_shell = if cfg!(target_os = "macos") { + let shell_name = pw.shell.rsplit('/').next().unwrap(); + let argv = vec![String::from("-c"), format!("exec -a -{} {}", shell_name, pw.shell)]; + + Shell::new_with_args("/bin/bash", argv) + } else { + Shell::new(pw.shell) + }; + let shell = config.shell().unwrap_or(&default_shell); + + let initial_command = options.command().unwrap_or(shell); + + let mut builder = Command::new(initial_command.program()); + for arg in initial_command.args() { + builder.arg(arg); + } + + // Setup child stdin/stdout/stderr as slave fd of pty + // Ownership of fd is transferred to the Stdio structs and will be closed by them at the end of + // this scope. (It is not an issue that the fd is closed three times since File::drop ignores + // error on libc::close.) + builder.stdin(unsafe { Stdio::from_raw_fd(slave) }); + builder.stderr(unsafe { Stdio::from_raw_fd(slave) }); + builder.stdout(unsafe { Stdio::from_raw_fd(slave) }); + + // Setup shell environment + builder.env("LOGNAME", pw.name); + builder.env("USER", pw.name); + builder.env("SHELL", pw.shell); + builder.env("HOME", pw.dir); + + if let Some(window_id) = window_id { + builder.env("WINDOWID", format!("{}", window_id)); + } + + builder.before_exec(move || { + // Create a new process group + unsafe { + let err = libc::setsid(); + if err == -1 { + die!("Failed to set session id: {}", errno()); + } + } + + set_controlling_terminal(slave); + + // No longer need slave/master fds + unsafe { + libc::close(slave); + libc::close(master); + } + + unsafe { + libc::signal(libc::SIGCHLD, libc::SIG_DFL); + libc::signal(libc::SIGHUP, libc::SIG_DFL); + libc::signal(libc::SIGINT, libc::SIG_DFL); + libc::signal(libc::SIGQUIT, libc::SIG_DFL); + libc::signal(libc::SIGTERM, libc::SIG_DFL); + libc::signal(libc::SIGALRM, libc::SIG_DFL); + } + Ok(()) + }); + + // Handle set working directory option + if let Some(ref dir) = options.working_dir { + builder.current_dir(dir.as_path()); + } + + // Prepare signal handling before spawning child + let signals = Signals::new(&[sighook::SIGCHLD]).expect("error preparing signal handling"); + + match builder.spawn() { + Ok(child) => { + // Remember child PID so other modules can use it + PID.store(child.id() as usize, Ordering::Relaxed); + + unsafe { + // Maybe this should be done outside of this function so nonblocking + // isn't forced upon consumers. Although maybe it should be? + set_nonblocking(master); + } + + let pty = Pty { + child, + fd: unsafe { File::from_raw_fd(master) }, + token: mio::Token::from(0), + signals, + signals_token: mio::Token::from(0), + }; + pty.resize(size); + pty + }, + Err(err) => { + die!("Failed to spawn command: {}", err); + }, + } +} + +impl EventedReadWrite for Pty { + type Reader = File; + type Writer = File; + + #[inline] + fn register( + &mut self, + poll: &mio::Poll, + token: &mut dyn Iterator<Item = mio::Token>, + interest: mio::Ready, + poll_opts: mio::PollOpt, + ) -> io::Result<()> { + self.token = token.next().unwrap(); + poll.register(&EventedFd(&self.fd.as_raw_fd()), self.token, interest, poll_opts)?; + + self.signals_token = token.next().unwrap(); + poll.register( + &self.signals, + self.signals_token, + mio::Ready::readable(), + mio::PollOpt::level(), + ) + } + + #[inline] + fn reregister( + &mut self, + poll: &mio::Poll, + interest: mio::Ready, + poll_opts: mio::PollOpt, + ) -> io::Result<()> { + poll.reregister(&EventedFd(&self.fd.as_raw_fd()), self.token, interest, poll_opts)?; + + poll.reregister( + &self.signals, + self.signals_token, + mio::Ready::readable(), + mio::PollOpt::level(), + ) + } + + #[inline] + fn deregister(&mut self, poll: &mio::Poll) -> io::Result<()> { + poll.deregister(&EventedFd(&self.fd.as_raw_fd()))?; + poll.deregister(&self.signals) + } + + #[inline] + fn reader(&mut self) -> &mut File { + &mut self.fd + } + + #[inline] + fn read_token(&self) -> mio::Token { + self.token + } + + #[inline] + fn writer(&mut self) -> &mut File { + &mut self.fd + } + + #[inline] + fn write_token(&self) -> mio::Token { + self.token + } +} + +impl EventedPty for Pty { + #[inline] + fn next_child_event(&mut self) -> Option<ChildEvent> { + self.signals.pending().next().and_then(|signal| { + if signal != sighook::SIGCHLD { + return None; + } + + match self.child.try_wait() { + Err(e) => { + error!("Error checking child process termination: {}", e); + None + }, + Ok(None) => None, + Ok(_) => Some(ChildEvent::Exited), + } + }) + } + + #[inline] + fn child_event_token(&self) -> mio::Token { + self.signals_token + } +} + +pub fn process_should_exit() -> bool { + false +} + +/// Types that can produce a `libc::winsize` +pub trait ToWinsize { + /// Get a `libc::winsize` + fn to_winsize(&self) -> winsize; +} + +impl<'a> ToWinsize for &'a SizeInfo { + fn to_winsize(&self) -> winsize { + winsize { + ws_row: self.lines().0 as libc::c_ushort, + ws_col: self.cols().0 as libc::c_ushort, + ws_xpixel: self.width as libc::c_ushort, + ws_ypixel: self.height as libc::c_ushort, + } + } +} + +impl OnResize for i32 { + fn on_resize(&mut self, size: &SizeInfo) { + let win = size.to_winsize(); + + let res = unsafe { libc::ioctl(*self, libc::TIOCSWINSZ, &win as *const _) }; + + if res < 0 { + die!("ioctl TIOCSWINSZ failed: {}", errno()); + } + } +} + +unsafe fn set_nonblocking(fd: c_int) { + use libc::{fcntl, F_GETFL, F_SETFL, O_NONBLOCK}; + + let res = fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK); + assert_eq!(res, 0); +} + +#[test] +fn test_get_pw_entry() { + let mut buf: [i8; 1024] = [0; 1024]; + let _pw = get_pw_entry(&mut buf); +} diff --git a/alacritty_terminal/src/tty/windows/conpty.rs b/alacritty_terminal/src/tty/windows/conpty.rs new file mode 100644 index 00000000..f23d78a7 --- /dev/null +++ b/alacritty_terminal/src/tty/windows/conpty.rs @@ -0,0 +1,289 @@ +// 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. + +use super::{Pty, HANDLE}; + +use std::i16; +use std::io::Error; +use std::mem; +use std::os::windows::io::IntoRawHandle; +use std::ptr; +use std::sync::Arc; + +use dunce::canonicalize; +use mio_anonymous_pipes::{EventedAnonRead, EventedAnonWrite}; +use miow; +use widestring::U16CString; +use winapi::shared::basetsd::{PSIZE_T, SIZE_T}; +use winapi::shared::minwindef::{BYTE, DWORD}; +use winapi::shared::ntdef::{HANDLE, HRESULT, LPWSTR}; +use winapi::shared::winerror::S_OK; +use winapi::um::libloaderapi::{GetModuleHandleA, GetProcAddress}; +use winapi::um::processthreadsapi::{ + CreateProcessW, InitializeProcThreadAttributeList, UpdateProcThreadAttribute, + PROCESS_INFORMATION, STARTUPINFOW, +}; +use winapi::um::winbase::{EXTENDED_STARTUPINFO_PRESENT, STARTF_USESTDHANDLES, STARTUPINFOEXW}; +use winapi::um::wincontypes::{COORD, HPCON}; + +use crate::cli::Options; +use crate::config::{Config, Shell}; +use crate::display::OnResize; +use crate::term::SizeInfo; + +/// Dynamically-loaded Pseudoconsole API from kernel32.dll +/// +/// The field names are deliberately PascalCase as this matches +/// the defined symbols in kernel32 and also is the convention +/// that the `winapi` crate follows. +#[allow(non_snake_case)] +struct ConptyApi { + CreatePseudoConsole: + unsafe extern "system" fn(COORD, HANDLE, HANDLE, DWORD, *mut HPCON) -> HRESULT, + ResizePseudoConsole: unsafe extern "system" fn(HPCON, COORD) -> HRESULT, + ClosePseudoConsole: unsafe extern "system" fn(HPCON), +} + +impl ConptyApi { + /// Load the API or None if it cannot be found. + pub fn new() -> Option<Self> { + // Unsafe because windows API calls + unsafe { + let hmodule = GetModuleHandleA("kernel32\0".as_ptr() as _); + assert!(!hmodule.is_null()); + + let cpc = GetProcAddress(hmodule, "CreatePseudoConsole\0".as_ptr() as _); + let rpc = GetProcAddress(hmodule, "ResizePseudoConsole\0".as_ptr() as _); + let clpc = GetProcAddress(hmodule, "ClosePseudoConsole\0".as_ptr() as _); + + if cpc.is_null() || rpc.is_null() || clpc.is_null() { + None + } else { + Some(Self { + CreatePseudoConsole: mem::transmute(cpc), + ResizePseudoConsole: mem::transmute(rpc), + ClosePseudoConsole: mem::transmute(clpc), + }) + } + } + } +} + +/// RAII Pseudoconsole +pub struct Conpty { + pub handle: HPCON, + api: ConptyApi, +} + +/// Handle can be cloned freely and moved between threads. +pub type ConptyHandle = Arc<Conpty>; + +impl Drop for Conpty { + fn drop(&mut self) { + unsafe { (self.api.ClosePseudoConsole)(self.handle) } + } +} + +// The Conpty API can be accessed from multiple threads. +unsafe impl Send for Conpty {} +unsafe impl Sync for Conpty {} + +pub fn new<'a>( + config: &Config, + options: &Options, + size: &SizeInfo, + _window_id: Option<usize>, +) -> Option<Pty<'a>> { + if !config.enable_experimental_conpty_backend() { + return None; + } + + let api = ConptyApi::new()?; + + let mut pty_handle = 0 as HPCON; + + // Passing 0 as the size parameter allows the "system default" buffer + // size to be used. There may be small performance and memory advantages + // to be gained by tuning this in the future, but it's likely a reasonable + // start point. + let (conout, conout_pty_handle) = miow::pipe::anonymous(0).unwrap(); + let (conin_pty_handle, conin) = miow::pipe::anonymous(0).unwrap(); + + let coord = + coord_from_sizeinfo(size).expect("Overflow when creating initial size on pseudoconsole"); + + // Create the Pseudo Console, using the pipes + let result = unsafe { + (api.CreatePseudoConsole)( + coord, + conin_pty_handle.into_raw_handle(), + conout_pty_handle.into_raw_handle(), + 0, + &mut pty_handle as *mut HPCON, + ) + }; + + assert!(result == S_OK); + + let mut success; + + // Prepare child process startup info + + let mut size: SIZE_T = 0; + + let mut startup_info_ex: STARTUPINFOEXW = Default::default(); + + let title = options.title.as_ref().map(String::as_str).unwrap_or("Alacritty"); + let title = U16CString::from_str(title).unwrap(); + startup_info_ex.StartupInfo.lpTitle = title.as_ptr() as LPWSTR; + + startup_info_ex.StartupInfo.cb = mem::size_of::<STARTUPINFOEXW>() as u32; + + // Setting this flag but leaving all the handles as default (null) ensures the + // pty process does not inherit any handles from this Alacritty process. + startup_info_ex.StartupInfo.dwFlags |= STARTF_USESTDHANDLES; + + // Create the appropriately sized thread attribute list. + unsafe { + let failure = + InitializeProcThreadAttributeList(ptr::null_mut(), 1, 0, &mut size as PSIZE_T) > 0; + + // This call was expected to return false. + if failure { + panic_shell_spawn(); + } + } + + let mut attr_list: Box<[BYTE]> = vec![0; size].into_boxed_slice(); + + // Set startup info's attribute list & initialize it + // + // Lint failure is spurious; it's because winapi's definition of PROC_THREAD_ATTRIBUTE_LIST + // implies it is one pointer in size (32 or 64 bits) but really this is just a dummy value. + // Casting a *mut u8 (pointer to 8 bit type) might therefore not be aligned correctly in + // the compiler's eyes. + #[allow(clippy::cast_ptr_alignment)] + { + startup_info_ex.lpAttributeList = attr_list.as_mut_ptr() as _; + } + + unsafe { + success = InitializeProcThreadAttributeList( + startup_info_ex.lpAttributeList, + 1, + 0, + &mut size as PSIZE_T, + ) > 0; + + if !success { + panic_shell_spawn(); + } + } + + // Set thread attribute list's Pseudo Console to the specified ConPTY + unsafe { + success = UpdateProcThreadAttribute( + startup_info_ex.lpAttributeList, + 0, + 22 | 0x0002_0000, // PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE + pty_handle, + mem::size_of::<HPCON>(), + ptr::null_mut(), + ptr::null_mut(), + ) > 0; + + if !success { + panic_shell_spawn(); + } + } + + // Get process commandline + let default_shell = &Shell::new("powershell"); + let shell = config.shell().unwrap_or(default_shell); + let initial_command = options.command().unwrap_or(shell); + let mut cmdline = initial_command.args().to_vec(); + cmdline.insert(0, initial_command.program().into()); + + // Warning, here be borrow hell + let cwd = options.working_dir.as_ref().map(|dir| canonicalize(dir).unwrap()); + let cwd = cwd.as_ref().map(|dir| dir.to_str().unwrap()); + + // Create the client application, using startup info containing ConPTY info + let cmdline = U16CString::from_str(&cmdline.join(" ")).unwrap(); + let cwd = cwd.map(|s| U16CString::from_str(&s).unwrap()); + + let mut proc_info: PROCESS_INFORMATION = Default::default(); + unsafe { + success = CreateProcessW( + ptr::null(), + cmdline.as_ptr() as LPWSTR, + ptr::null_mut(), + ptr::null_mut(), + false as i32, + EXTENDED_STARTUPINFO_PRESENT, + ptr::null_mut(), + cwd.as_ref().map_or_else(ptr::null, |s| s.as_ptr()), + &mut startup_info_ex.StartupInfo as *mut STARTUPINFOW, + &mut proc_info as *mut PROCESS_INFORMATION, + ) > 0; + + if !success { + panic_shell_spawn(); + } + } + + // Store handle to console + unsafe { + HANDLE = proc_info.hProcess; + } + + let conin = EventedAnonWrite::new(conin); + let conout = EventedAnonRead::new(conout); + + let agent = Conpty { handle: pty_handle, api }; + + Some(Pty { + handle: super::PtyHandle::Conpty(ConptyHandle::new(agent)), + conout: super::EventedReadablePipe::Anonymous(conout), + conin: super::EventedWritablePipe::Anonymous(conin), + read_token: 0.into(), + write_token: 0.into(), + }) +} + +// Panic with the last os error as message +fn panic_shell_spawn() { + panic!("Unable to spawn shell: {}", Error::last_os_error()); +} + +impl OnResize for ConptyHandle { + fn on_resize(&mut self, sizeinfo: &SizeInfo) { + if let Some(coord) = coord_from_sizeinfo(sizeinfo) { + let result = unsafe { (self.api.ResizePseudoConsole)(self.handle, coord) }; + assert!(result == S_OK); + } + } +} + +/// Helper to build a COORD from a SizeInfo, returing None in overflow cases. +fn coord_from_sizeinfo(sizeinfo: &SizeInfo) -> Option<COORD> { + let cols = sizeinfo.cols().0; + let lines = sizeinfo.lines().0; + + if cols <= i16::MAX as usize && lines <= i16::MAX as usize { + Some(COORD { X: sizeinfo.cols().0 as i16, Y: sizeinfo.lines().0 as i16 }) + } else { + None + } +} diff --git a/alacritty_terminal/src/tty/windows/mod.rs b/alacritty_terminal/src/tty/windows/mod.rs new file mode 100644 index 00000000..c87c5257 --- /dev/null +++ b/alacritty_terminal/src/tty/windows/mod.rs @@ -0,0 +1,303 @@ +// 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. + +use std::io::{self, Read, Write}; +use std::os::raw::c_void; +use std::sync::atomic::{AtomicBool, Ordering}; + +use mio::{self, Evented, Poll, PollOpt, Ready, Token}; +use mio_anonymous_pipes::{EventedAnonRead, EventedAnonWrite}; +use mio_named_pipes::NamedPipe; + +use winapi::shared::winerror::WAIT_TIMEOUT; +use winapi::um::synchapi::WaitForSingleObject; +use winapi::um::winbase::WAIT_OBJECT_0; + +use crate::cli::Options; +use crate::config::Config; +use crate::display::OnResize; +use crate::term::SizeInfo; +use crate::tty::{EventedPty, EventedReadWrite}; + +mod conpty; +mod winpty; + +/// Handle to the winpty agent or conpty process. Required so we know when it closes. +static mut HANDLE: *mut c_void = 0usize as *mut c_void; +static IS_CONPTY: AtomicBool = AtomicBool::new(false); + +pub fn process_should_exit() -> bool { + unsafe { + match WaitForSingleObject(HANDLE, 0) { + // Process has exited + WAIT_OBJECT_0 => { + info!("wait_object_0"); + true + }, + // Reached timeout of 0, process has not exited + WAIT_TIMEOUT => false, + // Error checking process, winpty gave us a bad agent handle? + _ => { + info!("Bad exit: {}", ::std::io::Error::last_os_error()); + true + }, + } + } +} + +pub fn is_conpty() -> bool { + IS_CONPTY.load(Ordering::Relaxed) +} + +#[derive(Clone)] +pub enum PtyHandle<'a> { + Winpty(winpty::WinptyHandle<'a>), + Conpty(conpty::ConptyHandle), +} + +pub struct Pty<'a> { + handle: PtyHandle<'a>, + // TODO: It's on the roadmap for the Conpty API to support Overlapped I/O. + // See https://github.com/Microsoft/console/issues/262 + // When support for that lands then it should be possible to use + // NamedPipe for the conout and conin handles + conout: EventedReadablePipe, + conin: EventedWritablePipe, + read_token: mio::Token, + write_token: mio::Token, +} + +impl<'a> Pty<'a> { + pub fn resize_handle(&self) -> impl OnResize + 'a { + self.handle.clone() + } +} + +pub fn new<'a>( + config: &Config, + options: &Options, + size: &SizeInfo, + window_id: Option<usize>, +) -> Pty<'a> { + if let Some(pty) = conpty::new(config, options, size, window_id) { + info!("Using Conpty agent"); + IS_CONPTY.store(true, Ordering::Relaxed); + pty + } else { + info!("Using Winpty agent"); + winpty::new(config, options, size, window_id) + } +} + +// TODO: The ConPTY API curently must use synchronous pipes as the input +// and output handles. This has led to the need to support two different +// types of pipe. +// +// When https://github.com/Microsoft/console/issues/262 lands then the +// Anonymous variant of this enum can be removed from the codebase and +// everything can just use NamedPipe. +pub enum EventedReadablePipe { + Anonymous(EventedAnonRead), + Named(NamedPipe), +} + +pub enum EventedWritablePipe { + Anonymous(EventedAnonWrite), + Named(NamedPipe), +} + +impl Evented for EventedReadablePipe { + fn register( + &self, + poll: &Poll, + token: Token, + interest: Ready, + opts: PollOpt, + ) -> io::Result<()> { + match self { + EventedReadablePipe::Anonymous(p) => p.register(poll, token, interest, opts), + EventedReadablePipe::Named(p) => p.register(poll, token, interest, opts), + } + } + + fn reregister( + &self, + poll: &Poll, + token: Token, + interest: Ready, + opts: PollOpt, + ) -> io::Result<()> { + match self { + EventedReadablePipe::Anonymous(p) => p.reregister(poll, token, interest, opts), + EventedReadablePipe::Named(p) => p.reregister(poll, token, interest, opts), + } + } + + fn deregister(&self, poll: &Poll) -> io::Result<()> { + match self { + EventedReadablePipe::Anonymous(p) => p.deregister(poll), + EventedReadablePipe::Named(p) => p.deregister(poll), + } + } +} + +impl Read for EventedReadablePipe { + fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { + match self { + EventedReadablePipe::Anonymous(p) => p.read(buf), + EventedReadablePipe::Named(p) => p.read(buf), + } + } +} + +impl Evented for EventedWritablePipe { + fn register( + &self, + poll: &Poll, + token: Token, + interest: Ready, + opts: PollOpt, + ) -> io::Result<()> { + match self { + EventedWritablePipe::Anonymous(p) => p.register(poll, token, interest, opts), + EventedWritablePipe::Named(p) => p.register(poll, token, interest, opts), + } + } + + fn reregister( + &self, + poll: &Poll, + token: Token, + interest: Ready, + opts: PollOpt, + ) -> io::Result<()> { + match self { + EventedWritablePipe::Anonymous(p) => p.reregister(poll, token, interest, opts), + EventedWritablePipe::Named(p) => p.reregister(poll, token, interest, opts), + } + } + + fn deregister(&self, poll: &Poll) -> io::Result<()> { + match self { + EventedWritablePipe::Anonymous(p) => p.deregister(poll), + EventedWritablePipe::Named(p) => p.deregister(poll), + } + } +} + +impl Write for EventedWritablePipe { + fn write(&mut self, buf: &[u8]) -> io::Result<usize> { + match self { + EventedWritablePipe::Anonymous(p) => p.write(buf), + EventedWritablePipe::Named(p) => p.write(buf), + } + } + + fn flush(&mut self) -> io::Result<()> { + match self { + EventedWritablePipe::Anonymous(p) => p.flush(), + EventedWritablePipe::Named(p) => p.flush(), + } + } +} + +impl<'a> OnResize for PtyHandle<'a> { + fn on_resize(&mut self, sizeinfo: &SizeInfo) { + match self { + PtyHandle::Winpty(w) => w.resize(sizeinfo), + PtyHandle::Conpty(c) => { + let mut handle = c.clone(); + handle.on_resize(sizeinfo) + }, + } + } +} + +impl<'a> EventedReadWrite for Pty<'a> { + type Reader = EventedReadablePipe; + type Writer = EventedWritablePipe; + + #[inline] + fn register( + &mut self, + poll: &mio::Poll, + token: &mut dyn Iterator<Item = mio::Token>, + interest: mio::Ready, + poll_opts: mio::PollOpt, + ) -> io::Result<()> { + self.read_token = token.next().unwrap(); + self.write_token = token.next().unwrap(); + + if interest.is_readable() { + poll.register(&self.conout, self.read_token, mio::Ready::readable(), poll_opts)? + } else { + poll.register(&self.conout, self.read_token, mio::Ready::empty(), poll_opts)? + } + if interest.is_writable() { + poll.register(&self.conin, self.write_token, mio::Ready::writable(), poll_opts)? + } else { + poll.register(&self.conin, self.write_token, mio::Ready::empty(), poll_opts)? + } + Ok(()) + } + + #[inline] + fn reregister( + &mut self, + poll: &mio::Poll, + interest: mio::Ready, + poll_opts: mio::PollOpt, + ) -> io::Result<()> { + if interest.is_readable() { + poll.reregister(&self.conout, self.read_token, mio::Ready::readable(), poll_opts)?; + } else { + poll.reregister(&self.conout, self.read_token, mio::Ready::empty(), poll_opts)?; + } + if interest.is_writable() { + poll.reregister(&self.conin, self.write_token, mio::Ready::writable(), poll_opts)?; + } else { + poll.reregister(&self.conin, self.write_token, mio::Ready::empty(), poll_opts)?; + } + Ok(()) + } + + #[inline] + fn deregister(&mut self, poll: &mio::Poll) -> io::Result<()> { + poll.deregister(&self.conout)?; + poll.deregister(&self.conin)?; + Ok(()) + } + + #[inline] + fn reader(&mut self) -> &mut Self::Reader { + &mut self.conout + } + + #[inline] + fn read_token(&self) -> mio::Token { + self.read_token + } + + #[inline] + fn writer(&mut self) -> &mut Self::Writer { + &mut self.conin + } + + #[inline] + fn write_token(&self) -> mio::Token { + self.write_token + } +} + +impl<'a> EventedPty for Pty<'a> {} diff --git a/alacritty_terminal/src/tty/windows/winpty.rs b/alacritty_terminal/src/tty/windows/winpty.rs new file mode 100644 index 00000000..10bd9d01 --- /dev/null +++ b/alacritty_terminal/src/tty/windows/winpty.rs @@ -0,0 +1,169 @@ +// 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. + +use super::{Pty, HANDLE}; + +use std::fs::OpenOptions; +use std::io; +use std::os::windows::fs::OpenOptionsExt; +use std::os::windows::io::{FromRawHandle, IntoRawHandle}; +use std::sync::Arc; +use std::u16; + +use dunce::canonicalize; +use mio_named_pipes::NamedPipe; +use winapi::um::winbase::FILE_FLAG_OVERLAPPED; +use winpty::Config as WinptyConfig; +use winpty::{ConfigFlags, MouseMode, SpawnConfig, SpawnFlags, Winpty}; + +use crate::cli::Options; +use crate::config::{Config, Shell}; +use crate::display::OnResize; +use crate::term::SizeInfo; + +// We store a raw pointer because we need mutable access to call +// on_resize from a separate thread. Winpty internally uses a mutex +// so this is safe, despite outwards appearance. +pub struct Agent<'a> { + winpty: *mut Winpty<'a>, +} + +/// Handle can be cloned freely and moved between threads. +pub type WinptyHandle<'a> = Arc<Agent<'a>>; + +// Because Winpty has a mutex, we can do this. +unsafe impl<'a> Send for Agent<'a> {} +unsafe impl<'a> Sync for Agent<'a> {} + +impl<'a> Agent<'a> { + pub fn new(winpty: Winpty<'a>) -> Self { + Self { winpty: Box::into_raw(Box::new(winpty)) } + } + + /// Get immutable access to Winpty. + pub fn winpty(&self) -> &Winpty<'a> { + unsafe { &*self.winpty } + } + + pub fn resize(&self, size: &SizeInfo) { + // This is safe since Winpty uses a mutex internally. + unsafe { + (&mut *self.winpty).on_resize(size); + } + } +} + +impl<'a> Drop for Agent<'a> { + fn drop(&mut self) { + unsafe { + Box::from_raw(self.winpty); + } + } +} + +/// How long the winpty agent should wait for any RPC request +/// This is a placeholder value until we see how often long responses happen +const AGENT_TIMEOUT: u32 = 10000; + +pub fn new<'a>( + config: &Config, + options: &Options, + size: &SizeInfo, + _window_id: Option<usize>, +) -> Pty<'a> { + // Create config + let mut wconfig = WinptyConfig::new(ConfigFlags::empty()).unwrap(); + + wconfig.set_initial_size(size.cols().0 as i32, size.lines().0 as i32); + wconfig.set_mouse_mode(&MouseMode::Auto); + wconfig.set_agent_timeout(AGENT_TIMEOUT); + + // Start agent + let mut winpty = Winpty::open(&wconfig).unwrap(); + let (conin, conout) = (winpty.conin_name(), winpty.conout_name()); + + // Get process commandline + let default_shell = &Shell::new("powershell"); + let shell = config.shell().unwrap_or(default_shell); + let initial_command = options.command().unwrap_or(shell); + let mut cmdline = initial_command.args().to_vec(); + cmdline.insert(0, initial_command.program().into()); + + // Warning, here be borrow hell + let cwd = options.working_dir.as_ref().map(|dir| canonicalize(dir).unwrap()); + let cwd = cwd.as_ref().map(|dir| dir.to_str().unwrap()); + + // Spawn process + let spawnconfig = SpawnConfig::new( + SpawnFlags::AUTO_SHUTDOWN | SpawnFlags::EXIT_AFTER_SHUTDOWN, + None, // appname + Some(&cmdline.join(" ")), + cwd, + None, // Env + ) + .unwrap(); + + let default_opts = &mut OpenOptions::new(); + default_opts.share_mode(0).custom_flags(FILE_FLAG_OVERLAPPED); + + let (conout_pipe, conin_pipe); + unsafe { + conout_pipe = NamedPipe::from_raw_handle( + default_opts.clone().read(true).open(conout).unwrap().into_raw_handle(), + ); + conin_pipe = NamedPipe::from_raw_handle( + default_opts.clone().write(true).open(conin).unwrap().into_raw_handle(), + ); + }; + + if let Some(err) = conout_pipe.connect().err() { + if err.kind() != io::ErrorKind::WouldBlock { + panic!(err); + } + } + assert!(conout_pipe.take_error().unwrap().is_none()); + + if let Some(err) = conin_pipe.connect().err() { + if err.kind() != io::ErrorKind::WouldBlock { + panic!(err); + } + } + assert!(conin_pipe.take_error().unwrap().is_none()); + + winpty.spawn(&spawnconfig).unwrap(); + + unsafe { + HANDLE = winpty.raw_handle(); + } + + let agent = Agent::new(winpty); + + Pty { + handle: super::PtyHandle::Winpty(WinptyHandle::new(agent)), + conout: super::EventedReadablePipe::Named(conout_pipe), + conin: super::EventedWritablePipe::Named(conin_pipe), + read_token: 0.into(), + write_token: 0.into(), + } +} + +impl<'a> OnResize for Winpty<'a> { + fn on_resize(&mut self, sizeinfo: &SizeInfo) { + let (cols, lines) = (sizeinfo.cols().0, sizeinfo.lines().0); + if cols > 0 && cols <= u16::MAX as usize && lines > 0 && lines <= u16::MAX as usize { + self.set_size(cols as u16, lines as u16) + .unwrap_or_else(|_| info!("Unable to set winpty size, did it die?")); + } + } +} diff --git a/alacritty_terminal/src/url.rs b/alacritty_terminal/src/url.rs new file mode 100644 index 00000000..d3caf9fc --- /dev/null +++ b/alacritty_terminal/src/url.rs @@ -0,0 +1,307 @@ +// 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. + +use unicode_width::UnicodeWidthChar; +use url; + +use crate::term::cell::{Cell, Flags}; + +// See https://tools.ietf.org/html/rfc3987#page-13 +const URL_SEPARATOR_CHARS: [char; 10] = ['<', '>', '"', ' ', '{', '}', '|', '\\', '^', '`']; +const URL_DENY_END_CHARS: [char; 8] = ['.', ',', ';', ':', '?', '!', '/', '(']; +const URL_SCHEMES: [&str; 8] = ["http", "https", "mailto", "news", "file", "git", "ssh", "ftp"]; + +/// URL text and origin of the original click position. +#[derive(Debug, PartialEq)] +pub struct Url { + pub text: String, + pub origin: usize, +} + +/// Parser for streaming inside-out detection of URLs. +pub struct UrlParser { + state: String, + origin: usize, +} + +impl UrlParser { + pub fn new() -> Self { + UrlParser { state: String::new(), origin: 0 } + } + + /// Advance the parser one character to the left. + pub fn advance_left(&mut self, cell: &Cell) -> bool { + if cell.flags.contains(Flags::WIDE_CHAR_SPACER) { + self.origin += 1; + return false; + } + + if self.advance(cell.c, 0) { + true + } else { + self.origin += 1; + false + } + } + + /// Advance the parser one character to the right. + pub fn advance_right(&mut self, cell: &Cell) -> bool { + if cell.flags.contains(Flags::WIDE_CHAR_SPACER) { + return false; + } + + self.advance(cell.c, self.state.len()) + } + + /// Returns the URL if the parser has found any. + pub fn url(mut self) -> Option<Url> { + // Remove non-alphabetical characters before the scheme + // https://tools.ietf.org/html/rfc3986#section-3.1 + if let Some(index) = self.state.find("://") { + let iter = + self.state.char_indices().rev().skip_while(|(byte_index, _)| *byte_index >= index); + for (byte_index, c) in iter { + match c { + 'a'...'z' | 'A'...'Z' => (), + _ => { + self.origin = + self.origin.saturating_sub(byte_index + c.width().unwrap_or(1)); + self.state = self.state.split_off(byte_index + c.len_utf8()); + break; + }, + } + } + } + + // Remove non-matching parenthesis and brackets + let mut open_parens_count: isize = 0; + let mut open_bracks_count: isize = 0; + for (i, c) in self.state.char_indices() { + match c { + '(' => open_parens_count += 1, + ')' if open_parens_count > 0 => open_parens_count -= 1, + '[' => open_bracks_count += 1, + ']' if open_bracks_count > 0 => open_bracks_count -= 1, + ')' | ']' => { + self.state.truncate(i); + break; + }, + _ => (), + } + } + + // Track number of quotes + let mut num_quotes = self.state.chars().filter(|&c| c == '\'').count(); + + // Remove all characters which aren't allowed at the end of a URL + while !self.state.is_empty() + && (URL_DENY_END_CHARS.contains(&self.state.chars().last().unwrap()) + || (num_quotes % 2 != 0 && self.state.ends_with('\'')) + || self.state.ends_with("''") + || self.state.ends_with("()")) + { + if self.state.pop().unwrap() == '\'' { + num_quotes -= 1; + } + } + + // Check if string is valid url + match url::Url::parse(&self.state) { + Ok(url) => { + if URL_SCHEMES.contains(&url.scheme()) && self.origin > 0 { + Some(Url { origin: self.origin - 1, text: self.state }) + } else { + None + } + }, + Err(_) => None, + } + } + + fn advance(&mut self, c: char, pos: usize) -> bool { + if URL_SEPARATOR_CHARS.contains(&c) + || (c >= '\u{00}' && c <= '\u{1F}') + || (c >= '\u{7F}' && c <= '\u{9F}') + { + true + } else { + self.state.insert(pos, c); + false + } + } +} + +#[cfg(test)] +mod tests { + use std::mem; + + use unicode_width::UnicodeWidthChar; + + use crate::grid::Grid; + use crate::index::{Column, Line, Point}; + use crate::message_bar::MessageBuffer; + use crate::term::cell::{Cell, Flags}; + use crate::term::{Search, SizeInfo, Term}; + + fn url_create_term(input: &str) -> Term { + let size = SizeInfo { + width: 21.0, + height: 51.0, + cell_width: 3.0, + cell_height: 3.0, + padding_x: 0.0, + padding_y: 0.0, + dpr: 1.0, + }; + + let width = input.chars().map(|c| if c.width() == Some(2) { 2 } else { 1 }).sum(); + let mut term = Term::new(&Default::default(), size, MessageBuffer::new()); + let mut grid: Grid<Cell> = Grid::new(Line(1), Column(width), 0, Cell::default()); + + let mut i = 0; + for c in input.chars() { + grid[Line(0)][Column(i)].c = c; + + if c.width() == Some(2) { + grid[Line(0)][Column(i)].flags.insert(Flags::WIDE_CHAR); + grid[Line(0)][Column(i + 1)].flags.insert(Flags::WIDE_CHAR_SPACER); + grid[Line(0)][Column(i + 1)].c = ' '; + i += 1; + } + + i += 1; + } + + mem::swap(term.grid_mut(), &mut grid); + + term + } + + fn url_test(input: &str, expected: &str) { + let term = url_create_term(input); + let url = term.url_search(Point::new(0, Column(15))); + assert_eq!(url.map(|u| u.text), Some(expected.into())); + } + + #[test] + fn url_skip_invalid() { + let term = url_create_term("no url here"); + let url = term.url_search(Point::new(0, Column(4))); + assert_eq!(url, None); + + let term = url_create_term(" https://example.org"); + let url = term.url_search(Point::new(0, Column(0))); + assert_eq!(url, None); + } + + #[test] + fn url_origin() { + let term = url_create_term(" test https://example.org "); + let url = term.url_search(Point::new(0, Column(10))); + assert_eq!(url.map(|u| u.origin), Some(4)); + + let term = url_create_term("https://example.org"); + let url = term.url_search(Point::new(0, Column(0))); + assert_eq!(url.map(|u| u.origin), Some(0)); + + let term = url_create_term("https://全.org"); + let url = term.url_search(Point::new(0, Column(10))); + assert_eq!(url.map(|u| u.origin), Some(10)); + + let term = url_create_term("https://全.org"); + let url = term.url_search(Point::new(0, Column(8))); + assert_eq!(url.map(|u| u.origin), Some(8)); + + let term = url_create_term("https://全.org"); + let url = term.url_search(Point::new(0, Column(9))); + assert_eq!(url.map(|u| u.origin), Some(9)); + + let term = url_create_term("test@https://example.org"); + let url = term.url_search(Point::new(0, Column(9))); + assert_eq!(url.map(|u| u.origin), Some(4)); + + let term = url_create_term("test全https://example.org"); + let url = term.url_search(Point::new(0, Column(9))); + assert_eq!(url.map(|u| u.origin), Some(3)); + } + + #[test] + fn url_matching_chars() { + url_test("(https://example.org/test(ing))", "https://example.org/test(ing)"); + url_test("https://example.org/test(ing)", "https://example.org/test(ing)"); + url_test("((https://example.org))", "https://example.org"); + url_test(")https://example.org(", "https://example.org"); + url_test("https://example.org)", "https://example.org"); + url_test("https://example.org(", "https://example.org"); + url_test("(https://one.org/)(https://two.org/)", "https://one.org"); + + url_test("https://[2001:db8:a0b:12f0::1]:80", "https://[2001:db8:a0b:12f0::1]:80"); + url_test("([(https://example.org/test(ing))])", "https://example.org/test(ing)"); + url_test("https://example.org/]()", "https://example.org"); + url_test("[https://example.org]", "https://example.org"); + + url_test("'https://example.org/test'ing'''", "https://example.org/test'ing'"); + url_test("https://example.org/test'ing'", "https://example.org/test'ing'"); + url_test("'https://example.org'", "https://example.org"); + url_test("'https://example.org", "https://example.org"); + url_test("https://example.org'", "https://example.org"); + + url_test("(https://example.org/test全)", "https://example.org/test全"); + } + + #[test] + fn url_detect_end() { + url_test("https://example.org/test\u{00}ing", "https://example.org/test"); + url_test("https://example.org/test\u{1F}ing", "https://example.org/test"); + url_test("https://example.org/test\u{7F}ing", "https://example.org/test"); + url_test("https://example.org/test\u{9F}ing", "https://example.org/test"); + url_test("https://example.org/test\ting", "https://example.org/test"); + url_test("https://example.org/test ing", "https://example.org/test"); + } + + #[test] + fn url_remove_end_chars() { + url_test("https://example.org/test?ing", "https://example.org/test?ing"); + url_test("https://example.org.,;:)'!/?", "https://example.org"); + url_test("https://example.org'.", "https://example.org"); + } + + #[test] + fn url_remove_start_chars() { + url_test("complicated:https://example.org", "https://example.org"); + url_test("test.https://example.org", "https://example.org"); + url_test(",https://example.org", "https://example.org"); + url_test("\u{2502}https://example.org", "https://example.org"); + } + + #[test] + fn url_unicode() { + url_test("https://xn--example-2b07f.org", "https://xn--example-2b07f.org"); + url_test("https://example.org/\u{2008A}", "https://example.org/\u{2008A}"); + url_test("https://example.org/\u{f17c}", "https://example.org/\u{f17c}"); + url_test("https://üñîçøðé.com/ä", "https://üñîçøðé.com/ä"); + } + + #[test] + fn url_schemes() { + url_test("mailto://example.org", "mailto://example.org"); + url_test("https://example.org", "https://example.org"); + url_test("http://example.org", "http://example.org"); + url_test("news://example.org", "news://example.org"); + url_test("file://example.org", "file://example.org"); + url_test("git://example.org", "git://example.org"); + url_test("ssh://example.org", "ssh://example.org"); + url_test("ftp://example.org", "ftp://example.org"); + } +} diff --git a/alacritty_terminal/src/util.rs b/alacritty_terminal/src/util.rs new file mode 100644 index 00000000..b8703012 --- /dev/null +++ b/alacritty_terminal/src/util.rs @@ -0,0 +1,143 @@ +// 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. + +use std::ffi::OsStr; +use std::process::{Command, Stdio}; +use std::{cmp, io}; + +#[cfg(not(windows))] +use std::os::unix::process::CommandExt; + +#[cfg(windows)] +use std::os::windows::process::CommandExt; +#[cfg(windows)] +use winapi::um::winbase::{CREATE_NEW_PROCESS_GROUP, CREATE_NO_WINDOW}; + +/// Threading utilities +pub mod thread { + /// Like `thread::spawn`, but with a `name` argument + pub fn spawn_named<F, T, S>(name: S, f: F) -> ::std::thread::JoinHandle<T> + where + F: FnOnce() -> T, + F: Send + 'static, + T: Send + 'static, + S: Into<String>, + { + ::std::thread::Builder::new().name(name.into()).spawn(f).expect("thread spawn works") + } + + pub use std::thread::*; +} + +pub fn limit<T: Ord>(value: T, min: T, max: T) -> T { + cmp::min(cmp::max(value, min), max) +} + +/// Utilities for writing to the +pub mod fmt { + use std::fmt; + + macro_rules! define_colors { + ($($(#[$attrs:meta])* pub struct $s:ident => $color:expr;)*) => { + $( + $(#[$attrs])* + pub struct $s<T>(pub T); + + impl<T: fmt::Display> fmt::Display for $s<T> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, concat!("\x1b[", $color, "m{}\x1b[0m"), self.0) + } + } + + impl<T: fmt::Debug> fmt::Debug for $s<T> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, concat!("\x1b[", $color, "m{:?}\x1b[0m"), self.0) + } + } + )* + } + } + + define_colors! { + /// Write a `Display` or `Debug` escaped with Red + pub struct Red => "31"; + + /// Write a `Display` or `Debug` escaped with Green + pub struct Green => "32"; + + /// Write a `Display` or `Debug` escaped with Yellow + pub struct Yellow => "33"; + } +} + +#[cfg(not(windows))] +pub fn start_daemon<I, S>(program: &str, args: I) -> io::Result<()> +where + I: IntoIterator<Item = S>, + S: AsRef<OsStr>, +{ + Command::new(program) + .args(args) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .before_exec(|| unsafe { + match ::libc::fork() { + -1 => return Err(io::Error::last_os_error()), + 0 => (), + _ => ::libc::_exit(0), + } + + if ::libc::setsid() == -1 { + return Err(io::Error::last_os_error()); + } + + Ok(()) + }) + .spawn()? + .wait() + .map(|_| ()) +} + +#[cfg(windows)] +pub fn start_daemon<I, S>(program: &str, args: I) -> io::Result<()> +where + I: IntoIterator<Item = S>, + S: AsRef<OsStr>, +{ + // Setting all the I/O handles to null and setting the + // CREATE_NEW_PROCESS_GROUP and CREATE_NO_WINDOW has the effect + // that console applications will run without opening a new + // console window. + Command::new(program) + .args(args) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .creation_flags(CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW) + .spawn() + .map(|_| ()) +} + +#[cfg(test)] +mod tests { + use super::limit; + + #[test] + fn limit_works() { + assert_eq!(10, limit(10, 0, 100)); + assert_eq!(10, limit(5, 10, 100)); + assert_eq!(100, limit(1000, 10, 100)); + } +} diff --git a/alacritty_terminal/src/window.rs b/alacritty_terminal/src/window.rs new file mode 100644 index 00000000..89e455dc --- /dev/null +++ b/alacritty_terminal/src/window.rs @@ -0,0 +1,497 @@ +// 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. +use std::convert::From; +use std::fmt::Display; + +use crate::gl; +use glutin::dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}; +#[cfg(not(any(target_os = "macos", windows)))] +use glutin::os::unix::EventsLoopExt; +#[cfg(windows)] +use glutin::Icon; +use glutin::{ + self, ContextBuilder, ControlFlow, Event, EventsLoop, MouseCursor, PossiblyCurrent, + WindowBuilder, +}; +#[cfg(windows)] +use image::ImageFormat; + +use crate::cli::Options; +use crate::config::{Decorations, StartupMode, WindowConfig}; + +#[cfg(windows)] +static WINDOW_ICON: &'static [u8] = include_bytes!("../../extra/windows/alacritty.ico"); + +/// Default Alacritty name, used for window title and class. +pub const DEFAULT_NAME: &str = "Alacritty"; + +/// Window errors +#[derive(Debug)] +pub enum Error { + /// Error creating the window + ContextCreation(glutin::CreationError), + + /// Error manipulating the rendering context + Context(glutin::ContextError), +} + +/// Result of fallible operations concerning a Window. +type Result<T> = ::std::result::Result<T, Error>; + +/// A window which can be used for displaying the terminal +/// +/// Wraps the underlying windowing library to provide a stable API in Alacritty +pub struct Window { + event_loop: EventsLoop, + windowed_context: glutin::WindowedContext<PossiblyCurrent>, + mouse_visible: bool, + + /// Whether or not the window is the focused window. + pub is_focused: bool, +} + +/// Threadsafe APIs for the window +pub struct Proxy { + inner: glutin::EventsLoopProxy, +} + +/// Information about where the window is being displayed +/// +/// Useful for subsystems like the font rasterized which depend on DPI and scale +/// factor. +pub struct DeviceProperties { + /// Scale factor for pixels <-> points. + /// + /// This will be 1. on standard displays and may have a different value on + /// hidpi displays. + pub scale_factor: f64, +} + +impl ::std::error::Error for Error { + fn cause(&self) -> Option<&dyn (::std::error::Error)> { + match *self { + Error::ContextCreation(ref err) => Some(err), + Error::Context(ref err) => Some(err), + } + } + + fn description(&self) -> &str { + match *self { + Error::ContextCreation(ref _err) => "Error creating gl context", + Error::Context(ref _err) => "Error operating on render context", + } + } +} + +impl Display for Error { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + match *self { + Error::ContextCreation(ref err) => write!(f, "Error creating GL context; {}", err), + Error::Context(ref err) => write!(f, "Error operating on render context; {}", err), + } + } +} + +impl From<glutin::CreationError> for Error { + fn from(val: glutin::CreationError) -> Error { + Error::ContextCreation(val) + } +} + +impl From<glutin::ContextError> for Error { + fn from(val: glutin::ContextError) -> Error { + Error::Context(val) + } +} + +fn create_gl_window( + mut window: WindowBuilder, + event_loop: &EventsLoop, + srgb: bool, + dimensions: Option<LogicalSize>, +) -> Result<glutin::WindowedContext<PossiblyCurrent>> { + if let Some(dimensions) = dimensions { + window = window.with_dimensions(dimensions); + } + + let windowed_context = ContextBuilder::new() + .with_srgb(srgb) + .with_vsync(true) + .with_hardware_acceleration(None) + .build_windowed(window, event_loop)?; + + // Make the context current so OpenGL operations can run + let windowed_context = unsafe { windowed_context.make_current().map_err(|(_, e)| e)? }; + + Ok(windowed_context) +} + +impl Window { + /// Create a new window + /// + /// This creates a window and fully initializes a window. + pub fn new( + event_loop: EventsLoop, + options: &Options, + window_config: &WindowConfig, + dimensions: Option<LogicalSize>, + ) -> Result<Window> { + let title = options.title.as_ref().map_or(DEFAULT_NAME, |t| t); + let class = options.class.as_ref().map_or(DEFAULT_NAME, |c| c); + let window_builder = Window::get_platform_window(title, class, window_config); + let windowed_context = + create_gl_window(window_builder.clone(), &event_loop, false, dimensions) + .or_else(|_| create_gl_window(window_builder, &event_loop, true, dimensions))?; + let window = windowed_context.window(); + window.show(); + + // Maximize window after mapping in X11 + #[cfg(not(any(target_os = "macos", windows)))] + { + if event_loop.is_x11() && window_config.startup_mode() == StartupMode::Maximized { + window.set_maximized(true); + } + } + + // Set window position + // + // TODO: replace `set_position` with `with_position` once available + // Upstream issue: https://github.com/tomaka/winit/issues/806 + let position = options.position().or_else(|| window_config.position()); + if let Some(position) = position { + let physical = PhysicalPosition::from((position.x, position.y)); + let logical = physical.to_logical(window.get_hidpi_factor()); + window.set_position(logical); + } + + if let StartupMode::Fullscreen = window_config.startup_mode() { + let current_monitor = window.get_current_monitor(); + window.set_fullscreen(Some(current_monitor)); + } + + #[cfg(target_os = "macos")] + { + if let StartupMode::SimpleFullscreen = window_config.startup_mode() { + use glutin::os::macos::WindowExt; + window.set_simple_fullscreen(true); + } + } + + // Text cursor + window.set_cursor(MouseCursor::Text); + + // Set OpenGL symbol loader. This call MUST be after window.make_current on windows. + gl::load_with(|symbol| windowed_context.get_proc_address(symbol) as *const _); + + let window = + Window { event_loop, windowed_context, mouse_visible: true, is_focused: false }; + + window.run_os_extensions(); + + Ok(window) + } + + /// Get some properties about the device + /// + /// Some window properties are provided since subsystems like font + /// rasterization depend on DPI and scale factor. + pub fn device_properties(&self) -> DeviceProperties { + DeviceProperties { scale_factor: self.window().get_hidpi_factor() } + } + + pub fn inner_size_pixels(&self) -> Option<LogicalSize> { + self.window().get_inner_size() + } + + pub fn set_inner_size(&mut self, size: LogicalSize) { + self.window().set_inner_size(size); + } + + #[inline] + pub fn hidpi_factor(&self) -> f64 { + self.window().get_hidpi_factor() + } + + #[inline] + pub fn create_window_proxy(&self) -> Proxy { + Proxy { inner: self.event_loop.create_proxy() } + } + + #[inline] + pub fn swap_buffers(&self) -> Result<()> { + self.windowed_context.swap_buffers().map_err(From::from) + } + + /// Poll for any available events + #[inline] + pub fn poll_events<F>(&mut self, func: F) + where + F: FnMut(Event), + { + self.event_loop.poll_events(func); + } + + #[inline] + pub fn resize(&self, size: PhysicalSize) { + self.windowed_context.resize(size); + } + + /// Block waiting for events + #[inline] + pub fn wait_events<F>(&mut self, func: F) + where + F: FnMut(Event) -> ControlFlow, + { + self.event_loop.run_forever(func); + } + + /// Set the window title + #[inline] + pub fn set_title(&self, title: &str) { + self.window().set_title(title); + } + + #[inline] + pub fn set_mouse_cursor(&self, cursor: MouseCursor) { + self.window().set_cursor(cursor); + } + + /// Set mouse cursor visible + pub fn set_mouse_visible(&mut self, visible: bool) { + if visible != self.mouse_visible { + self.mouse_visible = visible; + self.window().hide_cursor(!visible); + } + } + + #[cfg(not(any(target_os = "macos", windows)))] + pub fn get_platform_window( + title: &str, + class: &str, + window_config: &WindowConfig, + ) -> WindowBuilder { + use glutin::os::unix::WindowBuilderExt; + + let decorations = match window_config.decorations() { + Decorations::None => false, + _ => true, + }; + + WindowBuilder::new() + .with_title(title) + .with_visibility(false) + .with_transparency(true) + .with_decorations(decorations) + .with_maximized(window_config.startup_mode() == StartupMode::Maximized) + // X11 + .with_class(class.into(), DEFAULT_NAME.into()) + // Wayland + .with_app_id(class.into()) + } + + #[cfg(windows)] + pub fn get_platform_window( + title: &str, + _class: &str, + window_config: &WindowConfig, + ) -> WindowBuilder { + let icon = Icon::from_bytes_with_format(WINDOW_ICON, ImageFormat::ICO).unwrap(); + + let decorations = match window_config.decorations() { + Decorations::None => false, + _ => true, + }; + + WindowBuilder::new() + .with_title(title) + .with_visibility(cfg!(windows)) + .with_decorations(decorations) + .with_transparency(true) + .with_maximized(window_config.startup_mode() == StartupMode::Maximized) + .with_window_icon(Some(icon)) + } + + #[cfg(target_os = "macos")] + pub fn get_platform_window( + title: &str, + _class: &str, + window_config: &WindowConfig, + ) -> WindowBuilder { + use glutin::os::macos::WindowBuilderExt; + + let window = WindowBuilder::new() + .with_title(title) + .with_visibility(false) + .with_transparency(true) + .with_maximized(window_config.startup_mode() == StartupMode::Maximized); + + match window_config.decorations() { + Decorations::Full => window, + Decorations::Transparent => window + .with_title_hidden(true) + .with_titlebar_transparent(true) + .with_fullsize_content_view(true), + Decorations::Buttonless => window + .with_title_hidden(true) + .with_titlebar_buttons_hidden(true) + .with_titlebar_transparent(true) + .with_fullsize_content_view(true), + Decorations::None => window.with_titlebar_hidden(true), + } + } + + #[cfg(any( + target_os = "linux", + target_os = "freebsd", + target_os = "dragonfly", + target_os = "openbsd" + ))] + pub fn set_urgent(&self, is_urgent: bool) { + use glutin::os::unix::WindowExt; + self.window().set_urgent(is_urgent); + } + + #[cfg(target_os = "macos")] + pub fn set_urgent(&self, is_urgent: bool) { + use glutin::os::macos::WindowExt; + self.window().request_user_attention(is_urgent); + } + + #[cfg(windows)] + pub fn set_urgent(&self, _is_urgent: bool) {} + + pub fn set_ime_spot(&self, pos: LogicalPosition) { + self.window().set_ime_spot(pos); + } + + #[cfg(not(any(target_os = "macos", target_os = "windows")))] + pub fn get_window_id(&self) -> Option<usize> { + use glutin::os::unix::WindowExt; + + match self.window().get_xlib_window() { + Some(xlib_window) => Some(xlib_window as usize), + None => None, + } + } + + #[cfg(any(target_os = "macos", target_os = "windows"))] + pub fn get_window_id(&self) -> Option<usize> { + None + } + + /// Hide the window + pub fn hide(&self) { + self.window().hide(); + } + + /// Fullscreens the window on the current monitor. + pub fn set_fullscreen(&self, fullscreen: bool) { + let glutin_window = self.window(); + if fullscreen { + let current_monitor = glutin_window.get_current_monitor(); + glutin_window.set_fullscreen(Some(current_monitor)); + } else { + glutin_window.set_fullscreen(None); + } + } + + #[cfg(target_os = "macos")] + pub fn set_simple_fullscreen(&self, fullscreen: bool) { + use glutin::os::macos::WindowExt; + self.window().set_simple_fullscreen(fullscreen); + } + + fn window(&self) -> &glutin::Window { + self.windowed_context.window() + } +} + +pub trait OsExtensions { + fn run_os_extensions(&self) {} +} + +#[cfg(not(any( + target_os = "linux", + target_os = "freebsd", + target_os = "dragonfly", + target_os = "openbsd" +)))] +impl OsExtensions for Window {} + +#[cfg(any( + target_os = "linux", + target_os = "freebsd", + target_os = "dragonfly", + target_os = "openbsd" +))] +impl OsExtensions for Window { + fn run_os_extensions(&self) { + use glutin::os::unix::WindowExt; + use libc::getpid; + use std::ffi::CStr; + use std::ptr; + use x11_dl::xlib::{self, PropModeReplace, XA_CARDINAL}; + + let xlib_display = self.window().get_xlib_display(); + let xlib_window = self.window().get_xlib_window(); + + if let (Some(xlib_window), Some(xlib_display)) = (xlib_window, xlib_display) { + let xlib = xlib::Xlib::open().expect("get xlib"); + + // Set _NET_WM_PID to process pid + unsafe { + let _net_wm_pid = CStr::from_ptr(b"_NET_WM_PID\0".as_ptr() as *const _); + let atom = (xlib.XInternAtom)(xlib_display as *mut _, _net_wm_pid.as_ptr(), 0); + let pid = getpid(); + + (xlib.XChangeProperty)( + xlib_display as _, + xlib_window as _, + atom, + XA_CARDINAL, + 32, + PropModeReplace, + &pid as *const i32 as *const u8, + 1, + ); + } + // Although this call doesn't actually pass any data, it does cause + // WM_CLIENT_MACHINE to be set. WM_CLIENT_MACHINE MUST be set if _NET_WM_PID is set + // (which we do above). + unsafe { + (xlib.XSetWMProperties)( + xlib_display as _, + xlib_window as _, + ptr::null_mut(), + ptr::null_mut(), + ptr::null_mut(), + 0, + ptr::null_mut(), + ptr::null_mut(), + ptr::null_mut(), + ); + } + } + } +} + +impl Proxy { + /// Wakes up the event loop of the window + /// + /// This is useful for triggering a draw when the renderer would otherwise + /// be waiting on user input. + pub fn wakeup_event_loop(&self) { + self.inner.wakeup().unwrap(); + } +} |