diff options
| author | salmonik <salmonik@tuta.io> | 2026-07-31 16:43:04 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-07-31 16:43:04 +0200 |
| commit | 43c44ae6fe7dbfcf6af3e5e53c29f65bbf94c9c2 (patch) | |
| tree | 3fe86cc049f154dc33202b6573e091d6cace7305 /shell/src/lib.rs | |
| parent | vfs: everything is a file — dirs, ramfs, character devices (diff) | |
| download | rs-dos-43c44ae6fe7dbfcf6af3e5e53c29f65bbf94c9c2.tar.gz rs-dos-43c44ae6fe7dbfcf6af3e5e53c29f65bbf94c9c2.zip | |
shell: ^C cancels the line / aborts streams, ^D = EOF → poweroff
- ps2: Ctrl+C and Ctrl+D are now KeyEvents (ctrl modifier + c/d map);
other Ctrl+key combos are swallowed
- shell read_line: ^C clears the line and echoes '^C' (bash-style),
^D on an empty line echoes '^D' and shuts down (EOF), ^D with text
on the line is ignored
- cat/hexdump stream incrementally (was: build whole string then print)
and check for ^C between chunks, so a running dump can be aborted
- verified in QEMU: line cancel, standalone interrupt, ^C aborts an
infinite 'cat /dev/zero' stream, ^D powers off
Diffstat (limited to '')
| -rw-r--r-- | shell/src/lib.rs | 173 |
1 files changed, 116 insertions, 57 deletions
diff --git a/shell/src/lib.rs b/shell/src/lib.rs index 7e1439e..299956d 100644 --- a/shell/src/lib.rs +++ b/shell/src/lib.rs | |||
| @@ -28,57 +28,91 @@ pub fn run() -> ! { | |||
| 28 | serial_print!("> "); | 28 | serial_print!("> "); |
| 29 | 29 | ||
| 30 | // Read one line from keyboard | 30 | // Read one line from keyboard |
| 31 | let line = read_line(); | 31 | match read_line() { |
| 32 | 32 | LineInput::Interrupted => continue, // ^C — fresh prompt | |
| 33 | // Parse and execute | 33 | LineInput::Eof => { |
| 34 | let trimmed = line.trim(); | 34 | // ^D on an empty line: EOF, exit the shell. |
| 35 | if trimmed.is_empty() { | 35 | cmd_poweroff(); |
| 36 | continue; | ||
| 37 | } | ||
| 38 | |||
| 39 | let (cmd, args) = split_first_word(trimmed); | ||
| 40 | |||
| 41 | match cmd { | ||
| 42 | "help" => cmd_help(), | ||
| 43 | "echo" => cmd_echo(args), | ||
| 44 | "clear" => cmd_clear(), | ||
| 45 | "halt" => cmd_halt(), | ||
| 46 | "poweroff" | "exit" => cmd_poweroff(), | ||
| 47 | "hello" => { | ||
| 48 | gop_println!("World!"); | ||
| 49 | serial_println!("World!"); | ||
| 50 | } | 36 | } |
| 51 | "info" => cmd_info(), | 37 | LineInput::Line(line) => { |
| 52 | "mem" => cmd_mem(), | 38 | // Parse and execute |
| 53 | "mmap" => cmd_mmap(), | 39 | let trimmed = line.trim(); |
| 54 | "stress" => cmd_stress(args), | 40 | if trimmed.is_empty() { |
| 55 | "date" => cmd_date(), | 41 | continue; |
| 56 | "pci" => cmd_pci(), | 42 | } |
| 57 | "ls" => cmd_ls(args), | 43 | |
| 58 | "cat" => cmd_cat(args), | 44 | let (cmd, args) = split_first_word(trimmed); |
| 59 | "hexdump" => cmd_hexdump(args), | 45 | |
| 60 | "mkdir" => cmd_mkdir(args), | 46 | match cmd { |
| 61 | "touch" => cmd_touch(args), | 47 | "help" => cmd_help(), |
| 62 | "rm" => cmd_rm(args), | 48 | "echo" => cmd_echo(args), |
| 63 | _ => { | 49 | "clear" => cmd_clear(), |
| 64 | gop_println!("unknown command: {}", cmd); | 50 | "halt" => cmd_halt(), |
| 65 | serial_println!("unknown command: {}", cmd); | 51 | "poweroff" | "exit" => cmd_poweroff(), |
| 52 | "hello" => { | ||
| 53 | gop_println!("World!"); | ||
| 54 | serial_println!("World!"); | ||
| 55 | } | ||
| 56 | "info" => cmd_info(), | ||
| 57 | "mem" => cmd_mem(), | ||
| 58 | "mmap" => cmd_mmap(), | ||
| 59 | "stress" => cmd_stress(args), | ||
| 60 | "date" => cmd_date(), | ||
| 61 | "pci" => cmd_pci(), | ||
| 62 | "ls" => cmd_ls(args), | ||
| 63 | "cat" => cmd_cat(args), | ||
| 64 | "hexdump" => cmd_hexdump(args), | ||
| 65 | "mkdir" => cmd_mkdir(args), | ||
| 66 | "touch" => cmd_touch(args), | ||
| 67 | "rm" => cmd_rm(args), | ||
| 68 | _ => { | ||
| 69 | gop_println!("unknown command: {}", cmd); | ||
| 70 | serial_println!("unknown command: {}", cmd); | ||
| 71 | } | ||
| 72 | } | ||
| 66 | } | 73 | } |
| 67 | } | 74 | } |
| 68 | } | 75 | } |
| 69 | } | 76 | } |
| 70 | 77 | ||
| 78 | /// Result of one line-input session. | ||
| 79 | enum LineInput { | ||
| 80 | Line(heapless_str::String<LINE_BUF_SIZE>), | ||
| 81 | Interrupted, // ^C | ||
| 82 | Eof, // ^D on an empty line | ||
| 83 | } | ||
| 84 | |||
| 71 | // --------------------------------------------------------------------------- | 85 | // --------------------------------------------------------------------------- |
| 72 | // Line input (PS/2 keyboard, no heap) | 86 | // Line input (PS/2 keyboard, no heap) |
| 73 | // --------------------------------------------------------------------------- | 87 | // --------------------------------------------------------------------------- |
| 74 | fn read_line() -> heapless_str::String<LINE_BUF_SIZE> { | 88 | fn read_line() -> LineInput { |
| 75 | let mut buf = heapless_str::String::<LINE_BUF_SIZE>::new(); | 89 | let mut buf = heapless_str::String::<LINE_BUF_SIZE>::new(); |
| 76 | loop { | 90 | loop { |
| 77 | match ps2::poll_key() { | 91 | match ps2::poll_key() { |
| 78 | KeyEvent::Enter => { | 92 | KeyEvent::Enter => { |
| 79 | rsnix::gop::put_char('\n'); | 93 | rsnix::gop::put_char('\n'); |
| 80 | rsnix::serial::write_str("\r\n"); | 94 | rsnix::serial::write_str("\r\n"); |
| 81 | return buf; | 95 | return LineInput::Line(buf); |
| 96 | } | ||
| 97 | KeyEvent::CtrlC => { | ||
| 98 | // Cancel the current line — like a real terminal, echo ^C. | ||
| 99 | buf.clear(); | ||
| 100 | rsnix::gop::put_char('^'); | ||
| 101 | rsnix::gop::put_char('C'); | ||
| 102 | rsnix::gop::put_char('\n'); | ||
| 103 | rsnix::serial::write_str("^C\r\n"); | ||
| 104 | return LineInput::Interrupted; | ||
| 105 | } | ||
| 106 | KeyEvent::CtrlD => { | ||
| 107 | if buf.is_empty() { | ||
| 108 | // EOF on an empty line — exit the shell. | ||
| 109 | rsnix::gop::put_char('^'); | ||
| 110 | rsnix::gop::put_char('D'); | ||
| 111 | rsnix::gop::put_char('\n'); | ||
| 112 | rsnix::serial::write_str("^D\r\n"); | ||
| 113 | return LineInput::Eof; | ||
| 114 | } | ||
| 115 | // ^D with text on the line: ignored (bash semantics). | ||
| 82 | } | 116 | } |
| 83 | KeyEvent::Char(c) if c.is_ascii_graphic() || c == ' ' => { | 117 | KeyEvent::Char(c) if c.is_ascii_graphic() || c == ' ' => { |
| 84 | if buf.push(c).is_ok() { | 118 | if buf.push(c).is_ok() { |
| @@ -163,8 +197,7 @@ fn cmd_echo(args: &str) { | |||
| 163 | let content = alloc::format!("{}\n", text.trim_end()); | 197 | let content = alloc::format!("{}\n", text.trim_end()); |
| 164 | let result = rsnix::fs::ensure_file(path).and_then(|h| { | 198 | let result = rsnix::fs::ensure_file(path).and_then(|h| { |
| 165 | if append { | 199 | if append { |
| 166 | rsnix::fs::write(h, content.as_bytes(), rsnix::fs::size(h)) | 200 | rsnix::fs::write(h, content.as_bytes(), rsnix::fs::size(h)).map(|_| ()) |
| 167 | .map(|_| ()) | ||
| 168 | } else { | 201 | } else { |
| 169 | let _ = rsnix::fs::set_len(h, 0); | 202 | let _ = rsnix::fs::set_len(h, 0); |
| 170 | rsnix::fs::write(h, content.as_bytes(), 0).map(|_| ()) | 203 | rsnix::fs::write(h, content.as_bytes(), 0).map(|_| ()) |
| @@ -204,9 +237,7 @@ fn cmd_ls(args: &str) { | |||
| 204 | for (name, kind, size) in list { | 237 | for (name, kind, size) in list { |
| 205 | match kind { | 238 | match kind { |
| 206 | rsnix::fs::FileType::Dir => out.push_str(&alloc::format!("{}/\n", name)), | 239 | rsnix::fs::FileType::Dir => out.push_str(&alloc::format!("{}/\n", name)), |
| 207 | rsnix::fs::FileType::Char => { | 240 | rsnix::fs::FileType::Char => out.push_str(&alloc::format!("{}@\n", name)), |
| 208 | out.push_str(&alloc::format!("{}@\n", name)) | ||
| 209 | } | ||
| 210 | rsnix::fs::FileType::Regular => { | 241 | rsnix::fs::FileType::Regular => { |
| 211 | out.push_str(&alloc::format!("{} {} B\n", name, size)) | 242 | out.push_str(&alloc::format!("{} {} B\n", name, size)) |
| 212 | } | 243 | } |
| @@ -225,18 +256,28 @@ fn cmd_cat(args: &str) { | |||
| 225 | serial_println!("usage: cat <path> [path...]"); | 256 | serial_println!("usage: cat <path> [path...]"); |
| 226 | return; | 257 | return; |
| 227 | } | 258 | } |
| 228 | let mut out = alloc::string::String::new(); | ||
| 229 | let mut buf = [0u8; 512]; | 259 | let mut buf = [0u8; 512]; |
| 230 | for p in paths { | 260 | for p in paths { |
| 231 | match rsnix::fs::resolve(p) { | 261 | match rsnix::fs::resolve(p) { |
| 232 | None => out.push_str(&alloc::format!("cat: {}: no such file\n", p)), | 262 | None => { |
| 263 | let msg = alloc::format!("cat: {}: no such file\n", p); | ||
| 264 | gop_print!("{}", msg); | ||
| 265 | serial_print!("{}", msg); | ||
| 266 | } | ||
| 233 | Some(h) => { | 267 | Some(h) => { |
| 234 | let mut pos = 0u64; | 268 | let mut pos = 0u64; |
| 235 | loop { | 269 | loop { |
| 270 | if interrupt_pressed() { | ||
| 271 | gop_println!("^C"); | ||
| 272 | serial_println!("^C"); | ||
| 273 | return; | ||
| 274 | } | ||
| 236 | match rsnix::fs::read(h, &mut buf, pos) { | 275 | match rsnix::fs::read(h, &mut buf, pos) { |
| 237 | Ok(0) | Err(_) => break, | 276 | Ok(0) | Err(_) => break, |
| 238 | Ok(n) => { | 277 | Ok(n) => { |
| 239 | out.push_str(core::str::from_utf8(&buf[..n]).unwrap_or("?")); | 278 | let chunk = core::str::from_utf8(&buf[..n]).unwrap_or("?"); |
| 279 | gop_print!("{}", chunk); | ||
| 280 | serial_print!("{}", chunk); | ||
| 240 | pos += n as u64; | 281 | pos += n as u64; |
| 241 | } | 282 | } |
| 242 | } | 283 | } |
| @@ -244,8 +285,6 @@ fn cmd_cat(args: &str) { | |||
| 244 | } | 285 | } |
| 245 | } | 286 | } |
| 246 | } | 287 | } |
| 247 | gop_print!("{}", out); | ||
| 248 | serial_print!("{}", out); | ||
| 249 | } | 288 | } |
| 250 | 289 | ||
| 251 | fn cmd_hexdump(args: &str) { | 290 | fn cmd_hexdump(args: &str) { |
| @@ -263,15 +302,20 @@ fn cmd_hexdump(args: &str) { | |||
| 263 | return; | 302 | return; |
| 264 | } | 303 | } |
| 265 | }; | 304 | }; |
| 266 | let mut out = alloc::string::String::new(); | ||
| 267 | let mut buf = [0u8; 512]; | 305 | let mut buf = [0u8; 512]; |
| 268 | let mut pos = 0u64; | 306 | let mut pos = 0u64; |
| 269 | use core::fmt::Write as _; | 307 | loop { |
| 270 | 'outer: loop { | 308 | if interrupt_pressed() { |
| 309 | gop_println!("^C"); | ||
| 310 | serial_println!("^C"); | ||
| 311 | return; | ||
| 312 | } | ||
| 271 | let n = match rsnix::fs::read(h, &mut buf, pos) { | 313 | let n = match rsnix::fs::read(h, &mut buf, pos) { |
| 272 | Ok(0) | Err(_) => break, | 314 | Ok(0) | Err(_) => break, |
| 273 | Ok(n) => n, | 315 | Ok(n) => n, |
| 274 | }; | 316 | }; |
| 317 | let mut out = alloc::string::String::new(); | ||
| 318 | use core::fmt::Write as _; | ||
| 275 | for i in (0..n).step_by(16) { | 319 | for i in (0..n).step_by(16) { |
| 276 | let _ = core::write!(out, "{:08x} ", pos + i as u64); | 320 | let _ = core::write!(out, "{:08x} ", pos + i as u64); |
| 277 | for j in 0..16 { | 321 | for j in 0..16 { |
| @@ -294,14 +338,23 @@ fn cmd_hexdump(args: &str) { | |||
| 294 | }); | 338 | }); |
| 295 | } | 339 | } |
| 296 | out.push_str("|\n"); | 340 | out.push_str("|\n"); |
| 297 | if i + 16 >= n { | ||
| 298 | pos += n as u64; | ||
| 299 | continue 'outer; | ||
| 300 | } | ||
| 301 | } | 341 | } |
| 342 | gop_print!("{}", out); | ||
| 343 | serial_print!("{}", out); | ||
| 344 | pos += n as u64; | ||
| 302 | } | 345 | } |
| 303 | gop_print!("{}", out); | 346 | } |
| 304 | serial_print!("{}", out); | 347 | |
| 348 | /// Returns true if the user pressed ^C since the last check; drains the | ||
| 349 | /// keyboard buffer of any other buffered keys. | ||
| 350 | fn interrupt_pressed() -> bool { | ||
| 351 | let mut pressed = false; | ||
| 352 | while let Some(ev) = ps2::try_poll_key() { | ||
| 353 | if matches!(ev, KeyEvent::CtrlC) { | ||
| 354 | pressed = true; | ||
| 355 | } | ||
| 356 | } | ||
| 357 | pressed | ||
| 305 | } | 358 | } |
| 306 | 359 | ||
| 307 | fn cmd_mkdir(args: &str) { | 360 | fn cmd_mkdir(args: &str) { |
| @@ -346,8 +399,14 @@ fn cmd_rm(args: &str) { | |||
| 346 | match rsnix::fs::remove_path(path) { | 399 | match rsnix::fs::remove_path(path) { |
| 347 | Ok(()) => {} | 400 | Ok(()) => {} |
| 348 | Err(()) => { | 401 | Err(()) => { |
| 349 | gop_println!("rm: cannot remove '{}' (missing, dir not empty, or busy)", path); | 402 | gop_println!( |
| 350 | serial_println!("rm: cannot remove '{}' (missing, dir not empty, or busy)", path); | 403 | "rm: cannot remove '{}' (missing, dir not empty, or busy)", |
| 404 | path | ||
| 405 | ); | ||
| 406 | serial_println!( | ||
| 407 | "rm: cannot remove '{}' (missing, dir not empty, or busy)", | ||
| 408 | path | ||
| 409 | ); | ||
| 351 | } | 410 | } |
| 352 | } | 411 | } |
| 353 | } | 412 | } |
