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. --- rsnix/src/tty.rs | 239 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 239 insertions(+) create mode 100644 rsnix/src/tty.rs (limited to 'rsnix/src/tty.rs') diff --git a/rsnix/src/tty.rs b/rsnix/src/tty.rs new file mode 100644 index 0000000..8d745b3 --- /dev/null +++ b/rsnix/src/tty.rs @@ -0,0 +1,239 @@ +//! Line discipline over the PS/2 keyboard — the kernel's tty layer. +//! +//! Two modes, like a real UNIX terminal: +//! - **canonical** (`read_line`): input is echoed, buffered until Enter, +//! ^C cancels the line, ^D on an empty line is EOF. +//! - **raw** (`read_byte` / `try_read_byte`): keys are translated to the +//! classic terminal byte protocol (arrows → `ESC [ A–D`, Backspace → +//! `0x7f`, ^X → `0x18`, …) and delivered one byte at a time, with no echo. +//! Multi-byte sequences are queued internally so `read_byte` returns them +//! in order. +//! +//! `interrupt_pressed` is the cooperative-interrupt check used by streaming +//! commands (cat/hexdump): it drains the keyboard non-blockingly and reports +//! whether ^C was seen. + +use crate::ps2::{self, KeyEvent}; +use crate::strbuf; +use crate::{gop, serial}; + +pub const LINE_BUF_SIZE: usize = 256; + +/// Result of one canonical line-input session. +pub enum LineInput { + Line(strbuf::String), + Interrupted, // ^C + Eof, // ^D on an empty line +} + +// Raw-mode byte queue: holds the tail of a multi-byte key sequence. +static mut RAW_QUEUE: [u8; 8] = [0; 8]; +static mut RAW_LEN: usize = 0; + +// --------------------------------------------------------------------------- +// canonical mode +// --------------------------------------------------------------------------- + +/// Read one line in canonical mode: echo every key, ^C cancels the line, +/// ^D on an empty line is EOF. Echo goes to the terminal (GOP) and serial. +pub fn read_line() -> LineInput { + let mut buf = strbuf::String::::new(); + loop { + match ps2::poll_key() { + KeyEvent::Enter => { + gop::put_char('\n'); + serial::write_str("\r\n"); + return LineInput::Line(buf); + } + KeyEvent::Ctrl('c') => { + // Cancel the current line — like a real terminal, echo ^C. + buf.clear(); + gop::put_char('^'); + gop::put_char('C'); + gop::put_char('\n'); + serial::write_str("^C\r\n"); + return LineInput::Interrupted; + } + KeyEvent::Ctrl('d') => { + if buf.is_empty() { + // EOF on an empty line — the caller decides what to do. + gop::put_char('^'); + gop::put_char('D'); + gop::put_char('\n'); + 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() { + gop::put_char(c); + let mut s = [0u8; 4]; + let s = c.encode_utf8(&mut s); + serial::write_str(s); + } + } + KeyEvent::Backspace => { + if buf.pop().is_some() { + gop::put_char('\x08'); + // Serial backspace: BS SP BS (standard terminal protocol) + serial::write_str("\x08 \x08"); + } + } + KeyEvent::Tab => { + for _ in 0..4 { + if buf.push(' ').is_ok() { + gop::put_char(' '); + serial::write_str(" "); + } + } + } + _ => {} + } + } +} + +/// Non-blocking drain of the keyboard: true if a ^C is pending. Used by +/// streaming commands (cat/hexdump) so a long dump can be interrupted. +pub fn interrupt_pressed() -> bool { + let mut pressed = false; + while let Some(ev) = ps2::try_poll_key() { + if matches!(ev, KeyEvent::Ctrl('c')) { + pressed = true; + } + } + pressed +} + +// --------------------------------------------------------------------------- +// raw mode +// --------------------------------------------------------------------------- + +/// Blocking read of the next raw-mode byte. Keys arrive translated to the +/// terminal byte protocol; multi-byte sequences are delivered in order. +pub fn read_byte() -> u8 { + loop { + if let Some(b) = pop_raw() { + return b; + } + let ev = ps2::poll_key(); + enqueue_key(ev); + } +} + +/// Non-blocking raw read: Some(byte) if one is available, else None. +pub fn try_read_byte() -> Option { + if let Some(b) = pop_raw() { + return Some(b); + } + match ps2::try_poll_key() { + Some(ev) => { + enqueue_key(ev); + pop_raw() + } + None => None, + } +} + +fn pop_raw() -> Option { + unsafe { + if RAW_LEN == 0 { + return None; + } + let b = RAW_QUEUE[0]; + for i in 1..RAW_LEN { + RAW_QUEUE[i - 1] = RAW_QUEUE[i]; + } + RAW_LEN -= 1; + Some(b) + } +} + +fn enqueue_key(ev: KeyEvent) { + let mut q = [0u8; 8]; + let n: usize = match ev { + KeyEvent::Char(c) if c.is_ascii() => { + q[0] = c as u8; + 1 + } + KeyEvent::Enter => { + q[0] = 0x0d; + 1 + } + KeyEvent::Backspace => { + q[0] = 0x7f; + 1 + } + KeyEvent::Tab => { + q[0] = 0x09; + 1 + } + KeyEvent::Escape => { + q[0] = 0x1b; + 1 + } + KeyEvent::Ctrl(c) => { + q[0] = (c as u8) & 0x1f; + 1 + } + KeyEvent::ArrowUp => { + q[..3].copy_from_slice(b"\x1b[A"); + 3 + } + KeyEvent::ArrowDown => { + q[..3].copy_from_slice(b"\x1b[B"); + 3 + } + KeyEvent::ArrowRight => { + q[..3].copy_from_slice(b"\x1b[C"); + 3 + } + KeyEvent::ArrowLeft => { + q[..3].copy_from_slice(b"\x1b[D"); + 3 + } + KeyEvent::CtrlArrow(0) => { + q[..6].copy_from_slice(b"\x1b[1;5A"); + 6 + } + KeyEvent::CtrlArrow(1) => { + q[..6].copy_from_slice(b"\x1b[1;5B"); + 6 + } + KeyEvent::CtrlArrow(2) => { + q[..6].copy_from_slice(b"\x1b[1;5D"); + 6 + } + KeyEvent::CtrlArrow(3) => { + q[..6].copy_from_slice(b"\x1b[1;5C"); + 6 + } + KeyEvent::Home => { + q[..3].copy_from_slice(b"\x1b[H"); + 3 + } + KeyEvent::End => { + q[..3].copy_from_slice(b"\x1b[F"); + 3 + } + KeyEvent::PageUp => { + q[..4].copy_from_slice(b"\x1b[5~"); + 4 + } + KeyEvent::PageDown => { + q[..4].copy_from_slice(b"\x1b[6~"); + 4 + } + KeyEvent::Delete => { + q[..4].copy_from_slice(b"\x1b[3~"); + 4 + } + _ => return, + }; + unsafe { + for (i, b) in q.iter().take(n).enumerate() { + RAW_QUEUE[i] = *b; + } + RAW_LEN = n; + } +} -- cgit v1.2.3