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<bool> {
         let Some(selected) = self.scroll_state.selected() else {