Repository: atuinsh/atuin (language: rust)

Issue:
[Bug]: integer underflow when result list empty and pressing "up"
### What did you expect to happen?

No action.

### What happened?

```
thread 'main' panicked at crates/atuin/src/command/client/search/interactive.rs:537:41:
attempt to subtract with overflow
```

https://github.com/atuinsh/atuin/blob/6e15286f0c94b89d4c5f9b8fa1e9a72cd209017c/crates/atuin/src/command/client/search/interactive.rs#L537

### Atuin doctor output

```yaml
{
  "atuin": {
    "version": "18.4.0-beta.3",
    "sync": null,
    "sqlite_version": "3.46.0"
  },
  "shell": {
    "name": "zsh",
    "default": "zsh",
    "plugins": [
      "atuin"
    ],
    "preexec": "built-in"
  },
  "system": {
    "os": "Darwin",
    "arch": "arm64",
    "version": "15.0",
    "disks": [
      {
        "name": "Macintosh HD",
        "filesystem": "apfs"
      },
      {
        "name": "Macintosh HD",
        "filesystem": "apfs"
      }
    ]
  }
}
```


### Code of Conduct

- [X] I agree to follow this project's Code of Conduct


Discussion / hints:

Fix patch:
diff --git a/crates/atuin/src/command/client/search/interactive.rs b/crates/atuin/src/command/client/search/interactive.rs
index fae39e28337..dd9f4ed4b0d 100644
--- a/crates/atuin/src/command/client/search/interactive.rs
+++ b/crates/atuin/src/command/client/search/interactive.rs
@@ -534,7 +534,8 @@ impl State {
 
     fn scroll_up(&mut self, scroll_len: usize) {
         let i = self.results_state.selected() + scroll_len;
-        self.results_state.select(i.min(self.results_len - 1));
+        self.results_state
+            .select(i.min(self.results_len.saturating_sub(1)));
     }
 
     #[allow(clippy::cast_possible_truncation)]
@@ -1211,8 +1212,15 @@ fn set_clipboard(_s: String) {}
 
 #[cfg(test)]
 mod tests {
+    use atuin_client::database::Context;
     use atuin_client::history::History;
-    use atuin_client::settings::{Preview, PreviewStrategy, Settings};
+    use atuin_client::settings::{
+        FilterMode, KeymapMode, Preview, PreviewStrategy, SearchMode, Settings,
+    };
+    use time::OffsetDateTime;
+
+    use crate::command::client::search::engines::{self, SearchState};
+    use crate::command::client::search::history_list::ListState;
 
     use super::State;
 
@@ -1368,4 +1376,38 @@ mod tests {
         assert_eq!(preview_static_limit_at_4, 4 + border_space);
         assert_eq!(settings_preview_fixed, 15 + border_space);
     }
+
+    // Test when there's no results, scrolling up or down doesn't underflow
+    #[test]
+    fn state_scroll_up_underflow() {
+        let mut state = State {
+            history_count: 0,
+            update_needed: None,
+            results_state: ListState::default(),
+            switched_search_mode: false,
+            search_mode: SearchMode::Fuzzy,
+            results_len: 0,
+            accept: false,
+            keymap_mode: KeymapMode::Auto,
+            prefix: false,
+            current_cursor: None,
+            tab_index: 0,
+            search: SearchState {
+                input: String::new().into(),
+                filter_mode: FilterMode::Directory,
+                context: Context {
+                    session: String::new(),
+                    cwd: String::new(),
+                    hostname: String::new(),
+                    host_id: String::new(),
+                    git_root: None,
+                },
+            },
+            engine: engines::engine(SearchMode::Fuzzy),
+            now: Box::new(OffsetDateTime::now_utc),
+        };
+
+        state.scroll_up(1);
+        state.scroll_down(1);
+    }
 }


Test patch: