diff options
Diffstat (limited to 'alacritty')
-rw-r--r-- | alacritty/Cargo.toml | 2 | ||||
-rw-r--r-- | alacritty/src/config/bell.rs | 70 | ||||
-rw-r--r-- | alacritty/src/config/color.rs | 212 | ||||
-rw-r--r-- | alacritty/src/config/mod.rs | 14 | ||||
-rw-r--r-- | alacritty/src/config/ui_config.rs | 14 | ||||
-rw-r--r-- | alacritty/src/display/bell.rs | 122 | ||||
-rw-r--r-- | alacritty/src/display/color.rs | 167 | ||||
-rw-r--r-- | alacritty/src/display/content.rs | 404 | ||||
-rw-r--r-- | alacritty/src/display/cursor.rs (renamed from alacritty/src/cursor.rs) | 4 | ||||
-rw-r--r-- | alacritty/src/display/meter.rs (renamed from alacritty/src/meter.rs) | 0 | ||||
-rw-r--r-- | alacritty/src/display/mod.rs (renamed from alacritty/src/display.rs) | 71 | ||||
-rw-r--r-- | alacritty/src/display/wayland_theme.rs (renamed from alacritty/src/wayland_theme.rs) | 3 | ||||
-rw-r--r-- | alacritty/src/display/window.rs (renamed from alacritty/src/window.rs) | 11 | ||||
-rw-r--r-- | alacritty/src/event.rs | 249 | ||||
-rw-r--r-- | alacritty/src/input.rs | 157 | ||||
-rw-r--r-- | alacritty/src/main.rs | 6 | ||||
-rw-r--r-- | alacritty/src/renderer/mod.rs | 9 | ||||
-rw-r--r-- | alacritty/src/renderer/rects.rs | 16 | ||||
-rw-r--r-- | alacritty/src/url.rs | 37 |
19 files changed, 1312 insertions, 256 deletions
diff --git a/alacritty/Cargo.toml b/alacritty/Cargo.toml index 73724fbe..923659d2 100644 --- a/alacritty/Cargo.toml +++ b/alacritty/Cargo.toml @@ -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] diff --git a/alacritty/src/config/bell.rs b/alacritty/src/config/bell.rs new file mode 100644 index 00000000..2516e2b3 --- /dev/null +++ b/alacritty/src/config/bell.rs @@ -0,0 +1,70 @@ +use std::time::Duration; + +use alacritty_config_derive::ConfigDeserialize; + +use alacritty_terminal::config::Program; +use alacritty_terminal::term::color::Rgb; + +#[derive(ConfigDeserialize, Clone, Debug, PartialEq, Eq)] +pub struct BellConfig { + /// Visual bell animation function. + pub animation: BellAnimation, + + /// Command to run on bell. + pub command: Option<Program>, + + /// Visual bell flash color. + pub color: Rgb, + + /// Visual bell duration in milliseconds. + duration: u16, +} + +impl Default for BellConfig { + fn default() -> Self { + Self { + color: Rgb { r: 255, g: 255, b: 255 }, + animation: Default::default(), + command: Default::default(), + duration: Default::default(), + } + } +} + +impl BellConfig { + pub fn duration(&self) -> Duration { + Duration::from_millis(self.duration as u64) + } +} + +/// `VisualBellAnimations` are modeled after a subset of CSS transitions and Robert +/// Penner's Easing Functions. +#[derive(ConfigDeserialize, Clone, Copy, Debug, PartialEq, Eq)] +pub enum BellAnimation { + // CSS animation. + Ease, + // CSS animation. + EaseOut, + // Penner animation. + EaseOutSine, + // Penner animation. + EaseOutQuad, + // Penner animation. + EaseOutCubic, + // Penner animation. + EaseOutQuart, + // Penner animation. + EaseOutQuint, + // Penner animation. + EaseOutExpo, + // Penner animation. + EaseOutCirc, + // Penner animation. + Linear, +} + +impl Default for BellAnimation { + fn default() -> Self { + BellAnimation::EaseOutExpo + } +} diff --git a/alacritty/src/config/color.rs b/alacritty/src/config/color.rs new file mode 100644 index 00000000..cd5d964d --- /dev/null +++ b/alacritty/src/config/color.rs @@ -0,0 +1,212 @@ +use serde::de::Error as SerdeError; +use serde::{Deserialize, Deserializer}; + +use alacritty_config_derive::ConfigDeserialize; +use alacritty_terminal::term::color::{CellRgb, Rgb}; + +#[derive(ConfigDeserialize, Clone, Debug, Default, PartialEq, Eq)] +pub struct Colors { + pub primary: PrimaryColors, + pub cursor: InvertedCellColors, + pub vi_mode_cursor: InvertedCellColors, + pub selection: InvertedCellColors, + pub normal: NormalColors, + pub bright: BrightColors, + pub dim: Option<DimColors>, + pub indexed_colors: Vec<IndexedColor>, + pub search: SearchColors, + pub line_indicator: LineIndicatorColors, +} + +impl Colors { + pub fn search_bar_foreground(&self) -> Rgb { + self.search.bar.foreground.unwrap_or(self.primary.background) + } + + pub fn search_bar_background(&self) -> Rgb { + self.search.bar.background.unwrap_or(self.primary.foreground) + } +} + +#[derive(ConfigDeserialize, Copy, Clone, Default, Debug, PartialEq, Eq)] +pub struct LineIndicatorColors { + pub foreground: Option<Rgb>, + pub background: Option<Rgb>, +} + +#[derive(Deserialize, Copy, Clone, Default, Debug, PartialEq, Eq)] +pub struct IndexedColor { + pub color: Rgb, + + index: ColorIndex, +} + +impl IndexedColor { + #[inline] + pub fn index(&self) -> u8 { + self.index.0 + } +} + +#[derive(Copy, Clone, Default, Debug, PartialEq, Eq)] +struct ColorIndex(u8); + +impl<'de> Deserialize<'de> for ColorIndex { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + let index = u8::deserialize(deserializer)?; + + if index < 16 { + Err(SerdeError::custom( + "Config error: indexed_color's index is {}, but a value bigger than 15 was \ + expected; ignoring setting", + )) + } else { + Ok(Self(index)) + } + } +} + +#[derive(ConfigDeserialize, Debug, Copy, Clone, PartialEq, Eq)] +pub struct InvertedCellColors { + #[config(alias = "text")] + pub foreground: CellRgb, + #[config(alias = "cursor")] + pub background: CellRgb, +} + +impl Default for InvertedCellColors { + fn default() -> Self { + Self { foreground: CellRgb::CellBackground, background: CellRgb::CellForeground } + } +} + +#[derive(ConfigDeserialize, Debug, Copy, Clone, Default, PartialEq, Eq)] +pub struct SearchColors { + pub focused_match: InvertedCellColors, + pub matches: MatchColors, + bar: BarColors, +} + +#[derive(ConfigDeserialize, Debug, Copy, Clone, PartialEq, Eq)] +pub struct MatchColors { + pub foreground: CellRgb, + pub background: CellRgb, +} + +impl Default for MatchColors { + fn default() -> Self { + Self { + background: CellRgb::Rgb(Rgb { r: 0xff, g: 0xff, b: 0xff }), + foreground: CellRgb::Rgb(Rgb { r: 0x00, g: 0x00, b: 0x00 }), + } + } +} + +#[derive(ConfigDeserialize, Debug, Copy, Clone, Default, PartialEq, Eq)] +pub struct BarColors { + foreground: Option<Rgb>, + background: Option<Rgb>, +} + +#[derive(ConfigDeserialize, Clone, Debug, PartialEq, Eq)] +pub struct PrimaryColors { + pub foreground: Rgb, + pub background: Rgb, + pub bright_foreground: Option<Rgb>, + pub dim_foreground: Option<Rgb>, +} + +impl Default for PrimaryColors { + fn default() -> Self { + PrimaryColors { + background: Rgb { r: 0x1d, g: 0x1f, b: 0x21 }, + foreground: Rgb { r: 0xc5, g: 0xc8, b: 0xc6 }, + bright_foreground: Default::default(), + dim_foreground: Default::default(), + } + } +} + +#[derive(ConfigDeserialize, Clone, Debug, PartialEq, Eq)] +pub struct NormalColors { + pub black: Rgb, + pub red: Rgb, + pub green: Rgb, + pub yellow: Rgb, + pub blue: Rgb, + pub magenta: Rgb, + pub cyan: Rgb, + pub white: Rgb, +} + +impl Default for NormalColors { + fn default() -> Self { + NormalColors { + black: Rgb { r: 0x1d, g: 0x1f, b: 0x21 }, + red: Rgb { r: 0xcc, g: 0x66, b: 0x66 }, + green: Rgb { r: 0xb5, g: 0xbd, b: 0x68 }, + yellow: Rgb { r: 0xf0, g: 0xc6, b: 0x74 }, + blue: Rgb { r: 0x81, g: 0xa2, b: 0xbe }, + magenta: Rgb { r: 0xb2, g: 0x94, b: 0xbb }, + cyan: Rgb { r: 0x8a, g: 0xbe, b: 0xb7 }, + white: Rgb { r: 0xc5, g: 0xc8, b: 0xc6 }, + } + } +} + +#[derive(ConfigDeserialize, Clone, Debug, PartialEq, Eq)] +pub struct BrightColors { + pub black: Rgb, + pub red: Rgb, + pub green: Rgb, + pub yellow: Rgb, + pub blue: Rgb, + pub magenta: Rgb, + pub cyan: Rgb, + pub white: Rgb, +} + +impl Default for BrightColors { + fn default() -> Self { + BrightColors { + black: Rgb { r: 0x66, g: 0x66, b: 0x66 }, + red: Rgb { r: 0xd5, g: 0x4e, b: 0x53 }, + green: Rgb { r: 0xb9, g: 0xca, b: 0x4a }, + yellow: Rgb { r: 0xe7, g: 0xc5, b: 0x47 }, + blue: Rgb { r: 0x7a, g: 0xa6, b: 0xda }, + magenta: Rgb { r: 0xc3, g: 0x97, b: 0xd8 }, + cyan: Rgb { r: 0x70, g: 0xc0, b: 0xb1 }, + white: Rgb { r: 0xea, g: 0xea, b: 0xea }, + } + } +} + +#[derive(ConfigDeserialize, Clone, Debug, PartialEq, Eq)] +pub struct DimColors { + pub black: Rgb, + pub red: Rgb, + pub green: Rgb, + pub yellow: Rgb, + pub blue: Rgb, + pub magenta: Rgb, + pub cyan: Rgb, + pub white: Rgb, +} + +impl Default for DimColors { + fn default() -> Self { + DimColors { + black: Rgb { r: 0x13, g: 0x14, b: 0x15 }, + red: Rgb { r: 0x86, g: 0x43, b: 0x43 }, + green: Rgb { r: 0x77, g: 0x7c, b: 0x44 }, + yellow: Rgb { r: 0x9e, g: 0x82, b: 0x4c }, + blue: Rgb { r: 0x55, g: 0x6a, b: 0x7d }, + magenta: Rgb { r: 0x75, g: 0x61, b: 0x7b }, + cyan: Rgb { r: 0x5b, g: 0x7d, b: 0x78 }, + white: Rgb { r: 0x82, g: 0x84, b: 0x82 }, + } + } +} diff --git a/alacritty/src/config/mod.rs b/alacritty/src/config/mod.rs index 0673ffd5..a782f5fe 100644 --- a/alacritty/src/config/mod.rs +++ b/alacritty/src/config/mod.rs @@ -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> { diff --git a/alacritty/src/config/ui_config.rs b/alacritty/src/config/ui_config.rs index 25f9fb91..b3b3021a 100644 --- a/alacritty/src/config/ui_config.rs +++ b/alacritty/src/config/ui_config.rs @@ -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(), } } } diff --git a/alacritty/src/display/bell.rs b/alacritty/src/display/bell.rs new file mode 100644 index 00000000..1aee3ba6 --- /dev/null +++ b/alacritty/src/display/bell.rs @@ -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 +} diff --git a/alacritty/src/display/color.rs b/alacritty/src/display/color.rs new file mode 100644 index 00000000..6e0de048 --- /dev/null +++ b/alacritty/src/display/color.rs @@ -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] + } +} diff --git a/alacritty/src/display/content.rs b/alacritty/src/display/content.rs new file mode 100644 index 00000000..81c2977f --- /dev/null +++ b/alacritty/src/display/content.rs @@ -0,0 +1,404 @@ +use std::cmp::max; +use std::mem; +use std::ops::RangeInclusive; + +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; + +/// Maximum number of linewraps followed outside of the viewport during search highlighting. +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> { + terminal_content: TerminalContent<'a>, + terminal_cursor: TerminalCursor, + cursor: Option<RenderableCursor>, + search: RenderableSearch, + config: &'a Config<UIConfig>, + colors: &'a List, +} + +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(); + + // 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 { + 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. + pub fn cursor(mut self) -> Option<RenderableCursor> { + // Drain the iterator to make sure the cursor is created. + while self.next().is_some() && self.cursor.is_none() {} + + 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.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.terminal_cursor.point.column -= 1; + true + } else { + cell.flags.contains(Flags::WIDE_CHAR) + }; + + // Cursor colors. + let color = if self.terminal_content.mode.contains(TermMode::VI) { + self.config.ui_config.colors.vi_mode_cursor + } else { + 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. + if matches!( + cursor_color, + CellRgb::Rgb(color) if color.contrast(cell.bg) < MIN_CURSOR_CONTRAST + ) { + cursor_color = CellRgb::CellForeground; + text_color = CellRgb::CellBackground; + } + + // Convert from cell colors to RGB. + let text_color = text_color.color(cell.fg, cell.bg); + let cursor_color = cursor_color.color(cell.fg, cell.bg); + + Some(RenderableCursor { + point: self.terminal_cursor.point, + shape: self.terminal_cursor.shape, + cursor_color, + text_color, + is_wide, + }) + } +} + +impl<'a> Iterator for RenderableContent<'a> { + type Item = RenderableCell; + + /// Gets the next renderable cell. + /// + /// Skips empty (background) cells and applies any flags to the cell state + /// (eg. invert fg and bg colors). + #[inline] + fn next(&mut self) -> Option<Self::Item> { + loop { + 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 { + cell.fg = cursor.text_color; + cell.bg = cursor.cursor_color; + + // Since we draw Block cursor by drawing cell below it with a proper color, + // we must adjust alpha to make it visible. + cell.bg_alpha = 1.; + } + + cursor + }); + + return Some(cell); + } else if !cell.is_empty() && !cell.flags.contains(Flags::WIDE_CHAR_SPACER) { + // Skip empty cells and wide char spacers. + return Some(cell); + } + } + } +} + +/// Cell ready for rendering. +#[derive(Clone, Debug)] +pub struct RenderableCell { + pub character: char, + pub zerowidth: Option<Vec<char>>, + pub point: Point, + pub fg: Rgb, + pub bg: Rgb, + pub bg_alpha: f32, + pub flags: Flags, + pub is_match: bool, +} + +impl RenderableCell { + fn new<'a>(content: &mut RenderableContent<'a>, cell: Indexed<&Cell, Line>) -> Self { + // Lookup RGB values. + 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); + 1.0 + } else { + Self::compute_bg_alpha(cell.bg) + }; + + 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 = 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.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(cell.point) { + // Highlight the cell if it is part of a search match. + 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; + + if config_bg != CellRgb::CellBackground { + bg_alpha = 1.0; + } + + is_match = true; + } + + RenderableCell { + character: cell.c, + zerowidth: cell.zerowidth().map(|zerowidth| zerowidth.to_vec()), + point: cell.point, + fg: fg_rgb, + bg: bg_rgb, + bg_alpha, + flags: cell.flags, + is_match, + } + } + + /// Check if cell contains any renderable content. + fn is_empty(&self) -> bool { + self.bg_alpha == 0. + && !self.flags.intersects(Flags::UNDERLINE | Flags::STRIKEOUT | Flags::DOUBLE_UNDERLINE) + && self.character == ' ' + && self.zerowidth.is_none() + } + + /// Get the RGB color from a cell's foreground color. + 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 (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 + && ui_config.colors.primary.bright_foreground.is_none() => + { + content.color(NamedColor::DimForeground as usize) + }, + // Draw bold text in bright colors *and* contains bold flag. + (true, Flags::BOLD) => content.color(ansi.to_bright() as usize), + // Cell is marked as dim and not bold. + (_, Flags::DIM) | (false, Flags::DIM_BOLD) => { + content.color(ansi.to_dim() as usize) + }, + // None of the above, keep original color.. + _ => content.color(ansi as usize), + } + }, + Color::Indexed(idx) => { + let idx = match ( + ui_config.draw_bold_text_with_bright_colors, + flags & Flags::DIM_BOLD, + idx, + ) { + (true, Flags::BOLD, 0..=7) => idx as usize + 8, + (false, Flags::DIM, 8..=15) => idx as usize - 8, + (false, Flags::DIM, 0..=7) => NamedColor::DimBlack as usize + idx as usize, + _ => idx as usize, + }; + + content.color(idx) + }, + } + } + + /// Get the RGB color from a cell's background color. + #[inline] + fn compute_bg_rgb(content: &mut RenderableContent<'_>, bg: Color) -> Rgb { + match bg { + Color::Spec(rgb) => rgb, + Color::Named(ansi) => content.color(ansi as usize), + Color::Indexed(idx) => content.color(idx as usize), + } + } + + /// Compute background alpha based on cell's original color. + /// + /// Since an RGB color matching the background should not be transparent, this is computed + /// using the named input color, rather than checking the RGB of the background after its color + /// is computed. + #[inline] + fn compute_bg_alpha(bg: Color) -> f32 { + if bg == Color::Named(NamedColor::Background) { + 0. + } else { + 1. + } + } +} + +/// Cursor storing all information relevant for rendering. +#[derive(Debug, Eq, PartialEq, Copy, Clone)] +pub struct RenderableCursor { + shape: CursorShape, + cursor_color: Rgb, + text_color: Rgb, + is_wide: bool, + point: Point, +} + +impl RenderableCursor { + pub fn color(&self) -> Rgb { + self.cursor_color + } + + pub fn shape(&self) -> CursorShape { + self.shape + } + + pub fn is_wide(&self) -> bool { + self.is_wide + } + + pub fn point(&self) -> Point { + self.point + } +} + +/// Regex search highlight tracking. +#[derive(Default)] +pub struct RenderableSearch { + /// All visible search matches. + matches: Vec<RangeInclusive<Point>>, + + /// Index of the last match checked. + index: usize, +} + +impl RenderableSearch { + /// Create a new renderable search iterator. + 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; + + // Compute start of the first and end of the last line. + let start_point = Point::new(viewport_start, Column(0)); + let mut start = term.line_search_left(start_point); + let end_point = Point::new(viewport_end, term.cols() - 1); + let mut end = term.line_search_right(end_point); + + // Set upper bound on search before/after the viewport to prevent excessive blocking. + if start.line > viewport_start + MAX_SEARCH_LINES { + if start.line == 0 { + // Do not highlight anything if this line is the last. + return Self::default(); + } else { + // Start at next line if this one is too long. + start.line -= 1; + } + } + end.line = max(end.line, viewport_end.saturating_sub(MAX_SEARCH_LINES)); + + // Create an iterater for the current regex search for all visible matches. + let iter = 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 { 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) -> bool { + while let Some(regex_match) = self.matches.get(self.index) { + if regex_match.start() > &point { + break; + } else if regex_match.end() < &point { + self.index += 1; + } else { + return true; + } + } + false + } +} diff --git a/alacritty/src/cursor.rs b/alacritty/src/display/cursor.rs index a9fba66a..0750459d 100644 --- a/alacritty/src/cursor.rs +++ b/alacritty/src/display/cursor.rs @@ -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(); diff --git a/alacritty/src/meter.rs b/alacritty/src/display/meter.rs index c07d901f..c07d901f 100644 --- a/alacritty/src/meter.rs +++ b/alacritty/src/display/meter.rs diff --git a/alacritty/src/display.rs b/alacritty/src/display/mod.rs index 5e885b53..2a55402e 100644 --- a/alacritty/src/display.rs +++ b/alacritty/src/display/mod.rs @@ -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); diff --git a/alacritty/src/wayland_theme.rs b/alacritty/src/display/wayland_theme.rs index 5d3bd922..1932ae01 100644 --- a/alacritty/src/wayland_theme.rs +++ b/alacritty/src/display/wayland_theme.rs @@ -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)] diff --git a/alacritty/src/window.rs b/alacritty/src/display/window.rs index 3661d406..b500e8f2 100644 --- a/alacritty/src/window.rs +++ b/alacritty/src/display/window.rs @@ -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)); diff --git a/alacritty/src/event.rs b/alacritty/src/event.rs index d83469ee..26f781dc 100644 --- a/alacritty/src/event.rs +++ b/alacritty/src/event.rs @@ -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(®ex); + // Create search dfas for the new regex string. + self.search_state.dfas = RegexSearch::new(®ex).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::ModifiersChanged(modifiers) => { + processor.modifiers_input(modifiers) + }, WindowEvent::ReceivedCharacter(c) => processor.received_char(c), WindowEvent::MouseInput { state, button, .. } => { - processor.ctx.window.set_mouse_visible(true); + processor.ctx.window_mut().set_mouse_visible(true); processor.mouse_input(state, button); - processor.ctx.terminal.dirty = true; - }, - WindowEvent::ModifiersChanged(modifiers) => { - processor.modifiers_input(modifiers) + 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`. diff --git a/alacritty/src/input.rs b/alacritty/src/input.rs index 155fab07..778dffc7 100644 --- a/alacritty/src/input.rs +++ b/alacritty/src/input.rs @@ -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 { diff --git a/alacritty/src/main.rs b/alacritty/src/main.rs index 1bcf64b9..30eff73d 100644 --- a/alacritty/src/main.rs +++ b/alacritty/src/main.rs @@ -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)] diff --git a/alacritty/src/renderer/mod.rs b/alacritty/src/renderer/mod.rs index 39e53b82..1f203332 100644 --- a/alacritty/src/renderer/mod.rs +++ b/alacritty/src/renderer/mod.rs @@ -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(), diff --git a/alacritty/src/renderer/rects.rs b/alacritty/src/renderer/rects.rs index cfd17379..80886c95 100644 --- a/alacritty/src/renderer/rects.rs +++ b/alacritty/src/renderer/rects.rs @@ -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 => { diff --git a/alacritty/src/url.rs b/alacritty/src/url.rs index f4bf8205..d86b514a 100644 --- a/alacritty/src/url.rs +++ b/alacritty/src/url.rs @@ -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)); } } |