added search functionality
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
56
src/app.rs
56
src/app.rs
@@ -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) {
|
||||||
|
|||||||
@@ -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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
18
src/ui.rs
18
src/ui.rs
@@ -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![
|
||||||
|
|||||||
Reference in New Issue
Block a user