// 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 crate::ansi::{self, Color, NamedColor, Attr, Handler, CharsetIndex, StandardCharset, CursorStyle}; use crate::grid::{BidirectionalIterator, Grid, Indexed, IndexRegion, DisplayIter, Scroll, ViewportPosition}; use crate::index::{self, Point, Column, Line, IndexRange, Contains, RangeInclusive, Linear}; use crate::selection::{self, Selection, Locations}; use crate::config::{Config, VisualBellAnimation}; use crate::{MouseCursor, Rgb}; use copypasta::{Clipboard, Load, Store}; use crate::input::FONT_SIZE_STEP; use crate::logging::LoggerProxy; 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) -> Point; /// Find the nearest semantic boundary _to the point_ of provided point. fn semantic_search_right(&self, _: Point) -> Point; /// Find the nearest URL boundary in both directions. fn url_search(&self, _: Point) -> Option; } impl Search for Term { fn semantic_search_left(&self, mut point: Point) -> Point { // 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) -> Point { // 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) -> Option { 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, cursor: &'a Point, cursor_offset: usize, mode: TermMode, config: &'a Config, colors: &'a color::List, selection: Option>, cursor_cells: ArrayDeque<[Indexed; 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, cursor: &'b Point, colors: &'b color::List, mode: TermMode, config: &'b Config, selection: Option, 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 cell = &self.grid[self.cursor]; let text_color = self.config.cursor_text_color().unwrap_or(cell.bg); let cursor_color = self.config.cursor_cursor_color().unwrap_or(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.config.cursor_cursor_color().unwrap_or(cursor_cell.fg); 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() } /// 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], } } } #[derive(Debug)] pub struct RenderableCell { /// A _Display_ line (not necessarily an _Active_ line) pub line: Line, pub column: Column, pub chars: [char; cell::MAX_ZEROWIDTH_CHARS + 1], 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 { 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, chars: cell.chars(), fg: fg_rgb, bg: bg_rgb, bg_alpha, }) } } } 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 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 for Charsets { type Output = StandardCharset; fn index(&self, index: CharsetIndex) -> &StandardCharset { &self.0[index as usize] } } impl IndexMut 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, } 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, /// 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, /// Got a request to set the mouse cursor; it's buffered here until the next draw next_mouse_cursor: Option, /// Alternate grid alt_grid: Grid, /// 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, /// 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, /// 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, /// 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, /// Proxy object for clearing displayed errors and warnings logger_proxy: Option, } /// 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) -> 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, Line(self.lines().saturating_sub(1))), col: min(col, Column(self.cols().saturating_sub(1))) } } } impl Term { pub fn selection(&self) -> &Option { &self.grid.selection } /// Clear displayed errors and warnings. pub fn clear_log(&mut self) { if let Some(ref mut logger_proxy) = self.logger_proxy { logger_proxy.clear(); } } pub fn selection_mut(&mut self) -> &mut Option { &mut self.grid.selection } #[inline] pub fn get_next_title(&mut self) -> Option { 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 { 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 = TabStops::new(grid.num_cols(), tabspaces); 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, logger_proxy: None, } } pub fn set_logger_proxy(&mut self, logger_proxy: LoggerProxy) { self.logger_proxy = Some(logger_proxy); } 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 { /// Need a generic push() for the Append trait trait PushChar { fn push_char(&mut self, c: char); fn maybe_newline(&mut self, grid: &Grid, 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, tabs: &TabStops, line: usize, cols: Range, ); } impl Append for String { fn append( &mut self, grid: &Grid, tabs: &TabStops, mut line: usize, cols: Range ) { // 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 { 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 { 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 { &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 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.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 = 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!("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() { 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; 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 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.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!("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); self.goto(line, self.cursor.point.col) } #[inline] fn goto_col(&mut self, col: Column) { trace!("goto_col: {}", 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!("move_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!("move_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!("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(&mut self, writer: &mut W) { let _ = writer.write_all(b"\x1b[?6c"); } #[inline] fn device_status(&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); while self.cursor.point.col < self.grid.num_cols() && count != 0 { count -= 1; let cell = &mut self.grid[&self.cursor.point]; *cell = self.cursor.template; 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(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] = 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 = &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!("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[index::Column(i)] { 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.clear_log(); 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] = false; }, ansi::TabulationClearMode::All => { self.tabs.clear_all(); } } } // 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) { 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) { trace!("set_cursor_style {:?}", style); self.cursor_style = style; } } struct TabStops { tabs: Vec } 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::>() } } fn clear_all(&mut self) { unsafe { ptr::write_bytes(self.tabs.as_mut_ptr(), 0, self.tabs.len()); } } } impl Index for TabStops { type Output = bool; fn index(&self, index: Column) -> &bool { &self.tabs[index.0] } } impl IndexMut 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, Term, SizeInfo}; use crate::term::{cell, Search}; use crate::grid::{Grid, Scroll}; use crate::index::{Point, Line, Column, Side}; use crate::ansi::{self, Handler, CharsetIndex, StandardCharset}; use crate::selection::Selection; use std::mem; use crate::input::FONT_SIZE_STEP; use font::Size; use crate::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, dpr: 1.0, }; let mut term = Term::new(&Default::default(), size); let mut grid: Grid = 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); let mut grid: Grid = 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); let mut grid: Grid = 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 = Grid::new(Line(24), Column(80), 0, template); let serialized = serde_json::to_string(&grid).expect("ser"); let deserialized = serde_json::from_str::>(&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); 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); 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); 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); 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); // 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, dpr: 1.0, }; let mut term = Term::new(&Default::default(), size); let mut grid: Grid = 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, dpr: 1.0, }; let mut term = Term::new(&Default::default(), size); let mut grid: Grid = 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, dpr: 1.0, }; let mut term = Term::new(&Default::default(), size); let mut grid: Grid = 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 crate::grid::Grid; use crate::config::Config; use super::{SizeInfo, Term}; use super::cell::Cell; fn read_string

(path: P) -> String where P: AsRef { 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 = 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); } }) } }