diff options
| author | salmonik <salmonik@tuta.io> | 2026-08-21 20:13:59 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-08-21 20:13:59 +0200 |
| commit | 2f4cf666c0c7bd224932fa48973e7334360c1de3 (patch) | |
| tree | 7526314b18728b05c4e9d285b9a41cf6fcd6d6d6 /rsnix/src/tty.rs | |
| parent | image: single dd-able system.img replaces vvfat+data-disk pair (diff) | |
| download | rs-dos-2f4cf666c0c7bd224932fa48973e7334360c1de3.tar.gz rs-dos-2f4cf666c0c7bd224932fa48973e7334360c1de3.zip | |
input: total keyboard routing + per-terminal discipline (ADR 0006 ph1)
wm: FOCUSED is now a plain window id — window 0 (home terminal) is an
ordinary focusable window in the z-order. The CONSOLE sentinel
(usize::MAX) and the focused()==0 special cases are gone; focus_console()
is an alias for focus(0). Desktop clicks focus window 0. The WM input
pump routes every key unconditionally: IRQ ring -> pump -> focused
window's owner; nothing polls the scancode rings directly any more.
tty: read_line() reads from the calling task's message queue (the old
global-ring read_line and the duplicate read_line_from_queue are merged);
raw mode (read_byte/try_read_byte) consumes the same queue;
interrupt_pressed drains the queue instead of the ring.
shell: task 0 owns home terminal window 0 and blocks on its queue like a
windowed shell; cat/hexdump stdin paths pass the home terminal handle.
sched: watchdog comments updated (shell parks on recv_blocking).
Verified in QEMU: F1 spawns terminals, F2 cycles focus home->term1->
term2, keystrokes land only in the focused window, ^C cancels a line,
edit runs raw over the queue and exits to a working prompt; test-disk
regression PASS.
Diffstat (limited to '')
| -rw-r--r-- | rsnix/src/tty.rs | 149 |
1 files changed, 27 insertions, 122 deletions
diff --git a/rsnix/src/tty.rs b/rsnix/src/tty.rs index c44dca8..1358fa9 100644 --- a/rsnix/src/tty.rs +++ b/rsnix/src/tty.rs | |||
| @@ -1,8 +1,9 @@ | |||
| 1 | //! Line discipline over the keyboard — the kernel's tty layer. | 1 | //! Line discipline over the keyboard — the kernel's tty layer. |
| 2 | //! | 2 | //! |
| 3 | //! Keys come from either the PS/2 controller (IRQ-fed scancode ring) or the | 3 | //! Every key is routed by the WM input pump to the focused window's |
| 4 | //! USB HID boot keyboard (polled UHCI driver). Both are translated to the | 4 | //! owning task as `TaskMsg::Key`; the tty layer consumes only that task's |
| 5 | //! same `KeyEvent` space, so the shell is input-source agnostic. | 5 | //! message queue (ADR 0006 Phase 1: total routing, per-terminal |
| 6 | //! discipline). No code path reads the PS/2/USB scancode rings directly. | ||
| 6 | //! | 7 | //! |
| 7 | //! Two modes, like a real UNIX terminal: | 8 | //! Two modes, like a real UNIX terminal: |
| 8 | //! - **canonical** (`read_line`): input is echoed, buffered until Enter, | 9 | //! - **canonical** (`read_line`): input is echoed, buffered until Enter, |
| @@ -14,10 +15,10 @@ | |||
| 14 | //! in order. | 15 | //! in order. |
| 15 | //! | 16 | //! |
| 16 | //! `interrupt_pressed` is the cooperative-interrupt check used by streaming | 17 | //! `interrupt_pressed` is the cooperative-interrupt check used by streaming |
| 17 | //! commands (cat/hexdump): it drains the keyboard non-blockingly and reports | 18 | //! commands (cat/hexdump): it drains the queue non-blockingly and reports |
| 18 | //! whether ^C was seen. | 19 | //! whether ^C was seen. |
| 19 | 20 | ||
| 20 | use crate::ps2::{self, KeyEvent}; | 21 | use crate::ps2::KeyEvent; |
| 21 | use crate::strbuf; | 22 | use crate::strbuf; |
| 22 | use crate::{gop, serial}; | 23 | use crate::{gop, serial}; |
| 23 | 24 | ||
| @@ -38,115 +39,14 @@ pub enum LineInput { | |||
| 38 | static mut RAW_QUEUE: [u8; 8] = [0; 8]; | 39 | static mut RAW_QUEUE: [u8; 8] = [0; 8]; |
| 39 | static mut RAW_LEN: usize = 0; | 40 | static mut RAW_LEN: usize = 0; |
| 40 | 41 | ||
| 41 | /// Combined blocking key poll: USB HID keyboard first (polled UHCI), then | 42 | /// Read one line in canonical mode from the task's message queue. Keys |
| 42 | /// the PS/2 path (which parks via `hlt` until an IRQ-fed scancode lands). | 43 | /// arrive as `TaskMsg::Key`, pumped by the WM to the focused window's |
| 43 | fn poll_key() -> KeyEvent { | 44 | /// owner (this task, if its window is focused). Returns `None` on |
| 44 | loop { | 45 | /// `TaskMsg::Close` (window was closed). |
| 45 | // A GUI window has focus: the keyboard pump owns the keys. Park so | ||
| 46 | // the shell/editor don't steal input from the focused app. | ||
| 47 | if crate::wm::focused().is_some() { | ||
| 48 | crate::sched::sleep_ms(5); | ||
| 49 | continue; | ||
| 50 | } | ||
| 51 | if let Some(ev) = crate::uhci::try_poll_key() { | ||
| 52 | return ev; | ||
| 53 | } | ||
| 54 | if let Some(ev) = ps2::try_poll_key() { | ||
| 55 | return ev; | ||
| 56 | } | ||
| 57 | // No key ready: park briefly, then re-check focus (so a focus change | ||
| 58 | // to a GUI window is noticed instead of blocking on a stolen key). | ||
| 59 | crate::sched::sleep_ms(5); | ||
| 60 | } | ||
| 61 | } | ||
| 62 | |||
| 63 | /// Combined non-blocking key poll (USB first, then PS/2). Returns `None` | ||
| 64 | /// while a GUI window is focused (the keyboard pump owns the keys). | ||
| 65 | fn try_poll_key() -> Option<KeyEvent> { | ||
| 66 | if crate::wm::focused().is_some() { | ||
| 67 | return None; | ||
| 68 | } | ||
| 69 | crate::uhci::try_poll_key().or_else(ps2::try_poll_key) | ||
| 70 | } | ||
| 71 | |||
| 72 | // --------------------------------------------------------------------------- | ||
| 73 | // canonical mode | ||
| 74 | // --------------------------------------------------------------------------- | ||
| 75 | |||
| 76 | /// Read one line in canonical mode: echo every key, ^C cancels the line, | ||
| 77 | /// ^D on an empty line is EOF. Echo goes to the terminal (GOP) and serial. | ||
| 78 | pub fn read_line() -> LineInput { | ||
| 79 | // Interactive input: make sure deferred scrolls are pushed first so | ||
| 80 | // the screen shows the current state (no-op when nothing is pending). | ||
| 81 | crate::vt::sync_scroll(); | ||
| 82 | let mut buf = strbuf::String::<LINE_BUF_SIZE>::new(); | ||
| 83 | loop { | ||
| 84 | match poll_key() { | ||
| 85 | KeyEvent::Enter => { | ||
| 86 | gop::put_char('\n'); | ||
| 87 | serial::write_str("\r\n"); | ||
| 88 | return LineInput::Line(buf); | ||
| 89 | } | ||
| 90 | KeyEvent::Ctrl('c') => { | ||
| 91 | // Cancel the current line — like a real terminal, echo ^C. | ||
| 92 | buf.clear(); | ||
| 93 | gop::put_char('^'); | ||
| 94 | gop::put_char('C'); | ||
| 95 | gop::put_char('\n'); | ||
| 96 | serial::write_str("^C\r\n"); | ||
| 97 | return LineInput::Interrupted; | ||
| 98 | } | ||
| 99 | KeyEvent::Ctrl('d') => { | ||
| 100 | if buf.is_empty() { | ||
| 101 | // EOF on an empty line — the caller decides what to do. | ||
| 102 | gop::put_char('^'); | ||
| 103 | gop::put_char('D'); | ||
| 104 | gop::put_char('\n'); | ||
| 105 | serial::write_str("^D\r\n"); | ||
| 106 | return LineInput::Eof; | ||
| 107 | } | ||
| 108 | // ^D with text on the line: ignored (bash semantics). | ||
| 109 | } | ||
| 110 | KeyEvent::Char(c) if c.is_ascii_graphic() || c == ' ' => { | ||
| 111 | if buf.push(c).is_ok() { | ||
| 112 | gop::put_char(c); | ||
| 113 | let mut s = [0u8; 4]; | ||
| 114 | let s = c.encode_utf8(&mut s); | ||
| 115 | serial::write_str(s); | ||
| 116 | } | ||
| 117 | } | ||
| 118 | KeyEvent::Backspace => { | ||
| 119 | if buf.pop().is_some() { | ||
| 120 | gop::put_char('\x08'); | ||
| 121 | // Serial backspace: BS SP BS (standard terminal protocol) | ||
| 122 | serial::write_str("\x08 \x08"); | ||
| 123 | } | ||
| 124 | } | ||
| 125 | KeyEvent::Tab => { | ||
| 126 | for _ in 0..4 { | ||
| 127 | if buf.push(' ').is_ok() { | ||
| 128 | gop::put_char(' '); | ||
| 129 | serial::write_str(" "); | ||
| 130 | } | ||
| 131 | } | ||
| 132 | } | ||
| 133 | _ => {} | ||
| 134 | } | ||
| 135 | } | ||
| 136 | } | ||
| 137 | |||
| 138 | /// Read one line in canonical mode from the task's message queue (for | ||
| 139 | /// windowed shells). Same semantics as `read_line` but keys arrive as | ||
| 140 | /// `TaskMsg::Key` instead of from the global PS/2 queue. Returns `None` | ||
| 141 | /// on `TaskMsg::Close` (window was closed). | ||
| 142 | /// Read one line in canonical mode from the task's message queue (for | ||
| 143 | /// windowed shells). Same semantics as `read_line` but keys arrive as | ||
| 144 | /// `TaskMsg::Key` instead of from the global PS/2 queue. Returns `None` | ||
| 145 | /// on `TaskMsg::Close` (window was closed). | ||
| 146 | /// | 46 | /// |
| 147 | /// Handles `TaskMsg::Focus` to toggle the global `ACTIVE` redirect so | 47 | /// Handles `TaskMsg::Focus` to toggle the global `ACTIVE` redirect so |
| 148 | /// gop_print! goes to the right terminal buffer. | 48 | /// gop_print! goes to the right terminal buffer. |
| 149 | pub fn read_line_from_queue(term: &mut crate::vt::Terminal) -> Option<LineInput> { | 49 | pub fn read_line(term: &mut crate::vt::Terminal) -> Option<LineInput> { |
| 150 | crate::vt::sync_scroll(); | 50 | crate::vt::sync_scroll(); |
| 151 | let mut buf = strbuf::String::<LINE_BUF_SIZE>::new(); | 51 | let mut buf = strbuf::String::<LINE_BUF_SIZE>::new(); |
| 152 | loop { | 52 | loop { |
| @@ -211,12 +111,14 @@ pub fn read_line_from_queue(term: &mut crate::vt::Terminal) -> Option<LineInput> | |||
| 211 | } | 111 | } |
| 212 | } | 112 | } |
| 213 | 113 | ||
| 214 | /// Non-blocking drain of the keyboard: true if a ^C is pending. Used by | 114 | /// Non-blocking drain of this task's queue: true if a ^C is pending. |
| 215 | /// streaming commands (cat/hexdump) so a long dump can be interrupted. | 115 | /// Used by streaming commands (cat/hexdump) so a long dump can be |
| 116 | /// interrupted. Everything else in the queue is dropped — during a | ||
| 117 | /// streaming command the terminal owns the input stream. | ||
| 216 | pub fn interrupt_pressed() -> bool { | 118 | pub fn interrupt_pressed() -> bool { |
| 217 | let mut pressed = false; | 119 | let mut pressed = false; |
| 218 | while let Some(ev) = try_poll_key() { | 120 | while let Some(m) = crate::sched::recv() { |
| 219 | if matches!(ev, KeyEvent::Ctrl('c')) { | 121 | if matches!(m, crate::sched::TaskMsg::Key(KeyEvent::Ctrl('c'))) { |
| 220 | pressed = true; | 122 | pressed = true; |
| 221 | } | 123 | } |
| 222 | } | 124 | } |
| @@ -227,8 +129,9 @@ pub fn interrupt_pressed() -> bool { | |||
| 227 | // raw mode | 129 | // raw mode |
| 228 | // --------------------------------------------------------------------------- | 130 | // --------------------------------------------------------------------------- |
| 229 | 131 | ||
| 230 | /// Blocking read of the next raw-mode byte. Keys arrive translated to the | 132 | /// Blocking read of the next raw-mode byte. Keys arrive from the task's |
| 231 | /// terminal byte protocol; multi-byte sequences are delivered in order. | 133 | /// message queue (WM-pumped), translated to the terminal byte protocol; |
| 134 | /// multi-byte sequences are delivered in order. | ||
| 232 | pub fn read_byte() -> u8 { | 135 | pub fn read_byte() -> u8 { |
| 233 | // Interactive (raw) input: push deferred scrolls so the full-screen | 136 | // Interactive (raw) input: push deferred scrolls so the full-screen |
| 234 | // app draws over a current framebuffer. | 137 | // app draws over a current framebuffer. |
| @@ -237,8 +140,10 @@ pub fn read_byte() -> u8 { | |||
| 237 | if let Some(b) = pop_raw() { | 140 | if let Some(b) = pop_raw() { |
| 238 | return b; | 141 | return b; |
| 239 | } | 142 | } |
| 240 | let ev = poll_key(); | 143 | match crate::sched::recv_blocking() { |
| 241 | enqueue_key(ev); | 144 | crate::sched::TaskMsg::Key(ev) => enqueue_key(ev), |
| 145 | _ => {} | ||
| 146 | } | ||
| 242 | } | 147 | } |
| 243 | } | 148 | } |
| 244 | 149 | ||
| @@ -247,12 +152,12 @@ pub fn try_read_byte() -> Option<u8> { | |||
| 247 | if let Some(b) = pop_raw() { | 152 | if let Some(b) = pop_raw() { |
| 248 | return Some(b); | 153 | return Some(b); |
| 249 | } | 154 | } |
| 250 | match try_poll_key() { | 155 | match crate::sched::recv() { |
| 251 | Some(ev) => { | 156 | Some(crate::sched::TaskMsg::Key(ev)) => { |
| 252 | enqueue_key(ev); | 157 | enqueue_key(ev); |
| 253 | pop_raw() | 158 | pop_raw() |
| 254 | } | 159 | } |
| 255 | None => None, | 160 | _ => None, |
| 256 | } | 161 | } |
| 257 | } | 162 | } |
| 258 | 163 | ||
