diff options
| author | Ayose <ayosec@gmail.com> | 2024-02-11 00:00:00 +0000 |
|---|---|---|
| committer | Ayose <ayosec@gmail.com> | 2024-02-11 00:00:00 +0000 |
| commit | 5e75fc92bd6bd956873baa8e30d2c14f2fe83a54 (patch) | |
| tree | 25836c51f65adf60c2ea2d0f799f77b12446451a /alacritty/src | |
| parent | 713c5415262ab091a645f976907dacaf40703ae8 (diff) | |
| parent | 117719b3216d60ddde4096d9b2bdccecfd214c42 (diff) | |
| download | r-alacritty-5e75fc92bd6bd956873baa8e30d2c14f2fe83a54.tar.gz r-alacritty-5e75fc92bd6bd956873baa8e30d2c14f2fe83a54.tar.bz2 r-alacritty-5e75fc92bd6bd956873baa8e30d2c14f2fe83a54.zip | |
Merge remote-tracking branch 'vendor/master' into graphics
Diffstat (limited to 'alacritty/src')
42 files changed, 4446 insertions, 1974 deletions
diff --git a/alacritty/src/cli.rs b/alacritty/src/cli.rs index 38707036..fee2680f 100644 --- a/alacritty/src/cli.rs +++ b/alacritty/src/cli.rs @@ -1,24 +1,26 @@ use std::cmp::max; -use std::os::raw::c_ulong; +use std::ops::{Deref, DerefMut}; use std::path::PathBuf; +use std::rc::Rc; -#[cfg(unix)] -use clap::Subcommand; -use clap::{Args, Parser, ValueHint}; +use alacritty_config::SerdeReplace; +use clap::{ArgAction, Args, Parser, Subcommand, ValueHint}; use log::{self, error, LevelFilter}; use serde::{Deserialize, Serialize}; -use serde_yaml::Value; +use toml::Value; -use alacritty_terminal::config::{Program, PtyConfig}; +use alacritty_terminal::tty::Options as PtyOptions; +use crate::config::ui_config::Program; use crate::config::window::{Class, Identity}; -use crate::config::{serde_utils, UiConfig}; +use crate::config::UiConfig; +use crate::logging::LOG_TARGET_IPC_CONFIG; /// CLI options for the main Alacritty executable. #[derive(Parser, Default, Debug)] #[clap(author, about, version = env!("VERSION"))] pub struct Options { - /// Print all events to stdout. + /// Print all events to STDOUT. #[clap(long)] pub print_events: bool, @@ -30,17 +32,18 @@ pub struct Options { #[clap(long)] pub embed: Option<String>, - /// Specify alternative configuration file [default: $XDG_CONFIG_HOME/alacritty/alacritty.yml]. + /// Specify alternative configuration file [default: + /// $XDG_CONFIG_HOME/alacritty/alacritty.toml]. #[cfg(not(any(target_os = "macos", windows)))] #[clap(long, value_hint = ValueHint::FilePath)] pub config_file: Option<PathBuf>, - /// Specify alternative configuration file [default: %APPDATA%\alacritty\alacritty.yml]. + /// Specify alternative configuration file [default: %APPDATA%\alacritty\alacritty.toml]. #[cfg(windows)] #[clap(long, value_hint = ValueHint::FilePath)] pub config_file: Option<PathBuf>, - /// Specify alternative configuration file [default: $HOME/.config/alacritty/alacritty.yml]. + /// Specify alternative configuration file [default: $HOME/.config/alacritty/alacritty.toml]. #[cfg(target_os = "macos")] #[clap(long, value_hint = ValueHint::FilePath)] pub config_file: Option<PathBuf>, @@ -51,27 +54,22 @@ pub struct Options { pub socket: Option<PathBuf>, /// Reduces the level of verbosity (the min level is -qq). - #[clap(short, conflicts_with("verbose"), parse(from_occurrences))] + #[clap(short, conflicts_with("verbose"), action = ArgAction::Count)] quiet: u8, /// Increases the level of verbosity (the max level is -vvv). - #[clap(short, conflicts_with("quiet"), parse(from_occurrences))] + #[clap(short, conflicts_with("quiet"), action = ArgAction::Count)] verbose: u8, - /// Override configuration file options [example: cursor.style=Beam]. - #[clap(short = 'o', long, multiple_values = true)] - option: Vec<String>, - /// CLI options for config overrides. #[clap(skip)] - pub config_options: Value, + pub config_options: ParsedOptions, /// Options which can be passed via IPC. #[clap(flatten)] pub window_options: WindowOptions, /// Subcommand passed to the CLI. - #[cfg(unix)] #[clap(subcommand)] pub subcommands: Option<Subcommands>, } @@ -80,14 +78,14 @@ impl Options { pub fn new() -> Self { let mut options = Self::parse(); - // Convert `--option` flags into serde `Value`. - options.config_options = options_as_value(&options.option); + // Parse CLI config overrides. + options.config_options = options.window_options.config_overrides(); options } /// Override configuration file with options from the CLI. - pub fn override_config(&self, config: &mut UiConfig) { + pub fn override_config(&mut self, config: &mut UiConfig) { #[cfg(unix)] { config.ipc_socket |= self.socket.is_some(); @@ -102,6 +100,9 @@ impl Options { if config.debug.print_events { config.debug.log_level = max(config.debug.log_level, LevelFilter::Info); } + + // Replace CLI options. + self.config_options.override_config(config); } /// Logging filter level. @@ -125,42 +126,6 @@ impl Options { } } -/// Combine multiple options into a [`serde_yaml::Value`]. -pub fn options_as_value(options: &[String]) -> Value { - options.iter().fold(Value::default(), |value, option| match option_as_value(option) { - Ok(new_value) => serde_utils::merge(value, new_value), - Err(_) => { - eprintln!("Ignoring invalid option: {:?}", option); - value - }, - }) -} - -/// Parse an option in the format of `parent.field=value` as a serde Value. -fn option_as_value(option: &str) -> Result<Value, serde_yaml::Error> { - let mut yaml_text = String::with_capacity(option.len()); - let mut closing_brackets = String::new(); - - for (i, c) in option.chars().enumerate() { - match c { - '=' => { - yaml_text.push_str(": "); - yaml_text.push_str(&option[i + 1..]); - break; - }, - '.' => { - yaml_text.push_str(": {"); - closing_brackets.push('}'); - }, - _ => yaml_text.push(c), - } - } - - yaml_text += &closing_brackets; - - serde_yaml::from_str(&yaml_text) -} - /// Parse the class CLI parameter. fn parse_class(input: &str) -> Result<Class, String> { let (general, instance) = match input.split_once(',') { @@ -176,10 +141,10 @@ fn parse_class(input: &str) -> Result<Class, String> { } /// Convert to hex if possible, else decimal -fn parse_hex_or_decimal(input: &str) -> Option<c_ulong> { +fn parse_hex_or_decimal(input: &str) -> Option<u32> { input .strip_prefix("0x") - .and_then(|value| c_ulong::from_str_radix(value, 16).ok()) + .and_then(|value| u32::from_str_radix(value, 16).ok()) .or_else(|| input.parse().ok()) } @@ -195,7 +160,7 @@ pub struct TerminalOptions { pub hold: bool, /// Command and args to execute (must be last argument). - #[clap(short = 'e', long, allow_hyphen_values = true, multiple_values = true)] + #[clap(short = 'e', long, allow_hyphen_values = true, num_args = 1..)] command: Vec<String>, } @@ -206,8 +171,8 @@ impl TerminalOptions { Some(Program::WithArgs { program: program.clone(), args: args.to_vec() }) } - /// Override the [`PtyConfig`]'s fields with the [`TerminalOptions`]. - pub fn override_pty_config(&self, pty_config: &mut PtyConfig) { + /// Override the [`PtyOptions`]'s fields with the [`TerminalOptions`]. + pub fn override_pty_config(&self, pty_config: &mut PtyOptions) { if let Some(working_directory) = &self.working_directory { if working_directory.is_dir() { pty_config.working_directory = Some(working_directory.to_owned()); @@ -217,18 +182,18 @@ impl TerminalOptions { } if let Some(command) = self.command() { - pty_config.shell = Some(command); + pty_config.shell = Some(command.into()); } pty_config.hold |= self.hold; } } -impl From<TerminalOptions> for PtyConfig { +impl From<TerminalOptions> for PtyOptions { fn from(mut options: TerminalOptions) -> Self { - PtyConfig { + PtyOptions { working_directory: options.working_directory.take(), - shell: options.command(), + shell: options.command().map(Into::into), hold: options.hold, } } @@ -242,7 +207,7 @@ pub struct WindowIdentity { pub title: Option<String>, /// Defines window class/app_id on X11/Wayland [default: Alacritty]. - #[clap(long, value_name = "general> | <general>,<instance", parse(try_from_str = parse_class))] + #[clap(long, value_name = "general> | <general>,<instance", value_parser = parse_class)] pub class: Option<Class>, } @@ -259,10 +224,11 @@ impl WindowIdentity { } /// Available CLI subcommands. -#[cfg(unix)] #[derive(Subcommand, Debug)] pub enum Subcommands { + #[cfg(unix)] Msg(MessageOptions), + Migrate(MigrateOptions), } /// Send a message to the Alacritty socket. @@ -289,6 +255,30 @@ pub enum SocketMessage { Config(IpcConfig), } +/// Migrate the configuration file. +#[derive(Args, Clone, Debug)] +pub struct MigrateOptions { + /// Path to the configuration file. + #[clap(short, long, value_hint = ValueHint::FilePath)] + pub config_file: Option<PathBuf>, + + /// Only output TOML config to STDOUT. + #[clap(short, long)] + pub dry_run: bool, + + /// Do not recurse over imports. + #[clap(short = 'i', long)] + pub skip_imports: bool, + + /// Do not move renamed fields to their new location. + #[clap(long)] + pub skip_renames: bool, + + #[clap(short, long)] + /// Do not output to STDOUT. + pub silent: bool, +} + /// Subset of options that we pass to 'create-window' IPC subcommand. #[derive(Serialize, Deserialize, Args, Default, Clone, Debug, PartialEq, Eq)] pub struct WindowOptions { @@ -299,13 +289,29 @@ pub struct WindowOptions { #[clap(flatten)] /// Window options which could be passed via IPC. pub window_identity: WindowIdentity, + + #[clap(skip)] + #[cfg(target_os = "macos")] + /// The window tabbing identifier to use when building a window. + pub window_tabbing_id: Option<String>, + + /// Override configuration file options [example: 'cursor.style="Beam"']. + #[clap(short = 'o', long, num_args = 1..)] + option: Vec<String>, +} + +impl WindowOptions { + /// Get the parsed set of CLI config overrides. + pub fn config_overrides(&self) -> ParsedOptions { + ParsedOptions::from_options(&self.option) + } } /// Parameters to the `config` IPC subcommand. #[cfg(unix)] #[derive(Args, Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] pub struct IpcConfig { - /// Configuration file options [example: cursor.style=Beam]. + /// Configuration file options [example: 'cursor.style="Beam"']. #[clap(required = true, value_name = "CONFIG_OPTIONS")] pub options: Vec<String>, @@ -320,6 +326,78 @@ pub struct IpcConfig { pub reset: bool, } +/// Parsed CLI config overrides. +#[derive(Debug, Default)] +pub struct ParsedOptions { + config_options: Vec<(String, Value)>, +} + +impl ParsedOptions { + /// Parse CLI config overrides. + pub fn from_options(options: &[String]) -> Self { + let mut config_options = Vec::new(); + + for option in options { + let parsed = match toml::from_str(option) { + Ok(parsed) => parsed, + Err(err) => { + eprintln!("Ignoring invalid CLI option '{option}': {err}"); + continue; + }, + }; + config_options.push((option.clone(), parsed)); + } + + Self { config_options } + } + + /// Apply CLI config overrides, removing broken ones. + pub fn override_config(&mut self, config: &mut UiConfig) { + let mut i = 0; + while i < self.config_options.len() { + let (option, parsed) = &self.config_options[i]; + match config.replace(parsed.clone()) { + Err(err) => { + error!( + target: LOG_TARGET_IPC_CONFIG, + "Unable to override option '{}': {}", option, err + ); + self.config_options.swap_remove(i); + }, + Ok(_) => i += 1, + } + } + } + + /// Apply CLI config overrides to a CoW config. + pub fn override_config_rc(&mut self, config: Rc<UiConfig>) -> Rc<UiConfig> { + // Skip clone without write requirement. + if self.config_options.is_empty() { + return config; + } + + // Override cloned config. + let mut config = (*config).clone(); + self.override_config(&mut config); + + Rc::new(config) + } +} + +impl Deref for ParsedOptions { + type Target = Vec<(String, Value)>; + + fn deref(&self) -> &Self::Target { + &self.config_options + } +} + +impl DerefMut for ParsedOptions { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.config_options + } +} + #[cfg(test)] mod tests { use super::*; @@ -330,10 +408,10 @@ mod tests { use std::io::Read; #[cfg(target_os = "linux")] - use clap::IntoApp; + use clap::CommandFactory; #[cfg(target_os = "linux")] use clap_complete::Shell; - use serde_yaml::mapping::Mapping; + use toml::Table; #[test] fn dynamic_title_ignoring_options_by_default() { @@ -352,7 +430,7 @@ mod tests { let title = Some(String::from("foo")); let window_identity = WindowIdentity { title, ..WindowIdentity::default() }; let new_window_options = WindowOptions { window_identity, ..WindowOptions::default() }; - let options = Options { window_options: new_window_options, ..Options::default() }; + let mut options = Options { window_options: new_window_options, ..Options::default() }; options.override_config(&mut config); assert!(!config.window.dynamic_title); @@ -371,38 +449,38 @@ mod tests { #[test] fn valid_option_as_value() { // Test with a single field. - let value = option_as_value("field=true").unwrap(); + let value: Value = toml::from_str("field=true").unwrap(); - let mut mapping = Mapping::new(); - mapping.insert(Value::String(String::from("field")), Value::Bool(true)); + let mut table = Table::new(); + table.insert(String::from("field"), Value::Boolean(true)); - assert_eq!(value, Value::Mapping(mapping)); + assert_eq!(value, Value::Table(table)); // Test with nested fields - let value = option_as_value("parent.field=true").unwrap(); + let value: Value = toml::from_str("parent.field=true").unwrap(); - let mut parent_mapping = Mapping::new(); - parent_mapping.insert(Value::String(String::from("field")), Value::Bool(true)); - let mut mapping = Mapping::new(); - mapping.insert(Value::String(String::from("parent")), Value::Mapping(parent_mapping)); + let mut parent_table = Table::new(); + parent_table.insert(String::from("field"), Value::Boolean(true)); + let mut table = Table::new(); + table.insert(String::from("parent"), Value::Table(parent_table)); - assert_eq!(value, Value::Mapping(mapping)); + assert_eq!(value, Value::Table(table)); } #[test] fn invalid_option_as_value() { - let value = option_as_value("}"); + let value = toml::from_str::<Value>("}"); assert!(value.is_err()); } #[test] fn float_option_as_value() { - let value = option_as_value("float=3.4").unwrap(); + let value: Value = toml::from_str("float=3.4").unwrap(); - let mut expected = Mapping::new(); - expected.insert(Value::String(String::from("float")), Value::Number(3.4.into())); + let mut expected = Table::new(); + expected.insert(String::from("float"), Value::Float(3.4)); - assert_eq!(value, Value::Mapping(expected)); + assert_eq!(value, Value::Table(expected)); } #[test] @@ -446,7 +524,7 @@ mod tests { #[cfg(target_os = "linux")] #[test] fn completions() { - let mut clap = Options::into_app(); + let mut clap = Options::command(); for (shell, file) in &[ (Shell::Bash, "alacritty.bash"), diff --git a/alacritty/src/clipboard.rs b/alacritty/src/clipboard.rs index dd0a8348..b3818c75 100644 --- a/alacritty/src/clipboard.rs +++ b/alacritty/src/clipboard.rs @@ -1,7 +1,5 @@ -#[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] -use std::ffi::c_void; - use log::{debug, warn}; +use raw_window_handle::RawDisplayHandle; use alacritty_terminal::term::ClipboardType; @@ -21,20 +19,15 @@ pub struct Clipboard { } impl Clipboard { - #[cfg(any(not(feature = "wayland"), target_os = "macos", windows))] - pub fn new() -> Self { - Self::default() - } - - #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] - pub unsafe fn new(display: Option<*mut c_void>) -> Self { + pub unsafe fn new(display: RawDisplayHandle) -> Self { match display { - Some(display) => { + #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] + RawDisplayHandle::Wayland(display) => { let (selection, clipboard) = - wayland_clipboard::create_clipboards_from_external(display); + wayland_clipboard::create_clipboards_from_external(display.display); Self { clipboard: Box::new(clipboard), selection: Some(Box::new(selection)) } }, - None => Self::default(), + _ => Self::default(), } } diff --git a/alacritty/src/config/bell.rs b/alacritty/src/config/bell.rs index 2516e2b3..0d6874cb 100644 --- a/alacritty/src/config/bell.rs +++ b/alacritty/src/config/bell.rs @@ -2,8 +2,8 @@ use std::time::Duration; use alacritty_config_derive::ConfigDeserialize; -use alacritty_terminal::config::Program; -use alacritty_terminal::term::color::Rgb; +use crate::config::ui_config::Program; +use crate::display::color::Rgb; #[derive(ConfigDeserialize, Clone, Debug, PartialEq, Eq)] pub struct BellConfig { @@ -23,7 +23,7 @@ pub struct BellConfig { impl Default for BellConfig { fn default() -> Self { Self { - color: Rgb { r: 255, g: 255, b: 255 }, + color: Rgb::new(255, 255, 255), animation: Default::default(), command: Default::default(), duration: Default::default(), @@ -39,7 +39,7 @@ impl BellConfig { /// `VisualBellAnimations` are modeled after a subset of CSS transitions and Robert /// Penner's Easing Functions. -#[derive(ConfigDeserialize, Clone, Copy, Debug, PartialEq, Eq)] +#[derive(ConfigDeserialize, Default, Clone, Copy, Debug, PartialEq, Eq)] pub enum BellAnimation { // CSS animation. Ease, @@ -60,11 +60,6 @@ pub enum BellAnimation { // Penner animation. EaseOutCirc, // Penner animation. + #[default] Linear, } - -impl Default for BellAnimation { - fn default() -> Self { - BellAnimation::EaseOutExpo - } -} diff --git a/alacritty/src/config/bindings.rs b/alacritty/src/config/bindings.rs index e3ee41e7..62ca03a0 100644 --- a/alacritty/src/config/bindings.rs +++ b/alacritty/src/config/bindings.rs @@ -5,17 +5,19 @@ use std::fmt::{self, Debug, Display}; use bitflags::bitflags; use serde::de::{self, Error as SerdeError, MapAccess, Unexpected, Visitor}; use serde::{Deserialize, Deserializer}; -use serde_yaml::Value as SerdeValue; -use winit::event::VirtualKeyCode::*; -use winit::event::{ModifiersState, MouseButton, VirtualKeyCode}; +use toml::Value as SerdeValue; +use winit::event::MouseButton; +use winit::keyboard::{ + Key, KeyCode, KeyLocation as WinitKeyLocation, ModifiersState, NamedKey, PhysicalKey, +}; +use winit::platform::scancode::PhysicalKeyExtScancode; use alacritty_config_derive::{ConfigDeserialize, SerdeReplace}; -use alacritty_terminal::config::Program; use alacritty_terminal::term::TermMode; use alacritty_terminal::vi_mode::ViMotion; -use crate::config::ui_config::Hint; +use crate::config::ui_config::{Hint, Program, StringVisitor}; /// Describes a state and action to take in that state. /// @@ -41,7 +43,7 @@ pub struct Binding<T> { } /// Bindings that are triggered by a keyboard key. -pub type KeyBinding = Binding<Key>; +pub type KeyBinding = Binding<BindingKey>; /// Bindings that are triggered by a mouse button. pub type MouseBinding = Binding<MouseButton>; @@ -118,7 +120,6 @@ pub enum Action { /// Store current selection into clipboard. Copy, - #[cfg(not(any(target_os = "macos", windows)))] /// Store current selection into selection buffer. CopySelection, @@ -165,7 +166,6 @@ pub enum Action { Hide, /// Hide all windows other than Alacritty on macOS. - #[cfg(target_os = "macos")] HideOtherApplications, /// Minimize the Alacritty window. @@ -180,9 +180,48 @@ pub enum Action { /// Spawn a new instance of Alacritty. SpawnNewInstance, + /// Select next tab. + SelectNextTab, + + /// Select previous tab. + SelectPreviousTab, + + /// Select the first tab. + SelectTab1, + + /// Select the second tab. + SelectTab2, + + /// Select the third tab. + SelectTab3, + + /// Select the fourth tab. + SelectTab4, + + /// Select the fifth tab. + SelectTab5, + + /// Select the sixth tab. + SelectTab6, + + /// Select the seventh tab. + SelectTab7, + + /// Select the eighth tab. + SelectTab8, + + /// Select the ninth tab. + SelectTab9, + + /// Select the last tab. + SelectLastTab, + /// Create a new Alacritty window. CreateNewWindow, + /// Create new window in a tab. + CreateNewTab, + /// Toggle fullscreen. ToggleFullscreen, @@ -190,7 +229,6 @@ pub enum Action { ToggleMaximized, /// Toggle simple fullscreen on macOS. - #[cfg(target_os = "macos")] ToggleSimpleFullscreen, /// Clear active selection. @@ -277,6 +315,18 @@ pub enum ViAction { Open, /// Centers the screen around the vi mode cursor. CenterAroundViCursor, + /// Search forward within the current line. + InlineSearchForward, + /// Search backward within the current line. + InlineSearchBackward, + /// Search forward within the current line, stopping just short of the character. + InlineSearchForwardShort, + /// Search backward within the current line, stopping just short of the character. + InlineSearchBackwardShort, + /// Jump to the next inline search match. + InlineSearchNext, + /// Jump to the previous inline search match. + InlineSearchPrevious, } /// Search mode specific actions. @@ -310,31 +360,10 @@ pub enum MouseAction { macro_rules! bindings { ( - KeyBinding; - $( - $key:ident - $(,$mods:expr)* - $(,+$mode:expr)* - $(,~$notmode:expr)* - ;$action:expr - );* - $(;)* - ) => {{ - bindings!( - KeyBinding; - $( - Key::Keycode($key) - $(,$mods)* - $(,+$mode)* - $(,~$notmode)* - ;$action - );* - ) - }}; - ( $ty:ident; $( - $key:expr + $key:tt$(::$button:ident)? + $(=>$location:expr)? $(,$mods:expr)* $(,+$mode:expr)* $(,~$notmode:expr)* @@ -353,7 +382,7 @@ macro_rules! bindings { $(_notmode.insert($notmode);)* v.push($ty { - trigger: $key, + trigger: trigger!($ty, $key$(::$button)?, $($location)?), mods: _mods, mode: _mode, notmode: _notmode, @@ -365,297 +394,136 @@ macro_rules! bindings { }}; } +macro_rules! trigger { + (KeyBinding, $key:literal, $location:expr) => {{ + BindingKey::Keycode { key: Key::Character($key.into()), location: $location } + }}; + (KeyBinding, $key:literal,) => {{ + BindingKey::Keycode { key: Key::Character($key.into()), location: KeyLocation::Any } + }}; + (KeyBinding, $key:ident, $location:expr) => {{ + BindingKey::Keycode { key: Key::Named(NamedKey::$key), location: $location } + }}; + (KeyBinding, $key:ident,) => {{ + BindingKey::Keycode { key: Key::Named(NamedKey::$key), location: KeyLocation::Any } + }}; + (MouseBinding, $base:ident::$button:ident,) => {{ + $base::$button + }}; +} + pub fn default_mouse_bindings() -> Vec<MouseBinding> { bindings!( MouseBinding; - MouseButton::Right; MouseAction::ExpandSelection; - MouseButton::Right, ModifiersState::CTRL; MouseAction::ExpandSelection; - MouseButton::Middle, ~BindingMode::VI; Action::PasteSelection; + MouseButton::Right; MouseAction::ExpandSelection; + MouseButton::Right, ModifiersState::CONTROL; MouseAction::ExpandSelection; + MouseButton::Middle, ~BindingMode::VI; Action::PasteSelection; ) } +// NOTE: key sequences which are not present here, like F5-F20, PageUp/PageDown codes are +// built on the fly in input/keyboard.rs. pub fn default_key_bindings() -> Vec<KeyBinding> { let mut bindings = bindings!( KeyBinding; - Copy; Action::Copy; + Copy; Action::Copy; Copy, +BindingMode::VI; Action::ClearSelection; Paste, ~BindingMode::VI; Action::Paste; - L, ModifiersState::CTRL; Action::ClearLogNotice; - L, ModifiersState::CTRL, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc("\x0c".into()); - Tab, ModifiersState::SHIFT, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc("\x1b[Z".into()); - Back, ModifiersState::ALT, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc("\x1b\x7f".into()); - Back, ModifiersState::SHIFT, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc("\x7f".into()); - Home, ModifiersState::SHIFT, ~BindingMode::ALT_SCREEN; Action::ScrollToTop; - End, ModifiersState::SHIFT, ~BindingMode::ALT_SCREEN; Action::ScrollToBottom; - PageUp, ModifiersState::SHIFT, ~BindingMode::ALT_SCREEN; Action::ScrollPageUp; - PageDown, ModifiersState::SHIFT, ~BindingMode::ALT_SCREEN; Action::ScrollPageDown; - Home, ModifiersState::SHIFT, +BindingMode::ALT_SCREEN, - ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[1;2H".into()); - End, ModifiersState::SHIFT, +BindingMode::ALT_SCREEN, - ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[1;2F".into()); - PageUp, ModifiersState::SHIFT, +BindingMode::ALT_SCREEN, - ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[5;2~".into()); - PageDown, ModifiersState::SHIFT, +BindingMode::ALT_SCREEN, - ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[6;2~".into()); - Home, +BindingMode::APP_CURSOR, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc("\x1bOH".into()); - Home, ~BindingMode::APP_CURSOR, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc("\x1b[H".into()); - End, +BindingMode::APP_CURSOR, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc("\x1bOF".into()); - End, ~BindingMode::APP_CURSOR, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc("\x1b[F".into()); - Up, +BindingMode::APP_CURSOR, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc("\x1bOA".into()); - Up, ~BindingMode::APP_CURSOR, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc("\x1b[A".into()); - Down, +BindingMode::APP_CURSOR, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc("\x1bOB".into()); - Down, ~BindingMode::APP_CURSOR, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc("\x1b[B".into()); - Right, +BindingMode::APP_CURSOR, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc("\x1bOC".into()); - Right, ~BindingMode::APP_CURSOR, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc("\x1b[C".into()); - Left, +BindingMode::APP_CURSOR, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc("\x1bOD".into()); - Left, ~BindingMode::APP_CURSOR, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc("\x1b[D".into()); - Back, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x7f".into()); - Insert, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[2~".into()); - Delete, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[3~".into()); - PageUp, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[5~".into()); - PageDown, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[6~".into()); - F1, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1bOP".into()); - F2, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1bOQ".into()); - F3, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1bOR".into()); - F4, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1bOS".into()); - F5, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[15~".into()); - F6, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[17~".into()); - F7, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[18~".into()); - F8, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[19~".into()); - F9, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[20~".into()); - F10, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[21~".into()); - F11, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[23~".into()); - F12, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[24~".into()); - F13, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[25~".into()); - F14, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[26~".into()); - F15, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[28~".into()); - F16, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[29~".into()); - F17, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[31~".into()); - F18, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[32~".into()); - F19, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[33~".into()); - F20, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[34~".into()); - NumpadEnter, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\n".into()); - Space, ModifiersState::SHIFT | ModifiersState::CTRL, ~BindingMode::SEARCH; - Action::ToggleViMode; - Space, ModifiersState::SHIFT | ModifiersState::CTRL, +BindingMode::VI, ~BindingMode::SEARCH; - Action::ScrollToBottom; - Escape, +BindingMode::VI, ~BindingMode::SEARCH; - Action::ClearSelection; - I, +BindingMode::VI, ~BindingMode::SEARCH; - Action::ToggleViMode; - I, +BindingMode::VI, ~BindingMode::SEARCH; - Action::ScrollToBottom; - C, ModifiersState::CTRL, +BindingMode::VI, ~BindingMode::SEARCH; - Action::ToggleViMode; - Y, ModifiersState::CTRL, +BindingMode::VI, ~BindingMode::SEARCH; - Action::ScrollLineUp; - E, ModifiersState::CTRL, +BindingMode::VI, ~BindingMode::SEARCH; - Action::ScrollLineDown; - G, +BindingMode::VI, ~BindingMode::SEARCH; - Action::ScrollToTop; - G, ModifiersState::SHIFT, +BindingMode::VI, ~BindingMode::SEARCH; - Action::ScrollToBottom; - B, ModifiersState::CTRL, +BindingMode::VI, ~BindingMode::SEARCH; - Action::ScrollPageUp; - F, ModifiersState::CTRL, +BindingMode::VI, ~BindingMode::SEARCH; - Action::ScrollPageDown; - U, ModifiersState::CTRL, +BindingMode::VI, ~BindingMode::SEARCH; - Action::ScrollHalfPageUp; - D, ModifiersState::CTRL, +BindingMode::VI, ~BindingMode::SEARCH; - Action::ScrollHalfPageDown; - Y, +BindingMode::VI, ~BindingMode::SEARCH; Action::Copy; - Y, +BindingMode::VI, ~BindingMode::SEARCH; - Action::ClearSelection; - Slash, +BindingMode::VI, ~BindingMode::SEARCH; - Action::SearchForward; - Slash, ModifiersState::SHIFT, +BindingMode::VI, ~BindingMode::SEARCH; - Action::SearchBackward; - V, +BindingMode::VI, ~BindingMode::SEARCH; - ViAction::ToggleNormalSelection; - V, ModifiersState::SHIFT, +BindingMode::VI, ~BindingMode::SEARCH; - ViAction::ToggleLineSelection; - V, ModifiersState::CTRL, +BindingMode::VI, ~BindingMode::SEARCH; - ViAction::ToggleBlockSelection; - V, ModifiersState::ALT, +BindingMode::VI, ~BindingMode::SEARCH; - ViAction::ToggleSemanticSelection; - N, +BindingMode::VI, ~BindingMode::SEARCH; - ViAction::SearchNext; - N, ModifiersState::SHIFT, +BindingMode::VI, ~BindingMode::SEARCH; - ViAction::SearchPrevious; - Return, +BindingMode::VI, ~BindingMode::SEARCH; - ViAction::Open; - Z, +BindingMode::VI, ~BindingMode::SEARCH; - ViAction::CenterAroundViCursor; - K, +BindingMode::VI, ~BindingMode::SEARCH; - ViMotion::Up; - J, +BindingMode::VI, ~BindingMode::SEARCH; - ViMotion::Down; - H, +BindingMode::VI, ~BindingMode::SEARCH; - ViMotion::Left; - L, +BindingMode::VI, ~BindingMode::SEARCH; - ViMotion::Right; - Up, +BindingMode::VI, ~BindingMode::SEARCH; - ViMotion::Up; - Down, +BindingMode::VI, ~BindingMode::SEARCH; - ViMotion::Down; - Left, +BindingMode::VI, ~BindingMode::SEARCH; - ViMotion::Left; - Right, +BindingMode::VI, ~BindingMode::SEARCH; - ViMotion::Right; - Key0, +BindingMode::VI, ~BindingMode::SEARCH; - ViMotion::First; - Key4, ModifiersState::SHIFT, +BindingMode::VI, ~BindingMode::SEARCH; - ViMotion::Last; - Key6, ModifiersState::SHIFT, +BindingMode::VI, ~BindingMode::SEARCH; - ViMotion::FirstOccupied; - H, ModifiersState::SHIFT, +BindingMode::VI, ~BindingMode::SEARCH; - ViMotion::High; - M, ModifiersState::SHIFT, +BindingMode::VI, ~BindingMode::SEARCH; - ViMotion::Middle; - L, ModifiersState::SHIFT, +BindingMode::VI, ~BindingMode::SEARCH; - ViMotion::Low; - B, +BindingMode::VI, ~BindingMode::SEARCH; - ViMotion::SemanticLeft; - W, +BindingMode::VI, ~BindingMode::SEARCH; - ViMotion::SemanticRight; - E, +BindingMode::VI, ~BindingMode::SEARCH; - ViMotion::SemanticRightEnd; - B, ModifiersState::SHIFT, +BindingMode::VI, ~BindingMode::SEARCH; - ViMotion::WordLeft; - W, ModifiersState::SHIFT, +BindingMode::VI, ~BindingMode::SEARCH; - ViMotion::WordRight; - E, ModifiersState::SHIFT, +BindingMode::VI, ~BindingMode::SEARCH; - ViMotion::WordRightEnd; - Key5, ModifiersState::SHIFT, +BindingMode::VI, ~BindingMode::SEARCH; - ViMotion::Bracket; - Return, +BindingMode::SEARCH, +BindingMode::VI; - SearchAction::SearchConfirm; - Escape, +BindingMode::SEARCH; SearchAction::SearchCancel; - C, ModifiersState::CTRL, +BindingMode::SEARCH; SearchAction::SearchCancel; - U, ModifiersState::CTRL, +BindingMode::SEARCH; SearchAction::SearchClear; - W, ModifiersState::CTRL, +BindingMode::SEARCH; SearchAction::SearchDeleteWord; - P, ModifiersState::CTRL, +BindingMode::SEARCH; SearchAction::SearchHistoryPrevious; - N, ModifiersState::CTRL, +BindingMode::SEARCH; SearchAction::SearchHistoryNext; - Up, +BindingMode::SEARCH; SearchAction::SearchHistoryPrevious; - Down, +BindingMode::SEARCH; SearchAction::SearchHistoryNext; - Return, +BindingMode::SEARCH, ~BindingMode::VI; - SearchAction::SearchFocusNext; - Return, ModifiersState::SHIFT, +BindingMode::SEARCH, ~BindingMode::VI; - SearchAction::SearchFocusPrevious; + Paste, +BindingMode::VI, +BindingMode::SEARCH; Action::Paste; + "l", ModifiersState::CONTROL; Action::ClearLogNotice; + "l", ModifiersState::CONTROL; Action::ReceiveChar; + Home, ModifiersState::SHIFT, ~BindingMode::ALT_SCREEN; Action::ScrollToTop; + End, ModifiersState::SHIFT, ~BindingMode::ALT_SCREEN; Action::ScrollToBottom; + PageUp, ModifiersState::SHIFT, ~BindingMode::ALT_SCREEN; Action::ScrollPageUp; + PageDown, ModifiersState::SHIFT, ~BindingMode::ALT_SCREEN; Action::ScrollPageDown; + // App cursor mode. + Home, +BindingMode::APP_CURSOR, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1bOH".into()); + End, +BindingMode::APP_CURSOR, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1bOF".into()); + ArrowUp, +BindingMode::APP_CURSOR, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1bOA".into()); + ArrowDown, +BindingMode::APP_CURSOR, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1bOB".into()); + ArrowRight, +BindingMode::APP_CURSOR, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1bOC".into()); + ArrowLeft, +BindingMode::APP_CURSOR, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1bOD".into()); + // Legacy keys handling which can't be automatically encoded. + F1, ~BindingMode::VI, ~BindingMode::SEARCH, ~BindingMode::REPORT_ALL_KEYS_AS_ESC, ~BindingMode::DISAMBIGUATE_ESC_CODES; Action::Esc("\x1bOP".into()); + F2, ~BindingMode::VI, ~BindingMode::SEARCH, ~BindingMode::REPORT_ALL_KEYS_AS_ESC, ~BindingMode::DISAMBIGUATE_ESC_CODES; Action::Esc("\x1bOQ".into()); + F3, ~BindingMode::VI, ~BindingMode::SEARCH, ~BindingMode::REPORT_ALL_KEYS_AS_ESC, ~BindingMode::DISAMBIGUATE_ESC_CODES; Action::Esc("\x1bOR".into()); + F4, ~BindingMode::VI, ~BindingMode::SEARCH, ~BindingMode::REPORT_ALL_KEYS_AS_ESC, ~BindingMode::DISAMBIGUATE_ESC_CODES; Action::Esc("\x1bOS".into()); + Tab, ModifiersState::SHIFT, ~BindingMode::VI, ~BindingMode::SEARCH, ~BindingMode::REPORT_ALL_KEYS_AS_ESC; Action::Esc("\x1b[Z".into()); + Tab, ModifiersState::SHIFT | ModifiersState::ALT, ~BindingMode::VI, ~BindingMode::SEARCH, ~BindingMode::REPORT_ALL_KEYS_AS_ESC; Action::Esc("\x1b\x1b[Z".into()); + Backspace, ~BindingMode::VI, ~BindingMode::SEARCH, ~BindingMode::REPORT_ALL_KEYS_AS_ESC; Action::Esc("\x7f".into()); + Backspace, ModifiersState::ALT, ~BindingMode::VI, ~BindingMode::SEARCH, ~BindingMode::REPORT_ALL_KEYS_AS_ESC; Action::Esc("\x1b\x7f".into()); + Backspace, ModifiersState::SHIFT, ~BindingMode::VI, ~BindingMode::SEARCH, ~BindingMode::REPORT_ALL_KEYS_AS_ESC; Action::Esc("\x7f".into()); + Enter => KeyLocation::Numpad, ~BindingMode::VI, ~BindingMode::SEARCH, ~BindingMode::REPORT_ALL_KEYS_AS_ESC, ~BindingMode::DISAMBIGUATE_ESC_CODES; Action::Esc("\n".into()); + // Vi mode. + Space, ModifiersState::SHIFT | ModifiersState::CONTROL, ~BindingMode::SEARCH; Action::ToggleViMode; + Space, ModifiersState::SHIFT | ModifiersState::CONTROL, +BindingMode::VI, ~BindingMode::SEARCH; Action::ScrollToBottom; + Escape, +BindingMode::VI, ~BindingMode::SEARCH; Action::ClearSelection; + "i", +BindingMode::VI, ~BindingMode::SEARCH; Action::ToggleViMode; + "i", +BindingMode::VI, ~BindingMode::SEARCH; Action::ScrollToBottom; + "c", ModifiersState::CONTROL, +BindingMode::VI, ~BindingMode::SEARCH; Action::ToggleViMode; + "y", ModifiersState::CONTROL, +BindingMode::VI, ~BindingMode::SEARCH; Action::ScrollLineUp; + "e", ModifiersState::CONTROL, +BindingMode::VI, ~BindingMode::SEARCH; Action::ScrollLineDown; + "g", +BindingMode::VI, ~BindingMode::SEARCH; Action::ScrollToTop; + "g", ModifiersState::SHIFT, +BindingMode::VI, ~BindingMode::SEARCH; Action::ScrollToBottom; + "b", ModifiersState::CONTROL, +BindingMode::VI, ~BindingMode::SEARCH; Action::ScrollPageUp; + "f", ModifiersState::CONTROL, +BindingMode::VI, ~BindingMode::SEARCH; Action::ScrollPageDown; + "u", ModifiersState::CONTROL, +BindingMode::VI, ~BindingMode::SEARCH; Action::ScrollHalfPageUp; + "d", ModifiersState::CONTROL, +BindingMode::VI, ~BindingMode::SEARCH; Action::ScrollHalfPageDown; + "y", +BindingMode::VI, ~BindingMode::SEARCH; Action::Copy; + "y", +BindingMode::VI, ~BindingMode::SEARCH; Action::ClearSelection; + "/", +BindingMode::VI, ~BindingMode::SEARCH; Action::SearchForward; + "?", ModifiersState::SHIFT, +BindingMode::VI, ~BindingMode::SEARCH; Action::SearchBackward; + "v", +BindingMode::VI, ~BindingMode::SEARCH; ViAction::ToggleNormalSelection; + "v", ModifiersState::SHIFT, +BindingMode::VI, ~BindingMode::SEARCH; ViAction::ToggleLineSelection; + "v", ModifiersState::CONTROL, +BindingMode::VI, ~BindingMode::SEARCH; ViAction::ToggleBlockSelection; + "v", ModifiersState::ALT, +BindingMode::VI, ~BindingMode::SEARCH; ViAction::ToggleSemanticSelection; + "n", +BindingMode::VI, ~BindingMode::SEARCH; ViAction::SearchNext; + "n", ModifiersState::SHIFT, +BindingMode::VI, ~BindingMode::SEARCH; ViAction::SearchPrevious; + Enter, +BindingMode::VI, ~BindingMode::SEARCH; ViAction::Open; + "z", +BindingMode::VI, ~BindingMode::SEARCH; ViAction::CenterAroundViCursor; + "f", +BindingMode::VI, ~BindingMode::SEARCH; ViAction::InlineSearchForward; + "f", ModifiersState::SHIFT, +BindingMode::VI, ~BindingMode::SEARCH; ViAction::InlineSearchBackward; + "t", +BindingMode::VI, ~BindingMode::SEARCH; ViAction::InlineSearchForwardShort; + "t", ModifiersState::SHIFT, +BindingMode::VI, ~BindingMode::SEARCH; ViAction::InlineSearchBackwardShort; + ";", +BindingMode::VI, ~BindingMode::SEARCH; ViAction::InlineSearchNext; + ",", +BindingMode::VI, ~BindingMode::SEARCH; ViAction::InlineSearchPrevious; + "k", +BindingMode::VI, ~BindingMode::SEARCH; ViMotion::Up; + "j", +BindingMode::VI, ~BindingMode::SEARCH; ViMotion::Down; + "h", +BindingMode::VI, ~BindingMode::SEARCH; ViMotion::Left; + "l", +BindingMode::VI, ~BindingMode::SEARCH; ViMotion::Right; + ArrowUp, +BindingMode::VI, ~BindingMode::SEARCH; ViMotion::Up; + ArrowDown, +BindingMode::VI, ~BindingMode::SEARCH; ViMotion::Down; + ArrowLeft, +BindingMode::VI, ~BindingMode::SEARCH; ViMotion::Left; + ArrowRight, +BindingMode::VI, ~BindingMode::SEARCH; ViMotion::Right; + "0", +BindingMode::VI, ~BindingMode::SEARCH; ViMotion::First; + "$", ModifiersState::SHIFT, +BindingMode::VI, ~BindingMode::SEARCH; ViMotion::Last; + Home, +BindingMode::VI, ~BindingMode::SEARCH; ViMotion::First; + End, +BindingMode::VI, ~BindingMode::SEARCH; ViMotion::Last; + "^", ModifiersState::SHIFT, +BindingMode::VI, ~BindingMode::SEARCH; ViMotion::FirstOccupied; + "h", ModifiersState::SHIFT, +BindingMode::VI, ~BindingMode::SEARCH; ViMotion::High; + "m", ModifiersState::SHIFT, +BindingMode::VI, ~BindingMode::SEARCH; ViMotion::Middle; + "l", ModifiersState::SHIFT, +BindingMode::VI, ~BindingMode::SEARCH; ViMotion::Low; + "b", +BindingMode::VI, ~BindingMode::SEARCH; ViMotion::SemanticLeft; + "w", +BindingMode::VI, ~BindingMode::SEARCH; ViMotion::SemanticRight; + "e", +BindingMode::VI, ~BindingMode::SEARCH; ViMotion::SemanticRightEnd; + "b", ModifiersState::SHIFT, +BindingMode::VI, ~BindingMode::SEARCH; ViMotion::WordLeft; + "w", ModifiersState::SHIFT, +BindingMode::VI, ~BindingMode::SEARCH; ViMotion::WordRight; + "e", ModifiersState::SHIFT, +BindingMode::VI, ~BindingMode::SEARCH; ViMotion::WordRightEnd; + "%", ModifiersState::SHIFT, +BindingMode::VI, ~BindingMode::SEARCH; ViMotion::Bracket; + Enter, +BindingMode::VI, +BindingMode::SEARCH; SearchAction::SearchConfirm; + // Plain search. + Escape, +BindingMode::SEARCH; SearchAction::SearchCancel; + "c", ModifiersState::CONTROL, +BindingMode::SEARCH; SearchAction::SearchCancel; + "u", ModifiersState::CONTROL, +BindingMode::SEARCH; SearchAction::SearchClear; + "w", ModifiersState::CONTROL, +BindingMode::SEARCH; SearchAction::SearchDeleteWord; + "p", ModifiersState::CONTROL, +BindingMode::SEARCH; SearchAction::SearchHistoryPrevious; + "n", ModifiersState::CONTROL, +BindingMode::SEARCH; SearchAction::SearchHistoryNext; + ArrowUp, +BindingMode::SEARCH; SearchAction::SearchHistoryPrevious; + ArrowDown, +BindingMode::SEARCH; SearchAction::SearchHistoryNext; + Enter, +BindingMode::SEARCH, ~BindingMode::VI; SearchAction::SearchFocusNext; + Enter, ModifiersState::SHIFT, +BindingMode::SEARCH, ~BindingMode::VI; SearchAction::SearchFocusPrevious; ); - // Code Modifiers - // ---------+--------------------------- - // 2 | Shift - // 3 | Alt - // 4 | Shift + Alt - // 5 | Control - // 6 | Shift + Control - // 7 | Alt + Control - // 8 | Shift + Alt + Control - // ---------+--------------------------- - // - // from: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-PC-Style-Function-Keys - let mut modifiers = vec![ - ModifiersState::SHIFT, - ModifiersState::ALT, - ModifiersState::SHIFT | ModifiersState::ALT, - ModifiersState::CTRL, - ModifiersState::SHIFT | ModifiersState::CTRL, - ModifiersState::ALT | ModifiersState::CTRL, - ModifiersState::SHIFT | ModifiersState::ALT | ModifiersState::CTRL, - ]; - - for (index, mods) in modifiers.drain(..).enumerate() { - let modifiers_code = index + 2; - bindings.extend(bindings!( - KeyBinding; - Delete, mods, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc(format!("\x1b[3;{}~", modifiers_code)); - Up, mods, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc(format!("\x1b[1;{}A", modifiers_code)); - Down, mods, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc(format!("\x1b[1;{}B", modifiers_code)); - Right, mods, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc(format!("\x1b[1;{}C", modifiers_code)); - Left, mods, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc(format!("\x1b[1;{}D", modifiers_code)); - F1, mods, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc(format!("\x1b[1;{}P", modifiers_code)); - F2, mods, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc(format!("\x1b[1;{}Q", modifiers_code)); - F3, mods, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc(format!("\x1b[1;{}R", modifiers_code)); - F4, mods, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc(format!("\x1b[1;{}S", modifiers_code)); - F5, mods, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc(format!("\x1b[15;{}~", modifiers_code)); - F6, mods, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc(format!("\x1b[17;{}~", modifiers_code)); - F7, mods, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc(format!("\x1b[18;{}~", modifiers_code)); - F8, mods, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc(format!("\x1b[19;{}~", modifiers_code)); - F9, mods, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc(format!("\x1b[20;{}~", modifiers_code)); - F10, mods, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc(format!("\x1b[21;{}~", modifiers_code)); - F11, mods, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc(format!("\x1b[23;{}~", modifiers_code)); - F12, mods, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc(format!("\x1b[24;{}~", modifiers_code)); - F13, mods, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc(format!("\x1b[25;{}~", modifiers_code)); - F14, mods, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc(format!("\x1b[26;{}~", modifiers_code)); - F15, mods, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc(format!("\x1b[28;{}~", modifiers_code)); - F16, mods, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc(format!("\x1b[29;{}~", modifiers_code)); - F17, mods, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc(format!("\x1b[31;{}~", modifiers_code)); - F18, mods, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc(format!("\x1b[32;{}~", modifiers_code)); - F19, mods, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc(format!("\x1b[33;{}~", modifiers_code)); - F20, mods, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc(format!("\x1b[34;{}~", modifiers_code)); - )); - - // We're adding the following bindings with `Shift` manually above, so skipping them here. - if modifiers_code != 2 { - bindings.extend(bindings!( - KeyBinding; - Insert, mods, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc(format!("\x1b[2;{}~", modifiers_code)); - PageUp, mods, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc(format!("\x1b[5;{}~", modifiers_code)); - PageDown, mods, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc(format!("\x1b[6;{}~", modifiers_code)); - End, mods, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc(format!("\x1b[1;{}F", modifiers_code)); - Home, mods, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc(format!("\x1b[1;{}H", modifiers_code)); - )); - } - } - bindings.extend(platform_key_bindings()); bindings @@ -665,21 +533,19 @@ pub fn default_key_bindings() -> Vec<KeyBinding> { fn common_keybindings() -> Vec<KeyBinding> { bindings!( KeyBinding; - V, ModifiersState::CTRL | ModifiersState::SHIFT, ~BindingMode::VI; Action::Paste; - C, ModifiersState::CTRL | ModifiersState::SHIFT; Action::Copy; - F, ModifiersState::CTRL | ModifiersState::SHIFT, ~BindingMode::SEARCH; - Action::SearchForward; - B, ModifiersState::CTRL | ModifiersState::SHIFT, ~BindingMode::SEARCH; - Action::SearchBackward; - C, ModifiersState::CTRL | ModifiersState::SHIFT, - +BindingMode::VI, ~BindingMode::SEARCH; Action::ClearSelection; - Insert, ModifiersState::SHIFT, ~BindingMode::VI; Action::PasteSelection; - Key0, ModifiersState::CTRL; Action::ResetFontSize; - Equals, ModifiersState::CTRL; Action::IncreaseFontSize; - Plus, ModifiersState::CTRL; Action::IncreaseFontSize; - NumpadAdd, ModifiersState::CTRL; Action::IncreaseFontSize; - Minus, ModifiersState::CTRL; Action::DecreaseFontSize; - NumpadSubtract, ModifiersState::CTRL; Action::DecreaseFontSize; + "v", ModifiersState::CONTROL | ModifiersState::SHIFT, ~BindingMode::VI; Action::Paste; + "v", ModifiersState::CONTROL | ModifiersState::SHIFT, +BindingMode::VI, +BindingMode::SEARCH; Action::Paste; + "f", ModifiersState::CONTROL | ModifiersState::SHIFT, ~BindingMode::SEARCH; Action::SearchForward; + "b", ModifiersState::CONTROL | ModifiersState::SHIFT, ~BindingMode::SEARCH; Action::SearchBackward; + Insert, ModifiersState::SHIFT, ~BindingMode::VI; Action::PasteSelection; + "c", ModifiersState::CONTROL | ModifiersState::SHIFT; Action::Copy; + "c", ModifiersState::CONTROL | ModifiersState::SHIFT, +BindingMode::VI, ~BindingMode::SEARCH; Action::ClearSelection; + "0", ModifiersState::CONTROL; Action::ResetFontSize; + "=", ModifiersState::CONTROL; Action::IncreaseFontSize; + "+", ModifiersState::CONTROL; Action::IncreaseFontSize; + "-", ModifiersState::CONTROL; Action::DecreaseFontSize; + "+" => KeyLocation::Numpad, ModifiersState::CONTROL; Action::IncreaseFontSize; + "-" => KeyLocation::Numpad, ModifiersState::CONTROL; Action::DecreaseFontSize; ) } @@ -692,7 +558,7 @@ pub fn platform_key_bindings() -> Vec<KeyBinding> { pub fn platform_key_bindings() -> Vec<KeyBinding> { let mut bindings = bindings!( KeyBinding; - Return, ModifiersState::ALT; Action::ToggleFullscreen; + Enter, ModifiersState::ALT; Action::ToggleFullscreen; ); bindings.extend(common_keybindings()); bindings @@ -702,29 +568,43 @@ pub fn platform_key_bindings() -> Vec<KeyBinding> { pub fn platform_key_bindings() -> Vec<KeyBinding> { bindings!( KeyBinding; - Key0, ModifiersState::LOGO; Action::ResetFontSize; - Equals, ModifiersState::LOGO; Action::IncreaseFontSize; - Plus, ModifiersState::LOGO; Action::IncreaseFontSize; - NumpadAdd, ModifiersState::LOGO; Action::IncreaseFontSize; - Minus, ModifiersState::LOGO; Action::DecreaseFontSize; - NumpadSubtract, ModifiersState::LOGO; Action::DecreaseFontSize; - Insert, ModifiersState::SHIFT, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc("\x1b[2;2~".into()); - K, ModifiersState::LOGO, ~BindingMode::VI, ~BindingMode::SEARCH; - Action::Esc("\x0c".into()); - K, ModifiersState::LOGO, ~BindingMode::VI, ~BindingMode::SEARCH; Action::ClearHistory; - V, ModifiersState::LOGO, ~BindingMode::VI; Action::Paste; - N, ModifiersState::LOGO; Action::CreateNewWindow; - F, ModifiersState::CTRL | ModifiersState::LOGO; Action::ToggleFullscreen; - C, ModifiersState::LOGO; Action::Copy; - C, ModifiersState::LOGO, +BindingMode::VI, ~BindingMode::SEARCH; Action::ClearSelection; - H, ModifiersState::LOGO; Action::Hide; - H, ModifiersState::LOGO | ModifiersState::ALT; Action::HideOtherApplications; - M, ModifiersState::LOGO; Action::Minimize; - Q, ModifiersState::LOGO; Action::Quit; - W, ModifiersState::LOGO; Action::Quit; - F, ModifiersState::LOGO, ~BindingMode::SEARCH; Action::SearchForward; - B, ModifiersState::LOGO, ~BindingMode::SEARCH; Action::SearchBackward; + Insert, ModifiersState::SHIFT, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[2;2~".into()); + // Tabbing api. + "t", ModifiersState::SUPER; Action::CreateNewTab; + "]", ModifiersState::SUPER | ModifiersState::SHIFT; Action::SelectNextTab; + "[", ModifiersState::SUPER | ModifiersState::SHIFT; Action::SelectPreviousTab; + Tab, ModifiersState::SUPER; Action::SelectNextTab; + Tab, ModifiersState::SUPER | ModifiersState::SHIFT; Action::SelectPreviousTab; + "1", ModifiersState::SUPER; Action::SelectTab1; + "2", ModifiersState::SUPER; Action::SelectTab2; + "3", ModifiersState::SUPER; Action::SelectTab3; + "4", ModifiersState::SUPER; Action::SelectTab4; + "5", ModifiersState::SUPER; Action::SelectTab5; + "6", ModifiersState::SUPER; Action::SelectTab6; + "7", ModifiersState::SUPER; Action::SelectTab7; + "8", ModifiersState::SUPER; Action::SelectTab8; + "9", ModifiersState::SUPER; Action::SelectLastTab; + "0", ModifiersState::SUPER; Action::ResetFontSize; + "=", ModifiersState::SUPER; Action::IncreaseFontSize; + "+", ModifiersState::SUPER; Action::IncreaseFontSize; + "-", ModifiersState::SUPER; Action::DecreaseFontSize; + "k", ModifiersState::SUPER, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x0c".into()); + "k", ModifiersState::SUPER, ~BindingMode::VI, ~BindingMode::SEARCH; Action::ClearHistory; + "v", ModifiersState::SUPER, ~BindingMode::VI; Action::Paste; + "v", ModifiersState::SUPER, +BindingMode::VI, +BindingMode::SEARCH; Action::Paste; + "n", ModifiersState::SUPER; Action::CreateNewWindow; + "f", ModifiersState::CONTROL | ModifiersState::SUPER; Action::ToggleFullscreen; + "c", ModifiersState::SUPER; Action::Copy; + "c", ModifiersState::SUPER, +BindingMode::VI, ~BindingMode::SEARCH; Action::ClearSelection; + "h", ModifiersState::SUPER; Action::Hide; + "h", ModifiersState::SUPER | ModifiersState::ALT; Action::HideOtherApplications; + "m", ModifiersState::SUPER; Action::Minimize; + "q", ModifiersState::SUPER; Action::Quit; + "w", ModifiersState::SUPER; Action::Quit; + "f", ModifiersState::SUPER, ~BindingMode::SEARCH; Action::SearchForward; + "b", ModifiersState::SUPER, ~BindingMode::SEARCH; Action::SearchBackward; + "+" => KeyLocation::Numpad, ModifiersState::SUPER; Action::IncreaseFontSize; + "-" => KeyLocation::Numpad, ModifiersState::SUPER; Action::DecreaseFontSize; ) } @@ -734,23 +614,124 @@ pub fn platform_key_bindings() -> Vec<KeyBinding> { vec![] } -#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] -pub enum Key { - Scancode(u32), - Keycode(VirtualKeyCode), +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum BindingKey { + Scancode(PhysicalKey), + Keycode { key: Key, location: KeyLocation }, +} + +/// Key location for matching bindings. +#[derive(Debug, Clone, Copy, Eq)] +pub enum KeyLocation { + /// The key is in its standard position. + Standard, + /// The key is on the numeric pad. + Numpad, + /// The key could be anywhere on the keyboard. + Any, +} + +impl From<WinitKeyLocation> for KeyLocation { + fn from(value: WinitKeyLocation) -> Self { + match value { + WinitKeyLocation::Standard => KeyLocation::Standard, + WinitKeyLocation::Left => KeyLocation::Any, + WinitKeyLocation::Right => KeyLocation::Any, + WinitKeyLocation::Numpad => KeyLocation::Numpad, + } + } +} + +impl PartialEq for KeyLocation { + fn eq(&self, other: &Self) -> bool { + matches!( + (self, other), + (_, KeyLocation::Any) + | (KeyLocation::Any, _) + | (KeyLocation::Standard, KeyLocation::Standard) + | (KeyLocation::Numpad, KeyLocation::Numpad) + ) + } } -impl<'a> Deserialize<'a> for Key { +impl<'a> Deserialize<'a> for BindingKey { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'a>, { let value = SerdeValue::deserialize(deserializer)?; match u32::deserialize(value.clone()) { - Ok(scancode) => Ok(Key::Scancode(scancode)), + Ok(scancode) => Ok(BindingKey::Scancode(PhysicalKey::from_scancode(scancode))), Err(_) => { - let keycode = VirtualKeyCode::deserialize(value).map_err(D::Error::custom)?; - Ok(Key::Keycode(keycode)) + let keycode = String::deserialize(value.clone()).map_err(D::Error::custom)?; + let (key, location) = if keycode.chars().count() == 1 { + (Key::Character(keycode.to_lowercase().into()), KeyLocation::Any) + } else { + // Translate legacy winit codes into their modern counterparts. + match keycode.as_str() { + "Back" => (Key::Named(NamedKey::Backspace), KeyLocation::Any), + "Up" => (Key::Named(NamedKey::ArrowUp), KeyLocation::Any), + "Down" => (Key::Named(NamedKey::ArrowDown), KeyLocation::Any), + "Left" => (Key::Named(NamedKey::ArrowLeft), KeyLocation::Any), + "Right" => (Key::Named(NamedKey::ArrowRight), KeyLocation::Any), + "At" => (Key::Character("@".into()), KeyLocation::Any), + "Colon" => (Key::Character(":".into()), KeyLocation::Any), + "Period" => (Key::Character(".".into()), KeyLocation::Any), + "LBracket" => (Key::Character("[".into()), KeyLocation::Any), + "RBracket" => (Key::Character("]".into()), KeyLocation::Any), + "Semicolon" => (Key::Character(";".into()), KeyLocation::Any), + "Backslash" => (Key::Character("\\".into()), KeyLocation::Any), + + // The keys which has alternative on numeric pad. + "Enter" => (Key::Named(NamedKey::Enter), KeyLocation::Standard), + "Return" => (Key::Named(NamedKey::Enter), KeyLocation::Standard), + "Plus" => (Key::Character("+".into()), KeyLocation::Standard), + "Comma" => (Key::Character(",".into()), KeyLocation::Standard), + "Slash" => (Key::Character("/".into()), KeyLocation::Standard), + "Equals" => (Key::Character("=".into()), KeyLocation::Standard), + "Minus" => (Key::Character("-".into()), KeyLocation::Standard), + "Asterisk" => (Key::Character("*".into()), KeyLocation::Standard), + "Key1" => (Key::Character("1".into()), KeyLocation::Standard), + "Key2" => (Key::Character("2".into()), KeyLocation::Standard), + "Key3" => (Key::Character("3".into()), KeyLocation::Standard), + "Key4" => (Key::Character("4".into()), KeyLocation::Standard), + "Key5" => (Key::Character("5".into()), KeyLocation::Standard), + "Key6" => (Key::Character("6".into()), KeyLocation::Standard), + "Key7" => (Key::Character("7".into()), KeyLocation::Standard), + "Key8" => (Key::Character("8".into()), KeyLocation::Standard), + "Key9" => (Key::Character("9".into()), KeyLocation::Standard), + "Key0" => (Key::Character("0".into()), KeyLocation::Standard), + + // Special case numpad. + "NumpadEnter" => (Key::Named(NamedKey::Enter), KeyLocation::Numpad), + "NumpadAdd" => (Key::Character("+".into()), KeyLocation::Numpad), + "NumpadComma" => (Key::Character(",".into()), KeyLocation::Numpad), + "NumpadDecimal" => (Key::Character(".".into()), KeyLocation::Numpad), + "NumpadDivide" => (Key::Character("/".into()), KeyLocation::Numpad), + "NumpadEquals" => (Key::Character("=".into()), KeyLocation::Numpad), + "NumpadSubtract" => (Key::Character("-".into()), KeyLocation::Numpad), + "NumpadMultiply" => (Key::Character("*".into()), KeyLocation::Numpad), + "Numpad1" => (Key::Character("1".into()), KeyLocation::Numpad), + "Numpad2" => (Key::Character("2".into()), KeyLocation::Numpad), + "Numpad3" => (Key::Character("3".into()), KeyLocation::Numpad), + "Numpad4" => (Key::Character("4".into()), KeyLocation::Numpad), + "Numpad5" => (Key::Character("5".into()), KeyLocation::Numpad), + "Numpad6" => (Key::Character("6".into()), KeyLocation::Numpad), + "Numpad7" => (Key::Character("7".into()), KeyLocation::Numpad), + "Numpad8" => (Key::Character("8".into()), KeyLocation::Numpad), + "Numpad9" => (Key::Character("9".into()), KeyLocation::Numpad), + "Numpad0" => (Key::Character("0".into()), KeyLocation::Numpad), + _ if keycode.starts_with("Dead") => { + (Key::deserialize(value).map_err(D::Error::custom)?, KeyLocation::Any) + }, + _ => ( + Key::Named(NamedKey::deserialize(value).map_err(D::Error::custom)?), + KeyLocation::Any, + ), + } + }; + + Ok(BindingKey::Keycode { key, location }) }, } } @@ -764,12 +745,15 @@ pub struct ModeWrapper { bitflags! { /// Modes available for key bindings. + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct BindingMode: u8 { - const APP_CURSOR = 0b0000_0001; - const APP_KEYPAD = 0b0000_0010; - const ALT_SCREEN = 0b0000_0100; - const VI = 0b0000_1000; - const SEARCH = 0b0001_0000; + const APP_CURSOR = 0b0000_0001; + const APP_KEYPAD = 0b0000_0010; + const ALT_SCREEN = 0b0000_0100; + const VI = 0b0000_1000; + const SEARCH = 0b0001_0000; + const DISAMBIGUATE_ESC_CODES = 0b0010_0000; + const REPORT_ALL_KEYS_AS_ESC = 0b0100_0000; } } @@ -781,6 +765,14 @@ impl BindingMode { binding_mode.set(BindingMode::ALT_SCREEN, mode.contains(TermMode::ALT_SCREEN)); binding_mode.set(BindingMode::VI, mode.contains(TermMode::VI)); binding_mode.set(BindingMode::SEARCH, search); + binding_mode.set( + BindingMode::DISAMBIGUATE_ESC_CODES, + mode.contains(TermMode::DISAMBIGUATE_ESC_CODES), + ); + binding_mode.set( + BindingMode::REPORT_ALL_KEYS_AS_ESC, + mode.contains(TermMode::REPORT_ALL_KEYS_AS_ESC), + ); binding_mode } } @@ -856,7 +848,17 @@ impl<'a> Deserialize<'a> for MouseButtonWrapper { type Value = MouseButtonWrapper; fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("Left, Right, Middle, or a number from 0 to 65536") + f.write_str("Left, Right, Middle, Back, Forward, or a number from 0 to 65536") + } + + fn visit_i64<E>(self, value: i64) -> Result<MouseButtonWrapper, E> + where + E: de::Error, + { + match value { + 0..=65536 => Ok(MouseButtonWrapper(MouseButton::Other(value as u16))), + _ => Err(E::invalid_value(Unexpected::Signed(value), &self)), + } } fn visit_u64<E>(self, value: u64) -> Result<MouseButtonWrapper, E> @@ -877,6 +879,8 @@ impl<'a> Deserialize<'a> for MouseButtonWrapper { "Left" => Ok(MouseButtonWrapper(MouseButton::Left)), "Right" => Ok(MouseButtonWrapper(MouseButton::Right)), "Middle" => Ok(MouseButtonWrapper(MouseButton::Middle)), + "Back" => Ok(MouseButtonWrapper(MouseButton::Back)), + "Forward" => Ok(MouseButtonWrapper(MouseButton::Forward)), _ => Err(E::invalid_value(Unexpected::Str(value), &self)), } } @@ -890,7 +894,7 @@ impl<'a> Deserialize<'a> for MouseButtonWrapper { /// `KeyBinding` or `MouseBinding`. #[derive(PartialEq, Eq)] struct RawBinding { - key: Option<Key>, + key: Option<BindingKey>, mouse: Option<MouseButton>, mods: ModifiersState, mode: BindingMode, @@ -993,7 +997,7 @@ impl<'a> Deserialize<'a> for RawBinding { V: MapAccess<'a>, { let mut mods: Option<ModifiersState> = None; - let mut key: Option<Key> = None; + let mut key: Option<BindingKey> = None; let mut chars: Option<String> = None; let mut action: Option<Action> = None; let mut mode: Option<BindingMode> = None; @@ -1010,19 +1014,26 @@ impl<'a> Deserialize<'a> for RawBinding { return Err(<V::Error as Error>::duplicate_field("key")); } - let val = map.next_value::<SerdeValue>()?; - if val.is_u64() { - let scancode = val.as_u64().unwrap(); - if scancode > u64::from(u32::MAX) { - return Err(<V::Error as Error>::custom(format!( - "Invalid key binding, scancode too big: {}", - scancode - ))); - } - key = Some(Key::Scancode(scancode as u32)); - } else { - let k = Key::deserialize(val).map_err(V::Error::custom)?; - key = Some(k); + let value = map.next_value::<SerdeValue>()?; + match value.as_integer() { + Some(scancode) => match u32::try_from(scancode) { + Ok(scancode) => { + key = Some(BindingKey::Scancode(KeyCode::from_scancode( + scancode, + ))) + }, + Err(_) => { + return Err(<V::Error as Error>::custom(format!( + "Invalid key binding, scancode is too big: {}", + scancode + ))); + }, + }, + None => { + key = Some( + BindingKey::deserialize(value).map_err(V::Error::custom)?, + ) + }, } }, Field::Mods => { @@ -1050,8 +1061,9 @@ impl<'a> Deserialize<'a> for RawBinding { action = if let Ok(vi_action) = ViAction::deserialize(value.clone()) { Some(vi_action.into()) - } else if let Ok(vi_motion) = ViMotion::deserialize(value.clone()) { - Some(vi_motion.into()) + } else if let Ok(vi_motion) = SerdeViMotion::deserialize(value.clone()) + { + Some(vi_motion.0.into()) } else if let Ok(search_action) = SearchAction::deserialize(value.clone()) { @@ -1065,15 +1077,6 @@ impl<'a> Deserialize<'a> for RawBinding { Err(err) => { let value = match value { SerdeValue::String(string) => string, - SerdeValue::Mapping(map) if map.len() == 1 => { - match map.into_iter().next() { - Some(( - SerdeValue::String(string), - SerdeValue::Null, - )) => string, - _ => return Err(err), - } - }, _ => return Err(err), }; return Err(V::Error::custom(format!( @@ -1092,7 +1095,7 @@ impl<'a> Deserialize<'a> for RawBinding { chars = Some(map.next_value()?); }, Field::Mouse => { - if chars.is_some() { + if mouse.is_some() { return Err(<V::Error as Error>::duplicate_field("mouse")); } @@ -1114,26 +1117,8 @@ impl<'a> Deserialize<'a> for RawBinding { let action = match (action, chars, command) { (Some(action @ Action::ViMotion(_)), None, None) - | (Some(action @ Action::Vi(_)), None, None) => { - if !mode.intersects(BindingMode::VI) || not_mode.intersects(BindingMode::VI) - { - return Err(V::Error::custom(format!( - "action `{}` is only available in vi mode, try adding `mode: Vi`", - action, - ))); - } - action - }, - (Some(action @ Action::Search(_)), None, None) => { - if !mode.intersects(BindingMode::SEARCH) { - return Err(V::Error::custom(format!( - "action `{}` is only available in search mode, try adding `mode: \ - Search`", - action, - ))); - } - action - }, + | (Some(action @ Action::Vi(_)), None, None) => action, + (Some(action @ Action::Search(_)), None, None) => action, (Some(action @ Action::Mouse(_)), None, None) => { if mouse.is_none() { return Err(V::Error::custom(format!( @@ -1187,6 +1172,21 @@ impl<'a> Deserialize<'a> for KeyBinding { } } +#[derive(SerdeReplace, Debug, Copy, Clone, Eq, PartialEq)] +pub struct SerdeViMotion(ViMotion); + +impl<'de> Deserialize<'de> for SerdeViMotion { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + let value = deserializer.deserialize_str(StringVisitor)?; + ViMotion::deserialize(SerdeValue::String(value)) + .map(SerdeViMotion) + .map_err(de::Error::custom) + } +} + /// Newtype for implementing deserialize on winit Mods. /// /// Our deserialize impl wouldn't be covered by a derive(Deserialize); see the @@ -1221,10 +1221,10 @@ impl<'a> de::Deserialize<'a> for ModsWrapper { let mut res = ModifiersState::empty(); for modifier in value.split('|') { match modifier.trim().to_lowercase().as_str() { - "command" | "super" => res.insert(ModifiersState::LOGO), + "command" | "super" => res.insert(ModifiersState::SUPER), "shift" => res.insert(ModifiersState::SHIFT), "alt" | "option" => res.insert(ModifiersState::ALT), - "control" => res.insert(ModifiersState::CTRL), + "control" => res.insert(ModifiersState::CONTROL), "none" => (), _ => return Err(E::invalid_value(Unexpected::Str(modifier), &self)), } @@ -1242,7 +1242,7 @@ impl<'a> de::Deserialize<'a> for ModsWrapper { mod tests { use super::*; - use winit::event::ModifiersState; + use winit::keyboard::ModifiersState; type MockBinding = Binding<usize>; @@ -1405,7 +1405,7 @@ mod tests { #[test] fn binding_trigger_mods() { let binding = MockBinding { - mods: ModifiersState::ALT | ModifiersState::LOGO, + mods: ModifiersState::ALT | ModifiersState::SUPER, ..MockBinding::default() }; diff --git a/alacritty/src/config/color.rs b/alacritty/src/config/color.rs index 5028347c..995d0499 100644 --- a/alacritty/src/config/color.rs +++ b/alacritty/src/config/color.rs @@ -2,7 +2,8 @@ use serde::de::Error as SerdeError; use serde::{Deserialize, Deserializer}; use alacritty_config_derive::ConfigDeserialize; -use alacritty_terminal::term::color::{CellRgb, Rgb}; + +use crate::display::color::{CellRgb, Rgb}; #[derive(ConfigDeserialize, Clone, Debug, Default, PartialEq, Eq)] pub struct Colors { @@ -18,16 +19,17 @@ pub struct Colors { pub line_indicator: LineIndicatorColors, pub hints: HintColors, pub transparent_background_colors: bool, + pub draw_bold_text_with_bright_colors: bool, footer_bar: BarColors, } impl Colors { pub fn footer_bar_foreground(&self) -> Rgb { - self.search.bar.foreground.or(self.footer_bar.foreground).unwrap_or(self.primary.background) + self.footer_bar.foreground.unwrap_or(self.primary.background) } pub fn footer_bar_background(&self) -> Rgb { - self.search.bar.background.or(self.footer_bar.background).unwrap_or(self.primary.foreground) + self.footer_bar.background.unwrap_or(self.primary.foreground) } } @@ -52,8 +54,8 @@ pub struct HintStartColors { impl Default for HintStartColors { fn default() -> Self { Self { - foreground: CellRgb::Rgb(Rgb { r: 0x1d, g: 0x1f, b: 0x21 }), - background: CellRgb::Rgb(Rgb { r: 0xe9, g: 0xff, b: 0x5e }), + foreground: CellRgb::Rgb(Rgb::new(0x18, 0x18, 0x18)), + background: CellRgb::Rgb(Rgb::new(0xf4, 0xbf, 0x75)), } } } @@ -67,13 +69,14 @@ pub struct HintEndColors { impl Default for HintEndColors { fn default() -> Self { Self { - foreground: CellRgb::Rgb(Rgb { r: 0xe9, g: 0xff, b: 0x5e }), - background: CellRgb::Rgb(Rgb { r: 0x1d, g: 0x1f, b: 0x21 }), + foreground: CellRgb::Rgb(Rgb::new(0x18, 0x18, 0x18)), + background: CellRgb::Rgb(Rgb::new(0xac, 0x42, 0x42)), } } } #[derive(Deserialize, Copy, Clone, Default, Debug, PartialEq, Eq)] +#[serde(deny_unknown_fields)] pub struct IndexedColor { pub color: Rgb, @@ -126,8 +129,6 @@ impl Default for InvertedCellColors { pub struct SearchColors { pub focused_match: FocusedMatchColors, pub matches: MatchColors, - #[config(deprecated = "use `colors.footer_bar` instead")] - bar: BarColors, } #[derive(ConfigDeserialize, Debug, Copy, Clone, PartialEq, Eq)] @@ -139,8 +140,8 @@ pub struct FocusedMatchColors { impl Default for FocusedMatchColors { fn default() -> Self { Self { - background: CellRgb::Rgb(Rgb { r: 0x00, g: 0x00, b: 0x00 }), - foreground: CellRgb::Rgb(Rgb { r: 0xff, g: 0xff, b: 0xff }), + background: CellRgb::Rgb(Rgb::new(0xf4, 0xbf, 0x75)), + foreground: CellRgb::Rgb(Rgb::new(0x18, 0x18, 0x18)), } } } @@ -154,8 +155,8 @@ pub struct MatchColors { 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 }), + background: CellRgb::Rgb(Rgb::new(0xac, 0x42, 0x42)), + foreground: CellRgb::Rgb(Rgb::new(0x18, 0x18, 0x18)), } } } @@ -177,8 +178,8 @@ pub struct PrimaryColors { impl Default for PrimaryColors { fn default() -> Self { PrimaryColors { - background: Rgb { r: 0x1d, g: 0x1f, b: 0x21 }, - foreground: Rgb { r: 0xc5, g: 0xc8, b: 0xc6 }, + background: Rgb::new(0x18, 0x18, 0x18), + foreground: Rgb::new(0xd8, 0xd8, 0xd8), bright_foreground: Default::default(), dim_foreground: Default::default(), } @@ -200,14 +201,14 @@ pub struct NormalColors { 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 }, + black: Rgb::new(0x18, 0x18, 0x18), + red: Rgb::new(0xac, 0x42, 0x42), + green: Rgb::new(0x90, 0xa9, 0x59), + yellow: Rgb::new(0xf4, 0xbf, 0x75), + blue: Rgb::new(0x6a, 0x9f, 0xb5), + magenta: Rgb::new(0xaa, 0x75, 0x9f), + cyan: Rgb::new(0x75, 0xb5, 0xaa), + white: Rgb::new(0xd8, 0xd8, 0xd8), } } } @@ -226,15 +227,18 @@ pub struct BrightColors { impl Default for BrightColors { fn default() -> Self { + // Generated with oklab by multiplying brightness by 1.12 and then adjusting numbers + // to make them look "nicer". Yellow color was generated the same way, however the first + // srgb representable color was picked. 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 }, + black: Rgb::new(0x6b, 0x6b, 0x6b), + red: Rgb::new(0xc5, 0x55, 0x55), + green: Rgb::new(0xaa, 0xc4, 0x74), + yellow: Rgb::new(0xfe, 0xca, 0x88), + blue: Rgb::new(0x82, 0xb8, 0xc8), + magenta: Rgb::new(0xc2, 0x8c, 0xb8), + cyan: Rgb::new(0x93, 0xd3, 0xc3), + white: Rgb::new(0xf8, 0xf8, 0xf8), } } } @@ -253,15 +257,16 @@ pub struct DimColors { impl Default for DimColors { fn default() -> Self { + // Generated with builtin alacritty's color dimming function. 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 }, + black: Rgb::new(0x0f, 0x0f, 0x0f), + red: Rgb::new(0x71, 0x2b, 0x2b), + green: Rgb::new(0x5f, 0x6f, 0x3a), + yellow: Rgb::new(0xa1, 0x7e, 0x4d), + blue: Rgb::new(0x45, 0x68, 0x77), + magenta: Rgb::new(0x70, 0x4d, 0x68), + cyan: Rgb::new(0x4d, 0x77, 0x70), + white: Rgb::new(0x8e, 0x8e, 0x8e), } } } diff --git a/alacritty/src/config/cursor.rs b/alacritty/src/config/cursor.rs new file mode 100644 index 00000000..dc205b4b --- /dev/null +++ b/alacritty/src/config/cursor.rs @@ -0,0 +1,156 @@ +use std::cmp; +use std::time::Duration; + +use serde::Deserialize; + +use alacritty_config_derive::{ConfigDeserialize, SerdeReplace}; +use alacritty_terminal::vte::ansi::{CursorShape as VteCursorShape, CursorStyle as VteCursorStyle}; + +use crate::config::ui_config::Percentage; + +/// The minimum blink interval value in milliseconds. +const MIN_BLINK_INTERVAL: u64 = 10; + +/// The minimum number of blinks before pausing. +const MIN_BLINK_CYCLES_BEFORE_PAUSE: u64 = 1; + +#[derive(ConfigDeserialize, Copy, Clone, Debug, PartialEq)] +pub struct Cursor { + pub style: ConfigCursorStyle, + pub vi_mode_style: Option<ConfigCursorStyle>, + pub unfocused_hollow: bool, + + thickness: Percentage, + blink_interval: u64, + blink_timeout: u8, +} + +impl Default for Cursor { + fn default() -> Self { + Self { + thickness: Percentage::new(0.15), + unfocused_hollow: true, + blink_interval: 750, + blink_timeout: 5, + style: Default::default(), + vi_mode_style: Default::default(), + } + } +} + +impl Cursor { + #[inline] + pub fn thickness(self) -> f32 { + self.thickness.as_f32() + } + + #[inline] + pub fn style(self) -> VteCursorStyle { + self.style.into() + } + + #[inline] + pub fn vi_mode_style(self) -> Option<VteCursorStyle> { + self.vi_mode_style.map(Into::into) + } + + #[inline] + pub fn blink_interval(self) -> u64 { + cmp::max(self.blink_interval, MIN_BLINK_INTERVAL) + } + + #[inline] + pub fn blink_timeout(self) -> Duration { + if self.blink_timeout == 0 { + Duration::ZERO + } else { + cmp::max( + // Show/hide is what we consider a cycle, so multiply by `2`. + Duration::from_millis(self.blink_interval * 2 * MIN_BLINK_CYCLES_BEFORE_PAUSE), + Duration::from_secs(self.blink_timeout as u64), + ) + } + } +} + +#[derive(SerdeReplace, Deserialize, Debug, Copy, Clone, PartialEq, Eq)] +#[serde(untagged, deny_unknown_fields)] +pub enum ConfigCursorStyle { + Shape(CursorShape), + WithBlinking { + #[serde(default)] + shape: CursorShape, + #[serde(default)] + blinking: CursorBlinking, + }, +} + +impl Default for ConfigCursorStyle { + fn default() -> Self { + Self::Shape(CursorShape::default()) + } +} + +impl ConfigCursorStyle { + /// Check if blinking is force enabled/disabled. + pub fn blinking_override(&self) -> Option<bool> { + match self { + Self::Shape(_) => None, + Self::WithBlinking { blinking, .. } => blinking.blinking_override(), + } + } +} + +impl From<ConfigCursorStyle> for VteCursorStyle { + fn from(config_style: ConfigCursorStyle) -> Self { + match config_style { + ConfigCursorStyle::Shape(shape) => Self { shape: shape.into(), blinking: false }, + ConfigCursorStyle::WithBlinking { shape, blinking } => { + Self { shape: shape.into(), blinking: blinking.into() } + }, + } + } +} + +#[derive(ConfigDeserialize, Default, Debug, Copy, Clone, PartialEq, Eq)] +pub enum CursorBlinking { + Never, + #[default] + Off, + On, + Always, +} + +impl CursorBlinking { + fn blinking_override(&self) -> Option<bool> { + match self { + Self::Never => Some(false), + Self::Off | Self::On => None, + Self::Always => Some(true), + } + } +} + +impl From<CursorBlinking> for bool { + fn from(blinking: CursorBlinking) -> bool { + blinking == CursorBlinking::On || blinking == CursorBlinking::Always + } +} + +#[derive(ConfigDeserialize, Debug, Default, Eq, PartialEq, Copy, Clone, Hash)] +pub enum CursorShape { + #[default] + Block, + Underline, + Beam, +} + +impl From<CursorShape> for VteCursorShape { + fn from(value: CursorShape) -> Self { + match value { + CursorShape::Block => VteCursorShape::Block, + CursorShape::Underline => VteCursorShape::Underline, + CursorShape::Beam => VteCursorShape::Beam, + } + } +} diff --git a/alacritty/src/config/debug.rs b/alacritty/src/config/debug.rs index fe0c78f1..ffd396d5 100644 --- a/alacritty/src/config/debug.rs +++ b/alacritty/src/config/debug.rs @@ -1,7 +1,5 @@ use log::LevelFilter; -use serde::Deserialize; - use alacritty_config_derive::ConfigDeserialize; /// Debugging options. @@ -23,6 +21,9 @@ pub struct Debug { /// The renderer alacritty should be using. pub renderer: Option<RendererPreference>, + /// Use EGL as display API if the current platform allows it. + pub prefer_egl: bool, + /// Record ref test. #[config(skip)] pub ref_test: bool, @@ -38,22 +39,20 @@ impl Default for Debug { highlight_damage: Default::default(), ref_test: Default::default(), renderer: Default::default(), + prefer_egl: Default::default(), } } } /// The renderer configuration options. -#[derive(Deserialize, Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[derive(ConfigDeserialize, Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum RendererPreference { /// OpenGL 3.3 renderer. - #[serde(rename = "glsl3")] Glsl3, /// GLES 2 renderer, with optional extensions like dual source blending. - #[serde(rename = "gles2")] Gles2, /// Pure GLES 2 renderer. - #[serde(rename = "gles2_pure")] Gles2Pure, } diff --git a/alacritty/src/config/font.rs b/alacritty/src/config/font.rs index 9c431b15..061c0f42 100644 --- a/alacritty/src/config/font.rs +++ b/alacritty/src/config/font.rs @@ -134,7 +134,7 @@ struct Size(FontSize); impl Default for Size { fn default() -> Self { - Self(FontSize::new(11.)) + Self(FontSize::new(11.25)) } } @@ -148,14 +148,14 @@ impl<'de> Deserialize<'de> for Size { type Value = Size; fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("f64 or u64") + f.write_str("f64 or i64") } fn visit_f64<E: de::Error>(self, value: f64) -> Result<Self::Value, E> { Ok(Size(FontSize::new(value as f32))) } - fn visit_u64<E: de::Error>(self, value: u64) -> Result<Self::Value, E> { + fn visit_i64<E: de::Error>(self, value: i64) -> Result<Self::Value, E> { Ok(Size(FontSize::new(value as f32))) } } diff --git a/alacritty/src/config/mod.rs b/alacritty/src/config/mod.rs index df49db31..a77ed770 100644 --- a/alacritty/src/config/mod.rs +++ b/alacritty/src/config/mod.rs @@ -1,20 +1,25 @@ use std::fmt::{self, Display, Formatter}; use std::path::{Path, PathBuf}; +use std::result::Result as StdResult; use std::{env, fs, io}; -use log::{debug, error, info}; +use log::{debug, error, info, warn}; use serde::Deserialize; -use serde_yaml::mapping::Mapping; -use serde_yaml::Value; - -use alacritty_terminal::config::LOG_TARGET_CONFIG; +use serde_yaml::Error as YamlError; +use toml::de::Error as TomlError; +use toml::ser::Error as TomlSeError; +use toml::{Table, Value}; pub mod bell; pub mod color; +pub mod cursor; pub mod debug; pub mod font; pub mod monitor; +pub mod scrolling; +pub mod selection; pub mod serde_utils; +pub mod terminal; pub mod ui_config; pub mod window; @@ -22,15 +27,16 @@ mod bindings; mod mouse; use crate::cli::Options; +#[cfg(test)] +pub use crate::config::bindings::Binding; pub use crate::config::bindings::{ - Action, Binding, BindingMode, Key, MouseAction, SearchAction, ViAction, + Action, BindingKey, BindingMode, MouseAction, SearchAction, ViAction, }; -#[cfg(test)] -pub use crate::config::mouse::{ClickHandler, Mouse}; pub use crate::config::ui_config::UiConfig; +use crate::logging::LOG_TARGET_CONFIG; /// Maximum number of depth for the configuration file imports. -const IMPORT_RECURSION_LIMIT: usize = 5; +pub const IMPORT_RECURSION_LIMIT: usize = 5; /// Result from config loading. pub type Result<T> = std::result::Result<T, Error>; @@ -47,8 +53,14 @@ pub enum Error { /// io error reading file. Io(io::Error), - /// Not valid yaml or missing parameters. - Yaml(serde_yaml::Error), + /// Invalid toml. + Toml(TomlError), + + /// Failed toml serialization. + TomlSe(TomlSeError), + + /// Invalid yaml. + Yaml(YamlError), } impl std::error::Error for Error { @@ -57,6 +69,8 @@ impl std::error::Error for Error { Error::NotFound => None, Error::ReadingEnvHome(err) => err.source(), Error::Io(err) => err.source(), + Error::Toml(err) => err.source(), + Error::TomlSe(err) => err.source(), Error::Yaml(err) => err.source(), } } @@ -70,6 +84,8 @@ impl Display for Error { write!(f, "Unable to read $HOME environment variable: {}", err) }, Error::Io(err) => write!(f, "Error reading config file: {}", err), + Error::Toml(err) => write!(f, "Config error: {}", err), + Error::TomlSe(err) => write!(f, "Yaml conversion error: {}", err), Error::Yaml(err) => write!(f, "Config error: {}", err), } } @@ -91,16 +107,31 @@ impl From<io::Error> for Error { } } -impl From<serde_yaml::Error> for Error { - fn from(val: serde_yaml::Error) -> Self { +impl From<TomlError> for Error { + fn from(val: TomlError) -> Self { + Error::Toml(val) + } +} + +impl From<TomlSeError> for Error { + fn from(val: TomlSeError) -> Self { + Error::TomlSe(val) + } +} + +impl From<YamlError> for Error { + fn from(val: YamlError) -> Self { Error::Yaml(val) } } /// Load the configuration file. -pub fn load(options: &Options) -> UiConfig { - let config_options = options.config_options.clone(); - let config_path = options.config_file.clone().or_else(installed_config); +pub fn load(options: &mut Options) -> UiConfig { + let config_path = options + .config_file + .clone() + .or_else(|| installed_config("toml")) + .or_else(|| installed_config("yml")); // Load the config using the following fallback behavior: // - Config path + CLI overrides @@ -108,9 +139,9 @@ pub fn load(options: &Options) -> UiConfig { // - Default let mut config = config_path .as_ref() - .and_then(|config_path| load_from(config_path, config_options.clone()).ok()) + .and_then(|config_path| load_from(config_path).ok()) .unwrap_or_else(|| { - let mut config = UiConfig::deserialize(config_options).unwrap_or_default(); + let mut config = UiConfig::default(); match config_path { Some(config_path) => config.config_paths.push(config_path), None => info!(target: LOG_TARGET_CONFIG, "No config file found; using default"), @@ -124,12 +155,11 @@ pub fn load(options: &Options) -> UiConfig { } /// Attempt to reload the configuration file. -pub fn reload(config_path: &Path, options: &Options) -> Result<UiConfig> { +pub fn reload(config_path: &Path, options: &mut Options) -> Result<UiConfig> { debug!("Reloading configuration file: {:?}", config_path); // 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)?; after_loading(&mut config, options); @@ -137,7 +167,7 @@ pub fn reload(config_path: &Path, options: &Options) -> Result<UiConfig> { } /// Modifications after the `UiConfig` object is created. -fn after_loading(config: &mut UiConfig, options: &Options) { +fn after_loading(config: &mut UiConfig, options: &mut Options) { // Override config with CLI options. options.override_config(config); @@ -146,8 +176,8 @@ fn after_loading(config: &mut UiConfig, options: &Options) { } /// Load configuration file and log errors. -fn load_from(path: &Path, cli_config: Value) -> Result<UiConfig> { - match read_config(path, cli_config) { +fn load_from(path: &Path) -> Result<UiConfig> { + match read_config(path) { Ok(config) => Ok(config), Err(err) => { error!(target: LOG_TARGET_CONFIG, "Unable to load config {:?}: {}", path, err); @@ -157,12 +187,9 @@ fn load_from(path: &Path, cli_config: Value) -> Result<UiConfig> { } /// Deserialize configuration file from path. -fn read_config(path: &Path, cli_config: Value) -> Result<UiConfig> { +fn read_config(path: &Path) -> Result<UiConfig> { let mut config_paths = Vec::new(); - let mut config_value = parse_config(path, &mut config_paths, IMPORT_RECURSION_LIMIT)?; - - // Override config with CLI options. - config_value = serde_utils::merge(config_value, cli_config); + let config_value = parse_config(path, &mut config_paths, IMPORT_RECURSION_LIMIT)?; // Deserialize to concrete type. let mut config = UiConfig::deserialize(config_value)?; @@ -179,6 +206,16 @@ fn parse_config( ) -> Result<Value> { config_paths.push(path.to_owned()); + // Deserialize the configuration file. + let config = deserialize_config(path, false)?; + + // Merge config with imports. + let imports = load_imports(&config, config_paths, recursion_limit); + Ok(serde_utils::merge(imports, config)) +} + +/// Deserialize a configuration file. +pub fn deserialize_config(path: &Path, warn_pruned: bool) -> Result<Value> { let mut contents = fs::read_to_string(path)?; // Remove UTF-8 BOM. @@ -186,103 +223,161 @@ fn parse_config( contents = contents.split_off(3); } + // Convert YAML to TOML as a transitionary fallback mechanism. + let extension = path.extension().unwrap_or_default(); + if (extension == "yaml" || extension == "yml") && !contents.trim().is_empty() { + warn!( + "YAML config {path:?} is deprecated, please migrate to TOML using `alacritty migrate`" + ); + + let mut value: serde_yaml::Value = serde_yaml::from_str(&contents)?; + prune_yaml_nulls(&mut value, warn_pruned); + contents = toml::to_string(&value)?; + } + // Load configuration file as Value. - let config: Value = match serde_yaml::from_str(&contents) { - Ok(config) => config, - Err(error) => { - // Prevent parsing error with an empty string and commented out file. - if error.to_string() == "EOF while parsing a value" { - Value::Mapping(Mapping::new()) - } else { - return Err(Error::Yaml(error)); - } - }, - }; + let config: Value = toml::from_str(&contents)?; - // Merge config with imports. - let imports = load_imports(&config, config_paths, recursion_limit); - Ok(serde_utils::merge(imports, config)) + Ok(config) } /// Load all referenced configuration files. fn load_imports(config: &Value, config_paths: &mut Vec<PathBuf>, recursion_limit: usize) -> Value { - let imports = match config.get("import") { - Some(Value::Sequence(imports)) => imports, - Some(_) => { - error!(target: LOG_TARGET_CONFIG, "Invalid import type: expected a sequence"); - return Value::Null; + // Get paths for all imports. + let import_paths = match imports(config, recursion_limit) { + Ok(import_paths) => import_paths, + Err(err) => { + error!(target: LOG_TARGET_CONFIG, "{err}"); + return Value::Table(Table::new()); }, - None => return Value::Null, + }; + + // Parse configs for all imports recursively. + let mut merged = Value::Table(Table::new()); + for import_path in import_paths { + let path = match import_path { + Ok(path) => path, + Err(err) => { + error!(target: LOG_TARGET_CONFIG, "{err}"); + continue; + }, + }; + + if !path.exists() { + info!(target: LOG_TARGET_CONFIG, "Config import not found:\n {:?}", path.display()); + continue; + } + + match parse_config(&path, config_paths, recursion_limit - 1) { + Ok(config) => merged = serde_utils::merge(merged, config), + Err(err) => { + error!(target: LOG_TARGET_CONFIG, "Unable to import config {:?}: {}", path, err) + }, + } + } + + merged +} + +// TODO: Merge back with `load_imports` once `alacritty migrate` is dropped. +// +/// Get all import paths for a configuration. +pub fn imports( + config: &Value, + recursion_limit: usize, +) -> StdResult<Vec<StdResult<PathBuf, String>>, String> { + let imports = match config.get("import") { + Some(Value::Array(imports)) => imports, + Some(_) => return Err("Invalid import type: expected a sequence".into()), + None => return Ok(Vec::new()), }; // Limit recursion to prevent infinite loops. if !imports.is_empty() && recursion_limit == 0 { - error!(target: LOG_TARGET_CONFIG, "Exceeded maximum configuration import depth"); - return Value::Null; + return Err("Exceeded maximum configuration import depth".into()); } - let mut merged = Value::Null; + let mut import_paths = Vec::new(); for import in imports { let mut path = match import { Value::String(path) => PathBuf::from(path), _ => { - error!( - target: LOG_TARGET_CONFIG, - "Invalid import element type: expected path string" - ); + import_paths.push(Err("Invalid import element type: expected path string".into())); continue; }, }; // Resolve paths relative to user's home directory. - if let (Ok(stripped), Some(home_dir)) = (path.strip_prefix("~/"), dirs::home_dir()) { + if let (Ok(stripped), Some(home_dir)) = (path.strip_prefix("~/"), home::home_dir()) { path = home_dir.join(stripped); } - if !path.exists() { - info!(target: LOG_TARGET_CONFIG, "Config import not found:\n {:?}", path.display()); - continue; - } + import_paths.push(Ok(path)); + } - match parse_config(&path, config_paths, recursion_limit - 1) { - Ok(config) => merged = serde_utils::merge(merged, config), - Err(err) => { - error!(target: LOG_TARGET_CONFIG, "Unable to import config {:?}: {}", path, err) + Ok(import_paths) +} + +/// Prune the nulls from the YAML to ensure TOML compatibility. +fn prune_yaml_nulls(value: &mut serde_yaml::Value, warn_pruned: bool) { + fn walk(value: &mut serde_yaml::Value, warn_pruned: bool) -> bool { + match value { + serde_yaml::Value::Sequence(sequence) => { + sequence.retain_mut(|value| !walk(value, warn_pruned)); + sequence.is_empty() + }, + serde_yaml::Value::Mapping(mapping) => { + mapping.retain(|key, value| { + let retain = !walk(value, warn_pruned); + if let Some(key_name) = key.as_str().filter(|_| !retain && warn_pruned) { + eprintln!("Removing null key \"{key_name}\" from the end config"); + } + retain + }); + mapping.is_empty() }, + serde_yaml::Value::Null => true, + _ => false, } } - merged + if walk(value, warn_pruned) { + // When the value itself is null return the mapping. + *value = serde_yaml::Value::Mapping(Default::default()); + } } /// Get the location of the first found default config file paths /// according to the following order: /// -/// 1. $XDG_CONFIG_HOME/alacritty/alacritty.yml -/// 2. $XDG_CONFIG_HOME/alacritty.yml -/// 3. $HOME/.config/alacritty/alacritty.yml -/// 4. $HOME/.alacritty.yml +/// 1. $XDG_CONFIG_HOME/alacritty/alacritty.toml +/// 2. $XDG_CONFIG_HOME/alacritty.toml +/// 3. $HOME/.config/alacritty/alacritty.toml +/// 4. $HOME/.alacritty.toml #[cfg(not(windows))] -fn installed_config() -> Option<PathBuf> { +pub fn installed_config(suffix: &str) -> Option<PathBuf> { + let file_name = format!("alacritty.{suffix}"); + // Try using XDG location by default. xdg::BaseDirectories::with_prefix("alacritty") .ok() - .and_then(|xdg| xdg.find_config_file("alacritty.yml")) + .and_then(|xdg| xdg.find_config_file(&file_name)) .or_else(|| { xdg::BaseDirectories::new() .ok() - .and_then(|fallback| fallback.find_config_file("alacritty.yml")) + .and_then(|fallback| fallback.find_config_file(&file_name)) }) .or_else(|| { if let Ok(home) = env::var("HOME") { - // Fallback path: $HOME/.config/alacritty/alacritty.yml. - let fallback = PathBuf::from(&home).join(".config/alacritty/alacritty.yml"); + // Fallback path: $HOME/.config/alacritty/alacritty.toml. + let fallback = PathBuf::from(&home).join(".config/alacritty").join(&file_name); if fallback.exists() { return Some(fallback); } - // Fallback path: $HOME/.alacritty.yml. - let fallback = PathBuf::from(&home).join(".alacritty.yml"); + // Fallback path: $HOME/.alacritty.toml. + let hidden_name = format!(".{file_name}"); + let fallback = PathBuf::from(&home).join(hidden_name); if fallback.exists() { return Some(fallback); } @@ -292,22 +387,56 @@ fn installed_config() -> Option<PathBuf> { } #[cfg(windows)] -fn installed_config() -> Option<PathBuf> { - dirs::config_dir().map(|path| path.join("alacritty\\alacritty.yml")).filter(|new| new.exists()) +pub fn installed_config(suffix: &str) -> Option<PathBuf> { + let file_name = format!("alacritty.{suffix}"); + dirs::config_dir().map(|path| path.join("alacritty").join(file_name)).filter(|new| new.exists()) } #[cfg(test)] mod tests { use super::*; - static DEFAULT_ALACRITTY_CONFIG: &str = - concat!(env!("CARGO_MANIFEST_DIR"), "/../alacritty.yml"); + #[test] + fn empty_config() { + toml::from_str::<UiConfig>("").unwrap(); + } + + fn yaml_to_toml(contents: &str) -> String { + let mut value: serde_yaml::Value = serde_yaml::from_str(contents).unwrap(); + prune_yaml_nulls(&mut value, false); + toml::to_string(&value).unwrap() + } #[test] - fn config_read_eof() { - let config_path: PathBuf = DEFAULT_ALACRITTY_CONFIG.into(); - let mut config = read_config(&config_path, Value::Null).unwrap(); - config.config_paths = Vec::new(); - assert_eq!(config, UiConfig::default()); + fn yaml_with_nulls() { + let contents = r#" + window: + blinking: Always + cursor: + not_blinking: Always + some_array: + - { window: } + - { window: "Hello" } + + "#; + let toml = yaml_to_toml(contents); + assert_eq!( + toml.trim(), + r#"[window] +blinking = "Always" +not_blinking = "Always" + +[[window.some_array]] +window = "Hello""# + ); + } + + #[test] + fn empty_yaml_to_toml() { + let contents = r#" + + "#; + let toml = yaml_to_toml(contents); + assert!(toml.is_empty()); } } diff --git a/alacritty/src/config/monitor.rs b/alacritty/src/config/monitor.rs index 3548fc02..f4b39a22 100644 --- a/alacritty/src/config/monitor.rs +++ b/alacritty/src/config/monitor.rs @@ -24,7 +24,7 @@ pub fn watch(mut paths: Vec<PathBuf>, event_proxy: EventLoopProxy<Event>) { // Exclude char devices like `/dev/null`, sockets, and so on, by checking that file type is a // regular file. paths.retain(|path| { - // Call `metadata` to resolve symbolink links. + // Call `metadata` to resolve symbolic links. path.metadata().map_or(false, |metadata| metadata.file_type().is_file()) }); diff --git a/alacritty/src/config/mouse.rs b/alacritty/src/config/mouse.rs index 291e4c61..4afd7446 100644 --- a/alacritty/src/config/mouse.rs +++ b/alacritty/src/config/mouse.rs @@ -1,29 +1,30 @@ -use std::time::Duration; +use serde::{Deserialize, Deserializer}; -use alacritty_config_derive::ConfigDeserialize; +use alacritty_config_derive::{ConfigDeserialize, SerdeReplace}; + +use crate::config::bindings::{self, MouseBinding}; +use crate::config::ui_config; #[derive(ConfigDeserialize, Default, Clone, Debug, PartialEq, Eq)] pub struct Mouse { - pub double_click: ClickHandler, - pub triple_click: ClickHandler, pub hide_when_typing: bool, - #[config(deprecated = "use `hints` section instead")] - pub url: Option<serde_yaml::Value>, + pub bindings: MouseBindings, } -#[derive(ConfigDeserialize, Clone, Debug, PartialEq, Eq)] -pub struct ClickHandler { - threshold: u16, -} +#[derive(SerdeReplace, Clone, Debug, PartialEq, Eq)] +pub struct MouseBindings(pub Vec<MouseBinding>); -impl Default for ClickHandler { +impl Default for MouseBindings { fn default() -> Self { - Self { threshold: 300 } + Self(bindings::default_mouse_bindings()) } } -impl ClickHandler { - pub fn threshold(&self) -> Duration { - Duration::from_millis(self.threshold as u64) +impl<'de> Deserialize<'de> for MouseBindings { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + Ok(Self(ui_config::deserialize_bindings(deserializer, Self::default().0)?)) } } diff --git a/alacritty/src/config/scrolling.rs b/alacritty/src/config/scrolling.rs new file mode 100644 index 00000000..3b2b21f3 --- /dev/null +++ b/alacritty/src/config/scrolling.rs @@ -0,0 +1,53 @@ +use serde::de::Error as SerdeError; +use serde::{Deserialize, Deserializer}; + +use alacritty_config_derive::{ConfigDeserialize, SerdeReplace}; + +/// Maximum scrollback amount configurable. +pub const MAX_SCROLLBACK_LINES: u32 = 100_000; + +/// Struct for scrolling related settings. +#[derive(ConfigDeserialize, Copy, Clone, Debug, PartialEq, Eq)] +pub struct Scrolling { + pub multiplier: u8, + + history: ScrollingHistory, +} + +impl Default for Scrolling { + fn default() -> Self { + Self { multiplier: 3, history: Default::default() } + } +} + +impl Scrolling { + pub fn history(self) -> u32 { + self.history.0 + } +} + +#[derive(SerdeReplace, Copy, Clone, Debug, PartialEq, Eq)] +struct ScrollingHistory(u32); + +impl Default for ScrollingHistory { + fn default() -> Self { + Self(10_000) + } +} + +impl<'de> Deserialize<'de> for ScrollingHistory { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + let lines = u32::deserialize(deserializer)?; + + if lines > MAX_SCROLLBACK_LINES { + Err(SerdeError::custom(format!( + "exceeded maximum scrolling history ({lines}/{MAX_SCROLLBACK_LINES})" + ))) + } else { + Ok(Self(lines)) + } + } +} diff --git a/alacritty/src/config/selection.rs b/alacritty/src/config/selection.rs new file mode 100644 index 00000000..bf90b48f --- /dev/null +++ b/alacritty/src/config/selection.rs @@ -0,0 +1,17 @@ +use alacritty_config_derive::ConfigDeserialize; +use alacritty_terminal::term::SEMANTIC_ESCAPE_CHARS; + +#[derive(ConfigDeserialize, Clone, Debug, PartialEq, Eq)] +pub struct Selection { + pub semantic_escape_chars: String, + pub save_to_clipboard: bool, +} + +impl Default for Selection { + fn default() -> Self { + Self { + semantic_escape_chars: SEMANTIC_ESCAPE_CHARS.to_owned(), + save_to_clipboard: Default::default(), + } + } +} diff --git a/alacritty/src/config/serde_utils.rs b/alacritty/src/config/serde_utils.rs index beb9c36b..476133e0 100644 --- a/alacritty/src/config/serde_utils.rs +++ b/alacritty/src/config/serde_utils.rs @@ -1,7 +1,6 @@ //! Serde helpers. -use serde_yaml::mapping::Mapping; -use serde_yaml::Value; +use toml::{Table, Value}; /// Merge two serde structures. /// @@ -9,20 +8,19 @@ use serde_yaml::Value; /// `replacement`. pub fn merge(base: Value, replacement: Value) -> Value { match (base, replacement) { - (Value::Sequence(mut base), Value::Sequence(mut replacement)) => { + (Value::Array(mut base), Value::Array(mut replacement)) => { base.append(&mut replacement); - Value::Sequence(base) + Value::Array(base) }, - (Value::Mapping(base), Value::Mapping(replacement)) => { - Value::Mapping(merge_mapping(base, replacement)) + (Value::Table(base), Value::Table(replacement)) => { + Value::Table(merge_tables(base, replacement)) }, - (value, Value::Null) => value, (_, value) => value, } } -/// Merge two key/value mappings. -fn merge_mapping(mut base: Mapping, replacement: Mapping) -> Mapping { +/// Merge two key/value tables. +fn merge_tables(mut base: Table, replacement: Table) -> Table { for (key, value) in replacement { let value = match base.remove(&key) { Some(base_value) => merge(base_value, value), @@ -40,54 +38,54 @@ mod tests { #[test] fn merge_primitive() { - let base = Value::Null; - let replacement = Value::Bool(true); + let base = Value::Table(Table::new()); + let replacement = Value::Boolean(true); assert_eq!(merge(base, replacement.clone()), replacement); - let base = Value::Bool(false); - let replacement = Value::Bool(true); + let base = Value::Boolean(false); + let replacement = Value::Boolean(true); assert_eq!(merge(base, replacement.clone()), replacement); - let base = Value::Number(0.into()); - let replacement = Value::Number(1.into()); + let base = Value::Integer(0.into()); + let replacement = Value::Integer(1.into()); assert_eq!(merge(base, replacement.clone()), replacement); let base = Value::String(String::new()); let replacement = Value::String(String::from("test")); assert_eq!(merge(base, replacement.clone()), replacement); - let base = Value::Mapping(Mapping::new()); - let replacement = Value::Null; + let base = Value::Table(Table::new()); + let replacement = Value::Table(Table::new()); assert_eq!(merge(base.clone(), replacement), base); } #[test] fn merge_sequence() { - let base = Value::Sequence(vec![Value::Null]); - let replacement = Value::Sequence(vec![Value::Bool(true)]); - let expected = Value::Sequence(vec![Value::Null, Value::Bool(true)]); + let base = Value::Array(vec![Value::Table(Table::new())]); + let replacement = Value::Array(vec![Value::Boolean(true)]); + let expected = Value::Array(vec![Value::Table(Table::new()), Value::Boolean(true)]); assert_eq!(merge(base, replacement), expected); } #[test] - fn merge_mapping() { - let mut base_mapping = Mapping::new(); - base_mapping.insert(Value::String(String::from("a")), Value::Bool(true)); - base_mapping.insert(Value::String(String::from("b")), Value::Bool(false)); - let base = Value::Mapping(base_mapping); + fn merge_tables() { + let mut base_table = Table::new(); + base_table.insert(String::from("a"), Value::Boolean(true)); + base_table.insert(String::from("b"), Value::Boolean(false)); + let base = Value::Table(base_table); - let mut replacement_mapping = Mapping::new(); - replacement_mapping.insert(Value::String(String::from("a")), Value::Bool(true)); - replacement_mapping.insert(Value::String(String::from("c")), Value::Bool(false)); - let replacement = Value::Mapping(replacement_mapping); + let mut replacement_table = Table::new(); + replacement_table.insert(String::from("a"), Value::Boolean(true)); + replacement_table.insert(String::from("c"), Value::Boolean(false)); + let replacement = Value::Table(replacement_table); let merged = merge(base, replacement); - let mut expected_mapping = Mapping::new(); - expected_mapping.insert(Value::String(String::from("b")), Value::Bool(false)); - expected_mapping.insert(Value::String(String::from("a")), Value::Bool(true)); - expected_mapping.insert(Value::String(String::from("c")), Value::Bool(false)); - let expected = Value::Mapping(expected_mapping); + let mut expected_table = Table::new(); + expected_table.insert(String::from("b"), Value::Boolean(false)); + expected_table.insert(String::from("a"), Value::Boolean(true)); + expected_table.insert(String::from("c"), Value::Boolean(false)); + let expected = Value::Table(expected_table); assert_eq!(merged, expected); } diff --git a/alacritty/src/config/terminal.rs b/alacritty/src/config/terminal.rs new file mode 100644 index 00000000..b41af5db --- /dev/null +++ b/alacritty/src/config/terminal.rs @@ -0,0 +1,26 @@ +use serde::{de, Deserialize, Deserializer}; +use toml::Value; + +use alacritty_config_derive::{ConfigDeserialize, SerdeReplace}; +use alacritty_terminal::term::Osc52; + +use crate::config::ui_config::StringVisitor; + +#[derive(ConfigDeserialize, Default, Copy, Clone, Debug, PartialEq)] +pub struct Terminal { + /// OSC52 support mode. + pub osc52: SerdeOsc52, +} + +#[derive(SerdeReplace, Default, Copy, Clone, Debug, PartialEq)] +pub struct SerdeOsc52(pub Osc52); + +impl<'de> Deserialize<'de> for SerdeOsc52 { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + let value = deserializer.deserialize_str(StringVisitor)?; + Osc52::deserialize(Value::String(value)).map(SerdeOsc52).map_err(de::Error::custom) + } +} diff --git a/alacritty/src/config/ui_config.rs b/alacritty/src/config/ui_config.rs index 29ff2c4c..21059734 100644 --- a/alacritty/src/config/ui_config.rs +++ b/alacritty/src/config/ui_config.rs @@ -1,43 +1,64 @@ use std::cell::RefCell; +use std::collections::HashMap; +use std::error::Error; use std::fmt::{self, Formatter}; use std::path::PathBuf; use std::rc::Rc; -use log::error; +use alacritty_config::SerdeReplace; +use alacritty_terminal::term::Config as TermConfig; +use alacritty_terminal::tty::{Options as PtyOptions, Shell}; +use log::{error, warn}; use serde::de::{Error as SerdeError, MapAccess, Visitor}; use serde::{self, Deserialize, Deserializer}; use unicode_width::UnicodeWidthChar; -use winit::event::{ModifiersState, VirtualKeyCode}; +use winit::keyboard::{Key, ModifiersState}; use alacritty_config_derive::{ConfigDeserialize, SerdeReplace}; -use alacritty_terminal::config::{ - Config as TerminalConfig, Percentage, Program, LOG_TARGET_CONFIG, -}; use alacritty_terminal::term::search::RegexSearch; use crate::config::bell::BellConfig; use crate::config::bindings::{ - self, Action, Binding, Key, KeyBinding, ModeWrapper, ModsWrapper, MouseBinding, + self, Action, Binding, BindingKey, KeyBinding, KeyLocation, ModeWrapper, ModsWrapper, + MouseBinding, }; use crate::config::color::Colors; +use crate::config::cursor::Cursor; use crate::config::debug::Debug; use crate::config::font::Font; -use crate::config::mouse::Mouse; +use crate::config::mouse::{Mouse, MouseBindings}; +use crate::config::scrolling::Scrolling; +use crate::config::selection::Selection; +use crate::config::terminal::Terminal; use crate::config::window::WindowConfig; +use crate::config::LOG_TARGET_CONFIG; /// Regex used for the default URL hint. #[rustfmt::skip] -const URL_REGEX: &str = "(ipfs:|ipns:|magnet:|mailto:|gemini:|gopher:|https:|http:|news:|file:|git:|ssh:|ftp:)\ +const URL_REGEX: &str = "(ipfs:|ipns:|magnet:|mailto:|gemini://|gopher://|https://|http://|news:|file:|git://|ssh:|ftp://)\ [^\u{0000}-\u{001F}\u{007F}-\u{009F}<>\"\\s{-}\\^⟨⟩`]+"; #[derive(ConfigDeserialize, Clone, Debug, PartialEq)] pub struct UiConfig { + /// Extra environment variables. + pub env: HashMap<String, String>, + + /// How much scrolling history to keep. + pub scrolling: Scrolling, + + /// Cursor configuration. + pub cursor: Cursor, + + /// Selection configuration. + pub selection: Selection, + /// Font configuration. pub font: Font, /// Window configuration. pub window: WindowConfig, + /// Mouse configuration. pub mouse: Mouse, /// Debug options. @@ -57,9 +78,6 @@ pub struct UiConfig { /// 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>, @@ -72,109 +90,161 @@ pub struct UiConfig { pub ipc_socket: bool, /// Config for the alacritty_terminal itself. - #[config(flatten)] - pub terminal_config: TerminalConfig, + pub terminal: Terminal, + + /// Path to a shell program to run on startup. + pub shell: Option<Program>, + + /// Shell startup directory. + pub working_directory: Option<PathBuf>, + + /// Keyboard configuration. + keyboard: Keyboard, + + /// Should draw bold text with brighter colors instead of bold font. + #[config(deprecated = "use colors.draw_bold_text_with_bright_colors instead")] + draw_bold_text_with_bright_colors: bool, /// Keybindings. - key_bindings: KeyBindings, + #[config(deprecated = "use keyboard.bindings instead")] + key_bindings: Option<KeyBindings>, /// Bindings for the mouse. - mouse_bindings: MouseBindings, + #[config(deprecated = "use mouse.bindings instead")] + mouse_bindings: Option<MouseBindings>, - /// Background opacity from 0.0 to 1.0. - #[config(deprecated = "use window.opacity instead")] - background_opacity: Option<Percentage>, + /// Configuration file imports. + /// + /// This is never read since the field is directly accessed through the config's + /// [`toml::Value`], but still present to prevent unused field warnings. + import: Vec<String>, } impl Default for UiConfig { fn default() -> Self { Self { live_config_reload: true, - alt_send_esc: Default::default(), #[cfg(unix)] ipc_socket: true, - font: Default::default(), - window: Default::default(), - mouse: Default::default(), - debug: Default::default(), + draw_bold_text_with_bright_colors: Default::default(), + working_directory: Default::default(), + mouse_bindings: Default::default(), config_paths: Default::default(), key_bindings: Default::default(), - mouse_bindings: Default::default(), - terminal_config: Default::default(), - background_opacity: Default::default(), - bell: Default::default(), + alt_send_esc: Default::default(), + scrolling: Default::default(), + selection: Default::default(), + keyboard: Default::default(), + terminal: Default::default(), + import: Default::default(), + cursor: Default::default(), + window: Default::default(), colors: Default::default(), - draw_bold_text_with_bright_colors: Default::default(), + shell: Default::default(), + mouse: Default::default(), + debug: Default::default(), hints: Default::default(), + font: Default::default(), + bell: Default::default(), + env: Default::default(), } } } impl UiConfig { + /// Derive [`TermConfig`] from the config. + pub fn term_options(&self) -> TermConfig { + TermConfig { + semantic_escape_chars: self.selection.semantic_escape_chars.clone(), + scrolling_history: self.scrolling.history() as usize, + vi_mode_cursor_style: self.cursor.vi_mode_style(), + default_cursor_style: self.cursor.style(), + osc52: self.terminal.osc52.0, + kitty_keyboard: true, + } + } + + /// Derive [`PtyOptions`] from the config. + pub fn pty_config(&self) -> PtyOptions { + let shell = self.shell.clone().map(Into::into); + PtyOptions { shell, working_directory: self.working_directory.clone(), hold: false } + } + /// Generate key bindings for all keyboard hints. pub fn generate_hint_bindings(&mut self) { + // Check which key bindings is most likely to be the user's configuration. + // + // Both will be non-empty due to the presence of the default keybindings. + let key_bindings = if let Some(key_bindings) = self.key_bindings.as_mut() { + &mut key_bindings.0 + } else { + &mut self.keyboard.bindings.0 + }; + for hint in &self.hints.enabled { - let binding = match hint.binding { + let binding = match &hint.binding { Some(binding) => binding, None => continue, }; let binding = KeyBinding { - trigger: binding.key, + trigger: binding.key.clone(), mods: binding.mods.0, mode: binding.mode.mode, notmode: binding.mode.not_mode, action: Action::Hint(hint.clone()), }; - self.key_bindings.0.push(binding); + key_bindings.push(binding); } } #[inline] pub fn window_opacity(&self) -> f32 { - self.background_opacity.unwrap_or(self.window.opacity).as_f32() + self.window.opacity.as_f32() } #[inline] pub fn key_bindings(&self) -> &[KeyBinding] { - self.key_bindings.0.as_slice() + if let Some(key_bindings) = self.key_bindings.as_ref() { + &key_bindings.0 + } else { + &self.keyboard.bindings.0 + } } #[inline] pub fn mouse_bindings(&self) -> &[MouseBinding] { - self.mouse_bindings.0.as_slice() + if let Some(mouse_bindings) = self.mouse_bindings.as_ref() { + &mouse_bindings.0 + } else { + &self.mouse.bindings.0 + } } -} - -#[derive(SerdeReplace, Clone, Debug, PartialEq, Eq)] -struct KeyBindings(Vec<KeyBinding>); -impl Default for KeyBindings { - fn default() -> Self { - Self(bindings::default_key_bindings()) + #[inline] + pub fn draw_bold_text_with_bright_colors(&self) -> bool { + self.colors.draw_bold_text_with_bright_colors || self.draw_bold_text_with_bright_colors } } -impl<'de> Deserialize<'de> for KeyBindings { - fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> - where - D: Deserializer<'de>, - { - Ok(Self(deserialize_bindings(deserializer, Self::default().0)?)) - } +/// Keyboard configuration. +#[derive(ConfigDeserialize, Default, Clone, Debug, PartialEq)] +struct Keyboard { + /// Keybindings. + bindings: KeyBindings, } #[derive(SerdeReplace, Clone, Debug, PartialEq, Eq)] -struct MouseBindings(Vec<MouseBinding>); +struct KeyBindings(Vec<KeyBinding>); -impl Default for MouseBindings { +impl Default for KeyBindings { fn default() -> Self { - Self(bindings::default_mouse_bindings()) + Self(bindings::default_key_bindings()) } } -impl<'de> Deserialize<'de> for MouseBindings { +impl<'de> Deserialize<'de> for KeyBindings { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de>, @@ -183,16 +253,16 @@ impl<'de> Deserialize<'de> for MouseBindings { } } -fn deserialize_bindings<'a, D, T>( +pub fn deserialize_bindings<'a, D, T>( deserializer: D, mut default: Vec<Binding<T>>, ) -> Result<Vec<Binding<T>>, D::Error> where D: Deserializer<'a>, - T: Copy + Eq, + T: Clone + Eq, Binding<T>: Deserialize<'a>, { - let values = Vec::<serde_yaml::Value>::deserialize(deserializer)?; + let values = Vec::<toml::Value>::deserialize(deserializer)?; // Skip all invalid values. let mut bindings = Vec::with_capacity(values.len()); @@ -255,11 +325,15 @@ impl Default for Hints { enabled: vec![Hint { content, action, + persist: false, post_processing: true, mouse: Some(HintMouse { enabled: true, mods: Default::default() }), binding: Some(HintBinding { - key: Key::Keycode(VirtualKeyCode::U), - mods: ModsWrapper(ModifiersState::SHIFT | ModifiersState::CTRL), + key: BindingKey::Keycode { + key: Key::Character("u".into()), + location: KeyLocation::Standard, + }, + mods: ModsWrapper(ModifiersState::SHIFT | ModifiersState::CONTROL), mode: Default::default(), }), }], @@ -347,6 +421,10 @@ pub struct Hint { #[serde(default)] pub post_processing: bool, + /// Persist hints after selection. + #[serde(default)] + pub persist: bool, + /// Hint mouse highlighting. pub mouse: Option<HintMouse>, @@ -388,7 +466,7 @@ impl<'de> Deserialize<'de> for HintContent { { let mut content = Self::Value::default(); - while let Some((key, value)) = map.next_entry::<String, serde_yaml::Value>()? { + while let Some((key, value)) = map.next_entry::<String, toml::Value>()? { match key.as_str() { "regex" => match Option::<LazyRegex>::deserialize(value) { Ok(regex) => content.regex = regex, @@ -408,7 +486,8 @@ impl<'de> Deserialize<'de> for HintContent { ); }, }, - _ => (), + "command" | "action" => (), + key => warn!(target: LOG_TARGET_CONFIG, "Unrecognized hint field: {key}"), } } @@ -429,9 +508,10 @@ impl<'de> Deserialize<'de> for HintContent { } /// Binding for triggering a keyboard hint. -#[derive(Deserialize, Copy, Clone, Debug, PartialEq, Eq)] +#[derive(Deserialize, Clone, Debug, PartialEq, Eq)] +#[serde(deny_unknown_fields)] pub struct HintBinding { - pub key: Key, + pub key: BindingKey, #[serde(default)] pub mods: ModsWrapper, #[serde(default)] @@ -454,11 +534,11 @@ pub struct LazyRegex(Rc<RefCell<LazyRegexVariant>>); impl LazyRegex { /// Execute a function with the compiled regex DFAs as parameter. - pub fn with_compiled<T, F>(&self, mut f: F) -> T + pub fn with_compiled<T, F>(&self, f: F) -> Option<T> where - F: FnMut(&RegexSearch) -> T, + F: FnMut(&mut RegexSearch) -> T, { - f(self.0.borrow_mut().compiled()) + self.0.borrow_mut().compiled().map(f) } } @@ -477,6 +557,7 @@ impl<'de> Deserialize<'de> for LazyRegex { pub enum LazyRegexVariant { Compiled(Box<RegexSearch>), Pattern(String), + Uncompilable, } impl LazyRegexVariant { @@ -484,27 +565,29 @@ impl LazyRegexVariant { /// /// If the regex is not already compiled, this will compile the DFAs and store them for future /// access. - fn compiled(&mut self) -> &RegexSearch { + fn compiled(&mut self) -> Option<&mut RegexSearch> { // Check if the regex has already been compiled. let regex = match self { - Self::Compiled(regex_search) => return regex_search, + Self::Compiled(regex_search) => return Some(regex_search), + Self::Uncompilable => return None, Self::Pattern(regex) => regex, }; // Compile the regex. let regex_search = match RegexSearch::new(regex) { Ok(regex_search) => regex_search, - Err(error) => { - error!("hint regex is invalid: {}", error); - RegexSearch::new("").unwrap() + Err(err) => { + error!("could not compile hint regex: {err}"); + *self = Self::Uncompilable; + return None; }, }; *self = Self::Compiled(Box::new(regex_search)); // Return a reference to the compiled DFAs. match self { - Self::Compiled(dfas) => dfas, - Self::Pattern(_) => unreachable!(), + Self::Compiled(dfas) => Some(dfas), + _ => unreachable!(), } } } @@ -518,3 +601,141 @@ impl PartialEq for LazyRegexVariant { } } impl Eq for LazyRegexVariant {} + +/// Wrapper around f32 that represents a percentage value between 0.0 and 1.0. +#[derive(SerdeReplace, Deserialize, Clone, Copy, Debug, PartialEq)] +pub struct Percentage(f32); + +impl Default for Percentage { + fn default() -> Self { + Percentage(1.0) + } +} + +impl Percentage { + pub fn new(value: f32) -> Self { + Percentage(value.clamp(0., 1.)) + } + + pub fn as_f32(self) -> f32 { + self.0 + } +} + +#[derive(Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(untagged, deny_unknown_fields)] +pub enum Program { + Just(String), + WithArgs { + program: String, + #[serde(default)] + args: Vec<String>, + }, +} + +impl Program { + pub fn program(&self) -> &str { + match self { + Program::Just(program) => program, + Program::WithArgs { program, .. } => program, + } + } + + pub fn args(&self) -> &[String] { + match self { + Program::Just(_) => &[], + Program::WithArgs { args, .. } => args, + } + } +} + +impl From<Program> for Shell { + fn from(value: Program) -> Self { + match value { + Program::Just(program) => Shell::new(program, Vec::new()), + Program::WithArgs { program, args } => Shell::new(program, args), + } + } +} + +impl SerdeReplace for Program { + fn replace(&mut self, value: toml::Value) -> Result<(), Box<dyn Error>> { + *self = Self::deserialize(value)?; + + Ok(()) + } +} + +pub(crate) struct StringVisitor; +impl<'de> serde::de::Visitor<'de> for StringVisitor { + type Value = String; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("a string") + } + + fn visit_str<E>(self, s: &str) -> Result<Self::Value, E> + where + E: serde::de::Error, + { + Ok(s.to_lowercase()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use alacritty_terminal::term::test::mock_term; + + use crate::display::hint::visible_regex_match_iter; + + #[test] + fn positive_url_parsing_regex_test() { + for regular_url in [ + "ipfs:s0mEhAsh", + "ipns:an0TherHash1234", + "magnet:?xt=urn:btih:L0UDHA5H12", + "mailto:example@example.org", + "gemini://gemini.example.org/", + "gopher://gopher.example.org", + "https://www.example.org", + "http://example.org", + "news:some.news.portal", + "file:///C:/Windows/", + "file:/home/user/whatever", + "git://github.com/user/repo.git", + "ssh:git@github.com:user/repo.git", + "ftp://ftp.example.org", + ] { + let term = mock_term(regular_url); + let mut regex = RegexSearch::new(URL_REGEX).unwrap(); + let matches = visible_regex_match_iter(&term, &mut regex).collect::<Vec<_>>(); + assert_eq!( + matches.len(), + 1, + "Should have exactly one match url {regular_url}, but instead got: {matches:?}" + ) + } + } + + #[test] + fn negative_url_parsing_regex_test() { + for url_like in [ + "http::trace::on_request::log_parameters", + "http//www.example.org", + "/user:example.org", + "mailto: example@example.org", + "http://<script>alert('xss')</script>", + "mailto:", + ] { + let term = mock_term(url_like); + let mut regex = RegexSearch::new(URL_REGEX).unwrap(); + let matches = visible_regex_match_iter(&term, &mut regex).collect::<Vec<_>>(); + assert!( + matches.is_empty(), + "Should not match url in string {url_like}, but instead got: {matches:?}" + ) + } + } +} diff --git a/alacritty/src/config/window.rs b/alacritty/src/config/window.rs index 98bc18b6..380f7a6f 100644 --- a/alacritty/src/config/window.rs +++ b/alacritty/src/config/window.rs @@ -1,19 +1,17 @@ use std::fmt::{self, Formatter}; -use std::os::raw::c_ulong; use log::{error, warn}; use serde::de::{self, MapAccess, Visitor}; use serde::{Deserialize, Deserializer, Serialize}; -use winit::window::{Fullscreen, Theme}; #[cfg(target_os = "macos")] -use winit::platform::macos::OptionAsAlt; +use winit::platform::macos::OptionAsAlt as WinitOptionAsAlt; +use winit::window::{Fullscreen, Theme as WinitTheme}; use alacritty_config_derive::{ConfigDeserialize, SerdeReplace}; -use alacritty_terminal::config::{Percentage, LOG_TARGET_CONFIG}; -use alacritty_terminal::index::Column; -use crate::config::ui_config::Delta; +use crate::config::ui_config::{Delta, Percentage}; +use crate::config::LOG_TARGET_CONFIG; /// Default Alacritty name, used for window title and class. pub const DEFAULT_NAME: &str = "Alacritty"; @@ -31,10 +29,7 @@ pub struct WindowConfig { /// XEmbed parent. #[config(skip)] - pub embed: Option<c_ulong>, - - /// System decorations theme variant. - pub decorations_theme_variant: Option<Theme>, + pub embed: Option<u32>, /// Spread out additional padding evenly. pub dynamic_padding: bool, @@ -49,36 +44,41 @@ pub struct WindowConfig { /// Background opacity from 0.0 to 1.0. pub opacity: Percentage, + /// Request blur behind the window. + pub blur: bool, + /// Controls which `Option` key should be treated as `Alt`. - #[cfg(target_os = "macos")] - pub option_as_alt: OptionAsAlt, + option_as_alt: OptionAsAlt, /// Resize increments. pub resize_increments: bool, /// Pixel padding. - padding: Delta<u8>, + padding: Delta<u16>, /// Initial dimensions. dimensions: Dimensions, + + /// System decorations theme variant. + decorations_theme_variant: Option<Theme>, } impl Default for WindowConfig { fn default() -> Self { Self { dynamic_title: true, + blur: Default::default(), + embed: Default::default(), + padding: Default::default(), + opacity: Default::default(), position: Default::default(), + identity: Default::default(), + dimensions: Default::default(), decorations: Default::default(), startup_mode: Default::default(), - embed: Default::default(), - decorations_theme_variant: Default::default(), dynamic_padding: Default::default(), - identity: Identity::default(), - opacity: Default::default(), - padding: Default::default(), - dimensions: Default::default(), resize_increments: Default::default(), - #[cfg(target_os = "macos")] + decorations_theme_variant: Default::default(), option_as_alt: Default::default(), } } @@ -87,7 +87,7 @@ impl Default for WindowConfig { impl WindowConfig { #[inline] pub fn dimensions(&self) -> Option<Dimensions> { - let (lines, columns) = (self.dimensions.lines, self.dimensions.columns.0); + let (lines, columns) = (self.dimensions.lines, self.dimensions.columns); let (lines_is_non_zero, columns_is_non_zero) = (lines != 0, columns != 0); if lines_is_non_zero && columns_is_non_zero { @@ -137,6 +137,20 @@ impl WindowConfig { pub fn maximized(&self) -> bool { self.startup_mode == StartupMode::Maximized } + + #[cfg(target_os = "macos")] + pub fn option_as_alt(&self) -> WinitOptionAsAlt { + match self.option_as_alt { + OptionAsAlt::OnlyLeft => WinitOptionAsAlt::OnlyLeft, + OptionAsAlt::OnlyRight => WinitOptionAsAlt::OnlyRight, + OptionAsAlt::Both => WinitOptionAsAlt::Both, + OptionAsAlt::None => WinitOptionAsAlt::None, + } + } + + pub fn theme(&self) -> Option<WinitTheme> { + self.decorations_theme_variant.map(WinitTheme::from) + } } #[derive(ConfigDeserialize, Debug, Clone, PartialEq, Eq)] @@ -154,44 +168,31 @@ impl Default for Identity { } } -#[derive(ConfigDeserialize, Debug, Copy, Clone, PartialEq, Eq)] +#[derive(ConfigDeserialize, Default, Debug, Copy, Clone, PartialEq, Eq)] pub enum StartupMode { + #[default] Windowed, Maximized, Fullscreen, - #[cfg(target_os = "macos")] SimpleFullscreen, } -impl Default for StartupMode { - fn default() -> StartupMode { - StartupMode::Windowed - } -} - -#[derive(ConfigDeserialize, Debug, Copy, Clone, PartialEq, Eq)] +#[derive(ConfigDeserialize, Default, Debug, Copy, Clone, PartialEq, Eq)] pub enum Decorations { + #[default] Full, - #[cfg(target_os = "macos")] Transparent, - #[cfg(target_os = "macos")] Buttonless, None, } -impl Default for Decorations { - fn default() -> Decorations { - Decorations::Full - } -} - /// Window Dimensions. /// /// Newtype to avoid passing values incorrectly. #[derive(ConfigDeserialize, Default, Debug, Copy, Clone, PartialEq, Eq)] pub struct Dimensions { /// Window width in character columns. - pub columns: Column, + pub columns: usize, /// Window Height in character lines. pub lines: usize, @@ -242,7 +243,7 @@ impl<'de> Deserialize<'de> for Class { { let mut class = Self::Value::default(); - while let Some((key, value)) = map.next_entry::<String, serde_yaml::Value>()? { + while let Some((key, value)) = map.next_entry::<String, toml::Value>()? { match key.as_str() { "instance" => match String::deserialize(value) { Ok(instance) => class.instance = instance, @@ -262,7 +263,7 @@ impl<'de> Deserialize<'de> for Class { ); }, }, - _ => (), + key => warn!(target: LOG_TARGET_CONFIG, "Unrecognized class field: {key}"), } } @@ -273,3 +274,35 @@ impl<'de> Deserialize<'de> for Class { deserializer.deserialize_any(ClassVisitor) } } + +#[derive(ConfigDeserialize, Default, Debug, Clone, Copy, PartialEq, Eq)] +pub enum OptionAsAlt { + /// The left `Option` key is treated as `Alt`. + OnlyLeft, + + /// The right `Option` key is treated as `Alt`. + OnlyRight, + + /// Both `Option` keys are treated as `Alt`. + Both, + + /// No special handling is applied for `Option` key. + #[default] + None, +} + +/// System decorations theme variant. +#[derive(ConfigDeserialize, Debug, Clone, Copy, PartialEq, Eq)] +pub enum Theme { + Light, + Dark, +} + +impl From<Theme> for WinitTheme { + fn from(theme: Theme) -> Self { + match theme { + Theme::Light => WinitTheme::Light, + Theme::Dark => WinitTheme::Dark, + } + } +} diff --git a/alacritty/src/display/color.rs b/alacritty/src/display/color.rs index 6e0de048..669bf502 100644 --- a/alacritty/src/display/color.rs +++ b/alacritty/src/display/color.rs @@ -1,9 +1,14 @@ -use std::ops::{Index, IndexMut}; +use std::fmt::{self, Display, Formatter}; +use std::ops::{Add, Deref, Index, IndexMut, Mul}; +use std::str::FromStr; use log::trace; +use serde::de::{Error as SerdeError, Visitor}; +use serde::{Deserialize, Deserializer}; -use alacritty_terminal::ansi::NamedColor; -use alacritty_terminal::term::color::{Rgb, COUNT}; +use alacritty_config_derive::SerdeReplace; +use alacritty_terminal::term::color::COUNT; +use alacritty_terminal::vte::ansi::{NamedColor, Rgb as VteRgb}; use crate::config::color::Colors; @@ -95,11 +100,11 @@ impl List { { 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 }, - }; + self[index] = Rgb::new( + if r == 0 { 0 } else { r * 40 + 55 }, + if g == 0 { 0 } else { g * 40 + 55 }, + if b == 0 { 0 } else { b * 40 + 55 }, + ); } index += 1; } @@ -126,7 +131,7 @@ impl List { } let value = i * 10 + 8; - self[index] = Rgb { r: value, g: value, b: value }; + self[index] = Rgb::new(value, value, value); index += 1; } @@ -165,3 +170,191 @@ impl IndexMut<NamedColor> for List { &mut self.0[idx as usize] } } + +#[derive(SerdeReplace, Debug, Eq, PartialEq, Copy, Clone, Default)] +pub struct Rgb(pub VteRgb); + +impl Rgb { + #[inline] + pub const fn new(r: u8, g: u8, b: u8) -> Self { + Self(VteRgb { r, g, b }) + } + + #[inline] + pub fn as_tuple(self) -> (u8, u8, u8) { + (self.0.r, self.0.g, self.0.b) + } +} + +impl From<VteRgb> for Rgb { + fn from(value: VteRgb) -> Self { + Self(value) + } +} + +impl Deref for Rgb { + type Target = VteRgb; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Mul<f32> for Rgb { + type Output = Rgb; + + fn mul(self, rhs: f32) -> Self::Output { + Rgb(self.0 * rhs) + } +} + +impl Add<Rgb> for Rgb { + type Output = Rgb; + + fn add(self, rhs: Rgb) -> Self::Output { + Rgb(self.0 + rhs.0) + } +} + +/// Deserialize an Rgb from a hex string. +/// +/// This is *not* the deserialize impl for Rgb since we want a symmetric +/// serialize/deserialize impl for ref tests. +impl<'de> Deserialize<'de> for Rgb { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + struct RgbVisitor; + + // Used for deserializing reftests. + #[derive(Deserialize)] + struct RgbDerivedDeser { + r: u8, + g: u8, + b: u8, + } + + impl<'a> Visitor<'a> for RgbVisitor { + type Value = Rgb; + + fn expecting(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str("hex color like #ff00ff") + } + + fn visit_str<E>(self, value: &str) -> Result<Rgb, E> + where + E: serde::de::Error, + { + Rgb::from_str(value).map_err(|_| { + E::custom(format!( + "failed to parse rgb color {value}; expected hex color like #ff00ff" + )) + }) + } + } + + // Return an error if the syntax is incorrect. + let value = toml::Value::deserialize(deserializer)?; + + // Attempt to deserialize from struct form. + if let Ok(RgbDerivedDeser { r, g, b }) = RgbDerivedDeser::deserialize(value.clone()) { + return Ok(Rgb::new(r, g, b)); + } + + // Deserialize from hex notation (either 0xff00ff or #ff00ff). + value.deserialize_str(RgbVisitor).map_err(D::Error::custom) + } +} + +impl Display for Rgb { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "#{:02x}{:02x}{:02x}", self.r, self.g, self.b) + } +} + +impl FromStr for Rgb { + type Err = (); + + fn from_str(s: &str) -> Result<Rgb, ()> { + let chars = if s.starts_with("0x") && s.len() == 8 { + &s[2..] + } else if s.starts_with('#') && s.len() == 7 { + &s[1..] + } else { + return Err(()); + }; + + match u32::from_str_radix(chars, 16) { + Ok(mut color) => { + let b = (color & 0xff) as u8; + color >>= 8; + let g = (color & 0xff) as u8; + color >>= 8; + let r = color as u8; + Ok(Rgb::new(r, g, b)) + }, + Err(_) => Err(()), + } + } +} + +/// RGB color optionally referencing the cell's foreground or background. +#[derive(SerdeReplace, Copy, Clone, Debug, PartialEq, Eq)] +pub enum CellRgb { + CellForeground, + CellBackground, + Rgb(Rgb), +} + +impl CellRgb { + pub fn color(self, foreground: Rgb, background: Rgb) -> Rgb { + match self { + Self::CellForeground => foreground, + Self::CellBackground => background, + Self::Rgb(rgb) => rgb, + } + } +} + +impl Default for CellRgb { + fn default() -> Self { + Self::Rgb(Rgb::default()) + } +} + +impl<'de> Deserialize<'de> for CellRgb { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + const EXPECTING: &str = "CellForeground, CellBackground, or hex color like #ff00ff"; + + struct CellRgbVisitor; + impl<'a> Visitor<'a> for CellRgbVisitor { + type Value = CellRgb; + + fn expecting(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str(EXPECTING) + } + + fn visit_str<E>(self, value: &str) -> Result<CellRgb, E> + where + E: serde::de::Error, + { + // Attempt to deserialize as enum constants. + match value { + "CellForeground" => return Ok(CellRgb::CellForeground), + "CellBackground" => return Ok(CellRgb::CellBackground), + _ => (), + } + + Rgb::from_str(value).map(CellRgb::Rgb).map_err(|_| { + E::custom(format!("failed to parse color {value}; expected {EXPECTING}")) + }) + } + } + + deserializer.deserialize_str(CellRgbVisitor).map_err(D::Error::custom) + } +} diff --git a/alacritty/src/display/content.rs b/alacritty/src/display/content.rs index fa59bc13..f36f71b5 100644 --- a/alacritty/src/display/content.rs +++ b/alacritty/src/display/content.rs @@ -2,21 +2,20 @@ use std::borrow::Cow; use std::ops::Deref; use std::{cmp, mem}; -use alacritty_terminal::ansi::{Color, CursorShape, NamedColor}; use alacritty_terminal::event::EventListener; use alacritty_terminal::graphics::GraphicId; -use alacritty_terminal::grid::Indexed; +use alacritty_terminal::grid::{Dimensions, Indexed}; use alacritty_terminal::index::{Column, Line, Point}; use alacritty_terminal::selection::SelectionRange; use alacritty_terminal::term::cell::{Cell, Flags, Hyperlink}; -use alacritty_terminal::term::color::{CellRgb, Rgb}; use alacritty_terminal::term::search::{Match, RegexSearch}; use alacritty_terminal::term::{self, RenderableContent as TerminalContent, Term, TermMode}; +use alacritty_terminal::vte::ansi::{Color, CursorShape, NamedColor}; use crate::config::UiConfig; -use crate::display::color::{List, DIM_FACTOR}; +use crate::display::color::{CellRgb, List, Rgb, DIM_FACTOR}; use crate::display::hint::{self, HintState}; -use crate::display::Display; +use crate::display::{Display, SizeInfo}; use crate::event::SearchState; use smallvec::SmallVec; @@ -37,6 +36,7 @@ pub struct RenderableContent<'a> { config: &'a UiConfig, colors: &'a List, focused_match: Option<&'a Match>, + size: &'a SizeInfo, } impl<'a> RenderableContent<'a> { @@ -44,7 +44,7 @@ impl<'a> RenderableContent<'a> { config: &'a UiConfig, display: &'a mut Display, term: &'a Term<T>, - search_state: &'a SearchState, + search_state: &'a mut SearchState, ) -> Self { let search = search_state.dfas().map(|dfas| HintMatches::visible_regex_matches(term, dfas)); let focused_match = search_state.focused_match(); @@ -57,7 +57,7 @@ impl<'a> RenderableContent<'a> { || display.ime.preedit().is_some() { CursorShape::Hidden - } else if !term.is_focused && config.terminal_config.cursor.unfocused_hollow { + } else if !term.is_focused && config.cursor.unfocused_hollow { CursorShape::HollowBlock } else { terminal_content.cursor.shape @@ -77,6 +77,7 @@ impl<'a> RenderableContent<'a> { Self { colors: &display.colors, + size: &display.size_info, cursor: RenderableCursor::new_hidden(), terminal_content, focused_match, @@ -103,7 +104,7 @@ impl<'a> RenderableContent<'a> { /// 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]) + self.terminal_content.colors[color].map(Rgb).unwrap_or(self.colors[color]) } pub fn selection_range(&self) -> Option<SelectionRange> { @@ -118,13 +119,13 @@ impl<'a> RenderableContent<'a> { } else { self.config.colors.cursor }; - let cursor_color = - self.terminal_content.colors[NamedColor::Cursor].map_or(color.background, CellRgb::Rgb); + let cursor_color = self.terminal_content.colors[NamedColor::Cursor] + .map_or(color.background, |c| CellRgb::Rgb(Rgb(c))); let text_color = color.foreground; let insufficient_contrast = (!matches!(cursor_color, CellRgb::Rgb(_)) || !matches!(text_color, CellRgb::Rgb(_))) - && cell.fg.contrast(cell.bg) < MIN_CURSOR_CONTRAST; + && cell.fg.contrast(*cell.bg) < MIN_CURSOR_CONTRAST; // Convert from cell colors to RGB. let mut text_color = text_color.color(cell.fg, cell.bg); @@ -216,7 +217,7 @@ pub struct RenderableCellExtra { } impl RenderableCell { - fn new<'a>(content: &mut RenderableContent<'a>, cell: Indexed<&Cell>) -> Self { + fn new(content: &mut RenderableContent<'_>, cell: Indexed<&Cell>) -> Self { // Lookup RGB values. let mut fg = Self::compute_fg_rgb(content, cell.fg, cell.flags); let mut bg = Self::compute_bg_rgb(content, cell.bg); @@ -240,18 +241,27 @@ impl RenderableCell { let viewport_start = Point::new(Line(-(display_offset as i32)), Column(0)); let colors = &content.config.colors; let mut character = cell.c; + let mut flags = cell.flags; - if let Some((c, is_first)) = - content.hint.as_mut().and_then(|hint| hint.advance(viewport_start, cell.point)) + let num_cols = content.size.columns(); + if let Some((c, is_first)) = content + .hint + .as_mut() + .and_then(|hint| hint.advance(viewport_start, num_cols, cell.point)) { - let (config_fg, config_bg) = if is_first { - (colors.hints.start.foreground, colors.hints.start.background) + if is_first { + let (config_fg, config_bg) = + (colors.hints.start.foreground, colors.hints.start.background); + Self::compute_cell_rgb(&mut fg, &mut bg, &mut bg_alpha, config_fg, config_bg); + } else if c.is_some() { + let (config_fg, config_bg) = + (colors.hints.end.foreground, colors.hints.end.background); + Self::compute_cell_rgb(&mut fg, &mut bg, &mut bg_alpha, config_fg, config_bg); } else { - (colors.hints.end.foreground, colors.hints.end.background) - }; - Self::compute_cell_rgb(&mut fg, &mut bg, &mut bg_alpha, config_fg, config_bg); + flags.insert(Flags::UNDERLINE); + } - character = c; + character = c.unwrap_or(character); } else if is_selected { let config_fg = colors.selection.foreground; let config_bg = colors.selection.background; @@ -273,11 +283,15 @@ impl RenderableCell { Self::compute_cell_rgb(&mut fg, &mut bg, &mut bg_alpha, config_fg, config_bg); } + // Apply transparency to all renderable cells if `transparent_background_colors` is set + if bg_alpha > 0. && content.config.colors.transparent_background_colors { + bg_alpha = content.config.window_opacity(); + } + // Convert cell point to viewport position. let cell_point = cell.point; let point = term::point_to_viewport(display_offset, cell_point).unwrap(); - let flags = cell.flags; let underline = cell .underline_color() .map_or(fg, |underline| Self::compute_fg_rgb(content, underline, flags)); @@ -332,15 +346,18 @@ impl RenderableCell { } /// Get the RGB color from a cell's foreground color. - fn compute_fg_rgb(content: &mut RenderableContent<'_>, fg: Color, flags: Flags) -> Rgb { + fn compute_fg_rgb(content: &RenderableContent<'_>, fg: Color, flags: Flags) -> Rgb { let config = &content.config; match fg { Color::Spec(rgb) => match flags & Flags::DIM { - Flags::DIM => rgb * DIM_FACTOR, - _ => rgb, + Flags::DIM => { + let rgb: Rgb = rgb.into(); + rgb * DIM_FACTOR + }, + _ => rgb.into(), }, Color::Named(ansi) => { - match (config.draw_bold_text_with_bright_colors, flags & Flags::DIM_BOLD) { + match (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 @@ -360,7 +377,7 @@ impl RenderableCell { }, Color::Indexed(idx) => { let idx = match ( - config.draw_bold_text_with_bright_colors, + config.draw_bold_text_with_bright_colors(), flags & Flags::DIM_BOLD, idx, ) { @@ -377,9 +394,9 @@ impl RenderableCell { /// Get the RGB color from a cell's background color. #[inline] - fn compute_bg_rgb(content: &mut RenderableContent<'_>, bg: Color) -> Rgb { + fn compute_bg_rgb(content: &RenderableContent<'_>, bg: Color) -> Rgb { match bg { - Color::Spec(rgb) => rgb, + Color::Spec(rgb) => rgb.into(), Color::Named(ansi) => content.color(ansi as usize), Color::Indexed(idx) => content.color(idx as usize), } @@ -461,7 +478,15 @@ impl<'a> Hint<'a> { /// this position will be returned. /// /// The tuple's [`bool`] will be `true` when the character is the first for this hint. - fn advance(&mut self, viewport_start: Point, point: Point) -> Option<(char, bool)> { + /// + /// The tuple's [`Option<char>`] will be [`None`] when the point is part of the match, but not + /// part of the hint label. + fn advance( + &mut self, + viewport_start: Point, + num_cols: usize, + point: Point, + ) -> Option<(Option<char>, bool)> { // Check if we're within a match at all. if !self.matches.advance(point) { return None; @@ -471,15 +496,22 @@ impl<'a> Hint<'a> { let start = self .matches .get(self.matches.index) - .map(|bounds| cmp::max(*bounds.start(), viewport_start)) - .filter(|start| start.line == point.line)?; + .map(|bounds| cmp::max(*bounds.start(), viewport_start))?; // Position within the hint label. - let label_position = point.column.0 - start.column.0; + let line_delta = point.line.0 - start.line.0; + let col_delta = point.column.0 as i32 - start.column.0 as i32; + let label_position = usize::try_from(line_delta * num_cols as i32 + col_delta).unwrap_or(0); let is_first = label_position == 0; // Hint label character. - self.labels[self.matches.index].get(label_position).copied().map(|c| (c, is_first)) + let hint_char = self.labels[self.matches.index] + .get(label_position) + .copied() + .map(|c| (Some(c), is_first)) + .unwrap_or((None, false)); + + Some(hint_char) } } @@ -506,8 +538,8 @@ impl<'a> HintMatches<'a> { Self { matches: matches.into(), index: 0 } } - /// Create from regex matches on term visable part. - fn visible_regex_matches<T>(term: &Term<T>, dfas: &RegexSearch) -> Self { + /// Create from regex matches on term visible part. + fn visible_regex_matches<T>(term: &Term<T>, dfas: &mut RegexSearch) -> Self { let matches = hint::visible_regex_match_iter(term, dfas).collect::<Vec<_>>(); Self::new(matches) } diff --git a/alacritty/src/display/cursor.rs b/alacritty/src/display/cursor.rs index 8a4cc729..65933ccc 100644 --- a/alacritty/src/display/cursor.rs +++ b/alacritty/src/display/cursor.rs @@ -1,8 +1,8 @@ //! Convert a cursor into an iterator of rects. -use alacritty_terminal::ansi::CursorShape; -use alacritty_terminal::term::color::Rgb; +use alacritty_terminal::vte::ansi::CursorShape; +use crate::display::color::Rgb; use crate::display::content::RenderableCursor; use crate::display::SizeInfo; use crate::renderer::rects::RenderRect; diff --git a/alacritty/src/display/damage.rs b/alacritty/src/display/damage.rs index 82230dff..450643b7 100644 --- a/alacritty/src/display/damage.rs +++ b/alacritty/src/display/damage.rs @@ -1,20 +1,214 @@ -use std::cmp; use std::iter::Peekable; +use std::{cmp, mem}; use glutin::surface::Rect; +use alacritty_terminal::index::Point; +use alacritty_terminal::selection::SelectionRange; use alacritty_terminal::term::{LineDamageBounds, TermDamageIterator}; use crate::display::SizeInfo; +/// State of the damage tracking for the [`Display`]. +/// +/// [`Display`]: crate::display::Display +#[derive(Debug)] +pub struct DamageTracker { + /// Position of the previously drawn Vi cursor. + pub old_vi_cursor: Option<Point<usize>>, + /// The location of the old selection. + pub old_selection: Option<SelectionRange>, + /// Highlight damage submitted for the compositor. + pub debug: bool, + + /// The damage for the frames. + frames: [FrameDamage; 2], + screen_lines: usize, + columns: usize, +} + +impl DamageTracker { + pub fn new(screen_lines: usize, columns: usize) -> Self { + let mut tracker = Self { + columns, + screen_lines, + debug: false, + old_vi_cursor: None, + old_selection: None, + frames: Default::default(), + }; + tracker.resize(screen_lines, columns); + tracker + } + + #[inline] + #[must_use] + pub fn frame(&mut self) -> &mut FrameDamage { + &mut self.frames[0] + } + + #[inline] + #[must_use] + pub fn next_frame(&mut self) -> &mut FrameDamage { + &mut self.frames[1] + } + + /// Advance to the next frame resetting the state for the active frame. + #[inline] + pub fn swap_damage(&mut self) { + let screen_lines = self.screen_lines; + let columns = self.columns; + self.frame().reset(screen_lines, columns); + self.frames.swap(0, 1); + } + + /// Resize the damage information in the tracker. + pub fn resize(&mut self, screen_lines: usize, columns: usize) { + self.screen_lines = screen_lines; + self.columns = columns; + for frame in &mut self.frames { + frame.reset(screen_lines, columns); + } + self.frame().full = true; + } + + /// Damage vi cursor inside the viewport. + pub fn damage_vi_cursor(&mut self, mut vi_cursor: Option<Point<usize>>) { + mem::swap(&mut self.old_vi_cursor, &mut vi_cursor); + + if self.frame().full { + return; + } + + if let Some(vi_cursor) = self.old_vi_cursor { + self.frame().damage_point(vi_cursor); + } + + if let Some(vi_cursor) = vi_cursor { + self.frame().damage_point(vi_cursor); + } + } + + /// Get shaped frame damage for the active frame. + pub fn shape_frame_damage(&self, size_info: SizeInfo<u32>) -> Vec<Rect> { + if self.frames[0].full { + vec![Rect::new(0, 0, size_info.width() as i32, size_info.height() as i32)] + } else { + let lines_damage = RenderDamageIterator::new( + TermDamageIterator::new(&self.frames[0].lines, 0), + &size_info, + ); + lines_damage.chain(self.frames[0].rects.iter().copied()).collect() + } + } + + /// Add the current frame's selection damage. + pub fn damage_selection( + &mut self, + mut selection: Option<SelectionRange>, + display_offset: usize, + ) { + mem::swap(&mut self.old_selection, &mut selection); + + if self.frame().full || selection == self.old_selection { + return; + } + + for selection in self.old_selection.into_iter().chain(selection) { + let display_offset = display_offset as i32; + let last_visible_line = self.screen_lines as i32 - 1; + let columns = self.columns; + + // Ignore invisible selection. + if selection.end.line.0 + display_offset < 0 + || selection.start.line.0.abs() < display_offset - last_visible_line + { + continue; + }; + + let start = cmp::max(selection.start.line.0 + display_offset, 0) as usize; + let end = (selection.end.line.0 + display_offset).clamp(0, last_visible_line) as usize; + for line in start..=end { + self.frame().lines[line].expand(0, columns - 1); + } + } + } +} + +/// Damage state for the rendering frame. +#[derive(Debug, Default)] +pub struct FrameDamage { + /// The entire frame needs to be redrawn. + full: bool, + /// Terminal lines damaged in the given frame. + lines: Vec<LineDamageBounds>, + /// Rectangular regions damage in the given frame. + rects: Vec<Rect>, +} + +impl FrameDamage { + /// Damage line for the given frame. + #[inline] + pub fn damage_line(&mut self, damage: LineDamageBounds) { + self.lines[damage.line].expand(damage.left, damage.right); + } + + #[inline] + pub fn damage_point(&mut self, point: Point<usize>) { + self.lines[point.line].expand(point.column.0, point.column.0); + } + + /// Mark the frame as fully damaged. + #[inline] + pub fn mark_fully_damaged(&mut self) { + self.full = true; + } + + /// Add viewport rectangle to damage. + /// + /// This allows covering elements outside of the terminal viewport, like message bar. + #[inline] + pub fn add_viewport_rect( + &mut self, + size_info: &SizeInfo, + x: i32, + y: i32, + width: i32, + height: i32, + ) { + let y = viewport_y_to_damage_y(size_info, y, height); + self.rects.push(Rect { x, y, width, height }); + } + + fn reset(&mut self, num_lines: usize, num_cols: usize) { + self.full = false; + self.rects.clear(); + self.lines.clear(); + self.lines.reserve(num_lines); + for line in 0..num_lines { + self.lines.push(LineDamageBounds::undamaged(line, num_cols)); + } + } +} + +/// Convert viewport `y` coordinate to [`Rect`] damage coordinate. +pub fn viewport_y_to_damage_y(size_info: &SizeInfo, y: i32, height: i32) -> i32 { + size_info.height() as i32 - y - height +} + +/// Convert viewport `y` coordinate to [`Rect`] damage coordinate. +pub fn damage_y_to_viewport_y(size_info: &SizeInfo, rect: &Rect) -> i32 { + size_info.height() as i32 - rect.y - rect.height +} + /// Iterator which converts `alacritty_terminal` damage information into renderer damaged rects. -pub struct RenderDamageIterator<'a> { +struct RenderDamageIterator<'a> { damaged_lines: Peekable<TermDamageIterator<'a>>, - size_info: SizeInfo<u32>, + size_info: &'a SizeInfo<u32>, } impl<'a> RenderDamageIterator<'a> { - pub fn new(damaged_lines: TermDamageIterator<'a>, size_info: SizeInfo<u32>) -> Self { + pub fn new(damaged_lines: TermDamageIterator<'a>, size_info: &'a SizeInfo<u32>) -> Self { Self { damaged_lines: damaged_lines.peekable(), size_info } } @@ -140,4 +334,24 @@ mod tests { rect ); } + + #[test] + fn add_viewport_damage() { + let mut frame_damage = FrameDamage::default(); + let viewport_height = 100.; + let x = 0; + let y = 40; + let height = 5; + let width = 10; + let size_info = SizeInfo::new(viewport_height, viewport_height, 5., 5., 0., 0., true); + frame_damage.add_viewport_rect(&size_info, x, y, width, height); + assert_eq!(frame_damage.rects[0], Rect { + x, + y: viewport_height as i32 - y - height, + width, + height + }); + assert_eq!(frame_damage.rects[0].y, viewport_y_to_damage_y(&size_info, y, height)); + assert_eq!(damage_y_to_viewport_y(&size_info, &frame_damage.rects[0]), y); + } } diff --git a/alacritty/src/display/hint.rs b/alacritty/src/display/hint.rs index 202b8f97..bd09a881 100644 --- a/alacritty/src/display/hint.rs +++ b/alacritty/src/display/hint.rs @@ -2,7 +2,8 @@ use std::cmp::Reverse; use std::collections::HashSet; use std::iter; -use winit::event::ModifiersState; +use ahash::RandomState; +use winit::keyboard::ModifiersState; use alacritty_terminal::grid::{BidirectionalIterator, Dimensions}; use alacritty_terminal::index::{Boundary, Column, Direction, Line, Point}; @@ -89,7 +90,8 @@ impl HintState { // Apply post-processing and search for sub-matches if necessary. if hint.post_processing { - self.matches.extend(matches.flat_map(|rm| { + let mut matches = matches.collect::<Vec<_>>(); + self.matches.extend(matches.drain(..).flat_map(|rm| { HintPostProcessor::new(term, regex, rm).collect::<Vec<_>>() })); } else { @@ -150,7 +152,12 @@ impl HintState { let bounds = self.matches[index].clone(); let action = hint.action.clone(); - self.stop(); + // Exit hint mode unless it requires explicit dismissal. + if hint.persist { + self.keys.clear(); + } else { + self.stop(); + } // Hyperlinks take precedence over regex matches. let hyperlink = term.grid()[*bounds.start()].hyperlink(); @@ -283,7 +290,7 @@ impl HintLabels { /// Iterate over all visible regex matches. pub fn visible_regex_match_iter<'a, T>( term: &'a Term<T>, - regex: &'a RegexSearch, + regex: &'a mut RegexSearch, ) -> impl Iterator<Item = Match> + 'a { let viewport_start = Line(-(term.grid().display_offset() as i32)); let viewport_end = viewport_start + term.bottommost_line(); @@ -302,7 +309,7 @@ pub fn visible_unique_hyperlinks_iter<T>(term: &Term<T>) -> impl Iterator<Item = let mut display_iter = term.grid().display_iter().peekable(); // Avoid creating hints for the same hyperlinks, but from a different places. - let mut unique_hyperlinks = HashSet::new(); + let mut unique_hyperlinks = HashSet::<Hyperlink, RandomState>::default(); iter::from_fn(move || { // Find the start of the next unique hyperlink. @@ -338,7 +345,7 @@ pub fn visible_unique_hyperlinks_iter<T>(term: &Term<T>) -> impl Iterator<Item = fn regex_match_at<T>( term: &Term<T>, point: Point, - regex: &RegexSearch, + regex: &mut RegexSearch, post_processing: bool, ) -> Option<Match> { let regex_match = visible_regex_match_iter(term, regex).find(|rm| rm.contains(&point))?; @@ -381,9 +388,10 @@ pub fn highlighted_at<T>( }); } - if let Some(bounds) = hint.content.regex.as_ref().and_then(|regex| { + let bounds = hint.content.regex.as_ref().and_then(|regex| { regex.with_compiled(|regex| regex_match_at(term, point, regex, hint.post_processing)) - }) { + }); + if let Some(bounds) = bounds.flatten() { return Some(HintMatch { bounds, action: hint.action.clone(), hyperlink: None }); } @@ -405,7 +413,7 @@ fn hyperlink_at<T>(term: &Term<T>, point: Point) -> Option<(Hyperlink, Match)> { // Find adjacent lines that have the same `hyperlink`. The end purpose to highlight hyperlinks // that span across multiple lines or not directly attached to each other. - // Find the closest to the viewport start adjucent line. + // Find the closest to the viewport start adjacent line. while match_start.line > viewport_start { let next_line = match_start.line - 1i32; // Iterate over all the cells in the grid's line and check if any of those cells contains @@ -443,7 +451,7 @@ fn hyperlink_at<T>(term: &Term<T>, point: Point) -> Option<(Hyperlink, Match)> { /// Iterator over all post-processed matches inside an existing hint match. struct HintPostProcessor<'a, T> { /// Regex search DFAs. - regex: &'a RegexSearch, + regex: &'a mut RegexSearch, /// Terminal reference. term: &'a Term<T>, @@ -460,7 +468,7 @@ struct HintPostProcessor<'a, T> { impl<'a, T> HintPostProcessor<'a, T> { /// Create a new iterator for an unprocessed match. - fn new(term: &'a Term<T>, regex: &'a RegexSearch, regex_match: Match) -> Self { + fn new(term: &'a Term<T>, regex: &'a mut RegexSearch, regex_match: Match) -> Self { let mut post_processor = Self { next_match: None, start: *regex_match.start(), @@ -582,9 +590,9 @@ impl<'a, T> Iterator for HintPostProcessor<'a, T> { #[cfg(test)] mod tests { - use alacritty_terminal::ansi::Handler; use alacritty_terminal::index::{Column, Line}; use alacritty_terminal::term::test::mock_term; + use alacritty_terminal::vte::ansi::Handler; use super::*; @@ -631,11 +639,11 @@ mod tests { fn closed_bracket_does_not_result_in_infinite_iterator() { let term = mock_term(" ) "); - let search = RegexSearch::new("[^/ ]").unwrap(); + let mut search = RegexSearch::new("[^/ ]").unwrap(); let count = HintPostProcessor::new( &term, - &search, + &mut search, Point::new(Line(0), Column(1))..=Point::new(Line(0), Column(1)), ) .take(1) @@ -647,25 +655,25 @@ mod tests { #[test] fn collect_unique_hyperlinks() { let mut term = mock_term("000\r\n111"); - term.goto(Line(0), Column(0)); + term.goto(0, 0); - let hyperlink_foo = Hyperlink::new(Some("1"), "foo"); - let hyperlink_bar = Hyperlink::new(Some("2"), "bar"); + let hyperlink_foo = Hyperlink::new(Some("1"), String::from("foo")); + let hyperlink_bar = Hyperlink::new(Some("2"), String::from("bar")); // Create 2 hyperlinks on the first line. - term.set_hyperlink(Some(hyperlink_foo.clone())); + term.set_hyperlink(Some(hyperlink_foo.clone().into())); term.input('b'); term.input('a'); - term.set_hyperlink(Some(hyperlink_bar.clone())); + term.set_hyperlink(Some(hyperlink_bar.clone().into())); term.input('r'); - term.set_hyperlink(Some(hyperlink_foo.clone())); - term.goto(Line(1), Column(0)); + term.set_hyperlink(Some(hyperlink_foo.clone().into())); + term.goto(1, 0); // Ditto for the second line. - term.set_hyperlink(Some(hyperlink_foo)); + term.set_hyperlink(Some(hyperlink_foo.into())); term.input('b'); term.input('a'); - term.set_hyperlink(Some(hyperlink_bar)); + term.set_hyperlink(Some(hyperlink_bar.into())); term.input('r'); term.set_hyperlink(None); @@ -687,9 +695,9 @@ mod tests { // The Term returned from this call will have a viewport starting at 0 and ending at 4096. // That's good enough for this test, since it only cares about visible content. let term = mock_term(&content); - let regex = RegexSearch::new("match!").unwrap(); + let mut regex = RegexSearch::new("match!").unwrap(); - // The interator should match everything in the viewport. - assert_eq!(visible_regex_match_iter(&term, ®ex).count(), 4096); + // The iterator should match everything in the viewport. + assert_eq!(visible_regex_match_iter(&term, &mut regex).count(), 4096); } } diff --git a/alacritty/src/display/meter.rs b/alacritty/src/display/meter.rs index 9ccfe52d..e263100f 100644 --- a/alacritty/src/display/meter.rs +++ b/alacritty/src/display/meter.rs @@ -64,11 +64,6 @@ impl<'a> Drop for Sampler<'a> { } impl Meter { - /// Create a meter. - pub fn new() -> Meter { - Default::default() - } - /// Get a sampler. pub fn sampler(&mut self) -> Sampler<'_> { Sampler::new(self) diff --git a/alacritty/src/display/mod.rs b/alacritty/src/display/mod.rs index 91879f59..b2ff4106 100644 --- a/alacritty/src/display/mod.rs +++ b/alacritty/src/display/mod.rs @@ -6,32 +6,33 @@ use std::fmt::{self, Formatter}; use std::mem::{self, ManuallyDrop}; use std::num::NonZeroU32; use std::ops::{Deref, DerefMut}; -use std::sync::atomic::Ordering; use std::time::{Duration, Instant}; use glutin::context::{NotCurrentContext, PossiblyCurrentContext}; use glutin::prelude::*; -use glutin::surface::{Rect as DamageRect, Surface, SwapInterval, WindowSurface}; +use glutin::surface::{Surface, SwapInterval, WindowSurface}; use log::{debug, info}; use parking_lot::MutexGuard; +use raw_window_handle::RawWindowHandle; use serde::{Deserialize, Serialize}; use winit::dpi::PhysicalSize; -use winit::event::ModifiersState; +use winit::keyboard::ModifiersState; use winit::window::CursorIcon; -use crossfont::{self, Rasterize, Rasterizer}; +use crossfont::{self, Rasterize, Rasterizer, Size as FontSize}; use unicode_width::UnicodeWidthChar; -use alacritty_terminal::ansi::{CursorShape, NamedColor}; -use alacritty_terminal::config::MAX_SCROLLBACK_LINES; use alacritty_terminal::event::{EventListener, OnResize, WindowSize}; use alacritty_terminal::grid::Dimensions as TermDimensions; use alacritty_terminal::index::{Column, Direction, Line, Point}; -use alacritty_terminal::selection::{Selection, SelectionRange}; +use alacritty_terminal::selection::Selection; use alacritty_terminal::term::cell::Flags; -use alacritty_terminal::term::color::Rgb; -use alacritty_terminal::term::{self, Term, TermDamage, TermMode, MIN_COLUMNS, MIN_SCREEN_LINES}; +use alacritty_terminal::term::{ + self, point_to_viewport, LineDamageBounds, Term, TermDamage, TermMode, MIN_COLUMNS, + MIN_SCREEN_LINES, +}; +use alacritty_terminal::vte::ansi::{CursorShape, NamedColor}; use crate::config::font::Font; use crate::config::window::Dimensions; @@ -39,10 +40,10 @@ use crate::config::window::Dimensions; use crate::config::window::StartupMode; use crate::config::UiConfig; use crate::display::bell::VisualBell; -use crate::display::color::List; +use crate::display::color::{List, Rgb}; use crate::display::content::{RenderableContent, RenderableCursor}; use crate::display::cursor::IntoRects; -use crate::display::damage::RenderDamageIterator; +use crate::display::damage::{damage_y_to_viewport_y, DamageTracker}; use crate::display::hint::{HintMatch, HintState}; use crate::display::meter::Meter; use crate::display::window::Window; @@ -53,13 +54,13 @@ use crate::renderer::{self, GlyphCache, Renderer}; use crate::scheduler::{Scheduler, TimerId, Topic}; use crate::string::{ShortenDirection, StrShortener}; +pub mod color; pub mod content; pub mod cursor; pub mod hint; pub mod window; mod bell; -mod color; mod damage; mod meter; @@ -73,7 +74,7 @@ const BACKWARD_SEARCH_LABEL: &str = "Backward Search: "; const SHORTENER: char = '…'; /// Color which is used to highlight damaged rects when debugging. -const DAMAGE_RECT_COLOR: Rgb = Rgb { r: 255, g: 0, b: 255 }; +const DAMAGE_RECT_COLOR: Rgb = Rgb::new(255, 0, 255); #[derive(Debug)] pub enum Error { @@ -356,7 +357,7 @@ pub struct Display { /// Hint highlighted by the vi mode cursor. pub vi_highlighted_hint: Option<HintMatch>, - pub is_wayland: bool, + pub raw_window_handle: RawWindowHandle, /// UI cursor visibility for blinking. pub cursor_hidden: bool, @@ -381,6 +382,12 @@ pub struct Display { /// The state of the timer for frame scheduling. pub frame_timer: FrameTimer, + /// Damage tracker for the given display. + pub damage_tracker: DamageTracker, + + /// Font size used by the window. + pub font_size: FontSize, + // Mouse point position when highlighting hints. hint_mouse_point: Option<Point>, @@ -390,9 +397,6 @@ pub struct Display { context: ManuallyDrop<Replaceable<PossiblyCurrentContext>>, - debug_damage: bool, - damage_rects: Vec<DamageRect>, - next_frame_damage_rects: Vec<DamageRect>, glyph_cache: GlyphCache, meter: Meter, } @@ -402,17 +406,17 @@ impl Display { window: Window, gl_context: NotCurrentContext, config: &UiConfig, + _tabbed: bool, ) -> Result<Display, Error> { - #[cfg(any(not(feature = "wayland"), target_os = "macos", windows))] - let is_wayland = false; - #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] - let is_wayland = window.wayland_surface().is_some(); + let raw_window_handle = window.raw_window_handle(); let scale_factor = window.scale_factor as f32; - let rasterizer = Rasterizer::new(scale_factor)?; + let rasterizer = Rasterizer::new()?; + let font_size = config.font.size().scale(scale_factor); debug!("Loading \"{}\" font", &config.font.normal().family); - let mut glyph_cache = GlyphCache::new(rasterizer, &config.font)?; + let font = config.font.clone().with_size(font_size); + let mut glyph_cache = GlyphCache::new(rasterizer, &font)?; let metrics = glyph_cache.font_metrics(); let (cell_width, cell_height) = compute_cell_size(config, &metrics); @@ -420,7 +424,7 @@ impl Display { // Resize the window to account for the user configured size. if let Some(dimensions) = config.window.dimensions() { let size = window_size(config, dimensions, cell_width, cell_height, scale_factor); - window.set_inner_size(size); + window.request_inner_size(size); } // Create the GL surface to draw into. @@ -471,9 +475,10 @@ impl Display { #[cfg(target_os = "macos")] window.set_has_shadow(config.window_opacity() >= 1.0); + let is_wayland = matches!(raw_window_handle, RawWindowHandle::Wayland(_)); + // On Wayland we can safely ignore this call, since the window isn't visible until you // actually draw something into it and commit those changes. - #[cfg(not(any(target_os = "macos", windows)))] if !is_wayland { surface.swap_buffers(&context).expect("failed to swap buffers."); renderer.finish(); @@ -488,23 +493,19 @@ impl Display { #[allow(clippy::single_match)] #[cfg(not(windows))] - match config.window.startup_mode { - #[cfg(target_os = "macos")] - StartupMode::SimpleFullscreen => window.set_simple_fullscreen(true), - #[cfg(not(any(target_os = "macos", windows)))] - StartupMode::Maximized if !is_wayland => window.set_maximized(true), - _ => (), + if !_tabbed { + match config.window.startup_mode { + #[cfg(target_os = "macos")] + StartupMode::SimpleFullscreen => window.set_simple_fullscreen(true), + StartupMode::Maximized if !is_wayland => window.set_maximized(true), + _ => (), + } } let hint_state = HintState::new(config.hints.alphabet()); - let debug_damage = config.debug.highlight_damage; - let (damage_rects, next_frame_damage_rects) = if is_wayland || debug_damage { - let vec = Vec::with_capacity(size_info.screen_lines()); - (vec.clone(), vec) - } else { - (Vec::new(), Vec::new()) - }; + let mut damage_tracker = DamageTracker::new(size_info.screen_lines(), size_info.columns()); + damage_tracker.debug = config.debug.highlight_damage; // Disable vsync. if let Err(err) = surface.set_swap_interval(&context, SwapInterval::DontWait) { @@ -512,28 +513,27 @@ impl Display { } Ok(Self { - window, context: ManuallyDrop::new(Replaceable::new(context)), - surface: ManuallyDrop::new(surface), + visual_bell: VisualBell::from(&config.bell), renderer: ManuallyDrop::new(renderer), + surface: ManuallyDrop::new(surface), + colors: List::from(&config.colors), + frame_timer: FrameTimer::new(), + raw_window_handle, + damage_tracker, glyph_cache, hint_state, - meter: Meter::new(), size_info, - ime: Ime::new(), - highlighted_hint: None, - vi_highlighted_hint: None, - is_wayland, - cursor_hidden: false, - frame_timer: FrameTimer::new(), - visual_bell: VisualBell::from(&config.bell), - colors: List::from(&config.colors), - pending_update: Default::default(), + font_size, + window, pending_renderer_update: Default::default(), - debug_damage, - damage_rects, - next_frame_damage_rects, - hint_mouse_point: None, + vi_highlighted_hint: Default::default(), + highlighted_hint: Default::default(), + hint_mouse_point: Default::default(), + pending_update: Default::default(), + cursor_hidden: Default::default(), + meter: Default::default(), + ime: Default::default(), }) } @@ -564,9 +564,11 @@ impl Display { let res = match (self.surface.deref(), &self.context.get()) { #[cfg(not(any(target_os = "macos", windows)))] (Surface::Egl(surface), PossiblyCurrentContext::Egl(context)) - if self.is_wayland && !self.debug_damage => + if matches!(self.raw_window_handle, RawWindowHandle::Wayland(_)) + && !self.damage_tracker.debug => { - surface.swap_buffers_with_damage(context, &self.damage_rects) + let damage = self.damage_tracker.shape_frame_damage(self.size_info.into()); + surface.swap_buffers_with_damage(context, &damage) }, (surface, context) => surface.swap_buffers(context), }; @@ -580,11 +582,10 @@ impl Display { /// This will return a tuple of the cell width and height. fn update_font_size( glyph_cache: &mut GlyphCache, - scale_factor: f64, config: &UiConfig, font: &Font, ) -> (f32, f32) { - let _ = glyph_cache.update_font_size(font, scale_factor); + let _ = glyph_cache.update_font_size(font); // Compute new cell sizes. compute_cell_size(config, &glyph_cache.font_metrics()) @@ -598,17 +599,16 @@ impl Display { }); } + // XXX: this function must not call to any `OpenGL` related tasks. Renderer updates are + // performed in [`Self::process_renderer_update`] right before drawing. + // /// Process update events. - /// - /// XXX: this function must not call to any `OpenGL` related tasks. Only logical update - /// of the state is being performed here. Rendering update takes part right before the - /// actual rendering. pub fn handle_update<T>( &mut self, terminal: &mut Term<T>, pty_resize_handle: &mut dyn OnResize, message_buffer: &MessageBuffer, - search_active: bool, + search_state: &mut SearchState, config: &UiConfig, ) where T: EventListener, @@ -625,13 +625,15 @@ impl Display { // Update font size and cell dimensions. if let Some(font) = pending_update.font() { - let scale_factor = self.window.scale_factor; - let cell_dimensions = - Self::update_font_size(&mut self.glyph_cache, scale_factor, config, font); + let cell_dimensions = Self::update_font_size(&mut self.glyph_cache, config, font); cell_width = cell_dimensions.0; cell_height = cell_dimensions.1; info!("Cell size: {} x {}", cell_width, cell_height); + + // Mark entire terminal as damaged since glyph size could change without cell size + // changes. + self.damage_tracker.frame().mark_fully_damaged(); } let (mut width, mut height) = (self.size_info.width(), self.size_info.height()); @@ -653,6 +655,7 @@ impl Display { ); // Update number of column/lines in the viewport. + let search_active = search_state.history_index.is_some(); let message_bar_lines = message_buffer.message().map_or(0, |m| m.text(&new_size).len()); let search_lines = usize::from(search_active); new_size.reserve_lines(message_bar_lines + search_lines); @@ -662,28 +665,37 @@ impl Display { self.window.set_resize_increments(PhysicalSize::new(cell_width, cell_height)); } - // Resize PTY. - pty_resize_handle.on_resize(new_size.into()); + // Resize when terminal when its dimensions have changed. + if self.size_info.screen_lines() != new_size.screen_lines + || self.size_info.columns() != new_size.columns() + { + // Resize PTY. + pty_resize_handle.on_resize(new_size.into()); - // Resize terminal. - terminal.resize(new_size); + // Resize terminal. + terminal.resize(new_size); - // Queue renderer update if terminal dimensions/padding changed. + // Resize damage tracking. + self.damage_tracker.resize(new_size.screen_lines(), new_size.columns()); + } + + // Check if dimensions have changed. if new_size != self.size_info { + // Queue renderer update. let renderer_update = self.pending_renderer_update.get_or_insert(Default::default()); renderer_update.resize = true; + + // Clear focused search match. + search_state.clear_focused_match(); } self.size_info = new_size; } + // NOTE: Renderer updates are split off, since platforms like Wayland require resize and other + // OpenGL operations to be performed right before rendering. Otherwise they could lock the + // back buffer and render with the previous state. This also solves flickering during resizes. + // /// Update the state of the renderer. - /// - /// NOTE: The update to the renderer is split from the display update on purpose, since - /// on some platforms, like Wayland, resize and other OpenGL operations must be performed - /// right before rendering, otherwise they could lock the back buffer resulting in - /// rendering with the buffer of old size. - /// - /// This also resolves any flickering, since the resize is now synced with frame callbacks. pub fn process_renderer_update(&mut self) { let renderer_update = match self.pending_renderer_update.take() { Some(renderer_update) => renderer_update, @@ -706,62 +718,8 @@ impl Display { self.renderer.resize(&self.size_info); - if self.collect_damage() { - let lines = self.size_info.screen_lines(); - if lines > self.damage_rects.len() { - self.damage_rects.reserve(lines); - } else { - self.damage_rects.shrink_to(lines); - } - } - info!("Padding: {} x {}", self.size_info.padding_x(), self.size_info.padding_y()); info!("Width: {}, Height: {}", self.size_info.width(), self.size_info.height()); - - // Damage the entire screen after processing update. - self.fully_damage(); - } - - /// Damage the entire window. - fn fully_damage(&mut self) { - let screen_rect = - DamageRect::new(0, 0, self.size_info.width() as i32, self.size_info.height() as i32); - - self.damage_rects.push(screen_rect); - } - - fn update_damage<T: EventListener>( - &mut self, - terminal: &mut MutexGuard<'_, Term<T>>, - selection_range: Option<SelectionRange>, - search_state: &SearchState, - ) { - let requires_full_damage = self.visual_bell.intensity() != 0. - || self.hint_state.active() - || search_state.regex().is_some(); - if requires_full_damage { - terminal.mark_fully_damaged(); - } - - self.damage_highlighted_hints(terminal); - match terminal.damage(selection_range) { - TermDamage::Full => self.fully_damage(), - TermDamage::Partial(damaged_lines) => { - let damaged_rects = RenderDamageIterator::new(damaged_lines, self.size_info.into()); - for damaged_rect in damaged_rects { - self.damage_rects.push(damaged_rect); - } - }, - } - terminal.reset_damage(); - - // Ensure that the content requiring full damage is cleaned up again on the next frame. - if requires_full_damage { - terminal.mark_fully_damaged(); - } - - // Damage highlighted hints for the next frame as well, so we'll clear them. - self.damage_highlighted_hints(terminal); } /// Draw the screen. @@ -775,7 +733,7 @@ impl Display { scheduler: &mut Scheduler, message_buffer: &MessageBuffer, config: &UiConfig, - search_state: &SearchState, + search_state: &mut SearchState, ) { // Collect renderable content before the terminal is dropped. let mut content = RenderableContent::new(config, self, &terminal, search_state); @@ -797,8 +755,17 @@ impl Display { let vi_mode = terminal.mode().contains(TermMode::VI); let vi_cursor_point = if vi_mode { Some(terminal.vi_mode_cursor.point) } else { None }; + // Add damage from the terminal. if self.collect_damage() { - self.update_damage(&mut terminal, selection_range, search_state); + match terminal.damage() { + TermDamage::Full => self.damage_tracker.frame().mark_fully_damaged(), + TermDamage::Partial(damaged_lines) => { + for damage in damaged_lines { + self.damage_tracker.frame().damage_line(damage); + } + }, + } + terminal.reset_damage(); } let graphics_queues = terminal.graphics_take_queues(); @@ -806,6 +773,24 @@ impl Display { // Drop terminal as early as possible to free lock. drop(terminal); + // Add damage from alacritty's UI elements overlapping terminal. + if self.collect_damage() { + let requires_full_damage = self.visual_bell.intensity() != 0. + || self.hint_state.active() + || search_state.regex().is_some(); + + if requires_full_damage { + self.damage_tracker.frame().mark_fully_damaged(); + self.damage_tracker.next_frame().mark_fully_damaged(); + } + + let vi_cursor_viewport_point = + vi_cursor_point.and_then(|cursor| point_to_viewport(display_offset, cursor)); + + self.damage_tracker.damage_vi_cursor(vi_cursor_viewport_point); + self.damage_tracker.damage_selection(selection_range, display_offset); + } + // Make sure this window's OpenGL context is active. self.make_current(); @@ -832,6 +817,7 @@ impl Display { let glyph_cache = &mut self.glyph_cache; let highlighted_hint = &self.highlighted_hint; let vi_highlighted_hint = &self.vi_highlighted_hint; + let damage_tracker = &mut self.damage_tracker; self.renderer.draw_cells( &size_info, @@ -854,6 +840,9 @@ impl Display { { show_hint = true; cell.flags.insert(Flags::UNDERLINE); + // Damage hints for the current and next frames. + damage_tracker.frame().damage_point(cell.point); + damage_tracker.next_frame().damage_point(cell.point); } } @@ -885,7 +874,7 @@ impl Display { }; // Draw cursor. - rects.extend(cursor.rects(&size_info, config.terminal_config.cursor.thickness())); + rects.extend(cursor.rects(&size_info, config.cursor.thickness())); // Push visual bell after url/underline/strikeout rects. let visual_bell_intensity = self.visual_bell.intensity(); @@ -923,9 +912,7 @@ impl Display { let fg = config.colors.footer_bar_foreground(); let shape = CursorShape::Underline; let cursor = RenderableCursor::new(Point::new(line, column), shape, fg, false); - rects.extend( - cursor.rects(&size_info, config.terminal_config.cursor.thickness()), - ); + rects.extend(cursor.rects(&size_info, config.cursor.thickness())); } Some(Point::new(line, column)) @@ -950,10 +937,6 @@ impl Display { } } - if self.debug_damage { - self.highlight_damage(&mut rects); - } - if let Some(message) = message_buffer.message() { let search_offset = usize::from(search_state.regex().is_some()); let text = message.text(&size_info); @@ -967,12 +950,18 @@ impl Display { MessageType::Warning => config.colors.normal.yellow, }; + let x = 0; + let width = size_info.width() as i32; + let height = (size_info.height() - y) as i32; let message_bar_rect = - RenderRect::new(0., y, size_info.width(), size_info.height() - y, bg, 1.); + RenderRect::new(x as f32, y, width as f32, height as f32, bg, 1.); // Push message_bar in the end, so it'll be above all other content. rects.push(message_bar_rect); + // Always damage message bar, since it could have messages of the same size in it. + self.damage_tracker.frame().add_viewport_rect(&size_info, x, y as i32, width, height); + // Draw rectangles. self.renderer.draw_rects(&size_info, &metrics, rects); @@ -1003,17 +992,21 @@ impl Display { self.draw_hyperlink_preview(config, cursor_point, display_offset); } - // Frame event should be requested before swapping buffers on Wayland, since it requires - // surface `commit`, which is done by swap buffers under the hood. - if self.is_wayland { - self.request_frame(scheduler); + // Notify winit that we're about to present. + self.window.pre_present_notify(); + + // Highlight damage for debugging. + if self.damage_tracker.debug { + let damage = self.damage_tracker.shape_frame_damage(self.size_info.into()); + let mut rects = Vec::with_capacity(damage.len()); + self.highlight_damage(&mut rects); + self.renderer.draw_rects(&self.size_info, &metrics, rects); } // Clearing debug highlights from the previous frame requires full redraw. self.swap_buffers(); - #[cfg(all(feature = "x11", not(any(target_os = "macos", windows))))] - if !self.is_wayland { + if matches!(self.raw_window_handle, RawWindowHandle::Xcb(_) | RawWindowHandle::Xlib(_)) { // On X11 `swap_buffers` does not block for vsync. However the next OpenGl command // will block to synchronize (this is `glClear` in Alacritty), which causes a // permanent one frame delay. @@ -1022,19 +1015,16 @@ impl Display { // XXX: Request the new frame after swapping buffers, so the // time to finish OpenGL operations is accounted for in the timeout. - if !self.is_wayland { + if !matches!(self.raw_window_handle, RawWindowHandle::Wayland(_)) { self.request_frame(scheduler); } - self.damage_rects.clear(); - - // Append damage rects we've enqueued for the next frame. - mem::swap(&mut self.damage_rects, &mut self.next_frame_damage_rects); + self.damage_tracker.swap_damage(); } /// Update to a new configuration. pub fn update_config(&mut self, config: &UiConfig) { - self.debug_damage = config.debug.highlight_damage; + self.damage_tracker.debug = config.debug.highlight_damage; self.visual_bell.update_config(&config.bell); self.colors = List::from(&config.colors); } @@ -1077,7 +1067,7 @@ impl Display { // highlighted hint could be disrupted by the old preview. dirty = self.hint_mouse_point.map_or(false, |p| p.line != point.line); self.hint_mouse_point = Some(point); - self.window.set_mouse_cursor(CursorIcon::Hand); + self.window.set_mouse_cursor(CursorIcon::Pointer); } else if self.highlighted_hint.is_some() { self.hint_mouse_point = None; if term.mode().intersects(TermMode::MOUSE_MODE) && !term.mode().contains(TermMode::VI) { @@ -1147,10 +1137,11 @@ impl Display { glyph_cache, ); - if self.collect_damage() { - let damage = self.damage_from_point(Point::new(start.line, Column(0)), num_cols as u32); - self.damage_rects.push(damage); - self.next_frame_damage_rects.push(damage); + // Damage preedit inside the terminal viewport. + if self.collect_damage() && point.line < self.size_info.screen_lines() { + let damage = LineDamageBounds::new(start.line, 0, num_cols); + self.damage_tracker.frame().damage_line(damage); + self.damage_tracker.next_frame().damage_line(damage); } // Add underline for preedit text. @@ -1171,9 +1162,7 @@ impl Display { let cursor_point = Point::new(point.line, cursor_column); let cursor = RenderableCursor::new(cursor_point, CursorShape::HollowBlock, fg, is_wide); - rects.extend( - cursor.rects(&self.size_info, config.terminal_config.cursor.thickness()), - ); + rects.extend(cursor.rects(&self.size_info, config.cursor.thickness())); cursor_point }, _ => end, @@ -1230,10 +1219,9 @@ impl Display { // The maximum amount of protected lines including the ones we'll show preview on. let max_protected_lines = uris.len() * 2; - // Lines we shouldn't shouldn't show preview on, because it'll obscure the highlighted - // hint. + // Lines we shouldn't show preview on, because it'll obscure the highlighted hint. let mut protected_lines = Vec::with_capacity(max_protected_lines); - if self.size_info.screen_lines() >= max_protected_lines { + if self.size_info.screen_lines() > max_protected_lines { // Prefer to show preview even when it'll likely obscure the highlighted hint, when // there's no place left for it. protected_lines.push(self.hint_mouse_point.map(|point| point.line)); @@ -1262,11 +1250,11 @@ impl Display { for (uri, point) in uris.into_iter().zip(uri_lines) { // Damage the uri preview. if self.collect_damage() { - let uri_preview_damage = self.damage_from_point(point, num_cols as u32); - self.damage_rects.push(uri_preview_damage); + let damage = LineDamageBounds::new(point.line, point.column.0, num_cols); + self.damage_tracker.frame().damage_line(damage); // Damage the uri preview for the next frame as well. - self.next_frame_damage_rects.push(uri_preview_damage); + self.damage_tracker.next_frame().damage_line(damage); } self.renderer.draw_string(point, fg, bg, uri, &self.size_info, &mut self.glyph_cache); @@ -1308,13 +1296,10 @@ impl Display { let bg = config.colors.normal.red; if self.collect_damage() { - // Damage the entire line. - let render_timer_damage = - self.damage_from_point(point, self.size_info.columns() as u32); - self.damage_rects.push(render_timer_damage); - + let damage = LineDamageBounds::new(point.line, point.column.0, timing.len()); + self.damage_tracker.frame().damage_line(damage); // Damage the render timer for the next frame. - self.next_frame_damage_rects.push(render_timer_damage) + self.damage_tracker.next_frame().damage_line(damage); } let glyph_cache = &mut self.glyph_cache; @@ -1330,27 +1315,16 @@ impl Display { obstructed_column: Option<Column>, line: usize, ) { - const fn num_digits(mut number: u32) -> usize { - let mut res = 0; - loop { - number /= 10; - res += 1; - if number == 0 { - break res; - } - } - } - + let columns = self.size_info.columns(); let text = format!("[{}/{}]", line, total_lines - 1); let column = Column(self.size_info.columns().saturating_sub(text.len())); let point = Point::new(0, column); - // Damage the maximum possible length of the format text, which could be achieved when - // using `MAX_SCROLLBACK_LINES` as current and total lines adding a `3` for formatting. - const MAX_SIZE: usize = 2 * num_digits(MAX_SCROLLBACK_LINES) + 3; - let damage_point = Point::new(0, Column(self.size_info.columns().saturating_sub(MAX_SIZE))); if self.collect_damage() { - self.damage_rects.push(self.damage_from_point(damage_point, MAX_SIZE as u32)); + let damage = LineDamageBounds::new(point.line, point.column.0, columns - 1); + self.damage_tracker.frame().damage_line(damage); + // Damage it on the next frame in case it goes away. + self.damage_tracker.next_frame().damage_line(damage); } let colors = &config.colors; @@ -1364,71 +1338,31 @@ impl Display { } } - /// Damage `len` starting from a `point`. - /// - /// This method also enqueues damage for the next frame automatically. - fn damage_from_point(&self, point: Point<usize>, len: u32) -> DamageRect { - let size_info: SizeInfo<u32> = self.size_info.into(); - let x = size_info.padding_x() + point.column.0 as u32 * size_info.cell_width(); - let y_top = size_info.height() - size_info.padding_y(); - let y = y_top - (point.line as u32 + 1) * size_info.cell_height(); - let width = len * size_info.cell_width(); - DamageRect::new(x as i32, y as i32, width as i32, size_info.cell_height() as i32) - } - - /// Damage currently highlighted `Display` hints. - #[inline] - fn damage_highlighted_hints<T: EventListener>(&self, terminal: &mut Term<T>) { - let display_offset = terminal.grid().display_offset(); - let last_visible_line = terminal.screen_lines() - 1; - for hint in self.highlighted_hint.iter().chain(&self.vi_highlighted_hint) { - for point in - (hint.bounds().start().line.0..=hint.bounds().end().line.0).flat_map(|line| { - term::point_to_viewport(display_offset, Point::new(Line(line), Column(0))) - .filter(|point| point.line <= last_visible_line) - }) - { - terminal.damage_line(point.line, 0, terminal.columns() - 1); - } - } - } - /// Returns `true` if damage information should be collected, `false` otherwise. #[inline] fn collect_damage(&self) -> bool { - self.is_wayland || self.debug_damage + matches!(self.raw_window_handle, RawWindowHandle::Wayland(_)) || self.damage_tracker.debug } /// Highlight damaged rects. /// /// This function is for debug purposes only. fn highlight_damage(&self, render_rects: &mut Vec<RenderRect>) { - for damage_rect in &self.damage_rects { + for damage_rect in &self.damage_tracker.shape_frame_damage(self.size_info.into()) { let x = damage_rect.x as f32; let height = damage_rect.height as f32; let width = damage_rect.width as f32; - let y = self.size_info.height() - damage_rect.y as f32 - height; + let y = damage_y_to_viewport_y(&self.size_info, damage_rect) as f32; let render_rect = RenderRect::new(x, y, width, height, DAMAGE_RECT_COLOR, 0.5); render_rects.push(render_rect); } } - /// Requst a new frame for a window on Wayland. + /// Request a new frame for a window on Wayland. fn request_frame(&mut self, scheduler: &mut Scheduler) { // Mark that we've used a frame. - self.window.has_frame.store(false, Ordering::Relaxed); - - #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] - if let Some(surface) = self.window.wayland_surface() { - let has_frame = self.window.has_frame.clone(); - // Request a new frame. - surface.frame().quick_assign(move |_, _, _| { - has_frame.store(true, Ordering::Relaxed); - }); - - return; - } + self.window.has_frame = false; // Get the display vblank interval. let monitor_vblank_interval = 1_000_000. @@ -1455,7 +1389,7 @@ impl Display { impl Drop for Display { fn drop(&mut self) { // Switch OpenGL context before dropping, otherwise objects (like programs) from other - // contexts might be deleted during droping renderer. + // contexts might be deleted when dropping renderer. self.make_current(); unsafe { ManuallyDrop::drop(&mut self.renderer); @@ -1476,10 +1410,6 @@ pub struct Ime { } impl Ime { - pub fn new() -> Self { - Default::default() - } - #[inline] pub fn set_enabled(&mut self, is_enabled: bool) { if is_enabled { @@ -1661,7 +1591,7 @@ fn window_size( ) -> PhysicalSize<u32> { let padding = config.window.padding(scale_factor); - let grid_width = cell_width * dimensions.columns.0.max(MIN_COLUMNS) as f32; + let grid_width = cell_width * dimensions.columns.max(MIN_COLUMNS) as f32; let grid_height = cell_height * dimensions.lines.max(MIN_SCREEN_LINES) as f32; let width = (padding.0).mul_add(2., grid_width).floor(); diff --git a/alacritty/src/display/window.rs b/alacritty/src/display/window.rs index 962f93a1..e4bfa2cb 100644 --- a/alacritty/src/display/window.rs +++ b/alacritty/src/display/window.rs @@ -1,9 +1,6 @@ -#[rustfmt::skip] -#[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] -use { - wayland_client::protocol::wl_surface::WlSurface, - wayland_client::{Attached, EventQueue, Proxy}, - winit::platform::wayland::{EventLoopWindowTargetExtWayland, WindowExtWayland}, +#[cfg(not(any(target_os = "macos", windows)))] +use winit::platform::startup_notify::{ + self, EventLoopExtStartupNotify, WindowBuilderExtStartupNotify, }; #[cfg(all(not(feature = "x11"), not(any(target_os = "macos", windows))))] @@ -13,17 +10,13 @@ use winit::platform::wayland::WindowBuilderExtWayland; #[cfg(all(feature = "x11", not(any(target_os = "macos", windows))))] use { std::io::Cursor, - - winit::platform::x11::{WindowExtX11, WindowBuilderExtX11}, + winit::platform::x11::{WindowBuilderExtX11, EventLoopWindowTargetExtX11}, glutin::platform::x11::X11VisualInfo, - x11_dl::xlib::{Display as XDisplay, PropModeReplace, XErrorEvent, Xlib}, winit::window::Icon, png::Decoder, }; use std::fmt::{self, Display, Formatter}; -use std::sync::atomic::AtomicBool; -use std::sync::Arc; #[cfg(target_os = "macos")] use { @@ -34,15 +27,14 @@ use { }; use raw_window_handle::{HasRawWindowHandle, RawWindowHandle}; - use winit::dpi::{PhysicalPosition, PhysicalSize}; use winit::event_loop::EventLoopWindowTarget; use winit::monitor::MonitorHandle; #[cfg(windows)] use winit::platform::windows::IconExtWindows; use winit::window::{ - CursorIcon, Fullscreen, ImePurpose, UserAttentionType, Window as WinitWindow, WindowBuilder, - WindowId, + CursorIcon, Fullscreen, ImePurpose, Theme, UserAttentionType, Window as WinitWindow, + WindowBuilder, WindowId, }; use alacritty_terminal::index::Point; @@ -107,20 +99,20 @@ impl From<crossfont::Error> for Error { /// Wraps the underlying windowing library to provide a stable API in Alacritty. pub struct Window { /// Flag tracking that we have a frame we can draw. - pub has_frame: Arc<AtomicBool>, - - /// Attached Wayland surface to request new frame events. - #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] - pub wayland_surface: Option<Attached<WlSurface>>, + pub has_frame: bool, /// Cached scale factor for quickly scaling pixel sizes. pub scale_factor: f64, + /// Flag indicating whether redraw was requested. + pub requested_redraw: bool, + window: WinitWindow, /// Current window title. title: String, + is_x11: bool, current_mouse_cursor: CursorIcon, mouse_visible: bool, } @@ -133,8 +125,9 @@ impl Window { event_loop: &EventLoopWindowTarget<E>, config: &UiConfig, identity: &Identity, - #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] - wayland_event_queue: Option<&EventQueue>, + #[rustfmt::skip] + #[cfg(target_os = "macos")] + tabbing_id: &Option<String>, #[rustfmt::skip] #[cfg(all(feature = "x11", not(any(target_os = "macos", windows))))] x11_visual: Option<X11VisualInfo>, @@ -145,6 +138,8 @@ impl Window { &config.window, #[cfg(all(feature = "x11", not(any(target_os = "macos", windows))))] x11_visual, + #[cfg(target_os = "macos")] + tabbing_id, ); if let Some(position) = config.window.position { @@ -152,21 +147,32 @@ impl Window { .with_position(PhysicalPosition::<i32>::from((position.x, position.y))); } + #[cfg(not(any(target_os = "macos", windows)))] + if let Some(token) = event_loop.read_token_from_env() { + log::debug!("Activating window with token: {token:?}"); + window_builder = window_builder.with_activation_token(token); + + // Remove the token from the env. + startup_notify::reset_activation_token_env(); + } + + // On X11, embed the window inside another if the parent ID has been set. + #[cfg(all(feature = "x11", not(any(target_os = "macos", windows))))] + if let Some(parent_window_id) = event_loop.is_x11().then_some(config.window.embed).flatten() + { + window_builder = window_builder.with_embed_parent_window(parent_window_id); + } + let window = window_builder .with_title(&identity.title) - .with_theme(config.window.decorations_theme_variant) + .with_theme(config.window.theme()) .with_visible(false) .with_transparent(true) + .with_blur(config.window.blur) .with_maximized(config.window.maximized()) .with_fullscreen(config.window.fullscreen()) .build(event_loop)?; - // Check if we're running Wayland to disable vsync. - #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] - let is_wayland = event_loop.is_wayland(); - #[cfg(all(not(feature = "wayland"), not(any(target_os = "macos", windows))))] - let is_wayland = false; - // Text cursor. let current_mouse_cursor = CursorIcon::Text; window.set_cursor_icon(current_mouse_cursor); @@ -181,36 +187,19 @@ impl Window { #[cfg(target_os = "macos")] use_srgb_color_space(&window); - #[cfg(all(feature = "x11", not(any(target_os = "macos", windows))))] - if !is_wayland { - // On X11, embed the window inside another if the parent ID has been set. - if let Some(parent_window_id) = config.window.embed { - x_embed_window(&window, parent_window_id); - } - } - - #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] - let wayland_surface = if is_wayland { - // Attach surface to Alacritty's internal wayland queue to handle frame callbacks. - let surface = window.wayland_surface().unwrap(); - let proxy: Proxy<WlSurface> = unsafe { Proxy::from_c_ptr(surface as _) }; - Some(proxy.attach(wayland_event_queue.as_ref().unwrap().token())) - } else { - None - }; - let scale_factor = window.scale_factor(); log::info!("Window scale factor: {}", scale_factor); + let is_x11 = matches!(window.raw_window_handle(), RawWindowHandle::Xlib(_)); Ok(Self { + requested_redraw: false, + title: identity.title, current_mouse_cursor, mouse_visible: true, - window, - title: identity.title, - has_frame: Arc::new(AtomicBool::new(true)), - #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] - wayland_surface, + has_frame: true, scale_factor, + window, + is_x11, }) } @@ -220,8 +209,8 @@ impl Window { } #[inline] - pub fn set_inner_size(&self, size: PhysicalSize<u32>) { - self.window.set_inner_size(size); + pub fn request_inner_size(&self, size: PhysicalSize<u32>) { + let _ = self.window.request_inner_size(size); } #[inline] @@ -248,8 +237,11 @@ impl Window { } #[inline] - pub fn request_redraw(&self) { - self.window.request_redraw(); + pub fn request_redraw(&mut self) { + if !self.requested_redraw { + self.requested_redraw = true; + self.window.request_redraw(); + } } #[inline] @@ -296,7 +288,7 @@ impl Window { #[cfg(feature = "x11")] let builder = match x11_visual { - Some(visual) => builder.with_x11_visual(visual.into_raw()), + Some(visual) => builder.with_x11_visual(visual.visual_id() as u32), None => builder, }; @@ -313,8 +305,16 @@ impl Window { } #[cfg(target_os = "macos")] - pub fn get_platform_window(_: &Identity, window_config: &WindowConfig) -> WindowBuilder { - let window = WindowBuilder::new().with_option_as_alt(window_config.option_as_alt); + pub fn get_platform_window( + _: &Identity, + window_config: &WindowConfig, + tabbing_id: &Option<String>, + ) -> WindowBuilder { + let mut window = WindowBuilder::new().with_option_as_alt(window_config.option_as_alt()); + + if let Some(tabbing_id) = tabbing_id { + window = window.with_tabbing_identifier(tabbing_id); + } match window_config.decorations { Decorations::Full => window, @@ -345,6 +345,10 @@ impl Window { self.window.set_transparent(transparent); } + pub fn set_blur(&self, blur: bool) { + self.window.set_blur(blur); + } + pub fn set_maximized(&self, maximized: bool) { self.window.set_maximized(maximized); } @@ -367,6 +371,17 @@ impl Window { self.set_maximized(!self.window.is_maximized()); } + /// Inform windowing system about presenting to the window. + /// + /// Should be called right before presenting to the window with e.g. `eglSwapBuffers`. + pub fn pre_present_notify(&self) { + self.window.pre_present_notify(); + } + + pub fn set_theme(&self, theme: Option<Theme>) { + self.window.set_theme(theme); + } + #[cfg(target_os = "macos")] pub fn toggle_simple_fullscreen(&self) { self.set_simple_fullscreen(!self.window.simple_fullscreen()); @@ -394,21 +409,32 @@ impl Window { self.window.set_simple_fullscreen(simple_fullscreen); } - #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] - pub fn wayland_surface(&self) -> Option<&Attached<WlSurface>> { - self.wayland_surface.as_ref() - } - pub fn set_ime_allowed(&self, allowed: bool) { - self.window.set_ime_allowed(allowed); + // Skip runtime IME manipulation on X11 since it breaks some IMEs. + if !self.is_x11 { + self.window.set_ime_allowed(allowed); + } } /// Adjust the IME editor position according to the new location of the cursor. pub fn update_ime_position(&self, point: Point<usize>, size: &SizeInfo) { + // NOTE: X11 doesn't support cursor area, so we need to offset manually to not obscure + // the text. + let offset = if self.is_x11 { 1 } else { 0 }; 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 + 1) as f32 * size.cell_height()); - - self.window.set_ime_position(PhysicalPosition::new(nspot_x, nspot_y)); + let nspot_y = + f64::from(size.padding_y() + (point.line + offset) as f32 * size.cell_height()); + + // NOTE: some compositors don't like excluding too much and try to render popup at the + // bottom right corner of the provided area, so exclude just the full-width char to not + // obscure the cursor and not render popup at the end of the window. + let width = size.cell_width() as f64 * 2.; + let height = size.cell_height as f64; + + self.window.set_ime_cursor_area( + PhysicalPosition::new(nspot_x, nspot_y), + PhysicalSize::new(width, height), + ); } /// Disable macOS window shadows. @@ -426,39 +452,34 @@ impl Window { let _: id = msg_send![raw_window, setHasShadow: value]; } } -} -#[cfg(all(feature = "x11", not(any(target_os = "macos", windows))))] -fn x_embed_window(window: &WinitWindow, parent_id: std::os::raw::c_ulong) { - let (xlib_display, xlib_window) = match (window.xlib_display(), window.xlib_window()) { - (Some(display), Some(window)) => (display, window), - _ => return, - }; - - let xlib = Xlib::open().expect("get xlib"); + /// Select tab at the given `index`. + #[cfg(target_os = "macos")] + pub fn select_tab_at_index(&self, index: usize) { + self.window.select_tab_at_index(index); + } - unsafe { - let atom = (xlib.XInternAtom)(xlib_display as *mut _, "_XEMBED".as_ptr() as *const _, 0); - (xlib.XChangeProperty)( - xlib_display as _, - xlib_window as _, - atom, - atom, - 32, - PropModeReplace, - [0, 1].as_ptr(), - 2, - ); + /// Select the last tab. + #[cfg(target_os = "macos")] + pub fn select_last_tab(&self) { + self.window.select_tab_at_index(self.window.num_tabs() - 1); + } - // Register new error handler. - let old_handler = (xlib.XSetErrorHandler)(Some(xembed_error_handler)); + /// Select next tab. + #[cfg(target_os = "macos")] + pub fn select_next_tab(&self) { + self.window.select_next_tab(); + } - // Check for the existence of the target before attempting reparenting. - (xlib.XReparentWindow)(xlib_display as _, xlib_window as _, parent_id, 0, 0); + /// Select previous tab. + #[cfg(target_os = "macos")] + pub fn select_previous_tab(&self) { + self.window.select_previous_tab(); + } - // Drain errors and restore original error handler. - (xlib.XSync)(xlib_display as _, 0); - (xlib.XSetErrorHandler)(old_handler); + #[cfg(target_os = "macos")] + pub fn tabbing_id(&self) -> String { + self.window.tabbing_identifier() } } @@ -473,9 +494,3 @@ fn use_srgb_color_space(window: &WinitWindow) { let _: () = msg_send![raw_window, setColorSpace: NSColorSpace::sRGBColorSpace(nil)]; } } - -#[cfg(all(feature = "x11", not(any(target_os = "macos", windows))))] -unsafe extern "C" fn xembed_error_handler(_: *mut XDisplay, _: *mut XErrorEvent) -> i32 { - log::error!("Could not embed into specified window."); - std::process::exit(1); -} diff --git a/alacritty/src/event.rs b/alacritty/src/event.rs index 02309188..ff08557b 100644 --- a/alacritty/src/event.rs +++ b/alacritty/src/event.rs @@ -1,7 +1,7 @@ //! Process window events. use std::borrow::Cow; -use std::cmp::{max, min}; +use std::cmp::min; use std::collections::{HashMap, HashSet, VecDeque}; use std::error::Error; use std::ffi::OsStr; @@ -10,39 +10,33 @@ use std::fmt::Debug; use std::os::unix::io::RawFd; use std::path::PathBuf; use std::rc::Rc; -use std::sync::atomic::Ordering; use std::time::{Duration, Instant}; use std::{env, f32, mem}; +use ahash::RandomState; +use crossfont::Size as FontSize; +use glutin::display::{Display as GlutinDisplay, GetGlDisplay}; use log::{debug, error, info, warn}; -#[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] -use wayland_client::{Display as WaylandDisplay, EventQueue}; -use winit::dpi::PhysicalSize; +use raw_window_handle::HasRawDisplayHandle; use winit::event::{ - ElementState, Event as WinitEvent, Ime, ModifiersState, MouseButton, StartCause, + ElementState, Event as WinitEvent, Ime, Modifiers, MouseButton, StartCause, Touch as TouchEvent, WindowEvent, }; use winit::event_loop::{ - ControlFlow, DeviceEventFilter, EventLoop, EventLoopProxy, EventLoopWindowTarget, + ControlFlow, DeviceEvents, EventLoop, EventLoopProxy, EventLoopWindowTarget, }; -use winit::platform::run_return::EventLoopExtRunReturn; -#[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] -use winit::platform::wayland::EventLoopWindowTargetExtWayland; use winit::window::WindowId; -use crossfont::{self, Size}; - -use alacritty_terminal::config::LOG_TARGET_CONFIG; use alacritty_terminal::event::{Event as TerminalEvent, EventListener, Notify}; use alacritty_terminal::event_loop::Notifier; -use alacritty_terminal::grid::{Dimensions, Scroll}; +use alacritty_terminal::grid::{BidirectionalIterator, Dimensions, Scroll}; use alacritty_terminal::index::{Boundary, Column, Direction, Line, Point, Side}; use alacritty_terminal::selection::{Selection, SelectionType}; use alacritty_terminal::term::search::{Match, RegexSearch}; use alacritty_terminal::term::{self, ClipboardType, Term, TermMode}; #[cfg(unix)] -use crate::cli::IpcConfig; +use crate::cli::{IpcConfig, ParsedOptions}; use crate::cli::{Options as CliOptions, WindowOptions}; use crate::clipboard::Clipboard; use crate::config::ui_config::{HintAction, HintInternalAction}; @@ -50,10 +44,12 @@ use crate::config::{self, UiConfig}; #[cfg(not(windows))] use crate::daemon::foreground_process_path; use crate::daemon::spawn_daemon; +use crate::display::color::Rgb; use crate::display::hint::HintMatch; use crate::display::window::Window; use crate::display::{Display, Preedit, SizeInfo}; use crate::input::{self, ActionContext as _, FONT_SIZE_STEP}; +use crate::logging::LOG_TARGET_CONFIG; use crate::message_bar::{Message, MessageBuffer}; use crate::scheduler::{Scheduler, TimerId, Topic}; use crate::window_context::WindowContext; @@ -86,7 +82,7 @@ impl Event { } } -impl From<Event> for WinitEvent<'_, Event> { +impl From<Event> for WinitEvent<Event> { fn from(event: Event) -> Self { WinitEvent::UserEvent(event) } @@ -95,7 +91,6 @@ impl From<Event> for WinitEvent<'_, Event> { /// Alacritty events. #[derive(Debug, Clone)] pub enum EventType { - ScaleFactorChanged(f64, (u32, u32)), Terminal(TerminalEvent), ConfigReload(PathBuf), Message(Message), @@ -160,9 +155,14 @@ impl SearchState { self.focused_match.as_ref() } + /// Clear the focused match. + pub fn clear_focused_match(&mut self) { + self.focused_match = None; + } + /// Active search dfas. - pub fn dfas(&self) -> Option<&RegexSearch> { - self.dfas.as_ref() + pub fn dfas(&mut self) -> Option<&mut RegexSearch> { + self.dfas.as_mut() } /// Search regex text if a search is active. @@ -185,15 +185,34 @@ impl Default for SearchState { } } +/// Vi inline search state. +pub struct InlineSearchState { + /// Whether inline search is currently waiting for search character input. + pub char_pending: bool, + pub character: Option<char>, + + direction: Direction, + stop_short: bool, +} + +impl Default for InlineSearchState { + fn default() -> Self { + Self { + direction: Direction::Right, + char_pending: Default::default(), + stop_short: Default::default(), + character: Default::default(), + } + } +} + pub struct ActionContext<'a, N, T> { pub notifier: &'a mut N, pub terminal: &'a mut Term<T>, pub clipboard: &'a mut Clipboard, pub mouse: &'a mut Mouse, pub touch: &'a mut TouchPurpose, - pub received_count: &'a mut usize, - pub suppress_chars: &'a mut bool, - pub modifiers: &'a mut ModifiersState, + pub modifiers: &'a mut Modifiers, pub display: &'a mut Display, pub message_buffer: &'a mut MessageBuffer, pub config: &'a UiConfig, @@ -202,7 +221,7 @@ pub struct ActionContext<'a, N, T> { pub event_proxy: &'a EventLoopProxy<Event>, pub scheduler: &'a mut Scheduler, pub search_state: &'a mut SearchState, - pub font_size: &'a mut Size, + pub inline_search_state: &'a mut InlineSearchState, pub dirty: &'a mut bool, pub occluded: &'a mut bool, pub preserve_title: bool, @@ -232,6 +251,7 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon fn scroll(&mut self, scroll: Scroll) { let old_offset = self.terminal.grid().display_offset() as i32; + let old_vi_cursor = self.terminal.vi_mode_cursor; self.terminal.scroll_display(scroll); let lines_changed = old_offset - self.terminal.grid().display_offset() as i32; @@ -241,10 +261,10 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon self.search_state.display_offset_delta += lines_changed; } + let vi_mode = self.terminal.mode().contains(TermMode::VI); + // Update selection. - if self.terminal.mode().contains(TermMode::VI) - && self.terminal.selection.as_ref().map_or(false, |s| !s.is_empty()) - { + if vi_mode && self.terminal.selection.as_ref().map_or(false, |s| !s.is_empty()) { self.update_selection(self.terminal.vi_mode_cursor.point, Side::Right); } else if self.mouse.left_button_state == ElementState::Pressed || self.mouse.right_button_state == ElementState::Pressed @@ -254,8 +274,14 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon self.update_selection(point, self.mouse.cell_side); } - // Update dirty if actually scrolled or we're in the Vi mode. - *self.dirty |= lines_changed != 0; + // Scrolling inside Vi mode moves the cursor, so start typing. + if vi_mode { + self.on_typing_start(); + } + + // Update dirty if actually scrolled or moved Vi cursor in Vi mode. + *self.dirty |= + lines_changed != 0 || (vi_mode && old_vi_cursor != self.terminal.vi_mode_cursor); } // Copy text selection. @@ -265,8 +291,7 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon None => return, }; - if ty == ClipboardType::Selection && self.config.terminal_config.selection.save_to_clipboard - { + if ty == ClipboardType::Selection && self.config.selection.save_to_clipboard { self.clipboard.store(ClipboardType::Clipboard, text.clone()); } self.clipboard.store(ty, text); @@ -349,17 +374,7 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon } #[inline] - fn received_count(&mut self) -> &mut usize { - self.received_count - } - - #[inline] - fn suppress_chars(&mut self) -> &mut bool { - self.suppress_chars - } - - #[inline] - fn modifiers(&mut self) -> &mut ModifiersState { + fn modifiers(&mut self) -> &mut Modifiers { self.modifiers } @@ -411,12 +426,17 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon } #[cfg(not(windows))] - fn create_new_window(&mut self) { + fn create_new_window(&mut self, #[cfg(target_os = "macos")] tabbing_id: Option<String>) { let mut options = WindowOptions::default(); if let Ok(working_directory) = foreground_process_path(self.master_fd, self.shell_pid) { options.terminal_options.working_directory = Some(working_directory); } + #[cfg(target_os = "macos")] + { + options.window_tabbing_id = tabbing_id; + } + let _ = self.event_proxy.send_event(Event::new(EventType::CreateWindow(options), None)); } @@ -444,14 +464,19 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon } 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.font.clone().with_size(*self.font_size); + // Round to pick integral px steps, since fonts look better on them. + let new_size = self.display.font_size.as_px().round() + delta; + self.display.font_size = FontSize::from_px(new_size); + let font = self.config.font.clone().with_size(self.display.font_size); self.display.pending_update.set_font(font); } fn reset_font_size(&mut self) { - *self.font_size = self.config.font.size(); - self.display.pending_update.set_font(self.config.font.clone()); + let scale_factor = self.display.window.scale_factor as f32; + self.display.font_size = self.config.font.size().scale(scale_factor); + self.display + .pending_update + .set_font(self.config.font.clone().with_size(self.display.font_size)); } #[inline] @@ -465,7 +490,7 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon #[inline] fn start_search(&mut self, direction: Direction) { // Only create new history entry if the previous regex wasn't empty. - if self.search_state.history.get(0).map_or(true, |regex| !regex.is_empty()) { + if self.search_state.history.front().map_or(true, |regex| !regex.is_empty()) { self.search_state.history.push_front(String::new()); self.search_state.history.truncate(MAX_SEARCH_HISTORY_SIZE); } @@ -496,6 +521,7 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon // Enable IME so we can input into the search bar with it if we were in Vi mode. self.window().set_ime_allowed(true); + self.display.damage_tracker.frame().mark_fully_damaged(); self.display.pending_update.dirty = true; } @@ -650,7 +676,7 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon fn search_next(&mut self, origin: Point, direction: Direction, side: Side) -> Option<Match> { self.search_state .dfas - .as_ref() + .as_mut() .and_then(|dfas| self.terminal.search_next(dfas, origin, direction, side, None)) } @@ -724,9 +750,7 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon self.clipboard.store(ClipboardType::Clipboard, text); }, // Write the text to the PTY/search. - HintAction::Action(HintInternalAction::Paste) => { - self.paste(&text); - }, + HintAction::Action(HintInternalAction::Paste) => self.paste(&text, true), // Select the text. HintAction::Action(HintInternalAction::Select) => { self.start_selection(SelectionType::Simple, *hint_bounds.start(), Side::Left); @@ -751,7 +775,7 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon fn expand_selection(&mut self) { let selection_type = match self.mouse().click_state { ClickState::Click => { - if self.modifiers().ctrl() { + if self.modifiers().state().control_key() { SelectionType::Block } else { SelectionType::Simple @@ -782,32 +806,56 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon } } + /// Handle beginning of terminal text input. + fn on_terminal_input_start(&mut self) { + self.on_typing_start(); + self.clear_selection(); + + if self.terminal().grid().display_offset() != 0 { + self.scroll(Scroll::Bottom); + } + } + /// Paste a text into the terminal. - fn paste(&mut self, text: &str) { + fn paste(&mut self, text: &str, bracketed: bool) { if self.search_active() { for c in text.chars() { self.search_input(c); } - } else if self.terminal().mode().contains(TermMode::BRACKETED_PASTE) { + } else if bracketed && self.terminal().mode().contains(TermMode::BRACKETED_PASTE) { + self.on_terminal_input_start(); + self.write_to_pty(&b"\x1b[200~"[..]); // Write filtered escape sequences. // // We remove `\x1b` to ensure it's impossible for the pasted text to write the bracketed // paste end escape `\x1b[201~` and `\x03` since some shells incorrectly terminate - // bracketed paste on its receival. - let filtered = text.replace('\x1b', "").replace('\x03', ""); + // bracketed paste when they receive it. + let filtered = text.replace(['\x1b', '\x03'], ""); self.write_to_pty(filtered.into_bytes()); self.write_to_pty(&b"\x1b[201~"[..]); } else { - // In non-bracketed (ie: normal) mode, terminal applications cannot distinguish - // pasted data from keystrokes. - // In theory, we should construct the keystrokes needed to produce the data we are - // pasting... since that's neither practical nor sensible (and probably an impossible - // task to solve in a general way), we'll just replace line breaks (windows and unix - // style) with a single carriage return (\r, which is what the Enter key produces). - self.write_to_pty(text.replace("\r\n", "\r").replace('\n', "\r").into_bytes()); + self.on_terminal_input_start(); + + let payload = if bracketed { + // In non-bracketed (ie: normal) mode, terminal applications cannot distinguish + // pasted data from keystrokes. + // + // In theory, we should construct the keystrokes needed to produce the data we are + // pasting... since that's neither practical nor sensible (and probably an + // impossible task to solve in a general way), we'll just replace line breaks + // (windows and unix style) with a single carriage return (\r, which is what the + // Enter key produces). + text.replace("\r\n", "\r").replace('\n', "\r").into_bytes() + } else { + // When we explicitly disable bracketed paste don't manipulate with the input, + // so we pass user input as is. + text.to_owned().into_bytes() + }; + + self.write_to_pty(payload); } } @@ -819,10 +867,7 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon // If we had search running when leaving Vi mode we should mark terminal fully damaged // to cleanup highlighted results. if self.search_state.dfas.take().is_some() { - self.terminal.mark_fully_damaged(); - } else { - // Damage line indicator. - self.terminal.damage_line(0, 0, self.terminal.columns() - 1); + self.display.damage_tracker.frame().mark_fully_damaged(); } } else { self.clear_selection(); @@ -840,6 +885,30 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon *self.dirty = true; } + /// Get vi inline search state. + fn inline_search_state(&mut self) -> &mut InlineSearchState { + self.inline_search_state + } + + /// Start vi mode inline search. + fn start_inline_search(&mut self, direction: Direction, stop_short: bool) { + self.inline_search_state.stop_short = stop_short; + self.inline_search_state.direction = direction; + self.inline_search_state.char_pending = true; + } + + /// Jump to the next matching character in the line. + fn inline_search_next(&mut self) { + let direction = self.inline_search_state.direction; + self.inline_search(direction); + } + + /// Jump to the next matching character in the line. + fn inline_search_previous(&mut self) { + let direction = self.inline_search_state.direction.opposite(); + self.inline_search(direction); + } + fn message(&self) -> Option<&Message> { self.message_buffer.message() } @@ -914,7 +983,7 @@ impl<'a, N: Notify + 'a, T: EventListener> ActionContext<'a, N, T> { /// Jump to the first regex match from the search origin. fn goto_match(&mut self, mut limit: Option<usize>) { - let dfas = match &self.search_state.dfas { + let dfas = match &mut self.search_state.dfas { Some(dfas) => dfas, None => return, }; @@ -971,6 +1040,7 @@ impl<'a, N: Notify + 'a, T: EventListener> ActionContext<'a, N, T> { let vi_mode = self.terminal.mode().contains(TermMode::VI); self.window().set_ime_allowed(!vi_mode); + self.display.damage_tracker.frame().mark_fully_damaged(); self.display.pending_update.dirty = true; self.search_state.history_index = None; @@ -981,10 +1051,10 @@ impl<'a, N: Notify + 'a, T: EventListener> ActionContext<'a, N, T> { /// Update the cursor blinking state. fn update_cursor_blinking(&mut self) { // Get config cursor style. - let mut cursor_style = self.config.terminal_config.cursor.style; + let mut cursor_style = self.config.cursor.style; let vi_mode = self.terminal.mode().contains(TermMode::VI); if vi_mode { - cursor_style = self.config.terminal_config.cursor.vi_mode_style.unwrap_or(cursor_style); + cursor_style = self.config.cursor.vi_mode_style.unwrap_or(cursor_style); } // Check terminal cursor style. @@ -1014,23 +1084,56 @@ impl<'a, N: Notify + 'a, T: EventListener> ActionContext<'a, N, T> { let window_id = self.display.window.id(); let timer_id = TimerId::new(Topic::BlinkCursor, window_id); let event = Event::new(EventType::BlinkCursor, window_id); - let blinking_interval = - Duration::from_millis(self.config.terminal_config.cursor.blink_interval()); + let blinking_interval = Duration::from_millis(self.config.cursor.blink_interval()); self.scheduler.schedule(event, blinking_interval, true, timer_id); } fn schedule_blinking_timeout(&mut self) { - let blinking_timeout = self.config.terminal_config.cursor.blink_timeout(); - if blinking_timeout == 0 { + let blinking_timeout = self.config.cursor.blink_timeout(); + if blinking_timeout == Duration::ZERO { return; } let window_id = self.display.window.id(); - let blinking_timeout_interval = Duration::from_secs(blinking_timeout); let event = Event::new(EventType::BlinkCursorTimeout, window_id); let timer_id = TimerId::new(Topic::BlinkTimeout, window_id); - self.scheduler.schedule(event, blinking_timeout_interval, false, timer_id); + self.scheduler.schedule(event, blinking_timeout, false, timer_id); + } + + /// Perform vi mode inline search in the specified direction. + fn inline_search(&mut self, direction: Direction) { + let c = match self.inline_search_state.character { + Some(c) => c, + None => return, + }; + let mut buf = [0; 4]; + let search_character = c.encode_utf8(&mut buf); + + // Find next match in this line. + let vi_point = self.terminal.vi_mode_cursor.point; + let point = match direction { + Direction::Right => self.terminal.inline_search_right(vi_point, search_character), + Direction::Left => self.terminal.inline_search_left(vi_point, search_character), + }; + + // Jump to point if there's a match. + if let Ok(mut point) = point { + if self.inline_search_state.stop_short { + let grid = self.terminal.grid(); + point = match direction { + Direction::Right => { + grid.iter_from(point).prev().map_or(point, |cell| cell.point) + }, + Direction::Left => { + grid.iter_from(point).next().map_or(point, |cell| cell.point) + }, + }; + } + + self.terminal.vi_goto_point(point); + self.mark_dirty(); + } } } @@ -1042,7 +1145,7 @@ pub enum TouchPurpose { Scroll(TouchEvent), Zoom(TouchZoom), Tap(TouchEvent), - Invalid(HashSet<u64>), + Invalid(HashSet<u64, RandomState>), } impl Default for TouchPurpose { @@ -1083,8 +1186,8 @@ impl TouchZoom { } /// Get active touch slots. - pub fn slots(&self) -> HashSet<u64> { - let mut set = HashSet::new(); + pub fn slots(&self) -> HashSet<u64, RandomState> { + let mut set = HashSet::default(); set.insert(self.slots.0.id); set.insert(self.slots.1.id); set @@ -1175,29 +1278,18 @@ pub struct AccumulatedScroll { impl input::Processor<EventProxy, ActionContext<'_, Notifier, EventProxy>> { /// Handle events from winit. - pub fn handle_event(&mut self, event: WinitEvent<'_, Event>) { + pub fn handle_event(&mut self, event: WinitEvent<Event>) { match event { WinitEvent::UserEvent(Event { payload, .. }) => match payload { - EventType::ScaleFactorChanged(scale_factor, (width, height)) => { - let display_update_pending = &mut self.ctx.display.pending_update; - - // Push current font to update its scale factor. - let font = self.ctx.config.font.clone(); - display_update_pending.set_font(font.with_size(*self.ctx.font_size)); - - // Resize to event's dimensions, since no resize event is emitted on Wayland. - display_update_pending.set_dimensions(PhysicalSize::new(width, height)); - - self.ctx.window().scale_factor = scale_factor; - }, - EventType::Frame => { - self.ctx.display.window.has_frame.store(true, Ordering::Relaxed); - }, EventType::SearchNext => self.ctx.goto_match(None), EventType::Scroll(scroll) => self.ctx.scroll(scroll), EventType::BlinkCursor => { - self.ctx.display.cursor_hidden ^= true; - *self.ctx.dirty = true; + // Only change state when timeout isn't reached, since we could get + // BlinkCursor and BlinkCursorTimeout events at the same time. + if !*self.ctx.cursor_blink_timed_out { + self.ctx.display.cursor_hidden ^= true; + *self.ctx.dirty = true; + } }, EventType::BlinkCursorTimeout => { // Disable blinking after timeout reached. @@ -1207,7 +1299,8 @@ impl input::Processor<EventProxy, ActionContext<'_, Notifier, EventProxy>> { self.ctx.display.cursor_hidden = false; *self.ctx.dirty = true; }, - EventType::Message(message) => { + // Add message only if it's not already queued. + EventType::Message(message) if !self.ctx.message_buffer.is_queued(&message) => { self.ctx.message_buffer.push(message); self.ctx.display.pending_update.dirty = true; }, @@ -1223,12 +1316,11 @@ impl input::Processor<EventProxy, ActionContext<'_, Notifier, EventProxy>> { self.ctx.display.window.set_title(window_config.identity.title.clone()); } }, - TerminalEvent::Wakeup => *self.ctx.dirty = true, TerminalEvent::Bell => { - // Set window urgency. - if self.ctx.terminal.mode().contains(TermMode::URGENCY_HINTS) { - let focused = self.ctx.terminal.is_focused; - self.ctx.window().set_urgent(!focused); + // Set window urgency hint when window is not focused. + let focused = self.ctx.terminal.is_focused; + if !focused && self.ctx.terminal.mode().contains(TermMode::URGENCY_HINTS) { + self.ctx.window().set_urgent(true); } // Ring visual bell. @@ -1252,8 +1344,9 @@ impl input::Processor<EventProxy, ActionContext<'_, Notifier, EventProxy>> { }, TerminalEvent::ColorRequest(index, format) => { let color = self.ctx.terminal().colors()[index] + .map(Rgb) .unwrap_or(self.ctx.display.colors[index]); - self.ctx.write_to_pty(format(color).into_bytes()); + self.ctx.write_to_pty(format(color.0).into_bytes()); }, TerminalEvent::TextAreaSizeRequest(format) => { let text = format(self.ctx.size_info().into()); @@ -1261,17 +1354,32 @@ impl input::Processor<EventProxy, ActionContext<'_, Notifier, EventProxy>> { }, TerminalEvent::PtyWrite(text) => self.ctx.write_to_pty(text.into_bytes()), TerminalEvent::MouseCursorDirty => self.reset_mouse_cursor(), - TerminalEvent::Exit => (), TerminalEvent::CursorBlinkingChange => self.ctx.update_cursor_blinking(), + TerminalEvent::Exit | TerminalEvent::Wakeup => (), }, #[cfg(unix)] EventType::IpcConfig(_) => (), - EventType::ConfigReload(_) | EventType::CreateWindow(_) => (), + EventType::Message(_) + | EventType::ConfigReload(_) + | EventType::CreateWindow(_) + | EventType::Frame => (), }, - WinitEvent::RedrawRequested(_) => *self.ctx.dirty = true, WinitEvent::WindowEvent { event, .. } => { match event { WindowEvent::CloseRequested => self.ctx.terminal.exit(), + WindowEvent::ScaleFactorChanged { scale_factor, .. } => { + let old_scale_factor = + mem::replace(&mut self.ctx.window().scale_factor, scale_factor); + + let display_update_pending = &mut self.ctx.display.pending_update; + + // Rescale font size for the new factor. + let font_scale = scale_factor as f32 / old_scale_factor as f32; + self.ctx.display.font_size = self.ctx.display.font_size.scale(font_scale); + + let font = self.ctx.config.font.clone(); + display_update_pending.set_font(font.with_size(self.ctx.display.font_size)); + }, WindowEvent::Resized(size) => { // Ignore resize events to zero in any dimension, to avoid issues with Winit // and the ConPTY. A 0x0 resize will also occur when the window is minimized @@ -1282,11 +1390,10 @@ impl input::Processor<EventProxy, ActionContext<'_, Notifier, EventProxy>> { self.ctx.display.pending_update.set_dimensions(size); }, - WindowEvent::KeyboardInput { input, is_synthetic: false, .. } => { - self.key_input(input); + WindowEvent::KeyboardInput { event, is_synthetic: false, .. } => { + self.key_input(event); }, WindowEvent::ModifiersChanged(modifiers) => self.modifiers_input(modifiers), - WindowEvent::ReceivedCharacter(c) => self.received_char(c), WindowEvent::MouseInput { state, button, .. } => { self.ctx.window().set_mouse_visible(true); self.mouse_input(state, button); @@ -1304,10 +1411,11 @@ impl input::Processor<EventProxy, ActionContext<'_, Notifier, EventProxy>> { self.ctx.terminal.is_focused = is_focused; // When the unfocused hollow is used we must redraw on focus change. - if self.ctx.config.terminal_config.cursor.unfocused_hollow { + if self.ctx.config.cursor.unfocused_hollow { *self.ctx.dirty = true; } + // Reset the urgency hint when gaining focus. if is_focused { self.ctx.window().set_urgent(false); } @@ -1320,7 +1428,7 @@ impl input::Processor<EventProxy, ActionContext<'_, Notifier, EventProxy>> { }, WindowEvent::DroppedFile(path) => { let path: String = path.to_string_lossy().into(); - self.ctx.paste(&(path + " ")); + self.ctx.paste(&(path + " "), true); }, WindowEvent::CursorLeft { .. } => { self.ctx.mouse.inside_text_area = false; @@ -1332,11 +1440,8 @@ impl input::Processor<EventProxy, ActionContext<'_, Notifier, EventProxy>> { WindowEvent::Ime(ime) => match ime { Ime::Commit(text) => { *self.ctx.dirty = true; - - for ch in text.chars() { - self.received_char(ch); - } - + // Don't use bracketed paste for single char input. + self.ctx.paste(&text, text.chars().count() > 1); self.ctx.update_cursor_blinking(); }, Ime::Preedit(text, cursor_offset) => { @@ -1362,27 +1467,28 @@ impl input::Processor<EventProxy, ActionContext<'_, Notifier, EventProxy>> { }, }, WindowEvent::KeyboardInput { is_synthetic: true, .. } + | WindowEvent::ActivationTokenDone { .. } | WindowEvent::TouchpadPressure { .. } | WindowEvent::TouchpadMagnify { .. } | WindowEvent::TouchpadRotate { .. } | WindowEvent::SmartMagnify { .. } - | WindowEvent::ScaleFactorChanged { .. } | WindowEvent::CursorEntered { .. } | WindowEvent::AxisMotion { .. } | WindowEvent::HoveredFileCancelled | WindowEvent::Destroyed | WindowEvent::ThemeChanged(_) | WindowEvent::HoveredFile(_) + | WindowEvent::RedrawRequested | WindowEvent::Moved(_) => (), } }, WinitEvent::Suspended { .. } | WinitEvent::NewEvents { .. } | WinitEvent::DeviceEvent { .. } - | WinitEvent::MainEventsCleared - | WinitEvent::RedrawEventsCleared + | WinitEvent::LoopExiting | WinitEvent::Resumed - | WinitEvent::LoopDestroyed => (), + | WinitEvent::MemoryWarning + | WinitEvent::AboutToWait => (), } } } @@ -1392,9 +1498,10 @@ impl input::Processor<EventProxy, ActionContext<'_, Notifier, EventProxy>> { /// Stores some state from received events and dispatches actions when they are /// triggered. pub struct Processor { - #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] - wayland_event_queue: Option<EventQueue>, - windows: HashMap<WindowId, WindowContext>, + windows: HashMap<WindowId, WindowContext, RandomState>, + gl_display: Option<GlutinDisplay>, + #[cfg(unix)] + global_ipc_options: ParsedOptions, cli_options: CliOptions, config: Rc<UiConfig>, } @@ -1408,19 +1515,13 @@ impl Processor { cli_options: CliOptions, _event_loop: &EventLoop<Event>, ) -> Processor { - // Initialize Wayland event queue, to handle Wayland callbacks. - #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] - let wayland_event_queue = _event_loop.wayland_display().map(|display| { - let display = unsafe { WaylandDisplay::from_external_display(display as _) }; - display.create_event_queue() - }); - Processor { - windows: HashMap::new(), - config: Rc::new(config), cli_options, - #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] - wayland_event_queue, + gl_display: None, + config: Rc::new(config), + windows: Default::default(), + #[cfg(unix)] + global_ipc_options: Default::default(), } } @@ -1434,15 +1535,10 @@ impl Processor { proxy: EventLoopProxy<Event>, options: WindowOptions, ) -> Result<(), Box<dyn Error>> { - let window_context = WindowContext::initial( - event_loop, - proxy, - self.config.clone(), - options, - #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] - self.wayland_event_queue.as_ref(), - )?; + let window_context = + WindowContext::initial(event_loop, proxy, self.config.clone(), options)?; + self.gl_display = Some(window_context.display.gl_context().display()); self.windows.insert(window_context.id(), window_context); Ok(()) @@ -1456,14 +1552,17 @@ impl Processor { options: WindowOptions, ) -> Result<(), Box<dyn Error>> { let window = self.windows.iter().next().as_ref().unwrap().1; - let window_context = window.additional( - event_loop, - proxy, - self.config.clone(), - options, - #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] - self.wayland_event_queue.as_ref(), - )?; + + // Overide config with CLI/IPC options. + let mut config_overrides = options.config_overrides(); + #[cfg(unix)] + config_overrides.extend_from_slice(&self.global_ipc_options); + let mut config = self.config.clone(); + config = config_overrides.override_config_rc(config); + + #[allow(unused_mut)] + let mut window_context = + window.additional(event_loop, proxy, config, options, config_overrides)?; self.windows.insert(window_context.id(), window_context); Ok(()) @@ -1474,23 +1573,22 @@ impl Processor { /// The result is exit code generate from the loop. pub fn run( &mut self, - mut event_loop: EventLoop<Event>, + event_loop: EventLoop<Event>, initial_window_options: WindowOptions, ) -> Result<(), Box<dyn Error>> { let proxy = event_loop.create_proxy(); let mut scheduler = Scheduler::new(proxy.clone()); let mut initial_window_options = Some(initial_window_options); - // NOTE: Since this takes a pointer to the winit event loop, it MUST be dropped first. - #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] - let mut clipboard = unsafe { Clipboard::new(event_loop.wayland_display()) }; - #[cfg(any(not(feature = "wayland"), target_os = "macos", windows))] - let mut clipboard = Clipboard::new(); - // Disable all device events, since we don't care about them. - event_loop.set_device_event_filter(DeviceEventFilter::Always); - - let exit_code = event_loop.run_return(move |event, event_loop, control_flow| { + event_loop.listen_device_events(DeviceEvents::Never); + + let mut initial_window_error = Ok(()); + let initial_window_error_loop = &mut initial_window_error; + // SAFETY: Since this takes a pointer to the winit event loop, it MUST be dropped first, + // which is done by `move` into event loop. + let mut clipboard = unsafe { Clipboard::new(event_loop.raw_display_handle()) }; + let result = event_loop.run(move |event, event_loop| { if self.config.debug.print_events { info!("winit event: {:?}", event); } @@ -1504,8 +1602,8 @@ impl Processor { // The event loop just got initialized. Create a window. WinitEvent::Resumed => { // Creating window inside event loop is required for platforms like macOS to - // properly initialize state, like tab management. Othwerwise the first window - // won't handle tabs. + // properly initialize state, like tab management. Otherwise the first + // window won't handle tabs. let initial_window_options = match initial_window_options.take() { Some(initial_window_options) => initial_window_options, None => return, @@ -1516,14 +1614,54 @@ impl Processor { proxy.clone(), initial_window_options, ) { - // Log the error right away since we can't return it. - eprintln!("Error: {}", err); - *control_flow = ControlFlow::ExitWithCode(1); + *initial_window_error_loop = Err(err); + event_loop.exit(); return; } info!("Initialisation complete"); }, + WinitEvent::LoopExiting => { + match self.gl_display.take() { + #[cfg(not(target_os = "macos"))] + Some(glutin::display::Display::Egl(display)) => { + // Ensure that all the windows are dropped, so the destructors for + // Renderer and contexts ran. + self.windows.clear(); + + // SAFETY: the display is being destroyed after destroying all the + // windows, thus no attempt to access the EGL state will be made. + unsafe { + display.terminate(); + } + }, + _ => (), + } + }, + // NOTE: This event bypasses batching to minimize input latency. + WinitEvent::UserEvent(Event { + window_id: Some(window_id), + payload: EventType::Terminal(TerminalEvent::Wakeup), + }) => { + if let Some(window_context) = self.windows.get_mut(&window_id) { + window_context.dirty = true; + if window_context.display.window.has_frame { + window_context.display.window.request_redraw(); + } + } + }, + // NOTE: This event bypasses batching to minimize input latency. + WinitEvent::UserEvent(Event { + window_id: Some(window_id), + payload: EventType::Frame, + }) => { + if let Some(window_context) = self.windows.get_mut(&window_id) { + window_context.display.window.has_frame = true; + if window_context.dirty { + window_context.display.window.request_redraw(); + } + } + }, // Check for shutdown. WinitEvent::UserEvent(Event { window_id: Some(window_id), @@ -1545,19 +1683,27 @@ impl Processor { window_context.write_ref_test_results(); } - *control_flow = ControlFlow::Exit; + event_loop.exit(); } }, - // Process all pending events. - WinitEvent::RedrawEventsCleared => { - // Check for pending frame callbacks on Wayland. - #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] - if let Some(wayland_event_queue) = self.wayland_event_queue.as_mut() { - wayland_event_queue - .dispatch_pending(&mut (), |_, _, _| {}) - .expect("failed to dispatch wayland event queue"); - } + WinitEvent::WindowEvent { window_id, event: WindowEvent::RedrawRequested } => { + let window_context = match self.windows.get_mut(&window_id) { + Some(window_context) => window_context, + None => return, + }; + window_context.handle_event( + event_loop, + &proxy, + &mut clipboard, + &mut scheduler, + event, + ); + + window_context.draw(&mut scheduler); + }, + // Process all pending events. + WinitEvent::AboutToWait => { // Dispatch event to all windows. for window_context in self.windows.values_mut() { window_context.handle_event( @@ -1565,16 +1711,17 @@ impl Processor { &proxy, &mut clipboard, &mut scheduler, - WinitEvent::RedrawEventsCleared, + WinitEvent::AboutToWait, ); } // Update the scheduler after event processing to ensure // the event loop deadline is as accurate as possible. - *control_flow = match scheduler.update() { + let control_flow = match scheduler.update() { Some(instant) => ControlFlow::WaitUntil(instant), None => ControlFlow::Wait, }; + event_loop.set_control_flow(control_flow); }, // Process config update. WinitEvent::UserEvent(Event { payload: EventType::ConfigReload(path), .. }) => { @@ -1587,7 +1734,7 @@ impl Processor { } // Load config and update each terminal. - if let Ok(config) = config::reload(&path, &self.cli_options) { + if let Ok(config) = config::reload(&path, &mut self.cli_options) { self.config = Rc::new(config); for window_context in self.windows.values_mut() { @@ -1601,20 +1748,38 @@ impl Processor { payload: EventType::IpcConfig(ipc_config), window_id, }) => { + // Try and parse options as toml. + let mut options = ParsedOptions::from_options(&ipc_config.options); + + // Override IPC config for each window with matching ID. for (_, window_context) in self .windows .iter_mut() .filter(|(id, _)| window_id.is_none() || window_id == Some(**id)) { - window_context.update_ipc_config(self.config.clone(), ipc_config.clone()); + if ipc_config.reset { + window_context.reset_window_config(self.config.clone()); + } else { + window_context.add_window_config(self.config.clone(), &options); + } + } + + // Persist global options for future windows. + if window_id.is_none() { + if ipc_config.reset { + self.global_ipc_options.clear(); + } else { + self.global_ipc_options.append(&mut options); + } } }, // Create a new terminal window. WinitEvent::UserEvent(Event { payload: EventType::CreateWindow(options), .. }) => { - // XXX Ensure that no context is current when creating a new window, otherwise - // it may lock the backing buffer of the surface of current context when asking + // XXX Ensure that no context is current when creating a new window, + // otherwise it may lock the backing buffer of the + // surface of current context when asking // e.g. EGL on Wayland to create a new context. for window_context in self.windows.values_mut() { window_context.display.make_not_current(); @@ -1638,8 +1803,7 @@ impl Processor { }, // Process window-specific events. WinitEvent::WindowEvent { window_id, .. } - | WinitEvent::UserEvent(Event { window_id: Some(window_id), .. }) - | WinitEvent::RedrawRequested(window_id) => { + | WinitEvent::UserEvent(Event { window_id: Some(window_id), .. }) => { if let Some(window_context) = self.windows.get_mut(&window_id) { window_context.handle_event( event_loop, @@ -1654,15 +1818,15 @@ impl Processor { } }); - if exit_code == 0 { - Ok(()) + if initial_window_error.is_err() { + initial_window_error } else { - Err(format!("Event loop terminated with code: {}", exit_code).into()) + result.map_err(Into::into) } } /// Check if an event is irrelevant and can be skipped. - fn skip_event(event: &WinitEvent<'_, Event>) -> bool { + fn skip_event(event: &WinitEvent<Event>) -> bool { match event { WinitEvent::NewEvents(StartCause::Init) => false, WinitEvent::WindowEvent { event, .. } => matches!( @@ -1676,10 +1840,7 @@ impl Processor { | WindowEvent::HoveredFile(_) | WindowEvent::Moved(_) ), - WinitEvent::Suspended { .. } - | WinitEvent::NewEvents { .. } - | WinitEvent::MainEventsCleared - | WinitEvent::LoopDestroyed => true, + WinitEvent::Suspended { .. } | WinitEvent::NewEvents { .. } => true, _ => false, } } diff --git a/alacritty/src/input/keyboard.rs b/alacritty/src/input/keyboard.rs new file mode 100644 index 00000000..b7635bd9 --- /dev/null +++ b/alacritty/src/input/keyboard.rs @@ -0,0 +1,672 @@ +use std::borrow::Cow; +use std::mem; + +use winit::event::{ElementState, KeyEvent}; +#[cfg(target_os = "macos")] +use winit::keyboard::ModifiersKeyState; +use winit::keyboard::{Key, KeyLocation, ModifiersState, NamedKey}; +#[cfg(target_os = "macos")] +use winit::platform::macos::OptionAsAlt; + +use alacritty_terminal::event::EventListener; +use alacritty_terminal::term::TermMode; +use winit::platform::modifier_supplement::KeyEventExtModifierSupplement; + +use crate::config::{Action, BindingKey, BindingMode}; +use crate::event::TYPING_SEARCH_DELAY; +use crate::input::{ActionContext, Execute, Processor}; +use crate::scheduler::{TimerId, Topic}; + +impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { + /// Process key input. + pub fn key_input(&mut self, key: KeyEvent) { + // IME input will be applied on commit and shouldn't trigger key bindings. + if self.ctx.display().ime.preedit().is_some() { + return; + } + + let mode = *self.ctx.terminal().mode(); + let mods = self.ctx.modifiers().state(); + + if key.state == ElementState::Released { + self.key_release(key, mode, mods); + return; + } + + let text = key.text_with_all_modifiers().unwrap_or_default(); + + // All key bindings are disabled while a hint is being selected. + if self.ctx.display().hint_state.active() { + for character in text.chars() { + self.ctx.hint_input(character); + } + return; + } + + // First key after inline search is captured. + let inline_state = self.ctx.inline_search_state(); + if mem::take(&mut inline_state.char_pending) { + if let Some(c) = text.chars().next() { + inline_state.character = Some(c); + + // Immediately move to the captured character. + self.ctx.inline_search_next(); + } + + // Ignore all other characters in `text`. + return; + } + + // Reset search delay when the user is still typing. + self.reset_search_delay(); + + // Key bindings suppress the character input. + if self.process_key_bindings(&key) { + return; + } + + if self.ctx.search_active() { + for character in text.chars() { + self.ctx.search_input(character); + } + + return; + } + + // Vi mode on its own doesn't have any input, the search input was done before. + if mode.contains(TermMode::VI) { + return; + } + + // Mask `Alt` modifier from input when we won't send esc. + let mods = if self.alt_send_esc(&key, text) { mods } else { mods & !ModifiersState::ALT }; + + let build_key_sequence = Self::should_build_sequence(&key, text, mode, mods); + + let bytes = if build_key_sequence { + build_sequence(key, mods, mode) + } else { + let mut bytes = Vec::with_capacity(text.len() + 1); + if mods.alt_key() { + bytes.push(b'\x1b'); + } + + bytes.extend_from_slice(text.as_bytes()); + bytes + }; + + // Write only if we have something to write. + if !bytes.is_empty() { + self.ctx.on_terminal_input_start(); + self.ctx.write_to_pty(bytes); + } + } + + fn alt_send_esc(&mut self, key: &KeyEvent, text: &str) -> bool { + #[cfg(not(target_os = "macos"))] + let alt_send_esc = self.ctx.modifiers().state().alt_key(); + + #[cfg(target_os = "macos")] + let alt_send_esc = { + let option_as_alt = self.ctx.config().window.option_as_alt(); + self.ctx.modifiers().state().alt_key() + && (option_as_alt == OptionAsAlt::Both + || (option_as_alt == OptionAsAlt::OnlyLeft + && self.ctx.modifiers().lalt_state() == ModifiersKeyState::Pressed) + || (option_as_alt == OptionAsAlt::OnlyRight + && self.ctx.modifiers().ralt_state() == ModifiersKeyState::Pressed)) + }; + + match key.logical_key { + Key::Named(named) => { + if named.to_text().is_some() { + alt_send_esc + } else { + // Treat `Alt` as modifier for named keys without text, like ArrowUp. + self.ctx.modifiers().state().alt_key() + } + }, + _ => text.len() == 1 && alt_send_esc, + } + } + + /// Check whether we should try to build escape sequence for the [`KeyEvent`]. + fn should_build_sequence( + key: &KeyEvent, + text: &str, + mode: TermMode, + mods: ModifiersState, + ) -> bool { + if mode.contains(TermMode::REPORT_ALL_KEYS_AS_ESC) { + return true; + } + + let disambiguate = mode.contains(TermMode::DISAMBIGUATE_ESC_CODES) + && (key.logical_key == Key::Named(NamedKey::Escape) + || (!mods.is_empty() && mods != ModifiersState::SHIFT) + || key.location == KeyLocation::Numpad); + + match key.logical_key { + _ if disambiguate => true, + // Exclude all the named keys unless they have textual representation. + Key::Named(named) => named.to_text().is_none(), + _ => text.is_empty(), + } + } + + /// Attempt to find a binding and execute its action. + /// + /// The provided mode, mods, and key must match what is allowed by a binding + /// for its action to be executed. + fn process_key_bindings(&mut self, key: &KeyEvent) -> bool { + let mode = BindingMode::new(self.ctx.terminal().mode(), self.ctx.search_active()); + let mods = self.ctx.modifiers().state(); + + // Don't suppress char if no bindings were triggered. + let mut suppress_chars = None; + + // We don't want the key without modifier, because it means something else most of + // the time. However what we want is to manually lowercase the character to account + // for both small and capital letters on regular characters at the same time. + let logical_key = if let Key::Character(ch) = key.logical_key.as_ref() { + // Match `Alt` bindings without `Alt` being applied, otherwise they use the + // composed chars, which are not intuitive to bind. + // + // On Windows, the `Ctrl + Alt` mangles `logical_key` to unidentified values, thus + // preventing them from being used in bindings + // + // For more see https://github.com/rust-windowing/winit/issues/2945. + if (cfg!(target_os = "macos") || (cfg!(windows) && mods.control_key())) + && mods.alt_key() + { + key.key_without_modifiers() + } else { + Key::Character(ch.to_lowercase().into()) + } + } else { + key.logical_key.clone() + }; + + for i in 0..self.ctx.config().key_bindings().len() { + let binding = &self.ctx.config().key_bindings()[i]; + + let key = match (&binding.trigger, &logical_key) { + (BindingKey::Scancode(_), _) => BindingKey::Scancode(key.physical_key), + (_, code) => { + BindingKey::Keycode { key: code.clone(), location: key.location.into() } + }, + }; + + if binding.is_triggered_by(mode, mods, &key) { + // Pass through the key if any of the bindings has the `ReceiveChar` action. + *suppress_chars.get_or_insert(true) &= binding.action != Action::ReceiveChar; + + // Binding was triggered; run the action. + binding.action.clone().execute(&mut self.ctx); + } + } + + suppress_chars.unwrap_or(false) + } + + /// Handle key release. + fn key_release(&mut self, key: KeyEvent, mode: TermMode, mods: ModifiersState) { + if !mode.contains(TermMode::REPORT_EVENT_TYPES) + || mode.contains(TermMode::VI) + || self.ctx.search_active() + || self.ctx.display().hint_state.active() + { + return; + } + + // Mask `Alt` modifier from input when we won't send esc. + let text = key.text_with_all_modifiers().unwrap_or_default(); + let mods = if self.alt_send_esc(&key, text) { mods } else { mods & !ModifiersState::ALT }; + + let bytes: Cow<'static, [u8]> = match key.logical_key.as_ref() { + // NOTE: Echo the key back on release to follow kitty/foot behavior. When + // KEYBOARD_REPORT_ALL_KEYS_AS_ESC is used, we build proper escapes for + // the keys below. + _ if mode.contains(TermMode::REPORT_ALL_KEYS_AS_ESC) => { + build_sequence(key, mods, mode).into() + }, + // Winit uses different keys for `Backspace` so we expliictly specify the + // values, instead of using what was passed to us from it. + Key::Named(NamedKey::Tab) => [b'\t'].as_slice().into(), + Key::Named(NamedKey::Enter) => [b'\r'].as_slice().into(), + Key::Named(NamedKey::Backspace) => [b'\x7f'].as_slice().into(), + Key::Named(NamedKey::Escape) => [b'\x1b'].as_slice().into(), + _ => build_sequence(key, mods, mode).into(), + }; + + self.ctx.write_to_pty(bytes); + } + + /// Reset search delay. + fn reset_search_delay(&mut self) { + if self.ctx.search_active() { + let timer_id = TimerId::new(Topic::DelayedSearch, self.ctx.window().id()); + let scheduler = self.ctx.scheduler_mut(); + if let Some(timer) = scheduler.unschedule(timer_id) { + scheduler.schedule(timer.event, TYPING_SEARCH_DELAY, false, timer.id); + } + } + } +} + +/// Build a key's keyboard escape sequence based on the given `key`, `mods`, and `mode`. +/// +/// The key sequences for `APP_KEYPAD` and alike are handled inside the bindings. +#[inline(never)] +fn build_sequence(key: KeyEvent, mods: ModifiersState, mode: TermMode) -> Vec<u8> { + let mut modifiers = mods.into(); + + let kitty_seq = mode.intersects( + TermMode::REPORT_ALL_KEYS_AS_ESC + | TermMode::DISAMBIGUATE_ESC_CODES + | TermMode::REPORT_EVENT_TYPES, + ); + + let kitty_encode_all = mode.contains(TermMode::REPORT_ALL_KEYS_AS_ESC); + // The default parameter is 1, so we can omit it. + let kitty_event_type = mode.contains(TermMode::REPORT_EVENT_TYPES) + && (key.repeat || key.state == ElementState::Released); + + let context = + SequenceBuilder { mode, modifiers, kitty_seq, kitty_encode_all, kitty_event_type }; + + let associated_text = key.text_with_all_modifiers().filter(|text| { + mode.contains(TermMode::REPORT_ASSOCIATED_TEXT) + && key.state != ElementState::Released + && !text.is_empty() + && !is_control_character(text) + }); + + let sequence_base = context + .try_build_numpad(&key) + .or_else(|| context.try_build_named_kitty(&key)) + .or_else(|| context.try_build_named_normal(&key)) + .or_else(|| context.try_build_control_char_or_mod(&key, &mut modifiers)) + .or_else(|| context.try_build_textual(&key, associated_text)); + + let (payload, terminator) = match sequence_base { + Some(SequenceBase { payload, terminator }) => (payload, terminator), + _ => return Vec::new(), + }; + + let mut payload = format!("\x1b[{}", payload); + + // Add modifiers information. + if kitty_event_type || !modifiers.is_empty() || associated_text.is_some() { + payload.push_str(&format!(";{}", modifiers.encode_esc_sequence())); + } + + // Push event type. + if kitty_event_type { + payload.push(':'); + let event_type = match key.state { + _ if key.repeat => '2', + ElementState::Pressed => '1', + ElementState::Released => '3', + }; + payload.push(event_type); + } + + if let Some(text) = associated_text { + let mut codepoints = text.chars().map(u32::from); + if let Some(codepoint) = codepoints.next() { + payload.push_str(&format!(";{codepoint}")); + } + for codepoint in codepoints { + payload.push_str(&format!(":{codepoint}")); + } + } + + payload.push(terminator.encode_esc_sequence()); + + payload.into_bytes() +} + +/// Helper to build escape sequence payloads from [`KeyEvent`]. +pub struct SequenceBuilder { + mode: TermMode, + /// The emitted sequence should follow the kitty keyboard protocol. + kitty_seq: bool, + /// Encode all the keys according to the protocol. + kitty_encode_all: bool, + /// Report event types. + kitty_event_type: bool, + modifiers: SequenceModifiers, +} + +impl SequenceBuilder { + /// Try building sequence from the event's emitting text. + fn try_build_textual( + &self, + key: &KeyEvent, + associated_text: Option<&str>, + ) -> Option<SequenceBase> { + let character = match key.logical_key.as_ref() { + Key::Character(character) => character, + _ => return None, + }; + + if character.chars().count() == 1 { + let character = character.chars().next().unwrap(); + let base_character = character.to_lowercase().next().unwrap(); + + let alternate_key_code = u32::from(character); + let mut unicode_key_code = u32::from(base_character); + + // Try to get the base for keys which change based on modifier, like `1` for `!`. + match key.key_without_modifiers().as_ref() { + Key::Character(unmodded) if alternate_key_code == unicode_key_code => { + unicode_key_code = u32::from(unmodded.chars().next().unwrap_or(base_character)); + }, + _ => (), + } + + // NOTE: Base layouts are ignored, since winit doesn't expose this information + // yet. + let payload = if self.mode.contains(TermMode::REPORT_ALTERNATE_KEYS) + && alternate_key_code != unicode_key_code + { + format!("{unicode_key_code}:{alternate_key_code}") + } else { + alternate_key_code.to_string() + }; + + Some(SequenceBase::new(payload.into(), SequenceTerminator::Kitty)) + } else if self.kitty_encode_all && associated_text.is_some() { + // Fallback when need to report text, but we don't have any key associated with this + // text. + Some(SequenceBase::new("0".into(), SequenceTerminator::Kitty)) + } else { + None + } + } + + /// Try building from numpad key. + /// + /// `None` is returned when the key is neither known nor numpad. + fn try_build_numpad(&self, key: &KeyEvent) -> Option<SequenceBase> { + if !self.kitty_seq || key.location != KeyLocation::Numpad { + return None; + } + + let base = match key.logical_key.as_ref() { + Key::Character("0") => "57399", + Key::Character("1") => "57400", + Key::Character("2") => "57401", + Key::Character("3") => "57402", + Key::Character("4") => "57403", + Key::Character("5") => "57404", + Key::Character("6") => "57405", + Key::Character("7") => "57406", + Key::Character("8") => "57407", + Key::Character("9") => "57408", + Key::Character(".") => "57409", + Key::Character("/") => "57410", + Key::Character("*") => "57411", + Key::Character("-") => "57412", + Key::Character("+") => "57413", + Key::Character("=") => "57415", + Key::Named(named) => match named { + NamedKey::Enter => "57414", + NamedKey::ArrowLeft => "57417", + NamedKey::ArrowRight => "57418", + NamedKey::ArrowUp => "57419", + NamedKey::ArrowDown => "57420", + NamedKey::PageUp => "57421", + NamedKey::PageDown => "57422", + NamedKey::Home => "57423", + NamedKey::End => "57424", + NamedKey::Insert => "57425", + NamedKey::Delete => "57426", + _ => return None, + }, + _ => return None, + }; + + Some(SequenceBase::new(base.into(), SequenceTerminator::Kitty)) + } + + /// Try building from [`NamedKey`] using the kitty keyboard protocol encoding + /// for functional keys. + fn try_build_named_kitty(&self, key: &KeyEvent) -> Option<SequenceBase> { + let named = match key.logical_key { + Key::Named(named) if self.kitty_seq => named, + _ => return None, + }; + + let (base, terminator) = match named { + // F3 in kitty protocol diverges from alacritty's terminfo. + NamedKey::F3 => ("13", SequenceTerminator::Normal('~')), + NamedKey::F13 => ("57376", SequenceTerminator::Kitty), + NamedKey::F14 => ("57377", SequenceTerminator::Kitty), + NamedKey::F15 => ("57378", SequenceTerminator::Kitty), + NamedKey::F16 => ("57379", SequenceTerminator::Kitty), + NamedKey::F17 => ("57380", SequenceTerminator::Kitty), + NamedKey::F18 => ("57381", SequenceTerminator::Kitty), + NamedKey::F19 => ("57382", SequenceTerminator::Kitty), + NamedKey::F20 => ("57383", SequenceTerminator::Kitty), + NamedKey::F21 => ("57384", SequenceTerminator::Kitty), + NamedKey::F22 => ("57385", SequenceTerminator::Kitty), + NamedKey::F23 => ("57386", SequenceTerminator::Kitty), + NamedKey::F24 => ("57387", SequenceTerminator::Kitty), + NamedKey::F25 => ("57388", SequenceTerminator::Kitty), + NamedKey::F26 => ("57389", SequenceTerminator::Kitty), + NamedKey::F27 => ("57390", SequenceTerminator::Kitty), + NamedKey::F28 => ("57391", SequenceTerminator::Kitty), + NamedKey::F29 => ("57392", SequenceTerminator::Kitty), + NamedKey::F30 => ("57393", SequenceTerminator::Kitty), + NamedKey::F31 => ("57394", SequenceTerminator::Kitty), + NamedKey::F32 => ("57395", SequenceTerminator::Kitty), + NamedKey::F33 => ("57396", SequenceTerminator::Kitty), + NamedKey::F34 => ("57397", SequenceTerminator::Kitty), + NamedKey::F35 => ("57398", SequenceTerminator::Kitty), + NamedKey::ScrollLock => ("57359", SequenceTerminator::Kitty), + NamedKey::PrintScreen => ("57361", SequenceTerminator::Kitty), + NamedKey::Pause => ("57362", SequenceTerminator::Kitty), + NamedKey::ContextMenu => ("57363", SequenceTerminator::Kitty), + NamedKey::MediaPlay => ("57428", SequenceTerminator::Kitty), + NamedKey::MediaPause => ("57429", SequenceTerminator::Kitty), + NamedKey::MediaPlayPause => ("57430", SequenceTerminator::Kitty), + NamedKey::MediaStop => ("57432", SequenceTerminator::Kitty), + NamedKey::MediaFastForward => ("57433", SequenceTerminator::Kitty), + NamedKey::MediaRewind => ("57434", SequenceTerminator::Kitty), + NamedKey::MediaTrackNext => ("57435", SequenceTerminator::Kitty), + NamedKey::MediaTrackPrevious => ("57436", SequenceTerminator::Kitty), + NamedKey::MediaRecord => ("57437", SequenceTerminator::Kitty), + NamedKey::AudioVolumeDown => ("57438", SequenceTerminator::Kitty), + NamedKey::AudioVolumeUp => ("57439", SequenceTerminator::Kitty), + NamedKey::AudioVolumeMute => ("57440", SequenceTerminator::Kitty), + _ => return None, + }; + + Some(SequenceBase::new(base.into(), terminator)) + } + + /// Try building from [`NamedKey`]. + fn try_build_named_normal(&self, key: &KeyEvent) -> Option<SequenceBase> { + let named = match key.logical_key { + Key::Named(named) => named, + _ => return None, + }; + + // The default parameter is 1, so we can omit it. + let one_based = if self.modifiers.is_empty() && !self.kitty_event_type { "" } else { "1" }; + let (base, terminator) = match named { + NamedKey::PageUp => ("5", SequenceTerminator::Normal('~')), + NamedKey::PageDown => ("6", SequenceTerminator::Normal('~')), + NamedKey::Insert => ("2", SequenceTerminator::Normal('~')), + NamedKey::Delete => ("3", SequenceTerminator::Normal('~')), + NamedKey::Home => (one_based, SequenceTerminator::Normal('H')), + NamedKey::End => (one_based, SequenceTerminator::Normal('F')), + NamedKey::ArrowLeft => (one_based, SequenceTerminator::Normal('D')), + NamedKey::ArrowRight => (one_based, SequenceTerminator::Normal('C')), + NamedKey::ArrowUp => (one_based, SequenceTerminator::Normal('A')), + NamedKey::ArrowDown => (one_based, SequenceTerminator::Normal('B')), + NamedKey::F1 => (one_based, SequenceTerminator::Normal('P')), + NamedKey::F2 => (one_based, SequenceTerminator::Normal('Q')), + NamedKey::F3 => (one_based, SequenceTerminator::Normal('R')), + NamedKey::F4 => (one_based, SequenceTerminator::Normal('S')), + NamedKey::F5 => ("15", SequenceTerminator::Normal('~')), + NamedKey::F6 => ("17", SequenceTerminator::Normal('~')), + NamedKey::F7 => ("18", SequenceTerminator::Normal('~')), + NamedKey::F8 => ("19", SequenceTerminator::Normal('~')), + NamedKey::F9 => ("20", SequenceTerminator::Normal('~')), + NamedKey::F10 => ("21", SequenceTerminator::Normal('~')), + NamedKey::F11 => ("23", SequenceTerminator::Normal('~')), + NamedKey::F12 => ("24", SequenceTerminator::Normal('~')), + NamedKey::F13 => ("25", SequenceTerminator::Normal('~')), + NamedKey::F14 => ("26", SequenceTerminator::Normal('~')), + NamedKey::F15 => ("28", SequenceTerminator::Normal('~')), + NamedKey::F16 => ("29", SequenceTerminator::Normal('~')), + NamedKey::F17 => ("31", SequenceTerminator::Normal('~')), + NamedKey::F18 => ("32", SequenceTerminator::Normal('~')), + NamedKey::F19 => ("33", SequenceTerminator::Normal('~')), + NamedKey::F20 => ("34", SequenceTerminator::Normal('~')), + _ => return None, + }; + + Some(SequenceBase::new(base.into(), terminator)) + } + + /// Try building escape from control characters (e.g. Enter) and modifiers. + fn try_build_control_char_or_mod( + &self, + key: &KeyEvent, + mods: &mut SequenceModifiers, + ) -> Option<SequenceBase> { + if !self.kitty_encode_all && !self.kitty_seq { + return None; + } + + let named = match key.logical_key { + Key::Named(named) => named, + _ => return None, + }; + + let base = match named { + NamedKey::Tab => "9", + NamedKey::Enter => "13", + NamedKey::Escape => "27", + NamedKey::Space => "32", + NamedKey::Backspace => "127", + _ => "", + }; + + // Fail when the key is not a named control character and the active mode prohibits us + // from encoding modifier keys. + if !self.kitty_encode_all && base.is_empty() { + return None; + } + + let base = match (named, key.location) { + (NamedKey::Shift, KeyLocation::Left) => "57441", + (NamedKey::Control, KeyLocation::Left) => "57442", + (NamedKey::Alt, KeyLocation::Left) => "57443", + (NamedKey::Super, KeyLocation::Left) => "57444", + (NamedKey::Hyper, KeyLocation::Left) => "57445", + (NamedKey::Meta, KeyLocation::Left) => "57446", + (NamedKey::Shift, _) => "57447", + (NamedKey::Control, _) => "57448", + (NamedKey::Alt, _) => "57449", + (NamedKey::Super, _) => "57450", + (NamedKey::Hyper, _) => "57451", + (NamedKey::Meta, _) => "57452", + (NamedKey::CapsLock, _) => "57358", + (NamedKey::NumLock, _) => "57360", + _ => base, + }; + + // NOTE: Kitty's protocol mandates that the modifier state is applied before + // key press, however winit sends them after the key press, so for modifiers + // itself apply the state based on keysyms and not the _actual_ modifiers + // state, which is how kitty is doing so and what is suggested in such case. + let press = key.state.is_pressed(); + match named { + NamedKey::Shift => mods.set(SequenceModifiers::SHIFT, press), + NamedKey::Control => mods.set(SequenceModifiers::CONTROL, press), + NamedKey::Alt => mods.set(SequenceModifiers::ALT, press), + NamedKey::Super => mods.set(SequenceModifiers::SUPER, press), + _ => (), + } + + if base.is_empty() { + None + } else { + Some(SequenceBase::new(base.into(), SequenceTerminator::Kitty)) + } + } +} + +pub struct SequenceBase { + /// The base of the payload, which is the `number` and optionally an alt base from the kitty + /// spec. + payload: Cow<'static, str>, + terminator: SequenceTerminator, +} + +impl SequenceBase { + fn new(payload: Cow<'static, str>, terminator: SequenceTerminator) -> Self { + Self { payload, terminator } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SequenceTerminator { + /// The normal key esc sequence terminator defined by xterm/dec. + Normal(char), + /// The terminator is for kitty escape sequence. + Kitty, +} + +impl SequenceTerminator { + fn encode_esc_sequence(self) -> char { + match self { + SequenceTerminator::Normal(char) => char, + SequenceTerminator::Kitty => 'u', + } + } +} + +bitflags::bitflags! { + /// The modifiers encoding for escape sequence. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + struct SequenceModifiers : u8 { + const SHIFT = 0b0000_0001; + const ALT = 0b0000_0010; + const CONTROL = 0b0000_0100; + const SUPER = 0b0000_1000; + // NOTE: Kitty protocol defines additional modifiers to what is present here, like + // Capslock, but it's not a modifier as per winit. + } +} + +impl SequenceModifiers { + /// Get the value which should be passed to escape sequence. + pub fn encode_esc_sequence(self) -> u8 { + self.bits() + 1 + } +} + +impl From<ModifiersState> for SequenceModifiers { + fn from(mods: ModifiersState) -> Self { + let mut modifiers = Self::empty(); + modifiers.set(Self::SHIFT, mods.shift_key()); + modifiers.set(Self::ALT, mods.alt_key()); + modifiers.set(Self::CONTROL, mods.control_key()); + modifiers.set(Self::SUPER, mods.super_key()); + modifiers + } +} + +/// Check whether the `text` is `0x7f`, `C0` or `C1` control code. +fn is_control_character(text: &str) -> bool { + // 0x7f (DEL) is included here since it has a dedicated control code (`^?`) which generally + // does not match the reported text (`^H`), despite not technically being part of C0 or C1. + let codepoint = text.bytes().next().unwrap(); + text.len() == 1 && (codepoint < 0x20 || (0x7f..=0x9f).contains(&codepoint)) +} diff --git a/alacritty/src/input.rs b/alacritty/src/input/mod.rs index 5728665a..5bc95bdf 100644 --- a/alacritty/src/input.rs +++ b/alacritty/src/input/mod.rs @@ -14,17 +14,17 @@ use std::marker::PhantomData; use std::mem; use std::time::{Duration, Instant}; +use log::debug; use winit::dpi::PhysicalPosition; use winit::event::{ - ElementState, KeyboardInput, ModifiersState, MouseButton, MouseScrollDelta, - Touch as TouchEvent, TouchPhase, + ElementState, Modifiers, MouseButton, MouseScrollDelta, Touch as TouchEvent, TouchPhase, }; use winit::event_loop::EventLoopWindowTarget; +use winit::keyboard::ModifiersState; #[cfg(target_os = "macos")] -use winit::platform::macos::{EventLoopWindowTargetExtMacOS, OptionAsAlt}; +use winit::platform::macos::EventLoopWindowTargetExtMacOS; use winit::window::CursorIcon; -use alacritty_terminal::ansi::{ClearMode, Handler}; use alacritty_terminal::event::EventListener; use alacritty_terminal::grid::{Dimensions, Scroll}; use alacritty_terminal::index::{Boundary, Column, Direction, Point, Side}; @@ -32,20 +32,23 @@ use alacritty_terminal::selection::SelectionType; use alacritty_terminal::term::search::Match; use alacritty_terminal::term::{ClipboardType, Term, TermMode}; use alacritty_terminal::vi_mode::ViMotion; +use alacritty_terminal::vte::ansi::{ClearMode, Handler}; use crate::clipboard::Clipboard; -use crate::config::{Action, BindingMode, Key, MouseAction, SearchAction, UiConfig, ViAction}; +use crate::config::{Action, BindingMode, MouseAction, SearchAction, UiConfig, ViAction}; use crate::display::hint::HintMatch; use crate::display::window::Window; use crate::display::{Display, SizeInfo}; use crate::event::{ - ClickState, Event, EventType, Mouse, TouchPurpose, TouchZoom, TYPING_SEARCH_DELAY, + ClickState, Event, EventType, InlineSearchState, Mouse, TouchPurpose, TouchZoom, }; use crate::message_bar::{self, Message}; use crate::scheduler::{Scheduler, TimerId, Topic}; -/// Font size change interval. -pub const FONT_SIZE_STEP: f32 = 0.5; +pub mod keyboard; + +/// Font size change interval in px. +pub const FONT_SIZE_STEP: f32 = 1.; /// Interval for mouse scrolling during selection outside of the boundaries. const SELECTION_SCROLLING_INTERVAL: Duration = Duration::from_millis(15); @@ -56,12 +59,12 @@ const MIN_SELECTION_SCROLLING_HEIGHT: f64 = 5.; /// Number of pixels for increasing the selection scrolling speed factor by one. const SELECTION_SCROLLING_STEP: f64 = 20.; -/// Touch scroll speed. -const TOUCH_SCROLL_FACTOR: f64 = 0.35; - /// Distance before a touch input is considered a drag. const MAX_TAP_DISTANCE: f64 = 20.; +/// Threshold used for double_click/triple_click. +const CLICK_THRESHOLD: Duration = Duration::from_millis(400); + /// Processes input from winit. /// /// An escape sequence may be emitted in case specific keys or key combinations @@ -84,15 +87,16 @@ pub trait ActionContext<T: EventListener> { fn mouse_mut(&mut self) -> &mut Mouse; fn mouse(&self) -> &Mouse; fn touch_purpose(&mut self) -> &mut TouchPurpose; - fn received_count(&mut self) -> &mut usize; - fn suppress_chars(&mut self) -> &mut bool; - fn modifiers(&mut self) -> &mut ModifiersState; + fn modifiers(&mut self) -> &mut Modifiers; fn scroll(&mut self, _scroll: Scroll) {} fn window(&mut self) -> &mut Window; fn display(&mut self) -> &mut Display; fn terminal(&self) -> &Term<T>; fn terminal_mut(&mut self) -> &mut Term<T>; fn spawn_new_instance(&mut self) {} + #[cfg(target_os = "macos")] + fn create_new_window(&mut self, _tabbing_id: Option<String>) {} + #[cfg(not(target_os = "macos"))] fn create_new_window(&mut self) {} fn change_font_size(&mut self, _delta: f32) {} fn reset_font_size(&mut self) {} @@ -116,10 +120,15 @@ pub trait ActionContext<T: EventListener> { fn search_active(&self) -> bool; fn on_typing_start(&mut self) {} fn toggle_vi_mode(&mut self) {} + fn inline_search_state(&mut self) -> &mut InlineSearchState; + fn start_inline_search(&mut self, _direction: Direction, _stop_short: bool) {} + fn inline_search_next(&mut self) {} + fn inline_search_previous(&mut self) {} fn hint_input(&mut self, _character: char) {} fn trigger_hint(&mut self, _hint: &HintMatch) {} fn expand_selection(&mut self) {} - fn paste(&mut self, _text: &str) {} + fn on_terminal_input_start(&mut self) {} + fn paste(&mut self, _text: &str, _bracketed: bool) {} fn spawn_daemon<I, S>(&self, _program: &str, _args: I) where I: IntoIterator<Item = S> + Debug + Copy, @@ -151,12 +160,7 @@ impl<T: EventListener> Execute<T> for Action { #[inline] fn execute<A: ActionContext<T>>(&self, ctx: &mut A) { match self { - Action::Esc(s) => { - ctx.on_typing_start(); - ctx.clear_selection(); - ctx.scroll(Scroll::Bottom); - ctx.write_to_pty(s.clone().into_bytes()) - }, + Action::Esc(s) => ctx.paste(s, false), Action::Command(program) => ctx.spawn_daemon(program.program(), program.args()), Action::Hint(hint) => { ctx.display().hint_state.start(hint.clone()); @@ -166,6 +170,11 @@ impl<T: EventListener> Execute<T> for Action { ctx.on_typing_start(); ctx.toggle_vi_mode() }, + action @ (Action::ViMotion(_) | Action::Vi(_)) + if !ctx.terminal().mode().contains(TermMode::VI) => + { + debug!("Ignoring {action:?}: Vi mode inactive"); + }, Action::ViMotion(motion) => { ctx.on_typing_start(); ctx.terminal_mut().vi_motion(*motion); @@ -250,6 +259,23 @@ impl<T: EventListener> Execute<T> for Action { ctx.scroll(Scroll::Delta(scroll_lines)); }, + Action::Vi(ViAction::InlineSearchForward) => { + ctx.start_inline_search(Direction::Right, false) + }, + Action::Vi(ViAction::InlineSearchBackward) => { + ctx.start_inline_search(Direction::Left, false) + }, + Action::Vi(ViAction::InlineSearchForwardShort) => { + ctx.start_inline_search(Direction::Right, true) + }, + Action::Vi(ViAction::InlineSearchBackwardShort) => { + ctx.start_inline_search(Direction::Left, true) + }, + Action::Vi(ViAction::InlineSearchNext) => ctx.inline_search_next(), + Action::Vi(ViAction::InlineSearchPrevious) => ctx.inline_search_previous(), + action @ Action::Search(_) if !ctx.search_active() => { + debug!("Ignoring {action:?}: Search mode inactive"); + }, Action::Search(SearchAction::SearchFocusNext) => { ctx.advance_search_origin(ctx.search_direction()); }, @@ -276,11 +302,11 @@ impl<T: EventListener> Execute<T> for Action { Action::ClearSelection => ctx.clear_selection(), Action::Paste => { let text = ctx.clipboard_mut().load(ClipboardType::Clipboard); - ctx.paste(&text); + ctx.paste(&text, true); }, Action::PasteSelection => { let text = ctx.clipboard_mut().load(ClipboardType::Selection); - ctx.paste(&text); + ctx.paste(&text, true); }, Action::ToggleFullscreen => ctx.window().toggle_fullscreen(), Action::ToggleMaximized => ctx.window().toggle_maximized(), @@ -295,39 +321,35 @@ impl<T: EventListener> Execute<T> for Action { Action::Minimize => ctx.window().set_minimized(true), Action::Quit => ctx.terminal_mut().exit(), Action::IncreaseFontSize => ctx.change_font_size(FONT_SIZE_STEP), - Action::DecreaseFontSize => ctx.change_font_size(FONT_SIZE_STEP * -1.), + Action::DecreaseFontSize => ctx.change_font_size(-FONT_SIZE_STEP), Action::ResetFontSize => ctx.reset_font_size(), - Action::ScrollPageUp => { + Action::ScrollPageUp + | Action::ScrollPageDown + | Action::ScrollHalfPageUp + | Action::ScrollHalfPageDown => { // Move vi mode cursor. let term = ctx.terminal_mut(); - let scroll_lines = term.screen_lines() as i32; - term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, scroll_lines); - - ctx.scroll(Scroll::PageUp); - }, - Action::ScrollPageDown => { - // Move vi mode cursor. - let term = ctx.terminal_mut(); - let scroll_lines = -(term.screen_lines() as i32); - term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, scroll_lines); - - ctx.scroll(Scroll::PageDown); - }, - Action::ScrollHalfPageUp => { - // Move vi mode cursor. - let term = ctx.terminal_mut(); - let scroll_lines = term.screen_lines() as i32 / 2; - term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, scroll_lines); + let (scroll, amount) = match self { + Action::ScrollPageUp => (Scroll::PageUp, term.screen_lines() as i32), + Action::ScrollPageDown => (Scroll::PageDown, -(term.screen_lines() as i32)), + Action::ScrollHalfPageUp => { + let amount = term.screen_lines() as i32 / 2; + (Scroll::Delta(amount), amount) + }, + Action::ScrollHalfPageDown => { + let amount = -(term.screen_lines() as i32 / 2); + (Scroll::Delta(amount), amount) + }, + _ => unreachable!(), + }; - ctx.scroll(Scroll::Delta(scroll_lines)); - }, - Action::ScrollHalfPageDown => { - // Move vi mode cursor. - let term = ctx.terminal_mut(); - let scroll_lines = -(term.screen_lines() as i32 / 2); - term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, scroll_lines); + let old_vi_cursor = term.vi_mode_cursor; + term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, amount); + if old_vi_cursor != term.vi_mode_cursor { + ctx.mark_dirty(); + } - ctx.scroll(Scroll::Delta(scroll_lines)); + ctx.scroll(scroll); }, Action::ScrollLineUp => ctx.scroll(Scroll::Delta(1)), Action::ScrollLineDown => ctx.scroll(Scroll::Delta(-1)), @@ -354,9 +376,41 @@ impl<T: EventListener> Execute<T> for Action { }, Action::ClearHistory => ctx.terminal_mut().clear_screen(ClearMode::Saved), Action::ClearLogNotice => ctx.pop_message(), - Action::SpawnNewInstance => ctx.spawn_new_instance(), + #[cfg(not(target_os = "macos"))] Action::CreateNewWindow => ctx.create_new_window(), - Action::ReceiveChar | Action::None => (), + Action::SpawnNewInstance => ctx.spawn_new_instance(), + #[cfg(target_os = "macos")] + Action::CreateNewWindow => ctx.create_new_window(None), + #[cfg(target_os = "macos")] + Action::CreateNewTab => { + let tabbing_id = Some(ctx.window().tabbing_id()); + ctx.create_new_window(tabbing_id); + }, + #[cfg(target_os = "macos")] + Action::SelectNextTab => ctx.window().select_next_tab(), + #[cfg(target_os = "macos")] + Action::SelectPreviousTab => ctx.window().select_previous_tab(), + #[cfg(target_os = "macos")] + Action::SelectTab1 => ctx.window().select_tab_at_index(0), + #[cfg(target_os = "macos")] + Action::SelectTab2 => ctx.window().select_tab_at_index(1), + #[cfg(target_os = "macos")] + Action::SelectTab3 => ctx.window().select_tab_at_index(2), + #[cfg(target_os = "macos")] + Action::SelectTab4 => ctx.window().select_tab_at_index(3), + #[cfg(target_os = "macos")] + Action::SelectTab5 => ctx.window().select_tab_at_index(4), + #[cfg(target_os = "macos")] + Action::SelectTab6 => ctx.window().select_tab_at_index(5), + #[cfg(target_os = "macos")] + Action::SelectTab7 => ctx.window().select_tab_at_index(6), + #[cfg(target_os = "macos")] + Action::SelectTab8 => ctx.window().select_tab_at_index(7), + #[cfg(target_os = "macos")] + Action::SelectTab9 => ctx.window().select_tab_at_index(8), + #[cfg(target_os = "macos")] + Action::SelectLastTab => ctx.window().select_last_tab(), + _ => (), } } } @@ -413,7 +467,8 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { // Don't launch URLs if mouse has moved. self.ctx.mouse_mut().block_hint_launcher = true; - if (lmb_pressed || rmb_pressed) && (self.ctx.modifiers().shift() || !self.ctx.mouse_mode()) + if (lmb_pressed || rmb_pressed) + && (self.ctx.modifiers().state().shift_key() || !self.ctx.mouse_mode()) { self.ctx.update_selection(point, cell_side); } else if cell_changed @@ -464,14 +519,14 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { // Calculate modifiers value. let mut mods = 0; - let modifiers = self.ctx.modifiers(); - if modifiers.shift() { + let modifiers = self.ctx.modifiers().state(); + if modifiers.shift_key() { mods += 4; } - if modifiers.alt() { + if modifiers.alt_key() { mods += 8; } - if modifiers.ctrl() { + if modifiers.control_key() { mods += 16; } @@ -531,7 +586,7 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { fn on_mouse_press(&mut self, button: MouseButton) { // Handle mouse mode. - if !self.ctx.modifiers().shift() && self.ctx.mouse_mode() { + if !self.ctx.modifiers().state().shift_key() && self.ctx.mouse_mode() { self.ctx.mouse_mut().click_state = ClickState::None; let code = match button { @@ -539,7 +594,7 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { MouseButton::Middle => 1, MouseButton::Right => 2, // Can't properly report more than three buttons.. - MouseButton::Other(_) => return, + MouseButton::Back | MouseButton::Forward | MouseButton::Other(_) => return, }; self.mouse_report(code, ElementState::Pressed); @@ -550,19 +605,14 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { self.ctx.mouse_mut().last_click_timestamp = now; // Update multi-click state. - let mouse_config = &self.ctx.config().mouse; self.ctx.mouse_mut().click_state = match self.ctx.mouse().click_state { // Reset click state if button has changed. _ if button != self.ctx.mouse().last_click_button => { self.ctx.mouse_mut().last_click_button = button; ClickState::Click }, - ClickState::Click if elapsed < mouse_config.double_click.threshold() => { - ClickState::DoubleClick - }, - ClickState::DoubleClick if elapsed < mouse_config.triple_click.threshold() => { - ClickState::TripleClick - }, + ClickState::Click if elapsed < CLICK_THRESHOLD => ClickState::DoubleClick, + ClickState::DoubleClick if elapsed < CLICK_THRESHOLD => ClickState::TripleClick, _ => ClickState::Click, }; @@ -588,7 +638,7 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { self.ctx.clear_selection(); // Start new empty selection. - if self.ctx.modifiers().ctrl() { + if self.ctx.modifiers().state().control_key() { self.ctx.start_selection(SelectionType::Block, point, side); } else { self.ctx.start_selection(SelectionType::Simple, point, side); @@ -613,13 +663,13 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { } fn on_mouse_release(&mut self, button: MouseButton) { - if !self.ctx.modifiers().shift() && self.ctx.mouse_mode() { + if !self.ctx.modifiers().state().shift_key() && self.ctx.mouse_mode() { let code = match button { MouseButton::Left => 0, MouseButton::Middle => 1, MouseButton::Right => 2, // Can't properly report more than three buttons. - MouseButton::Other(_) => return, + MouseButton::Back | MouseButton::Forward | MouseButton::Other(_) => return, }; self.mouse_report(code, ElementState::Released); return; @@ -642,11 +692,16 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { } pub fn mouse_wheel_input(&mut self, delta: MouseScrollDelta, phase: TouchPhase) { + let multiplier = self.ctx.config().scrolling.multiplier; match delta { MouseScrollDelta::LineDelta(columns, lines) => { let new_scroll_px_x = columns * self.ctx.size_info().cell_width(); let new_scroll_px_y = lines * self.ctx.size_info().cell_height(); - self.scroll_terminal(new_scroll_px_x as f64, new_scroll_px_y as f64); + self.scroll_terminal( + new_scroll_px_x as f64, + new_scroll_px_y as f64, + multiplier as f64, + ); }, MouseScrollDelta::PixelDelta(mut lpos) => { match phase { @@ -663,7 +718,7 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { lpos.x = 0.; } - self.scroll_terminal(lpos.x, lpos.y); + self.scroll_terminal(lpos.x, lpos.y, multiplier as f64); }, _ => (), } @@ -671,7 +726,7 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { } } - fn scroll_terminal(&mut self, new_scroll_x_px: f64, new_scroll_y_px: f64) { + fn scroll_terminal(&mut self, new_scroll_x_px: f64, new_scroll_y_px: f64, multiplier: f64) { const MOUSE_WHEEL_UP: u8 = 64; const MOUSE_WHEEL_DOWN: u8 = 65; const MOUSE_WHEEL_LEFT: u8 = 66; @@ -702,10 +757,8 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { .terminal() .mode() .contains(TermMode::ALT_SCREEN | TermMode::ALTERNATE_SCROLL) - && !self.ctx.modifiers().shift() + && !self.ctx.modifiers().state().shift_key() { - let multiplier = f64::from(self.ctx.config().terminal_config.scrolling.multiplier); - self.ctx.mouse_mut().accumulated_scroll.x += new_scroll_x_px * multiplier; self.ctx.mouse_mut().accumulated_scroll.y += new_scroll_y_px * multiplier; @@ -732,7 +785,6 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { self.ctx.write_to_pty(content); } else { - let multiplier = f64::from(self.ctx.config().terminal_config.scrolling.multiplier); self.ctx.mouse_mut().accumulated_scroll.y += new_scroll_y_px * multiplier; let lines = (self.ctx.mouse().accumulated_scroll.y / height) as i32; @@ -772,7 +824,7 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { TouchPurpose::Tap(start) => TouchPurpose::Zoom(TouchZoom::new((start, touch))), TouchPurpose::Zoom(zoom) => TouchPurpose::Invalid(zoom.slots()), TouchPurpose::Scroll(event) | TouchPurpose::Select(event) => { - let mut set = HashSet::new(); + let mut set = HashSet::default(); set.insert(event.id); TouchPurpose::Invalid(set) }, @@ -820,7 +872,8 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { let delta_y = touch.location.y - last_touch.location.y; *touch_purpose = TouchPurpose::Scroll(touch); - self.scroll_terminal(0., delta_y * TOUCH_SCROLL_FACTOR); + // Use a fixed scroll factor for touchscreens, to accurately track finger motion. + self.scroll_terminal(0., delta_y, 1.0); }, TouchPurpose::Select(_) => self.mouse_moved(touch.location), TouchPurpose::Invalid(_) => (), @@ -867,6 +920,25 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { } } + /// Reset mouse cursor based on modifier and terminal state. + #[inline] + pub fn reset_mouse_cursor(&mut self) { + let mouse_state = self.cursor_state(); + self.ctx.window().set_mouse_cursor(mouse_state); + } + + /// Modifier state change. + pub fn modifiers_input(&mut self, modifiers: Modifiers) { + *self.ctx.modifiers() = modifiers; + + // Prompt hint highlight update. + self.ctx.mouse_mut().hint_highlight_dirty = true; + + // Update mouse state and check for URL change. + let mouse_state = self.cursor_state(); + self.ctx.window().set_mouse_cursor(mouse_state); + } + pub fn mouse_input(&mut self, state: ElementState, button: MouseButton) { match button { MouseButton::Left => self.ctx.mouse_mut().left_button_state = state, @@ -876,7 +948,7 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { } // Skip normal mouse events if the message bar has been clicked. - if self.message_bar_cursor_state() == Some(CursorIcon::Hand) + if self.message_bar_cursor_state() == Some(CursorIcon::Pointer) && state == ElementState::Pressed { let size = self.ctx.size_info(); @@ -891,7 +963,7 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { let new_icon = match current_lines.cmp(&new_lines) { Ordering::Less => CursorIcon::Default, - Ordering::Equal => CursorIcon::Hand, + Ordering::Equal => CursorIcon::Pointer, Ordering::Greater => { if self.ctx.mouse_mode() { CursorIcon::Default @@ -914,144 +986,6 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { } } - /// Process key input. - pub fn key_input(&mut self, input: KeyboardInput) { - // IME input will be applied on commit and shouldn't trigger key bindings. - if self.ctx.display().ime.preedit().is_some() { - return; - } - - // All key bindings are disabled while a hint is being selected. - if self.ctx.display().hint_state.active() { - *self.ctx.suppress_chars() = false; - return; - } - - // Reset search delay when the user is still typing. - if self.ctx.search_active() { - let timer_id = TimerId::new(Topic::DelayedSearch, self.ctx.window().id()); - let scheduler = self.ctx.scheduler_mut(); - if let Some(timer) = scheduler.unschedule(timer_id) { - scheduler.schedule(timer.event, TYPING_SEARCH_DELAY, false, timer.id); - } - } - - match input.state { - ElementState::Pressed => { - *self.ctx.received_count() = 0; - self.process_key_bindings(input); - }, - ElementState::Released => *self.ctx.suppress_chars() = false, - } - } - - /// Modifier state change. - pub fn modifiers_input(&mut self, modifiers: ModifiersState) { - *self.ctx.modifiers() = modifiers; - - // Prompt hint highlight update. - self.ctx.mouse_mut().hint_highlight_dirty = true; - - // Update mouse state and check for URL change. - let mouse_state = self.cursor_state(); - self.ctx.window().set_mouse_cursor(mouse_state); - } - - /// Reset mouse cursor based on modifier and terminal state. - #[inline] - pub fn reset_mouse_cursor(&mut self) { - let mouse_state = self.cursor_state(); - self.ctx.window().set_mouse_cursor(mouse_state); - } - - /// Process a received character. - pub fn received_char(&mut self, c: char) { - let suppress_chars = *self.ctx.suppress_chars(); - - // Don't insert chars when we have IME running. - if self.ctx.display().ime.preedit().is_some() { - return; - } - - // Handle hint selection over anything else. - if self.ctx.display().hint_state.active() && !suppress_chars { - self.ctx.hint_input(c); - return; - } - - // Pass keys to search and ignore them during `suppress_chars`. - let search_active = self.ctx.search_active(); - if suppress_chars || search_active || self.ctx.terminal().mode().contains(TermMode::VI) { - if search_active && !suppress_chars { - self.ctx.search_input(c); - } - - return; - } - - self.ctx.on_typing_start(); - - if self.ctx.terminal().grid().display_offset() != 0 { - self.ctx.scroll(Scroll::Bottom); - } - self.ctx.clear_selection(); - - let utf8_len = c.len_utf8(); - let mut bytes = vec![0; utf8_len]; - c.encode_utf8(&mut bytes[..]); - - #[cfg(not(target_os = "macos"))] - let alt_send_esc = true; - - // Don't send ESC when `OptionAsAlt` is used. This doesn't handle - // `Only{Left,Right}` variants due to inability to distinguish them. - #[cfg(target_os = "macos")] - let alt_send_esc = self.ctx.config().window.option_as_alt != OptionAsAlt::None; - - if alt_send_esc - && *self.ctx.received_count() == 0 - && self.ctx.modifiers().alt() - && utf8_len == 1 - { - bytes.insert(0, b'\x1b'); - } - - self.ctx.write_to_pty(bytes); - - *self.ctx.received_count() += 1; - } - - /// Attempt to find a binding and execute its action. - /// - /// The provided mode, mods, and key must match what is allowed by a binding - /// for its action to be executed. - fn process_key_bindings(&mut self, input: KeyboardInput) { - let mode = BindingMode::new(self.ctx.terminal().mode(), self.ctx.search_active()); - let mods = *self.ctx.modifiers(); - let mut suppress_chars = None; - - for i in 0..self.ctx.config().key_bindings().len() { - let binding = &self.ctx.config().key_bindings()[i]; - - let key = match (binding.trigger, input.virtual_keycode) { - (Key::Scancode(_), _) => Key::Scancode(input.scancode), - (_, Some(key)) => Key::Keycode(key), - _ => continue, - }; - - if binding.is_triggered_by(mode, mods, &key) { - // Pass through the key if any of the bindings has the `ReceiveChar` action. - *suppress_chars.get_or_insert(true) &= binding.action != Action::ReceiveChar; - - // Binding was triggered; run the action. - binding.action.clone().execute(&mut self.ctx); - } - } - - // Don't suppress char if no bindings were triggered. - *self.ctx.suppress_chars() = suppress_chars.unwrap_or(false); - } - /// Attempt to find a binding and execute its action. /// /// The provided mode, mods, and key must match what is allowed by a binding @@ -1059,18 +993,26 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { fn process_mouse_bindings(&mut self, button: MouseButton) { let mode = BindingMode::new(self.ctx.terminal().mode(), self.ctx.search_active()); let mouse_mode = self.ctx.mouse_mode(); - let mods = *self.ctx.modifiers(); + let mods = self.ctx.modifiers().state(); + let mouse_bindings = self.ctx.config().mouse_bindings().to_owned(); - for i in 0..self.ctx.config().mouse_bindings().len() { - let mut binding = self.ctx.config().mouse_bindings()[i].clone(); + // If mouse mode is active, also look for bindings without shift. + let mut check_fallback = mouse_mode && mods.contains(ModifiersState::SHIFT); - // Require shift for all modifiers when mouse mode is active. - if mouse_mode { - binding.mods |= ModifiersState::SHIFT; + for binding in &mouse_bindings { + // Don't trigger normal bindings in mouse mode unless Shift is pressed. + if binding.is_triggered_by(mode, mods, &button) && (check_fallback || !mouse_mode) { + binding.action.execute(&mut self.ctx); + check_fallback = false; } + } - if binding.is_triggered_by(mode, mods, &button) { - binding.action.execute(&mut self.ctx); + if check_fallback { + let fallback_mods = mods & !ModifiersState::SHIFT; + for binding in &mouse_bindings { + if binding.is_triggered_by(mode, fallback_mods, &button) { + binding.action.execute(&mut self.ctx); + } } } } @@ -1094,7 +1036,7 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { } else if mouse.y <= terminal_end + size.cell_height() as usize && point.column + message_bar::CLOSE_BUTTON_TEXT.len() >= size.columns() { - Some(CursorIcon::Hand) + Some(CursorIcon::Pointer) } else { Some(CursorIcon::Default) } @@ -1112,8 +1054,8 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { if let Some(mouse_state) = self.message_bar_cursor_state() { mouse_state } else if self.ctx.display().highlighted_hint.as_ref().map_or(false, hint_highlighted) { - CursorIcon::Hand - } else if !self.ctx.modifiers().shift() && self.ctx.mouse_mode() { + CursorIcon::Pointer + } else if !self.ctx.modifiers().state().shift_key() && self.ctx.mouse_mode() { CursorIcon::Default } else { CursorIcon::Text @@ -1160,7 +1102,8 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { mod tests { use super::*; - use winit::event::{DeviceId, Event as WinitEvent, VirtualKeyCode, WindowEvent}; + use winit::event::{DeviceId, Event as WinitEvent, WindowEvent}; + use winit::keyboard::Key; use winit::window::WindowId; use alacritty_terminal::event::Event as TerminalEvent; @@ -1168,7 +1111,7 @@ mod tests { use crate::config::Binding; use crate::message_bar::MessageBuffer; - const KEY: VirtualKeyCode = VirtualKeyCode::Key0; + const KEY: Key<&'static str> = Key::Character("0"); struct MockEventProxy; impl EventListener for MockEventProxy {} @@ -1179,10 +1122,9 @@ mod tests { pub mouse: &'a mut Mouse, pub clipboard: &'a mut Clipboard, pub message_buffer: &'a mut MessageBuffer, - pub received_count: usize, - pub suppress_chars: bool, - pub modifiers: ModifiersState, + pub modifiers: Modifiers, config: &'a UiConfig, + inline_search_state: &'a mut InlineSearchState, } impl<'a, T: EventListener> super::ActionContext<T> for ActionContext<'a, T> { @@ -1199,6 +1141,10 @@ mod tests { Direction::Right } + fn inline_search_state(&mut self) -> &mut InlineSearchState { + self.inline_search_state + } + fn search_active(&self) -> bool { false } @@ -1242,15 +1188,7 @@ mod tests { unimplemented!(); } - fn received_count(&mut self) -> &mut usize { - &mut self.received_count - } - - fn suppress_chars(&mut self) -> &mut bool { - &mut self.suppress_chars - } - - fn modifiers(&mut self) -> &mut ModifiersState { + fn modifiers(&mut self) -> &mut Modifiers { &mut self.modifiers } @@ -1294,6 +1232,7 @@ mod tests { initial_button: $initial_button:expr, input: $input:expr, end_state: $end_state:expr, + input_delay: $input_delay:expr, } => { #[test] fn $name() { @@ -1309,14 +1248,16 @@ mod tests { false, ); - let mut terminal = Term::new(&cfg.terminal_config, &size, MockEventProxy); + let mut terminal = Term::new(cfg.term_options(), &size, MockEventProxy); let mut mouse = Mouse { click_state: $initial_state, last_click_button: $initial_button, + last_click_timestamp: Instant::now() - $input_delay, ..Mouse::default() }; + let mut inline_search_state = InlineSearchState::default(); let mut message_buffer = MessageBuffer::default(); let context = ActionContext { @@ -1324,16 +1265,15 @@ mod tests { mouse: &mut mouse, size_info: &size, clipboard: &mut clipboard, - received_count: 0, - suppress_chars: false, modifiers: Default::default(), message_buffer: &mut message_buffer, + inline_search_state: &mut inline_search_state, config: &cfg, }; let mut processor = Processor::new(context); - let event: WinitEvent::<'_, TerminalEvent> = $input; + let event: WinitEvent::<TerminalEvent> = $input; if let WinitEvent::WindowEvent { event: WindowEvent::MouseInput { state, @@ -1379,11 +1319,11 @@ mod tests { state: ElementState::Pressed, button: MouseButton::Left, device_id: unsafe { DeviceId::dummy() }, - modifiers: ModifiersState::default(), }, window_id: unsafe { WindowId::dummy() }, }, end_state: ClickState::Click, + input_delay: Duration::ZERO, } test_clickstate! { @@ -1395,11 +1335,11 @@ mod tests { state: ElementState::Pressed, button: MouseButton::Right, device_id: unsafe { DeviceId::dummy() }, - modifiers: ModifiersState::default(), }, window_id: unsafe { WindowId::dummy() }, }, end_state: ClickState::Click, + input_delay: Duration::ZERO, } test_clickstate! { @@ -1411,11 +1351,11 @@ mod tests { state: ElementState::Pressed, button: MouseButton::Middle, device_id: unsafe { DeviceId::dummy() }, - modifiers: ModifiersState::default(), }, window_id: unsafe { WindowId::dummy() }, }, end_state: ClickState::Click, + input_delay: Duration::ZERO, } test_clickstate! { @@ -1427,11 +1367,27 @@ mod tests { state: ElementState::Pressed, button: MouseButton::Left, device_id: unsafe { DeviceId::dummy() }, - modifiers: ModifiersState::default(), }, window_id: unsafe { WindowId::dummy() }, }, end_state: ClickState::DoubleClick, + input_delay: Duration::ZERO, + } + + test_clickstate! { + name: double_click_failed, + initial_state: ClickState::Click, + initial_button: MouseButton::Left, + input: WinitEvent::WindowEvent { + event: WindowEvent::MouseInput { + state: ElementState::Pressed, + button: MouseButton::Left, + device_id: unsafe { DeviceId::dummy() }, + }, + window_id: unsafe { WindowId::dummy() }, + }, + end_state: ClickState::Click, + input_delay: CLICK_THRESHOLD, } test_clickstate! { @@ -1443,11 +1399,27 @@ mod tests { state: ElementState::Pressed, button: MouseButton::Left, device_id: unsafe { DeviceId::dummy() }, - modifiers: ModifiersState::default(), }, window_id: unsafe { WindowId::dummy() }, }, end_state: ClickState::TripleClick, + input_delay: Duration::ZERO, + } + + test_clickstate! { + name: triple_click_failed, + initial_state: ClickState::DoubleClick, + initial_button: MouseButton::Left, + input: WinitEvent::WindowEvent { + event: WindowEvent::MouseInput { + state: ElementState::Pressed, + button: MouseButton::Left, + device_id: unsafe { DeviceId::dummy() }, + }, + window_id: unsafe { WindowId::dummy() }, + }, + end_state: ClickState::Click, + input_delay: CLICK_THRESHOLD, } test_clickstate! { @@ -1459,11 +1431,11 @@ mod tests { state: ElementState::Pressed, button: MouseButton::Right, device_id: unsafe { DeviceId::dummy() }, - modifiers: ModifiersState::default(), }, window_id: unsafe { WindowId::dummy() }, }, end_state: ClickState::Click, + input_delay: Duration::ZERO, } test_process_binding! { @@ -1484,10 +1456,10 @@ mod tests { test_process_binding! { name: process_binding_nomode_controlmod, - binding: Binding { trigger: KEY, mods: ModifiersState::CTRL, action: Action::from("\x1b[1;5D"), mode: BindingMode::empty(), notmode: BindingMode::empty() }, + binding: Binding { trigger: KEY, mods: ModifiersState::CONTROL, action: Action::from("\x1b[1;5D"), mode: BindingMode::empty(), notmode: BindingMode::empty() }, triggers: true, mode: BindingMode::empty(), - mods: ModifiersState::CTRL, + mods: ModifiersState::CONTROL, } test_process_binding! { @@ -1524,9 +1496,9 @@ mod tests { test_process_binding! { name: process_binding_fail_with_extra_mods, - binding: Binding { trigger: KEY, mods: ModifiersState::LOGO, action: Action::from("arst"), mode: BindingMode::empty(), notmode: BindingMode::empty() }, + binding: Binding { trigger: KEY, mods: ModifiersState::SUPER, action: Action::from("arst"), mode: BindingMode::empty(), notmode: BindingMode::empty() }, triggers: false, mode: BindingMode::empty(), - mods: ModifiersState::ALT | ModifiersState::LOGO, + mods: ModifiersState::ALT | ModifiersState::SUPER, } } diff --git a/alacritty/src/logging.rs b/alacritty/src/logging.rs index 846ab1c5..42f1536e 100644 --- a/alacritty/src/logging.rs +++ b/alacritty/src/logging.rs @@ -8,25 +8,41 @@ use std::fs::{File, OpenOptions}; use std::io::{self, LineWriter, Stdout, Write}; use std::path::PathBuf; use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Mutex, OnceLock}; use std::time::Instant; use std::{env, process}; use log::{self, Level, LevelFilter}; use winit::event_loop::EventLoopProxy; -use alacritty_terminal::config::LOG_TARGET_CONFIG; - use crate::cli::Options; use crate::event::{Event, EventType}; use crate::message_bar::{Message, MessageType}; /// Logging target for IPC config error messages. -pub const LOG_TARGET_IPC_CONFIG: &str = "alacritty_log_ipc_config"; +pub const LOG_TARGET_IPC_CONFIG: &str = "alacritty_log_window_config"; /// Name for the environment variable containing the log file's path. const ALACRITTY_LOG_ENV: &str = "ALACRITTY_LOG"; +/// Logging target for config error messages. +pub const LOG_TARGET_CONFIG: &str = "alacritty_config_derive"; + +/// Name for the environment variable containing extra logging targets. +/// +/// The targets are semicolon separated. +const ALACRITTY_EXTRA_LOG_TARGETS_ENV: &str = "ALACRITTY_EXTRA_LOG_TARGETS"; + +/// User configurable extra log targets to include. +fn extra_log_targets() -> &'static [String] { + static EXTRA_LOG_TARGETS: OnceLock<Vec<String>> = OnceLock::new(); + + EXTRA_LOG_TARGETS.get_or_init(|| { + env::var(ALACRITTY_EXTRA_LOG_TARGETS_ENV) + .map_or(Vec::new(), |targets| targets.split(';').map(ToString::to_string).collect()) + }) +} + /// List of targets which will be logged by Alacritty. const ALLOWED_TARGETS: &[&str] = &[ LOG_TARGET_IPC_CONFIG, @@ -37,6 +53,7 @@ const ALLOWED_TARGETS: &[&str] = &[ "crossfont", ]; +/// Initialize the logger to its defaults. pub fn initialize( options: &Options, event_proxy: EventLoopProxy<Event>, @@ -167,7 +184,7 @@ fn create_log_message(record: &log::Record<'_>, target: &str, start: Instant) -> fn is_allowed_target(level: Level, target: &str) -> bool { match (level, log::max_level()) { (Level::Error, LevelFilter::Trace) | (Level::Warn, LevelFilter::Trace) => true, - _ => ALLOWED_TARGETS.contains(&target), + _ => ALLOWED_TARGETS.contains(&target) || extra_log_targets().iter().any(|t| t == target), } } diff --git a/alacritty/src/main.rs b/alacritty/src/main.rs index d9d26022..91d99cbe 100644 --- a/alacritty/src/main.rs +++ b/alacritty/src/main.rs @@ -12,13 +12,11 @@ #[cfg(not(any(feature = "x11", feature = "wayland", target_os = "macos", windows)))] compile_error!(r#"at least one of the "x11"/"wayland" features must be enabled"#); -#[cfg(target_os = "macos")] -use std::env; use std::error::Error; use std::fmt::Write as _; -use std::fs; use std::io::{self, Write}; use std::path::PathBuf; +use std::{env, fs}; use log::info; #[cfg(windows)] @@ -42,6 +40,7 @@ mod logging; #[cfg(target_os = "macos")] mod macos; mod message_bar; +mod migrate; #[cfg(windows)] mod panic; mod renderer; @@ -54,9 +53,9 @@ mod gl { include!(concat!(env!("OUT_DIR"), "/gl_bindings.rs")); } -use crate::cli::Options; #[cfg(unix)] -use crate::cli::{MessageOptions, Subcommands}; +use crate::cli::MessageOptions; +use crate::cli::{Options, Subcommands}; use crate::config::{monitor, UiConfig}; use crate::event::{Event, Processor}; #[cfg(target_os = "macos")] @@ -77,14 +76,14 @@ fn main() -> Result<(), Box<dyn Error>> { // Load command line options. let options = Options::new(); - #[cfg(unix)] match options.subcommands { - Some(Subcommands::Msg(options)) => msg(options), - None => alacritty(options), + #[cfg(unix)] + Some(Subcommands::Msg(options)) => msg(options)?, + Some(Subcommands::Migrate(options)) => migrate::migrate(options), + None => alacritty(options)?, } - #[cfg(not(unix))] - alacritty(options) + Ok(()) } /// `msg` subcommand entrypoint. @@ -123,9 +122,9 @@ impl Drop for TemporaryFiles { /// /// Creates a window, the terminal state, PTY, I/O event loop, input processor, /// config change monitor, and runs the main display loop. -fn alacritty(options: Options) -> Result<(), Box<dyn Error>> { +fn alacritty(mut options: Options) -> Result<(), Box<dyn Error>> { // Setup winit event loop. - let window_event_loop = WinitEventLoopBuilder::<Event>::with_user_event().build(); + let window_event_loop = WinitEventLoopBuilder::<Event>::with_user_event().build()?; // Initialize the logger as soon as possible as to capture output from other subsystems. let log_file = logging::initialize(&options, window_event_loop.create_proxy()) @@ -140,18 +139,23 @@ fn alacritty(options: Options) -> Result<(), Box<dyn Error>> { info!("Running on Wayland"); // Load configuration file. - let config = config::load(&options); + let config = config::load(&mut options); log_config_path(&config); // Update the log level from config. log::set_max_level(config.debug.log_level); - // Set environment variables. - tty::setup_env(&config.terminal_config); + // Set tty environment variables. + tty::setup_env(); + + // Set env vars from config. + for (key, value) in config.env.iter() { + env::set_var(key, value); + } // Switch to home directory. #[cfg(target_os = "macos")] - env::set_current_dir(dirs::home_dir().unwrap()).unwrap(); + env::set_current_dir(home::home_dir().unwrap()).unwrap(); // Set macOS locale. #[cfg(target_os = "macos")] diff --git a/alacritty/src/message_bar.rs b/alacritty/src/message_bar.rs index 988a6a31..267f8322 100644 --- a/alacritty/src/message_bar.rs +++ b/alacritty/src/message_bar.rs @@ -181,6 +181,12 @@ impl MessageBuffer { pub fn push(&mut self, message: Message) { self.messages.push_back(message); } + + /// Check whether the message is already queued in the message bar. + #[inline] + pub fn is_queued(&self, message: &Message) -> bool { + self.messages.contains(message) + } } #[cfg(test)] diff --git a/alacritty/src/migrate.rs b/alacritty/src/migrate.rs new file mode 100644 index 00000000..dbcfb2ae --- /dev/null +++ b/alacritty/src/migrate.rs @@ -0,0 +1,266 @@ +//! Configuration file migration. + +use std::fs; +use std::path::Path; + +use toml::map::Entry; +use toml::{Table, Value}; + +use crate::cli::MigrateOptions; +use crate::config; + +/// Handle migration. +pub fn migrate(options: MigrateOptions) { + // Find configuration file path. + let config_path = options + .config_file + .clone() + .or_else(|| config::installed_config("toml")) + .or_else(|| config::installed_config("yml")); + + // Abort if system has no installed configuration. + let config_path = match config_path { + Some(config_path) => config_path, + None => { + eprintln!("No configuration file found"); + std::process::exit(1); + }, + }; + + // If we're doing a wet run, perform a dry run first for safety. + if !options.dry_run { + #[allow(clippy::redundant_clone)] + let mut options = options.clone(); + options.silent = true; + options.dry_run = true; + if let Err(err) = migrate_config(&options, &config_path, config::IMPORT_RECURSION_LIMIT) { + eprintln!("Configuration file migration failed:"); + eprintln!(" {config_path:?}: {err}"); + std::process::exit(1); + } + } + + // Migrate the root config. + match migrate_config(&options, &config_path, config::IMPORT_RECURSION_LIMIT) { + Ok(new_path) => { + if !options.silent { + println!("Successfully migrated {config_path:?} to {new_path:?}"); + } + }, + Err(err) => { + eprintln!("Configuration file migration failed:"); + eprintln!(" {config_path:?}: {err}"); + std::process::exit(1); + }, + } +} + +/// Migrate a specific configuration file. +fn migrate_config( + options: &MigrateOptions, + path: &Path, + recursion_limit: usize, +) -> Result<String, String> { + // Ensure configuration file has an extension. + let path_str = path.to_string_lossy(); + let (prefix, suffix) = match path_str.rsplit_once('.') { + Some((prefix, suffix)) => (prefix, suffix), + None => return Err("missing file extension".to_string()), + }; + + // Abort if config is already toml. + if suffix == "toml" { + return Err("already in TOML format".to_string()); + } + + // Try to parse the configuration file. + let mut config = match config::deserialize_config(path, !options.dry_run) { + Ok(config) => config, + Err(err) => return Err(format!("parsing error: {err}")), + }; + + // Migrate config imports. + if !options.skip_imports { + migrate_imports(options, &mut config, recursion_limit)?; + } + + // Migrate deprecated field names to their new location. + if !options.skip_renames { + migrate_renames(&mut config)?; + } + + // Convert to TOML format. + let toml = toml::to_string(&config).map_err(|err| format!("conversion error: {err}"))?; + let new_path = format!("{prefix}.toml"); + + if options.dry_run && !options.silent { + // Output new content to STDOUT. + println!( + "\nv-----Start TOML for {path:?}-----v\n\n{toml}\n^-----End TOML for {path:?}-----^\n" + ); + } else if !options.dry_run { + // Write the new toml configuration. + fs::write(&new_path, toml).map_err(|err| format!("filesystem error: {err}"))?; + } + + Ok(new_path) +} + +/// Migrate the imports of a config. +fn migrate_imports( + options: &MigrateOptions, + config: &mut Value, + recursion_limit: usize, +) -> Result<(), String> { + let imports = match config::imports(config, recursion_limit) { + Ok(imports) => imports, + Err(err) => return Err(format!("import error: {err}")), + }; + + // Migrate the individual imports. + let mut new_imports = Vec::new(); + for import in imports { + let import = match import { + Ok(import) => import, + Err(err) => return Err(format!("import error: {err}")), + }; + + // Keep yaml import if path does not exist. + if !import.exists() { + if options.dry_run { + eprintln!("Keeping yaml config for nonexistent import: {import:?}"); + } + new_imports.push(Value::String(import.to_string_lossy().into())); + continue; + } + + let new_path = migrate_config(options, &import, recursion_limit - 1)?; + + // Print new import path. + if options.dry_run { + println!("Successfully migrated import {import:?} to {new_path:?}"); + } + + new_imports.push(Value::String(new_path)); + } + + // Update the imports field. + if let Some(import) = config.get_mut("import") { + *import = Value::Array(new_imports); + } + + Ok(()) +} + +/// Migrate deprecated fields. +fn migrate_renames(config: &mut Value) -> Result<(), String> { + let config_table = match config.as_table_mut() { + Some(config_table) => config_table, + None => return Ok(()), + }; + + // draw_bold_text_with_bright_colors -> colors.draw_bold_text_with_bright_colors + move_value(config_table, &["draw_bold_text_with_bright_colors"], &[ + "colors", + "draw_bold_text_with_bright_colors", + ])?; + + // key_bindings -> keyboard.bindings + move_value(config_table, &["key_bindings"], &["keyboard", "bindings"])?; + + // mouse_bindings -> mouse.bindings + move_value(config_table, &["mouse_bindings"], &["mouse", "bindings"])?; + + Ok(()) +} + +/// Move a toml value from one map to another. +fn move_value(config_table: &mut Table, origin: &[&str], target: &[&str]) -> Result<(), String> { + if let Some(value) = remove_node(config_table, origin)? { + if !insert_node_if_empty(config_table, target, value)? { + return Err(format!( + "conflict: both `{}` and `{}` are set", + origin.join("."), + target.join(".") + )); + } + } + + Ok(()) +} + +/// Remove a node from a tree of tables. +fn remove_node(table: &mut Table, path: &[&str]) -> Result<Option<Value>, String> { + if path.len() == 1 { + Ok(table.remove(path[0])) + } else { + let next_table_value = match table.get_mut(path[0]) { + Some(next_table_value) => next_table_value, + None => return Ok(None), + }; + + let next_table = match next_table_value.as_table_mut() { + Some(next_table) => next_table, + None => return Err(format!("invalid `{}` table", path[0])), + }; + + remove_node(next_table, &path[1..]) + } +} + +/// Try to insert a node into a tree of tables. +/// +/// Returns `false` if the node already exists. +fn insert_node_if_empty(table: &mut Table, path: &[&str], node: Value) -> Result<bool, String> { + if path.len() == 1 { + match table.entry(path[0]) { + Entry::Vacant(vacant_entry) => { + vacant_entry.insert(node); + Ok(true) + }, + Entry::Occupied(_) => Ok(false), + } + } else { + let next_table_value = table.entry(path[0]).or_insert_with(|| Value::Table(Table::new())); + + let next_table = match next_table_value.as_table_mut() { + Some(next_table) => next_table, + None => return Err(format!("invalid `{}` table", path[0])), + }; + + insert_node_if_empty(next_table, &path[1..], node) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn move_values() { + let input = r#" +root_value = 3 + +[table] +table_value = 5 + +[preexisting] +not_moved = 9 + "#; + + let mut value: Value = toml::from_str(input).unwrap(); + let table = value.as_table_mut().unwrap(); + + move_value(table, &["root_value"], &["new_table", "root_value"]).unwrap(); + move_value(table, &["table", "table_value"], &["preexisting", "subtable", "new_name"]) + .unwrap(); + + let output = toml::to_string(table).unwrap(); + + assert_eq!( + output, + "[new_table]\nroot_value = 3\n\n[preexisting]\nnot_moved = \ + 9\n\n[preexisting.subtable]\nnew_name = 5\n\n[table]\n" + ); + } +} diff --git a/alacritty/src/renderer/mod.rs b/alacritty/src/renderer/mod.rs index 28447db1..d869b503 100644 --- a/alacritty/src/renderer/mod.rs +++ b/alacritty/src/renderer/mod.rs @@ -1,20 +1,23 @@ +use std::borrow::Cow; use std::collections::HashSet; use std::ffi::{CStr, CString}; -use std::fmt; use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::OnceLock; +use std::{fmt, ptr}; +use ahash::RandomState; use crossfont::Metrics; use glutin::context::{ContextApi, GlContext, PossiblyCurrentContext}; use glutin::display::{GetGlDisplay, GlDisplay}; -use log::info; -use once_cell::sync::OnceCell; +use log::{debug, error, info, warn, LevelFilter}; +use unicode_width::UnicodeWidthChar; use alacritty_terminal::graphics::UpdateQueues; use alacritty_terminal::index::Point; use alacritty_terminal::term::cell::Flags; -use alacritty_terminal::term::color::Rgb; use crate::config::debug::RendererPreference; +use crate::display::color::Rgb; use crate::display::content::RenderableCell; use crate::display::SizeInfo; use crate::gl; @@ -48,12 +51,16 @@ pub static GL_FUNS_LOADED: AtomicBool = AtomicBool::new(false); pub enum Error { /// Shader error. Shader(ShaderError), + + /// Other error. + Other(String), } impl std::error::Error for Error { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { Error::Shader(err) => err.source(), + Error::Other(_) => None, } } } @@ -64,6 +71,9 @@ impl fmt::Display for Error { Error::Shader(err) => { write!(f, "There was an error initializing the shaders: {}", err) }, + Error::Other(err) => { + write!(f, "{}", err) + }, } } } @@ -74,6 +84,12 @@ impl From<ShaderError> for Error { } } +impl From<String> for Error { + fn from(val: String) -> Self { + Error::Other(val) + } +} + #[derive(Debug)] enum TextRendererProvider { Gles2(Gles2Renderer), @@ -87,6 +103,25 @@ pub struct Renderer { graphics_renderer: GraphicsRenderer, } +/// Wrapper around gl::GetString with error checking and reporting. +fn gl_get_string( + string_id: gl::types::GLenum, + description: &str, +) -> Result<Cow<'static, str>, Error> { + unsafe { + let string_ptr = gl::GetString(string_id); + match gl::GetError() { + gl::NO_ERROR if !string_ptr.is_null() => { + Ok(CStr::from_ptr(string_ptr as *const _).to_string_lossy()) + }, + gl::INVALID_ENUM => { + Err(format!("OpenGL error requesting {}: invalid enum", description).into()) + }, + error_id => Err(format!("OpenGL error {} requesting {}", error_id, description).into()), + } + } +} + impl Renderer { /// Create a new renderer. /// @@ -94,7 +129,7 @@ impl Renderer { /// supported OpenGL version. pub fn new( context: &PossiblyCurrentContext, - renderer_prefernce: Option<RendererPreference>, + renderer_preference: Option<RendererPreference>, ) -> Result<Self, Error> { // We need to load OpenGL functions once per instance, but only after we make our context // current due to WGL limitations. @@ -106,22 +141,21 @@ impl Renderer { }); } - let (version, renderer) = unsafe { - let renderer = CStr::from_ptr(gl::GetString(gl::RENDERER) as *mut _); - let version = CStr::from_ptr(gl::GetString(gl::SHADING_LANGUAGE_VERSION) as *mut _); - (version.to_string_lossy(), renderer.to_string_lossy()) - }; + let shader_version = gl_get_string(gl::SHADING_LANGUAGE_VERSION, "shader version")?; + let gl_version = gl_get_string(gl::VERSION, "OpenGL version")?; + let renderer = gl_get_string(gl::RENDERER, "renderer version")?; - info!("Running on {}", renderer); + info!("Running on {renderer}"); + info!("OpenGL version {gl_version}, shader_version {shader_version}"); let is_gles_context = matches!(context.context_api(), ContextApi::Gles(_)); // Use the config option to enforce a particular renderer configuration. - let (use_glsl3, allow_dsb) = match renderer_prefernce { + let (use_glsl3, allow_dsb) = match renderer_preference { Some(RendererPreference::Glsl3) => (true, true), Some(RendererPreference::Gles2) => (false, true), Some(RendererPreference::Gles2Pure) => (false, false), - None => (version.as_ref() >= "3.3" && !is_gles_context, true), + None => (shader_version.as_ref() >= "3.3" && !is_gles_context, true), }; let (text_renderer, rect_renderer, graphics_renderer) = if use_glsl3 { @@ -137,6 +171,16 @@ impl Renderer { (text_renderer, rect_renderer, graphics_renderer) }; + // Enable debug logging for OpenGL as well. + if log::max_level() >= LevelFilter::Debug && GlExtensions::contains("GL_KHR_debug") { + debug!("Enabled debug logging for OpenGL"); + unsafe { + gl::Enable(gl::DEBUG_OUTPUT); + gl::Enable(gl::DEBUG_OUTPUT_SYNCHRONOUS); + gl::DebugMessageCallback(Some(gl_debug_log), ptr::null_mut()); + } + } + Ok(Self { text_renderer, rect_renderer, graphics_renderer }) } @@ -167,15 +211,30 @@ impl Renderer { size_info: &SizeInfo, glyph_cache: &mut GlyphCache, ) { - let cells = string_chars.enumerate().map(|(i, character)| RenderableCell { - point: Point::new(point.line, point.column + i), - character, - extra: None, - flags: Flags::empty(), - bg_alpha: 1.0, - fg, - bg, - underline: fg, + let mut skip_next = false; + let cells = string_chars.enumerate().filter_map(|(i, character)| { + if skip_next { + skip_next = false; + return None; + } + + let mut flags = Flags::empty(); + if character.width() == Some(2) { + flags.insert(Flags::WIDE_CHAR); + // Wide character is always followed by a spacer, so skip it. + skip_next = true; + } + + Some(RenderableCell { + point: Point::new(point.line, point.column + i), + character, + extra: None, + flags: Flags::empty(), + bg_alpha: 1.0, + fg, + bg, + underline: fg, + }) }); self.draw_cells(size_info, glyph_cache, cells); @@ -229,7 +288,6 @@ impl Renderer { } } - #[cfg(not(any(target_os = "macos", windows)))] pub fn finish(&self) { unsafe { gl::Finish(); @@ -296,15 +354,15 @@ struct GlExtensions; impl GlExtensions { /// Check if the given `extension` is supported. /// - /// This function will lazyly load OpenGL extensions. + /// This function will lazily load OpenGL extensions. fn contains(extension: &str) -> bool { - static OPENGL_EXTENSIONS: OnceCell<HashSet<&'static str>> = OnceCell::new(); + static OPENGL_EXTENSIONS: OnceLock<HashSet<&'static str, RandomState>> = OnceLock::new(); OPENGL_EXTENSIONS.get_or_init(Self::load_extensions).contains(extension) } /// Load available OpenGL extensions. - fn load_extensions() -> HashSet<&'static str> { + fn load_extensions() -> HashSet<&'static str, RandomState> { unsafe { let extensions = gl::GetString(gl::EXTENSIONS); @@ -321,9 +379,28 @@ impl GlExtensions { } else { match CStr::from_ptr(extensions as *mut _).to_str() { Ok(ext) => ext.split_whitespace().collect(), - Err(_) => HashSet::new(), + Err(_) => Default::default(), } } } } } + +extern "system" fn gl_debug_log( + _: gl::types::GLenum, + kind: gl::types::GLenum, + _: gl::types::GLuint, + _: gl::types::GLenum, + _: gl::types::GLsizei, + msg: *const gl::types::GLchar, + _: *mut std::os::raw::c_void, +) { + let msg = unsafe { CStr::from_ptr(msg).to_string_lossy() }; + match kind { + gl::DEBUG_TYPE_ERROR | gl::DEBUG_TYPE_UNDEFINED_BEHAVIOR => { + error!("[gl_render] {}", msg) + }, + gl::DEBUG_TYPE_DEPRECATED_BEHAVIOR => warn!("[gl_render] {}", msg), + _ => debug!("[gl_render] {}", msg), + } +} diff --git a/alacritty/src/renderer/platform.rs b/alacritty/src/renderer/platform.rs index c9802e0a..3568bd20 100644 --- a/alacritty/src/renderer/platform.rs +++ b/alacritty/src/renderer/platform.rs @@ -10,6 +10,7 @@ use glutin::display::{Display, DisplayApiPreference, GetGlDisplay}; use glutin::error::Result as GlutinResult; use glutin::prelude::*; use glutin::surface::{Surface, SurfaceAttributesBuilder, WindowSurface}; +use log::{debug, LevelFilter}; use raw_window_handle::{RawDisplayHandle, RawWindowHandle}; use winit::dpi::PhysicalSize; @@ -20,15 +21,24 @@ use winit::platform::x11; pub fn create_gl_display( raw_display_handle: RawDisplayHandle, _raw_window_handle: Option<RawWindowHandle>, + _prefer_egl: bool, ) -> GlutinResult<Display> { #[cfg(target_os = "macos")] let preference = DisplayApiPreference::Cgl; #[cfg(windows)] - let preference = DisplayApiPreference::Wgl(Some(_raw_window_handle.unwrap())); + let preference = if _prefer_egl { + DisplayApiPreference::EglThenWgl(Some(_raw_window_handle.unwrap())) + } else { + DisplayApiPreference::WglThenEgl(Some(_raw_window_handle.unwrap())) + }; #[cfg(all(feature = "x11", not(any(target_os = "macos", windows))))] - let preference = DisplayApiPreference::GlxThenEgl(Box::new(x11::register_xlib_error_hook)); + let preference = if _prefer_egl { + DisplayApiPreference::EglThenGlx(Box::new(x11::register_xlib_error_hook)) + } else { + DisplayApiPreference::GlxThenEgl(Box::new(x11::register_xlib_error_hook)) + }; #[cfg(all(not(feature = "x11"), not(any(target_os = "macos", windows))))] let preference = DisplayApiPreference::Egl; @@ -69,6 +79,24 @@ pub fn pick_gl_config( }; if let Some(gl_config) = gl_config { + debug!( + r#"Picked GL Config: + buffer_type: {:?} + alpha_size: {} + num_samples: {} + hardware_accelerated: {:?} + supports_transparency: {:?} + config_api: {:?} + srgb_capable: {}"#, + gl_config.color_buffer_type(), + gl_config.alpha_size(), + gl_config.num_samples(), + gl_config.hardware_accelerated(), + gl_config.supports_transparency(), + gl_config.api(), + gl_config.srgb_capable(), + ); + return Ok(gl_config); } } @@ -81,15 +109,19 @@ pub fn create_gl_context( gl_config: &Config, raw_window_handle: Option<RawWindowHandle>, ) -> GlutinResult<NotCurrentContext> { + let debug = log::max_level() >= LevelFilter::Debug; let mut profiles = [ ContextAttributesBuilder::new() + .with_debug(debug) .with_context_api(ContextApi::OpenGl(Some(Version::new(3, 3)))) .build(raw_window_handle), // Try gles before OpenGL 2.1 as it tends to be more stable. ContextAttributesBuilder::new() + .with_debug(debug) .with_context_api(ContextApi::Gles(Some(Version::new(2, 0)))) .build(raw_window_handle), ContextAttributesBuilder::new() + .with_debug(debug) .with_profile(GlProfile::Compatibility) .with_context_api(ContextApi::OpenGl(Some(Version::new(2, 1)))) .build(raw_window_handle), diff --git a/alacritty/src/renderer/rects.rs b/alacritty/src/renderer/rects.rs index 0ce80664..0bbcb832 100644 --- a/alacritty/src/renderer/rects.rs +++ b/alacritty/src/renderer/rects.rs @@ -1,13 +1,15 @@ use std::collections::HashMap; use std::mem; +use ahash::RandomState; use crossfont::Metrics; +use log::info; use alacritty_terminal::grid::Dimensions; use alacritty_terminal::index::{Column, Point}; use alacritty_terminal::term::cell::Flags; -use alacritty_terminal::term::color::Rgb; +use crate::display::color::Rgb; use crate::display::content::RenderableCell; use crate::display::SizeInfo; use crate::gl; @@ -157,7 +159,7 @@ impl RenderLine { /// Lines for underline and strikeout. #[derive(Default)] pub struct RenderLines { - inner: HashMap<Flags, Vec<RenderLine>>, + inner: HashMap<Flags, Vec<RenderLine>, RandomState>, } impl RenderLines { @@ -264,7 +266,16 @@ impl RectRenderer { let rect_program = RectShaderProgram::new(shader_version, RectKind::Normal)?; let undercurl_program = RectShaderProgram::new(shader_version, RectKind::Undercurl)?; - let dotted_program = RectShaderProgram::new(shader_version, RectKind::DottedUnderline)?; + // This shader has way more ALU operations than other rect shaders, so use a fallback + // to underline just for it when we can't compile it. + let dotted_program = match RectShaderProgram::new(shader_version, RectKind::DottedUnderline) + { + Ok(dotted_program) => dotted_program, + Err(err) => { + info!("Error compiling dotted shader: {err}\n falling back to underline"); + RectShaderProgram::new(shader_version, RectKind::Normal)? + }, + }; let dashed_program = RectShaderProgram::new(shader_version, RectKind::DashedUnderline)?; unsafe { @@ -370,7 +381,7 @@ impl RectRenderer { let y = -rect.y / half_height + 1.0; let width = rect.width / half_width; let height = rect.height / half_height; - let Rgb { r, g, b } = rect.color; + let (r, g, b) = rect.color.as_tuple(); let a = (rect.alpha * 255.) as u8; // Make quad vertices. diff --git a/alacritty/src/renderer/shader.rs b/alacritty/src/renderer/shader.rs index 588937cc..e3baab9e 100644 --- a/alacritty/src/renderer/shader.rs +++ b/alacritty/src/renderer/shader.rs @@ -91,17 +91,17 @@ impl Shader { ) -> Result<Self, ShaderError> { let version_header = shader_version.shader_header(); let mut sources = Vec::<*const GLchar>::with_capacity(3); - let mut lengthes = Vec::<GLint>::with_capacity(3); + let mut lengths = Vec::<GLint>::with_capacity(3); sources.push(version_header.as_ptr().cast()); - lengthes.push(version_header.len() as GLint); + lengths.push(version_header.len() as GLint); if let Some(shader_header) = shader_header { sources.push(shader_header.as_ptr().cast()); - lengthes.push(shader_header.len() as GLint); + lengths.push(shader_header.len() as GLint); } sources.push(source.as_ptr().cast()); - lengthes.push(source.len() as GLint); + lengths.push(source.len() as GLint); let shader = unsafe { Self(gl::CreateShader(kind)) }; @@ -109,9 +109,9 @@ impl Shader { unsafe { gl::ShaderSource( shader.id(), - lengthes.len() as GLint, + lengths.len() as GLint, sources.as_ptr().cast(), - lengthes.as_ptr(), + lengths.as_ptr(), ); gl::CompileShader(shader.id()); gl::GetShaderiv(shader.id(), gl::COMPILE_STATUS, &mut success); diff --git a/alacritty/src/renderer/text/builtin_font.rs b/alacritty/src/renderer/text/builtin_font.rs index f2c0e3ea..ece7eb86 100644 --- a/alacritty/src/renderer/text/builtin_font.rs +++ b/alacritty/src/renderer/text/builtin_font.rs @@ -1,5 +1,4 @@ -//! Hand-rolled drawing of unicode [box drawing](http://www.unicode.org/charts/PDF/U2500.pdf) -//! and [block elements](https://www.unicode.org/charts/PDF/U2580.pdf). +//! Hand-rolled drawing of unicode characters that need to fully cover their character area. use std::{cmp, mem, ops}; @@ -15,6 +14,11 @@ const COLOR_FILL_ALPHA_STEP_3: Pixel = Pixel { _r: 64, _g: 64, _b: 64 }; /// Default color used for filling. const COLOR_FILL: Pixel = Pixel { _r: 255, _g: 255, _b: 255 }; +const POWERLINE_TRIANGLE_LTR: char = '\u{e0b0}'; +const POWERLINE_ARROW_LTR: char = '\u{e0b1}'; +const POWERLINE_TRIANGLE_RTL: char = '\u{e0b2}'; +const POWERLINE_ARROW_RTL: char = '\u{e0b3}'; + /// Returns the rasterized glyph if the character is part of the built-in font. pub fn builtin_glyph( character: char, @@ -24,7 +28,13 @@ pub fn builtin_glyph( ) -> Option<RasterizedGlyph> { let mut glyph = match character { // Box drawing characters and block elements. - '\u{2500}'..='\u{259f}' => box_drawing(character, metrics, offset), + '\u{2500}'..='\u{259f}' | '\u{1fb00}'..='\u{1fb3b}' => { + box_drawing(character, metrics, offset) + }, + // Powerline symbols: '','','','' + POWERLINE_TRIANGLE_LTR..=POWERLINE_ARROW_RTL => { + powerline_drawing(character, metrics, offset)? + }, _ => return None, }; @@ -40,8 +50,7 @@ fn box_drawing(character: char, metrics: &Metrics, offset: &Delta<i8>) -> Raster // Ensure that width and height is at least one. let height = (metrics.line_height as i32 + offset.y as i32).max(1) as usize; let width = (metrics.average_advance as i32 + offset.x as i32).max(1) as usize; - // Use one eight of the cell width, since this is used as a step size for block elemenets. - let stroke_size = cmp::max((width as f32 / 8.).round() as usize, 1); + let stroke_size = calculate_stroke_size(width); let heavy_stroke_size = stroke_size * 2; // Certain symbols require larger canvas than the cell itself, since for proper contiguous @@ -479,6 +488,89 @@ fn box_drawing(character: char, metrics: &Metrics, offset: &Delta<i8>) -> Raster // Fourth quadrant. canvas.draw_rect(x_center, y_center, w_fourth, h_fourth, COLOR_FILL); }, + // Sextants: '🬀', '🬁', '🬂', '🬃', '🬄', '🬅', '🬆', '🬇', '🬈', '🬉', '🬊', '🬋', '🬌', '🬍', '🬎', + // '🬏', '🬐', '🬑', '🬒', '🬓', '🬔', '🬕', '🬖', '🬗', '🬘', '🬙', '🬚', '🬛', '🬜', '🬝', '🬞', '🬟', + // '🬠', '🬡', '🬢', '🬣', '🬤', '🬥', '🬦', '🬧', '🬨', '🬩', '🬪', '🬫', '🬬', '🬭', '🬮', '🬯', '🬰', + // '🬱', '🬲', '🬳', '🬴', '🬵', '🬶', '🬷', '🬸', '🬹', '🬺', '🬻'. + '\u{1fb00}'..='\u{1fb3b}' => { + let x_center = canvas.x_center().round().max(1.); + let y_third = (height as f32 / 3.).round().max(1.); + let y_last_third = height as f32 - 2. * y_third; + + let (w_top_left, h_top_left) = match character { + '\u{1fb00}' | '\u{1fb02}' | '\u{1fb04}' | '\u{1fb06}' | '\u{1fb08}' + | '\u{1fb0a}' | '\u{1fb0c}' | '\u{1fb0e}' | '\u{1fb10}' | '\u{1fb12}' + | '\u{1fb15}' | '\u{1fb17}' | '\u{1fb19}' | '\u{1fb1b}' | '\u{1fb1d}' + | '\u{1fb1f}' | '\u{1fb21}' | '\u{1fb23}' | '\u{1fb25}' | '\u{1fb27}' + | '\u{1fb28}' | '\u{1fb2a}' | '\u{1fb2c}' | '\u{1fb2e}' | '\u{1fb30}' + | '\u{1fb32}' | '\u{1fb34}' | '\u{1fb36}' | '\u{1fb38}' | '\u{1fb3a}' => { + (x_center, y_third) + }, + _ => (0., 0.), + }; + let (w_top_right, h_top_right) = match character { + '\u{1fb01}' | '\u{1fb02}' | '\u{1fb05}' | '\u{1fb06}' | '\u{1fb09}' + | '\u{1fb0a}' | '\u{1fb0d}' | '\u{1fb0e}' | '\u{1fb11}' | '\u{1fb12}' + | '\u{1fb14}' | '\u{1fb15}' | '\u{1fb18}' | '\u{1fb19}' | '\u{1fb1c}' + | '\u{1fb1d}' | '\u{1fb20}' | '\u{1fb21}' | '\u{1fb24}' | '\u{1fb25}' + | '\u{1fb28}' | '\u{1fb2b}' | '\u{1fb2c}' | '\u{1fb2f}' | '\u{1fb30}' + | '\u{1fb33}' | '\u{1fb34}' | '\u{1fb37}' | '\u{1fb38}' | '\u{1fb3b}' => { + (x_center, y_third) + }, + _ => (0., 0.), + }; + let (w_mid_left, h_mid_left) = match character { + '\u{1fb03}' | '\u{1fb04}' | '\u{1fb05}' | '\u{1fb06}' | '\u{1fb0b}' + | '\u{1fb0c}' | '\u{1fb0d}' | '\u{1fb0e}' | '\u{1fb13}' | '\u{1fb14}' + | '\u{1fb15}' | '\u{1fb1a}' | '\u{1fb1b}' | '\u{1fb1c}' | '\u{1fb1d}' + | '\u{1fb22}' | '\u{1fb23}' | '\u{1fb24}' | '\u{1fb25}' | '\u{1fb29}' + | '\u{1fb2a}' | '\u{1fb2b}' | '\u{1fb2c}' | '\u{1fb31}' | '\u{1fb32}' + | '\u{1fb33}' | '\u{1fb34}' | '\u{1fb39}' | '\u{1fb3a}' | '\u{1fb3b}' => { + (x_center, y_third) + }, + _ => (0., 0.), + }; + let (w_mid_right, h_mid_right) = match character { + '\u{1fb07}' | '\u{1fb08}' | '\u{1fb09}' | '\u{1fb0a}' | '\u{1fb0b}' + | '\u{1fb0c}' | '\u{1fb0d}' | '\u{1fb0e}' | '\u{1fb16}' | '\u{1fb17}' + | '\u{1fb18}' | '\u{1fb19}' | '\u{1fb1a}' | '\u{1fb1b}' | '\u{1fb1c}' + | '\u{1fb1d}' | '\u{1fb26}' | '\u{1fb27}' | '\u{1fb28}' | '\u{1fb29}' + | '\u{1fb2a}' | '\u{1fb2b}' | '\u{1fb2c}' | '\u{1fb35}' | '\u{1fb36}' + | '\u{1fb37}' | '\u{1fb38}' | '\u{1fb39}' | '\u{1fb3a}' | '\u{1fb3b}' => { + (x_center, y_third) + }, + _ => (0., 0.), + }; + let (w_bottom_left, h_bottom_left) = match character { + '\u{1fb0f}' | '\u{1fb10}' | '\u{1fb11}' | '\u{1fb12}' | '\u{1fb13}' + | '\u{1fb14}' | '\u{1fb15}' | '\u{1fb16}' | '\u{1fb17}' | '\u{1fb18}' + | '\u{1fb19}' | '\u{1fb1a}' | '\u{1fb1b}' | '\u{1fb1c}' | '\u{1fb1d}' + | '\u{1fb2d}' | '\u{1fb2e}' | '\u{1fb2f}' | '\u{1fb30}' | '\u{1fb31}' + | '\u{1fb32}' | '\u{1fb33}' | '\u{1fb34}' | '\u{1fb35}' | '\u{1fb36}' + | '\u{1fb37}' | '\u{1fb38}' | '\u{1fb39}' | '\u{1fb3a}' | '\u{1fb3b}' => { + (x_center, y_last_third) + }, + _ => (0., 0.), + }; + let (w_bottom_right, h_bottom_right) = match character { + '\u{1fb1e}' | '\u{1fb1f}' | '\u{1fb20}' | '\u{1fb21}' | '\u{1fb22}' + | '\u{1fb23}' | '\u{1fb24}' | '\u{1fb25}' | '\u{1fb26}' | '\u{1fb27}' + | '\u{1fb28}' | '\u{1fb29}' | '\u{1fb2a}' | '\u{1fb2b}' | '\u{1fb2c}' + | '\u{1fb2d}' | '\u{1fb2e}' | '\u{1fb2f}' | '\u{1fb30}' | '\u{1fb31}' + | '\u{1fb32}' | '\u{1fb33}' | '\u{1fb34}' | '\u{1fb35}' | '\u{1fb36}' + | '\u{1fb37}' | '\u{1fb38}' | '\u{1fb39}' | '\u{1fb3a}' | '\u{1fb3b}' => { + (x_center, y_last_third) + }, + _ => (0., 0.), + }; + + canvas.draw_rect(0., 0., w_top_left, h_top_left, COLOR_FILL); + canvas.draw_rect(x_center, 0., w_top_right, h_top_right, COLOR_FILL); + canvas.draw_rect(0., y_third, w_mid_left, h_mid_left, COLOR_FILL); + canvas.draw_rect(x_center, y_third, w_mid_right, h_mid_right, COLOR_FILL); + canvas.draw_rect(0., y_third * 2., w_bottom_left, h_bottom_left, COLOR_FILL); + canvas.draw_rect(x_center, y_third * 2., w_bottom_right, h_bottom_right, COLOR_FILL); + }, _ => unreachable!(), } @@ -495,6 +587,79 @@ fn box_drawing(character: char, metrics: &Metrics, offset: &Delta<i8>) -> Raster } } +fn powerline_drawing( + character: char, + metrics: &Metrics, + offset: &Delta<i8>, +) -> Option<RasterizedGlyph> { + let height = (metrics.line_height as i32 + offset.y as i32) as usize; + let width = (metrics.average_advance as i32 + offset.x as i32) as usize; + let extra_thickness = calculate_stroke_size(width) as i32 - 1; + + let mut canvas = Canvas::new(width, height); + + let slope = 1; + let top_y = 1; + let bottom_y = height as i32 - top_y - 1; + + // Start with offset `1` and draw until the intersection of the f(x) = slope * x + 1 and + // g(x) = H - slope * x - 1 lines. The intersection happens when f(x) = g(x), which is at + // x = (H - 2) / (2 * slope). + let x_intersection = (height as i32 + 1) / 2 - 1; + + // Don't use built-in font if we'd cut the tip too much, for example when the font is really + // narrow. + if x_intersection - width as i32 > 1 { + return None; + } + + let top_line = (0..x_intersection).map(|x| line_equation(slope, x, top_y)); + let bottom_line = (0..x_intersection).map(|x| line_equation(-slope, x, bottom_y)); + + // Inner lines to make arrows thicker. + let mut top_inner_line = (0..x_intersection - extra_thickness) + .map(|x| line_equation(slope, x, top_y + extra_thickness)); + let mut bottom_inner_line = (0..x_intersection - extra_thickness) + .map(|x| line_equation(-slope, x, bottom_y - extra_thickness)); + + // NOTE: top_line and bottom_line have the same amount of iterations. + for (p1, p2) in top_line.zip(bottom_line) { + if character == POWERLINE_TRIANGLE_LTR || character == POWERLINE_TRIANGLE_RTL { + canvas.draw_rect(0., p1.1, p1.0 + 1., 1., COLOR_FILL); + canvas.draw_rect(0., p2.1, p2.0 + 1., 1., COLOR_FILL); + } else if character == POWERLINE_ARROW_LTR || character == POWERLINE_ARROW_RTL { + let p3 = top_inner_line.next().unwrap_or(p2); + let p4 = bottom_inner_line.next().unwrap_or(p1); + + // If we can't fit the entire arrow in the cell, we cut off the tip of the arrow by + // drawing a rectangle between the two lines. + if p1.0 as usize + 1 == width { + canvas.draw_rect(p1.0, p1.1, 1., p2.1 - p1.1 + 1., COLOR_FILL); + break; + } else { + canvas.draw_rect(p1.0, p1.1, 1., p3.1 - p1.1 + 1., COLOR_FILL); + canvas.draw_rect(p4.0, p4.1, 1., p2.1 - p4.1 + 1., COLOR_FILL); + } + } + } + + if character == POWERLINE_TRIANGLE_RTL || character == POWERLINE_ARROW_RTL { + canvas.flip_horizontal(); + } + + let top = height as i32 + metrics.descent as i32; + let buffer = BitmapBuffer::Rgb(canvas.into_raw()); + Some(RasterizedGlyph { + character, + top, + left: 0, + height: height as i32, + width: width as i32, + buffer, + advance: (width as i32, height as i32), + }) +} + #[repr(packed)] #[derive(Clone, Copy, Debug, Default)] struct Pixel { @@ -593,6 +758,16 @@ impl Canvas { (start_x, end_x) } + /// Flip horizontally. + fn flip_horizontal(&mut self) { + for row in 0..self.height { + for col in 0..self.width / 2 { + let index = row * self.width; + self.buffer.swap(index + col, index + self.width - col - 1) + } + } + } + /// Draws a horizontal straight line from (`x`, `y`) of `size` with the given `stroke_size`. fn draw_h_line(&mut self, x: f32, y: f32, size: f32, stroke_size: usize) { let (start_y, end_y) = self.h_line_bounds(y, stroke_size); @@ -704,10 +879,10 @@ impl Canvas { /// vertex and co-vertex respectively using a given `stroke` in the bottom-right quadrant of the /// `Canvas` coordinate system. fn draw_ellipse_arc(&mut self, stroke_size: usize) { - fn colors_with_error(error: f32, max_transparancy: f32) -> (Pixel, Pixel) { - let transparancy = error * max_transparancy; - let alpha_1 = 1. - transparancy; - let alpha_2 = 1. - (max_transparancy - transparancy); + fn colors_with_error(error: f32, max_transparency: f32) -> (Pixel, Pixel) { + let transparency = error * max_transparency; + let alpha_1 = 1. - transparency; + let alpha_2 = 1. - (max_transparency - transparency); let color_1 = Pixel::gray((COLOR_FILL._r as f32 * alpha_1) as u8); let color_2 = Pixel::gray((COLOR_FILL._r as f32 * alpha_2) as u8); (color_1, color_2) @@ -717,11 +892,10 @@ impl Canvas { let v_line_bounds = self.v_line_bounds(self.x_center(), stroke_size); let h_line_bounds = (h_line_bounds.0 as usize, h_line_bounds.1 as usize); let v_line_bounds = (v_line_bounds.0 as usize, v_line_bounds.1 as usize); - let max_transparancy = 0.5; + let max_transparency = 0.5; - for (radius_y, radius_x) in (h_line_bounds.0..h_line_bounds.1) - .into_iter() - .zip((v_line_bounds.0..v_line_bounds.1).into_iter()) + for (radius_y, radius_x) in + (h_line_bounds.0..h_line_bounds.1).zip(v_line_bounds.0..v_line_bounds.1) { let radius_x = radius_x as f32; let radius_y = radius_y as f32; @@ -734,7 +908,7 @@ impl Canvas { let y = radius_y * f32::sqrt(1. - x * x / radius_x2); let error = y.fract(); - let (color_1, color_2) = colors_with_error(error, max_transparancy); + let (color_1, color_2) = colors_with_error(error, max_transparency); let x = x.clamp(0., radius_x); let y_next = (y + 1.).clamp(0., h_line_bounds.1 as f32 - 1.); @@ -750,7 +924,7 @@ impl Canvas { let x = radius_x * f32::sqrt(1. - y * y / radius_y2); let error = x - x.fract(); - let (color_1, color_2) = colors_with_error(error, max_transparancy); + let (color_1, color_2) = colors_with_error(error, max_transparency); let x_next = (x + 1.).clamp(0., v_line_bounds.1 as f32 - 1.); let x = x.clamp(0., v_line_bounds.1 as f32 - 1.); @@ -803,34 +977,60 @@ impl Canvas { } } +/// Compute line width. +fn calculate_stroke_size(cell_width: usize) -> usize { + // Use one eight of the cell width, since this is used as a step size for block elements. + cmp::max((cell_width as f32 / 8.).round() as usize, 1) +} + +/// `f(x) = slope * x + offset` equation. +fn line_equation(slope: i32, x: i32, offset: i32) -> (f32, f32) { + (x as f32, (slope * x + offset) as f32) +} + #[cfg(test)] mod tests { use super::*; use crossfont::Metrics; + // Dummy metrics values to test builtin glyphs coverage. + const METRICS: Metrics = Metrics { + average_advance: 6., + line_height: 16., + descent: 4., + underline_position: 2., + underline_thickness: 2., + strikeout_position: 2., + strikeout_thickness: 2., + }; + #[test] fn builtin_line_drawing_glyphs_coverage() { - // Dummy metrics values to test built-in glyphs coverage. - let metrics = Metrics { - average_advance: 6., - line_height: 16., - descent: 4., - underline_position: 2., - underline_thickness: 2., - strikeout_position: 2., - strikeout_thickness: 2., - }; - let offset = Default::default(); let glyph_offset = Default::default(); // Test coverage of box drawing characters. - for character in '\u{2500}'..='\u{259f}' { - assert!(builtin_glyph(character, &metrics, &offset, &glyph_offset).is_some()); + for character in ('\u{2500}'..='\u{259f}').chain('\u{1fb00}'..='\u{1fb3b}') { + assert!(builtin_glyph(character, &METRICS, &offset, &glyph_offset).is_some()); } for character in ('\u{2450}'..'\u{2500}').chain('\u{25a0}'..'\u{2600}') { - assert!(builtin_glyph(character, &metrics, &offset, &glyph_offset).is_none()); + assert!(builtin_glyph(character, &METRICS, &offset, &glyph_offset).is_none()); + } + } + + #[test] + fn builtin_powerline_glyphs_coverage() { + let offset = Default::default(); + let glyph_offset = Default::default(); + + // Test coverage of box drawing characters. + for character in '\u{e0b0}'..='\u{e0b3}' { + assert!(builtin_glyph(character, &METRICS, &offset, &glyph_offset).is_some()); + } + + for character in ('\u{e0a0}'..'\u{e0b0}').chain('\u{e0b4}'..'\u{e0c0}') { + assert!(builtin_glyph(character, &METRICS, &offset, &glyph_offset).is_none()); } } } diff --git a/alacritty/src/renderer/text/glyph_cache.rs b/alacritty/src/renderer/text/glyph_cache.rs index 72415900..957cde1a 100644 --- a/alacritty/src/renderer/text/glyph_cache.rs +++ b/alacritty/src/renderer/text/glyph_cache.rs @@ -1,11 +1,10 @@ use std::collections::HashMap; -use std::hash::BuildHasherDefault; +use ahash::RandomState; use crossfont::{ Error as RasterizerError, FontDesc, FontKey, GlyphKey, Metrics, Rasterize, RasterizedGlyph, Rasterizer, Size, Slant, Style, Weight, }; -use fnv::FnvHasher; use log::{error, info}; use unicode_width::UnicodeWidthChar; @@ -46,7 +45,7 @@ pub struct Glyph { /// representations of the same code point. pub struct GlyphCache { /// Cache of buffered glyphs. - cache: HashMap<GlyphKey, Glyph, BuildHasherDefault<FnvHasher>>, + cache: HashMap<GlyphKey, Glyph, RandomState>, /// Rasterizer for loading new glyphs. rasterizer: Rasterizer, @@ -91,7 +90,7 @@ impl GlyphCache { let metrics = rasterizer.metrics(regular, font.size())?; Ok(Self { - cache: HashMap::default(), + cache: Default::default(), rasterizer, font_size: font.size(), font_key: regular, @@ -276,13 +275,8 @@ impl GlyphCache { /// /// NOTE: To reload the renderers's fonts [`Self::reset_glyph_cache`] should be called /// afterwards. - pub fn update_font_size( - &mut self, - font: &Font, - scale_factor: f64, - ) -> Result<(), crossfont::Error> { + pub fn update_font_size(&mut self, font: &Font) -> Result<(), crossfont::Error> { // Update dpi scaling. - self.rasterizer.update_dpr(scale_factor as f32); self.font_offset = font.offset; self.glyph_offset = font.glyph_offset; @@ -297,7 +291,7 @@ impl GlyphCache { })?; let metrics = self.rasterizer.metrics(regular, font.size())?; - info!("Font size changed to {:?} with scale factor of {}", font.size(), scale_factor); + info!("Font size changed to {:?} px", font.size().as_px()); self.font_size = font.size(); self.font_key = regular; diff --git a/alacritty/src/renderer/text/mod.rs b/alacritty/src/renderer/text/mod.rs index f02202b5..5ee6bc4c 100644 --- a/alacritty/src/renderer/text/mod.rs +++ b/alacritty/src/renderer/text/mod.rs @@ -23,6 +23,7 @@ use glyph_cache::{Glyph, LoadGlyph}; // NOTE: These flags must be in sync with their usage in the text.*.glsl shaders. bitflags! { #[repr(C)] + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] struct RenderingGlyphFlags: u8 { const COLORED = 0b0000_0001; const WIDE_CHAR = 0b0000_0010; diff --git a/alacritty/src/scheduler.rs b/alacritty/src/scheduler.rs index ea8e6271..aebed5e9 100644 --- a/alacritty/src/scheduler.rs +++ b/alacritty/src/scheduler.rs @@ -69,7 +69,7 @@ impl Scheduler { } } - self.timers.get(0).map(|timer| timer.deadline) + self.timers.front().map(|timer| timer.deadline) } /// Schedule a new event. diff --git a/alacritty/src/string.rs b/alacritty/src/string.rs index a111166d..e41b0785 100644 --- a/alacritty/src/string.rs +++ b/alacritty/src/string.rs @@ -30,7 +30,7 @@ pub enum ShortenDirection { /// Iterator that yield shortened version of the text. pub struct StrShortener<'a> { chars: Skip<Chars<'a>>, - accumulted_len: usize, + accumulated_len: usize, max_width: usize, direction: ShortenDirection, shortener: Option<char>, @@ -52,7 +52,7 @@ impl<'a> StrShortener<'a> { if direction == ShortenDirection::Right { return Self { chars: text.chars().skip(0), - accumulted_len: 0, + accumulated_len: 0, text_action: TextAction::Char, max_width, direction, @@ -101,7 +101,7 @@ impl<'a> StrShortener<'a> { let chars = text.chars().skip(skip_chars); - Self { chars, accumulted_len: 0, text_action, max_width, direction, shortener } + Self { chars, accumulated_len: 0, text_action, max_width, direction, shortener } } } @@ -134,12 +134,12 @@ impl<'a> Iterator for StrShortener<'a> { let ch_width = ch.width().unwrap_or(1); // Advance width. - self.accumulted_len += ch_width; + self.accumulated_len += ch_width; - if self.accumulted_len > self.max_width { + if self.accumulated_len > self.max_width { self.text_action = TextAction::Terminate; return self.shortener; - } else if self.accumulted_len == self.max_width && self.shortener.is_some() { + } else if self.accumulated_len == self.max_width && self.shortener.is_some() { // Check if we have a next char. let has_next = self.chars.clone().next().is_some(); diff --git a/alacritty/src/window_context.rs b/alacritty/src/window_context.rs index 885d71a4..891551bb 100644 --- a/alacritty/src/window_context.rs +++ b/alacritty/src/window_context.rs @@ -7,25 +7,19 @@ use std::mem; #[cfg(not(windows))] use std::os::unix::io::{AsRawFd, RawFd}; use std::rc::Rc; -use std::sync::atomic::Ordering; use std::sync::Arc; -use crossfont::Size; use glutin::config::GetGlConfig; -use glutin::context::NotCurrentContext; use glutin::display::GetGlDisplay; #[cfg(all(feature = "x11", not(any(target_os = "macos", windows))))] use glutin::platform::x11::X11GlConfigExt; -use log::{error, info}; +use log::info; use raw_window_handle::HasRawDisplayHandle; use serde_json as json; -#[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] -use wayland_client::EventQueue; -use winit::event::{Event as WinitEvent, ModifiersState, WindowEvent}; +use winit::event::{Event as WinitEvent, Modifiers, WindowEvent}; use winit::event_loop::{EventLoopProxy, EventLoopWindowTarget}; use winit::window::WindowId; -use alacritty_config::SerdeReplace; use alacritty_terminal::event::Event as TerminalEvent; use alacritty_terminal::event_loop::{EventLoop as PtyEventLoop, Msg, Notifier}; use alacritty_terminal::grid::{Dimensions, Scroll}; @@ -35,14 +29,15 @@ use alacritty_terminal::term::test::TermSize; use alacritty_terminal::term::{Term, TermMode}; use alacritty_terminal::tty; -#[cfg(unix)] -use crate::cli::IpcConfig; -use crate::cli::WindowOptions; +use crate::cli::{ParsedOptions, WindowOptions}; use crate::clipboard::Clipboard; use crate::config::UiConfig; use crate::display::window::Window; use crate::display::Display; -use crate::event::{ActionContext, Event, EventProxy, EventType, Mouse, SearchState, TouchPurpose}; +use crate::event::{ + ActionContext, Event, EventProxy, InlineSearchState, Mouse, SearchState, TouchPurpose, +}; +#[cfg(unix)] use crate::logging::LOG_TARGET_IPC_CONFIG; use crate::message_bar::MessageBuffer; use crate::scheduler::Scheduler; @@ -52,37 +47,33 @@ use crate::{input, renderer}; pub struct WindowContext { pub message_buffer: MessageBuffer, pub display: Display, - event_queue: Vec<WinitEvent<'static, Event>>, + pub dirty: bool, + event_queue: Vec<WinitEvent<Event>>, terminal: Arc<FairMutex<Term<EventProxy>>>, cursor_blink_timed_out: bool, - modifiers: ModifiersState, + modifiers: Modifiers, + inline_search_state: InlineSearchState, search_state: SearchState, - received_count: usize, - suppress_chars: bool, notifier: Notifier, - font_size: Size, mouse: Mouse, touch: TouchPurpose, - dirty: bool, occluded: bool, preserve_title: bool, #[cfg(not(windows))] master_fd: RawFd, #[cfg(not(windows))] shell_pid: u32, - ipc_config: Vec<(String, serde_yaml::Value)>, + window_config: ParsedOptions, config: Rc<UiConfig>, } impl WindowContext { - /// Create initial window context that dous bootstrapping the graphics Api we're going to use. + /// Create initial window context that does bootstrapping the graphics API we're going to use. pub fn initial( event_loop: &EventLoopWindowTarget<Event>, proxy: EventLoopProxy<Event>, config: Rc<UiConfig>, options: WindowOptions, - #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] - wayland_event_queue: Option<&EventQueue>, ) -> Result<Self, Box<dyn Error>> { let raw_display_handle = event_loop.raw_display_handle(); @@ -99,8 +90,11 @@ impl WindowContext { #[cfg(not(windows))] let raw_window_handle = None; - let gl_display = - renderer::platform::create_gl_display(raw_display_handle, raw_window_handle)?; + let gl_display = renderer::platform::create_gl_display( + raw_display_handle, + raw_window_handle, + config.debug.prefer_egl, + )?; let gl_config = renderer::platform::pick_gl_config(&gl_display, raw_window_handle)?; #[cfg(not(windows))] @@ -108,17 +102,19 @@ impl WindowContext { event_loop, &config, &identity, - #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] - wayland_event_queue, #[cfg(all(feature = "x11", not(any(target_os = "macos", windows))))] gl_config.x11_visual(), + #[cfg(target_os = "macos")] + &options.window_tabbing_id, )?; // Create context. let gl_context = renderer::platform::create_gl_context(&gl_display, &gl_config, raw_window_handle)?; - Self::new(window, gl_context, config, options, proxy) + let display = Display::new(window, gl_context, &config, false)?; + + Self::new(display, config, options, proxy) } /// Create additional context with the graphics platform other windows are using. @@ -128,8 +124,7 @@ impl WindowContext { proxy: EventLoopProxy<Event>, config: Rc<UiConfig>, options: WindowOptions, - #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] - wayland_event_queue: Option<&EventQueue>, + config_overrides: ParsedOptions, ) -> Result<Self, Box<dyn Error>> { // Get any window and take its GL config and display to build a new context. let (gl_display, gl_config) = { @@ -144,10 +139,10 @@ impl WindowContext { event_loop, &config, &identity, - #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] - wayland_event_queue, #[cfg(all(feature = "x11", not(any(target_os = "macos", windows))))] gl_config.x11_visual(), + #[cfg(target_os = "macos")] + &options.window_tabbing_id, )?; // Create context. @@ -158,27 +153,36 @@ impl WindowContext { Some(raw_window_handle), )?; - Self::new(window, gl_context, config, options, proxy) + // Check if new window will be opened as a tab. + #[cfg(target_os = "macos")] + let tabbed = options.window_tabbing_id.is_some(); + #[cfg(not(target_os = "macos"))] + let tabbed = false; + + let display = Display::new(window, gl_context, &config, tabbed)?; + + let mut window_context = Self::new(display, config, options, proxy)?; + + // Set the config overrides at startup. + // + // These are already applied to `config`, so no update is necessary. + window_context.window_config = config_overrides; + + Ok(window_context) } /// Create a new terminal window context. fn new( - window: Window, - context: NotCurrentContext, + display: Display, config: Rc<UiConfig>, options: WindowOptions, proxy: EventLoopProxy<Event>, ) -> Result<Self, Box<dyn Error>> { - let mut pty_config = config.terminal_config.pty_config.clone(); + let mut pty_config = config.pty_config(); options.terminal_options.override_pty_config(&mut pty_config); let preserve_title = options.window_identity.title.is_some(); - // Create a display. - // - // The display manages a window and can draw the terminal. - let display = Display::new(window, context, &config)?; - info!( "PTY dimensions: {:?} x {:?}", display.size_info.screen_lines(), @@ -192,7 +196,7 @@ impl WindowContext { // This object contains all of the state about what's being displayed. It's // wrapped in a clonable mutex since both the I/O loop and display need to // access it. - let terminal = Term::new(&config.terminal_config, &display.size_info, event_proxy.clone()); + let terminal = Term::new(config.term_options(), &display.size_info, event_proxy.clone()); let terminal = Arc::new(FairMutex::new(terminal)); // Create the PTY. @@ -219,7 +223,7 @@ impl WindowContext { pty, pty_config.hold, config.debug.ref_test, - ); + )?; // The event loop channel allows write requests from the event processor // to be sent to the pty loop and ultimately written to the pty. @@ -229,16 +233,13 @@ impl WindowContext { let _io_thread = event_loop.spawn(); // Start cursor blinking, in case `Focused` isn't sent on startup. - if config.terminal_config.cursor.style().blinking { + if config.cursor.style().blinking { event_proxy.send_event(TerminalEvent::CursorBlinkingChange.into()); } - let font_size = config.font.size(); - // Create context for the Alacritty window. Ok(WindowContext { preserve_title, - font_size, terminal, display, #[cfg(not(windows))] @@ -248,17 +249,16 @@ impl WindowContext { config, notifier: Notifier(loop_tx), cursor_blink_timed_out: Default::default(), - suppress_chars: Default::default(), + inline_search_state: Default::default(), message_buffer: Default::default(), - received_count: Default::default(), + window_config: Default::default(), search_state: Default::default(), event_queue: Default::default(), - ipc_config: Default::default(), modifiers: Default::default(), + occluded: Default::default(), mouse: Default::default(), touch: Default::default(), dirty: Default::default(), - occluded: Default::default(), }) } @@ -267,51 +267,30 @@ impl WindowContext { let old_config = mem::replace(&mut self.config, new_config); // Apply ipc config if there are overrides. - if !self.ipc_config.is_empty() { - let mut config = (*self.config).clone(); - - // Apply each option, removing broken ones. - let mut i = 0; - while i < self.ipc_config.len() { - let (key, value) = &self.ipc_config[i]; - - match config.replace(key, value.clone()) { - Err(err) => { - error!( - target: LOG_TARGET_IPC_CONFIG, - "Unable to override option '{}': {}", key, err - ); - self.ipc_config.swap_remove(i); - }, - Ok(_) => i += 1, - } - } - - self.config = Rc::new(config); - } + self.config = self.window_config.override_config_rc(self.config.clone()); self.display.update_config(&self.config); - self.terminal.lock().update_config(&self.config.terminal_config); + self.terminal.lock().set_options(self.config.term_options()); // Reload cursor if its thickness has changed. - if (old_config.terminal_config.cursor.thickness() - - self.config.terminal_config.cursor.thickness()) - .abs() - > f32::EPSILON - { + if (old_config.cursor.thickness() - self.config.cursor.thickness()).abs() > f32::EPSILON { self.display.pending_update.set_cursor_dirty(); } if old_config.font != self.config.font { + let scale_factor = self.display.window.scale_factor as f32; // Do not update font size if it has been changed at runtime. - if self.font_size == old_config.font.size() { - self.font_size = self.config.font.size(); + if self.display.font_size == old_config.font.size().scale(scale_factor) { + self.display.font_size = self.config.font.size().scale(scale_factor); } - let font = self.config.font.clone().with_size(self.font_size); + let font = self.config.font.clone().with_size(self.display.font_size); self.display.pending_update.set_font(font); } + // Always reload the theme to account for auto-theme switching. + self.display.window.set_theme(self.config.window.theme()); + // Update display if either padding options or resize increments were changed. let window_config = &old_config.window; if window_config.padding(1.) != self.config.window.padding(1.) @@ -342,10 +321,11 @@ impl WindowContext { self.display.window.set_has_shadow(opaque); #[cfg(target_os = "macos")] - self.display.window.set_option_as_alt(self.config.window.option_as_alt); + self.display.window.set_option_as_alt(self.config.window.option_as_alt()); - // Change opacity state. + // Change opacity and blur state. self.display.window.set_transparent(!opaque); + self.display.window.set_blur(self.config.window.blur); // Update hint keys. self.display.hint_state.update_alphabet(self.config.hints.alphabet()); @@ -357,43 +337,65 @@ impl WindowContext { self.dirty = true; } - /// Update the IPC config overrides. + /// Clear the window config overrides. #[cfg(unix)] - pub fn update_ipc_config(&mut self, config: Rc<UiConfig>, ipc_config: IpcConfig) { - // Clear previous IPC errors. + pub fn reset_window_config(&mut self, config: Rc<UiConfig>) { + // Clear previous window errors. self.message_buffer.remove_target(LOG_TARGET_IPC_CONFIG); - if ipc_config.reset { - self.ipc_config.clear(); - } else { - for option in &ipc_config.options { - // Separate config key/value. - let (key, value) = match option.split_once('=') { - Some(split) => split, - None => { - error!( - target: LOG_TARGET_IPC_CONFIG, - "'{}': IPC config option missing value", option - ); - continue; - }, - }; - - // Try and parse value as yaml. - match serde_yaml::from_str(value) { - Ok(value) => self.ipc_config.push((key.to_owned(), value)), - Err(err) => error!( - target: LOG_TARGET_IPC_CONFIG, - "'{}': Invalid IPC config value: {:?}", option, err - ), - } - } - } + self.window_config.clear(); + + // Reload current config to pull new IPC config. + self.update_config(config); + } + + /// Add new window config overrides. + #[cfg(unix)] + pub fn add_window_config(&mut self, config: Rc<UiConfig>, options: &ParsedOptions) { + // Clear previous window errors. + self.message_buffer.remove_target(LOG_TARGET_IPC_CONFIG); + + self.window_config.extend_from_slice(options); // Reload current config to pull new IPC config. self.update_config(config); } + /// Draw the window. + pub fn draw(&mut self, scheduler: &mut Scheduler) { + self.display.window.requested_redraw = false; + + if self.occluded { + return; + } + + self.dirty = false; + + // Force the display to process any pending display update. + self.display.process_renderer_update(); + + // Request immediate re-draw if visual bell animation is not finished yet. + if !self.display.visual_bell.completed() { + // We can get an OS redraw which bypasses alacritty's frame throttling, thus + // marking the window as dirty when we don't have frame yet. + if self.display.window.has_frame { + self.display.window.request_redraw(); + } else { + self.dirty = true; + } + } + + // Redraw the window. + let terminal = self.terminal.lock(); + self.display.draw( + terminal, + scheduler, + &self.message_buffer, + &self.config, + &mut self.search_state, + ); + } + /// Process events for this terminal window. pub fn handle_event( &mut self, @@ -401,30 +403,20 @@ impl WindowContext { event_proxy: &EventLoopProxy<Event>, clipboard: &mut Clipboard, scheduler: &mut Scheduler, - event: WinitEvent<'_, Event>, + event: WinitEvent<Event>, ) { match event { - // Skip further event handling with no staged updates. - WinitEvent::RedrawEventsCleared if self.event_queue.is_empty() && !self.dirty => { - return; - }, - // Continue to process all pending events. - WinitEvent::RedrawEventsCleared => (), - // Remap scale_factor change event to remove the lifetime. - WinitEvent::WindowEvent { - event: WindowEvent::ScaleFactorChanged { scale_factor, new_inner_size }, - window_id, - } => { - let size = (new_inner_size.width, new_inner_size.height); - let event = - Event::new(EventType::ScaleFactorChanged(scale_factor, size), window_id); - self.event_queue.push(event.into()); - return; + WinitEvent::AboutToWait + | WinitEvent::WindowEvent { event: WindowEvent::RedrawRequested, .. } => { + // Skip further event handling with no staged updates. + if self.event_queue.is_empty() { + return; + } + + // Continue to process all pending events. }, - // Transmute to extend lifetime, which exists only for `ScaleFactorChanged` event. - // Since we remap that event to remove the lifetime, this is safe. - event => unsafe { - self.event_queue.push(mem::transmute(event)); + event => { + self.event_queue.push(event); return; }, } @@ -436,11 +428,9 @@ impl WindowContext { let context = ActionContext { cursor_blink_timed_out: &mut self.cursor_blink_timed_out, message_buffer: &mut self.message_buffer, - received_count: &mut self.received_count, - suppress_chars: &mut self.suppress_chars, + inline_search_state: &mut self.inline_search_state, search_state: &mut self.search_state, modifiers: &mut self.modifiers, - font_size: &mut self.font_size, notifier: &mut self.notifier, display: &mut self.display, mouse: &mut self.mouse, @@ -472,7 +462,7 @@ impl WindowContext { &mut self.display, &mut self.notifier, &self.message_buffer, - &self.search_state, + &mut self.search_state, old_is_searching, &self.config, ); @@ -484,35 +474,19 @@ impl WindowContext { &terminal, &self.config, &self.mouse, - self.modifiers, + self.modifiers.state(), ); self.mouse.hint_highlight_dirty = false; } - // Skip rendering until we get a new frame. - if !self.display.window.has_frame.load(Ordering::Relaxed) { - return; - } - - if self.dirty && !self.occluded { - // Force the display to process any pending display update. - self.display.process_renderer_update(); - - self.dirty = false; - - // Request immediate re-draw if visual bell animation is not finished yet. - if !self.display.visual_bell.completed() { - self.display.window.request_redraw(); - } - - // Redraw the window. - self.display.draw( - terminal, - scheduler, - &self.message_buffer, - &self.config, - &self.search_state, - ); + // Don't call `request_redraw` when event is `RedrawRequested` since the `dirty` flag + // represents the current frame, but redraw is for the next frame. + if self.dirty + && self.display.window.has_frame + && !self.occluded + && !matches!(event, WinitEvent::WindowEvent { event: WindowEvent::RedrawRequested, .. }) + { + self.display.window.request_redraw(); } } @@ -555,7 +529,7 @@ impl WindowContext { display: &mut Display, notifier: &mut Notifier, message_buffer: &MessageBuffer, - search_state: &SearchState, + search_state: &mut SearchState, old_is_searching: bool, config: &UiConfig, ) { @@ -568,13 +542,7 @@ impl WindowContext { search_state.direction == Direction::Left }; - display.handle_update( - terminal, - notifier, - message_buffer, - search_state.history_index.is_some(), - config, - ); + display.handle_update(terminal, notifier, message_buffer, search_state, config); let new_is_searching = search_state.history_index.is_some(); if !old_is_searching && new_is_searching { |