diff --git a/src/list.rs b/src/list.rs index 481fb2f4..5d7c8dd9 100644 --- a/src/list.rs +++ b/src/list.rs @@ -21,6 +21,7 @@ mod state; fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> { let mut list_state = ListState::new(app_state, stdout)?; + let mut is_searching = false; loop { match event::read().context("Failed to read terminal event")? { @@ -32,6 +33,29 @@ fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> list_state.message.clear(); + let curr_key = key.code; + + if is_searching { + match curr_key { + KeyCode::Esc | KeyCode::Enter => { + is_searching = false; + list_state.search_query.clear(); + } + KeyCode::Char(k) => { + list_state.search_query.push(k); + list_state.apply_search_query(); + list_state.draw(stdout)?; + } + KeyCode::Backspace => { + list_state.search_query.pop(); + list_state.apply_search_query(); + list_state.draw(stdout)?; + } + _ => {} + } + continue; + } + match key.code { KeyCode::Char('q') => return Ok(()), KeyCode::Down | KeyCode::Char('j') => list_state.select_next(), @@ -66,6 +90,10 @@ fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> return Ok(()); } } + KeyCode::Char('s' | '/') => { + list_state.message.push_str("search:|"); + is_searching = true; + } // Redraw to remove the message. KeyCode::Esc => (), _ => continue, diff --git a/src/list/scroll_state.rs b/src/list/scroll_state.rs index 25a73736..2c02ed4f 100644 --- a/src/list/scroll_state.rs +++ b/src/list/scroll_state.rs @@ -46,7 +46,7 @@ impl ScrollState { self.selected } - fn set_selected(&mut self, selected: usize) { + pub fn set_selected(&mut self, selected: usize) { self.selected = Some(selected); self.update_offset(); } diff --git a/src/list/state.rs b/src/list/state.rs index 7a2d3bf0..60077c78 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -44,6 +44,7 @@ pub struct ListState<'a> { term_width: u16, term_height: u16, show_footer: bool, + pub search_query: String, } impl<'a> ListState<'a> { @@ -76,6 +77,7 @@ impl<'a> ListState<'a> { term_width: 0, term_height: 0, show_footer: true, + search_query: String::new(), }; slf.set_term_size(width, height); @@ -345,6 +347,37 @@ impl<'a> ListState<'a> { Ok(()) } + pub fn apply_search_query(&mut self) { + self.message.push_str("search:"); + self.message.push_str(&self.search_query); + self.message.push('|'); + + if self.search_query.is_empty() { + return; + } + + let idx = self + .app_state + .exercises() + .iter() + .filter(|exercise| match self.filter() { + Filter::None => true, + Filter::Done => exercise.done, + Filter::Pending => !exercise.done, + }) + .position(|exercise| exercise.name.contains(self.search_query.as_str())); + + match idx { + Some(exercise_ind) => { + self.scroll_state.set_selected(exercise_ind); + } + None => { + let msg = String::from(" (not found)"); + self.message.push_str(&msg); + } + } + } + // Return `true` if there was something to select. pub fn selected_to_current_exercise(&mut self) -> Result { let Some(selected) = self.scroll_state.selected() else {