From fa06681a8418b7e31af96e010bb424e178402f4d Mon Sep 17 00:00:00 2001 From: salmonik Date: Fri, 31 Jul 2026 17:19:09 +0200 Subject: tty + vt terminal split, rsdosedit editor (1:1 UNIX console) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 ' 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. --- shell/src/lib.rs | 180 +++++++++---------------------------------------------- 1 file changed, 28 insertions(+), 152 deletions(-) (limited to 'shell/src/lib.rs') 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; use alloc::string::ToString; /// Minimal command shell — reads from PS/2 keyboard, writes to GOP + serial. -use rsnix::ps2::{self, KeyEvent}; +use rsnix::tty::{self, LineInput}; use rsnix::{gop_print, gop_println, serial_print, serial_println}; -/// Maximum input line length. -const LINE_BUF_SIZE: usize = 256; - /// Run the shell forever. pub fn run() -> ! { gop_println!( @@ -27,8 +24,8 @@ pub fn run() -> ! { gop_print!("> "); serial_print!("> "); - // Read one line from keyboard - match read_line() { + // Read one line from keyboard (tty canonical mode) + match tty::read_line() { LineInput::Interrupted => continue, // ^C — fresh prompt LineInput::Eof => { // ^D on an empty line: EOF, exit the shell. @@ -65,6 +62,7 @@ pub fn run() -> ! { "mkdir" => cmd_mkdir(args), "touch" => cmd_touch(args), "rm" => cmd_rm(args), + "edit" => cmd_edit(args), _ => { gop_println!("unknown command: {}", cmd); serial_println!("unknown command: {}", cmd); @@ -75,74 +73,9 @@ pub fn run() -> ! { } } -/// 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) +// Line input is handled by the tty line discipline (rsnix::tty). // --------------------------------------------------------------------------- -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 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() { - rsnix::gop::put_char(c); - // Echo to serial too - let mut s = [0u8; 4]; - let s = c.encode_utf8(&mut s); - rsnix::serial::write_str(s); - } - } - KeyEvent::Backspace => { - if buf.pop().is_some() { - // GOP backspace: move cursor back and erase - rsnix::gop::put_char('\x08'); - // Serial backspace: send BS SP BS (standard terminal protocol) - rsnix::serial::write_str("\x08 \x08"); - } - } - KeyEvent::Tab => { - for _ in 0..4 { - if buf.push(' ').is_ok() { - rsnix::gop::put_char(' '); - rsnix::serial::write_str(" "); - } - } - } - _ => {} - } - } -} /// Split a command line into the first word and the rest. fn split_first_word(s: &str) -> (&str, &str) { @@ -176,13 +109,14 @@ fn cmd_help() { gop_println!(" mkdir — create a directory"); gop_println!(" touch — create an empty file"); gop_println!(" rm — remove a file or empty dir"); + gop_println!(" edit — full-screen editor (^S save, ^X exit, ^W search)"); gop_println!(" echo > /dev/null, > file, >> file"); gop_println!(" halt — halt the system (hlt loop)"); gop_println!(" poweroff — shut down via ACPI (ResetSystem)"); gop_println!(" exit — alias for poweroff"); serial_println!("available commands:"); serial_println!( - " help, echo, clear, hello, info, mem, mmap, stress, date, pci, ls, cat, hexdump, mkdir, touch, rm, halt, poweroff, exit" + " help, echo, clear, hello, info, mem, mmap, stress, date, pci, ls, cat, hexdump, mkdir, touch, rm, edit, halt, poweroff, exit" ); } @@ -267,7 +201,7 @@ fn cmd_cat(args: &str) { Some(h) => { let mut pos = 0u64; loop { - if interrupt_pressed() { + if rsnix::tty::interrupt_pressed() { gop_println!("^C"); serial_println!("^C"); return; @@ -305,7 +239,7 @@ fn cmd_hexdump(args: &str) { let mut buf = [0u8; 512]; let mut pos = 0u64; loop { - if interrupt_pressed() { + if rsnix::tty::interrupt_pressed() { gop_println!("^C"); serial_println!("^C"); return; @@ -345,16 +279,26 @@ fn cmd_hexdump(args: &str) { } } -/// 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; - } + +// --------------------------------------------------------------------------- +// edit — run the built-in editor (rsdosedit) on a file +// --------------------------------------------------------------------------- + +fn cmd_edit(args: &str) { + let path = args.trim(); + if path.is_empty() { + gop_println!("usage: edit "); + serial_println!("usage: edit "); + return; + } + if rsnix::fs::ensure_file(path).is_err() { + gop_println!("edit: cannot create {}", path); + serial_println!("edit: cannot create {}", path); + return; } - pressed + rsnix::gop::clear_screen(); + rsdosedit::run(path); + rsnix::gop::clear_screen(); } fn cmd_mkdir(args: &str) { @@ -682,71 +626,3 @@ fn cmd_poweroff() -> ! { serial_println!("shutting down..."); rsnix::power::poweroff() } - -// --------------------------------------------------------------------------- -// Minimal fixed-capacity string for no-stack use (avoids alloc dependency) -// --------------------------------------------------------------------------- -mod heapless_str { - use core::fmt; - - /// A fixed-capacity string allocated entirely on the stack. - pub struct String { - buf: [u8; N], - len: usize, - } - - impl String { - pub const fn new() -> Self { - String { - buf: [0u8; N], - len: 0, - } - } - - pub fn as_str(&self) -> &str { - // SAFETY: we only write valid ASCII bytes. - unsafe { core::str::from_utf8_unchecked(&self.buf[..self.len]) } - } - - pub fn push(&mut self, c: char) -> Result<(), ()> { - if self.len >= N { - return Err(()); - } - self.buf[self.len] = c as u8; - self.len += 1; - Ok(()) - } - - pub fn pop(&mut self) -> Option { - if self.len == 0 { - return None; - } - self.len -= 1; - Some(self.buf[self.len] as char) - } - - pub fn is_empty(&self) -> bool { - self.len == 0 - } - - pub fn clear(&mut self) { - self.len = 0; - } - - pub fn trim(&self) -> &str { - self.as_str().trim() - } - } - - impl fmt::Display for String { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.as_str()) - } - } - - impl fmt::Debug for String { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:?}", self.as_str()) - } - } -} -- cgit v1.2.3