Repository: alacritty/alacritty (language: rust)

Issue:
Semantic characters in Vi Mode and moving left
### System

OS: Linux 6.11.9 (arch)
Version: alacritty 0.14.0 (22a44757)
Sway 1:1.10-1  

```
[0.000000942s] [INFO ] [alacritty] Welcome to Alacritty
[0.000051958s] [INFO ] [alacritty] Version 0.14.0 (22a44757)
[0.000061095s] [INFO ] [alacritty] Running on Wayland
[0.000525696s] [INFO ] [alacritty] Configuration files loaded from:
                                     ".config/alacritty/alacritty.toml"
                                     ".config/alacritty/themes/themes/tokyo-night.toml"
[0.032020983s] [INFO ] [alacritty] Using EGL 1.5
[0.032067079s] [DEBUG] [alacritty] Picked GL Config:
                                     buffer_type: Some(Rgb { r_size: 8, g_size: 8, b_size: 8 })
                                     alpha_size: 8
                                     num_samples: 0
                                     hardware_accelerated: true
                                     supports_transparency: Some(true)
                                     config_api: Api(OPENGL | GLES1 | GLES2 | GLES3)
                                     srgb_capable: true
[0.035075269s] [INFO ] [alacritty] Window scale factor: 1
[0.036997955s] [DEBUG] [alacritty] Loading "JetBrains Mono" font
[0.048522765s] [DEBUG] [crossfont] Loaded Face Face { ft_face: Font Face: Regular, load_flags: LoadFlag(FORCE_AUTOHINT | TARGET_LIGHT | TARGET_MONO), render_mode: "Lcd", lcd_filter: 1 }
[0.052494282s] [DEBUG] [crossfont] Loaded Face Face { ft_face: Font Face: Italic, load_flags: LoadFlag(FORCE_AUTOHINT | TARGET_LIGHT | TARGET_MONO), render_mode: "Lcd", lcd_filter: 1 }
[0.056145538s] [DEBUG] [crossfont] Loaded Face Face { ft_face: Font Face: Bold Italic, load_flags: LoadFlag(FORCE_AUTOHINT | TARGET_LIGHT | TARGET_MONO), render_mode: "Lcd", lcd_filter: 1 }
[0.061237837s] [INFO ] [alacritty] Running on AMD Radeon Graphics (radeonsi, renoir, LLVM 18.1.8, DRM 3.59, 6.11.9-zen1-1-zen)
[0.061260339s] [INFO ] [alacritty] OpenGL version 4.6 (Core Profile) Mesa 24.2.7-arch1.1, shader_version 4.60
[0.061267753s] [INFO ] [alacritty] Using OpenGL 3.3 renderer
[0.068799656s] [DEBUG] [alacritty] Enabled debug logging for OpenGL
[0.070534168s] [DEBUG] [alacritty] Filling glyph cache with common glyphs
[0.080691745s] [INFO ] [alacritty] Cell size: 10 x 25
[0.080720799s] [INFO ] [alacritty] Padding: 0 x 0
[0.080725207s] [INFO ] [alacritty] Width: 800, Height: 600
[0.080761626s] [INFO ] [alacritty] PTY dimensions: 24 x 80
[0.084459059s] [INFO ] [alacritty] Initialisation complete
[0.105657768s] [INFO ] [alacritty] Font size changed to 18.666666 px
[0.105695439s] [INFO ] [alacritty] Cell size: 10 x 25
[0.105708323s] [DEBUG] [alacritty_terminal] New num_cols is 191 and num_lines is 40
[0.110029896s] [INFO ] [alacritty] Padding: 0 x 0
[0.110058470s] [INFO ] [alacritty] Width: 1916, Height: 1020
```
When in Vi Mode [Shift]+[Ctrl]+[Space], moving to the left with [B] "SemanticLeft" does not work for all semantic characters:

`this is a test－STAHP．STAHP—yes this is a test-move test•lets go`

In the above, the characters to the left of "STAHP" causes the cursor to... stop. Moving forward with [E] is fine. The only way to move past those characters is to use the cursor keys.

Please be so kind as to look into this if you have the time at hand to spare.

With humble thanks.
macOS specific keyboard input crashes alacritty 0.14.0
edit: changed the topic title to better reflect the issue given what others have commented regarding their own experience with crashing.

I use a piece of software on macOS called [BetterTouchTool](https://folivora.ai/) and one of its features is being able to turn caps lock into a hyper key. Hyper key for the uninitiated is actually a simultaneous key press event for the actual modifier keys `Command|Shift|Control|Option`.  I use the hyper key in combination with keyboard bindings within alacritty such that my hyper key essentially handles the standard bash / readline Ctrl-* key sequences--that is to say,  Hyper-C acts as Ctrl-c  and terminates a process. 

```
[[keyboard.bindings]]
chars = "\u0003"
key = "C"
mods = "Command|Shift|Control|Option"
```

Previously, (i.e. the past two years) this was working just fine.

Expected behavior: The character sequence defined in the key binding is sent, the desired effect occurs, and the terminal continues to operate without issue.

Actual behavior: Repeated (although sometimes on the first try) use of the hyper key crashes the terminal causing it to suddenly close.

### System

OS:
```
> sw_vers
ProductName:		macOS
ProductVersion:		15.0.1
BuildVersion:  		24A348
``` 
Version:
```
> alacritty --version
alacritty 0.14.0 (22a4475)
```

### Logs

```
[2.758060875s] [INFO ] [alacritty_winit_event] Event { window_id: Some(WindowId(5198973488)), payload: Frame }
[2.758098667s] [INFO ] [alacritty_winit_event] About to wait
[2.758120958s] [INFO ] [alacritty_winit_event] About to wait
[2.765228375s] [INFO ] [alacritty_winit_event] About to wait
[2.771734167s] [INFO ] [alacritty_winit_event] ModifiersChanged(Modifiers { state: ModifiersState(0x0), pressed_mods: ModifiersKeys(0x0) })
[2.771803708s] [INFO ] [alacritty_winit_event] KeyboardInput { device_id: DeviceId(DeviceId), event: KeyEvent { physical_key: Code(ControlLeft), logical_key: Named(Control), text: None, location: Left, state: Released, repeat: false, platform_specific: KeyEventExtra { text_with_all_modifiers: None, key_without_modifiers: Named(Control) } }, is_synthetic: false }
[2.771818125s] [INFO ] [alacritty_winit_event] ModifiersChanged(Modifiers { state: ModifiersState(SHIFT | ALT | SUPER), pressed_mods: ModifiersKeys(0x0) })
[2.771874583s] [INFO ] [alacritty_winit_event] KeyboardInput { device_id: DeviceId(DeviceId), event: KeyEvent { physical_key: Code(ShiftLeft), logical_key: Named(Shift), text: None, location: Left, state: Released, repeat: false, platform_specific: KeyEventExtra { text_with_all_modifiers: None, key_without_modifiers: Named(Shift) } }, is_synthetic: false }
[2.771896125s] [INFO ] [alacritty_winit_event] ModifiersChanged(Modifiers { state: ModifiersState(ALT | SUPER), pressed_mods: ModifiersKeys(0x0) })
[2.771950375s] [INFO ] [alacritty_winit_event] KeyboardInput { device_id: DeviceId(DeviceId), event: KeyEvent { physical_key: Code(AltLeft), logical_key: Named(Alt), text: None, location: Left, state: Released, repeat: false, platform_specific: KeyEventExtra { text_with_all_modifiers: None, key_without_modifiers: Named(Alt) } }, is_synthetic: false }
[2.771962542s] [INFO ] [alacritty_winit_event] ModifiersChanged(Modifiers { state: ModifiersState(SUPER), pressed_mods: ModifiersKeys(0x0) })
[2.772015583s] [INFO ] [alacritty_winit_event] KeyboardInput { device_id: DeviceId(DeviceId), event: KeyEvent { physical_key: Code(SuperLeft), logical_key: Named(Super), text: None, location: Left, state: Released, repeat: false, platform_specific: KeyEventExtra { text_with_all_modifiers: None, key_without_modifiers: Named(Super) } }, is_synthetic: false }
[2.772026875s] [INFO ] [alacritty_winit_event] ModifiersChanged(Modifiers { state: ModifiersState(0x0), pressed_mods: ModifiersKeys(0x0) })
[2.772066917s] [INFO ] [alacritty_winit_event] About to wait
[2.772157250s] [INFO ] [alacritty_winit_event] About to wait
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid message sent to event "NSEvent: type=FlagsChanged loc=(0,744) time=3879.1 flags=0x100 win=0x135e20a30 winNum=1167 ctxt=0x0 keyCode=15"'
*** First throw call stack:
(
	0   CoreFoundation                      0x00000001955b0ec0 __exceptionPreprocess + 176
	1   libobjc.A.dylib                     0x0000000195096cd8 objc_exception_throw + 88
	2   Foundation                          0x00000001967ad588 -[NSCalendarDate initWithCoder:] + 0
	3   AppKit                              0x0000000199225cec -[NSEvent charactersIgnoringModifiers] + 604
	4   alacritty                           0x0000000104969154 _ZN5winit13platform_impl5macos5event16create_key_event17h5ffd7f45014bc6b9E + 2508
	5   alacritty                           0x00000001049594a8 _ZN5winit13platform_impl5macos4view9WinitView13flags_changed17h58c32ee31fdc0f1bE + 344
	6   AppKit                              0x00000001991a80a8 -[NSWindow(NSEventRouting) _reallySendEvent:isDelayedEvent:] + 484
	7   AppKit                              0x00000001991a7cf4 -[NSWindow(NSEventRouting) sendEvent:] + 284
	8   AppKit                              0x00000001999a53e0 -[NSApplication(NSEventRouting) sendEvent:] + 1212
	9   alacritty                           0x0000000104961a3c _ZN5winit13platform_impl5macos3app16WinitApplication10send_event17h9d7b11ed09e187deE + 1040
	10  AppKit                              0x00000001995b8984 -[NSApplication _handleEvent:] + 60
	11  AppKit                              0x0000000199073ba4 -[NSApplication run] + 520
	12  alacritty                           0x000000010469e090 _ZN5winit13platform_impl5macos13event_handler12EventHandler3set17ha6185e593ba31ad4E + 280
	13  alacritty                           0x000000010474d378 _ZN9alacritty4main17h290211ac615fedbeE + 7624
	14  alacritty                           0x00000001046c2060 _ZN3std3sys9backtrace28__rust_begin_short_backtrace17h86645a6c57eec1ecE + 12
	15  alacritty                           0x000000010468392c _ZN3std2rt10lang_start28_$u7b$$u7b$closure$u7d$$u7d$17h7b27810acbbe86f2E + 16
	16  alacritty                           0x00000001048f1598 _ZN3std2rt19lang_start_internal17h9e88109c8deb8787E + 808
	17  alacritty                           0x000000010474de4c main + 52
	18  dyld                                0x00000001950d4274 start + 2840
)
libc++abi: terminating due to uncaught exception of type NSException
[1]    14442 abort      alacritty -vv --print-events
```
I cannot confirm if this hyper key issue applies to any other piece of software that adds this functionality (such as karabiner elements) or if this is an issue specific to how bettertouchtool implements hyperkey. For meanwhile, I've downgraded back to [alacritty 0.13.2](https://github.com/Homebrew/homebrew-cask/blob/ee57cb02e89536c4eb0624e7175705c947693ac7/Casks/a/alacritty.rb) which has resolved the issue.

```
curl 'https://raw.githubusercontent.com/Homebrew/homebrew-cask/ee57cb02e89536c4eb0624e7175705c947693ac7/Casks/a/alacritty.rb' -o alacritty.rb
brew install -s alacritty.rb
```

Not sure if there's something I'm overlooking with the latest update. Appreciate any help.


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2b3d2b46b97..d756a7c29e7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 Notable changes to the `alacritty_terminal` crate are documented in its
 [CHANGELOG](./alacritty_terminal/CHANGELOG.md).
 
+## 0.14.1-rc1
+
+### Changed
+
+- Always emit `1` for the first parameter when having modifiers in kitty keyboard protocol
+
+### Fixed
+
+- Mouse/Vi cursor hint highlighting broken on the terminal cursor line
+- Hint launcher opening arbitrary text, when terminal content changed while opening
+- `SemanticRight`/`SemanticLeft` vi motions breaking with wide semantic escape characters
+- `alacritty migrate` crashing with recursive toml imports
+- Migrating nonexistent toml import breaking the entire migration
+- Crash when pressing certain modifier keys on macOS 15+
+- First daemon mode window ignoring window options passed through CLI
+
 ## 0.14.0
 
 ### Packaging
diff --git a/Cargo.lock b/Cargo.lock
index 961ef3ab82d..e0ac83e7e3f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -32,7 +32,7 @@ dependencies = [
 
 [[package]]
 name = "alacritty"
-version = "0.14.0"
+version = "0.14.1-rc1"
 dependencies = [
  "ahash",
  "alacritty_config",
@@ -92,7 +92,7 @@ dependencies = [
 
 [[package]]
 name = "alacritty_terminal"
-version = "0.24.1"
+version = "0.24.2-rc1"
 dependencies = [
  "base64",
  "bitflags 2.6.0",
@@ -966,10 +966,11 @@ dependencies = [
 
 [[package]]
 name = "js-sys"
-version = "0.3.69"
+version = "0.3.76"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
+checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7"
 dependencies = [
+ "once_cell",
  "wasm-bindgen",
 ]
 
@@ -2096,23 +2097,23 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
 
 [[package]]
 name = "wasm-bindgen"
-version = "0.2.92"
+version = "0.2.99"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
+checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396"
 dependencies = [
  "cfg-if",
+ "once_cell",
  "wasm-bindgen-macro",
 ]
 
 [[package]]
 name = "wasm-bindgen-backend"
-version = "0.2.92"
+version = "0.2.99"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
+checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79"
 dependencies = [
  "bumpalo",
  "log",
- "once_cell",
  "proc-macro2",
  "quote",
  "syn",
@@ -2121,21 +2122,22 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-futures"
-version = "0.4.42"
+version = "0.4.49"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0"
+checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2"
 dependencies = [
  "cfg-if",
  "js-sys",
+ "once_cell",
  "wasm-bindgen",
  "web-sys",
 ]
 
 [[package]]
 name = "wasm-bindgen-macro"
-version = "0.2.92"
+version = "0.2.99"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
+checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe"
 dependencies = [
  "quote",
  "wasm-bindgen-macro-support",
@@ -2143,9 +2145,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro-support"
-version = "0.2.92"
+version = "0.2.99"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
+checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2156,9 +2158,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-shared"
-version = "0.2.92"
+version = "0.2.99"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
+checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6"
 
 [[package]]
 name = "wayland-backend"
@@ -2271,9 +2273,9 @@ dependencies = [
 
 [[package]]
 name = "web-sys"
-version = "0.3.69"
+version = "0.3.76"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef"
+checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc"
 dependencies = [
  "js-sys",
  "wasm-bindgen",
@@ -2536,9 +2538,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
 
 [[package]]
 name = "winit"
-version = "0.30.4"
+version = "0.30.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4225ddd8ab67b8b59a2fee4b34889ebf13c0460c1c3fa297c58e21eb87801b33"
+checksum = "dba50bc8ef4b6f1a75c9274fb95aa9a8f63fbc66c56f391bd85cf68d51e7b1a3"
 dependencies = [
  "ahash",
  "android-activity",
diff --git a/INSTALL.md b/INSTALL.md
index b5f6871c098..6974e1ee2c5 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -84,7 +84,7 @@ to build Alacritty. Here's an apt command that should install all of them. If
 something is still found to be missing, please open an issue.
 
 ```sh
-apt install cmake pkg-config libfreetype6-dev libfontconfig1-dev libxcb-xfixes0-dev libxkbcommon-dev python3
+apt install cmake g++ pkg-config libfreetype6-dev libfontconfig1-dev libxcb-xfixes0-dev libxkbcommon-dev python3
 ```
 
 #### Arch Linux
diff --git a/alacritty/Cargo.toml b/alacritty/Cargo.toml
index d80a5986467..3a0fc568b39 100644
--- a/alacritty/Cargo.toml
+++ b/alacritty/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "alacritty"
-version = "0.14.0"
+version = "0.14.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"
@@ -12,7 +12,7 @@ rust-version = "1.74.0"
 
 [dependencies.alacritty_terminal]
 path = "../alacritty_terminal"
-version = "0.24.1"
+version = "0.24.2-rc1"
 
 [dependencies.alacritty_config_derive]
 path = "../alacritty_config_derive"
@@ -41,7 +41,7 @@ tempfile = "3.12.0"
 toml = "0.8.2"
 toml_edit = "0.22.21"
 unicode-width = "0.1"
-winit = { version = "0.30.4", default-features = false, features = ["rwh_06", "serde"] }
+winit = { version = "0.30.7", default-features = false, features = ["rwh_06", "serde"] }
 
 [build-dependencies]
 gl_generator = "0.14.0"
diff --git a/alacritty/src/config/bindings.rs b/alacritty/src/config/bindings.rs
index dfe31853bc0..931b0583ba2 100644
--- a/alacritty/src/config/bindings.rs
+++ b/alacritty/src/config/bindings.rs
@@ -5,6 +5,7 @@ use std::fmt::{self, Debug, Display};
 use bitflags::bitflags;
 use serde::de::{self, Error as SerdeError, MapAccess, Unexpected, Visitor};
 use serde::{Deserialize, Deserializer};
+use std::rc::Rc;
 use toml::Value as SerdeValue;
 use winit::event::MouseButton;
 use winit::keyboard::{
@@ -96,7 +97,7 @@ pub enum Action {
 
     /// Regex keyboard hints.
     #[config(skip)]
-    Hint(Hint),
+    Hint(Rc<Hint>),
 
     /// Move vi mode cursor.
     #[config(skip)]
@@ -790,7 +791,7 @@ impl<'a> Deserialize<'a> for ModeWrapper {
     {
         struct ModeVisitor;
 
-        impl<'a> Visitor<'a> for ModeVisitor {
+        impl Visitor<'_> for ModeVisitor {
             type Value = ModeWrapper;
 
             fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@@ -844,7 +845,7 @@ impl<'a> Deserialize<'a> for MouseButtonWrapper {
     {
         struct MouseButtonVisitor;
 
-        impl<'a> Visitor<'a> for MouseButtonVisitor {
+        impl Visitor<'_> for MouseButtonVisitor {
             type Value = MouseButtonWrapper;
 
             fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@@ -956,7 +957,7 @@ impl<'a> Deserialize<'a> for RawBinding {
             {
                 struct FieldVisitor;
 
-                impl<'a> Visitor<'a> for FieldVisitor {
+                impl Visitor<'_> for FieldVisitor {
                     type Value = Field;
 
                     fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@@ -1204,7 +1205,7 @@ impl<'a> de::Deserialize<'a> for ModsWrapper {
     {
         struct ModsVisitor;
 
-        impl<'a> Visitor<'a> for ModsVisitor {
+        impl Visitor<'_> for ModsVisitor {
             type Value = ModsWrapper;
 
             fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
diff --git a/alacritty/src/config/font.rs b/alacritty/src/config/font.rs
index 061c0f42a36..760c26d5832 100644
--- a/alacritty/src/config/font.rs
+++ b/alacritty/src/config/font.rs
@@ -144,7 +144,7 @@ impl<'de> Deserialize<'de> for Size {
         D: Deserializer<'de>,
     {
         struct NumVisitor;
-        impl<'v> Visitor<'v> for NumVisitor {
+        impl Visitor<'_> for NumVisitor {
             type Value = Size;
 
             fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
diff --git a/alacritty/src/config/ui_config.rs b/alacritty/src/config/ui_config.rs
index 69716dee3cf..b44bda0d757 100644
--- a/alacritty/src/config/ui_config.rs
+++ b/alacritty/src/config/ui_config.rs
@@ -253,7 +253,7 @@ pub struct Hints {
     alphabet: HintsAlphabet,
 
     /// All configured terminal hints.
-    pub enabled: Vec<Hint>,
+    pub enabled: Vec<Rc<Hint>>,
 }
 
 impl Default for Hints {
@@ -274,7 +274,7 @@ impl Default for Hints {
         });
 
         Self {
-            enabled: vec![Hint {
+            enabled: vec![Rc::new(Hint {
                 content,
                 action,
                 persist: false,
@@ -288,7 +288,7 @@ impl Default for Hints {
                     mods: ModsWrapper(ModifiersState::SHIFT | ModifiersState::CONTROL),
                     mode: Default::default(),
                 }),
-            }],
+            })],
             alphabet: Default::default(),
         }
     }
@@ -619,7 +619,7 @@ impl SerdeReplace for Program {
 }
 
 pub(crate) struct StringVisitor;
-impl<'de> serde::de::Visitor<'de> for StringVisitor {
+impl serde::de::Visitor<'_> for StringVisitor {
     type Value = String;
 
     fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
diff --git a/alacritty/src/display/color.rs b/alacritty/src/display/color.rs
index 669bf502f89..2e854a3fa12 100644
--- a/alacritty/src/display/color.rs
+++ b/alacritty/src/display/color.rs
@@ -18,7 +18,7 @@ pub const DIM_FACTOR: f32 = 0.66;
 #[derive(Copy, Clone)]
 pub struct List([Rgb; COUNT]);
 
-impl<'a> From<&'a Colors> for List {
+impl From<&'_ Colors> for List {
     fn from(colors: &Colors) -> List {
         // Type inference fails without this annotation.
         let mut list = List([Rgb::default(); COUNT]);
@@ -235,7 +235,7 @@ impl<'de> Deserialize<'de> for Rgb {
             b: u8,
         }
 
-        impl<'a> Visitor<'a> for RgbVisitor {
+        impl Visitor<'_> for RgbVisitor {
             type Value = Rgb;
 
             fn expecting(&self, f: &mut Formatter<'_>) -> fmt::Result {
@@ -331,7 +331,7 @@ impl<'de> Deserialize<'de> for CellRgb {
         const EXPECTING: &str = "CellForeground, CellBackground, or hex color like #ff00ff";
 
         struct CellRgbVisitor;
-        impl<'a> Visitor<'a> for CellRgbVisitor {
+        impl Visitor<'_> for CellRgbVisitor {
             type Value = CellRgb;
 
             fn expecting(&self, f: &mut Formatter<'_>) -> fmt::Result {
diff --git a/alacritty/src/display/content.rs b/alacritty/src/display/content.rs
index 2fbbdec42ae..edd2709f063 100644
--- a/alacritty/src/display/content.rs
+++ b/alacritty/src/display/content.rs
@@ -150,7 +150,7 @@ impl<'a> RenderableContent<'a> {
     }
 }
 
-impl<'a> Iterator for RenderableContent<'a> {
+impl Iterator for RenderableContent<'_> {
     type Item = RenderableCell;
 
     /// Gets the next renderable cell.
@@ -453,7 +453,7 @@ struct Hint<'a> {
     labels: &'a Vec<Vec<char>>,
 }
 
-impl<'a> Hint<'a> {
+impl Hint<'_> {
     /// Advance the hint iterator.
     ///
     /// If the point is within a hint, the keyboard shortcut character that should be displayed at
@@ -543,7 +543,7 @@ impl<'a> HintMatches<'a> {
     }
 }
 
-impl<'a> Deref for HintMatches<'a> {
+impl Deref for HintMatches<'_> {
     type Target = [Match];
 
     fn deref(&self) -> &Self::Target {
diff --git a/alacritty/src/display/damage.rs b/alacritty/src/display/damage.rs
index 8efe0133ba8..b07363753f7 100644
--- a/alacritty/src/display/damage.rs
+++ b/alacritty/src/display/damage.rs
@@ -193,9 +193,11 @@ impl FrameDamage {
     /// Check if a range is damaged.
     #[inline]
     pub fn intersects(&self, start: Point<usize>, end: Point<usize>) -> bool {
+        let start_line = &self.lines[start.line];
+        let end_line = &self.lines[end.line];
         self.full
-            || self.lines[start.line].left <= start.column
-            || self.lines[end.line].right >= end.column
+            || (start_line.left..=start_line.right).contains(&start.column)
+            || (end_line.left..=end_line.right).contains(&end.column)
             || (start.line + 1..end.line).any(|line| self.lines[line].is_damaged())
     }
 }
@@ -249,7 +251,7 @@ impl<'a> RenderDamageIterator<'a> {
     }
 }
 
-impl<'a> Iterator for RenderDamageIterator<'a> {
+impl Iterator for RenderDamageIterator<'_> {
     type Item = Rect;
 
     fn next(&mut self) -> Option<Rect> {
diff --git a/alacritty/src/display/hint.rs b/alacritty/src/display/hint.rs
index a01a1d03cf9..3f10b4e5639 100644
--- a/alacritty/src/display/hint.rs
+++ b/alacritty/src/display/hint.rs
@@ -1,6 +1,8 @@
+use std::borrow::Cow;
 use std::cmp::Reverse;
 use std::collections::HashSet;
 use std::iter;
+use std::rc::Rc;
 
 use ahash::RandomState;
 use winit::keyboard::ModifiersState;
@@ -23,7 +25,7 @@ const HINT_SPLIT_PERCENTAGE: f32 = 0.5;
 /// Keyboard regex hint state.
 pub struct HintState {
     /// Hint currently in use.
-    hint: Option<Hint>,
+    hint: Option<Rc<Hint>>,
 
     /// Alphabet for hint labels.
     alphabet: String,
@@ -56,7 +58,7 @@ impl HintState {
     }
 
     /// Start the hint selection process.
-    pub fn start(&mut self, hint: Hint) {
+    pub fn start(&mut self, hint: Rc<Hint>) {
         self.hint = Some(hint);
     }
 
@@ -150,7 +152,7 @@ impl HintState {
         // Check if the selected label is fully matched.
         if label.len() == 1 {
             let bounds = self.matches[index].clone();
-            let action = hint.action.clone();
+            let hint = hint.clone();
 
             // Exit hint mode unless it requires explicit dismissal.
             if hint.persist {
@@ -161,7 +163,7 @@ impl HintState {
 
             // Hyperlinks take precedence over regex matches.
             let hyperlink = term.grid()[*bounds.start()].hyperlink();
-            Some(HintMatch { action, bounds, hyperlink })
+            Some(HintMatch { bounds, hyperlink, hint })
         } else {
             // Store character to preserve the selection.
             self.keys.push(c);
@@ -192,13 +194,14 @@ impl HintState {
 /// Hint match which was selected by the user.
 #[derive(PartialEq, Eq, Debug, Clone)]
 pub struct HintMatch {
-    /// Action for handling the text.
-    action: HintAction,
-
     /// Terminal range matching the hint.
     bounds: Match,
 
+    /// OSC 8 hyperlink.
     hyperlink: Option<Hyperlink>,
+
+    /// Hint which triggered this match.
+    hint: Rc<Hint>,
 }
 
 impl HintMatch {
@@ -210,7 +213,7 @@ impl HintMatch {
 
     #[inline]
     pub fn action(&self) -> &HintAction {
-        &self.action
+        &self.hint.action
     }
 
     #[inline]
@@ -221,6 +224,29 @@ impl HintMatch {
     pub fn hyperlink(&self) -> Option<&Hyperlink> {
         self.hyperlink.as_ref()
     }
+
+    /// Get the text content of the hint match.
+    ///
+    /// This will always revalidate the hint text, to account for terminal content
+    /// changes since the [`HintMatch`] was constructed. The text of the hint might
+    /// be different from its original value, but it will **always** be a valid
+    /// match for this hint.
+    pub fn text<T>(&self, term: &Term<T>) -> Option<Cow<'_, str>> {
+        // Revalidate hyperlink match.
+        if let Some(hyperlink) = &self.hyperlink {
+            let (validated, bounds) = hyperlink_at(term, *self.bounds.start())?;
+            return (&validated == hyperlink && bounds == self.bounds)
+                .then(|| hyperlink.uri().into());
+        }
+
+        // Revalidate regex match.
+        let regex = self.hint.content.regex.as_ref()?;
+        let bounds = regex.with_compiled(|regex| {
+            regex_match_at(term, *self.bounds.start(), regex, self.hint.post_processing)
+        })??;
+        (bounds == self.bounds)
+            .then(|| term.bounds_to_string(*bounds.start(), *bounds.end()).into())
+    }
 }
 
 /// Generator for creating new hint labels.
@@ -382,18 +408,14 @@ pub fn highlighted_at<T>(
         if let Some((hyperlink, bounds)) =
             hint.content.hyperlinks.then(|| hyperlink_at(term, point)).flatten()
         {
-            return Some(HintMatch {
-                bounds,
-                action: hint.action.clone(),
-                hyperlink: Some(hyperlink),
-            });
+            return Some(HintMatch { bounds, hyperlink: Some(hyperlink), hint: hint.clone() });
         }
 
         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 });
+            return Some(HintMatch { bounds, hint: hint.clone(), hyperlink: None });
         }
 
         None
@@ -554,7 +576,7 @@ impl<'a, T> HintPostProcessor<'a, T> {
     }
 }
 
-impl<'a, T> Iterator for HintPostProcessor<'a, T> {
+impl<T> Iterator for HintPostProcessor<'_, T> {
     type Item = Match;
 
     fn next(&mut self) -> Option<Self::Item> {
diff --git a/alacritty/src/display/meter.rs b/alacritty/src/display/meter.rs
index e263100fdfb..20cbbd1b172 100644
--- a/alacritty/src/display/meter.rs
+++ b/alacritty/src/display/meter.rs
@@ -57,7 +57,7 @@ impl<'a> Sampler<'a> {
     }
 }
 
-impl<'a> Drop for Sampler<'a> {
+impl Drop for Sampler<'_> {
     fn drop(&mut self) {
         self.meter.add_sample(self.alive_duration());
     }
diff --git a/alacritty/src/display/mod.rs b/alacritty/src/display/mod.rs
index d0044f8bf15..d358a7c5d26 100644
--- a/alacritty/src/display/mod.rs
+++ b/alacritty/src/display/mod.rs
@@ -342,9 +342,13 @@ pub struct Display {
 
     /// Hint highlighted by the mouse.
     pub highlighted_hint: Option<HintMatch>,
+    /// Frames since hint highlight was created.
+    highlighted_hint_age: usize,
 
     /// Hint highlighted by the vi mode cursor.
     pub vi_highlighted_hint: Option<HintMatch>,
+    /// Frames since hint highlight was created.
+    vi_highlighted_hint_age: usize,
 
     pub raw_window_handle: RawWindowHandle,
 
@@ -516,6 +520,8 @@ impl Display {
             font_size,
             window,
             pending_renderer_update: Default::default(),
+            vi_highlighted_hint_age: Default::default(),
+            highlighted_hint_age: Default::default(),
             vi_highlighted_hint: Default::default(),
             highlighted_hint: Default::default(),
             hint_mouse_point: Default::default(),
@@ -759,7 +765,7 @@ impl Display {
         drop(terminal);
 
         // Invalidate highlighted hints if grid has changed.
-        self.validate_hints(display_offset);
+        self.validate_hint_highlights(display_offset);
 
         // Add damage from alacritty's UI elements overlapping terminal.
 
@@ -1017,9 +1023,10 @@ impl Display {
         };
         let mut dirty = vi_highlighted_hint != self.vi_highlighted_hint;
         self.vi_highlighted_hint = vi_highlighted_hint;
+        self.vi_highlighted_hint_age = 0;
 
         // Force full redraw if the vi mode highlight was cleared.
-        if dirty && self.vi_highlighted_hint.is_none() {
+        if dirty {
             self.damage_tracker.frame().mark_fully_damaged();
         }
 
@@ -1055,9 +1062,10 @@ impl Display {
         let mouse_highlight_dirty = self.highlighted_hint != highlighted_hint;
         dirty |= mouse_highlight_dirty;
         self.highlighted_hint = highlighted_hint;
+        self.highlighted_hint_age = 0;
 
-        // Force full redraw if the mouse cursor highlight was cleared.
-        if mouse_highlight_dirty && self.highlighted_hint.is_none() {
+        // Force full redraw if the mouse cursor highlight was changed.
+        if mouse_highlight_dirty {
             self.damage_tracker.frame().mark_fully_damaged();
         }
 
@@ -1331,16 +1339,24 @@ impl Display {
     }
 
     /// Check whether a hint highlight needs to be cleared.
-    fn validate_hints(&mut self, display_offset: usize) {
+    fn validate_hint_highlights(&mut self, display_offset: usize) {
         let frame = self.damage_tracker.frame();
-        for (hint, reset_mouse) in
-            [(&mut self.highlighted_hint, true), (&mut self.vi_highlighted_hint, false)]
-        {
+        let hints = [
+            (&mut self.highlighted_hint, &mut self.highlighted_hint_age, true),
+            (&mut self.vi_highlighted_hint, &mut self.vi_highlighted_hint_age, false),
+        ];
+        for (hint, hint_age, reset_mouse) in hints {
             let (start, end) = match hint {
                 Some(hint) => (*hint.bounds().start(), *hint.bounds().end()),
-                None => return,
+                None => continue,
             };
 
+            // Ignore hints that were created this frame.
+            *hint_age += 1;
+            if *hint_age == 1 {
+                continue;
+            }
+
             // Convert hint bounds to viewport coordinates.
             let start = term::point_to_viewport(display_offset, start).unwrap_or_default();
             let end = term::point_to_viewport(display_offset, end).unwrap_or_else(|| {
diff --git a/alacritty/src/event.rs b/alacritty/src/event.rs
index f01590603b2..79f31eef044 100644
--- a/alacritty/src/event.rs
+++ b/alacritty/src/event.rs
@@ -140,14 +140,14 @@ impl Processor {
     pub fn create_initial_window(
         &mut self,
         event_loop: &ActiveEventLoop,
+        window_options: WindowOptions,
     ) -> Result<(), Box<dyn Error>> {
-        let options = match self.initial_window_options.take() {
-            Some(options) => options,
-            None => return Ok(()),
-        };
-
-        let window_context =
-            WindowContext::initial(event_loop, self.proxy.clone(), self.config.clone(), options)?;
+        let window_context = WindowContext::initial(
+            event_loop,
+            self.proxy.clone(),
+            self.config.clone(),
+            window_options,
+        )?;
 
         self.gl_config = Some(window_context.display.gl_context().config());
         self.windows.insert(window_context.id(), window_context);
@@ -225,10 +225,12 @@ impl ApplicationHandler<Event> for Processor {
             return;
         }
 
-        if let Err(err) = self.create_initial_window(event_loop) {
-            self.initial_window_error = Some(err);
-            event_loop.exit();
-            return;
+        if let Some(window_options) = self.initial_window_options.take() {
+            if let Err(err) = self.create_initial_window(event_loop, window_options) {
+                self.initial_window_error = Some(err);
+                event_loop.exit();
+                return;
+            }
         }
 
         info!("Initialisation complete");
@@ -276,7 +278,7 @@ impl ApplicationHandler<Event> for Processor {
         }
 
         // Handle events which don't mandate the WindowId.
-        match (&event.payload, event.window_id.as_ref()) {
+        match (event.payload, event.window_id.as_ref()) {
             // Process IPC config update.
             #[cfg(unix)]
             (EventType::IpcConfig(ipc_config), window_id) => {
@@ -315,7 +317,7 @@ impl ApplicationHandler<Event> for Processor {
                 }
 
                 // Load config and update each terminal.
-                if let Ok(config) = config::reload(path, &mut self.cli_options) {
+                if let Ok(config) = config::reload(&path, &mut self.cli_options) {
                     self.config = Rc::new(config);
 
                     // Restart config monitor if imports changed.
@@ -346,17 +348,17 @@ impl ApplicationHandler<Event> for Processor {
 
                 if self.gl_config.is_none() {
                     // Handle initial window creation in daemon mode.
-                    if let Err(err) = self.create_initial_window(event_loop) {
+                    if let Err(err) = self.create_initial_window(event_loop, options) {
                         self.initial_window_error = Some(err);
                         event_loop.exit();
                     }
-                } else if let Err(err) = self.create_window(event_loop, options.clone()) {
+                } else if let Err(err) = self.create_window(event_loop, options) {
                     error!("Could not open window: {:?}", err);
                 }
             },
             // Process events affecting all windows.
-            (_, None) => {
-                let event = WinitEvent::UserEvent(event);
+            (payload, None) => {
+                let event = WinitEvent::UserEvent(Event::new(payload, None));
                 for window_context in self.windows.values_mut() {
                     window_context.handle_event(
                         #[cfg(target_os = "macos")]
@@ -405,7 +407,7 @@ impl ApplicationHandler<Event> for Processor {
                     }
                 }
             },
-            (_, Some(window_id)) => {
+            (payload, Some(window_id)) => {
                 if let Some(window_context) = self.windows.get_mut(window_id) {
                     window_context.handle_event(
                         #[cfg(target_os = "macos")]
@@ -413,7 +415,7 @@ impl ApplicationHandler<Event> for Processor {
                         &self.proxy,
                         &mut self.clipboard,
                         &mut self.scheduler,
-                        WinitEvent::UserEvent(event),
+                        WinitEvent::UserEvent(Event::new(payload, *window_id)),
                     );
                 }
             },
@@ -1141,16 +1143,16 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon
         }
 
         let hint_bounds = hint.bounds();
-        let text = match hint.hyperlink() {
-            Some(hyperlink) => hyperlink.uri().to_owned(),
-            None => self.terminal.bounds_to_string(*hint_bounds.start(), *hint_bounds.end()),
+        let text = match hint.text(self.terminal) {
+            Some(text) => text,
+            None => return,
         };
 
         match &hint.action() {
             // Launch an external program.
             HintAction::Command(command) => {
                 let mut args = command.args().to_vec();
-                args.push(text);
+                args.push(text.into());
                 self.spawn_daemon(command.program(), &args);
             },
             // Copy the text to the clipboard.
diff --git a/alacritty/src/input/keyboard.rs b/alacritty/src/input/keyboard.rs
index 147555940bd..85734109db9 100644
--- a/alacritty/src/input/keyboard.rs
+++ b/alacritty/src/input/keyboard.rs
@@ -280,7 +280,7 @@ fn build_sequence(key: KeyEvent, mods: ModifiersState, mode: TermMode) -> Vec<u8
     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_named_normal(&key, associated_text.is_some()))
         .or_else(|| context.try_build_control_char_or_mod(&key, &mut modifiers))
         .or_else(|| context.try_build_textual(&key, associated_text));
 
@@ -483,14 +483,23 @@ impl SequenceBuilder {
     }
 
     /// Try building from [`NamedKey`].
-    fn try_build_named_normal(&self, key: &KeyEvent) -> Option<SequenceBase> {
+    fn try_build_named_normal(
+        &self,
+        key: &KeyEvent,
+        has_associated_text: bool,
+    ) -> 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 one_based =
+            if self.modifiers.is_empty() && !self.kitty_event_type && !has_associated_text {
+                ""
+            } else {
+                "1"
+            };
         let (base, terminator) = match named {
             NamedKey::PageUp => ("5", SequenceTerminator::Normal('~')),
             NamedKey::PageDown => ("6", SequenceTerminator::Normal('~')),
diff --git a/alacritty/src/input/mod.rs b/alacritty/src/input/mod.rs
index bbd8673f7bb..1c9d6008e07 100644
--- a/alacritty/src/input/mod.rs
+++ b/alacritty/src/input/mod.rs
@@ -1136,7 +1136,7 @@ mod tests {
         inline_search_state: &'a mut InlineSearchState,
     }
 
-    impl<'a, T: EventListener> super::ActionContext<T> for ActionContext<'a, T> {
+    impl<T: EventListener> super::ActionContext<T> for ActionContext<'_, T> {
         fn search_next(
             &mut self,
             _origin: Point,
diff --git a/alacritty/src/migrate/mod.rs b/alacritty/src/migrate/mod.rs
index 2f806d361a1..bfa72294a28 100644
--- a/alacritty/src/migrate/mod.rs
+++ b/alacritty/src/migrate/mod.rs
@@ -151,7 +151,15 @@ fn migrate_imports(
     // Migrate each import.
     for import in imports.into_iter().filter_map(|item| item.as_str()) {
         let normalized_path = config::normalize_import(path, import);
-        let migration = migrate_config(options, &normalized_path, recursion_limit)?;
+
+        if !normalized_path.exists() {
+            if options.dry_run {
+                println!("Skipping migration for nonexistent path: {}", normalized_path.display());
+            }
+            continue;
+        }
+
+        let migration = migrate_config(options, &normalized_path, recursion_limit - 1)?;
         if options.dry_run {
             println!("{}", migration.success_message(true));
         }
@@ -244,7 +252,7 @@ enum Migration<'a> {
     Yaml((&'a Path, String)),
 }
 
-impl<'a> Migration<'a> {
+impl Migration<'_> {
     /// Get the success message for this migration.
     fn success_message(&self, import: bool) -> String {
         match self {
diff --git a/alacritty/src/renderer/text/gles2.rs b/alacritty/src/renderer/text/gles2.rs
index 427a60e397f..8756ea80d10 100644
--- a/alacritty/src/renderer/text/gles2.rs
+++ b/alacritty/src/renderer/text/gles2.rs
@@ -346,7 +346,7 @@ pub struct RenderApi<'a> {
     dual_source_blending: bool,
 }
 
-impl<'a> Drop for RenderApi<'a> {
+impl Drop for RenderApi<'_> {
     fn drop(&mut self) {
         if !self.batch.is_empty() {
             self.render_batch();
@@ -354,7 +354,7 @@ impl<'a> Drop for RenderApi<'a> {
     }
 }
 
-impl<'a> LoadGlyph for RenderApi<'a> {
+impl LoadGlyph for RenderApi<'_> {
     fn load_glyph(&mut self, rasterized: &RasterizedGlyph) -> Glyph {
         Atlas::load_glyph(self.active_tex, self.atlas, self.current_atlas, rasterized)
     }
@@ -364,7 +364,7 @@ impl<'a> LoadGlyph for RenderApi<'a> {
     }
 }
 
-impl<'a> TextRenderApi<Batch> for RenderApi<'a> {
+impl TextRenderApi<Batch> for RenderApi<'_> {
     fn batch(&mut self) -> &mut Batch {
         self.batch
     }
diff --git a/alacritty/src/renderer/text/glsl3.rs b/alacritty/src/renderer/text/glsl3.rs
index 7c32bf9f9e9..fee95ce32d8 100644
--- a/alacritty/src/renderer/text/glsl3.rs
+++ b/alacritty/src/renderer/text/glsl3.rs
@@ -215,7 +215,7 @@ pub struct RenderApi<'a> {
     program: &'a mut TextShaderProgram,
 }
 
-impl<'a> TextRenderApi<Batch> for RenderApi<'a> {
+impl TextRenderApi<Batch> for RenderApi<'_> {
     fn batch(&mut self) -> &mut Batch {
         self.batch
     }
@@ -261,7 +261,7 @@ impl<'a> TextRenderApi<Batch> for RenderApi<'a> {
     }
 }
 
-impl<'a> LoadGlyph for RenderApi<'a> {
+impl LoadGlyph for RenderApi<'_> {
     fn load_glyph(&mut self, rasterized: &RasterizedGlyph) -> Glyph {
         Atlas::load_glyph(self.active_tex, self.atlas, self.current_atlas, rasterized)
     }
@@ -271,7 +271,7 @@ impl<'a> LoadGlyph for RenderApi<'a> {
     }
 }
 
-impl<'a> Drop for RenderApi<'a> {
+impl Drop for RenderApi<'_> {
     fn drop(&mut self) {
         if !self.batch.is_empty() {
             self.render_batch();
diff --git a/alacritty/src/renderer/text/mod.rs b/alacritty/src/renderer/text/mod.rs
index 886b7f8bd49..acd1b521673 100644
--- a/alacritty/src/renderer/text/mod.rs
+++ b/alacritty/src/renderer/text/mod.rs
@@ -186,7 +186,7 @@ pub struct LoaderApi<'a> {
     current_atlas: &'a mut usize,
 }
 
-impl<'a> LoadGlyph for LoaderApi<'a> {
+impl LoadGlyph for LoaderApi<'_> {
     fn load_glyph(&mut self, rasterized: &RasterizedGlyph) -> Glyph {
         Atlas::load_glyph(self.active_tex, self.atlas, self.current_atlas, rasterized)
     }
diff --git a/alacritty/src/string.rs b/alacritty/src/string.rs
index a7af439465d..b8c47d3bd4d 100644
--- a/alacritty/src/string.rs
+++ b/alacritty/src/string.rs
@@ -106,7 +106,7 @@ impl<'a> StrShortener<'a> {
     }
 }
 
-impl<'a> Iterator for StrShortener<'a> {
+impl Iterator for StrShortener<'_> {
     type Item = char;
 
     fn next(&mut self) -> Option<Self::Item> {
diff --git a/alacritty/windows/wix/alacritty.wxs b/alacritty/windows/wix/alacritty.wxs
index cd00bda0bcd..f348865c49e 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.14.0" Manufacturer="Alacritty" InstallerVersion="200">
+   <Package Name="Alacritty" UpgradeCode="87c21c74-dbd5-4584-89d5-46d9cd0c40a7" Language="1033" Codepage="1252" Version="0.14.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_terminal/CHANGELOG.md b/alacritty_terminal/CHANGELOG.md
index e612e7498ec..e17f992cc38 100644
--- a/alacritty_terminal/CHANGELOG.md
+++ b/alacritty_terminal/CHANGELOG.md
@@ -8,6 +8,12 @@ sections should follow the order `Added`, `Changed`, `Deprecated`, `Fixed` and
 
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 
+## 0.24.2
+
+### Fixed
+
+- Semantic escape search across wide characters
+
 ## 0.24.1
 
 ### Changed
diff --git a/alacritty_terminal/Cargo.toml b/alacritty_terminal/Cargo.toml
index aca99825353..0727ef367d5 100644
--- a/alacritty_terminal/Cargo.toml
+++ b/alacritty_terminal/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "alacritty_terminal"
-version = "0.24.1"
+version = "0.24.2-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/grid/mod.rs b/alacritty_terminal/src/grid/mod.rs
index 278bea3bc17..fbd2c79eb41 100644
--- a/alacritty_terminal/src/grid/mod.rs
+++ b/alacritty_terminal/src/grid/mod.rs
@@ -615,7 +615,7 @@ pub trait BidirectionalIterator: Iterator {
     fn prev(&mut self) -> Option<Self::Item>;
 }
 
-impl<'a, T> BidirectionalIterator for GridIterator<'a, T> {
+impl<T> BidirectionalIterator for GridIterator<'_, T> {
     fn prev(&mut self) -> Option<Self::Item> {
         let topmost_line = self.grid.topmost_line();
         let last_column = self.grid.last_column();
diff --git a/alacritty_terminal/src/term/mod.rs b/alacritty_terminal/src/term/mod.rs
index 8a0410dc343..c2d77ec764c 100644
--- a/alacritty_terminal/src/term/mod.rs
+++ b/alacritty_terminal/src/term/mod.rs
@@ -199,7 +199,7 @@ impl<'a> TermDamageIterator<'a> {
     }
 }
 
-impl<'a> Iterator for TermDamageIterator<'a> {
+impl Iterator for TermDamageIterator<'_> {
     type Item = LineDamageBounds;
 
     fn next(&mut self) -> Option<Self::Item> {
@@ -930,6 +930,11 @@ impl<T> Term<T> {
         &self.config.semantic_escape_chars
     }
 
+    #[cfg(test)]
+    pub(crate) fn set_semantic_escape_chars(&mut self, semantic_escape_chars: &str) {
+        self.config.semantic_escape_chars = semantic_escape_chars.into();
+    }
+
     /// Active terminal cursor style.
     ///
     /// While vi mode is active, this will automatically return the vi mode cursor style.
diff --git a/alacritty_terminal/src/term/search.rs b/alacritty_terminal/src/term/search.rs
index 33f6ee056b5..20a427b707b 100644
--- a/alacritty_terminal/src/term/search.rs
+++ b/alacritty_terminal/src/term/search.rs
@@ -656,7 +656,7 @@ impl<'a, T> RegexIter<'a, T> {
     }
 }
 
-impl<'a, T> Iterator for RegexIter<'a, T> {
+impl<T> Iterator for RegexIter<'_, T> {
     type Item = Match;
 
     fn next(&mut self) -> Option<Self::Item> {
diff --git a/alacritty_terminal/src/vi_mode.rs b/alacritty_terminal/src/vi_mode.rs
index 7065544ea72..e23e9b80d96 100644
--- a/alacritty_terminal/src/vi_mode.rs
+++ b/alacritty_terminal/src/vi_mode.rs
@@ -265,14 +265,14 @@ fn semantic<T: EventListener>(
         }
     };
 
-    // Make sure we jump above wide chars.
-    point = term.expand_wide(point, direction);
-
     // Move to word boundary.
     if direction != side && !is_boundary(term, point, direction) {
         point = expand_semantic(point);
     }
 
+    // Make sure we jump above wide chars.
+    point = term.expand_wide(point, direction);
+
     // Skip whitespace.
     let mut next_point = advance(term, point, direction);
     while !is_boundary(term, point, direction) && is_space(term, next_point) {
@@ -283,6 +283,11 @@ fn semantic<T: EventListener>(
     // Assure minimum movement of one cell.
     if !is_boundary(term, point, direction) {
         point = advance(term, point, direction);
+
+        // Skip over wide cell spacers.
+        if direction == Direction::Left {
+            point = term.expand_wide(point, direction);
+        }
     }
 
     // Move to word boundary.
@@ -820,4 +825,42 @@ mod tests {
         cursor = cursor.scroll(&term, -20);
         assert_eq!(cursor.point, Point::new(Line(19), Column(0)));
     }
+
+    #[test]
+    fn wide_semantic_char() {
+        let mut term = term();
+        term.set_semantic_escape_chars("－");
+        term.grid_mut()[Line(0)][Column(0)].c = 'x';
+        term.grid_mut()[Line(0)][Column(1)].c = 'x';
+        term.grid_mut()[Line(0)][Column(2)].c = '－';
+        term.grid_mut()[Line(0)][Column(2)].flags.insert(Flags::WIDE_CHAR);
+        term.grid_mut()[Line(0)][Column(3)].c = ' ';
+        term.grid_mut()[Line(0)][Column(3)].flags.insert(Flags::WIDE_CHAR_SPACER);
+        term.grid_mut()[Line(0)][Column(4)].c = 'x';
+        term.grid_mut()[Line(0)][Column(5)].c = 'x';
+
+        // Test motion to the right.
+
+        let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(0)));
+        cursor = cursor.motion(&mut term, ViMotion::SemanticRight);
+        assert_eq!(cursor.point, Point::new(Line(0), Column(2)));
+
+        let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(2)));
+        cursor = cursor.motion(&mut term, ViMotion::SemanticRight);
+        assert_eq!(cursor.point, Point::new(Line(0), Column(4)));
+
+        // Test motion to the left.
+
+        let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(5)));
+        cursor = cursor.motion(&mut term, ViMotion::SemanticLeft);
+        assert_eq!(cursor.point, Point::new(Line(0), Column(4)));
+
+        let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(4)));
+        cursor = cursor.motion(&mut term, ViMotion::SemanticLeft);
+        assert_eq!(cursor.point, Point::new(Line(0), Column(2)));
+
+        let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(2)));
+        cursor = cursor.motion(&mut term, ViMotion::SemanticLeft);
+        assert_eq!(cursor.point, Point::new(Line(0), Column(0)));
+    }
 }
diff --git a/extra/osx/Alacritty.app/Contents/Info.plist b/extra/osx/Alacritty.app/Contents/Info.plist
index 08bf4270b19..2822a5d04ba 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.14.0</string>
+  <string>0.14.1-rc1</string>
   <key>CFBundleSupportedPlatforms</key>
   <array>
     <string>MacOSX</string>


Test patch: