added search functionality

This commit is contained in:
2026-03-17 09:24:58 +01:00
parent 7b2217886c
commit 0c217c5129
6 changed files with 114 additions and 2 deletions

View File

@@ -48,6 +48,9 @@ mould config.template.json -o config.json
- `j` / `Down`: Move selection down - `j` / `Down`: Move selection down
- `k` / `Up`: Move selection up - `k` / `Up`: Move selection up
- `i`: Edit the value of the currently selected key (Enter Insert Mode) - `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 - `:w` or `Enter`: Save the current configuration to the output file
- `:q` or `q`: Quit the application - `:q` or `q`: Quit the application
- `:wq`: Save and quit - `:wq`: Save and quit

View File

@@ -44,3 +44,6 @@ edit = "i"
save = ":w" save = ":w"
quit = ":q" quit = ":q"
normal_mode = "Esc" normal_mode = "Esc"
search = "/"
next_match = "n"
previous_match = "N"

View File

@@ -7,6 +7,8 @@ pub enum Mode {
Normal, Normal,
/// Active text entry mode for modifying values. /// Active text entry mode for modifying values.
Insert, Insert,
/// Active search mode for filtering keys.
Search,
} }
/// The core application state, holding all configuration variables and UI status. /// The core application state, holding all configuration variables and UI status.
@@ -23,6 +25,8 @@ pub struct App {
pub status_message: Option<String>, pub status_message: Option<String>,
/// The active text input buffer for the selected variable. /// The active text input buffer for the selected variable.
pub input: Input, pub input: Input,
/// The current search query for filtering keys.
pub search_query: String,
} }
impl App { impl App {
@@ -36,9 +40,24 @@ impl App {
running: true, running: true,
status_message: None, status_message: None,
input: Input::new(initial_input), 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<usize> {
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. /// Moves the selection to the next variable in the list, wrapping around if necessary.
pub fn next(&mut self) { pub fn next(&mut self) {
if !self.vars.is_empty() { 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. /// Updates the input buffer to reflect the value of the currently selected variable.
pub fn sync_input_with_selected(&mut self) { pub fn sync_input_with_selected(&mut self) {
if let Some(var) = self.vars.get(self.selected) { if let Some(var) = self.vars.get(self.selected) {

View File

@@ -98,6 +98,9 @@ pub struct KeybindsConfig {
pub save: String, pub save: String,
pub quit: String, pub quit: String,
pub normal_mode: String, pub normal_mode: String,
pub search: String,
pub next_match: String,
pub previous_match: String,
} }
impl Default for KeybindsConfig { impl Default for KeybindsConfig {
@@ -109,6 +112,9 @@ impl Default for KeybindsConfig {
save: ":w".to_string(), save: ":w".to_string(),
quit: ":q".to_string(), quit: ":q".to_string(),
normal_mode: "Esc".to_string(), normal_mode: "Esc".to_string(),
search: "/".to_string(),
next_match: "n".to_string(),
previous_match: "N".to_string(),
} }
} }
} }

View File

@@ -64,6 +64,7 @@ where
match self.app.mode { match self.app.mode {
Mode::Normal => self.handle_normal_mode(key), Mode::Normal => self.handle_normal_mode(key),
Mode::Insert => self.handle_insert_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(); self.sync_command_status();
} else if c_str == "q" { } else if c_str == "q" {
self.app.running = false; 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 { } else {
match key.code { match key.code {
@@ -143,6 +151,28 @@ where
Ok(()) 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. /// Logic to map command strings (like ":w") to internal application actions.
fn execute_command(&mut self, cmd: &str) -> io::Result<()> { fn execute_command(&mut self, cmd: &str) -> io::Result<()> {
if cmd == self.config.keybinds.save { if cmd == self.config.keybinds.save {

View File

@@ -44,12 +44,14 @@ pub fn draw(f: &mut Frame, app: &mut App, config: &Config) {
.split(outer_layout[1]); .split(outer_layout[1]);
// Build the interactive list of configuration variables. // Build the interactive list of configuration variables.
let matching_indices = app.matching_indices();
let items: Vec<ListItem> = app let items: Vec<ListItem> = app
.vars .vars
.iter() .iter()
.enumerate() .enumerate()
.map(|(i, var)| { .map(|(i, var)| {
let is_selected = i == app.selected; 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. // Show live input text for the selected item if in Insert mode.
let val = if is_selected && matches!(app.mode, Mode::Insert) { 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() Style::default()
.fg(theme.crust()) .fg(theme.crust())
.add_modifier(Modifier::BOLD) .add_modifier(Modifier::BOLD)
} else if is_match {
Style::default()
.fg(theme.mauve())
.add_modifier(Modifier::UNDERLINED)
} else { } else {
Style::default().fg(theme.lavender()) 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 { let input_border_color = match app.mode {
Mode::Insert => theme.green(), Mode::Insert => theme.green(),
Mode::Normal => theme.surface1(), Mode::Normal | Mode::Search => theme.surface1(),
}; };
let input_text = app.input.value(); let input_text = app.input.value();
@@ -197,14 +203,22 @@ pub fn draw(f: &mut Frame, app: &mut App, config: &Config) {
.fg(theme.crust()) .fg(theme.crust())
.add_modifier(Modifier::BOLD), .add_modifier(Modifier::BOLD),
), ),
Mode::Search => (
" SEARCH ",
Style::default()
.bg(theme.mauve())
.fg(theme.crust())
.add_modifier(Modifier::BOLD),
),
}; };
let status_msg = app let status_msg = app
.status_message .status_message
.as_deref() .as_deref()
.unwrap_or_else(|| match app.mode { .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::Insert => " Esc: back to normal | Enter: commit ",
Mode::Search => " Esc: back to normal | type to filter ",
}); });
let status_line = Line::from(vec![ let status_line = Line::from(vec![