aboutsummaryrefslogtreecommitdiff
path: root/alacritty_terminal/src
diff options
context:
space:
mode:
authorTheodore Dubois <tblodt@icloud.com>2019-04-28 06:24:58 -0700
committerChristian Duerr <chrisduerr@users.noreply.github.com>2019-04-28 13:24:58 +0000
commitdbd8538762ef8968a493e1bf996e8693479ca783 (patch)
tree32ac2a6a5e01238a272d4ba534551d2e42903c7a /alacritty_terminal/src
parent9c6d12ea2c863ba76015bdedc00db13b7307725a (diff)
downloadalacritty-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')
-rw-r--r--alacritty_terminal/src/ansi.rs1568
-rw-r--r--alacritty_terminal/src/cli.rs243
-rw-r--r--alacritty_terminal/src/config/bindings.rs233
-rw-r--r--alacritty_terminal/src/config/mod.rs2749
-rw-r--r--alacritty_terminal/src/cursor.rs99
-rw-r--r--alacritty_terminal/src/display.rs560
-rw-r--r--alacritty_terminal/src/event.rs595
-rw-r--r--alacritty_terminal/src/event_loop.rs436
-rw-r--r--alacritty_terminal/src/grid/mod.rs886
-rw-r--r--alacritty_terminal/src/grid/row.rs264
-rw-r--r--alacritty_terminal/src/grid/storage.rs922
-rw-r--r--alacritty_terminal/src/grid/tests.rs292
-rw-r--r--alacritty_terminal/src/index.rs406
-rw-r--r--alacritty_terminal/src/input.rs1300
-rw-r--r--alacritty_terminal/src/lib.rs60
-rw-r--r--alacritty_terminal/src/locale.rs89
-rw-r--r--alacritty_terminal/src/macros.rs21
-rw-r--r--alacritty_terminal/src/message_bar.rs473
-rw-r--r--alacritty_terminal/src/meter.rs110
-rw-r--r--alacritty_terminal/src/panic.rs53
-rw-r--r--alacritty_terminal/src/renderer/mod.rs1629
-rw-r--r--alacritty_terminal/src/renderer/rects.rs156
-rw-r--r--alacritty_terminal/src/selection.rs571
-rw-r--r--alacritty_terminal/src/sync.rs44
-rw-r--r--alacritty_terminal/src/term/cell.rs205
-rw-r--r--alacritty_terminal/src/term/color.rs224
-rw-r--r--alacritty_terminal/src/term/mod.rs2442
-rw-r--r--alacritty_terminal/src/tty/mod.rs96
-rw-r--r--alacritty_terminal/src/tty/unix.rs405
-rw-r--r--alacritty_terminal/src/tty/windows/conpty.rs289
-rw-r--r--alacritty_terminal/src/tty/windows/mod.rs303
-rw-r--r--alacritty_terminal/src/tty/windows/winpty.rs169
-rw-r--r--alacritty_terminal/src/url.rs307
-rw-r--r--alacritty_terminal/src/util.rs143
-rw-r--r--alacritty_terminal/src/window.rs497
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 &params[1..] {
+ match parse_number(param) {
+ Some(index) => self.handler.reset_color(index as usize),
+ None => unhandled(params),
+ }
+ }
+ },
+
+ // Reset foreground color
+ b"110" => self.handler.reset_color(NamedColor::Foreground as usize),
+
+ // Reset background color
+ b"111" => self.handler.reset_color(NamedColor::Background as usize),
+
+ // Reset text cursor color
+ b"112" => self.handler.reset_color(NamedColor::Cursor as usize),
+
+ _ => unhandled(params),
+ }
+ }
+
+ #[inline]
+ fn csi_dispatch(&mut self, args: &[i64], intermediates: &[u8], _ignore: bool, action: char) {
+ let private = intermediates.get(0).map(|b| *b == b'?').unwrap_or(false);
+ let handler = &mut self.handler;
+ let writer = &mut self.writer;
+
+ macro_rules! unhandled {
+ () => {{
+ 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(&regular_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(&regular_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();
+ }
+}