aboutsummaryrefslogtreecommitdiff
path: root/alacritty/src
diff options
context:
space:
mode:
authorAyose <ayosec@gmail.com>2024-02-11 00:00:00 +0000
committerAyose <ayosec@gmail.com>2024-02-11 00:00:00 +0000
commit5e75fc92bd6bd956873baa8e30d2c14f2fe83a54 (patch)
tree25836c51f65adf60c2ea2d0f799f77b12446451a /alacritty/src
parent713c5415262ab091a645f976907dacaf40703ae8 (diff)
parent117719b3216d60ddde4096d9b2bdccecfd214c42 (diff)
downloadr-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')
-rw-r--r--alacritty/src/cli.rs256
-rw-r--r--alacritty/src/clipboard.rs19
-rw-r--r--alacritty/src/config/bell.rs15
-rw-r--r--alacritty/src/config/bindings.rs830
-rw-r--r--alacritty/src/config/color.rs83
-rw-r--r--alacritty/src/config/cursor.rs156
-rw-r--r--alacritty/src/config/debug.rs11
-rw-r--r--alacritty/src/config/font.rs6
-rw-r--r--alacritty/src/config/mod.rs301
-rw-r--r--alacritty/src/config/monitor.rs2
-rw-r--r--alacritty/src/config/mouse.rs31
-rw-r--r--alacritty/src/config/scrolling.rs53
-rw-r--r--alacritty/src/config/selection.rs17
-rw-r--r--alacritty/src/config/serde_utils.rs66
-rw-r--r--alacritty/src/config/terminal.rs26
-rw-r--r--alacritty/src/config/ui_config.rs363
-rw-r--r--alacritty/src/config/window.rs115
-rw-r--r--alacritty/src/display/color.rs211
-rw-r--r--alacritty/src/display/content.rs102
-rw-r--r--alacritty/src/display/cursor.rs4
-rw-r--r--alacritty/src/display/damage.rs222
-rw-r--r--alacritty/src/display/hint.rs60
-rw-r--r--alacritty/src/display/meter.rs5
-rw-r--r--alacritty/src/display/mod.rs414
-rw-r--r--alacritty/src/display/window.rs217
-rw-r--r--alacritty/src/event.rs545
-rw-r--r--alacritty/src/input/keyboard.rs672
-rw-r--r--alacritty/src/input/mod.rs (renamed from alacritty/src/input.rs)488
-rw-r--r--alacritty/src/logging.rs27
-rw-r--r--alacritty/src/main.rs36
-rw-r--r--alacritty/src/message_bar.rs6
-rw-r--r--alacritty/src/migrate.rs266
-rw-r--r--alacritty/src/renderer/mod.rs131
-rw-r--r--alacritty/src/renderer/platform.rs36
-rw-r--r--alacritty/src/renderer/rects.rs19
-rw-r--r--alacritty/src/renderer/shader.rs12
-rw-r--r--alacritty/src/renderer/text/builtin_font.rs258
-rw-r--r--alacritty/src/renderer/text/glyph_cache.rs16
-rw-r--r--alacritty/src/renderer/text/mod.rs1
-rw-r--r--alacritty/src/scheduler.rs2
-rw-r--r--alacritty/src/string.rs12
-rw-r--r--alacritty/src/window_context.rs308
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, &regex).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 {