/ firmware / src / services / ssh / history.rs
history.rs
  1  use heapless::{String, Vec};
  2  
  3  /// Configuration for command history
  4  #[derive(Clone, Copy)]
  5  pub struct HistoryConfig {
  6      /// Maximum number of history entries
  7      pub max_entries: usize,
  8      /// Whether to deduplicate consecutive identical commands
  9      pub deduplicate: bool,
 10  }
 11  
 12  impl Default for HistoryConfig {
 13      fn default() -> Self {
 14          Self {
 15              max_entries: 10,
 16              deduplicate: true,
 17          }
 18      }
 19  }
 20  
 21  /// Command history manager
 22  pub struct History<const BUF_SIZE: usize> {
 23      entries: Vec<String<BUF_SIZE>, 16>,
 24      config: HistoryConfig,
 25      current_index: Option<usize>,
 26  }
 27  
 28  impl<const BUF_SIZE: usize> History<BUF_SIZE> {
 29      /// Create a new history manager
 30      pub fn new(config: HistoryConfig) -> Self {
 31          Self {
 32              entries: Vec::new(),
 33              config,
 34              current_index: None,
 35          }
 36      }
 37  
 38      /// Add a command to history
 39      pub fn add(&mut self, command: &str) -> Result<(), ()> {
 40          // Skip empty commands
 41          if command.trim().is_empty() {
 42              return Ok(());
 43          }
 44  
 45          // Check for deduplication
 46          if self.config.deduplicate {
 47              if let Some(last) = self.entries.last() {
 48                  if last.as_str() == command {
 49                      return Ok(());
 50                  }
 51              }
 52          }
 53  
 54          let entry = String::try_from(command).map_err(|_| ())?;
 55  
 56          // If at capacity, remove oldest
 57          if self.entries.len() >= self.config.max_entries {
 58              self.entries.remove(0);
 59          }
 60  
 61          self.entries.push(entry).map_err(|_| ())?;
 62          self.current_index = None;
 63          Ok(())
 64      }
 65  
 66      /// Get the previous command in history
 67      pub fn previous(&mut self) -> Option<&str> {
 68          if self.entries.is_empty() {
 69              return None;
 70          }
 71  
 72          let new_index = match self.current_index {
 73              None => self.entries.len() - 1,
 74              Some(0) => return Some(&self.entries[0]),
 75              Some(i) => i - 1,
 76          };
 77  
 78          self.current_index = Some(new_index);
 79          Some(&self.entries[new_index])
 80      }
 81  
 82      /// Get the next command in history
 83      pub fn next(&mut self) -> Option<&str> {
 84          match self.current_index {
 85              None => None,
 86              Some(i) if i >= self.entries.len() - 1 => {
 87                  self.current_index = None;
 88                  None
 89              }
 90              Some(i) => {
 91                  self.current_index = Some(i + 1);
 92                  Some(&self.entries[i + 1])
 93              }
 94          }
 95      }
 96  
 97      /// Reset the history navigation position
 98      pub fn reset_position(&mut self) {
 99          self.current_index = None;
100      }
101  
102      /// Get the number of entries in history
103      pub fn len(&self) -> usize {
104          self.entries.len()
105      }
106  
107      /// Check if history is empty
108      pub fn is_empty(&self) -> bool {
109          self.entries.is_empty()
110      }
111  
112      /// Clear all history
113      pub fn clear(&mut self) {
114          self.entries.clear();
115          self.current_index = None;
116      }
117  
118      /// Get an iterator over history entries (oldest to newest)
119      pub fn iter(&self) -> impl Iterator<Item = &str> {
120          self.entries.iter().map(|s| s.as_str())
121      }
122  
123      /// Get an iterator over history entries in reverse (newest to oldest)
124      pub fn iter_rev(&self) -> impl Iterator<Item = &str> {
125          self.entries.iter().rev().map(|s| s.as_str())
126      }
127  }
128  
129  #[cfg(test)]
130  mod tests {
131      use super::*;
132  
133      #[test]
134      fn test_history_add() {
135          let mut history = History::<64>::new(HistoryConfig::default());
136          history.add("command1").unwrap();
137          history.add("command2").unwrap();
138          assert_eq!(history.len(), 2);
139      }
140  
141      #[test]
142      fn test_history_deduplicate() {
143          let mut history = History::<64>::new(HistoryConfig {
144              deduplicate: true,
145              ..Default::default()
146          });
147          history.add("command1").unwrap();
148          history.add("command1").unwrap();
149          assert_eq!(history.len(), 1);
150      }
151  
152      #[test]
153      fn test_history_navigation() {
154          let mut history = History::<64>::new(HistoryConfig::default());
155          history.add("cmd1").unwrap();
156          history.add("cmd2").unwrap();
157          history.add("cmd3").unwrap();
158  
159          assert_eq!(history.previous(), Some("cmd3"));
160          assert_eq!(history.previous(), Some("cmd2"));
161          assert_eq!(history.next(), Some("cmd3"));
162          assert_eq!(history.next(), None);
163      }
164  }