diff options
| author | salmonik <salmonik@tuta.io> | 2026-07-31 02:58:31 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-07-31 02:58:31 +0200 |
| commit | 6a5c32fa6ae8ec34e3e5b44d7e5a78ded8fe729b (patch) | |
| tree | ed59a5bd59a94b3a731b28c68c507f18afc44b0b /shell/src | |
| parent | auto-version all strings from Cargo.toml via env!("CARGO_PKG_VERSION") (diff) | |
| download | rs-dos-6a5c32fa6ae8ec34e3e5b44d7e5a78ded8fe729b.tar.gz rs-dos-6a5c32fa6ae8ec34e3e5b44d7e5a78ded8fe729b.zip | |
restructure: rsnix kernel + shell crate + rsdos entry point
Architecture:
rs-dos/ (workspace root, was raisernics/)
├── rsnix/ — kernel lib crate (uefi, serial, gop, ps2, interrupts, memory)
├── shell/ — shell lib crate (prompt, commands, line input)
├── rsdos/ — binary crate (efi_main, init, panic handler)
└── rsboot/ — future git submodule
No functional changes — boots to shell prompt identically.
Version auto-pulled from Cargo.toml via env!("CARGO_PKG_VERSION").
Diffstat (limited to 'shell/src')
| -rw-r--r-- | shell/src/lib.rs | 285 |
1 files changed, 285 insertions, 0 deletions
diff --git a/shell/src/lib.rs b/shell/src/lib.rs new file mode 100644 index 0000000..bdd79c6 --- /dev/null +++ b/shell/src/lib.rs | |||
| @@ -0,0 +1,285 @@ | |||
| 1 | #![no_std] | ||
| 2 | |||
| 3 | extern crate alloc; | ||
| 4 | |||
| 5 | /// Minimal command shell — reads from PS/2 keyboard, writes to GOP + serial. | ||
| 6 | use rsnix::ps2::{self, KeyEvent}; | ||
| 7 | use rsnix::{gop_print, gop_println, serial_print, serial_println}; | ||
| 8 | |||
| 9 | /// Maximum input line length. | ||
| 10 | const LINE_BUF_SIZE: usize = 256; | ||
| 11 | |||
| 12 | /// Run the shell forever. | ||
| 13 | pub fn run() -> ! { | ||
| 14 | gop_println!( | ||
| 15 | "rs-dos v{} — type 'help' for commands", | ||
| 16 | env!("CARGO_PKG_VERSION") | ||
| 17 | ); | ||
| 18 | serial_println!( | ||
| 19 | "========== rs-dos v{} ==========", | ||
| 20 | env!("CARGO_PKG_VERSION") | ||
| 21 | ); | ||
| 22 | |||
| 23 | loop { | ||
| 24 | // Print prompt | ||
| 25 | gop_print!("> "); | ||
| 26 | serial_print!("> "); | ||
| 27 | |||
| 28 | // Read one line from keyboard | ||
| 29 | let line = read_line(); | ||
| 30 | |||
| 31 | // Parse and execute | ||
| 32 | let trimmed = line.trim(); | ||
| 33 | if trimmed.is_empty() { | ||
| 34 | continue; | ||
| 35 | } | ||
| 36 | |||
| 37 | let (cmd, args) = split_first_word(trimmed); | ||
| 38 | |||
| 39 | match cmd { | ||
| 40 | "help" => cmd_help(), | ||
| 41 | "echo" => cmd_echo(args), | ||
| 42 | "clear" => cmd_clear(), | ||
| 43 | "halt" | "poweroff" | "exit" => cmd_halt(), | ||
| 44 | "hello" => { | ||
| 45 | gop_println!("World!"); | ||
| 46 | serial_println!("World!"); | ||
| 47 | } | ||
| 48 | "info" => cmd_info(), | ||
| 49 | "mem" => cmd_mem(), | ||
| 50 | _ => { | ||
| 51 | gop_println!("unknown command: {}", cmd); | ||
| 52 | serial_println!("unknown command: {}", cmd); | ||
| 53 | } | ||
| 54 | } | ||
| 55 | } | ||
| 56 | } | ||
| 57 | |||
| 58 | // --------------------------------------------------------------------------- | ||
| 59 | // Line input (PS/2 keyboard, no heap) | ||
| 60 | // --------------------------------------------------------------------------- | ||
| 61 | fn read_line() -> heapless_str::String<LINE_BUF_SIZE> { | ||
| 62 | let mut buf = heapless_str::String::<LINE_BUF_SIZE>::new(); | ||
| 63 | loop { | ||
| 64 | match ps2::poll_key() { | ||
| 65 | KeyEvent::Enter => { | ||
| 66 | rsnix::gop::put_char('\n'); | ||
| 67 | rsnix::serial::write_str("\r\n"); | ||
| 68 | return buf; | ||
| 69 | } | ||
| 70 | KeyEvent::Char(c) if c.is_ascii_graphic() || c == ' ' => { | ||
| 71 | if buf.push(c).is_ok() { | ||
| 72 | rsnix::gop::put_char(c); | ||
| 73 | // Echo to serial too | ||
| 74 | let mut s = [0u8; 4]; | ||
| 75 | let s = c.encode_utf8(&mut s); | ||
| 76 | rsnix::serial::write_str(s); | ||
| 77 | } | ||
| 78 | } | ||
| 79 | KeyEvent::Backspace => { | ||
| 80 | if buf.pop().is_some() { | ||
| 81 | // GOP backspace: move cursor back and erase | ||
| 82 | rsnix::gop::put_char('\x08'); | ||
| 83 | // Serial backspace: send BS SP BS (standard terminal protocol) | ||
| 84 | rsnix::serial::write_str("\x08 \x08"); | ||
| 85 | } | ||
| 86 | } | ||
| 87 | KeyEvent::Tab => { | ||
| 88 | for _ in 0..4 { | ||
| 89 | if buf.push(' ').is_ok() { | ||
| 90 | rsnix::gop::put_char(' '); | ||
| 91 | rsnix::serial::write_str(" "); | ||
| 92 | } | ||
| 93 | } | ||
| 94 | } | ||
| 95 | _ => {} | ||
| 96 | } | ||
| 97 | } | ||
| 98 | } | ||
| 99 | |||
| 100 | /// Split a command line into the first word and the rest. | ||
| 101 | fn split_first_word(s: &str) -> (&str, &str) { | ||
| 102 | let s = s.trim_start(); | ||
| 103 | if let Some(i) = s.find(char::is_whitespace) { | ||
| 104 | (&s[..i], s[i..].trim_start()) | ||
| 105 | } else { | ||
| 106 | (s, "") | ||
| 107 | } | ||
| 108 | } | ||
| 109 | |||
| 110 | // --------------------------------------------------------------------------- | ||
| 111 | // Built-in commands | ||
| 112 | // --------------------------------------------------------------------------- | ||
| 113 | |||
| 114 | fn cmd_help() { | ||
| 115 | gop_println!("available commands:"); | ||
| 116 | gop_println!(" help — show this message"); | ||
| 117 | gop_println!(" echo — echo arguments"); | ||
| 118 | gop_println!(" clear — clear screen"); | ||
| 119 | gop_println!(" hello — print 'World!'"); | ||
| 120 | gop_println!(" info — show system info"); | ||
| 121 | gop_println!(" mem — show memory info"); | ||
| 122 | gop_println!(" halt — halt the system"); | ||
| 123 | gop_println!(" exit — alias for halt"); | ||
| 124 | serial_println!("available commands:"); | ||
| 125 | serial_println!(" help, echo, clear, hello, info, mem, halt, exit"); | ||
| 126 | } | ||
| 127 | |||
| 128 | fn cmd_echo(args: &str) { | ||
| 129 | gop_println!("{}", args); | ||
| 130 | } | ||
| 131 | |||
| 132 | fn cmd_clear() { | ||
| 133 | rsnix::gop::clear_screen(); | ||
| 134 | } | ||
| 135 | |||
| 136 | fn cmd_info() { | ||
| 137 | // Test that the global allocator works: build a String on the heap | ||
| 138 | let mut s = alloc::string::String::from("rs-dos v"); | ||
| 139 | s.push_str(env!("CARGO_PKG_VERSION")); | ||
| 140 | s.push_str(" — rsnix kernel"); | ||
| 141 | gop_println!("{}", s); | ||
| 142 | gop_println!("arch: x86_64, freestanding, no userspace"); | ||
| 143 | serial_println!("{}", s); | ||
| 144 | serial_println!("arch: x86_64, freestanding, no userspace"); | ||
| 145 | // String is freed (dropped) when this scope ends — tests dealloc too | ||
| 146 | } | ||
| 147 | |||
| 148 | fn cmd_mem() { | ||
| 149 | let (map, key) = rsnix::memory::get_map(); | ||
| 150 | |||
| 151 | let mut total_kb = 0u64; | ||
| 152 | let mut conventional_kb = 0u64; | ||
| 153 | let mut loader_kb = 0u64; | ||
| 154 | let mut boot_code_kb = 0u64; | ||
| 155 | let mut boot_data_kb = 0u64; | ||
| 156 | let mut reserved_kb = 0u64; | ||
| 157 | let mut acpi_kb = 0u64; | ||
| 158 | let mut mmio_kb = 0u64; | ||
| 159 | |||
| 160 | for desc in map { | ||
| 161 | let pages = desc.number_of_pages; | ||
| 162 | let bytes_4k = pages * 4; | ||
| 163 | total_kb += bytes_4k; | ||
| 164 | match desc.kind { | ||
| 165 | 1 => reserved_kb += bytes_4k, | ||
| 166 | 3 => boot_code_kb += bytes_4k, | ||
| 167 | 4 => boot_data_kb += bytes_4k, | ||
| 168 | 5 | 10 => acpi_kb += bytes_4k, | ||
| 169 | 6 => loader_kb += bytes_4k, | ||
| 170 | 7 => conventional_kb += bytes_4k, | ||
| 171 | 11 | 12 => mmio_kb += bytes_4k, | ||
| 172 | _ => reserved_kb += bytes_4k, | ||
| 173 | } | ||
| 174 | } | ||
| 175 | |||
| 176 | // Build a big heap string with format! to test the allocator | ||
| 177 | let mut out = alloc::string::String::new(); | ||
| 178 | use alloc::fmt::Write; | ||
| 179 | let _ = writeln!(out, "memory map (key=0x{:x}, {} entries)", key, map.len()); | ||
| 180 | let _ = writeln!( | ||
| 181 | out, | ||
| 182 | " usable {:4} MB Conventional", | ||
| 183 | conventional_kb / 1024 | ||
| 184 | ); | ||
| 185 | let _ = writeln!( | ||
| 186 | out, | ||
| 187 | " boot code {:4} MB BootServicesCode", | ||
| 188 | boot_code_kb / 1024 | ||
| 189 | ); | ||
| 190 | let _ = writeln!( | ||
| 191 | out, | ||
| 192 | " boot data {:4} MB BootServicesData", | ||
| 193 | boot_data_kb / 1024 | ||
| 194 | ); | ||
| 195 | let _ = writeln!(out, " loader {:4} MB LoaderData", loader_kb / 1024); | ||
| 196 | let _ = writeln!(out, " ACPI {:4} MB", acpi_kb / 1024); | ||
| 197 | let _ = writeln!(out, " MMIO {:4} MB", mmio_kb / 1024); | ||
| 198 | let _ = writeln!(out, " reserved {:4} MB", reserved_kb / 1024); | ||
| 199 | let _ = writeln!( | ||
| 200 | out, | ||
| 201 | " raw total {:4} MB (incl. PCIe MMIO)", | ||
| 202 | total_kb / 1024 | ||
| 203 | ); | ||
| 204 | |||
| 205 | gop_print!("{}", out); | ||
| 206 | serial_print!("{}", out); | ||
| 207 | } | ||
| 208 | |||
| 209 | fn cmd_halt() -> ! { | ||
| 210 | gop_println!("halting..."); | ||
| 211 | serial_println!("halting..."); | ||
| 212 | loop { | ||
| 213 | unsafe { | ||
| 214 | core::arch::asm!("cli; hlt"); | ||
| 215 | } | ||
| 216 | } | ||
| 217 | } | ||
| 218 | |||
| 219 | // --------------------------------------------------------------------------- | ||
| 220 | // Minimal fixed-capacity string for no-stack use (avoids alloc dependency) | ||
| 221 | // --------------------------------------------------------------------------- | ||
| 222 | mod heapless_str { | ||
| 223 | use core::fmt; | ||
| 224 | |||
| 225 | /// A fixed-capacity string allocated entirely on the stack. | ||
| 226 | pub struct String<const N: usize> { | ||
| 227 | buf: [u8; N], | ||
| 228 | len: usize, | ||
| 229 | } | ||
| 230 | |||
| 231 | impl<const N: usize> String<N> { | ||
| 232 | pub const fn new() -> Self { | ||
| 233 | String { | ||
| 234 | buf: [0u8; N], | ||
| 235 | len: 0, | ||
| 236 | } | ||
| 237 | } | ||
| 238 | |||
| 239 | pub fn as_str(&self) -> &str { | ||
| 240 | // SAFETY: we only write valid ASCII bytes. | ||
| 241 | unsafe { core::str::from_utf8_unchecked(&self.buf[..self.len]) } | ||
| 242 | } | ||
| 243 | |||
| 244 | pub fn push(&mut self, c: char) -> Result<(), ()> { | ||
| 245 | if self.len >= N { | ||
| 246 | return Err(()); | ||
| 247 | } | ||
| 248 | self.buf[self.len] = c as u8; | ||
| 249 | self.len += 1; | ||
| 250 | Ok(()) | ||
| 251 | } | ||
| 252 | |||
| 253 | pub fn pop(&mut self) -> Option<char> { | ||
| 254 | if self.len == 0 { | ||
| 255 | return None; | ||
| 256 | } | ||
| 257 | self.len -= 1; | ||
| 258 | Some(self.buf[self.len] as char) | ||
| 259 | } | ||
| 260 | |||
| 261 | pub fn is_empty(&self) -> bool { | ||
| 262 | self.len == 0 | ||
| 263 | } | ||
| 264 | |||
| 265 | pub fn clear(&mut self) { | ||
| 266 | self.len = 0; | ||
| 267 | } | ||
| 268 | |||
| 269 | pub fn trim(&self) -> &str { | ||
| 270 | self.as_str().trim() | ||
| 271 | } | ||
| 272 | } | ||
| 273 | |||
| 274 | impl<const N: usize> fmt::Display for String<N> { | ||
| 275 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
| 276 | write!(f, "{}", self.as_str()) | ||
| 277 | } | ||
| 278 | } | ||
| 279 | |||
| 280 | impl<const N: usize> fmt::Debug for String<N> { | ||
| 281 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
| 282 | write!(f, "{:?}", self.as_str()) | ||
| 283 | } | ||
| 284 | } | ||
| 285 | } | ||
