alacritty/alacritty_terminal/src/term/mod.rs

2991 lines
100 KiB
Rust

//! Exports the `Term` type which is a high-level API for the Grid.
use std::ops::{Index, IndexMut, Range};
use std::sync::Arc;
use std::{cmp, mem, ptr, slice, str};
use bitflags::bitflags;
use log::{debug, trace};
use unicode_width::UnicodeWidthChar;
use crate::ansi::{
self, Attr, CharsetIndex, Color, CursorShape, CursorStyle, Handler, NamedColor, StandardCharset,
};
use crate::config::Config;
use crate::event::{Event, EventListener};
use crate::grid::{Dimensions, Grid, GridIterator, Scroll};
use crate::index::{self, Boundary, Column, Direction, Line, Point, Side};
use crate::selection::{Selection, SelectionRange, SelectionType};
use crate::term::cell::{Cell, Flags, Hyperlink, LineLength};
use crate::term::color::{Colors, Rgb};
use crate::vi_mode::{ViModeCursor, ViMotion};
pub mod cell;
pub mod color;
pub mod search;
/// Minimum number of columns.
///
/// A minimum of 2 is necessary to hold fullwidth unicode characters.
pub const MIN_COLUMNS: usize = 2;
/// Minimum number of visible lines.
pub const MIN_SCREEN_LINES: usize = 1;
/// Max size of the window title stack.
const TITLE_STACK_MAX_DEPTH: usize = 4096;
/// Default tab interval, corresponding to terminfo `it` value.
const INITIAL_TABSTOPS: usize = 8;
bitflags! {
pub struct TermMode: u32 {
const NONE = 0;
const SHOW_CURSOR = 0b0000_0000_0000_0000_0001;
const APP_CURSOR = 0b0000_0000_0000_0000_0010;
const APP_KEYPAD = 0b0000_0000_0000_0000_0100;
const MOUSE_REPORT_CLICK = 0b0000_0000_0000_0000_1000;
const BRACKETED_PASTE = 0b0000_0000_0000_0001_0000;
const SGR_MOUSE = 0b0000_0000_0000_0010_0000;
const MOUSE_MOTION = 0b0000_0000_0000_0100_0000;
const LINE_WRAP = 0b0000_0000_0000_1000_0000;
const LINE_FEED_NEW_LINE = 0b0000_0000_0001_0000_0000;
const ORIGIN = 0b0000_0000_0010_0000_0000;
const INSERT = 0b0000_0000_0100_0000_0000;
const FOCUS_IN_OUT = 0b0000_0000_1000_0000_0000;
const ALT_SCREEN = 0b0000_0001_0000_0000_0000;
const MOUSE_DRAG = 0b0000_0010_0000_0000_0000;
const MOUSE_MODE = 0b0000_0010_0000_0100_1000;
const UTF8_MOUSE = 0b0000_0100_0000_0000_0000;
const ALTERNATE_SCROLL = 0b0000_1000_0000_0000_0000;
const VI = 0b0001_0000_0000_0000_0000;
const URGENCY_HINTS = 0b0010_0000_0000_0000_0000;
const ANY = u32::MAX;
}
}
impl Default for TermMode {
fn default() -> TermMode {
TermMode::SHOW_CURSOR
| TermMode::LINE_WRAP
| TermMode::ALTERNATE_SCROLL
| TermMode::URGENCY_HINTS
}
}
/// Convert a terminal point to a viewport relative point.
#[inline]
pub fn point_to_viewport(display_offset: usize, point: Point) -> Option<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();
}
}
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(),
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,
{
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);
// 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<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;
let viewport_top = Line(-(self.grid.display_offset() as i32));
let viewport_bottom = viewport_top + self.bottommost_line();
self.vi_mode_cursor.point.line =
cmp::max(cmp::min(vi_point.line, viewport_bottom), viewport_top);
self.vi_mode_cursor.point.column = cmp::min(vi_point.column, self.last_column());
// Reset scrolling region.
self.scroll_region = Line(0)..Line(self.screen_lines() as i32);
// Resize damage information.
self.damage.resize(num_cols, num_lines);
}
/// Active terminal modes.
#[inline]
pub fn mode(&self) -> &TermMode {
&self.mode
}
/// Swap primary and alternate screen buffer.
pub fn swap_alt(&mut self) {
if !self.mode.contains(TermMode::ALT_SCREEN) {
// Set alt screen cursor to the current primary screen cursor.
self.inactive_grid.cursor = self.grid.cursor.clone();
// Drop information about the primary screens saved cursor.
self.grid.saved_cursor = self.grid.cursor.clone();
// Reset alternate screen contents.
self.inactive_grid.reset_region(..);
}
mem::swap(&mut self.grid, &mut self.inactive_grid);
self.mode ^= TermMode::ALT_SCREEN;
self.selection = None;
self.mark_fully_damaged();
}
/// Scroll screen down.
///
/// Text moves down; clear at bottom
/// Expects origin to be in scroll range.
#[inline]
fn scroll_down_relative(&mut self, origin: Line, mut lines: usize) {
trace!("Scrolling down relative: origin={}, lines={}", origin, lines);
lines = cmp::min(lines, (self.scroll_region.end - self.scroll_region.start).0 as usize);
lines = cmp::min(lines, (self.scroll_region.end - origin).0 as usize);
let region = origin..self.scroll_region.end;
// Scroll selection.
self.selection =
self.selection.take().and_then(|s| s.rotate(self, &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,
{
// Setting 132 column font makes no sense, but run the other side effects.
// Clear scrolling region.
self.set_scrolling_region(1, None);
// Clear grid.
self.grid.reset_region(..);
self.mark_fully_damaged();
}
#[inline]
pub fn exit(&mut self)
where
T: EventListener,
{
self.event_proxy.send_event(Event::Exit);
}
/// Toggle the vi mode.
#[inline]
pub fn toggle_vi_mode(&mut self)
where
T: EventListener,
{
self.mode ^= TermMode::VI;
if self.mode.contains(TermMode::VI) {
let display_offset = self.grid.display_offset() as i32;
if self.grid.cursor.point.line > self.bottommost_line() - display_offset {
// Move cursor to top-left if terminal cursor is not visible.
let point = Point::new(Line(-display_offset), Column(0));
self.vi_mode_cursor = ViModeCursor::new(point);
} else {
// Reset vi mode cursor position to match primary cursor.
self.vi_mode_cursor = ViModeCursor::new(self.grid.cursor.point);
}
}
// Update UI about cursor blinking state changes.
self.event_proxy.send_event(Event::CursorBlinkingChange);
}
/// Move vi mode cursor.
#[inline]
pub fn vi_motion(&mut self, motion: ViMotion)
where
T: EventListener,
{
// Require vi mode to be active.
if !self.mode.contains(TermMode::VI) {
return;
}
// Move cursor.
self.vi_mode_cursor = self.vi_mode_cursor.motion(self, motion);
self.vi_mode_recompute_selection();
}
/// Move vi cursor to a point in the grid.
#[inline]
pub fn vi_goto_point(&mut self, point: Point)
where
T: EventListener,
{
// Move viewport to make point visible.
self.scroll_to_point(point);
// Move vi cursor to the point.
self.vi_mode_cursor.point = point;
self.vi_mode_recompute_selection();
}
/// Update the active selection to match the vi mode cursor position.
#[inline]
fn vi_mode_recompute_selection(&mut self) {
// Require vi mode to be active.
if !self.mode.contains(TermMode::VI) {
return;
}
// Update only if non-empty selection is present.
if let Some(selection) = self.selection.as_mut().filter(|s| !s.is_empty()) {
selection.update(self.vi_mode_cursor.point, Side::Left);
selection.include_all();
}
}
/// Scroll display to point if it is outside of viewport.
pub fn scroll_to_point(&mut self, point: Point)
where
T: EventListener,
{
let display_offset = self.grid.display_offset() as i32;
let screen_lines = self.grid.screen_lines() as i32;
if point.line < -display_offset {
let lines = point.line + display_offset;
self.scroll_display(Scroll::Delta(-lines.0));
} else if point.line >= (screen_lines - display_offset) {
let lines = point.line + display_offset - screen_lines + 1i32;
self.scroll_display(Scroll::Delta(-lines.0));
}
}
/// Jump to the end of a wide cell.
pub fn expand_wide(&self, mut point: Point, direction: Direction) -> Point {
let flags = self.grid[point.line][point.column].flags;
match direction {
Direction::Right if flags.contains(Flags::LEADING_WIDE_CHAR_SPACER) => {
point.column = Column(1);
point.line += 1;
},
Direction::Right if flags.contains(Flags::WIDE_CHAR) => {
point.column = cmp::min(point.column + 1, self.last_column());
},
Direction::Left if flags.intersects(Flags::WIDE_CHAR | Flags::WIDE_CHAR_SPACER) => {
if flags.contains(Flags::WIDE_CHAR_SPACER) {
point.column -= 1;
}
let prev = point.sub(self, Boundary::Grid, 1);
if self.grid[prev].flags.contains(Flags::LEADING_WIDE_CHAR_SPACER) {
point = prev;
}
},
_ => (),
}
point
}
#[inline]
pub fn semantic_escape_chars(&self) -> &str {
&self.semantic_escape_chars
}
/// Active terminal cursor style.
///
/// While vi mode is active, this will automatically return the vi mode cursor style.
#[inline]
pub fn cursor_style(&self) -> CursorStyle {
let cursor_style = self.cursor_style.unwrap_or(self.default_cursor_style);
if self.mode.contains(TermMode::VI) {
self.vi_mode_cursor_style.unwrap_or(cursor_style)
} else {
cursor_style
}
}
pub fn colors(&self) -> &Colors {
&self.colors
}
/// Insert a linebreak at the current cursor position.
#[inline]
fn wrapline(&mut self)
where
T: EventListener,
{
if !self.mode.contains(TermMode::LINE_WRAP) {
return;
}
trace!("Wrapping input");
self.grid.cursor_cell().flags.insert(Flags::WRAPLINE);
if self.grid.cursor.point.line + 1 >= self.scroll_region.end {
self.linefeed();
} else {
self.damage_cursor();
self.grid.cursor.point.line += 1;
}
self.grid.cursor.point.column = Column(0);
self.grid.cursor.input_needs_wrap = false;
self.damage_cursor();
}
/// Write `c` to the cell at the cursor position.
#[inline(always)]
fn write_at_cursor(&mut self, c: char) {
let c = self.grid.cursor.charsets[self.active_charset].map(c);
let fg = self.grid.cursor.template.fg;
let bg = self.grid.cursor.template.bg;
let flags = self.grid.cursor.template.flags;
let extra = self.grid.cursor.template.extra.clone();
let mut cursor_cell = self.grid.cursor_cell();
// Clear all related cells when overwriting a fullwidth cell.
if cursor_cell.flags.intersects(Flags::WIDE_CHAR | Flags::WIDE_CHAR_SPACER) {
// Remove wide char and spacer.
let wide = cursor_cell.flags.contains(Flags::WIDE_CHAR);
let point = self.grid.cursor.point;
if wide && point.column < self.last_column() {
self.grid[point.line][point.column + 1].flags.remove(Flags::WIDE_CHAR_SPACER);
} else if point.column > 0 {
self.grid[point.line][point.column - 1].clear_wide();
}
// Remove leading spacers.
if point.column <= 1 && point.line != self.topmost_line() {
let column = self.last_column();
self.grid[point.line - 1i32][column].flags.remove(Flags::LEADING_WIDE_CHAR_SPACER);
}
cursor_cell = self.grid.cursor_cell();
}
cursor_cell.c = c;
cursor_cell.fg = fg;
cursor_cell.bg = bg;
cursor_cell.flags = flags;
cursor_cell.extra = extra;
}
#[inline]
fn damage_cursor(&mut self) {
// The normal cursor coordinates are always in viewport.
let point =
Point::new(self.grid.cursor.point.line.0 as usize, self.grid.cursor.point.column);
self.damage.damage_point(point);
}
}
impl<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';
}
}
self.mark_fully_damaged();
}
#[inline]
fn goto(&mut self, line: Line, col: Column) {
trace!("Going to: line={}, col={}", line, col);
let (y_offset, max_y) = if self.mode.contains(TermMode::ORIGIN) {
(self.scroll_region.start, self.scroll_region.end - 1)
} else {
(Line(0), self.bottommost_line())
};
self.damage_cursor();
self.grid.cursor.point.line = cmp::max(cmp::min(line + y_offset, max_y), Line(0));
self.grid.cursor.point.column = cmp::min(col, self.last_column());
self.damage_cursor();
self.grid.cursor.input_needs_wrap = false;
}
#[inline]
fn goto_line(&mut self, line: Line) {
trace!("Going to line: {}", line);
self.goto(line, self.grid.cursor.point.column)
}
#[inline]
fn goto_col(&mut self, col: Column) {
trace!("Going to column: {}", col);
self.goto(self.grid.cursor.point.line, col)
}
#[inline]
fn insert_blank(&mut self, count: usize) {
let cursor = &self.grid.cursor;
let bg = cursor.template.bg;
// Ensure inserting within terminal bounds
let count = cmp::min(count, self.columns() - cursor.point.column.0);
let source = cursor.point.column;
let destination = cursor.point.column.0 + count;
let num_cells = self.columns() - destination;
let line = cursor.point.line;
self.damage.damage_line(line.0 as usize, 0, self.columns() - 1);
let row = &mut self.grid[line][..];
for offset in (0..num_cells).rev() {
row.swap(destination + offset, source.0 + offset);
}
// Cells were just moved out toward the end of the line;
// fill in between source and dest with blanks.
for cell in &mut row[source.0..destination] {
*cell = bg.into();
}
}
#[inline]
fn move_up(&mut self, lines: usize) {
trace!("Moving up: {}", lines);
self.goto(self.grid.cursor.point.line - lines, self.grid.cursor.point.column)
}
#[inline]
fn move_down(&mut self, lines: usize) {
trace!("Moving down: {}", lines);
self.goto(self.grid.cursor.point.line + lines, self.grid.cursor.point.column)
}
#[inline]
fn move_forward(&mut self, cols: Column) {
trace!("Moving forward: {}", cols);
let last_column = cmp::min(self.grid.cursor.point.column + cols, self.last_column());
let cursor_line = self.grid.cursor.point.line.0 as usize;
self.damage.damage_line(cursor_line, self.grid.cursor.point.column.0, last_column.0);
self.grid.cursor.point.column = last_column;
self.grid.cursor.input_needs_wrap = false;
}
#[inline]
fn move_backward(&mut self, cols: Column) {
trace!("Moving backward: {}", cols);
let column = self.grid.cursor.point.column.saturating_sub(cols.0);
let cursor_line = self.grid.cursor.point.line.0 as usize;
self.damage.damage_line(cursor_line, column, self.grid.cursor.point.column.0);
self.grid.cursor.point.column = Column(column);
self.grid.cursor.input_needs_wrap = false;
}
#[inline]
fn identify_terminal(&mut self, intermediate: Option<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) {
let line = self.grid.cursor.point.line.0 as usize;
let column = self.grid.cursor.point.column.0 as usize;
self.grid.cursor.point.column -= 1;
self.grid.cursor.input_needs_wrap = false;
self.damage.damage_line(line, column - 1, column);
}
}
/// Carriage return.
#[inline]
fn carriage_return(&mut self) {
trace!("Carriage return");
let new_col = 0;
let line = self.grid.cursor.point.line.0 as usize;
self.damage_line(line, new_col, self.grid.cursor.point.column.0);
self.grid.cursor.point.column = Column(new_col);
self.grid.cursor.input_needs_wrap = false;
}
/// Linefeed.
#[inline]
fn linefeed(&mut self) {
trace!("Linefeed");
let next = self.grid.cursor.point.line + 1;
if next == self.scroll_region.end {
self.scroll_up(1);
} else if next < self.screen_lines() {
self.damage_cursor();
self.grid.cursor.point.line += 1;
self.damage_cursor();
}
}
/// Set current position as a tabstop.
#[inline]
fn bell(&mut self) {
trace!("Bell");
self.event_proxy.send_event(Event::Bell);
}
#[inline]
fn substitute(&mut self) {
trace!("[unimplemented] Substitute");
}
/// Run LF/NL.
///
/// LF/NL mode has some interesting history. According to ECMA-48 4th
/// edition, in LINE FEED mode,
///
/// > The execution of the formatter functions LINE FEED (LF), FORM FEED
/// (FF), LINE TABULATION (VT) cause only movement of the active position in
/// the direction of the line progression.
///
/// In NEW LINE mode,
///
/// > The execution of the formatter functions LINE FEED (LF), FORM FEED
/// (FF), LINE TABULATION (VT) cause movement to the line home position on
/// the following line, the following form, etc. In the case of LF this is
/// referred to as the New Line (NL) option.
///
/// Additionally, ECMA-48 4th edition says that this option is deprecated.
/// ECMA-48 5th edition only mentions this option (without explanation)
/// saying that it's been removed.
///
/// As an emulator, we need to support it since applications may still rely
/// on it.
#[inline]
fn newline(&mut self) {
self.linefeed();
if self.mode.contains(TermMode::LINE_FEED_NEW_LINE) {
self.carriage_return();
}
}
#[inline]
fn set_horizontal_tabstop(&mut self) {
trace!("Setting horizontal tabstop");
self.tabs[self.grid.cursor.point.column] = true;
}
#[inline]
fn scroll_up(&mut self, lines: usize) {
let origin = self.scroll_region.start;
self.scroll_up_relative(origin, lines);
}
#[inline]
fn scroll_down(&mut self, lines: usize) {
let origin = self.scroll_region.start;
self.scroll_down_relative(origin, lines);
}
#[inline]
fn insert_blank_lines(&mut self, lines: usize) {
trace!("Inserting blank {} lines", lines);
let origin = self.grid.cursor.point.line;
if self.scroll_region.contains(&origin) {
self.scroll_down_relative(origin, lines);
}
}
#[inline]
fn delete_lines(&mut self, lines: usize) {
let origin = self.grid.cursor.point.line;
let lines = cmp::min(self.screen_lines() - origin.0 as usize, lines);
trace!("Deleting {} lines", lines);
if lines > 0 && self.scroll_region.contains(&origin) {
self.scroll_up_relative(origin, lines);
}
}
#[inline]
fn erase_chars(&mut self, count: Column) {
let cursor = &self.grid.cursor;
trace!("Erasing chars: count={}, col={}", count, cursor.point.column);
let start = cursor.point.column;
let end = cmp::min(start + count, Column(self.columns()));
// Cleared cells have current background color set.
let bg = self.grid.cursor.template.bg;
let line = cursor.point.line;
self.damage.damage_line(line.0 as usize, start.0, end.0);
let row = &mut self.grid[line];
for cell in &mut row[start..end] {
*cell = bg.into();
}
}
#[inline]
fn delete_chars(&mut self, count: usize) {
let columns = self.columns();
let cursor = &self.grid.cursor;
let bg = cursor.template.bg;
// Ensure deleting within terminal bounds.
let count = cmp::min(count, columns);
let start = cursor.point.column.0;
let end = cmp::min(start + count, columns - 1);
let num_cells = columns - end;
let line = cursor.point.line;
self.damage.damage_line(line.0 as usize, 0, self.columns() - 1);
let row = &mut self.grid[line][..];
for offset in 0..num_cells {
row.swap(start + offset, end + offset);
}
// Clear last `count` cells in the row. If deleting 1 char, need to delete
// 1 cell.
let end = columns - count;
for cell in &mut row[end..] {
*cell = bg.into();
}
}
#[inline]
fn move_backward_tabs(&mut self, count: u16) {
trace!("Moving backward {} tabs", count);
self.damage_cursor();
let old_col = self.grid.cursor.point.column.0;
for _ in 0..count {
let mut col = self.grid.cursor.point.column;
for i in (0..(col.0)).rev() {
if self.tabs[index::Column(i)] {
col = index::Column(i);
break;
}
}
self.grid.cursor.point.column = col;
}
let line = self.grid.cursor.point.line.0 as usize;
self.damage_line(line, self.grid.cursor.point.column.0, old_col);
}
#[inline]
fn move_forward_tabs(&mut self, count: u16) {
trace!("[unimplemented] Moving forward {} tabs", count);
}
#[inline]
fn save_cursor_position(&mut self) {
trace!("Saving cursor position");
self.grid.saved_cursor = self.grid.cursor.clone();
}
#[inline]
fn restore_cursor_position(&mut self) {
trace!("Restoring cursor position");
self.damage_cursor();
self.grid.cursor = self.grid.saved_cursor.clone();
self.damage_cursor();
}
#[inline]
fn clear_line(&mut self, mode: ansi::LineClearMode) {
trace!("Clearing line: {:?}", mode);
let cursor = &self.grid.cursor;
let bg = cursor.template.bg;
let point = cursor.point;
let (left, right) = match mode {
ansi::LineClearMode::Right => (point.column, Column(self.columns())),
ansi::LineClearMode::Left => (Column(0), point.column + 1),
ansi::LineClearMode::All => (Column(0), Column(self.columns())),
};
self.damage.damage_line(point.line.0 as usize, left.0, right.0 - 1);
let row = &mut self.grid[point.line];
for cell in &mut row[left..right] {
*cell = bg.into();
}
let range = self.grid.cursor.point.line..=self.grid.cursor.point.line;
self.selection = self.selection.take().filter(|s| !s.intersects_range(range));
}
/// Set the indexed color value.
#[inline]
fn set_color(&mut self, index: usize, color: Rgb) {
trace!("Setting color[{}] = {:?}", index, color);
// Damage terminal if the color changed and it's not the cursor.
if index != NamedColor::Cursor as usize && self.colors[index] != Some(color) {
self.mark_fully_damaged();
}
self.colors[index] = Some(color);
}
/// Respond to a color query escape sequence.
#[inline]
fn dynamic_color_sequence(&mut self, prefix: String, index: usize, terminator: &str) {
trace!("Requested write of escape sequence for color code {}: color[{}]", prefix, index);
let terminator = terminator.to_owned();
self.event_proxy.send_event(Event::ColorRequest(
index,
Arc::new(move |color| {
format!(
"\x1b]{};rgb:{1:02x}{1:02x}/{2:02x}{2:02x}/{3:02x}{3:02x}{4}",
prefix, color.r, color.g, color.b, terminator
)
}),
));
}
/// Reset the indexed color to original value.
#[inline]
fn reset_color(&mut self, index: usize) {
trace!("Resetting color[{}]", index);
// Damage terminal if the color changed and it's not the cursor.
if index != NamedColor::Cursor as usize && self.colors[index].is_some() {
self.mark_fully_damaged();
}
self.colors[index] = None;
}
/// Store data into clipboard.
#[inline]
fn clipboard_store(&mut self, clipboard: u8, base64: &[u8]) {
let clipboard_type = match clipboard {
b'c' => ClipboardType::Clipboard,
b'p' | b's' => ClipboardType::Selection,
_ => return,
};
if let Ok(bytes) = base64::decode(base64) {
if let Ok(text) = String::from_utf8(bytes) {
self.event_proxy.send_event(Event::ClipboardStore(clipboard_type, text));
}
}
}
/// Load data from clipboard.
#[inline]
fn clipboard_load(&mut self, clipboard: u8, terminator: &str) {
let clipboard_type = match clipboard {
b'c' => ClipboardType::Clipboard,
b'p' | b's' => ClipboardType::Selection,
_ => return,
};
let terminator = terminator.to_owned();
self.event_proxy.send_event(Event::ClipboardLoad(
clipboard_type,
Arc::new(move |text| {
let base64 = base64::encode(&text);
format!("\x1b]52;{};{}{}", clipboard as char, base64, terminator)
}),
));
}
#[inline]
fn clear_screen(&mut self, mode: ansi::ClearMode) {
trace!("Clearing screen: {:?}", mode);
let bg = self.grid.cursor.template.bg;
let screen_lines = self.screen_lines();
match mode {
ansi::ClearMode::Above => {
let cursor = self.grid.cursor.point;
// If clearing more than one line.
if cursor.line > 1 {
// Fully clear all lines before the current line.
self.grid.reset_region(..cursor.line);
}
// Clear up to the current column in the current line.
let end = cmp::min(cursor.column + 1, Column(self.columns()));
for cell in &mut self.grid[cursor.line][..end] {
*cell = bg.into();
}
let range = Line(0)..=cursor.line;
self.selection = self.selection.take().filter(|s| !s.intersects_range(range));
},
ansi::ClearMode::Below => {
let cursor = self.grid.cursor.point;
for cell in &mut self.grid[cursor.line][cursor.column..] {
*cell = bg.into();
}
if (cursor.line.0 as usize) < screen_lines - 1 {
self.grid.reset_region((cursor.line + 1)..);
}
let range = cursor.line..Line(screen_lines as i32);
self.selection = self.selection.take().filter(|s| !s.intersects_range(range));
},
ansi::ClearMode::All => {
if self.mode.contains(TermMode::ALT_SCREEN) {
self.grid.reset_region(..);
} else {
let old_offset = self.grid.display_offset();
self.grid.clear_viewport();
// Compute number of lines scrolled by clearing the viewport.
let lines = self.grid.display_offset().saturating_sub(old_offset);
self.vi_mode_cursor.point.line =
(self.vi_mode_cursor.point.line - lines).grid_clamp(self, Boundary::Grid);
}
self.selection = None;
},
ansi::ClearMode::Saved if self.history_size() > 0 => {
self.grid.clear_history();
self.vi_mode_cursor.point.line =
self.vi_mode_cursor.point.line.grid_clamp(self, Boundary::Cursor);
self.selection = self.selection.take().filter(|s| !s.intersects_range(..Line(0)));
},
// We have no history to clear.
ansi::ClearMode::Saved => (),
}
self.mark_fully_damaged();
}
#[inline]
fn clear_tabs(&mut self, mode: ansi::TabulationClearMode) {
trace!("Clearing tabs: {:?}", mode);
match mode {
ansi::TabulationClearMode::Current => {
self.tabs[self.grid.cursor.point.column] = false;
},
ansi::TabulationClearMode::All => {
self.tabs.clear_all();
},
}
}
/// Reset all important fields in the term struct.
#[inline]
fn reset_state(&mut self) {
if self.mode.contains(TermMode::ALT_SCREEN) {
mem::swap(&mut self.grid, &mut self.inactive_grid);
}
self.active_charset = Default::default();
self.cursor_style = None;
self.grid.reset();
self.inactive_grid.reset();
self.scroll_region = Line(0)..Line(self.screen_lines() as i32);
self.tabs = TabStops::new(self.columns());
self.title_stack = Vec::new();
self.title = None;
self.selection = None;
self.vi_mode_cursor = Default::default();
// Preserve vi mode across resets.
self.mode &= TermMode::VI;
self.mode.insert(TermMode::default());
self.event_proxy.send_event(Event::CursorBlinkingChange);
self.mark_fully_damaged();
}
#[inline]
fn reverse_index(&mut self) {
trace!("Reversing index");
// If cursor is at the top.
if self.grid.cursor.point.line == self.scroll_region.start {
self.scroll_down(1);
} else {
self.damage_cursor();
self.grid.cursor.point.line = cmp::max(self.grid.cursor.point.line - 1, Line(0));
self.damage_cursor();
}
}
#[inline]
fn set_hyperlink(&mut self, hyperlink: Option<Hyperlink>) {
trace!("Setting hyperlink: {:?}", hyperlink);
self.grid.cursor.template.set_hyperlink(hyperlink);
}
/// Set a terminal attribute.
#[inline]
fn terminal_attribute(&mut self, attr: Attr) {
trace!("Setting attribute: {:?}", attr);
let cursor = &mut self.grid.cursor;
match attr {
Attr::Foreground(color) => cursor.template.fg = color,
Attr::Background(color) => cursor.template.bg = color,
Attr::UnderlineColor(color) => cursor.template.set_underline_color(color),
Attr::Reset => {
cursor.template.fg = Color::Named(NamedColor::Foreground);
cursor.template.bg = Color::Named(NamedColor::Background);
cursor.template.flags = Flags::empty();
cursor.template.set_underline_color(None);
},
Attr::Reverse => cursor.template.flags.insert(Flags::INVERSE),
Attr::CancelReverse => cursor.template.flags.remove(Flags::INVERSE),
Attr::Bold => cursor.template.flags.insert(Flags::BOLD),
Attr::CancelBold => cursor.template.flags.remove(Flags::BOLD),
Attr::Dim => cursor.template.flags.insert(Flags::DIM),
Attr::CancelBoldDim => cursor.template.flags.remove(Flags::BOLD | Flags::DIM),
Attr::Italic => cursor.template.flags.insert(Flags::ITALIC),
Attr::CancelItalic => cursor.template.flags.remove(Flags::ITALIC),
Attr::Underline => {
cursor.template.flags.remove(Flags::ALL_UNDERLINES);
cursor.template.flags.insert(Flags::UNDERLINE);
},
Attr::DoubleUnderline => {
cursor.template.flags.remove(Flags::ALL_UNDERLINES);
cursor.template.flags.insert(Flags::DOUBLE_UNDERLINE);
},
Attr::Undercurl => {
cursor.template.flags.remove(Flags::ALL_UNDERLINES);
cursor.template.flags.insert(Flags::UNDERCURL);
},
Attr::DottedUnderline => {
cursor.template.flags.remove(Flags::ALL_UNDERLINES);
cursor.template.flags.insert(Flags::DOTTED_UNDERLINE);
},
Attr::DashedUnderline => {
cursor.template.flags.remove(Flags::ALL_UNDERLINES);
cursor.template.flags.insert(Flags::DASHED_UNDERLINE);
},
Attr::CancelUnderline => cursor.template.flags.remove(Flags::ALL_UNDERLINES),
Attr::Hidden => cursor.template.flags.insert(Flags::HIDDEN),
Attr::CancelHidden => cursor.template.flags.remove(Flags::HIDDEN),
Attr::Strike => cursor.template.flags.insert(Flags::STRIKEOUT),
Attr::CancelStrike => cursor.template.flags.remove(Flags::STRIKEOUT),
_ => {
debug!("Term got unhandled attr: {:?}", attr);
},
}
}
#[inline]
fn set_mode(&mut self, mode: ansi::Mode) {
trace!("Setting mode: {:?}", mode);
match mode {
ansi::Mode::UrgencyHints => self.mode.insert(TermMode::URGENCY_HINTS),
ansi::Mode::SwapScreenAndSetRestoreCursor => {
if !self.mode.contains(TermMode::ALT_SCREEN) {
self.swap_alt();
}
},
ansi::Mode::ShowCursor => self.mode.insert(TermMode::SHOW_CURSOR),
ansi::Mode::CursorKeys => self.mode.insert(TermMode::APP_CURSOR),
// Mouse protocols are mutually exclusive.
ansi::Mode::ReportMouseClicks => {
self.mode.remove(TermMode::MOUSE_MODE);
self.mode.insert(TermMode::MOUSE_REPORT_CLICK);
self.event_proxy.send_event(Event::MouseCursorDirty);
},
ansi::Mode::ReportCellMouseMotion => {
self.mode.remove(TermMode::MOUSE_MODE);
self.mode.insert(TermMode::MOUSE_DRAG);
self.event_proxy.send_event(Event::MouseCursorDirty);
},
ansi::Mode::ReportAllMouseMotion => {
self.mode.remove(TermMode::MOUSE_MODE);
self.mode.insert(TermMode::MOUSE_MOTION);
self.event_proxy.send_event(Event::MouseCursorDirty);
},
ansi::Mode::ReportFocusInOut => self.mode.insert(TermMode::FOCUS_IN_OUT),
ansi::Mode::BracketedPaste => self.mode.insert(TermMode::BRACKETED_PASTE),
// Mouse encodings are mutually exclusive.
ansi::Mode::SgrMouse => {
self.mode.remove(TermMode::UTF8_MOUSE);
self.mode.insert(TermMode::SGR_MOUSE);
},
ansi::Mode::Utf8Mouse => {
self.mode.remove(TermMode::SGR_MOUSE);
self.mode.insert(TermMode::UTF8_MOUSE);
},
ansi::Mode::AlternateScroll => self.mode.insert(TermMode::ALTERNATE_SCROLL),
ansi::Mode::LineWrap => self.mode.insert(TermMode::LINE_WRAP),
ansi::Mode::LineFeedNewLine => self.mode.insert(TermMode::LINE_FEED_NEW_LINE),
ansi::Mode::Origin => self.mode.insert(TermMode::ORIGIN),
ansi::Mode::ColumnMode => self.deccolm(),
ansi::Mode::Insert => self.mode.insert(TermMode::INSERT),
ansi::Mode::BlinkingCursor => {
let style = self.cursor_style.get_or_insert(self.default_cursor_style);
style.blinking = true;
self.event_proxy.send_event(Event::CursorBlinkingChange);
},
}
}
#[inline]
fn unset_mode(&mut self, mode: ansi::Mode) {
trace!("Unsetting mode: {:?}", mode);
match mode {
ansi::Mode::UrgencyHints => self.mode.remove(TermMode::URGENCY_HINTS),
ansi::Mode::SwapScreenAndSetRestoreCursor => {
if self.mode.contains(TermMode::ALT_SCREEN) {
self.swap_alt();
}
},
ansi::Mode::ShowCursor => self.mode.remove(TermMode::SHOW_CURSOR),
ansi::Mode::CursorKeys => self.mode.remove(TermMode::APP_CURSOR),
ansi::Mode::ReportMouseClicks => {
self.mode.remove(TermMode::MOUSE_REPORT_CLICK);
self.event_proxy.send_event(Event::MouseCursorDirty);
},
ansi::Mode::ReportCellMouseMotion => {
self.mode.remove(TermMode::MOUSE_DRAG);
self.event_proxy.send_event(Event::MouseCursorDirty);
},
ansi::Mode::ReportAllMouseMotion => {
self.mode.remove(TermMode::MOUSE_MOTION);
self.event_proxy.send_event(Event::MouseCursorDirty);
},
ansi::Mode::ReportFocusInOut => self.mode.remove(TermMode::FOCUS_IN_OUT),
ansi::Mode::BracketedPaste => self.mode.remove(TermMode::BRACKETED_PASTE),
ansi::Mode::SgrMouse => self.mode.remove(TermMode::SGR_MOUSE),
ansi::Mode::Utf8Mouse => self.mode.remove(TermMode::UTF8_MOUSE),
ansi::Mode::AlternateScroll => self.mode.remove(TermMode::ALTERNATE_SCROLL),
ansi::Mode::LineWrap => self.mode.remove(TermMode::LINE_WRAP),
ansi::Mode::LineFeedNewLine => self.mode.remove(TermMode::LINE_FEED_NEW_LINE),
ansi::Mode::Origin => self.mode.remove(TermMode::ORIGIN),
ansi::Mode::ColumnMode => self.deccolm(),
ansi::Mode::Insert => {
self.mode.remove(TermMode::INSERT);
self.mark_fully_damaged();
},
ansi::Mode::BlinkingCursor => {
let style = self.cursor_style.get_or_insert(self.default_cursor_style);
style.blinking = false;
self.event_proxy.send_event(Event::CursorBlinkingChange);
},
}
}
#[inline]
fn set_scrolling_region(&mut self, top: usize, bottom: Option<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");
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) {
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 {
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;
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")));
}
/// 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 = TermSize::new(7, 17);
let mut term = Term::new(&Config::default(), &size, ());
let cursor = Point::new(Line(0), Column(0));
term.configure_charset(CharsetIndex::G0, StandardCharset::SpecialCharacterAndLineDrawing);
term.input('a');
assert_eq!(term.grid()[cursor].c, '▒');
}
#[test]
fn clearing_viewport_keeps_history_position() {
let size = TermSize::new(10, 20);
let mut term = Term::new(&Config::default(), &size, ());
// Create 10 lines of scrollback.
for _ in 0..29 {
term.newline();
}
// Change the display area.
term.scroll_display(Scroll::Top);
assert_eq!(term.grid.display_offset(), 10);
// Clear the viewport.
term.clear_screen(ansi::ClearMode::All);
assert_eq!(term.grid.display_offset(), 10);
}
#[test]
fn clearing_viewport_with_vi_mode_keeps_history_position() {
let size = TermSize::new(10, 20);
let mut term = Term::new(&Config::default(), &size, ());
// Create 10 lines of scrollback.
for _ in 0..29 {
term.newline();
}
// Enable vi mode.
term.toggle_vi_mode();
// Change the display area and the vi cursor position.
term.scroll_display(Scroll::Top);
term.vi_mode_cursor.point = Point::new(Line(-5), Column(3));
assert_eq!(term.grid.display_offset(), 10);
// Clear the viewport.
term.clear_screen(ansi::ClearMode::All);
assert_eq!(term.grid.display_offset(), 10);
assert_eq!(term.vi_mode_cursor.point, Point::new(Line(-5), Column(3)));
}
#[test]
fn clearing_scrollback_resets_display_offset() {
let size = TermSize::new(10, 20);
let mut term = Term::new(&Config::default(), &size, ());
// Create 10 lines of scrollback.
for _ in 0..29 {
term.newline();
}
// Change the display area.
term.scroll_display(Scroll::Top);
assert_eq!(term.grid.display_offset(), 10);
// Clear the scrollback buffer.
term.clear_screen(ansi::ClearMode::Saved);
assert_eq!(term.grid.display_offset(), 0);
}
#[test]
fn clearing_scrollback_sets_vi_cursor_into_viewport() {
let size = TermSize::new(10, 20);
let mut term = Term::new(&Config::default(), &size, ());
// Create 10 lines of scrollback.
for _ in 0..29 {
term.newline();
}
// Enable vi mode.
term.toggle_vi_mode();
// Change the display area and the vi cursor position.
term.scroll_display(Scroll::Top);
term.vi_mode_cursor.point = Point::new(Line(-5), Column(3));
assert_eq!(term.grid.display_offset(), 10);
// Clear the scrollback buffer.
term.clear_screen(ansi::ClearMode::Saved);
assert_eq!(term.grid.display_offset(), 0);
assert_eq!(term.vi_mode_cursor.point, Point::new(Line(0), Column(3)));
}
#[test]
fn clear_saved_lines() {
let size = TermSize::new(7, 17);
let mut term = Term::new(&Config::default(), &size, ());
// Add one line of scrollback.
term.grid.scroll_up(&(Line(0)..Line(1)), 1);
// Clear the history.
term.clear_screen(ansi::ClearMode::Saved);
// Make sure that scrolling does not change the grid.
let mut scrolled_grid = term.grid.clone();
scrolled_grid.scroll_display(Scroll::Top);
// Truncate grids for comparison.
scrolled_grid.truncate();
term.grid.truncate();
assert_eq!(term.grid, scrolled_grid);
}
#[test]
fn vi_cursor_keep_pos_on_scrollback_buffer() {
let size = TermSize::new(5, 10);
let mut term = Term::new(&Config::default(), &size, ());
// Create 11 lines of scrollback.
for _ in 0..20 {
term.newline();
}
// Enable vi mode.
term.toggle_vi_mode();
term.scroll_display(Scroll::Top);
term.vi_mode_cursor.point.line = Line(-11);
term.linefeed();
assert_eq!(term.vi_mode_cursor.point.line, Line(-12));
}
#[test]
fn grow_lines_updates_active_cursor_pos() {
let mut size = TermSize::new(100, 10);
let mut term = Term::new(&Config::default(), &size, ());
// Create 10 lines of scrollback.
for _ in 0..19 {
term.newline();
}
assert_eq!(term.history_size(), 10);
assert_eq!(term.grid.cursor.point, Point::new(Line(9), Column(0)));
// Increase visible lines.
size.screen_lines = 30;
term.resize(size);
assert_eq!(term.history_size(), 0);
assert_eq!(term.grid.cursor.point, Point::new(Line(19), Column(0)));
}
#[test]
fn grow_lines_updates_inactive_cursor_pos() {
let mut size = TermSize::new(100, 10);
let mut term = Term::new(&Config::default(), &size, ());
// Create 10 lines of scrollback.
for _ in 0..19 {
term.newline();
}
assert_eq!(term.history_size(), 10);
assert_eq!(term.grid.cursor.point, Point::new(Line(9), Column(0)));
// Enter alt screen.
term.set_mode(ansi::Mode::SwapScreenAndSetRestoreCursor);
// Increase visible lines.
size.screen_lines = 30;
term.resize(size);
// Leave alt screen.
term.unset_mode(ansi::Mode::SwapScreenAndSetRestoreCursor);
assert_eq!(term.history_size(), 0);
assert_eq!(term.grid.cursor.point, Point::new(Line(19), Column(0)));
}
#[test]
fn shrink_lines_updates_active_cursor_pos() {
let mut size = TermSize::new(100, 10);
let mut term = Term::new(&Config::default(), &size, ());
// Create 10 lines of scrollback.
for _ in 0..19 {
term.newline();
}
assert_eq!(term.history_size(), 10);
assert_eq!(term.grid.cursor.point, Point::new(Line(9), Column(0)));
// Increase visible lines.
size.screen_lines = 5;
term.resize(size);
assert_eq!(term.history_size(), 15);
assert_eq!(term.grid.cursor.point, Point::new(Line(4), Column(0)));
}
#[test]
fn shrink_lines_updates_inactive_cursor_pos() {
let mut size = TermSize::new(100, 10);
let mut term = Term::new(&Config::default(), &size, ());
// Create 10 lines of scrollback.
for _ in 0..19 {
term.newline();
}
assert_eq!(term.history_size(), 10);
assert_eq!(term.grid.cursor.point, Point::new(Line(9), Column(0)));
// Enter alt screen.
term.set_mode(ansi::Mode::SwapScreenAndSetRestoreCursor);
// Increase visible lines.
size.screen_lines = 5;
term.resize(size);
// Leave alt screen.
term.unset_mode(ansi::Mode::SwapScreenAndSetRestoreCursor);
assert_eq!(term.history_size(), 15);
assert_eq!(term.grid.cursor.point, Point::new(Line(4), Column(0)));
}
#[test]
fn damage_public_usage() {
let size = TermSize::new(10, 10);
let mut term = Term::new(&Config::default(), &size, ());
// Reset terminal for partial damage tests since it's initialized as fully damaged.
term.reset_damage();
// Test that we damage input form [`Term::input`].
let left = term.grid.cursor.point.column.0;
term.input('d');
term.input('a');
term.input('m');
term.input('a');
term.input('g');
term.input('e');
let right = term.grid.cursor.point.column.0;
let mut damaged_lines = match term.damage(None) {
TermDamage::Full => panic!("Expected partial damage, however got Full"),
TermDamage::Partial(damaged_lines) => damaged_lines,
};
assert_eq!(damaged_lines.next(), Some(LineDamageBounds { line: 0, left, right }));
assert_eq!(damaged_lines.next(), None);
term.reset_damage();
// Check that selection we've passed was properly damaged.
let line = 1;
let left = 0;
let right = term.columns() - 1;
let mut selection =
Selection::new(SelectionType::Block, Point::new(Line(line), Column(3)), Side::Left);
selection.update(Point::new(Line(line), Column(5)), Side::Left);
let selection_range = selection.to_range(&term);
let mut damaged_lines = match term.damage(selection_range) {
TermDamage::Full => panic!("Expected partial damage, however got Full"),
TermDamage::Partial(damaged_lines) => damaged_lines,
};
let line = line as usize;
// Skip cursor damage information, since we're just testing selection.
damaged_lines.next();
assert_eq!(damaged_lines.next(), Some(LineDamageBounds { line, left, right }));
assert_eq!(damaged_lines.next(), None);
term.reset_damage();
// Check that existing selection gets damaged when it is removed.
let mut damaged_lines = match term.damage(None) {
TermDamage::Full => panic!("Expected partial damage, however got Full"),
TermDamage::Partial(damaged_lines) => damaged_lines,
};
// Skip cursor damage information, since we're just testing selection clearing.
damaged_lines.next();
assert_eq!(damaged_lines.next(), Some(LineDamageBounds { line, left, right }));
assert_eq!(damaged_lines.next(), None);
term.reset_damage();
// Check that `Vi` cursor in vi mode is being always damaged.
term.toggle_vi_mode();
// Put Vi cursor to a different location than normal cursor.
term.vi_goto_point(Point::new(Line(5), Column(5)));
// Reset damage, so the damage information from `vi_goto_point` won't affect test.
term.reset_damage();
let vi_cursor_point = term.vi_mode_cursor.point;
let line = vi_cursor_point.line.0 as usize;
let left = vi_cursor_point.column.0 as usize;
let right = left;
let mut damaged_lines = match term.damage(None) {
TermDamage::Full => panic!("Expected partial damage, however got Full"),
TermDamage::Partial(damaged_lines) => damaged_lines,
};
// Skip cursor damage information, since we're just testing Vi cursor.
damaged_lines.next();
assert_eq!(damaged_lines.next(), Some(LineDamageBounds { line, left, right }));
assert_eq!(damaged_lines.next(), None);
// Ensure that old Vi cursor got damaged as well.
term.reset_damage();
term.toggle_vi_mode();
let mut damaged_lines = match term.damage(None) {
TermDamage::Full => panic!("Expected partial damage, however got Full"),
TermDamage::Partial(damaged_lines) => damaged_lines,
};
// Skip cursor damage information, since we're just testing Vi cursor.
damaged_lines.next();
assert_eq!(damaged_lines.next(), Some(LineDamageBounds { line, left, right }));
assert_eq!(damaged_lines.next(), None);
}
#[test]
fn damage_cursor_movements() {
let size = TermSize::new(10, 10);
let mut term = Term::new(&Config::default(), &size, ());
let num_cols = term.columns();
// Reset terminal for partial damage tests since it's initialized as fully damaged.
term.reset_damage();
term.goto(Line(1), Column(1));
// NOTE While we can use `[Term::damage]` to access terminal damage information, in the
// following tests we will be accessing `term.damage.lines` directly to avoid adding extra
// damage information (like cursor and Vi cursor), which we're not testing.
assert_eq!(term.damage.lines[0], LineDamageBounds { line: 0, left: 0, right: 0 });
assert_eq!(term.damage.lines[1], LineDamageBounds { line: 1, left: 1, right: 1 });
term.damage.reset(num_cols);
term.move_forward(Column(3));
assert_eq!(term.damage.lines[1], LineDamageBounds { line: 1, left: 1, right: 4 });
term.damage.reset(num_cols);
term.move_backward(Column(8));
assert_eq!(term.damage.lines[1], LineDamageBounds { line: 1, left: 0, right: 4 });
term.goto(Line(5), Column(5));
term.damage.reset(num_cols);
term.backspace();
term.backspace();
assert_eq!(term.damage.lines[5], LineDamageBounds { line: 5, left: 3, right: 5 });
term.damage.reset(num_cols);
term.move_up(1);
assert_eq!(term.damage.lines[5], LineDamageBounds { line: 5, left: 3, right: 3 });
assert_eq!(term.damage.lines[4], LineDamageBounds { line: 4, left: 3, right: 3 });
term.damage.reset(num_cols);
term.move_down(1);
term.move_down(1);
assert_eq!(term.damage.lines[4], LineDamageBounds { line: 4, left: 3, right: 3 });
assert_eq!(term.damage.lines[5], LineDamageBounds { line: 5, left: 3, right: 3 });
assert_eq!(term.damage.lines[6], LineDamageBounds { line: 6, left: 3, right: 3 });
term.damage.reset(num_cols);
term.wrapline();
assert_eq!(term.damage.lines[6], LineDamageBounds { line: 6, left: 3, right: 3 });
assert_eq!(term.damage.lines[7], LineDamageBounds { line: 7, left: 0, right: 0 });
term.move_forward(Column(3));
term.move_up(1);
term.damage.reset(num_cols);
term.linefeed();
assert_eq!(term.damage.lines[6], LineDamageBounds { line: 6, left: 3, right: 3 });
assert_eq!(term.damage.lines[7], LineDamageBounds { line: 7, left: 3, right: 3 });
term.damage.reset(num_cols);
term.carriage_return();
assert_eq!(term.damage.lines[7], LineDamageBounds { line: 7, left: 0, right: 3 });
term.damage.reset(num_cols);
term.erase_chars(Column(5));
assert_eq!(term.damage.lines[7], LineDamageBounds { line: 7, left: 0, right: 5 });
term.damage.reset(num_cols);
term.delete_chars(3);
let right = term.columns() - 1;
assert_eq!(term.damage.lines[7], LineDamageBounds { line: 7, left: 0, right });
term.move_forward(Column(term.columns()));
term.damage.reset(num_cols);
term.move_backward_tabs(1);
assert_eq!(term.damage.lines[7], LineDamageBounds { line: 7, left: 8, right });
term.save_cursor_position();
term.goto(Line(1), Column(1));
term.damage.reset(num_cols);
term.restore_cursor_position();
assert_eq!(term.damage.lines[1], LineDamageBounds { line: 1, left: 1, right: 1 });
assert_eq!(term.damage.lines[7], LineDamageBounds { line: 7, left: 8, right: 8 });
term.damage.reset(num_cols);
term.clear_line(ansi::LineClearMode::All);
assert_eq!(term.damage.lines[7], LineDamageBounds { line: 7, left: 0, right });
term.damage.reset(num_cols);
term.clear_line(ansi::LineClearMode::Left);
assert_eq!(term.damage.lines[7], LineDamageBounds { line: 7, left: 0, right: 8 });
term.damage.reset(num_cols);
term.clear_line(ansi::LineClearMode::Right);
assert_eq!(term.damage.lines[7], LineDamageBounds { line: 7, left: 8, right });
term.damage.reset(num_cols);
term.reverse_index();
assert_eq!(term.damage.lines[7], LineDamageBounds { line: 7, left: 8, right: 8 });
assert_eq!(term.damage.lines[6], LineDamageBounds { line: 6, left: 8, right: 8 });
}
#[test]
fn full_damage() {
let size = TermSize::new(100, 10);
let mut term = Term::new(&Config::default(), &size, ());
assert!(term.damage.is_fully_damaged);
for _ in 0..20 {
term.newline();
}
term.reset_damage();
term.clear_screen(ansi::ClearMode::Above);
assert!(term.damage.is_fully_damaged);
term.reset_damage();
term.scroll_display(Scroll::Top);
assert!(term.damage.is_fully_damaged);
term.reset_damage();
// Sequential call to scroll display without doing anything shouldn't damage.
term.scroll_display(Scroll::Top);
assert!(!term.damage.is_fully_damaged);
term.reset_damage();
term.update_config(&Config::default());
assert!(term.damage.is_fully_damaged);
term.reset_damage();
term.scroll_down_relative(Line(5), 2);
assert!(term.damage.is_fully_damaged);
term.reset_damage();
term.scroll_up_relative(Line(3), 2);
assert!(term.damage.is_fully_damaged);
term.reset_damage();
term.deccolm();
assert!(term.damage.is_fully_damaged);
term.reset_damage();
term.decaln();
assert!(term.damage.is_fully_damaged);
term.reset_damage();
term.set_mode(ansi::Mode::Insert);
// Just setting `Insert` mode shouldn't mark terminal as damaged.
assert!(!term.damage.is_fully_damaged);
term.reset_damage();
let color_index = 257;
term.set_color(color_index, Rgb::default());
assert!(term.damage.is_fully_damaged);
term.reset_damage();
// Setting the same color once again shouldn't trigger full damage.
term.set_color(color_index, Rgb::default());
assert!(!term.damage.is_fully_damaged);
term.reset_color(color_index);
assert!(term.damage.is_fully_damaged);
term.reset_damage();
// We shouldn't trigger fully damage when cursor gets update.
term.set_color(NamedColor::Cursor as usize, Rgb::default());
assert!(!term.damage.is_fully_damaged);
// However requesting terminal damage should mark terminal as fully damaged in `Insert`
// mode.
let _ = term.damage(None);
assert!(term.damage.is_fully_damaged);
term.reset_damage();
term.unset_mode(ansi::Mode::Insert);
assert!(term.damage.is_fully_damaged);
term.reset_damage();
// Keep this as a last check, so we don't have to deal with restoring from alt-screen.
term.swap_alt();
assert!(term.damage.is_fully_damaged);
term.reset_damage();
let size = TermSize::new(10, 10);
term.resize(size);
assert!(term.damage.is_fully_damaged);
}
#[test]
fn window_title() {
let size = TermSize::new(7, 17);
let mut term = Term::new(&Config::default(), &size, ());
// Title None by default.
assert_eq!(term.title, None);
// Title can be set.
term.set_title(Some("Test".into()));
assert_eq!(term.title, Some("Test".into()));
// Title can be pushed onto stack.
term.push_title();
term.set_title(Some("Next".into()));
assert_eq!(term.title, Some("Next".into()));
assert_eq!(term.title_stack.get(0).unwrap(), &Some("Test".into()));
// Title can be popped from stack and set as the window title.
term.pop_title();
assert_eq!(term.title, Some("Test".into()));
assert!(term.title_stack.is_empty());
// Title stack doesn't grow infinitely.
for _ in 0..4097 {
term.push_title();
}
assert_eq!(term.title_stack.len(), 4096);
// Title and title stack reset when terminal state is reset.
term.push_title();
term.reset_state();
assert_eq!(term.title, None);
assert!(term.title_stack.is_empty());
// Title stack pops back to default.
term.title = None;
term.push_title();
term.set_title(Some("Test".into()));
term.pop_title();
assert_eq!(term.title, None);
// Title can be reset to default.
term.title = Some("Test".into());
term.set_title(None);
assert_eq!(term.title, None);
}
#[test]
fn parse_cargo_version() {
assert!(version_number(env!("CARGO_PKG_VERSION")) >= 10_01);
assert_eq!(version_number("0.0.1-dev"), 1);
assert_eq!(version_number("0.1.2-dev"), 1_02);
assert_eq!(version_number("1.2.3-dev"), 1_02_03);
assert_eq!(version_number("999.99.99"), 9_99_99_99);
}
}