alacritty/alacritty_terminal/src/term/mod.rs

2854 lines
94 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.
2019-03-30 16:48:36 +00:00
use std::cmp::{max, min};
use std::iter::Peekable;
use std::ops::{Index, IndexMut, Range, RangeInclusive};
use std::sync::Arc;
use std::time::{Duration, Instant};
use std::{io, iter, mem, ptr, str};
use log::{debug, trace};
use serde::{Deserialize, Serialize};
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, CursorStyle, Handler, NamedColor, StandardCharset,
2019-03-30 16:48:36 +00:00
};
use crate::config::{BellAnimation, BellConfig, Config};
use crate::event::{Event, EventListener};
use crate::grid::{Dimensions, DisplayIter, Grid, IndexRegion, Indexed, Scroll};
use crate::index::{self, Boundary, Column, Direction, IndexRange, Line, Point, Side};
use crate::selection::{Selection, SelectionRange};
2019-03-30 16:48:36 +00:00
use crate::term::cell::{Cell, Flags, LineLength};
use crate::term::color::{CellRgb, Rgb, DIM_FACTOR};
use crate::term::search::{RegexIter, RegexSearch};
use crate::vi_mode::{ViModeCursor, ViMotion};
pub mod cell;
pub mod color;
mod search;
/// Max size of the window title stack.
const TITLE_STACK_MAX_DEPTH: usize = 4096;
/// Minimum contrast between a fixed cursor color and the cell's background.
const MIN_CURSOR_CONTRAST: f64 = 1.5;
/// Maximum number of linewraps followed outside of the viewport during search highlighting.
const MAX_SEARCH_LINES: usize = 100;
/// Default tab interval, corresponding to terminfo `it` value.
const INITIAL_TABSTOPS: usize = 8;
/// Minimum number of columns.
///
/// A minimum of 2 is necessary to hold fullwidth unicode characters.
pub const MIN_COLS: usize = 2;
/// Minimum number of visible lines.
pub const MIN_SCREEN_LINES: usize = 1;
/// Cursor storing all information relevant for rendering.
#[derive(Debug, Eq, PartialEq, Copy, Clone, Deserialize)]
struct RenderableCursor {
text_color: CellRgb,
cursor_color: CellRgb,
key: CursorKey,
point: Point,
rendered: bool,
}
/// A key for caching cursor glyphs.
#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash, Deserialize)]
pub struct CursorKey {
pub style: CursorStyle,
pub is_wide: bool,
}
type MatchIter<'a> = Box<dyn Iterator<Item = RangeInclusive<Point<usize>>> + 'a>;
/// Regex search highlight tracking.
pub struct RenderableSearch<'a> {
iter: Peekable<MatchIter<'a>>,
}
impl<'a> RenderableSearch<'a> {
/// Create a new renderable search iterator.
fn new<T>(term: &'a Term<T>) -> Self {
let viewport_end = term.grid().display_offset();
let viewport_start = viewport_end + term.screen_lines().0 - 1;
// Compute start of the first and end of the last line.
let start_point = Point::new(viewport_start, Column(0));
let mut start = term.line_search_left(start_point);
let end_point = Point::new(viewport_end, term.cols() - 1);
let mut end = term.line_search_right(end_point);
// Set upper bound on search before/after the viewport to prevent excessive blocking.
if start.line > viewport_start + MAX_SEARCH_LINES {
if start.line == 0 {
// Do not highlight anything if this line is the last.
let iter: MatchIter<'a> = Box::new(iter::empty());
return Self { iter: iter.peekable() };
} else {
// Start at next line if this one is too long.
start.line -= 1;
}
}
end.line = max(end.line, viewport_end.saturating_sub(MAX_SEARCH_LINES));
// Create an iterater for the current regex search for all visible matches.
let iter: MatchIter<'a> = Box::new(
RegexIter::new(start, end, Direction::Right, &term)
.skip_while(move |rm| rm.end().line > viewport_start)
.take_while(move |rm| rm.start().line >= viewport_end),
);
Self { iter: iter.peekable() }
}
/// Advance the search tracker to the next point.
///
/// This will return `true` if the point passed is part of a search match.
fn advance(&mut self, point: Point<usize>) -> bool {
while let Some(regex_match) = &self.iter.peek() {
if regex_match.start() > &point {
break;
} else if regex_match.end() < &point {
let _ = self.iter.next();
} else {
return true;
}
}
false
}
}
2020-05-05 22:50:23 +00:00
/// Iterator that yields cells needing render.
///
/// Yields cells that require work to be displayed (that is, not a an empty
/// background cell). Additionally, this manages some state of the grid only
/// relevant for rendering like temporarily changing the cell with the cursor.
///
/// This manages the cursor during a render. The cursor location is inverted to
/// draw it, and reverted after drawing to maintain state.
pub struct RenderableCellsIter<'a, C> {
inner: DisplayIter<'a, Cell>,
grid: &'a Grid<Cell>,
cursor: RenderableCursor,
config: &'a Config<C>,
colors: &'a color::List,
selection: Option<SelectionRange<Line>>,
search: RenderableSearch<'a>,
}
impl<'a, C> RenderableCellsIter<'a, C> {
2020-05-05 22:50:23 +00:00
/// Create the renderable cells iterator.
///
/// The cursor and terminal mode are required for properly displaying the
/// cursor.
fn new<T>(
term: &'a Term<T>,
config: &'a Config<C>,
selection: Option<SelectionRange>,
) -> RenderableCellsIter<'a, C> {
let grid = &term.grid;
let selection_range = selection.and_then(|span| {
let (limit_start, limit_end) = if span.is_block {
(span.start.col, span.end.col)
} else {
(Column(0), grid.cols() - 1)
};
2020-05-05 22:50:23 +00:00
// Do not render completely offscreen selection.
let viewport_end = grid.display_offset();
let viewport_start = viewport_end + grid.screen_lines().0 - 1;
if span.end.line > viewport_start || span.start.line < viewport_end {
return None;
}
2020-05-05 22:50:23 +00:00
// Get on-screen lines of the selection's locations.
let mut start = grid.clamp_buffer_to_visible(span.start);
let mut end = grid.clamp_buffer_to_visible(span.end);
2020-05-05 22:50:23 +00:00
// Trim start/end with partially visible block selection.
start.col = max(limit_start, start.col);
end.col = min(limit_end, end.col);
Some(SelectionRange::new(start, end, span.is_block))
});
RenderableCellsIter {
cursor: term.renderable_cursor(config),
grid,
inner: grid.display_iter(),
selection: selection_range,
config,
colors: &term.colors,
search: RenderableSearch::new(term),
2019-03-30 16:48:36 +00:00
}
}
/// Check selection state of a cell.
fn is_selected(&self, point: Point) -> bool {
let selection = match self.selection {
Some(selection) => selection,
None => return false,
};
2020-05-05 22:50:23 +00:00
// Do not invert block cursor at selection boundaries.
if self.cursor.key.style == CursorStyle::Block
&& self.cursor.point == point
&& (selection.start == point
|| selection.end == point
|| (selection.is_block
&& ((selection.start.line == point.line && selection.end.col == point.col)
|| (selection.end.line == point.line && selection.start.col == point.col))))
{
return false;
}
2020-05-05 22:50:23 +00:00
// Point itself is selected.
if selection.contains(point.col, point.line) {
return true;
}
let num_cols = self.grid.cols();
// Convert to absolute coordinates to adjust for the display offset.
let buffer_point = self.grid.visible_to_buffer(point);
let cell = &self.grid[buffer_point];
2020-05-05 22:50:23 +00:00
// Check if wide char's spacers are selected.
if cell.flags.contains(Flags::WIDE_CHAR) {
let prev = point.sub(num_cols, 1);
let buffer_prev = self.grid.visible_to_buffer(prev);
let next = point.add(num_cols, 1);
2020-05-05 22:50:23 +00:00
// Check trailing spacer.
selection.contains(next.col, next.line)
2020-05-05 22:50:23 +00:00
// Check line-wrapping, leading spacer.
|| (self.grid[buffer_prev].flags.contains(Flags::LEADING_WIDE_CHAR_SPACER)
&& selection.contains(prev.col, prev.line))
} else if cell.flags.contains(Flags::WIDE_CHAR_SPACER) {
2020-05-05 22:50:23 +00:00
// Check if spacer's wide char is selected.
let prev = point.sub(num_cols, 1);
let buffer_prev = self.grid.visible_to_buffer(prev);
if self.grid[buffer_prev].flags.contains(Flags::WIDE_CHAR) {
2020-05-05 22:50:23 +00:00
// Check previous cell for trailing spacer.
self.is_selected(prev)
} else {
2020-05-05 22:50:23 +00:00
// Check next cell for line-wrapping, leading spacer.
self.is_selected(point.add(num_cols, 1))
}
} else {
false
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RenderableCellContent {
Chars((char, Option<Vec<char>>)),
Cursor(CursorKey),
}
#[derive(Clone, Debug)]
pub struct RenderableCell {
2020-05-05 22:50:23 +00:00
/// A _Display_ line (not necessarily an _Active_ line).
pub line: Line,
pub column: Column,
pub inner: RenderableCellContent,
pub fg: Rgb,
pub bg: Rgb,
pub bg_alpha: f32,
pub flags: Flags,
}
impl RenderableCell {
fn new<'a, C>(iter: &mut RenderableCellsIter<'a, C>, cell: Indexed<&Cell>) -> Self {
let point = Point::new(cell.line, cell.column);
2020-05-05 22:50:23 +00:00
// Lookup RGB values.
let mut fg_rgb = Self::compute_fg_rgb(iter.config, iter.colors, cell.fg, cell.flags);
let mut bg_rgb = Self::compute_bg_rgb(iter.colors, cell.bg);
let mut bg_alpha = if cell.flags.contains(Flags::INVERSE) {
mem::swap(&mut fg_rgb, &mut bg_rgb);
1.0
} else {
Self::compute_bg_alpha(cell.bg)
};
if iter.is_selected(point) {
let config_bg = iter.config.colors.selection.background();
let selected_fg = iter.config.colors.selection.text().color(fg_rgb, bg_rgb);
bg_rgb = config_bg.color(fg_rgb, bg_rgb);
fg_rgb = selected_fg;
if fg_rgb == bg_rgb && !cell.flags.contains(Flags::HIDDEN) {
2020-05-05 22:50:23 +00:00
// Reveal inversed text when fg/bg is the same.
fg_rgb = iter.colors[NamedColor::Background];
bg_rgb = iter.colors[NamedColor::Foreground];
bg_alpha = 1.0;
} else if config_bg != CellRgb::CellBackground {
bg_alpha = 1.0;
}
} else if iter.search.advance(iter.grid.visible_to_buffer(point)) {
// Highlight the cell if it is part of a search match.
let config_bg = iter.config.colors.search.matches.background;
let matched_fg = iter.config.colors.search.matches.foreground.color(fg_rgb, bg_rgb);
bg_rgb = config_bg.color(fg_rgb, bg_rgb);
fg_rgb = matched_fg;
if config_bg != CellRgb::CellBackground {
bg_alpha = 1.0;
}
}
let zerowidth = cell.zerowidth().map(|zerowidth| zerowidth.to_vec());
RenderableCell {
line: cell.line,
column: cell.column,
inner: RenderableCellContent::Chars((cell.c, zerowidth)),
fg: fg_rgb,
bg: bg_rgb,
bg_alpha,
flags: cell.flags,
}
}
fn is_empty(&self) -> bool {
self.bg_alpha == 0.
&& !self.flags.intersects(Flags::UNDERLINE | Flags::STRIKEOUT | Flags::DOUBLE_UNDERLINE)
&& self.inner == RenderableCellContent::Chars((' ', None))
}
fn compute_fg_rgb<C>(config: &Config<C>, colors: &color::List, fg: Color, flags: Flags) -> Rgb {
match fg {
Color::Spec(rgb) => match flags & Flags::DIM {
Flags::DIM => rgb * DIM_FACTOR,
_ => rgb,
},
Color::Named(ansi) => {
match (config.draw_bold_text_with_bright_colors(), flags & Flags::DIM_BOLD) {
2020-05-05 22:50:23 +00:00
// If no bright foreground is set, treat it like the BOLD flag doesn't exist.
(_, Flags::DIM_BOLD)
Merge master into scrollback * Allow disabling DPI scaling This makes it possible to disable DPI scaling completely, instead the the display pixel ration will always be fixed to 1.0. By default nothing has changed and DPI is still enabled, this just seems like a better way than running `WINIT_HIDPI_FACTOR=1.0 alacritty` every time the user wants to start alacritty. It would be possible to allow specifying any DPR, however I've decided against this since I'd assume it's a very rare usecase. It's also still possible to make use of `WINIT_HIDPI_FACTOR` to do this on X11. Currently this is not updated at runtime using the live config update, there is not really much of a technical limitation why this woudn't be possible, however a solution for that issue should be first added in jwilm/alacritty#1346, once a system is established for changing DPI at runtime, porting that functionality to this PR should be simple. * Add working --class and --title CLI parameters * Reduce Increase-/DecreaseFontSize step to 0.5 Until now the Increase-/DecreaseFontSize keybinds hand a step size of 1.0. Since the font size however is multiplied by two to allow more granular font size control, this lead to the bindings skipping one font size (incrementing/decrementing by +-2). To fix this the step size of the Increase-/DecreaseFontSize bindings has been reduced to the minimum step size that exists with the current font configuration (0.5). This should allow users to increment and decrement the font size by a single point instead of two. This also adds a few tests to make sure the methods for increasing/decreasing/resetting font size work properly. * Add Copy/Cut/Paste keys This just adds support for the Copy/Cut/Paste keys and sets up Copy/Paste as alternative defaults for Ctrl+Shift+C/V. * Move to cargo clippy Using clippy as a library has been deprecated, instead the `cargo clippy` command should be used instead. To comply with this change clippy has been removed from the `Cargo.toml` and is now installed with cargo when building in CI. This has also lead to a few new clippy issues to show up, this includes everything in the `font` subdirectory. This has been fixed and `font` should now be covered by clippy CI too. This also upgrades all dependencies, as a result this fixes #1341 and this fixes #1344. * Override dynamic_title when --title is specified * Change green implementation to use the macro * Ignore mouse input if window is unfocused * Make compilation of binary a phony target * Add opensuse zypper install method to readme * Fix clippy issues * Update manpage to document all CLI options The introduction of `--class` has added a flag to the CLI without adding it to the manpage. This has been fixed by updating the manpage. This also adds the default values of `--class` and `--title` to the CLI options. * Remove unnecessary clippy lint annotations We moved to "cargo clippy" in 5ba34d4f9766a55a06ed5e3e44cc384af1b09f65 and removing the clippy lint annotations in `src/lib.rs` does not cause any additional warnings. This also changes `cargo clippy` to use the flags required for checking integration tests. * Enable clippy in font/copypasta crates Enabled clippy in the sub-crates font and copypasta. All issues that were discovered by this change have also been fixed. * Remove outdated comment about NixOS * Replace debug asserts with static_assertions To check that transmutes will work correctly without having to rely on error-prone runtime checking, the `static_assertions` crate has been introduced. This allows comparing the size of types at compile time, preventing potentially silent breakage. This fixes #1417. * Add `cargo deb` build instructions Updated the `Cargo.toml` file and added a `package.metadata.deb` subsection to define how to build a debian "deb" install file using `cargo deb`. This will allow debian/ubuntu users to install `alacritty` using their system's package manager. It also will make it easier to provide pre-built binaries for those systems. Also fixed a stray debug line in the bash autocomplete script that was writting to a tempfile. * Add config for unfocused window cursor change * Add support for cursor shape escape sequence * Add bright foreground color option It was requested in jwilm/alacritty#825 that it should be possible to add an optional bright foreground color. This is now added to the primary colors structure and allows the user to set a foreground color for bold normal text. This has no effect unless the draw_bold_text_with_bright_colors option is also enabled. If the color is not specified, the bright foreground color will fall back to the normal foreground color. This fixes #825. * Fix clone URL in deb install instructions * Fix 'cargo-deb' desktop file name * Remove redundant dependency from deb build * Switch from deprecated `std::env::home_dir` to `dirs::home_dir` * Allow specifying modifiers for mouse bindings * Send newline with NumpadEnter * Add support for LCD-V pixel mode * Add binding action for hiding the window * Switch to rustup clippy component * Add optional dim foreground color Add optional color for the dim foreground (`\e[2m;`) Defaults to 2/3 of the foreground color. (same as other colors). If a bright color is dimmed, it's displayed as the normal color. The exception for this is when the bright foreground is dimmed when no bright foreground color is set. In that case it's treated as a normal foreground color and dimmed to DimForeground. To minimize the surprise for the user, the bright and dim colors have been completely removed from the default configuration file. Some documentation has also been added to make it clear to users what these options can be used for. This fixes #1448. * Fix clippy lints and run font tests on travis This fixes some existing clippy issues and runs the `font` tests through travis. Testing of copypasta crate was omitted due to problens when running on headless travis-ci environment (x11 clipboard would fail). * Ignore errors when logger can't write to output The (e)print macro will panic when there is no output available to write to, however in our scenario where we only log user errors to stderr, the better choice would be to ignore when writing to stdout or stderr is not possible. This changes the (e)print macro to make use of `write` and ignore any potential errors. Since (e)println rely on (e)print, this also solves potential failuers when calling (e)println. With this change implemented, all of logging, (e)println and (e)print should never fail even if the stdout/stderr is not available.
2018-07-28 23:10:13 +00:00
if ansi == NamedColor::Foreground
&& config.colors.primary.bright_foreground.is_none() =>
Merge master into scrollback * Allow disabling DPI scaling This makes it possible to disable DPI scaling completely, instead the the display pixel ration will always be fixed to 1.0. By default nothing has changed and DPI is still enabled, this just seems like a better way than running `WINIT_HIDPI_FACTOR=1.0 alacritty` every time the user wants to start alacritty. It would be possible to allow specifying any DPR, however I've decided against this since I'd assume it's a very rare usecase. It's also still possible to make use of `WINIT_HIDPI_FACTOR` to do this on X11. Currently this is not updated at runtime using the live config update, there is not really much of a technical limitation why this woudn't be possible, however a solution for that issue should be first added in jwilm/alacritty#1346, once a system is established for changing DPI at runtime, porting that functionality to this PR should be simple. * Add working --class and --title CLI parameters * Reduce Increase-/DecreaseFontSize step to 0.5 Until now the Increase-/DecreaseFontSize keybinds hand a step size of 1.0. Since the font size however is multiplied by two to allow more granular font size control, this lead to the bindings skipping one font size (incrementing/decrementing by +-2). To fix this the step size of the Increase-/DecreaseFontSize bindings has been reduced to the minimum step size that exists with the current font configuration (0.5). This should allow users to increment and decrement the font size by a single point instead of two. This also adds a few tests to make sure the methods for increasing/decreasing/resetting font size work properly. * Add Copy/Cut/Paste keys This just adds support for the Copy/Cut/Paste keys and sets up Copy/Paste as alternative defaults for Ctrl+Shift+C/V. * Move to cargo clippy Using clippy as a library has been deprecated, instead the `cargo clippy` command should be used instead. To comply with this change clippy has been removed from the `Cargo.toml` and is now installed with cargo when building in CI. This has also lead to a few new clippy issues to show up, this includes everything in the `font` subdirectory. This has been fixed and `font` should now be covered by clippy CI too. This also upgrades all dependencies, as a result this fixes #1341 and this fixes #1344. * Override dynamic_title when --title is specified * Change green implementation to use the macro * Ignore mouse input if window is unfocused * Make compilation of binary a phony target * Add opensuse zypper install method to readme * Fix clippy issues * Update manpage to document all CLI options The introduction of `--class` has added a flag to the CLI without adding it to the manpage. This has been fixed by updating the manpage. This also adds the default values of `--class` and `--title` to the CLI options. * Remove unnecessary clippy lint annotations We moved to "cargo clippy" in 5ba34d4f9766a55a06ed5e3e44cc384af1b09f65 and removing the clippy lint annotations in `src/lib.rs` does not cause any additional warnings. This also changes `cargo clippy` to use the flags required for checking integration tests. * Enable clippy in font/copypasta crates Enabled clippy in the sub-crates font and copypasta. All issues that were discovered by this change have also been fixed. * Remove outdated comment about NixOS * Replace debug asserts with static_assertions To check that transmutes will work correctly without having to rely on error-prone runtime checking, the `static_assertions` crate has been introduced. This allows comparing the size of types at compile time, preventing potentially silent breakage. This fixes #1417. * Add `cargo deb` build instructions Updated the `Cargo.toml` file and added a `package.metadata.deb` subsection to define how to build a debian "deb" install file using `cargo deb`. This will allow debian/ubuntu users to install `alacritty` using their system's package manager. It also will make it easier to provide pre-built binaries for those systems. Also fixed a stray debug line in the bash autocomplete script that was writting to a tempfile. * Add config for unfocused window cursor change * Add support for cursor shape escape sequence * Add bright foreground color option It was requested in jwilm/alacritty#825 that it should be possible to add an optional bright foreground color. This is now added to the primary colors structure and allows the user to set a foreground color for bold normal text. This has no effect unless the draw_bold_text_with_bright_colors option is also enabled. If the color is not specified, the bright foreground color will fall back to the normal foreground color. This fixes #825. * Fix clone URL in deb install instructions * Fix 'cargo-deb' desktop file name * Remove redundant dependency from deb build * Switch from deprecated `std::env::home_dir` to `dirs::home_dir` * Allow specifying modifiers for mouse bindings * Send newline with NumpadEnter * Add support for LCD-V pixel mode * Add binding action for hiding the window * Switch to rustup clippy component * Add optional dim foreground color Add optional color for the dim foreground (`\e[2m;`) Defaults to 2/3 of the foreground color. (same as other colors). If a bright color is dimmed, it's displayed as the normal color. The exception for this is when the bright foreground is dimmed when no bright foreground color is set. In that case it's treated as a normal foreground color and dimmed to DimForeground. To minimize the surprise for the user, the bright and dim colors have been completely removed from the default configuration file. Some documentation has also been added to make it clear to users what these options can be used for. This fixes #1448. * Fix clippy lints and run font tests on travis This fixes some existing clippy issues and runs the `font` tests through travis. Testing of copypasta crate was omitted due to problens when running on headless travis-ci environment (x11 clipboard would fail). * Ignore errors when logger can't write to output The (e)print macro will panic when there is no output available to write to, however in our scenario where we only log user errors to stderr, the better choice would be to ignore when writing to stdout or stderr is not possible. This changes the (e)print macro to make use of `write` and ignore any potential errors. Since (e)println rely on (e)print, this also solves potential failuers when calling (e)println. With this change implemented, all of logging, (e)println and (e)print should never fail even if the stdout/stderr is not available.
2018-07-28 23:10:13 +00:00
{
colors[NamedColor::DimForeground]
2019-03-30 16:48:36 +00:00
},
// Draw bold text in bright colors *and* contains bold flag.
(true, Flags::BOLD) => colors[ansi.to_bright()],
2020-05-05 22:50:23 +00:00
// Cell is marked as dim and not bold.
(_, Flags::DIM) | (false, Flags::DIM_BOLD) => colors[ansi.to_dim()],
2020-05-05 22:50:23 +00:00
// None of the above, keep original color..
_ => colors[ansi],
}
},
Color::Indexed(idx) => {
let idx = match (
config.draw_bold_text_with_bright_colors(),
flags & Flags::DIM_BOLD,
2019-03-30 16:48:36 +00:00
idx,
) {
(true, Flags::BOLD, 0..=7) => idx as usize + 8,
(false, Flags::DIM, 8..=15) => idx as usize - 8,
(false, Flags::DIM, 0..=7) => idx as usize + 260,
_ => idx as usize,
};
colors[idx]
2019-03-30 16:48:36 +00:00
},
}
}
/// Compute background alpha based on cell's original color.
///
/// Since an RGB color matching the background should not be transparent, this is computed
/// using the named input color, rather than checking the RGB of the background after its color
/// is computed.
#[inline]
fn compute_bg_alpha(bg: Color) -> f32 {
if bg == Color::Named(NamedColor::Background) {
0.
} else {
1.
}
}
#[inline]
fn compute_bg_rgb(colors: &color::List, bg: Color) -> Rgb {
match bg {
Color::Spec(rgb) => rgb,
Color::Named(ansi) => colors[ansi],
Color::Indexed(idx) => colors[idx],
}
}
}
impl<'a, C> Iterator for RenderableCellsIter<'a, C> {
type Item = RenderableCell;
2020-05-05 22:50:23 +00:00
/// Gets the next renderable cell.
///
/// Skips empty (background) cells and applies any flags to the cell state
/// (eg. invert fg and bg colors).
#[inline]
fn next(&mut self) -> Option<Self::Item> {
loop {
if self.cursor.point.line == self.inner.line()
&& self.cursor.point.col == self.inner.column()
{
if self.cursor.rendered {
// Handle cell below cursor.
let cell = self.inner.next()?;
let mut cell = RenderableCell::new(self, cell);
2019-06-06 00:02:20 +00:00
if self.cursor.key.style == CursorStyle::Block {
cell.fg = match self.cursor.cursor_color {
// Apply cursor color, or invert the cursor if it has a fixed background
// close to the cell's background.
CellRgb::Rgb(col) if col.contrast(cell.bg) < MIN_CURSOR_CONTRAST => {
cell.bg
},
_ => self.cursor.text_color.color(cell.fg, cell.bg),
};
}
return Some(cell);
} else {
2020-05-05 22:50:23 +00:00
// Handle cursor.
self.cursor.rendered = true;
let buffer_point = self.grid.visible_to_buffer(self.cursor.point);
let cell = Indexed {
inner: &self.grid[buffer_point.line][buffer_point.col],
column: self.cursor.point.col,
line: self.cursor.point.line,
};
2019-06-06 00:02:20 +00:00
let mut cell = RenderableCell::new(self, cell);
cell.inner = RenderableCellContent::Cursor(self.cursor.key);
// Apply cursor color, or invert the cursor if it has a fixed background close
// to the cell's background.
if !matches!(
self.cursor.cursor_color,
CellRgb::Rgb(color) if color.contrast(cell.bg) < MIN_CURSOR_CONTRAST
) {
cell.fg = self.cursor.cursor_color.color(cell.fg, cell.bg);
}
return Some(cell);
}
} else {
let cell = self.inner.next()?;
let cell = RenderableCell::new(self, cell);
if !cell.is_empty() {
return Some(cell);
}
}
}
}
}
pub mod mode {
use bitflags::bitflags;
bitflags! {
pub struct TermMode: u32 {
2017-08-30 18:43:37 +00:00
const NONE = 0;
const SHOW_CURSOR = 0b0000_0000_0000_0000_0001;
const APP_CURSOR = 0b0000_0000_0000_0000_0010;
const APP_KEYPAD = 0b0000_0000_0000_0000_0100;
const MOUSE_REPORT_CLICK = 0b0000_0000_0000_0000_1000;
const BRACKETED_PASTE = 0b0000_0000_0000_0001_0000;
const SGR_MOUSE = 0b0000_0000_0000_0010_0000;
const MOUSE_MOTION = 0b0000_0000_0000_0100_0000;
const LINE_WRAP = 0b0000_0000_0000_1000_0000;
const LINE_FEED_NEW_LINE = 0b0000_0000_0001_0000_0000;
const ORIGIN = 0b0000_0000_0010_0000_0000;
const INSERT = 0b0000_0000_0100_0000_0000;
const FOCUS_IN_OUT = 0b0000_0000_1000_0000_0000;
const ALT_SCREEN = 0b0000_0001_0000_0000_0000;
const MOUSE_DRAG = 0b0000_0010_0000_0000_0000;
const MOUSE_MODE = 0b0000_0010_0000_0100_1000;
const UTF8_MOUSE = 0b0000_0100_0000_0000_0000;
const ALTERNATE_SCROLL = 0b0000_1000_0000_0000_0000;
const VI = 0b0001_0000_0000_0000_0000;
const URGENCY_HINTS = 0b0010_0000_0000_0000_0000;
const ANY = std::u32::MAX;
}
}
impl Default for TermMode {
fn default() -> TermMode {
TermMode::SHOW_CURSOR
| TermMode::LINE_WRAP
| TermMode::ALTERNATE_SCROLL
| TermMode::URGENCY_HINTS
}
}
}
2019-04-19 20:56:11 +00:00
pub use crate::term::mode::TermMode;
pub struct VisualBell {
2020-05-05 22:50:23 +00:00
/// Visual bell animation.
animation: BellAnimation,
2020-05-05 22:50:23 +00:00
/// Visual bell duration.
duration: Duration,
2020-05-05 22:50:23 +00:00
/// The last time the visual bell rang, if at all.
start_time: Option<Instant>,
}
fn cubic_bezier(p0: f64, p1: f64, p2: f64, p3: f64, x: f64) -> f64 {
2019-03-30 16:48:36 +00:00
(1.0 - x).powi(3) * p0
+ 3.0 * (1.0 - x).powi(2) * x * p1
+ 3.0 * (1.0 - x) * x.powi(2) * p2
+ x.powi(3) * p3
}
impl VisualBell {
/// Ring the visual bell, and return its intensity.
pub fn ring(&mut self) -> f64 {
let now = Instant::now();
self.start_time = Some(now);
self.intensity_at_instant(now)
}
2017-10-30 15:03:58 +00:00
/// Get the currently intensity of the visual bell. The bell's intensity
/// ramps down from 1.0 to 0.0 at a rate determined by the bell's duration.
pub fn intensity(&self) -> f64 {
self.intensity_at_instant(Instant::now())
}
/// Check whether or not the visual bell has completed "ringing".
pub fn completed(&mut self) -> bool {
match self.start_time {
Some(earlier) => {
if Instant::now().duration_since(earlier) >= self.duration {
self.start_time = None;
}
false
},
2019-03-30 16:48:36 +00:00
None => true,
}
}
/// Get the intensity of the visual bell at a particular instant. The bell's
/// intensity ramps down from 1.0 to 0.0 at a rate determined by the bell's
/// duration.
pub fn intensity_at_instant(&self, instant: Instant) -> f64 {
// If `duration` is zero, then the VisualBell is disabled; therefore,
// its `intensity` is zero.
if self.duration == Duration::from_secs(0) {
return 0.0;
}
match self.start_time {
// Similarly, if `start_time` is `None`, then the VisualBell has not
// been "rung"; therefore, its `intensity` is zero.
None => 0.0,
Some(earlier) => {
// Finally, if the `instant` at which we wish to compute the
// VisualBell's `intensity` occurred before the VisualBell was
// "rung", then its `intensity` is also zero.
if instant < earlier {
return 0.0;
}
let elapsed = instant.duration_since(earlier);
2019-03-30 16:48:36 +00:00
let elapsed_f =
elapsed.as_secs() as f64 + f64::from(elapsed.subsec_nanos()) / 1e9f64;
let duration_f = self.duration.as_secs() as f64
+ f64::from(self.duration.subsec_nanos()) / 1e9f64;
// Otherwise, we compute a value `time` from 0.0 to 1.0
// inclusive that represents the ratio of `elapsed` time to the
// `duration` of the VisualBell.
let time = (elapsed_f / duration_f).min(1.0);
// We use this to compute the inverse `intensity` of the
// VisualBell. When `time` is 0.0, `inverse_intensity` is 0.0,
// and when `time` is 1.0, `inverse_intensity` is 1.0.
let inverse_intensity = match self.animation {
BellAnimation::Ease | BellAnimation::EaseOut => {
cubic_bezier(0.25, 0.1, 0.25, 1.0, time)
},
BellAnimation::EaseOutSine => cubic_bezier(0.39, 0.575, 0.565, 1.0, time),
BellAnimation::EaseOutQuad => cubic_bezier(0.25, 0.46, 0.45, 0.94, time),
BellAnimation::EaseOutCubic => cubic_bezier(0.215, 0.61, 0.355, 1.0, time),
BellAnimation::EaseOutQuart => cubic_bezier(0.165, 0.84, 0.44, 1.0, time),
BellAnimation::EaseOutQuint => cubic_bezier(0.23, 1.0, 0.32, 1.0, time),
BellAnimation::EaseOutExpo => cubic_bezier(0.19, 1.0, 0.22, 1.0, time),
BellAnimation::EaseOutCirc => cubic_bezier(0.075, 0.82, 0.165, 1.0, time),
BellAnimation::Linear => time,
};
// Since we want the `intensity` of the VisualBell to decay over
// `time`, we subtract the `inverse_intensity` from 1.0.
1.0 - inverse_intensity
2019-03-30 16:48:36 +00:00
},
}
}
pub fn update_config<C>(&mut self, config: &Config<C>) {
let bell_config = config.bell();
self.animation = bell_config.animation;
self.duration = bell_config.duration();
}
}
impl From<&BellConfig> for VisualBell {
fn from(bell_config: &BellConfig) -> VisualBell {
VisualBell {
animation: bell_config.animation,
duration: bell_config.duration(),
start_time: None,
}
}
}
/// Terminal size info.
#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq)]
pub struct SizeInfo {
/// Terminal window width.
width: f32,
/// Terminal window height.
height: f32,
/// Width of individual cell.
cell_width: f32,
/// Height of individual cell.
cell_height: f32,
/// Horizontal window padding.
padding_x: f32,
/// Horizontal window padding.
padding_y: f32,
/// Number of lines in the viewport.
screen_lines: Line,
/// Number of columns in the viewport.
cols: Column,
}
impl SizeInfo {
#[allow(clippy::too_many_arguments)]
pub fn new(
width: f32,
height: f32,
cell_width: f32,
cell_height: f32,
mut padding_x: f32,
mut padding_y: f32,
dynamic_padding: bool,
) -> SizeInfo {
if dynamic_padding {
padding_x = Self::dynamic_padding(padding_x.floor(), width, cell_width);
padding_y = Self::dynamic_padding(padding_y.floor(), height, cell_height);
}
let lines = (height - 2. * padding_y) / cell_height;
let screen_lines = Line(max(lines as usize, MIN_SCREEN_LINES));
let cols = (width - 2. * padding_x) / cell_width;
let cols = Column(max(cols as usize, MIN_COLS));
SizeInfo {
width,
height,
cell_width,
cell_height,
padding_x: padding_x.floor(),
padding_y: padding_y.floor(),
screen_lines,
cols,
}
}
#[inline]
pub fn reserve_lines(&mut self, count: usize) {
self.screen_lines = Line(max(self.screen_lines.saturating_sub(count), MIN_SCREEN_LINES));
}
/// Check if coordinates are inside the terminal grid.
///
/// The padding, message bar or search are not counted as part of the grid.
#[inline]
pub fn contains_point(&self, x: usize, y: usize) -> bool {
x <= (self.padding_x + self.cols.0 as f32 * self.cell_width) as usize
&& x > self.padding_x as usize
&& y <= (self.padding_y + self.screen_lines.0 as f32 * self.cell_height) as usize
&& y > self.padding_y as usize
}
/// Convert window space pixels to terminal grid coordinates.
///
/// If the coordinates are outside of the terminal grid, like positions inside the padding, the
/// coordinates will be clamped to the closest grid coordinates.
pub fn pixels_to_coords(&self, x: usize, y: usize) -> Point {
let col = Column(x.saturating_sub(self.padding_x as usize) / (self.cell_width as usize));
let line = Line(y.saturating_sub(self.padding_y as usize) / (self.cell_height as usize));
Point {
line: min(line, Line(self.screen_lines.saturating_sub(1))),
col: min(col, Column(self.cols.saturating_sub(1))),
}
}
#[inline]
pub fn width(&self) -> f32 {
self.width
}
#[inline]
pub fn height(&self) -> f32 {
self.height
}
#[inline]
pub fn cell_width(&self) -> f32 {
self.cell_width
}
#[inline]
pub fn cell_height(&self) -> f32 {
self.cell_height
}
#[inline]
pub fn padding_x(&self) -> f32 {
self.padding_x
}
#[inline]
pub fn padding_y(&self) -> f32 {
self.padding_y
}
#[inline]
pub fn screen_lines(&self) -> Line {
self.screen_lines
}
#[inline]
pub fn cols(&self) -> Column {
self.cols
}
/// Calculate padding to spread it evenly around the terminal content.
#[inline]
fn dynamic_padding(padding: f32, dimension: f32, cell_dimension: f32) -> f32 {
padding + ((dimension - 2. * padding) % cell_dimension) / 2.
}
}
pub struct Term<T> {
/// Terminal requires redraw.
pub dirty: bool,
/// Visual bell configuration and status.
pub visual_bell: VisualBell,
/// 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,
/// Colors used for rendering.
colors: color::List,
/// Is color in `colors` modified or not.
color_modified: [bool; color::COUNT],
/// Original colors from config.
original_colors: color::List,
/// Current style of the cursor.
cursor_style: Option<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>>,
/// Current forward and backward buffer search regexes.
regex_search: Option<RegexSearch>,
/// Information about cell dimensions.
cell_width: usize,
cell_height: usize,
}
impl<T> Term<T> {
#[inline]
pub fn scroll_display(&mut self, scroll: Scroll)
where
T: EventListener,
{
self.grid.scroll_display(scroll);
self.event_proxy.send_event(Event::MouseCursorDirty);
self.dirty = true;
2018-02-17 01:33:32 +00:00
}
pub fn new<C>(config: &Config<C>, size: SizeInfo, event_proxy: T) -> Term<T> {
let num_cols = size.cols;
let num_lines = size.screen_lines;
let history_size = config.scrolling.history() as usize;
let grid = Grid::new(num_lines, num_cols, history_size);
let alt = Grid::new(num_lines, num_cols, 0);
let tabs = TabStops::new(grid.cols());
let scroll_region = Line(0)..grid.screen_lines();
let colors = color::List::from(&config.colors);
Term {
dirty: false,
visual_bell: config.bell().into(),
grid,
inactive_grid: alt,
active_charset: Default::default(),
vi_mode_cursor: Default::default(),
tabs,
mode: Default::default(),
scroll_region,
colors,
color_modified: [false; color::COUNT],
original_colors: colors,
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,
regex_search: None,
cell_width: size.cell_width as usize,
cell_height: size.cell_height as usize,
}
}
pub fn update_config<C>(&mut self, config: &Config<C>)
where
T: EventListener,
{
self.semantic_escape_chars = config.selection.semantic_escape_chars().to_owned();
self.original_colors.fill_named(&config.colors);
self.original_colors.fill_cube(&config.colors);
self.original_colors.fill_gray_ramp(&config.colors);
for i in 0..color::COUNT {
if !self.color_modified[i] {
self.colors[i] = self.original_colors[i];
}
}
self.visual_bell.update_config(config);
if let Some(0) = config.scrolling.faux_multiplier() {
self.mode.remove(TermMode::ALTERNATE_SCROLL);
}
self.default_cursor_style = config.cursor.style;
self.vi_mode_cursor_style = config.cursor.vi_mode_style;
let title_event = match &self.title {
Some(title) => Event::Title(title.clone()),
None => Event::ResetTitle,
};
self.event_proxy.send_event(title_event);
if self.mode.contains(TermMode::ALT_SCREEN) {
self.inactive_grid.update_history(config.scrolling.history() as usize);
} else {
self.grid.update_history(config.scrolling.history() as usize);
}
}
/// Convert the active selection to a String.
pub fn selection_to_string(&self) -> Option<String> {
let selection_range = self.selection.as_ref().and_then(|s| s.to_range(self))?;
let SelectionRange { start, end, is_block } = selection_range;
let mut res = String::new();
if is_block {
for line in (end.line + 1..=start.line).rev() {
res += &self.line_to_string(line, start.col..end.col, start.col.0 != 0);
2020-05-05 22:50:23 +00:00
// If the last column is included, newline is appended automatically.
if end.col != self.cols() - 1 {
res += "\n";
}
}
res += &self.line_to_string(end.line, start.col..end.col, true);
} else {
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<usize>, end: Point<usize>) -> String {
let mut res = String::new();
for line in (end.line..=start.line).rev() {
let start_col = if line == start.line { start.col } else { Column(0) };
let end_col = if line == end.line { end.col } else { self.cols() - 1 };
res += &self.line_to_string(line, start_col..end_col, line == end.line);
}
res
}
/// Convert a single line in the grid to a String.
fn line_to_string(
&self,
line: usize,
mut cols: Range<Column>,
include_wrapped_wide: bool,
) -> String {
let mut text = String::new();
let grid_line = &self.grid[line];
let line_length = min(grid_line.line_length(), cols.end + 1);
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 col in IndexRange::from(cols.start..line_length) {
let cell = &grid_line[col];
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[col] {
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.cols() - 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.cols()
&& line_length.0 >= 2
&& grid_line[line_length - 1].flags.contains(Flags::LEADING_WIDE_CHAR_SPACER)
&& include_wrapped_wide
{
text.push(self.grid[line - 1][Column(0)].c);
}
text
}
pub fn visible_to_buffer(&self, point: Point) -> Point<usize> {
self.grid.visible_to_buffer(point)
}
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
/// Iterate over the *renderable* cells in the terminal.
///
/// A renderable cell is any cell which has content other than the default
/// background color. Cells with an alternate background color are
/// considered renderable as are cells with any text content.
pub fn renderable_cells<'b, C>(&'b self, config: &'b Config<C>) -> RenderableCellsIter<'_, C> {
let selection = self.selection.as_ref().and_then(|s| s.to_range(self));
2018-07-28 23:16:28 +00:00
RenderableCellsIter::new(&self, config, selection)
}
2020-05-05 22:50:23 +00:00
/// Resize terminal to new dimensions.
pub fn resize(&mut self, size: SizeInfo) {
self.cell_width = size.cell_width as usize;
self.cell_height = size.cell_height as usize;
let old_cols = self.cols();
let old_lines = self.screen_lines();
let num_cols = size.cols;
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);
// 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() {
// Move the selection if only number of lines changed.
let delta = if num_lines > old_lines {
(num_lines - old_lines.0).saturating_sub(self.history_size()) as isize
} else {
let cursor_line = self.grid.cursor.point.line;
-(min(old_lines - cursor_line - 1, old_lines - num_lines).0 as isize)
};
self.selection = selection.rotate(self, &(Line(0)..num_lines), 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.
self.vi_mode_cursor.point.col = min(self.vi_mode_cursor.point.col, num_cols - 1);
self.vi_mode_cursor.point.line = min(self.vi_mode_cursor.point.line, num_lines - 1);
// Reset scrolling region.
self.scroll_region = Line(0)..self.screen_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.
let bg = self.inactive_grid.cursor.template.bg;
self.inactive_grid.region_mut(..).each(|cell| *cell = bg.into());
}
mem::swap(&mut self.grid, &mut self.inactive_grid);
self.mode ^= TermMode::ALT_SCREEN;
self.selection = None;
}
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: Line) {
trace!("Scrolling down relative: origin={}, lines={}", origin, lines);
let num_lines = self.screen_lines();
lines = min(lines, self.scroll_region.end - self.scroll_region.start);
lines = min(lines, self.scroll_region.end - origin);
let region = origin..self.scroll_region.end;
let absolute_region = (num_lines - region.end)..(num_lines - region.start);
// Scroll selection.
self.selection = self
.selection
.take()
.and_then(|s| s.rotate(self, &absolute_region, -(lines.0 as isize)));
// Scroll between origin and bottom
self.grid.scroll_down(&region, lines);
}
/// Scroll screen up
///
/// Text moves up; clear at top
/// Expects origin to be in scroll range.
#[inline]
fn scroll_up_relative(&mut self, origin: Line, mut lines: Line) {
trace!("Scrolling up relative: origin={}, lines={}", origin, lines);
let num_lines = self.screen_lines();
lines = min(lines, self.scroll_region.end - self.scroll_region.start);
let region = origin..self.scroll_region.end;
let absolute_region = (num_lines - region.end)..(num_lines - region.start);
// Scroll selection.
self.selection =
self.selection.take().and_then(|s| s.rotate(self, &absolute_region, lines.0 as isize));
2020-05-05 22:50:23 +00:00
// Scroll from origin to bottom less number of lines.
self.grid.scroll_up(&region, lines);
}
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.
let bg = self.grid.cursor.template.bg;
self.grid.region_mut(..).each(|cell| *cell = bg.into());
}
#[inline]
pub fn background_color(&self) -> Rgb {
self.colors[NamedColor::Background]
}
#[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) {
self.mode ^= TermMode::VI;
let vi_mode = self.mode.contains(TermMode::VI);
// Do not clear selection when entering search.
if self.regex_search.is_none() || !vi_mode {
self.selection = None;
}
if vi_mode {
// Reset vi mode cursor position to match primary cursor.
let cursor = self.grid.cursor.point;
let line = min(cursor.line + self.grid.display_offset(), self.screen_lines() - 1);
self.vi_mode_cursor = ViModeCursor::new(Point::new(line, cursor.col));
} else {
self.cancel_search();
}
self.dirty = true;
}
/// 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();
self.dirty = true;
}
/// Move vi cursor to absolute point in grid.
#[inline]
pub fn vi_goto_point(&mut self, point: Point<usize>)
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 = self.grid.clamp_buffer_to_visible(point);
self.vi_mode_recompute_selection();
self.dirty = true;
}
/// 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;
}
let viewport_point = self.visible_to_buffer(self.vi_mode_cursor.point);
// Update only if non-empty selection is present.
let selection = match &mut self.selection {
Some(selection) if !selection.is_empty() => selection,
_ => return,
};
selection.update(viewport_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<usize>)
where
T: EventListener,
{
let display_offset = self.grid.display_offset();
let num_lines = self.screen_lines().0;
if point.line >= display_offset + num_lines {
let lines = point.line.saturating_sub(display_offset + num_lines - 1);
self.scroll_display(Scroll::Delta(lines as isize));
} else if point.line < display_offset {
let lines = display_offset.saturating_sub(point.line);
self.scroll_display(Scroll::Delta(-(lines as isize)));
}
}
/// Jump to the end of a wide cell.
pub fn expand_wide(&self, mut point: Point<usize>, direction: Direction) -> Point<usize> {
let flags = self.grid[point.line][point.col].flags;
match direction {
Direction::Right if flags.contains(Flags::LEADING_WIDE_CHAR_SPACER) => {
point.col = Column(1);
point.line -= 1;
},
Direction::Right if flags.contains(Flags::WIDE_CHAR) => point.col += 1,
Direction::Left if flags.intersects(Flags::WIDE_CHAR | Flags::WIDE_CHAR_SPACER) => {
if flags.contains(Flags::WIDE_CHAR_SPACER) {
point.col -= 1;
}
let prev = point.sub_absolute(self, Boundary::Clamp, 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
}
/// Insert a linebreak at the current cursor position.
#[inline]
fn wrapline(&mut self)
where
T: EventListener,
{
if !self.mode.contains(TermMode::LINE_WRAP) {
return;
}
trace!("Wrapping input");
self.grid.cursor_cell().flags.insert(Flags::WRAPLINE);
if (self.grid.cursor.point.line + 1) >= self.scroll_region.end {
self.linefeed();
} else {
self.grid.cursor.point.line += 1;
}
self.grid.cursor.point.col = Column(0);
self.grid.cursor.input_needs_wrap = false;
}
/// Write `c` to the cell at the cursor position.
#[inline(always)]
fn write_at_cursor(&mut self, c: char) -> &mut Cell
where
T: EventListener,
{
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 cursor_cell = self.grid.cursor_cell();
cursor_cell.drop_extra();
cursor_cell.c = c;
cursor_cell.fg = fg;
cursor_cell.bg = bg;
cursor_cell.flags = flags;
cursor_cell
}
/// Get rendering information about the active cursor.
fn renderable_cursor<C>(&self, config: &Config<C>) -> RenderableCursor {
let vi_mode = self.mode.contains(TermMode::VI);
2020-05-05 22:50:23 +00:00
// Cursor position.
let mut point = if vi_mode {
self.vi_mode_cursor.point
} else {
let mut point = self.grid.cursor.point;
point.line += self.grid.display_offset();
point
};
2020-05-05 22:50:23 +00:00
// Cursor shape.
let hidden =
!self.mode.contains(TermMode::SHOW_CURSOR) || point.line >= self.screen_lines();
let cursor_style = if hidden && !vi_mode {
point.line = Line(0);
CursorStyle::Hidden
} else if !self.is_focused && config.cursor.unfocused_hollow() {
CursorStyle::HollowBlock
} else {
let cursor_style = self.cursor_style.unwrap_or(self.default_cursor_style);
if vi_mode {
self.vi_mode_cursor_style.unwrap_or(cursor_style)
} else {
cursor_style
}
};
2020-05-05 22:50:23 +00:00
// Cursor colors.
let color = if vi_mode { config.colors.vi_mode_cursor } else { config.colors.cursor };
let cursor_color = if self.color_modified[NamedColor::Cursor as usize] {
CellRgb::Rgb(self.colors[NamedColor::Cursor])
} else {
color.cursor()
};
let text_color = color.text();
2020-05-05 22:50:23 +00:00
// Expand across wide cell when inside wide char or spacer.
let buffer_point = self.visible_to_buffer(point);
let cell = &self.grid[buffer_point.line][buffer_point.col];
let is_wide = if cell.flags.contains(Flags::WIDE_CHAR_SPACER) {
point.col -= 1;
true
} else {
cell.flags.contains(Flags::WIDE_CHAR)
};
RenderableCursor {
text_color,
cursor_color,
key: CursorKey { style: cursor_style, is_wide },
point,
rendered: false,
}
}
}
impl<T> Dimensions for Term<T> {
#[inline]
fn cols(&self) -> Column {
self.grid.cols()
}
#[inline]
fn screen_lines(&self) -> Line {
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 {
let mut col = self.grid.cursor.point.col.0.saturating_sub(1);
let line = self.grid.cursor.point.line;
if self.grid[line][Column(col)].flags.contains(Flags::WIDE_CHAR_SPACER) {
col = col.saturating_sub(1);
}
self.grid[line][Column(col)].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();
}
let num_cols = self.cols();
2020-05-05 22:50:23 +00:00
// If in insert mode, first shift cells to the right.
if self.mode.contains(TermMode::INSERT) && self.grid.cursor.point.col + width < num_cols {
let line = self.grid.cursor.point.line;
let col = self.grid.cursor.point.col;
let line = &mut self.grid[line];
let src = line[col..].as_ptr();
let dst = line[(col + width)..].as_mut_ptr();
unsafe {
ptr::copy(src, dst, (num_cols - col - width).0);
}
}
if width == 1 {
self.write_at_cursor(c);
} else {
if self.grid.cursor.point.col + 1 >= num_cols {
if self.mode.contains(TermMode::LINE_WRAP) {
// Insert placeholder before wide char if glyph does not fit in this row.
self.write_at_cursor(' ').flags.insert(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.write_at_cursor(c).flags.insert(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.col += 1;
self.write_at_cursor(' ').flags.insert(Flags::WIDE_CHAR_SPACER);
}
if self.grid.cursor.point.col + 1 < num_cols {
self.grid.cursor.point.col += 1;
} else {
self.grid.cursor.input_needs_wrap = true;
}
}
#[inline]
fn decaln(&mut self) {
trace!("Decalnning");
self.grid.region_mut(..).each(|cell| {
*cell = Cell::default();
cell.c = 'E';
});
}
#[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.screen_lines() - 1)
2017-04-19 05:59:24 +00:00
};
self.grid.cursor.point.line = min(line + y_offset, max_y);
self.grid.cursor.point.col = min(col, self.cols() - 1);
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.col)
}
#[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: Column) {
let cursor = &self.grid.cursor;
let bg = cursor.template.bg;
// Ensure inserting within terminal bounds
let count = min(count, self.cols() - cursor.point.col);
let source = cursor.point.col;
let destination = cursor.point.col + count;
let num_cells = (self.cols() - destination).0;
let line = cursor.point.line;
let row = &mut self.grid[line];
unsafe {
let src = row[source..].as_ptr();
let dst = row[destination..].as_mut_ptr();
ptr::copy(src, dst, num_cells);
}
// 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..destination] {
*cell = bg.into();
}
}
#[inline]
fn move_up(&mut self, lines: Line) {
trace!("Moving up: {}", lines);
let move_to = Line(self.grid.cursor.point.line.0.saturating_sub(lines.0));
self.goto(move_to, self.grid.cursor.point.col)
}
#[inline]
fn move_down(&mut self, lines: Line) {
trace!("Moving down: {}", lines);
let move_to = self.grid.cursor.point.line + lines;
self.goto(move_to, self.grid.cursor.point.col)
}
#[inline]
fn move_forward(&mut self, cols: Column) {
trace!("Moving forward: {}", cols);
let num_cols = self.cols();
self.grid.cursor.point.col = min(self.grid.cursor.point.col + cols, num_cols - 1);
self.grid.cursor.input_needs_wrap = false;
}
#[inline]
fn move_backward(&mut self, cols: Column) {
trace!("Moving backward: {}", cols);
self.grid.cursor.point.col = Column(self.grid.cursor.point.col.saturating_sub(cols.0));
self.grid.cursor.input_needs_wrap = false;
}
#[inline]
fn identify_terminal<W: io::Write>(&mut self, writer: &mut W, intermediate: Option<char>) {
match intermediate {
None => {
trace!("Reporting primary device attributes");
let _ = writer.write_all(b"\x1b[?6c");
},
Some('>') => {
trace!("Reporting secondary device attributes");
let version = version_number(env!("CARGO_PKG_VERSION"));
let _ = writer.write_all(format!("\x1b[>0;{};1c", version).as_bytes());
},
_ => debug!("Unsupported device attributes intermediate"),
}
}
2017-05-07 20:08:23 +00:00
#[inline]
2017-05-08 14:50:34 +00:00
fn device_status<W: io::Write>(&mut self, writer: &mut W, arg: usize) {
trace!("Reporting device status: {}", arg);
2017-05-08 14:50:34 +00:00
match arg {
5 => {
let _ = writer.write_all(b"\x1b[0n");
},
6 => {
let pos = self.grid.cursor.point;
let response = format!("\x1b[{};{}R", pos.line + 1, pos.col + 1);
let _ = writer.write_all(response.as_bytes());
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: Line) {
trace!("Moving down and cr: {}", lines);
let move_to = self.grid.cursor.point.line + lines;
self.goto(move_to, Column(0))
}
#[inline]
fn move_up_and_cr(&mut self, lines: Line) {
trace!("Moving up and cr: {}", lines);
let move_to = Line(self.grid.cursor.point.line.0.saturating_sub(lines.0));
self.goto(move_to, Column(0))
}
/// Insert tab at cursor position.
#[inline]
fn put_tab(&mut self, mut count: i64) {
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.col < self.cols() && 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.col + 1) == self.cols() {
break;
}
self.grid.cursor.point.col += 1;
2017-04-04 12:08:10 +00:00
if self.tabs[self.grid.cursor.point.col] {
2017-04-04 12:08:10 +00:00
break;
}
}
}
}
2020-05-05 22:50:23 +00:00
/// Backspace `count` characters.
#[inline]
fn backspace(&mut self) {
trace!("Backspace");
if self.grid.cursor.point.col > Column(0) {
self.grid.cursor.point.col -= 1;
self.grid.cursor.input_needs_wrap = false;
}
}
2020-05-05 22:50:23 +00:00
/// Carriage return.
#[inline]
fn carriage_return(&mut self) {
trace!("Carriage return");
self.grid.cursor.point.col = Column(0);
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(Line(1));
} else if next < self.screen_lines() {
self.grid.cursor.point.line += 1;
}
}
2020-05-05 22:50:23 +00:00
/// Set current position as a tabstop.
#[inline]
fn bell(&mut self) {
trace!("Bell");
self.visual_bell.ring();
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.col] = true;
}
#[inline]
fn scroll_up(&mut self, lines: Line) {
let origin = self.scroll_region.start;
self.scroll_up_relative(origin, lines);
}
#[inline]
fn scroll_down(&mut self, lines: Line) {
let origin = self.scroll_region.start;
self.scroll_down_relative(origin, lines);
}
#[inline]
fn insert_blank_lines(&mut self, lines: Line) {
trace!("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: Line) {
let origin = self.grid.cursor.point.line;
let lines = min(self.screen_lines() - origin, lines);
trace!("Deleting {} lines", lines);
if lines.0 > 0 && self.scroll_region.contains(&self.grid.cursor.point.line) {
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.col);
let start = cursor.point.col;
let end = min(start + count, self.cols());
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;
let row = &mut self.grid[line];
for cell in &mut row[start..end] {
*cell = bg.into();
}
}
#[inline]
fn delete_chars(&mut self, count: Column) {
let cols = self.cols();
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 = min(count, cols);
let start = cursor.point.col;
let end = min(start + count, cols - 1);
let n = (cols - end).0;
let line = cursor.point.line;
let row = &mut self.grid[line];
unsafe {
let src = row[end..].as_ptr();
let dst = row[start..].as_mut_ptr();
ptr::copy(src, dst, n);
}
// 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 = cols - count;
for cell in &mut row[end..] {
*cell = bg.into();
}
}
#[inline]
fn move_backward_tabs(&mut self, count: i64) {
trace!("Moving backward {} tabs", count);
for _ in 0..count {
let mut col = self.grid.cursor.point.col;
for i in (0..(col.0)).rev() {
if self.tabs[index::Column(i)] {
col = index::Column(i);
break;
}
}
self.grid.cursor.point.col = col;
}
}
#[inline]
fn move_forward_tabs(&mut self, count: i64) {
trace!("[unimplemented] Moving forward {} tabs", count);
}
#[inline]
fn save_cursor_position(&mut self) {
trace!("Saving cursor position");
self.grid.saved_cursor = self.grid.cursor.clone();
}
#[inline]
fn restore_cursor_position(&mut self) {
trace!("Restoring cursor position");
self.grid.cursor = self.grid.saved_cursor.clone();
}
#[inline]
fn clear_line(&mut self, mode: ansi::LineClearMode) {
trace!("Clearing line: {:?}", mode);
let cursor = &self.grid.cursor;
let bg = cursor.template.bg;
let point = cursor.point;
let row = &mut self.grid[point.line];
match mode {
ansi::LineClearMode::Right => {
for cell in &mut row[point.col..] {
*cell = bg.into();
}
},
ansi::LineClearMode::Left => {
for cell in &mut row[..=point.col] {
*cell = bg.into();
}
},
ansi::LineClearMode::All => {
for cell in &mut row[..] {
*cell = bg.into();
}
},
}
let cursor_buffer_line = (self.screen_lines() - self.grid.cursor.point.line - 1).0;
self.selection = self
.selection
.take()
.filter(|s| !s.intersects_range(cursor_buffer_line..=cursor_buffer_line));
}
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);
self.colors[index] = color;
self.color_modified[index] = true;
}
2020-05-05 22:50:23 +00:00
/// Write a foreground/background color escape sequence with the current color.
#[inline]
fn dynamic_color_sequence<W: io::Write>(
&mut self,
writer: &mut W,
code: u8,
index: usize,
terminator: &str,
) {
trace!("Writing escape sequence for dynamic color code {}: color[{}]", code, index);
let color = self.colors[index];
let response = format!(
"\x1b]{};rgb:{1:02x}{1:02x}/{2:02x}{2:02x}/{3:02x}{3:02x}{4}",
code, color.r, color.g, color.b, terminator
);
let _ = writer.write_all(response.as_bytes());
}
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);
self.colors[index] = self.original_colors[index];
self.color_modified[index] = false;
}
/// 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 num_lines = self.screen_lines().0;
let cursor_buffer_line = num_lines - self.grid.cursor.point.line.0 - 1;
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 > Line(1) {
2020-05-05 22:50:23 +00:00
// Fully clear all lines before the current line.
self.grid.region_mut(..cursor.line).each(|cell| *cell = bg.into());
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 = min(cursor.col + 1, self.cols());
for cell in &mut self.grid[cursor.line][..end] {
*cell = bg.into();
2017-02-06 21:13:25 +00:00
}
self.selection = self
.selection
.take()
.filter(|s| !s.intersects_range(cursor_buffer_line..num_lines));
},
ansi::ClearMode::Below => {
let cursor = self.grid.cursor.point;
for cell in &mut self.grid[cursor.line][cursor.col..] {
*cell = bg.into();
}
if cursor.line.0 < num_lines - 1 {
self.grid.region_mut((cursor.line + 1)..).each(|cell| *cell = bg.into());
}
self.selection =
self.selection.take().filter(|s| !s.intersects_range(..=cursor_buffer_line));
},
ansi::ClearMode::All => {
if self.mode.contains(TermMode::ALT_SCREEN) {
self.grid.region_mut(..).each(|cell| *cell = bg.into());
} else {
self.grid.clear_viewport();
}
self.selection = self.selection.take().filter(|s| !s.intersects_range(..num_lines));
},
ansi::ClearMode::Saved if self.history_size() > 0 => {
self.grid.clear_history();
self.selection = self.selection.take().filter(|s| !s.intersects_range(num_lines..));
},
// We have no history to clear.
ansi::ClearMode::Saved => (),
}
}
#[inline]
fn clear_tabs(&mut self, mode: ansi::TabulationClearMode) {
trace!("Clearing tabs: {:?}", mode);
match mode {
ansi::TabulationClearMode::Current => {
self.tabs[self.grid.cursor.point.col] = 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.mode = Default::default();
self.colors = self.original_colors;
self.color_modified = [false; color::COUNT];
self.cursor_style = None;
self.grid.reset();
self.inactive_grid.reset();
self.scroll_region = Line(0)..self.screen_lines();
self.tabs = TabStops::new(self.cols());
self.title_stack = Vec::new();
self.title = None;
self.selection = None;
self.regex_search = None;
}
#[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(Line(1));
} else {
self.grid.cursor.point.line = Line(self.grid.cursor.point.line.saturating_sub(1));
}
}
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::Reset => {
cursor.template.fg = Color::Named(NamedColor::Foreground);
cursor.template.bg = Color::Named(NamedColor::Background);
cursor.template.flags = Flags::empty();
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::DOUBLE_UNDERLINE);
cursor.template.flags.insert(Flags::UNDERLINE);
},
Attr::DoubleUnderline => {
cursor.template.flags.remove(Flags::UNDERLINE);
cursor.template.flags.insert(Flags::DOUBLE_UNDERLINE);
},
Attr::CancelUnderline => {
cursor.template.flags.remove(Flags::UNDERLINE | Flags::DOUBLE_UNDERLINE);
},
Attr::Hidden => cursor.template.flags.insert(Flags::HIDDEN),
Attr::CancelHidden => cursor.template.flags.remove(Flags::HIDDEN),
Attr::Strike => cursor.template.flags.insert(Flags::STRIKEOUT),
Attr::CancelStrike => cursor.template.flags.remove(Flags::STRIKEOUT),
_ => {
debug!("Term got unhandled attr: {:?}", attr);
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::DECCOLM => self.deccolm(),
2020-05-05 22:50:23 +00:00
ansi::Mode::Insert => self.mode.insert(TermMode::INSERT),
ansi::Mode::BlinkingCursor => {
trace!("... unimplemented mode");
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::DECCOLM => self.deccolm(),
2019-04-19 20:56:11 +00:00
ansi::Mode::Insert => self.mode.remove(TermMode::INSERT),
ansi::Mode::BlinkingCursor => {
trace!("... unimplemented mode");
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().0);
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 - 1);
let end = Line(bottom);
trace!("Setting scrolling region: ({};{})", start, end);
self.scroll_region.start = min(start, self.screen_lines());
self.scroll_region.end = min(end, self.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;
}
#[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<W: io::Write>(&mut self, writer: &mut W) {
let width = self.cell_width * self.cols().0;
let height = self.cell_height * self.screen_lines().0;
let _ = write!(writer, "\x1b[4;{};{}t", height, width);
}
#[inline]
fn text_area_size_chars<W: io::Write>(&mut self, writer: &mut W) {
let _ = write!(writer, "\x1b[8;{};{}t", self.screen_lines(), self.cols());
}
}
/// 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(num_cols: Column) -> TabStops {
TabStops {
tabs: IndexRange::from(Column(0)..num_cols)
.map(|i| (*i as usize) % INITIAL_TABSTOPS == 0)
2019-03-30 16:48:36 +00:00
.collect::<Vec<bool>>(),
}
}
/// 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, num_cols: Column) {
let mut index = self.tabs.len();
self.tabs.resize_with(num_cols.0, || {
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 test helpers.
pub mod test {
use super::*;
use unicode_width::UnicodeWidthChar;
use crate::config::Config;
use crate::index::Column;
/// Construct a terminal from its content as string.
///
/// A `\n` will break line and `\r\n` will break line without wrapping.
///
/// # Examples
///
/// ```rust
/// use alacritty_terminal::term::test::mock_term;
///
/// // Create a terminal with the following cells:
/// //
/// // [h][e][l][l][o] <- WRAPLINE flag set
/// // [:][)][ ][ ][ ]
/// // [t][e][s][t][ ]
/// mock_term(
/// "\
/// hello\n:)\r\ntest",
/// );
/// ```
pub fn mock_term(content: &str) -> Term<()> {
let lines: Vec<&str> = content.split('\n').collect();
let num_cols = lines
.iter()
.map(|line| line.chars().filter(|c| *c != '\r').map(|c| c.width().unwrap()).sum())
.max()
.unwrap_or(0);
// Create terminal with the appropriate dimensions.
let size = SizeInfo::new(num_cols as f32, lines.len() as f32, 1., 1., 0., 0., false);
let mut term = Term::new(&Config::<()>::default(), size, ());
// Fill terminal with content.
for (line, text) in lines.iter().rev().enumerate() {
if !text.ends_with('\r') && line != 0 {
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::MockConfig;
use crate::event::{Event, EventListener};
use crate::grid::{Grid, Scroll};
2019-03-30 16:48:36 +00:00
use crate::index::{Column, Line, Point, Side};
use crate::selection::{Selection, SelectionType};
use crate::term::cell::{Cell, Flags};
struct Mock;
impl EventListener for Mock {
fn send_event(&self, _event: Event) {}
}
#[test]
fn semantic_selection_works() {
let size = SizeInfo::new(21.0, 51.0, 3.0, 3.0, 0.0, 0.0, false);
let mut term = Term::new(&MockConfig::default(), size, Mock);
let mut grid: Grid<Cell> = Grid::new(Line(3), Column(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: 2, col: Column(1) },
Side::Left,
));
assert_eq!(term.selection_to_string(), Some(String::from("aa")));
}
{
term.selection = Some(Selection::new(
SelectionType::Semantic,
Point { line: 2, col: Column(4) },
Side::Left,
));
assert_eq!(term.selection_to_string(), Some(String::from("aaa")));
}
{
term.selection = Some(Selection::new(
SelectionType::Semantic,
Point { line: 1, col: Column(1) },
Side::Left,
));
assert_eq!(term.selection_to_string(), Some(String::from("aaa")));
}
}
#[test]
fn line_selection_works() {
let size = SizeInfo::new(21.0, 51.0, 3.0, 3.0, 0.0, 0.0, false);
let mut term = Term::new(&MockConfig::default(), size, Mock);
let mut grid: Grid<Cell> = Grid::new(Line(1), Column(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: 0, col: Column(3) },
Side::Left,
));
assert_eq!(term.selection_to_string(), Some(String::from("\"aa\"a\n")));
}
#[test]
fn selecting_empty_line() {
let size = SizeInfo::new(21.0, 51.0, 3.0, 3.0, 0.0, 0.0, false);
let mut term = Term::new(&MockConfig::default(), size, Mock);
let mut grid: Grid<Cell> = Grid::new(Line(3), Column(3), 0);
for l in 0..3 {
if l != 1 {
for c in 0..3 {
grid[Line(l)][Column(c)].c = 'a';
}
}
}
mem::swap(&mut term.grid, &mut grid);
let mut selection =
Selection::new(SelectionType::Simple, Point { line: 2, col: Column(0) }, Side::Left);
selection.update(Point { line: 0, col: Column(2) }, Side::Right);
term.selection = Some(selection);
assert_eq!(term.selection_to_string(), Some("aaa\n\naaa\n".into()));
}
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(Line(24), Column(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 = SizeInfo::new(21.0, 51.0, 3.0, 3.0, 0.0, 0.0, false);
let mut term = Term::new(&MockConfig::default(), size, Mock);
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 clear_saved_lines() {
let size = SizeInfo::new(21.0, 51.0, 3.0, 3.0, 0.0, 0.0, false);
let mut term = Term::new(&MockConfig::default(), size, Mock);
2020-05-05 22:50:23 +00:00
// Add one line of scrollback.
term.grid.scroll_up(&(Line(0)..Line(1)), Line(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 grow_lines_updates_active_cursor_pos() {
let mut size = SizeInfo::new(100.0, 10.0, 1.0, 1.0, 0.0, 0.0, false);
let mut term = Term::new(&MockConfig::default(), size, Mock);
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.0 = 30;
term.resize(size);
assert_eq!(term.history_size(), 0);
assert_eq!(term.grid.cursor.point, Point::new(Line(19), Column(0)));
}
#[test]
fn grow_lines_updates_inactive_cursor_pos() {
let mut size = SizeInfo::new(100.0, 10.0, 1.0, 1.0, 0.0, 0.0, false);
let mut term = Term::new(&MockConfig::default(), size, Mock);
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.0 = 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 = SizeInfo::new(100.0, 10.0, 1.0, 1.0, 0.0, 0.0, false);
let mut term = Term::new(&MockConfig::default(), size, Mock);
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.0 = 5;
term.resize(size);
assert_eq!(term.history_size(), 15);
assert_eq!(term.grid.cursor.point, Point::new(Line(4), Column(0)));
}
#[test]
fn shrink_lines_updates_inactive_cursor_pos() {
let mut size = SizeInfo::new(100.0, 10.0, 1.0, 1.0, 0.0, 0.0, false);
let mut term = Term::new(&MockConfig::default(), size, Mock);
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.0 = 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 window_title() {
let size = SizeInfo::new(21.0, 51.0, 3.0, 3.0, 0.0, 0.0, false);
let mut term = Term::new(&MockConfig::default(), size, Mock);
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);
}
}
#[cfg(all(test, feature = "bench"))]
mod benches {
extern crate serde_json as json;
2019-03-30 16:48:36 +00:00
extern crate test;
use std::fs;
use std::mem;
use crate::config::MockConfig;
use crate::event::{Event, EventListener};
2019-03-30 16:48:36 +00:00
use crate::grid::Grid;
use super::cell::Cell;
2019-03-30 16:48:36 +00:00
use super::{SizeInfo, Term};
struct Mock;
impl EventListener for Mock {
fn send_event(&self, _event: Event) {}
}
2020-05-05 22:50:23 +00:00
/// Benchmark for the renderable cells iterator.
///
2016-12-17 06:48:04 +00:00
/// The renderable cells iterator yields cells that require work to be
2020-05-05 22:50:23 +00:00
/// displayed (that is, not an empty background cell). This benchmark
2016-12-17 06:48:04 +00:00
/// measures how long it takes to process the whole iterator.
///
2016-12-17 06:48:04 +00:00
/// When this benchmark was first added, it averaged ~78usec on my macbook
/// pro. The total render time for this grid is anywhere between ~1500 and
/// ~2000usec (measured imprecisely with the visual meter).
#[bench]
fn render_iter(b: &mut test::Bencher) {
// Need some realistic grid state; using one of the ref files.
let serialized_grid = fs::read_to_string(concat!(
2019-03-30 16:48:36 +00:00
env!("CARGO_MANIFEST_DIR"),
"/tests/ref/vim_large_window_scroll/grid.json"
))
.unwrap();
let serialized_size = fs::read_to_string(concat!(
2019-03-30 16:48:36 +00:00
env!("CARGO_MANIFEST_DIR"),
"/tests/ref/vim_large_window_scroll/size.json"
))
.unwrap();
let mut grid: Grid<Cell> = json::from_str(&serialized_grid).unwrap();
let size: SizeInfo = json::from_str(&serialized_size).unwrap();
let config = MockConfig::default();
let mut terminal = Term::new(&config, size, Mock);
mem::swap(&mut terminal.grid, &mut grid);
b.iter(|| {
let iter = terminal.renderable_cells(&config);
for cell in iter {
test::black_box(cell);
}
})
}
}