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 --- CONTEXT.md | 3 + rsnix/src/fs.rs | 32 ++++++---- rsnix/src/ps2.rs | 11 ++++ shell/src/lib.rs | 173 +++++++++++++++++++++++++++++++++++++------------------ 4 files changed, 152 insertions(+), 67 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index 5b76eaa..66ab320 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -130,6 +130,9 @@ Single Rust crate (`x86_64-unknown-uefi` target). Module breakdown: ✅ **VFS** — “everything is a file”: dirs, ramfs, char devices (`ls`/`cat`/`hexdump`/`mkdir`/`touch`/`rm`, `echo >`/`>>`; devices regenerate on read, `/dev/mem` is raw physical RAM) +✅ **^C / ^D** — Ctrl+C cancels the line or aborts a running stream + (`cat`/`hexdump` check between chunks), Ctrl+D on an empty line + powers off (EOF) ## Target audience diff --git a/rsnix/src/fs.rs b/rsnix/src/fs.rs index 56820bb..95a84bf 100644 --- a/rsnix/src/fs.rs +++ b/rsnix/src/fs.rs @@ -15,14 +15,14 @@ extern crate alloc; -use alloc::boxed::Box; -use alloc::string::String; -use alloc::vec::Vec; use crate::gop; use crate::memory; use crate::pci; use crate::rtc; use crate::serial; +use alloc::boxed::Box; +use alloc::string::String; +use alloc::vec::Vec; /// /dev/mem covers the low physical-memory window (identity-mapped RAM). pub const MEM_WINDOW: u64 = 64 * 1024; @@ -96,9 +96,24 @@ pub fn init() -> (usize, usize) { let _tmp = add("tmp", Some(root), FileType::Dir, NodeData::Dir(Vec::new())); let etc = add("etc", Some(root), FileType::Dir, NodeData::Dir(Vec::new())); - add("mem", Some(dev), FileType::Char, NodeData::Char(CharDevice::Mem)); - add("rtc", Some(dev), FileType::Char, NodeData::Char(CharDevice::Rtc)); - add("pci", Some(dev), FileType::Char, NodeData::Char(CharDevice::Pci)); + add( + "mem", + Some(dev), + FileType::Char, + NodeData::Char(CharDevice::Mem), + ); + add( + "rtc", + Some(dev), + FileType::Char, + NodeData::Char(CharDevice::Rtc), + ); + add( + "pci", + Some(dev), + FileType::Char, + NodeData::Char(CharDevice::Pci), + ); add( "version", Some(dev), @@ -146,10 +161,7 @@ pub fn init() -> (usize, usize) { ), ); - let dirs = nodes() - .iter() - .filter(|n| n.kind == FileType::Dir) - .count(); + let dirs = nodes().iter().filter(|n| n.kind == FileType::Dir).count(); let files = nodes().len() - dirs; (dirs, files) } diff --git a/rsnix/src/ps2.rs b/rsnix/src/ps2.rs index 477d146..1b10b3b 100644 --- a/rsnix/src/ps2.rs +++ b/rsnix/src/ps2.rs @@ -22,6 +22,8 @@ pub enum KeyEvent { Backspace, Tab, Escape, + CtrlC, + CtrlD, ArrowUp, ArrowDown, ArrowLeft, @@ -304,6 +306,15 @@ fn process_byte(byte: u8) -> Option { ch.to_ascii_uppercase() }; } + // Ctrl+letter — the shell uses ^C (interrupt) and ^D (EOF). + let ctrl = state.modifiers & (MOD_LCTRL | MOD_RCTRL) != 0; + if ctrl { + return match ch.to_ascii_lowercase() { + b'c' => Some(KeyEvent::CtrlC), + b'd' => Some(KeyEvent::CtrlD), + _ => None, // swallow other Ctrl+key combinations + }; + } if ch != 0 { Some(KeyEvent::Char(ch as char)) } else { 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