summaryrefslogtreecommitdiffstats
path: root/shell/src
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-07-31 17:19:09 +0200
committersalmonik <salmonik@tuta.io>2026-07-31 17:19:09 +0200
commitfa06681a8418b7e31af96e010bb424e178402f4d (patch)
tree3b2fa8674384078f73b62274c71e8279ca6d0644 /shell/src
parentshell: ^C cancels the line / aborts streams, ^D = EOF → poweroff (diff)
downloadrs-dos-fa06681a8418b7e31af96e010bb424e178402f4d.tar.gz
rs-dos-fa06681a8418b7e31af96e010bb424e178402f4d.zip
tty + vt terminal split, rsdosedit editor (1:1 UNIX console)
Kernel now has the classic UNIX console split, so full-screen ANSI apps can run in ring 0: - vt.rs: terminal emulator owning the text screen — cell grid (char + fg/bg), logical cursor (inverse-video), scrolling, and a small ANSI CSI parser (cursor position H/f, erase J/K, moves A-D, SGR 16-colour with correct ANSI→VGA mapping, cursor show/hide ?25). Blits only changed cells; scroll shifts cell grids + pixels and re-rasterises only the exposed bottom row. gop_println!/clear_screen now render through the vt; gop.rs is the raw rasteriser (colored glyph blits, pixel shift). - tty.rs: line discipline — canonical read_line (echo, line buffer, ^C cancel, ^D EOF) moved out of the shell, and raw read_byte that translates KeyEvents to the terminal byte protocol (arrows → ESC[A-D, Backspace → 0x7f, ^X → 0x18, Ctrl+arrows → ESC[1;5A-D, Home/End/PgUp/ PgDn/Delete). interrupt_pressed() stays for streaming commands. - ps2: generic Ctrl(char) + CtrlArrow events (was: only CtrlC/CtrlD). - rsdosedit: no_std port of the RNG rsedit editor as the first real app — undo, syntax highlighting (verified green/gray/yellow on the framebuffer), search, word nav, smart indent, ^S save / ^X exit / ^X again to discard. termios → tty raw, stdout → vt, std::fs → VFS (ramfs), SIGWINCH → fixed geometry, force-exit window → key counter. - shell: 'edit <path>' command; read_line/strbuf moved to rsnix. - CONTEXT.md: layout + milestones + known-issue perf notes. Verified in QEMU: full edit → type → arrow-move → insert → save → exit round trip with byte-exact file content; ANSI colors via screendump.
Diffstat (limited to '')
-rw-r--r--shell/src/lib.rs180
1 files changed, 28 insertions, 152 deletions
diff --git a/shell/src/lib.rs b/shell/src/lib.rs
index 299956d..ec2474d 100644
--- a/shell/src/lib.rs
+++ b/shell/src/lib.rs
@@ -5,12 +5,9 @@ extern crate alloc;
5use alloc::string::ToString; 5use alloc::string::ToString;
6 6
7/// Minimal command shell — reads from PS/2 keyboard, writes to GOP + serial. 7/// Minimal command shell — reads from PS/2 keyboard, writes to GOP + serial.
8use rsnix::ps2::{self, KeyEvent}; 8use rsnix::tty::{self, LineInput};
9use rsnix::{gop_print, gop_println, serial_print, serial_println}; 9use rsnix::{gop_print, gop_println, serial_print, serial_println};
10 10
11/// Maximum input line length.
12const LINE_BUF_SIZE: usize = 256;
13
14/// Run the shell forever. 11/// Run the shell forever.
15pub fn run() -> ! { 12pub fn run() -> ! {
16 gop_println!( 13 gop_println!(
@@ -27,8 +24,8 @@ pub fn run() -> ! {
27 gop_print!("> "); 24 gop_print!("> ");
28 serial_print!("> "); 25 serial_print!("> ");
29 26
30 // Read one line from keyboard 27 // Read one line from keyboard (tty canonical mode)
31 match read_line() { 28 match tty::read_line() {
32 LineInput::Interrupted => continue, // ^C — fresh prompt 29 LineInput::Interrupted => continue, // ^C — fresh prompt
33 LineInput::Eof => { 30 LineInput::Eof => {
34 // ^D on an empty line: EOF, exit the shell. 31 // ^D on an empty line: EOF, exit the shell.
@@ -65,6 +62,7 @@ pub fn run() -> ! {
65 "mkdir" => cmd_mkdir(args), 62 "mkdir" => cmd_mkdir(args),
66 "touch" => cmd_touch(args), 63 "touch" => cmd_touch(args),
67 "rm" => cmd_rm(args), 64 "rm" => cmd_rm(args),
65 "edit" => cmd_edit(args),
68 _ => { 66 _ => {
69 gop_println!("unknown command: {}", cmd); 67 gop_println!("unknown command: {}", cmd);
70 serial_println!("unknown command: {}", cmd); 68 serial_println!("unknown command: {}", cmd);
@@ -75,74 +73,9 @@ pub fn run() -> ! {
75 } 73 }
76} 74}
77 75
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
85// --------------------------------------------------------------------------- 76// ---------------------------------------------------------------------------
86// Line input (PS/2 keyboard, no heap) 77// Line input is handled by the tty line discipline (rsnix::tty).
87// --------------------------------------------------------------------------- 78// ---------------------------------------------------------------------------
88fn read_line() -> LineInput {
89 let mut buf = heapless_str::String::<LINE_BUF_SIZE>::new();
90 loop {
91 match ps2::poll_key() {
92 KeyEvent::Enter => {
93 rsnix::gop::put_char('\n');
94 rsnix::serial::write_str("\r\n");
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).
116 }
117 KeyEvent::Char(c) if c.is_ascii_graphic() || c == ' ' => {
118 if buf.push(c).is_ok() {
119 rsnix::gop::put_char(c);
120 // Echo to serial too
121 let mut s = [0u8; 4];
122 let s = c.encode_utf8(&mut s);
123 rsnix::serial::write_str(s);
124 }
125 }
126 KeyEvent::Backspace => {
127 if buf.pop().is_some() {
128 // GOP backspace: move cursor back and erase
129 rsnix::gop::put_char('\x08');
130 // Serial backspace: send BS SP BS (standard terminal protocol)
131 rsnix::serial::write_str("\x08 \x08");
132 }
133 }
134 KeyEvent::Tab => {
135 for _ in 0..4 {
136 if buf.push(' ').is_ok() {
137 rsnix::gop::put_char(' ');
138 rsnix::serial::write_str(" ");
139 }
140 }
141 }
142 _ => {}
143 }
144 }
145}
146 79
147/// Split a command line into the first word and the rest. 80/// Split a command line into the first word and the rest.
148fn split_first_word(s: &str) -> (&str, &str) { 81fn split_first_word(s: &str) -> (&str, &str) {
@@ -176,13 +109,14 @@ fn cmd_help() {
176 gop_println!(" mkdir — create a directory"); 109 gop_println!(" mkdir — create a directory");
177 gop_println!(" touch — create an empty file"); 110 gop_println!(" touch — create an empty file");
178 gop_println!(" rm — remove a file or empty dir"); 111 gop_println!(" rm — remove a file or empty dir");
112 gop_println!(" edit — full-screen editor (^S save, ^X exit, ^W search)");
179 gop_println!(" echo > /dev/null, > file, >> file"); 113 gop_println!(" echo > /dev/null, > file, >> file");
180 gop_println!(" halt — halt the system (hlt loop)"); 114 gop_println!(" halt — halt the system (hlt loop)");
181 gop_println!(" poweroff — shut down via ACPI (ResetSystem)"); 115 gop_println!(" poweroff — shut down via ACPI (ResetSystem)");
182 gop_println!(" exit — alias for poweroff"); 116 gop_println!(" exit — alias for poweroff");
183 serial_println!("available commands:"); 117 serial_println!("available commands:");
184 serial_println!( 118 serial_println!(
185 " help, echo, clear, hello, info, mem, mmap, stress, date, pci, ls, cat, hexdump, mkdir, touch, rm, halt, poweroff, exit" 119 " help, echo, clear, hello, info, mem, mmap, stress, date, pci, ls, cat, hexdump, mkdir, touch, rm, edit, halt, poweroff, exit"
186 ); 120 );
187} 121}
188 122
@@ -267,7 +201,7 @@ fn cmd_cat(args: &str) {
267 Some(h) => { 201 Some(h) => {
268 let mut pos = 0u64; 202 let mut pos = 0u64;
269 loop { 203 loop {
270 if interrupt_pressed() { 204 if rsnix::tty::interrupt_pressed() {
271 gop_println!("^C"); 205 gop_println!("^C");
272 serial_println!("^C"); 206 serial_println!("^C");
273 return; 207 return;
@@ -305,7 +239,7 @@ fn cmd_hexdump(args: &str) {
305 let mut buf = [0u8; 512]; 239 let mut buf = [0u8; 512];
306 let mut pos = 0u64; 240 let mut pos = 0u64;
307 loop { 241 loop {
308 if interrupt_pressed() { 242 if rsnix::tty::interrupt_pressed() {
309 gop_println!("^C"); 243 gop_println!("^C");
310 serial_println!("^C"); 244 serial_println!("^C");
311 return; 245 return;
@@ -345,16 +279,26 @@ fn cmd_hexdump(args: &str) {
345 } 279 }
346} 280}
347 281
348/// Returns true if the user pressed ^C since the last check; drains the 282
349/// keyboard buffer of any other buffered keys. 283// ---------------------------------------------------------------------------
350fn interrupt_pressed() -> bool { 284// edit — run the built-in editor (rsdosedit) on a file
351 let mut pressed = false; 285// ---------------------------------------------------------------------------
352 while let Some(ev) = ps2::try_poll_key() { 286
353 if matches!(ev, KeyEvent::CtrlC) { 287fn cmd_edit(args: &str) {
354 pressed = true; 288 let path = args.trim();
355 } 289 if path.is_empty() {
290 gop_println!("usage: edit <path>");
291 serial_println!("usage: edit <path>");
292 return;
293 }
294 if rsnix::fs::ensure_file(path).is_err() {
295 gop_println!("edit: cannot create {}", path);
296 serial_println!("edit: cannot create {}", path);
297 return;
356 } 298 }
357 pressed 299 rsnix::gop::clear_screen();
300 rsdosedit::run(path);
301 rsnix::gop::clear_screen();
358} 302}
359 303
360fn cmd_mkdir(args: &str) { 304fn cmd_mkdir(args: &str) {
@@ -682,71 +626,3 @@ fn cmd_poweroff() -> ! {
682 serial_println!("shutting down..."); 626 serial_println!("shutting down...");
683 rsnix::power::poweroff() 627 rsnix::power::poweroff()
684} 628}
685
686// ---------------------------------------------------------------------------
687// Minimal fixed-capacity string for no-stack use (avoids alloc dependency)
688// ---------------------------------------------------------------------------
689mod heapless_str {
690 use core::fmt;
691
692 /// A fixed-capacity string allocated entirely on the stack.
693 pub struct String<const N: usize> {
694 buf: [u8; N],
695 len: usize,
696 }
697
698 impl<const N: usize> String<N> {
699 pub const fn new() -> Self {
700 String {
701 buf: [0u8; N],
702 len: 0,
703 }
704 }
705
706 pub fn as_str(&self) -> &str {
707 // SAFETY: we only write valid ASCII bytes.
708 unsafe { core::str::from_utf8_unchecked(&self.buf[..self.len]) }
709 }
710
711 pub fn push(&mut self, c: char) -> Result<(), ()> {
712 if self.len >= N {
713 return Err(());
714 }
715 self.buf[self.len] = c as u8;
716 self.len += 1;
717 Ok(())
718 }
719
720 pub fn pop(&mut self) -> Option<char> {
721 if self.len == 0 {
722 return None;
723 }
724 self.len -= 1;
725 Some(self.buf[self.len] as char)
726 }
727
728 pub fn is_empty(&self) -> bool {
729 self.len == 0
730 }
731
732 pub fn clear(&mut self) {
733 self.len = 0;
734 }
735
736 pub fn trim(&self) -> &str {
737 self.as_str().trim()
738 }
739 }
740
741 impl<const N: usize> fmt::Display for String<N> {
742 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
743 write!(f, "{}", self.as_str())
744 }
745 }
746
747 impl<const N: usize> fmt::Debug for String<N> {
748 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
749 write!(f, "{:?}", self.as_str())
750 }
751 }
752}