| //! Exports the `Term` type which is a high-level API for the Grid. |
| |
| use std::cmp::{max, min}; |
| use std::ops::{Index, IndexMut, Range}; |
| use std::sync::Arc; |
| use std::{mem, ptr, str}; |
| |
| use bitflags::bitflags; |
| use log::{debug, trace}; |
| use serde::{Deserialize, Serialize}; |
| 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, 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 = std::u32::MAX; |
| } |
| } |
| |
| impl Default for TermMode { |
| fn default() -> TermMode { |
| TermMode::SHOW_CURSOR |
| | TermMode::LINE_WRAP |
| | TermMode::ALTERNATE_SCROLL |
| | TermMode::URGENCY_HINTS |
| } |
| } |
| |
| /// Terminal size info. |
| #[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq)] |
| pub struct SizeInfo { |
| /// Terminal window width. |
| width: f32, |
| |
| /// Terminal window height. |
| height: f32, |
| |
| /// Width of individual cell. |
| cell_width: f32, |
| |
| /// Height of individual cell. |
| cell_height: f32, |
| |
| /// Horizontal window padding. |
| padding_x: f32, |
| |
| /// Horizontal window padding. |
| padding_y: f32, |
| |
| /// Number of lines in the viewport. |
| screen_lines: usize, |
| |
| /// Number of columns in the viewport. |
| columns: usize, |
| } |
| |
| impl SizeInfo { |
| #[allow(clippy::too_many_arguments)] |
| pub fn new( |
| width: f32, |
| height: f32, |
| cell_width: f32, |
| cell_height: f32, |
| mut padding_x: f32, |
| mut padding_y: f32, |
| dynamic_padding: bool, |
| ) -> SizeInfo { |
| if dynamic_padding { |
| padding_x = Self::dynamic_padding(padding_x.floor(), width, cell_width); |
| padding_y = Self::dynamic_padding(padding_y.floor(), height, cell_height); |
| } |
| |
| let lines = (height - 2. * padding_y) / cell_height; |
| let screen_lines = max(lines as usize, MIN_SCREEN_LINES); |
| |
| let columns = (width - 2. * padding_x) / cell_width; |
| let columns = max(columns as usize, MIN_COLUMNS); |
| |
| SizeInfo { |
| width, |
| height, |
| cell_width, |
| cell_height, |
| padding_x: padding_x.floor(), |
| padding_y: padding_y.floor(), |
| screen_lines, |
| columns, |
| } |
| } |
| |
| #[inline] |
| pub fn reserve_lines(&mut self, count: usize) { |
| self.screen_lines = max(self.screen_lines.saturating_sub(count), MIN_SCREEN_LINES); |
| } |
| |
| /// Check if coordinates are inside the terminal grid. |
| /// |
| /// The padding, message bar or search are not counted as part of the grid. |
| #[inline] |
| pub fn contains_point(&self, x: usize, y: usize) -> bool { |
| x <= (self.padding_x + self.columns as f32 * self.cell_width) as usize |
| && x > self.padding_x as usize |
| && y <= (self.padding_y + self.screen_lines as f32 * self.cell_height) as usize |
| && y > self.padding_y as usize |
| } |
| |
| #[inline] |
| pub fn width(&self) -> f32 { |
| self.width |
| } |
| |
| #[inline] |
| pub fn height(&self) -> f32 { |
| self.height |
| } |
| |
| #[inline] |
| pub fn cell_width(&self) -> f32 { |
| self.cell_width |
| } |
| |
| #[inline] |
| pub fn cell_height(&self) -> f32 { |
| self.cell_height |
| } |
| |
| #[inline] |
| pub fn padding_x(&self) -> f32 { |
| self.padding_x |
| } |
| |
| #[inline] |
| pub fn padding_y(&self) -> f32 { |
| self.padding_y |
| } |
| |
| /// Calculate padding to spread it evenly around the terminal content. |
| #[inline] |
| fn dynamic_padding(padding: f32, dimension: f32, cell_dimension: f32) -> f32 { |
| padding + ((dimension - 2. * padding) % cell_dimension) / 2. |
| } |
| } |
| |
| impl Dimensions for SizeInfo { |
| #[inline] |
| fn columns(&self) -> usize { |
| self.columns |
| } |
| |
| #[inline] |
| fn screen_lines(&self) -> usize { |
| self.screen_lines |
| } |
| |
| #[inline] |
| fn total_lines(&self) -> usize { |
| self.screen_lines() |
| } |
| } |
| |
| pub struct Term<T> { |
| /// Terminal focus controlling the cursor shape. |
| pub is_focused: bool, |
| |
| /// Cursor for keyboard selection. |
| pub vi_mode_cursor: ViModeCursor, |
| |
| pub selection: Option<Selection>, |
| |
| /// 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<Cell>, |
| |
| /// 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<Cell>, |
| |
| /// 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<Line>, |
| |
| semantic_escape_chars: String, |
| |
| /// Modified terminal colors. |
| colors: Colors, |
| |
| /// Current style of the cursor. |
| cursor_style: Option<CursorStyle>, |
| |
| /// Default style for resetting the cursor. |
| default_cursor_style: CursorStyle, |
| |
| /// Style of the vi mode cursor. |
| vi_mode_cursor_style: Option<CursorStyle>, |
| |
| /// Proxy for sending events to the event loop. |
| event_proxy: T, |
| |
| /// Current title of the window. |
| title: Option<String>, |
| |
| /// Stack of saved window titles. When a title is popped from this stack, the `title` for the |
| /// term is set. |
| title_stack: Vec<Option<String>>, |
| |
| /// Information about cell dimensions. |
| cell_width: usize, |
| cell_height: usize, |
| } |
| |
| impl<T> Term<T> { |
| #[inline] |
| pub fn scroll_display(&mut self, scroll: Scroll) |
| where |
| T: EventListener, |
| { |
| 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 = min(viewport_end, max(viewport_start, *vi_cursor_line)); |
| self.vi_mode_recompute_selection(); |
| } |
| |
| pub fn new(config: &Config, size: SizeInfo, event_proxy: T) -> Term<T> { |
| let num_cols = size.columns; |
| let num_lines = size.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); |
| |
| 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, |
| cell_width: size.cell_width as usize, |
| cell_height: size.cell_height as usize, |
| } |
| } |
| |
| 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); |
| } |
| } |
| |
| /// Convert the active selection to a String. |
| pub fn selection_to_string(&self) -> Option<String> { |
| 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<Column>, |
| include_wrapped_wide: bool, |
| ) -> String { |
| let mut text = String::new(); |
| |
| let grid_line = &self.grid[line]; |
| let line_length = 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<Cell> { |
| &self.grid |
| } |
| |
| /// Mutable access for swapping out the grid during tests. |
| #[cfg(test)] |
| pub fn grid_mut(&mut self) -> &mut Grid<Cell> { |
| &mut self.grid |
| } |
| |
| /// Resize terminal to new dimensions. |
| pub fn resize(&mut self, size: SizeInfo) { |
| self.cell_width = size.cell_width as usize; |
| self.cell_height = size.cell_height as usize; |
| |
| 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 = min(0, num_lines as i32 - self.grid.cursor.point.line.0 - 1); |
| delta = min(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 = max(min(vi_point.line, viewport_bottom), viewport_top); |
| self.vi_mode_cursor.point.column = min(vi_point.column, self.last_column()); |
| |
| // Reset scrolling region. |
| self.scroll_region = Line(0)..Line(self.screen_lines() as i32); |
| } |
| |
| /// 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; |
| } |
| |
| /// 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 = min(lines, (self.scroll_region.end - self.scroll_region.start).0 as usize); |
| lines = 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 = min(*line + lines, region.end - 1); |
| } |
| |
| // Scroll between origin and bottom |
| self.grid.scroll_down(®ion, lines); |
| } |
| |
| /// 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 = 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 = max(*line - lines, top); |
| } |
| } |
| |
| 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(..); |
| } |
| |
| #[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 = 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 |
| } |
| } |
| |
| /// 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.grid.cursor.point.line += 1; |
| } |
| |
| self.grid.cursor.point.column = Column(0); |
| self.grid.cursor.input_needs_wrap = false; |
| } |
| |
| /// 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 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.drop_extra(); |
| |
| cursor_cell.c = c; |
| cursor_cell.fg = fg; |
| cursor_cell.bg = bg; |
| cursor_cell.flags = flags; |
| } |
| } |
| |
| impl<T> Dimensions for Term<T> { |
| #[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<T: EventListener> Handler for Term<T> { |
| /// 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'; |
| } |
| } |
| } |
| |
| #[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.grid.cursor.point.line = max(min(line + y_offset, max_y), Line(0)); |
| self.grid.cursor.point.column = min(col, self.last_column()); |
| 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 = 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; |
| 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 = self.last_column(); |
| self.grid.cursor.point.column = min(self.grid.cursor.point.column + cols, last_column); |
| self.grid.cursor.input_needs_wrap = false; |
| } |
| |
| #[inline] |
| fn move_backward(&mut self, cols: Column) { |
| trace!("Moving backward: {}", cols); |
| self.grid.cursor.point.column = |
| Column(self.grid.cursor.point.column.saturating_sub(cols.0)); |
| self.grid.cursor.input_needs_wrap = false; |
| } |
| |
| #[inline] |
| fn identify_terminal(&mut self, intermediate: Option<char>) { |
| 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) { |
| self.grid.cursor.point.column -= 1; |
| self.grid.cursor.input_needs_wrap = false; |
| } |
| } |
| |
| /// Carriage return. |
| #[inline] |
| fn carriage_return(&mut self) { |
| trace!("Carriage return"); |
| self.grid.cursor.point.column = Column(0); |
| 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.grid.cursor.point.line += 1; |
| } |
| } |
| |
| /// 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 = 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 = 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; |
| 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 = min(count, columns); |
| |
| let start = cursor.point.column.0; |
| let end = min(start + count, columns - 1); |
| let num_cells = columns - end; |
| |
| let line = cursor.point.line; |
| 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); |
| |
| 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; |
| } |
| } |
| |
| #[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.grid.cursor = self.grid.saved_cursor.clone(); |
| } |
| |
| #[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 row = &mut self.grid[point.line]; |
| |
| match mode { |
| ansi::LineClearMode::Right => { |
| for cell in &mut row[point.column..] { |
| *cell = bg.into(); |
| } |
| }, |
| ansi::LineClearMode::Left => { |
| for cell in &mut row[..=point.column] { |
| *cell = bg.into(); |
| } |
| }, |
| ansi::LineClearMode::All => { |
| for cell in &mut row[..] { |
| *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); |
| self.colors[index] = Some(color); |
| } |
| |
| /// Write a foreground/background color escape sequence with the current color. |
| #[inline] |
| fn dynamic_color_sequence(&mut self, code: u8, index: usize, terminator: &str) { |
| trace!("Requested write of escape sequence for color code {}: color[{}]", code, 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}", |
| code, 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); |
| 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 = 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 => (), |
| } |
| } |
| |
| #[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; |
| |
| // Preserve vi mode across resets. |
| self.mode &= TermMode::VI; |
| self.mode.insert(TermMode::default()); |
| |
| self.event_proxy.send_event(Event::CursorBlinkingChange); |
| } |
| |
| #[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.grid.cursor.point.line = max(self.grid.cursor.point.line - 1, Line(0)); |
| } |
| } |
| |
| /// 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::Reset => { |
| cursor.template.fg = Color::Named(NamedColor::Foreground); |
| cursor.template.bg = Color::Named(NamedColor::Background); |
| cursor.template.flags = Flags::empty(); |
| }, |
| 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::DOUBLE_UNDERLINE); |
| cursor.template.flags.insert(Flags::UNDERLINE); |
| }, |
| Attr::DoubleUnderline => { |
| cursor.template.flags.remove(Flags::UNDERLINE); |
| cursor.template.flags.insert(Flags::DOUBLE_UNDERLINE); |
| }, |
| Attr::CancelUnderline => { |
| cursor.template.flags.remove(Flags::UNDERLINE | Flags::DOUBLE_UNDERLINE); |
| }, |
| 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), |
| 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<usize>) { |
| // 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 = min(start, screen_lines); |
| self.scroll_region.end = 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<CursorStyle>) { |
| 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<String>) { |
| 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) { |
| let width = self.cell_width * self.columns(); |
| let height = self.cell_height * self.screen_lines(); |
| let text = format!("\x1b[4;{};{}t", height, width); |
| self.event_proxy.send_event(Event::PtyWrite(text)); |
| } |
| |
| #[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::<usize>().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<bool>, |
| } |
| |
| 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<Column> for TabStops { |
| type Output = bool; |
| |
| fn index(&self, index: Column) -> &bool { |
| &self.tabs[index.0] |
| } |
| } |
| |
| impl IndexMut<Column> for TabStops { |
| fn index_mut(&mut self, index: Column) -> &mut bool { |
| self.tabs.index_mut(index.0) |
| } |
| } |
| |
| /// Terminal cursor rendering information. |
| #[derive(Copy, Clone)] |
| pub struct RenderableCursor { |
| pub shape: CursorShape, |
| pub point: Point, |
| } |
| |
| impl RenderableCursor { |
| fn new<T>(term: &Term<T>) -> 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<SelectionRange>, |
| pub cursor: RenderableCursor, |
| pub display_offset: usize, |
| pub colors: &'a color::Colors, |
| pub mode: TermMode, |
| } |
| |
| impl<'a> RenderableContent<'a> { |
| fn new<T>(term: &'a Term<T>) -> 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 unicode_width::UnicodeWidthChar; |
| |
| use crate::config::Config; |
| use crate::index::Column; |
| |
| /// 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 = SizeInfo::new(num_cols as f32, lines.len() as f32, 1., 1., 0., 0., false); |
| 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}; |
| |
| #[test] |
| fn scroll_display_page_up() { |
| let size = SizeInfo::new(5., 10., 1.0, 1.0, 0.0, 0.0, false); |
| 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 = SizeInfo::new(5., 10., 1.0, 1.0, 0.0, 0.0, false); |
| 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 = SizeInfo::new(5., 5., 1.0, 1.0, 0.0, 0.0, false); |
| 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 = SizeInfo::new(5., 3., 1.0, 1.0, 0.0, 0.0, false); |
| let mut term = Term::new(&Config::default(), size, ()); |
| let mut grid: Grid<Cell> = 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 = SizeInfo::new(5., 1., 1.0, 1.0, 0.0, 0.0, false); |
| let mut term = Term::new(&Config::default(), size, ()); |
| let mut grid: Grid<Cell> = 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 = SizeInfo::new(5., 5., 1.0, 1.0, 0.0, 0.0, false); |
| 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<Cell> = Grid::new(24, 80, 0); |
| let serialized = serde_json::to_string(&grid).expect("ser"); |
| let deserialized = serde_json::from_str::<Grid<Cell>>(&serialized).expect("de"); |
| |
| assert_eq!(deserialized, grid); |
| } |
| |
| #[test] |
| fn input_line_drawing_character() { |
| let size = SizeInfo::new(21.0, 51.0, 3.0, 3.0, 0.0, 0.0, false); |
| 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 = SizeInfo::new(10.0, 20.0, 1.0, 1.0, 0.0, 0.0, false); |
| 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 = SizeInfo::new(10.0, 20.0, 1.0, 1.0, 0.0, 0.0, false); |
| 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 = SizeInfo::new(10.0, 20.0, 1.0, 1.0, 0.0, 0.0, false); |
| 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 = SizeInfo::new(10.0, 20.0, 1.0, 1.0, 0.0, 0.0, false); |
| 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 = SizeInfo::new(21.0, 51.0, 3.0, 3.0, 0.0, 0.0, false); |
| 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 = SizeInfo::new(5., 10., 1.0, 1.0, 0.0, 0.0, false); |
| 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 = SizeInfo::new(100.0, 10.0, 1.0, 1.0, 0.0, 0.0, false); |
| 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 = SizeInfo::new(100.0, 10.0, 1.0, 1.0, 0.0, 0.0, false); |
| 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 = SizeInfo::new(100.0, 10.0, 1.0, 1.0, 0.0, 0.0, false); |
| 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 = SizeInfo::new(100.0, 10.0, 1.0, 1.0, 0.0, 0.0, false); |
| 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 window_title() { |
| let size = SizeInfo::new(21.0, 51.0, 3.0, 3.0, 0.0, 0.0, false); |
| 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); |
| } |
| } |