//! 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}; /// Max characters a single typed line can hold. Long enough that no /// realistic command line gets silently cut — the terminal wraps the /// echo at the screen edge, and the full line is what's submitted. pub const LINE_BUF_SIZE: usize = 1024; /// Result of one canonical line-input session. #[allow(clippy::large_enum_variant)] // stack buffer by design (no heap churn) 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 { // Interactive input: make sure deferred scrolls are pushed first so // the screen shows the current state (no-op when nothing is pending). crate::vt::sync_scroll(); 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 { // Interactive (raw) input: push deferred scrolls so the full-screen // app draws over a current framebuffer. crate::vt::sync_scroll(); 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; } }