From 0c217c5129fe9cb7468dc98f71457f9cc2852ce2 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Tue, 17 Mar 2026 09:24:58 +0100 Subject: [PATCH] added search functionality --- README.md | 3 +++ config.example.toml | 3 +++ src/app.rs | 56 +++++++++++++++++++++++++++++++++++++++++++++ src/config.rs | 6 +++++ src/runner.rs | 30 ++++++++++++++++++++++++ src/ui.rs | 18 +++++++++++++-- 6 files changed, 114 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 62fa897..723bbbc 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,9 @@ mould config.template.json -o config.json - `j` / `Down`: Move selection down - `k` / `Up`: Move selection up - `i`: Edit the value of the currently selected key (Enter Insert Mode) + - `/`: Search for configuration keys (Jump to matches) + - `n`: Jump to the next search match + - `N`: Jump to the previous search match - `:w` or `Enter`: Save the current configuration to the output file - `:q` or `q`: Quit the application - `:wq`: Save and quit diff --git a/config.example.toml b/config.example.toml index e48616c..7a9be3f 100644 --- a/config.example.toml +++ b/config.example.toml @@ -44,3 +44,6 @@ edit = "i" save = ":w" quit = ":q" normal_mode = "Esc" +search = "/" +next_match = "n" +previous_match = "N" diff --git a/src/app.rs b/src/app.rs index bac4047..2fba0f1 100644 --- a/src/app.rs +++ b/src/app.rs @@ -7,6 +7,8 @@ pub enum Mode { Normal, /// Active text entry mode for modifying values. Insert, + /// Active search mode for filtering keys. + Search, } /// The core application state, holding all configuration variables and UI status. @@ -23,6 +25,8 @@ pub struct App { pub status_message: Option, /// The active text input buffer for the selected variable. pub input: Input, + /// The current search query for filtering keys. + pub search_query: String, } impl App { @@ -36,9 +40,24 @@ impl App { running: true, status_message: None, input: Input::new(initial_input), + search_query: String::new(), } } + /// Returns the indices of variables that match the search query. + pub fn matching_indices(&self) -> Vec { + if self.search_query.is_empty() { + return Vec::new(); + } + let query = self.search_query.to_lowercase(); + self.vars + .iter() + .enumerate() + .filter(|(_, v)| v.key.to_lowercase().contains(&query)) + .map(|(i, _)| i) + .collect() + } + /// Moves the selection to the next variable in the list, wrapping around if necessary. pub fn next(&mut self) { if !self.vars.is_empty() { @@ -59,6 +78,43 @@ impl App { } } + /// Jumps to the next variable that matches the search query. + pub fn jump_next_match(&mut self) { + let indices = self.matching_indices(); + if indices.is_empty() { + return; + } + + let next_match = indices + .iter() + .find(|&&i| i > self.selected) + .or_else(|| indices.first()); + + if let Some(&index) = next_match { + self.selected = index; + self.sync_input_with_selected(); + } + } + + /// Jumps to the previous variable that matches the search query. + pub fn jump_previous_match(&mut self) { + let indices = self.matching_indices(); + if indices.is_empty() { + return; + } + + let prev_match = indices + .iter() + .rev() + .find(|&&i| i < self.selected) + .or_else(|| indices.last()); + + if let Some(&index) = prev_match { + self.selected = index; + self.sync_input_with_selected(); + } + } + /// Updates the input buffer to reflect the value of the currently selected variable. pub fn sync_input_with_selected(&mut self) { if let Some(var) = self.vars.get(self.selected) { diff --git a/src/config.rs b/src/config.rs index 223785e..83883ca 100644 --- a/src/config.rs +++ b/src/config.rs @@ -98,6 +98,9 @@ pub struct KeybindsConfig { pub save: String, pub quit: String, pub normal_mode: String, + pub search: String, + pub next_match: String, + pub previous_match: String, } impl Default for KeybindsConfig { @@ -109,6 +112,9 @@ impl Default for KeybindsConfig { save: ":w".to_string(), quit: ":q".to_string(), normal_mode: "Esc".to_string(), + search: "/".to_string(), + next_match: "n".to_string(), + previous_match: "N".to_string(), } } } diff --git a/src/runner.rs b/src/runner.rs index 7084de8..ca3a3d2 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -64,6 +64,7 @@ where match self.app.mode { Mode::Normal => self.handle_normal_mode(key), Mode::Insert => self.handle_insert_mode(key), + Mode::Search => self.handle_search_mode(key), } } @@ -118,6 +119,13 @@ where self.sync_command_status(); } else if c_str == "q" { self.app.running = false; + } else if c_str == self.config.keybinds.search { + self.app.mode = Mode::Search; + self.app.status_message = Some(format!("{} ", self.config.keybinds.search)); + } else if c_str == self.config.keybinds.next_match { + self.app.jump_next_match(); + } else if c_str == self.config.keybinds.previous_match { + self.app.jump_previous_match(); } } else { match key.code { @@ -143,6 +151,28 @@ where Ok(()) } + /// Handles search mode key events. + fn handle_search_mode(&mut self, key: KeyEvent) -> io::Result<()> { + match key.code { + KeyCode::Enter | KeyCode::Esc => { + self.app.mode = Mode::Normal; + self.app.status_message = None; + } + KeyCode::Backspace => { + self.app.search_query.pop(); + self.app.status_message = Some(format!("{}{}", self.config.keybinds.search, self.app.search_query)); + self.app.jump_next_match(); + } + KeyCode::Char(c) => { + self.app.search_query.push(c); + self.app.status_message = Some(format!("{}{}", self.config.keybinds.search, self.app.search_query)); + self.app.jump_next_match(); + } + _ => {} + } + Ok(()) + } + /// Logic to map command strings (like ":w") to internal application actions. fn execute_command(&mut self, cmd: &str) -> io::Result<()> { if cmd == self.config.keybinds.save { diff --git a/src/ui.rs b/src/ui.rs index 4b59599..b92efe4 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -44,12 +44,14 @@ pub fn draw(f: &mut Frame, app: &mut App, config: &Config) { .split(outer_layout[1]); // Build the interactive list of configuration variables. + let matching_indices = app.matching_indices(); let items: Vec = app .vars .iter() .enumerate() .map(|(i, var)| { let is_selected = i == app.selected; + let is_match = matching_indices.contains(&i); // Show live input text for the selected item if in Insert mode. let val = if is_selected && matches!(app.mode, Mode::Insert) { @@ -62,6 +64,10 @@ pub fn draw(f: &mut Frame, app: &mut App, config: &Config) { Style::default() .fg(theme.crust()) .add_modifier(Modifier::BOLD) + } else if is_match { + Style::default() + .fg(theme.mauve()) + .add_modifier(Modifier::UNDERLINED) } else { Style::default().fg(theme.lavender()) }; @@ -151,7 +157,7 @@ pub fn draw(f: &mut Frame, app: &mut App, config: &Config) { let input_border_color = match app.mode { Mode::Insert => theme.green(), - Mode::Normal => theme.surface1(), + Mode::Normal | Mode::Search => theme.surface1(), }; let input_text = app.input.value(); @@ -197,14 +203,22 @@ pub fn draw(f: &mut Frame, app: &mut App, config: &Config) { .fg(theme.crust()) .add_modifier(Modifier::BOLD), ), + Mode::Search => ( + " SEARCH ", + Style::default() + .bg(theme.mauve()) + .fg(theme.crust()) + .add_modifier(Modifier::BOLD), + ), }; let status_msg = app .status_message .as_deref() .unwrap_or_else(|| match app.mode { - Mode::Normal => " navigation | i: edit | :w: save | :q: quit ", + Mode::Normal => " navigation | i: edit | /: search | :w: save | :q: quit ", Mode::Insert => " Esc: back to normal | Enter: commit ", + Mode::Search => " Esc: back to normal | type to filter ", }); let status_line = Line::from(vec![