From 9b9758e5ee2564b04685eadf2b9ce23dd1060b86 Mon Sep 17 00:00:00 2001 From: salmonik Date: Sat, 1 Aug 2026 19:34:40 +0200 Subject: rsnix v0.4.0: GUI input foundation — PS/2 mouse + scheduler message queues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mouse (IRQ12, stream mode): ByteRing generalized for both PS/2 devices, ISR routes data-port bytes by the controller's aux bit; mouse_init with bounded-ACK handshake (boots without a mouse); packet decoder with bit-3 resync, screen-relative deltas, buttons. Exposed as /dev/mouse, the `mouse` stream command, and `make test-mouse`. Message queues (ADR 0003's GUI-era primitive): every task gets a static 16-slot TaskMsg ring; send/recv/recv_blocking with direct wake + tick scan safety net for the lost-wakeup race; `msgrecv`/`send` commands, `mouse ` routes real IRQ12 events into a task's queue, jobs shows q=. `make test-msg` + `make test-disk` regression pass. --- shell/src/lib.rs | 211 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 209 insertions(+), 2 deletions(-) (limited to 'shell/src/lib.rs') diff --git a/shell/src/lib.rs b/shell/src/lib.rs index 4640697..c18d319 100644 --- a/shell/src/lib.rs +++ b/shell/src/lib.rs @@ -703,6 +703,7 @@ write `{} TIL `", "date" => cmd_date(&parsed), "pci" => cmd_pci(&parsed), "rescan" => cmd_rescan(&parsed), + "mouse" => cmd_mouse(&parsed), "ls" => cmd_ls(&parsed), "cat" => cmd_cat(&parsed), "klog" => cmd_klog(&parsed), @@ -713,6 +714,8 @@ write `{} TIL `", "edit" => cmd_edit(&parsed), "bg" => cmd_bg(&parsed), "jobs" => cmd_jobs(&parsed), + "msgrecv" => cmd_msgrecv(&parsed), + "send" => cmd_send(&parsed), "sleep" => cmd_sleep(&parsed), "kill" => cmd_kill(&parsed), "demo" => cmd_demo(&parsed), @@ -738,8 +741,16 @@ fn cmd_help(_p: &Parsed) { date — show current date/time (RTC) pci — list PCI devices rescan — re-scan for hotplugged/removed disks (also runs automatically) + mouse — live mouse events (dx/dy/buttons); ^C stops + mouse — post decoded mouse events to task 's queue (input routing) bg — spawn a background task (spinner, ticker, counter, boom, overflow) - jobs — list tasks (id, name, state, ticks) + msgrecv — spawn a task that blocks on its message queue and logs every + message to serial (ids from `jobs`) + send [args] — post a message to a task's queue: + user · timer · mouse (1=L 2=R 4=M) + key (enter esc tab space bksp up down left right home + end pgup pgdn del ctrl-c f1..f12, or a single char) + jobs — list tasks (id, name, state, ticks, q=pending messages) sleep — block the shell for ms; background tasks keep running kill — stop a background task (id from jobs) demo — spawn spinner + ticker + counter @@ -1517,7 +1528,12 @@ fn rescan_all() -> (usize, usize, usize, usize, usize, usize) { fn report_rescan((da, dr, ma, mu, na, nr): (usize, usize, usize, usize, usize, usize)) -> bool { term_println(&alloc::format!( "rescan: {} disk(s) added, {} removed · {} volume(s) mounted, {} unmounted · {} node(s) added, {} removed", - da, dr, ma, mu, na, nr + da, + dr, + ma, + mu, + na, + nr )); da + dr + ma + mu + na + nr > 0 } @@ -1530,6 +1546,62 @@ fn cmd_rescan(_p: &Parsed) { } } +/// Live mouse event stream: print each decoded packet (dx/dy/buttons) as +/// it arrives until ^C. With an optional task id, events are **posted to +/// that task's message queue** instead of printed — the input-routing +/// path the window manager will use (`mouse ` + `msgrecv` proves the +/// whole chain: IRQ12 → ring → decoder → queue → blocked task). Polls +/// the PS/2 ring every 10 ms (via `sleep_ms`, which only blocks the +/// shell task) so background tasks keep running. +fn cmd_mouse(p: &Parsed) { + let target: Option = p.args.first().and_then(|s| s.parse::().ok()); + if let Some(id) = target { + if !rsnix::sched::is_alive(id) { + term_err(&alloc::format!("mouse: no such task {}", id)); + return; + } + term_println(&alloc::format!( + "mouse: posting events to task {} — ^C to stop", + id + )); + } else { + if !rsnix::ps2::mouse_present() { + term_err("mouse: no PS/2 mouse at boot — cat /dev/mouse still works"); + return; + } + term_println("mouse: streaming events — ^C to stop"); + } + loop { + if tty::interrupt_pressed() { + term_println("mouse: stopped"); + return; + } + while let Some(ev) = rsnix::ps2::try_poll_mouse() { + match target { + Some(id) => { + if let Err(e) = rsnix::sched::send(id, rsnix::sched::TaskMsg::Mouse(ev)) { + term_err(&alloc::format!("mouse: {}", e)); + return; + } + } + None => { + let mut b = String::new(); + b.push(if ev.left { 'L' } else { '-' }); + b.push(if ev.right { 'R' } else { '-' }); + b.push(if ev.middle { 'M' } else { '-' }); + term_println(&alloc::format!( + "dx={:+} dy={:+} buttons={}", + ev.dx, + ev.dy, + b + )); + } + } + } + rsnix::sched::sleep_ms(10); + } +} + // --------------------------------------------------------------------------- // Multitasking: bg / jobs / sleep / kill / demo // @@ -1577,6 +1649,141 @@ fn cmd_jobs(p: &Parsed) { pipe.end(); } +/// Demo of the scheduler message queues: spawn a task that blocks on +/// `recv_blocking` and logs every message it receives to serial. The +/// shell then posts to it with `send …` (id from `jobs`). +fn cmd_msgrecv(_p: &Parsed) { + match rsnix::sched::spawn(task_msgrecv, "msgrecv") { + Some(id) => term_println(&alloc::format!( + "msgrecv: task {} listening — post with: send {} …", + id, + id + )), + None => term_err("msgrecv: task table full"), + } +} + +/// The recv demo body: block forever, log each message. Headless by +/// design (serial only — it never touches tty/vt/fs, per ADR 0003). +fn task_msgrecv() { + loop { + let msg = rsnix::sched::recv_blocking(); + rsnix::serial_println!( + "msgrecv[{}]: {}", + rsnix::sched::current(), + rsnix::sched::msg_describe(&msg) + ); + } +} + +/// `send [args]` — post a TaskMsg to a task's queue. This +/// is the shell-side knob for the primitive the window manager will use +/// to route input; types: user , timer, mouse +/// (1=L 2=R 4=M), key (enter esc tab space bksp up down left +/// right home end pgup pgdn del ctrl-c f1..f12, or a single char). +fn cmd_send(p: &Parsed) { + let mut args = p.args.iter(); + let id: usize = match args.next().and_then(|s| s.parse().ok()) { + Some(v) => v, + None => { + term_err("send: needs a task id — jobs lists them, msgrecv spawns a listener"); + return; + } + }; + let kind = match args.next() { + Some(k) => k.as_str(), + None => { + term_err("send: needs a type — user | timer | mouse | key "); + return; + } + }; + let msg = match kind { + "user" => { + let Some(n) = args.next().and_then(|s| s.parse().ok()) else { + term_err("send: user needs a number"); + return; + }; + rsnix::sched::TaskMsg::User(n) + } + "timer" => rsnix::sched::TaskMsg::Timer, + "mouse" => { + let dx: i16 = args.next().and_then(|s| s.parse().ok()).unwrap_or(0); + let dy: i16 = args.next().and_then(|s| s.parse().ok()).unwrap_or(0); + let btn: u8 = args.next().and_then(|s| s.parse().ok()).unwrap_or(0); + rsnix::sched::TaskMsg::Mouse(rsnix::ps2::MouseEvent { + dx, + dy, + left: btn & 1 != 0, + right: btn & 2 != 0, + middle: btn & 4 != 0, + }) + } + "key" => match args.next() { + Some(k) => match key_from_name(k.as_str()) { + Some(ev) => rsnix::sched::TaskMsg::Key(ev), + None => { + term_err("send: unknown key name"); + return; + } + }, + None => { + term_err("send: key needs a name"); + return; + } + }, + _ => { + term_err("send: unknown type — user | timer | mouse | key "); + return; + } + }; + match rsnix::sched::send(id, msg) { + Ok(()) => {} + Err(e) => term_err(&alloc::format!("send: {}", e)), + } +} + +/// Map a small key-name vocabulary to a KeyEvent for `send key …`. +fn key_from_name(name: &str) -> Option { + use rsnix::ps2::KeyEvent; + Some(match name { + "enter" => KeyEvent::Enter, + "esc" | "escape" => KeyEvent::Escape, + "tab" => KeyEvent::Tab, + "space" => KeyEvent::Char(' '), + "bksp" | "backspace" => KeyEvent::Backspace, + "up" => KeyEvent::ArrowUp, + "down" => KeyEvent::ArrowDown, + "left" => KeyEvent::ArrowLeft, + "right" => KeyEvent::ArrowRight, + "home" => KeyEvent::Home, + "end" => KeyEvent::End, + "pgup" => KeyEvent::PageUp, + "pgdn" => KeyEvent::PageDown, + "del" | "delete" => KeyEvent::Delete, + "ctrl-c" => KeyEvent::Ctrl('c'), + "ctrl-d" => KeyEvent::Ctrl('d'), + "ctrl-s" => KeyEvent::Ctrl('s'), + "ctrl-x" => KeyEvent::Ctrl('x'), + _ => { + if let Some(n) = name.strip_prefix('f') { + if let Ok(n) = n.parse::() { + if (1..=12).contains(&n) { + KeyEvent::F(n) + } else { + return None; + } + } else { + return None; + } + } else if name.chars().count() == 1 { + KeyEvent::Char(name.chars().next().unwrap()) + } else { + return None; + } + } + }) +} + fn cmd_sleep(p: &Parsed) { let ms: u64 = match p.args.first().and_then(|s| s.parse().ok()) { Some(m) if m > 0 => m, -- cgit v1.2.3