| // 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::ops::{Range, Index, IndexMut}; |
| use std::{ptr, io, mem}; |
| use std::cmp::{min, max}; |
| use std::time::{Duration, Instant}; |
| |
| use arraydeque::ArrayDeque; |
| use unicode_width::UnicodeWidthChar; |
| use url::Url; |
| |
| use font::{self, Size}; |
| use ansi::{self, Color, NamedColor, Attr, Handler, CharsetIndex, StandardCharset, CursorStyle}; |
| use grid::{BidirectionalIterator, Grid, Indexed, IndexRegion, DisplayIter, Scroll, ViewportPosition}; |
| use index::{self, Point, Column, Line, IndexRange, Contains, RangeInclusive, Linear}; |
| use selection::{self, Selection, Locations}; |
| use config::{Config, VisualBellAnimation}; |
| use {MouseCursor, Rgb}; |
| use copypasta::{Clipboard, Load, Store}; |
| use input::FONT_SIZE_STEP; |
| |
| pub mod cell; |
| pub mod color; |
| pub use self::cell::Cell; |
| use self::cell::LineLength; |
| |
| const URL_SEPARATOR_CHARS: [char; 3] = [' ', '"', '\'']; |
| |
| /// 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<String>; |
| } |
| |
| 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() - Column(1); |
| |
| while let Some(cell) = iter.next() { |
| if self.semantic_escape_chars.contains(cell.c) { |
| break; |
| } |
| |
| point = iter.cur; |
| |
| if iter.cur.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<String> { |
| point.line = self.grid.num_lines().0 - point.line - 1; |
| |
| // Limit the starting point to the last line in the history |
| point.line = min(point.line, self.grid.len() - 1); |
| |
| // Create forwards and backwards iterators |
| let iterf = self.grid.iter_from(point); |
| point.col += 1; |
| let mut iterb = self.grid.iter_from(point); |
| |
| // Put all characters until separators into a string |
| let mut buf = String::new(); |
| while let Some(cell) = iterb.prev() { |
| if URL_SEPARATOR_CHARS.contains(&cell.c) { |
| break; |
| } |
| buf.insert(0, cell.c); |
| } |
| for cell in iterf { |
| if URL_SEPARATOR_CHARS.contains(&cell.c) { |
| break; |
| } |
| buf.push(cell.c); |
| } |
| |
| // Heuristic to remove all leading '(' |
| while buf.starts_with('(') { |
| buf.remove(0); |
| } |
| |
| // Heuristic to remove all ')' from end of URLs without matching '(' |
| let str_count = |text: &str, c: char| { |
| text.chars().filter(|tc| *tc == c).count() |
| }; |
| while buf.ends_with(')') && str_count(&buf, '(') < str_count(&buf, ')') { |
| buf.pop(); |
| } |
| |
| // Check if string is valid url |
| match Url::parse(&buf) { |
| Ok(_) => Some(buf), |
| Err(_) => None, |
| } |
| } |
| } |
| |
| 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, |
| mode: TermMode, |
| config: &'a Config, |
| colors: &'a color::List, |
| selection: Option<RangeInclusive<index::Linear>>, |
| cursor_cells: ArrayDeque<[Indexed<Cell>; 3]>, |
| } |
| |
| impl<'a> RenderableCellsIter<'a> { |
| /// Create the renderable cells iterator |
| /// |
| /// The cursor and terminal mode are required for properly displaying the |
| /// cursor. |
| fn new<'b>( |
| grid: &'b Grid<Cell>, |
| cursor: &'b Point, |
| colors: &'b color::List, |
| mode: TermMode, |
| config: &'b Config, |
| selection: Option<Locations>, |
| cursor_style: CursorStyle, |
| ) -> RenderableCellsIter<'b> { |
| let cursor_offset = grid.line_to_offset(cursor.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(start.line.0 * cols.0 + start.col.0); |
| let end = Linear(end.line.0 * cols.0 + end.col.0); |
| |
| // Update the selection |
| selection_range = Some(RangeInclusive::new(start, end)); |
| } |
| } |
| |
| RenderableCellsIter { |
| cursor, |
| cursor_offset, |
| grid, |
| inner, |
| mode, |
| selection: selection_range, |
| config, |
| colors, |
| cursor_cells: ArrayDeque::new(), |
| }.initialize(cursor_style) |
| } |
| |
| fn push_cursor_cells(&mut self, original: Cell, cursor: Cell, wide: Cell) { |
| // Prints the char under the cell if cursor is situated on a non-empty cell |
| self.cursor_cells.push_back(Indexed { |
| line: self.cursor.line, |
| column: self.cursor.col, |
| inner: original, |
| }).expect("won't exceed capacity"); |
| |
| // Prints the cursor |
| self.cursor_cells.push_back(Indexed { |
| line: self.cursor.line, |
| column: self.cursor.col, |
| inner: cursor, |
| }).expect("won't exceed capacity"); |
| |
| // If cursor is over a wide (2 cell size) character, |
| // print the second cursor cell |
| if self.is_wide_cursor(&cursor) { |
| self.cursor_cells.push_back(Indexed { |
| line: self.cursor.line, |
| column: self.cursor.col + 1, |
| inner: wide, |
| }).expect("won't exceed capacity"); |
| } |
| } |
| |
| fn populate_block_cursor(&mut self) { |
| let (text_color, cursor_color) = if self.config.custom_cursor_colors() { |
| ( |
| Color::Named(NamedColor::CursorText), |
| Color::Named(NamedColor::Cursor) |
| ) |
| } else { |
| // Swap fg, bg |
| let cell = &self.grid[self.cursor]; |
| (cell.bg, cell.fg) |
| }; |
| |
| let original_cell = self.grid[self.cursor]; |
| |
| let mut cursor_cell = self.grid[self.cursor]; |
| cursor_cell.fg = text_color; |
| cursor_cell.bg = cursor_color; |
| |
| let mut wide_cell = cursor_cell; |
| wide_cell.c = ' '; |
| |
| self.push_cursor_cells(original_cell, cursor_cell, wide_cell); |
| } |
| |
| fn populate_char_cursor(&mut self, cursor_cell_char: char, wide_cell_char: char) { |
| let original_cell = self.grid[self.cursor]; |
| |
| let mut cursor_cell = self.grid[self.cursor]; |
| let cursor_color = self.text_cursor_color(&cursor_cell); |
| cursor_cell.c = cursor_cell_char; |
| cursor_cell.fg = cursor_color; |
| |
| let mut wide_cell = cursor_cell; |
| wide_cell.c = wide_cell_char; |
| |
| self.push_cursor_cells(original_cell, cursor_cell, wide_cell); |
| } |
| |
| fn populate_underline_cursor(&mut self) { |
| self.populate_char_cursor(font::UNDERLINE_CURSOR_CHAR, font::UNDERLINE_CURSOR_CHAR); |
| } |
| |
| fn populate_beam_cursor(&mut self) { |
| self.populate_char_cursor(font::BEAM_CURSOR_CHAR, ' '); |
| } |
| |
| fn populate_box_cursor(&mut self) { |
| self.populate_char_cursor(font::BOX_CURSOR_CHAR, ' '); |
| } |
| |
| #[inline] |
| fn is_wide_cursor(&self, cell: &Cell) -> bool { |
| cell.flags.contains(cell::Flags::WIDE_CHAR) && (self.cursor.col + 1) < self.grid.num_cols() |
| } |
| |
| fn text_cursor_color(&self, cell: &Cell) -> Color { |
| if self.config.custom_cursor_colors() { |
| Color::Named(NamedColor::Cursor) |
| } else { |
| // Cursor is same color as text |
| cell.fg |
| } |
| } |
| |
| /// Populates list of cursor cells with the original cell |
| fn populate_no_cursor(&mut self) { |
| self.cursor_cells.push_back(Indexed { |
| line: self.cursor.line, |
| column: self.cursor.col, |
| inner: self.grid[self.cursor], |
| }).expect("won't exceed capacity"); |
| } |
| |
| fn initialize(mut self, cursor_style: CursorStyle) -> Self { |
| if self.cursor_is_visible() { |
| match cursor_style { |
| CursorStyle::HollowBlock => { |
| self.populate_box_cursor(); |
| }, |
| CursorStyle::Block => { |
| self.populate_block_cursor(); |
| }, |
| CursorStyle::Beam => { |
| self.populate_beam_cursor(); |
| }, |
| CursorStyle::Underline => { |
| self.populate_underline_cursor(); |
| } |
| } |
| } else { |
| self.populate_no_cursor(); |
| } |
| self |
| } |
| |
| /// Check if the cursor should be rendered. |
| #[inline] |
| fn cursor_is_visible(&self) -> bool { |
| self.mode.contains(mode::TermMode::SHOW_CURSOR) && self.grid.contains(self.cursor) |
| } |
| |
| fn compute_fg_rgb(&self, fg: Color, cell: &Cell) -> Rgb { |
| use self::cell::Flags; |
| match fg { |
| Color::Spec(rgb) => rgb, |
| Color::Named(ansi) => { |
| match (self.config.draw_bold_text_with_bright_colors(), cell.flags & Flags::DIM_BOLD) { |
| // If no bright foreground is set, treat it like the BOLD flag doesn't exist |
| (_, self::cell::Flags::DIM_BOLD) |
| if ansi == NamedColor::Foreground |
| && self.config.colors().primary.bright_foreground.is_none() => |
| { |
| self.colors[NamedColor::DimForeground] |
| } |
| // Draw bold text in bright colors *and* contains bold flag. |
| (true, self::cell::Flags::BOLD) => self.colors[ansi.to_bright()], |
| // Cell is marked as dim and not bold |
| (_, self::cell::Flags::DIM) | |
| (false, self::cell::Flags::DIM_BOLD) => self.colors[ansi.to_dim()], |
| // None of the above, keep original color. |
| _ => self.colors[ansi] |
| } |
| }, |
| Color::Indexed(idx) => { |
| let idx = match ( |
| self.config.draw_bold_text_with_bright_colors(), |
| cell.flags & Flags::DIM_BOLD, |
| idx |
| ) { |
| (true, self::cell::Flags::BOLD, 0...7) => idx as usize + 8, |
| (false, self::cell::Flags::DIM, 8...15) => idx as usize - 8, |
| (false, self::cell::Flags::DIM, 0...7) => idx as usize + 260, |
| _ => idx as usize, |
| }; |
| |
| self.colors[idx] |
| } |
| } |
| } |
| |
| #[inline] |
| fn compute_bg_alpha(&self, bg: Color) -> f32 { |
| match bg { |
| Color::Named(NamedColor::Background) => 0.0, |
| _ => 1.0 |
| } |
| } |
| |
| fn compute_bg_rgb(&self, bg: Color) -> Rgb { |
| match bg { |
| Color::Spec(rgb) => rgb, |
| Color::Named(ansi) => self.colors[ansi], |
| Color::Indexed(idx) => self.colors[idx], |
| } |
| } |
| } |
| |
| pub struct RenderableCell { |
| /// A _Display_ line (not necessarily an _Active_ line) |
| pub line: Line, |
| pub column: Column, |
| pub c: char, |
| pub fg: Rgb, |
| pub bg: Rgb, |
| pub bg_alpha: f32, |
| pub flags: cell::Flags, |
| } |
| |
| 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 { |
| // Handle cursor |
| let (cell, selected) = if self.cursor_offset == self.inner.offset() && |
| self.inner.column() == self.cursor.col |
| { |
| // Cursor cell |
| let mut cell = self.cursor_cells.pop_front().unwrap(); |
| cell.line = self.inner.line(); |
| |
| // Since there may be multiple cursor cells (for a wide |
| // char), only update iteration position after all cursor |
| // cells have been drawn. |
| if self.cursor_cells.is_empty() { |
| self.inner.next(); |
| } |
| (cell, false) |
| } else { |
| let cell = self.inner.next()?; |
| |
| let index = Linear(cell.line.0 * self.grid.num_cols().0 + cell.column.0); |
| |
| let selected = self.selection.as_ref() |
| .map(|range| range.contains_(index)) |
| .unwrap_or(false); |
| |
| // Skip empty cells |
| if cell.is_empty() && !selected { |
| continue; |
| } |
| |
| (cell, selected) |
| }; |
| |
| // Apply inversion and lookup RGB values |
| let mut fg_rgb = self.compute_fg_rgb(cell.fg, &cell); |
| let mut bg_rgb = self.compute_bg_rgb(cell.bg); |
| |
| let bg_alpha = if selected ^ cell.inverse() { |
| mem::swap(&mut fg_rgb, &mut bg_rgb); |
| self.compute_bg_alpha(cell.fg) |
| } else { |
| self.compute_bg_alpha(cell.bg) |
| }; |
| |
| return Some(RenderableCell { |
| line: cell.line, |
| column: cell.column, |
| flags: cell.flags, |
| c: cell.c, |
| fg: fg_rgb, |
| bg: bg_rgb, |
| bg_alpha, |
| }) |
| } |
| } |
| |
| } |
| |
| pub mod mode { |
| 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 self::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: Vec<bool>, |
| |
| /// 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, |
| |
| dynamic_title: bool, |
| |
| /// Number of spaces in one tab |
| tabspaces: usize, |
| |
| /// Automatically scroll to bottom when new lines are added |
| auto_scroll: 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, |
| } |
| |
| 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) -> bool { |
| 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, self.lines() - 1), |
| col: min(col, self.cols() - 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() |
| } |
| |
| pub fn scroll_display(&mut self, scroll: Scroll) { |
| self.grid.scroll_display(scroll); |
| 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) -> 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 = IndexRange::from(Column(0)..grid.num_cols()) |
| .map(|i| (*i as usize) % tabspaces == 0) |
| .collect::<Vec<bool>>(); |
| |
| let scroll_region = Line(0)..grid.num_lines(); |
| |
| 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::List::from(config.colors()), |
| color_modified: [false; color::COUNT], |
| original_colors: color::List::from(config.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, |
| } |
| } |
| |
| 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); |
| } |
| } |
| |
| use std::ops::Range; |
| |
| trait Append : PushChar { |
| fn append(&mut self, grid: &Grid<Cell>, line: usize, cols: Range<Column>); |
| } |
| |
| impl Append for String { |
| fn append(&mut self, grid: &Grid<Cell>, 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 { |
| for cell in &grid_line[cols.start..line_end] { |
| if !cell.flags.contains(cell::Flags::WIDE_CHAR_SPACER) { |
| self.push(cell.c); |
| } |
| } |
| |
| if cols.end >= grid.num_cols() - 1 { |
| self.maybe_newline(grid, line, line_end); |
| } |
| } |
| } |
| } |
| |
| let selection = self.grid.selection.clone()?; |
| let span = selection.to_span(self)?; |
| |
| 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, start.line, start.col..end.col); |
| }, |
| |
| // Selection ends on line following start |
| 1 => { |
| // Ending line |
| res.append(&self.grid, end.line, end.col..max_col); |
| |
| // Starting line |
| res.append(&self.grid, start.line, Column(0)..start.col); |
| |
| }, |
| |
| // Multi line selection |
| _ => { |
| // Ending line |
| res.append(&self.grid, end.line, end.col..max_col); |
| |
| let middle_range = (start.line + 1)..(end.line); |
| for line in middle_range.rev() { |
| res.append(&self.grid, line, Column(0)..max_col); |
| } |
| |
| // Starting line |
| res.append(&self.grid, 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 screen |
| pub fn pixels_to_coords(&self, x: usize, y: usize) -> Option<Point> { |
| if self.size_info.contains_point(x, y) { |
| 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 |
| } |
| |
| /// 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, |
| ) -> RenderableCellsIter { |
| let selection = self.grid.selection.as_ref() |
| .and_then(|s| s.to_span(self)) |
| .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.grid, |
| &self.cursor.point, |
| &self.colors, |
| self.mode, |
| config, |
| selection, |
| cursor, |
| ) |
| } |
| |
| /// Resize terminal to new dimensions |
| pub fn resize(&mut self, size : &SizeInfo) { |
| debug!("Term::resize"); |
| |
| // 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(); |
| |
| 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; |
| |
| // 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!("num_cols, num_lines = {}, {}", num_cols, num_lines); |
| |
| // Resize grids to new size |
| self.grid.resize(num_lines, num_cols, &Cell::default()); |
| self.alt_grid.resize(num_lines, num_cols, &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 = IndexRange::from(Column(0)..self.grid.num_cols()) |
| .map(|i| (*i as usize) % self.tabspaces == 0) |
| .collect::<Vec<bool>>(); |
| } |
| |
| #[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!("scroll_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 |
| self.grid.scroll_down(&(origin..self.scroll_region.end), lines, &self.cursor.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!("scroll_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 |
| self.grid.scroll_up(&(origin..self.scroll_region.end), lines, &self.cursor.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] |
| } |
| } |
| |
| 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()); |
| } |
| } |
| |
| /// 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(mode::TermMode::LINE_WRAP) { |
| return; |
| } |
| |
| trace!("wrapping"); |
| |
| { |
| 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() { |
| // Sigh, borrowck making us check the width twice. Hopefully the |
| // optimizer can fix it. |
| let num_cols = self.grid.num_cols(); |
| { |
| // If in insert mode, first shift cells to the right. |
| if self.mode.contains(mode::TermMode::INSERT) && self.cursor.point.col + width < num_cols { |
| let line = self.cursor.point.line; // borrowck |
| 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); |
| } |
| } |
| |
| 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); |
| } |
| } |
| |
| // Set spacer cell for wide chars. |
| if width == 2 && 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!("dectest"); |
| 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!("goto: line={}, col={}", line, col); |
| let (y_offset, max_y) = if self.mode.contains(mode::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!("goto_line: {}", line); |
| let col = self.cursor.point.col; // borrowck |
| self.goto(line, col) |
| } |
| |
| #[inline] |
| fn goto_col(&mut self, col: Column) { |
| trace!("goto_col: {}", col); |
| let line = self.cursor.point.line; // borrowck |
| self.goto(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 = self.cursor.point.line; // borrowck |
| let line = &mut self.grid[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!("move_up: {}", lines); |
| let move_to = Line(self.cursor.point.line.0.saturating_sub(lines.0)); |
| let col = self.cursor.point.col; // borrowck |
| self.goto(move_to, col) |
| } |
| |
| #[inline] |
| fn move_down(&mut self, lines: Line) { |
| trace!("move_down: {}", lines); |
| let move_to = self.cursor.point.line + lines; |
| let col = self.cursor.point.col; // borrowck |
| self.goto(move_to, col) |
| } |
| |
| #[inline] |
| fn move_forward(&mut self, cols: Column) { |
| trace!("move_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!("move_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!("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!("move_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!("move_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!("put_tab: {}", count); |
| |
| let mut col = self.cursor.point.col; |
| while col < self.grid.num_cols() && count != 0 { |
| count -= 1; |
| loop { |
| if (col + 1) == self.grid.num_cols() { |
| break; |
| } |
| |
| col += 1; |
| |
| if self.tabs[*col as usize] { |
| break; |
| } |
| } |
| } |
| |
| self.cursor.point.col = col; |
| 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(mode::TermMode::LINE_FEED_NEW_LINE) { |
| self.carriage_return(); |
| } |
| } |
| |
| #[inline] |
| fn set_horizontal_tabstop(&mut self) { |
| trace!("set_horizontal_tabstop"); |
| let column = self.cursor.point.col; |
| self.tabs[column.0] = 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!("insert_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!("delete_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!("erase_chars: {}, {}", count, self.cursor.point.col); |
| let start = self.cursor.point.col; |
| let end = min(start + count, self.grid.num_cols() - 1); |
| |
| 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 = self.cursor.point.line; // borrowck |
| let line = &mut self.grid[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!("move_backward_tabs: {}", count); |
| |
| for _ in 0..count { |
| let mut col = self.cursor.point.col; |
| for i in (0..(col.0)).rev() { |
| if self.tabs[i as usize] { |
| col = index::Column(i); |
| break; |
| } |
| } |
| self.cursor.point.col = col; |
| } |
| } |
| |
| #[inline] |
| fn move_forward_tabs(&mut self, count: i64) { |
| trace!("[unimplemented] move_forward_tabs: {}", count); |
| } |
| |
| #[inline] |
| fn save_cursor_position(&mut self) { |
| trace!("CursorSave"); |
| 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!("CursorRestore"); |
| 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!("clear_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!("set_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!("reset_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!("clear_screen: {:?}", mode); |
| let mut template = self.cursor.template; |
| template.flags ^= template.flags; |
| |
| 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); |
| } |
| }, |
| // If scrollback is implemented, this should clear it |
| ansi::ClearMode::Saved => { |
| self.grid.clear_history(); |
| } |
| } |
| } |
| |
| #[inline] |
| fn clear_tabs(&mut self, mode: ansi::TabulationClearMode) { |
| trace!("clear_tabs: {:?}", mode); |
| match mode { |
| ansi::TabulationClearMode::Current => { |
| let column = self.cursor.point.col; |
| self.tabs[column.0] = false; |
| }, |
| ansi::TabulationClearMode::All => { |
| let len = self.tabs.len(); |
| // Safe since false boolean is null, each item occupies only 1 |
| // byte, and called on the length of the vec. |
| unsafe { |
| ::std::ptr::write_bytes(self.tabs.as_mut_ptr(), 0, len); |
| } |
| } |
| } |
| } |
| |
| // Reset all important fields in the term struct |
| #[inline] |
| fn reset_state(&mut self) { |
| self.input_needs_wrap = false; |
| self.next_title = None; |
| self.next_mouse_cursor = None; |
| self.alt = false; |
| 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.clear_history(); |
| self.grid.region_mut(..).each(|c| c.reset(&Cell::default())); |
| } |
| |
| #[inline] |
| fn reverse_index(&mut self) { |
| trace!("reverse_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!("Set 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), |
| _ => { |
| debug!("Term got unhandled attr: {:?}", attr); |
| } |
| } |
| } |
| |
| #[inline] |
| fn set_mode(&mut self, mode: ansi::Mode) { |
| trace!("set_mode: {:?}", mode); |
| match mode { |
| ansi::Mode::SwapScreenAndSetRestoreCursor => { |
| self.mode.insert(mode::TermMode::ALT_SCREEN); |
| self.save_cursor_position(); |
| if !self.alt { |
| self.swap_alt(); |
| } |
| self.save_cursor_position(); |
| }, |
| ansi::Mode::ShowCursor => self.mode.insert(mode::TermMode::SHOW_CURSOR), |
| ansi::Mode::CursorKeys => self.mode.insert(mode::TermMode::APP_CURSOR), |
| ansi::Mode::ReportMouseClicks => { |
| self.mode.insert(mode::TermMode::MOUSE_REPORT_CLICK); |
| self.set_mouse_cursor(MouseCursor::Arrow); |
| }, |
| ansi::Mode::ReportCellMouseMotion => { |
| self.mode.insert(mode::TermMode::MOUSE_DRAG); |
| self.set_mouse_cursor(MouseCursor::Arrow); |
| }, |
| ansi::Mode::ReportAllMouseMotion => { |
| self.mode.insert(mode::TermMode::MOUSE_MOTION); |
| self.set_mouse_cursor(MouseCursor::Arrow); |
| }, |
| ansi::Mode::ReportFocusInOut => self.mode.insert(mode::TermMode::FOCUS_IN_OUT), |
| ansi::Mode::BracketedPaste => self.mode.insert(mode::TermMode::BRACKETED_PASTE), |
| ansi::Mode::SgrMouse => self.mode.insert(mode::TermMode::SGR_MOUSE), |
| ansi::Mode::LineWrap => self.mode.insert(mode::TermMode::LINE_WRAP), |
| ansi::Mode::LineFeedNewLine => self.mode.insert(mode::TermMode::LINE_FEED_NEW_LINE), |
| ansi::Mode::Origin => self.mode.insert(mode::TermMode::ORIGIN), |
| ansi::Mode::DECCOLM => self.deccolm(), |
| ansi::Mode::Insert => self.mode.insert(mode::TermMode::INSERT), // heh |
| _ => { |
| trace!(".. ignoring set_mode"); |
| } |
| } |
| } |
| |
| #[inline] |
| fn unset_mode(&mut self,mode: ansi::Mode) { |
| trace!("unset_mode: {:?}", mode); |
| match mode { |
| ansi::Mode::SwapScreenAndSetRestoreCursor => { |
| self.mode.remove(mode::TermMode::ALT_SCREEN); |
| self.restore_cursor_position(); |
| if self.alt { |
| self.swap_alt(); |
| } |
| self.restore_cursor_position(); |
| }, |
| ansi::Mode::ShowCursor => self.mode.remove(mode::TermMode::SHOW_CURSOR), |
| ansi::Mode::CursorKeys => self.mode.remove(mode::TermMode::APP_CURSOR), |
| ansi::Mode::ReportMouseClicks => { |
| self.mode.remove(mode::TermMode::MOUSE_REPORT_CLICK); |
| self.set_mouse_cursor(MouseCursor::Text); |
| }, |
| ansi::Mode::ReportCellMouseMotion => { |
| self.mode.remove(mode::TermMode::MOUSE_DRAG); |
| self.set_mouse_cursor(MouseCursor::Text); |
| }, |
| ansi::Mode::ReportAllMouseMotion => { |
| self.mode.remove(mode::TermMode::MOUSE_MOTION); |
| self.set_mouse_cursor(MouseCursor::Text); |
| }, |
| ansi::Mode::ReportFocusInOut => self.mode.remove(mode::TermMode::FOCUS_IN_OUT), |
| ansi::Mode::BracketedPaste => self.mode.remove(mode::TermMode::BRACKETED_PASTE), |
| ansi::Mode::SgrMouse => self.mode.remove(mode::TermMode::SGR_MOUSE), |
| ansi::Mode::LineWrap => self.mode.remove(mode::TermMode::LINE_WRAP), |
| ansi::Mode::LineFeedNewLine => self.mode.remove(mode::TermMode::LINE_FEED_NEW_LINE), |
| ansi::Mode::Origin => self.mode.remove(mode::TermMode::ORIGIN), |
| ansi::Mode::DECCOLM => self.deccolm(), |
| ansi::Mode::Insert => self.mode.remove(mode::TermMode::INSERT), |
| _ => { |
| trace!(".. ignoring unset_mode"); |
| } |
| } |
| } |
| |
| #[inline] |
| fn set_scrolling_region(&mut self, region: Range<Line>) { |
| trace!("set scroll 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!("set mode::TermMode::APP_KEYPAD"); |
| self.mode.insert(mode::TermMode::APP_KEYPAD); |
| } |
| |
| #[inline] |
| fn unset_keypad_application_mode(&mut self) { |
| trace!("unset mode::TermMode::APP_KEYPAD"); |
| self.mode.remove(mode::TermMode::APP_KEYPAD); |
| } |
| |
| #[inline] |
| fn configure_charset(&mut self, index: CharsetIndex, charset: StandardCharset) { |
| trace!("designate {:?} character set as {:?}", index, charset); |
| self.cursor.charsets[index] = charset; |
| } |
| |
| #[inline] |
| fn set_active_charset(&mut self, index: CharsetIndex) { |
| trace!("Activate {:?} character set", index); |
| self.active_charset = index; |
| } |
| |
| #[inline] |
| fn set_cursor_style(&mut self, style: Option<CursorStyle>) { |
| trace!("set_cursor_style {:?}", style); |
| self.cursor_style = style; |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| extern crate serde_json; |
| |
| use super::{Cell, Term, SizeInfo}; |
| use term::{cell, Search}; |
| |
| use grid::{Grid, Scroll}; |
| use index::{Point, Line, Column, Side}; |
| use ansi::{self, Handler, CharsetIndex, StandardCharset}; |
| use selection::Selection; |
| use std::mem; |
| use input::FONT_SIZE_STEP; |
| use font::Size; |
| use config::Config; |
| |
| #[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, |
| }; |
| let mut term = Term::new(&Default::default(), size); |
| 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, |
| }; |
| let mut term = Term::new(&Default::default(), size); |
| 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, |
| }; |
| let mut term = Term::new(&Default::default(), size); |
| 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, |
| }; |
| let mut term = Term::new(&Default::default(), size); |
| 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, |
| }; |
| let config: Config = Default::default(); |
| let mut term: Term = Term::new(&config, size); |
| 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, |
| }; |
| let config: Config = Default::default(); |
| let mut term: Term = Term::new(&config, size); |
| |
| 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, |
| }; |
| let config: Config = Default::default(); |
| let mut term: Term = Term::new(&config, size); |
| |
| 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, |
| }; |
| let config: Config = Default::default(); |
| let mut term: Term = Term::new(&config, size); |
| |
| // 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); |
| } |
| |
| // `((ftp://a.de))` -> `Some("ftp://a.de")` |
| #[test] |
| fn url_trim_unmatched_parens() { |
| 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, |
| }; |
| let mut term = Term::new(&Default::default(), size); |
| let mut grid: Grid<Cell> = Grid::new(Line(1), Column(15), 0, Cell::default()); |
| grid[Line(0)][Column(0)].c = '('; |
| grid[Line(0)][Column(1)].c = '('; |
| grid[Line(0)][Column(2)].c = 'f'; |
| grid[Line(0)][Column(3)].c = 't'; |
| grid[Line(0)][Column(4)].c = 'p'; |
| grid[Line(0)][Column(5)].c = ':'; |
| grid[Line(0)][Column(6)].c = '/'; |
| grid[Line(0)][Column(7)].c = '/'; |
| grid[Line(0)][Column(8)].c = 'a'; |
| grid[Line(0)][Column(9)].c = '.'; |
| grid[Line(0)][Column(10)].c = 'd'; |
| grid[Line(0)][Column(11)].c = 'e'; |
| grid[Line(0)][Column(12)].c = ')'; |
| grid[Line(0)][Column(13)].c = ')'; |
| mem::swap(&mut term.grid, &mut grid); |
| |
| // Search for URL in grid |
| let url = term.url_search(Point::new(0, Column(4))); |
| |
| assert_eq!(url, Some("ftp://a.de".into())); |
| } |
| |
| // `ftp://a.de/()` -> `Some("ftp://a.de/()")` |
| #[test] |
| fn url_allow_matching_parens() { |
| 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, |
| }; |
| let mut term = Term::new(&Default::default(), size); |
| let mut grid: Grid<Cell> = Grid::new(Line(1), Column(15), 0, Cell::default()); |
| grid[Line(0)][Column(0)].c = 'f'; |
| grid[Line(0)][Column(1)].c = 't'; |
| grid[Line(0)][Column(2)].c = 'p'; |
| grid[Line(0)][Column(3)].c = ':'; |
| grid[Line(0)][Column(4)].c = '/'; |
| grid[Line(0)][Column(5)].c = '/'; |
| grid[Line(0)][Column(6)].c = 'a'; |
| grid[Line(0)][Column(7)].c = '.'; |
| grid[Line(0)][Column(8)].c = 'd'; |
| grid[Line(0)][Column(9)].c = 'e'; |
| grid[Line(0)][Column(10)].c = '/'; |
| grid[Line(0)][Column(11)].c = '('; |
| grid[Line(0)][Column(12)].c = ')'; |
| mem::swap(&mut term.grid, &mut grid); |
| |
| // Search for URL in grid |
| let url = term.url_search(Point::new(0, Column(4))); |
| |
| assert_eq!(url, Some("ftp://a.de/()".into())); |
| } |
| |
| // `aze` -> `None` |
| #[test] |
| fn url_skip_invalid() { |
| 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, |
| }; |
| let mut term = Term::new(&Default::default(), size); |
| let mut grid: Grid<Cell> = Grid::new(Line(1), Column(15), 0, Cell::default()); |
| grid[Line(0)][Column(0)].c = 'a'; |
| grid[Line(0)][Column(1)].c = 'z'; |
| grid[Line(0)][Column(2)].c = 'e'; |
| mem::swap(&mut term.grid, &mut grid); |
| |
| // Search for URL in grid |
| let url = term.url_search(Point::new(0, Column(1))); |
| |
| assert_eq!(url, None); |
| } |
| } |
| |
| #[cfg(all(test, feature = "bench"))] |
| mod benches { |
| extern crate test; |
| extern crate serde_json as json; |
| |
| use std::io::Read; |
| use std::fs::File; |
| use std::mem; |
| use std::path::Path; |
| |
| use grid::Grid; |
| use config::Config; |
| |
| use super::{SizeInfo, Term}; |
| use super::cell::Cell; |
| |
| 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); |
| mem::swap(&mut terminal.grid, &mut grid); |
| |
| b.iter(|| { |
| let iter = terminal.renderable_cells(&config, false); |
| for cell in iter { |
| test::black_box(cell); |
| } |
| }) |
| } |
| } |