From 43c44ae6fe7dbfcf6af3e5e53c29f65bbf94c9c2 Mon Sep 17 00:00:00 2001 From: salmonik Date: Fri, 31 Jul 2026 16:43:04 +0200 Subject: shell: ^C cancels the line / aborts streams, ^D = EOF → poweroff MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- shell/src/lib.rs | 173 +++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 116 insertions(+), 57 deletions(-) (limited to 'shell') 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() -> ! { serial_print!("> "); // Read one line from keyboard - let line = read_line(); - - // Parse and execute - let trimmed = line.trim(); - if trimmed.is_empty() { - continue; - } - - let (cmd, args) = split_first_word(trimmed); - - match cmd { - "help" => cmd_help(), - "echo" => cmd_echo(args), - "clear" => cmd_clear(), - "halt" => cmd_halt(), - "poweroff" | "exit" => cmd_poweroff(), - "hello" => { - gop_println!("World!"); - serial_println!("World!"); + match read_line() { + LineInput::Interrupted => continue, // ^C — fresh prompt + LineInput::Eof => { + // ^D on an empty line: EOF, exit the shell. + cmd_poweroff(); } - "info" => cmd_info(), - "mem" => cmd_mem(), - "mmap" => cmd_mmap(), - "stress" => cmd_stress(args), - "date" => cmd_date(), - "pci" => cmd_pci(), - "ls" => cmd_ls(args), - "cat" => cmd_cat(args), - "hexdump" => cmd_hexdump(args), - "mkdir" => cmd_mkdir(args), - "touch" => cmd_touch(args), - "rm" => cmd_rm(args), - _ => { - gop_println!("unknown command: {}", cmd); - serial_println!("unknown command: {}", cmd); + LineInput::Line(line) => { + // Parse and execute + let trimmed = line.trim(); + if trimmed.is_empty() { + continue; + } + + let (cmd, args) = split_first_word(trimmed); + + match cmd { + "help" => cmd_help(), + "echo" => cmd_echo(args), + "clear" => cmd_clear(), + "halt" => cmd_halt(), + "poweroff" | "exit" => cmd_poweroff(), + "hello" => { + gop_println!("World!"); + serial_println!("World!"); + } + "info" => cmd_info(), + "mem" => cmd_mem(), + "mmap" => cmd_mmap(), + "stress" => cmd_stress(args), + "date" => cmd_date(), + "pci" => cmd_pci(), + "ls" => cmd_ls(args), + "cat" => cmd_cat(args), + "hexdump" => cmd_hexdump(args), + "mkdir" => cmd_mkdir(args), + "touch" => cmd_touch(args), + "rm" => cmd_rm(args), + _ => { + gop_println!("unknown command: {}", cmd); + serial_println!("unknown command: {}", cmd); + } + } } } } } +/// Result of one line-input session. +enum LineInput { + Line(heapless_str::String), + Interrupted, // ^C + Eof, // ^D on an empty line +} + // --------------------------------------------------------------------------- // Line input (PS/2 keyboard, no heap) // --------------------------------------------------------------------------- -fn read_line() -> heapless_str::String { +fn read_line() -> LineInput { let mut buf = heapless_str::String::::new(); loop { match ps2::poll_key() { KeyEvent::Enter => { rsnix::gop::put_char('\n'); rsnix::serial::write_str("\r\n"); - return buf; + return LineInput::Line(buf); + } + KeyEvent::CtrlC => { + // Cancel the current line — like a real terminal, echo ^C. + buf.clear(); + rsnix::gop::put_char('^'); + rsnix::gop::put_char('C'); + rsnix::gop::put_char('\n'); + rsnix::serial::write_str("^C\r\n"); + return LineInput::Interrupted; + } + KeyEvent::CtrlD => { + if buf.is_empty() { + // EOF on an empty line — exit the shell. + rsnix::gop::put_char('^'); + rsnix::gop::put_char('D'); + rsnix::gop::put_char('\n'); + rsnix::serial::write_str("^D\r\n"); + return LineInput::Eof; + } + // ^D with text on the line: ignored (bash semantics). } KeyEvent::Char(c) if c.is_ascii_graphic() || c == ' ' => { if buf.push(c).is_ok() { @@ -163,8 +197,7 @@ fn cmd_echo(args: &str) { let content = alloc::format!("{}\n", text.trim_end()); let result = rsnix::fs::ensure_file(path).and_then(|h| { if append { - rsnix::fs::write(h, content.as_bytes(), rsnix::fs::size(h)) - .map(|_| ()) + rsnix::fs::write(h, content.as_bytes(), rsnix::fs::size(h)).map(|_| ()) } else { let _ = rsnix::fs::set_len(h, 0); rsnix::fs::write(h, content.as_bytes(), 0).map(|_| ()) @@ -204,9 +237,7 @@ fn cmd_ls(args: &str) { for (name, kind, size) in list { match kind { rsnix::fs::FileType::Dir => out.push_str(&alloc::format!("{}/\n", name)), - rsnix::fs::FileType::Char => { - out.push_str(&alloc::format!("{}@\n", name)) - } + rsnix::fs::FileType::Char => out.push_str(&alloc::format!("{}@\n", name)), rsnix::fs::FileType::Regular => { out.push_str(&alloc::format!("{} {} B\n", name, size)) } @@ -225,18 +256,28 @@ fn cmd_cat(args: &str) { serial_println!("usage: cat [path...]"); return; } - let mut out = alloc::string::String::new(); let mut buf = [0u8; 512]; for p in paths { match rsnix::fs::resolve(p) { - None => out.push_str(&alloc::format!("cat: {}: no such file\n", p)), + None => { + let msg = alloc::format!("cat: {}: no such file\n", p); + gop_print!("{}", msg); + serial_print!("{}", msg); + } Some(h) => { let mut pos = 0u64; loop { + if interrupt_pressed() { + gop_println!("^C"); + serial_println!("^C"); + return; + } match rsnix::fs::read(h, &mut buf, pos) { Ok(0) | Err(_) => break, Ok(n) => { - out.push_str(core::str::from_utf8(&buf[..n]).unwrap_or("?")); + let chunk = core::str::from_utf8(&buf[..n]).unwrap_or("?"); + gop_print!("{}", chunk); + serial_print!("{}", chunk); pos += n as u64; } } @@ -244,8 +285,6 @@ fn cmd_cat(args: &str) { } } } - gop_print!("{}", out); - serial_print!("{}", out); } fn cmd_hexdump(args: &str) { @@ -263,15 +302,20 @@ fn cmd_hexdump(args: &str) { return; } }; - let mut out = alloc::string::String::new(); let mut buf = [0u8; 512]; let mut pos = 0u64; - use core::fmt::Write as _; - 'outer: loop { + loop { + if interrupt_pressed() { + gop_println!("^C"); + serial_println!("^C"); + return; + } let n = match rsnix::fs::read(h, &mut buf, pos) { Ok(0) | Err(_) => break, Ok(n) => n, }; + let mut out = alloc::string::String::new(); + use core::fmt::Write as _; for i in (0..n).step_by(16) { let _ = core::write!(out, "{:08x} ", pos + i as u64); for j in 0..16 { @@ -294,14 +338,23 @@ fn cmd_hexdump(args: &str) { }); } out.push_str("|\n"); - if i + 16 >= n { - pos += n as u64; - continue 'outer; - } } + gop_print!("{}", out); + serial_print!("{}", out); + pos += n as u64; } - gop_print!("{}", out); - serial_print!("{}", out); +} + +/// Returns true if the user pressed ^C since the last check; drains the +/// keyboard buffer of any other buffered keys. +fn interrupt_pressed() -> bool { + let mut pressed = false; + while let Some(ev) = ps2::try_poll_key() { + if matches!(ev, KeyEvent::CtrlC) { + pressed = true; + } + } + pressed } fn cmd_mkdir(args: &str) { @@ -346,8 +399,14 @@ fn cmd_rm(args: &str) { match rsnix::fs::remove_path(path) { Ok(()) => {} Err(()) => { - gop_println!("rm: cannot remove '{}' (missing, dir not empty, or busy)", path); - serial_println!("rm: cannot remove '{}' (missing, dir not empty, or busy)", path); + gop_println!( + "rm: cannot remove '{}' (missing, dir not empty, or busy)", + path + ); + serial_println!( + "rm: cannot remove '{}' (missing, dir not empty, or busy)", + path + ); } } } -- cgit v1.2.3