Repository: alacritty/alacritty (language: rust)

Issue:
Numpad up/left arrow mixup
### Issue

There seems to be a mixup where the numpad up arrow is interpreted as a left arrow.

Here's the output from `alacritty --print-events | grep Numpad` when I press the numpad arrow keys in clockwise order. As you see, both `Numpad8` and `Numpad4` are being interpreted as `ArrowLeft`.


````
[1.693534113s] [INFO ] [alacritty] winit event: WindowEvent { window_id: WindowId(WindowId(94267818674144)), event: KeyboardInput { device_id: DeviceId(Wayland(DeviceId)), event: KeyEvent { physical_key: Code(Numpad8), logical_key: Named(ArrowLeft), text: None, location: Numpad, state: Pressed, repeat: false, platform_specific: KeyEventExtra { key_without_modifiers: Named(ArrowLeft), text_with_all_modifiers: None } }, is_synthetic: false } }
[1.818030063s] [INFO ] [alacritty] winit event: WindowEvent { window_id: WindowId(WindowId(94267818674144)), event: KeyboardInput { device_id: DeviceId(Wayland(DeviceId)), event: KeyEvent { physical_key: Code(Numpad8), logical_key: Named(ArrowLeft), text: None, location: Numpad, state: Released, repeat: false, platform_specific: KeyEventExtra { key_without_modifiers: Named(ArrowLeft), text_with_all_modifiers: None } }, is_synthetic: false } }
[2.183437954s] [INFO ] [alacritty] winit event: WindowEvent { window_id: WindowId(WindowId(94267818674144)), event: KeyboardInput { device_id: DeviceId(Wayland(DeviceId)), event: KeyEvent { physical_key: Code(Numpad6), logical_key: Named(ArrowRight), text: None, location: Numpad, state: Pressed, repeat: false, platform_specific: KeyEventExtra { key_without_modifiers: Named(ArrowRight), text_with_all_modifiers: None } }, is_synthetic: false } }
[2.281193344s] [INFO ] [alacritty] winit event: WindowEvent { window_id: WindowId(WindowId(94267818674144)), event: KeyboardInput { device_id: DeviceId(Wayland(DeviceId)), event: KeyEvent { physical_key: Code(Numpad6), logical_key: Named(ArrowRight), text: None, location: Numpad, state: Released, repeat: false, platform_specific: KeyEventExtra { key_without_modifiers: Named(ArrowRight), text_with_all_modifiers: None } }, is_synthetic: false } }
[2.552385010s] [INFO ] [alacritty] winit event: WindowEvent { window_id: WindowId(WindowId(94267818674144)), event: KeyboardInput { device_id: DeviceId(Wayland(DeviceId)), event: KeyEvent { physical_key: Code(Numpad2), logical_key: Named(ArrowDown), text: None, location: Numpad, state: Pressed, repeat: false, platform_specific: KeyEventExtra { key_without_modifiers: Named(ArrowDown), text_with_all_modifiers: None } }, is_synthetic: false } }
[2.674907529s] [INFO ] [alacritty] winit event: WindowEvent { window_id: WindowId(WindowId(94267818674144)), event: KeyboardInput { device_id: DeviceId(Wayland(DeviceId)), event: KeyEvent { physical_key: Code(Numpad2), logical_key: Named(ArrowDown), text: None, location: Numpad, state: Released, repeat: false, platform_specific: KeyEventExtra { key_without_modifiers: Named(ArrowDown), text_with_all_modifiers: None } }, is_synthetic: false } }
[3.081043834s] [INFO ] [alacritty] winit event: WindowEvent { window_id: WindowId(WindowId(94267818674144)), event: KeyboardInput { device_id: DeviceId(Wayland(DeviceId)), event: KeyEvent { physical_key: Code(Numpad4), logical_key: Named(ArrowLeft), text: None, location: Numpad, state: Pressed, repeat: false, platform_specific: KeyEventExtra { key_without_modifiers: Named(ArrowLeft), text_with_all_modifiers: None } }, is_synthetic: false } }
[3.168097783s] [INFO ] [alacritty] winit event: WindowEvent { window_id: WindowId(WindowId(94267818674144)), event: KeyboardInput { device_id: DeviceId(Wayland(DeviceId)), event: KeyEvent { physical_key: Code(Numpad4), logical_key: Named(ArrowLeft), text: None, location: Numpad, state: Released, repeat: false, platform_specific: KeyEventExtra { key_without_modifiers: Named(ArrowLeft), text_with_all_modifiers: None } }, is_synthetic: false } }
````

### System

OS: Linux
Version: alacritty 0.13.0 (78fa4d6f)
Linux: Wayland (sway)

### Logs

Output from `alacritty --print-events` included above.

### Version/Commit History

- Numpad arrow keys work fine in v0.12.3, up to commit 09c4471b
- From commit db903503 onward, numpad arrow keys don't work at all, until...
- From commit cb03806e onward, numpad arrow keys work again, but with up/left mixup described above.

Paste in Vi Search with key binding 
I haven't seen a key binding that allows pasting in vi search, like `Control+Shift+V` in normal search. That could be useful. 


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c0a3b788b23..0a0b5f6fe27 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -17,6 +17,8 @@ jobs:
       - uses: actions/checkout@v3
       - name: Stable
         run: cargo test
+      - name: Stable (no default features)
+        run: cargo test -p alacritty_terminal --no-default-features
       - name: Oldstable
         run: |
           rustup default 1.70.0
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f7ebbb001c9..4c44959aaee 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,34 @@ The sections should follow the order `Packaging`, `Added`, `Changed`, `Fixed` an
 
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 
+## 0.13.1-rc1
+
+### Added
+
+- Support for pasting in Vi + Search mode
+
+### Changed
+
+- `alacritty migrate` will ignore null values in yaml instead of erroring out
+
+### Fixed
+
+- `alacritty migrate` failing with nonexistent imports
+- `Alt` bindings requiring composed key rather than pre-composed one on macOS
+- `Alt + Control` bindings not working on Windows
+- `chars = "\u000A"` action in bindings inserting `\n`
+- Alternate keys not sent for `Shift + <number>` when using kitty protocol
+- Alternative keys being swapped in kitty protocol implementation
+- Powerline glyphs being cut for narrow fonts
+- Xmodmap not working on X11
+- Occasional slow startup on some X11 window managers
+- Blurry window when using `window.dimensions` on some Wayland compositors
+- IME input lagging behind on X11
+- xdotool modifiers input not working correctly on X11
+- Parsing numbers fails for mouse bindings
+- Some config options overriding each other in CLI/IPC
+- Numpad `Left` used for numpad `Up`
+
 ## 0.13.0
 
 ### Packaging
diff --git a/Cargo.lock b/Cargo.lock
index 0a3b62317c9..36a0e418b54 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -32,7 +32,7 @@ dependencies = [
 
 [[package]]
 name = "alacritty"
-version = "0.13.0"
+version = "0.13.1-rc1"
 dependencies = [
  "ahash",
  "alacritty_config",
@@ -53,7 +53,6 @@ dependencies = [
  "log",
  "notify",
  "objc",
- "once_cell",
  "parking_lot",
  "png",
  "raw-window-handle",
@@ -69,8 +68,9 @@ dependencies = [
 
 [[package]]
 name = "alacritty_config"
-version = "0.1.2"
+version = "0.2.0"
 dependencies = [
+ "alacritty_config_derive",
  "log",
  "serde",
  "toml",
@@ -91,7 +91,7 @@ dependencies = [
 
 [[package]]
 name = "alacritty_terminal"
-version = "0.20.0"
+version = "0.21.0-rc1"
 dependencies = [
  "base64",
  "bitflags 2.4.1",
@@ -2310,9 +2310,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
 
 [[package]]
 name = "winit"
-version = "0.29.7"
+version = "0.29.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7fd430cd4560ee9c48885a4ef473b609a56796e37b1e18222abee146143f7457"
+checksum = "c2376dab13e09c01ad8b679f0dbc7038af4ec43d9a91344338e37bd686481550"
 dependencies = [
  "ahash",
  "android-activity",
diff --git a/README.md b/README.md
index 776f9900b5b..af678ff8868 100644
--- a/README.md
+++ b/README.md
@@ -55,10 +55,6 @@ For everyone else, the detailed instructions to install Alacritty can be found
 
 ## Configuration
 
-**Alacritty versions before 0.13.0 are using YAML configuration files
-and all their settings were documented in the `alacritty.yml`. The example
-`alacritty.yml` file for each release can be found on [GitHub releases page](https://github.com/alacritty/alacritty/releases).**
-
 You can find the documentation for Alacritty's configuration in `man 5
 alacritty`, or by looking at [the website] if you do not have the manpages
 installed.
diff --git a/alacritty/Cargo.toml b/alacritty/Cargo.toml
index 519afdb447f..c071b310115 100644
--- a/alacritty/Cargo.toml
+++ b/alacritty/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "alacritty"
-version = "0.13.0"
+version = "0.13.1-rc1"
 authors = ["Christian Duerr <contact@christianduerr.com>", "Joe Wilm <joe@jwilm.com>"]
 license = "Apache-2.0"
 description = "A fast, cross-platform, OpenGL terminal emulator"
@@ -11,7 +11,7 @@ rust-version = "1.70.0"
 
 [dependencies.alacritty_terminal]
 path = "../alacritty_terminal"
-version = "0.20.0"
+version = "0.21.0-rc1"
 
 [dependencies.alacritty_config_derive]
 path = "../alacritty_config_derive"
@@ -19,7 +19,7 @@ version = "0.2.2"
 
 [dependencies.alacritty_config]
 path = "../alacritty_config"
-version = "0.1.2"
+version = "0.2.0-rc1"
 
 [dependencies]
 ahash = { version = "0.8.6", features = ["no-rng"] }
@@ -32,7 +32,6 @@ home = "0.5.5"
 libc = "0.2"
 log = { version = "0.4", features = ["std", "serde"] }
 notify = "6.1.1"
-once_cell = "1.12"
 parking_lot = "0.12.0"
 raw-window-handle = "0.5"
 serde = { version = "1", features = ["derive"] }
@@ -40,7 +39,7 @@ serde_json = "1"
 serde_yaml = "0.9.25"
 toml = "0.8.2"
 unicode-width = "0.1"
-winit = { version = "0.29.7", default-features = false, features = ["rwh_05", "serde"] }
+winit = { version = "0.29.9", default-features = false, features = ["rwh_05", "serde"] }
 
 [build-dependencies]
 gl_generator = "0.14.0"
diff --git a/alacritty/src/config/bindings.rs b/alacritty/src/config/bindings.rs
index b75676a33bf..9e98941948f 100644
--- a/alacritty/src/config/bindings.rs
+++ b/alacritty/src/config/bindings.rs
@@ -445,6 +445,7 @@ pub fn default_key_bindings() -> Vec<KeyBinding> {
         Copy; Action::Copy;
         Copy,  +BindingMode::VI; Action::ClearSelection;
         Paste, ~BindingMode::VI; Action::Paste;
+        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;
@@ -546,6 +547,7 @@ fn common_keybindings() -> Vec<KeyBinding> {
     bindings!(
         KeyBinding;
         "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;
@@ -602,6 +604,7 @@ pub fn platform_key_bindings() -> Vec<KeyBinding> {
         "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;
@@ -860,6 +863,16 @@ impl<'a> Deserialize<'a> for MouseButtonWrapper {
                 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>
             where
                 E: de::Error,
diff --git a/alacritty/src/config/debug.rs b/alacritty/src/config/debug.rs
index a8be77d9de9..15c06454397 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.
@@ -47,17 +45,17 @@ impl Default for Debug {
 }
 
 /// 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")]
+    #[config(rename = "glsl3")]
     Glsl3,
 
     /// GLES 2 renderer, with optional extensions like dual source blending.
-    #[serde(rename = "gles2")]
+    #[config(rename = "gles2")]
     Gles2,
 
     /// Pure GLES 2 renderer.
-    #[serde(rename = "gles2_pure")]
+    #[config(rename = "gles2_pure")]
     Gles2Pure,
 }
diff --git a/alacritty/src/config/mod.rs b/alacritty/src/config/mod.rs
index 95a1ff007ed..a77ed770fbb 100644
--- a/alacritty/src/config/mod.rs
+++ b/alacritty/src/config/mod.rs
@@ -207,7 +207,7 @@ fn parse_config(
     config_paths.push(path.to_owned());
 
     // Deserialize the configuration file.
-    let config = deserialize_config(path)?;
+    let config = deserialize_config(path, false)?;
 
     // Merge config with imports.
     let imports = load_imports(&config, config_paths, recursion_limit);
@@ -215,7 +215,7 @@ fn parse_config(
 }
 
 /// Deserialize a configuration file.
-pub fn deserialize_config(path: &Path) -> Result<Value> {
+pub fn deserialize_config(path: &Path, warn_pruned: bool) -> Result<Value> {
     let mut contents = fs::read_to_string(path)?;
 
     // Remove UTF-8 BOM.
@@ -230,7 +230,8 @@ pub fn deserialize_config(path: &Path) -> Result<Value> {
             "YAML config {path:?} is deprecated, please migrate to TOML using `alacritty migrate`"
         );
 
-        let value: serde_yaml::Value = serde_yaml::from_str(&contents)?;
+        let mut value: serde_yaml::Value = serde_yaml::from_str(&contents)?;
+        prune_yaml_nulls(&mut value, warn_pruned);
         contents = toml::to_string(&value)?;
     }
 
@@ -318,6 +319,35 @@ pub fn imports(
     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,
+        }
+    }
+
+    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:
 ///
@@ -370,4 +400,43 @@ mod tests {
     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 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/ui_config.rs b/alacritty/src/config/ui_config.rs
index f4f67cb6360..21059734f55 100644
--- a/alacritty/src/config/ui_config.rs
+++ b/alacritty/src/config/ui_config.rs
@@ -1,9 +1,11 @@
 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 alacritty_config::SerdeReplace;
 use alacritty_terminal::term::Config as TermConfig;
 use alacritty_terminal::tty::{Options as PtyOptions, Shell};
 use log::{error, warn};
@@ -656,6 +658,14 @@ impl From<Program> for Shell {
     }
 }
 
+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;
diff --git a/alacritty/src/config/window.rs b/alacritty/src/config/window.rs
index 3ae4e29e0cd..ed84622ef99 100644
--- a/alacritty/src/config/window.rs
+++ b/alacritty/src/config/window.rs
@@ -3,10 +3,10 @@ use std::fmt::{self, Formatter};
 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 as WinitOptionAsAlt;
+use winit::window::{Fullscreen, Theme as WinitTheme};
 
 use alacritty_config_derive::{ConfigDeserialize, SerdeReplace};
 
@@ -31,9 +31,6 @@ pub struct WindowConfig {
     #[config(skip)]
     pub embed: Option<u32>,
 
-    /// System decorations theme variant.
-    pub decorations_theme_variant: Option<Theme>,
-
     /// Spread out additional padding evenly.
     pub dynamic_padding: bool,
 
@@ -62,6 +59,9 @@ pub struct WindowConfig {
 
     /// Initial dimensions.
     dimensions: Dimensions,
+
+    /// System decorations theme variant.
+    decorations_theme_variant: Option<Theme>,
 }
 
 impl Default for WindowConfig {
@@ -149,6 +149,10 @@ impl WindowConfig {
             OptionAsAlt::None => WinitOptionAsAlt::None,
         }
     }
+
+    pub fn theme(&self) -> Option<WinitTheme> {
+        self.decorations_theme_variant.map(WinitTheme::from)
+    }
 }
 
 #[derive(ConfigDeserialize, Debug, Clone, PartialEq, Eq)]
@@ -292,3 +296,19 @@ pub enum OptionAsAlt {
     #[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/window.rs b/alacritty/src/display/window.rs
index f36d05e972d..e4bfa2cb845 100644
--- a/alacritty/src/display/window.rs
+++ b/alacritty/src/display/window.rs
@@ -165,7 +165,7 @@ impl Window {
 
         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)
diff --git a/alacritty/src/event.rs b/alacritty/src/event.rs
index 3b3d8297847..ff08557b9b7 100644
--- a/alacritty/src/event.rs
+++ b/alacritty/src/event.rs
@@ -839,13 +839,23 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon
         } else {
             self.on_terminal_input_start();
 
-            // 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());
+            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);
         }
     }
 
diff --git a/alacritty/src/input/keyboard.rs b/alacritty/src/input/keyboard.rs
index 423f8baea02..0968c0de176 100644
--- a/alacritty/src/input/keyboard.rs
+++ b/alacritty/src/input/keyboard.rs
@@ -172,7 +172,20 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> {
             // 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() {
-                Key::Character(ch.to_lowercase().into())
+                // 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()
             };
@@ -337,17 +350,25 @@ impl SequenceBuilder {
             let character = character.chars().next().unwrap();
             let base_character = character.to_lowercase().next().unwrap();
 
-            let codepoint = u32::from(character);
-            let base_codepoint = u32::from(base_character);
+            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)
-                && codepoint != base_codepoint
+                && alternate_key_code != unicode_key_code
             {
-                format!("{codepoint}:{base_codepoint}")
+                format!("{unicode_key_code}:{alternate_key_code}")
             } else {
-                codepoint.to_string()
+                alternate_key_code.to_string()
             };
 
             Some(SequenceBase::new(payload.into(), SequenceTerminator::Kitty))
diff --git a/alacritty/src/logging.rs b/alacritty/src/logging.rs
index 6bede4e79f9..42f1536ef3b 100644
--- a/alacritty/src/logging.rs
+++ b/alacritty/src/logging.rs
@@ -8,12 +8,11 @@ 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 once_cell::sync::Lazy;
 use winit::event_loop::EventLoopProxy;
 
 use crate::cli::Options;
@@ -35,10 +34,14 @@ pub const LOG_TARGET_CONFIG: &str = "alacritty_config_derive";
 const ALACRITTY_EXTRA_LOG_TARGETS_ENV: &str = "ALACRITTY_EXTRA_LOG_TARGETS";
 
 /// User configurable extra log targets to include.
-static EXTRA_LOG_TARGETS: Lazy<Vec<String>> = Lazy::new(|| {
-    env::var(ALACRITTY_EXTRA_LOG_TARGETS_ENV)
-        .map_or(Vec::new(), |targets| targets.split(';').map(ToString::to_string).collect())
-});
+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] = &[
@@ -181,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) || EXTRA_LOG_TARGETS.iter().any(|t| t == target),
+        _ => ALLOWED_TARGETS.contains(&target) || extra_log_targets().iter().any(|t| t == target),
     }
 }
 
diff --git a/alacritty/src/migrate.rs b/alacritty/src/migrate.rs
index 4d632af4619..dbcfb2ae7d2 100644
--- a/alacritty/src/migrate.rs
+++ b/alacritty/src/migrate.rs
@@ -74,7 +74,7 @@ fn migrate_config(
     }
 
     // Try to parse the configuration file.
-    let mut config = match config::deserialize_config(path) {
+    let mut config = match config::deserialize_config(path, !options.dry_run) {
         Ok(config) => config,
         Err(err) => return Err(format!("parsing error: {err}")),
     };
@@ -96,7 +96,7 @@ fn migrate_config(
     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:?}-----^"
+            "\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.
@@ -124,7 +124,23 @@ fn migrate_imports(
             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));
     }
 
diff --git a/alacritty/src/renderer/mod.rs b/alacritty/src/renderer/mod.rs
index d8fd64fa083..02bfd762b40 100644
--- a/alacritty/src/renderer/mod.rs
+++ b/alacritty/src/renderer/mod.rs
@@ -2,6 +2,7 @@ use std::borrow::Cow;
 use std::collections::HashSet;
 use std::ffi::{CStr, CString};
 use std::sync::atomic::{AtomicBool, Ordering};
+use std::sync::OnceLock;
 use std::{fmt, ptr};
 
 use ahash::RandomState;
@@ -9,7 +10,6 @@ use crossfont::Metrics;
 use glutin::context::{ContextApi, GlContext, PossiblyCurrentContext};
 use glutin::display::{GetGlDisplay, GlDisplay};
 use log::{debug, error, info, warn, LevelFilter};
-use once_cell::sync::OnceCell;
 use unicode_width::UnicodeWidthChar;
 
 use alacritty_terminal::index::Point;
@@ -318,7 +318,7 @@ impl GlExtensions {
     ///
     /// This function will lazily load OpenGL extensions.
     fn contains(extension: &str) -> bool {
-        static OPENGL_EXTENSIONS: OnceCell<HashSet<&'static str, RandomState>> = OnceCell::new();
+        static OPENGL_EXTENSIONS: OnceLock<HashSet<&'static str, RandomState>> = OnceLock::new();
 
         OPENGL_EXTENSIONS.get_or_init(Self::load_extensions).contains(extension)
     }
diff --git a/alacritty/src/renderer/text/builtin_font.rs b/alacritty/src/renderer/text/builtin_font.rs
index c3f843b6704..f59616d5a0d 100644
--- a/alacritty/src/renderer/text/builtin_font.rs
+++ b/alacritty/src/renderer/text/builtin_font.rs
@@ -32,7 +32,7 @@ pub fn builtin_glyph(
         '\u{2500}'..='\u{259f}' => box_drawing(character, metrics, offset),
         // Powerline symbols: '','','',''
         POWERLINE_TRIANGLE_LTR..=POWERLINE_ARROW_RTL => {
-            powerline_drawing(character, metrics, offset)
+            powerline_drawing(character, metrics, offset)?
         },
         _ => return None,
     };
@@ -503,7 +503,11 @@ fn box_drawing(character: char, metrics: &Metrics, offset: &Delta<i8>) -> Raster
     }
 }
 
-fn powerline_drawing(character: char, metrics: &Metrics, offset: &Delta<i8>) -> RasterizedGlyph {
+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;
@@ -519,6 +523,12 @@ fn powerline_drawing(character: char, metrics: &Metrics, offset: &Delta<i8>) ->
     // 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));
 
@@ -555,7 +565,7 @@ fn powerline_drawing(character: char, metrics: &Metrics, offset: &Delta<i8>) ->
 
     let top = height as i32 + metrics.descent as i32;
     let buffer = BitmapBuffer::Rgb(canvas.into_raw());
-    RasterizedGlyph {
+    Some(RasterizedGlyph {
         character,
         top,
         left: 0,
@@ -563,7 +573,7 @@ fn powerline_drawing(character: char, metrics: &Metrics, offset: &Delta<i8>) ->
         width: width as i32,
         buffer,
         advance: (width as i32, height as i32),
-    }
+    })
 }
 
 #[repr(packed)]
diff --git a/alacritty/src/window_context.rs b/alacritty/src/window_context.rs
index 724a5515b1f..3f3e6807d6d 100644
--- a/alacritty/src/window_context.rs
+++ b/alacritty/src/window_context.rs
@@ -289,7 +289,7 @@ impl WindowContext {
         }
 
         // Always reload the theme to account for auto-theme switching.
-        self.display.window.set_theme(self.config.window.decorations_theme_variant);
+        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;
@@ -560,6 +560,6 @@ impl WindowContext {
 impl Drop for WindowContext {
     fn drop(&mut self) {
         // Shutdown the terminal's PTY.
-        self.notifier.0.send(Msg::Shutdown);
+        let _ = self.notifier.0.send(Msg::Shutdown);
     }
 }
diff --git a/alacritty/windows/wix/alacritty.wxs b/alacritty/windows/wix/alacritty.wxs
index 0da1e5b5713..71979ab1c12 100644
--- a/alacritty/windows/wix/alacritty.wxs
+++ b/alacritty/windows/wix/alacritty.wxs
@@ -1,5 +1,5 @@
 <Wix xmlns="http://wixtoolset.org/schemas/v4/wxs" xmlns:ui="http://wixtoolset.org/schemas/v4/wxs/ui">
-   <Package Name="Alacritty" UpgradeCode="87c21c74-dbd5-4584-89d5-46d9cd0c40a7" Language="1033" Codepage="1252" Version="0.13.0" Manufacturer="Alacritty" InstallerVersion="200">
+   <Package Name="Alacritty" UpgradeCode="87c21c74-dbd5-4584-89d5-46d9cd0c40a7" Language="1033" Codepage="1252" Version="0.13.1-rc1" Manufacturer="Alacritty" InstallerVersion="200">
       <MajorUpgrade AllowSameVersionUpgrades="yes" DowngradeErrorMessage="A newer version of [ProductName] is already installed." />
       <Icon Id="AlacrittyIco" SourceFile=".\alacritty\windows\alacritty.ico" />
       <WixVariable Id="WixUILicenseRtf" Value=".\alacritty\windows\wix\license.rtf" />
diff --git a/alacritty_config/Cargo.toml b/alacritty_config/Cargo.toml
index a3df5b5b7eb..85cd8e6bd70 100644
--- a/alacritty_config/Cargo.toml
+++ b/alacritty_config/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "alacritty_config"
-version = "0.1.2"
+version = "0.2.0"
 authors = ["Christian Duerr <contact@christianduerr.com>"]
 license = "MIT OR Apache-2.0"
 description = "Alacritty configuration abstractions"
@@ -12,3 +12,7 @@ rust-version = "1.70.0"
 log = { version = "0.4.17", features = ["serde"] }
 serde = "1.0.163"
 toml = "0.8.2"
+
+[dev-dependencies]
+alacritty_config_derive = { path = "../alacritty_config_derive" }
+serde = { version = "1.0.163", features = ["derive"] }
diff --git a/alacritty_config/src/lib.rs b/alacritty_config/src/lib.rs
index c5a43b87250..81e43bb8066 100644
--- a/alacritty_config/src/lib.rs
+++ b/alacritty_config/src/lib.rs
@@ -1,5 +1,6 @@
 use std::collections::HashMap;
 use std::error::Error;
+use std::path::PathBuf;
 
 use log::LevelFilter;
 use serde::Deserialize;
@@ -9,6 +10,7 @@ pub trait SerdeReplace {
     fn replace(&mut self, value: Value) -> Result<(), Box<dyn Error>>;
 }
 
+#[macro_export]
 macro_rules! impl_replace {
     ($($ty:ty),*$(,)*) => {
         $(
@@ -29,6 +31,7 @@ impl_replace!(
     bool,
     char,
     String,
+    PathBuf,
     LevelFilter,
 );
 
@@ -47,9 +50,12 @@ impl<'de, T: Deserialize<'de>> SerdeReplace for Vec<T> {
     }
 }
 
-impl<'de, T: Deserialize<'de>> SerdeReplace for Option<T> {
+impl<'de, T: SerdeReplace + Deserialize<'de>> SerdeReplace for Option<T> {
     fn replace(&mut self, value: Value) -> Result<(), Box<dyn Error>> {
-        replace_simple(self, value)
+        match self {
+            Some(inner) => inner.replace(value),
+            None => replace_simple(self, value),
+        }
     }
 }
 
@@ -58,3 +64,30 @@ impl<'de, T: Deserialize<'de>> SerdeReplace for HashMap<String, T> {
         replace_simple(self, value)
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    use crate as alacritty_config;
+    use alacritty_config_derive::ConfigDeserialize;
+
+    #[test]
+    fn replace_option() {
+        #[derive(ConfigDeserialize, Default, PartialEq, Eq, Debug)]
+        struct ReplaceOption {
+            a: usize,
+            b: usize,
+        }
+
+        let mut subject: Option<ReplaceOption> = None;
+
+        let value: Value = toml::from_str("a=1").unwrap();
+        SerdeReplace::replace(&mut subject, value).unwrap();
+
+        let value: Value = toml::from_str("b=2").unwrap();
+        SerdeReplace::replace(&mut subject, value).unwrap();
+
+        assert_eq!(subject, Some(ReplaceOption { a: 1, b: 2 }));
+    }
+}
diff --git a/alacritty_config_derive/Cargo.toml b/alacritty_config_derive/Cargo.toml
index efc94b49beb..db3dec7d006 100644
--- a/alacritty_config_derive/Cargo.toml
+++ b/alacritty_config_derive/Cargo.toml
@@ -18,7 +18,7 @@ syn = { version = "2.0.16", features = ["derive", "parsing", "proc-macro", "prin
 
 [dev-dependencies.alacritty_config]
 path = "../alacritty_config"
-version = "0.1.2"
+version = "0.2.0"
 
 [dev-dependencies]
 log = "0.4.11"
diff --git a/alacritty_terminal/Cargo.toml b/alacritty_terminal/Cargo.toml
index 5fc9a8afcbb..37742c1ddfb 100644
--- a/alacritty_terminal/Cargo.toml
+++ b/alacritty_terminal/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "alacritty_terminal"
-version = "0.20.0"
+version = "0.21.0-rc1"
 authors = ["Christian Duerr <contact@christianduerr.com>", "Joe Wilm <joe@jwilm.com>"]
 license = "Apache-2.0"
 description = "Library for writing terminal emulators"
diff --git a/alacritty_terminal/src/event_loop.rs b/alacritty_terminal/src/event_loop.rs
index 9b0893132b1..57045f4a300 100644
--- a/alacritty_terminal/src/event_loop.rs
+++ b/alacritty_terminal/src/event_loop.rs
@@ -2,6 +2,7 @@
 
 use std::borrow::Cow;
 use std::collections::VecDeque;
+use std::fmt::{self, Display, Formatter};
 use std::fs::File;
 use std::io::{self, ErrorKind, Read, Write};
 use std::marker::Send;
@@ -339,25 +340,53 @@ impl event::Notify for Notifier {
             return;
         }
 
-        self.0.send(Msg::Input(bytes));
+        let _ = self.0.send(Msg::Input(bytes));
     }
 }
 
 impl event::OnResize for Notifier {
     fn on_resize(&mut self, window_size: WindowSize) {
-        self.0.send(Msg::Resize(window_size));
+        let _ = self.0.send(Msg::Resize(window_size));
     }
 }
 
+#[derive(Debug)]
+pub enum EventLoopSendError {
+    /// Error polling the event loop.
+    Io(io::Error),
+
+    /// Error sending a message to the event loop.
+    Send(mpsc::SendError<Msg>),
+}
+
+impl Display for EventLoopSendError {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        match self {
+            EventLoopSendError::Io(err) => err.fmt(f),
+            EventLoopSendError::Send(err) => err.fmt(f),
+        }
+    }
+}
+
+impl std::error::Error for EventLoopSendError {
+    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+        match self {
+            EventLoopSendError::Io(err) => err.source(),
+            EventLoopSendError::Send(err) => err.source(),
+        }
+    }
+}
+
+#[derive(Clone)]
 pub struct EventLoopSender {
     sender: Sender<Msg>,
     poller: Arc<polling::Poller>,
 }
 
 impl EventLoopSender {
-    pub fn send(&self, msg: Msg) {
-        let _ = self.sender.send(msg);
-        let _ = self.poller.notify();
+    pub fn send(&self, msg: Msg) -> Result<(), EventLoopSendError> {
+        self.sender.send(msg).map_err(EventLoopSendError::Send)?;
+        self.poller.notify().map_err(EventLoopSendError::Io)
     }
 }
 
diff --git a/alacritty_terminal/src/term/mod.rs b/alacritty_terminal/src/term/mod.rs
index 2553270c8e1..ec79886c085 100644
--- a/alacritty_terminal/src/term/mod.rs
+++ b/alacritty_terminal/src/term/mod.rs
@@ -2710,6 +2710,7 @@ mod tests {
     /// This test is in the term module as opposed to the grid since we want to
     /// test this property with a T=Cell.
     #[test]
+    #[cfg(feature = "serde")]
     fn grid_serde() {
         let grid: Grid<Cell> = Grid::new(24, 80, 0);
         let serialized = serde_json::to_string(&grid).expect("ser");
diff --git a/extra/man/alacritty-bindings.5.scd b/extra/man/alacritty-bindings.5.scd
index a1c63a099ec..36dd69a9bcf 100644
--- a/extra/man/alacritty-bindings.5.scd
+++ b/extra/man/alacritty-bindings.5.scd
@@ -42,6 +42,10 @@ configuration. See *alacritty*(5) for full configuration format documentation.
 :  _"Control"_
 :[
 :  _"ClearLogNotice"_
+|  _"Paste"_
+:[
+:  _"Vi|Search"_
+:  _"Paste"_
 |  _"L"_
 :  _"Control"_
 :  _"~Vi|~Search"_
@@ -344,6 +348,10 @@ configuration. See *alacritty*(5) for full configuration format documentation.
 :  _"Control|Shift"_
 :  _"~Vi"_
 :  _"Paste"_
+|  _"V"_
+:  _"Control|Shift"_
+:  _"Vi|Search"_
+:  _"Paste"_
 |  _"C"_
 :  _"Control|Shift"_
 :[
@@ -440,7 +448,11 @@ configuration. See *alacritty*(5) for full configuration format documentation.
 :  _"DecreaseFontSize"_
 |  _"V"_
 :  _"Command"_
-:[
+:  _"~Vi"_
+:  _"Paste"_
+|  _"V"_
+:  _"Command"_
+:  _"Vi|Search"_
 :  _"Paste"_
 |  _"C"_
 :  _"Command"_
diff --git a/extra/man/alacritty.5.scd b/extra/man/alacritty.5.scd
index 272c861fc4f..8b1b91df6a1 100644
--- a/extra/man/alacritty.5.scd
+++ b/extra/man/alacritty.5.scd
@@ -143,6 +143,15 @@ This section documents the *[window]* table of the configuration file.
 
 	Startup mode (changes require restart)
 
+	*Windowed*
+		Regular window.
+	*Maximized*
+		The window will be maximized on startup.
+	*Fullscreen*
+		The window will be fullscreened on startup.
+	*SimpleFullscreen* _(macOS only)_
+		Same as _Fullscreen_, but you can stack windows on top.
+
 	Default: _"Windowed"_
 
 *title* = _"<string>"_
diff --git a/extra/osx/Alacritty.app/Contents/Info.plist b/extra/osx/Alacritty.app/Contents/Info.plist
index 7451240b9c8..1b0c0fafe75 100644
--- a/extra/osx/Alacritty.app/Contents/Info.plist
+++ b/extra/osx/Alacritty.app/Contents/Info.plist
@@ -15,7 +15,7 @@
   <key>CFBundlePackageType</key>
   <string>APPL</string>
   <key>CFBundleShortVersionString</key>
-  <string>0.13.0</string>
+  <string>0.13.1-rc1</string>
   <key>CFBundleSupportedPlatforms</key>
   <array>
     <string>MacOSX</string>


Test patch:
diff --git a/alacritty_terminal/tests/ref.rs b/alacritty_terminal/tests/ref.rs
index 7f864ae8a93..587d2d3eeb9 100644
--- a/alacritty_terminal/tests/ref.rs
+++ b/alacritty_terminal/tests/ref.rs
@@ -1,3 +1,4 @@
+#![cfg(feature = "serde")]
 use serde::Deserialize;
 use serde_json as json;