alacritty/alacritty_terminal/src/term/mod.rs

2991 lines
100 KiB
Rust
Raw Normal View History

2020-05-05 22:50:23 +00:00
//! 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};
2019-03-30 16:48:36 +00:00
use unicode_width::UnicodeWidthChar;
2017-03-02 06:24:37 +00:00
2019-03-30 16:48:36 +00:00
use crate::ansi::{
self, Attr, CharsetIndex, Color, CursorShape, CursorStyle, Handler, NamedColor, StandardCharset,
2019-03-30 16:48:36 +00:00
};
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<Point<usize>> {
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<usize>) -> 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::Item> {
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<LineDamageBounds>,
/// Old terminal cursor point.
last_cursor: Point,
/// Last Vi cursor point.
last_vi_cursor_point: Option<Point<usize>>,
/// Old selection range.
last_selection: Option<SelectionRange>,
}
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<usize>) {
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<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 damaged cells.
damage: TermDamageState,
}
impl<T> Term<T> {
#[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();
}
2018-02-17 01:33:32 +00:00
}
pub fn new<D: Dimensions>(config: &Config, dimensions: &D, event_proxy: T) -> Term<T> {
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(),
Replace serde's derive with custom proc macro This replaces the existing `Deserialize` derive from serde with a `ConfigDeserialize` derive. The goal of this new proc macro is to allow a more error-friendly deserialization for the Alacritty configuration file without having to manage a lot of boilerplate code inside the configuration modules. The first part of the derive macro is for struct deserialization. This takes structs which have `Default` implemented and will only replace fields which can be successfully deserialized. Otherwise the `log` crate is used for printing errors. Since this deserialization takes the default value from the struct instead of the value, it removes the necessity for creating new types just to implement `Default` on them for deserialization. Additionally, the struct deserialization also checks for `Option` values and makes sure that explicitly specifying `none` as text literal is allowed for all options. The other part of the derive macro is responsible for deserializing enums. While only enums with Unit variants are supported, it will automatically implement a deserializer for these enums which accepts any form of capitalization. Since this custom derive prevents us from using serde's attributes on fields, some of the attributes have been reimplemented for `ConfigDeserialize`. These include `#[config(flatten)]`, `#[config(skip)]` and `#[config(alias = "alias)]`. The flatten attribute is currently limited to at most one per struct. Additionally the `#[config(deprecated = "optional message")]` attribute allows easily defining uniform deprecation messages for fields on structs.
2020-12-21 02:44:38 +00:00
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<SelectionRange>) -> 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,
{
Replace serde's derive with custom proc macro This replaces the existing `Deserialize` derive from serde with a `ConfigDeserialize` derive. The goal of this new proc macro is to allow a more error-friendly deserialization for the Alacritty configuration file without having to manage a lot of boilerplate code inside the configuration modules. The first part of the derive macro is for struct deserialization. This takes structs which have `Default` implemented and will only replace fields which can be successfully deserialized. Otherwise the `log` crate is used for printing errors. Since this deserialization takes the default value from the struct instead of the value, it removes the necessity for creating new types just to implement `Default` on them for deserialization. Additionally, the struct deserialization also checks for `Option` values and makes sure that explicitly specifying `none` as text literal is allowed for all options. The other part of the derive macro is responsible for deserializing enums. While only enums with Unit variants are supported, it will automatically implement a deserializer for these enums which accepts any form of capitalization. Since this custom derive prevents us from using serde's attributes on fields, some of the attributes have been reimplemented for `ConfigDeserialize`. These include `#[config(flatten)]`, `#[config(skip)]` and `#[config(alias = "alias)]`. The flatten attribute is currently limited to at most one per struct. Additionally the `#[config(deprecated = "optional message")]` attribute allows easily defining uniform deprecation messages for fields on structs.
2020-12-21 02:44:38 +00:00
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<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 = cmp::min(grid_line.line_length(), cols.end + 1);
2020-05-05 22:50:23 +00:00
// 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];
2020-05-05 22:50:23 +00:00
// 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) {
2020-05-05 22:50:23 +00:00
// Push cells primary character.
text.push(cell.c);
2020-05-05 22:50:23 +00:00
// 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');
}
2020-05-05 22:50:23 +00:00
// 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)
}
2020-05-05 22:50:23 +00:00
/// 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
}
2020-05-05 22:50:23 +00:00
/// Mutable access for swapping out the grid during tests.
#[cfg(test)]
pub fn grid_mut(&mut self) -> &mut Grid<Cell> {
&mut self.grid
}
2020-05-05 22:50:23 +00:00
/// Resize terminal to new dimensions.
pub fn resize<S: Dimensions>(&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;
2021-07-12 01:22:49 +00:00
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();
}
2020-05-05 22:50:23 +00:00
/// 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, &region, -(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(&region, 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, &region, lines as i32));
self.grid.scroll_up(&region, 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,
{
2020-05-05 22:50:23 +00:00
// Setting 132 column font makes no sense, but run the other side effects.
// Clear scrolling region.
self.set_scrolling_region(1, None);
2020-05-05 22:50:23 +00:00
// 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,
{
2020-05-05 22:50:23 +00:00
// Require vi mode to be active.
if !self.mode.contains(TermMode::VI) {
return;
}
2020-05-05 22:50:23 +00:00
// 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;
}
2022-01-15 03:35:05 +00:00
#[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<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> {
2020-05-05 22:50:23 +00:00
/// A character to be displayed.
#[inline(never)]
fn input(&mut self, c: char) {
2020-05-05 22:50:23 +00:00
// Number of cells the char will occupy.
let width = match c.width() {
Some(width) => width,
None => return,
};
2020-05-05 22:50:23 +00:00
// 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;
}
2020-05-05 22:50:23 +00:00
// Move cursor to next line.
if self.grid.cursor.input_needs_wrap {
self.wrapline();
}
2020-05-05 22:50:23 +00:00
// 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;
}
}
2020-05-05 22:50:23 +00:00
// 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);
2017-03-02 06:24:37 +00:00
2020-05-05 22:50:23 +00:00
// 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);
2019-04-19 20:56:11 +00:00
let (y_offset, max_y) = if self.mode.contains(TermMode::ORIGIN) {
(self.scroll_region.start, self.scroll_region.end - 1)
2017-04-19 05:59:24 +00:00
} else {
(Line(0), self.bottommost_line())
2017-04-19 05:59:24 +00:00
};
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<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"),
}
}
2017-05-07 20:08:23 +00:00
#[inline]
fn device_status(&mut self, arg: usize) {
trace!("Reporting device status: {}", arg);
2017-05-08 14:50:34 +00:00
match arg {
5 => {
let text = String::from("\x1b[0n");
self.event_proxy.send_event(Event::PtyWrite(text));
2017-05-08 14:50:34 +00:00
},
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));
2017-05-08 14:50:34 +00:00
},
_ => debug!("unknown device status query: {}", arg),
};
2017-05-07 20:08:23 +00:00
}
#[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) {
2020-05-05 22:50:23 +00:00
// 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;
2017-04-04 12:08:10 +00:00
if self.tabs[self.grid.cursor.point.column] {
2017-04-04 12:08:10 +00:00
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);
}
}
2020-05-05 22:50:23 +00:00
/// 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;
}
2020-05-05 22:50:23 +00:00
/// 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();
}
}
2020-05-05 22:50:23 +00:00
/// 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");
}
2020-05-05 22:50:23 +00:00
/// Run LF/NL.
2017-04-18 17:41:11 +00:00
///
/// LF/NL mode has some interesting history. According to ECMA-48 4th
/// edition, in LINE FEED mode,
///
2017-10-30 15:03:58 +00:00
/// > The execution of the formatter functions LINE FEED (LF), FORM FEED
2017-04-18 17:41:11 +00:00
/// (FF), LINE TABULATION (VT) cause only movement of the active position in
/// the direction of the line progression.
///
/// In NEW LINE mode,
///
2017-10-30 15:03:58 +00:00
/// > The execution of the formatter functions LINE FEED (LF), FORM FEED
2017-04-18 17:41:11 +00:00
/// (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) {
2017-04-18 17:41:11 +00:00
self.linefeed();
2019-04-19 20:56:11 +00:00
if self.mode.contains(TermMode::LINE_FEED_NEW_LINE) {
2017-04-18 17:41:11 +00:00
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()));
2020-05-05 22:50:23 +00:00
// 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;
2020-05-05 22:50:23 +00:00
// 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
2016-12-17 06:48:04 +00:00
// 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));
}
2020-05-05 22:50:23 +00:00
/// 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
)
}),
));
}
2020-05-05 22:50:23 +00:00
/// Reset the indexed color to original value.
#[inline]
fn reset_color(&mut self, index: usize) {
2019-04-28 21:42:43 +00:00
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]) {
2019-11-11 00:12:14 +00:00
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));
2019-11-11 00:12:14 +00:00
}
}
}
/// Load data from clipboard.
2019-11-11 00:12:14 +00:00
#[inline]
fn clipboard_load(&mut self, clipboard: u8, terminator: &str) {
2019-11-11 00:12:14 +00:00
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 {
2017-02-06 21:13:25 +00:00
ansi::ClearMode::Above => {
let cursor = self.grid.cursor.point;
2020-05-05 22:50:23 +00:00
// If clearing more than one line.
if cursor.line > 1 {
2020-05-05 22:50:23 +00:00
// Fully clear all lines before the current line.
self.grid.reset_region(..cursor.line);
2017-02-06 21:13:25 +00:00
}
2020-05-05 22:50:23 +00:00
// 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();
2017-02-06 21:13:25 +00:00
}
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();
2019-03-30 16:48:36 +00:00
},
}
}
2020-05-05 22:50:23 +00:00
/// 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<Hyperlink>) {
trace!("Setting hyperlink: {:?}", hyperlink);
self.grid.cursor.template.set_hyperlink(hyperlink);
}
2020-05-05 22:50:23 +00:00
/// 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);
2019-03-30 16:48:36 +00:00
},
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);
2019-03-30 16:48:36 +00:00
},
}
}
#[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();
}
},
2019-04-19 20:56:11 +00:00
ansi::Mode::ShowCursor => self.mode.insert(TermMode::SHOW_CURSOR),
ansi::Mode::CursorKeys => self.mode.insert(TermMode::APP_CURSOR),
2020-05-05 22:50:23 +00:00
// Mouse protocols are mutually exclusive.
ansi::Mode::ReportMouseClicks => {
self.mode.remove(TermMode::MOUSE_MODE);
2019-04-19 20:56:11 +00:00
self.mode.insert(TermMode::MOUSE_REPORT_CLICK);
self.event_proxy.send_event(Event::MouseCursorDirty);
},
ansi::Mode::ReportCellMouseMotion => {
self.mode.remove(TermMode::MOUSE_MODE);
2019-04-19 20:56:11 +00:00
self.mode.insert(TermMode::MOUSE_DRAG);
self.event_proxy.send_event(Event::MouseCursorDirty);
},
ansi::Mode::ReportAllMouseMotion => {
self.mode.remove(TermMode::MOUSE_MODE);
2019-04-19 20:56:11 +00:00
self.mode.insert(TermMode::MOUSE_MOTION);
self.event_proxy.send_event(Event::MouseCursorDirty);
},
2019-04-19 20:56:11 +00:00
ansi::Mode::ReportFocusInOut => self.mode.insert(TermMode::FOCUS_IN_OUT),
ansi::Mode::BracketedPaste => self.mode.insert(TermMode::BRACKETED_PASTE),
2020-05-05 22:50:23 +00:00
// 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),
2019-04-19 20:56:11 +00:00
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(),
2020-05-05 22:50:23 +00:00
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);
2019-03-30 16:48:36 +00:00
},
}
}
#[inline]
2019-03-30 16:48:36 +00:00
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();
}
},
2019-04-19 20:56:11 +00:00
ansi::Mode::ShowCursor => self.mode.remove(TermMode::SHOW_CURSOR),
ansi::Mode::CursorKeys => self.mode.remove(TermMode::APP_CURSOR),
ansi::Mode::ReportMouseClicks => {
2019-04-19 20:56:11 +00:00
self.mode.remove(TermMode::MOUSE_REPORT_CLICK);
self.event_proxy.send_event(Event::MouseCursorDirty);
},
ansi::Mode::ReportCellMouseMotion => {
2019-04-19 20:56:11 +00:00
self.mode.remove(TermMode::MOUSE_DRAG);
self.event_proxy.send_event(Event::MouseCursorDirty);
},
ansi::Mode::ReportAllMouseMotion => {
2019-04-19 20:56:11 +00:00
self.mode.remove(TermMode::MOUSE_MOTION);
self.event_proxy.send_event(Event::MouseCursorDirty);
},
2019-04-19 20:56:11 +00:00
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),
2019-04-19 20:56:11 +00:00
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);
2019-03-30 16:48:36 +00:00
},
}
}
#[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 = 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");
2019-04-19 20:56:11 +00:00
self.mode.insert(TermMode::APP_KEYPAD);
}
#[inline]
fn unset_keypad_application_mode(&mut self) {
trace!("Unsetting keypad application mode");
2019-04-19 20:56:11 +00:00
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) {
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::<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 {
2019-03-30 16:48:36 +00:00
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, PartialEq, Eq)]
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 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;
2019-03-30 16:48:36 +00:00
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<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 = TermSize::new(5, 1);
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 = 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")));
}
2020-05-05 22:50:23 +00:00
/// 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");
2019-03-30 16:48:36 +00:00
let deserialized = serde_json::from_str::<Grid<Cell>>(&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));
2019-03-30 16:48:36 +00:00
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, ());
2020-05-05 22:50:23 +00:00
// Add one line of scrollback.
term.grid.scroll_up(&(Line(0)..Line(1)), 1);
2020-05-05 22:50:23 +00:00
// Clear the history.
term.clear_screen(ansi::ClearMode::Saved);
2020-05-05 22:50:23 +00:00
// Make sure that scrolling does not change the grid.
let mut scrolled_grid = term.grid.clone();
scrolled_grid.scroll_display(Scroll::Top);
2020-05-05 22:50:23 +00:00
// 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, ());
2020-05-05 22:50:23 +00:00
// 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)));
2020-05-05 22:50:23 +00:00
// 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, ());
2020-05-05 22:50:23 +00:00
// 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)));
2020-05-05 22:50:23 +00:00
// Enter alt screen.
term.set_mode(ansi::Mode::SwapScreenAndSetRestoreCursor);
2020-05-05 22:50:23 +00:00
// Increase visible lines.
size.screen_lines = 30;
term.resize(size);
2020-05-05 22:50:23 +00:00
// 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, ());
2020-05-05 22:50:23 +00:00
// 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)));
2020-05-05 22:50:23 +00:00
// 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, ());
2020-05-05 22:50:23 +00:00
// 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)));
2020-05-05 22:50:23 +00:00
// Enter alt screen.
term.set_mode(ansi::Mode::SwapScreenAndSetRestoreCursor);
2020-05-05 22:50:23 +00:00
// Increase visible lines.
size.screen_lines = 5;
term.resize(size);
2020-05-05 22:50:23 +00:00
// 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, ());
2020-05-05 22:50:23 +00:00
// Title None by default.
assert_eq!(term.title, None);
2020-05-05 22:50:23 +00:00
// Title can be set.
term.set_title(Some("Test".into()));
assert_eq!(term.title, Some("Test".into()));
2020-05-05 22:50:23 +00:00
// 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()));
2020-05-05 22:50:23 +00:00
// 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());
2020-05-05 22:50:23 +00:00
// Title stack doesn't grow infinitely.
for _ in 0..4097 {
term.push_title();
}
assert_eq!(term.title_stack.len(), 4096);
2020-05-05 22:50:23 +00:00
// 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());
2020-05-05 22:50:23 +00:00
// 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);
2020-05-05 22:50:23 +00:00
// 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);
}
}