diff options
author | Christian Duerr <contact@christianduerr.com> | 2021-01-24 21:45:36 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-01-24 21:45:36 +0000 |
commit | 530de00049c2afcc562d36ccdb3e6afa2fe396a5 (patch) | |
tree | 3dabbcef3fc4a2041f9027d82243aa0d70928153 /alacritty/src | |
parent | 7291702f6b4fff10f2470f084abe0785b95659a0 (diff) | |
download | r-alacritty-530de00049c2afcc562d36ccdb3e6afa2fe396a5.tar.gz r-alacritty-530de00049c2afcc562d36ccdb3e6afa2fe396a5.tar.bz2 r-alacritty-530de00049c2afcc562d36ccdb3e6afa2fe396a5.zip |
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.
Diffstat (limited to 'alacritty/src')
-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 |
18 files changed, 1311 insertions, 255 deletions
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)); } } |