alacritty/alacritty_terminal/src/term/mod.rs

2509 lines
83 KiB
Rust
Raw Normal View History

2016-06-30 03:56:12 +00:00
// Copyright 2016 Joe Wilm, The Alacritty Project Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
//! 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::ops::{Index, IndexMut, Range};
use std::time::{Duration, Instant};
2019-11-11 00:12:14 +00:00
use std::{io, 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, TermInfo,
2019-03-30 16:48:36 +00:00
};
use crate::clipboard::{Clipboard, ClipboardType};
use crate::config::{Config, VisualBellAnimation, DEFAULT_NAME};
use crate::event::{Event, EventListener};
use crate::grid::{
BidirectionalIterator, DisplayIter, Grid, GridCell, IndexRegion, Indexed, Scroll,
};
use crate::index::{self, Column, IndexRange, Line, Point};
use crate::selection::{Selection, SelectionRange};
2019-03-30 16:48:36 +00:00
use crate::term::cell::{Cell, Flags, LineLength};
use crate::term::color::Rgb;
#[cfg(windows)]
use crate::tty;
pub mod cell;
pub mod color;
/// Used to match equal brackets, when performing a bracket-pair selection.
const BRACKET_PAIRS: [(char, char); 4] = [('(', ')'), ('[', ']'), ('{', '}'), ('<', '>')];
/// Max size of the window title stack.
const TITLE_STACK_MAX_DEPTH: usize = 4096;
/// A type that can expand a given point to a region
///
/// Usually this is implemented for some 2-D array type since
/// points are two dimensional indices.
pub trait Search {
/// Find the nearest semantic boundary _to the left_ of provided point.
fn semantic_search_left(&self, _: Point<usize>) -> Point<usize>;
/// Find the nearest semantic boundary _to the point_ of provided point.
fn semantic_search_right(&self, _: Point<usize>) -> Point<usize>;
/// Find the beginning of a line, following line wraps.
fn line_search_left(&self, _: Point<usize>) -> Point<usize>;
/// Find the end of a line, following line wraps.
fn line_search_right(&self, _: Point<usize>) -> Point<usize>;
/// Find the nearest matching bracket.
fn bracket_search(&self, _: Point<usize>) -> Option<Point<usize>>;
}
impl<T> Search for Term<T> {
fn semantic_search_left(&self, mut point: Point<usize>) -> Point<usize> {
Fix scrollback accessing indices out of bounds There have been two instances of the scrollback trying to access indices which were moved out of bounds due to new lines (`yes` command). These have both been fixed. The first instance was during semantic selection, since the logic of limiting the selection start point was moved outside of `compute_index`, it was necessary to add this to semantic selection too. Now semantic selection, line selection and normal selection should all work without crashing when new lines are shoving the selection out of bounds. The other error was with the viewport being outside of the scrollback history. Since the default is to keep the scrollback buffer at its current position when new lines are added, it is possible that the position the scrollback buffer is at is suddenly shoved out of the visible area. To fix this the `display_offset` is now limited to always be an allowed value. If a single line of the viewport is moved out of the history now, the viewport should move down a single line now, so only valid content is displayed, with multiple lines this process is repeated. This fixes #1400. There was another error where the iterator would attempt to iterate before the first line in the history buffer, this was because the bounds of the `prev` iterator weren't setup correctly. The iterator should now properly iterate from the first cell in the terminal until the last one. This also fixes #1406, since these semantic selection errors were partiall related to indexing.
2018-07-02 22:03:04 +00:00
// Limit the starting point to the last line in the history
point.line = min(point.line, self.grid.len() - 1);
let mut iter = self.grid.iter_from(point);
let last_col = self.grid.num_cols() - Column(1);
while let Some(cell) = iter.prev() {
if !cell.flags.intersects(Flags::WIDE_CHAR | Flags::WIDE_CHAR_SPACER)
&& self.semantic_escape_chars.contains(cell.c)
{
break;
}
if iter.point().col == last_col && !cell.flags.contains(Flags::WRAPLINE) {
break; // cut off if on new line or hit escape char
}
point = iter.point();
}
point
}
fn semantic_search_right(&self, mut point: Point<usize>) -> Point<usize> {
Fix scrollback accessing indices out of bounds There have been two instances of the scrollback trying to access indices which were moved out of bounds due to new lines (`yes` command). These have both been fixed. The first instance was during semantic selection, since the logic of limiting the selection start point was moved outside of `compute_index`, it was necessary to add this to semantic selection too. Now semantic selection, line selection and normal selection should all work without crashing when new lines are shoving the selection out of bounds. The other error was with the viewport being outside of the scrollback history. Since the default is to keep the scrollback buffer at its current position when new lines are added, it is possible that the position the scrollback buffer is at is suddenly shoved out of the visible area. To fix this the `display_offset` is now limited to always be an allowed value. If a single line of the viewport is moved out of the history now, the viewport should move down a single line now, so only valid content is displayed, with multiple lines this process is repeated. This fixes #1400. There was another error where the iterator would attempt to iterate before the first line in the history buffer, this was because the bounds of the `prev` iterator weren't setup correctly. The iterator should now properly iterate from the first cell in the terminal until the last one. This also fixes #1406, since these semantic selection errors were partiall related to indexing.
2018-07-02 22:03:04 +00:00
// Limit the starting point to the last line in the history
point.line = min(point.line, self.grid.len() - 1);
let mut iter = self.grid.iter_from(point);
let last_col = self.grid.num_cols() - 1;
while let Some(cell) = iter.next() {
if !cell.flags.intersects(Flags::WIDE_CHAR | Flags::WIDE_CHAR_SPACER)
&& self.semantic_escape_chars.contains(cell.c)
{
break;
}
point = iter.point();
if point.col == last_col && !cell.flags.contains(Flags::WRAPLINE) {
break; // cut off if on new line or hit escape char
}
}
point
}
fn line_search_left(&self, mut point: Point<usize>) -> Point<usize> {
while point.line + 1 < self.grid.len()
&& self.grid[point.line + 1][self.grid.num_cols() - 1].flags.contains(Flags::WRAPLINE)
{
point.line += 1;
}
point.col = Column(0);
point
}
fn line_search_right(&self, mut point: Point<usize>) -> Point<usize> {
while self.grid[point.line][self.grid.num_cols() - 1].flags.contains(Flags::WRAPLINE) {
point.line -= 1;
}
point.col = self.grid.num_cols() - 1;
point
}
fn bracket_search(&self, point: Point<usize>) -> Option<Point<usize>> {
let start_char = self.grid[point.line][point.col].c;
// Find the matching bracket we're looking for
let (forwards, end_char) = BRACKET_PAIRS.iter().find_map(|(open, close)| {
if open == &start_char {
Some((true, *close))
} else if close == &start_char {
Some((false, *open))
} else {
None
}
})?;
let mut iter = self.grid.iter_from(point);
// For every character match that equals the starting bracket, we
// ignore one bracket of the opposite type.
let mut skip_pairs = 0;
loop {
// Check the next cell
let cell = if forwards { iter.next() } else { iter.prev() };
// Break if there are no more cells
let c = match cell {
Some(cell) => cell.c,
None => break,
};
// Check if the bracket matches
if c == end_char && skip_pairs == 0 {
return Some(iter.point());
} else if c == start_char {
skip_pairs += 1;
} else if c == end_char {
skip_pairs -= 1;
}
}
None
}
}
/// A key for caching cursor glyphs
#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash, Deserialize)]
pub struct CursorKey {
pub style: CursorStyle,
pub is_wide: bool,
}
/// 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: &'a Point,
cursor_offset: usize,
cursor_key: Option<CursorKey>,
cursor_style: CursorStyle,
config: &'a Config<C>,
colors: &'a color::List,
selection: Option<SelectionRange<Line>>,
}
impl<'a, C> RenderableCellsIter<'a, C> {
/// Create the renderable cells iterator
///
/// The cursor and terminal mode are required for properly displaying the
/// cursor.
fn new<'b, T>(
term: &'b Term<T>,
config: &'b Config<C>,
selection: Option<SelectionRange>,
mut cursor_style: CursorStyle,
) -> RenderableCellsIter<'b, C> {
let grid = &term.grid;
let num_cols = grid.num_cols();
let num_lines = grid.num_lines();
let cursor_offset = grid.line_to_offset(term.cursor.point.line);
let inner = grid.display_iter();
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), num_cols - 1)
};
// Get on-screen lines of the selection's locations
let start = term.buffer_to_visible(span.start);
let end = term.buffer_to_visible(span.end);
// Clamp visible selection to the viewport
let (mut start, mut end) = match (start, end) {
(Some(start), Some(end)) => (start, end),
(Some(start), None) => {
let end = Point::new(num_lines.0 - 1, num_cols - 1);
(start, end)
},
(None, Some(end)) => {
let start = Point::new(0, Column(0));
(start, end)
},
(None, None) => return None,
};
// 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.into(), end.into(), span.is_block))
});
// Load cursor glyph
let cursor = &term.cursor.point;
2019-04-19 20:56:11 +00:00
let cursor_visible = term.mode.contains(TermMode::SHOW_CURSOR) && grid.contains(cursor);
let cursor_key = if cursor_visible {
let is_wide =
grid[cursor].flags.contains(Flags::WIDE_CHAR) && (cursor.col + 1) < num_cols;
Some(CursorKey { style: cursor_style, is_wide })
2019-04-19 20:56:11 +00:00
} else {
// Use hidden cursor so text will not get inverted
cursor_style = CursorStyle::Hidden;
2019-04-19 20:56:11 +00:00
None
};
RenderableCellsIter {
cursor,
cursor_offset,
grid,
inner,
selection: selection_range,
config,
colors: &term.colors,
cursor_key,
cursor_style,
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,
};
// Point itself is selected
if selection.contains(point.col, point.line) {
return true;
}
let num_cols = self.grid.num_cols().0;
let cell = self.grid[&point];
// Check if wide char's spacers are selected
if cell.flags.contains(Flags::WIDE_CHAR) {
let prevprev = point.sub(num_cols, 2);
let prev = point.sub(num_cols, 1);
let next = point.add(num_cols, 1);
// Check trailing spacer
selection.contains(next.col, next.line)
// Check line-wrapping, leading spacer
|| (self.grid[&prev].flags.contains(Flags::WIDE_CHAR_SPACER)
&& !self.grid[&prevprev].flags.contains(Flags::WIDE_CHAR)
&& selection.contains(prev.col, prev.line))
} else if cell.flags.contains(Flags::WIDE_CHAR_SPACER) {
// Check if spacer's wide char is selected
let prev = point.sub(num_cols, 1);
if self.grid[&prev].flags.contains(Flags::WIDE_CHAR) {
// Check previous cell for trailing spacer
self.is_selected(prev)
} else {
// Check next cell for line-wrapping, leading spacer
self.is_selected(point.add(num_cols, 1))
}
} else {
false
}
}
}
#[derive(Copy, Clone, Debug)]
pub enum RenderableCellContent {
Chars([char; cell::MAX_ZEROWIDTH_CHARS + 1]),
Cursor(CursorKey),
}
#[derive(Copy, Clone, Debug)]
pub struct RenderableCell {
/// 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<C>(
config: &Config<C>,
colors: &color::List,
cell: Indexed<Cell>,
selected: bool,
) -> Self {
// Lookup RGB values
let mut fg_rgb = Self::compute_fg_rgb(config, colors, cell.fg, cell.flags);
let mut bg_rgb = Self::compute_bg_rgb(colors, cell.bg);
let mut bg_alpha = Self::compute_bg_alpha(cell.bg);
let selection_background = config.colors.selection.background;
if let (true, Some(col)) = (selected, selection_background) {
// Override selection background with config colors
bg_rgb = col;
bg_alpha = 1.0;
} else if selected ^ cell.inverse() {
if fg_rgb == bg_rgb && !cell.flags.contains(Flags::HIDDEN) {
// Reveal inversed text when fg/bg is the same
fg_rgb = colors[NamedColor::Background];
bg_rgb = colors[NamedColor::Foreground];
} else {
// Invert cell fg and bg colors
mem::swap(&mut fg_rgb, &mut bg_rgb);
}
bg_alpha = 1.0;
}
// Override selection text with config colors
if let (true, Some(col)) = (selected, config.colors.selection.text) {
fg_rgb = col;
}
RenderableCell {
line: cell.line,
column: cell.column,
inner: RenderableCellContent::Chars(cell.chars()),
fg: fg_rgb,
bg: bg_rgb,
bg_alpha,
flags: cell.flags,
}
}
fn compute_fg_rgb<C>(config: &Config<C>, colors: &color::List, fg: Color, flags: Flags) -> Rgb {
match fg {
Color::Spec(rgb) => rgb,
Color::Named(ansi) => {
match (config.draw_bold_text_with_bright_colors(), flags & 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 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()],
// Cell is marked as dim and not bold
(_, Flags::DIM) | (false, Flags::DIM_BOLD) => colors[ansi.to_dim()],
// 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
},
}
}
#[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;
/// 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_offset == self.inner.offset() && self.inner.column() == self.cursor.col {
let selected = self.is_selected(Point::new(self.cursor.line, self.cursor.col));
2019-06-06 00:02:20 +00:00
// Handle cursor
if let Some(cursor_key) = self.cursor_key.take() {
let cell = Indexed {
inner: self.grid[self.cursor],
column: self.cursor.col,
// Using `self.cursor.line` leads to inconsitent cursor position when
// scrolling. See https://github.com/alacritty/alacritty/issues/2570 for more
// info.
line: self.inner.line(),
};
2019-06-06 00:02:20 +00:00
let mut renderable_cell =
2019-06-06 00:02:20 +00:00
RenderableCell::new(self.config, self.colors, cell, selected);
renderable_cell.inner = RenderableCellContent::Cursor(cursor_key);
if let Some(color) = self.config.cursor_cursor_color() {
renderable_cell.fg = RenderableCell::compute_bg_rgb(self.colors, color);
}
return Some(renderable_cell);
} else {
let mut cell =
2019-06-06 00:02:20 +00:00
RenderableCell::new(self.config, self.colors, self.inner.next()?, selected);
if self.cursor_style == CursorStyle::Block {
std::mem::swap(&mut cell.bg, &mut cell.fg);
if let Some(color) = self.config.cursor_text_color() {
cell.fg = color;
}
}
return Some(cell);
}
} else {
let cell = self.inner.next()?;
let selected = self.is_selected(Point::new(cell.line, cell.column));
if !cell.is_empty() || selected {
return Some(RenderableCell::new(self.config, self.colors, cell, selected));
}
}
}
}
}
pub mod mode {
use bitflags::bitflags;
bitflags! {
2017-08-30 18:43:37 +00:00
pub struct TermMode: u16 {
const SHOW_CURSOR = 0b0000_0000_0000_0001;
const APP_CURSOR = 0b0000_0000_0000_0010;
const APP_KEYPAD = 0b0000_0000_0000_0100;
const MOUSE_REPORT_CLICK = 0b0000_0000_0000_1000;
const BRACKETED_PASTE = 0b0000_0000_0001_0000;
const SGR_MOUSE = 0b0000_0000_0010_0000;
const MOUSE_MOTION = 0b0000_0000_0100_0000;
const LINE_WRAP = 0b0000_0000_1000_0000;
const LINE_FEED_NEW_LINE = 0b0000_0001_0000_0000;
const ORIGIN = 0b0000_0010_0000_0000;
const INSERT = 0b0000_0100_0000_0000;
const FOCUS_IN_OUT = 0b0000_1000_0000_0000;
const ALT_SCREEN = 0b0001_0000_0000_0000;
const MOUSE_DRAG = 0b0010_0000_0000_0000;
const MOUSE_MODE = 0b0010_0000_0100_1000;
const UTF8_MOUSE = 0b0100_0000_0000_0000;
const ALTERNATE_SCROLL = 0b1000_0000_0000_0000;
const ANY = 0b1111_1111_1111_1111;
2017-08-30 18:43:37 +00:00
const NONE = 0;
}
}
impl Default for TermMode {
fn default() -> TermMode {
TermMode::SHOW_CURSOR | TermMode::LINE_WRAP | TermMode::ALTERNATE_SCROLL
}
}
}
2019-04-19 20:56:11 +00:00
pub use crate::term::mode::TermMode;
trait CharsetMapping {
fn map(&self, c: char) -> char {
c
}
}
impl CharsetMapping for StandardCharset {
/// Switch/Map character to the active charset. Ascii is the common case and
/// for that we want to do as little as possible.
#[inline]
fn map(&self, c: char) -> char {
match *self {
StandardCharset::Ascii => c,
2019-03-30 16:48:36 +00:00
StandardCharset::SpecialCharacterAndLineDrawing => match c {
'`' => '◆',
'a' => '▒',
'b' => '\t',
'c' => '\u{000c}',
'd' => '\r',
'e' => '\n',
'f' => '°',
'g' => '±',
'h' => '\u{2424}',
'i' => '\u{000b}',
'j' => '┘',
'k' => '┐',
'l' => '┌',
'm' => '└',
'n' => '┼',
'o' => '⎺',
'p' => '⎻',
'q' => '─',
'r' => '⎼',
's' => '⎽',
't' => '├',
'u' => '┤',
'v' => '┴',
'w' => '┬',
'x' => '│',
'y' => '≤',
'z' => '≥',
'{' => 'π',
'|' => '≠',
'}' => '£',
'~' => '·',
_ => c,
},
}
}
}
#[derive(Default, Copy, Clone)]
struct Charsets([StandardCharset; 4]);
impl Index<CharsetIndex> for Charsets {
type Output = StandardCharset;
2019-03-30 16:48:36 +00:00
fn index(&self, index: CharsetIndex) -> &StandardCharset {
&self.0[index as usize]
}
}
impl IndexMut<CharsetIndex> for Charsets {
fn index_mut(&mut self, index: CharsetIndex) -> &mut StandardCharset {
&mut self.0[index as usize]
}
}
#[derive(Default, Copy, Clone)]
pub struct Cursor {
/// The location of this cursor
2017-07-25 00:54:06 +00:00
pub point: Point,
/// Template cell when using this cursor
template: Cell,
/// Currently configured graphic character sets
charsets: Charsets,
}
pub struct VisualBell {
/// Visual bell animation
animation: VisualBellAnimation,
/// Visual bell duration
duration: Duration,
/// 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 {
pub fn new<C>(config: &Config<C>) -> VisualBell {
let visual_bell_config = &config.visual_bell;
VisualBell {
animation: visual_bell_config.animation,
duration: visual_bell_config.duration(),
start_time: None,
}
}
/// 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 {
VisualBellAnimation::Ease | VisualBellAnimation::EaseOut => {
cubic_bezier(0.25, 0.1, 0.25, 1.0, time)
},
VisualBellAnimation::EaseOutSine => cubic_bezier(0.39, 0.575, 0.565, 1.0, time),
VisualBellAnimation::EaseOutQuad => cubic_bezier(0.25, 0.46, 0.45, 0.94, time),
2019-03-30 16:48:36 +00:00
VisualBellAnimation::EaseOutCubic => {
cubic_bezier(0.215, 0.61, 0.355, 1.0, time)
},
VisualBellAnimation::EaseOutQuart => cubic_bezier(0.165, 0.84, 0.44, 1.0, time),
VisualBellAnimation::EaseOutQuint => cubic_bezier(0.23, 1.0, 0.32, 1.0, time),
VisualBellAnimation::EaseOutExpo => cubic_bezier(0.19, 1.0, 0.22, 1.0, time),
VisualBellAnimation::EaseOutCirc => cubic_bezier(0.075, 0.82, 0.165, 1.0, time),
VisualBellAnimation::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 visual_bell_config = &config.visual_bell;
self.animation = visual_bell_config.animation;
self.duration = visual_bell_config.duration();
}
}
pub struct Term<T> {
/// Terminal focus
pub is_focused: bool,
/// The grid
grid: Grid<Cell>,
/// Tracks if the next call to input will need to first handle wrapping.
/// This is true after the last column is set with the input function. Any function that
/// implicitly sets the line or column needs to set this to false to avoid wrapping twice.
/// input_needs_wrap ensures that cursor.col is always valid for use into indexing into
2017-10-30 15:03:58 +00:00
/// arrays. Without it we would have to sanitize cursor.col every time we used it.
input_needs_wrap: bool,
/// Alternate grid
alt_grid: Grid<Cell>,
/// Alt is active
alt: bool,
/// The cursor
cursor: Cursor,
/// The graphic character set, out of `charsets`, which 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>,
pub dirty: bool,
pub visual_bell: VisualBell,
/// Saved cursor from main grid
cursor_save: Cursor,
/// Saved cursor from alt grid
cursor_save_alt: Cursor,
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,
2019-03-07 20:37:11 +00:00
/// Whether to permit updating the terminal title
dynamic_title: bool,
/// Clipboard access coupled to the active window
clipboard: Clipboard,
/// Proxy for sending events to the event loop
event_proxy: T,
/// Current title of the window
title: String,
/// Stack of saved window titles. When a title is popped from this stack, the `title` for the
/// term is set, and the Glutin window's title attribute is changed through the event listener.
title_stack: Vec<String>,
}
/// Terminal size info
#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq)]
pub struct SizeInfo {
/// Terminal window width
pub width: f32,
/// Terminal window height
pub height: f32,
/// Width of individual cell
pub cell_width: f32,
/// Height of individual cell
pub cell_height: f32,
/// Horizontal window padding
pub padding_x: f32,
/// Horizontal window padding
pub padding_y: f32,
Upgrade Glutin to v0.19.0 Some changes include: • Use the with_hardware_acceleration function on the ContextBuilder to not require the discrete GPU • Remove the LMenu and RMenu virtual key codes (winit 0.16.0 removed these because Windows now generates LAlt and RAlt instead • Replace set_cursor_state with hide_cursor (winit 0.16.0 removed the set_cursor_state function) • Replace GlWindow::hidpi_factor with GlWindow::get_hidpi_factor and change to expecting an f64 • Use the glutin/winit dpi size and position types where possible Glutin's dpi change event has been implemented. All size events now return logical sizes. As a result of that, the logical sizes are translated in the `display::handle_rezize` method so DPI scaling works correctly. When the DPI is changed, the glyph cache is updated to make use of the correct font size again. Moving a window to a different screen which is a different DPI caused a racing condition where the logical size of the event was sent to the `handle_resize` method in `src/display.rs`, however if there was a DPI change event before `handle_resize` is able to process this message, it would incorrectly use the new DPI to scale the resize event. To solve this issue instead of sending the logical size to the `handle_resize` method and then converting it to a physical size in there, the `LogicalSize` of the resize event is transformed into a `PhysicalSize` as soon as it's received. This fixes potential racing conditions since all events are processed in order. The padding has been changed so it's also scaled by DPR. The `scale_with_dpi` config option has been removed. If it's not present a warning will be emitted. The `winit` dependency on Windows has been removed. All interactions with winit in Alacritty are handled through glutin.
2018-11-10 16:08:48 +00:00
/// DPI factor of the current window
#[serde(default)]
pub dpr: f64,
}
impl SizeInfo {
#[inline]
pub fn lines(&self) -> Line {
Line(((self.height - 2. * self.padding_y) / self.cell_height) as usize)
}
#[inline]
pub fn cols(&self) -> Column {
Column(((self.width - 2. * self.padding_x) / self.cell_width) as usize)
}
/// Check if coordinates are inside the terminal grid.
///
/// The padding is not counted as part of the grid.
pub fn contains_point(&self, x: usize, y: usize) -> bool {
x < (self.width - self.padding_x) as usize
&& x >= self.padding_x as usize
&& y < (self.height - self.padding_y) as usize
&& y >= self.padding_y as usize
}
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.lines().saturating_sub(1))),
2019-03-30 16:48:36 +00:00
col: min(col, Column(self.cols().saturating_sub(1))),
}
}
}
impl<T> Term<T> {
pub fn selection(&self) -> &Option<Selection> {
&self.grid.selection
}
pub fn selection_mut(&mut self) -> &mut Option<Selection> {
&mut self.grid.selection
}
#[inline]
pub fn scroll_display(&mut self, scroll: Scroll)
where
T: EventListener,
{
self.event_proxy.send_event(Event::MouseCursorDirty);
self.grid.scroll_display(scroll);
self.dirty = true;
2018-02-17 01:33:32 +00:00
}
pub fn new<C>(
config: &Config<C>,
size: &SizeInfo,
clipboard: Clipboard,
event_proxy: T,
) -> Term<T> {
let num_cols = size.cols();
let num_lines = size.lines();
let history_size = config.scrolling.history() as usize;
let grid = Grid::new(num_lines, num_cols, history_size, Cell::default());
let alt = Grid::new(num_lines, num_cols, 0 /* scroll history */, Cell::default());
let tabspaces = config.tabspaces();
let tabs = TabStops::new(grid.num_cols(), tabspaces);
let scroll_region = Line(0)..grid.num_lines();
let colors = color::List::from(&config.colors);
Term {
dirty: false,
visual_bell: VisualBell::new(config),
input_needs_wrap: false,
grid,
alt_grid: alt,
alt: false,
active_charset: Default::default(),
cursor: Default::default(),
cursor_save: Default::default(),
cursor_save_alt: 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,
dynamic_title: config.dynamic_title(),
clipboard,
event_proxy,
is_focused: true,
title: config.window.title.clone(),
title_stack: Vec::new(),
}
}
pub fn update_config<C>(&mut self, config: &Config<C>) {
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.dynamic_title = config.dynamic_title();
if self.alt {
self.alt_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 = self.grid.selection.clone()?;
let SelectionRange { start, end, is_block } = selection.to_range(self)?;
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);
// 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);
// 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];
// 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.contains(Flags::WIDE_CHAR_SPACER) {
// Push cells primary character
text.push(cell.c);
// Push zero-width characters
for c in (&cell.chars()[1..]).iter().take_while(|c| **c != ' ') {
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');
}
// If wide char is not part of the selection, but leading spacer is, include it
if line_length == self.grid.num_cols()
&& line_length.0 >= 2
&& grid_line[line_length - 1].flags.contains(Flags::WIDE_CHAR_SPACER)
&& !grid_line[line_length - 2].flags.contains(Flags::WIDE_CHAR)
&& 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)
}
pub fn buffer_to_visible(&self, point: impl Into<Point<usize>>) -> Option<Point<usize>> {
self.grid.buffer_to_visible(point)
}
/// Access to the raw grid data structure
///
/// This is a bit of a hack; when the window is closed, the event processor
/// serializes the grid state to a file.
pub fn grid(&self) -> &Grid<Cell> {
&self.grid
}
/// Mutable access for swapping out the grid during tests
#[cfg(test)]
pub fn grid_mut(&mut self) -> &mut Grid<Cell> {
&mut self.grid
}
/// 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.grid.selection.as_ref().and_then(|s| s.to_range(self));
2018-07-28 23:16:28 +00:00
let cursor = if self.is_focused || !config.cursor.unfocused_hollow() {
self.cursor_style.unwrap_or(self.default_cursor_style)
} else {
CursorStyle::HollowBlock
};
RenderableCellsIter::new(&self, config, selection, cursor)
}
/// Resize terminal to new dimensions
pub fn resize(&mut self, size: &SizeInfo) {
let old_cols = self.grid.num_cols();
let old_lines = self.grid.num_lines();
let mut num_cols = size.cols();
let mut num_lines = size.lines();
if old_cols == num_cols && old_lines == num_lines {
debug!("Term::resize dimensions unchanged");
return;
}
self.grid.selection = None;
self.alt_grid.selection = None;
// Should not allow less than 1 col, causes all sorts of checks to be required.
if num_cols <= Column(1) {
num_cols = Column(2);
}
// Should not allow less than 1 line, causes all sorts of checks to be required.
if num_lines <= Line(1) {
num_lines = Line(2);
}
// Scroll up to keep cursor in terminal
if self.cursor.point.line >= num_lines {
let lines = self.cursor.point.line - num_lines + 1;
let template = Cell { bg: self.cursor.template.bg, ..Cell::default() };
self.grid.scroll_up(&(Line(0)..old_lines), lines, &template);
2017-04-20 17:13:28 +00:00
}
// Scroll up alt grid as well
if self.cursor_save_alt.point.line >= num_lines {
let lines = self.cursor_save_alt.point.line - num_lines + 1;
let template = Cell { bg: self.cursor_save_alt.template.bg, ..Cell::default() };
self.alt_grid.scroll_up(&(Line(0)..old_lines), lines, &template);
}
// Move prompt down when growing if scrollback lines are available
if num_lines > old_lines && !self.mode.contains(TermMode::ALT_SCREEN) {
let growage = min(num_lines - old_lines, Line(self.grid.history_size()));
self.cursor.point.line += growage;
}
debug!("New num_cols is {} and num_lines is {}", num_cols, num_lines);
// Resize grids to new size
let is_alt = self.mode.contains(TermMode::ALT_SCREEN);
let alt_cursor_point =
if is_alt { &mut self.cursor_save.point } else { &mut self.cursor_save_alt.point };
self.grid.resize(!is_alt, num_lines, num_cols, &mut self.cursor.point, &Cell::default());
self.alt_grid.resize(is_alt, num_lines, num_cols, alt_cursor_point, &Cell::default());
2017-04-20 17:13:28 +00:00
// Reset scrolling region to new size
self.scroll_region = Line(0)..self.grid.num_lines();
// Ensure cursors are in-bounds.
self.cursor.point.col = min(self.cursor.point.col, num_cols - 1);
self.cursor.point.line = min(self.cursor.point.line, num_lines - 1);
self.cursor_save.point.col = min(self.cursor_save.point.col, num_cols - 1);
self.cursor_save.point.line = min(self.cursor_save.point.line, num_lines - 1);
self.cursor_save_alt.point.col = min(self.cursor_save_alt.point.col, num_cols - 1);
self.cursor_save_alt.point.line = min(self.cursor_save_alt.point.line, num_lines - 1);
// Recreate tabs list
self.tabs.resize(self.grid.num_cols());
}
#[inline]
pub fn mode(&self) -> &TermMode {
&self.mode
}
2017-07-25 00:54:06 +00:00
#[inline]
pub fn cursor(&self) -> &Cursor {
&self.cursor
}
pub fn swap_alt(&mut self) {
if self.alt {
let template = self.cursor.template;
self.grid.region_mut(..).each(|c| c.reset(&template));
}
self.alt = !self.alt;
std::mem::swap(&mut self.grid, &mut self.alt_grid);
}
/// 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);
lines = min(lines, self.scroll_region.end - self.scroll_region.start);
lines = min(lines, self.scroll_region.end - origin);
// Scroll between origin and bottom
let template = Cell { bg: self.cursor.template.bg, ..Cell::default() };
self.grid.scroll_down(&(origin..self.scroll_region.end), lines, &template);
}
/// 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, lines: Line) {
trace!("Scrolling up relative: origin={}, lines={}", origin, lines);
let lines = min(lines, self.scroll_region.end - self.scroll_region.start);
// Scroll from origin to bottom less number of lines
let template = Cell { bg: self.cursor.template.bg, ..Cell::default() };
self.grid.scroll_up(&(origin..self.scroll_region.end), lines, &template);
}
fn deccolm(&mut self)
where
T: EventListener,
{
// Setting 132 column font makes no sense, but run the other side effects
// Clear scrolling region
self.set_scrolling_region(1, self.grid.num_lines().0);
// Clear grid
let template = self.cursor.template;
2017-10-13 03:12:29 +00:00
self.grid.region_mut(..).each(|c| c.reset(&template));
}
#[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);
}
pub fn clipboard(&mut self) -> &mut Clipboard {
&mut self.clipboard
}
/// 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[&self.cursor.point].flags.insert(Flags::WRAPLINE);
if (self.cursor.point.line + 1) >= self.scroll_region.end {
self.linefeed();
} else {
self.cursor.point.line += 1;
}
self.cursor.point.col = Column(0);
self.input_needs_wrap = false;
}
/// Write `c` to the cell at the cursor position.
#[inline]
fn write_at_cursor(&mut self, c: char) -> &mut Cell
where
T: EventListener,
{
let cell = &mut self.grid[&self.cursor.point];
*cell = self.cursor.template;
cell.c = self.cursor.charsets[self.active_charset].map(c);
cell
}
}
impl<T> TermInfo for Term<T> {
#[inline]
fn lines(&self) -> Line {
self.grid.num_lines()
}
#[inline]
fn cols(&self) -> Column {
self.grid.num_cols()
}
}
impl<T: EventListener> Handler for Term<T> {
#[inline]
#[cfg(not(windows))]
fn set_title(&mut self, title: &str) {
if self.dynamic_title {
trace!("Setting window title to '{}'", title);
self.title = title.into();
self.event_proxy.send_event(Event::Title(title.to_owned()));
}
}
#[inline]
#[cfg(windows)]
fn set_title(&mut self, title: &str) {
if self.dynamic_title {
// cmd.exe in winpty: winpty incorrectly sets the title to ' ' instead of
// 'Alacritty' - thus we have to substitute this back to get equivalent
// behaviour as conpty.
//
// The starts_with check is necessary because other shells e.g. bash set a
// different title and don't need Alacritty prepended.
trace!("Setting window title to '{}'", title);
let title = if !tty::is_conpty() && title.starts_with(' ') {
format!("Alacritty {}", title.trim())
} else {
title.to_owned()
};
self.title = title.clone();
2019-12-21 01:18:01 +00:00
self.event_proxy.send_event(Event::Title(title));
}
}
/// A character to be displayed
#[inline]
fn input(&mut self, c: char) {
// Number of cells the char will occupy
let width = match c.width() {
Some(width) => width,
None => return,
};
// Handle zero-width characters
if width == 0 {
let mut col = self.cursor.point.col.0.saturating_sub(1);
let line = self.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_extra(c);
return;
}
// Move cursor to next line
if self.input_needs_wrap {
self.wrapline();
}
let num_cols = self.grid.num_cols();
// If in insert mode, first shift cells to the right
if self.mode.contains(TermMode::INSERT) && self.cursor.point.col + width < num_cols {
let line = self.cursor.point.line;
let col = self.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 {
// Insert extra placeholder before wide char if glyph doesn't fit in this row anymore
if self.cursor.point.col + 1 >= num_cols {
self.write_at_cursor(' ').flags.insert(Flags::WIDE_CHAR_SPACER);
self.wrapline();
}
// 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
// Write spacer to cell following the wide glyph
self.cursor.point.col += 1;
self.write_at_cursor(' ').flags.insert(Flags::WIDE_CHAR_SPACER);
}
if self.cursor.point.col + 1 < num_cols {
self.cursor.point.col += 1;
} else {
self.input_needs_wrap = true;
}
}
#[inline]
fn decaln(&mut self) {
trace!("Decalnning");
let template = Cell { c: 'E', ..Cell::default() };
2019-03-30 16:48:36 +00:00
self.grid.region_mut(..).each(|c| c.reset(&template));
}
#[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.grid.num_lines() - 1)
};
self.cursor.point.line = min(line + y_offset, max_y);
self.cursor.point.col = min(col, self.grid.num_cols() - 1);
self.input_needs_wrap = false;
}
#[inline]
fn goto_line(&mut self, line: Line) {
trace!("Going to line: {}", line);
self.goto(line, self.cursor.point.col)
}
#[inline]
fn goto_col(&mut self, col: Column) {
trace!("Going to column: {}", col);
self.goto(self.cursor.point.line, col)
}
#[inline]
fn insert_blank(&mut self, count: Column) {
// Ensure inserting within terminal bounds
let count = min(count, self.grid.num_cols() - self.cursor.point.col);
let source = self.cursor.point.col;
let destination = self.cursor.point.col + count;
let num_cells = (self.grid.num_cols() - destination).0;
let line = &mut self.grid[self.cursor.point.line];
unsafe {
let src = line[source..].as_ptr();
let dst = line[destination..].as_mut_ptr();
ptr::copy(src, dst, num_cells);
}
// Cells were just moved out towards the end of the line; fill in
// between source and dest with blanks.
for c in &mut line[source..destination] {
c.reset(&self.cursor.template);
}
}
#[inline]
fn move_up(&mut self, lines: Line) {
trace!("Moving up: {}", lines);
2017-04-19 05:59:24 +00:00
let move_to = Line(self.cursor.point.line.0.saturating_sub(lines.0));
self.goto(move_to, self.cursor.point.col)
}
#[inline]
fn move_down(&mut self, lines: Line) {
trace!("Moving down: {}", lines);
2017-04-19 05:59:24 +00:00
let move_to = self.cursor.point.line + lines;
self.goto(move_to, self.cursor.point.col)
}
#[inline]
fn move_forward(&mut self, cols: Column) {
trace!("Moving forward: {}", cols);
self.cursor.point.col = min(self.cursor.point.col + cols, self.grid.num_cols() - 1);
self.input_needs_wrap = false;
}
#[inline]
fn move_backward(&mut self, cols: Column) {
trace!("Moving backward: {}", cols);
self.cursor.point.col -= min(self.cursor.point.col, cols);
self.input_needs_wrap = false;
}
#[inline]
fn identify_terminal<W: io::Write>(&mut self, writer: &mut W) {
trace!("Reporting terminal identity");
let _ = writer.write_all(b"\x1b[?6c");
}
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.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.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.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) {
// A tab after the last column is the same as a linebreak
if self.input_needs_wrap {
self.wrapline();
return;
}
while self.cursor.point.col < self.grid.num_cols() && count != 0 {
count -= 1;
let cell = &mut self.grid[&self.cursor.point];
if cell.c == ' ' {
cell.c = self.cursor.charsets[self.active_charset].map('\t');
}
loop {
if (self.cursor.point.col + 1) == self.grid.num_cols() {
break;
}
self.cursor.point.col += 1;
2017-04-04 12:08:10 +00:00
if self.tabs[self.cursor.point.col] {
2017-04-04 12:08:10 +00:00
break;
}
}
}
}
/// Backspace `count` characters
#[inline]
fn backspace(&mut self) {
trace!("Backspace");
if self.cursor.point.col > Column(0) {
self.cursor.point.col -= 1;
self.input_needs_wrap = false;
}
}
/// Carriage return
#[inline]
fn carriage_return(&mut self) {
trace!("Carriage return");
self.cursor.point.col = Column(0);
self.input_needs_wrap = false;
}
/// Linefeed
#[inline]
fn linefeed(&mut self) {
trace!("Linefeed");
let next = self.cursor.point.line + 1;
if next == self.scroll_region.end {
self.scroll_up(Line(1));
} else if next < self.grid.num_lines() {
self.cursor.point.line += 1;
}
}
/// Set current position as a tabstop
#[inline]
fn bell(&mut self) {
trace!("Bell");
self.visual_bell.ring();
self.event_proxy.send_event(Event::Urgent);
}
#[inline]
fn substitute(&mut self) {
trace!("[unimplemented] Substitute");
}
2017-04-18 17:41:11 +00:00
/// Run LF/NL
///
/// 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");
let column = self.cursor.point.col;
self.tabs[column] = 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);
if self.scroll_region.contains(&self.cursor.point.line) {
let origin = self.cursor.point.line;
self.scroll_down_relative(origin, lines);
}
}
#[inline]
fn delete_lines(&mut self, lines: Line) {
let origin = self.cursor.point.line;
let lines = min(self.lines() - origin, lines);
trace!("Deleting {} lines", lines);
if lines.0 > 0 && self.scroll_region.contains(&self.cursor.point.line) {
self.scroll_up_relative(origin, lines);
}
}
#[inline]
fn erase_chars(&mut self, count: Column) {
trace!("Erasing chars: count={}, col={}", count, self.cursor.point.col);
let start = self.cursor.point.col;
2019-01-21 21:59:10 +00:00
let end = min(start + count, self.grid.num_cols());
let row = &mut self.grid[self.cursor.point.line];
// Cleared cells have current background color set
for c in &mut row[start..end] {
c.reset(&self.cursor.template);
}
}
#[inline]
fn delete_chars(&mut self, count: Column) {
let cols = self.grid.num_cols();
// Ensure deleting within terminal bounds
let count = min(count, cols);
let start = self.cursor.point.col;
let end = min(start + count, cols - 1);
let n = (cols - end).0;
let line = &mut self.grid[self.cursor.point.line];
unsafe {
let src = line[end..].as_ptr();
let dst = line[start..].as_mut_ptr();
ptr::copy(src, dst, n);
}
2016-12-17 06:48:04 +00:00
// Clear last `count` cells in line. If deleting 1 char, need to delete
// 1 cell.
let end = cols - count;
for c in &mut line[end..] {
c.reset(&self.cursor.template);
}
}
#[inline]
fn move_backward_tabs(&mut self, count: i64) {
trace!("Moving backward {} tabs", count);
for _ in 0..count {
let mut col = self.cursor.point.col;
for i in (0..(col.0)).rev() {
if self.tabs[index::Column(i)] {
col = index::Column(i);
break;
}
}
self.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");
2019-03-30 16:48:36 +00:00
let cursor = if self.alt { &mut self.cursor_save_alt } else { &mut self.cursor_save };
2017-08-09 18:24:42 +00:00
*cursor = self.cursor;
}
#[inline]
fn restore_cursor_position(&mut self) {
trace!("Restoring cursor position");
2019-03-30 16:48:36 +00:00
let source = if self.alt { &self.cursor_save_alt } else { &self.cursor_save };
2017-08-09 18:24:42 +00:00
self.cursor = *source;
self.cursor.point.line = min(self.cursor.point.line, self.grid.num_lines() - 1);
self.cursor.point.col = min(self.cursor.point.col, self.grid.num_cols() - 1);
}
#[inline]
fn clear_line(&mut self, mode: ansi::LineClearMode) {
trace!("Clearing line: {:?}", mode);
2019-03-30 16:48:36 +00:00
let col = self.cursor.point.col;
match mode {
ansi::LineClearMode::Right => {
let row = &mut self.grid[self.cursor.point.line];
2017-01-07 05:08:52 +00:00
for cell in &mut row[col..] {
cell.reset(&self.cursor.template);
}
},
ansi::LineClearMode::Left => {
let row = &mut self.grid[self.cursor.point.line];
2018-09-17 23:34:56 +00:00
for cell in &mut row[..=col] {
cell.reset(&self.cursor.template);
}
},
ansi::LineClearMode::All => {
let row = &mut self.grid[self.cursor.point.line];
for cell in &mut row[..] {
cell.reset(&self.cursor.template);
}
},
}
}
/// 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;
}
/// 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());
}
/// 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;
}
/// Set the clipboard
#[inline]
2019-11-11 00:12:14 +00:00
fn set_clipboard(&mut self, clipboard: u8, base64: &[u8]) {
let clipboard_type = match clipboard {
b'c' => ClipboardType::Clipboard,
b'p' | b's' => ClipboardType::Selection,
_ => return,
};
if let Ok(bytes) = base64::decode(base64) {
if let Ok(text) = str::from_utf8(&bytes) {
self.clipboard.store(clipboard_type, text);
}
}
}
/// Write clipboard data to child.
#[inline]
fn write_clipboard<W: io::Write>(&mut self, clipboard: u8, writer: &mut W, 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 text = self.clipboard.load(clipboard_type);
let base64 = base64::encode(&text);
let escape = format!("\x1b]52;{};{}{}", clipboard as char, base64, terminator);
2019-11-11 00:12:14 +00:00
let _ = writer.write_all(escape.as_bytes());
}
#[inline]
fn clear_screen(&mut self, mode: ansi::ClearMode) {
trace!("Clearing screen: {:?}", mode);
let template = self.cursor.template;
// Remove active selections
self.grid.selection = None;
match mode {
2017-02-06 21:13:25 +00:00
ansi::ClearMode::Above => {
// If clearing more than one line
if self.cursor.point.line > Line(1) {
// Fully clear all lines before the current line
2019-03-30 16:48:36 +00:00
self.grid
.region_mut(..self.cursor.point.line)
2017-10-13 03:12:29 +00:00
.each(|cell| cell.reset(&template));
2017-02-06 21:13:25 +00:00
}
// Clear up to the current column in the current line
let end = min(self.cursor.point.col + 1, self.grid.num_cols());
for cell in &mut self.grid[self.cursor.point.line][..end] {
2017-02-06 21:13:25 +00:00
cell.reset(&template);
}
},
ansi::ClearMode::Below => {
for cell in &mut self.grid[self.cursor.point.line][self.cursor.point.col..] {
cell.reset(&template);
}
if self.cursor.point.line < self.grid.num_lines() - 1 {
self.grid
.region_mut((self.cursor.point.line + 1)..)
.each(|cell| cell.reset(&template));
}
},
ansi::ClearMode::All => {
if self.mode.contains(TermMode::ALT_SCREEN) {
self.grid.region_mut(..).each(|c| c.reset(&template));
} else {
let template = Cell { bg: template.bg, ..Cell::default() };
self.grid.clear_viewport(&template);
}
},
ansi::ClearMode::Saved => self.grid.clear_history(),
}
}
#[inline]
fn clear_tabs(&mut self, mode: ansi::TabulationClearMode) {
trace!("Clearing tabs: {:?}", mode);
match mode {
ansi::TabulationClearMode::Current => {
let column = self.cursor.point.col;
self.tabs[column] = false;
},
ansi::TabulationClearMode::All => {
self.tabs.clear_all();
2019-03-30 16:48:36 +00:00
},
}
}
// Reset all important fields in the term struct
#[inline]
fn reset_state(&mut self) {
if self.alt {
self.swap_alt();
}
self.input_needs_wrap = false;
self.cursor = Default::default();
self.active_charset = Default::default();
self.mode = Default::default();
self.cursor_save = Default::default();
self.cursor_save_alt = Default::default();
self.colors = self.original_colors;
self.color_modified = [false; color::COUNT];
self.cursor_style = None;
self.grid.reset(&Cell::default());
self.alt_grid.reset(&Cell::default());
self.scroll_region = Line(0)..self.grid.num_lines();
self.title = DEFAULT_NAME.to_string();
self.title_stack.clear();
}
#[inline]
fn reverse_index(&mut self) {
trace!("Reversing index");
// if cursor is at the top
if self.cursor.point.line == self.scroll_region.start {
self.scroll_down(Line(1));
} else {
self.cursor.point.line -= min(self.cursor.point.line, Line(1));
}
}
/// set a terminal attribute
#[inline]
fn terminal_attribute(&mut self, attr: Attr) {
trace!("Setting attribute: {:?}", attr);
match attr {
Attr::Foreground(color) => self.cursor.template.fg = color,
Attr::Background(color) => self.cursor.template.bg = color,
Attr::Reset => {
self.cursor.template.fg = Color::Named(NamedColor::Foreground);
self.cursor.template.bg = Color::Named(NamedColor::Background);
self.cursor.template.flags = Flags::empty();
2019-03-30 16:48:36 +00:00
},
Attr::Reverse => self.cursor.template.flags.insert(Flags::INVERSE),
Attr::CancelReverse => self.cursor.template.flags.remove(Flags::INVERSE),
Attr::Bold => self.cursor.template.flags.insert(Flags::BOLD),
Attr::CancelBold => self.cursor.template.flags.remove(Flags::BOLD),
Attr::Dim => self.cursor.template.flags.insert(Flags::DIM),
Attr::CancelBoldDim => self.cursor.template.flags.remove(Flags::BOLD | Flags::DIM),
Attr::Italic => self.cursor.template.flags.insert(Flags::ITALIC),
Attr::CancelItalic => self.cursor.template.flags.remove(Flags::ITALIC),
Attr::Underline => self.cursor.template.flags.insert(Flags::UNDERLINE),
Attr::CancelUnderline => self.cursor.template.flags.remove(Flags::UNDERLINE),
Attr::Hidden => self.cursor.template.flags.insert(Flags::HIDDEN),
Attr::CancelHidden => self.cursor.template.flags.remove(Flags::HIDDEN),
Attr::Strike => self.cursor.template.flags.insert(Flags::STRIKEOUT),
Attr::CancelStrike => self.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::SwapScreenAndSetRestoreCursor => {
if !self.alt {
2019-04-19 20:56:11 +00:00
self.mode.insert(TermMode::ALT_SCREEN);
self.save_cursor_position();
self.swap_alt();
self.save_cursor_position();
}
},
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),
// Mouse protocols are mutually exlusive
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),
// Mouse encodings are mutually exlusive
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(),
2019-04-19 20:56:11 +00:00
ansi::Mode::Insert => self.mode.insert(TermMode::INSERT), // heh
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::SwapScreenAndSetRestoreCursor => {
if self.alt {
2019-04-19 20:56:11 +00:00
self.mode.remove(TermMode::ALT_SCREEN);
self.restore_cursor_position();
self.swap_alt();
self.restore_cursor_position();
}
},
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: usize) {
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.grid.num_lines());
self.scroll_region.end = min(end, self.grid.num_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.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 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);
}
}
}
struct TabStops {
2019-03-30 16:48:36 +00:00
tabs: Vec<bool>,
tabspaces: usize,
}
impl TabStops {
#[inline]
fn new(num_cols: Column, tabspaces: usize) -> TabStops {
TabStops {
tabspaces,
tabs: IndexRange::from(Column(0)..num_cols)
.map(|i| (*i as usize) % tabspaces == 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 tabspaces = self.tabspaces;
let mut index = self.tabs.len();
self.tabs.resize_with(num_cols.0, || {
let is_tabstop = index % tabspaces == 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)
}
}
#[cfg(test)]
mod tests {
use std::mem;
2019-03-30 16:48:36 +00:00
use crate::ansi::{self, CharsetIndex, Handler, StandardCharset};
use crate::clipboard::Clipboard;
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;
use crate::term::cell::{Cell, Flags};
use crate::term::{SizeInfo, Term};
struct Mock;
impl EventListener for Mock {
fn send_event(&self, _event: Event) {}
}
#[test]
fn semantic_selection_works() {
let size = SizeInfo {
width: 21.0,
height: 51.0,
cell_width: 3.0,
cell_height: 3.0,
padding_x: 0.0,
padding_y: 0.0,
Upgrade Glutin to v0.19.0 Some changes include: • Use the with_hardware_acceleration function on the ContextBuilder to not require the discrete GPU • Remove the LMenu and RMenu virtual key codes (winit 0.16.0 removed these because Windows now generates LAlt and RAlt instead • Replace set_cursor_state with hide_cursor (winit 0.16.0 removed the set_cursor_state function) • Replace GlWindow::hidpi_factor with GlWindow::get_hidpi_factor and change to expecting an f64 • Use the glutin/winit dpi size and position types where possible Glutin's dpi change event has been implemented. All size events now return logical sizes. As a result of that, the logical sizes are translated in the `display::handle_rezize` method so DPI scaling works correctly. When the DPI is changed, the glyph cache is updated to make use of the correct font size again. Moving a window to a different screen which is a different DPI caused a racing condition where the logical size of the event was sent to the `handle_resize` method in `src/display.rs`, however if there was a DPI change event before `handle_resize` is able to process this message, it would incorrectly use the new DPI to scale the resize event. To solve this issue instead of sending the logical size to the `handle_resize` method and then converting it to a physical size in there, the `LogicalSize` of the resize event is transformed into a `PhysicalSize` as soon as it's received. This fixes potential racing conditions since all events are processed in order. The padding has been changed so it's also scaled by DPR. The `scale_with_dpi` config option has been removed. If it's not present a warning will be emitted. The `winit` dependency on Windows has been removed. All interactions with winit in Alacritty are handled through glutin.
2018-11-10 16:08:48 +00:00
dpr: 1.0,
};
let mut term = Term::new(&MockConfig::default(), &size, Clipboard::new_nop(), Mock);
let mut grid: Grid<Cell> = Grid::new(Line(3), Column(5), 0, Cell::default());
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_mut() = Some(Selection::semantic(Point { line: 2, col: Column(1) }));
assert_eq!(term.selection_to_string(), Some(String::from("aa")));
}
{
*term.selection_mut() = Some(Selection::semantic(Point { line: 2, col: Column(4) }));
assert_eq!(term.selection_to_string(), Some(String::from("aaa")));
}
{
*term.selection_mut() = Some(Selection::semantic(Point { line: 1, col: Column(1) }));
assert_eq!(term.selection_to_string(), Some(String::from("aaa")));
}
}
#[test]
fn line_selection_works() {
let size = SizeInfo {
width: 21.0,
height: 51.0,
cell_width: 3.0,
cell_height: 3.0,
padding_x: 0.0,
padding_y: 0.0,
Upgrade Glutin to v0.19.0 Some changes include: • Use the with_hardware_acceleration function on the ContextBuilder to not require the discrete GPU • Remove the LMenu and RMenu virtual key codes (winit 0.16.0 removed these because Windows now generates LAlt and RAlt instead • Replace set_cursor_state with hide_cursor (winit 0.16.0 removed the set_cursor_state function) • Replace GlWindow::hidpi_factor with GlWindow::get_hidpi_factor and change to expecting an f64 • Use the glutin/winit dpi size and position types where possible Glutin's dpi change event has been implemented. All size events now return logical sizes. As a result of that, the logical sizes are translated in the `display::handle_rezize` method so DPI scaling works correctly. When the DPI is changed, the glyph cache is updated to make use of the correct font size again. Moving a window to a different screen which is a different DPI caused a racing condition where the logical size of the event was sent to the `handle_resize` method in `src/display.rs`, however if there was a DPI change event before `handle_resize` is able to process this message, it would incorrectly use the new DPI to scale the resize event. To solve this issue instead of sending the logical size to the `handle_resize` method and then converting it to a physical size in there, the `LogicalSize` of the resize event is transformed into a `PhysicalSize` as soon as it's received. This fixes potential racing conditions since all events are processed in order. The padding has been changed so it's also scaled by DPR. The `scale_with_dpi` config option has been removed. If it's not present a warning will be emitted. The `winit` dependency on Windows has been removed. All interactions with winit in Alacritty are handled through glutin.
2018-11-10 16:08:48 +00:00
dpr: 1.0,
};
let mut term = Term::new(&MockConfig::default(), &size, Clipboard::new_nop(), Mock);
let mut grid: Grid<Cell> = Grid::new(Line(1), Column(5), 0, Cell::default());
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_mut() = Some(Selection::lines(Point { line: 0, col: Column(3) }));
assert_eq!(term.selection_to_string(), Some(String::from("\"aa\"a\n")));
}
#[test]
fn selecting_empty_line() {
let size = SizeInfo {
width: 21.0,
height: 51.0,
cell_width: 3.0,
cell_height: 3.0,
padding_x: 0.0,
padding_y: 0.0,
Upgrade Glutin to v0.19.0 Some changes include: • Use the with_hardware_acceleration function on the ContextBuilder to not require the discrete GPU • Remove the LMenu and RMenu virtual key codes (winit 0.16.0 removed these because Windows now generates LAlt and RAlt instead • Replace set_cursor_state with hide_cursor (winit 0.16.0 removed the set_cursor_state function) • Replace GlWindow::hidpi_factor with GlWindow::get_hidpi_factor and change to expecting an f64 • Use the glutin/winit dpi size and position types where possible Glutin's dpi change event has been implemented. All size events now return logical sizes. As a result of that, the logical sizes are translated in the `display::handle_rezize` method so DPI scaling works correctly. When the DPI is changed, the glyph cache is updated to make use of the correct font size again. Moving a window to a different screen which is a different DPI caused a racing condition where the logical size of the event was sent to the `handle_resize` method in `src/display.rs`, however if there was a DPI change event before `handle_resize` is able to process this message, it would incorrectly use the new DPI to scale the resize event. To solve this issue instead of sending the logical size to the `handle_resize` method and then converting it to a physical size in there, the `LogicalSize` of the resize event is transformed into a `PhysicalSize` as soon as it's received. This fixes potential racing conditions since all events are processed in order. The padding has been changed so it's also scaled by DPR. The `scale_with_dpi` config option has been removed. If it's not present a warning will be emitted. The `winit` dependency on Windows has been removed. All interactions with winit in Alacritty are handled through glutin.
2018-11-10 16:08:48 +00:00
dpr: 1.0,
};
let mut term = Term::new(&MockConfig::default(), &size, Clipboard::new_nop(), Mock);
let mut grid: Grid<Cell> = Grid::new(Line(3), Column(3), 0, Cell::default());
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::simple(Point { line: 2, col: Column(0) }, Side::Left);
selection.update(Point { line: 0, col: Column(2) }, Side::Right);
*term.selection_mut() = Some(selection);
assert_eq!(term.selection_to_string(), Some("aaa\n\naaa\n".into()));
}
/// 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 template = Cell::default();
let grid: Grid<Cell> = Grid::new(Line(24), Column(80), 0, template);
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 {
width: 21.0,
height: 51.0,
cell_width: 3.0,
cell_height: 3.0,
padding_x: 0.0,
padding_y: 0.0,
Upgrade Glutin to v0.19.0 Some changes include: • Use the with_hardware_acceleration function on the ContextBuilder to not require the discrete GPU • Remove the LMenu and RMenu virtual key codes (winit 0.16.0 removed these because Windows now generates LAlt and RAlt instead • Replace set_cursor_state with hide_cursor (winit 0.16.0 removed the set_cursor_state function) • Replace GlWindow::hidpi_factor with GlWindow::get_hidpi_factor and change to expecting an f64 • Use the glutin/winit dpi size and position types where possible Glutin's dpi change event has been implemented. All size events now return logical sizes. As a result of that, the logical sizes are translated in the `display::handle_rezize` method so DPI scaling works correctly. When the DPI is changed, the glyph cache is updated to make use of the correct font size again. Moving a window to a different screen which is a different DPI caused a racing condition where the logical size of the event was sent to the `handle_resize` method in `src/display.rs`, however if there was a DPI change event before `handle_resize` is able to process this message, it would incorrectly use the new DPI to scale the resize event. To solve this issue instead of sending the logical size to the `handle_resize` method and then converting it to a physical size in there, the `LogicalSize` of the resize event is transformed into a `PhysicalSize` as soon as it's received. This fixes potential racing conditions since all events are processed in order. The padding has been changed so it's also scaled by DPR. The `scale_with_dpi` config option has been removed. If it's not present a warning will be emitted. The `winit` dependency on Windows has been removed. All interactions with winit in Alacritty are handled through glutin.
2018-11-10 16:08:48 +00:00
dpr: 1.0,
};
let mut term = Term::new(&MockConfig::default(), &size, Clipboard::new_nop(), 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 {
width: 21.0,
height: 51.0,
cell_width: 3.0,
cell_height: 3.0,
padding_x: 0.0,
padding_y: 0.0,
2019-03-30 16:48:36 +00:00
dpr: 1.0,
};
let mut term = Term::new(&MockConfig::default(), &size, Clipboard::new_nop(), Mock);
// Add one line of scrollback
term.grid.scroll_up(&(Line(0)..Line(1)), Line(1), &Cell::default());
// Clear the history
term.clear_screen(ansi::ClearMode::Saved);
// Make sure that scrolling does not change the grid
let mut scrolled_grid = term.grid.clone();
scrolled_grid.scroll_display(Scroll::Top);
// Truncate grids for comparison
scrolled_grid.truncate();
term.grid.truncate();
assert_eq!(term.grid, scrolled_grid);
}
#[test]
fn window_title() {
let size = SizeInfo {
width: 21.0,
height: 51.0,
cell_width: 3.0,
cell_height: 3.0,
padding_x: 0.0,
padding_y: 0.0,
dpr: 1.0,
};
let mut term = Term::new(&MockConfig::default(), &size, Clipboard::new_nop(), Mock);
// Title can be set
{
term.title = "Test".to_string();
assert_eq!(term.title, "Test");
}
// Title can be pushed onto stack
{
term.push_title();
term.title = "Next".to_string();
assert_eq!(term.title, "Next");
assert_eq!(term.title_stack.get(0).unwrap(), "Test");
}
// Title can be popped from stack and set as the window title
{
term.pop_title();
assert_eq!(term.title, "Test");
assert!(term.title_stack.is_empty());
}
// Title stack doesn't grow infinitely
{
for _ in 0..4097 {
term.push_title();
}
assert_eq!(term.title_stack.len(), 4096);
}
// Title and title stack reset when terminal state is reset
{
term.push_title();
term.reset_state();
assert_eq!(term.title, "Alacritty");
assert!(term.title_stack.is_empty());
}
}
}
#[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::File;
2019-03-30 16:48:36 +00:00
use std::io::Read;
use std::mem;
use std::path::Path;
use crate::clipboard::Clipboard;
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) {}
}
fn read_string<P>(path: P) -> String
2019-03-30 16:48:36 +00:00
where
P: AsRef<Path>,
{
let mut res = String::new();
2019-03-30 16:48:36 +00:00
File::open(path.as_ref()).unwrap().read_to_string(&mut res).unwrap();
res
}
/// 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
/// displayed (that is, not a an empty background cell). This benchmark
/// 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.
2019-03-30 16:48:36 +00:00
let serialized_grid = read_string(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/ref/vim_large_window_scroll/grid.json"
));
let serialized_size = read_string(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/ref/vim_large_window_scroll/size.json"
));
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, Clipboard::new_nop(), Mock);
mem::swap(&mut terminal.grid, &mut grid);
b.iter(|| {
let iter = terminal.renderable_cells(&config);
for cell in iter {
test::black_box(cell);
}
})
}
}