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 | |
| 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.
| -rw-r--r-- | rsnix/src/sched.rs | 13 | ||||
| -rw-r--r-- | rsnix/src/tty.rs | 149 | ||||
| -rw-r--r-- | rsnix/src/wm.rs | 144 | ||||
| -rwxr-xr-x | scripts/test-focus.py | 56 | ||||
| -rw-r--r-- | shell/src/lib.rs | 58 |
5 files changed, 208 insertions, 212 deletions
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; | |||
| 201 | const WATCHDOG_BOOT_SPIN_TICKS: u32 = 750; | 201 | const WATCHDOG_BOOT_SPIN_TICKS: u32 = 750; |
| 202 | 202 | ||
| 203 | /// True once the shell REPL is live (set by main.rs before shell::run()). | 203 | /// True once the shell REPL is live (set by main.rs before shell::run()). |
| 204 | /// The shell legitimately parks in poll_key with a fixed frame when it is; | 204 | /// The shell legitimately blocks on its message queue once live; |
| 205 | /// before that, task 0 running boot code must not sit in a fixed frame. | 205 | /// before that, task 0 running boot code must not sit in a fixed frame. |
| 206 | static SHELL_RUNNING: AtomicBool = AtomicBool::new(false); | 206 | static SHELL_RUNNING: AtomicBool = AtomicBool::new(false); |
| 207 | 207 | ||
| @@ -264,16 +264,17 @@ pub fn on_tick(cur: usize) -> usize { | |||
| 264 | TASKS[CURRENT].ticks += 1; | 264 | TASKS[CURRENT].ticks += 1; |
| 265 | // Spin detection: a task whose interrupted frame never moves | 265 | // Spin detection: a task whose interrupted frame never moves |
| 266 | // (identical RSP on every tick) is stuck in a hot loop — no yield, | 266 | // (identical RSP on every tick) is stuck in a hot loop — no yield, |
| 267 | // no sleep, no block. The shell (id 0) legitimately parks in | 267 | // no sleep, no block. The shell (id 0) legitimately blocks on |
| 268 | // poll_key with a fixed frame, and idle (id 1) hlt's; both exempt. | 268 | // recv_blocking with a fixed frame, and idle (id 1) hlt's; both |
| 269 | // exempt. | ||
| 269 | let id = CURRENT; | 270 | let id = CURRENT; |
| 270 | let w = &mut SPIN_WATCH[id]; | 271 | let w = &mut SPIN_WATCH[id]; |
| 271 | if w.last_rsp == cur { | 272 | if w.last_rsp == cur { |
| 272 | w.same += 1; | 273 | w.same += 1; |
| 273 | // Background tasks (id >= 2): spinning is never legitimate. | 274 | // Background tasks (id >= 2): spinning is never legitimate. |
| 274 | // Task 0 (shell): exempt once the REPL is live (poll_key parks | 275 | // Task 0 (shell): exempt once the REPL is live (recv_blocking |
| 275 | // with a fixed frame); during the boot phase a fixed frame | 276 | // parks with a fixed frame); during the boot phase a fixed |
| 276 | // means usb/ehci init is wedged on real hardware. | 277 | // frame means usb/ehci init is wedged on real hardware. |
| 277 | let threshold = if id == SHELL_TID && !SHELL_RUNNING.load(Ordering::Relaxed) { | 278 | let threshold = if id == SHELL_TID && !SHELL_RUNNING.load(Ordering::Relaxed) { |
| 278 | WATCHDOG_BOOT_SPIN_TICKS | 279 | WATCHDOG_BOOT_SPIN_TICKS |
| 279 | } else if id >= 2 { | 280 | } 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 @@ | |||
| 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 | ||
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; | |||
| 16 | /// Height of a window's title bar in pixels (one 8×16 glyph row). | 16 | /// Height of a window's title bar in pixels (one 8×16 glyph row). |
| 17 | pub const TITLE_BAR_H: usize = 16; | 17 | pub const TITLE_BAR_H: usize = 16; |
| 18 | 18 | ||
| 19 | /// Sentinel: `FOCUSED == CONSOLE` means the console (vt) has focus. | 19 | /// Window 0 is the home terminal — an ordinary focusable window like any |
| 20 | pub const CONSOLE: usize = usize::MAX; | 20 | /// other (ADR 0006 Phase 1: no CONSOLE sentinel, keyboard routing is |
| 21 | /// total: every key goes to `focused()`'s owner). | ||
| 21 | 22 | ||
| 22 | const MAX_WINDOWS: usize = 16; | 23 | const MAX_WINDOWS: usize = 16; |
| 23 | const MIN_W: usize = 64; | 24 | const MIN_W: usize = 64; |
| @@ -87,7 +88,7 @@ const EMPTY: Window = Window { | |||
| 87 | static mut WINDOWS: [Window; MAX_WINDOWS] = [EMPTY; MAX_WINDOWS]; | 88 | static mut WINDOWS: [Window; MAX_WINDOWS] = [EMPTY; MAX_WINDOWS]; |
| 88 | static mut ZORDER: [usize; MAX_WINDOWS] = [usize::MAX; MAX_WINDOWS]; | 89 | static mut ZORDER: [usize; MAX_WINDOWS] = [usize::MAX; MAX_WINDOWS]; |
| 89 | static mut ZLEN: usize = 0; | 90 | static mut ZLEN: usize = 0; |
| 90 | static FOCUSED: AtomicUsize = AtomicUsize::new(CONSOLE); | 91 | static FOCUSED: AtomicUsize = AtomicUsize::new(0); |
| 91 | static mut CASCADE_X: usize = 0; | 92 | static mut CASCADE_X: usize = 0; |
| 92 | static mut CASCADE_Y: usize = 0; | 93 | static mut CASCADE_Y: usize = 0; |
| 93 | 94 | ||
| @@ -111,7 +112,6 @@ pub fn init() { | |||
| 111 | CASCADE_X = 0; | 112 | CASCADE_X = 0; |
| 112 | CASCADE_Y = 0; | 113 | CASCADE_Y = 0; |
| 113 | } | 114 | } |
| 114 | FOCUSED.store(CONSOLE, Ordering::Relaxed); | ||
| 115 | } | 115 | } |
| 116 | 116 | ||
| 117 | /// Create window 0 (the home terminal): full-screen, chrome-less, | 117 | /// 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 { | |||
| 124 | state: WinState::Visible, | 124 | state: WinState::Visible, |
| 125 | title: [0; TITLE_MAX], | 125 | title: [0; TITLE_MAX], |
| 126 | title_len: 0, | 126 | title_len: 0, |
| 127 | owner: usize::MAX, | 127 | owner: crate::sched::SHELL_TID, |
| 128 | x: 0, | 128 | x: 0, |
| 129 | y: 0, | 129 | y: 0, |
| 130 | w, | 130 | w, |
| @@ -135,12 +135,17 @@ pub fn create_home_window(buf: *mut u32, w: usize, h: usize) -> usize { | |||
| 135 | ZORDER[0] = id; | 135 | ZORDER[0] = id; |
| 136 | ZLEN = 1; | 136 | ZLEN = 1; |
| 137 | } | 137 | } |
| 138 | FOCUSED.store(0, Ordering::Relaxed); | ||
| 138 | id | 139 | id |
| 139 | } | 140 | } |
| 140 | 141 | ||
| 141 | pub fn focused() -> Option<usize> { | 142 | pub fn focused() -> Option<usize> { |
| 142 | let v = FOCUSED.load(Ordering::Relaxed); | 143 | let v = FOCUSED.load(Ordering::Relaxed); |
| 143 | if v == CONSOLE || v == 0 { None } else { Some(v) } | 144 | if v < MAX_WINDOWS && get(v).is_some() { |
| 145 | Some(v) | ||
| 146 | } else { | ||
| 147 | None | ||
| 148 | } | ||
| 144 | } | 149 | } |
| 145 | 150 | ||
| 146 | fn get(id: usize) -> Option<Window> { | 151 | fn get(id: usize) -> Option<Window> { |
| @@ -231,8 +236,10 @@ pub fn flush(id: usize) { | |||
| 231 | let outer_h = win.h + if win.chrome { TITLE_BAR_H } else { 0 }; | 236 | let outer_h = win.h + if win.chrome { TITLE_BAR_H } else { 0 }; |
| 232 | let pad = if id != 0 { 1 } else { 0 }; | 237 | let pad = if id != 0 { 1 } else { 0 }; |
| 233 | composite_rect( | 238 | composite_rect( |
| 234 | win.x.saturating_sub(pad), win.y.saturating_sub(pad), | 239 | win.x.saturating_sub(pad), |
| 235 | win.w + 2 * pad, outer_h + 2 * pad, | 240 | win.y.saturating_sub(pad), |
| 241 | win.w + 2 * pad, | ||
| 242 | outer_h + 2 * pad, | ||
| 236 | ); | 243 | ); |
| 237 | } | 244 | } |
| 238 | 245 | ||
| @@ -256,8 +263,8 @@ pub fn destroy(id: usize) { | |||
| 256 | if focused() == Some(id) { | 263 | if focused() == Some(id) { |
| 257 | if unsafe { ZLEN } > 0 { | 264 | if unsafe { ZLEN } > 0 { |
| 258 | focus(unsafe { ZORDER[ZLEN - 1] }); | 265 | focus(unsafe { ZORDER[ZLEN - 1] }); |
| 259 | } else { | 266 | } else if id != 0 { |
| 260 | focus_console(); | 267 | focus(0); |
| 261 | } | 268 | } |
| 262 | } | 269 | } |
| 263 | composite_rect(x, y, w, h); | 270 | composite_rect(x, y, w, h); |
| @@ -277,7 +284,7 @@ pub fn minimize(id: usize) { | |||
| 277 | } | 284 | } |
| 278 | remove_from_zorder(id); | 285 | remove_from_zorder(id); |
| 279 | if focused() == Some(id) { | 286 | if focused() == Some(id) { |
| 280 | focus_console(); | 287 | focus(0); |
| 281 | } | 288 | } |
| 282 | composite_rect(x, y, w, h); | 289 | composite_rect(x, y, w, h); |
| 283 | } | 290 | } |
| @@ -307,20 +314,12 @@ pub fn focus(id: usize) { | |||
| 307 | return; | 314 | return; |
| 308 | } | 315 | } |
| 309 | let prev = focused(); | 316 | let prev = focused(); |
| 310 | // Window 0 (home terminal) is the console — store the CONSOLE | 317 | FOCUSED.store(id, Ordering::Relaxed); |
| 311 | // sentinel so `focused()` returns None and the tty layer reads | 318 | // Every window lives in the z-order now, window 0 included. |
| 312 | // keyboard input directly (same as focus_console). | 319 | remove_from_zorder(id); |
| 313 | if id == 0 { | 320 | unsafe { |
| 314 | FOCUSED.store(CONSOLE, Ordering::Relaxed); | 321 | ZORDER[ZLEN] = id; |
| 315 | } else { | 322 | ZLEN += 1; |
| 316 | FOCUSED.store(id, Ordering::Relaxed); | ||
| 317 | } | ||
| 318 | if id != 0 { | ||
| 319 | remove_from_zorder(id); | ||
| 320 | unsafe { | ||
| 321 | ZORDER[ZLEN] = id; | ||
| 322 | ZLEN += 1; | ||
| 323 | } | ||
| 324 | } | 323 | } |
| 325 | if let Some(p) = prev | 324 | if let Some(p) = prev |
| 326 | && p != id | 325 | && p != id |
| @@ -331,8 +330,10 @@ pub fn focus(id: usize) { | |||
| 331 | let oh = pw.h + if pw.chrome { TITLE_BAR_H } else { 0 }; | 330 | let oh = pw.h + if pw.chrome { TITLE_BAR_H } else { 0 }; |
| 332 | let pad = if p != 0 { 1 } else { 0 }; | 331 | let pad = if p != 0 { 1 } else { 0 }; |
| 333 | composite_rect( | 332 | composite_rect( |
| 334 | pw.x.saturating_sub(pad), pw.y.saturating_sub(pad), | 333 | pw.x.saturating_sub(pad), |
| 335 | pw.w + 2 * pad, oh + 2 * pad, | 334 | pw.y.saturating_sub(pad), |
| 335 | pw.w + 2 * pad, | ||
| 336 | oh + 2 * pad, | ||
| 336 | ); | 337 | ); |
| 337 | } | 338 | } |
| 338 | let _ = crate::sched::send(win.owner, TaskMsg::Focus(true)); | 339 | let _ = crate::sched::send(win.owner, TaskMsg::Focus(true)); |
| @@ -340,35 +341,26 @@ pub fn focus(id: usize) { | |||
| 340 | let oh = win.h + if win.chrome { TITLE_BAR_H } else { 0 }; | 341 | let oh = win.h + if win.chrome { TITLE_BAR_H } else { 0 }; |
| 341 | let pad = if id != 0 { 1 } else { 0 }; | 342 | let pad = if id != 0 { 1 } else { 0 }; |
| 342 | composite_rect( | 343 | composite_rect( |
| 343 | win.x.saturating_sub(pad), win.y.saturating_sub(pad), | 344 | win.x.saturating_sub(pad), |
| 344 | win.w + 2 * pad, oh + 2 * pad, | 345 | win.y.saturating_sub(pad), |
| 346 | win.w + 2 * pad, | ||
| 347 | oh + 2 * pad, | ||
| 345 | ); | 348 | ); |
| 346 | } | 349 | } |
| 347 | 350 | ||
| 351 | /// Legacy name — the home terminal is now just window 0. | ||
| 348 | pub fn focus_console() { | 352 | pub fn focus_console() { |
| 349 | let prev = focused(); | 353 | focus(0); |
| 350 | FOCUSED.store(CONSOLE, Ordering::Relaxed); | ||
| 351 | if let Some(p) = prev | ||
| 352 | && let Some(pw) = get(p) | ||
| 353 | { | ||
| 354 | let _ = crate::sched::send(pw.owner, TaskMsg::Focus(false)); | ||
| 355 | let oh = pw.h + if pw.chrome { TITLE_BAR_H } else { 0 }; | ||
| 356 | let pad = if p != 0 { 1 } else { 0 }; | ||
| 357 | composite_rect( | ||
| 358 | pw.x.saturating_sub(pad), pw.y.saturating_sub(pad), | ||
| 359 | pw.w + 2 * pad, oh + 2 * pad, | ||
| 360 | ); | ||
| 361 | } | ||
| 362 | } | 354 | } |
| 363 | 355 | ||
| 364 | pub fn focus_next() { | 356 | pub fn focus_next() { |
| 365 | unsafe { | 357 | unsafe { |
| 366 | if ZLEN == 0 { | ||
| 367 | focus_console(); | ||
| 368 | return; | ||
| 369 | } | ||
| 370 | match focused() { | 358 | match focused() { |
| 371 | None => focus(ZORDER[ZLEN - 1]), | 359 | None => { |
| 360 | if ZLEN > 0 { | ||
| 361 | focus(ZORDER[ZLEN - 1]); | ||
| 362 | } | ||
| 363 | } | ||
| 372 | Some(cur) => { | 364 | Some(cur) => { |
| 373 | let mut idx = usize::MAX; | 365 | let mut idx = usize::MAX; |
| 374 | for i in 0..ZLEN { | 366 | for i in 0..ZLEN { |
| @@ -378,7 +370,9 @@ pub fn focus_next() { | |||
| 378 | } | 370 | } |
| 379 | } | 371 | } |
| 380 | if idx == usize::MAX || idx == 0 { | 372 | if idx == usize::MAX || idx == 0 { |
| 381 | focus_console(); | 373 | if ZLEN > 0 { |
| 374 | focus(ZORDER[ZLEN - 1]); | ||
| 375 | } | ||
| 382 | } else { | 376 | } else { |
| 383 | focus(ZORDER[idx - 1]); | 377 | focus(ZORDER[idx - 1]); |
| 384 | } | 378 | } |
| @@ -530,6 +524,7 @@ fn mouse_press(x: i32, y: i32, buttons: u8) { | |||
| 530 | Hit::None => {} | 524 | Hit::None => {} |
| 531 | } | 525 | } |
| 532 | } else { | 526 | } else { |
| 527 | // Click on the bare desktop: focus the home terminal (window 0). | ||
| 533 | focus_console(); | 528 | focus_console(); |
| 534 | } | 529 | } |
| 535 | } | 530 | } |
| @@ -606,8 +601,18 @@ fn move_window(id: usize, nx: usize, ny: usize) { | |||
| 606 | } | 601 | } |
| 607 | let pad: usize = 1; | 602 | let pad: usize = 1; |
| 608 | let oh = w.h + TITLE_BAR_H; | 603 | let oh = w.h + TITLE_BAR_H; |
| 609 | composite_rect(ox.saturating_sub(pad), oy.saturating_sub(pad), w.w + 2 * pad, oh + 2 * pad); | 604 | composite_rect( |
| 610 | composite_rect(nx.saturating_sub(pad), ny.saturating_sub(pad), w.w + 2 * pad, oh + 2 * pad); | 605 | ox.saturating_sub(pad), |
| 606 | oy.saturating_sub(pad), | ||
| 607 | w.w + 2 * pad, | ||
| 608 | oh + 2 * pad, | ||
| 609 | ); | ||
| 610 | composite_rect( | ||
| 611 | nx.saturating_sub(pad), | ||
| 612 | ny.saturating_sub(pad), | ||
| 613 | w.w + 2 * pad, | ||
| 614 | oh + 2 * pad, | ||
| 615 | ); | ||
| 611 | } | 616 | } |
| 612 | 617 | ||
| 613 | pub fn owner(id: usize) -> Option<usize> { | 618 | pub fn owner(id: usize) -> Option<usize> { |
| @@ -637,13 +642,14 @@ pub fn spawn_input_pump() -> Option<usize> { | |||
| 637 | } | 642 | } |
| 638 | 643 | ||
| 639 | fn pump_main() { | 644 | fn pump_main() { |
| 645 | // Total routing (ADR 0006 Phase 1): every key from every source is | ||
| 646 | // pumped to the focused window's owner. No task ever polls the | ||
| 647 | // global rings directly; when nothing is focused the keys drop. | ||
| 640 | loop { | 648 | loop { |
| 641 | if let Some(id) = focused() { | 649 | let o = focused().and_then(owner).unwrap_or(crate::sched::SHELL_TID); |
| 642 | let o = owner(id).unwrap_or(0); | 650 | while let Some(ev) = crate::ps2::try_poll_key().or_else(crate::uhci::try_poll_key) { |
| 643 | while let Some(ev) = crate::ps2::try_poll_key().or_else(crate::uhci::try_poll_key) { | 651 | if !handle_key(ev) { |
| 644 | if !handle_key(ev) { | 652 | let _ = crate::sched::send(o, TaskMsg::Key(ev)); |
| 645 | let _ = crate::sched::send(o, TaskMsg::Key(ev)); | ||
| 646 | } | ||
| 647 | } | 653 | } |
| 648 | } | 654 | } |
| 649 | crate::sched::sleep_ms(5); | 655 | crate::sched::sleep_ms(5); |
| @@ -669,11 +675,19 @@ fn composite_rect(x: usize, y: usize, w: usize, h: usize) { | |||
| 669 | /// 1px border drawn around every window except the root (id 0). | 675 | /// 1px border drawn around every window except the root (id 0). |
| 670 | /// Blue when focused, grey when not. | 676 | /// Blue when focused, grey when not. |
| 671 | fn draw_window_border(id: usize, rx: usize, ry: usize, rw: usize, rh: usize) { | 677 | fn draw_window_border(id: usize, rx: usize, ry: usize, rw: usize, rh: usize) { |
| 672 | if id == 0 { return; } | 678 | if id == 0 { |
| 679 | return; | ||
| 680 | } | ||
| 673 | let win = unsafe { WINDOWS[id] }; | 681 | let win = unsafe { WINDOWS[id] }; |
| 674 | if !win.used || win.state != WinState::Visible { return; } | 682 | if !win.used || win.state != WinState::Visible { |
| 683 | return; | ||
| 684 | } | ||
| 675 | let outer_h = win.h + if win.chrome { TITLE_BAR_H } else { 0 }; | 685 | let outer_h = win.h + if win.chrome { TITLE_BAR_H } else { 0 }; |
| 676 | let bg = if focused() == Some(id) { TITLE_BG_FOCUS } else { TITLE_BG }; | 686 | let bg = if focused() == Some(id) { |
| 687 | TITLE_BG_FOCUS | ||
| 688 | } else { | ||
| 689 | TITLE_BG | ||
| 690 | }; | ||
| 677 | let bc = gop::rgb(bg.0, bg.1, bg.2); | 691 | let bc = gop::rgb(bg.0, bg.1, bg.2); |
| 678 | // Border sits 1px outside the window's outer rect. | 692 | // Border sits 1px outside the window's outer rect. |
| 679 | let bx = win.x.saturating_sub(1); | 693 | 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) { | |||
| 684 | let iy = by.max(ry); | 698 | let iy = by.max(ry); |
| 685 | let ix2 = (bx + bw).min(rx + rw); | 699 | let ix2 = (bx + bw).min(rx + rw); |
| 686 | let iy2 = (by + bh).min(ry + rh); | 700 | let iy2 = (by + bh).min(ry + rh); |
| 687 | if ix >= ix2 || iy >= iy2 { return; } | 701 | if ix >= ix2 || iy >= iy2 { |
| 702 | return; | ||
| 703 | } | ||
| 688 | // Four edges. | 704 | // Four edges. |
| 689 | gop::shadow_fill_rect(ix, iy, ix2 - ix, 1, bc); // top | 705 | gop::shadow_fill_rect(ix, iy, ix2 - ix, 1, bc); // top |
| 690 | gop::shadow_fill_rect(ix, iy2 - 1, ix2 - ix, 1, bc); // bottom | 706 | gop::shadow_fill_rect(ix, iy2 - 1, ix2 - ix, 1, bc); // bottom |
| 691 | gop::shadow_fill_rect(ix, iy + 1, 1, iy2 - iy - 2, bc); // left | 707 | gop::shadow_fill_rect(ix, iy + 1, 1, iy2 - iy - 2, bc); // left |
| 692 | gop::shadow_fill_rect(ix2 - 1, iy + 1, 1, iy2 - iy - 2, bc); // right | 708 | gop::shadow_fill_rect(ix2 - 1, iy + 1, 1, iy2 - iy - 2, bc); // right |
| 693 | } | 709 | } |
| 694 | 710 | ||
| 695 | /// Blit the portion of a window that intersects `(rx, ry, rw, rh)`. | 711 | /// 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 @@ | |||
| 1 | #!/usr/bin/env python3 | ||
| 2 | """Drive the focus-routing gate: 3 windows, type markers, verify routing. | ||
| 3 | |||
| 4 | Connects to the QEMU monitor once and keeps the socket open (the per-call | ||
| 5 | scripts/sendkeys.py pattern loses the socket when the monitor resets). | ||
| 6 | |||
| 7 | Usage: python3 scripts/test-focus.py /tmp/qmon.sock | ||
| 8 | """ | ||
| 9 | import socket | ||
| 10 | import sys | ||
| 11 | import time | ||
| 12 | |||
| 13 | sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) | ||
| 14 | sock.connect(sys.argv[1] if len(sys.argv) > 1 else "/tmp/sysboot-mon.sock") | ||
| 15 | |||
| 16 | |||
| 17 | def sk(k, d=0.35): | ||
| 18 | sock.sendall(f"sendkey {k}\n".encode()) | ||
| 19 | time.sleep(d) | ||
| 20 | |||
| 21 | |||
| 22 | def line(txt, d=0.8): | ||
| 23 | for ch in txt: | ||
| 24 | if ch == " ": | ||
| 25 | sk("spc", 0.1) | ||
| 26 | elif ch.isupper(): | ||
| 27 | sk("shift-" + ch.lower(), 0.12) | ||
| 28 | else: | ||
| 29 | sk(ch, 0.12) | ||
| 30 | sk("ret", d) | ||
| 31 | |||
| 32 | |||
| 33 | time.sleep(0.5) | ||
| 34 | |||
| 35 | # Home is focused at boot. | ||
| 36 | line("windows") # expect: home + nothing (or existing windows) | ||
| 37 | time.sleep(1.5) | ||
| 38 | |||
| 39 | # F1 -> term1 (focused on creation) | ||
| 40 | sk("f1", 1.5) | ||
| 41 | line("echo TERM1MARK") | ||
| 42 | time.sleep(1.0) | ||
| 43 | |||
| 44 | # F1 -> term2 (focused on creation) | ||
| 45 | sk("f1", 1.5) | ||
| 46 | line("echo TERM2MARK") | ||
| 47 | time.sleep(1.0) | ||
| 48 | |||
| 49 | # F2 cycles z-order: term2 -> term1 | ||
| 50 | sk("f2", 0.8) | ||
| 51 | line("echo CYCLED") | ||
| 52 | time.sleep(1.0) | ||
| 53 | |||
| 54 | # F2 again: term1 -> home (window 0, oldest) | ||
| 55 | sk("f2", 0.8) | ||
| 56 | 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; | |||
| 30 | use rsnix::tty::{self, LineInput}; | 30 | use rsnix::tty::{self, LineInput}; |
| 31 | use rsnix::{gop_print, gop_println}; | 31 | use rsnix::{gop_print, gop_println}; |
| 32 | 32 | ||
| 33 | /// Run the shell forever. | 33 | /// Run the home-terminal shell (task 0). The home terminal is an |
| 34 | /// ordinary WM window (id 0) owned by this task, so input flows through | ||
| 35 | /// the same pumped queue as any windowed shell. | ||
| 34 | pub fn run() -> ! { | 36 | pub fn run() -> ! { |
| 35 | // Row 0 is the boot-marker status line (white-on-black, the last boot | 37 | // Row 0 is the boot-marker status line (white-on-black, the last boot |
| 36 | // stage drawn during init). Real hardware has no serial console, so | 38 | // stage drawn during init). Real hardware has no serial console, so |
| @@ -64,6 +66,7 @@ pub fn run() -> ! { | |||
| 64 | } | 66 | } |
| 65 | term_println("!!! end of crash report"); | 67 | term_println("!!! end of crash report"); |
| 66 | } | 68 | } |
| 69 | let term = rsnix::vt::home_terminal(); | ||
| 67 | 70 | ||
| 68 | loop { | 71 | loop { |
| 69 | // Push any deferred scrolls so the prompt renders on a current | 72 | // Push any deferred scrolls so the prompt renders on a current |
| @@ -77,12 +80,18 @@ pub fn run() -> ! { | |||
| 77 | } | 80 | } |
| 78 | // Print prompt | 81 | // Print prompt |
| 79 | term_print("> "); | 82 | term_print("> "); |
| 80 | 83 | // Read one line (tty canonical mode over the pumped queue). | |
| 81 | // Read one line from keyboard (tty canonical mode) | 84 | match tty::read_line(term) { |
| 82 | match tty::read_line() { | 85 | None => { |
| 83 | LineInput::Interrupted => continue, // ^C - fresh prompt | 86 | // Window 0 is never closed; a Close message here means |
| 84 | LineInput::Eof => continue, // ^D on an empty line - fresh prompt (no poweroff) | 87 | // shutdown. Park forever (the idle task takes over). |
| 85 | LineInput::Line(line) => { | 88 | loop { |
| 89 | rsnix::sched::sleep_ms(1000); | ||
| 90 | } | ||
| 91 | } | ||
| 92 | Some(LineInput::Interrupted) => continue, // ^C - fresh prompt | ||
| 93 | Some(LineInput::Eof) => continue, // ^D - fresh prompt (no poweroff) | ||
| 94 | Some(LineInput::Line(line)) => { | ||
| 86 | let trimmed = line.trim(); | 95 | let trimmed = line.trim(); |
| 87 | if trimmed.is_empty() { | 96 | if trimmed.is_empty() { |
| 88 | continue; | 97 | continue; |
| @@ -94,15 +103,14 @@ pub fn run() -> ! { | |||
| 94 | } | 103 | } |
| 95 | 104 | ||
| 96 | /// Run a shell instance inside a windowed terminal. Same as `run()` but | 105 | /// Run a shell instance inside a windowed terminal. Same as `run()` but |
| 97 | /// reads from the task's message queue (Fed by the WM input pump) and | 106 | /// for a spawned F1 terminal (own task + window). |
| 98 | /// writes to the active terminal (set by the caller via `vt::set_active`). | ||
| 99 | /// Returns when the window is closed (Close message or ^D on empty line). | 107 | /// Returns when the window is closed (Close message or ^D on empty line). |
| 100 | pub fn run_windowed(term: &mut rsnix::vt::Terminal) { | 108 | pub fn run_windowed(term: &mut rsnix::vt::Terminal) { |
| 101 | term_println(&alloc::format!("rs-dos v{}", env!("CARGO_PKG_VERSION"))); | 109 | term_println(&alloc::format!("rs-dos v{}", env!("CARGO_PKG_VERSION"))); |
| 102 | loop { | 110 | loop { |
| 103 | rsnix::vt::sync_scroll(); | 111 | rsnix::vt::sync_scroll(); |
| 104 | term_print("> "); | 112 | term_print("> "); |
| 105 | match tty::read_line_from_queue(term) { | 113 | match tty::read_line(term) { |
| 106 | None => return, // window closed | 114 | None => return, // window closed |
| 107 | Some(LineInput::Interrupted) => continue, | 115 | Some(LineInput::Interrupted) => continue, |
| 108 | Some(LineInput::Eof) => return, // ^D in windowed mode = close | 116 | Some(LineInput::Eof) => return, // ^D in windowed mode = close |
| @@ -1033,9 +1041,11 @@ fn cmd_cat(p: &Parsed) { | |||
| 1033 | /// Read lines from the tty until ^C/^D (or an exact marker line, when | 1041 | /// Read lines from the tty until ^C/^D (or an exact marker line, when |
| 1034 | /// `marker` is Some - the marker itself is not part of the input). | 1042 | /// `marker` is Some - the marker itself is not part of the input). |
| 1035 | fn cat_stdin(pipe: &mut Pipeline, marker: Option<&str>) -> bool { | 1043 | fn cat_stdin(pipe: &mut Pipeline, marker: Option<&str>) -> bool { |
| 1044 | let term = rsnix::vt::home_terminal(); | ||
| 1036 | loop { | 1045 | loop { |
| 1037 | match tty::read_line() { | 1046 | match tty::read_line(term) { |
| 1038 | LineInput::Line(l) => { | 1047 | None => return true, // window closed = EOF |
| 1048 | Some(LineInput::Line(l)) => { | ||
| 1039 | if marker.is_some() && l.as_str() == marker.unwrap() { | 1049 | if marker.is_some() && l.as_str() == marker.unwrap() { |
| 1040 | return true; | 1050 | return true; |
| 1041 | } | 1051 | } |
| @@ -1043,8 +1053,8 @@ fn cat_stdin(pipe: &mut Pipeline, marker: Option<&str>) -> bool { | |||
| 1043 | return false; | 1053 | return false; |
| 1044 | } | 1054 | } |
| 1045 | } | 1055 | } |
| 1046 | LineInput::Interrupted => return false, // ^C aborts the stream | 1056 | Some(LineInput::Interrupted) => return false, // ^C aborts the stream |
| 1047 | LineInput::Eof => return true, // ^D = EOF | 1057 | Some(LineInput::Eof) => return true, // ^D = EOF |
| 1048 | } | 1058 | } |
| 1049 | } | 1059 | } |
| 1050 | } | 1060 | } |
| @@ -1280,9 +1290,11 @@ fn hexdump_stdin(pipe: &mut Pipeline, marker: Option<&str>) { | |||
| 1280 | row_len: 0, | 1290 | row_len: 0, |
| 1281 | alive: true, | 1291 | alive: true, |
| 1282 | }; | 1292 | }; |
| 1293 | let term = rsnix::vt::home_terminal(); | ||
| 1283 | loop { | 1294 | loop { |
| 1284 | match tty::read_line() { | 1295 | match tty::read_line(term) { |
| 1285 | LineInput::Line(l) => { | 1296 | None => break, |
| 1297 | Some(LineInput::Line(l)) => { | ||
| 1286 | if marker.is_some() && l.as_str() == marker.unwrap() { | 1298 | if marker.is_some() && l.as_str() == marker.unwrap() { |
| 1287 | break; | 1299 | break; |
| 1288 | } | 1300 | } |
| @@ -1295,8 +1307,8 @@ fn hexdump_stdin(pipe: &mut Pipeline, marker: Option<&str>) { | |||
| 1295 | break; | 1307 | break; |
| 1296 | } | 1308 | } |
| 1297 | } | 1309 | } |
| 1298 | LineInput::Interrupted => break, | 1310 | Some(LineInput::Interrupted) => break, |
| 1299 | LineInput::Eof => break, | 1311 | Some(LineInput::Eof) => break, |
| 1300 | } | 1312 | } |
| 1301 | } | 1313 | } |
| 1302 | ctx.flush(); | 1314 | ctx.flush(); |
| @@ -1429,7 +1441,11 @@ fn cmd_draw(_p: &Parsed) { | |||
| 1429 | 1441 | ||
| 1430 | fn cmd_term(_p: &Parsed) { | 1442 | fn cmd_term(_p: &Parsed) { |
| 1431 | // Debug: check if the callback is registered. | 1443 | // Debug: check if the callback is registered. |
| 1432 | let cb_set = unsafe { core::ptr::addr_of!(rsnix::wm::SPAWN_TERMINAL_CB).read().is_some() }; | 1444 | let cb_set = unsafe { |
| 1445 | core::ptr::addr_of!(rsnix::wm::SPAWN_TERMINAL_CB) | ||
| 1446 | .read() | ||
| 1447 | .is_some() | ||
| 1448 | }; | ||
| 1433 | term_println(&alloc::format!("term: SPAWN_TERMINAL_CB set = {}", cb_set)); | 1449 | term_println(&alloc::format!("term: SPAWN_TERMINAL_CB set = {}", cb_set)); |
| 1434 | if !cb_set { | 1450 | if !cb_set { |
| 1435 | term_err("term: callback not registered — cannot spawn"); | 1451 | term_err("term: callback not registered — cannot spawn"); |
| @@ -1441,7 +1457,9 @@ fn cmd_term(_p: &Parsed) { | |||
| 1441 | let h = 400usize.min(ch); | 1457 | let h = 400usize.min(ch); |
| 1442 | term_println(&alloc::format!("term: requesting window {}x{}", w, h)); | 1458 | term_println(&alloc::format!("term: requesting window {}x{}", w, h)); |
| 1443 | unsafe { | 1459 | unsafe { |
| 1444 | let cb = core::ptr::addr_of!(rsnix::wm::SPAWN_TERMINAL_CB).read().unwrap(); | 1460 | let cb = core::ptr::addr_of!(rsnix::wm::SPAWN_TERMINAL_CB) |
| 1461 | .read() | ||
| 1462 | .unwrap(); | ||
| 1445 | cb("term dbg", w, h); | 1463 | cb("term dbg", w, h); |
| 1446 | } | 1464 | } |
| 1447 | term_println("term: callback returned (task should be spawning)"); | 1465 | term_println("term: callback returned (task should be spawning)"); |
