/ firmware / src / services / ssh / terminal.rs
terminal.rs
  1  use embassy_futures::select::{select, Either};
  2  use embassy_sync::{blocking_mutex::raw::RawMutex, signal::Signal};
  3  use embedded_io_async::{Read, Write as AsyncWrite};
  4  use heapless::{String, Vec};
  5  
  6  use super::history::History;
  7  use super::writer::TerminalWriter;
  8  
  9  /// Configuration for the terminal
 10  #[derive(Clone, Copy)]
 11  pub struct TerminalConfig {
 12      /// Maximum command buffer size
 13      pub buffer_size: usize,
 14      /// Prompt string to display
 15      pub prompt: &'static str,
 16      /// Enable echo of typed characters
 17      pub echo: bool,
 18      /// Enable ANSI escape codes for better terminal control
 19      pub ansi_enabled: bool,
 20  }
 21  
 22  impl Default for TerminalConfig {
 23      fn default() -> Self {
 24          Self {
 25              buffer_size: 128,
 26              prompt: "> ",
 27              echo: true,
 28              ansi_enabled: true,
 29          }
 30      }
 31  }
 32  
 33  /// Key codes for special keys
 34  #[derive(Debug, Clone, Copy, PartialEq)]
 35  pub enum KeyCode {
 36      Backspace,
 37      Delete,
 38      Enter,
 39      Tab,
 40      Escape,
 41      ArrowUp,
 42      ArrowDown,
 43      ArrowLeft,
 44      ArrowRight,
 45      CtrlC,
 46      CtrlD,
 47      Char(u8),
 48  }
 49  
 50  /// Main terminal structure
 51  pub struct Terminal<const BUF_SIZE: usize> {
 52      config: TerminalConfig,
 53      buffer: Vec<u8, BUF_SIZE>,
 54      cursor_pos: usize,
 55      escape_state: EscapeState,
 56  }
 57  
 58  /// State machine for parsing ANSI escape sequences
 59  #[derive(Debug, Clone, Copy, PartialEq)]
 60  enum EscapeState {
 61      Normal,
 62      Escape,
 63      Bracket,
 64  }
 65  
 66  impl<const BUF_SIZE: usize> Terminal<BUF_SIZE> {
 67      /// Create a new terminal instance
 68      pub fn new(config: TerminalConfig) -> Self {
 69          Self {
 70              config,
 71              buffer: Vec::new(),
 72              cursor_pos: 0,
 73              escape_state: EscapeState::Normal,
 74          }
 75      }
 76  
 77      /// Get the current buffer as a string slice
 78      pub fn buffer_str(&self) -> Result<&str, core::str::Utf8Error> {
 79          core::str::from_utf8(self.buffer.as_slice())
 80      }
 81  
 82      /// Clear the current buffer
 83      pub fn clear_buffer(&mut self) {
 84          self.buffer.clear();
 85          self.cursor_pos = 0;
 86      }
 87  
 88      /// Get the current cursor position
 89      pub fn cursor_position(&self) -> usize {
 90          self.cursor_pos
 91      }
 92  
 93      /// Process a single byte of input, handling ANSI escape sequences
 94      pub fn process_byte(&mut self, byte: u8) -> Option<KeyCode> {
 95          match self.escape_state {
 96              EscapeState::Normal => {
 97                  match byte {
 98                      b'\r' | b'\n' => Some(KeyCode::Enter),
 99                      0x08 | 0x7F => Some(KeyCode::Backspace),
100                      0x03 => Some(KeyCode::CtrlC),
101                      0x04 => Some(KeyCode::CtrlD),
102                      0x09 => Some(KeyCode::Tab),
103                      0x1B => {
104                          self.escape_state = EscapeState::Escape;
105                          None
106                      }
107                      byte if byte >= 0x20 && byte < 0x7F => Some(KeyCode::Char(byte)),
108                      _ => None,
109                  }
110              }
111              EscapeState::Escape => {
112                  if byte == b'[' {
113                      self.escape_state = EscapeState::Bracket;
114                      None
115                  } else {
116                      self.escape_state = EscapeState::Normal;
117                      Some(KeyCode::Escape)
118                  }
119              }
120              EscapeState::Bracket => {
121                  self.escape_state = EscapeState::Normal;
122                  match byte {
123                      b'A' => Some(KeyCode::ArrowUp),
124                      b'B' => Some(KeyCode::ArrowDown),
125                      b'C' => Some(KeyCode::ArrowRight),
126                      b'D' => Some(KeyCode::ArrowLeft),
127                      b'3' => Some(KeyCode::Delete), // Delete sends ESC[3~
128                      _ => None,
129                  }
130              }
131          }
132      }
133  
134      /// Handle a key press
135      pub fn handle_key(&mut self, key: KeyCode) -> TerminalEvent {
136          match key {
137              KeyCode::Enter => {
138                  if self.buffer.is_empty() {
139                      TerminalEvent::EmptyCommand
140                  } else {
141                      TerminalEvent::CommandReady
142                  }
143              }
144              KeyCode::Backspace => {
145                  if self.cursor_pos > 0 && !self.buffer.is_empty() {
146                      self.buffer.remove(self.cursor_pos - 1);
147                      self.cursor_pos -= 1;
148                      TerminalEvent::BufferChanged
149                  } else {
150                      TerminalEvent::None
151                  }
152              }
153              KeyCode::Delete => {
154                  if self.cursor_pos < self.buffer.len() {
155                      self.buffer.remove(self.cursor_pos);
156                      TerminalEvent::BufferChanged
157                  } else {
158                      TerminalEvent::None
159                  }
160              }
161              KeyCode::ArrowLeft => {
162                  if self.cursor_pos > 0 {
163                      self.cursor_pos -= 1;
164                      TerminalEvent::CursorMoved
165                  } else {
166                      TerminalEvent::None
167                  }
168              }
169              KeyCode::ArrowRight => {
170                  if self.cursor_pos < self.buffer.len() {
171                      self.cursor_pos += 1;
172                      TerminalEvent::CursorMoved
173                  } else {
174                      TerminalEvent::None
175                  }
176              }
177              KeyCode::ArrowUp => TerminalEvent::HistoryPrevious,
178              KeyCode::ArrowDown => TerminalEvent::HistoryNext,
179              KeyCode::CtrlC => TerminalEvent::Interrupt,
180              KeyCode::CtrlD => TerminalEvent::EndOfFile,
181              KeyCode::Char(byte) => {
182                  if self.buffer.len() < BUF_SIZE {
183                      // Insert at cursor position
184                      if self.cursor_pos == self.buffer.len() {
185                          let _ = self.buffer.push(byte);
186                      } else {
187                          let _ = self.buffer.insert(self.cursor_pos, byte);
188                      }
189                      self.cursor_pos += 1;
190                      TerminalEvent::BufferChanged
191                  } else {
192                      TerminalEvent::BufferFull
193                  }
194              }
195              _ => TerminalEvent::None,
196          }
197      }
198  
199      /// Get the current command buffer and clear it
200      pub fn take_command(&mut self) -> Result<String<BUF_SIZE>, ()> {
201          let result = String::from_utf8(self.buffer.clone()).map_err(|_| ())?;
202          self.clear_buffer();
203          Ok(result)
204      }
205  
206      /// Set the buffer content (useful for history navigation)
207      pub fn set_buffer(&mut self, content: &str) -> Result<(), ()> {
208          self.buffer.clear();
209          self.buffer.extend_from_slice(content.as_bytes()).map_err(|_| ())?;
210          self.cursor_pos = self.buffer.len();
211          Ok(())
212      }
213  }
214  
215  /// Events that can occur during terminal operation
216  #[derive(Debug, Clone, Copy, PartialEq)]
217  pub enum TerminalEvent {
218      None,
219      BufferChanged,
220      CursorMoved,
221      CommandReady,
222      EmptyCommand,
223      BufferFull,
224      Interrupt,
225      EndOfFile,
226      HistoryPrevious,
227      HistoryNext,
228  }
229  
230  /// Terminal reader task that handles async I/O
231  pub struct TerminalReader<const BUF_SIZE: usize> {
232      terminal: Terminal<BUF_SIZE>,
233      history: Option<History<BUF_SIZE>>,
234  }
235  
236  impl<const BUF_SIZE: usize> TerminalReader<BUF_SIZE> {
237      pub fn new(config: TerminalConfig, history: Option<History<BUF_SIZE>>) -> Self {
238          Self {
239              terminal: Terminal::new(config),
240              history,
241          }
242      }
243  
244      /// Read a complete line from the input
245      pub async fn read_line<R, W, M>(
246          &mut self,
247          reader: &mut R,
248          writer: &mut TerminalWriter<'_, W>,
249          redraw_signal: Option<&Signal<M, ()>>,
250      ) -> Result<String<BUF_SIZE>, ReadLineError>
251      where
252          R: Read,
253          W: AsyncWrite,
254          M: RawMutex,
255      {
256          // Display initial prompt
257          let _ = writer.write_prompt(self.terminal.config.prompt).await;
258  
259          let mut byte_buf = [0u8; 1];
260  
261          loop {
262              let event = if let Some(signal) = redraw_signal {
263                  // Wait for either input or redraw signal
264                  match select(reader.read(&mut byte_buf), signal.wait()).await {
265                      Either::First(Ok(1)) => {
266                          if let Some(key) = self.terminal.process_byte(byte_buf[0]) {
267                              self.terminal.handle_key(key)
268                          } else {
269                              TerminalEvent::None
270                          }
271                      }
272                      Either::First(Ok(0)) => TerminalEvent::EndOfFile,
273                      Either::First(Err(_)) => return Err(ReadLineError::IoError),
274                      Either::Second(_) => {
275                          // Redraw requested
276                          signal.reset();
277                          let _ = writer.clear_line().await;
278                          let _ = writer.write_prompt(self.terminal.config.prompt).await;
279                          let _ = writer.write_str(self.terminal.buffer_str().unwrap_or("")).await;
280                          continue;
281                      }
282                      _ => continue,
283                  }
284              } else {
285                  // Simple read without redraw support
286                  match reader.read(&mut byte_buf).await {
287                      Ok(1) => {
288                          if let Some(key) = self.terminal.process_byte(byte_buf[0]) {
289                              self.terminal.handle_key(key)
290                          } else {
291                              TerminalEvent::None
292                          }
293                      }
294                      Ok(0) => TerminalEvent::EndOfFile,
295                      Err(_) => return Err(ReadLineError::IoError),
296                      _ => continue,
297                  }
298              };
299  
300              match event {
301                  TerminalEvent::CommandReady => {
302                      let _ = writer.write_str("\r\n").await;
303                      let command = self.terminal.take_command()?;
304                      
305                      // Add to history if available
306                      if let Some(ref mut hist) = self.history {
307                          let _ = hist.add(&command);
308                      }
309                      
310                      return Ok(command);
311                  }
312                  TerminalEvent::EmptyCommand => {
313                      let _ = writer.write_str("\r\n").await;
314                      let _ = writer.write_prompt(self.terminal.config.prompt).await;
315                  }
316                  TerminalEvent::BufferChanged => {
317                      if self.terminal.config.echo {
318                          // Redraw the line
319                          let _ = writer.clear_line().await;
320                          let _ = writer.write_prompt(self.terminal.config.prompt).await;
321                          let _ = writer.write_str(self.terminal.buffer_str().unwrap_or("")).await;
322                      }
323                  }
324                  TerminalEvent::Interrupt => {
325                      self.terminal.clear_buffer();
326                      let _ = writer.write_str("^C\r\n").await;
327                      let _ = writer.write_prompt(self.terminal.config.prompt).await;
328                  }
329                  TerminalEvent::EndOfFile => {
330                      return Err(ReadLineError::EndOfFile);
331                  }
332                  TerminalEvent::HistoryPrevious => {
333                      if let Some(ref mut hist) = self.history {
334                          if let Some(entry) = hist.previous() {
335                              let _ = self.terminal.set_buffer(entry);
336                              // Redraw the line
337                              let _ = writer.clear_line().await;
338                              let _ = writer.write_prompt(self.terminal.config.prompt).await;
339                              let _ = writer.write_str(self.terminal.buffer_str().unwrap_or("")).await;
340                          }
341                      }
342                  }
343                  TerminalEvent::HistoryNext => {
344                      if let Some(ref mut hist) = self.history {
345                          if let Some(entry) = hist.next() {
346                              let _ = self.terminal.set_buffer(entry);
347                          } else {
348                              // At the end of history, clear buffer
349                              self.terminal.clear_buffer();
350                          }
351                          // Redraw the line
352                          let _ = writer.clear_line().await;
353                          let _ = writer.write_prompt(self.terminal.config.prompt).await;
354                          let _ = writer.write_str(self.terminal.buffer_str().unwrap_or("")).await;
355                      }
356                  }
357                  TerminalEvent::BufferFull => {
358                      // Optionally signal buffer full (beep?)
359                  }
360                  _ => {}
361              }
362          }
363      }
364  }
365  
366  /// Errors that can occur while reading a line
367  #[derive(Debug, Clone, Copy)]
368  pub enum ReadLineError {
369      IoError,
370      Utf8Error,
371      EndOfFile,
372  }
373  
374  impl From<()> for ReadLineError {
375      fn from(_: ()) -> Self {
376          ReadLineError::Utf8Error
377      }
378  }