summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-07-31 16:43:04 +0200
committersalmonik <salmonik@tuta.io>2026-07-31 16:43:04 +0200
commit43c44ae6fe7dbfcf6af3e5e53c29f65bbf94c9c2 (patch)
tree3fe86cc049f154dc33202b6573e091d6cace7305
parentvfs: everything is a file — dirs, ramfs, character devices (diff)
downloadrs-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
-rw-r--r--CONTEXT.md3
-rw-r--r--rsnix/src/fs.rs32
-rw-r--r--rsnix/src/ps2.rs11
-rw-r--r--shell/src/lib.rs173
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:
130✅ **VFS** — “everything is a file”: dirs, ramfs, char devices 130✅ **VFS** — “everything is a file”: dirs, ramfs, char devices
131 (`ls`/`cat`/`hexdump`/`mkdir`/`touch`/`rm`, `echo >`/`>>`; devices 131 (`ls`/`cat`/`hexdump`/`mkdir`/`touch`/`rm`, `echo >`/`>>`; devices
132 regenerate on read, `/dev/mem` is raw physical RAM) 132 regenerate on read, `/dev/mem` is raw physical RAM)
133✅ **^C / ^D** — Ctrl+C cancels the line or aborts a running stream
134 (`cat`/`hexdump` check between chunks), Ctrl+D on an empty line
135 powers off (EOF)
133 136
134## Target audience 137## Target audience
135 138
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 @@
15 15
16extern crate alloc; 16extern crate alloc;
17 17
18use alloc::boxed::Box;
19use alloc::string::String;
20use alloc::vec::Vec;
21use crate::gop; 18use crate::gop;
22use crate::memory; 19use crate::memory;
23use crate::pci; 20use crate::pci;
24use crate::rtc; 21use crate::rtc;
25use crate::serial; 22use crate::serial;
23use alloc::boxed::Box;
24use alloc::string::String;
25use alloc::vec::Vec;
26 26
27/// /dev/mem covers the low physical-memory window (identity-mapped RAM). 27/// /dev/mem covers the low physical-memory window (identity-mapped RAM).
28pub const MEM_WINDOW: u64 = 64 * 1024; 28pub const MEM_WINDOW: u64 = 64 * 1024;
@@ -96,9 +96,24 @@ pub fn init() -> (usize, usize) {
96 let _tmp = add("tmp", Some(root), FileType::Dir, NodeData::Dir(Vec::new())); 96 let _tmp = add("tmp", Some(root), FileType::Dir, NodeData::Dir(Vec::new()));
97 let etc = add("etc", Some(root), FileType::Dir, NodeData::Dir(Vec::new())); 97 let etc = add("etc", Some(root), FileType::Dir, NodeData::Dir(Vec::new()));
98 98
99 add("mem", Some(dev), FileType::Char, NodeData::Char(CharDevice::Mem)); 99 add(
100 add("rtc", Some(dev), FileType::Char, NodeData::Char(CharDevice::Rtc)); 100 "mem",
101 add("pci", Some(dev), FileType::Char, NodeData::Char(CharDevice::Pci)); 101 Some(dev),
102 FileType::Char,
103 NodeData::Char(CharDevice::Mem),
104 );
105 add(
106 "rtc",
107 Some(dev),
108 FileType::Char,
109 NodeData::Char(CharDevice::Rtc),
110 );
111 add(
112 "pci",
113 Some(dev),
114 FileType::Char,
115 NodeData::Char(CharDevice::Pci),
116 );
102 add( 117 add(
103 "version", 118 "version",
104 Some(dev), 119 Some(dev),
@@ -146,10 +161,7 @@ pub fn init() -> (usize, usize) {
146 ), 161 ),
147 ); 162 );
148 163
149 let dirs = nodes() 164 let dirs = nodes().iter().filter(|n| n.kind == FileType::Dir).count();
150 .iter()
151 .filter(|n| n.kind == FileType::Dir)
152 .count();
153 let files = nodes().len() - dirs; 165 let files = nodes().len() - dirs;
154 (dirs, files) 166 (dirs, files)
155} 167}
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 {
22 Backspace, 22 Backspace,
23 Tab, 23 Tab,
24 Escape, 24 Escape,
25 CtrlC,
26 CtrlD,
25 ArrowUp, 27 ArrowUp,
26 ArrowDown, 28 ArrowDown,
27 ArrowLeft, 29 ArrowLeft,
@@ -304,6 +306,15 @@ fn process_byte(byte: u8) -> Option<KeyEvent> {
304 ch.to_ascii_uppercase() 306 ch.to_ascii_uppercase()
305 }; 307 };
306 } 308 }
309 // Ctrl+letter — the shell uses ^C (interrupt) and ^D (EOF).
310 let ctrl = state.modifiers & (MOD_LCTRL | MOD_RCTRL) != 0;
311 if ctrl {
312 return match ch.to_ascii_lowercase() {
313 b'c' => Some(KeyEvent::CtrlC),
314 b'd' => Some(KeyEvent::CtrlD),
315 _ => None, // swallow other Ctrl+key combinations
316 };
317 }
307 if ch != 0 { 318 if ch != 0 {
308 Some(KeyEvent::Char(ch as char)) 319 Some(KeyEvent::Char(ch as char))
309 } else { 320 } 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() -> ! {
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.
79enum 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// ---------------------------------------------------------------------------
74fn read_line() -> heapless_str::String<LINE_BUF_SIZE> { 88fn 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
251fn cmd_hexdump(args: &str) { 290fn 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.
350fn 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
307fn cmd_mkdir(args: &str) { 360fn 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}