From 2f4cf666c0c7bd224932fa48973e7334360c1de3 Mon Sep 17 00:00:00 2001 From: salmonik Date: Fri, 21 Aug 2026 20:13:59 +0200 Subject: input: total keyboard routing + per-terminal discipline (ADR 0006 ph1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- rsnix/src/sched.rs | 13 +++-- rsnix/src/tty.rs | 149 +++++++++----------------------------------------- rsnix/src/wm.rs | 144 ++++++++++++++++++++++++++---------------------- scripts/test-focus.py | 56 +++++++++++++++++++ shell/src/lib.rs | 58 +++++++++++++------- 5 files changed, 208 insertions(+), 212 deletions(-) create mode 100755 scripts/test-focus.py diff --git a/rsnix/src/sched.rs b/rsnix/src/sched.rs index 39ecb65..0c4938e 100644 --- a/rsnix/src/sched.rs +++ b/rsnix/src/sched.rs @@ -201,7 +201,7 @@ const WATCHDOG_SPIN_TICKS: u32 = 1500; const WATCHDOG_BOOT_SPIN_TICKS: u32 = 750; /// True once the shell REPL is live (set by main.rs before shell::run()). -/// The shell legitimately parks in poll_key with a fixed frame when it is; +/// The shell legitimately blocks on its message queue once live; /// before that, task 0 running boot code must not sit in a fixed frame. static SHELL_RUNNING: AtomicBool = AtomicBool::new(false); @@ -264,16 +264,17 @@ pub fn on_tick(cur: usize) -> usize { TASKS[CURRENT].ticks += 1; // Spin detection: a task whose interrupted frame never moves // (identical RSP on every tick) is stuck in a hot loop — no yield, - // no sleep, no block. The shell (id 0) legitimately parks in - // poll_key with a fixed frame, and idle (id 1) hlt's; both exempt. + // no sleep, no block. The shell (id 0) legitimately blocks on + // recv_blocking with a fixed frame, and idle (id 1) hlt's; both + // exempt. let id = CURRENT; let w = &mut SPIN_WATCH[id]; if w.last_rsp == cur { w.same += 1; // Background tasks (id >= 2): spinning is never legitimate. - // Task 0 (shell): exempt once the REPL is live (poll_key parks - // with a fixed frame); during the boot phase a fixed frame - // means usb/ehci init is wedged on real hardware. + // Task 0 (shell): exempt once the REPL is live (recv_blocking + // parks with a fixed frame); during the boot phase a fixed + // frame means usb/ehci init is wedged on real hardware. let threshold = if id == SHELL_TID && !SHELL_RUNNING.load(Ordering::Relaxed) { WATCHDOG_BOOT_SPIN_TICKS } else if id >= 2 { 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 @@ //! Line discipline over the keyboard — the kernel's tty layer. //! -//! Keys come from either the PS/2 controller (IRQ-fed scancode ring) or the -//! USB HID boot keyboard (polled UHCI driver). Both are translated to the -//! same `KeyEvent` space, so the shell is input-source agnostic. +//! Every key is routed by the WM input pump to the focused window's +//! owning task as `TaskMsg::Key`; the tty layer consumes only that task's +//! message queue (ADR 0006 Phase 1: total routing, per-terminal +//! discipline). No code path reads the PS/2/USB scancode rings directly. //! //! Two modes, like a real UNIX terminal: //! - **canonical** (`read_line`): input is echoed, buffered until Enter, @@ -14,10 +15,10 @@ //! in order. //! //! `interrupt_pressed` is the cooperative-interrupt check used by streaming -//! commands (cat/hexdump): it drains the keyboard non-blockingly and reports +//! commands (cat/hexdump): it drains the queue non-blockingly and reports //! whether ^C was seen. -use crate::ps2::{self, KeyEvent}; +use crate::ps2::KeyEvent; use crate::strbuf; use crate::{gop, serial}; @@ -38,115 +39,14 @@ pub enum LineInput { static mut RAW_QUEUE: [u8; 8] = [0; 8]; static mut RAW_LEN: usize = 0; -/// Combined blocking key poll: USB HID keyboard first (polled UHCI), then -/// the PS/2 path (which parks via `hlt` until an IRQ-fed scancode lands). -fn poll_key() -> KeyEvent { - loop { - // A GUI window has focus: the keyboard pump owns the keys. Park so - // the shell/editor don't steal input from the focused app. - if crate::wm::focused().is_some() { - crate::sched::sleep_ms(5); - continue; - } - if let Some(ev) = crate::uhci::try_poll_key() { - return ev; - } - if let Some(ev) = ps2::try_poll_key() { - return ev; - } - // No key ready: park briefly, then re-check focus (so a focus change - // to a GUI window is noticed instead of blocking on a stolen key). - crate::sched::sleep_ms(5); - } -} - -/// Combined non-blocking key poll (USB first, then PS/2). Returns `None` -/// while a GUI window is focused (the keyboard pump owns the keys). -fn try_poll_key() -> Option { - if crate::wm::focused().is_some() { - return None; - } - crate::uhci::try_poll_key().or_else(ps2::try_poll_key) -} - -// --------------------------------------------------------------------------- -// 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 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(" "); - } - } - } - _ => {} - } - } -} - -/// Read one line in canonical mode from the task's message queue (for -/// windowed shells). Same semantics as `read_line` but keys arrive as -/// `TaskMsg::Key` instead of from the global PS/2 queue. Returns `None` -/// on `TaskMsg::Close` (window was closed). -/// Read one line in canonical mode from the task's message queue (for -/// windowed shells). Same semantics as `read_line` but keys arrive as -/// `TaskMsg::Key` instead of from the global PS/2 queue. Returns `None` -/// on `TaskMsg::Close` (window was closed). +/// Read one line in canonical mode from the task's message queue. Keys +/// arrive as `TaskMsg::Key`, pumped by the WM to the focused window's +/// owner (this task, if its window is focused). Returns `None` on +/// `TaskMsg::Close` (window was closed). /// /// Handles `TaskMsg::Focus` to toggle the global `ACTIVE` redirect so /// gop_print! goes to the right terminal buffer. -pub fn read_line_from_queue(term: &mut crate::vt::Terminal) -> Option { +pub fn read_line(term: &mut crate::vt::Terminal) -> Option { crate::vt::sync_scroll(); let mut buf = strbuf::String::::new(); loop { @@ -211,12 +111,14 @@ pub fn read_line_from_queue(term: &mut crate::vt::Terminal) -> Option } } -/// 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. +/// Non-blocking drain of this task's queue: true if a ^C is pending. +/// Used by streaming commands (cat/hexdump) so a long dump can be +/// interrupted. Everything else in the queue is dropped — during a +/// streaming command the terminal owns the input stream. pub fn interrupt_pressed() -> bool { let mut pressed = false; - while let Some(ev) = try_poll_key() { - if matches!(ev, KeyEvent::Ctrl('c')) { + while let Some(m) = crate::sched::recv() { + if matches!(m, crate::sched::TaskMsg::Key(KeyEvent::Ctrl('c'))) { pressed = true; } } @@ -227,8 +129,9 @@ pub fn interrupt_pressed() -> bool { // 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. +/// Blocking read of the next raw-mode byte. Keys arrive from the task's +/// message queue (WM-pumped), 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. @@ -237,8 +140,10 @@ pub fn read_byte() -> u8 { if let Some(b) = pop_raw() { return b; } - let ev = poll_key(); - enqueue_key(ev); + match crate::sched::recv_blocking() { + crate::sched::TaskMsg::Key(ev) => enqueue_key(ev), + _ => {} + } } } @@ -247,12 +152,12 @@ pub fn try_read_byte() -> Option { if let Some(b) = pop_raw() { return Some(b); } - match try_poll_key() { - Some(ev) => { + match crate::sched::recv() { + Some(crate::sched::TaskMsg::Key(ev)) => { enqueue_key(ev); pop_raw() } - None => None, + _ => None, } } diff --git a/rsnix/src/wm.rs b/rsnix/src/wm.rs index 39ad1be..9f3806f 100644 --- a/rsnix/src/wm.rs +++ b/rsnix/src/wm.rs @@ -16,8 +16,9 @@ use crate::sched::TaskMsg; /// Height of a window's title bar in pixels (one 8×16 glyph row). pub const TITLE_BAR_H: usize = 16; -/// Sentinel: `FOCUSED == CONSOLE` means the console (vt) has focus. -pub const CONSOLE: usize = usize::MAX; +/// Window 0 is the home terminal — an ordinary focusable window like any +/// other (ADR 0006 Phase 1: no CONSOLE sentinel, keyboard routing is +/// total: every key goes to `focused()`'s owner). const MAX_WINDOWS: usize = 16; const MIN_W: usize = 64; @@ -87,7 +88,7 @@ const EMPTY: Window = Window { static mut WINDOWS: [Window; MAX_WINDOWS] = [EMPTY; MAX_WINDOWS]; static mut ZORDER: [usize; MAX_WINDOWS] = [usize::MAX; MAX_WINDOWS]; static mut ZLEN: usize = 0; -static FOCUSED: AtomicUsize = AtomicUsize::new(CONSOLE); +static FOCUSED: AtomicUsize = AtomicUsize::new(0); static mut CASCADE_X: usize = 0; static mut CASCADE_Y: usize = 0; @@ -111,7 +112,6 @@ pub fn init() { CASCADE_X = 0; CASCADE_Y = 0; } - FOCUSED.store(CONSOLE, Ordering::Relaxed); } /// Create window 0 (the home terminal): full-screen, chrome-less, @@ -124,7 +124,7 @@ pub fn create_home_window(buf: *mut u32, w: usize, h: usize) -> usize { state: WinState::Visible, title: [0; TITLE_MAX], title_len: 0, - owner: usize::MAX, + owner: crate::sched::SHELL_TID, x: 0, y: 0, w, @@ -135,12 +135,17 @@ pub fn create_home_window(buf: *mut u32, w: usize, h: usize) -> usize { ZORDER[0] = id; ZLEN = 1; } + FOCUSED.store(0, Ordering::Relaxed); id } pub fn focused() -> Option { let v = FOCUSED.load(Ordering::Relaxed); - if v == CONSOLE || v == 0 { None } else { Some(v) } + if v < MAX_WINDOWS && get(v).is_some() { + Some(v) + } else { + None + } } fn get(id: usize) -> Option { @@ -231,8 +236,10 @@ pub fn flush(id: usize) { let outer_h = win.h + if win.chrome { TITLE_BAR_H } else { 0 }; let pad = if id != 0 { 1 } else { 0 }; composite_rect( - win.x.saturating_sub(pad), win.y.saturating_sub(pad), - win.w + 2 * pad, outer_h + 2 * pad, + win.x.saturating_sub(pad), + win.y.saturating_sub(pad), + win.w + 2 * pad, + outer_h + 2 * pad, ); } @@ -256,8 +263,8 @@ pub fn destroy(id: usize) { if focused() == Some(id) { if unsafe { ZLEN } > 0 { focus(unsafe { ZORDER[ZLEN - 1] }); - } else { - focus_console(); + } else if id != 0 { + focus(0); } } composite_rect(x, y, w, h); @@ -277,7 +284,7 @@ pub fn minimize(id: usize) { } remove_from_zorder(id); if focused() == Some(id) { - focus_console(); + focus(0); } composite_rect(x, y, w, h); } @@ -307,20 +314,12 @@ pub fn focus(id: usize) { return; } let prev = focused(); - // Window 0 (home terminal) is the console — store the CONSOLE - // sentinel so `focused()` returns None and the tty layer reads - // keyboard input directly (same as focus_console). - if id == 0 { - FOCUSED.store(CONSOLE, Ordering::Relaxed); - } else { - FOCUSED.store(id, Ordering::Relaxed); - } - if id != 0 { - remove_from_zorder(id); - unsafe { - ZORDER[ZLEN] = id; - ZLEN += 1; - } + FOCUSED.store(id, Ordering::Relaxed); + // Every window lives in the z-order now, window 0 included. + remove_from_zorder(id); + unsafe { + ZORDER[ZLEN] = id; + ZLEN += 1; } if let Some(p) = prev && p != id @@ -331,8 +330,10 @@ pub fn focus(id: usize) { let oh = pw.h + if pw.chrome { TITLE_BAR_H } else { 0 }; let pad = if p != 0 { 1 } else { 0 }; composite_rect( - pw.x.saturating_sub(pad), pw.y.saturating_sub(pad), - pw.w + 2 * pad, oh + 2 * pad, + pw.x.saturating_sub(pad), + pw.y.saturating_sub(pad), + pw.w + 2 * pad, + oh + 2 * pad, ); } let _ = crate::sched::send(win.owner, TaskMsg::Focus(true)); @@ -340,35 +341,26 @@ pub fn focus(id: usize) { let oh = win.h + if win.chrome { TITLE_BAR_H } else { 0 }; let pad = if id != 0 { 1 } else { 0 }; composite_rect( - win.x.saturating_sub(pad), win.y.saturating_sub(pad), - win.w + 2 * pad, oh + 2 * pad, + win.x.saturating_sub(pad), + win.y.saturating_sub(pad), + win.w + 2 * pad, + oh + 2 * pad, ); } +/// Legacy name — the home terminal is now just window 0. pub fn focus_console() { - let prev = focused(); - FOCUSED.store(CONSOLE, Ordering::Relaxed); - if let Some(p) = prev - && let Some(pw) = get(p) - { - let _ = crate::sched::send(pw.owner, TaskMsg::Focus(false)); - let oh = pw.h + if pw.chrome { TITLE_BAR_H } else { 0 }; - let pad = if p != 0 { 1 } else { 0 }; - composite_rect( - pw.x.saturating_sub(pad), pw.y.saturating_sub(pad), - pw.w + 2 * pad, oh + 2 * pad, - ); - } + focus(0); } pub fn focus_next() { unsafe { - if ZLEN == 0 { - focus_console(); - return; - } match focused() { - None => focus(ZORDER[ZLEN - 1]), + None => { + if ZLEN > 0 { + focus(ZORDER[ZLEN - 1]); + } + } Some(cur) => { let mut idx = usize::MAX; for i in 0..ZLEN { @@ -378,7 +370,9 @@ pub fn focus_next() { } } if idx == usize::MAX || idx == 0 { - focus_console(); + if ZLEN > 0 { + focus(ZORDER[ZLEN - 1]); + } } else { focus(ZORDER[idx - 1]); } @@ -530,6 +524,7 @@ fn mouse_press(x: i32, y: i32, buttons: u8) { Hit::None => {} } } else { + // Click on the bare desktop: focus the home terminal (window 0). focus_console(); } } @@ -606,8 +601,18 @@ fn move_window(id: usize, nx: usize, ny: usize) { } let pad: usize = 1; let oh = w.h + TITLE_BAR_H; - composite_rect(ox.saturating_sub(pad), oy.saturating_sub(pad), w.w + 2 * pad, oh + 2 * pad); - composite_rect(nx.saturating_sub(pad), ny.saturating_sub(pad), w.w + 2 * pad, oh + 2 * pad); + composite_rect( + ox.saturating_sub(pad), + oy.saturating_sub(pad), + w.w + 2 * pad, + oh + 2 * pad, + ); + composite_rect( + nx.saturating_sub(pad), + ny.saturating_sub(pad), + w.w + 2 * pad, + oh + 2 * pad, + ); } pub fn owner(id: usize) -> Option { @@ -637,13 +642,14 @@ pub fn spawn_input_pump() -> Option { } fn pump_main() { + // Total routing (ADR 0006 Phase 1): every key from every source is + // pumped to the focused window's owner. No task ever polls the + // global rings directly; when nothing is focused the keys drop. loop { - if let Some(id) = focused() { - let o = owner(id).unwrap_or(0); - while let Some(ev) = crate::ps2::try_poll_key().or_else(crate::uhci::try_poll_key) { - if !handle_key(ev) { - let _ = crate::sched::send(o, TaskMsg::Key(ev)); - } + let o = focused().and_then(owner).unwrap_or(crate::sched::SHELL_TID); + while let Some(ev) = crate::ps2::try_poll_key().or_else(crate::uhci::try_poll_key) { + if !handle_key(ev) { + let _ = crate::sched::send(o, TaskMsg::Key(ev)); } } crate::sched::sleep_ms(5); @@ -669,11 +675,19 @@ fn composite_rect(x: usize, y: usize, w: usize, h: usize) { /// 1px border drawn around every window except the root (id 0). /// Blue when focused, grey when not. fn draw_window_border(id: usize, rx: usize, ry: usize, rw: usize, rh: usize) { - if id == 0 { return; } + if id == 0 { + return; + } let win = unsafe { WINDOWS[id] }; - if !win.used || win.state != WinState::Visible { return; } + if !win.used || win.state != WinState::Visible { + return; + } let outer_h = win.h + if win.chrome { TITLE_BAR_H } else { 0 }; - let bg = if focused() == Some(id) { TITLE_BG_FOCUS } else { TITLE_BG }; + let bg = if focused() == Some(id) { + TITLE_BG_FOCUS + } else { + TITLE_BG + }; let bc = gop::rgb(bg.0, bg.1, bg.2); // Border sits 1px outside the window's outer rect. let bx = win.x.saturating_sub(1); @@ -684,12 +698,14 @@ fn draw_window_border(id: usize, rx: usize, ry: usize, rw: usize, rh: usize) { let iy = by.max(ry); let ix2 = (bx + bw).min(rx + rw); let iy2 = (by + bh).min(ry + rh); - if ix >= ix2 || iy >= iy2 { return; } + if ix >= ix2 || iy >= iy2 { + return; + } // Four edges. - gop::shadow_fill_rect(ix, iy, ix2 - ix, 1, bc); // top - gop::shadow_fill_rect(ix, iy2 - 1, ix2 - ix, 1, bc); // bottom - gop::shadow_fill_rect(ix, iy + 1, 1, iy2 - iy - 2, bc); // left - gop::shadow_fill_rect(ix2 - 1, iy + 1, 1, iy2 - iy - 2, bc); // right + gop::shadow_fill_rect(ix, iy, ix2 - ix, 1, bc); // top + gop::shadow_fill_rect(ix, iy2 - 1, ix2 - ix, 1, bc); // bottom + gop::shadow_fill_rect(ix, iy + 1, 1, iy2 - iy - 2, bc); // left + gop::shadow_fill_rect(ix2 - 1, iy + 1, 1, iy2 - iy - 2, bc); // right } /// Blit the portion of a window that intersects `(rx, ry, rw, rh)`. diff --git a/scripts/test-focus.py b/scripts/test-focus.py new file mode 100755 index 0000000..e1bbe2d --- /dev/null +++ b/scripts/test-focus.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +"""Drive the focus-routing gate: 3 windows, type markers, verify routing. + +Connects to the QEMU monitor once and keeps the socket open (the per-call +scripts/sendkeys.py pattern loses the socket when the monitor resets). + +Usage: python3 scripts/test-focus.py /tmp/qmon.sock +""" +import socket +import sys +import time + +sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) +sock.connect(sys.argv[1] if len(sys.argv) > 1 else "/tmp/sysboot-mon.sock") + + +def sk(k, d=0.35): + sock.sendall(f"sendkey {k}\n".encode()) + time.sleep(d) + + +def line(txt, d=0.8): + for ch in txt: + if ch == " ": + sk("spc", 0.1) + elif ch.isupper(): + sk("shift-" + ch.lower(), 0.12) + else: + sk(ch, 0.12) + sk("ret", d) + + +time.sleep(0.5) + +# Home is focused at boot. +line("windows") # expect: home + nothing (or existing windows) +time.sleep(1.5) + +# F1 -> term1 (focused on creation) +sk("f1", 1.5) +line("echo TERM1MARK") +time.sleep(1.0) + +# F1 -> term2 (focused on creation) +sk("f1", 1.5) +line("echo TERM2MARK") +time.sleep(1.0) + +# F2 cycles z-order: term2 -> term1 +sk("f2", 0.8) +line("echo CYCLED") +time.sleep(1.0) + +# F2 again: term1 -> home (window 0, oldest) +sk("f2", 0.8) +line("windows") diff --git a/shell/src/lib.rs b/shell/src/lib.rs index f2a0e6e..d1710cc 100644 --- a/shell/src/lib.rs +++ b/shell/src/lib.rs @@ -30,7 +30,9 @@ use alloc::vec::Vec; use rsnix::tty::{self, LineInput}; use rsnix::{gop_print, gop_println}; -/// Run the shell forever. +/// Run the home-terminal shell (task 0). The home terminal is an +/// ordinary WM window (id 0) owned by this task, so input flows through +/// the same pumped queue as any windowed shell. pub fn run() -> ! { // Row 0 is the boot-marker status line (white-on-black, the last boot // stage drawn during init). Real hardware has no serial console, so @@ -64,6 +66,7 @@ pub fn run() -> ! { } term_println("!!! end of crash report"); } + let term = rsnix::vt::home_terminal(); loop { // Push any deferred scrolls so the prompt renders on a current @@ -77,12 +80,18 @@ pub fn run() -> ! { } // Print prompt term_print("> "); - - // Read one line from keyboard (tty canonical mode) - match tty::read_line() { - LineInput::Interrupted => continue, // ^C - fresh prompt - LineInput::Eof => continue, // ^D on an empty line - fresh prompt (no poweroff) - LineInput::Line(line) => { + // Read one line (tty canonical mode over the pumped queue). + match tty::read_line(term) { + None => { + // Window 0 is never closed; a Close message here means + // shutdown. Park forever (the idle task takes over). + loop { + rsnix::sched::sleep_ms(1000); + } + } + Some(LineInput::Interrupted) => continue, // ^C - fresh prompt + Some(LineInput::Eof) => continue, // ^D - fresh prompt (no poweroff) + Some(LineInput::Line(line)) => { let trimmed = line.trim(); if trimmed.is_empty() { continue; @@ -94,15 +103,14 @@ pub fn run() -> ! { } /// Run a shell instance inside a windowed terminal. Same as `run()` but -/// reads from the task's message queue (Fed by the WM input pump) and -/// writes to the active terminal (set by the caller via `vt::set_active`). +/// for a spawned F1 terminal (own task + window). /// Returns when the window is closed (Close message or ^D on empty line). pub fn run_windowed(term: &mut rsnix::vt::Terminal) { term_println(&alloc::format!("rs-dos v{}", env!("CARGO_PKG_VERSION"))); loop { rsnix::vt::sync_scroll(); term_print("> "); - match tty::read_line_from_queue(term) { + match tty::read_line(term) { None => return, // window closed Some(LineInput::Interrupted) => continue, Some(LineInput::Eof) => return, // ^D in windowed mode = close @@ -1033,9 +1041,11 @@ fn cmd_cat(p: &Parsed) { /// Read lines from the tty until ^C/^D (or an exact marker line, when /// `marker` is Some - the marker itself is not part of the input). fn cat_stdin(pipe: &mut Pipeline, marker: Option<&str>) -> bool { + let term = rsnix::vt::home_terminal(); loop { - match tty::read_line() { - LineInput::Line(l) => { + match tty::read_line(term) { + None => return true, // window closed = EOF + Some(LineInput::Line(l)) => { if marker.is_some() && l.as_str() == marker.unwrap() { return true; } @@ -1043,8 +1053,8 @@ fn cat_stdin(pipe: &mut Pipeline, marker: Option<&str>) -> bool { return false; } } - LineInput::Interrupted => return false, // ^C aborts the stream - LineInput::Eof => return true, // ^D = EOF + Some(LineInput::Interrupted) => return false, // ^C aborts the stream + Some(LineInput::Eof) => return true, // ^D = EOF } } } @@ -1280,9 +1290,11 @@ fn hexdump_stdin(pipe: &mut Pipeline, marker: Option<&str>) { row_len: 0, alive: true, }; + let term = rsnix::vt::home_terminal(); loop { - match tty::read_line() { - LineInput::Line(l) => { + match tty::read_line(term) { + None => break, + Some(LineInput::Line(l)) => { if marker.is_some() && l.as_str() == marker.unwrap() { break; } @@ -1295,8 +1307,8 @@ fn hexdump_stdin(pipe: &mut Pipeline, marker: Option<&str>) { break; } } - LineInput::Interrupted => break, - LineInput::Eof => break, + Some(LineInput::Interrupted) => break, + Some(LineInput::Eof) => break, } } ctx.flush(); @@ -1429,7 +1441,11 @@ fn cmd_draw(_p: &Parsed) { fn cmd_term(_p: &Parsed) { // Debug: check if the callback is registered. - let cb_set = unsafe { core::ptr::addr_of!(rsnix::wm::SPAWN_TERMINAL_CB).read().is_some() }; + let cb_set = unsafe { + core::ptr::addr_of!(rsnix::wm::SPAWN_TERMINAL_CB) + .read() + .is_some() + }; term_println(&alloc::format!("term: SPAWN_TERMINAL_CB set = {}", cb_set)); if !cb_set { term_err("term: callback not registered — cannot spawn"); @@ -1441,7 +1457,9 @@ fn cmd_term(_p: &Parsed) { let h = 400usize.min(ch); term_println(&alloc::format!("term: requesting window {}x{}", w, h)); unsafe { - let cb = core::ptr::addr_of!(rsnix::wm::SPAWN_TERMINAL_CB).read().unwrap(); + let cb = core::ptr::addr_of!(rsnix::wm::SPAWN_TERMINAL_CB) + .read() + .unwrap(); cb("term dbg", w, h); } term_println("term: callback returned (task should be spawning)"); -- cgit v1.2.3