Repository: raphamorim/rio (language: rust)

Issue:
Support searching
Similar to iTerm2 and Alacritty, can we have Cmd-F / Ctrl-F and be able to search for a string? Is it deemed a desirable feature?

So far I am loving Rio, seems to need a bit more love still.


Discussion / hints:
Hey there @dimitarvp ,

Thanks for the issue. Yes searching is something I wanted to do since 0.0.6 but haven't prioritized it yet. I will move this issue to wishlist.
+1 ❤️ 
It will be a nice function -- currently a blocker for me.
A very simple implementation (mvp) would satisfy most people, later the functionality could be improved ( similar to iterm2 / macOS Terminal). 

![image](https://github.com/user-attachments/assets/c97bfe11-7cf9-484a-b089-64e600b32108)
![image](https://github.com/user-attachments/assets/ebec16bd-77b4-420d-b431-000ad49dedfa)


maybe the mvp could be implemented in vi mode (`/` `search pattern` `ENTER`") and `n` to find the next occurrence or `N` to find the previous.
Since Rio uses [Winit](https://github.com/rust-windowing/winit), it would be great to leverage if it offers some handy helper to perform the behavior suggested in the images above.

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 51f7461fe6..7a7a2f1814 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,9 +11,13 @@
 
 ## Unreleased
 
-- Support writing the config to a custom/default location via `--write-config` (Ref: #605)
+- Search support.
+- New theme properties `search-match-background`, `search-match-foreground`, `search-focused-match-background` and `search-focused-match-foreground`.
+- Fix color automation on tabs for linux.
+- Update tabs UI styles (make it larger and able to show more text when necessary).
+- Support writing the config to a custom/default location via `--write-config` (Ref: #605).
 - Fix scale update on transitioning between screens with different DPI.
-- Support a short variant (`-w`) for `--working-dir` argument
+- Support a short variant (`-w`) for `--working-dir` argument.
 
 ## 0.1.8
 
diff --git a/Cargo.lock b/Cargo.lock
index f2bf777436..88255cf7d4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2515,9 +2515,9 @@ dependencies = [
 
 [[package]]
 name = "regex-automata"
-version = "0.4.6"
+version = "0.4.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea"
+checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
 dependencies = [
  "aho-corasick",
  "memchr",
@@ -2563,6 +2563,7 @@ dependencies = [
  "parking_lot",
  "raw-window-handle",
  "regex",
+ "regex-automata",
  "rio-winit-fork",
  "serde",
  "smallvec",
diff --git a/docs/docs/themes.md b/docs/docs/themes.md
index 8fea8f6ebf..4fd7035583 100644
--- a/docs/docs/themes.md
+++ b/docs/docs/themes.md
@@ -53,13 +53,24 @@ Note: Missing fields will use the default for Rio.
 [colors]
 background = ""
 foreground = ""
+
+# Selection
 selection-background = ""
 selection-foreground = ""
+
+# Navigation
 tabs-active = ""
 tabs-active-highlight = ""
 bar = ""
 cursor = ""
 vi-cursor = ""
+
+# Search
+search-match-background = ""
+search-match-foreground = ""
+search-focused-match-background = ""
+search-focused-match-foreground = ""
+
 # Regular colors
 black = ""
 blue = ""
@@ -70,6 +81,7 @@ red = ""
 tabs = ""
 white = ""
 yellow = ""
+
 # Dim colors
 dim-black = ""
 dim-blue = ""
@@ -80,6 +92,7 @@ dim-magenta = ""
 dim-red = ""
 dim-white = ""
 dim-yellow = ""
+
 # Light colors
 light-black = ""
 light-blue = ""
diff --git a/docs/static/assets/features/demo-bottom-tab.png b/docs/static/assets/features/demo-bottom-tab.png
index 242a7065d9..b583f8ddd3 100644
Binary files a/docs/static/assets/features/demo-bottom-tab.png and b/docs/static/assets/features/demo-bottom-tab.png differ
diff --git a/frontends/rioterm/src/bindings/bindings_winit/mod.rs b/frontends/rioterm/src/bindings/bindings_winit/mod.rs
index 61abf4d051..f3506e67b5 100644
--- a/frontends/rioterm/src/bindings/bindings_winit/mod.rs
+++ b/frontends/rioterm/src/bindings/bindings_winit/mod.rs
@@ -23,6 +23,47 @@ pub enum FontSizeAction {
     Reset,
 }
 
+/// Mouse binding specific actions.
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+pub enum MouseAction {
+    /// Expand the selection to the current mouse cursor position.
+    ExpandSelection,
+}
+
+impl From<MouseAction> for Action {
+    fn from(action: MouseAction) -> Self {
+        Self::Mouse(action)
+    }
+}
+
+/// Search mode specific actions.
+#[allow(clippy::enum_variant_names)]
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+pub enum SearchAction {
+    /// Move the focus to the next search match.
+    SearchFocusNext,
+    /// Move the focus to the previous search match.
+    SearchFocusPrevious,
+    /// Confirm the active search.
+    SearchConfirm,
+    /// Cancel the active search.
+    SearchCancel,
+    /// Reset the search regex.
+    SearchClear,
+    /// Delete the last word in the search regex.
+    SearchDeleteWord,
+    /// Go to the previous regex in the search history.
+    SearchHistoryPrevious,
+    /// Go to the next regex in the search history.
+    SearchHistoryNext,
+}
+
+impl From<SearchAction> for Action {
+    fn from(action: SearchAction) -> Self {
+        Self::Search(action)
+    }
+}
+
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub struct Binding<T> {
     /// Modifier keys required to activate binding.
@@ -118,17 +159,19 @@ bitflags! {
         const APP_KEYPAD          = 0b0000_0010;
         const ALT_SCREEN          = 0b0000_0100;
         const VI                  = 0b0000_1000;
+        const SEARCH              = 0b0001_0000;
         const DISAMBIGUATE_KEYS   = 0b0010_0000;
         const ALL_KEYS_AS_ESC     = 0b0100_0000;
     }
 }
 
 impl BindingMode {
-    pub fn new(mode: &Mode) -> BindingMode {
+    pub fn new(mode: &Mode, search: bool) -> BindingMode {
         let mut binding_mode = BindingMode::empty();
         binding_mode.set(BindingMode::APP_CURSOR, mode.contains(Mode::APP_CURSOR));
         binding_mode.set(BindingMode::APP_KEYPAD, mode.contains(Mode::APP_KEYPAD));
         binding_mode.set(BindingMode::ALT_SCREEN, mode.contains(Mode::ALT_SCREEN));
+        binding_mode.set(BindingMode::SEARCH, search);
         binding_mode.set(
             BindingMode::DISAMBIGUATE_KEYS,
             mode.contains(Mode::KEYBOARD_DISAMBIGUATE_ESC_CODES),
@@ -173,6 +216,22 @@ impl From<String> for Action {
             "paste" => Some(Action::Paste),
             "quit" => Some(Action::Quit),
             "copy" => Some(Action::Copy),
+            "search-forward" => Some(Action::SearchForward),
+            "search-backward" => Some(Action::SearchBackward),
+            "search-confirm" => Some(Action::Search(SearchAction::SearchConfirm)),
+            "search-cancel" => Some(Action::Search(SearchAction::SearchCancel)),
+            "search-clear" => Some(Action::Search(SearchAction::SearchClear)),
+            "search-focus-next" => Some(Action::Search(SearchAction::SearchFocusNext)),
+            "search-focus-previous" => {
+                Some(Action::Search(SearchAction::SearchFocusPrevious))
+            }
+            "search-delete-word" => Some(Action::Search(SearchAction::SearchDeleteWord)),
+            "search-history-next" => {
+                Some(Action::Search(SearchAction::SearchHistoryNext))
+            }
+            "search-history-previous" => {
+                Some(Action::Search(SearchAction::SearchHistoryPrevious))
+            }
             "clearhistory" => Some(Action::ClearHistory),
             "resetfontsize" => Some(Action::ResetFontSize),
             "increasefontsize" => Some(Action::IncreaseFontSize),
@@ -371,15 +430,22 @@ pub enum Action {
     /// Toggle vi mode.
     ToggleViMode,
 
+    // Tab selections
+    SelectTab(usize),
+    SelectLastTab,
+
+    Search(SearchAction),
+    /// Start a forward buffer search.
+    SearchForward,
+
+    /// Start a backward buffer search.
+    SearchBackward,
+
     /// Allow receiving char input.
     ReceiveChar,
 
     /// No action.
     None,
-
-    // Tab selections
-    SelectTab(usize),
-    SelectLastTab,
 }
 
 impl From<&'static str> for Action {
@@ -475,19 +541,6 @@ macro_rules! trigger {
     }};
 }
 
-/// Mouse binding specific actions.
-#[derive(Debug, Copy, Clone, PartialEq, Eq)]
-pub enum MouseAction {
-    /// Expand the selection to the current mouse cursor position.
-    ExpandSelection,
-}
-
-impl From<MouseAction> for Action {
-    fn from(action: MouseAction) -> Self {
-        Self::Mouse(action)
-    }
-}
-
 pub fn default_mouse_bindings() -> Vec<MouseBinding> {
     bindings!(
         MouseBinding;
@@ -594,38 +647,38 @@ pub fn default_key_bindings(
             Key::Named(Home), ModifiersState::SHIFT, +BindingMode::ALT_SCREEN, ~BindingMode::VI; Action::Esc("\x1b[1;2H".into());
             Key::Named(End), ModifiersState::SHIFT, +BindingMode::ALT_SCREEN, ~BindingMode::VI; Action::Esc("\x1b[1;2F".into());
             Key::Named(End),  ~BindingMode::APP_CURSOR, ~BindingMode::VI; Action::Esc("\x1b[F".into());
-            Key::Named(PageUp), ModifiersState::SHIFT, +BindingMode::ALT_SCREEN, ~BindingMode::VI; Action::Esc("\x1b[5;2~".into());
-            Key::Named(PageDown), ModifiersState::SHIFT, +BindingMode::ALT_SCREEN, ~BindingMode::VI; Action::Esc("\x1b[6;2~".into());
-            Key::Named(Home),  ~BindingMode::APP_CURSOR, ~BindingMode::VI; Action::Esc("\x1b[H".into());
-            Key::Named(ArrowUp), ~BindingMode::APP_CURSOR, ~BindingMode::VI; Action::Esc("\x1b[A".into());
-            Key::Named(ArrowDown), ~BindingMode::APP_CURSOR, ~BindingMode::VI; Action::Esc("\x1b[B".into());
-            Key::Named(ArrowRight), ~BindingMode::APP_CURSOR, ~BindingMode::VI; Action::Esc("\x1b[C".into());
-            Key::Named(ArrowLeft),  ~BindingMode::APP_CURSOR, ~BindingMode::VI; Action::Esc("\x1b[D".into());
-            Key::Named(Backspace), ~BindingMode::VI; Action::Esc("\x7f".into());
-            Key::Named(Insert), ~BindingMode::VI; Action::Esc("\x1b[2~".into());
-            Key::Named(Delete), ~BindingMode::VI; Action::Esc("\x1b[3~".into());
-            Key::Named(PageUp), ~BindingMode::VI; Action::Esc("\x1b[5~".into());
-            Key::Named(PageDown), ~BindingMode::VI; Action::Esc("\x1b[6~".into());
-            Key::Named(F1), ~BindingMode::VI; Action::Esc("\x1bOP".into());
-            Key::Named(F2), ~BindingMode::VI; Action::Esc("\x1bOQ".into());
-            Key::Named(F3), ~BindingMode::VI; Action::Esc("\x1bOR".into());
-            Key::Named(F4), ~BindingMode::VI; Action::Esc("\x1bOS".into());
-            Key::Named(F5), ~BindingMode::VI; Action::Esc("\x1b[15~".into());
-            Key::Named(F6), ~BindingMode::VI; Action::Esc("\x1b[17~".into());
-            Key::Named(F7), ~BindingMode::VI; Action::Esc("\x1b[18~".into());
-            Key::Named(F8), ~BindingMode::VI; Action::Esc("\x1b[19~".into());
-            Key::Named(F9), ~BindingMode::VI; Action::Esc("\x1b[20~".into());
-            Key::Named(F10), ~BindingMode::VI; Action::Esc("\x1b[21~".into());
-            Key::Named(F11), ~BindingMode::VI; Action::Esc("\x1b[23~".into());
-            Key::Named(F12), ~BindingMode::VI; Action::Esc("\x1b[24~".into());
-            Key::Named(F13), ~BindingMode::VI; Action::Esc("\x1b[25~".into());
-            Key::Named(F14), ~BindingMode::VI; Action::Esc("\x1b[26~".into());
-            Key::Named(F15), ~BindingMode::VI; Action::Esc("\x1b[28~".into());
-            Key::Named(F16), ~BindingMode::VI; Action::Esc("\x1b[29~".into());
-            Key::Named(F17), ~BindingMode::VI; Action::Esc("\x1b[31~".into());
-            Key::Named(F18), ~BindingMode::VI; Action::Esc("\x1b[32~".into());
-            Key::Named(F19), ~BindingMode::VI; Action::Esc("\x1b[33~".into());
-            Key::Named(F20), ~BindingMode::VI; Action::Esc("\x1b[34~".into());
+            Key::Named(PageUp), ModifiersState::SHIFT, +BindingMode::ALT_SCREEN, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[5;2~".into());
+            Key::Named(PageDown), ModifiersState::SHIFT, +BindingMode::ALT_SCREEN, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[6;2~".into());
+            Key::Named(Home),  ~BindingMode::APP_CURSOR, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[H".into());
+            Key::Named(ArrowUp), ~BindingMode::APP_CURSOR, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[A".into());
+            Key::Named(ArrowDown), ~BindingMode::APP_CURSOR, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[B".into());
+            Key::Named(ArrowRight), ~BindingMode::APP_CURSOR, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[C".into());
+            Key::Named(ArrowLeft),  ~BindingMode::APP_CURSOR, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[D".into());
+            Key::Named(Backspace), ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x7f".into());
+            Key::Named(Insert), ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[2~".into());
+            Key::Named(Delete), ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[3~".into());
+            Key::Named(PageUp), ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[5~".into());
+            Key::Named(PageDown), ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[6~".into());
+            Key::Named(F1), ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1bOP".into());
+            Key::Named(F2), ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1bOQ".into());
+            Key::Named(F3), ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1bOR".into());
+            Key::Named(F4), ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1bOS".into());
+            Key::Named(F5), ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[15~".into());
+            Key::Named(F6), ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[17~".into());
+            Key::Named(F7), ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[18~".into());
+            Key::Named(F8), ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[19~".into());
+            Key::Named(F9), ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[20~".into());
+            Key::Named(F10), ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[21~".into());
+            Key::Named(F11), ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[23~".into());
+            Key::Named(F12), ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[24~".into());
+            Key::Named(F13), ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[25~".into());
+            Key::Named(F14), ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[26~".into());
+            Key::Named(F15), ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[28~".into());
+            Key::Named(F16), ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[29~".into());
+            Key::Named(F17), ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[31~".into());
+            Key::Named(F18), ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[32~".into());
+            Key::Named(F19), ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[33~".into());
+            Key::Named(F20), ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[34~".into());
         ));
 
         //   Code     Modifiers
@@ -733,23 +786,23 @@ pub fn default_key_bindings(
     } else {
         bindings.extend(bindings!(
             KeyBinding;
-            Key::Named(ArrowUp), ~BindingMode::APP_CURSOR, ~BindingMode::VI, ~BindingMode::ALL_KEYS_AS_ESC; Action::Esc("\x1b[A".into());
-            Key::Named(ArrowDown), ~BindingMode::APP_CURSOR, ~BindingMode::VI, ~BindingMode::ALL_KEYS_AS_ESC; Action::Esc("\x1b[B".into());
-            Key::Named(ArrowRight), ~BindingMode::APP_CURSOR, ~BindingMode::VI, ~BindingMode::ALL_KEYS_AS_ESC; Action::Esc("\x1b[C".into());
-            Key::Named(ArrowLeft),  ~BindingMode::APP_CURSOR, ~BindingMode::VI, ~BindingMode::ALL_KEYS_AS_ESC; Action::Esc("\x1b[D".into());
-            Key::Named(Insert),     ~BindingMode::VI, ~BindingMode::ALL_KEYS_AS_ESC, ~BindingMode::DISAMBIGUATE_KEYS; Action::Esc("\x1b[2~".into());
-            Key::Named(Delete),     ~BindingMode::VI, ~BindingMode::ALL_KEYS_AS_ESC, ~BindingMode::DISAMBIGUATE_KEYS; Action::Esc("\x1b[3~".into());
-            Key::Named(PageUp),     ~BindingMode::VI, ~BindingMode::ALL_KEYS_AS_ESC, ~BindingMode::DISAMBIGUATE_KEYS; Action::Esc("\x1b[5~".into());
-            Key::Named(PageDown),   ~BindingMode::VI, ~BindingMode::ALL_KEYS_AS_ESC, ~BindingMode::DISAMBIGUATE_KEYS; Action::Esc("\x1b[6~".into());
-            Key::Named(Backspace),  ~BindingMode::VI, ~BindingMode::ALL_KEYS_AS_ESC; Action::Esc("\x7f".into());
-            Key::Named(Backspace), ModifiersState::ALT,     ~BindingMode::VI, ~BindingMode::ALL_KEYS_AS_ESC; Action::Esc("\x1b\x7f".into());
-            Key::Named(Backspace), ModifiersState::SHIFT,   ~BindingMode::VI, ~BindingMode::ALL_KEYS_AS_ESC; Action::Esc("\x7f".into());
-            Key::Named(F1), ~BindingMode::VI, ~BindingMode::ALL_KEYS_AS_ESC, ~BindingMode::DISAMBIGUATE_KEYS; Action::Esc("\x1bOP".into());
-            Key::Named(F2), ~BindingMode::VI, ~BindingMode::ALL_KEYS_AS_ESC, ~BindingMode::DISAMBIGUATE_KEYS; Action::Esc("\x1bOQ".into());
-            Key::Named(F3), ~BindingMode::VI, ~BindingMode::ALL_KEYS_AS_ESC, ~BindingMode::DISAMBIGUATE_KEYS; Action::Esc("\x1bOR".into());
-            Key::Named(F4), ~BindingMode::VI, ~BindingMode::ALL_KEYS_AS_ESC, ~BindingMode::DISAMBIGUATE_KEYS; Action::Esc("\x1bOS".into());
-            Key::Named(Tab), ModifiersState::SHIFT, ~BindingMode::VI,   ~BindingMode::ALL_KEYS_AS_ESC; Action::Esc("\x1b[Z".into());
-            Key::Named(Tab), ModifiersState::SHIFT | ModifiersState::ALT, ~BindingMode::VI, ~BindingMode::ALL_KEYS_AS_ESC; Action::Esc("\x1b\x1b[Z".into());
+            Key::Named(ArrowUp), ~BindingMode::APP_CURSOR, ~BindingMode::VI, ~BindingMode::SEARCH, ~BindingMode::ALL_KEYS_AS_ESC; Action::Esc("\x1b[A".into());
+            Key::Named(ArrowDown), ~BindingMode::APP_CURSOR, ~BindingMode::VI, ~BindingMode::SEARCH, ~BindingMode::ALL_KEYS_AS_ESC; Action::Esc("\x1b[B".into());
+            Key::Named(ArrowRight), ~BindingMode::APP_CURSOR, ~BindingMode::VI, ~BindingMode::SEARCH, ~BindingMode::ALL_KEYS_AS_ESC; Action::Esc("\x1b[C".into());
+            Key::Named(ArrowLeft),  ~BindingMode::APP_CURSOR, ~BindingMode::VI, ~BindingMode::SEARCH, ~BindingMode::ALL_KEYS_AS_ESC; Action::Esc("\x1b[D".into());
+            Key::Named(Insert),     ~BindingMode::VI, ~BindingMode::SEARCH, ~BindingMode::ALL_KEYS_AS_ESC, ~BindingMode::DISAMBIGUATE_KEYS; Action::Esc("\x1b[2~".into());
+            Key::Named(Delete),     ~BindingMode::VI, ~BindingMode::SEARCH, ~BindingMode::ALL_KEYS_AS_ESC, ~BindingMode::DISAMBIGUATE_KEYS; Action::Esc("\x1b[3~".into());
+            Key::Named(PageUp),     ~BindingMode::VI, ~BindingMode::SEARCH, ~BindingMode::ALL_KEYS_AS_ESC, ~BindingMode::DISAMBIGUATE_KEYS; Action::Esc("\x1b[5~".into());
+            Key::Named(PageDown),   ~BindingMode::VI, ~BindingMode::SEARCH, ~BindingMode::ALL_KEYS_AS_ESC, ~BindingMode::DISAMBIGUATE_KEYS; Action::Esc("\x1b[6~".into());
+            Key::Named(Backspace),  ~BindingMode::VI, ~BindingMode::SEARCH, ~BindingMode::ALL_KEYS_AS_ESC; Action::Esc("\x7f".into());
+            Key::Named(Backspace), ModifiersState::ALT,     ~BindingMode::VI, ~BindingMode::SEARCH, ~BindingMode::ALL_KEYS_AS_ESC; Action::Esc("\x1b\x7f".into());
+            Key::Named(Backspace), ModifiersState::SHIFT,   ~BindingMode::VI, ~BindingMode::SEARCH, ~BindingMode::ALL_KEYS_AS_ESC; Action::Esc("\x7f".into());
+            Key::Named(F1), ~BindingMode::VI, ~BindingMode::SEARCH, ~BindingMode::ALL_KEYS_AS_ESC, ~BindingMode::DISAMBIGUATE_KEYS; Action::Esc("\x1bOP".into());
+            Key::Named(F2), ~BindingMode::VI, ~BindingMode::SEARCH, ~BindingMode::ALL_KEYS_AS_ESC, ~BindingMode::DISAMBIGUATE_KEYS; Action::Esc("\x1bOQ".into());
+            Key::Named(F3), ~BindingMode::VI, ~BindingMode::SEARCH, ~BindingMode::ALL_KEYS_AS_ESC, ~BindingMode::DISAMBIGUATE_KEYS; Action::Esc("\x1bOR".into());
+            Key::Named(F4), ~BindingMode::VI, ~BindingMode::SEARCH, ~BindingMode::ALL_KEYS_AS_ESC, ~BindingMode::DISAMBIGUATE_KEYS; Action::Esc("\x1bOS".into());
+            Key::Named(Tab), ModifiersState::SHIFT, ~BindingMode::VI, ~BindingMode::SEARCH,   ~BindingMode::ALL_KEYS_AS_ESC; Action::Esc("\x1b[Z".into());
+            Key::Named(Tab), ModifiersState::SHIFT | ModifiersState::ALT, ~BindingMode::VI, ~BindingMode::SEARCH, ~BindingMode::ALL_KEYS_AS_ESC; Action::Esc("\x1b\x1b[Z".into());
         ));
     }
 
@@ -958,9 +1011,9 @@ pub fn platform_key_bindings(
         "+", ModifiersState::SUPER; Action::IncreaseFontSize;
         "-", ModifiersState::SUPER; Action::DecreaseFontSize;
         "-", ModifiersState::SUPER; Action::DecreaseFontSize;
-        Key::Named(Insert), ModifiersState::SHIFT, ~BindingMode::VI;
+        Key::Named(Insert), ModifiersState::SHIFT, ~BindingMode::VI, ~BindingMode::SEARCH;
             Action::Esc("\x1b[2;2~".into());
-        "k", ModifiersState::SUPER, ~BindingMode::VI;
+        "k", ModifiersState::SUPER, ~BindingMode::VI, ~BindingMode::SEARCH;
             Action::Esc("\x0c".into());
         "k", ModifiersState::SUPER, ~BindingMode::VI;  Action::ClearHistory;
         "v", ModifiersState::SUPER, ~BindingMode::VI; Action::Paste;
@@ -973,6 +1026,12 @@ pub fn platform_key_bindings(
         "q", ModifiersState::SUPER; Action::Quit;
         "n", ModifiersState::SUPER; Action::WindowCreateNew;
         ",", ModifiersState::SUPER; Action::ConfigEditor;
+        "f", ModifiersState::SUPER, ~BindingMode::SEARCH; Action::SearchForward;
+        "b", ModifiersState::SUPER, ~BindingMode::SEARCH; Action::SearchBackward;
+        Key::Named(Escape), +BindingMode::SEARCH; SearchAction::SearchCancel;
+        Key::Named(Enter), +BindingMode::SEARCH; SearchAction::SearchFocusNext;
+        "c", ModifiersState::CONTROL, +BindingMode::SEARCH; SearchAction::SearchCancel;
+        "u", ModifiersState::CONTROL, +BindingMode::SEARCH; SearchAction::SearchClear;
     );
 
     if use_navigation_key_bindings {
diff --git a/frontends/rioterm/src/context.rs b/frontends/rioterm/src/context.rs
index f93c3d2856..ad2019b565 100644
--- a/frontends/rioterm/src/context.rs
+++ b/frontends/rioterm/src/context.rs
@@ -467,7 +467,7 @@ impl<T: EventListener + Clone + std::marker::Send + 'static> ContextManager<T> {
             return;
         }
 
-        #[cfg(not(target_os = "windows"))]
+        #[cfg(unix)]
         {
             let interval_time = Duration::from_secs(2);
             if self.titles.last_title_update.elapsed() > interval_time {
@@ -479,7 +479,6 @@ impl<T: EventListener + Clone + std::marker::Send + 'static> ContextManager<T> {
                         context.shell_pid,
                     );
 
-                    #[cfg(any(use_wa, target_os = "macos"))]
                     let path = teletypewriter::foreground_process_path(
                         *context.main_fd,
                         context.shell_pid,
@@ -487,22 +486,10 @@ impl<T: EventListener + Clone + std::marker::Send + 'static> ContextManager<T> {
                     .map(|p| p.to_string_lossy().to_string())
                     .unwrap_or_default();
 
-                    #[cfg(not(any(use_wa, target_os = "macos")))]
-                    let path = String::default();
-
-                    #[cfg(not(target_os = "macos"))]
-                    let terminal_title = String::from("");
-
-                    #[cfg(target_os = "macos")]
-                    #[allow(unused)]
-                    let mut terminal_title = String::from("");
-
-                    #[cfg(target_os = "macos")]
-                    {
+                    let terminal_title = {
                         let terminal = context.terminal.lock();
-                        terminal_title = terminal.title.to_string();
-                        drop(terminal);
-                    }
+                        terminal.title.to_string()
+                    };
 
                     if self.config.is_native {
                         let window_title = if terminal_title.is_empty() {
@@ -530,9 +517,9 @@ impl<T: EventListener + Clone + std::marker::Send + 'static> ContextManager<T> {
             }
         }
 
-        #[cfg(target_os = "windows")]
+        #[cfg(not(unix))]
         {
-            if self.titles.last_title_update.elapsed() > Duration::from_secs(5) {
+            if self.titles.last_title_update.elapsed() > Duration::from_secs(2) {
                 self.titles.last_title_update = Instant::now();
                 let mut id = String::from("");
                 for (i, context) in self.contexts.iter_mut().enumerate() {
diff --git a/frontends/rioterm/src/main.rs b/frontends/rioterm/src/main.rs
index 3d83998d70..ef779209dd 100644
--- a/frontends/rioterm/src/main.rs
+++ b/frontends/rioterm/src/main.rs
@@ -26,7 +26,6 @@ mod scheduler;
 mod screen;
 #[cfg(not(use_wa))]
 mod sequencer;
-mod state;
 mod watcher;
 
 use clap::Parser;
diff --git a/frontends/rioterm/src/renderer/mod.rs b/frontends/rioterm/src/renderer/mod.rs
index 97e8ca7a16..33cfd2ef8e 100644
--- a/frontends/rioterm/src/renderer/mod.rs
+++ b/frontends/rioterm/src/renderer/mod.rs
@@ -1,49 +1,738 @@
-use crate::constants;
-use rio_backend::config::navigation::{Navigation, NavigationMode};
+pub mod navigation;
+mod search;
+pub mod utils;
 
-#[inline]
-pub fn padding_top_from_config(
-    navigation: &Navigation,
-    padding_y_top: f32,
-    num_tabs: usize,
-) -> f32 {
-    let default_padding = constants::PADDING_Y + padding_y_top;
+use crate::ansi::CursorShape;
+use crate::crosswords::grid::row::Row;
+use crate::crosswords::pos::{Column, CursorState, Line, Pos};
+use crate::crosswords::square::{Flags, Square};
+use crate::ime::Preedit;
+use crate::screen::hint::HintMatches;
+use crate::selection::SelectionRange;
+use navigation::ScreenNavigation;
+use rio_backend::config::colors::{
+    term::{List, TermColors},
+    AnsiColor, ColorArray, Colors, NamedColor,
+};
+use rio_backend::config::Config;
+use rio_backend::sugarloaf::{
+    Content, ContentBuilder, FragmentStyle, FragmentStyleDecoration, Stretch, Style,
+    SugarCursor, Sugarloaf, UnderlineInfo, UnderlineShape, Weight,
+};
+use std::collections::HashMap;
+use std::ops::RangeInclusive;
+use std::time::{Duration, Instant};
+#[cfg(not(use_wa))]
+use winit::window::Theme;
 
-    if navigation.hide_if_single && num_tabs == 1 {
-        return default_padding;
+use rustc_hash::FxHashMap;
+use unicode_width::UnicodeWidthChar;
+
+struct Cursor {
+    state: CursorState,
+    content: char,
+    content_ref: char,
+}
+
+pub struct Renderer {
+    #[allow(unused)]
+    pub option_as_alt: String,
+    is_ime_enabled: bool,
+    is_vi_mode_enabled: bool,
+    pub is_kitty_keyboard_enabled: bool,
+    pub last_typing: Option<Instant>,
+    pub named_colors: Colors,
+    font_size: f32,
+    pub colors: List,
+    pub navigation: ScreenNavigation,
+    cursor: Cursor,
+    pub selection_range: Option<SelectionRange>,
+    pub config_has_blinking_enabled: bool,
+    term_has_blinking_enabled: bool,
+    pub is_blinking: bool,
+    ignore_selection_fg_color: bool,
+    // Dynamic background keep track of the original bg color and
+    // the same r,g,b with the mutated alpha channel.
+    pub dynamic_background: ([f32; 4], wgpu::Color, bool),
+    hyperlink_range: Option<SelectionRange>,
+    width_cache: FxHashMap<char, f32>,
+    active_search: Option<String>,
+}
+
+impl Renderer {
+    pub fn new(
+        #[cfg(not(use_wa))] config: &Config,
+        #[cfg(use_wa)] config: &Config,
+        #[cfg(not(use_wa))] current_theme: Option<Theme>,
+        #[cfg(use_wa)] appearance: wa::Appearance,
+    ) -> Renderer {
+        let term_colors = TermColors::default();
+        let colors = List::from(&term_colors);
+        let mut named_colors = config.colors;
+
+        #[cfg(not(use_wa))]
+        {
+            if let Some(theme) = current_theme {
+                if let Some(adaptive_colors) = &config.adaptive_colors {
+                    match theme {
+                        Theme::Light => {
+                            named_colors = adaptive_colors.light.unwrap_or(named_colors);
+                        }
+                        Theme::Dark => {
+                            named_colors = adaptive_colors.dark.unwrap_or(named_colors);
+                        }
+                    }
+                }
+            }
+        }
+
+        #[cfg(use_wa)]
+        {
+            if let Some(adaptive_colors) = &config.adaptive_colors {
+                match appearance {
+                    wa::Appearance::Light => {
+                        named_colors = adaptive_colors.light.unwrap_or(named_colors);
+                    }
+                    wa::Appearance::Dark => {
+                        named_colors = adaptive_colors.dark.unwrap_or(named_colors);
+                    }
+                    // TODO
+                    wa::Appearance::LightHighContrast => {}
+                    wa::Appearance::DarkHighContrast => {}
+                }
+            }
+        }
+
+        let mut dynamic_background =
+            (named_colors.background.0, named_colors.background.1, false);
+        if config.window.opacity < 1. {
+            dynamic_background.1.a = config.window.opacity as f64;
+            dynamic_background.2 = true;
+        } else if config.window.background_image.is_some() {
+            dynamic_background.1 = wgpu::Color::TRANSPARENT;
+            dynamic_background.2 = true;
+        }
+
+        let mut color_automation: HashMap<String, HashMap<String, [f32; 4]>> =
+            HashMap::new();
+
+        for rule in &config.navigation.color_automation {
+            color_automation
+                .entry(rule.program.clone())
+                .or_default()
+                .insert(rule.path.clone(), rule.color);
+        }
+
+        Renderer {
+            option_as_alt: config.option_as_alt.to_lowercase(),
+            is_kitty_keyboard_enabled: config.keyboard.use_kitty_keyboard_protocol,
+            is_ime_enabled: false,
+            is_vi_mode_enabled: false,
+            is_blinking: false,
+            last_typing: None,
+            config_has_blinking_enabled: config.blinking_cursor,
+            term_has_blinking_enabled: false,
+            ignore_selection_fg_color: config.ignore_selection_fg_color,
+            colors,
+            navigation: ScreenNavigation::new(
+                config.navigation.clone(),
+                color_automation,
+                config.padding_y,
+            ),
+            font_size: config.fonts.size,
+            selection_range: None,
+            hyperlink_range: None,
+            named_colors,
+            dynamic_background,
+            active_search: None,
+            cursor: Cursor {
+                content: config.cursor,
+                content_ref: config.cursor,
+                state: CursorState::new(config.cursor),
+            },
+            width_cache: FxHashMap::default(),
+        }
+    }
+
+    #[inline]
+    pub fn has_blinking_enabled(&self) -> bool {
+        self.config_has_blinking_enabled && self.term_has_blinking_enabled
+    }
+
+    #[inline]
+    pub fn get_cursor_state_from_ref(&self) -> CursorState {
+        CursorState::new(self.cursor.content_ref)
+    }
+
+    #[inline]
+    pub fn get_cursor_state(&self) -> CursorState {
+        self.cursor.state.clone()
+    }
+
+    #[inline]
+    pub fn set_active_search(&mut self, active_search: Option<String>) {
+        self.active_search = active_search;
+    }
+
+    #[inline]
+    pub fn set_hyperlink_range(&mut self, hyperlink_range: Option<SelectionRange>) {
+        self.hyperlink_range = hyperlink_range;
+    }
+
+    #[inline]
+    pub fn has_hyperlink_range(&self) -> bool {
+        self.hyperlink_range.is_some()
     }
 
-    #[cfg(not(target_os = "macos"))]
-    {
-        if navigation.mode == NavigationMode::TopTab {
-            return constants::PADDING_Y_WITH_TAB_ON_TOP + padding_y_top;
+    #[inline]
+    fn create_style(&mut self, square: &Square) -> (FragmentStyle, char) {
+        let flags = square.flags;
+
+        let mut foreground_color = self.compute_color(&square.fg, flags);
+        let mut background_color = self.compute_bg_color(square);
+
+        let content = if square.c == '\t' || flags.contains(Flags::HIDDEN) {
+            ' '
+        } else {
+            square.c
+        };
+
+        let width = if let Some(w) = self.width_cache.get(&content) {
+            *w
+        } else {
+            let w = square.c.width().unwrap_or(1) as f32;
+            self.width_cache.insert(square.c, w);
+            w
+        };
+
+        let font_attrs = match (
+            flags.contains(Flags::ITALIC),
+            flags.contains(Flags::BOLD_ITALIC),
+            flags.contains(Flags::BOLD),
+        ) {
+            (true, _, _) => (Stretch::NORMAL, Weight::NORMAL, Style::Italic),
+            (_, true, _) => (Stretch::NORMAL, Weight::BOLD, Style::Italic),
+            (_, _, true) => (Stretch::NORMAL, Weight::BOLD, Style::Normal),
+            _ => (Stretch::NORMAL, Weight::NORMAL, Style::Normal),
+        };
+
+        if flags.contains(Flags::INVERSE) {
+            std::mem::swap(&mut background_color, &mut foreground_color);
         }
+
+        let background_color = if self.dynamic_background.2
+            && background_color[0] == self.dynamic_background.0[0]
+            && background_color[1] == self.dynamic_background.0[1]
+            && background_color[2] == self.dynamic_background.0[2]
+        {
+            None
+        } else {
+            Some(background_color)
+        };
+
+        let (decoration, decoration_color) = self.compute_decoration(square, false);
+
+        (
+            FragmentStyle {
+                width,
+                color: foreground_color,
+                background_color,
+                font_attrs,
+                decoration,
+                decoration_color,
+                ..FragmentStyle::default()
+            },
+            content,
+        )
     }
 
-    #[cfg(target_os = "macos")]
-    {
-        if navigation.mode == NavigationMode::NativeTab {
-            return 0.0 + padding_y_top;
+    fn compute_decoration(
+        &self,
+        square: &Square,
+        skip_underline: bool,
+    ) -> (Option<FragmentStyleDecoration>, Option<[f32; 4]>) {
+        let mut decoration = None;
+        let mut decoration_color = None;
+
+        if square.flags.contains(Flags::UNDERLINE) {
+            if !skip_underline {
+                decoration = Some(FragmentStyleDecoration::Underline(UnderlineInfo {
+                    offset: -2.0,
+                    size: 2.0,
+                    is_doubled: false,
+                    shape: UnderlineShape::Regular,
+                }));
+            }
+        } else if square.flags.contains(Flags::STRIKEOUT) {
+            decoration = Some(FragmentStyleDecoration::Strikethrough);
+        } else if square.flags.contains(Flags::DOUBLE_UNDERLINE) {
+            decoration = Some(FragmentStyleDecoration::Underline(UnderlineInfo {
+                offset: -4.0,
+                size: 1.0,
+                is_doubled: true,
+                shape: UnderlineShape::Regular,
+            }));
+        } else if square.flags.contains(Flags::DOTTED_UNDERLINE) {
+            decoration = Some(FragmentStyleDecoration::Underline(UnderlineInfo {
+                offset: -2.0,
+                size: 2.0,
+                is_doubled: false,
+                shape: UnderlineShape::Dotted,
+            }));
+        } else if square.flags.contains(Flags::DASHED_UNDERLINE) {
+            decoration = Some(FragmentStyleDecoration::Underline(UnderlineInfo {
+                offset: -2.0,
+                size: 2.0,
+                is_doubled: false,
+                shape: UnderlineShape::Dashed,
+            }));
+        } else if square.flags.contains(Flags::UNDERCURL) {
+            decoration = Some(FragmentStyleDecoration::Underline(UnderlineInfo {
+                offset: -2.0,
+                size: 1.0,
+                is_doubled: false,
+                shape: UnderlineShape::Curly,
+            }));
         }
+
+        if decoration.is_some() {
+            if let Some(color) = square.underline_color() {
+                decoration_color = Some(self.compute_color(&color, square.flags));
+            }
+        };
+
+        (decoration, decoration_color)
     }
 
-    default_padding
-}
+    #[inline]
+    fn create_line(
+        &mut self,
+        content_builder: &mut ContentBuilder,
+        row: &Row<Square>,
+        has_cursor: bool,
+        line: Line,
+        search_hints: &mut Option<HintMatches>,
+        focused_match: &Option<RangeInclusive<Pos>>,
+    ) {
+        let columns: usize = row.len();
+        let mut content = String::default();
+        let mut last_style = FragmentStyle::default();
+
+        for column in 0..columns {
+            let square = &row.inner[column];
+
+            if square.flags.contains(Flags::WIDE_CHAR_SPACER) {
+                continue;
+            }
+
+            if square.flags.contains(Flags::GRAPHICS) {
+                // &self.create_graphic_sugar(square);
+                continue;
+            }
+
+            let (mut style, square_content) =
+                if has_cursor && column == self.cursor.state.pos.col {
+                    self.create_cursor_style(square)
+                } else {
+                    self.create_style(square)
+                };
+
+            if self.hyperlink_range.is_some()
+                && square.hyperlink().is_some()
+                && self
+                    .hyperlink_range
+                    .unwrap()
+                    .contains(Pos::new(line, Column(column)))
+            {
+                style.decoration =
+                    Some(FragmentStyleDecoration::Underline(UnderlineInfo {
+                        offset: -1.0,
+                        size: -1.0,
+                        is_doubled: false,
+                        shape: UnderlineShape::Regular,
+                    }));
+            } else if self.selection_range.is_some()
+                && self
+                    .selection_range
+                    .unwrap()
+                    .contains(Pos::new(line, Column(column)))
+            {
+                style.color = if self.ignore_selection_fg_color {
+                    self.compute_color(&square.fg, square.flags)
+                } else {
+                    self.named_colors.selection_foreground
+                };
+                style.background_color = Some(self.named_colors.selection_background);
+            } else if search_hints.is_some()
+                && search_hints.as_mut().map_or(false, |search| {
+                    search.advance(Pos::new(line, Column(column)))
+                })
+            {
+                let is_focused = focused_match
+                    .as_ref()
+                    .map_or(false, |fm| fm.contains(&Pos::new(line, Column(column))));
+                if is_focused {
+                    style.color = self.named_colors.search_focused_match_foreground;
+                    style.background_color =
+                        Some(self.named_colors.search_focused_match_background);
+                } else {
+                    style.color = self.named_colors.search_match_foreground;
+                    style.background_color =
+                        Some(self.named_colors.search_match_background);
+                }
+            }
+
+            if last_style != style {
+                if !content.is_empty() {
+                    content_builder.add_text(&content, last_style);
+                }
+
+                content.clear();
+                last_style = style;
+            }
+
+            content.push(square_content);
+
+            // Render last column and break row
+            if column == (columns - 1) {
+                if !content.is_empty() {
+                    content_builder.add_text(&content, last_style);
+                }
+
+                break;
+            }
+        }
+
+        content_builder.finish_line();
+    }
+
+    #[inline]
+    #[cfg(use_wa)]
+    pub fn decrease_foreground_opacity(&mut self, _acc: f32) {
+        // self.foreground_opacity -= acc;
+    }
+
+    #[inline]
+    #[cfg(use_wa)]
+    pub fn increase_foreground_opacity(&mut self, _acc: f32) {
+        // self.foreground_opacity += acc;
+    }
+
+    #[inline]
+    fn compute_color(&self, color: &AnsiColor, flags: Flags) -> ColorArray {
+        match color {
+            AnsiColor::Named(ansi_name) => match (ansi_name, flags) {
+                (NamedColor::Background, _) => self.named_colors.background.0,
+                (NamedColor::Cursor, _) => self.named_colors.cursor,
+
+                (NamedColor::Black, Flags::DIM) => self.named_colors.dim_black,
+                (NamedColor::Black, Flags::BOLD) => self.named_colors.light_black,
+                (NamedColor::Black, _) => self.named_colors.black,
+                (NamedColor::Blue, Flags::DIM) => self.named_colors.dim_blue,
+                (NamedColor::Blue, Flags::BOLD) => self.named_colors.light_blue,
+                (NamedColor::Blue, _) => self.named_colors.blue,
+                (NamedColor::Cyan, Flags::DIM) => self.named_colors.dim_cyan,
+                (NamedColor::Cyan, Flags::BOLD) => self.named_colors.light_cyan,
+                (NamedColor::Cyan, _) => self.named_colors.cyan,
+                (NamedColor::Foreground, _) => self.named_colors.foreground,
+                (NamedColor::Green, Flags::DIM) => self.named_colors.dim_green,
+                (NamedColor::Green, Flags::BOLD) => self.named_colors.light_green,
+                (NamedColor::Green, _) => self.named_colors.green,
+                (NamedColor::Magenta, Flags::DIM) => self.named_colors.dim_magenta,
+                (NamedColor::Magenta, Flags::BOLD) => self.named_colors.light_magenta,
+                (NamedColor::Magenta, _) => self.named_colors.magenta,
+                (NamedColor::Red, Flags::DIM) => self.named_colors.dim_red,
+                (NamedColor::Red, Flags::BOLD) => self.named_colors.light_red,
+                (NamedColor::Red, _) => self.named_colors.red,
+                (NamedColor::White, Flags::DIM) => self.named_colors.dim_white,
+                (NamedColor::White, Flags::BOLD) => self.named_colors.light_white,
+                (NamedColor::White, _) => self.named_colors.white,
+                (NamedColor::Yellow, Flags::DIM) => self.named_colors.dim_yellow,
+                (NamedColor::Yellow, Flags::BOLD) => self.named_colors.light_yellow,
+                (NamedColor::Yellow, _) => self.named_colors.yellow,
+                (NamedColor::LightBlack, _) => self.named_colors.light_black,
+                (NamedColor::LightBlue, _) => self.named_colors.light_blue,
+                (NamedColor::LightCyan, _) => self.named_colors.light_cyan,
+                (NamedColor::LightForeground, _) => self.named_colors.light_foreground,
+                (NamedColor::LightGreen, _) => self.named_colors.light_green,
+                (NamedColor::LightMagenta, _) => self.named_colors.light_magenta,
+                (NamedColor::LightRed, _) => self.named_colors.light_red,
+                (NamedColor::LightWhite, _) => self.named_colors.light_white,
+                (NamedColor::LightYellow, _) => self.named_colors.light_yellow,
+                (NamedColor::DimBlack, _) => self.named_colors.dim_black,
+                (NamedColor::DimBlue, _) => self.named_colors.dim_blue,
+                (NamedColor::DimCyan, _) => self.named_colors.dim_cyan,
+                (NamedColor::DimForeground, _) => self.named_colors.dim_foreground,
+                (NamedColor::DimGreen, _) => self.named_colors.dim_green,
+                (NamedColor::DimMagenta, _) => self.named_colors.dim_magenta,
+                (NamedColor::DimRed, _) => self.named_colors.dim_red,
+                (NamedColor::DimWhite, _) => self.named_colors.dim_white,
+                (NamedColor::DimYellow, _) => self.named_colors.dim_yellow,
+            },
+            AnsiColor::Spec(rgb) => {
+                if !flags.contains(Flags::DIM) {
+                    rgb.to_arr()
+                } else {
+                    rgb.to_arr_with_dim()
+                }
+            }
+            AnsiColor::Indexed(index) => {
+                let index = match (flags & Flags::DIM_BOLD, index) {
+                    (Flags::DIM, 8..=15) => *index as usize - 8,
+                    (Flags::DIM, 0..=7) => {
+                        NamedColor::DimBlack as usize + *index as usize
+                    }
+                    _ => *index as usize,
+                };
+
+                self.colors[index]
+            }
+        }
+    }
+
+    #[inline]
+    fn compute_bg_color(&self, square: &Square) -> ColorArray {
+        match square.bg {
+            AnsiColor::Named(ansi_name) => match (ansi_name, square.flags) {
+                (NamedColor::Background, _) => self.named_colors.background.0,
+                (NamedColor::Cursor, _) => self.named_colors.cursor,
+
+                (NamedColor::Black, Flags::DIM) => self.named_colors.dim_black,
+                (NamedColor::Black, Flags::BOLD) => self.named_colors.light_black,
+                (NamedColor::Black, _) => self.named_colors.black,
+                (NamedColor::Blue, Flags::DIM) => self.named_colors.dim_blue,
+                (NamedColor::Blue, Flags::BOLD) => self.named_colors.light_blue,
+                (NamedColor::Blue, _) => self.named_colors.blue,
+                (NamedColor::Cyan, Flags::DIM) => self.named_colors.dim_cyan,
+                (NamedColor::Cyan, Flags::BOLD) => self.named_colors.light_cyan,
+                (NamedColor::Cyan, _) => self.named_colors.cyan,
+                (NamedColor::Foreground, _) => self.named_colors.foreground,
+                (NamedColor::Green, Flags::DIM) => self.named_colors.dim_green,
+                (NamedColor::Green, Flags::BOLD) => self.named_colors.light_green,
+                (NamedColor::Green, _) => self.named_colors.green,
+                (NamedColor::Magenta, Flags::DIM) => self.named_colors.dim_magenta,
+                (NamedColor::Magenta, Flags::BOLD) => self.named_colors.light_magenta,
+                (NamedColor::Magenta, _) => self.named_colors.magenta,
+                (NamedColor::Red, Flags::DIM) => self.named_colors.dim_red,
+                (NamedColor::Red, Flags::BOLD) => self.named_colors.light_red,
+                (NamedColor::Red, _) => self.named_colors.red,
+                (NamedColor::White, Flags::DIM) => self.named_colors.dim_white,
+                (NamedColor::White, Flags::BOLD) => self.named_colors.light_white,
+                (NamedColor::White, _) => self.named_colors.white,
+                (NamedColor::Yellow, Flags::DIM) => self.named_colors.dim_yellow,
+                (NamedColor::Yellow, Flags::BOLD) => self.named_colors.light_yellow,
+                (NamedColor::Yellow, _) => self.named_colors.yellow,
+                (NamedColor::LightBlack, _) => self.named_colors.light_black,
+                (NamedColor::LightBlue, _) => self.named_colors.light_blue,
+                (NamedColor::LightCyan, _) => self.named_colors.light_cyan,
+                (NamedColor::LightForeground, _) => self.named_colors.light_foreground,
+                (NamedColor::LightGreen, _) => self.named_colors.light_green,
+                (NamedColor::LightMagenta, _) => self.named_colors.light_magenta,
+                (NamedColor::LightRed, _) => self.named_colors.light_red,
+                (NamedColor::LightWhite, _) => self.named_colors.light_white,
+                (NamedColor::LightYellow, _) => self.named_colors.light_yellow,
+                (NamedColor::DimBlack, _) => self.named_colors.dim_black,
+                (NamedColor::DimBlue, _) => self.named_colors.dim_blue,
+                (NamedColor::DimCyan, _) => self.named_colors.dim_cyan,
+                (NamedColor::DimForeground, _) => self.named_colors.dim_foreground,
+                (NamedColor::DimGreen, _) => self.named_colors.dim_green,
+                (NamedColor::DimMagenta, _) => self.named_colors.dim_magenta,
+                (NamedColor::DimRed, _) => self.named_colors.dim_red,
+                (NamedColor::DimWhite, _) => self.named_colors.dim_white,
+                (NamedColor::DimYellow, _) => self.named_colors.dim_yellow,
+            },
+            AnsiColor::Spec(rgb) => rgb.to_arr(),
+            AnsiColor::Indexed(idx) => self.colors[idx as usize],
+        }
+    }
+
+    // #[inline]
+    // #[allow(dead_code)]
+    // fn create_graphic_sugar(&self, square: &Square) -> Sugar {
+    //     let media = &square.graphics().unwrap()[0].texture;
+    //     Sugar {
+    //         media: Some(SugarGraphic {
+    //             id: media.id,
+    //             width: media.width,
+    //             height: media.height,
+    //         }),
+    //         ..Sugar::default()
+    //     }
+    // }
+
+    #[inline]
+    fn create_cursor_style(&self, square: &Square) -> (FragmentStyle, char) {
+        let font_attrs = match (
+            square.flags.contains(Flags::ITALIC),
+            square.flags.contains(Flags::BOLD_ITALIC),
+            square.flags.contains(Flags::BOLD),
+        ) {
+            (true, _, _) => (Stretch::NORMAL, Weight::NORMAL, Style::Italic),
+            (_, true, _) => (Stretch::NORMAL, Weight::BOLD, Style::Italic),
+            (_, _, true) => (Stretch::NORMAL, Weight::BOLD, Style::Normal),
+            _ => (Stretch::NORMAL, Weight::NORMAL, Style::Normal),
+        };
+
+        let mut color = self.compute_color(&square.fg, square.flags);
+        let mut background_color = self.compute_bg_color(square);
+        // If IME is enabled we get the current content to cursor
+        let content = if self.is_ime_enabled {
+            self.cursor.content
+        } else {
+            square.c
+        };
+
+        if square.flags.contains(Flags::INVERSE) {
+            std::mem::swap(&mut background_color, &mut color);
+        }
+
+        // If IME is enabled or is a block cursor, put background color
+        // when cursor is over the character
+        if self.is_ime_enabled || self.cursor.state.content == CursorShape::Block {
+            color = self.named_colors.background.0;
+        }
+
+        let mut style = FragmentStyle {
+            color,
+            background_color: Some(background_color),
+            font_attrs,
+            ..FragmentStyle::default()
+        };
+
+        let cursor_color = if !self.is_vi_mode_enabled {
+            self.named_colors.cursor
+        } else {
+            self.named_colors.vi_cursor
+        };
 
-#[inline]
-pub fn padding_bottom_from_config(
-    navigation: &Navigation,
-    padding_y_bottom: f32,
-    num_tabs: usize,
-) -> f32 {
-    let default_padding = 0.0 + padding_y_bottom;
-    if navigation.hide_if_single && num_tabs == 1 {
-        return default_padding;
+        let mut has_underline_cursor = false;
+
+        match self.cursor.state.content {
+            CursorShape::Underline => {
+                style.decoration =
+                    Some(FragmentStyleDecoration::Underline(UnderlineInfo {
+                        offset: -1.0,
+                        size: -1.0,
+                        is_doubled: false,
+                        shape: UnderlineShape::Regular,
+                    }));
+                style.decoration_color = Some(cursor_color);
+
+                has_underline_cursor = true;
+            }
+            CursorShape::Block => {
+                style.cursor = SugarCursor::Block(cursor_color);
+            }
+            CursorShape::Beam => {
+                style.cursor = SugarCursor::Caret(cursor_color);
+            }
+            CursorShape::Hidden => {}
+        }
+
+        let (decoration, decoration_color) =
+            self.compute_decoration(square, has_underline_cursor);
+        style.decoration = decoration;
+        style.decoration_color = decoration_color;
+
+        (style, content)
     }
 
-    if navigation.mode == NavigationMode::BottomTab {
-        return padding_y_bottom + constants::PADDING_Y_BOTTOM_TABS;
+    #[inline]
+    pub fn set_ime(&mut self, ime_preedit: Option<&Preedit>) {
+        if let Some(preedit) = ime_preedit {
+            if let Some(content) = preedit.text.chars().next() {
+                self.cursor.content = content;
+                self.is_ime_enabled = true;
+                return;
+            }
+        }
+
+        self.is_ime_enabled = false;
+        self.cursor.content = self.cursor.content_ref;
     }
 
-    default_padding
+    #[inline]
+    pub fn set_selection(&mut self, selection_range: Option<SelectionRange>) {
+        self.selection_range = selection_range;
+    }
+
+    #[inline]
+    pub fn set_vi_mode(&mut self, is_vi_mode_enabled: bool) {
+        self.is_vi_mode_enabled = is_vi_mode_enabled;
+    }
+
+    #[inline]
+    #[allow(clippy::too_many_arguments)]
+    pub fn prepare_term(
+        &mut self,
+        rows: &[Row<Square>],
+        cursor: CursorState,
+        sugarloaf: &mut Sugarloaf,
+        context_manager: &crate::context::ContextManager<rio_backend::event::EventProxy>,
+        display_offset: i32,
+        has_blinking_enabled: bool,
+        hints: &mut Option<HintMatches>,
+        focused_match: &Option<RangeInclusive<Pos>>,
+    ) {
+        let layout = sugarloaf.layout();
+        self.cursor.state = cursor;
+        let mut is_cursor_visible = self.cursor.state.is_visible();
+
+        self.font_size = layout.font_size;
+        self.term_has_blinking_enabled = has_blinking_enabled;
+
+        // Only blink cursor if does not contain selection
+        let has_selection = self.selection_range.is_some();
+        if !has_selection && self.has_blinking_enabled() {
+            let mut should_blink = true;
+            if let Some(last_typing_time) = self.last_typing {
+                if last_typing_time.elapsed() < Duration::from_secs(1) {
+                    should_blink = false;
+                }
+            }
+
+            if should_blink {
+                self.is_blinking = !self.is_blinking;
+                is_cursor_visible = self.is_blinking;
+            }
+        }
+
+        let mut content_builder = Content::builder();
+
+        for (i, row) in rows.iter().enumerate() {
+            let has_cursor = is_cursor_visible && self.cursor.state.pos.row == i;
+            self.create_line(
+                &mut content_builder,
+                row,
+                has_cursor,
+                Line((i as i32) - display_offset),
+                hints,
+                focused_match,
+            );
+        }
+
+        sugarloaf.set_content(content_builder.build());
+
+        let mut objects = Vec::with_capacity(30);
+        self.navigation.build_objects(
+            (layout.width, layout.height, layout.dimensions.scale),
+            &self.named_colors,
+            context_manager,
+            self.active_search.is_some(),
+            &mut objects,
+        );
+
+        if let Some(active_search_content) = &self.active_search {
+            search::draw_search_bar(
+                &mut objects,
+                &self.named_colors,
+                (layout.width, layout.height, layout.dimensions.scale),
+                active_search_content,
+            );
+
+            self.active_search = None;
+        }
+
+        sugarloaf.set_objects(objects);
+    }
 }
diff --git a/frontends/rioterm/src/state/navigation.rs b/frontends/rioterm/src/renderer/navigation.rs
similarity index 69%
rename from frontends/rioterm/src/state/navigation.rs
rename to frontends/rioterm/src/renderer/navigation.rs
index 89a9aae2bc..c03d21e585 100644
--- a/frontends/rioterm/src/state/navigation.rs
+++ b/frontends/rioterm/src/renderer/navigation.rs
@@ -1,22 +1,15 @@
 use crate::constants::*;
+use rio_backend::config::colors::Colors;
 use rio_backend::config::navigation::{Navigation, NavigationMode};
 use rio_backend::sugarloaf::{Object, Rect, Text};
 use std::collections::HashMap;
 
-pub struct ScreenNavigationColors {
-    foreground: [f32; 4],
-    bar: [f32; 4],
-    active: [f32; 4],
-    inactive: [f32; 4],
-    active_highlight: [f32; 4],
-}
-
 pub struct ScreenNavigation {
     pub navigation: Navigation,
     pub objects: Vec<Object>,
     keys: String,
     current: usize,
-    colors: ScreenNavigationColors,
+    len: usize,
     width: f32,
     height: f32,
     scale: f32,
@@ -27,27 +20,16 @@ pub struct ScreenNavigation {
 impl ScreenNavigation {
     pub fn new(
         navigation: Navigation,
-        colors: [[f32; 4]; 5],
         color_automation: HashMap<String, HashMap<String, [f32; 4]>>,
         padding_y: [f32; 2],
     ) -> ScreenNavigation {
-        let colors = {
-            ScreenNavigationColors {
-                foreground: colors[0],
-                bar: colors[1],
-                inactive: colors[2],
-                active: colors[3],
-                active_highlight: colors[4],
-            }
-        };
-
         ScreenNavigation {
             navigation,
-            objects: vec![],
+            objects: Vec::with_capacity(26),
             keys: String::from(""),
             color_automation,
             current: 0,
-            colors,
+            len: 0,
             padding_y,
             width: 0.0,
             height: 0.0,
@@ -56,24 +38,24 @@ impl ScreenNavigation {
     }
 
     #[inline]
-    pub fn content(
+    pub fn build_objects(
         &mut self,
-        dimensions: (f32, f32),
-        scale: f32,
-        keys: &str,
-        titles: &HashMap<usize, [String; 3]>,
-        current: usize,
-        len: usize,
+        dimensions: (f32, f32, f32),
+        colors: &Colors,
+        context_manager: &crate::context::ContextManager<rio_backend::event::EventProxy>,
+        is_search_active: bool,
+        objects: &mut Vec<Object>,
     ) {
         let mut has_changes = false;
+        let (width, height, scale) = dimensions;
 
-        if dimensions.0 != self.width {
-            self.width = dimensions.0;
+        if width != self.width {
+            self.width = width;
             has_changes = true;
         }
 
-        if dimensions.1 != self.height {
-            self.height = dimensions.1;
+        if height != self.height {
+            self.height = height;
             has_changes = true;
         }
 
@@ -82,45 +64,78 @@ impl ScreenNavigation {
             has_changes = true;
         }
 
-        if keys != self.keys {
+        // When search is active then BottomTab should not be rendered
+        if is_search_active && self.navigation.mode == NavigationMode::BottomTab {
+            self.objects.clear();
+            self.keys.clear();
+            return;
+        }
+
+        let keys = &context_manager.titles.key;
+        if keys != &self.keys {
             self.keys = keys.to_string();
             has_changes = true;
         }
 
+        let current = context_manager.current_index();
         if current != self.current {
             self.current = current;
             has_changes = true;
         }
 
+        let len = context_manager.len();
+        if len != self.len {
+            self.len = len;
+            has_changes = true;
+        }
+
         if !has_changes {
+            objects.extend(self.objects.clone());
             return;
         }
 
         self.objects.clear();
 
+        let titles = &context_manager.titles.titles;
+
         match self.navigation.mode {
             #[cfg(target_os = "macos")]
             NavigationMode::NativeTab => {}
             NavigationMode::CollapsedTab => {
-                self.collapsed_tab(titles, len, self.navigation.hide_if_single)
+                self.collapsed_tab(titles, colors, len, self.navigation.hide_if_single)
             }
             NavigationMode::TopTab => {
                 let position_y = 0.0;
-                self.tab(titles, len, position_y, self.navigation.hide_if_single);
+                self.tab(
+                    titles,
+                    colors,
+                    len,
+                    position_y,
+                    self.navigation.hide_if_single,
+                );
             }
             NavigationMode::BottomTab => {
                 let position_y = (self.height / self.scale) - PADDING_Y_BOTTOM_TABS;
-                self.tab(titles, len, position_y, self.navigation.hide_if_single);
+                self.tab(
+                    titles,
+                    colors,
+                    len,
+                    position_y,
+                    self.navigation.hide_if_single,
+                );
             }
             // Minimal simply does not do anything
             NavigationMode::Plain => {}
         }
+
+        objects.extend(self.objects.clone());
     }
 
     #[inline]
     pub fn collapsed_tab(
         &mut self,
         titles: &HashMap<usize, [String; 3]>,
+        colors: &Colors,
         len: usize,
         hide_if_single: bool,
     ) {
@@ -131,19 +146,17 @@ impl ScreenNavigation {
         let mut initial_position = (self.width / self.scale) - PADDING_X_COLLAPSED_TABS;
         let position_modifier = 20.;
         for i in (0..len).rev() {
-            let mut color = self.colors.inactive;
+            let mut color = colors.tabs;
             let mut size = INACTIVE_TAB_WIDTH_SIZE;
             if i == self.current {
-                color = self.colors.active;
+                color = colors.tabs_active;
                 size = ACTIVE_TAB_WIDTH_SIZE;
             }
 
-            if let Some(name_idx) = titles.get(&i) {
-                if let Some(color_overwrite) = get_color_overwrite(
-                    &self.color_automation,
-                    &name_idx[0],
-                    &name_idx[2],
-                ) {
+            if let Some(title) = titles.get(&i) {
+                if let Some(color_overwrite) =
+                    get_color_overwrite(&self.color_automation, &title[0], &title[2])
+                {
                     color = *color_overwrite;
                 }
             }
@@ -162,6 +175,7 @@ impl ScreenNavigation {
     pub fn tab(
         &mut self,
         titles: &HashMap<usize, [String; 3]>,
+        colors: &Colors,
         len: usize,
         position_y: f32,
         hide_if_single: bool,
@@ -174,7 +188,7 @@ impl ScreenNavigation {
 
         let renderable = Rect {
             position: [initial_position_x, position_y],
-            color: self.colors.bar,
+            color: colors.bar,
             size: [
                 (self.width + PADDING_Y_BOTTOM_TABS) * self.scale,
                 PADDING_Y_BOTTOM_TABS,
@@ -186,7 +200,7 @@ impl ScreenNavigation {
         let iter = 0..len;
         let mut tabs = Vec::from_iter(iter);
 
-        let max_tab_width = 90.;
+        let max_tab_width = 140.;
         let screen_limit = ((self.width / self.scale) / max_tab_width).floor() as usize;
         if len > screen_limit && self.current > screen_limit {
             tabs = Vec::from_iter(self.current - screen_limit..len);
@@ -194,42 +208,40 @@ impl ScreenNavigation {
 
         let text_pos_mod = 11.;
         for i in tabs {
-            let mut background_color = self.colors.bar;
-            let mut foreground_color = self.colors.active;
+            let mut background_color = colors.bar;
+            let mut foreground_color = colors.tabs_active;
 
             let is_current = i == self.current;
             if is_current {
-                foreground_color = self.colors.foreground;
-                background_color = self.colors.active;
+                foreground_color = colors.foreground;
+                background_color = colors.tabs_active;
             }
 
             let mut name = String::from("tab");
-            if let Some(name_idx) = titles.get(&i) {
-                if !name_idx[1].is_empty() {
-                    name = name_idx[1].to_string();
+            if let Some(title) = titles.get(&i) {
+                if title[1].is_empty() {
+                    name = title[0].to_string();
                 } else {
-                    name = name_idx[0].to_string();
+                    name = format!("{} ({})", title[0], title[1]);
                 }
 
-                if let Some(color_overwrite) = get_color_overwrite(
-                    &self.color_automation,
-                    &name_idx[0],
-                    &name_idx[2],
-                ) {
-                    foreground_color = self.colors.inactive;
+                if let Some(color_overwrite) =
+                    get_color_overwrite(&self.color_automation, &title[0], &title[2])
+                {
+                    foreground_color = colors.tabs;
                     background_color = *color_overwrite;
                 }
             }
 
-            let name_modifier = 80.;
-            if name.len() >= 10 {
-                name = name[0..10].to_string();
+            let name_modifier = 90.;
+            if name.len() >= 14 {
+                name = name[0..14].to_string();
             }
 
             self.objects.push(Object::Rect(Rect {
                 position: [initial_position_x, position_y],
                 color: background_color,
-                size: [180., PADDING_Y_BOTTOM_TABS],
+                size: [250., PADDING_Y_BOTTOM_TABS],
             }));
 
             if is_current {
@@ -242,8 +254,8 @@ impl ScreenNavigation {
 
                 self.objects.push(Object::Rect(Rect {
                     position: [initial_position_x, position],
-                    color: self.colors.active_highlight,
-                    size: [180., PADDING_Y_BOTTOM_TABS / 10.],
+                    color: colors.tabs_active_highlight,
+                    size: [250., PADDING_Y_BOTTOM_TABS / 10.],
                 }));
             }
 
@@ -260,7 +272,7 @@ impl ScreenNavigation {
                 foreground_color,
             )));
 
-            initial_position_x += name_modifier + 10.;
+            initial_position_x += name_modifier + 40.;
         }
     }
 }
@@ -281,7 +293,7 @@ fn get_color_overwrite<'a>(
 mod tests {
     use std::collections::HashMap;
 
-    use crate::state::navigation::get_color_overwrite;
+    use crate::renderer::navigation::get_color_overwrite;
 
     #[test]
     fn test_get_color_overwrite() {
diff --git a/frontends/rioterm/src/renderer/search.rs b/frontends/rioterm/src/renderer/search.rs
new file mode 100644
index 0000000000..cba5a1bdb0
--- /dev/null
+++ b/frontends/rioterm/src/renderer/search.rs
@@ -0,0 +1,40 @@
+use crate::constants::*;
+use rio_backend::config::colors::Colors;
+use rio_backend::sugarloaf::{Object, Rect, Text};
+
+#[inline]
+pub fn draw_search_bar(
+    objects: &mut Vec<Object>,
+    colors: &Colors,
+    dimensions: (f32, f32, f32),
+    content: &String,
+) {
+    let (width, height, scale) = dimensions;
+    let position_y = (height / scale) - PADDING_Y_BOTTOM_TABS;
+
+    objects.push(Object::Rect(Rect {
+        position: [0.0, position_y],
+        color: colors.bar,
+        size: [
+            (width + PADDING_Y_BOTTOM_TABS) * scale,
+            PADDING_Y_BOTTOM_TABS,
+        ],
+    }));
+
+    if content.is_empty() {
+        objects.push(Object::Text(Text::single_line(
+            (4., position_y + 10.),
+            String::from("Search: type something..."),
+            14.,
+            colors.foreground,
+        )));
+        return;
+    }
+
+    objects.push(Object::Text(Text::single_line(
+        (4., position_y + 10.),
+        format!("Search: {}", content),
+        14.,
+        colors.foreground,
+    )));
+}
diff --git a/frontends/rioterm/src/renderer/utils.rs b/frontends/rioterm/src/renderer/utils.rs
new file mode 100644
index 0000000000..4ee3bd6c72
--- /dev/null
+++ b/frontends/rioterm/src/renderer/utils.rs
@@ -0,0 +1,55 @@
+use crate::constants;
+use rio_backend::config::navigation::{Navigation, NavigationMode};
+
+#[inline]
+pub fn padding_top_from_config(
+    navigation: &Navigation,
+    padding_y_top: f32,
+    num_tabs: usize,
+) -> f32 {
+    let default_padding = constants::PADDING_Y + padding_y_top;
+
+    if navigation.hide_if_single && num_tabs == 1 {
+        return default_padding;
+    }
+
+    #[cfg(not(target_os = "macos"))]
+    {
+        if navigation.mode == NavigationMode::TopTab {
+            return constants::PADDING_Y_WITH_TAB_ON_TOP + padding_y_top;
+        }
+    }
+
+    #[cfg(target_os = "macos")]
+    {
+        if navigation.mode == NavigationMode::NativeTab {
+            return 0.0 + padding_y_top;
+        }
+    }
+
+    default_padding
+}
+
+#[inline]
+pub fn padding_bottom_from_config(
+    navigation: &Navigation,
+    padding_y_bottom: f32,
+    num_tabs: usize,
+    is_search_active: bool,
+) -> f32 {
+    let default_padding = 0.0 + padding_y_bottom;
+
+    if is_search_active {
+        return padding_y_bottom + constants::PADDING_Y_BOTTOM_TABS;
+    }
+
+    if navigation.hide_if_single && num_tabs == 1 {
+        return default_padding;
+    }
+
+    if navigation.mode == NavigationMode::BottomTab {
+        return padding_y_bottom + constants::PADDING_Y_BOTTOM_TABS;
+    }
+
+    default_padding
+}
diff --git a/frontends/rioterm/src/screen/hint.rs b/frontends/rioterm/src/screen/hint.rs
new file mode 100644
index 0000000000..6b41aac426
--- /dev/null
+++ b/frontends/rioterm/src/screen/hint.rs
@@ -0,0 +1,80 @@
+use rio_backend::crosswords::pos::{Column, Direction, Line, Pos};
+use rio_backend::crosswords::search::Match;
+use rio_backend::crosswords::search::{RegexIter, RegexSearch};
+use rio_backend::crosswords::Crosswords;
+use std::borrow::Cow;
+use std::ops::Deref;
+
+/// Maximum number of linewraps followed outside of the viewport during search highlighting.
+pub const MAX_SEARCH_LINES: usize = 100;
+
+/// Iterate over all visible regex matches.
+pub fn visible_regex_match_iter<'a, T: rio_backend::event::EventListener>(
+    term: &'a Crosswords<T>,
+    regex: &'a mut RegexSearch,
+) -> impl Iterator<Item = Match> + 'a {
+    let viewport_start = Line(-(term.grid.display_offset() as i32));
+    let viewport_end = viewport_start + term.bottommost_line();
+    let mut start = term.line_search_left(Pos::new(viewport_start, Column(0)));
+    let mut end = term.line_search_right(Pos::new(viewport_end, Column(0)));
+    start.row = start.row.max(viewport_start - MAX_SEARCH_LINES);
+    end.row = end.row.min(viewport_end + MAX_SEARCH_LINES);
+
+    RegexIter::new(start, end, Direction::Right, term, regex)
+        .skip_while(move |rm| rm.end().row < viewport_start)
+        .take_while(move |rm| rm.start().row <= viewport_end)
+}
+
+/// Visible hint match tracking.
+#[derive(Default)]
+pub struct HintMatches<'a> {
+    /// All visible matches.
+    matches: Cow<'a, [Match]>,
+
+    /// Index of the last match checked.
+    index: usize,
+}
+
+impl<'a> HintMatches<'a> {
+    /// Create new renderable matches iterator..
+    fn new(matches: impl Into<Cow<'a, [Match]>>) -> Self {
+        Self {
+            matches: matches.into(),
+            index: 0,
+        }
+    }
+
+    /// Create from regex matches on term visible part.
+    #[inline]
+    pub fn visible_regex_matches<T: rio_backend::event::EventListener>(
+        term: &Crosswords<T>,
+        dfas: &mut RegexSearch,
+    ) -> Self {
+        let matches = visible_regex_match_iter(term, dfas).collect::<Vec<_>>();
+        Self::new(matches)
+    }
+
+    /// Advance the regex tracker to the next point.
+    ///
+    /// This will return `true` if the point passed is part of a regex match.
+    pub fn advance(&mut self, point: Pos) -> bool {
+        while let Some(bounds) = self.get(self.index) {
+            if bounds.start() > &point {
+                break;
+            } else if bounds.end() < &point {
+                self.index += 1;
+            } else {
+                return true;
+            }
+        }
+        false
+    }
+}
+
+impl<'a> Deref for HintMatches<'a> {
+    type Target = [Match];
+
+    fn deref(&self) -> &Self::Target {
+        self.matches.deref()
+    }
+}
diff --git a/frontends/rioterm/src/screen/mod.rs b/frontends/rioterm/src/screen/mod.rs
index d55a0b343d..281d71331b 100644
--- a/frontends/rioterm/src/screen/mod.rs
+++ b/frontends/rioterm/src/screen/mod.rs
@@ -6,10 +6,12 @@
 // were retired from https://github.com/alacritty/alacritty/blob/c39c3c97f1a1213418c3629cc59a1d46e34070e0/alacritty/src/input.rs
 // which is licensed under Apache 2.0 license.
 
+pub mod hint;
 pub mod touch;
 
 use crate::bindings::{
-    Action as Act, BindingKey, BindingMode, FontSizeAction, MouseBinding, ViAction,
+    Action as Act, BindingKey, BindingMode, FontSizeAction, MouseBinding, SearchAction,
+    ViAction,
 };
 #[cfg(target_os = "macos")]
 use crate::constants::{DEADZONE_END_Y, DEADZONE_START_X, DEADZONE_START_Y};
@@ -23,9 +25,12 @@ use crate::crosswords::{
 };
 use crate::ime::Ime;
 use crate::mouse::{calculate_mouse_position, Mouse};
-use crate::renderer::{padding_bottom_from_config, padding_top_from_config};
+use crate::renderer::{
+    utils::{padding_bottom_from_config, padding_top_from_config},
+    Renderer,
+};
+use crate::screen::hint::HintMatches;
 use crate::selection::{Selection, SelectionType};
-use crate::state;
 use core::fmt::Debug;
 use raw_window_handle::{RawDisplayHandle, RawWindowHandle};
 use rio_backend::clipboard::{Clipboard, ClipboardType};
@@ -33,12 +38,13 @@ use rio_backend::config::{
     colors::term::List,
     renderer::{Backend as RendererBackend, Performance as RendererPerformance},
 };
-use rio_backend::event::{ClickState, EventProxy};
+use rio_backend::crosswords::pos::{Boundary, Direction, Line};
+use rio_backend::crosswords::search::RegexSearch;
+use rio_backend::event::{ClickState, EventProxy, SearchState};
 use rio_backend::sugarloaf::{
     layout::SugarloafLayout, Sugarloaf, SugarloafErrors, SugarloafRenderer,
     SugarloafWindow, SugarloafWindowSize,
 };
-use state::State;
 use std::borrow::Cow;
 use std::cmp::{max, min};
 use std::error::Error;
@@ -58,6 +64,12 @@ const MIN_SELECTION_SCROLLING_HEIGHT: f32 = 5.;
 /// Number of pixels for increasing the selection scrolling speed factor by one.
 const SELECTION_SCROLLING_STEP: f32 = 10.;
 
+/// Maximum number of lines for the blocking search while still typing the search regex.
+const MAX_SEARCH_WHILE_TYPING: Option<usize> = Some(1000);
+
+/// Maximum number of search terms stored in the history.
+const MAX_SEARCH_HISTORY_SIZE: usize = 255;
+
 pub struct Screen<'screen> {
     bindings: crate::bindings::KeyBindings,
     mouse_bindings: Vec<MouseBinding>,
@@ -65,8 +77,9 @@ pub struct Screen<'screen> {
     pub modifiers: Modifiers,
     pub mouse: Mouse,
     pub touchpurpose: TouchPurpose,
+    pub search_state: SearchState,
     pub ime: Ime,
-    pub state: State,
+    pub renderer: Renderer,
     pub sugarloaf: Sugarloaf<'screen>,
     pub context_manager: context::ContextManager<EventProxy>,
 }
@@ -98,7 +111,7 @@ impl Screen<'_> {
         let padding_y_top =
             padding_top_from_config(&config.navigation, config.padding_y[0], 1);
         let padding_y_bottom =
-            padding_bottom_from_config(&config.navigation, config.padding_y[1], 1);
+            padding_bottom_from_config(&config.navigation, config.padding_y[1], 1, false);
 
         let sugarloaf_layout = SugarloafLayout::new(
             size.width as f32,
@@ -162,7 +175,7 @@ impl Screen<'_> {
             }
         };
 
-        let state = State::new(config, theme);
+        let renderer = Renderer::new(config, theme);
 
         // let clipboard = unsafe { Clipboard::new(raw_display_handle.into()) };
         let clipboard = unsafe { Clipboard::new(raw_display_handle) };
@@ -197,7 +210,7 @@ impl Screen<'_> {
                 && config.navigation.color_automation.is_empty()),
         };
         let context_manager = context::ContextManager::start(
-            (&state.get_cursor_state(), config.blinking_cursor),
+            (&renderer.get_cursor_state(), config.blinking_cursor),
             event_proxy,
             window_id,
             0,
@@ -206,13 +219,14 @@ impl Screen<'_> {
             sugarloaf_errors,
         )?;
 
-        sugarloaf.set_background_color(state.dynamic_background.1);
+        sugarloaf.set_background_color(renderer.dynamic_background.1);
         if let Some(image) = &config.window.background_image {
             sugarloaf.set_background_image(image);
         }
         sugarloaf.render();
 
         Ok(Screen {
+            search_state: SearchState::default(),
             mouse_bindings: crate::bindings::default_mouse_bindings(),
             modifiers: Modifiers::default(),
             context_manager,
@@ -220,7 +234,7 @@ impl Screen<'_> {
             sugarloaf,
             mouse: Mouse::new(config.scroll.multiplier, config.scroll.divider),
             touchpurpose: TouchPurpose::default(),
-            state,
+            renderer,
             bindings,
             clipboard,
         })
@@ -241,6 +255,11 @@ impl Screen<'_> {
         self.modifiers = modifiers;
     }
 
+    #[inline]
+    pub fn search_active(&self) -> bool {
+        self.search_state.history_index.is_some()
+    }
+
     #[inline]
     pub fn reset_mouse(&mut self) {
         self.mouse.accumulated_scroll = crate::mouse::AccumulatedScroll::default();
@@ -294,8 +313,12 @@ impl Screen<'_> {
         let num_tabs = self.ctx().len();
         let padding_y_top =
             padding_top_from_config(&config.navigation, config.padding_y[0], num_tabs);
-        let padding_y_bottom =
-            padding_bottom_from_config(&config.navigation, config.padding_y[1], num_tabs);
+        let padding_y_bottom = padding_bottom_from_config(
+            &config.navigation,
+            config.padding_y[1],
+            num_tabs,
+            self.search_active(),
+        );
 
         self.sugarloaf.update_font(font_library);
         self.sugarloaf.layout_mut().recalculate(
@@ -307,11 +330,11 @@ impl Screen<'_> {
         );
 
         self.sugarloaf.layout_mut().update();
-        self.state = State::new(config, current_theme);
+        self.renderer = Renderer::new(config, current_theme);
 
         for context in self.ctx().contexts() {
             let mut terminal = context.terminal.lock();
-            let cursor = self.state.get_cursor_state_from_ref().content;
+            let cursor = self.renderer.get_cursor_state_from_ref().content;
             terminal.cursor_shape = cursor;
             terminal.default_cursor_shape = cursor;
             terminal.blinking_cursor = config.blinking_cursor;
@@ -321,13 +344,12 @@ impl Screen<'_> {
             .set_multiplier_and_divider(config.scroll.multiplier, config.scroll.divider);
 
         self.sugarloaf
-            .set_background_color(self.state.dynamic_background.1);
+            .set_background_color(self.renderer.dynamic_background.1);
         if let Some(image) = &config.window.background_image {
             self.sugarloaf.set_background_image(image);
         }
 
-        self.update_content();
-        self.render();
+        self.demand_render();
         self.resize_all_contexts();
     }
 
@@ -341,8 +363,7 @@ impl Screen<'_> {
 
         self.sugarloaf.update_font_size(action);
 
-        self.update_content();
-        self.render();
+        self.demand_render();
         self.resize_all_contexts();
     }
 
@@ -365,8 +386,7 @@ impl Screen<'_> {
         self.render();
         self.resize_all_contexts();
 
-        self.update_content();
-        self.render();
+        self.demand_render();
 
         self
     }
@@ -403,7 +423,7 @@ impl Screen<'_> {
 
     #[inline]
     pub fn scroll_bottom_when_cursor_not_visible(&mut self) {
-        let mut terminal = self.ctx_mut().current().terminal.lock();
+        let mut terminal = self.ctx_mut().current_mut().terminal.lock();
         if terminal.display_offset() != 0 {
             terminal.scroll_display(Scroll::Bottom);
         }
@@ -446,7 +466,7 @@ impl Screen<'_> {
         // 1. In case there is a key released event and Rio is not using kitty keyboard protocol
         // then should return drop the key processing
         // 2. In case IME has preedit then also should drop the key processing
-        if !self.state.is_kitty_keyboard_enabled && key.state == ElementState::Released
+        if !self.renderer.is_kitty_keyboard_enabled && key.state == ElementState::Released
             || self.ime.preedit().is_some()
         {
             return;
@@ -455,7 +475,8 @@ impl Screen<'_> {
         let mode = self.get_mode();
         let mods = self.modifiers.state();
 
-        if self.state.is_kitty_keyboard_enabled && key.state == ElementState::Released {
+        if self.renderer.is_kitty_keyboard_enabled && key.state == ElementState::Released
+        {
             if mode.contains(Mode::KEYBOARD_REPORT_EVENT_TYPES)
                 && !mode.contains(Mode::VI)
             {
@@ -480,7 +501,9 @@ impl Screen<'_> {
             return;
         }
 
-        let binding_mode = BindingMode::new(&mode);
+        let search_active = self.search_active();
+
+        let binding_mode = BindingMode::new(&mode, search_active);
         let mut ignore_chars = None;
 
         for i in 0..self.bindings.len() {
@@ -525,12 +548,14 @@ impl Screen<'_> {
                     Act::Run(program) => self.exec(program.program(), program.args()),
                     Act::Esc(s) => {
                         let current_context = self.context_manager.current_mut();
-                        self.state.set_selection(None);
+                        self.renderer.set_selection(None);
                         let mut terminal = current_context.terminal.lock();
                         terminal.selection.take();
                         terminal.scroll_display(Scroll::Bottom);
                         drop(terminal);
-                        current_context.messenger.send_bytes(s.clone().into_bytes());
+                        current_context
+                            .messenger
+                            .send_bytes(s.to_owned().into_bytes());
                     }
                     Act::Paste => {
                         let content = self.clipboard.get(ClipboardType::Clipboard);
@@ -546,13 +571,52 @@ impl Screen<'_> {
                     Act::Copy => {
                         self.copy_selection(ClipboardType::Clipboard);
                     }
+                    Act::SearchForward => {
+                        self.start_search(Direction::Right);
+                        self.resize_top_or_bottom_line(self.ctx().len());
+                        self.demand_render();
+                    }
+                    Act::SearchBackward => {
+                        self.start_search(Direction::Left);
+                        self.resize_top_or_bottom_line(self.ctx().len());
+                        self.demand_render();
+                    }
+                    Act::Search(SearchAction::SearchConfirm) => {
+                        self.confirm_search();
+                        self.resize_top_or_bottom_line(self.ctx().len());
+                        self.demand_render();
+                    }
+                    Act::Search(SearchAction::SearchCancel) => {
+                        self.cancel_search();
+                        self.resize_top_or_bottom_line(self.ctx().len());
+                        self.demand_render();
+                    }
+                    Act::Search(SearchAction::SearchClear) => {
+                        let direction = self.search_state.direction;
+                        self.cancel_search();
+                        self.start_search(direction);
+                        self.resize_top_or_bottom_line(self.ctx().len());
+                        self.demand_render();
+                    }
+                    Act::Search(SearchAction::SearchFocusNext) => {
+                        self.advance_search_origin(self.search_state.direction);
+                        self.resize_top_or_bottom_line(self.ctx().len());
+                        self.demand_render();
+                    }
+                    Act::Search(SearchAction::SearchFocusPrevious) => {
+                        let direction = self.search_state.direction.opposite();
+                        self.advance_search_origin(direction);
+                        self.resize_top_or_bottom_line(self.ctx().len());
+                        self.demand_render();
+                    }
                     Act::ToggleViMode => {
                         let mut terminal =
                             self.context_manager.current_mut().terminal.lock();
                         terminal.toggle_vi_mode();
                         let has_vi_mode_enabled = terminal.mode().contains(Mode::VI);
                         drop(terminal);
-                        self.state.set_vi_mode(has_vi_mode_enabled);
+                        self.renderer.set_vi_mode(has_vi_mode_enabled);
+                        self.demand_render();
                     }
                     Act::ViMotion(motion) => {
                         let mut terminal =
@@ -562,11 +626,10 @@ impl Screen<'_> {
                         }
 
                         if let Some(selection) = &terminal.selection {
-                            self.state.set_selection(selection.to_range(&terminal));
+                            self.renderer.set_selection(selection.to_range(&terminal));
                         };
                         drop(terminal);
-                        self.update_content();
-                        self.render();
+                        self.demand_render();
                     }
                     Act::Vi(ViAction::CenterAroundViCursor) => {
                         let mut terminal =
@@ -582,15 +645,19 @@ impl Screen<'_> {
                     }
                     Act::Vi(ViAction::ToggleNormalSelection) => {
                         self.toggle_selection(SelectionType::Simple, Side::Left);
+                        self.demand_render();
                     }
                     Act::Vi(ViAction::ToggleLineSelection) => {
                         self.toggle_selection(SelectionType::Lines, Side::Left);
+                        self.demand_render();
                     }
                     Act::Vi(ViAction::ToggleBlockSelection) => {
                         self.toggle_selection(SelectionType::Block, Side::Left);
+                        self.demand_render();
                     }
                     Act::Vi(ViAction::ToggleSemanticSelection) => {
                         self.toggle_selection(SelectionType::Semantic, Side::Left);
+                        self.demand_render();
                     }
                     Act::ConfigEditor => {
                         self.context_manager.switch_to_settings();
@@ -606,41 +673,14 @@ impl Screen<'_> {
                             redirect,
                             layout,
                             (
-                                &self.state.get_cursor_state_from_ref(),
-                                self.state.config_has_blinking_enabled,
+                                &self.renderer.get_cursor_state_from_ref(),
+                                self.renderer.config_has_blinking_enabled,
                             ),
                         );
 
-                        let previous_margin = layout.margin;
                         let num_tabs = self.ctx().len();
-                        let padding_y_top = padding_top_from_config(
-                            &self.state.navigation.navigation,
-                            self.state.navigation.padding_y[0],
-                            num_tabs,
-                        );
-                        let padding_y_bottom = padding_bottom_from_config(
-                            &self.state.navigation.navigation,
-                            self.state.navigation.padding_y[1],
-                            num_tabs,
-                        );
-
-                        if previous_margin.top_y != padding_y_top
-                            || previous_margin.bottom_y != padding_y_bottom
-                        {
-                            let layout = self.sugarloaf.layout();
-                            self.sugarloaf.layout_mut().recalculate(
-                                layout.font_size,
-                                layout.line_height,
-                                layout.margin.x,
-                                padding_y_top,
-                                padding_y_bottom,
-                            );
-                            self.sugarloaf.layout_mut().update();
-                            self.resize_all_contexts();
-                        }
-
-                        self.update_content();
-                        self.render();
+                        self.resize_top_or_bottom_line(num_tabs);
+                        self.demand_render();
                     }
                     Act::TabCloseCurrent => {
                         self.clear_selection();
@@ -658,37 +698,9 @@ impl Screen<'_> {
                             return;
                         }
 
-                        let layout = self.sugarloaf.layout();
-                        let previous_margin = layout.margin;
                         let num_tabs = self.ctx().len().wrapping_sub(1);
-                        let padding_y_top = padding_top_from_config(
-                            &self.state.navigation.navigation,
-                            self.state.navigation.padding_y[0],
-                            num_tabs,
-                        );
-                        let padding_y_bottom = padding_bottom_from_config(
-                            &self.state.navigation.navigation,
-                            self.state.navigation.padding_y[1],
-                            num_tabs,
-                        );
-
-                        if previous_margin.top_y != padding_y_top
-                            || previous_margin.bottom_y != padding_y_bottom
-                        {
-                            let layout = self.sugarloaf.layout();
-                            self.sugarloaf.layout_mut().recalculate(
-                                layout.font_size,
-                                layout.line_height,
-                                layout.margin.x,
-                                padding_y_top,
-                                padding_y_bottom,
-                            );
-                            self.sugarloaf.layout_mut().update();
-                            self.resize_all_contexts();
-                        }
-
-                        self.update_content();
-                        self.render();
+                        self.resize_top_or_bottom_line(num_tabs);
+                        self.demand_render();
                     }
                     Act::Quit => {
                         self.context_manager.quit();
@@ -711,6 +723,7 @@ impl Screen<'_> {
                             terminal.vi_mode_cursor.scroll(&terminal, scroll_lines);
                         terminal.scroll_display(Scroll::PageUp);
                         drop(terminal);
+                        self.demand_render();
                     }
                     Act::ScrollPageDown => {
                         // Move vi mode cursor.
@@ -723,6 +736,7 @@ impl Screen<'_> {
 
                         terminal.scroll_display(Scroll::PageDown);
                         drop(terminal);
+                        self.demand_render();
                     }
                     Act::ScrollHalfPageUp => {
                         // Move vi mode cursor.
@@ -735,6 +749,7 @@ impl Screen<'_> {
 
                         terminal.scroll_display(Scroll::Delta(scroll_lines));
                         drop(terminal);
+                        self.demand_render();
                     }
                     Act::ScrollHalfPageDown => {
                         // Move vi mode cursor.
@@ -747,6 +762,7 @@ impl Screen<'_> {
 
                         terminal.scroll_display(Scroll::Delta(scroll_lines));
                         drop(terminal);
+                        self.demand_render();
                     }
                     Act::ScrollToTop => {
                         let mut terminal =
@@ -757,6 +773,7 @@ impl Screen<'_> {
                         terminal.vi_mode_cursor.pos.row = topmost_line;
                         terminal.vi_motion(ViMotion::FirstOccupied);
                         drop(terminal);
+                        self.demand_render();
                     }
                     Act::ScrollToBottom => {
                         let mut terminal =
@@ -770,20 +787,21 @@ impl Screen<'_> {
                         terminal.vi_motion(ViMotion::FirstOccupied);
                         terminal.vi_motion(ViMotion::FirstOccupied);
                         drop(terminal);
+                        self.demand_render();
                     }
                     Act::Scroll(delta) => {
                         let mut terminal =
                             self.context_manager.current_mut().terminal.lock();
                         terminal.scroll_display(Scroll::Delta(*delta));
                         drop(terminal);
+                        self.demand_render();
                     }
                     Act::ClearHistory => {
                         let mut terminal =
                             self.context_manager.current_mut().terminal.lock();
                         terminal.clear_saved_history();
                         drop(terminal);
-                        self.update_content();
-                        self.render();
+                        self.demand_render();
                     }
                     Act::ToggleFullscreen => self.context_manager.toggle_full_screen(),
                     Act::Minimize => {
@@ -798,25 +816,21 @@ impl Screen<'_> {
                     }
                     Act::SelectTab(tab_index) => {
                         self.context_manager.select_tab(*tab_index);
-                        self.update_content();
-                        self.render();
+                        self.demand_render();
                     }
                     Act::SelectLastTab => {
                         self.context_manager.select_last_tab();
-                        self.update_content();
-                        self.render();
+                        self.demand_render();
                     }
                     Act::SelectNextTab => {
                         self.clear_selection();
                         self.context_manager.switch_to_next();
-                        self.update_content();
-                        self.render();
+                        self.demand_render();
                     }
                     Act::SelectPrevTab => {
                         self.clear_selection();
                         self.context_manager.switch_to_prev();
-                        self.update_content();
-                        self.render();
+                        self.demand_render();
                     }
                     Act::ReceiveChar | Act::None => (),
                     _ => (),
@@ -829,10 +843,18 @@ impl Screen<'_> {
             return;
         }
 
-        self.sugarloaf.mark_dirty();
         let text = key.text_with_all_modifiers().unwrap_or_default();
 
-        let bytes = if !self.state.is_kitty_keyboard_enabled {
+        if self.search_active() {
+            for character in text.chars() {
+                self.search_input(character);
+            }
+
+            self.demand_render();
+            return;
+        }
+
+        let bytes = if !self.renderer.is_kitty_keyboard_enabled {
             // If text is empty then leave without input bytes
             if text.is_empty() {
                 return;
@@ -878,6 +900,7 @@ impl Screen<'_> {
         };
 
         if !bytes.is_empty() {
+            self.sugarloaf.mark_dirty();
             self.scroll_bottom_when_cursor_not_visible();
             self.clear_selection();
 
@@ -888,7 +911,7 @@ impl Screen<'_> {
     #[inline]
     pub fn process_mouse_bindings(&mut self, button: MouseButton) {
         let mode = self.get_mode();
-        let binding_mode = BindingMode::new(&mode);
+        let binding_mode = BindingMode::new(&mode, self.search_active());
         let mouse_mode = self.mouse_mode();
         let mods = self.modifiers.state();
 
@@ -909,6 +932,90 @@ impl Screen<'_> {
         }
     }
 
+    fn resize_top_or_bottom_line(&mut self, num_tabs: usize) {
+        let layout = self.sugarloaf.layout();
+        let previous_margin = layout.margin;
+        let padding_y_top = padding_top_from_config(
+            &self.renderer.navigation.navigation,
+            self.renderer.navigation.padding_y[0],
+            num_tabs,
+        );
+        let padding_y_bottom = padding_bottom_from_config(
+            &self.renderer.navigation.navigation,
+            self.renderer.navigation.padding_y[1],
+            num_tabs,
+            self.search_active(),
+        );
+
+        if previous_margin.top_y != padding_y_top
+            || previous_margin.bottom_y != padding_y_bottom
+        {
+            let layout = self.sugarloaf.layout();
+            self.sugarloaf.layout_mut().recalculate(
+                layout.font_size,
+                layout.line_height,
+                layout.margin.x,
+                padding_y_top,
+                padding_y_bottom,
+            );
+            self.sugarloaf.layout_mut().update();
+            self.resize_all_contexts();
+        }
+    }
+
+    #[inline]
+    fn advance_search_origin(&mut self, direction: Direction) {
+        // Use focused match as new search origin if available.
+        if let Some(focused_match) = &self.search_state.focused_match {
+            let mut terminal = self.context_manager.current_mut().terminal.lock();
+            let new_origin = match direction {
+                Direction::Right => {
+                    focused_match.end().add(&*terminal, Boundary::None, 1)
+                }
+                Direction::Left => {
+                    focused_match.start().sub(&*terminal, Boundary::None, 1)
+                }
+            };
+
+            terminal.scroll_to_pos(new_origin);
+
+            self.search_state.display_offset_delta = 0;
+            self.search_state.origin = new_origin;
+        }
+
+        // Search for the next match using the supplied direction.
+        let search_direction =
+            std::mem::replace(&mut self.search_state.direction, direction);
+        self.goto_match(None);
+        self.search_state.direction = search_direction;
+
+        // If we found a match, we set the search origin right in front of it to make sure that
+        // after modifications to the regex the search is started without moving the focused match
+        // around.
+        let focused_match = match &self.search_state.focused_match {
+            Some(focused_match) => focused_match,
+            None => return,
+        };
+
+        // Set new origin to the left/right of the match, depending on search direction.
+        let new_origin = match self.search_state.direction {
+            Direction::Right => *focused_match.start(),
+            Direction::Left => *focused_match.end(),
+        };
+
+        let mut terminal = self.context_manager.current_mut().terminal.lock();
+
+        // Store the search origin with display offset by checking how far we need to scroll to it.
+        let old_display_offset = terminal.display_offset() as i32;
+        terminal.scroll_to_pos(new_origin);
+        let new_display_offset = terminal.display_offset() as i32;
+        self.search_state.display_offset_delta = new_display_offset - old_display_offset;
+
+        // Store origin and scroll back to the match.
+        terminal.scroll_display(Scroll::Delta(-self.search_state.display_offset_delta));
+        self.search_state.origin = new_origin;
+    }
+
     /// Whether we should send `ESC` due to `Alt` being pressed.
     #[cfg(not(target_os = "macos"))]
     #[inline]
@@ -920,10 +1027,10 @@ impl Screen<'_> {
     #[inline]
     fn alt_send_esc(&mut self) -> bool {
         self.modifiers.state().alt_key()
-            && (self.state.option_as_alt == *"both"
-                || (self.state.option_as_alt == *"left"
+            && (self.renderer.option_as_alt == *"both"
+                || (self.renderer.option_as_alt == *"left"
                     && self.modifiers.lalt_state() == ModifiersKeyState::Pressed)
-                || (self.state.option_as_alt == *"right"
+                || (self.renderer.option_as_alt == *"right"
                     && self.modifiers.ralt_state() == ModifiersKeyState::Pressed))
     }
 
@@ -957,7 +1064,7 @@ impl Screen<'_> {
         let mut terminal = self.ctx().current().terminal.lock();
         terminal.selection.take();
         drop(terminal);
-        self.state.set_selection(None);
+        self.renderer.set_selection(None);
     }
 
     #[inline]
@@ -965,7 +1072,7 @@ impl Screen<'_> {
         self.copy_selection(ClipboardType::Selection);
         let mut terminal = self.context_manager.current().terminal.lock();
         let selection = Selection::new(ty, point, side);
-        self.state.set_selection(selection.to_range(&terminal));
+        self.renderer.set_selection(selection.to_range(&terminal));
         terminal.selection = Some(selection);
         drop(terminal);
     }
@@ -1000,7 +1107,7 @@ impl Screen<'_> {
         };
 
         selection.include_all();
-        self.state.set_selection(selection.to_range(&terminal));
+        self.renderer.set_selection(selection.to_range(&terminal));
         terminal.selection = Some(selection);
         drop(terminal);
     }
@@ -1025,7 +1132,7 @@ impl Screen<'_> {
             selection.include_all();
         }
 
-        self.state.set_selection(selection.to_range(&terminal));
+        self.renderer.set_selection(selection.to_range(&terminal));
         terminal.selection = Some(selection);
         drop(terminal);
     }
@@ -1049,11 +1156,11 @@ impl Screen<'_> {
         drop(terminal);
 
         if let Some(hyperlink_range) = search_result {
-            self.state.set_hyperlink_range(Some(hyperlink_range));
+            self.renderer.set_hyperlink_range(Some(hyperlink_range));
             return true;
         }
 
-        self.state.set_hyperlink_range(None);
+        self.renderer.set_hyperlink_range(None);
         false
     }
 
@@ -1065,7 +1172,7 @@ impl Screen<'_> {
         #[cfg(not(target_os = "macos"))]
         let is_hyperlink_key_active = self.modifiers.state().alt_key();
 
-        if !is_hyperlink_key_active || !self.state.has_hyperlink_range() {
+        if !is_hyperlink_key_active || !self.renderer.has_hyperlink_range() {
             return false;
         }
 
@@ -1183,7 +1290,7 @@ impl Screen<'_> {
 
     #[inline]
     pub fn selection_is_empty(&self) -> bool {
-        self.state.selection_range.is_none()
+        self.renderer.selection_range.is_none()
     }
 
     #[inline]
@@ -1219,97 +1326,252 @@ impl Screen<'_> {
     }
 
     #[inline]
-    pub fn paste(&mut self, text: &str, bracketed: bool) {
-        if bracketed && self.get_mode().contains(Mode::BRACKETED_PASTE) {
-            self.ctx_mut()
-                .current_mut()
-                .messenger
-                .send_bytes(b"\x1b[200~"[..].to_vec());
+    fn start_search(&mut self, direction: Direction) {
+        // Only create new history entry if the previous regex wasn't empty.
+        if self
+            .search_state
+            .history
+            .front()
+            .map_or(true, |regex| !regex.is_empty())
+        {
+            self.search_state.history.push_front(String::new());
+            self.search_state.history.truncate(MAX_SEARCH_HISTORY_SIZE);
+        }
 
-            // Write filtered escape sequences.
-            //
-            // We remove `\x1b` to ensure it's impossible for the pasted text to write the bracketed
-            // paste end escape `\x1b[201~` and `\x03` since some shells incorrectly terminate
-            // bracketed paste on its receival.
-            let filtered = text.replace(['\x1b', '\x03'], "");
-            self.ctx_mut()
-                .current_mut()
-                .messenger
-                .send_bytes(filtered.into_bytes());
+        self.search_state.history_index = Some(0);
+        self.search_state.direction = direction;
+        self.search_state.focused_match = None;
 
-            self.ctx_mut()
-                .current_mut()
-                .messenger
-                .send_bytes(b"\x1b[201~"[..].to_vec());
+        // Store original search position as origin and reset location.
+        if self.get_mode().contains(Mode::VI) {
+            let terminal = self.context_manager.current().terminal.lock();
+            self.search_state.origin = terminal.vi_mode_cursor.pos;
+            self.search_state.display_offset_delta = 0;
+
+            // Adjust origin for content moving upward on search start.
+            if terminal.grid.cursor.pos.row + 1 == terminal.screen_lines() {
+                self.search_state.origin.row -= 1;
+            }
+            drop(terminal);
         } else {
-            self.ctx_mut()
-                .current_mut()
-                .messenger
-                .send_bytes(text.replace("\r\n", "\r").replace('\n', "\r").into_bytes());
+            let terminal = self.context_manager.current().terminal.lock();
+            let viewport_top = Line(-(terminal.grid.display_offset() as i32)) - 1;
+            let viewport_bottom = viewport_top + terminal.bottommost_line();
+            let last_column = terminal.last_column();
+            self.search_state.origin = match direction {
+                Direction::Right => Pos::new(viewport_top, Column(0)),
+                Direction::Left => Pos::new(viewport_bottom, last_column),
+            };
+            drop(terminal);
         }
+
+        // Enable IME so we can input into the search bar with it if we were in Vi mode.
+        // self.window().set_ime_allowed(true);
+
+        self.sugarloaf.mark_dirty();
+        self.render();
     }
 
-    pub fn render_assistant(&mut self, assistant: &crate::routes::assistant::Assistant) {
-        self.sugarloaf.clear();
-        crate::routes::assistant::screen(&mut self.sugarloaf, assistant);
-        self.sugarloaf.render();
+    #[inline]
+    fn confirm_search(&mut self) {
+        // Just cancel search when not in vi mode.
+        if !self.get_mode().contains(Mode::VI) {
+            self.cancel_search();
+            return;
+        }
+
+        // Force unlimited search if the previous one was interrupted.
+        // let timer_id = TimerId::new(Topic::DelayedSearch, self.display.window.id());
+        // if self.scheduler.scheduled(timer_id) {
+        //     self.goto_match(None);
+        // }
+
+        self.exit_search();
     }
 
-    pub fn render_welcome(&mut self) {
-        self.sugarloaf.clear();
-        crate::routes::welcome::screen(&mut self.sugarloaf);
-        self.sugarloaf.render();
+    #[inline]
+    fn cancel_search(&mut self) {
+        if self.get_mode().contains(Mode::VI) {
+            // Recover pre-search state in vi mode.
+            self.search_reset_state();
+        } else if let Some(focused_match) = &self.search_state.focused_match {
+            // Create a selection for the focused match.
+            let start = *focused_match.start();
+            let end = *focused_match.end();
+            self.start_selection(SelectionType::Simple, start, Side::Left);
+            self.update_selection(end, Side::Right);
+            self.copy_selection(ClipboardType::Selection);
+        }
+
+        self.search_state.dfas = None;
+
+        self.exit_search();
     }
 
-    pub fn render_dialog(&mut self, content: &str) {
-        self.sugarloaf.clear();
-        crate::routes::dialog::screen(&mut self.sugarloaf, content);
-        self.sugarloaf.render();
+    /// Cleanup the search state.
+    fn exit_search(&mut self) {
+        // let vi_mode = self.get_mode().contains(Mode::VI);
+        // self.window().set_ime_allowed(!vi_mode);
+
+        self.sugarloaf.mark_dirty();
+        self.search_state.history_index = None;
+
+        // Clear focused match.
+        self.search_state.focused_match = None;
+
+        self.render();
     }
 
-    pub fn update_content(&mut self) {
-        let (rows, cursor, display_offset, has_blinking_enabled) = {
-            let terminal = self.context_manager.current().terminal.lock();
-            (
-                terminal.visible_rows(),
-                terminal.cursor(),
-                terminal.display_offset(),
-                terminal.blinking_cursor,
-            )
+    #[inline]
+    fn search_input(&mut self, c: char) {
+        match self.search_state.history_index {
+            Some(0) => (),
+            // When currently in history, replace active regex with history on change.
+            Some(index) => {
+                self.search_state.history[0] = self.search_state.history[index].clone();
+                self.search_state.history_index = Some(0);
+            }
+            None => return,
+        }
+        let regex = &mut self.search_state.history[0];
+
+        match c {
+            // Handle backspace/ctrl+h.
+            '\x08' | '\x7f' => {
+                let _ = regex.pop();
+            }
+            // Add ascii and unicode text.
+            ' '..='~' | '\u{a0}'..='\u{10ffff}' => regex.push(c),
+            // Ignore non-printable characters.
+            _ => return,
+        }
+
+        let mode = self.get_mode();
+        if !mode.contains(Mode::VI) {
+            // Clear selection so we do not obstruct any matches.
+            self.renderer.set_selection(None);
+        }
+
+        self.update_search();
+        self.render();
+    }
+
+    fn update_search(&mut self) {
+        let regex = match self.search_state.regex() {
+            Some(regex) => regex,
+            None => return,
         };
 
-        self.context_manager.update_titles();
-        self.state.set_ime(self.ime.preedit());
+        if regex.is_empty() {
+            // Stop search if there's nothing to search for.
+            self.search_reset_state();
+            self.search_state.dfas = None;
+        } else {
+            // Create search dfas for the new regex string.
+            self.search_state.dfas = RegexSearch::new(regex).ok();
 
-        self.state.prepare_term(
-            &rows,
-            cursor,
-            &mut self.sugarloaf,
-            &self.context_manager,
-            display_offset as i32,
-            has_blinking_enabled,
-        );
+            // Update search highlighting.
+            self.goto_match(MAX_SEARCH_WHILE_TYPING);
+        }
+
+        self.sugarloaf.mark_dirty();
     }
 
-    #[inline]
-    pub fn render(&mut self) {
-        // If sugarloaf does have pending updates to process then
-        // should abort current render
+    /// Reset terminal to the state before search was started.
+    fn search_reset_state(&mut self) {
+        // Unschedule pending timers.
+        // let timer_id = TimerId::new(Topic::DelayedSearch, self.display.window.id());
+        // self.scheduler.unschedule(timer_id);
 
-        // let start = std::time::Instant::now();
-        // println!("Render time elapsed");
+        // Clear focused match.
+        self.search_state.focused_match = None;
 
-        self.sugarloaf.render();
+        // The viewport reset logic is only needed for vi mode, since without it our origin is
+        // always at the current display offset instead of at the vi cursor position which we need
+        // to recover to.
+        let mode = self.get_mode();
+        if !mode.contains(Mode::VI) {
+            return;
+        }
 
-        // In this case the configuration of blinking cursor is enabled
-        // and the terminal also have instructions of blinking enabled
-        // TODO: enable blinking for selection after adding debounce (https://github.com/raphamorim/rio/issues/437)
-        if self.state.has_blinking_enabled() && self.selection_is_empty() {
-            self.context_manager.schedule_render_on_route(800);
+        // Reset display offset and cursor position.
+        {
+            let mut terminal = self.ctx().current().terminal.lock();
+            terminal.vi_mode_cursor.pos = self.search_state.origin;
+            terminal
+                .scroll_display(Scroll::Delta(self.search_state.display_offset_delta));
         }
+        self.search_state.display_offset_delta = 0;
+        self.sugarloaf.mark_dirty();
+    }
 
-        // let duration = start.elapsed();
-        // println!("Total render time is: {:?}\n", duration);
+    /// Jump to the first regex match from the search origin.
+    fn goto_match(&mut self, mut limit: Option<usize>) {
+        let dfas = match &mut self.search_state.dfas {
+            Some(dfas) => dfas,
+            None => return,
+        };
+
+        let mut should_reset_search_state = false;
+
+        // Jump to the next match.
+        {
+            let mut terminal = self.context_manager.current_mut().terminal.lock();
+            // Limit search only when enough lines are available to run into the limit.
+            limit = limit.filter(|&limit| limit <= terminal.total_lines());
+
+            let direction = self.search_state.direction;
+            let clamped_origin = self
+                .search_state
+                .origin
+                .grid_clamp(&*terminal, Boundary::Grid);
+            match terminal.search_next(dfas, clamped_origin, direction, Side::Left, limit)
+            {
+                Some(regex_match) => {
+                    let old_offset = terminal.display_offset() as i32;
+                    if terminal.mode().contains(Mode::VI) {
+                        // Move vi cursor to the start of the match.
+                        terminal.vi_goto_pos(*regex_match.start());
+                    } else {
+                        // Select the match when vi mode is not active.
+                        terminal.scroll_to_pos(*regex_match.start());
+                    }
+
+                    // Update the focused match.
+                    self.search_state.focused_match = Some(regex_match);
+
+                    // Store number of lines the viewport had to be moved.
+                    let display_offset = terminal.display_offset();
+                    self.search_state.display_offset_delta +=
+                        old_offset - display_offset as i32;
+
+                    // Since we found a result, we require no delayed re-search.
+                    // let timer_id = TimerId::new(Topic::DelayedSearch, self.display.window.id());
+                    // self.scheduler.unschedule(timer_id);
+                }
+                // Reset viewport only when we know there is no match, to prevent unnecessary jumping.
+                None if limit.is_none() => {
+                    should_reset_search_state = true;
+                }
+                None => {
+                    // Schedule delayed search if we ran into our search limit.
+                    // let timer_id = TimerId::new(Topic::DelayedSearch, self.display.window.id());
+                    // if !self.scheduler.scheduled(timer_id) {
+                    // let event = Event::new(EventType::SearchNext, self.display.window.id());
+                    // self.scheduler.schedule(event, TYPING_SEARCH_DELAY, false, timer_id);
+                    // }
+
+                    // Clear focused match.
+                    self.search_state.focused_match = None;
+                }
+            }
+        }
+
+        if should_reset_search_state {
+            self.search_reset_state();
+        }
+
+        self.sugarloaf.mark_dirty();
     }
 
     fn sgr_mouse_report(&mut self, pos: Pos, button: u8, state: ElementState) {
@@ -1509,4 +1771,134 @@ impl Screen<'_> {
         self.mouse.accumulated_scroll.x %= width;
         self.mouse.accumulated_scroll.y %= height;
     }
+
+    #[inline]
+    pub fn paste(&mut self, text: &str, bracketed: bool) {
+        if self.search_active() {
+            for c in text.chars() {
+                self.search_input(c);
+            }
+        } else if bracketed && self.get_mode().contains(Mode::BRACKETED_PASTE) {
+            self.ctx_mut()
+                .current_mut()
+                .messenger
+                .send_bytes(b"\x1b[200~"[..].to_vec());
+
+            // Write filtered escape sequences.
+            //
+            // We remove `\x1b` to ensure it's impossible for the pasted text to write the bracketed
+            // paste end escape `\x1b[201~` and `\x03` since some shells incorrectly terminate
+            // bracketed paste on its receival.
+            let filtered = text.replace(['\x1b', '\x03'], "");
+            self.ctx_mut()
+                .current_mut()
+                .messenger
+                .send_bytes(filtered.into_bytes());
+
+            self.ctx_mut()
+                .current_mut()
+                .messenger
+                .send_bytes(b"\x1b[201~"[..].to_vec());
+        } else {
+            self.ctx_mut()
+                .current_mut()
+                .messenger
+                .send_bytes(text.replace("\r\n", "\r").replace('\n', "\r").into_bytes());
+        }
+    }
+
+    pub fn render_assistant(&mut self, assistant: &crate::routes::assistant::Assistant) {
+        self.sugarloaf.clear();
+        crate::routes::assistant::screen(&mut self.sugarloaf, assistant);
+        self.sugarloaf.render();
+    }
+
+    pub fn render_welcome(&mut self) {
+        self.sugarloaf.clear();
+        crate::routes::welcome::screen(&mut self.sugarloaf);
+        self.sugarloaf.render();
+    }
+
+    pub fn render_dialog(&mut self, content: &str) {
+        self.sugarloaf.clear();
+        crate::routes::dialog::screen(&mut self.sugarloaf, content);
+        self.sugarloaf.render();
+    }
+
+    #[inline]
+    fn demand_render(&mut self) {
+        self.update_content();
+        self.render();
+    }
+
+    pub fn update_content(&mut self) {
+        // let start = std::time::Instant::now();
+        // println!("Render time elapsed");
+        let is_search_active = self.search_active();
+        if is_search_active {
+            if let Some(history_index) = self.search_state.history_index {
+                self.renderer.set_active_search(
+                    self.search_state.history.get(history_index).cloned(),
+                );
+            }
+        }
+
+        let mut search_hints = if is_search_active {
+            let terminal = self.context_manager.current().terminal.lock();
+            let hints = self
+                .search_state
+                .dfas_mut()
+                .map(|dfas| HintMatches::visible_regex_matches(&terminal, dfas));
+            drop(terminal);
+            hints
+        } else {
+            None
+        };
+
+        let (rows, cursor, display_offset, has_blinking_enabled) = {
+            let terminal = self.context_manager.current().terminal.lock();
+            let data = (
+                terminal.visible_rows(),
+                terminal.cursor(),
+                terminal.display_offset(),
+                terminal.blinking_cursor,
+            );
+            drop(terminal);
+            data
+        };
+
+        self.context_manager.update_titles();
+        self.renderer.set_ime(self.ime.preedit());
+
+        self.renderer.prepare_term(
+            &rows,
+            cursor,
+            &mut self.sugarloaf,
+            &self.context_manager,
+            display_offset as i32,
+            has_blinking_enabled,
+            &mut search_hints,
+            &self.search_state.focused_match,
+        );
+        // let duration = start.elapsed();
+        // println!("Total update_content is: {:?}\n", duration);
+    }
+
+    #[inline]
+    pub fn render(&mut self) {
+        // let start = std::time::Instant::now();
+        // println!("Render time elapsed");
+
+        self.sugarloaf.render();
+
+        // In this case the configuration of blinking cursor is enabled
+        // and the terminal also have instructions of blinking enabled
+        // TODO: enable blinking for selection after adding debounce (https://github.com/raphamorim/rio/issues/437)
+        if self.renderer.has_blinking_enabled() && self.selection_is_empty() {
+            self.context_manager.schedule_render_on_route(800);
+        }
+
+        // let duration = start.elapsed();
+        // println!("Total render time is: {:?}\n", duration);
+    }
 }
diff --git a/frontends/rioterm/src/sequencer.rs b/frontends/rioterm/src/sequencer.rs
index 7356a4ff8e..95a6157fb9 100644
--- a/frontends/rioterm/src/sequencer.rs
+++ b/frontends/rioterm/src/sequencer.rs
@@ -363,11 +363,12 @@ impl Sequencer {
                                 //
                                 // In the future should try first get
                                 // from Crosswords then state colors
-                                // screen.colors()[index] or screen.state.colors[index]
+                                // screen.colors()[index] or screen.renderer.colors[index]
                                 if let Some(route) =
                                     self.router.routes.get_mut(&window_id)
                                 {
-                                    let color = route.window.screen.state.colors[index];
+                                    let color =
+                                        route.window.screen.renderer.colors[index];
                                     let rgb = ColorRgb::from_color_arr(color);
                                     route
                                         .window
@@ -913,8 +914,12 @@ impl Sequencer {
                                 route.window.winit_window.set_cursor(cursor_icon);
 
                                 // In case hyperlink range has cleaned trigger one more render
-                                if route.window.screen.state.has_hyperlink_range() {
-                                    route.window.screen.state.set_hyperlink_range(None);
+                                if route.window.screen.renderer.has_hyperlink_range() {
+                                    route
+                                        .window
+                                        .screen
+                                        .renderer
+                                        .set_hyperlink_range(None);
                                     route
                                         .window
                                         .screen
@@ -1038,7 +1043,8 @@ impl Sequencer {
                                 return;
                             }
 
-                            route.window.screen.state.last_typing = Some(Instant::now());
+                            route.window.screen.renderer.last_typing =
+                                Some(Instant::now());
                             route.window.screen.process_key_event(&key_event);
 
                             if key_event.state == ElementState::Released
diff --git a/frontends/rioterm/src/state/mod.rs b/frontends/rioterm/src/state/mod.rs
deleted file mode 100644
index 654660d8bb..0000000000
--- a/frontends/rioterm/src/state/mod.rs
+++ /dev/null
@@ -1,700 +0,0 @@
-pub mod navigation;
-
-use crate::ansi::CursorShape;
-use crate::crosswords::grid::row::Row;
-use crate::crosswords::pos;
-use crate::crosswords::pos::CursorState;
-use crate::crosswords::square::{Flags, Square};
-use crate::ime::Preedit;
-use crate::selection::SelectionRange;
-use navigation::ScreenNavigation;
-use rio_backend::config::colors::{
-    term::{List, TermColors},
-    AnsiColor, ColorArray, Colors, NamedColor,
-};
-use rio_backend::config::Config;
-use rio_backend::sugarloaf::{
-    Content, ContentBuilder, FragmentStyle, FragmentStyleDecoration, Stretch, Style,
-    SugarCursor, Sugarloaf, UnderlineInfo, UnderlineShape, Weight,
-};
-use std::collections::HashMap;
-use std::time::{Duration, Instant};
-#[cfg(not(use_wa))]
-use winit::window::Theme;
-
-use rustc_hash::FxHashMap;
-use unicode_width::UnicodeWidthChar;
-
-struct Cursor {
-    state: CursorState,
-    content: char,
-    content_ref: char,
-}
-
-pub struct State {
-    #[allow(unused)]
-    pub option_as_alt: String,
-    is_ime_enabled: bool,
-    is_vi_mode_enabled: bool,
-    pub is_kitty_keyboard_enabled: bool,
-    pub last_typing: Option<Instant>,
-    pub named_colors: Colors,
-    font_size: f32,
-    pub colors: List,
-    pub navigation: ScreenNavigation,
-    cursor: Cursor,
-    pub selection_range: Option<SelectionRange>,
-    pub config_has_blinking_enabled: bool,
-    term_has_blinking_enabled: bool,
-    pub is_blinking: bool,
-    ignore_selection_fg_color: bool,
-    // Dynamic background keep track of the original bg color and
-    // the same r,g,b with the mutated alpha channel.
-    pub dynamic_background: ([f32; 4], wgpu::Color, bool),
-    hyperlink_range: Option<SelectionRange>,
-    width_cache: FxHashMap<char, f32>,
-}
-
-impl State {
-    pub fn new(
-        #[cfg(not(use_wa))] config: &Config,
-        #[cfg(use_wa)] config: &Config,
-        #[cfg(not(use_wa))] current_theme: Option<Theme>,
-        #[cfg(use_wa)] appearance: wa::Appearance,
-    ) -> State {
-        let term_colors = TermColors::default();
-        let colors = List::from(&term_colors);
-        let mut named_colors = config.colors;
-
-        #[cfg(not(use_wa))]
-        {
-            if let Some(theme) = current_theme {
-                if let Some(adaptive_colors) = &config.adaptive_colors {
-                    match theme {
-                        Theme::Light => {
-                            named_colors = adaptive_colors.light.unwrap_or(named_colors);
-                        }
-                        Theme::Dark => {
-                            named_colors = adaptive_colors.dark.unwrap_or(named_colors);
-                        }
-                    }
-                }
-            }
-        }
-
-        #[cfg(use_wa)]
-        {
-            if let Some(adaptive_colors) = &config.adaptive_colors {
-                match appearance {
-                    wa::Appearance::Light => {
-                        named_colors = adaptive_colors.light.unwrap_or(named_colors);
-                    }
-                    wa::Appearance::Dark => {
-                        named_colors = adaptive_colors.dark.unwrap_or(named_colors);
-                    }
-                    // TODO
-                    wa::Appearance::LightHighContrast => {}
-                    wa::Appearance::DarkHighContrast => {}
-                }
-            }
-        }
-
-        let mut dynamic_background =
-            (named_colors.background.0, named_colors.background.1, false);
-        if config.window.opacity < 1. {
-            dynamic_background.1.a = config.window.opacity as f64;
-            dynamic_background.2 = true;
-        } else if config.window.background_image.is_some() {
-            dynamic_background.1 = wgpu::Color::TRANSPARENT;
-            dynamic_background.2 = true;
-        }
-
-        let mut color_automation: HashMap<String, HashMap<String, [f32; 4]>> =
-            HashMap::new();
-
-        for rule in &config.navigation.color_automation {
-            color_automation
-                .entry(rule.program.clone())
-                .or_default()
-                .insert(rule.path.clone(), rule.color);
-        }
-
-        State {
-            option_as_alt: config.option_as_alt.to_lowercase(),
-            is_kitty_keyboard_enabled: config.keyboard.use_kitty_keyboard_protocol,
-            is_ime_enabled: false,
-            is_vi_mode_enabled: false,
-            is_blinking: false,
-            last_typing: None,
-            config_has_blinking_enabled: config.blinking_cursor,
-            term_has_blinking_enabled: false,
-            ignore_selection_fg_color: config.ignore_selection_fg_color,
-            colors,
-            navigation: ScreenNavigation::new(
-                config.navigation.clone(),
-                [
-                    named_colors.foreground,
-                    named_colors.bar,
-                    named_colors.tabs,
-                    named_colors.tabs_active,
-                    named_colors.tabs_active_highlight,
-                ],
-                color_automation,
-                config.padding_y,
-            ),
-            font_size: config.fonts.size,
-            selection_range: None,
-            hyperlink_range: None,
-            named_colors,
-            dynamic_background,
-            cursor: Cursor {
-                content: config.cursor,
-                content_ref: config.cursor,
-                state: CursorState::new(config.cursor),
-            },
-            width_cache: FxHashMap::default(),
-        }
-    }
-
-    #[inline]
-    pub fn has_blinking_enabled(&self) -> bool {
-        self.config_has_blinking_enabled && self.term_has_blinking_enabled
-    }
-
-    #[inline]
-    pub fn get_cursor_state_from_ref(&self) -> CursorState {
-        CursorState::new(self.cursor.content_ref)
-    }
-
-    #[inline]
-    pub fn get_cursor_state(&self) -> CursorState {
-        self.cursor.state.clone()
-    }
-
-    #[inline]
-    pub fn set_hyperlink_range(&mut self, hyperlink_range: Option<SelectionRange>) {
-        self.hyperlink_range = hyperlink_range;
-    }
-
-    #[inline]
-    pub fn has_hyperlink_range(&self) -> bool {
-        self.hyperlink_range.is_some()
-    }
-
-    #[inline]
-    fn create_style(&mut self, square: &Square) -> (FragmentStyle, char) {
-        let flags = square.flags;
-
-        let mut foreground_color = self.compute_color(&square.fg, flags);
-        let mut background_color = self.compute_bg_color(square);
-
-        let content = if square.c == '\t' || flags.contains(Flags::HIDDEN) {
-            ' '
-        } else {
-            square.c
-        };
-
-        let width = if let Some(w) = self.width_cache.get(&content) {
-            *w
-        } else {
-            let w = square.c.width().unwrap_or(1) as f32;
-            self.width_cache.insert(square.c, w);
-            w
-        };
-
-        let font_attrs = match (
-            flags.contains(Flags::ITALIC),
-            flags.contains(Flags::BOLD_ITALIC),
-            flags.contains(Flags::BOLD),
-        ) {
-            (true, _, _) => (Stretch::NORMAL, Weight::NORMAL, Style::Italic),
-            (_, true, _) => (Stretch::NORMAL, Weight::BOLD, Style::Italic),
-            (_, _, true) => (Stretch::NORMAL, Weight::BOLD, Style::Normal),
-            _ => (Stretch::NORMAL, Weight::NORMAL, Style::Normal),
-        };
-
-        if flags.contains(Flags::INVERSE) {
-            std::mem::swap(&mut background_color, &mut foreground_color);
-        }
-
-        let background_color = if self.dynamic_background.2
-            && background_color[0] == self.dynamic_background.0[0]
-            && background_color[1] == self.dynamic_background.0[1]
-            && background_color[2] == self.dynamic_background.0[2]
-        {
-            None
-        } else {
-            Some(background_color)
-        };
-
-        let (decoration, decoration_color) = self.compute_decoration(square, false);
-
-        (
-            FragmentStyle {
-                width,
-                color: foreground_color,
-                background_color,
-                font_attrs,
-                decoration,
-                decoration_color,
-                ..FragmentStyle::default()
-            },
-            content,
-        )
-    }
-
-    fn compute_decoration(
-        &self,
-        square: &Square,
-        skip_underline: bool,
-    ) -> (Option<FragmentStyleDecoration>, Option<[f32; 4]>) {
-        let mut decoration = None;
-        let mut decoration_color = None;
-
-        if square.flags.contains(Flags::UNDERLINE) {
-            if !skip_underline {
-                decoration = Some(FragmentStyleDecoration::Underline(UnderlineInfo {
-                    offset: -2.0,
-                    size: 2.0,
-                    is_doubled: false,
-                    shape: UnderlineShape::Regular,
-                }));
-            }
-        } else if square.flags.contains(Flags::STRIKEOUT) {
-            decoration = Some(FragmentStyleDecoration::Strikethrough);
-        } else if square.flags.contains(Flags::DOUBLE_UNDERLINE) {
-            decoration = Some(FragmentStyleDecoration::Underline(UnderlineInfo {
-                offset: -4.0,
-                size: 1.0,
-                is_doubled: true,
-                shape: UnderlineShape::Regular,
-            }));
-        } else if square.flags.contains(Flags::DOTTED_UNDERLINE) {
-            decoration = Some(FragmentStyleDecoration::Underline(UnderlineInfo {
-                offset: -2.0,
-                size: 2.0,
-                is_doubled: false,
-                shape: UnderlineShape::Dotted,
-            }));
-        } else if square.flags.contains(Flags::DASHED_UNDERLINE) {
-            decoration = Some(FragmentStyleDecoration::Underline(UnderlineInfo {
-                offset: -2.0,
-                size: 2.0,
-                is_doubled: false,
-                shape: UnderlineShape::Dashed,
-            }));
-        } else if square.flags.contains(Flags::UNDERCURL) {
-            decoration = Some(FragmentStyleDecoration::Underline(UnderlineInfo {
-                offset: -2.0,
-                size: 1.0,
-                is_doubled: false,
-                shape: UnderlineShape::Curly,
-            }));
-        }
-
-        if decoration.is_some() {
-            if let Some(color) = square.underline_color() {
-                decoration_color = Some(self.compute_color(&color, square.flags));
-            }
-        };
-
-        (decoration, decoration_color)
-    }
-
-    #[inline]
-    fn create_line(
-        &mut self,
-        content_builder: &mut ContentBuilder,
-        row: &Row<Square>,
-        has_cursor: bool,
-        current_line: pos::Line,
-    ) {
-        let columns: usize = row.len();
-        let mut content = String::default();
-        let mut last_style = FragmentStyle::default();
-
-        for column in 0..columns {
-            let square = &row.inner[column];
-
-            if square.flags.contains(Flags::WIDE_CHAR_SPACER) {
-                continue;
-            }
-
-            if square.flags.contains(Flags::GRAPHICS) {
-                // &self.create_graphic_sugar(square);
-                continue;
-            }
-
-            let (mut style, square_content) =
-                if has_cursor && column == self.cursor.state.pos.col {
-                    self.create_cursor_style(square)
-                } else {
-                    self.create_style(square)
-                };
-
-            if self.hyperlink_range.is_some()
-                && square.hyperlink().is_some()
-                && self
-                    .hyperlink_range
-                    .unwrap()
-                    .contains(pos::Pos::new(current_line, pos::Column(column)))
-            {
-                style.decoration =
-                    Some(FragmentStyleDecoration::Underline(UnderlineInfo {
-                        offset: -1.0,
-                        size: -1.0,
-                        is_doubled: false,
-                        shape: UnderlineShape::Regular,
-                    }));
-            } else if self.selection_range.is_some()
-                && self
-                    .selection_range
-                    .unwrap()
-                    .contains(pos::Pos::new(current_line, pos::Column(column)))
-            {
-                style.color = if self.ignore_selection_fg_color {
-                    self.compute_color(&square.fg, square.flags)
-                } else {
-                    self.named_colors.selection_foreground
-                };
-                style.background_color = Some(self.named_colors.selection_background);
-            }
-
-            if last_style != style {
-                if !content.is_empty() {
-                    content_builder.add_text(&content, last_style);
-                }
-
-                content.clear();
-                last_style = style;
-            }
-
-            content.push(square_content);
-
-            // Render last column and break row
-            if column == (columns - 1) {
-                if !content.is_empty() {
-                    content_builder.add_text(&content, last_style);
-                }
-
-                break;
-            }
-        }
-
-        content_builder.finish_line();
-    }
-
-    #[inline]
-    #[cfg(use_wa)]
-    pub fn decrease_foreground_opacity(&mut self, _acc: f32) {
-        // self.foreground_opacity -= acc;
-    }
-
-    #[inline]
-    #[cfg(use_wa)]
-    pub fn increase_foreground_opacity(&mut self, _acc: f32) {
-        // self.foreground_opacity += acc;
-    }
-
-    #[inline]
-    fn compute_color(&self, color: &AnsiColor, flags: Flags) -> ColorArray {
-        match color {
-            AnsiColor::Named(ansi_name) => match (ansi_name, flags) {
-                (NamedColor::Background, _) => self.named_colors.background.0,
-                (NamedColor::Cursor, _) => self.named_colors.cursor,
-
-                (NamedColor::Black, Flags::DIM) => self.named_colors.dim_black,
-                (NamedColor::Black, Flags::BOLD) => self.named_colors.light_black,
-                (NamedColor::Black, _) => self.named_colors.black,
-                (NamedColor::Blue, Flags::DIM) => self.named_colors.dim_blue,
-                (NamedColor::Blue, Flags::BOLD) => self.named_colors.light_blue,
-                (NamedColor::Blue, _) => self.named_colors.blue,
-                (NamedColor::Cyan, Flags::DIM) => self.named_colors.dim_cyan,
-                (NamedColor::Cyan, Flags::BOLD) => self.named_colors.light_cyan,
-                (NamedColor::Cyan, _) => self.named_colors.cyan,
-                (NamedColor::Foreground, _) => self.named_colors.foreground,
-                (NamedColor::Green, Flags::DIM) => self.named_colors.dim_green,
-                (NamedColor::Green, Flags::BOLD) => self.named_colors.light_green,
-                (NamedColor::Green, _) => self.named_colors.green,
-                (NamedColor::Magenta, Flags::DIM) => self.named_colors.dim_magenta,
-                (NamedColor::Magenta, Flags::BOLD) => self.named_colors.light_magenta,
-                (NamedColor::Magenta, _) => self.named_colors.magenta,
-                (NamedColor::Red, Flags::DIM) => self.named_colors.dim_red,
-                (NamedColor::Red, Flags::BOLD) => self.named_colors.light_red,
-                (NamedColor::Red, _) => self.named_colors.red,
-                (NamedColor::White, Flags::DIM) => self.named_colors.dim_white,
-                (NamedColor::White, Flags::BOLD) => self.named_colors.light_white,
-                (NamedColor::White, _) => self.named_colors.white,
-                (NamedColor::Yellow, Flags::DIM) => self.named_colors.dim_yellow,
-                (NamedColor::Yellow, Flags::BOLD) => self.named_colors.light_yellow,
-                (NamedColor::Yellow, _) => self.named_colors.yellow,
-                (NamedColor::LightBlack, _) => self.named_colors.light_black,
-                (NamedColor::LightBlue, _) => self.named_colors.light_blue,
-                (NamedColor::LightCyan, _) => self.named_colors.light_cyan,
-                (NamedColor::LightForeground, _) => self.named_colors.light_foreground,
-                (NamedColor::LightGreen, _) => self.named_colors.light_green,
-                (NamedColor::LightMagenta, _) => self.named_colors.light_magenta,
-                (NamedColor::LightRed, _) => self.named_colors.light_red,
-                (NamedColor::LightWhite, _) => self.named_colors.light_white,
-                (NamedColor::LightYellow, _) => self.named_colors.light_yellow,
-                (NamedColor::DimBlack, _) => self.named_colors.dim_black,
-                (NamedColor::DimBlue, _) => self.named_colors.dim_blue,
-                (NamedColor::DimCyan, _) => self.named_colors.dim_cyan,
-                (NamedColor::DimForeground, _) => self.named_colors.dim_foreground,
-                (NamedColor::DimGreen, _) => self.named_colors.dim_green,
-                (NamedColor::DimMagenta, _) => self.named_colors.dim_magenta,
-                (NamedColor::DimRed, _) => self.named_colors.dim_red,
-                (NamedColor::DimWhite, _) => self.named_colors.dim_white,
-                (NamedColor::DimYellow, _) => self.named_colors.dim_yellow,
-            },
-            AnsiColor::Spec(rgb) => {
-                if !flags.contains(Flags::DIM) {
-                    rgb.to_arr()
-                } else {
-                    rgb.to_arr_with_dim()
-                }
-            }
-            AnsiColor::Indexed(index) => {
-                let index = match (flags & Flags::DIM_BOLD, index) {
-                    (Flags::DIM, 8..=15) => *index as usize - 8,
-                    (Flags::DIM, 0..=7) => {
-                        NamedColor::DimBlack as usize + *index as usize
-                    }
-                    _ => *index as usize,
-                };
-
-                self.colors[index]
-            }
-        }
-    }
-
-    #[inline]
-    fn compute_bg_color(&self, square: &Square) -> ColorArray {
-        match square.bg {
-            AnsiColor::Named(ansi_name) => match (ansi_name, square.flags) {
-                (NamedColor::Background, _) => self.named_colors.background.0,
-                (NamedColor::Cursor, _) => self.named_colors.cursor,
-
-                (NamedColor::Black, Flags::DIM) => self.named_colors.dim_black,
-                (NamedColor::Black, Flags::BOLD) => self.named_colors.light_black,
-                (NamedColor::Black, _) => self.named_colors.black,
-                (NamedColor::Blue, Flags::DIM) => self.named_colors.dim_blue,
-                (NamedColor::Blue, Flags::BOLD) => self.named_colors.light_blue,
-                (NamedColor::Blue, _) => self.named_colors.blue,
-                (NamedColor::Cyan, Flags::DIM) => self.named_colors.dim_cyan,
-                (NamedColor::Cyan, Flags::BOLD) => self.named_colors.light_cyan,
-                (NamedColor::Cyan, _) => self.named_colors.cyan,
-                (NamedColor::Foreground, _) => self.named_colors.foreground,
-                (NamedColor::Green, Flags::DIM) => self.named_colors.dim_green,
-                (NamedColor::Green, Flags::BOLD) => self.named_colors.light_green,
-                (NamedColor::Green, _) => self.named_colors.green,
-                (NamedColor::Magenta, Flags::DIM) => self.named_colors.dim_magenta,
-                (NamedColor::Magenta, Flags::BOLD) => self.named_colors.light_magenta,
-                (NamedColor::Magenta, _) => self.named_colors.magenta,
-                (NamedColor::Red, Flags::DIM) => self.named_colors.dim_red,
-                (NamedColor::Red, Flags::BOLD) => self.named_colors.light_red,
-                (NamedColor::Red, _) => self.named_colors.red,
-                (NamedColor::White, Flags::DIM) => self.named_colors.dim_white,
-                (NamedColor::White, Flags::BOLD) => self.named_colors.light_white,
-                (NamedColor::White, _) => self.named_colors.white,
-                (NamedColor::Yellow, Flags::DIM) => self.named_colors.dim_yellow,
-                (NamedColor::Yellow, Flags::BOLD) => self.named_colors.light_yellow,
-                (NamedColor::Yellow, _) => self.named_colors.yellow,
-                (NamedColor::LightBlack, _) => self.named_colors.light_black,
-                (NamedColor::LightBlue, _) => self.named_colors.light_blue,
-                (NamedColor::LightCyan, _) => self.named_colors.light_cyan,
-                (NamedColor::LightForeground, _) => self.named_colors.light_foreground,
-                (NamedColor::LightGreen, _) => self.named_colors.light_green,
-                (NamedColor::LightMagenta, _) => self.named_colors.light_magenta,
-                (NamedColor::LightRed, _) => self.named_colors.light_red,
-                (NamedColor::LightWhite, _) => self.named_colors.light_white,
-                (NamedColor::LightYellow, _) => self.named_colors.light_yellow,
-                (NamedColor::DimBlack, _) => self.named_colors.dim_black,
-                (NamedColor::DimBlue, _) => self.named_colors.dim_blue,
-                (NamedColor::DimCyan, _) => self.named_colors.dim_cyan,
-                (NamedColor::DimForeground, _) => self.named_colors.dim_foreground,
-                (NamedColor::DimGreen, _) => self.named_colors.dim_green,
-                (NamedColor::DimMagenta, _) => self.named_colors.dim_magenta,
-                (NamedColor::DimRed, _) => self.named_colors.dim_red,
-                (NamedColor::DimWhite, _) => self.named_colors.dim_white,
-                (NamedColor::DimYellow, _) => self.named_colors.dim_yellow,
-            },
-            AnsiColor::Spec(rgb) => rgb.to_arr(),
-            AnsiColor::Indexed(idx) => self.colors[idx as usize],
-        }
-    }
-
-    // #[inline]
-    // #[allow(dead_code)]
-    // fn create_graphic_sugar(&self, square: &Square) -> Sugar {
-    //     let media = &square.graphics().unwrap()[0].texture;
-    //     Sugar {
-    //         media: Some(SugarGraphic {
-    //             id: media.id,
-    //             width: media.width,
-    //             height: media.height,
-    //         }),
-    //         ..Sugar::default()
-    //     }
-    // }
-
-    #[inline]
-    fn create_cursor_style(&self, square: &Square) -> (FragmentStyle, char) {
-        let font_attrs = match (
-            square.flags.contains(Flags::ITALIC),
-            square.flags.contains(Flags::BOLD_ITALIC),
-            square.flags.contains(Flags::BOLD),
-        ) {
-            (true, _, _) => (Stretch::NORMAL, Weight::NORMAL, Style::Italic),
-            (_, true, _) => (Stretch::NORMAL, Weight::BOLD, Style::Italic),
-            (_, _, true) => (Stretch::NORMAL, Weight::BOLD, Style::Normal),
-            _ => (Stretch::NORMAL, Weight::NORMAL, Style::Normal),
-        };
-
-        let mut color = self.compute_color(&square.fg, square.flags);
-        let mut background_color = self.compute_bg_color(square);
-        // If IME is enabled we get the current content to cursor
-        let content = if self.is_ime_enabled {
-            self.cursor.content
-        } else {
-            square.c
-        };
-
-        if square.flags.contains(Flags::INVERSE) {
-            std::mem::swap(&mut background_color, &mut color);
-        }
-
-        // If IME is enabled or is a block cursor, put background color
-        // when cursor is over the character
-        if self.is_ime_enabled || self.cursor.state.content == CursorShape::Block {
-            color = self.named_colors.background.0;
-        }
-
-        let mut style = FragmentStyle {
-            color,
-            background_color: Some(background_color),
-            font_attrs,
-            ..FragmentStyle::default()
-        };
-
-        let cursor_color = if !self.is_vi_mode_enabled {
-            self.named_colors.cursor
-        } else {
-            self.named_colors.vi_cursor
-        };
-
-        let mut has_underline_cursor = false;
-
-        match self.cursor.state.content {
-            CursorShape::Underline => {
-                style.decoration =
-                    Some(FragmentStyleDecoration::Underline(UnderlineInfo {
-                        offset: -1.0,
-                        size: -1.0,
-                        is_doubled: false,
-                        shape: UnderlineShape::Regular,
-                    }));
-                style.decoration_color = Some(cursor_color);
-
-                has_underline_cursor = true;
-            }
-            CursorShape::Block => {
-                style.cursor = SugarCursor::Block(cursor_color);
-            }
-            CursorShape::Beam => {
-                style.cursor = SugarCursor::Caret(cursor_color);
-            }
-            CursorShape::Hidden => {}
-        }
-
-        let (decoration, decoration_color) =
-            self.compute_decoration(square, has_underline_cursor);
-        style.decoration = decoration;
-        style.decoration_color = decoration_color;
-
-        (style, content)
-    }
-
-    #[inline]
-    pub fn set_ime(&mut self, ime_preedit: Option<&Preedit>) {
-        if let Some(preedit) = ime_preedit {
-            if let Some(content) = preedit.text.chars().next() {
-                self.cursor.content = content;
-                self.is_ime_enabled = true;
-                return;
-            }
-        }
-
-        self.is_ime_enabled = false;
-        self.cursor.content = self.cursor.content_ref;
-    }
-
-    #[inline]
-    pub fn set_selection(&mut self, selection_range: Option<SelectionRange>) {
-        self.selection_range = selection_range;
-    }
-
-    #[inline]
-    pub fn set_vi_mode(&mut self, is_vi_mode_enabled: bool) {
-        self.is_vi_mode_enabled = is_vi_mode_enabled;
-    }
-
-    #[inline]
-    pub fn prepare_term(
-        &mut self,
-        rows: &[Row<Square>],
-        cursor: CursorState,
-        sugarloaf: &mut Sugarloaf,
-        context_manager: &crate::context::ContextManager<rio_backend::event::EventProxy>,
-        display_offset: i32,
-        has_blinking_enabled: bool,
-    ) {
-        let layout = sugarloaf.layout();
-        self.cursor.state = cursor;
-        let mut is_cursor_visible = self.cursor.state.is_visible();
-
-        self.font_size = layout.font_size;
-        self.term_has_blinking_enabled = has_blinking_enabled;
-
-        // Only blink cursor if does not contain selection
-        let has_selection = self.selection_range.is_some();
-        if !has_selection && self.has_blinking_enabled() {
-            let mut should_blink = true;
-            if let Some(last_typing_time) = self.last_typing {
-                if last_typing_time.elapsed() < Duration::from_secs(1) {
-                    should_blink = false;
-                }
-            }
-
-            if should_blink {
-                self.is_blinking = !self.is_blinking;
-                is_cursor_visible = self.is_blinking;
-            }
-        }
-
-        let mut content_builder = Content::builder();
-
-        for (i, row) in rows.iter().enumerate() {
-            let has_cursor = is_cursor_visible && self.cursor.state.pos.row == i;
-            self.create_line(
-                &mut content_builder,
-                row,
-                has_cursor,
-                pos::Line((i as i32) - display_offset),
-            );
-        }
-
-        sugarloaf.set_content(content_builder.build());
-
-        self.navigation.content(
-            (layout.width, layout.height),
-            layout.dimensions.scale,
-            context_manager.titles.key.as_str(),
-            &context_manager.titles.titles,
-            context_manager.current_index(),
-            context_manager.len(),
-        );
-
-        sugarloaf.set_objects(self.navigation.objects.clone());
-    }
-}
diff --git a/rio-backend/Cargo.toml b/rio-backend/Cargo.toml
index c561025307..e4ff45a4d8 100644
--- a/rio-backend/Cargo.toml
+++ b/rio-backend/Cargo.toml
@@ -35,6 +35,7 @@ teletypewriter = { workspace = true }
 unicode-width = { workspace = true }
 copa = { workspace = true }
 wgpu = { workspace = true }
+regex-automata = "0.4.7"
 cursor-icon = { version = "1.1.0", default-features = false }
 smallvec = { version = "1.13.2", default-features = false }
 
diff --git a/rio-backend/src/config/colors/defaults.rs b/rio-backend/src/config/colors/defaults.rs
index 7b3cc596ae..67b980f7a2 100644
--- a/rio-backend/src/config/colors/defaults.rs
+++ b/rio-backend/src/config/colors/defaults.rs
@@ -257,3 +257,24 @@ pub fn selection_background() -> ColorArray {
         .unwrap()
         .to_arr()
 }
+
+#[inline]
+pub fn search_match_background() -> ColorArray {
+    ColorBuilder::from_hex(String::from("#44C9F0"), Format::SRGB0_1)
+        .unwrap()
+        .to_arr()
+}
+#[inline]
+pub fn search_match_foreground() -> ColorArray {
+    [1., 1., 1., 1.]
+}
+#[inline]
+pub fn search_focused_match_background() -> ColorArray {
+    ColorBuilder::from_hex(String::from("#E6A003"), Format::SRGB0_1)
+        .unwrap()
+        .to_arr()
+}
+#[inline]
+pub fn search_focused_match_foreground() -> ColorArray {
+    [1., 1., 1., 1.]
+}
diff --git a/rio-backend/src/config/colors/mod.rs b/rio-backend/src/config/colors/mod.rs
index cbc1b7d687..a1a557dbd3 100644
--- a/rio-backend/src/config/colors/mod.rs
+++ b/rio-backend/src/config/colors/mod.rs
@@ -230,6 +230,30 @@ pub struct Colors {
     pub selection_foreground: ColorArray,
     #[serde(default = "defaults::cursor", deserialize_with = "deserialize_to_arr")]
     pub split: ColorArray,
+    #[serde(
+        default = "defaults::search_match_background",
+        deserialize_with = "deserialize_to_arr",
+        rename = "search-match-background"
+    )]
+    pub search_match_background: ColorArray,
+    #[serde(
+        default = "defaults::search_match_foreground",
+        deserialize_with = "deserialize_to_arr",
+        rename = "search-match-foreground"
+    )]
+    pub search_match_foreground: ColorArray,
+    #[serde(
+        default = "defaults::search_focused_match_background",
+        deserialize_with = "deserialize_to_arr",
+        rename = "search-focused-match-background"
+    )]
+    pub search_focused_match_background: ColorArray,
+    #[serde(
+        default = "defaults::search_focused_match_foreground",
+        deserialize_with = "deserialize_to_arr",
+        rename = "search-focused-match-foreground"
+    )]
+    pub search_focused_match_foreground: ColorArray,
 }
 
 impl Default for Colors {
@@ -272,6 +296,10 @@ impl Default for Colors {
             light_yellow: defaults::light_yellow(),
             selection_background: defaults::selection_background(),
             selection_foreground: defaults::selection_foreground(),
+            search_match_background: defaults::search_match_background(),
+            search_match_foreground: defaults::search_match_foreground(),
+            search_focused_match_background: defaults::search_focused_match_background(),
+            search_focused_match_foreground: defaults::search_focused_match_foreground(),
         }
     }
 }
diff --git a/rio-backend/src/crosswords/mod.rs b/rio-backend/src/crosswords/mod.rs
index f324c324fb..0b3fd147b7 100644
--- a/rio-backend/src/crosswords/mod.rs
+++ b/rio-backend/src/crosswords/mod.rs
@@ -17,6 +17,7 @@
 pub mod attr;
 pub mod grid;
 pub mod pos;
+pub mod search;
 pub mod square;
 pub mod vi_mode;
 
@@ -65,7 +66,6 @@ pub type NamedColor = colors::NamedColor;
 
 pub const MIN_COLUMNS: usize = 2;
 pub const MIN_LINES: usize = 1;
-const BRACKET_PAIRS: [(char, char); 4] = [('(', ')'), ('[', ']'), ('{', '}'), ('<', '>')];
 
 /// Max. number of graphics stored in a single cell.
 const MAX_GRAPHICS_PER_CELL: usize = 20;
@@ -330,7 +330,7 @@ impl IndexMut<Column> for TabStops {
 
 /// Terminal version for escape sequence reports.
 ///
-/// This returns the current terminal version as a unique number based on alacritty_terminal's
+/// This returns the current terminal version as a unique number based on rio's
 /// semver version. The different versions are padded to ensure that a higher semver version will
 /// always report a higher version number.
 fn version_number(mut version: &str) -> usize {
@@ -619,6 +619,21 @@ impl<U: EventListener> Crosswords<U> {
         self.vi_mode_recompute_selection();
     }
 
+    /// Move vi cursor to a point in the grid.
+    #[inline]
+    pub fn vi_goto_pos(&mut self, pos: Pos)
+    where
+        U: EventListener,
+    {
+        // Move viewport to make pos visible.
+        self.scroll_to_pos(pos);
+
+        // Move vi cursor to the pos.
+        self.vi_mode_cursor.pos = pos;
+
+        self.vi_mode_recompute_selection();
+    }
+
     /// Scroll display to point if it is outside of viewport.
     #[inline]
     pub fn scroll_to_pos(&mut self, pos: Pos)
@@ -780,105 +795,6 @@ impl<U: EventListener> Crosswords<U> {
         self.mark_fully_damaged();
     }
 
-    #[inline]
-    pub fn bracket_search(&self, point: Pos) -> Option<Pos> {
-        let start_char = self.grid[point].c;
-
-        // Find the matching bracket we're looking for
-        let (forward, end_char) = BRACKET_PAIRS.iter().find_map(|(open, close)| {
-            if open == &start_char {
-                Some((true, *close))
-            } else if close == &start_char {
-                Some((false, *open))
-            } else {
-                None
-            }
-        })?;
-
-        let mut iter = self.grid.iter_from(point);
-
-        // For every character match that equals the starting bracket, we
-        // ignore one bracket of the opposite type.
-        let mut skip_pairs = 0;
-
-        loop {
-            // Check the next cell
-            let cell = if forward { iter.next() } else { iter.prev() };
-
-            // Break if there are no more cells
-            let cell = match cell {
-                Some(cell) => cell,
-                None => break,
-            };
-
-            // Check if the bracket matches
-            if cell.c == end_char && skip_pairs == 0 {
-                return Some(cell.pos);
-            } else if cell.c == start_char {
-                skip_pairs += 1;
-            } else if cell.c == end_char {
-                skip_pairs -= 1;
-            }
-        }
-
-        None
-    }
-
-    #[inline]
-    pub fn semantic_search_left(&self, mut point: Pos) -> Pos {
-        // Limit the starting point to the last line in the history
-        point.row = std::cmp::max(point.row, self.grid.topmost_line());
-
-        let mut iter = self.grid.iter_from(point);
-        let last_column = self.grid.columns() - 1;
-
-        let wide = square::Flags::WIDE_CHAR
-            | square::Flags::WIDE_CHAR_SPACER
-            | square::Flags::LEADING_WIDE_CHAR_SPACER;
-        while let Some(cell) = iter.prev() {
-            if !cell.flags.intersects(wide) && self.semantic_escape_chars.contains(cell.c)
-            {
-                break;
-            }
-
-            if cell.pos.col == last_column
-                && !cell.flags.contains(square::Flags::WRAPLINE)
-            {
-                break; // cut off if on new line or hit escape char
-            }
-
-            point = cell.pos;
-        }
-
-        point
-    }
-
-    #[inline]
-    pub fn semantic_search_right(&self, mut point: Pos) -> Pos {
-        // Limit the starting point to the last line in the history
-        point.row = std::cmp::max(point.row, self.grid.topmost_line());
-
-        let wide = square::Flags::WIDE_CHAR
-            | square::Flags::WIDE_CHAR_SPACER
-            | square::Flags::LEADING_WIDE_CHAR_SPACER;
-        let last_column = self.grid.columns() - 1;
-
-        for cell in self.grid.iter_from(point) {
-            if !cell.flags.intersects(wide) && self.semantic_escape_chars.contains(cell.c)
-            {
-                break;
-            }
-
-            point = cell.pos;
-
-            if point.col == last_column && !cell.flags.contains(square::Flags::WRAPLINE) {
-                break; // cut off if on new line or hit escape char
-            }
-        }
-
-        point
-    }
-
     #[inline]
     pub fn search_nearest_hyperlink_from_pos(
         &mut self,
@@ -1044,7 +960,7 @@ impl<U: EventListener> Crosswords<U> {
         }
 
         for row in start..end {
-            visible_rows.push(self.grid[Line(row)].to_owned());
+            visible_rows.push(self.grid[Line(row)].clone());
         }
 
         visible_rows
@@ -2727,6 +2643,31 @@ impl Dimensions for CrosswordsSize {
     }
 }
 
+impl<T: EventListener> Dimensions for Crosswords<T> {
+    #[inline]
+    fn columns(&self) -> usize {
+        self.grid.columns()
+    }
+
+    #[inline]
+    fn screen_lines(&self) -> usize {
+        self.grid.screen_lines()
+    }
+
+    #[inline]
+    fn total_lines(&self) -> usize {
+        self.grid.total_lines()
+    }
+
+    fn square_width(&self) -> f32 {
+        0.
+    }
+
+    fn square_height(&self) -> f32 {
+        0.
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/rio-backend/src/crosswords/search.rs b/rio-backend/src/crosswords/search.rs
new file mode 100644
index 0000000000..db6ac82455
--- /dev/null
+++ b/rio-backend/src/crosswords/search.rs
@@ -0,0 +1,1407 @@
+// search.rs was originally taken from Alacritty https://github.com/alacritty/alacritty/blob/e35e5ad14fce8456afdd89f2b392b9924bb27471/alacritty_terminal/src/term/search.rs
+// which is licensed under Apache 2.0 license.
+
+use crate::event;
+use std::cmp::max;
+use std::error::Error;
+use std::mem;
+use std::ops::RangeInclusive;
+
+use log::{debug, warn};
+use regex_automata::hybrid::dfa::{Builder, Cache, Config, DFA};
+pub use regex_automata::hybrid::BuildError;
+use regex_automata::nfa::thompson::Config as ThompsonConfig;
+use regex_automata::util::syntax::Config as SyntaxConfig;
+use regex_automata::{Anchored, Input, MatchKind};
+
+use crate::crosswords::grid::{BidirectionalIterator, Dimensions, GridIterator, Indexed};
+use crate::crosswords::square::{Flags, Square};
+use crate::crosswords::Crosswords;
+use crate::crosswords::{Boundary, Column, Direction, Pos, Side};
+
+/// Used to match equal brackets, when performing a bracket-pair selection.
+const BRACKET_PAIRS: [(char, char); 4] = [('(', ')'), ('[', ']'), ('{', '}'), ('<', '>')];
+
+pub type Match = RangeInclusive<Pos>;
+
+/// Crosswordsinal regex search state.
+#[derive(Clone, Debug)]
+pub struct RegexSearch {
+    left_fdfa: LazyDfa,
+    left_rdfa: LazyDfa,
+    right_rdfa: LazyDfa,
+    right_fdfa: LazyDfa,
+}
+
+impl RegexSearch {
+    /// Build the forward and backward search DFAs.
+    pub fn new(search: &str) -> Result<RegexSearch, Box<BuildError>> {
+        // Setup configs for both DFA directions.
+        //
+        // Bounds are based on Regex's meta engine:
+        // https://github.com/rust-lang/regex/blob/061ee815ef2c44101dba7b0b124600fcb03c1912/regex-automata/src/meta/wrappers.rs#L581-L599
+        let has_uppercase = search.chars().any(|c| c.is_uppercase());
+        let syntax_config = SyntaxConfig::new().case_insensitive(!has_uppercase);
+        let config = Config::new()
+            .minimum_cache_clear_count(Some(3))
+            .minimum_bytes_per_state(Some(10));
+        let max_size = config.get_cache_capacity();
+        let thompson_config = ThompsonConfig::new().nfa_size_limit(Some(max_size));
+
+        // Create DFAs to find start/end in right-to-left search.
+        let left_rdfa = LazyDfa::new(
+            search,
+            config.clone(),
+            syntax_config,
+            thompson_config.clone(),
+            Direction::Right,
+            true,
+        )?;
+        let has_empty = left_rdfa.dfa.get_nfa().has_empty();
+        let left_fdfa = LazyDfa::new(
+            search,
+            config.clone(),
+            syntax_config,
+            thompson_config.clone(),
+            Direction::Left,
+            has_empty,
+        )?;
+
+        // Create DFAs to find start/end in left-to-right search.
+        let right_fdfa = LazyDfa::new(
+            search,
+            config.clone(),
+            syntax_config,
+            thompson_config.clone(),
+            Direction::Right,
+            has_empty,
+        )?;
+        let right_rdfa = LazyDfa::new(
+            search,
+            config,
+            syntax_config,
+            thompson_config,
+            Direction::Left,
+            true,
+        )?;
+
+        Ok(RegexSearch {
+            left_fdfa,
+            left_rdfa,
+            right_fdfa,
+            right_rdfa,
+        })
+    }
+}
+
+/// Runtime-evaluated DFA.
+#[derive(Clone, Debug)]
+struct LazyDfa {
+    dfa: DFA,
+    cache: Cache,
+    direction: Direction,
+    match_all: bool,
+}
+
+impl LazyDfa {
+    fn new(
+        search: &str,
+        mut config: Config,
+        syntax: SyntaxConfig,
+        mut thompson: ThompsonConfig,
+        direction: Direction,
+        match_all: bool,
+    ) -> Result<Self, Box<BuildError>> {
+        thompson = match direction {
+            Direction::Left => thompson.reverse(true),
+            Direction::Right => thompson.reverse(false),
+        };
+        config = if match_all {
+            config.match_kind(MatchKind::All)
+        } else {
+            config.match_kind(MatchKind::LeftmostFirst)
+        };
+
+        // Create the DFA.
+        let dfa = Builder::new()
+            .configure(config)
+            .syntax(syntax)
+            .thompson(thompson)
+            .build(search)?;
+
+        let cache = dfa.create_cache();
+
+        Ok(Self {
+            direction,
+            cache,
+            dfa,
+            match_all,
+        })
+    }
+}
+
+impl<T: event::EventListener> Crosswords<T> {
+    /// Get next search match in the specified direction.
+    pub fn search_next(
+        &self,
+        regex: &mut RegexSearch,
+        mut origin: Pos,
+        direction: Direction,
+        side: Side,
+        mut max_lines: Option<usize>,
+    ) -> Option<Match> {
+        origin = self.expand_wide(origin, direction);
+
+        max_lines = max_lines.filter(|max_lines| max_lines + 1 < self.grid.total_lines());
+
+        match direction {
+            Direction::Right => self.next_match_right(regex, origin, side, max_lines),
+            Direction::Left => self.next_match_left(regex, origin, side, max_lines),
+        }
+    }
+
+    /// Find the next match to the right of the origin.
+    fn next_match_right(
+        &self,
+        regex: &mut RegexSearch,
+        origin: Pos,
+        side: Side,
+        max_lines: Option<usize>,
+    ) -> Option<Match> {
+        let start = self.row_search_left(origin);
+        let mut end = start;
+
+        // Limit maximum number of lines searched.
+        end = match max_lines {
+            Some(max_lines) => {
+                let line = (start.row + max_lines).grid_clamp(self, Boundary::None);
+                Pos::new(line, self.grid.last_column())
+            }
+            _ => end.sub(self, Boundary::None, 1),
+        };
+
+        let mut regex_iter =
+            RegexIter::new(start, end, Direction::Right, self, regex).peekable();
+
+        // Check if there's any match at all.
+        let first_match = regex_iter.peek()?.clone();
+
+        let regex_match = regex_iter
+            .find(|regex_match| {
+                let match_point = Self::match_side(regex_match, side);
+
+                // If the match's point is beyond the origin, we're done.
+                match_point.row < start.row
+                    || match_point.row > origin.row
+                    || (match_point.row == origin.row && match_point.col >= origin.col)
+            })
+            .unwrap_or(first_match);
+
+        Some(regex_match)
+    }
+
+    /// Find the next match to the left of the origin.
+    fn next_match_left(
+        &self,
+        regex: &mut RegexSearch,
+        origin: Pos,
+        side: Side,
+        max_lines: Option<usize>,
+    ) -> Option<Match> {
+        let start = self.row_search_right(origin);
+        let mut end = start;
+
+        // Limit maximum number of lines searched.
+        end = match max_lines {
+            Some(max_lines) => {
+                let line = (start.row - max_lines).grid_clamp(self, Boundary::None);
+                Pos::new(line, Column(0))
+            }
+            _ => end.add(self, Boundary::None, 1),
+        };
+
+        let mut regex_iter =
+            RegexIter::new(start, end, Direction::Left, self, regex).peekable();
+
+        // Check if there's any match at all.
+        let first_match = regex_iter.peek()?.clone();
+
+        let regex_match = regex_iter
+            .find(|regex_match| {
+                let match_point = Self::match_side(regex_match, side);
+
+                // If the match's point is beyond the origin, we're done.
+                match_point.row > start.row
+                    || match_point.row < origin.row
+                    || (match_point.row == origin.row && match_point.col <= origin.col)
+            })
+            .unwrap_or(first_match);
+
+        Some(regex_match)
+    }
+
+    /// Get the side of a match.
+    fn match_side(regex_match: &Match, side: Side) -> Pos {
+        match side {
+            Side::Right => *regex_match.end(),
+            Side::Left => *regex_match.start(),
+        }
+    }
+
+    /// Find the next regex match to the left of the origin point.
+    ///
+    /// The origin is always included in the regex.
+    pub fn regex_search_left(
+        &self,
+        regex: &mut RegexSearch,
+        start: Pos,
+        end: Pos,
+    ) -> Option<Match> where {
+        // Find start and end of match.
+        let match_start = self.regex_search(start, end, &mut regex.left_fdfa)?;
+        let match_end = self.regex_search(match_start, start, &mut regex.left_rdfa)?;
+
+        Some(match_start..=match_end)
+    }
+
+    /// Find the next regex match to the right of the origin point.
+    ///
+    /// The origin is always included in the regex.
+    pub fn regex_search_right(
+        &self,
+        regex: &mut RegexSearch,
+        start: Pos,
+        end: Pos,
+    ) -> Option<Match> {
+        // Find start and end of match.
+        let match_end = self.regex_search(start, end, &mut regex.right_fdfa)?;
+        let match_start = self.regex_search(match_end, start, &mut regex.right_rdfa)?;
+
+        Some(match_start..=match_end)
+    }
+
+    /// Find the next regex match.
+    ///
+    /// This will always return the side of the first match which is farthest from the start point.
+    fn regex_search(&self, start: Pos, end: Pos, regex: &mut LazyDfa) -> Option<Pos> {
+        match self.regex_search_internal(start, end, regex) {
+            Ok(regex_match) => regex_match,
+            Err(err) => {
+                warn!("Regex exceeded complexity limit");
+                debug!("    {err}");
+                None
+            }
+        }
+    }
+
+    /// Find the next regex match.
+    ///
+    /// To automatically log regex complexity errors, use [`Self::regex_search`] instead.
+    fn regex_search_internal(
+        &self,
+        start: Pos,
+        end: Pos,
+        regex: &mut LazyDfa,
+    ) -> Result<Option<Pos>, Box<dyn Error>> {
+        let topmost_line = self.grid.topmost_line();
+        let screen_lines = self.grid.screen_lines() as i32;
+        let last_column = self.grid.last_column();
+
+        // Advance the iterator.
+        let next = match regex.direction {
+            Direction::Right => GridIterator::next,
+            Direction::Left => GridIterator::prev,
+        };
+
+        // Get start state for the DFA.
+        let regex_anchored = if regex.match_all {
+            Anchored::Yes
+        } else {
+            Anchored::No
+        };
+        let input = Input::new(&[]).anchored(regex_anchored);
+        let mut state = regex
+            .dfa
+            .start_state_forward(&mut regex.cache, &input)
+            .unwrap();
+
+        let mut iter = self.grid.iter_from(start);
+        let mut regex_match = None;
+        let mut done = false;
+
+        let mut cell = iter.square();
+        self.skip_fullwidth(&mut iter, &mut cell, regex.direction);
+        let mut last_wrapped = cell.flags.contains(Flags::WRAPLINE);
+        let mut c = cell.c;
+
+        let mut point = iter.pos();
+        let mut last_point = point;
+        let mut consumed_bytes = 0;
+
+        // Reset the regex state to restart the search.
+        macro_rules! reset_state {
+            () => {{
+                state = regex.dfa.start_state_forward(&mut regex.cache, &input)?;
+                consumed_bytes = 0;
+                regex_match = None;
+            }};
+        }
+
+        'outer: loop {
+            // Convert char to array of bytes.
+            let mut buf = [0; 4];
+            let utf8_len = c.encode_utf8(&mut buf).len();
+
+            // Pass char to DFA as individual bytes.
+            for i in 0..utf8_len {
+                // Inverse byte order when going left.
+                let byte = match regex.direction {
+                    Direction::Right => buf[i],
+                    Direction::Left => buf[utf8_len - i - 1],
+                };
+
+                state = regex.dfa.next_state(&mut regex.cache, state, byte)?;
+                consumed_bytes += 1;
+
+                if i == 0 && state.is_match() {
+                    // Matches require one additional BYTE of lookahead, so we check the match state
+                    // for the first byte of every new character to determine if the last character
+                    // was a match.
+                    regex_match = Some(last_point);
+                } else if state.is_dead() {
+                    if consumed_bytes == 2 {
+                        // Reset search if we found an empty match.
+                        //
+                        // With an unanchored search, a dead state only occurs after the end of a
+                        // match has been found. While we want to abort after the first match has
+                        // ended, we don't want empty matches since we cannot highlight them.
+                        //
+                        // So once we encounter an empty match, we reset our parser state and clear
+                        // the match, effectively starting a new search one character farther than
+                        // before.
+                        //
+                        // An empty match requires consuming `2` bytes, since the first byte will
+                        // report the match for the empty string, while the second byte then
+                        // reports the dead state indicating the first character isn't part of the
+                        // match.
+                        reset_state!();
+
+                        // Retry this character if first byte caused failure.
+                        //
+                        // After finding an empty match, we want to advance the search start by one
+                        // character. So if the first character has multiple bytes and the dead
+                        // state isn't reached at `i == 0`, then we continue with the rest of the
+                        // loop to advance the parser by one character.
+                        if i == 0 {
+                            continue 'outer;
+                        }
+                    } else {
+                        // Abort on dead state.
+                        break 'outer;
+                    }
+                }
+            }
+
+            // Stop once we've reached the target point.
+            if point == end || done {
+                // When reaching the end-of-input, we need to notify the parser that no look-ahead
+                // is possible and check for state changes.
+                state = regex.dfa.next_eoi_state(&mut regex.cache, state)?;
+                if state.is_match() {
+                    regex_match = Some(point);
+                } else if state.is_dead() && consumed_bytes == 1 {
+                    // Ignore empty matches.
+                    regex_match = None;
+                }
+
+                break;
+            }
+
+            // Advance grid cell iterator.
+            let mut cell = match next(&mut iter) {
+                Some(Indexed { square, .. }) => square,
+                None => {
+                    // Wrap around to other end of the scrollback buffer.
+                    let line = topmost_line - point.row + screen_lines - 1;
+                    let start = Pos::new(line, last_column - point.col);
+                    iter = self.grid.iter_from(start);
+                    iter.square()
+                }
+            };
+
+            // Check for completion before potentially skipping over fullwidth characters.
+            done = iter.pos() == end;
+
+            self.skip_fullwidth(&mut iter, &mut cell, regex.direction);
+
+            let wrapped = cell.flags.contains(Flags::WRAPLINE);
+            c = cell.c;
+
+            last_point = mem::replace(&mut point, iter.pos());
+
+            // Handle linebreaks.
+            if (last_point.col == last_column && point.col == Column(0) && !last_wrapped)
+                || (last_point.col == Column(0) && point.col == last_column && !wrapped)
+            {
+                // When reaching the end-of-input, we need to notify the parser that no
+                // look-ahead is possible and check if the current state is still a match.
+                state = regex.dfa.next_eoi_state(&mut regex.cache, state)?;
+                if state.is_match() {
+                    regex_match = Some(last_point);
+                }
+
+                match regex_match {
+                    // Stop if we found a non-empty match before the linebreak.
+                    Some(_)
+                        if (!state.is_dead() || consumed_bytes > 1)
+                            && consumed_bytes != 0 =>
+                    {
+                        break;
+                    }
+                    _ => reset_state!(),
+                }
+            }
+
+            last_wrapped = wrapped;
+        }
+
+        Ok(regex_match)
+    }
+
+    /// Advance a grid iterator over fullwidth characters.
+    fn skip_fullwidth<'a>(
+        &self,
+        iter: &'a mut GridIterator<'_, Square>,
+        square: &mut &'a Square,
+        direction: Direction,
+    ) {
+        match direction {
+            // In the alternate screen buffer there might not be a wide char spacer after a wide
+            // char, so we only advance the iterator when the wide char is not in the last column.
+            Direction::Right
+                if square.flags.contains(Flags::WIDE_CHAR)
+                    && iter.pos().col < self.grid.last_column() =>
+            {
+                iter.next();
+            }
+            Direction::Right
+                if square.flags.contains(Flags::LEADING_WIDE_CHAR_SPACER) =>
+            {
+                if let Some(Indexed {
+                    square: new_cell, ..
+                }) = iter.next()
+                {
+                    *square = new_cell;
+                }
+                iter.next();
+            }
+            Direction::Left if square.flags.contains(Flags::WIDE_CHAR_SPACER) => {
+                if let Some(Indexed {
+                    square: new_cell, ..
+                }) = iter.prev()
+                {
+                    *square = new_cell;
+                }
+
+                let prev = iter.pos().sub(&self.grid, Boundary::Grid, 1);
+                if self.grid[prev]
+                    .flags
+                    .contains(Flags::LEADING_WIDE_CHAR_SPACER)
+                {
+                    iter.prev();
+                }
+            }
+            _ => (),
+        }
+    }
+
+    /// Find next matching bracket.
+    pub fn bracket_search(&self, point: Pos) -> Option<Pos> {
+        let start_char = self.grid[point].c;
+
+        // Find the matching bracket we're looking for
+        let (forward, end_char) = BRACKET_PAIRS.iter().find_map(|(open, close)| {
+            if open == &start_char {
+                Some((true, *close))
+            } else if close == &start_char {
+                Some((false, *open))
+            } else {
+                None
+            }
+        })?;
+
+        let mut iter = self.grid.iter_from(point);
+
+        // For every character match that equals the starting bracket, we
+        // ignore one bracket of the opposite type.
+        let mut skip_pairs = 0;
+
+        loop {
+            // Check the next cell
+            let cell = if forward { iter.next() } else { iter.prev() };
+
+            // Break if there are no more cells
+            let cell = match cell {
+                Some(cell) => cell,
+                None => break,
+            };
+
+            // Check if the bracket matches
+            if cell.c == end_char && skip_pairs == 0 {
+                return Some(cell.pos);
+            } else if cell.c == start_char {
+                skip_pairs += 1;
+            } else if cell.c == end_char {
+                skip_pairs -= 1;
+            }
+        }
+
+        None
+    }
+
+    /// Find left end of semantic block.
+    #[must_use]
+    pub fn semantic_search_left(&self, point: Pos) -> Pos {
+        match self.inline_search_left(point, self.semantic_escape_chars()) {
+            Ok(point) => self
+                .grid
+                .iter_from(point)
+                .next()
+                .map_or(point, |cell| cell.pos),
+            Err(point) => point,
+        }
+    }
+
+    /// Find right end of semantic block.
+    #[must_use]
+    pub fn semantic_search_right(&self, point: Pos) -> Pos {
+        match self.inline_search_right(point, self.semantic_escape_chars()) {
+            Ok(point) => self
+                .grid
+                .iter_from(point)
+                .prev()
+                .map_or(point, |cell| cell.pos),
+            Err(point) => point,
+        }
+    }
+
+    /// Searching to the left, find the next character contained in `needles`.
+    pub fn inline_search_left(&self, mut point: Pos, needles: &str) -> Result<Pos, Pos> {
+        // Limit the starting point to the last line in the history
+        point.row = max(point.row, self.grid.topmost_line());
+
+        let mut iter = self.grid.iter_from(point);
+        let last_column = self.grid.columns() - 1;
+
+        let wide =
+            Flags::WIDE_CHAR | Flags::WIDE_CHAR_SPACER | Flags::LEADING_WIDE_CHAR_SPACER;
+        while let Some(cell) = iter.prev() {
+            if cell.pos.col == last_column && !cell.flags.contains(Flags::WRAPLINE) {
+                break;
+            }
+
+            point = cell.pos;
+
+            if !cell.flags.intersects(wide) && needles.contains(cell.c) {
+                return Ok(point);
+            }
+        }
+
+        Err(point)
+    }
+
+    /// Searching to the right, find the next character contained in `needles`.
+    pub fn inline_search_right(&self, mut point: Pos, needles: &str) -> Result<Pos, Pos> {
+        // Limit the starting point to the last line in the history
+        point.row = max(point.row, self.grid.topmost_line());
+
+        let wide =
+            Flags::WIDE_CHAR | Flags::WIDE_CHAR_SPACER | Flags::LEADING_WIDE_CHAR_SPACER;
+        let last_column = self.grid.columns() - 1;
+
+        // Immediately stop if start point in on line break.
+        if point.col == last_column && !self.grid[point].flags.contains(Flags::WRAPLINE) {
+            return Err(point);
+        }
+
+        for cell in self.grid.iter_from(point) {
+            point = cell.pos;
+
+            if !cell.flags.intersects(wide) && needles.contains(cell.c) {
+                return Ok(point);
+            }
+
+            if point.col == last_column && !cell.flags.contains(Flags::WRAPLINE) {
+                break;
+            }
+        }
+
+        Err(point)
+    }
+
+    /// Find the beginning of the current line across linewraps.
+    pub fn line_search_left(&self, mut point: Pos) -> Pos {
+        while point.row > self.grid.topmost_line()
+            && self.grid[point.row - 1i32][self.grid.last_column()]
+                .flags
+                .contains(Flags::WRAPLINE)
+        {
+            point.row -= 1;
+        }
+
+        point.col = Column(0);
+
+        point
+    }
+
+    /// Find the end of the current line across linewraps.
+    pub fn line_search_right(&self, mut point: Pos) -> Pos {
+        while point.row + 1 < self.grid.screen_lines()
+            && self.grid[point.row][self.grid.last_column()]
+                .flags
+                .contains(Flags::WRAPLINE)
+        {
+            point.row += 1;
+        }
+
+        point.col = self.grid.last_column();
+
+        point
+    }
+}
+
+/// Iterator over regex matches.
+pub struct RegexIter<'a, T: event::EventListener> {
+    pos: Pos,
+    end: Pos,
+    direction: Direction,
+    regex: &'a mut RegexSearch,
+    term: &'a Crosswords<T>,
+    done: bool,
+}
+
+impl<'a, T: event::EventListener> RegexIter<'a, T> {
+    pub fn new(
+        pos: Pos,
+        end: Pos,
+        direction: Direction,
+        term: &'a Crosswords<T>,
+        regex: &'a mut RegexSearch,
+    ) -> Self {
+        Self {
+            pos,
+            done: false,
+            end,
+            direction,
+            term,
+            regex,
+        }
+    }
+
+    /// Skip one cell, advancing the origin point to the next one.
+    fn skip(&mut self) {
+        self.pos = self.term.expand_wide(self.pos, self.direction);
+
+        self.pos = match self.direction {
+            Direction::Right => self.pos.add(self.term, Boundary::None, 1),
+            Direction::Left => self.pos.sub(self.term, Boundary::None, 1),
+        };
+    }
+
+    /// Get the next match in the specified direction.
+    fn next_match(&mut self) -> Option<Match> {
+        match self.direction {
+            Direction::Right => {
+                self.term.regex_search_right(self.regex, self.pos, self.end)
+            }
+            Direction::Left => {
+                self.term.regex_search_left(self.regex, self.pos, self.end)
+            }
+        }
+    }
+}
+
+impl<'a, T: event::EventListener> Iterator for RegexIter<'a, T> {
+    type Item = Match;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if self.done {
+            return None;
+        }
+
+        // Since the end itself might be a single cell match, we search one more time.
+        if self.pos == self.end {
+            self.done = true;
+        }
+
+        let regex_match = self.next_match()?;
+
+        self.pos = *regex_match.end();
+        if self.pos == self.end {
+            // Stop when the match terminates right on the end limit.
+            self.done = true;
+        } else {
+            // Move the new search origin past the match.
+            self.skip();
+        }
+
+        Some(regex_match)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    use crate::crosswords::pos::{Column, Line};
+    use crate::crosswords::CrosswordsSize;
+    use crate::crosswords::CursorShape;
+    use crate::event::VoidListener;
+    use unicode_width::UnicodeWidthChar;
+
+    pub fn mock_term(content: &str) -> Crosswords<VoidListener> {
+        let lines: Vec<&str> = content.split('\n').collect();
+        let num_cols = lines
+            .iter()
+            .map(|line| {
+                line.chars()
+                    .filter(|c| *c != '\r')
+                    .map(|c| c.width().unwrap())
+                    .sum()
+            })
+            .max()
+            .unwrap_or(0);
+
+        // Create terminal with the appropriate dimensions.
+        #[cfg(not(use_wa))]
+        let window_id = crate::event::WindowId::from(0);
+        #[cfg(use_wa)]
+        let window_id = 0;
+        let size = CrosswordsSize::new(num_cols, lines.len());
+        let mut term =
+            Crosswords::new(size, CursorShape::Block, VoidListener {}, window_id, 0);
+
+        // Fill terminal with content.
+        for (line, text) in lines.iter().enumerate() {
+            let line = Line(line as i32);
+            if !text.ends_with('\r') && line + 1 != lines.len() {
+                term.grid[line][Column(num_cols - 1)]
+                    .flags
+                    .insert(Flags::WRAPLINE);
+            }
+
+            let mut index = 0;
+            for c in text.chars().take_while(|c| *c != '\r') {
+                term.grid[line][Column(index)].c = c;
+
+                // Handle fullwidth characters.
+                let width = c.width().unwrap();
+                if width == 2 {
+                    term.grid[line][Column(index)]
+                        .flags
+                        .insert(Flags::WIDE_CHAR);
+                    term.grid[line][Column(index + 1)]
+                        .flags
+                        .insert(Flags::WIDE_CHAR_SPACER);
+                }
+
+                index += width;
+            }
+        }
+
+        term
+    }
+
+    #[test]
+    fn regex_right() {
+        #[rustfmt::skip]
+        let term = mock_term("\
+            testing66\r\n\
+            Rio Terminal\n\
+            123\r\n\
+            Rio Terminal\r\n\
+            123\
+        ");
+
+        // Check regex across wrapped and unwrapped lines.
+        let mut regex = RegexSearch::new("Rio.*123").unwrap();
+        let start = Pos::new(Line(1), Column(0));
+        let end = Pos::new(Line(4), Column(2));
+        let match_start = Pos::new(Line(1), Column(0));
+        let match_end = Pos::new(Line(2), Column(2));
+        assert_eq!(
+            term.regex_search_right(&mut regex, start, end),
+            Some(match_start..=match_end)
+        );
+    }
+
+    #[test]
+    fn regex_left() {
+        #[rustfmt::skip]
+        let term = mock_term("\
+            testing66\r\n\
+            Rio Terminal\n\
+            123\r\n\
+            Rio Terminal\r\n\
+            123\
+        ");
+
+        // Check regex across wrapped and unwrapped lines.
+        let mut regex = RegexSearch::new("Rio.*123").unwrap();
+        let start = Pos::new(Line(4), Column(2));
+        let end = Pos::new(Line(1), Column(0));
+        let match_start = Pos::new(Line(1), Column(0));
+        let match_end = Pos::new(Line(2), Column(2));
+        assert_eq!(
+            term.regex_search_left(&mut regex, start, end),
+            Some(match_start..=match_end)
+        );
+    }
+
+    #[test]
+    fn nested_regex() {
+        #[rustfmt::skip]
+        let term = mock_term("\
+            Rio -> Riotermin -> termin\r\n\
+            termin\
+        ");
+
+        // Greedy stopped at linebreak.
+        let mut regex = RegexSearch::new("Rio.*termin").unwrap();
+        let start = Pos::new(Line(0), Column(0));
+        let end = Pos::new(Line(0), Column(25));
+        assert_eq!(
+            term.regex_search_right(&mut regex, start, end),
+            Some(start..=end)
+        );
+
+        // Greedy stopped at dead state.
+        let mut regex = RegexSearch::new("Rio[^y]*termin").unwrap();
+        let start = Pos::new(Line(0), Column(0));
+        let end = Pos::new(Line(0), Column(15));
+        assert_eq!(
+            term.regex_search_right(&mut regex, start, end),
+            Some(start..=end)
+        );
+    }
+
+    #[test]
+    fn no_match_right() {
+        #[rustfmt::skip]
+        let term = mock_term("\
+            first line\n\
+            broken second\r\n\
+            third\
+        ");
+
+        let mut regex = RegexSearch::new("nothing").unwrap();
+        let start = Pos::new(Line(0), Column(0));
+        let end = Pos::new(Line(2), Column(4));
+        assert_eq!(term.regex_search_right(&mut regex, start, end), None);
+    }
+
+    #[test]
+    fn no_match_left() {
+        #[rustfmt::skip]
+        let term = mock_term("\
+            first line\n\
+            broken second\r\n\
+            third\
+        ");
+
+        let mut regex = RegexSearch::new("nothing").unwrap();
+        let start = Pos::new(Line(2), Column(4));
+        let end = Pos::new(Line(0), Column(0));
+        assert_eq!(term.regex_search_left(&mut regex, start, end), None);
+    }
+
+    #[test]
+    fn include_linebreak_left() {
+        #[rustfmt::skip]
+        let term = mock_term("\
+            testing123\r\n\
+            xxx\
+        ");
+
+        // Make sure the cell containing the linebreak is not skipped.
+        let mut regex = RegexSearch::new("te.*123").unwrap();
+        let start = Pos::new(Line(1), Column(0));
+        let end = Pos::new(Line(0), Column(0));
+        let match_start = Pos::new(Line(0), Column(0));
+        let match_end = Pos::new(Line(0), Column(9));
+        assert_eq!(
+            term.regex_search_left(&mut regex, start, end),
+            Some(match_start..=match_end)
+        );
+    }
+
+    #[test]
+    fn include_linebreak_right() {
+        #[rustfmt::skip]
+        let term = mock_term("\
+            xxx\r\n\
+            testing123\
+        ");
+
+        // Make sure the cell containing the linebreak is not skipped.
+        let mut regex = RegexSearch::new("te.*123").unwrap();
+        let start = Pos::new(Line(0), Column(2));
+        let end = Pos::new(Line(1), Column(9));
+        let match_start = Pos::new(Line(1), Column(0));
+        assert_eq!(
+            term.regex_search_right(&mut regex, start, end),
+            Some(match_start..=end)
+        );
+    }
+
+    #[test]
+    fn skip_dead_cell() {
+        let term = mock_term("rioterminal");
+
+        // Make sure dead state cell is skipped when reversing.
+        let mut regex = RegexSearch::new("rioterm").unwrap();
+        let start = Pos::new(Line(0), Column(0));
+        let end = Pos::new(Line(0), Column(6));
+        assert_eq!(
+            term.regex_search_right(&mut regex, start, end),
+            Some(start..=end)
+        );
+    }
+
+    #[test]
+    fn reverse_search_dead_recovery() {
+        let term = mock_term("zooo lense");
+
+        // Make sure the reverse DFA operates the same as a forward DFA.
+        let mut regex = RegexSearch::new("zoo").unwrap();
+        let start = Pos::new(Line(0), Column(9));
+        let end = Pos::new(Line(0), Column(0));
+        let match_start = Pos::new(Line(0), Column(0));
+        let match_end = Pos::new(Line(0), Column(2));
+        assert_eq!(
+            term.regex_search_left(&mut regex, start, end),
+            Some(match_start..=match_end)
+        );
+    }
+
+    #[test]
+    fn multibyte_unicode() {
+        let term = mock_term("testвосибing");
+
+        let mut regex = RegexSearch::new("te.*ing").unwrap();
+        let start = Pos::new(Line(0), Column(0));
+        let end = Pos::new(Line(0), Column(11));
+        assert_eq!(
+            term.regex_search_right(&mut regex, start, end),
+            Some(start..=end)
+        );
+
+        let mut regex = RegexSearch::new("te.*ing").unwrap();
+        let start = Pos::new(Line(0), Column(11));
+        let end = Pos::new(Line(0), Column(0));
+        assert_eq!(
+            term.regex_search_left(&mut regex, start, end),
+            Some(end..=start)
+        );
+    }
+
+    #[test]
+    fn end_on_multibyte_unicode() {
+        let term = mock_term("testвосиб");
+
+        let mut regex = RegexSearch::new("te.*и").unwrap();
+        let start = Pos::new(Line(0), Column(0));
+        let end = Pos::new(Line(0), Column(8));
+        let match_end = Pos::new(Line(0), Column(7));
+        assert_eq!(
+            term.regex_search_right(&mut regex, start, end),
+            Some(start..=match_end)
+        );
+    }
+
+    #[test]
+    fn fullwidth() {
+        let term = mock_term("a🦇x🦇");
+
+        let mut regex = RegexSearch::new("[^ ]*").unwrap();
+        let start = Pos::new(Line(0), Column(0));
+        let end = Pos::new(Line(0), Column(5));
+        assert_eq!(
+            term.regex_search_right(&mut regex, start, end),
+            Some(start..=end)
+        );
+
+        let mut regex = RegexSearch::new("[^ ]*").unwrap();
+        let start = Pos::new(Line(0), Column(5));
+        let end = Pos::new(Line(0), Column(0));
+        assert_eq!(
+            term.regex_search_left(&mut regex, start, end),
+            Some(end..=start)
+        );
+    }
+
+    #[test]
+    fn singlecell_fullwidth() {
+        let term = mock_term("🦇");
+
+        let mut regex = RegexSearch::new("🦇").unwrap();
+        let start = Pos::new(Line(0), Column(0));
+        let end = Pos::new(Line(0), Column(1));
+        assert_eq!(
+            term.regex_search_right(&mut regex, start, end),
+            Some(start..=end)
+        );
+
+        let mut regex = RegexSearch::new("🦇").unwrap();
+        let start = Pos::new(Line(0), Column(1));
+        let end = Pos::new(Line(0), Column(0));
+        assert_eq!(
+            term.regex_search_left(&mut regex, start, end),
+            Some(end..=start)
+        );
+    }
+
+    #[test]
+    fn end_on_fullwidth() {
+        let term = mock_term("jarr🦇");
+
+        let start = Pos::new(Line(0), Column(0));
+        let end = Pos::new(Line(0), Column(4));
+
+        // Ensure ending without a match doesn't loop indefinitely.
+        let mut regex = RegexSearch::new("x").unwrap();
+        assert_eq!(term.regex_search_right(&mut regex, start, end), None);
+
+        let mut regex = RegexSearch::new("x").unwrap();
+        let match_end = Pos::new(Line(0), Column(5));
+        assert_eq!(term.regex_search_right(&mut regex, start, match_end), None);
+
+        // Ensure match is captured when only partially inside range.
+        let mut regex = RegexSearch::new("jarr🦇").unwrap();
+        assert_eq!(
+            term.regex_search_right(&mut regex, start, end),
+            Some(start..=match_end)
+        );
+    }
+
+    #[test]
+    fn wrapping() {
+        #[rustfmt::skip]
+        let term = mock_term("\
+            xxx\r\n\
+            xxx\
+        ");
+
+        let mut regex = RegexSearch::new("xxx").unwrap();
+        let start = Pos::new(Line(0), Column(2));
+        let end = Pos::new(Line(1), Column(2));
+        let match_start = Pos::new(Line(1), Column(0));
+        assert_eq!(
+            term.regex_search_right(&mut regex, start, end),
+            Some(match_start..=end)
+        );
+
+        let mut regex = RegexSearch::new("xxx").unwrap();
+        let start = Pos::new(Line(1), Column(0));
+        let end = Pos::new(Line(0), Column(0));
+        let match_end = Pos::new(Line(0), Column(2));
+        assert_eq!(
+            term.regex_search_left(&mut regex, start, end),
+            Some(end..=match_end)
+        );
+    }
+
+    #[test]
+    fn wrapping_into_fullwidth() {
+        #[rustfmt::skip]
+        let term = mock_term("\
+            🦇xx\r\n\
+            xx🦇\
+        ");
+
+        let mut regex = RegexSearch::new("🦇x").unwrap();
+        let start = Pos::new(Line(0), Column(0));
+        let end = Pos::new(Line(1), Column(3));
+        let match_start = Pos::new(Line(0), Column(0));
+        let match_end = Pos::new(Line(0), Column(2));
+        assert_eq!(
+            term.regex_search_right(&mut regex, start, end),
+            Some(match_start..=match_end)
+        );
+
+        let mut regex = RegexSearch::new("x🦇").unwrap();
+        let start = Pos::new(Line(1), Column(2));
+        let end = Pos::new(Line(0), Column(0));
+        let match_start = Pos::new(Line(1), Column(1));
+        let match_end = Pos::new(Line(1), Column(3));
+        assert_eq!(
+            term.regex_search_left(&mut regex, start, end),
+            Some(match_start..=match_end)
+        );
+    }
+
+    #[test]
+    fn multiline() {
+        #[rustfmt::skip]
+        let term = mock_term("\
+            test \r\n\
+            test\
+        ");
+
+        const PATTERN: &str = "[a-z]*";
+        let mut regex = RegexSearch::new(PATTERN).unwrap();
+        let start = Pos::new(Line(0), Column(0));
+        let end = Pos::new(Line(0), Column(3));
+        let match_start = Pos::new(Line(0), Column(0));
+        assert_eq!(
+            term.regex_search_right(&mut regex, start, end),
+            Some(match_start..=end)
+        );
+
+        let mut regex = RegexSearch::new(PATTERN).unwrap();
+        let start = Pos::new(Line(0), Column(4));
+        let end = Pos::new(Line(0), Column(0));
+        let match_start = Pos::new(Line(1), Column(0));
+        let match_end = Pos::new(Line(1), Column(3));
+        assert_eq!(
+            term.regex_search_right(&mut regex, start, end),
+            Some(match_start..=match_end)
+        );
+    }
+
+    #[test]
+    fn empty_match() {
+        #[rustfmt::skip]
+        let term = mock_term(" abc ");
+
+        const PATTERN: &str = "[a-z]*";
+        let mut regex = RegexSearch::new(PATTERN).unwrap();
+        let start = Pos::new(Line(0), Column(0));
+        let end = Pos::new(Line(0), Column(4));
+        let match_start = Pos::new(Line(0), Column(1));
+        let match_end = Pos::new(Line(0), Column(3));
+        assert_eq!(
+            term.regex_search_right(&mut regex, start, end),
+            Some(match_start..=match_end)
+        );
+    }
+
+    #[test]
+    fn empty_match_multibyte() {
+        #[rustfmt::skip]
+        let term = mock_term(" ↑");
+
+        const PATTERN: &str = "[a-z]*";
+        let mut regex = RegexSearch::new(PATTERN).unwrap();
+        let start = Pos::new(Line(0), Column(0));
+        let end = Pos::new(Line(0), Column(1));
+        assert_eq!(term.regex_search_right(&mut regex, start, end), None);
+    }
+
+    #[test]
+    fn empty_match_multiline() {
+        #[rustfmt::skip]
+        let term = mock_term("abc          \nxxx");
+
+        const PATTERN: &str = "[a-z]*";
+        let mut regex = RegexSearch::new(PATTERN).unwrap();
+        let start = Pos::new(Line(0), Column(3));
+        let end = Pos::new(Line(1), Column(2));
+        let match_start = Pos::new(Line(1), Column(0));
+        let match_end = Pos::new(Line(1), Column(2));
+        assert_eq!(
+            term.regex_search_right(&mut regex, start, end),
+            Some(match_start..=match_end)
+        );
+    }
+
+    #[test]
+    fn leading_spacer() {
+        #[rustfmt::skip]
+        let mut term = mock_term("\
+            xxx \n\
+            🦇xx\
+        ");
+        term.grid[Line(0)][Column(3)]
+            .flags
+            .insert(Flags::LEADING_WIDE_CHAR_SPACER);
+
+        let mut regex = RegexSearch::new("🦇x").unwrap();
+        let start = Pos::new(Line(0), Column(0));
+        let end = Pos::new(Line(1), Column(3));
+        let match_start = Pos::new(Line(0), Column(3));
+        let match_end = Pos::new(Line(1), Column(2));
+        assert_eq!(
+            term.regex_search_right(&mut regex, start, end),
+            Some(match_start..=match_end)
+        );
+
+        let mut regex = RegexSearch::new("🦇x").unwrap();
+        let start = Pos::new(Line(1), Column(3));
+        let end = Pos::new(Line(0), Column(0));
+        let match_start = Pos::new(Line(0), Column(3));
+        let match_end = Pos::new(Line(1), Column(2));
+        assert_eq!(
+            term.regex_search_left(&mut regex, start, end),
+            Some(match_start..=match_end)
+        );
+
+        let mut regex = RegexSearch::new("x🦇").unwrap();
+        let start = Pos::new(Line(0), Column(0));
+        let end = Pos::new(Line(1), Column(3));
+        let match_start = Pos::new(Line(0), Column(2));
+        let match_end = Pos::new(Line(1), Column(1));
+        assert_eq!(
+            term.regex_search_right(&mut regex, start, end),
+            Some(match_start..=match_end)
+        );
+
+        let mut regex = RegexSearch::new("x🦇").unwrap();
+        let start = Pos::new(Line(1), Column(3));
+        let end = Pos::new(Line(0), Column(0));
+        let match_start = Pos::new(Line(0), Column(2));
+        let match_end = Pos::new(Line(1), Column(1));
+        assert_eq!(
+            term.regex_search_left(&mut regex, start, end),
+            Some(match_start..=match_end)
+        );
+    }
+
+    #[test]
+    fn wide_without_spacer() {
+        #[cfg(not(use_wa))]
+        let window_id = crate::event::WindowId::from(0);
+        #[cfg(use_wa)]
+        let window_id = 0;
+        let size = CrosswordsSize::new(2, 2);
+        let mut term =
+            Crosswords::new(size, CursorShape::Block, VoidListener {}, window_id, 0);
+        term.grid[Line(0)][Column(0)].c = 'x';
+        term.grid[Line(0)][Column(1)].c = '字';
+        term.grid[Line(0)][Column(1)].flags = Flags::WIDE_CHAR;
+
+        let mut regex = RegexSearch::new("test").unwrap();
+
+        let start = Pos::new(Line(0), Column(0));
+        let end = Pos::new(Line(0), Column(1));
+
+        let mut iter = RegexIter::new(start, end, Direction::Right, &term, &mut regex);
+        assert_eq!(iter.next(), None);
+    }
+
+    #[test]
+    fn wrap_around_to_another_end() {
+        #[rustfmt::skip]
+        let term = mock_term("\
+            abc\r\n\
+            def\
+        ");
+
+        // Bottom to top.
+        let mut regex = RegexSearch::new("abc").unwrap();
+        let start = Pos::new(Line(1), Column(0));
+        let end = Pos::new(Line(0), Column(2));
+        let match_start = Pos::new(Line(0), Column(0));
+        let match_end = Pos::new(Line(0), Column(2));
+        assert_eq!(
+            term.regex_search_right(&mut regex, start, end),
+            Some(match_start..=match_end)
+        );
+
+        // Top to bottom.
+        let mut regex = RegexSearch::new("def").unwrap();
+        let start = Pos::new(Line(0), Column(2));
+        let end = Pos::new(Line(1), Column(0));
+        let match_start = Pos::new(Line(1), Column(0));
+        let match_end = Pos::new(Line(1), Column(2));
+        assert_eq!(
+            term.regex_search_left(&mut regex, start, end),
+            Some(match_start..=match_end)
+        );
+    }
+
+    #[test]
+    fn nfa_compile_error() {
+        assert!(RegexSearch::new("[0-9A-Za-z]{9999999}").is_err());
+    }
+
+    #[test]
+    fn runtime_cache_error() {
+        let term = mock_term(&str::repeat("i", 9999));
+
+        let mut regex = RegexSearch::new("[0-9A-Za-z]{9999}").unwrap();
+        let start = Pos::new(Line(0), Column(0));
+        let end = Pos::new(Line(0), Column(9999));
+        assert_eq!(term.regex_search_right(&mut regex, start, end), None);
+    }
+
+    #[test]
+    fn greed_is_good() {
+        #[rustfmt::skip]
+        let term = mock_term("https://github.com");
+
+        // Bottom to top.
+        let mut regex = RegexSearch::new("/github.com|https://github.com").unwrap();
+        let start = Pos::new(Line(0), Column(0));
+        let end = Pos::new(Line(0), Column(17));
+        assert_eq!(
+            term.regex_search_right(&mut regex, start, end),
+            Some(start..=end)
+        );
+    }
+
+    #[test]
+    fn anchored_empty() {
+        #[rustfmt::skip]
+        let term = mock_term("rust");
+
+        // Bottom to top.
+        let mut regex = RegexSearch::new(";*|rust").unwrap();
+        let start = Pos::new(Line(0), Column(0));
+        let end = Pos::new(Line(0), Column(3));
+        assert_eq!(
+            term.regex_search_right(&mut regex, start, end),
+            Some(start..=end)
+        );
+    }
+
+    #[test]
+    fn newline_breaking_semantic() {
+        #[rustfmt::skip]
+        let term = mock_term("\
+            test abc\r\n\
+            def test\
+        ");
+
+        // Start at last character.
+        let start = term.semantic_search_left(Pos::new(Line(0), Column(7)));
+        let end = term.semantic_search_right(Pos::new(Line(0), Column(7)));
+        assert_eq!(start, Pos::new(Line(0), Column(5)));
+        assert_eq!(end, Pos::new(Line(0), Column(7)));
+
+        // Start at first character.
+        let start = term.semantic_search_left(Pos::new(Line(1), Column(0)));
+        let end = term.semantic_search_right(Pos::new(Line(1), Column(0)));
+        assert_eq!(start, Pos::new(Line(1), Column(0)));
+        assert_eq!(end, Pos::new(Line(1), Column(2)));
+    }
+
+    #[test]
+    fn inline_word_search() {
+        #[rustfmt::skip]
+        let term = mock_term("\
+            word word word word w\n\
+            ord word word word\
+        ");
+
+        let mut regex = RegexSearch::new("word").unwrap();
+        let start = Pos::new(Line(1), Column(4));
+        let end = Pos::new(Line(0), Column(0));
+        let match_start = Pos::new(Line(0), Column(20));
+        let match_end = Pos::new(Line(1), Column(2));
+        assert_eq!(
+            term.regex_search_left(&mut regex, start, end),
+            Some(match_start..=match_end)
+        );
+    }
+}
diff --git a/rio-backend/src/event/mod.rs b/rio-backend/src/event/mod.rs
index 262933c5a0..6fe0b16312 100644
--- a/rio-backend/src/event/mod.rs
+++ b/rio-backend/src/event/mod.rs
@@ -3,8 +3,11 @@ pub mod sync;
 use crate::clipboard::ClipboardType;
 use crate::config::colors::ColorRgb;
 use crate::crosswords::grid::Scroll;
+use crate::crosswords::pos::{Direction, Pos};
+use crate::crosswords::search::{Match, RegexSearch};
 use crate::error::RioError;
 use std::borrow::Cow;
+use std::collections::VecDeque;
 use std::fmt::Debug;
 use std::fmt::Formatter;
 use std::sync::Arc;
@@ -264,3 +267,85 @@ impl EventListener for EventProxy {
         let _ = self.proxy.send_event(EventPayload::new(event.into(), id));
     }
 }
+
+/// Regex search state.
+pub struct SearchState {
+    /// Search direction.
+    pub direction: Direction,
+
+    /// Current position in the search history.
+    pub history_index: Option<usize>,
+
+    /// Change in display offset since the beginning of the search.
+    pub display_offset_delta: i32,
+
+    /// Search origin in viewport coordinates relative to original display offset.
+    pub origin: Pos,
+
+    /// Focused match during active search.
+    pub focused_match: Option<Match>,
+
+    /// Search regex and history.
+    ///
+    /// During an active search, the first element is the user's current input.
+    ///
+    /// While going through history, the [`SearchState::history_index`] will point to the element
+    /// in history which is currently being previewed.
+    pub history: VecDeque<String>,
+
+    /// Compiled search automatons.
+    pub dfas: Option<RegexSearch>,
+}
+
+impl SearchState {
+    /// Search regex text if a search is active.
+    pub fn regex(&self) -> Option<&String> {
+        self.history_index.and_then(|index| self.history.get(index))
+    }
+
+    /// Direction of the search from the search origin.
+    pub fn direction(&self) -> Direction {
+        self.direction
+    }
+
+    /// Focused match during vi-less search.
+    pub fn focused_match(&self) -> Option<&Match> {
+        self.focused_match.as_ref()
+    }
+
+    /// Clear the focused match.
+    pub fn clear_focused_match(&mut self) {
+        self.focused_match = None;
+    }
+
+    /// Active search dfas.
+    pub fn dfas_mut(&mut self) -> Option<&mut RegexSearch> {
+        self.dfas.as_mut()
+    }
+
+    /// Active search dfas.
+    pub fn dfas(&self) -> Option<&RegexSearch> {
+        self.dfas.as_ref()
+    }
+
+    /// Search regex text if a search is active.
+    #[allow(dead_code)]
+    fn regex_mut(&mut self) -> Option<&mut String> {
+        self.history_index
+            .and_then(move |index| self.history.get_mut(index))
+    }
+}
+
+impl Default for SearchState {
+    fn default() -> Self {
+        Self {
+            direction: Direction::Right,
+            display_offset_delta: Default::default(),
+            focused_match: Default::default(),
+            history_index: Default::default(),
+            history: Default::default(),
+            origin: Default::default(),
+            dfas: Default::default(),
+        }
+    }
+}


Test patch: