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 }