Move renderable cell transformation to alacritty

This refactors a large chunk of the alacritty_terminal API to expose all
data necessary for rendering uniformly through the `renderable_content`
call. This also no longer transforms the cells for rendering by a GUI
but instead just reports the content from a terminal emulation
perspective. The transformation into renderable cells is now done inside
the alacritty crate.

Since the terminal itself only ever needs to know about modified color
RGB values, the configuration for colors was moved to the alacritty UI
code.
This commit is contained in:
Christian Duerr 2021-01-24 21:45:36 +00:00 committed by GitHub
parent 7291702f6b
commit 530de00049
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 1447 additions and 1561 deletions

View File

@ -7,6 +7,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## 0.8.0-dev
### Packaging
- Updated shell completions
### Added
- IME composition preview not appearing on Windows
@ -15,9 +19,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Crash due to assertion failure on 32-bit architectures
### Packaging
### Removed
- Updated shell completions
- Config field `visual_bell`, you should use `bell` instead
## 0.7.1

2
Cargo.lock generated
View File

@ -61,7 +61,7 @@ dependencies = [
[[package]]
name = "alacritty_terminal"
version = "0.12.1-dev"
version = "0.13.0-dev"
dependencies = [
"alacritty_config_derive",
"base64 0.12.3",

View File

@ -10,7 +10,7 @@ edition = "2018"
[dependencies.alacritty_terminal]
path = "../alacritty_terminal"
version = "0.12.1-dev"
version = "0.13.0-dev"
default-features = false
[dependencies.alacritty_config_derive]

View File

@ -2,8 +2,8 @@ use std::time::Duration;
use alacritty_config_derive::ConfigDeserialize;
use crate::config::Program;
use crate::term::color::Rgb;
use alacritty_terminal::config::Program;
use alacritty_terminal::term::color::Rgb;
#[derive(ConfigDeserialize, Clone, Debug, PartialEq, Eq)]
pub struct BellConfig {

View File

@ -2,8 +2,7 @@ use serde::de::Error as SerdeError;
use serde::{Deserialize, Deserializer};
use alacritty_config_derive::ConfigDeserialize;
use crate::term::color::{CellRgb, Rgb};
use alacritty_terminal::term::color::{CellRgb, Rgb};
#[derive(ConfigDeserialize, Clone, Debug, Default, PartialEq, Eq)]
pub struct Colors {

View File

@ -1,5 +1,5 @@
use std::fmt::{self, Display, Formatter};
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::{env, fs, io};
use log::{error, info};
@ -9,6 +9,8 @@ use serde_yaml::Value;
use alacritty_terminal::config::{Config as TermConfig, LOG_TARGET_CONFIG};
pub mod bell;
pub mod color;
pub mod debug;
pub mod font;
pub mod monitor;
@ -123,10 +125,10 @@ pub fn load(options: &Options) -> Config {
}
/// Attempt to reload the configuration file.
pub fn reload(config_path: &PathBuf, options: &Options) -> Result<Config> {
pub fn reload(config_path: &Path, options: &Options) -> Result<Config> {
// Load config, propagating errors.
let config_options = options.config_options().clone();
let mut config = load_from(&config_path, config_options)?;
let mut config = load_from(config_path, config_options)?;
// Override config with CLI options.
options.override_config(&mut config);
@ -135,7 +137,7 @@ pub fn reload(config_path: &PathBuf, options: &Options) -> Result<Config> {
}
/// Load configuration file and log errors.
fn load_from(path: &PathBuf, cli_config: Value) -> Result<Config> {
fn load_from(path: &Path, cli_config: Value) -> Result<Config> {
match read_config(path, cli_config) {
Ok(config) => Ok(config),
Err(err) => {
@ -146,7 +148,7 @@ fn load_from(path: &PathBuf, cli_config: Value) -> Result<Config> {
}
/// Deserialize configuration file from path.
fn read_config(path: &PathBuf, cli_config: Value) -> Result<Config> {
fn read_config(path: &Path, cli_config: Value) -> Result<Config> {
let mut config_paths = Vec::new();
let mut config_value = parse_config(&path, &mut config_paths, IMPORT_RECURSION_LIMIT)?;
@ -162,7 +164,7 @@ fn read_config(path: &PathBuf, cli_config: Value) -> Result<Config> {
/// Deserialize all configuration files as generic Value.
fn parse_config(
path: &PathBuf,
path: &Path,
config_paths: &mut Vec<PathBuf>,
recursion_limit: usize,
) -> Result<Value> {

View File

@ -6,7 +6,9 @@ use serde::{Deserialize, Deserializer};
use alacritty_config_derive::ConfigDeserialize;
use alacritty_terminal::config::{Percentage, LOG_TARGET_CONFIG};
use crate::config::bell::BellConfig;
use crate::config::bindings::{self, Binding, KeyBinding, MouseBinding};
use crate::config::color::Colors;
use crate::config::debug::Debug;
use crate::config::font::Font;
use crate::config::mouse::Mouse;
@ -31,6 +33,15 @@ pub struct UIConfig {
/// Live config reload.
pub live_config_reload: bool,
/// Bell configuration.
pub bell: BellConfig,
/// RGB values for colors.
pub colors: Colors,
/// Should draw bold text with brighter colors instead of bold font.
pub draw_bold_text_with_bright_colors: bool,
/// Path where config was loaded from.
#[config(skip)]
pub config_paths: Vec<PathBuf>,
@ -58,6 +69,9 @@ impl Default for UIConfig {
key_bindings: Default::default(),
mouse_bindings: Default::default(),
background_opacity: Default::default(),
bell: Default::default(),
colors: Default::default(),
draw_bold_text_with_bright_colors: Default::default(),
}
}
}

View File

@ -0,0 +1,122 @@
use std::time::{Duration, Instant};
use crate::config::bell::{BellAnimation, BellConfig};
pub struct VisualBell {
/// Visual bell animation.
animation: BellAnimation,
/// Visual bell duration.
duration: Duration,
/// The last time the visual bell rang, if at all.
start_time: Option<Instant>,
}
impl VisualBell {
/// Ring the visual bell, and return its intensity.
pub fn ring(&mut self) -> f64 {
let now = Instant::now();
self.start_time = Some(now);
self.intensity_at_instant(now)
}
/// 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
},
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);
let elapsed_f =
elapsed.as_secs() as f64 + f64::from(elapsed.subsec_nanos()) / 1e9f64;
let duration_f = self.duration.as_secs() as f64
+ f64::from(self.duration.subsec_nanos()) / 1e9f64;
// Otherwise, we compute a value `time` from 0.0 to 1.0
// inclusive that represents the ratio of `elapsed` time to the
// `duration` of the VisualBell.
let time = (elapsed_f / duration_f).min(1.0);
// We use this to compute the inverse `intensity` of the
// VisualBell. When `time` is 0.0, `inverse_intensity` is 0.0,
// and when `time` is 1.0, `inverse_intensity` is 1.0.
let inverse_intensity = match self.animation {
BellAnimation::Ease | BellAnimation::EaseOut => {
cubic_bezier(0.25, 0.1, 0.25, 1.0, time)
},
BellAnimation::EaseOutSine => cubic_bezier(0.39, 0.575, 0.565, 1.0, time),
BellAnimation::EaseOutQuad => cubic_bezier(0.25, 0.46, 0.45, 0.94, time),
BellAnimation::EaseOutCubic => cubic_bezier(0.215, 0.61, 0.355, 1.0, time),
BellAnimation::EaseOutQuart => cubic_bezier(0.165, 0.84, 0.44, 1.0, time),
BellAnimation::EaseOutQuint => cubic_bezier(0.23, 1.0, 0.32, 1.0, time),
BellAnimation::EaseOutExpo => cubic_bezier(0.19, 1.0, 0.22, 1.0, time),
BellAnimation::EaseOutCirc => cubic_bezier(0.075, 0.82, 0.165, 1.0, time),
BellAnimation::Linear => time,
};
// Since we want the `intensity` of the VisualBell to decay over
// `time`, we subtract the `inverse_intensity` from 1.0.
1.0 - inverse_intensity
},
}
}
pub fn update_config(&mut self, bell_config: &BellConfig) {
self.animation = bell_config.animation;
self.duration = bell_config.duration();
}
}
impl From<&BellConfig> for VisualBell {
fn from(bell_config: &BellConfig) -> VisualBell {
VisualBell {
animation: bell_config.animation,
duration: bell_config.duration(),
start_time: None,
}
}
}
fn cubic_bezier(p0: f64, p1: f64, p2: f64, p3: f64, x: f64) -> f64 {
(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
}

View File

@ -0,0 +1,167 @@
use std::ops::{Index, IndexMut};
use log::trace;
use alacritty_terminal::ansi::NamedColor;
use alacritty_terminal::term::color::{Rgb, COUNT};
use crate::config::color::Colors;
/// Factor for automatic computation of dim colors.
pub const DIM_FACTOR: f32 = 0.66;
#[derive(Copy, Clone)]
pub struct List([Rgb; COUNT]);
impl<'a> From<&'a Colors> for List {
fn from(colors: &Colors) -> List {
// Type inference fails without this annotation.
let mut list = List([Rgb::default(); COUNT]);
list.fill_named(colors);
list.fill_cube(colors);
list.fill_gray_ramp(colors);
list
}
}
impl List {
pub fn fill_named(&mut self, colors: &Colors) {
// Normals.
self[NamedColor::Black] = colors.normal.black;
self[NamedColor::Red] = colors.normal.red;
self[NamedColor::Green] = colors.normal.green;
self[NamedColor::Yellow] = colors.normal.yellow;
self[NamedColor::Blue] = colors.normal.blue;
self[NamedColor::Magenta] = colors.normal.magenta;
self[NamedColor::Cyan] = colors.normal.cyan;
self[NamedColor::White] = colors.normal.white;
// Brights.
self[NamedColor::BrightBlack] = colors.bright.black;
self[NamedColor::BrightRed] = colors.bright.red;
self[NamedColor::BrightGreen] = colors.bright.green;
self[NamedColor::BrightYellow] = colors.bright.yellow;
self[NamedColor::BrightBlue] = colors.bright.blue;
self[NamedColor::BrightMagenta] = colors.bright.magenta;
self[NamedColor::BrightCyan] = colors.bright.cyan;
self[NamedColor::BrightWhite] = colors.bright.white;
self[NamedColor::BrightForeground] =
colors.primary.bright_foreground.unwrap_or(colors.primary.foreground);
// Foreground and background.
self[NamedColor::Foreground] = colors.primary.foreground;
self[NamedColor::Background] = colors.primary.background;
// Dims.
self[NamedColor::DimForeground] =
colors.primary.dim_foreground.unwrap_or(colors.primary.foreground * DIM_FACTOR);
match colors.dim {
Some(ref dim) => {
trace!("Using config-provided dim colors");
self[NamedColor::DimBlack] = dim.black;
self[NamedColor::DimRed] = dim.red;
self[NamedColor::DimGreen] = dim.green;
self[NamedColor::DimYellow] = dim.yellow;
self[NamedColor::DimBlue] = dim.blue;
self[NamedColor::DimMagenta] = dim.magenta;
self[NamedColor::DimCyan] = dim.cyan;
self[NamedColor::DimWhite] = dim.white;
},
None => {
trace!("Deriving dim colors from normal colors");
self[NamedColor::DimBlack] = colors.normal.black * DIM_FACTOR;
self[NamedColor::DimRed] = colors.normal.red * DIM_FACTOR;
self[NamedColor::DimGreen] = colors.normal.green * DIM_FACTOR;
self[NamedColor::DimYellow] = colors.normal.yellow * DIM_FACTOR;
self[NamedColor::DimBlue] = colors.normal.blue * DIM_FACTOR;
self[NamedColor::DimMagenta] = colors.normal.magenta * DIM_FACTOR;
self[NamedColor::DimCyan] = colors.normal.cyan * DIM_FACTOR;
self[NamedColor::DimWhite] = colors.normal.white * DIM_FACTOR;
},
}
}
pub fn fill_cube(&mut self, colors: &Colors) {
let mut index: usize = 16;
// Build colors.
for r in 0..6 {
for g in 0..6 {
for b in 0..6 {
// Override colors 16..232 with the config (if present).
if let Some(indexed_color) =
colors.indexed_colors.iter().find(|ic| ic.index() == index as u8)
{
self[index] = indexed_color.color;
} else {
self[index] = Rgb {
r: if r == 0 { 0 } else { r * 40 + 55 },
b: if b == 0 { 0 } else { b * 40 + 55 },
g: if g == 0 { 0 } else { g * 40 + 55 },
};
}
index += 1;
}
}
}
debug_assert!(index == 232);
}
pub fn fill_gray_ramp(&mut self, colors: &Colors) {
let mut index: usize = 232;
for i in 0..24 {
// Index of the color is number of named colors + number of cube colors + i.
let color_index = 16 + 216 + i;
// Override colors 232..256 with the config (if present).
if let Some(indexed_color) =
colors.indexed_colors.iter().find(|ic| ic.index() == color_index)
{
self[index] = indexed_color.color;
index += 1;
continue;
}
let value = i * 10 + 8;
self[index] = Rgb { r: value, g: value, b: value };
index += 1;
}
debug_assert!(index == 256);
}
}
impl Index<usize> for List {
type Output = Rgb;
#[inline]
fn index(&self, idx: usize) -> &Self::Output {
&self.0[idx]
}
}
impl IndexMut<usize> for List {
#[inline]
fn index_mut(&mut self, idx: usize) -> &mut Self::Output {
&mut self.0[idx]
}
}
impl Index<NamedColor> for List {
type Output = Rgb;
#[inline]
fn index(&self, idx: NamedColor) -> &Self::Output {
&self.0[idx as usize]
}
}
impl IndexMut<NamedColor> for List {
#[inline]
fn index_mut(&mut self, idx: NamedColor) -> &mut Self::Output {
&mut self.0[idx as usize]
}
}

View File

@ -1,18 +1,21 @@
use std::cmp::max;
use std::iter;
use std::iter::Peekable;
use std::mem;
use std::ops::RangeInclusive;
use crate::ansi::{Color, CursorShape, NamedColor};
use crate::config::Config;
use crate::grid::{Dimensions, DisplayIter, Indexed};
use crate::index::{Column, Direction, Line, Point};
use crate::selection::SelectionRange;
use crate::term::cell::{Cell, Flags};
use crate::term::color::{self, CellRgb, Rgb, DIM_FACTOR};
use crate::term::search::RegexIter;
use crate::term::{Term, TermMode};
use alacritty_terminal::ansi::{Color, CursorShape, NamedColor};
use alacritty_terminal::config::Config;
use alacritty_terminal::event::EventListener;
use alacritty_terminal::grid::{Dimensions, Indexed};
use alacritty_terminal::index::{Column, Direction, Line, Point};
use alacritty_terminal::term::cell::{Cell, Flags};
use alacritty_terminal::term::color::{CellRgb, Rgb};
use alacritty_terminal::term::search::{RegexIter, RegexSearch};
use alacritty_terminal::term::{
RenderableContent as TerminalContent, RenderableCursor as TerminalCursor, Term, TermMode,
};
use crate::config::ui_config::UIConfig;
use crate::display::color::{List, DIM_FACTOR};
/// Minimum contrast between a fixed cursor color and the cell's background.
pub const MIN_CURSOR_CONTRAST: f64 = 1.5;
@ -23,58 +26,40 @@ const MAX_SEARCH_LINES: usize = 100;
/// Renderable terminal content.
///
/// This provides the terminal cursor and an iterator over all non-empty cells.
pub struct RenderableContent<'a, T, C> {
term: &'a Term<T>,
config: &'a Config<C>,
display_iter: DisplayIter<'a, Cell>,
selection: Option<SelectionRange<Line>>,
search: RenderableSearch<'a>,
pub struct RenderableContent<'a> {
terminal_content: TerminalContent<'a>,
terminal_cursor: TerminalCursor,
cursor: Option<RenderableCursor>,
cursor_shape: CursorShape,
cursor_point: Point,
search: RenderableSearch,
config: &'a Config<UIConfig>,
colors: &'a List,
}
impl<'a, T, C> RenderableContent<'a, T, C> {
pub fn new(term: &'a Term<T>, config: &'a Config<C>, show_cursor: bool) -> Self {
// Cursor position.
let vi_mode = term.mode.contains(TermMode::VI);
let mut cursor_point = if vi_mode {
term.vi_mode_cursor.point
} else {
let mut point = term.grid.cursor.point;
point.line += term.grid.display_offset();
point
};
impl<'a> RenderableContent<'a> {
pub fn new<T: EventListener>(
term: &'a Term<T>,
dfas: Option<&RegexSearch>,
config: &'a Config<UIConfig>,
colors: &'a List,
show_cursor: bool,
) -> Self {
let search = dfas.map(|dfas| RenderableSearch::new(&term, dfas)).unwrap_or_default();
let terminal_content = term.renderable_content();
// Cursor shape.
let cursor_shape = if !show_cursor
|| (!term.mode.contains(TermMode::SHOW_CURSOR) && !vi_mode)
|| cursor_point.line >= term.screen_lines()
{
cursor_point.line = Line(0);
CursorShape::Hidden
// Copy the cursor and override its shape if necessary.
let mut terminal_cursor = terminal_content.cursor;
if !show_cursor {
terminal_cursor.shape = CursorShape::Hidden;
} else if !term.is_focused && config.cursor.unfocused_hollow {
CursorShape::HollowBlock
} else {
let cursor_style = term.cursor_style.unwrap_or(term.default_cursor_style);
if vi_mode {
term.vi_mode_cursor_style.unwrap_or(cursor_style).shape
} else {
cursor_style.shape
}
};
Self {
display_iter: term.grid.display_iter(),
selection: term.visible_selection(),
search: RenderableSearch::new(term),
cursor: None,
cursor_shape,
cursor_point,
config,
term,
terminal_cursor.shape = CursorShape::HollowBlock;
}
Self { cursor: None, terminal_content, terminal_cursor, search, config, colors }
}
/// Viewport offset.
pub fn display_offset(&self) -> usize {
self.terminal_content.display_offset
}
/// Get the terminal cursor.
@ -85,33 +70,35 @@ impl<'a, T, C> RenderableContent<'a, T, C> {
self.cursor
}
/// Get the RGB value for a color index.
pub fn color(&self, color: usize) -> Rgb {
self.terminal_content.colors[color].unwrap_or(self.colors[color])
}
/// Assemble the information required to render the terminal cursor.
///
/// This will return `None` when there is no cursor visible.
fn renderable_cursor(&mut self, cell: &RenderableCell) -> Option<RenderableCursor> {
if self.cursor_shape == CursorShape::Hidden {
if self.terminal_cursor.shape == CursorShape::Hidden {
return None;
}
// Expand across wide cell when inside wide char or spacer.
let is_wide = if cell.flags.contains(Flags::WIDE_CHAR_SPACER) {
self.cursor_point.col -= 1;
self.terminal_cursor.point.column -= 1;
true
} else {
cell.flags.contains(Flags::WIDE_CHAR)
};
// Cursor colors.
let color = if self.term.mode.contains(TermMode::VI) {
self.config.colors.vi_mode_cursor
let color = if self.terminal_content.mode.contains(TermMode::VI) {
self.config.ui_config.colors.vi_mode_cursor
} else {
self.config.colors.cursor
};
let mut cursor_color = if self.term.color_modified[NamedColor::Cursor as usize] {
CellRgb::Rgb(self.term.colors[NamedColor::Cursor])
} else {
color.background
self.config.ui_config.colors.cursor
};
let mut cursor_color =
self.terminal_content.colors[NamedColor::Cursor].map_or(color.background, CellRgb::Rgb);
let mut text_color = color.foreground;
// Invert the cursor if it has a fixed background close to the cell's background.
@ -128,8 +115,8 @@ impl<'a, T, C> RenderableContent<'a, T, C> {
let cursor_color = cursor_color.color(cell.fg, cell.bg);
Some(RenderableCursor {
point: self.cursor_point,
shape: self.cursor_shape,
point: self.terminal_cursor.point,
shape: self.terminal_cursor.shape,
cursor_color,
text_color,
is_wide,
@ -137,7 +124,7 @@ impl<'a, T, C> RenderableContent<'a, T, C> {
}
}
impl<'a, T, C> Iterator for RenderableContent<'a, T, C> {
impl<'a> Iterator for RenderableContent<'a> {
type Item = RenderableCell;
/// Gets the next renderable cell.
@ -147,11 +134,10 @@ impl<'a, T, C> Iterator for RenderableContent<'a, T, C> {
#[inline]
fn next(&mut self) -> Option<Self::Item> {
loop {
if self.cursor_point == self.display_iter.point() {
// Handle cell at cursor position.
let cell = self.display_iter.next()?;
let mut cell = RenderableCell::new(self, cell);
let cell = self.terminal_content.display_iter.next()?;
let mut cell = RenderableCell::new(self, cell);
if self.terminal_cursor.point == cell.point {
// Store the cursor which should be rendered.
self.cursor = self.renderable_cursor(&cell).map(|cursor| {
if cursor.shape == CursorShape::Block {
@ -167,15 +153,9 @@ impl<'a, T, C> Iterator for RenderableContent<'a, T, C> {
});
return Some(cell);
} else {
// Handle non-cursor cells.
let cell = self.display_iter.next()?;
let cell = RenderableCell::new(self, cell);
} else if !cell.is_empty() && !cell.flags.contains(Flags::WIDE_CHAR_SPACER) {
// Skip empty cells and wide char spacers.
if !cell.is_empty() && !cell.flags.contains(Flags::WIDE_CHAR_SPACER) {
return Some(cell);
}
return Some(cell);
}
}
}
@ -186,8 +166,7 @@ impl<'a, T, C> Iterator for RenderableContent<'a, T, C> {
pub struct RenderableCell {
pub character: char,
pub zerowidth: Option<Vec<char>>,
pub line: Line,
pub column: Column,
pub point: Point,
pub fg: Rgb,
pub bg: Rgb,
pub bg_alpha: f32,
@ -196,13 +175,10 @@ pub struct RenderableCell {
}
impl RenderableCell {
fn new<'a, T, C>(content: &mut RenderableContent<'a, T, C>, cell: Indexed<&Cell>) -> Self {
let point = Point::new(cell.line, cell.column);
fn new<'a>(content: &mut RenderableContent<'a>, cell: Indexed<&Cell, Line>) -> Self {
// Lookup RGB values.
let mut fg_rgb =
Self::compute_fg_rgb(content.config, &content.term.colors, cell.fg, cell.flags);
let mut bg_rgb = Self::compute_bg_rgb(&content.term.colors, cell.bg);
let mut fg_rgb = Self::compute_fg_rgb(content, cell.fg, cell.flags);
let mut bg_rgb = Self::compute_bg_rgb(content, cell.bg);
let mut bg_alpha = if cell.flags.contains(Flags::INVERSE) {
mem::swap(&mut fg_rgb, &mut bg_rgb);
@ -211,30 +187,31 @@ impl RenderableCell {
Self::compute_bg_alpha(cell.bg)
};
let grid = content.term.grid();
let is_selected = content.selection.map_or(false, |selection| {
selection.contains_cell(grid, point, content.cursor_point, content.cursor_shape)
});
let is_selected = content
.terminal_content
.selection
.map_or(false, |selection| selection.contains_cell(&cell, content.terminal_cursor));
let mut is_match = false;
let colors = &content.config.ui_config.colors;
if is_selected {
let config_bg = content.config.colors.selection.background;
let selected_fg = content.config.colors.selection.foreground.color(fg_rgb, bg_rgb);
let config_bg = colors.selection.background;
let selected_fg = colors.selection.foreground.color(fg_rgb, bg_rgb);
bg_rgb = config_bg.color(fg_rgb, bg_rgb);
fg_rgb = selected_fg;
if fg_rgb == bg_rgb && !cell.flags.contains(Flags::HIDDEN) {
// Reveal inversed text when fg/bg is the same.
fg_rgb = content.term.colors[NamedColor::Background];
bg_rgb = content.term.colors[NamedColor::Foreground];
fg_rgb = content.color(NamedColor::Background as usize);
bg_rgb = content.color(NamedColor::Foreground as usize);
bg_alpha = 1.0;
} else if config_bg != CellRgb::CellBackground {
bg_alpha = 1.0;
}
} else if content.search.advance(grid.visible_to_buffer(point)) {
} else if content.search.advance(cell.point) {
// Highlight the cell if it is part of a search match.
let config_bg = content.config.colors.search.matches.background;
let matched_fg = content.config.colors.search.matches.foreground.color(fg_rgb, bg_rgb);
let config_bg = colors.search.matches.background;
let matched_fg = colors.search.matches.foreground.color(fg_rgb, bg_rgb);
bg_rgb = config_bg.color(fg_rgb, bg_rgb);
fg_rgb = matched_fg;
@ -248,8 +225,7 @@ impl RenderableCell {
RenderableCell {
character: cell.c,
zerowidth: cell.zerowidth().map(|zerowidth| zerowidth.to_vec()),
line: cell.line,
column: cell.column,
point: cell.point,
fg: fg_rgb,
bg: bg_rgb,
bg_alpha,
@ -258,11 +234,6 @@ impl RenderableCell {
}
}
/// Position of the cell.
pub fn point(&self) -> Point {
Point::new(self.line, self.column)
}
/// Check if cell contains any renderable content.
fn is_empty(&self) -> bool {
self.bg_alpha == 0.
@ -272,32 +243,35 @@ impl RenderableCell {
}
/// Get the RGB color from a cell's foreground color.
fn compute_fg_rgb<C>(config: &Config<C>, colors: &color::List, fg: Color, flags: Flags) -> Rgb {
fn compute_fg_rgb(content: &mut RenderableContent<'_>, fg: Color, flags: Flags) -> Rgb {
let ui_config = &content.config.ui_config;
match fg {
Color::Spec(rgb) => match flags & Flags::DIM {
Flags::DIM => rgb * DIM_FACTOR,
_ => rgb,
},
Color::Named(ansi) => {
match (config.draw_bold_text_with_bright_colors, flags & Flags::DIM_BOLD) {
match (ui_config.draw_bold_text_with_bright_colors, flags & Flags::DIM_BOLD) {
// If no bright foreground is set, treat it like the BOLD flag doesn't exist.
(_, Flags::DIM_BOLD)
if ansi == NamedColor::Foreground
&& config.colors.primary.bright_foreground.is_none() =>
&& ui_config.colors.primary.bright_foreground.is_none() =>
{
colors[NamedColor::DimForeground]
content.color(NamedColor::DimForeground as usize)
},
// Draw bold text in bright colors *and* contains bold flag.
(true, Flags::BOLD) => colors[ansi.to_bright()],
(true, Flags::BOLD) => content.color(ansi.to_bright() as usize),
// Cell is marked as dim and not bold.
(_, Flags::DIM) | (false, Flags::DIM_BOLD) => colors[ansi.to_dim()],
(_, Flags::DIM) | (false, Flags::DIM_BOLD) => {
content.color(ansi.to_dim() as usize)
},
// None of the above, keep original color..
_ => colors[ansi],
_ => content.color(ansi as usize),
}
},
Color::Indexed(idx) => {
let idx = match (
config.draw_bold_text_with_bright_colors,
ui_config.draw_bold_text_with_bright_colors,
flags & Flags::DIM_BOLD,
idx,
) {
@ -307,18 +281,18 @@ impl RenderableCell {
_ => idx as usize,
};
colors[idx]
content.color(idx)
},
}
}
/// Get the RGB color from a cell's background color.
#[inline]
fn compute_bg_rgb(colors: &color::List, bg: Color) -> Rgb {
fn compute_bg_rgb(content: &mut RenderableContent<'_>, bg: Color) -> Rgb {
match bg {
Color::Spec(rgb) => rgb,
Color::Named(ansi) => colors[ansi],
Color::Indexed(idx) => colors[idx],
Color::Named(ansi) => content.color(ansi as usize),
Color::Indexed(idx) => content.color(idx as usize),
}
}
@ -365,22 +339,19 @@ impl RenderableCursor {
}
}
type MatchIter<'a> = Box<dyn Iterator<Item = RangeInclusive<Point<usize>>> + 'a>;
/// Regex search highlight tracking.
struct RenderableSearch<'a> {
iter: Peekable<MatchIter<'a>>,
#[derive(Default)]
pub struct RenderableSearch {
/// All visible search matches.
matches: Vec<RangeInclusive<Point>>,
/// Index of the last match checked.
index: usize,
}
impl<'a> RenderableSearch<'a> {
impl RenderableSearch {
/// Create a new renderable search iterator.
fn new<T>(term: &'a Term<T>) -> Self {
// Avoid constructing search if there is none.
if term.regex_search.is_none() {
let iter: MatchIter<'a> = Box::new(iter::empty());
return Self { iter: iter.peekable() };
}
pub fn new<T>(term: &Term<T>, dfas: &RegexSearch) -> Self {
let viewport_end = term.grid().display_offset();
let viewport_start = viewport_end + term.screen_lines().0 - 1;
@ -394,8 +365,7 @@ impl<'a> RenderableSearch<'a> {
if start.line > viewport_start + MAX_SEARCH_LINES {
if start.line == 0 {
// Do not highlight anything if this line is the last.
let iter: MatchIter<'a> = Box::new(iter::empty());
return Self { iter: iter.peekable() };
return Self::default();
} else {
// Start at next line if this one is too long.
start.line -= 1;
@ -404,24 +374,27 @@ impl<'a> RenderableSearch<'a> {
end.line = max(end.line, viewport_end.saturating_sub(MAX_SEARCH_LINES));
// Create an iterater for the current regex search for all visible matches.
let iter: MatchIter<'a> = Box::new(
RegexIter::new(start, end, Direction::Right, &term)
.skip_while(move |rm| rm.end().line > viewport_start)
.take_while(move |rm| rm.start().line >= viewport_end),
);
let iter = RegexIter::new(start, end, Direction::Right, term, dfas)
.skip_while(move |rm| rm.end().line > viewport_start)
.take_while(move |rm| rm.start().line >= viewport_end)
.map(|rm| {
let viewport_start = term.grid().clamp_buffer_to_visible(*rm.start());
let viewport_end = term.grid().clamp_buffer_to_visible(*rm.end());
viewport_start..=viewport_end
});
Self { iter: iter.peekable() }
Self { matches: iter.collect(), index: 0 }
}
/// Advance the search tracker to the next point.
///
/// This will return `true` if the point passed is part of a search match.
fn advance(&mut self, point: Point<usize>) -> bool {
while let Some(regex_match) = &self.iter.peek() {
fn advance(&mut self, point: Point) -> bool {
while let Some(regex_match) = self.matches.get(self.index) {
if regex_match.start() > &point {
break;
} else if regex_match.end() < &point {
let _ = self.iter.next();
self.index += 1;
} else {
return true;
}

View File

@ -2,9 +2,9 @@
use alacritty_terminal::ansi::CursorShape;
use alacritty_terminal::term::color::Rgb;
use alacritty_terminal::term::render::RenderableCursor;
use alacritty_terminal::term::SizeInfo;
use crate::display::content::RenderableCursor;
use crate::renderer::rects::RenderRect;
/// Trait for conversion into the iterator.
@ -16,7 +16,7 @@ pub trait IntoRects {
impl IntoRects for RenderableCursor {
fn rects(self, size_info: &SizeInfo, thickness: f32) -> CursorRects {
let point = self.point();
let x = point.col.0 as f32 * size_info.cell_width() + size_info.padding_x();
let x = point.column.0 as f32 * size_info.cell_width() + size_info.padding_x();
let y = point.line.0 as f32 * size_info.cell_height() + size_info.padding_y();
let mut width = size_info.cell_width();

View File

@ -22,6 +22,7 @@ use wayland_client::{Display as WaylandDisplay, EventQueue};
use crossfont::{self, Rasterize, Rasterizer};
use alacritty_terminal::ansi::NamedColor;
use alacritty_terminal::event::{EventListener, OnResize};
use alacritty_terminal::grid::Dimensions as _;
use alacritty_terminal::index::{Column, Direction, Line, Point};
@ -33,14 +34,27 @@ use crate::config::window::Dimensions;
#[cfg(not(windows))]
use crate::config::window::StartupMode;
use crate::config::Config;
use crate::cursor::IntoRects;
use crate::display::bell::VisualBell;
use crate::display::color::List;
use crate::display::content::RenderableContent;
use crate::display::cursor::IntoRects;
use crate::display::meter::Meter;
use crate::display::window::Window;
use crate::event::{Mouse, SearchState};
use crate::message_bar::{MessageBuffer, MessageType};
use crate::meter::Meter;
use crate::renderer::rects::{RenderLines, RenderRect};
use crate::renderer::{self, GlyphCache, QuadRenderer};
use crate::url::{Url, Urls};
use crate::window::{self, Window};
pub mod content;
pub mod cursor;
pub mod window;
mod bell;
mod color;
mod meter;
#[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
mod wayland_theme;
const FORWARD_SEARCH_LABEL: &str = "Search: ";
const BACKWARD_SEARCH_LABEL: &str = "Backward Search: ";
@ -162,6 +176,11 @@ pub struct Display {
/// UI cursor visibility for blinking.
pub cursor_hidden: bool,
pub visual_bell: VisualBell,
/// Mapped RGB values for each terminal color.
pub colors: List,
renderer: QuadRenderer,
glyph_cache: GlyphCache,
meter: Meter,
@ -246,7 +265,7 @@ impl Display {
renderer.resize(&size_info);
// Clear screen.
let background_color = config.colors.primary.background;
let background_color = config.ui_config.colors.primary.background;
renderer.with_api(&config.ui_config, &size_info, |api| {
api.clear(background_color);
});
@ -307,6 +326,8 @@ impl Display {
#[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
wayland_event_queue,
cursor_hidden: false,
visual_bell: VisualBell::from(&config.ui_config.bell),
colors: List::from(&config.ui_config.colors),
})
}
@ -435,7 +456,7 @@ impl Display {
/// A reference to Term whose state is being drawn must be provided.
///
/// This call may block if vsync is enabled.
pub fn draw<T>(
pub fn draw<T: EventListener>(
&mut self,
terminal: MutexGuard<'_, Term<T>>,
message_buffer: &MessageBuffer,
@ -452,16 +473,17 @@ impl Display {
let cursor_hidden = self.cursor_hidden || search_state.regex().is_some();
// Collect renderable content before the terminal is dropped.
let mut content = terminal.renderable_content(config, !cursor_hidden);
let dfas = search_state.dfas();
let colors = &self.colors;
let mut content = RenderableContent::new(&terminal, dfas, config, colors, !cursor_hidden);
let mut grid_cells = Vec::new();
while let Some(cell) = content.next() {
grid_cells.push(cell);
}
let background_color = content.color(NamedColor::Background as usize);
let display_offset = content.display_offset();
let cursor = content.cursor();
let visual_bell_intensity = terminal.visual_bell.intensity();
let display_offset = terminal.grid().display_offset();
let background_color = terminal.background_color();
let cursor_point = terminal.grid().cursor.point;
let total_lines = terminal.grid().total_lines();
let metrics = self.glyph_cache.font_metrics();
@ -496,9 +518,9 @@ impl Display {
if cell.is_match
&& viewport_match
.as_ref()
.map_or(false, |viewport_match| viewport_match.contains(&cell.point()))
.map_or(false, |viewport_match| viewport_match.contains(&cell.point))
{
let colors = config.colors.search.focused_match;
let colors = config.ui_config.colors.search.focused_match;
let match_fg = colors.foreground.color(cell.fg, cell.bg);
cell.bg = colors.background.color(cell.fg, cell.bg);
cell.fg = match_fg;
@ -560,13 +582,14 @@ impl Display {
}
// Push visual bell after url/underline/strikeout rects.
let visual_bell_intensity = self.visual_bell.intensity();
if visual_bell_intensity != 0. {
let visual_bell_rect = RenderRect::new(
0.,
0.,
size_info.width(),
size_info.height(),
config.bell().color,
config.ui_config.bell.color,
visual_bell_intensity as f32,
);
rects.push(visual_bell_rect);
@ -581,8 +604,8 @@ impl Display {
let y = size_info.cell_height().mul_add(start_line.0 as f32, size_info.padding_y());
let bg = match message.ty() {
MessageType::Error => config.colors.normal.red,
MessageType::Warning => config.colors.normal.yellow,
MessageType::Error => config.ui_config.colors.normal.red,
MessageType::Warning => config.ui_config.colors.normal.yellow,
};
let message_bar_rect =
@ -596,7 +619,7 @@ impl Display {
// Relay messages to the user.
let glyph_cache = &mut self.glyph_cache;
let fg = config.colors.primary.background;
let fg = config.ui_config.colors.primary.background;
for (i, message_text) in text.iter().enumerate() {
let point = Point::new(start_line + i, Column(0));
self.renderer.with_api(&config.ui_config, &size_info, |mut api| {
@ -650,6 +673,12 @@ impl Display {
}
}
/// Update to a new configuration.
pub fn update_config(&mut self, config: &Config) {
self.visual_bell.update_config(&config.ui_config.bell);
self.colors = List::from(&config.ui_config.colors);
}
/// Format search regex to account for the cursor and fullwidth characters.
fn format_search(size_info: &SizeInfo, search_regex: &str, search_label: &str) -> String {
// Add spacers for wide chars.
@ -690,8 +719,8 @@ impl Display {
let text = format!("{:<1$}", text, num_cols);
let point = Point::new(size_info.screen_lines(), Column(0));
let fg = config.colors.search_bar_foreground();
let bg = config.colors.search_bar_background();
let fg = config.ui_config.colors.search_bar_foreground();
let bg = config.ui_config.colors.search_bar_background();
self.renderer.with_api(&config.ui_config, &size_info, |mut api| {
api.render_string(glyph_cache, point, fg, bg, &text);
@ -708,8 +737,8 @@ impl Display {
let timing = format!("{:.3} usec", self.meter.average());
let point = Point::new(size_info.screen_lines() - 2, Column(0));
let fg = config.colors.primary.background;
let bg = config.colors.normal.red;
let fg = config.ui_config.colors.primary.background;
let bg = config.ui_config.colors.normal.red;
self.renderer.with_api(&config.ui_config, &size_info, |mut api| {
api.render_string(glyph_cache, point, fg, bg, &timing);
@ -727,12 +756,12 @@ impl Display {
) {
let text = format!("[{}/{}]", line, total_lines - 1);
let column = Column(size_info.cols().0.saturating_sub(text.len()));
let colors = &config.colors;
let colors = &config.ui_config.colors;
let fg = colors.line_indicator.foreground.unwrap_or(colors.primary.background);
let bg = colors.line_indicator.background.unwrap_or(colors.primary.foreground);
// Do not render anything if it would obscure the vi mode cursor.
if vi_mode_point.map_or(true, |point| point.line.0 != 0 || point.col < column) {
if vi_mode_point.map_or(true, |point| point.line.0 != 0 || point.column < column) {
let glyph_cache = &mut self.glyph_cache;
self.renderer.with_api(&config.ui_config, &size_info, |mut api| {
api.render_string(glyph_cache, Point::new(Line(0), column), fg, bg, &text);

View File

@ -1,8 +1,9 @@
use glutin::platform::unix::{ARGBColor, Button, ButtonState, Element, Theme as WaylandTheme};
use alacritty_terminal::config::Colors;
use alacritty_terminal::term::color::Rgb;
use crate::config::color::Colors;
const INACTIVE_OPACITY: u8 = 127;
#[derive(Debug, Clone)]

View File

@ -14,9 +14,8 @@ use {
wayland_client::{Attached, EventQueue, Proxy},
glutin::platform::unix::EventLoopWindowTargetExtUnix,
alacritty_terminal::config::Colors,
crate::wayland_theme::AlacrittyWaylandTheme,
crate::config::color::Colors,
crate::display::wayland_theme::AlacrittyWaylandTheme,
};
#[rustfmt::skip]
@ -59,7 +58,7 @@ use crate::gl;
/// Window icon for `_NET_WM_ICON` property.
#[cfg(all(feature = "x11", not(any(target_os = "macos", windows))))]
static WINDOW_ICON: &[u8] = include_bytes!("../alacritty.png");
static WINDOW_ICON: &[u8] = include_bytes!("../../alacritty.png");
/// This should match the definition of IDI_ICON from `windows.rc`.
#[cfg(windows)]
@ -206,7 +205,7 @@ impl Window {
#[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
let wayland_surface = if is_wayland {
// Apply client side decorations theme.
let theme = AlacrittyWaylandTheme::new(&config.colors);
let theme = AlacrittyWaylandTheme::new(&config.ui_config.colors);
windowed_context.window().set_wayland_theme(theme);
// Attach surface to Alacritty's internal wayland queue to handle frame callbacks.
@ -422,7 +421,7 @@ impl Window {
/// Adjust the IME editor position according to the new location of the cursor.
pub fn update_ime_position(&mut self, point: Point, size: &SizeInfo) {
let nspot_x = f64::from(size.padding_x() + point.col.0 as f32 * size.cell_width());
let nspot_x = f64::from(size.padding_x() + point.column.0 as f32 * size.cell_width());
let nspot_y = f64::from(size.padding_y() + (point.line.0 + 1) as f32 * size.cell_height());
self.window().set_ime_position(PhysicalPosition::new(nspot_x, nspot_y));

View File

@ -12,7 +12,7 @@ use std::fs::File;
use std::io::Write;
use std::mem;
use std::ops::RangeInclusive;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
#[cfg(not(any(target_os = "macos", windows)))]
use std::sync::atomic::Ordering;
use std::sync::Arc;
@ -35,6 +35,7 @@ use alacritty_terminal::grid::{Dimensions, Scroll};
use alacritty_terminal::index::{Boundary, Column, Direction, Line, Point, Side};
use alacritty_terminal::selection::{Selection, SelectionType};
use alacritty_terminal::sync::FairMutex;
use alacritty_terminal::term::search::{Match, RegexSearch};
use alacritty_terminal::term::{ClipboardType, SizeInfo, Term, TermMode};
#[cfg(not(windows))]
use alacritty_terminal::tty;
@ -44,6 +45,7 @@ use crate::clipboard::Clipboard;
use crate::config;
use crate::config::Config;
use crate::daemon::start_daemon;
use crate::display::window::Window;
use crate::display::{Display, DisplayUpdate};
use crate::input::{self, ActionContext as _, FONT_SIZE_STEP};
#[cfg(target_os = "macos")]
@ -51,7 +53,6 @@ use crate::macos;
use crate::message_bar::{Message, MessageBuffer};
use crate::scheduler::{Scheduler, TimerId};
use crate::url::{Url, Urls};
use crate::window::Window;
/// Duration after the last user input until an unlimited search is performed.
pub const TYPING_SEARCH_DELAY: Duration = Duration::from_millis(500);
@ -102,13 +103,17 @@ pub struct SearchState {
/// Search regex and history.
///
/// When a search is currently active, the first element will be what the user can modify in
/// the current search session. While going through history, the [`history_index`] will point
/// to the element in history which is currently being previewed.
/// During an active search, the first element is the user's current input.
///
/// While going through history, the [`SearchState::history_index`] will point to the element
/// in history which is currently being previewed.
history: VecDeque<String>,
/// Current position in the search history.
history_index: Option<usize>,
/// Compiled search automatons.
dfas: Option<RegexSearch>,
}
impl SearchState {
@ -131,6 +136,11 @@ impl SearchState {
self.focused_match.as_ref()
}
/// Active search dfas.
pub fn dfas(&self) -> Option<&RegexSearch> {
self.dfas.as_ref()
}
/// Search regex text if a search is active.
fn regex_mut(&mut self) -> Option<&mut String> {
self.history_index.and_then(move |index| self.history.get_mut(index))
@ -146,6 +156,7 @@ impl Default for SearchState {
history_index: Default::default(),
history: Default::default(),
origin: Default::default(),
dfas: Default::default(),
}
}
}
@ -154,31 +165,37 @@ pub struct ActionContext<'a, N, T> {
pub notifier: &'a mut N,
pub terminal: &'a mut Term<T>,
pub clipboard: &'a mut Clipboard,
pub size_info: &'a mut SizeInfo,
pub mouse: &'a mut Mouse,
pub received_count: &'a mut usize,
pub suppress_chars: &'a mut bool,
pub modifiers: &'a mut ModifiersState,
pub window: &'a mut Window,
pub display: &'a mut Display,
pub message_buffer: &'a mut MessageBuffer,
pub display_update_pending: &'a mut DisplayUpdate,
pub config: &'a mut Config,
pub event_loop: &'a EventLoopWindowTarget<Event>,
pub urls: &'a Urls,
pub scheduler: &'a mut Scheduler,
pub search_state: &'a mut SearchState,
cursor_hidden: &'a mut bool,
cli_options: &'a CLIOptions,
font_size: &'a mut Size,
dirty: bool,
}
impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionContext<'a, N, T> {
#[inline]
fn write_to_pty<B: Into<Cow<'static, [u8]>>>(&mut self, val: B) {
self.notifier.notify(val);
}
/// Request a redraw.
#[inline]
fn mark_dirty(&mut self) {
self.dirty = true;
}
#[inline]
fn size_info(&self) -> SizeInfo {
*self.size_info
self.display.size_info
}
fn scroll(&mut self, scroll: Scroll) {
@ -202,8 +219,10 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon
{
let point = self.size_info().pixels_to_coords(self.mouse().x, self.mouse().y);
let cell_side = self.mouse().cell_side;
self.update_selection(Point { line: point.line, col: point.col }, cell_side);
self.update_selection(point, cell_side);
}
self.dirty = true;
}
fn copy_selection(&mut self, ty: ClipboardType) {
@ -220,7 +239,7 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon
fn clear_selection(&mut self) {
self.terminal.selection = None;
self.terminal.dirty = true;
self.dirty = true;
}
fn update_selection(&mut self, mut point: Point, side: Side) {
@ -243,13 +262,13 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon
}
self.terminal.selection = Some(selection);
self.terminal.dirty = true;
self.dirty = true;
}
fn start_selection(&mut self, ty: SelectionType, point: Point, side: Side) {
let point = self.terminal.visible_to_buffer(point);
self.terminal.selection = Some(Selection::new(ty, point, side));
self.terminal.dirty = true;
self.dirty = true;
}
fn toggle_selection(&mut self, ty: SelectionType, point: Point, side: Side) {
@ -259,7 +278,7 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon
},
Some(selection) if !selection.is_empty() => {
selection.ty = ty;
self.terminal.dirty = true;
self.dirty = true;
},
_ => self.start_selection(ty, point, side),
}
@ -269,8 +288,8 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon
let x = self.mouse.x as usize;
let y = self.mouse.y as usize;
if self.size_info.contains_point(x, y) {
Some(self.size_info.pixels_to_coords(x, y))
if self.display.size_info.contains_point(x, y) {
Some(self.display.size_info.pixels_to_coords(x, y))
} else {
None
}
@ -309,12 +328,12 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon
#[inline]
fn window(&self) -> &Window {
self.window
&self.display.window
}
#[inline]
fn window_mut(&mut self) -> &mut Window {
self.window
&mut self.display.window
}
#[inline]
@ -387,17 +406,21 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon
}
}
fn highlighted_url(&self) -> Option<&Url> {
self.display.highlighted_url.as_ref()
}
fn change_font_size(&mut self, delta: f32) {
*self.font_size = max(*self.font_size + delta, Size::new(FONT_SIZE_STEP));
let font = self.config.ui_config.font.clone().with_size(*self.font_size);
self.display_update_pending.set_font(font);
self.terminal.dirty = true;
self.dirty = true;
}
fn reset_font_size(&mut self) {
*self.font_size = self.config.ui_config.font.size();
self.display_update_pending.set_font(self.config.ui_config.font.clone());
self.terminal.dirty = true;
self.dirty = true;
}
#[inline]
@ -405,6 +428,7 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon
if !self.message_buffer.is_empty() {
self.display_update_pending.dirty = true;
self.message_buffer.pop();
self.dirty = true;
}
}
@ -437,7 +461,7 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon
}
self.display_update_pending.dirty = true;
self.terminal.dirty = true;
self.dirty = true;
}
#[inline]
@ -458,8 +482,6 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon
#[inline]
fn cancel_search(&mut self) {
self.terminal.cancel_search();
if self.terminal.mode().contains(TermMode::VI) {
// Recover pre-search state in vi mode.
self.search_reset_state();
@ -471,6 +493,8 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon
self.update_selection(end, Side::Right);
}
self.search_state.dfas = None;
self.exit_search();
}
@ -591,6 +615,29 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon
self.search_state.origin = origin_relative;
}
/// Find the next search match.
fn search_next(
&mut self,
origin: Point<usize>,
direction: Direction,
side: Side,
) -> Option<Match> {
self.search_state
.dfas
.as_ref()
.and_then(|dfas| self.terminal.search_next(dfas, origin, direction, side, None))
}
#[inline]
fn search_direction(&self) -> Direction {
self.search_state.direction
}
#[inline]
fn search_active(&self) -> bool {
self.search_state.history_index.is_some()
}
/// Handle keyboard typing start.
///
/// This will temporarily disable some features like terminal cursor blinking or the mouse
@ -603,24 +650,27 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon
let blink_interval = self.config.cursor.blink_interval();
if let Some(timer) = self.scheduler.get_mut(TimerId::BlinkCursor) {
timer.deadline = Instant::now() + Duration::from_millis(blink_interval);
*self.cursor_hidden = false;
self.terminal.dirty = true;
self.display.cursor_hidden = false;
self.dirty = true;
}
// Hide mouse cursor.
if self.config.ui_config.mouse.hide_when_typing {
self.window.set_mouse_visible(false);
self.display.window.set_mouse_visible(false);
}
}
/// Toggle the vi mode status.
#[inline]
fn search_direction(&self) -> Direction {
self.search_state.direction
}
fn toggle_vi_mode(&mut self) {
if !self.terminal.mode().contains(TermMode::VI) {
self.clear_selection();
}
#[inline]
fn search_active(&self) -> bool {
self.search_state.history_index.is_some()
self.cancel_search();
self.terminal.toggle_vi_mode();
self.dirty = true;
}
fn message(&self) -> Option<&Message> {
@ -636,7 +686,7 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon
}
fn urls(&self) -> &Urls {
self.urls
&self.display.urls
}
fn clipboard_mut(&mut self) -> &mut Clipboard {
@ -657,22 +707,22 @@ impl<'a, N: Notify + 'a, T: EventListener> ActionContext<'a, N, T> {
// Hide cursor while typing into the search bar.
if self.config.ui_config.mouse.hide_when_typing {
self.window.set_mouse_visible(false);
self.display.window.set_mouse_visible(false);
}
if regex.is_empty() {
// Stop search if there's nothing to search for.
self.search_reset_state();
self.terminal.cancel_search();
self.search_state.dfas = None;
} else {
// Create terminal search from the new regex string.
self.terminal.start_search(&regex);
// Create search dfas for the new regex string.
self.search_state.dfas = RegexSearch::new(&regex).ok();
// Update search highlighting.
self.goto_match(MAX_SEARCH_WHILE_TYPING);
}
self.terminal.dirty = true;
self.dirty = true;
}
/// Reset terminal to the state before search was started.
@ -697,22 +747,26 @@ impl<'a, N: Notify + 'a, T: EventListener> ActionContext<'a, N, T> {
// Reset vi mode cursor.
let mut origin = self.search_state.origin;
origin.line = min(origin.line, self.terminal.screen_lines() - 1);
origin.col = min(origin.col, self.terminal.cols() - 1);
origin.column = min(origin.column, self.terminal.cols() - 1);
self.terminal.vi_mode_cursor.point = origin;
self.dirty = true;
}
/// Jump to the first regex match from the search origin.
fn goto_match(&mut self, mut limit: Option<usize>) {
if self.search_state.history_index.is_none() {
return;
}
let dfas = match &self.search_state.dfas {
Some(dfas) => dfas,
None => return,
};
// Limit search only when enough lines are available to run into the limit.
limit = limit.filter(|&limit| limit <= self.terminal.total_lines());
// Jump to the next match.
let direction = self.search_state.direction;
match self.terminal.search_next(self.absolute_origin(), direction, Side::Left, limit) {
let origin = self.absolute_origin();
match self.terminal.search_next(dfas, origin, direction, Side::Left, limit) {
Some(regex_match) => {
let old_offset = self.terminal.grid().display_offset() as isize;
@ -752,7 +806,7 @@ impl<'a, N: Notify + 'a, T: EventListener> ActionContext<'a, N, T> {
},
}
self.terminal.dirty = true;
self.dirty = true;
}
/// Cleanup the search state.
@ -767,7 +821,7 @@ impl<'a, N: Notify + 'a, T: EventListener> ActionContext<'a, N, T> {
self.display_update_pending.dirty = true;
self.search_state.history_index = None;
self.terminal.dirty = true;
self.dirty = true;
// Clear focused match.
self.search_state.focused_match = None;
@ -781,7 +835,7 @@ impl<'a, N: Notify + 'a, T: EventListener> ActionContext<'a, N, T> {
fn absolute_origin(&self) -> Point<usize> {
let mut relative_origin = self.search_state.origin;
relative_origin.line = min(relative_origin.line, self.terminal.screen_lines() - 1);
relative_origin.col = min(relative_origin.col, self.terminal.cols() - 1);
relative_origin.column = min(relative_origin.column, self.terminal.cols() - 1);
let mut origin = self.terminal.visible_to_buffer(relative_origin);
origin.line = (origin.line as isize + self.search_state.display_offset_delta) as usize;
origin
@ -809,8 +863,8 @@ impl<'a, N: Notify + 'a, T: EventListener> ActionContext<'a, N, T> {
TimerId::BlinkCursor,
)
} else {
*self.cursor_hidden = false;
self.terminal.dirty = true;
self.display.cursor_hidden = false;
self.dirty = true;
}
}
}
@ -1012,27 +1066,26 @@ impl<N: Notify + OnResize> Processor<N> {
notifier: &mut self.notifier,
mouse: &mut self.mouse,
clipboard: &mut self.clipboard,
size_info: &mut self.display.size_info,
received_count: &mut self.received_count,
suppress_chars: &mut self.suppress_chars,
modifiers: &mut self.modifiers,
message_buffer: &mut self.message_buffer,
display_update_pending: &mut display_update_pending,
window: &mut self.display.window,
display: &mut self.display,
font_size: &mut self.font_size,
config: &mut self.config,
urls: &self.display.urls,
scheduler: &mut scheduler,
search_state: &mut self.search_state,
cli_options: &self.cli_options,
cursor_hidden: &mut self.display.cursor_hidden,
dirty: false,
event_loop,
};
let mut processor = input::Processor::new(context, &self.display.highlighted_url);
let mut processor = input::Processor::new(context);
for event in self.event_queue.drain(..) {
Processor::handle_event(event, &mut processor);
}
let dirty = processor.ctx.dirty;
// Process DisplayUpdate events.
if display_update_pending.dirty {
@ -1045,11 +1098,9 @@ impl<N: Notify + OnResize> Processor<N> {
return;
}
if terminal.dirty {
terminal.dirty = false;
if dirty {
// Request immediate re-draw if visual bell animation is not finished yet.
if !terminal.visual_bell.completed() {
if !self.display.visual_bell.completed() {
let event: Event = TerminalEvent::Wakeup.into();
self.event_queue.push(event.into());
@ -1079,7 +1130,7 @@ impl<N: Notify + OnResize> Processor<N> {
/// Doesn't take self mutably due to borrow checking.
fn handle_event<T>(
event: GlutinEvent<'_, Event>,
processor: &mut input::Processor<'_, T, ActionContext<'_, N, T>>,
processor: &mut input::Processor<T, ActionContext<'_, N, T>>,
) where
T: EventListener,
{
@ -1095,40 +1146,48 @@ impl<N: Notify + OnResize> Processor<N> {
// Resize to event's dimensions, since no resize event is emitted on Wayland.
display_update_pending.set_dimensions(PhysicalSize::new(width, height));
processor.ctx.window.dpr = scale_factor;
processor.ctx.terminal.dirty = true;
processor.ctx.window_mut().dpr = scale_factor;
processor.ctx.dirty = true;
},
Event::Message(message) => {
processor.ctx.message_buffer.push(message);
processor.ctx.display_update_pending.dirty = true;
processor.ctx.terminal.dirty = true;
processor.ctx.dirty = true;
},
Event::SearchNext => processor.ctx.goto_match(None),
Event::ConfigReload(path) => Self::reload_config(&path, processor),
Event::Scroll(scroll) => processor.ctx.scroll(scroll),
Event::BlinkCursor => {
*processor.ctx.cursor_hidden ^= true;
processor.ctx.terminal.dirty = true;
processor.ctx.display.cursor_hidden ^= true;
processor.ctx.dirty = true;
},
Event::TerminalEvent(event) => match event {
TerminalEvent::Title(title) => {
let ui_config = &processor.ctx.config.ui_config;
if ui_config.window.dynamic_title {
processor.ctx.window.set_title(&title);
processor.ctx.window_mut().set_title(&title);
}
},
TerminalEvent::ResetTitle => {
let ui_config = &processor.ctx.config.ui_config;
if ui_config.window.dynamic_title {
processor.ctx.window.set_title(&ui_config.window.title);
processor.ctx.display.window.set_title(&ui_config.window.title);
}
},
TerminalEvent::Wakeup => processor.ctx.terminal.dirty = true,
TerminalEvent::Wakeup => processor.ctx.dirty = true,
TerminalEvent::Bell => {
let bell_command = processor.ctx.config.bell().command.as_ref();
let _ = bell_command.map(|cmd| start_daemon(cmd.program(), cmd.args()));
// Set window urgency.
if processor.ctx.terminal.mode().contains(TermMode::URGENCY_HINTS) {
processor.ctx.window.set_urgent(!processor.ctx.terminal.is_focused);
let focused = processor.ctx.terminal.is_focused;
processor.ctx.window_mut().set_urgent(!focused);
}
// Ring visual bell.
processor.ctx.display.visual_bell.ring();
// Execute bell command.
if let Some(bell_command) = &processor.ctx.config.ui_config.bell.command {
start_daemon(bell_command.program(), bell_command.args());
}
},
TerminalEvent::ClipboardStore(clipboard_type, content) => {
@ -1138,6 +1197,10 @@ impl<N: Notify + OnResize> Processor<N> {
let text = format(processor.ctx.clipboard.load(clipboard_type).as_str());
processor.ctx.write_to_pty(text.into_bytes());
},
TerminalEvent::ColorRequest(index, format) => {
let text = format(processor.ctx.display.colors[index]);
processor.ctx.write_to_pty(text.into_bytes());
},
TerminalEvent::MouseCursorDirty => processor.reset_mouse_cursor(),
TerminalEvent::Exit => (),
TerminalEvent::CursorBlinkingChange(_) => {
@ -1145,7 +1208,7 @@ impl<N: Notify + OnResize> Processor<N> {
},
},
},
GlutinEvent::RedrawRequested(_) => processor.ctx.terminal.dirty = true,
GlutinEvent::RedrawRequested(_) => processor.ctx.dirty = true,
GlutinEvent::WindowEvent { event, window_id, .. } => {
match event {
WindowEvent::CloseRequested => processor.ctx.terminal.exit(),
@ -1159,37 +1222,37 @@ impl<N: Notify + OnResize> Processor<N> {
}
processor.ctx.display_update_pending.set_dimensions(size);
processor.ctx.terminal.dirty = true;
processor.ctx.dirty = true;
},
WindowEvent::KeyboardInput { input, is_synthetic: false, .. } => {
processor.key_input(input);
},
WindowEvent::ReceivedCharacter(c) => processor.received_char(c),
WindowEvent::MouseInput { state, button, .. } => {
processor.ctx.window.set_mouse_visible(true);
processor.mouse_input(state, button);
processor.ctx.terminal.dirty = true;
},
WindowEvent::ModifiersChanged(modifiers) => {
processor.modifiers_input(modifiers)
},
WindowEvent::ReceivedCharacter(c) => processor.received_char(c),
WindowEvent::MouseInput { state, button, .. } => {
processor.ctx.window_mut().set_mouse_visible(true);
processor.mouse_input(state, button);
processor.ctx.dirty = true;
},
WindowEvent::CursorMoved { position, .. } => {
processor.ctx.window.set_mouse_visible(true);
processor.ctx.window_mut().set_mouse_visible(true);
processor.mouse_moved(position);
},
WindowEvent::MouseWheel { delta, phase, .. } => {
processor.ctx.window.set_mouse_visible(true);
processor.ctx.window_mut().set_mouse_visible(true);
processor.mouse_wheel_input(delta, phase);
},
WindowEvent::Focused(is_focused) => {
if window_id == processor.ctx.window.window_id() {
if window_id == processor.ctx.window().window_id() {
processor.ctx.terminal.is_focused = is_focused;
processor.ctx.terminal.dirty = true;
processor.ctx.dirty = true;
if is_focused {
processor.ctx.window.set_urgent(false);
processor.ctx.window_mut().set_urgent(false);
} else {
processor.ctx.window.set_mouse_visible(true);
processor.ctx.window_mut().set_mouse_visible(true);
}
processor.ctx.update_cursor_blinking();
@ -1203,8 +1266,8 @@ impl<N: Notify + OnResize> Processor<N> {
WindowEvent::CursorLeft { .. } => {
processor.ctx.mouse.inside_text_area = false;
if processor.highlighted_url.is_some() {
processor.ctx.terminal.dirty = true;
if processor.ctx.highlighted_url().is_some() {
processor.ctx.dirty = true;
}
},
WindowEvent::KeyboardInput { is_synthetic: true, .. }
@ -1253,10 +1316,9 @@ impl<N: Notify + OnResize> Processor<N> {
}
}
fn reload_config<T>(
path: &PathBuf,
processor: &mut input::Processor<'_, T, ActionContext<'_, N, T>>,
) where
/// Reload the configuration files from disk.
fn reload_config<T>(path: &Path, processor: &mut input::Processor<T, ActionContext<'_, N, T>>)
where
T: EventListener,
{
if !processor.ctx.message_buffer.is_empty() {
@ -1269,6 +1331,7 @@ impl<N: Notify + OnResize> Processor<N> {
Err(_) => return,
};
processor.ctx.display.update_config(&config);
processor.ctx.terminal.update_config(&config);
// Reload cursor if its thickness has changed.
@ -1300,12 +1363,12 @@ impl<N: Notify + OnResize> Processor<N> {
if !config.ui_config.window.dynamic_title
|| processor.ctx.config.ui_config.window.title != config.ui_config.window.title
{
processor.ctx.window.set_title(&config.ui_config.window.title);
processor.ctx.window_mut().set_title(&config.ui_config.window.title);
}
#[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
if processor.ctx.event_loop.is_wayland() {
processor.ctx.window.set_wayland_theme(&config.colors);
processor.ctx.window_mut().set_wayland_theme(&config.ui_config.colors);
}
// Set subpixel anti-aliasing.
@ -1314,14 +1377,14 @@ impl<N: Notify + OnResize> Processor<N> {
// Disable shadows for transparent windows on macOS.
#[cfg(target_os = "macos")]
processor.ctx.window.set_has_shadow(config.ui_config.background_opacity() >= 1.0);
processor.ctx.window_mut().set_has_shadow(config.ui_config.background_opacity() >= 1.0);
*processor.ctx.config = config;
// Update cursor blinking.
processor.ctx.update_cursor_blinking();
processor.ctx.terminal.dirty = true;
processor.ctx.dirty = true;
}
/// Submit the pending changes to the `Display`.

View File

@ -26,17 +26,18 @@ use alacritty_terminal::event::EventListener;
use alacritty_terminal::grid::{Dimensions, Scroll};
use alacritty_terminal::index::{Boundary, Column, Direction, Line, Point, Side};
use alacritty_terminal::selection::SelectionType;
use alacritty_terminal::term::search::Match;
use alacritty_terminal::term::{ClipboardType, SizeInfo, Term, TermMode};
use alacritty_terminal::vi_mode::ViMotion;
use crate::clipboard::Clipboard;
use crate::config::{Action, Binding, BindingMode, Config, Key, SearchAction, ViAction};
use crate::daemon::start_daemon;
use crate::display::window::Window;
use crate::event::{ClickState, Event, Mouse, TYPING_SEARCH_DELAY};
use crate::message_bar::{self, Message};
use crate::scheduler::{Scheduler, TimerId};
use crate::url::{Url, Urls};
use crate::window::Window;
/// Font size change interval.
pub const FONT_SIZE_STEP: f32 = 0.5;
@ -54,20 +55,20 @@ const SELECTION_SCROLLING_STEP: f64 = 20.;
///
/// An escape sequence may be emitted in case specific keys or key combinations
/// are activated.
pub struct Processor<'a, T: EventListener, A: ActionContext<T>> {
pub struct Processor<T: EventListener, A: ActionContext<T>> {
pub ctx: A,
pub highlighted_url: &'a Option<Url>,
_phantom: PhantomData<T>,
}
pub trait ActionContext<T: EventListener> {
fn write_to_pty<B: Into<Cow<'static, [u8]>>>(&mut self, data: B);
fn write_to_pty<B: Into<Cow<'static, [u8]>>>(&mut self, _data: B) {}
fn mark_dirty(&mut self) {}
fn size_info(&self) -> SizeInfo;
fn copy_selection(&mut self, ty: ClipboardType);
fn start_selection(&mut self, ty: SelectionType, point: Point, side: Side);
fn toggle_selection(&mut self, ty: SelectionType, point: Point, side: Side);
fn update_selection(&mut self, point: Point, side: Side);
fn clear_selection(&mut self);
fn copy_selection(&mut self, _ty: ClipboardType) {}
fn start_selection(&mut self, _ty: SelectionType, _point: Point, _side: Side) {}
fn toggle_selection(&mut self, _ty: SelectionType, _point: Point, _side: Side) {}
fn update_selection(&mut self, _point: Point, _side: Side) {}
fn clear_selection(&mut self) {}
fn selection_is_empty(&self) -> bool;
fn mouse_mut(&mut self) -> &mut Mouse;
fn mouse(&self) -> &Mouse;
@ -75,34 +76,42 @@ pub trait ActionContext<T: EventListener> {
fn received_count(&mut self) -> &mut usize;
fn suppress_chars(&mut self) -> &mut bool;
fn modifiers(&mut self) -> &mut ModifiersState;
fn scroll(&mut self, scroll: Scroll);
fn scroll(&mut self, _scroll: Scroll) {}
fn window(&self) -> &Window;
fn window_mut(&mut self) -> &mut Window;
fn terminal(&self) -> &Term<T>;
fn terminal_mut(&mut self) -> &mut Term<T>;
fn spawn_new_instance(&mut self);
fn change_font_size(&mut self, delta: f32);
fn reset_font_size(&mut self);
fn pop_message(&mut self);
fn spawn_new_instance(&mut self) {}
fn change_font_size(&mut self, _delta: f32) {}
fn reset_font_size(&mut self) {}
fn pop_message(&mut self) {}
fn message(&self) -> Option<&Message>;
fn config(&self) -> &Config;
fn event_loop(&self) -> &EventLoopWindowTarget<Event>;
fn urls(&self) -> &Urls;
fn launch_url(&self, url: Url);
fn launch_url(&self, _url: Url) {}
fn highlighted_url(&self) -> Option<&Url>;
fn mouse_mode(&self) -> bool;
fn clipboard_mut(&mut self) -> &mut Clipboard;
fn scheduler_mut(&mut self) -> &mut Scheduler;
fn start_search(&mut self, direction: Direction);
fn confirm_search(&mut self);
fn cancel_search(&mut self);
fn search_input(&mut self, c: char);
fn search_pop_word(&mut self);
fn search_history_previous(&mut self);
fn search_history_next(&mut self);
fn advance_search_origin(&mut self, direction: Direction);
fn start_search(&mut self, _direction: Direction) {}
fn confirm_search(&mut self) {}
fn cancel_search(&mut self) {}
fn search_input(&mut self, _c: char) {}
fn search_pop_word(&mut self) {}
fn search_history_previous(&mut self) {}
fn search_history_next(&mut self) {}
fn search_next(
&mut self,
origin: Point<usize>,
direction: Direction,
side: Side,
) -> Option<Match>;
fn advance_search_origin(&mut self, _direction: Direction) {}
fn search_direction(&self) -> Direction;
fn search_active(&self) -> bool;
fn on_typing_start(&mut self);
fn on_typing_start(&mut self) {}
fn toggle_vi_mode(&mut self) {}
}
trait Execute<T: EventListener> {
@ -120,8 +129,8 @@ impl<T, U: EventListener> Execute<U> for Binding<T> {
impl Action {
fn toggle_selection<T, A>(ctx: &mut A, ty: SelectionType)
where
T: EventListener,
A: ActionContext<T>,
T: EventListener,
{
let cursor_point = ctx.terminal().vi_mode_cursor.point;
ctx.toggle_selection(ty, cursor_point, Side::Left);
@ -151,10 +160,11 @@ impl<T: EventListener> Execute<T> for Action {
start_daemon(program, args);
},
Action::ToggleViMode => ctx.terminal_mut().toggle_vi_mode(),
Action::ToggleViMode => ctx.toggle_vi_mode(),
Action::ViMotion(motion) => {
ctx.on_typing_start();
ctx.terminal_mut().vi_motion(motion)
ctx.terminal_mut().vi_motion(motion);
ctx.mark_dirty();
},
Action::ViAction(ViAction::ToggleNormalSelection) => {
Self::toggle_selection(ctx, SelectionType::Simple)
@ -183,9 +193,9 @@ impl<T: EventListener> Execute<T> for Action {
Direction::Left => vi_point.sub_absolute(terminal, Boundary::Wrap, 1),
};
let regex_match = terminal.search_next(origin, direction, Side::Left, None);
if let Some(regex_match) = regex_match {
if let Some(regex_match) = ctx.search_next(origin, direction, Side::Left) {
ctx.terminal_mut().vi_goto_point(*regex_match.start());
ctx.mark_dirty();
}
},
Action::ViAction(ViAction::SearchPrevious) => {
@ -197,9 +207,9 @@ impl<T: EventListener> Execute<T> for Action {
Direction::Left => vi_point.sub_absolute(terminal, Boundary::Wrap, 1),
};
let regex_match = terminal.search_next(origin, direction, Side::Left, None);
if let Some(regex_match) = regex_match {
if let Some(regex_match) = ctx.search_next(origin, direction, Side::Left) {
ctx.terminal_mut().vi_goto_point(*regex_match.start());
ctx.mark_dirty();
}
},
Action::ViAction(ViAction::SearchStart) => {
@ -208,9 +218,9 @@ impl<T: EventListener> Execute<T> for Action {
.visible_to_buffer(terminal.vi_mode_cursor.point)
.sub_absolute(terminal, Boundary::Wrap, 1);
let regex_match = terminal.search_next(origin, Direction::Left, Side::Left, None);
if let Some(regex_match) = regex_match {
if let Some(regex_match) = ctx.search_next(origin, Direction::Left, Side::Left) {
ctx.terminal_mut().vi_goto_point(*regex_match.start());
ctx.mark_dirty();
}
},
Action::ViAction(ViAction::SearchEnd) => {
@ -219,9 +229,9 @@ impl<T: EventListener> Execute<T> for Action {
.visible_to_buffer(terminal.vi_mode_cursor.point)
.add_absolute(terminal, Boundary::Wrap, 1);
let regex_match = terminal.search_next(origin, Direction::Right, Side::Right, None);
if let Some(regex_match) = regex_match {
if let Some(regex_match) = ctx.search_next(origin, Direction::Right, Side::Right) {
ctx.terminal_mut().vi_goto_point(*regex_match.end());
ctx.mark_dirty();
}
},
Action::SearchAction(SearchAction::SearchFocusNext) => {
@ -328,6 +338,7 @@ impl<T: EventListener> Execute<T> for Action {
// Move vi mode cursor.
ctx.terminal_mut().vi_mode_cursor.point.line = Line(0);
ctx.terminal_mut().vi_motion(ViMotion::FirstOccupied);
ctx.mark_dirty();
},
Action::ScrollToBottom => {
ctx.scroll(Scroll::Bottom);
@ -339,6 +350,7 @@ impl<T: EventListener> Execute<T> for Action {
// Move to beginning twice, to always jump across linewraps.
term.vi_motion(ViMotion::FirstOccupied);
term.vi_motion(ViMotion::FirstOccupied);
ctx.mark_dirty();
},
Action::ClearHistory => ctx.terminal_mut().clear_screen(ClearMode::Saved),
Action::ClearLogNotice => ctx.pop_message(),
@ -387,9 +399,9 @@ impl From<MouseState> for CursorIcon {
}
}
impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
pub fn new(ctx: A, highlighted_url: &'a Option<Url>) -> Self {
Self { ctx, highlighted_url, _phantom: Default::default() }
impl<T: EventListener, A: ActionContext<T>> Processor<T, A> {
pub fn new(ctx: A) -> Self {
Self { ctx, _phantom: Default::default() }
}
#[inline]
@ -415,7 +427,7 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
let cell_side = self.get_mouse_side();
let cell_changed =
point.line != self.ctx.mouse().line || point.col != self.ctx.mouse().column;
point.line != self.ctx.mouse().line || point.column != self.ctx.mouse().column;
// Update mouse state and check for URL change.
let mouse_state = self.mouse_state();
@ -433,7 +445,7 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
self.ctx.mouse_mut().inside_text_area = inside_text_area;
self.ctx.mouse_mut().cell_side = cell_side;
self.ctx.mouse_mut().line = point.line;
self.ctx.mouse_mut().column = point.col;
self.ctx.mouse_mut().column = point.column;
// Don't launch URLs if mouse has moved.
self.ctx.mouse_mut().block_url_launcher = true;
@ -757,7 +769,7 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
// Try to restore vi mode cursor position, to keep it above its previous content.
let term = self.ctx.terminal_mut();
term.vi_mode_cursor.point = term.grid().clamp_buffer_to_visible(absolute);
term.vi_mode_cursor.point.col = absolute.col;
term.vi_mode_cursor.point.column = absolute.column;
// Update selection.
if term.mode().contains(TermMode::VI) {
@ -986,12 +998,13 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
/// Trigger redraw when URL highlight changed.
#[inline]
fn update_url_state(&mut self, mouse_state: &MouseState) {
let highlighted_url = self.ctx.highlighted_url();
if let MouseState::Url(url) = mouse_state {
if Some(url) != self.highlighted_url.as_ref() {
self.ctx.terminal_mut().dirty = true;
if Some(url) != highlighted_url {
self.ctx.mark_dirty();
}
} else if self.highlighted_url.is_some() {
self.ctx.terminal_mut().dirty = true;
} else if highlighted_url.is_some() {
self.ctx.mark_dirty();
}
}
@ -1084,10 +1097,7 @@ mod tests {
const KEY: VirtualKeyCode = VirtualKeyCode::Key0;
struct MockEventProxy;
impl EventListener for MockEventProxy {
fn send_event(&self, _event: TerminalEvent) {}
}
impl EventListener for MockEventProxy {}
struct ActionContext<'a, T> {
pub terminal: &'a mut Term<T>,
@ -1103,39 +1113,14 @@ mod tests {
}
impl<'a, T: EventListener> super::ActionContext<T> for ActionContext<'a, T> {
fn write_to_pty<B: Into<Cow<'static, [u8]>>>(&mut self, _val: B) {}
fn update_selection(&mut self, _point: Point, _side: Side) {}
fn start_selection(&mut self, _ty: SelectionType, _point: Point, _side: Side) {}
fn toggle_selection(&mut self, _ty: SelectionType, _point: Point, _side: Side) {}
fn copy_selection(&mut self, _: ClipboardType) {}
fn clear_selection(&mut self) {}
fn spawn_new_instance(&mut self) {}
fn change_font_size(&mut self, _delta: f32) {}
fn reset_font_size(&mut self) {}
fn start_search(&mut self, _direction: Direction) {}
fn confirm_search(&mut self) {}
fn cancel_search(&mut self) {}
fn search_input(&mut self, _c: char) {}
fn search_pop_word(&mut self) {}
fn search_history_previous(&mut self) {}
fn search_history_next(&mut self) {}
fn advance_search_origin(&mut self, _direction: Direction) {}
fn search_next(
&mut self,
_origin: Point<usize>,
_direction: Direction,
_side: Side,
) -> Option<Match> {
None
}
fn search_direction(&self) -> Direction {
Direction::Right
@ -1234,17 +1219,13 @@ mod tests {
unimplemented!();
}
fn launch_url(&self, _: Url) {
fn highlighted_url(&self) -> Option<&Url> {
unimplemented!();
}
fn scheduler_mut(&mut self) -> &mut Scheduler {
unimplemented!();
}
fn on_typing_start(&mut self) {
unimplemented!();
}
}
macro_rules! test_clickstate {
@ -1293,7 +1274,7 @@ mod tests {
config: &cfg,
};
let mut processor = Processor::new(context, &None);
let mut processor = Processor::new(context);
let event: GlutinEvent::<'_, TerminalEvent> = $input;
if let GlutinEvent::WindowEvent {

View File

@ -32,7 +32,6 @@ use alacritty_terminal::tty;
mod cli;
mod clipboard;
mod config;
mod cursor;
mod daemon;
mod display;
mod event;
@ -41,16 +40,11 @@ mod logging;
#[cfg(target_os = "macos")]
mod macos;
mod message_bar;
mod meter;
#[cfg(windows)]
mod panic;
mod renderer;
mod scheduler;
mod url;
mod window;
#[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
mod wayland_theme;
mod gl {
#![allow(clippy::all)]

View File

@ -17,11 +17,11 @@ use unicode_width::UnicodeWidthChar;
use alacritty_terminal::index::Point;
use alacritty_terminal::term::cell::Flags;
use alacritty_terminal::term::color::Rgb;
use alacritty_terminal::term::render::RenderableCell;
use alacritty_terminal::term::SizeInfo;
use crate::config::font::{Font, FontDescription};
use crate::config::ui_config::{Delta, UIConfig};
use crate::display::content::RenderableCell;
use crate::gl;
use crate::gl::types::*;
use crate::renderer::rects::{RectRenderer, RenderRect};
@ -480,8 +480,8 @@ impl Batch {
cell_flags.set(RenderingGlyphFlags::WIDE_CHAR, cell.flags.contains(Flags::WIDE_CHAR));
self.instances.push(InstanceData {
col: cell.column.0 as u16,
row: cell.line.0 as u16,
col: cell.point.column.0 as u16,
row: cell.point.line.0 as u16,
top: glyph.top,
left: glyph.left,
@ -829,8 +829,7 @@ impl<'a> RenderApi<'a> {
.chars()
.enumerate()
.map(|(i, character)| RenderableCell {
line: point.line,
column: point.col + i,
point: Point::new(point.line, point.column + i),
character,
zerowidth: None,
flags: Flags::empty(),

View File

@ -6,9 +6,9 @@ use crossfont::Metrics;
use alacritty_terminal::index::{Column, Point};
use alacritty_terminal::term::cell::Flags;
use alacritty_terminal::term::color::Rgb;
use alacritty_terminal::term::render::RenderableCell;
use alacritty_terminal::term::SizeInfo;
use crate::display::content::RenderableCell;
use crate::gl;
use crate::gl::types::*;
use crate::renderer;
@ -105,8 +105,8 @@ impl RenderLine {
mut thickness: f32,
color: Rgb,
) -> RenderRect {
let start_x = start.col.0 as f32 * size.cell_width();
let end_x = (end.col.0 + 1) as f32 * size.cell_width();
let start_x = start.column.0 as f32 * size.cell_width();
let end_x = (end.column.0 + 1) as f32 * size.cell_width();
let width = end_x - start_x;
// Make sure lines are always visible.
@ -169,16 +169,16 @@ impl RenderLines {
}
// Include wide char spacer if the current cell is a wide char.
let mut end: Point = cell.into();
let mut end = cell.point;
if cell.flags.contains(Flags::WIDE_CHAR) {
end.col += 1;
end.column += 1;
}
// Check if there's an active line.
if let Some(line) = self.inner.get_mut(&flag).and_then(|lines| lines.last_mut()) {
if cell.fg == line.color
&& cell.column == line.end.col + 1
&& cell.line == line.end.line
&& cell.point.column == line.end.column + 1
&& cell.point.line == line.end.line
{
// Update the length of the line.
line.end = end;
@ -187,7 +187,7 @@ impl RenderLines {
}
// Start new line if there currently is none.
let line = RenderLine { start: cell.into(), end, color: cell.fg };
let line = RenderLine { start: cell.point, end, color: cell.fg };
match self.inner.get_mut(&flag) {
Some(lines) => lines.push(line),
None => {

View File

@ -8,10 +8,10 @@ use urlocator::{UrlLocation, UrlLocator};
use alacritty_terminal::index::{Column, Point};
use alacritty_terminal::term::cell::Flags;
use alacritty_terminal::term::color::Rgb;
use alacritty_terminal::term::render::RenderableCell;
use alacritty_terminal::term::SizeInfo;
use crate::config::Config;
use crate::display::content::RenderableCell;
use crate::event::Mouse;
use crate::renderer::rects::{RenderLine, RenderRect};
@ -73,12 +73,12 @@ impl Urls {
// Update tracked URLs.
pub fn update(&mut self, num_cols: Column, cell: &RenderableCell) {
let point: Point = cell.into();
let point = cell.point;
let mut end = point;
// Include the following wide char spacer.
if cell.flags.contains(Flags::WIDE_CHAR) {
end.col += 1;
end.column += 1;
}
// Reset URL when empty cells have been skipped.
@ -119,13 +119,13 @@ impl Urls {
(UrlLocation::Url(_length, end_offset), UrlLocation::Url(..)) => {
self.extend_url(point, end, cell.fg, end_offset);
},
(UrlLocation::Scheme, _) => self.scheme_buffer.push((cell.into(), cell.fg)),
(UrlLocation::Scheme, _) => self.scheme_buffer.push((cell.point, cell.fg)),
(UrlLocation::Reset, _) => self.reset(),
_ => (),
}
// Reset at un-wrapped linebreak.
if cell.column + 1 == num_cols && !cell.flags.contains(Flags::WRAPLINE) {
if cell.point.column + 1 == num_cols && !cell.flags.contains(Flags::WRAPLINE) {
self.reset();
}
}
@ -202,8 +202,7 @@ mod tests {
.map(|(i, character)| RenderableCell {
character,
zerowidth: None,
line: Line(0),
column: Column(i),
point: Point::new(Line(0), Column(i)),
fg: Default::default(),
bg: Default::default(),
bg_alpha: 0.,
@ -227,8 +226,8 @@ mod tests {
}
let url = urls.urls.first().unwrap();
assert_eq!(url.start().col, Column(5));
assert_eq!(url.end().col, Column(23));
assert_eq!(url.start().column, Column(5));
assert_eq!(url.end().column, Column(23));
}
#[test]
@ -244,14 +243,14 @@ mod tests {
assert_eq!(urls.urls.len(), 3);
assert_eq!(urls.urls[0].start().col, Column(5));
assert_eq!(urls.urls[0].end().col, Column(9));
assert_eq!(urls.urls[0].start().column, Column(5));
assert_eq!(urls.urls[0].end().column, Column(9));
assert_eq!(urls.urls[1].start().col, Column(11));
assert_eq!(urls.urls[1].end().col, Column(15));
assert_eq!(urls.urls[1].start().column, Column(11));
assert_eq!(urls.urls[1].end().column, Column(15));
assert_eq!(urls.urls[2].start().col, Column(17));
assert_eq!(urls.urls[2].end().col, Column(21));
assert_eq!(urls.urls[2].start().column, Column(17));
assert_eq!(urls.urls[2].end().column, Column(21));
}
#[test]
@ -267,10 +266,10 @@ mod tests {
assert_eq!(urls.urls.len(), 2);
assert_eq!(urls.urls[0].start().col, Column(5));
assert_eq!(urls.urls[0].end().col, Column(17));
assert_eq!(urls.urls[0].start().column, Column(5));
assert_eq!(urls.urls[0].end().column, Column(17));
assert_eq!(urls.urls[1].start().col, Column(20));
assert_eq!(urls.urls[1].end().col, Column(28));
assert_eq!(urls.urls[1].start().column, Column(20));
assert_eq!(urls.urls[1].end().column, Column(28));
}
}

View File

@ -1,6 +1,6 @@
[package]
name = "alacritty_terminal"
version = "0.12.1-dev"
version = "0.13.0-dev"
authors = ["Christian Duerr <contact@christianduerr.com>", "Joe Wilm <joe@jwilm.com>"]
license = "Apache-2.0"
description = "Library for writing terminal emulators"

View File

@ -306,7 +306,7 @@ pub trait Handler {
fn set_color(&mut self, _: usize, _: Rgb) {}
/// Write a foreground/background color escape sequence with the current color.
fn dynamic_color_sequence<W: io::Write>(&mut self, _: &mut W, _: u8, _: usize, _: &str) {}
fn dynamic_color_sequence(&mut self, _: u8, _: usize, _: &str) {}
/// Reset an indexed color to original value.
fn reset_color(&mut self, _: usize) {}
@ -778,10 +778,10 @@ where
}
#[inline]
fn hook(&mut self, params: &Params, intermediates: &[u8], ignore: bool, _c: char) {
fn hook(&mut self, params: &Params, intermediates: &[u8], ignore: bool, action: char) {
debug!(
"[unhandled hook] params={:?}, ints: {:?}, ignore: {:?}",
params, intermediates, ignore
"[unhandled hook] params={:?}, ints: {:?}, ignore: {:?}, action: {:?}",
params, intermediates, ignore, action
);
}
@ -798,7 +798,6 @@ where
// TODO replace OSC parsing with parser combinators.
#[inline]
fn osc_dispatch(&mut self, params: &[&[u8]], bell_terminated: bool) {
let writer = &mut self.writer;
let terminator = if bell_terminated { "\x07" } else { "\x1b\\" };
fn unhandled(params: &[&[u8]]) {
@ -868,7 +867,6 @@ where
self.handler.set_color(index, color);
} else if param == b"?" {
self.handler.dynamic_color_sequence(
writer,
dynamic_code,
index,
terminator,

View File

@ -6,14 +6,10 @@ use serde::Deserialize;
use alacritty_config_derive::ConfigDeserialize;
mod bell;
mod colors;
mod scrolling;
use crate::ansi::{CursorShape, CursorStyle};
pub use crate::config::bell::{BellAnimation, BellConfig};
pub use crate::config::colors::Colors;
pub use crate::config::scrolling::Scrolling;
pub const LOG_TARGET_CONFIG: &str = "alacritty_config_derive";
@ -27,11 +23,6 @@ pub struct Config<T> {
/// TERM env variable.
pub env: HashMap<String, String>,
/// Should draw bold text with brighter colors instead of bold font.
pub draw_bold_text_with_bright_colors: bool,
pub colors: Colors,
pub selection: Selection,
/// Path to a shell program to run on startup.
@ -53,19 +44,6 @@ pub struct Config<T> {
/// Remain open after child process exits.
#[config(skip)]
pub hold: bool,
/// Bell configuration.
bell: BellConfig,
#[config(deprecated = "use `bell` instead")]
pub visual_bell: Option<BellConfig>,
}
impl<T> Config<T> {
#[inline]
pub fn bell(&self) -> &BellConfig {
self.visual_bell.as_ref().unwrap_or(&self.bell)
}
}
#[derive(ConfigDeserialize, Clone, Debug, PartialEq, Eq)]
@ -190,9 +168,9 @@ impl CursorBlinking {
}
}
impl Into<bool> for CursorBlinking {
fn into(self) -> bool {
self == Self::On || self == Self::Always
impl From<CursorBlinking> for bool {
fn from(blinking: CursorBlinking) -> bool {
blinking == CursorBlinking::On || blinking == CursorBlinking::Always
}
}

View File

@ -2,18 +2,49 @@ use std::borrow::Cow;
use std::fmt::{self, Debug, Formatter};
use std::sync::Arc;
use crate::term::color::Rgb;
use crate::term::{ClipboardType, SizeInfo};
/// Terminal event.
///
/// These events instruct the UI over changes that can't be handled by the terminal emulation layer
/// itself.
#[derive(Clone)]
pub enum Event {
/// Grid has changed possibly requiring a mouse cursor shape change.
MouseCursorDirty,
/// Window title change.
Title(String),
/// Reset to the default window title.
ResetTitle,
/// Request to store a text string in the clipboard.
ClipboardStore(ClipboardType, String),
/// Request to write the contents of the clipboard to the PTY.
///
/// The attached function is a formatter which will corectly transform the clipboard content
/// into the expected escape sequence format.
ClipboardLoad(ClipboardType, Arc<dyn Fn(&str) -> String + Sync + Send + 'static>),
/// Request to write the RGB value of a color to the PTY.
///
/// The attached function is a formatter which will corectly transform the RGB color into the
/// expected escape sequence format.
ColorRequest(usize, Arc<dyn Fn(Rgb) -> String + Sync + Send + 'static>),
/// Cursor blinking state has changed.
CursorBlinkingChange(bool),
/// New terminal content available.
Wakeup,
/// Terminal bell ring.
Bell,
/// Shutdown request.
Exit,
}
@ -25,6 +56,7 @@ impl Debug for Event {
Event::ResetTitle => write!(f, "ResetTitle"),
Event::ClipboardStore(ty, text) => write!(f, "ClipboardStore({:?}, {})", ty, text),
Event::ClipboardLoad(ty, _) => write!(f, "ClipboardLoad({:?})", ty),
Event::ColorRequest(index, _) => write!(f, "ColorRequest({})", index),
Event::Wakeup => write!(f, "Wakeup"),
Event::Bell => write!(f, "Bell"),
Event::Exit => write!(f, "Exit"),
@ -48,5 +80,9 @@ pub trait OnResize {
/// Event Loop for notifying the renderer about terminal events.
pub trait EventListener {
fn send_event(&self, event: Event);
fn send_event(&self, _event: Event) {}
}
/// Placeholder implementation for tests.
#[cfg(test)]
impl EventListener for () {}

View File

@ -1,7 +1,8 @@
//! A specialized 2D grid implementation optimized for use in a terminal.
use std::cmp::{max, min};
use std::ops::{Deref, Index, IndexMut, Range, RangeFrom, RangeFull, RangeInclusive, RangeTo};
use std::iter::{Map, TakeWhile};
use std::ops::{Bound, Deref, Index, IndexMut, Range, RangeBounds, RangeInclusive};
use serde::{Deserialize, Serialize};
@ -18,37 +19,6 @@ mod tests;
pub use self::row::Row;
use self::storage::Storage;
/// Bidirectional iterator.
pub trait BidirectionalIterator: Iterator {
fn prev(&mut self) -> Option<Self::Item>;
}
/// An item in the grid along with its Line and Column.
pub struct Indexed<T> {
pub inner: T,
pub line: Line,
pub column: Column,
}
impl<T> Deref for Indexed<T> {
type Target = T;
#[inline]
fn deref(&self) -> &T {
&self.inner
}
}
impl<T: PartialEq> ::std::cmp::PartialEq for Grid<T> {
fn eq(&self, other: &Self) -> bool {
// Compare struct fields and check result of grid comparison.
self.raw.eq(&other.raw)
&& self.cols.eq(&other.cols)
&& self.lines.eq(&other.lines)
&& self.display_offset.eq(&other.display_offset)
}
}
pub trait GridCell: Sized {
/// Check if the cell contains any content.
fn is_empty(&self) -> bool;
@ -99,6 +69,15 @@ impl IndexMut<CharsetIndex> for Charsets {
}
}
#[derive(Debug, Copy, Clone)]
pub enum Scroll {
Delta(isize),
PageUp,
PageDown,
Top,
Bottom,
}
/// Grid based terminal content storage.
///
/// ```notrust
@ -157,15 +136,6 @@ pub struct Grid<T> {
max_scroll_limit: usize,
}
#[derive(Debug, Copy, Clone)]
pub enum Scroll {
Delta(isize),
PageUp,
PageDown,
Top,
Bottom,
}
impl<T: GridCell + Default + PartialEq + Clone> Grid<T> {
pub fn new(lines: Line, cols: Column, max_scroll_limit: usize) -> Grid<T> {
Grid {
@ -341,15 +311,15 @@ impl<T: GridCell + Default + PartialEq + Clone> Grid<T> {
D: PartialEq,
{
// Determine how many lines to scroll up by.
let end = Point { line: 0, col: self.cols() };
let end = Point { line: 0, column: self.cols() };
let mut iter = self.iter_from(end);
while let Some(cell) = iter.prev() {
if !cell.is_empty() || iter.cur.line >= *self.lines {
if !cell.is_empty() || cell.point.line >= *self.lines {
break;
}
}
debug_assert!(iter.cur.line <= *self.lines);
let positions = self.lines - iter.cur.line;
debug_assert!(iter.point.line <= *self.lines);
let positions = self.lines - iter.point.line;
let region = Line(0)..self.screen_lines();
// Reset display offset.
@ -383,8 +353,33 @@ impl<T: GridCell + Default + PartialEq + Clone> Grid<T> {
}
}
#[allow(clippy::len_without_is_empty)]
impl<T> Grid<T> {
/// Reset a visible region within the grid.
pub fn reset_region<D, R: RangeBounds<Line>>(&mut self, bounds: R)
where
T: ResetDiscriminant<D> + GridCell + Clone + Default,
D: PartialEq,
{
let start = match bounds.start_bound() {
Bound::Included(line) => *line,
Bound::Excluded(line) => *line + 1,
Bound::Unbounded => Line(0),
};
let end = match bounds.end_bound() {
Bound::Included(line) => *line + 1,
Bound::Excluded(line) => *line,
Bound::Unbounded => self.screen_lines(),
};
debug_assert!(start < self.screen_lines());
debug_assert!(end <= self.screen_lines());
for row in start.0..end.0 {
self.raw[Line(row)].reset(&self.cursor.template);
}
}
/// Clamp a buffer point to the visible region.
pub fn clamp_buffer_to_visible(&self, point: Point<usize>) -> Point {
if point.line < self.display_offset {
@ -424,12 +419,7 @@ impl<T> Grid<T> {
/// Convert viewport relative point to global buffer indexing.
#[inline]
pub fn visible_to_buffer(&self, point: Point) -> Point<usize> {
Point { line: self.lines.0 + self.display_offset - point.line.0 - 1, col: point.col }
}
#[inline]
pub fn display_iter(&self) -> DisplayIter<'_, T> {
DisplayIter::new(self)
Point { line: self.lines.0 + self.display_offset - point.line.0 - 1, column: point.column }
}
#[inline]
@ -459,7 +449,27 @@ impl<T> Grid<T> {
#[inline]
pub fn iter_from(&self, point: Point<usize>) -> GridIterator<'_, T> {
GridIterator { grid: self, cur: point }
GridIterator { grid: self, point }
}
/// Iterator over all visible cells.
#[inline]
pub fn display_iter(&self) -> DisplayIter<'_, T> {
let start = Point::new(self.display_offset + self.lines.0, self.cols() - 1);
let end = Point::new(self.display_offset, self.cols());
let iter = GridIterator { grid: self, point: start };
let display_offset = self.display_offset;
let lines = self.lines.0;
let take_while: DisplayIterTakeFun<'_, T> =
Box::new(move |indexed: &Indexed<&T>| indexed.point <= end);
let map: DisplayIterMapFun<'_, T> = Box::new(move |indexed: Indexed<&T>| {
let line = Line(lines + display_offset - indexed.point.line - 1);
Indexed { point: Point::new(line, indexed.point.column), cell: indexed.cell }
});
iter.take_while(take_while).map(map)
}
#[inline]
@ -470,7 +480,81 @@ impl<T> Grid<T> {
#[inline]
pub fn cursor_cell(&mut self) -> &mut T {
let point = self.cursor.point;
&mut self[&point]
&mut self[point.line][point.column]
}
}
impl<T: PartialEq> PartialEq for Grid<T> {
fn eq(&self, other: &Self) -> bool {
// Compare struct fields and check result of grid comparison.
self.raw.eq(&other.raw)
&& self.cols.eq(&other.cols)
&& self.lines.eq(&other.lines)
&& self.display_offset.eq(&other.display_offset)
}
}
impl<T> Index<Line> for Grid<T> {
type Output = Row<T>;
#[inline]
fn index(&self, index: Line) -> &Row<T> {
&self.raw[index]
}
}
impl<T> IndexMut<Line> for Grid<T> {
#[inline]
fn index_mut(&mut self, index: Line) -> &mut Row<T> {
&mut self.raw[index]
}
}
impl<T> Index<usize> for Grid<T> {
type Output = Row<T>;
#[inline]
fn index(&self, index: usize) -> &Row<T> {
&self.raw[index]
}
}
impl<T> IndexMut<usize> for Grid<T> {
#[inline]
fn index_mut(&mut self, index: usize) -> &mut Row<T> {
&mut self.raw[index]
}
}
impl<T> Index<Point<usize>> for Grid<T> {
type Output = T;
#[inline]
fn index(&self, point: Point<usize>) -> &T {
&self[point.line][point.column]
}
}
impl<T> IndexMut<Point<usize>> for Grid<T> {
#[inline]
fn index_mut(&mut self, point: Point<usize>) -> &mut T {
&mut self[point.line][point.column]
}
}
impl<T> Index<Point> for Grid<T> {
type Output = T;
#[inline]
fn index(&self, point: Point) -> &T {
&self[point.line][point.column]
}
}
impl<T> IndexMut<Point> for Grid<T> {
#[inline]
fn index_mut(&mut self, point: Point) -> &mut T {
&mut self[point.line][point.column]
}
}
@ -524,325 +608,85 @@ impl Dimensions for (Line, Column) {
}
}
#[derive(Debug, PartialEq)]
pub struct Indexed<T, L = usize> {
pub point: Point<L>,
pub cell: T,
}
impl<T, L> Deref for Indexed<T, L> {
type Target = T;
#[inline]
fn deref(&self) -> &T {
&self.cell
}
}
/// Grid cell iterator.
pub struct GridIterator<'a, T> {
/// Immutable grid reference.
grid: &'a Grid<T>,
/// Current position of the iterator within the grid.
cur: Point<usize>,
point: Point<usize>,
}
impl<'a, T> GridIterator<'a, T> {
/// Current iteratior position.
pub fn point(&self) -> Point<usize> {
self.cur
self.point
}
/// Cell at the current iteratior position.
pub fn cell(&self) -> &'a T {
&self.grid[self.cur]
&self.grid[self.point]
}
}
impl<'a, T> Iterator for GridIterator<'a, T> {
type Item = &'a T;
type Item = Indexed<&'a T>;
fn next(&mut self) -> Option<Self::Item> {
let last_col = self.grid.cols() - 1;
match self.cur {
Point { line, col } if line == 0 && col == last_col => return None,
Point { col, .. } if (col == last_col) => {
self.cur.line -= 1;
self.cur.col = Column(0);
match self.point {
Point { line, column: col } if line == 0 && col == last_col => return None,
Point { column: col, .. } if (col == last_col) => {
self.point.line -= 1;
self.point.column = Column(0);
},
_ => self.cur.col += Column(1),
_ => self.point.column += Column(1),
}
Some(&self.grid[self.cur])
Some(Indexed { cell: &self.grid[self.point], point: self.point })
}
}
/// Bidirectional iterator.
pub trait BidirectionalIterator: Iterator {
fn prev(&mut self) -> Option<Self::Item>;
}
impl<'a, T> BidirectionalIterator for GridIterator<'a, T> {
fn prev(&mut self) -> Option<Self::Item> {
let last_col = self.grid.cols() - 1;
match self.cur {
Point { line, col: Column(0) } if line == self.grid.total_lines() - 1 => return None,
Point { col: Column(0), .. } => {
self.cur.line += 1;
self.cur.col = last_col;
match self.point {
Point { line, column: Column(0) } if line == self.grid.total_lines() - 1 => {
return None
},
_ => self.cur.col -= Column(1),
Point { column: Column(0), .. } => {
self.point.line += 1;
self.point.column = last_col;
},
_ => self.point.column -= Column(1),
}
Some(&self.grid[self.cur])
Some(Indexed { cell: &self.grid[self.point], point: self.point })
}
}
/// Index active region by line.
impl<T> Index<Line> for Grid<T> {
type Output = Row<T>;
#[inline]
fn index(&self, index: Line) -> &Row<T> {
&self.raw[index]
}
}
/// Index with buffer offset.
impl<T> Index<usize> for Grid<T> {
type Output = Row<T>;
#[inline]
fn index(&self, index: usize) -> &Row<T> {
&self.raw[index]
}
}
impl<T> IndexMut<Line> for Grid<T> {
#[inline]
fn index_mut(&mut self, index: Line) -> &mut Row<T> {
&mut self.raw[index]
}
}
impl<T> IndexMut<usize> for Grid<T> {
#[inline]
fn index_mut(&mut self, index: usize) -> &mut Row<T> {
&mut self.raw[index]
}
}
impl<'point, T> Index<&'point Point> for Grid<T> {
type Output = T;
#[inline]
fn index<'a>(&'a self, point: &Point) -> &'a T {
&self[point.line][point.col]
}
}
impl<'point, T> IndexMut<&'point Point> for Grid<T> {
#[inline]
fn index_mut<'a, 'b>(&'a mut self, point: &'b Point) -> &'a mut T {
&mut self[point.line][point.col]
}
}
impl<T> Index<Point<usize>> for Grid<T> {
type Output = T;
#[inline]
fn index(&self, point: Point<usize>) -> &T {
&self[point.line][point.col]
}
}
impl<T> IndexMut<Point<usize>> for Grid<T> {
#[inline]
fn index_mut(&mut self, point: Point<usize>) -> &mut T {
&mut self[point.line][point.col]
}
}
/// A subset of lines in the grid.
///
/// May be constructed using Grid::region(..).
pub struct Region<'a, T> {
start: Line,
end: Line,
raw: &'a Storage<T>,
}
/// A mutable subset of lines in the grid.
///
/// May be constructed using Grid::region_mut(..).
pub struct RegionMut<'a, T> {
start: Line,
end: Line,
raw: &'a mut Storage<T>,
}
impl<'a, T> RegionMut<'a, T> {
/// Call the provided function for every item in this region.
pub fn each<F: Fn(&mut T)>(self, func: F) {
for row in self {
for item in row {
func(item)
}
}
}
}
pub trait IndexRegion<I, T> {
/// Get an immutable region of Self.
fn region(&self, _: I) -> Region<'_, T>;
/// Get a mutable region of Self.
fn region_mut(&mut self, _: I) -> RegionMut<'_, T>;
}
impl<T> IndexRegion<Range<Line>, T> for Grid<T> {
fn region(&self, index: Range<Line>) -> Region<'_, T> {
assert!(index.start < self.screen_lines());
assert!(index.end <= self.screen_lines());
assert!(index.start <= index.end);
Region { start: index.start, end: index.end, raw: &self.raw }
}
fn region_mut(&mut self, index: Range<Line>) -> RegionMut<'_, T> {
assert!(index.start < self.screen_lines());
assert!(index.end <= self.screen_lines());
assert!(index.start <= index.end);
RegionMut { start: index.start, end: index.end, raw: &mut self.raw }
}
}
impl<T> IndexRegion<RangeTo<Line>, T> for Grid<T> {
fn region(&self, index: RangeTo<Line>) -> Region<'_, T> {
assert!(index.end <= self.screen_lines());
Region { start: Line(0), end: index.end, raw: &self.raw }
}
fn region_mut(&mut self, index: RangeTo<Line>) -> RegionMut<'_, T> {
assert!(index.end <= self.screen_lines());
RegionMut { start: Line(0), end: index.end, raw: &mut self.raw }
}
}
impl<T> IndexRegion<RangeFrom<Line>, T> for Grid<T> {
fn region(&self, index: RangeFrom<Line>) -> Region<'_, T> {
assert!(index.start < self.screen_lines());
Region { start: index.start, end: self.screen_lines(), raw: &self.raw }
}
fn region_mut(&mut self, index: RangeFrom<Line>) -> RegionMut<'_, T> {
assert!(index.start < self.screen_lines());
RegionMut { start: index.start, end: self.screen_lines(), raw: &mut self.raw }
}
}
impl<T> IndexRegion<RangeFull, T> for Grid<T> {
fn region(&self, _: RangeFull) -> Region<'_, T> {
Region { start: Line(0), end: self.screen_lines(), raw: &self.raw }
}
fn region_mut(&mut self, _: RangeFull) -> RegionMut<'_, T> {
RegionMut { start: Line(0), end: self.screen_lines(), raw: &mut self.raw }
}
}
pub struct RegionIter<'a, T> {
end: Line,
cur: Line,
raw: &'a Storage<T>,
}
pub struct RegionIterMut<'a, T> {
end: Line,
cur: Line,
raw: &'a mut Storage<T>,
}
impl<'a, T> IntoIterator for Region<'a, T> {
type IntoIter = RegionIter<'a, T>;
type Item = &'a Row<T>;
fn into_iter(self) -> Self::IntoIter {
RegionIter { end: self.end, cur: self.start, raw: self.raw }
}
}
impl<'a, T> IntoIterator for RegionMut<'a, T> {
type IntoIter = RegionIterMut<'a, T>;
type Item = &'a mut Row<T>;
fn into_iter(self) -> Self::IntoIter {
RegionIterMut { end: self.end, cur: self.start, raw: self.raw }
}
}
impl<'a, T> Iterator for RegionIter<'a, T> {
type Item = &'a Row<T>;
fn next(&mut self) -> Option<Self::Item> {
if self.cur < self.end {
let index = self.cur;
self.cur += 1;
Some(&self.raw[index])
} else {
None
}
}
}
impl<'a, T> Iterator for RegionIterMut<'a, T> {
type Item = &'a mut Row<T>;
fn next(&mut self) -> Option<Self::Item> {
if self.cur < self.end {
let index = self.cur;
self.cur += 1;
unsafe { Some(&mut *(&mut self.raw[index] as *mut _)) }
} else {
None
}
}
}
/// Iterates over the visible area accounting for buffer transform.
pub struct DisplayIter<'a, T> {
grid: &'a Grid<T>,
offset: usize,
limit: usize,
col: Column,
line: Line,
}
impl<'a, T: 'a> DisplayIter<'a, T> {
pub fn new(grid: &'a Grid<T>) -> DisplayIter<'a, T> {
let offset = grid.display_offset + *grid.screen_lines() - 1;
let limit = grid.display_offset;
let col = Column(0);
let line = Line(0);
DisplayIter { grid, offset, col, limit, line }
}
pub fn offset(&self) -> usize {
self.offset
}
pub fn point(&self) -> Point {
Point::new(self.line, self.col)
}
}
impl<'a, T: 'a> Iterator for DisplayIter<'a, T> {
type Item = Indexed<&'a T>;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
// Return None if we've reached the end.
if self.offset == self.limit && self.grid.cols() == self.col {
return None;
}
// Get the next item.
let item = Some(Indexed {
inner: &self.grid.raw[self.offset][self.col],
line: self.line,
column: self.col,
});
// Update line/col to point to next item.
self.col += 1;
if self.col == self.grid.cols() && self.offset != self.limit {
self.offset -= 1;
self.col = Column(0);
self.line = Line(*self.grid.lines - 1 - (self.offset - self.limit));
}
item
}
}
pub type DisplayIter<'a, T> =
Map<TakeWhile<GridIterator<'a, T>, DisplayIterTakeFun<'a, T>>, DisplayIterMapFun<'a, T>>;
type DisplayIterTakeFun<'a, T> = Box<dyn Fn(&Indexed<&'a T>) -> bool>;
type DisplayIterMapFun<'a, T> = Box<dyn FnMut(Indexed<&'a T>) -> Indexed<&'a T, Line>>;

View File

@ -113,7 +113,7 @@ impl<T: GridCell + Default + PartialEq + Clone> Grid<T> {
// Remove the linewrap special case, by moving the cursor outside of the grid.
if self.cursor.input_needs_wrap && reflow {
self.cursor.input_needs_wrap = false;
self.cursor.point.col += 1;
self.cursor.point.column += 1;
}
let mut rows = self.raw.take_all();
@ -171,11 +171,11 @@ impl<T: GridCell + Default + PartialEq + Clone> Grid<T> {
let mut target = self.cursor.point.sub(cols, num_wrapped);
// Clamp to the last column, if no content was reflown with the cursor.
if target.col.0 == 0 && row.is_clear() {
if target.column.0 == 0 && row.is_clear() {
self.cursor.input_needs_wrap = true;
target = target.sub(cols, 1);
}
self.cursor.point.col = target.col;
self.cursor.point.column = target.column;
// Get required cursor line changes. Since `num_wrapped` is smaller than `cols`
// this will always be either `0` or `1`.
@ -248,7 +248,7 @@ impl<T: GridCell + Default + PartialEq + Clone> Grid<T> {
// Remove the linewrap special case, by moving the cursor outside of the grid.
if self.cursor.input_needs_wrap && reflow {
self.cursor.input_needs_wrap = false;
self.cursor.point.col += 1;
self.cursor.point.column += 1;
}
let mut new_raw = Vec::with_capacity(self.raw.len());
@ -262,7 +262,7 @@ impl<T: GridCell + Default + PartialEq + Clone> Grid<T> {
// width it is then later reflown.
let cursor_buffer_line = (self.lines - self.cursor.point.line - 1).0;
if i == cursor_buffer_line {
self.cursor.point.col += buffered.len();
self.cursor.point.column += buffered.len();
}
row.append_front(buffered);
@ -274,7 +274,7 @@ impl<T: GridCell + Default + PartialEq + Clone> Grid<T> {
Some(wrapped) if reflow => wrapped,
_ => {
let cursor_buffer_line = (self.lines - self.cursor.point.line - 1).0;
if reflow && i == cursor_buffer_line && self.cursor.point.col > cols {
if reflow && i == cursor_buffer_line && self.cursor.point.column > cols {
// If there are empty cells before the cursor, we assume it is explicit
// whitespace and need to wrap it like normal content.
Vec::new()
@ -333,17 +333,17 @@ impl<T: GridCell + Default + PartialEq + Clone> Grid<T> {
} else {
// Reflow cursor if a line below it is deleted.
let cursor_buffer_line = (self.lines - self.cursor.point.line - 1).0;
if (i == cursor_buffer_line && self.cursor.point.col < cols)
if (i == cursor_buffer_line && self.cursor.point.column < cols)
|| i < cursor_buffer_line
{
self.cursor.point.line.0 = self.cursor.point.line.saturating_sub(1);
}
// Reflow the cursor if it is on this line beyond the width.
if i == cursor_buffer_line && self.cursor.point.col >= cols {
if i == cursor_buffer_line && self.cursor.point.column >= cols {
// Since only a single new line is created, we subtract only `cols`
// from the cursor instead of reflowing it completely.
self.cursor.point.col -= cols;
self.cursor.point.column -= cols;
}
// Make sure new row is at least as long as new width.
@ -363,17 +363,17 @@ impl<T: GridCell + Default + PartialEq + Clone> Grid<T> {
// Reflow the primary cursor, or clamp it if reflow is disabled.
if !reflow {
self.cursor.point.col = min(self.cursor.point.col, cols - 1);
} else if self.cursor.point.col == cols
self.cursor.point.column = min(self.cursor.point.column, cols - 1);
} else if self.cursor.point.column == cols
&& !self[self.cursor.point.line][cols - 1].flags().contains(Flags::WRAPLINE)
{
self.cursor.input_needs_wrap = true;
self.cursor.point.col -= 1;
self.cursor.point.column -= 1;
} else {
self.cursor.point = self.cursor.point.add(cols, 0);
}
// Clamp the saved cursor to the grid.
self.saved_cursor.point.col = min(self.saved_cursor.point.col, cols - 1);
self.saved_cursor.point.column = min(self.saved_cursor.point.column, cols - 1);
}
}

View File

@ -123,6 +123,10 @@ fn scroll_down() {
// Test that GridIterator works.
#[test]
fn test_iter() {
let assert_indexed = |value: usize, indexed: Option<Indexed<&usize>>| {
assert_eq!(Some(&value), indexed.map(|indexed| indexed.cell));
};
let mut grid = Grid::<usize>::new(Line(5), Column(5), 0);
for i in 0..5 {
for j in 0..5 {
@ -130,33 +134,33 @@ fn test_iter() {
}
}
let mut iter = grid.iter_from(Point { line: 4, col: Column(0) });
let mut iter = grid.iter_from(Point::new(4, Column(0)));
assert_eq!(None, iter.prev());
assert_eq!(Some(&1), iter.next());
assert_eq!(Column(1), iter.point().col);
assert_indexed(1, iter.next());
assert_eq!(Column(1), iter.point().column);
assert_eq!(4, iter.point().line);
assert_eq!(Some(&2), iter.next());
assert_eq!(Some(&3), iter.next());
assert_eq!(Some(&4), iter.next());
assert_indexed(2, iter.next());
assert_indexed(3, iter.next());
assert_indexed(4, iter.next());
// Test line-wrapping.
assert_eq!(Some(&5), iter.next());
assert_eq!(Column(0), iter.point().col);
assert_indexed(5, iter.next());
assert_eq!(Column(0), iter.point().column);
assert_eq!(3, iter.point().line);
assert_eq!(Some(&4), iter.prev());
assert_eq!(Column(4), iter.point().col);
assert_indexed(4, iter.prev());
assert_eq!(Column(4), iter.point().column);
assert_eq!(4, iter.point().line);
// Make sure iter.cell() returns the current iterator position.
assert_eq!(&4, iter.cell());
// Test that iter ends at end of grid.
let mut final_iter = grid.iter_from(Point { line: 0, col: Column(4) });
let mut final_iter = grid.iter_from(Point { line: 0, column: Column(4) });
assert_eq!(None, final_iter.next());
assert_eq!(Some(&23), final_iter.prev());
assert_indexed(23, final_iter.prev());
}
#[test]

View File

@ -8,7 +8,6 @@ use std::ops::{self, Add, AddAssign, Deref, Range, Sub, SubAssign};
use serde::{Deserialize, Serialize};
use crate::grid::Dimensions;
use crate::term::render::RenderableCell;
/// The side of a cell.
pub type Side = Direction;
@ -48,12 +47,12 @@ pub enum Boundary {
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default, Eq, PartialEq)]
pub struct Point<L = Line> {
pub line: L,
pub col: Column,
pub column: Column,
}
impl<L> Point<L> {
pub fn new(line: L, col: Column) -> Point<L> {
Point { line, col }
Point { line, column: col }
}
#[inline]
@ -63,10 +62,10 @@ impl<L> Point<L> {
L: Copy + Default + Into<Line> + Add<usize, Output = L> + Sub<usize, Output = L>,
{
let num_cols = num_cols.0;
let line_changes = (rhs + num_cols - 1).saturating_sub(self.col.0) / num_cols;
let line_changes = (rhs + num_cols - 1).saturating_sub(self.column.0) / num_cols;
if self.line.into() >= Line(line_changes) {
self.line = self.line - line_changes;
self.col = Column((num_cols + self.col.0 - rhs % num_cols) % num_cols);
self.column = Column((num_cols + self.column.0 - rhs % num_cols) % num_cols);
self
} else {
Point::new(L::default(), Column(0))
@ -80,8 +79,8 @@ impl<L> Point<L> {
L: Copy + Default + Into<Line> + Add<usize, Output = L> + Sub<usize, Output = L>,
{
let num_cols = num_cols.0;
self.line = self.line + (rhs + self.col.0) / num_cols;
self.col = Column((self.col.0 + rhs) % num_cols);
self.line = self.line + (rhs + self.column.0) / num_cols;
self.column = Column((self.column.0 + rhs) % num_cols);
self
}
}
@ -96,13 +95,13 @@ impl Point<usize> {
let total_lines = dimensions.total_lines();
let num_cols = dimensions.cols().0;
self.line += (rhs + num_cols - 1).saturating_sub(self.col.0) / num_cols;
self.col = Column((num_cols + self.col.0 - rhs % num_cols) % num_cols);
self.line += (rhs + num_cols - 1).saturating_sub(self.column.0) / num_cols;
self.column = Column((num_cols + self.column.0 - rhs % num_cols) % num_cols);
if self.line >= total_lines {
match boundary {
Boundary::Clamp => Point::new(total_lines - 1, Column(0)),
Boundary::Wrap => Point::new(self.line - total_lines, self.col),
Boundary::Wrap => Point::new(self.line - total_lines, self.column),
}
} else {
self
@ -117,17 +116,17 @@ impl Point<usize> {
{
let num_cols = dimensions.cols();
let line_delta = (rhs + self.col.0) / num_cols.0;
let line_delta = (rhs + self.column.0) / num_cols.0;
if self.line >= line_delta {
self.line -= line_delta;
self.col = Column((self.col.0 + rhs) % num_cols.0);
self.column = Column((self.column.0 + rhs) % num_cols.0);
self
} else {
match boundary {
Boundary::Clamp => Point::new(0, num_cols - 1),
Boundary::Wrap => {
let col = Column((self.col.0 + rhs) % num_cols.0);
let col = Column((self.column.0 + rhs) % num_cols.0);
let line = dimensions.total_lines() + self.line - line_delta;
Point::new(line, col)
},
@ -144,7 +143,7 @@ impl PartialOrd for Point {
impl Ord for Point {
fn cmp(&self, other: &Point) -> Ordering {
match (self.line.cmp(&other.line), self.col.cmp(&other.col)) {
match (self.line.cmp(&other.line), self.column.cmp(&other.column)) {
(Ordering::Equal, ord) | (ord, _) => ord,
}
}
@ -158,7 +157,7 @@ impl PartialOrd for Point<usize> {
impl Ord for Point<usize> {
fn cmp(&self, other: &Point<usize>) -> Ordering {
match (self.line.cmp(&other.line), self.col.cmp(&other.col)) {
match (self.line.cmp(&other.line), self.column.cmp(&other.column)) {
(Ordering::Equal, ord) => ord,
(Ordering::Less, _) => Ordering::Greater,
(Ordering::Greater, _) => Ordering::Less,
@ -168,31 +167,25 @@ impl Ord for Point<usize> {
impl From<Point<usize>> for Point<isize> {
fn from(point: Point<usize>) -> Self {
Point::new(point.line as isize, point.col)
Point::new(point.line as isize, point.column)
}
}
impl From<Point<usize>> for Point<Line> {
fn from(point: Point<usize>) -> Self {
Point::new(Line(point.line), point.col)
Point::new(Line(point.line), point.column)
}
}
impl From<Point<isize>> for Point<usize> {
fn from(point: Point<isize>) -> Self {
Point::new(point.line as usize, point.col)
Point::new(point.line as usize, point.column)
}
}
impl From<Point> for Point<usize> {
fn from(point: Point) -> Self {
Point::new(point.line.0, point.col)
}
}
impl From<&RenderableCell> for Point<Line> {
fn from(cell: &RenderableCell) -> Self {
Point::new(cell.line, cell.column)
Point::new(point.line.0, point.column)
}
}
@ -485,7 +478,7 @@ mod tests {
let result = point.sub(num_cols, 1);
assert_eq!(result, Point::new(0, point.col - 1));
assert_eq!(result, Point::new(0, point.column - 1));
}
#[test]
@ -515,7 +508,7 @@ mod tests {
let result = point.add(num_cols, 1);
assert_eq!(result, Point::new(0, point.col + 1));
assert_eq!(result, Point::new(0, point.column + 1));
}
#[test]
@ -534,7 +527,7 @@ mod tests {
let result = point.add_absolute(&(Line(1), Column(42)), Boundary::Clamp, 1);
assert_eq!(result, Point::new(0, point.col + 1));
assert_eq!(result, Point::new(0, point.column + 1));
}
#[test]
@ -588,7 +581,7 @@ mod tests {
let result = point.sub_absolute(&(Line(1), Column(42)), Boundary::Clamp, 1);
assert_eq!(result, Point::new(0, point.col - 1));
assert_eq!(result, Point::new(0, point.column - 1));
}
#[test]

View File

@ -10,10 +10,10 @@ use std::mem;
use std::ops::{Bound, Range, RangeBounds};
use crate::ansi::CursorShape;
use crate::grid::{Dimensions, Grid, GridCell};
use crate::grid::{Dimensions, GridCell, Indexed};
use crate::index::{Column, Line, Point, Side};
use crate::term::cell::Flags;
use crate::term::Term;
use crate::term::cell::{Cell, Flags};
use crate::term::{RenderableCursor, Term};
/// A Point and side within that point.
#[derive(Debug, Copy, Clone, PartialEq)]
@ -43,68 +43,42 @@ impl<L> SelectionRange<L> {
pub fn new(start: Point<L>, end: Point<L>, is_block: bool) -> Self {
Self { start, end, is_block }
}
/// Check if a point lies within the selection.
pub fn contains(&self, point: Point<L>) -> bool
where
L: PartialEq + PartialOrd,
{
self.start.line <= point.line
&& self.end.line >= point.line
&& (self.start.col <= point.col || (self.start.line != point.line && !self.is_block))
&& (self.end.col >= point.col || (self.end.line != point.line && !self.is_block))
}
}
impl SelectionRange<Line> {
/// Check if a point lies within the selection.
pub fn contains(&self, point: Point) -> bool {
self.start.line <= point.line
&& self.end.line >= point.line
&& (self.start.column <= point.column
|| (self.start.line != point.line && !self.is_block))
&& (self.end.column >= point.column || (self.end.line != point.line && !self.is_block))
}
/// Check if the cell at a point is part of the selection.
pub fn contains_cell<T>(
&self,
grid: &Grid<T>,
point: Point,
cursor_point: Point,
cursor_shape: CursorShape,
) -> bool
where
T: GridCell,
{
pub fn contains_cell(&self, indexed: &Indexed<&Cell, Line>, cursor: RenderableCursor) -> bool {
// Do not invert block cursor at selection boundaries.
if cursor_shape == CursorShape::Block
&& cursor_point == point
&& (self.start == point
|| self.end == point
if cursor.shape == CursorShape::Block
&& cursor.point == indexed.point
&& (self.start == indexed.point
|| self.end == indexed.point
|| (self.is_block
&& ((self.start.line == point.line && self.end.col == point.col)
|| (self.end.line == point.line && self.start.col == point.col))))
&& ((self.start.line == indexed.point.line
&& self.end.column == indexed.point.column)
|| (self.end.line == indexed.point.line
&& self.start.column == indexed.point.column))))
{
return false;
}
// Point itself is selected.
if self.contains(point) {
if self.contains(indexed.point) {
return true;
}
let num_cols = grid.cols();
// Convert to absolute coordinates to adjust for the display offset.
let buffer_point = grid.visible_to_buffer(point);
let cell = &grid[buffer_point];
// Check if wide char's spacers are selected.
if cell.flags().contains(Flags::WIDE_CHAR) {
let prev = point.sub(num_cols, 1);
let buffer_prev = grid.visible_to_buffer(prev);
let next = point.add(num_cols, 1);
// Check trailing spacer.
self.contains(next)
// Check line-wrapping, leading spacer.
|| (grid[buffer_prev].flags().contains(Flags::LEADING_WIDE_CHAR_SPACER)
&& self.contains(prev))
} else {
false
}
// Check if a wide char's trailing spacer is selected.
indexed.cell.flags().contains(Flags::WIDE_CHAR)
&& self.contains(Point::new(indexed.point.line, indexed.point.column + 1))
}
}
@ -184,7 +158,7 @@ impl Selection {
// Clamp selection to start of region.
if start.point.line >= range_top && range_top != num_lines {
if self.ty != SelectionType::Block {
start.point.col = Column(0);
start.point.column = Column(0);
start.side = Side::Left;
}
start.point.line = range_top - 1;
@ -204,7 +178,7 @@ impl Selection {
// Clamp selection to end of region.
if end.point.line < range_bottom {
if self.ty != SelectionType::Block {
end.point.col = Column(num_cols - 1);
end.point.column = Column(num_cols - 1);
end.side = Side::Right;
}
end.point.line = range_bottom;
@ -228,7 +202,7 @@ impl Selection {
|| (start.side == Side::Right
&& end.side == Side::Left
&& (start.point.line == end.point.line)
&& start.point.col + 1 == end.point.col)
&& start.point.column + 1 == end.point.column)
},
SelectionType::Block => {
let (start, end) = (self.region.start, self.region.end);
@ -236,11 +210,11 @@ impl Selection {
// Block selection is empty when the points' columns and sides are identical
// or two cells with adjacent columns have the sides right -> left,
// regardless of their lines
(start.point.col == end.point.col && start.side == end.side)
|| (start.point.col + 1 == end.point.col
(start.point.column == end.point.column && start.side == end.side)
|| (start.point.column + 1 == end.point.column
&& start.side == Side::Right
&& end.side == Side::Left)
|| (end.point.col + 1 == start.point.col
|| (end.point.column + 1 == start.point.column
&& start.side == Side::Left
&& end.side == Side::Right)
},
@ -277,7 +251,8 @@ impl Selection {
let (start, end) = (self.region.start.point, self.region.end.point);
let (start_side, end_side) = match self.ty {
SelectionType::Block
if start.col > end.col || (start.col == end.col && start.line < end.line) =>
if start.column > end.column
|| (start.column == end.column && start.line < end.line) =>
{
(Side::Right, Side::Left)
},
@ -317,7 +292,7 @@ impl Selection {
/// Bring start and end points in the correct order.
fn points_need_swap(start: Point<usize>, end: Point<usize>) -> bool {
start.line < end.line || start.line == end.line && start.col > end.col
start.line < end.line || start.line == end.line && start.column > end.column
}
/// Clamp selection inside grid to prevent OOB.
@ -337,7 +312,7 @@ impl Selection {
// Clamp to grid if it is still partially visible.
if !is_block {
start.side = Side::Left;
start.point.col = Column(0);
start.point.column = Column(0);
}
start.point.line = lines - 1;
}
@ -352,7 +327,7 @@ impl Selection {
) -> SelectionRange {
if start == end {
if let Some(matching) = term.bracket_search(start) {
if (matching.line == start.line && matching.col < start.col)
if (matching.line == start.line && matching.column < start.column)
|| (matching.line > start.line)
{
start = matching;
@ -394,20 +369,20 @@ impl Selection {
// Remove last cell if selection ends to the left of a cell.
if end.side == Side::Left && start.point != end.point {
// Special case when selection ends to left of first cell.
if end.point.col == Column(0) {
end.point.col = num_cols - 1;
if end.point.column == Column(0) {
end.point.column = num_cols - 1;
end.point.line += 1;
} else {
end.point.col -= 1;
end.point.column -= 1;
}
}
// Remove first cell if selection starts at the right of a cell.
if start.side == Side::Right && start.point != end.point {
start.point.col += 1;
start.point.column += 1;
// Wrap to next line when selection starts to the right of last column.
if start.point.col == num_cols {
if start.point.column == num_cols {
start.point = Point::new(start.point.line.saturating_sub(1), Column(0));
}
}
@ -421,19 +396,19 @@ impl Selection {
}
// Always go top-left -> bottom-right.
if start.point.col > end.point.col {
if start.point.column > end.point.column {
mem::swap(&mut start.side, &mut end.side);
mem::swap(&mut start.point.col, &mut end.point.col);
mem::swap(&mut start.point.column, &mut end.point.column);
}
// Remove last cell if selection ends to the left of a cell.
if end.side == Side::Left && start.point != end.point && end.point.col.0 > 0 {
end.point.col -= 1;
if end.side == Side::Left && start.point != end.point && end.point.column.0 > 0 {
end.point.column -= 1;
}
// Remove first cell if selection starts at the right of a cell.
if start.side == Side::Right && start.point != end.point {
start.point.col += 1;
start.point.column += 1;
}
Some(SelectionRange { start: start.point, end: end.point, is_block: true })
@ -454,18 +429,12 @@ mod tests {
use super::*;
use crate::config::MockConfig;
use crate::event::{Event, EventListener};
use crate::index::{Column, Line, Point, Side};
use crate::term::{SizeInfo, Term};
struct Mock;
impl EventListener for Mock {
fn send_event(&self, _event: Event) {}
}
fn term(height: usize, width: usize) -> Term<Mock> {
fn term(height: usize, width: usize) -> Term<()> {
let size = SizeInfo::new(width as f32, height as f32, 1.0, 1.0, 0.0, 0.0, false);
Term::new(&MockConfig::default(), size, Mock)
Term::new(&MockConfig::default(), size, ())
}
/// Test case of single cell selection.
@ -475,7 +444,7 @@ mod tests {
/// 3. [BE]
#[test]
fn single_cell_left_to_right() {
let location = Point { line: 0, col: Column(0) };
let location = Point { line: 0, column: Column(0) };
let mut selection = Selection::new(SelectionType::Simple, location, Side::Left);
selection.update(location, Side::Right);
@ -493,7 +462,7 @@ mod tests {
/// 3. [EB]
#[test]
fn single_cell_right_to_left() {
let location = Point { line: 0, col: Column(0) };
let location = Point { line: 0, column: Column(0) };
let mut selection = Selection::new(SelectionType::Simple, location, Side::Right);
selection.update(location, Side::Left);

View File

@ -7,14 +7,11 @@ use serde::de::{Error as _, Visitor};
use serde::{Deserialize, Deserializer, Serialize};
use serde_yaml::Value;
use crate::ansi;
use crate::config::Colors;
use crate::ansi::NamedColor;
/// Number of terminal colors.
pub const COUNT: usize = 269;
/// Factor for automatic computation of dim colors used by terminal.
pub const DIM_FACTOR: f32 = 0.66;
#[derive(Debug, Eq, PartialEq, Copy, Clone, Default, Serialize)]
pub struct Rgb {
pub r: u8,
@ -42,8 +39,9 @@ impl Rgb {
0.2126 * r_luminance + 0.7152 * g_luminance + 0.0722 * b_luminance
}
/// Implementation of W3C's contrast algorithm:
/// https://www.w3.org/TR/WCAG20/#contrast-ratiodef
/// Implementation of [W3C's contrast algorithm].
///
/// [W3C's contrast algorithm]: https://www.w3.org/TR/WCAG20/#contrast-ratiodef
pub fn contrast(self, other: Rgb) -> f64 {
let self_luminance = self.luminance();
let other_luminance = other.luminance();
@ -231,188 +229,57 @@ impl<'de> Deserialize<'de> for CellRgb {
}
}
/// List of indexed colors.
/// Array of indexed colors.
///
/// The first 16 entries are the standard ansi named colors. Items 16..232 are
/// the color cube. Items 233..256 are the grayscale ramp. Item 256 is
/// the configured foreground color, item 257 is the configured background
/// color, item 258 is the cursor color. Following that are 8 positions for dim colors.
/// Item 267 is the bright foreground color, 268 the dim foreground.
/// | Indices | Description |
/// | -------- | ----------------- |
/// | 0..16 | Named ANSI colors |
/// | 16..232 | Color cube |
/// | 233..256 | Grayscale ramp |
/// | 256 | Foreground |
/// | 257 | Background |
/// | 258 | Cursor |
/// | 259..267 | Dim colors |
/// | 267 | Bright foreground |
/// | 268 | Dim background |
#[derive(Copy, Clone)]
pub struct List([Rgb; COUNT]);
pub struct Colors([Option<Rgb>; COUNT]);
impl<'a> From<&'a Colors> for List {
fn from(colors: &Colors) -> List {
// Type inference fails without this annotation.
let mut list = List([Rgb::default(); COUNT]);
list.fill_named(colors);
list.fill_cube(colors);
list.fill_gray_ramp(colors);
list
impl Default for Colors {
fn default() -> Self {
Self([None; COUNT])
}
}
impl List {
pub fn fill_named(&mut self, colors: &Colors) {
// Normals.
self[ansi::NamedColor::Black] = colors.normal.black;
self[ansi::NamedColor::Red] = colors.normal.red;
self[ansi::NamedColor::Green] = colors.normal.green;
self[ansi::NamedColor::Yellow] = colors.normal.yellow;
self[ansi::NamedColor::Blue] = colors.normal.blue;
self[ansi::NamedColor::Magenta] = colors.normal.magenta;
self[ansi::NamedColor::Cyan] = colors.normal.cyan;
self[ansi::NamedColor::White] = colors.normal.white;
// Brights.
self[ansi::NamedColor::BrightBlack] = colors.bright.black;
self[ansi::NamedColor::BrightRed] = colors.bright.red;
self[ansi::NamedColor::BrightGreen] = colors.bright.green;
self[ansi::NamedColor::BrightYellow] = colors.bright.yellow;
self[ansi::NamedColor::BrightBlue] = colors.bright.blue;
self[ansi::NamedColor::BrightMagenta] = colors.bright.magenta;
self[ansi::NamedColor::BrightCyan] = colors.bright.cyan;
self[ansi::NamedColor::BrightWhite] = colors.bright.white;
self[ansi::NamedColor::BrightForeground] =
colors.primary.bright_foreground.unwrap_or(colors.primary.foreground);
// Foreground and background.
self[ansi::NamedColor::Foreground] = colors.primary.foreground;
self[ansi::NamedColor::Background] = colors.primary.background;
// Dims.
self[ansi::NamedColor::DimForeground] =
colors.primary.dim_foreground.unwrap_or(colors.primary.foreground * DIM_FACTOR);
match colors.dim {
Some(ref dim) => {
trace!("Using config-provided dim colors");
self[ansi::NamedColor::DimBlack] = dim.black;
self[ansi::NamedColor::DimRed] = dim.red;
self[ansi::NamedColor::DimGreen] = dim.green;
self[ansi::NamedColor::DimYellow] = dim.yellow;
self[ansi::NamedColor::DimBlue] = dim.blue;
self[ansi::NamedColor::DimMagenta] = dim.magenta;
self[ansi::NamedColor::DimCyan] = dim.cyan;
self[ansi::NamedColor::DimWhite] = dim.white;
},
None => {
trace!("Deriving dim colors from normal colors");
self[ansi::NamedColor::DimBlack] = colors.normal.black * DIM_FACTOR;
self[ansi::NamedColor::DimRed] = colors.normal.red * DIM_FACTOR;
self[ansi::NamedColor::DimGreen] = colors.normal.green * DIM_FACTOR;
self[ansi::NamedColor::DimYellow] = colors.normal.yellow * DIM_FACTOR;
self[ansi::NamedColor::DimBlue] = colors.normal.blue * DIM_FACTOR;
self[ansi::NamedColor::DimMagenta] = colors.normal.magenta * DIM_FACTOR;
self[ansi::NamedColor::DimCyan] = colors.normal.cyan * DIM_FACTOR;
self[ansi::NamedColor::DimWhite] = colors.normal.white * DIM_FACTOR;
},
}
}
pub fn fill_cube(&mut self, colors: &Colors) {
let mut index: usize = 16;
// Build colors.
for r in 0..6 {
for g in 0..6 {
for b in 0..6 {
// Override colors 16..232 with the config (if present).
if let Some(indexed_color) =
colors.indexed_colors.iter().find(|ic| ic.index() == index as u8)
{
self[index] = indexed_color.color;
} else {
self[index] = Rgb {
r: if r == 0 { 0 } else { r * 40 + 55 },
b: if b == 0 { 0 } else { b * 40 + 55 },
g: if g == 0 { 0 } else { g * 40 + 55 },
};
}
index += 1;
}
}
}
debug_assert!(index == 232);
}
pub fn fill_gray_ramp(&mut self, colors: &Colors) {
let mut index: usize = 232;
for i in 0..24 {
// Index of the color is number of named colors + number of cube colors + i.
let color_index = 16 + 216 + i;
// Override colors 232..256 with the config (if present).
if let Some(indexed_color) =
colors.indexed_colors.iter().find(|ic| ic.index() == color_index)
{
self[index] = indexed_color.color;
index += 1;
continue;
}
let value = i * 10 + 8;
self[index] = Rgb { r: value, g: value, b: value };
index += 1;
}
debug_assert!(index == 256);
}
}
impl fmt::Debug for List {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("List[..]")
}
}
impl Index<ansi::NamedColor> for List {
type Output = Rgb;
impl Index<usize> for Colors {
type Output = Option<Rgb>;
#[inline]
fn index(&self, idx: ansi::NamedColor) -> &Self::Output {
&self.0[idx as usize]
fn index(&self, index: usize) -> &Self::Output {
&self.0[index]
}
}
impl IndexMut<ansi::NamedColor> for List {
impl IndexMut<usize> for Colors {
#[inline]
fn index_mut(&mut self, idx: ansi::NamedColor) -> &mut Self::Output {
&mut self.0[idx as usize]
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
&mut self.0[index]
}
}
impl Index<usize> for List {
type Output = Rgb;
impl Index<NamedColor> for Colors {
type Output = Option<Rgb>;
#[inline]
fn index(&self, idx: usize) -> &Self::Output {
&self.0[idx]
fn index(&self, index: NamedColor) -> &Self::Output {
&self.0[index as usize]
}
}
impl IndexMut<usize> for List {
impl IndexMut<NamedColor> for Colors {
#[inline]
fn index_mut(&mut self, idx: usize) -> &mut Self::Output {
&mut self.0[idx]
}
}
impl Index<u8> for List {
type Output = Rgb;
#[inline]
fn index(&self, idx: u8) -> &Self::Output {
&self.0[idx as usize]
}
}
impl IndexMut<u8> for List {
#[inline]
fn index_mut(&mut self, idx: u8) -> &mut Self::Output {
&mut self.0[idx as usize]
fn index_mut(&mut self, index: NamedColor) -> &mut Self::Output {
&mut self.0[index as usize]
}
}

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@ use std::ops::RangeInclusive;
use regex_automata::{dense, DenseDFA, Error as RegexError, DFA};
use crate::grid::{BidirectionalIterator, Dimensions, GridIterator};
use crate::grid::{BidirectionalIterator, Dimensions, GridIterator, Indexed};
use crate::index::{Boundary, Column, Direction, Point, Side};
use crate::term::cell::{Cell, Flags};
use crate::term::Term;
@ -48,23 +48,10 @@ impl RegexSearch {
}
impl<T> Term<T> {
/// Enter terminal buffer search mode.
#[inline]
pub fn start_search(&mut self, search: &str) {
self.regex_search = RegexSearch::new(search).ok();
self.dirty = true;
}
/// Cancel active terminal buffer search.
#[inline]
pub fn cancel_search(&mut self) {
self.regex_search = None;
self.dirty = true;
}
/// Get next search match in the specified direction.
pub fn search_next(
&self,
dfas: &RegexSearch,
mut origin: Point<usize>,
direction: Direction,
side: Side,
@ -75,14 +62,15 @@ impl<T> Term<T> {
max_lines = max_lines.filter(|max_lines| max_lines + 1 < self.total_lines());
match direction {
Direction::Right => self.next_match_right(origin, side, max_lines),
Direction::Left => self.next_match_left(origin, side, max_lines),
Direction::Right => self.next_match_right(dfas, origin, side, max_lines),
Direction::Left => self.next_match_left(dfas, origin, side, max_lines),
}
}
/// Find the next match to the right of the origin.
fn next_match_right(
&self,
dfas: &RegexSearch,
origin: Point<usize>,
side: Side,
max_lines: Option<usize>,
@ -100,7 +88,7 @@ impl<T> Term<T> {
_ => end.sub_absolute(self, Boundary::Wrap, 1),
};
let mut regex_iter = RegexIter::new(start, end, Direction::Right, &self).peekable();
let mut regex_iter = RegexIter::new(start, end, Direction::Right, &self, dfas).peekable();
// Check if there's any match at all.
let first_match = regex_iter.peek()?.clone();
@ -112,7 +100,7 @@ impl<T> Term<T> {
// If the match's point is beyond the origin, we're done.
match_point.line > start.line
|| match_point.line < origin.line
|| (match_point.line == origin.line && match_point.col >= origin.col)
|| (match_point.line == origin.line && match_point.column >= origin.column)
})
.unwrap_or(first_match);
@ -122,6 +110,7 @@ impl<T> Term<T> {
/// Find the next match to the left of the origin.
fn next_match_left(
&self,
dfas: &RegexSearch,
origin: Point<usize>,
side: Side,
max_lines: Option<usize>,
@ -135,7 +124,7 @@ impl<T> Term<T> {
_ => end.add_absolute(self, Boundary::Wrap, 1),
};
let mut regex_iter = RegexIter::new(start, end, Direction::Left, &self).peekable();
let mut regex_iter = RegexIter::new(start, end, Direction::Left, &self, dfas).peekable();
// Check if there's any match at all.
let first_match = regex_iter.peek()?.clone();
@ -147,7 +136,7 @@ impl<T> Term<T> {
// If the match's point is beyond the origin, we're done.
match_point.line < start.line
|| match_point.line > origin.line
|| (match_point.line == origin.line && match_point.col <= origin.col)
|| (match_point.line == origin.line && match_point.column <= origin.column)
})
.unwrap_or(first_match);
@ -165,12 +154,15 @@ impl<T> Term<T> {
/// Find the next regex match to the left of the origin point.
///
/// The origin is always included in the regex.
pub fn regex_search_left(&self, start: Point<usize>, end: Point<usize>) -> Option<Match> {
let RegexSearch { left_fdfa: fdfa, left_rdfa: rdfa, .. } = self.regex_search.as_ref()?;
pub fn regex_search_left(
&self,
dfas: &RegexSearch,
start: Point<usize>,
end: Point<usize>,
) -> Option<Match> {
// Find start and end of match.
let match_start = self.regex_search(start, end, Direction::Left, &fdfa)?;
let match_end = self.regex_search(match_start, start, Direction::Right, &rdfa)?;
let match_start = self.regex_search(start, end, Direction::Left, &dfas.left_fdfa)?;
let match_end = self.regex_search(match_start, start, Direction::Right, &dfas.left_rdfa)?;
Some(match_start..=match_end)
}
@ -178,12 +170,15 @@ impl<T> Term<T> {
/// Find the next regex match to the right of the origin point.
///
/// The origin is always included in the regex.
pub fn regex_search_right(&self, start: Point<usize>, end: Point<usize>) -> Option<Match> {
let RegexSearch { right_fdfa: fdfa, right_rdfa: rdfa, .. } = self.regex_search.as_ref()?;
pub fn regex_search_right(
&self,
dfas: &RegexSearch,
start: Point<usize>,
end: Point<usize>,
) -> Option<Match> {
// Find start and end of match.
let match_end = self.regex_search(start, end, Direction::Right, &fdfa)?;
let match_start = self.regex_search(match_end, start, Direction::Left, &rdfa)?;
let match_end = self.regex_search(start, end, Direction::Right, &dfas.right_fdfa)?;
let match_start = self.regex_search(match_end, start, Direction::Left, &dfas.right_rdfa)?;
Some(match_start..=match_end)
}
@ -251,10 +246,10 @@ impl<T> Term<T> {
// Advance grid cell iterator.
let mut cell = match next(&mut iter) {
Some(cell) => cell,
Some(Indexed { cell, .. }) => cell,
None => {
// Wrap around to other end of the scrollback buffer.
let start = Point::new(last_line - point.line, last_col - point.col);
let start = Point::new(last_line - point.line, last_col - point.column);
iter = self.grid.iter_from(start);
iter.cell()
},
@ -266,8 +261,8 @@ impl<T> Term<T> {
let last_point = mem::replace(&mut point, iter.point());
// Handle linebreaks.
if (last_point.col == last_col && point.col == Column(0) && !last_wrapped)
|| (last_point.col == Column(0) && point.col == last_col && !wrapped)
if (last_point.column == last_col && point.column == Column(0) && !last_wrapped)
|| (last_point.column == Column(0) && point.column == last_col && !wrapped)
{
match regex_match {
Some(_) => break,
@ -293,13 +288,13 @@ impl<T> Term<T> {
iter.next();
},
Direction::Right if cell.flags.contains(Flags::LEADING_WIDE_CHAR_SPACER) => {
if let Some(new_cell) = iter.next() {
if let Some(Indexed { cell: new_cell, .. }) = iter.next() {
*cell = new_cell;
}
iter.next();
},
Direction::Left if cell.flags.contains(Flags::WIDE_CHAR_SPACER) => {
if let Some(new_cell) = iter.prev() {
if let Some(Indexed { cell: new_cell, .. }) = iter.prev() {
*cell = new_cell;
}
@ -314,7 +309,7 @@ impl<T> Term<T> {
/// Find next matching bracket.
pub fn bracket_search(&self, point: Point<usize>) -> Option<Point<usize>> {
let start_char = self.grid[point.line][point.col].c;
let start_char = self.grid[point.line][point.column].c;
// Find the matching bracket we're looking for
let (forward, end_char) = BRACKET_PAIRS.iter().find_map(|(open, close)| {
@ -338,17 +333,17 @@ impl<T> Term<T> {
let cell = if forward { iter.next() } else { iter.prev() };
// Break if there are no more cells
let c = match cell {
Some(cell) => cell.c,
let cell = match cell {
Some(cell) => cell,
None => break,
};
// Check if the bracket matches
if c == end_char && skip_pairs == 0 {
return Some(iter.point());
} else if c == start_char {
if cell.c == end_char && skip_pairs == 0 {
return Some(cell.point);
} else if cell.c == start_char {
skip_pairs += 1;
} else if c == end_char {
} else if cell.c == end_char {
skip_pairs -= 1;
}
}
@ -370,11 +365,11 @@ impl<T> Term<T> {
break;
}
if iter.point().col == last_col && !cell.flags.contains(Flags::WRAPLINE) {
if cell.point.column == last_col && !cell.flags.contains(Flags::WRAPLINE) {
break; // cut off if on new line or hit escape char
}
point = iter.point();
point = cell.point;
}
point
@ -385,18 +380,17 @@ impl<T> Term<T> {
// Limit the starting point to the last line in the history
point.line = min(point.line, self.total_lines() - 1);
let mut iter = self.grid.iter_from(point);
let wide = Flags::WIDE_CHAR | Flags::WIDE_CHAR_SPACER | Flags::LEADING_WIDE_CHAR_SPACER;
let last_col = self.cols() - 1;
let wide = Flags::WIDE_CHAR | Flags::WIDE_CHAR_SPACER | Flags::LEADING_WIDE_CHAR_SPACER;
while let Some(cell) = iter.next() {
for cell in self.grid.iter_from(point) {
if !cell.flags.intersects(wide) && self.semantic_escape_chars.contains(cell.c) {
break;
}
point = iter.point();
point = cell.point;
if point.col == last_col && !cell.flags.contains(Flags::WRAPLINE) {
if point.column == last_col && !cell.flags.contains(Flags::WRAPLINE) {
break; // cut off if on new line or hit escape char
}
}
@ -412,7 +406,7 @@ impl<T> Term<T> {
point.line += 1;
}
point.col = Column(0);
point.column = Column(0);
point
}
@ -425,7 +419,7 @@ impl<T> Term<T> {
point.line -= 1;
}
point.col = self.cols() - 1;
point.column = self.cols() - 1;
point
}
@ -436,6 +430,7 @@ pub struct RegexIter<'a, T> {
point: Point<usize>,
end: Point<usize>,
direction: Direction,
dfas: &'a RegexSearch,
term: &'a Term<T>,
done: bool,
}
@ -446,8 +441,9 @@ impl<'a, T> RegexIter<'a, T> {
end: Point<usize>,
direction: Direction,
term: &'a Term<T>,
dfas: &'a RegexSearch,
) -> Self {
Self { point: start, done: false, end, direction, term }
Self { point: start, done: false, end, direction, term, dfas }
}
/// Skip one cell, advancing the origin point to the next one.
@ -463,8 +459,8 @@ impl<'a, T> RegexIter<'a, T> {
/// Get the next match in the specified direction.
fn next_match(&self) -> Option<Match> {
match self.direction {
Direction::Right => self.term.regex_search_right(self.point, self.end),
Direction::Left => self.term.regex_search_left(self.point, self.end),
Direction::Right => self.term.regex_search_right(self.dfas, self.point, self.end),
Direction::Left => self.term.regex_search_left(self.dfas, self.point, self.end),
}
}
}
@ -498,7 +494,7 @@ mod tests {
#[test]
fn regex_right() {
#[rustfmt::skip]
let mut term = mock_term("\
let term = mock_term("\
testing66\r\n\
Alacritty\n\
123\r\n\
@ -507,18 +503,18 @@ mod tests {
");
// Check regex across wrapped and unwrapped lines.
term.regex_search = Some(RegexSearch::new("Ala.*123").unwrap());
let dfas = RegexSearch::new("Ala.*123").unwrap();
let start = Point::new(3, Column(0));
let end = Point::new(0, Column(2));
let match_start = Point::new(3, Column(0));
let match_end = Point::new(2, Column(2));
assert_eq!(term.regex_search_right(start, end), Some(match_start..=match_end));
assert_eq!(term.regex_search_right(&dfas, start, end), Some(match_start..=match_end));
}
#[test]
fn regex_left() {
#[rustfmt::skip]
let mut term = mock_term("\
let term = mock_term("\
testing66\r\n\
Alacritty\n\
123\r\n\
@ -527,209 +523,209 @@ mod tests {
");
// Check regex across wrapped and unwrapped lines.
term.regex_search = Some(RegexSearch::new("Ala.*123").unwrap());
let dfas = RegexSearch::new("Ala.*123").unwrap();
let start = Point::new(0, Column(2));
let end = Point::new(3, Column(0));
let match_start = Point::new(3, Column(0));
let match_end = Point::new(2, Column(2));
assert_eq!(term.regex_search_left(start, end), Some(match_start..=match_end));
assert_eq!(term.regex_search_left(&dfas, start, end), Some(match_start..=match_end));
}
#[test]
fn nested_regex() {
#[rustfmt::skip]
let mut term = mock_term("\
let term = mock_term("\
Ala -> Alacritty -> critty\r\n\
critty\
");
// Greedy stopped at linebreak.
term.regex_search = Some(RegexSearch::new("Ala.*critty").unwrap());
let dfas = RegexSearch::new("Ala.*critty").unwrap();
let start = Point::new(1, Column(0));
let end = Point::new(1, Column(25));
assert_eq!(term.regex_search_right(start, end), Some(start..=end));
assert_eq!(term.regex_search_right(&dfas, start, end), Some(start..=end));
// Greedy stopped at dead state.
term.regex_search = Some(RegexSearch::new("Ala[^y]*critty").unwrap());
let dfas = RegexSearch::new("Ala[^y]*critty").unwrap();
let start = Point::new(1, Column(0));
let end = Point::new(1, Column(15));
assert_eq!(term.regex_search_right(start, end), Some(start..=end));
assert_eq!(term.regex_search_right(&dfas, start, end), Some(start..=end));
}
#[test]
fn no_match_right() {
#[rustfmt::skip]
let mut term = mock_term("\
let term = mock_term("\
first line\n\
broken second\r\n\
third\
");
term.regex_search = Some(RegexSearch::new("nothing").unwrap());
let dfas = RegexSearch::new("nothing").unwrap();
let start = Point::new(2, Column(0));
let end = Point::new(0, Column(4));
assert_eq!(term.regex_search_right(start, end), None);
assert_eq!(term.regex_search_right(&dfas, start, end), None);
}
#[test]
fn no_match_left() {
#[rustfmt::skip]
let mut term = mock_term("\
let term = mock_term("\
first line\n\
broken second\r\n\
third\
");
term.regex_search = Some(RegexSearch::new("nothing").unwrap());
let dfas = RegexSearch::new("nothing").unwrap();
let start = Point::new(0, Column(4));
let end = Point::new(2, Column(0));
assert_eq!(term.regex_search_left(start, end), None);
assert_eq!(term.regex_search_left(&dfas, start, end), None);
}
#[test]
fn include_linebreak_left() {
#[rustfmt::skip]
let mut term = mock_term("\
let term = mock_term("\
testing123\r\n\
xxx\
");
// Make sure the cell containing the linebreak is not skipped.
term.regex_search = Some(RegexSearch::new("te.*123").unwrap());
let dfas = RegexSearch::new("te.*123").unwrap();
let start = Point::new(0, Column(0));
let end = Point::new(1, Column(0));
let match_start = Point::new(1, Column(0));
let match_end = Point::new(1, Column(9));
assert_eq!(term.regex_search_left(start, end), Some(match_start..=match_end));
assert_eq!(term.regex_search_left(&dfas, start, end), Some(match_start..=match_end));
}
#[test]
fn include_linebreak_right() {
#[rustfmt::skip]
let mut term = mock_term("\
let term = mock_term("\
xxx\r\n\
testing123\
");
// Make sure the cell containing the linebreak is not skipped.
term.regex_search = Some(RegexSearch::new("te.*123").unwrap());
let dfas = RegexSearch::new("te.*123").unwrap();
let start = Point::new(1, Column(2));
let end = Point::new(0, Column(9));
let match_start = Point::new(0, Column(0));
assert_eq!(term.regex_search_right(start, end), Some(match_start..=end));
assert_eq!(term.regex_search_right(&dfas, start, end), Some(match_start..=end));
}
#[test]
fn skip_dead_cell() {
let mut term = mock_term("alacritty");
let term = mock_term("alacritty");
// Make sure dead state cell is skipped when reversing.
term.regex_search = Some(RegexSearch::new("alacrit").unwrap());
let dfas = RegexSearch::new("alacrit").unwrap();
let start = Point::new(0, Column(0));
let end = Point::new(0, Column(6));
assert_eq!(term.regex_search_right(start, end), Some(start..=end));
assert_eq!(term.regex_search_right(&dfas, start, end), Some(start..=end));
}
#[test]
fn reverse_search_dead_recovery() {
let mut term = mock_term("zooo lense");
let term = mock_term("zooo lense");
// Make sure the reverse DFA operates the same as a forward DFA.
term.regex_search = Some(RegexSearch::new("zoo").unwrap());
let dfas = RegexSearch::new("zoo").unwrap();
let start = Point::new(0, Column(9));
let end = Point::new(0, Column(0));
let match_start = Point::new(0, Column(0));
let match_end = Point::new(0, Column(2));
assert_eq!(term.regex_search_left(start, end), Some(match_start..=match_end));
assert_eq!(term.regex_search_left(&dfas, start, end), Some(match_start..=match_end));
}
#[test]
fn multibyte_unicode() {
let mut term = mock_term("testвосибing");
let term = mock_term("testвосибing");
term.regex_search = Some(RegexSearch::new("te.*ing").unwrap());
let dfas = RegexSearch::new("te.*ing").unwrap();
let start = Point::new(0, Column(0));
let end = Point::new(0, Column(11));
assert_eq!(term.regex_search_right(start, end), Some(start..=end));
assert_eq!(term.regex_search_right(&dfas, start, end), Some(start..=end));
term.regex_search = Some(RegexSearch::new("te.*ing").unwrap());
let dfas = RegexSearch::new("te.*ing").unwrap();
let start = Point::new(0, Column(11));
let end = Point::new(0, Column(0));
assert_eq!(term.regex_search_left(start, end), Some(end..=start));
assert_eq!(term.regex_search_left(&dfas, start, end), Some(end..=start));
}
#[test]
fn fullwidth() {
let mut term = mock_term("a🦇x🦇");
let term = mock_term("a🦇x🦇");
term.regex_search = Some(RegexSearch::new("[^ ]*").unwrap());
let dfas = RegexSearch::new("[^ ]*").unwrap();
let start = Point::new(0, Column(0));
let end = Point::new(0, Column(5));
assert_eq!(term.regex_search_right(start, end), Some(start..=end));
assert_eq!(term.regex_search_right(&dfas, start, end), Some(start..=end));
term.regex_search = Some(RegexSearch::new("[^ ]*").unwrap());
let dfas = RegexSearch::new("[^ ]*").unwrap();
let start = Point::new(0, Column(5));
let end = Point::new(0, Column(0));
assert_eq!(term.regex_search_left(start, end), Some(end..=start));
assert_eq!(term.regex_search_left(&dfas, start, end), Some(end..=start));
}
#[test]
fn singlecell_fullwidth() {
let mut term = mock_term("🦇");
let term = mock_term("🦇");
term.regex_search = Some(RegexSearch::new("🦇").unwrap());
let dfas = RegexSearch::new("🦇").unwrap();
let start = Point::new(0, Column(0));
let end = Point::new(0, Column(1));
assert_eq!(term.regex_search_right(start, end), Some(start..=end));
assert_eq!(term.regex_search_right(&dfas, start, end), Some(start..=end));
term.regex_search = Some(RegexSearch::new("🦇").unwrap());
let dfas = RegexSearch::new("🦇").unwrap();
let start = Point::new(0, Column(1));
let end = Point::new(0, Column(0));
assert_eq!(term.regex_search_left(start, end), Some(end..=start));
assert_eq!(term.regex_search_left(&dfas, start, end), Some(end..=start));
}
#[test]
fn wrapping() {
#[rustfmt::skip]
let mut term = mock_term("\
let term = mock_term("\
xxx\r\n\
xxx\
");
term.regex_search = Some(RegexSearch::new("xxx").unwrap());
let dfas = RegexSearch::new("xxx").unwrap();
let start = Point::new(0, Column(2));
let end = Point::new(1, Column(2));
let match_start = Point::new(1, Column(0));
assert_eq!(term.regex_search_right(start, end), Some(match_start..=end));
assert_eq!(term.regex_search_right(&dfas, start, end), Some(match_start..=end));
term.regex_search = Some(RegexSearch::new("xxx").unwrap());
let dfas = RegexSearch::new("xxx").unwrap();
let start = Point::new(1, Column(0));
let end = Point::new(0, Column(0));
let match_end = Point::new(0, Column(2));
assert_eq!(term.regex_search_left(start, end), Some(end..=match_end));
assert_eq!(term.regex_search_left(&dfas, start, end), Some(end..=match_end));
}
#[test]
fn wrapping_into_fullwidth() {
#[rustfmt::skip]
let mut term = mock_term("\
let term = mock_term("\
🦇xx\r\n\
xx🦇\
");
term.regex_search = Some(RegexSearch::new("🦇x").unwrap());
let dfas = RegexSearch::new("🦇x").unwrap();
let start = Point::new(0, Column(0));
let end = Point::new(1, Column(3));
let match_start = Point::new(1, Column(0));
let match_end = Point::new(1, Column(2));
assert_eq!(term.regex_search_right(start, end), Some(match_start..=match_end));
assert_eq!(term.regex_search_right(&dfas, start, end), Some(match_start..=match_end));
term.regex_search = Some(RegexSearch::new("x🦇").unwrap());
let dfas = RegexSearch::new("x🦇").unwrap();
let start = Point::new(1, Column(2));
let end = Point::new(0, Column(0));
let match_start = Point::new(0, Column(1));
let match_end = Point::new(0, Column(3));
assert_eq!(term.regex_search_left(start, end), Some(match_start..=match_end));
assert_eq!(term.regex_search_left(&dfas, start, end), Some(match_start..=match_end));
}
#[test]
@ -741,32 +737,32 @@ mod tests {
");
term.grid[1][Column(3)].flags.insert(Flags::LEADING_WIDE_CHAR_SPACER);
term.regex_search = Some(RegexSearch::new("🦇x").unwrap());
let dfas = RegexSearch::new("🦇x").unwrap();
let start = Point::new(1, Column(0));
let end = Point::new(0, Column(3));
let match_start = Point::new(1, Column(3));
let match_end = Point::new(0, Column(2));
assert_eq!(term.regex_search_right(start, end), Some(match_start..=match_end));
assert_eq!(term.regex_search_right(&dfas, start, end), Some(match_start..=match_end));
term.regex_search = Some(RegexSearch::new("🦇x").unwrap());
let dfas = RegexSearch::new("🦇x").unwrap();
let start = Point::new(0, Column(3));
let end = Point::new(1, Column(0));
let match_start = Point::new(1, Column(3));
let match_end = Point::new(0, Column(2));
assert_eq!(term.regex_search_left(start, end), Some(match_start..=match_end));
assert_eq!(term.regex_search_left(&dfas, start, end), Some(match_start..=match_end));
term.regex_search = Some(RegexSearch::new("x🦇").unwrap());
let dfas = RegexSearch::new("x🦇").unwrap();
let start = Point::new(1, Column(0));
let end = Point::new(0, Column(3));
let match_start = Point::new(1, Column(2));
let match_end = Point::new(0, Column(1));
assert_eq!(term.regex_search_right(start, end), Some(match_start..=match_end));
assert_eq!(term.regex_search_right(&dfas, start, end), Some(match_start..=match_end));
term.regex_search = Some(RegexSearch::new("x🦇").unwrap());
let dfas = RegexSearch::new("x🦇").unwrap();
let start = Point::new(0, Column(3));
let end = Point::new(1, Column(0));
let match_start = Point::new(1, Column(2));
let match_end = Point::new(0, Column(1));
assert_eq!(term.regex_search_left(start, end), Some(match_start..=match_end));
assert_eq!(term.regex_search_left(&dfas, start, end), Some(match_start..=match_end));
}
}

View File

@ -81,13 +81,13 @@ impl ViModeCursor {
ViMotion::Left => {
buffer_point = term.expand_wide(buffer_point, Direction::Left);
let wrap_point = Point::new(buffer_point.line + 1, cols - 1);
if buffer_point.col.0 == 0
if buffer_point.column.0 == 0
&& buffer_point.line + 1 < term.total_lines()
&& is_wrap(term, wrap_point)
{
buffer_point = wrap_point;
} else {
buffer_point.col = Column(buffer_point.col.saturating_sub(1));
buffer_point.column = Column(buffer_point.column.saturating_sub(1));
}
},
ViMotion::Right => {
@ -95,34 +95,34 @@ impl ViModeCursor {
if is_wrap(term, buffer_point) {
buffer_point = Point::new(buffer_point.line - 1, Column(0));
} else {
buffer_point.col = min(buffer_point.col + 1, cols - 1);
buffer_point.column = min(buffer_point.column + 1, cols - 1);
}
},
ViMotion::First => {
buffer_point = term.expand_wide(buffer_point, Direction::Left);
while buffer_point.col.0 == 0
while buffer_point.column.0 == 0
&& buffer_point.line + 1 < term.total_lines()
&& is_wrap(term, Point::new(buffer_point.line + 1, cols - 1))
{
buffer_point.line += 1;
}
buffer_point.col = Column(0);
buffer_point.column = Column(0);
},
ViMotion::Last => buffer_point = last(term, buffer_point),
ViMotion::FirstOccupied => buffer_point = first_occupied(term, buffer_point),
ViMotion::High => {
let line = display_offset + lines.0 - 1;
let col = first_occupied_in_line(term, line).unwrap_or_default().col;
let col = first_occupied_in_line(term, line).unwrap_or_default().column;
buffer_point = Point::new(line, col);
},
ViMotion::Middle => {
let line = display_offset + lines.0 / 2;
let col = first_occupied_in_line(term, line).unwrap_or_default().col;
let col = first_occupied_in_line(term, line).unwrap_or_default().column;
buffer_point = Point::new(line, col);
},
ViMotion::Low => {
let line = display_offset;
let col = first_occupied_in_line(term, line).unwrap_or_default().col;
let col = first_occupied_in_line(term, line).unwrap_or_default().column;
buffer_point = Point::new(line, col);
},
ViMotion::SemanticLeft => {
@ -181,7 +181,7 @@ impl ViModeCursor {
let buffer_point = term.visible_to_buffer(self.point);
let mut target_line = buffer_point.line as isize + lines;
target_line = max(0, min(term.total_lines() as isize - 1, target_line));
let col = first_occupied_in_line(term, target_line as usize).unwrap_or_default().col;
let col = first_occupied_in_line(term, target_line as usize).unwrap_or_default().column;
// Move cursor.
self.point = Point::new(Line(line as usize), col);
@ -200,7 +200,7 @@ fn last<T>(term: &Term<T>, mut point: Point<usize>) -> Point<usize> {
// Find last non-empty cell in the current line.
let occupied = last_occupied_in_line(term, point.line).unwrap_or_default();
if point.col < occupied.col {
if point.column < occupied.column {
// Jump to last occupied cell when not already at or beyond it.
occupied
} else if is_wrap(term, point) {
@ -269,7 +269,7 @@ fn semantic<T: EventListener>(
// Expand semantically based on movement direction.
let expand_semantic = |point: Point<usize>| {
// Do not expand when currently on a semantic escape char.
let cell = &term.grid()[point.line][point.col];
let cell = &term.grid()[point.line][point.column];
if term.semantic_escape_chars().contains(cell.c)
&& !cell.flags.intersects(Flags::WIDE_CHAR_SPACER | Flags::LEADING_WIDE_CHAR_SPACER)
{
@ -375,21 +375,21 @@ fn advance<T>(term: &Term<T>, point: Point<usize>, direction: Direction) -> Poin
/// Check if cell at point contains whitespace.
fn is_space<T>(term: &Term<T>, point: Point<usize>) -> bool {
let cell = &term.grid()[point.line][point.col];
let cell = &term.grid()[point.line][point.column];
!cell.flags().intersects(Flags::WIDE_CHAR_SPACER | Flags::LEADING_WIDE_CHAR_SPACER)
&& (cell.c == ' ' || cell.c == '\t')
}
fn is_wrap<T>(term: &Term<T>, point: Point<usize>) -> bool {
point.line != 0 && term.grid()[point.line][point.col].flags.contains(Flags::WRAPLINE)
point.line != 0 && term.grid()[point.line][point.column].flags.contains(Flags::WRAPLINE)
}
/// Check if point is at screen boundary.
fn is_boundary<T>(term: &Term<T>, point: Point<usize>, direction: Direction) -> bool {
let total_lines = term.total_lines();
let num_cols = term.cols();
(point.line + 1 >= total_lines && point.col.0 == 0 && direction == Direction::Left)
|| (point.line == 0 && point.col + 1 >= num_cols && direction == Direction::Right)
(point.line + 1 >= total_lines && point.column.0 == 0 && direction == Direction::Left)
|| (point.line == 0 && point.column + 1 >= num_cols && direction == Direction::Right)
}
#[cfg(test)]
@ -397,18 +397,12 @@ mod tests {
use super::*;
use crate::config::MockConfig;
use crate::event::Event;
use crate::index::{Column, Line};
use crate::term::{SizeInfo, Term};
struct Mock;
impl EventListener for Mock {
fn send_event(&self, _event: Event) {}
}
fn term() -> Term<Mock> {
fn term() -> Term<()> {
let size = SizeInfo::new(20., 20., 1.0, 1.0, 0.0, 0.0, false);
Term::new(&MockConfig::default(), size, Mock)
Term::new(&MockConfig::default(), size, ())
}
#[test]
@ -515,7 +509,7 @@ mod tests {
assert_eq!(cursor.point, Point::new(Line(0), Column(0)));
}
fn motion_semantic_term() -> Term<Mock> {
fn motion_semantic_term() -> Term<()> {
let mut term = term();
term.grid_mut()[Line(0)][Column(0)].c = 'x';

View File

@ -84,7 +84,9 @@ struct RefConfig {
history_size: u32,
}
#[derive(Copy, Clone)]
struct Mock;
impl EventListener for Mock {
fn send_event(&self, _event: Event) {}
}