Microkernel based hobby OS

Eladrin shell improvements

Changed files
+245 -5
aethelos-source
heartwood
isodir
boot
aethelos
aethelos-source/aethelos.iso

This is a binary file and will not be displayed.

+23 -3
aethelos-source/heartwood/src/attunement/keyboard.rs
··· 96 96 0x32 => Some('m'), 97 97 0x39 => Some(' '), // Space 98 98 0x1C => Some('\n'), // Enter 99 + 0x0E => Some('\x08'), // Backspace 100 + 0x48 => Some('\x01'), // Up arrow (special control char) 101 + 0x50 => Some('\x02'), // Down arrow (special control char) 99 102 _ => None, // Ignore other keys 100 103 }; 101 104 102 - // If we got a valid character, send it to both display and shell buffer 105 + // Handle the character 103 106 if let Some(character) = ch { 104 - crate::print!("{}", character); 105 - crate::eldarin::handle_char(character); 107 + match character { 108 + '\x08' => { 109 + // Backspace: erase character visually and update buffer 110 + crate::eldarin::handle_backspace(); 111 + } 112 + '\x01' => { 113 + // Up arrow: navigate to previous command 114 + crate::eldarin::handle_arrow_up(); 115 + } 116 + '\x02' => { 117 + // Down arrow: navigate to next command 118 + crate::eldarin::handle_arrow_down(); 119 + } 120 + _ => { 121 + // Regular character: display and buffer it 122 + crate::print!("{}", character); 123 + crate::eldarin::handle_char(character); 124 + } 125 + } 106 126 } 107 127 } 108 128 }
+200 -1
aethelos-source/heartwood/src/eldarin.rs
··· 16 16 /// Maximum command buffer size 17 17 const BUFFER_SIZE: usize = 256; 18 18 19 + /// Maximum number of commands to store in history 20 + const HISTORY_SIZE: usize = 32; 21 + 19 22 /// The Scroll Buffer - stores the command being typed 20 23 pub struct CommandBuffer { 21 24 buffer: [u8; BUFFER_SIZE], 22 25 position: usize, 26 + } 27 + 28 + /// Command History - stores previous commands for recall 29 + pub struct CommandHistory { 30 + /// Ring buffer of previous commands 31 + commands: [[u8; BUFFER_SIZE]; HISTORY_SIZE], 32 + /// Length of each command in the ring buffer 33 + lengths: [usize; HISTORY_SIZE], 34 + /// Current write position in ring buffer 35 + write_pos: usize, 36 + /// Current read position when navigating history 37 + read_pos: usize, 38 + /// Total number of commands in history (up to HISTORY_SIZE) 39 + count: usize, 40 + /// True if currently navigating history 41 + navigating: bool, 23 42 } 24 43 25 44 impl CommandBuffer { ··· 67 86 pub fn is_empty(&self) -> bool { 68 87 self.position == 0 69 88 } 89 + 90 + /// Set buffer contents from a byte slice 91 + pub fn set_from_bytes(&mut self, bytes: &[u8]) { 92 + self.clear(); 93 + let len = bytes.len().min(BUFFER_SIZE); 94 + self.buffer[..len].copy_from_slice(&bytes[..len]); 95 + self.position = len; 96 + } 97 + } 98 + 99 + impl CommandHistory { 100 + pub const fn new() -> Self { 101 + CommandHistory { 102 + commands: [[0; BUFFER_SIZE]; HISTORY_SIZE], 103 + lengths: [0; HISTORY_SIZE], 104 + write_pos: 0, 105 + read_pos: 0, 106 + count: 0, 107 + navigating: false, 108 + } 109 + } 110 + 111 + /// Add a command to history 112 + pub fn add(&mut self, cmd: &str) { 113 + if cmd.is_empty() { 114 + return; 115 + } 116 + 117 + let bytes = cmd.as_bytes(); 118 + let len = bytes.len().min(BUFFER_SIZE); 119 + 120 + self.commands[self.write_pos][..len].copy_from_slice(&bytes[..len]); 121 + self.lengths[self.write_pos] = len; 122 + 123 + self.write_pos = (self.write_pos + 1) % HISTORY_SIZE; 124 + if self.count < HISTORY_SIZE { 125 + self.count += 1; 126 + } 127 + 128 + // Reset navigation state 129 + self.navigating = false; 130 + self.read_pos = self.write_pos; 131 + } 132 + 133 + /// Navigate to previous command (up arrow) 134 + /// Returns Some(command) if available, None if at the beginning 135 + pub fn previous(&mut self) -> Option<&[u8]> { 136 + if self.count == 0 { 137 + return None; 138 + } 139 + 140 + if !self.navigating { 141 + // First time navigating, start from most recent 142 + self.navigating = true; 143 + self.read_pos = if self.write_pos == 0 { 144 + self.count - 1 145 + } else { 146 + self.write_pos - 1 147 + }; 148 + } else { 149 + // Already navigating, go back one more 150 + if self.read_pos == 0 { 151 + self.read_pos = self.count - 1; 152 + } else { 153 + self.read_pos -= 1; 154 + } 155 + } 156 + 157 + let len = self.lengths[self.read_pos]; 158 + Some(&self.commands[self.read_pos][..len]) 159 + } 160 + 161 + /// Navigate to next command (down arrow) 162 + /// Returns Some(command) if available, None if at the end 163 + pub fn next(&mut self) -> Option<&[u8]> { 164 + if !self.navigating { 165 + return None; 166 + } 167 + 168 + self.read_pos = (self.read_pos + 1) % self.count; 169 + 170 + // If we're back at the write position, stop navigating (return to empty) 171 + if self.read_pos == self.write_pos { 172 + self.navigating = false; 173 + return None; 174 + } 175 + 176 + let len = self.lengths[self.read_pos]; 177 + Some(&self.commands[self.read_pos][..len]) 178 + } 70 179 } 71 180 72 181 /// Global command buffer (interrupt-safe for keyboard input) 73 182 static mut COMMAND_BUFFER: MaybeUninit<InterruptSafeLock<CommandBuffer>> = MaybeUninit::uninit(); 74 183 static mut BUFFER_INITIALIZED: bool = false; 75 184 185 + /// Global command history 186 + static mut COMMAND_HISTORY: MaybeUninit<InterruptSafeLock<CommandHistory>> = MaybeUninit::uninit(); 187 + static mut HISTORY_INITIALIZED: bool = false; 188 + 76 189 /// Initialize the shell 77 190 pub fn init() { 78 191 unsafe { ··· 80 193 let lock = InterruptSafeLock::new(buffer); 81 194 core::ptr::write(COMMAND_BUFFER.as_mut_ptr(), lock); 82 195 BUFFER_INITIALIZED = true; 196 + 197 + let history = CommandHistory::new(); 198 + let history_lock = InterruptSafeLock::new(history); 199 + core::ptr::write(COMMAND_HISTORY.as_mut_ptr(), history_lock); 200 + HISTORY_INITIALIZED = true; 83 201 } 84 202 } 85 203 86 204 /// Get reference to command buffer 87 205 unsafe fn get_buffer() -> &'static InterruptSafeLock<CommandBuffer> { 88 206 COMMAND_BUFFER.assume_init_ref() 207 + } 208 + 209 + /// Get reference to command history 210 + unsafe fn get_history() -> &'static InterruptSafeLock<CommandHistory> { 211 + COMMAND_HISTORY.assume_init_ref() 89 212 } 90 213 91 214 /// Handle a character from keyboard input ··· 122 245 } 123 246 } 124 247 248 + /// Handle backspace key - erase character visually and from buffer 249 + pub fn handle_backspace() { 250 + unsafe { 251 + if !BUFFER_INITIALIZED { 252 + return; 253 + } 254 + 255 + let mut buffer = get_buffer().lock(); 256 + if buffer.pop() { 257 + // Erase character visually (VGA driver handles the erasure) 258 + crate::print!("\x08"); 259 + } 260 + } 261 + } 262 + 263 + /// Handle up arrow - navigate to previous command in history 264 + pub fn handle_arrow_up() { 265 + unsafe { 266 + if !BUFFER_INITIALIZED || !HISTORY_INITIALIZED { 267 + return; 268 + } 269 + 270 + let mut history = get_history().lock(); 271 + if let Some(cmd_bytes) = history.previous() { 272 + let mut buffer = get_buffer().lock(); 273 + let current_len = buffer.as_str().len(); 274 + 275 + // Erase current line (VGA driver erases each character as we backspace) 276 + for _ in 0..current_len { 277 + crate::print!("\x08"); 278 + } 279 + 280 + // Set buffer to historical command and display it 281 + buffer.set_from_bytes(cmd_bytes); 282 + if let Ok(cmd_str) = core::str::from_utf8(cmd_bytes) { 283 + crate::print!("{}", cmd_str); 284 + } 285 + } 286 + } 287 + } 288 + 289 + /// Handle down arrow - navigate to next command in history 290 + pub fn handle_arrow_down() { 291 + unsafe { 292 + if !BUFFER_INITIALIZED || !HISTORY_INITIALIZED { 293 + return; 294 + } 295 + 296 + let mut history = get_history().lock(); 297 + let mut buffer = get_buffer().lock(); 298 + let current_len = buffer.as_str().len(); 299 + 300 + // Erase current line (VGA driver erases each character as we backspace) 301 + for _ in 0..current_len { 302 + crate::print!("\x08"); 303 + } 304 + 305 + if let Some(cmd_bytes) = history.next() { 306 + // Set buffer to next historical command and display it 307 + buffer.set_from_bytes(cmd_bytes); 308 + if let Ok(cmd_str) = core::str::from_utf8(cmd_bytes) { 309 + crate::print!("{}", cmd_str); 310 + } 311 + } else { 312 + // At the end of history, clear buffer 313 + buffer.clear(); 314 + } 315 + } 316 + } 317 + 125 318 /// Check for pending commands and execute them (call from shell thread) 126 319 pub fn poll() { 127 320 unsafe { ··· 167 360 buffer.clear(); 168 361 } 169 362 170 - // Execute command 363 + // Execute command and save to history 171 364 if cmd_len > 0 { 172 365 if let Ok(cmd_str) = core::str::from_utf8(&cmd_copy[..cmd_len]) { 366 + // Save to history before executing 367 + if HISTORY_INITIALIZED { 368 + let mut history = get_history().lock(); 369 + history.add(cmd_str); 370 + } 371 + 173 372 execute_command(cmd_str); 174 373 } 175 374 }
+22 -1
aethelos-source/heartwood/src/vga_buffer.rs
··· 102 102 pub fn write_byte(&mut self, byte: u8) { 103 103 match byte { 104 104 b'\n' => self.new_line(), 105 + b'\x08' => { 106 + // Backspace: move cursor back and erase character 107 + if self.column_position > 0 { 108 + self.column_position -= 1; 109 + let row = self.row_position; 110 + let col = self.column_position; 111 + 112 + // Write a space to erase the character 113 + unsafe { 114 + core::ptr::write_volatile( 115 + &mut self.buffer.chars[row][col] as *mut ScreenChar, 116 + ScreenChar { 117 + ascii_character: b' ', 118 + color_code: self.color_code, 119 + } 120 + ); 121 + } 122 + 123 + move_cursor(self.row_position, self.column_position); 124 + } 125 + } 105 126 byte => { 106 127 if self.column_position >= BUFFER_WIDTH { 107 128 self.new_line(); ··· 135 156 } 136 157 for byte in s.bytes() { 137 158 match byte { 138 - 0x20..=0x7e | b'\n' => self.write_byte(byte), 159 + 0x20..=0x7e | b'\n' | b'\x08' => self.write_byte(byte), 139 160 _ => self.write_byte(0xfe), // ■ for unprintable 140 161 } 141 162 }
aethelos-source/isodir/boot/aethelos/heartwood.bin

This is a binary file and will not be displayed.