//! Exports the `Term` type which is a high-level API for the Grid. use std::ops::{Index, IndexMut, Range}; use std::sync::Arc; use std::{cmp, mem, ptr, slice, str}; use bitflags::bitflags; use log::{debug, trace}; use unicode_width::UnicodeWidthChar; use crate::ansi::{ self, Attr, CharsetIndex, Color, CursorShape, CursorStyle, Handler, NamedColor, StandardCharset, }; use crate::config::Config; use crate::event::{Event, EventListener}; use crate::grid::{Dimensions, Grid, GridIterator, Scroll}; use crate::index::{self, Boundary, Column, Direction, Line, Point, Side}; use crate::selection::{Selection, SelectionRange, SelectionType}; use crate::term::cell::{Cell, Flags, Hyperlink, LineLength}; use crate::term::color::{Colors, Rgb}; use crate::vi_mode::{ViModeCursor, ViMotion}; pub mod cell; pub mod color; pub mod search; /// Minimum number of columns. /// /// A minimum of 2 is necessary to hold fullwidth unicode characters. pub const MIN_COLUMNS: usize = 2; /// Minimum number of visible lines. pub const MIN_SCREEN_LINES: usize = 1; /// Max size of the window title stack. const TITLE_STACK_MAX_DEPTH: usize = 4096; /// Default tab interval, corresponding to terminfo `it` value. const INITIAL_TABSTOPS: usize = 8; bitflags! { pub struct TermMode: u32 { const NONE = 0; const SHOW_CURSOR = 0b0000_0000_0000_0000_0001; const APP_CURSOR = 0b0000_0000_0000_0000_0010; const APP_KEYPAD = 0b0000_0000_0000_0000_0100; const MOUSE_REPORT_CLICK = 0b0000_0000_0000_0000_1000; const BRACKETED_PASTE = 0b0000_0000_0000_0001_0000; const SGR_MOUSE = 0b0000_0000_0000_0010_0000; const MOUSE_MOTION = 0b0000_0000_0000_0100_0000; const LINE_WRAP = 0b0000_0000_0000_1000_0000; const LINE_FEED_NEW_LINE = 0b0000_0000_0001_0000_0000; const ORIGIN = 0b0000_0000_0010_0000_0000; const INSERT = 0b0000_0000_0100_0000_0000; const FOCUS_IN_OUT = 0b0000_0000_1000_0000_0000; const ALT_SCREEN = 0b0000_0001_0000_0000_0000; const MOUSE_DRAG = 0b0000_0010_0000_0000_0000; const MOUSE_MODE = 0b0000_0010_0000_0100_1000; const UTF8_MOUSE = 0b0000_0100_0000_0000_0000; const ALTERNATE_SCROLL = 0b0000_1000_0000_0000_0000; const VI = 0b0001_0000_0000_0000_0000; const URGENCY_HINTS = 0b0010_0000_0000_0000_0000; const ANY = u32::MAX; } } impl Default for TermMode { fn default() -> TermMode { TermMode::SHOW_CURSOR | TermMode::LINE_WRAP | TermMode::ALTERNATE_SCROLL | TermMode::URGENCY_HINTS } } /// Convert a terminal point to a viewport relative point. #[inline] pub fn point_to_viewport(display_offset: usize, point: Point) -> Option> { let viewport_line = point.line.0 + display_offset as i32; usize::try_from(viewport_line).ok().map(|line| Point::new(line, point.column)) } /// Convert a viewport relative point to a terminal point. #[inline] pub fn viewport_to_point(display_offset: usize, point: Point) -> Point { let line = Line(point.line as i32) - display_offset; Point::new(line, point.column) } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct LineDamageBounds { /// Damaged line number. pub line: usize, /// Leftmost damaged column. pub left: usize, /// Rightmost damaged column. pub right: usize, } impl LineDamageBounds { #[inline] pub fn undamaged(line: usize, num_cols: usize) -> Self { Self { line, left: num_cols, right: 0 } } #[inline] pub fn reset(&mut self, num_cols: usize) { *self = Self::undamaged(self.line, num_cols); } #[inline] pub fn expand(&mut self, left: usize, right: usize) { self.left = cmp::min(self.left, left); self.right = cmp::max(self.right, right); } #[inline] pub fn is_damaged(&self) -> bool { self.left <= self.right } } /// Terminal damage information collected since the last [`Term::reset_damage`] call. #[derive(Debug)] pub enum TermDamage<'a> { /// The entire terminal is damaged. Full, /// Iterator over damaged lines in the terminal. Partial(TermDamageIterator<'a>), } /// Iterator over the terminal's damaged lines. #[derive(Clone, Debug)] pub struct TermDamageIterator<'a> { line_damage: slice::Iter<'a, LineDamageBounds>, } impl<'a> TermDamageIterator<'a> { fn new(line_damage: &'a [LineDamageBounds]) -> Self { Self { line_damage: line_damage.iter() } } } impl<'a> Iterator for TermDamageIterator<'a> { type Item = LineDamageBounds; fn next(&mut self) -> Option { self.line_damage.find(|line| line.is_damaged()).copied() } } /// State of the terminal damage. struct TermDamageState { /// Hint whether terminal should be damaged entirely regardless of the actual damage changes. is_fully_damaged: bool, /// Information about damage on terminal lines. lines: Vec, /// Old terminal cursor point. last_cursor: Point, /// Last Vi cursor point. last_vi_cursor_point: Option>, /// Old selection range. last_selection: Option, } impl TermDamageState { fn new(num_cols: usize, num_lines: usize) -> Self { let lines = (0..num_lines).map(|line| LineDamageBounds::undamaged(line, num_cols)).collect(); Self { is_fully_damaged: true, lines, last_cursor: Default::default(), last_vi_cursor_point: Default::default(), last_selection: Default::default(), } } #[inline] fn resize(&mut self, num_cols: usize, num_lines: usize) { // Reset point, so old cursor won't end up outside of the viewport. self.last_cursor = Default::default(); self.last_vi_cursor_point = None; self.last_selection = None; self.is_fully_damaged = true; self.lines.clear(); self.lines.reserve(num_lines); for line in 0..num_lines { self.lines.push(LineDamageBounds::undamaged(line, num_cols)); } } /// Damage point inside of the viewport. #[inline] fn damage_point(&mut self, point: Point) { self.damage_line(point.line, point.column.0 as usize, point.column.0 as usize); } /// Expand `line`'s damage to span at least `left` to `right` column. #[inline] fn damage_line(&mut self, line: usize, left: usize, right: usize) { self.lines[line].expand(left, right); } fn damage_selection( &mut self, selection: SelectionRange, display_offset: usize, num_cols: usize, ) { let display_offset = display_offset as i32; let last_visible_line = self.lines.len() as i32 - 1; // Don't damage invisible selection. if selection.end.line.0 + display_offset < 0 || selection.start.line.0.abs() < display_offset - last_visible_line { return; }; let start = cmp::max(selection.start.line.0 + display_offset, 0); let end = cmp::min(cmp::max(selection.end.line.0 + display_offset, 0), last_visible_line); for line in start as usize..=end as usize { self.damage_line(line, 0, num_cols - 1); } } /// Reset information about terminal damage. fn reset(&mut self, num_cols: usize) { self.is_fully_damaged = false; self.lines.iter_mut().for_each(|line| line.reset(num_cols)); } } pub struct Term { /// Terminal focus controlling the cursor shape. pub is_focused: bool, /// Cursor for keyboard selection. pub vi_mode_cursor: ViModeCursor, pub selection: Option, /// Currently active grid. /// /// Tracks the screen buffer currently in use. While the alternate screen buffer is active, /// this will be the alternate grid. Otherwise it is the primary screen buffer. grid: Grid, /// Currently inactive grid. /// /// Opposite of the active grid. While the alternate screen buffer is active, this will be the /// primary grid. Otherwise it is the alternate screen buffer. inactive_grid: Grid, /// Index into `charsets`, pointing to what ASCII is currently being mapped to. active_charset: CharsetIndex, /// Tabstops. tabs: TabStops, /// Mode flags. mode: TermMode, /// Scroll region. /// /// Range going from top to bottom of the terminal, indexed from the top of the viewport. scroll_region: Range, semantic_escape_chars: String, /// Modified terminal colors. colors: Colors, /// Current style of the cursor. cursor_style: Option, /// Default style for resetting the cursor. default_cursor_style: CursorStyle, /// Style of the vi mode cursor. vi_mode_cursor_style: Option, /// Proxy for sending events to the event loop. event_proxy: T, /// Current title of the window. title: Option, /// Stack of saved window titles. When a title is popped from this stack, the `title` for the /// term is set. title_stack: Vec>, /// Information about damaged cells. damage: TermDamageState, } impl Term { #[inline] pub fn scroll_display(&mut self, scroll: Scroll) where T: EventListener, { let old_display_offset = self.grid.display_offset(); self.grid.scroll_display(scroll); self.event_proxy.send_event(Event::MouseCursorDirty); // Clamp vi mode cursor to the viewport. let viewport_start = -(self.grid.display_offset() as i32); let viewport_end = viewport_start + self.bottommost_line().0; let vi_cursor_line = &mut self.vi_mode_cursor.point.line.0; *vi_cursor_line = cmp::min(viewport_end, cmp::max(viewport_start, *vi_cursor_line)); self.vi_mode_recompute_selection(); // Damage everything if display offset changed. if old_display_offset != self.grid().display_offset() { self.mark_fully_damaged(); } } pub fn new(config: &Config, dimensions: &D, event_proxy: T) -> Term { let num_cols = dimensions.columns(); let num_lines = dimensions.screen_lines(); let history_size = config.scrolling.history() as usize; let grid = Grid::new(num_lines, num_cols, history_size); let alt = Grid::new(num_lines, num_cols, 0); let tabs = TabStops::new(grid.columns()); let scroll_region = Line(0)..Line(grid.screen_lines() as i32); // Initialize terminal damage, covering the entire terminal upon launch. let damage = TermDamageState::new(num_cols, num_lines); Term { grid, inactive_grid: alt, active_charset: Default::default(), vi_mode_cursor: Default::default(), tabs, mode: Default::default(), scroll_region, colors: color::Colors::default(), semantic_escape_chars: config.selection.semantic_escape_chars.to_owned(), cursor_style: None, default_cursor_style: config.cursor.style(), vi_mode_cursor_style: config.cursor.vi_mode_style(), event_proxy, is_focused: true, title: None, title_stack: Vec::new(), selection: None, damage, } } #[must_use] pub fn damage(&mut self, selection: Option) -> TermDamage<'_> { // Ensure the entire terminal is damaged after entering insert mode. // Leaving is handled in the ansi handler. if self.mode.contains(TermMode::INSERT) { self.mark_fully_damaged(); } // Update tracking of cursor, selection, and vi mode cursor. let display_offset = self.grid().display_offset(); let vi_cursor_point = if self.mode.contains(TermMode::VI) { point_to_viewport(display_offset, self.vi_mode_cursor.point) } else { None }; let previous_cursor = mem::replace(&mut self.damage.last_cursor, self.grid.cursor.point); let previous_selection = mem::replace(&mut self.damage.last_selection, selection); let previous_vi_cursor_point = mem::replace(&mut self.damage.last_vi_cursor_point, vi_cursor_point); // Early return if the entire terminal is damaged. if self.damage.is_fully_damaged { return TermDamage::Full; } // Add information about old cursor position and new one if they are not the same, so we // cover everything that was produced by `Term::input`. if self.damage.last_cursor != previous_cursor { // Cursor cooridanates are always inside viewport even if you have `display_offset`. let point = Point::new(previous_cursor.line.0 as usize, previous_cursor.column); self.damage.damage_point(point); } // Always damage current cursor. self.damage_cursor(); // Vi mode doesn't update the terminal content, thus only last vi cursor position and the // new one should be damaged. if let Some(previous_vi_cursor_point) = previous_vi_cursor_point { self.damage.damage_point(previous_vi_cursor_point) } // Damage Vi cursor if it's present. if let Some(vi_cursor_point) = self.damage.last_vi_cursor_point { self.damage.damage_point(vi_cursor_point); } if self.damage.last_selection != previous_selection { for selection in self.damage.last_selection.into_iter().chain(previous_selection) { self.damage.damage_selection(selection, display_offset, self.columns()); } } TermDamage::Partial(TermDamageIterator::new(&self.damage.lines)) } /// Resets the terminal damage information. pub fn reset_damage(&mut self) { self.damage.reset(self.columns()); } #[inline] pub fn mark_fully_damaged(&mut self) { self.damage.is_fully_damaged = true; } /// Damage line in a terminal viewport. #[inline] pub fn damage_line(&mut self, line: usize, left: usize, right: usize) { self.damage.damage_line(line, left, right); } pub fn update_config(&mut self, config: &Config) where T: EventListener, { self.semantic_escape_chars = config.selection.semantic_escape_chars.to_owned(); self.default_cursor_style = config.cursor.style(); self.vi_mode_cursor_style = config.cursor.vi_mode_style(); let title_event = match &self.title { Some(title) => Event::Title(title.clone()), None => Event::ResetTitle, }; self.event_proxy.send_event(title_event); if self.mode.contains(TermMode::ALT_SCREEN) { self.inactive_grid.update_history(config.scrolling.history() as usize); } else { self.grid.update_history(config.scrolling.history() as usize); } // Damage everything on config updates. self.mark_fully_damaged(); } /// Convert the active selection to a String. pub fn selection_to_string(&self) -> Option { let selection_range = self.selection.as_ref().and_then(|s| s.to_range(self))?; let SelectionRange { start, end, .. } = selection_range; let mut res = String::new(); match self.selection.as_ref() { Some(Selection { ty: SelectionType::Block, .. }) => { for line in (start.line.0..end.line.0).map(Line::from) { res += self .line_to_string(line, start.column..end.column, start.column.0 != 0) .trim_end(); res += "\n"; } res += self.line_to_string(end.line, start.column..end.column, true).trim_end(); }, Some(Selection { ty: SelectionType::Lines, .. }) => { res = self.bounds_to_string(start, end) + "\n"; }, _ => { res = self.bounds_to_string(start, end); }, } Some(res) } /// Convert range between two points to a String. pub fn bounds_to_string(&self, start: Point, end: Point) -> String { let mut res = String::new(); for line in (start.line.0..=end.line.0).map(Line::from) { let start_col = if line == start.line { start.column } else { Column(0) }; let end_col = if line == end.line { end.column } else { self.last_column() }; res += &self.line_to_string(line, start_col..end_col, line == end.line); } res.strip_suffix('\n').map(str::to_owned).unwrap_or(res) } /// Convert a single line in the grid to a String. fn line_to_string( &self, line: Line, mut cols: Range, include_wrapped_wide: bool, ) -> String { let mut text = String::new(); let grid_line = &self.grid[line]; let line_length = cmp::min(grid_line.line_length(), cols.end + 1); // Include wide char when trailing spacer is selected. if grid_line[cols.start].flags.contains(Flags::WIDE_CHAR_SPACER) { cols.start -= 1; } let mut tab_mode = false; for column in (cols.start.0..line_length.0).map(Column::from) { let cell = &grid_line[column]; // Skip over cells until next tab-stop once a tab was found. if tab_mode { if self.tabs[column] || cell.c != ' ' { tab_mode = false; } else { continue; } } if cell.c == '\t' { tab_mode = true; } if !cell.flags.intersects(Flags::WIDE_CHAR_SPACER | Flags::LEADING_WIDE_CHAR_SPACER) { // Push cells primary character. text.push(cell.c); // Push zero-width characters. for c in cell.zerowidth().into_iter().flatten() { text.push(*c); } } } if cols.end >= self.columns() - 1 && (line_length.0 == 0 || !self.grid[line][line_length - 1].flags.contains(Flags::WRAPLINE)) { text.push('\n'); } // If wide char is not part of the selection, but leading spacer is, include it. if line_length == self.columns() && line_length.0 >= 2 && grid_line[line_length - 1].flags.contains(Flags::LEADING_WIDE_CHAR_SPACER) && include_wrapped_wide { text.push(self.grid[line - 1i32][Column(0)].c); } text } /// Terminal content required for rendering. #[inline] pub fn renderable_content(&self) -> RenderableContent<'_> where T: EventListener, { RenderableContent::new(self) } /// 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 } /// Mutable access for swapping out the grid during tests. #[cfg(test)] pub fn grid_mut(&mut self) -> &mut Grid { &mut self.grid } /// Resize terminal to new dimensions. pub fn resize(&mut self, size: S) { let old_cols = self.columns(); let old_lines = self.screen_lines(); let num_cols = size.columns(); let num_lines = size.screen_lines(); if old_cols == num_cols && old_lines == num_lines { debug!("Term::resize dimensions unchanged"); return; } debug!("New num_cols is {} and num_lines is {}", num_cols, num_lines); // Move vi mode cursor with the content. let history_size = self.history_size(); let mut delta = num_lines as i32 - old_lines as i32; let min_delta = cmp::min(0, num_lines as i32 - self.grid.cursor.point.line.0 - 1); delta = cmp::min(cmp::max(delta, min_delta), history_size as i32); self.vi_mode_cursor.point.line += delta; // Invalidate selection and tabs only when necessary. if old_cols != num_cols { self.selection = None; // Recreate tabs list. self.tabs.resize(num_cols); } else if let Some(selection) = self.selection.take() { let range = Line(0)..Line(num_lines as i32); self.selection = selection.rotate(self, &range, -delta); } let is_alt = self.mode.contains(TermMode::ALT_SCREEN); self.grid.resize(!is_alt, num_lines, num_cols); self.inactive_grid.resize(is_alt, num_lines, num_cols); // Clamp vi cursor to viewport. let vi_point = self.vi_mode_cursor.point; let viewport_top = Line(-(self.grid.display_offset() as i32)); let viewport_bottom = viewport_top + self.bottommost_line(); self.vi_mode_cursor.point.line = cmp::max(cmp::min(vi_point.line, viewport_bottom), viewport_top); self.vi_mode_cursor.point.column = cmp::min(vi_point.column, self.last_column()); // Reset scrolling region. self.scroll_region = Line(0)..Line(self.screen_lines() as i32); // Resize damage information. self.damage.resize(num_cols, num_lines); } /// Active terminal modes. #[inline] pub fn mode(&self) -> &TermMode { &self.mode } /// Swap primary and alternate screen buffer. pub fn swap_alt(&mut self) { if !self.mode.contains(TermMode::ALT_SCREEN) { // Set alt screen cursor to the current primary screen cursor. self.inactive_grid.cursor = self.grid.cursor.clone(); // Drop information about the primary screens saved cursor. self.grid.saved_cursor = self.grid.cursor.clone(); // Reset alternate screen contents. self.inactive_grid.reset_region(..); } mem::swap(&mut self.grid, &mut self.inactive_grid); self.mode ^= TermMode::ALT_SCREEN; self.selection = None; self.mark_fully_damaged(); } /// 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: usize) { trace!("Scrolling down relative: origin={}, lines={}", origin, lines); lines = cmp::min(lines, (self.scroll_region.end - self.scroll_region.start).0 as usize); lines = cmp::min(lines, (self.scroll_region.end - origin).0 as usize); let region = origin..self.scroll_region.end; // Scroll selection. self.selection = self.selection.take().and_then(|s| s.rotate(self, ®ion, -(lines as i32))); // Scroll vi mode cursor. let line = &mut self.vi_mode_cursor.point.line; if region.start <= *line && region.end > *line { *line = cmp::min(*line + lines, region.end - 1); } // Scroll between origin and bottom self.grid.scroll_down(®ion, lines); self.mark_fully_damaged(); } /// 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, mut lines: usize) { trace!("Scrolling up relative: origin={}, lines={}", origin, lines); lines = cmp::min(lines, (self.scroll_region.end - self.scroll_region.start).0 as usize); let region = origin..self.scroll_region.end; // Scroll selection. self.selection = self.selection.take().and_then(|s| s.rotate(self, ®ion, lines as i32)); self.grid.scroll_up(®ion, lines); // Scroll vi mode cursor. let viewport_top = Line(-(self.grid.display_offset() as i32)); let top = if region.start == 0 { viewport_top } else { region.start }; let line = &mut self.vi_mode_cursor.point.line; if (top <= *line) && region.end > *line { *line = cmp::max(*line - lines, top); } self.mark_fully_damaged(); } fn deccolm(&mut self) where T: EventListener, { // Setting 132 column font makes no sense, but run the other side effects. // Clear scrolling region. self.set_scrolling_region(1, None); // Clear grid. self.grid.reset_region(..); self.mark_fully_damaged(); } #[inline] pub fn exit(&mut self) where T: EventListener, { self.event_proxy.send_event(Event::Exit); } /// Toggle the vi mode. #[inline] pub fn toggle_vi_mode(&mut self) where T: EventListener, { self.mode ^= TermMode::VI; if self.mode.contains(TermMode::VI) { let display_offset = self.grid.display_offset() as i32; if self.grid.cursor.point.line > self.bottommost_line() - display_offset { // Move cursor to top-left if terminal cursor is not visible. let point = Point::new(Line(-display_offset), Column(0)); self.vi_mode_cursor = ViModeCursor::new(point); } else { // Reset vi mode cursor position to match primary cursor. self.vi_mode_cursor = ViModeCursor::new(self.grid.cursor.point); } } // Update UI about cursor blinking state changes. self.event_proxy.send_event(Event::CursorBlinkingChange); } /// Move vi mode cursor. #[inline] pub fn vi_motion(&mut self, motion: ViMotion) where T: EventListener, { // Require vi mode to be active. if !self.mode.contains(TermMode::VI) { return; } // Move cursor. self.vi_mode_cursor = self.vi_mode_cursor.motion(self, motion); self.vi_mode_recompute_selection(); } /// Move vi cursor to a point in the grid. #[inline] pub fn vi_goto_point(&mut self, point: Point) where T: EventListener, { // Move viewport to make point visible. self.scroll_to_point(point); // Move vi cursor to the point. self.vi_mode_cursor.point = point; self.vi_mode_recompute_selection(); } /// Update the active selection to match the vi mode cursor position. #[inline] fn vi_mode_recompute_selection(&mut self) { // Require vi mode to be active. if !self.mode.contains(TermMode::VI) { return; } // Update only if non-empty selection is present. if let Some(selection) = self.selection.as_mut().filter(|s| !s.is_empty()) { selection.update(self.vi_mode_cursor.point, Side::Left); selection.include_all(); } } /// Scroll display to point if it is outside of viewport. pub fn scroll_to_point(&mut self, point: Point) where T: EventListener, { let display_offset = self.grid.display_offset() as i32; let screen_lines = self.grid.screen_lines() as i32; if point.line < -display_offset { let lines = point.line + display_offset; self.scroll_display(Scroll::Delta(-lines.0)); } else if point.line >= (screen_lines - display_offset) { let lines = point.line + display_offset - screen_lines + 1i32; self.scroll_display(Scroll::Delta(-lines.0)); } } /// Jump to the end of a wide cell. pub fn expand_wide(&self, mut point: Point, direction: Direction) -> Point { let flags = self.grid[point.line][point.column].flags; match direction { Direction::Right if flags.contains(Flags::LEADING_WIDE_CHAR_SPACER) => { point.column = Column(1); point.line += 1; }, Direction::Right if flags.contains(Flags::WIDE_CHAR) => { point.column = cmp::min(point.column + 1, self.last_column()); }, Direction::Left if flags.intersects(Flags::WIDE_CHAR | Flags::WIDE_CHAR_SPACER) => { if flags.contains(Flags::WIDE_CHAR_SPACER) { point.column -= 1; } let prev = point.sub(self, Boundary::Grid, 1); if self.grid[prev].flags.contains(Flags::LEADING_WIDE_CHAR_SPACER) { point = prev; } }, _ => (), } point } #[inline] pub fn semantic_escape_chars(&self) -> &str { &self.semantic_escape_chars } /// Active terminal cursor style. /// /// While vi mode is active, this will automatically return the vi mode cursor style. #[inline] pub fn cursor_style(&self) -> CursorStyle { let cursor_style = self.cursor_style.unwrap_or(self.default_cursor_style); if self.mode.contains(TermMode::VI) { self.vi_mode_cursor_style.unwrap_or(cursor_style) } else { cursor_style } } pub fn colors(&self) -> &Colors { &self.colors } /// Insert a linebreak at the current cursor position. #[inline] fn wrapline(&mut self) where T: EventListener, { if !self.mode.contains(TermMode::LINE_WRAP) { return; } trace!("Wrapping input"); self.grid.cursor_cell().flags.insert(Flags::WRAPLINE); if self.grid.cursor.point.line + 1 >= self.scroll_region.end { self.linefeed(); } else { self.damage_cursor(); self.grid.cursor.point.line += 1; } self.grid.cursor.point.column = Column(0); self.grid.cursor.input_needs_wrap = false; self.damage_cursor(); } /// Write `c` to the cell at the cursor position. #[inline(always)] fn write_at_cursor(&mut self, c: char) { let c = self.grid.cursor.charsets[self.active_charset].map(c); let fg = self.grid.cursor.template.fg; let bg = self.grid.cursor.template.bg; let flags = self.grid.cursor.template.flags; let extra = self.grid.cursor.template.extra.clone(); let mut cursor_cell = self.grid.cursor_cell(); // Clear all related cells when overwriting a fullwidth cell. if cursor_cell.flags.intersects(Flags::WIDE_CHAR | Flags::WIDE_CHAR_SPACER) { // Remove wide char and spacer. let wide = cursor_cell.flags.contains(Flags::WIDE_CHAR); let point = self.grid.cursor.point; if wide && point.column < self.last_column() { self.grid[point.line][point.column + 1].flags.remove(Flags::WIDE_CHAR_SPACER); } else if point.column > 0 { self.grid[point.line][point.column - 1].clear_wide(); } // Remove leading spacers. if point.column <= 1 && point.line != self.topmost_line() { let column = self.last_column(); self.grid[point.line - 1i32][column].flags.remove(Flags::LEADING_WIDE_CHAR_SPACER); } cursor_cell = self.grid.cursor_cell(); } cursor_cell.c = c; cursor_cell.fg = fg; cursor_cell.bg = bg; cursor_cell.flags = flags; cursor_cell.extra = extra; } #[inline] fn damage_cursor(&mut self) { // The normal cursor coordinates are always in viewport. let point = Point::new(self.grid.cursor.point.line.0 as usize, self.grid.cursor.point.column); self.damage.damage_point(point); } } impl Dimensions for Term { #[inline] fn columns(&self) -> usize { self.grid.columns() } #[inline] fn screen_lines(&self) -> usize { self.grid.screen_lines() } #[inline] fn total_lines(&self) -> usize { self.grid.total_lines() } } impl Handler for Term { /// A character to be displayed. #[inline(never)] fn input(&mut self, c: char) { // Number of cells the char will occupy. let width = match c.width() { Some(width) => width, None => return, }; // Handle zero-width characters. if width == 0 { // Get previous column. let mut column = self.grid.cursor.point.column; if !self.grid.cursor.input_needs_wrap { column.0 = column.saturating_sub(1); } // Put zerowidth characters over first fullwidth character cell. let line = self.grid.cursor.point.line; if self.grid[line][column].flags.contains(Flags::WIDE_CHAR_SPACER) { column.0 = column.saturating_sub(1); } self.grid[line][column].push_zerowidth(c); return; } // Move cursor to next line. if self.grid.cursor.input_needs_wrap { self.wrapline(); } // If in insert mode, first shift cells to the right. let columns = self.columns(); if self.mode.contains(TermMode::INSERT) && self.grid.cursor.point.column + width < columns { let line = self.grid.cursor.point.line; let col = self.grid.cursor.point.column; let row = &mut self.grid[line][..]; for col in (col.0..(columns - width)).rev() { row.swap(col + width, col); } } if width == 1 { self.write_at_cursor(c); } else { if self.grid.cursor.point.column + 1 >= columns { if self.mode.contains(TermMode::LINE_WRAP) { // Insert placeholder before wide char if glyph does not fit in this row. self.grid.cursor.template.flags.insert(Flags::LEADING_WIDE_CHAR_SPACER); self.write_at_cursor(' '); self.grid.cursor.template.flags.remove(Flags::LEADING_WIDE_CHAR_SPACER); self.wrapline(); } else { // Prevent out of bounds crash when linewrapping is disabled. self.grid.cursor.input_needs_wrap = true; return; } } // Write full width glyph to current cursor cell. self.grid.cursor.template.flags.insert(Flags::WIDE_CHAR); self.write_at_cursor(c); self.grid.cursor.template.flags.remove(Flags::WIDE_CHAR); // Write spacer to cell following the wide glyph. self.grid.cursor.point.column += 1; self.grid.cursor.template.flags.insert(Flags::WIDE_CHAR_SPACER); self.write_at_cursor(' '); self.grid.cursor.template.flags.remove(Flags::WIDE_CHAR_SPACER); } if self.grid.cursor.point.column + 1 < columns { self.grid.cursor.point.column += 1; } else { self.grid.cursor.input_needs_wrap = true; } } #[inline] fn decaln(&mut self) { trace!("Decalnning"); for line in (0..self.screen_lines()).map(Line::from) { for column in 0..self.columns() { let cell = &mut self.grid[line][Column(column)]; *cell = Cell::default(); cell.c = 'E'; } } self.mark_fully_damaged(); } #[inline] fn goto(&mut self, line: Line, col: Column) { trace!("Going to: line={}, col={}", line, col); let (y_offset, max_y) = if self.mode.contains(TermMode::ORIGIN) { (self.scroll_region.start, self.scroll_region.end - 1) } else { (Line(0), self.bottommost_line()) }; self.damage_cursor(); self.grid.cursor.point.line = cmp::max(cmp::min(line + y_offset, max_y), Line(0)); self.grid.cursor.point.column = cmp::min(col, self.last_column()); self.damage_cursor(); self.grid.cursor.input_needs_wrap = false; } #[inline] fn goto_line(&mut self, line: Line) { trace!("Going to line: {}", line); self.goto(line, self.grid.cursor.point.column) } #[inline] fn goto_col(&mut self, col: Column) { trace!("Going to column: {}", col); self.goto(self.grid.cursor.point.line, col) } #[inline] fn insert_blank(&mut self, count: usize) { let cursor = &self.grid.cursor; let bg = cursor.template.bg; // Ensure inserting within terminal bounds let count = cmp::min(count, self.columns() - cursor.point.column.0); let source = cursor.point.column; let destination = cursor.point.column.0 + count; let num_cells = self.columns() - destination; let line = cursor.point.line; self.damage.damage_line(line.0 as usize, 0, self.columns() - 1); let row = &mut self.grid[line][..]; for offset in (0..num_cells).rev() { row.swap(destination + offset, source.0 + offset); } // Cells were just moved out toward the end of the line; // fill in between source and dest with blanks. for cell in &mut row[source.0..destination] { *cell = bg.into(); } } #[inline] fn move_up(&mut self, lines: usize) { trace!("Moving up: {}", lines); self.goto(self.grid.cursor.point.line - lines, self.grid.cursor.point.column) } #[inline] fn move_down(&mut self, lines: usize) { trace!("Moving down: {}", lines); self.goto(self.grid.cursor.point.line + lines, self.grid.cursor.point.column) } #[inline] fn move_forward(&mut self, cols: Column) { trace!("Moving forward: {}", cols); let last_column = cmp::min(self.grid.cursor.point.column + cols, self.last_column()); let cursor_line = self.grid.cursor.point.line.0 as usize; self.damage.damage_line(cursor_line, self.grid.cursor.point.column.0, last_column.0); self.grid.cursor.point.column = last_column; self.grid.cursor.input_needs_wrap = false; } #[inline] fn move_backward(&mut self, cols: Column) { trace!("Moving backward: {}", cols); let column = self.grid.cursor.point.column.saturating_sub(cols.0); let cursor_line = self.grid.cursor.point.line.0 as usize; self.damage.damage_line(cursor_line, column, self.grid.cursor.point.column.0); self.grid.cursor.point.column = Column(column); self.grid.cursor.input_needs_wrap = false; } #[inline] fn identify_terminal(&mut self, intermediate: Option) { match intermediate { None => { trace!("Reporting primary device attributes"); let text = String::from("\x1b[?6c"); self.event_proxy.send_event(Event::PtyWrite(text)); }, Some('>') => { trace!("Reporting secondary device attributes"); let version = version_number(env!("CARGO_PKG_VERSION")); let text = format!("\x1b[>0;{};1c", version); self.event_proxy.send_event(Event::PtyWrite(text)); }, _ => debug!("Unsupported device attributes intermediate"), } } #[inline] fn device_status(&mut self, arg: usize) { trace!("Reporting device status: {}", arg); match arg { 5 => { let text = String::from("\x1b[0n"); self.event_proxy.send_event(Event::PtyWrite(text)); }, 6 => { let pos = self.grid.cursor.point; let text = format!("\x1b[{};{}R", pos.line + 1, pos.column + 1); self.event_proxy.send_event(Event::PtyWrite(text)); }, _ => debug!("unknown device status query: {}", arg), }; } #[inline] fn move_down_and_cr(&mut self, lines: usize) { trace!("Moving down and cr: {}", lines); self.goto(self.grid.cursor.point.line + lines, Column(0)) } #[inline] fn move_up_and_cr(&mut self, lines: usize) { trace!("Moving up and cr: {}", lines); self.goto(self.grid.cursor.point.line - lines, Column(0)) } /// Insert tab at cursor position. #[inline] fn put_tab(&mut self, mut count: u16) { // A tab after the last column is the same as a linebreak. if self.grid.cursor.input_needs_wrap { self.wrapline(); return; } while self.grid.cursor.point.column < self.columns() && count != 0 { count -= 1; let c = self.grid.cursor.charsets[self.active_charset].map('\t'); let cell = self.grid.cursor_cell(); if cell.c == ' ' { cell.c = c; } loop { if (self.grid.cursor.point.column + 1) == self.columns() { break; } self.grid.cursor.point.column += 1; if self.tabs[self.grid.cursor.point.column] { break; } } } } /// Backspace. #[inline] fn backspace(&mut self) { trace!("Backspace"); if self.grid.cursor.point.column > Column(0) { let line = self.grid.cursor.point.line.0 as usize; let column = self.grid.cursor.point.column.0 as usize; self.grid.cursor.point.column -= 1; self.grid.cursor.input_needs_wrap = false; self.damage.damage_line(line, column - 1, column); } } /// Carriage return. #[inline] fn carriage_return(&mut self) { trace!("Carriage return"); let new_col = 0; let line = self.grid.cursor.point.line.0 as usize; self.damage_line(line, new_col, self.grid.cursor.point.column.0); self.grid.cursor.point.column = Column(new_col); self.grid.cursor.input_needs_wrap = false; } /// Linefeed. #[inline] fn linefeed(&mut self) { trace!("Linefeed"); let next = self.grid.cursor.point.line + 1; if next == self.scroll_region.end { self.scroll_up(1); } else if next < self.screen_lines() { self.damage_cursor(); self.grid.cursor.point.line += 1; self.damage_cursor(); } } /// Set current position as a tabstop. #[inline] fn bell(&mut self) { trace!("Bell"); self.event_proxy.send_event(Event::Bell); } #[inline] fn substitute(&mut self) { trace!("[unimplemented] Substitute"); } /// Run LF/NL. /// /// LF/NL mode has some interesting history. According to ECMA-48 4th /// edition, in LINE FEED mode, /// /// > The execution of the formatter functions LINE FEED (LF), FORM FEED /// (FF), LINE TABULATION (VT) cause only movement of the active position in /// the direction of the line progression. /// /// In NEW LINE mode, /// /// > The execution of the formatter functions LINE FEED (LF), FORM FEED /// (FF), LINE TABULATION (VT) cause movement to the line home position on /// the following line, the following form, etc. In the case of LF this is /// referred to as the New Line (NL) option. /// /// Additionally, ECMA-48 4th edition says that this option is deprecated. /// ECMA-48 5th edition only mentions this option (without explanation) /// saying that it's been removed. /// /// As an emulator, we need to support it since applications may still rely /// on it. #[inline] fn newline(&mut self) { self.linefeed(); if self.mode.contains(TermMode::LINE_FEED_NEW_LINE) { self.carriage_return(); } } #[inline] fn set_horizontal_tabstop(&mut self) { trace!("Setting horizontal tabstop"); self.tabs[self.grid.cursor.point.column] = true; } #[inline] fn scroll_up(&mut self, lines: usize) { let origin = self.scroll_region.start; self.scroll_up_relative(origin, lines); } #[inline] fn scroll_down(&mut self, lines: usize) { let origin = self.scroll_region.start; self.scroll_down_relative(origin, lines); } #[inline] fn insert_blank_lines(&mut self, lines: usize) { trace!("Inserting blank {} lines", lines); let origin = self.grid.cursor.point.line; if self.scroll_region.contains(&origin) { self.scroll_down_relative(origin, lines); } } #[inline] fn delete_lines(&mut self, lines: usize) { let origin = self.grid.cursor.point.line; let lines = cmp::min(self.screen_lines() - origin.0 as usize, lines); trace!("Deleting {} lines", lines); if lines > 0 && self.scroll_region.contains(&origin) { self.scroll_up_relative(origin, lines); } } #[inline] fn erase_chars(&mut self, count: Column) { let cursor = &self.grid.cursor; trace!("Erasing chars: count={}, col={}", count, cursor.point.column); let start = cursor.point.column; let end = cmp::min(start + count, Column(self.columns())); // Cleared cells have current background color set. let bg = self.grid.cursor.template.bg; let line = cursor.point.line; self.damage.damage_line(line.0 as usize, start.0, end.0); let row = &mut self.grid[line]; for cell in &mut row[start..end] { *cell = bg.into(); } } #[inline] fn delete_chars(&mut self, count: usize) { let columns = self.columns(); let cursor = &self.grid.cursor; let bg = cursor.template.bg; // Ensure deleting within terminal bounds. let count = cmp::min(count, columns); let start = cursor.point.column.0; let end = cmp::min(start + count, columns - 1); let num_cells = columns - end; let line = cursor.point.line; self.damage.damage_line(line.0 as usize, 0, self.columns() - 1); let row = &mut self.grid[line][..]; for offset in 0..num_cells { row.swap(start + offset, end + offset); } // Clear last `count` cells in the row. If deleting 1 char, need to delete // 1 cell. let end = columns - count; for cell in &mut row[end..] { *cell = bg.into(); } } #[inline] fn move_backward_tabs(&mut self, count: u16) { trace!("Moving backward {} tabs", count); self.damage_cursor(); let old_col = self.grid.cursor.point.column.0; for _ in 0..count { let mut col = self.grid.cursor.point.column; for i in (0..(col.0)).rev() { if self.tabs[index::Column(i)] { col = index::Column(i); break; } } self.grid.cursor.point.column = col; } let line = self.grid.cursor.point.line.0 as usize; self.damage_line(line, self.grid.cursor.point.column.0, old_col); } #[inline] fn move_forward_tabs(&mut self, count: u16) { trace!("[unimplemented] Moving forward {} tabs", count); } #[inline] fn save_cursor_position(&mut self) { trace!("Saving cursor position"); self.grid.saved_cursor = self.grid.cursor.clone(); } #[inline] fn restore_cursor_position(&mut self) { trace!("Restoring cursor position"); self.damage_cursor(); self.grid.cursor = self.grid.saved_cursor.clone(); self.damage_cursor(); } #[inline] fn clear_line(&mut self, mode: ansi::LineClearMode) { trace!("Clearing line: {:?}", mode); let cursor = &self.grid.cursor; let bg = cursor.template.bg; let point = cursor.point; let (left, right) = match mode { ansi::LineClearMode::Right => (point.column, Column(self.columns())), ansi::LineClearMode::Left => (Column(0), point.column + 1), ansi::LineClearMode::All => (Column(0), Column(self.columns())), }; self.damage.damage_line(point.line.0 as usize, left.0, right.0 - 1); let row = &mut self.grid[point.line]; for cell in &mut row[left..right] { *cell = bg.into(); } let range = self.grid.cursor.point.line..=self.grid.cursor.point.line; self.selection = self.selection.take().filter(|s| !s.intersects_range(range)); } /// Set the indexed color value. #[inline] fn set_color(&mut self, index: usize, color: Rgb) { trace!("Setting color[{}] = {:?}", index, color); // Damage terminal if the color changed and it's not the cursor. if index != NamedColor::Cursor as usize && self.colors[index] != Some(color) { self.mark_fully_damaged(); } self.colors[index] = Some(color); } /// Respond to a color query escape sequence. #[inline] fn dynamic_color_sequence(&mut self, prefix: String, index: usize, terminator: &str) { trace!("Requested write of escape sequence for color code {}: color[{}]", prefix, index); let terminator = terminator.to_owned(); self.event_proxy.send_event(Event::ColorRequest( index, Arc::new(move |color| { format!( "\x1b]{};rgb:{1:02x}{1:02x}/{2:02x}{2:02x}/{3:02x}{3:02x}{4}", prefix, color.r, color.g, color.b, terminator ) }), )); } /// Reset the indexed color to original value. #[inline] fn reset_color(&mut self, index: usize) { trace!("Resetting color[{}]", index); // Damage terminal if the color changed and it's not the cursor. if index != NamedColor::Cursor as usize && self.colors[index].is_some() { self.mark_fully_damaged(); } self.colors[index] = None; } /// Store data into clipboard. #[inline] fn clipboard_store(&mut self, clipboard: u8, base64: &[u8]) { let clipboard_type = match clipboard { b'c' => ClipboardType::Clipboard, b'p' | b's' => ClipboardType::Selection, _ => return, }; if let Ok(bytes) = base64::decode(base64) { if let Ok(text) = String::from_utf8(bytes) { self.event_proxy.send_event(Event::ClipboardStore(clipboard_type, text)); } } } /// Load data from clipboard. #[inline] fn clipboard_load(&mut self, clipboard: u8, terminator: &str) { let clipboard_type = match clipboard { b'c' => ClipboardType::Clipboard, b'p' | b's' => ClipboardType::Selection, _ => return, }; let terminator = terminator.to_owned(); self.event_proxy.send_event(Event::ClipboardLoad( clipboard_type, Arc::new(move |text| { let base64 = base64::encode(&text); format!("\x1b]52;{};{}{}", clipboard as char, base64, terminator) }), )); } #[inline] fn clear_screen(&mut self, mode: ansi::ClearMode) { trace!("Clearing screen: {:?}", mode); let bg = self.grid.cursor.template.bg; let screen_lines = self.screen_lines(); match mode { ansi::ClearMode::Above => { let cursor = self.grid.cursor.point; // If clearing more than one line. if cursor.line > 1 { // Fully clear all lines before the current line. self.grid.reset_region(..cursor.line); } // Clear up to the current column in the current line. let end = cmp::min(cursor.column + 1, Column(self.columns())); for cell in &mut self.grid[cursor.line][..end] { *cell = bg.into(); } let range = Line(0)..=cursor.line; self.selection = self.selection.take().filter(|s| !s.intersects_range(range)); }, ansi::ClearMode::Below => { let cursor = self.grid.cursor.point; for cell in &mut self.grid[cursor.line][cursor.column..] { *cell = bg.into(); } if (cursor.line.0 as usize) < screen_lines - 1 { self.grid.reset_region((cursor.line + 1)..); } let range = cursor.line..Line(screen_lines as i32); self.selection = self.selection.take().filter(|s| !s.intersects_range(range)); }, ansi::ClearMode::All => { if self.mode.contains(TermMode::ALT_SCREEN) { self.grid.reset_region(..); } else { let old_offset = self.grid.display_offset(); self.grid.clear_viewport(); // Compute number of lines scrolled by clearing the viewport. let lines = self.grid.display_offset().saturating_sub(old_offset); self.vi_mode_cursor.point.line = (self.vi_mode_cursor.point.line - lines).grid_clamp(self, Boundary::Grid); } self.selection = None; }, ansi::ClearMode::Saved if self.history_size() > 0 => { self.grid.clear_history(); self.vi_mode_cursor.point.line = self.vi_mode_cursor.point.line.grid_clamp(self, Boundary::Cursor); self.selection = self.selection.take().filter(|s| !s.intersects_range(..Line(0))); }, // We have no history to clear. ansi::ClearMode::Saved => (), } self.mark_fully_damaged(); } #[inline] fn clear_tabs(&mut self, mode: ansi::TabulationClearMode) { trace!("Clearing tabs: {:?}", mode); match mode { ansi::TabulationClearMode::Current => { self.tabs[self.grid.cursor.point.column] = false; }, ansi::TabulationClearMode::All => { self.tabs.clear_all(); }, } } /// Reset all important fields in the term struct. #[inline] fn reset_state(&mut self) { if self.mode.contains(TermMode::ALT_SCREEN) { mem::swap(&mut self.grid, &mut self.inactive_grid); } self.active_charset = Default::default(); self.cursor_style = None; self.grid.reset(); self.inactive_grid.reset(); self.scroll_region = Line(0)..Line(self.screen_lines() as i32); self.tabs = TabStops::new(self.columns()); self.title_stack = Vec::new(); self.title = None; self.selection = None; self.vi_mode_cursor = Default::default(); // Preserve vi mode across resets. self.mode &= TermMode::VI; self.mode.insert(TermMode::default()); self.event_proxy.send_event(Event::CursorBlinkingChange); self.mark_fully_damaged(); } #[inline] fn reverse_index(&mut self) { trace!("Reversing index"); // If cursor is at the top. if self.grid.cursor.point.line == self.scroll_region.start { self.scroll_down(1); } else { self.damage_cursor(); self.grid.cursor.point.line = cmp::max(self.grid.cursor.point.line - 1, Line(0)); self.damage_cursor(); } } #[inline] fn set_hyperlink(&mut self, hyperlink: Option) { trace!("Setting hyperlink: {:?}", hyperlink); self.grid.cursor.template.set_hyperlink(hyperlink); } /// Set a terminal attribute. #[inline] fn terminal_attribute(&mut self, attr: Attr) { trace!("Setting attribute: {:?}", attr); let cursor = &mut self.grid.cursor; match attr { Attr::Foreground(color) => cursor.template.fg = color, Attr::Background(color) => cursor.template.bg = color, Attr::UnderlineColor(color) => cursor.template.set_underline_color(color), Attr::Reset => { cursor.template.fg = Color::Named(NamedColor::Foreground); cursor.template.bg = Color::Named(NamedColor::Background); cursor.template.flags = Flags::empty(); cursor.template.set_underline_color(None); }, Attr::Reverse => cursor.template.flags.insert(Flags::INVERSE), Attr::CancelReverse => cursor.template.flags.remove(Flags::INVERSE), Attr::Bold => cursor.template.flags.insert(Flags::BOLD), Attr::CancelBold => cursor.template.flags.remove(Flags::BOLD), Attr::Dim => cursor.template.flags.insert(Flags::DIM), Attr::CancelBoldDim => cursor.template.flags.remove(Flags::BOLD | Flags::DIM), Attr::Italic => cursor.template.flags.insert(Flags::ITALIC), Attr::CancelItalic => cursor.template.flags.remove(Flags::ITALIC), Attr::Underline => { cursor.template.flags.remove(Flags::ALL_UNDERLINES); cursor.template.flags.insert(Flags::UNDERLINE); }, Attr::DoubleUnderline => { cursor.template.flags.remove(Flags::ALL_UNDERLINES); cursor.template.flags.insert(Flags::DOUBLE_UNDERLINE); }, Attr::Undercurl => { cursor.template.flags.remove(Flags::ALL_UNDERLINES); cursor.template.flags.insert(Flags::UNDERCURL); }, Attr::DottedUnderline => { cursor.template.flags.remove(Flags::ALL_UNDERLINES); cursor.template.flags.insert(Flags::DOTTED_UNDERLINE); }, Attr::DashedUnderline => { cursor.template.flags.remove(Flags::ALL_UNDERLINES); cursor.template.flags.insert(Flags::DASHED_UNDERLINE); }, Attr::CancelUnderline => cursor.template.flags.remove(Flags::ALL_UNDERLINES), Attr::Hidden => cursor.template.flags.insert(Flags::HIDDEN), Attr::CancelHidden => cursor.template.flags.remove(Flags::HIDDEN), Attr::Strike => cursor.template.flags.insert(Flags::STRIKEOUT), Attr::CancelStrike => cursor.template.flags.remove(Flags::STRIKEOUT), _ => { debug!("Term got unhandled attr: {:?}", attr); }, } } #[inline] fn set_mode(&mut self, mode: ansi::Mode) { trace!("Setting mode: {:?}", mode); match mode { ansi::Mode::UrgencyHints => self.mode.insert(TermMode::URGENCY_HINTS), ansi::Mode::SwapScreenAndSetRestoreCursor => { if !self.mode.contains(TermMode::ALT_SCREEN) { self.swap_alt(); } }, ansi::Mode::ShowCursor => self.mode.insert(TermMode::SHOW_CURSOR), ansi::Mode::CursorKeys => self.mode.insert(TermMode::APP_CURSOR), // Mouse protocols are mutually exclusive. ansi::Mode::ReportMouseClicks => { self.mode.remove(TermMode::MOUSE_MODE); self.mode.insert(TermMode::MOUSE_REPORT_CLICK); self.event_proxy.send_event(Event::MouseCursorDirty); }, ansi::Mode::ReportCellMouseMotion => { self.mode.remove(TermMode::MOUSE_MODE); self.mode.insert(TermMode::MOUSE_DRAG); self.event_proxy.send_event(Event::MouseCursorDirty); }, ansi::Mode::ReportAllMouseMotion => { self.mode.remove(TermMode::MOUSE_MODE); self.mode.insert(TermMode::MOUSE_MOTION); self.event_proxy.send_event(Event::MouseCursorDirty); }, ansi::Mode::ReportFocusInOut => self.mode.insert(TermMode::FOCUS_IN_OUT), ansi::Mode::BracketedPaste => self.mode.insert(TermMode::BRACKETED_PASTE), // Mouse encodings are mutually exclusive. ansi::Mode::SgrMouse => { self.mode.remove(TermMode::UTF8_MOUSE); self.mode.insert(TermMode::SGR_MOUSE); }, ansi::Mode::Utf8Mouse => { self.mode.remove(TermMode::SGR_MOUSE); self.mode.insert(TermMode::UTF8_MOUSE); }, ansi::Mode::AlternateScroll => self.mode.insert(TermMode::ALTERNATE_SCROLL), ansi::Mode::LineWrap => self.mode.insert(TermMode::LINE_WRAP), ansi::Mode::LineFeedNewLine => self.mode.insert(TermMode::LINE_FEED_NEW_LINE), ansi::Mode::Origin => self.mode.insert(TermMode::ORIGIN), ansi::Mode::ColumnMode => self.deccolm(), ansi::Mode::Insert => self.mode.insert(TermMode::INSERT), ansi::Mode::BlinkingCursor => { let style = self.cursor_style.get_or_insert(self.default_cursor_style); style.blinking = true; self.event_proxy.send_event(Event::CursorBlinkingChange); }, } } #[inline] fn unset_mode(&mut self, mode: ansi::Mode) { trace!("Unsetting mode: {:?}", mode); match mode { ansi::Mode::UrgencyHints => self.mode.remove(TermMode::URGENCY_HINTS), ansi::Mode::SwapScreenAndSetRestoreCursor => { if self.mode.contains(TermMode::ALT_SCREEN) { self.swap_alt(); } }, ansi::Mode::ShowCursor => self.mode.remove(TermMode::SHOW_CURSOR), ansi::Mode::CursorKeys => self.mode.remove(TermMode::APP_CURSOR), ansi::Mode::ReportMouseClicks => { self.mode.remove(TermMode::MOUSE_REPORT_CLICK); self.event_proxy.send_event(Event::MouseCursorDirty); }, ansi::Mode::ReportCellMouseMotion => { self.mode.remove(TermMode::MOUSE_DRAG); self.event_proxy.send_event(Event::MouseCursorDirty); }, ansi::Mode::ReportAllMouseMotion => { self.mode.remove(TermMode::MOUSE_MOTION); self.event_proxy.send_event(Event::MouseCursorDirty); }, ansi::Mode::ReportFocusInOut => self.mode.remove(TermMode::FOCUS_IN_OUT), ansi::Mode::BracketedPaste => self.mode.remove(TermMode::BRACKETED_PASTE), ansi::Mode::SgrMouse => self.mode.remove(TermMode::SGR_MOUSE), ansi::Mode::Utf8Mouse => self.mode.remove(TermMode::UTF8_MOUSE), ansi::Mode::AlternateScroll => self.mode.remove(TermMode::ALTERNATE_SCROLL), ansi::Mode::LineWrap => self.mode.remove(TermMode::LINE_WRAP), ansi::Mode::LineFeedNewLine => self.mode.remove(TermMode::LINE_FEED_NEW_LINE), ansi::Mode::Origin => self.mode.remove(TermMode::ORIGIN), ansi::Mode::ColumnMode => self.deccolm(), ansi::Mode::Insert => { self.mode.remove(TermMode::INSERT); self.mark_fully_damaged(); }, ansi::Mode::BlinkingCursor => { let style = self.cursor_style.get_or_insert(self.default_cursor_style); style.blinking = false; self.event_proxy.send_event(Event::CursorBlinkingChange); }, } } #[inline] fn set_scrolling_region(&mut self, top: usize, bottom: Option) { // Fallback to the last line as default. let bottom = bottom.unwrap_or_else(|| self.screen_lines()); if top >= bottom { debug!("Invalid scrolling region: ({};{})", top, bottom); return; } // Bottom should be included in the range, but range end is not // usually included. One option would be to use an inclusive // range, but instead we just let the open range end be 1 // higher. let start = Line(top as i32 - 1); let end = Line(bottom as i32); trace!("Setting scrolling region: ({};{})", start, end); let screen_lines = Line(self.screen_lines() as i32); self.scroll_region.start = cmp::min(start, screen_lines); self.scroll_region.end = cmp::min(end, screen_lines); self.goto(Line(0), Column(0)); } #[inline] fn set_keypad_application_mode(&mut self) { trace!("Setting keypad application mode"); self.mode.insert(TermMode::APP_KEYPAD); } #[inline] fn unset_keypad_application_mode(&mut self) { trace!("Unsetting keypad application mode"); self.mode.remove(TermMode::APP_KEYPAD); } #[inline] fn configure_charset(&mut self, index: CharsetIndex, charset: StandardCharset) { trace!("Configuring charset {:?} as {:?}", index, charset); self.grid.cursor.charsets[index] = charset; } #[inline] fn set_active_charset(&mut self, index: CharsetIndex) { trace!("Setting active charset {:?}", index); self.active_charset = index; } #[inline] fn set_cursor_style(&mut self, style: Option) { trace!("Setting cursor style {:?}", style); self.cursor_style = style; // Notify UI about blinking changes. self.event_proxy.send_event(Event::CursorBlinkingChange); } #[inline] fn set_cursor_shape(&mut self, shape: CursorShape) { trace!("Setting cursor shape {:?}", shape); let style = self.cursor_style.get_or_insert(self.default_cursor_style); style.shape = shape; } #[inline] fn set_title(&mut self, title: Option) { trace!("Setting title to '{:?}'", title); self.title = title.clone(); let title_event = match title { Some(title) => Event::Title(title), None => Event::ResetTitle, }; self.event_proxy.send_event(title_event); } #[inline] fn push_title(&mut self) { trace!("Pushing '{:?}' onto title stack", self.title); if self.title_stack.len() >= TITLE_STACK_MAX_DEPTH { let removed = self.title_stack.remove(0); trace!( "Removing '{:?}' from bottom of title stack that exceeds its maximum depth", removed ); } self.title_stack.push(self.title.clone()); } #[inline] fn pop_title(&mut self) { trace!("Attempting to pop title from stack..."); if let Some(popped) = self.title_stack.pop() { trace!("Title '{:?}' popped from stack", popped); self.set_title(popped); } } #[inline] fn text_area_size_pixels(&mut self) { self.event_proxy.send_event(Event::TextAreaSizeRequest(Arc::new(move |window_size| { let height = window_size.num_lines * window_size.cell_height; let width = window_size.num_cols * window_size.cell_width; format!("\x1b[4;{};{}t", height, width) }))); } #[inline] fn text_area_size_chars(&mut self) { let text = format!("\x1b[8;{};{}t", self.screen_lines(), self.columns()); self.event_proxy.send_event(Event::PtyWrite(text)); } } /// Terminal version for escape sequence reports. /// /// This returns the current terminal version as a unique number based on alacritty_terminal's /// semver version. The different versions are padded to ensure that a higher semver version will /// always report a higher version number. fn version_number(mut version: &str) -> usize { if let Some(separator) = version.rfind('-') { version = &version[..separator]; } let mut version_number = 0; let semver_versions = version.split('.'); for (i, semver_version) in semver_versions.rev().enumerate() { let semver_number = semver_version.parse::().unwrap_or(0); version_number += usize::pow(100, i as u32) * semver_number; } version_number } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ClipboardType { Clipboard, Selection, } struct TabStops { tabs: Vec, } impl TabStops { #[inline] fn new(columns: usize) -> TabStops { TabStops { tabs: (0..columns).map(|i| i % INITIAL_TABSTOPS == 0).collect() } } /// Remove all tabstops. #[inline] fn clear_all(&mut self) { unsafe { ptr::write_bytes(self.tabs.as_mut_ptr(), 0, self.tabs.len()); } } /// Increase tabstop capacity. #[inline] fn resize(&mut self, columns: usize) { let mut index = self.tabs.len(); self.tabs.resize_with(columns, || { let is_tabstop = index % INITIAL_TABSTOPS == 0; index += 1; is_tabstop }); } } 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) } } /// Terminal cursor rendering information. #[derive(Copy, Clone, PartialEq, Eq)] pub struct RenderableCursor { pub shape: CursorShape, pub point: Point, } impl RenderableCursor { fn new(term: &Term) -> Self { // Cursor position. let vi_mode = term.mode().contains(TermMode::VI); let mut point = if vi_mode { term.vi_mode_cursor.point } else { term.grid.cursor.point }; if term.grid[point].flags.contains(Flags::WIDE_CHAR_SPACER) { point.column -= 1; } // Cursor shape. let shape = if !vi_mode && !term.mode().contains(TermMode::SHOW_CURSOR) { CursorShape::Hidden } else { term.cursor_style().shape }; Self { shape, point } } } /// Visible terminal content. /// /// This contains all content required to render the current terminal view. pub struct RenderableContent<'a> { pub display_iter: GridIterator<'a, Cell>, pub selection: Option, pub cursor: RenderableCursor, pub display_offset: usize, pub colors: &'a color::Colors, pub mode: TermMode, } impl<'a> RenderableContent<'a> { fn new(term: &'a Term) -> Self { Self { display_iter: term.grid().display_iter(), display_offset: term.grid().display_offset(), cursor: RenderableCursor::new(term), selection: term.selection.as_ref().and_then(|s| s.to_range(term)), colors: &term.colors, mode: *term.mode(), } } } /// Terminal test helpers. pub mod test { use super::*; use serde::{Deserialize, Serialize}; use unicode_width::UnicodeWidthChar; use crate::config::Config; use crate::index::Column; #[derive(Serialize, Deserialize)] pub struct TermSize { pub columns: usize, pub screen_lines: usize, } impl TermSize { pub fn new(columns: usize, screen_lines: usize) -> Self { Self { columns, screen_lines } } } impl Dimensions for TermSize { fn total_lines(&self) -> usize { self.screen_lines() } fn screen_lines(&self) -> usize { self.screen_lines } fn columns(&self) -> usize { self.columns } } /// Construct a terminal from its content as string. /// /// A `\n` will break line and `\r\n` will break line without wrapping. /// /// # Examples /// /// ```rust /// use alacritty_terminal::term::test::mock_term; /// /// // Create a terminal with the following cells: /// // /// // [h][e][l][l][o] <- WRAPLINE flag set /// // [:][)][ ][ ][ ] /// // [t][e][s][t][ ] /// mock_term( /// "\ /// hello\n:)\r\ntest", /// ); /// ``` pub fn mock_term(content: &str) -> Term<()> { let lines: Vec<&str> = content.split('\n').collect(); let num_cols = lines .iter() .map(|line| line.chars().filter(|c| *c != '\r').map(|c| c.width().unwrap()).sum()) .max() .unwrap_or(0); // Create terminal with the appropriate dimensions. let size = TermSize::new(num_cols, lines.len()); let mut term = Term::new(&Config::default(), &size, ()); // Fill terminal with content. for (line, text) in lines.iter().enumerate() { let line = Line(line as i32); if !text.ends_with('\r') && line + 1 != lines.len() { term.grid[line][Column(num_cols - 1)].flags.insert(Flags::WRAPLINE); } let mut index = 0; for c in text.chars().take_while(|c| *c != '\r') { term.grid[line][Column(index)].c = c; // Handle fullwidth characters. let width = c.width().unwrap(); if width == 2 { term.grid[line][Column(index)].flags.insert(Flags::WIDE_CHAR); term.grid[line][Column(index + 1)].flags.insert(Flags::WIDE_CHAR_SPACER); } index += width; } } term } } #[cfg(test)] mod tests { use super::*; use std::mem; use crate::ansi::{self, CharsetIndex, Handler, StandardCharset}; use crate::config::Config; use crate::grid::{Grid, Scroll}; use crate::index::{Column, Point, Side}; use crate::selection::{Selection, SelectionType}; use crate::term::cell::{Cell, Flags}; use crate::term::test::TermSize; #[test] fn scroll_display_page_up() { let size = TermSize::new(5, 10); let mut term = Term::new(&Config::default(), &size, ()); // Create 11 lines of scrollback. for _ in 0..20 { term.newline(); } // Scrollable amount to top is 11. term.scroll_display(Scroll::PageUp); assert_eq!(term.vi_mode_cursor.point, Point::new(Line(-1), Column(0))); assert_eq!(term.grid.display_offset(), 10); // Scrollable amount to top is 1. term.scroll_display(Scroll::PageUp); assert_eq!(term.vi_mode_cursor.point, Point::new(Line(-2), Column(0))); assert_eq!(term.grid.display_offset(), 11); // Scrollable amount to top is 0. term.scroll_display(Scroll::PageUp); assert_eq!(term.vi_mode_cursor.point, Point::new(Line(-2), Column(0))); assert_eq!(term.grid.display_offset(), 11); } #[test] fn scroll_display_page_down() { let size = TermSize::new(5, 10); let mut term = Term::new(&Config::default(), &size, ()); // Create 11 lines of scrollback. for _ in 0..20 { term.newline(); } // Change display_offset to topmost. term.grid_mut().scroll_display(Scroll::Top); term.vi_mode_cursor = ViModeCursor::new(Point::new(Line(-11), Column(0))); // Scrollable amount to bottom is 11. term.scroll_display(Scroll::PageDown); assert_eq!(term.vi_mode_cursor.point, Point::new(Line(-1), Column(0))); assert_eq!(term.grid.display_offset(), 1); // Scrollable amount to bottom is 1. term.scroll_display(Scroll::PageDown); assert_eq!(term.vi_mode_cursor.point, Point::new(Line(0), Column(0))); assert_eq!(term.grid.display_offset(), 0); // Scrollable amount to bottom is 0. term.scroll_display(Scroll::PageDown); assert_eq!(term.vi_mode_cursor.point, Point::new(Line(0), Column(0))); assert_eq!(term.grid.display_offset(), 0); } #[test] fn simple_selection_works() { let size = TermSize::new(5, 5); let mut term = Term::new(&Config::default(), &size, ()); let grid = term.grid_mut(); for i in 0..4 { if i == 1 { continue; } grid[Line(i)][Column(0)].c = '"'; for j in 1..4 { grid[Line(i)][Column(j)].c = 'a'; } grid[Line(i)][Column(4)].c = '"'; } grid[Line(2)][Column(0)].c = ' '; grid[Line(2)][Column(4)].c = ' '; grid[Line(2)][Column(4)].flags.insert(Flags::WRAPLINE); grid[Line(3)][Column(0)].c = ' '; // Multiple lines contain an empty line. term.selection = Some(Selection::new( SelectionType::Simple, Point { line: Line(0), column: Column(0) }, Side::Left, )); if let Some(s) = term.selection.as_mut() { s.update(Point { line: Line(2), column: Column(4) }, Side::Right); } assert_eq!(term.selection_to_string(), Some(String::from("\"aaa\"\n\n aaa "))); // A wrapline. term.selection = Some(Selection::new( SelectionType::Simple, Point { line: Line(2), column: Column(0) }, Side::Left, )); if let Some(s) = term.selection.as_mut() { s.update(Point { line: Line(3), column: Column(4) }, Side::Right); } assert_eq!(term.selection_to_string(), Some(String::from(" aaa aaa\""))); } #[test] fn semantic_selection_works() { let size = TermSize::new(5, 3); let mut term = Term::new(&Config::default(), &size, ()); let mut grid: Grid = Grid::new(3, 5, 0); 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(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 = Some(Selection::new( SelectionType::Semantic, Point { line: Line(0), column: Column(1) }, Side::Left, )); assert_eq!(term.selection_to_string(), Some(String::from("aa"))); } { term.selection = Some(Selection::new( SelectionType::Semantic, Point { line: Line(0), column: Column(4) }, Side::Left, )); assert_eq!(term.selection_to_string(), Some(String::from("aaa"))); } { term.selection = Some(Selection::new( SelectionType::Semantic, Point { line: Line(1), column: Column(1) }, Side::Left, )); assert_eq!(term.selection_to_string(), Some(String::from("aaa"))); } } #[test] fn line_selection_works() { let size = TermSize::new(5, 1); let mut term = Term::new(&Config::default(), &size, ()); let mut grid: Grid = Grid::new(1, 5, 0); 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 = Some(Selection::new( SelectionType::Lines, Point { line: Line(0), column: Column(3) }, Side::Left, )); assert_eq!(term.selection_to_string(), Some(String::from("\"aa\"a\n"))); } #[test] fn block_selection_works() { let size = TermSize::new(5, 5); let mut term = Term::new(&Config::default(), &size, ()); let grid = term.grid_mut(); for i in 1..4 { grid[Line(i)][Column(0)].c = '"'; for j in 1..4 { grid[Line(i)][Column(j)].c = 'a'; } grid[Line(i)][Column(4)].c = '"'; } grid[Line(2)][Column(2)].c = ' '; grid[Line(2)][Column(4)].flags.insert(Flags::WRAPLINE); grid[Line(3)][Column(4)].c = ' '; term.selection = Some(Selection::new( SelectionType::Block, Point { line: Line(0), column: Column(3) }, Side::Left, )); // The same column. if let Some(s) = term.selection.as_mut() { s.update(Point { line: Line(3), column: Column(3) }, Side::Right); } assert_eq!(term.selection_to_string(), Some(String::from("\na\na\na"))); // The first column. if let Some(s) = term.selection.as_mut() { s.update(Point { line: Line(3), column: Column(0) }, Side::Left); } assert_eq!(term.selection_to_string(), Some(String::from("\n\"aa\n\"a\n\"aa"))); // The last column. if let Some(s) = term.selection.as_mut() { s.update(Point { line: Line(3), column: Column(4) }, Side::Right); } assert_eq!(term.selection_to_string(), Some(String::from("\na\"\na\"\na"))); } /// 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 grid: Grid = Grid::new(24, 80, 0); 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 = TermSize::new(7, 17); let mut term = Term::new(&Config::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, '▒'); } #[test] fn clearing_viewport_keeps_history_position() { let size = TermSize::new(10, 20); let mut term = Term::new(&Config::default(), &size, ()); // Create 10 lines of scrollback. for _ in 0..29 { term.newline(); } // Change the display area. term.scroll_display(Scroll::Top); assert_eq!(term.grid.display_offset(), 10); // Clear the viewport. term.clear_screen(ansi::ClearMode::All); assert_eq!(term.grid.display_offset(), 10); } #[test] fn clearing_viewport_with_vi_mode_keeps_history_position() { let size = TermSize::new(10, 20); let mut term = Term::new(&Config::default(), &size, ()); // Create 10 lines of scrollback. for _ in 0..29 { term.newline(); } // Enable vi mode. term.toggle_vi_mode(); // Change the display area and the vi cursor position. term.scroll_display(Scroll::Top); term.vi_mode_cursor.point = Point::new(Line(-5), Column(3)); assert_eq!(term.grid.display_offset(), 10); // Clear the viewport. term.clear_screen(ansi::ClearMode::All); assert_eq!(term.grid.display_offset(), 10); assert_eq!(term.vi_mode_cursor.point, Point::new(Line(-5), Column(3))); } #[test] fn clearing_scrollback_resets_display_offset() { let size = TermSize::new(10, 20); let mut term = Term::new(&Config::default(), &size, ()); // Create 10 lines of scrollback. for _ in 0..29 { term.newline(); } // Change the display area. term.scroll_display(Scroll::Top); assert_eq!(term.grid.display_offset(), 10); // Clear the scrollback buffer. term.clear_screen(ansi::ClearMode::Saved); assert_eq!(term.grid.display_offset(), 0); } #[test] fn clearing_scrollback_sets_vi_cursor_into_viewport() { let size = TermSize::new(10, 20); let mut term = Term::new(&Config::default(), &size, ()); // Create 10 lines of scrollback. for _ in 0..29 { term.newline(); } // Enable vi mode. term.toggle_vi_mode(); // Change the display area and the vi cursor position. term.scroll_display(Scroll::Top); term.vi_mode_cursor.point = Point::new(Line(-5), Column(3)); assert_eq!(term.grid.display_offset(), 10); // Clear the scrollback buffer. term.clear_screen(ansi::ClearMode::Saved); assert_eq!(term.grid.display_offset(), 0); assert_eq!(term.vi_mode_cursor.point, Point::new(Line(0), Column(3))); } #[test] fn clear_saved_lines() { let size = TermSize::new(7, 17); let mut term = Term::new(&Config::default(), &size, ()); // Add one line of scrollback. term.grid.scroll_up(&(Line(0)..Line(1)), 1); // 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); // Truncate grids for comparison. scrolled_grid.truncate(); term.grid.truncate(); assert_eq!(term.grid, scrolled_grid); } #[test] fn vi_cursor_keep_pos_on_scrollback_buffer() { let size = TermSize::new(5, 10); let mut term = Term::new(&Config::default(), &size, ()); // Create 11 lines of scrollback. for _ in 0..20 { term.newline(); } // Enable vi mode. term.toggle_vi_mode(); term.scroll_display(Scroll::Top); term.vi_mode_cursor.point.line = Line(-11); term.linefeed(); assert_eq!(term.vi_mode_cursor.point.line, Line(-12)); } #[test] fn grow_lines_updates_active_cursor_pos() { let mut size = TermSize::new(100, 10); let mut term = Term::new(&Config::default(), &size, ()); // Create 10 lines of scrollback. for _ in 0..19 { term.newline(); } assert_eq!(term.history_size(), 10); assert_eq!(term.grid.cursor.point, Point::new(Line(9), Column(0))); // Increase visible lines. size.screen_lines = 30; term.resize(size); assert_eq!(term.history_size(), 0); assert_eq!(term.grid.cursor.point, Point::new(Line(19), Column(0))); } #[test] fn grow_lines_updates_inactive_cursor_pos() { let mut size = TermSize::new(100, 10); let mut term = Term::new(&Config::default(), &size, ()); // Create 10 lines of scrollback. for _ in 0..19 { term.newline(); } assert_eq!(term.history_size(), 10); assert_eq!(term.grid.cursor.point, Point::new(Line(9), Column(0))); // Enter alt screen. term.set_mode(ansi::Mode::SwapScreenAndSetRestoreCursor); // Increase visible lines. size.screen_lines = 30; term.resize(size); // Leave alt screen. term.unset_mode(ansi::Mode::SwapScreenAndSetRestoreCursor); assert_eq!(term.history_size(), 0); assert_eq!(term.grid.cursor.point, Point::new(Line(19), Column(0))); } #[test] fn shrink_lines_updates_active_cursor_pos() { let mut size = TermSize::new(100, 10); let mut term = Term::new(&Config::default(), &size, ()); // Create 10 lines of scrollback. for _ in 0..19 { term.newline(); } assert_eq!(term.history_size(), 10); assert_eq!(term.grid.cursor.point, Point::new(Line(9), Column(0))); // Increase visible lines. size.screen_lines = 5; term.resize(size); assert_eq!(term.history_size(), 15); assert_eq!(term.grid.cursor.point, Point::new(Line(4), Column(0))); } #[test] fn shrink_lines_updates_inactive_cursor_pos() { let mut size = TermSize::new(100, 10); let mut term = Term::new(&Config::default(), &size, ()); // Create 10 lines of scrollback. for _ in 0..19 { term.newline(); } assert_eq!(term.history_size(), 10); assert_eq!(term.grid.cursor.point, Point::new(Line(9), Column(0))); // Enter alt screen. term.set_mode(ansi::Mode::SwapScreenAndSetRestoreCursor); // Increase visible lines. size.screen_lines = 5; term.resize(size); // Leave alt screen. term.unset_mode(ansi::Mode::SwapScreenAndSetRestoreCursor); assert_eq!(term.history_size(), 15); assert_eq!(term.grid.cursor.point, Point::new(Line(4), Column(0))); } #[test] fn damage_public_usage() { let size = TermSize::new(10, 10); let mut term = Term::new(&Config::default(), &size, ()); // Reset terminal for partial damage tests since it's initialized as fully damaged. term.reset_damage(); // Test that we damage input form [`Term::input`]. let left = term.grid.cursor.point.column.0; term.input('d'); term.input('a'); term.input('m'); term.input('a'); term.input('g'); term.input('e'); let right = term.grid.cursor.point.column.0; let mut damaged_lines = match term.damage(None) { TermDamage::Full => panic!("Expected partial damage, however got Full"), TermDamage::Partial(damaged_lines) => damaged_lines, }; assert_eq!(damaged_lines.next(), Some(LineDamageBounds { line: 0, left, right })); assert_eq!(damaged_lines.next(), None); term.reset_damage(); // Check that selection we've passed was properly damaged. let line = 1; let left = 0; let right = term.columns() - 1; let mut selection = Selection::new(SelectionType::Block, Point::new(Line(line), Column(3)), Side::Left); selection.update(Point::new(Line(line), Column(5)), Side::Left); let selection_range = selection.to_range(&term); let mut damaged_lines = match term.damage(selection_range) { TermDamage::Full => panic!("Expected partial damage, however got Full"), TermDamage::Partial(damaged_lines) => damaged_lines, }; let line = line as usize; // Skip cursor damage information, since we're just testing selection. damaged_lines.next(); assert_eq!(damaged_lines.next(), Some(LineDamageBounds { line, left, right })); assert_eq!(damaged_lines.next(), None); term.reset_damage(); // Check that existing selection gets damaged when it is removed. let mut damaged_lines = match term.damage(None) { TermDamage::Full => panic!("Expected partial damage, however got Full"), TermDamage::Partial(damaged_lines) => damaged_lines, }; // Skip cursor damage information, since we're just testing selection clearing. damaged_lines.next(); assert_eq!(damaged_lines.next(), Some(LineDamageBounds { line, left, right })); assert_eq!(damaged_lines.next(), None); term.reset_damage(); // Check that `Vi` cursor in vi mode is being always damaged. term.toggle_vi_mode(); // Put Vi cursor to a different location than normal cursor. term.vi_goto_point(Point::new(Line(5), Column(5))); // Reset damage, so the damage information from `vi_goto_point` won't affect test. term.reset_damage(); let vi_cursor_point = term.vi_mode_cursor.point; let line = vi_cursor_point.line.0 as usize; let left = vi_cursor_point.column.0 as usize; let right = left; let mut damaged_lines = match term.damage(None) { TermDamage::Full => panic!("Expected partial damage, however got Full"), TermDamage::Partial(damaged_lines) => damaged_lines, }; // Skip cursor damage information, since we're just testing Vi cursor. damaged_lines.next(); assert_eq!(damaged_lines.next(), Some(LineDamageBounds { line, left, right })); assert_eq!(damaged_lines.next(), None); // Ensure that old Vi cursor got damaged as well. term.reset_damage(); term.toggle_vi_mode(); let mut damaged_lines = match term.damage(None) { TermDamage::Full => panic!("Expected partial damage, however got Full"), TermDamage::Partial(damaged_lines) => damaged_lines, }; // Skip cursor damage information, since we're just testing Vi cursor. damaged_lines.next(); assert_eq!(damaged_lines.next(), Some(LineDamageBounds { line, left, right })); assert_eq!(damaged_lines.next(), None); } #[test] fn damage_cursor_movements() { let size = TermSize::new(10, 10); let mut term = Term::new(&Config::default(), &size, ()); let num_cols = term.columns(); // Reset terminal for partial damage tests since it's initialized as fully damaged. term.reset_damage(); term.goto(Line(1), Column(1)); // NOTE While we can use `[Term::damage]` to access terminal damage information, in the // following tests we will be accessing `term.damage.lines` directly to avoid adding extra // damage information (like cursor and Vi cursor), which we're not testing. assert_eq!(term.damage.lines[0], LineDamageBounds { line: 0, left: 0, right: 0 }); assert_eq!(term.damage.lines[1], LineDamageBounds { line: 1, left: 1, right: 1 }); term.damage.reset(num_cols); term.move_forward(Column(3)); assert_eq!(term.damage.lines[1], LineDamageBounds { line: 1, left: 1, right: 4 }); term.damage.reset(num_cols); term.move_backward(Column(8)); assert_eq!(term.damage.lines[1], LineDamageBounds { line: 1, left: 0, right: 4 }); term.goto(Line(5), Column(5)); term.damage.reset(num_cols); term.backspace(); term.backspace(); assert_eq!(term.damage.lines[5], LineDamageBounds { line: 5, left: 3, right: 5 }); term.damage.reset(num_cols); term.move_up(1); assert_eq!(term.damage.lines[5], LineDamageBounds { line: 5, left: 3, right: 3 }); assert_eq!(term.damage.lines[4], LineDamageBounds { line: 4, left: 3, right: 3 }); term.damage.reset(num_cols); term.move_down(1); term.move_down(1); assert_eq!(term.damage.lines[4], LineDamageBounds { line: 4, left: 3, right: 3 }); assert_eq!(term.damage.lines[5], LineDamageBounds { line: 5, left: 3, right: 3 }); assert_eq!(term.damage.lines[6], LineDamageBounds { line: 6, left: 3, right: 3 }); term.damage.reset(num_cols); term.wrapline(); assert_eq!(term.damage.lines[6], LineDamageBounds { line: 6, left: 3, right: 3 }); assert_eq!(term.damage.lines[7], LineDamageBounds { line: 7, left: 0, right: 0 }); term.move_forward(Column(3)); term.move_up(1); term.damage.reset(num_cols); term.linefeed(); assert_eq!(term.damage.lines[6], LineDamageBounds { line: 6, left: 3, right: 3 }); assert_eq!(term.damage.lines[7], LineDamageBounds { line: 7, left: 3, right: 3 }); term.damage.reset(num_cols); term.carriage_return(); assert_eq!(term.damage.lines[7], LineDamageBounds { line: 7, left: 0, right: 3 }); term.damage.reset(num_cols); term.erase_chars(Column(5)); assert_eq!(term.damage.lines[7], LineDamageBounds { line: 7, left: 0, right: 5 }); term.damage.reset(num_cols); term.delete_chars(3); let right = term.columns() - 1; assert_eq!(term.damage.lines[7], LineDamageBounds { line: 7, left: 0, right }); term.move_forward(Column(term.columns())); term.damage.reset(num_cols); term.move_backward_tabs(1); assert_eq!(term.damage.lines[7], LineDamageBounds { line: 7, left: 8, right }); term.save_cursor_position(); term.goto(Line(1), Column(1)); term.damage.reset(num_cols); term.restore_cursor_position(); assert_eq!(term.damage.lines[1], LineDamageBounds { line: 1, left: 1, right: 1 }); assert_eq!(term.damage.lines[7], LineDamageBounds { line: 7, left: 8, right: 8 }); term.damage.reset(num_cols); term.clear_line(ansi::LineClearMode::All); assert_eq!(term.damage.lines[7], LineDamageBounds { line: 7, left: 0, right }); term.damage.reset(num_cols); term.clear_line(ansi::LineClearMode::Left); assert_eq!(term.damage.lines[7], LineDamageBounds { line: 7, left: 0, right: 8 }); term.damage.reset(num_cols); term.clear_line(ansi::LineClearMode::Right); assert_eq!(term.damage.lines[7], LineDamageBounds { line: 7, left: 8, right }); term.damage.reset(num_cols); term.reverse_index(); assert_eq!(term.damage.lines[7], LineDamageBounds { line: 7, left: 8, right: 8 }); assert_eq!(term.damage.lines[6], LineDamageBounds { line: 6, left: 8, right: 8 }); } #[test] fn full_damage() { let size = TermSize::new(100, 10); let mut term = Term::new(&Config::default(), &size, ()); assert!(term.damage.is_fully_damaged); for _ in 0..20 { term.newline(); } term.reset_damage(); term.clear_screen(ansi::ClearMode::Above); assert!(term.damage.is_fully_damaged); term.reset_damage(); term.scroll_display(Scroll::Top); assert!(term.damage.is_fully_damaged); term.reset_damage(); // Sequential call to scroll display without doing anything shouldn't damage. term.scroll_display(Scroll::Top); assert!(!term.damage.is_fully_damaged); term.reset_damage(); term.update_config(&Config::default()); assert!(term.damage.is_fully_damaged); term.reset_damage(); term.scroll_down_relative(Line(5), 2); assert!(term.damage.is_fully_damaged); term.reset_damage(); term.scroll_up_relative(Line(3), 2); assert!(term.damage.is_fully_damaged); term.reset_damage(); term.deccolm(); assert!(term.damage.is_fully_damaged); term.reset_damage(); term.decaln(); assert!(term.damage.is_fully_damaged); term.reset_damage(); term.set_mode(ansi::Mode::Insert); // Just setting `Insert` mode shouldn't mark terminal as damaged. assert!(!term.damage.is_fully_damaged); term.reset_damage(); let color_index = 257; term.set_color(color_index, Rgb::default()); assert!(term.damage.is_fully_damaged); term.reset_damage(); // Setting the same color once again shouldn't trigger full damage. term.set_color(color_index, Rgb::default()); assert!(!term.damage.is_fully_damaged); term.reset_color(color_index); assert!(term.damage.is_fully_damaged); term.reset_damage(); // We shouldn't trigger fully damage when cursor gets update. term.set_color(NamedColor::Cursor as usize, Rgb::default()); assert!(!term.damage.is_fully_damaged); // However requesting terminal damage should mark terminal as fully damaged in `Insert` // mode. let _ = term.damage(None); assert!(term.damage.is_fully_damaged); term.reset_damage(); term.unset_mode(ansi::Mode::Insert); assert!(term.damage.is_fully_damaged); term.reset_damage(); // Keep this as a last check, so we don't have to deal with restoring from alt-screen. term.swap_alt(); assert!(term.damage.is_fully_damaged); term.reset_damage(); let size = TermSize::new(10, 10); term.resize(size); assert!(term.damage.is_fully_damaged); } #[test] fn window_title() { let size = TermSize::new(7, 17); let mut term = Term::new(&Config::default(), &size, ()); // Title None by default. assert_eq!(term.title, None); // Title can be set. term.set_title(Some("Test".into())); assert_eq!(term.title, Some("Test".into())); // Title can be pushed onto stack. term.push_title(); term.set_title(Some("Next".into())); assert_eq!(term.title, Some("Next".into())); assert_eq!(term.title_stack.get(0).unwrap(), &Some("Test".into())); // Title can be popped from stack and set as the window title. term.pop_title(); assert_eq!(term.title, Some("Test".into())); assert!(term.title_stack.is_empty()); // Title stack doesn't grow infinitely. for _ in 0..4097 { term.push_title(); } assert_eq!(term.title_stack.len(), 4096); // Title and title stack reset when terminal state is reset. term.push_title(); term.reset_state(); assert_eq!(term.title, None); assert!(term.title_stack.is_empty()); // Title stack pops back to default. term.title = None; term.push_title(); term.set_title(Some("Test".into())); term.pop_title(); assert_eq!(term.title, None); // Title can be reset to default. term.title = Some("Test".into()); term.set_title(None); assert_eq!(term.title, None); } #[test] fn parse_cargo_version() { assert!(version_number(env!("CARGO_PKG_VERSION")) >= 10_01); assert_eq!(version_number("0.0.1-dev"), 1); assert_eq!(version_number("0.1.2-dev"), 1_02); assert_eq!(version_number("1.2.3-dev"), 1_02_03); assert_eq!(version_number("999.99.99"), 9_99_99_99); } }