diff options
| author | salmonik <salmonik@tuta.io> | 2026-08-01 19:34:40 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-08-01 19:34:40 +0200 |
| commit | 9b9758e5ee2564b04685eadf2b9ce23dd1060b86 (patch) | |
| tree | 57fce9e715510ec70df70dc399b4440f3480fd77 /shell/src | |
| parent | fix pi-lens warnings: unwraps, shell SC2046, dup code (fmtbuf/scripts/hexdump... (diff) | |
| download | rs-dos-9b9758e5ee2564b04685eadf2b9ce23dd1060b86.tar.gz rs-dos-9b9758e5ee2564b04685eadf2b9ce23dd1060b86.zip | |
rsnix v0.4.0: GUI input foundation — PS/2 mouse + scheduler message queues
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 <id>` routes real IRQ12 events into a task's queue, jobs shows
q=. `make test-msg` + `make test-disk` regression pass.
Diffstat (limited to '')
| -rw-r--r-- | shell/src/lib.rs | 211 |
1 files changed, 209 insertions, 2 deletions
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 `{} <path> TIL <marker>`", | |||
| 703 | "date" => cmd_date(&parsed), | 703 | "date" => cmd_date(&parsed), |
| 704 | "pci" => cmd_pci(&parsed), | 704 | "pci" => cmd_pci(&parsed), |
| 705 | "rescan" => cmd_rescan(&parsed), | 705 | "rescan" => cmd_rescan(&parsed), |
| 706 | "mouse" => cmd_mouse(&parsed), | ||
| 706 | "ls" => cmd_ls(&parsed), | 707 | "ls" => cmd_ls(&parsed), |
| 707 | "cat" => cmd_cat(&parsed), | 708 | "cat" => cmd_cat(&parsed), |
| 708 | "klog" => cmd_klog(&parsed), | 709 | "klog" => cmd_klog(&parsed), |
| @@ -713,6 +714,8 @@ write `{} <path> TIL <marker>`", | |||
| 713 | "edit" => cmd_edit(&parsed), | 714 | "edit" => cmd_edit(&parsed), |
| 714 | "bg" => cmd_bg(&parsed), | 715 | "bg" => cmd_bg(&parsed), |
| 715 | "jobs" => cmd_jobs(&parsed), | 716 | "jobs" => cmd_jobs(&parsed), |
| 717 | "msgrecv" => cmd_msgrecv(&parsed), | ||
| 718 | "send" => cmd_send(&parsed), | ||
| 716 | "sleep" => cmd_sleep(&parsed), | 719 | "sleep" => cmd_sleep(&parsed), |
| 717 | "kill" => cmd_kill(&parsed), | 720 | "kill" => cmd_kill(&parsed), |
| 718 | "demo" => cmd_demo(&parsed), | 721 | "demo" => cmd_demo(&parsed), |
| @@ -738,8 +741,16 @@ fn cmd_help(_p: &Parsed) { | |||
| 738 | date — show current date/time (RTC) | 741 | date — show current date/time (RTC) |
| 739 | pci — list PCI devices | 742 | pci — list PCI devices |
| 740 | rescan — re-scan for hotplugged/removed disks (also runs automatically) | 743 | rescan — re-scan for hotplugged/removed disks (also runs automatically) |
| 744 | mouse — live mouse events (dx/dy/buttons); ^C stops | ||
| 745 | mouse <id> — post decoded mouse events to task <id>'s queue (input routing) | ||
| 741 | bg <name> — spawn a background task (spinner, ticker, counter, boom, overflow) | 746 | bg <name> — spawn a background task (spinner, ticker, counter, boom, overflow) |
| 742 | jobs — list tasks (id, name, state, ticks) | 747 | msgrecv — spawn a task that blocks on its message queue and logs every |
| 748 | message to serial (ids from `jobs`) | ||
| 749 | send <id> <type> [args] — post a message to a task's queue: | ||
| 750 | user <n> · timer · mouse <dx> <dy> <btn> (1=L 2=R 4=M) | ||
| 751 | key <name> (enter esc tab space bksp up down left right home | ||
| 752 | end pgup pgdn del ctrl-c f1..f12, or a single char) | ||
| 753 | jobs — list tasks (id, name, state, ticks, q=pending messages) | ||
| 743 | sleep <ms> — block the shell for ms; background tasks keep running | 754 | sleep <ms> — block the shell for ms; background tasks keep running |
| 744 | kill <id> — stop a background task (id from jobs) | 755 | kill <id> — stop a background task (id from jobs) |
| 745 | demo — spawn spinner + ticker + counter | 756 | demo — spawn spinner + ticker + counter |
| @@ -1517,7 +1528,12 @@ fn rescan_all() -> (usize, usize, usize, usize, usize, usize) { | |||
| 1517 | fn report_rescan((da, dr, ma, mu, na, nr): (usize, usize, usize, usize, usize, usize)) -> bool { | 1528 | fn report_rescan((da, dr, ma, mu, na, nr): (usize, usize, usize, usize, usize, usize)) -> bool { |
| 1518 | term_println(&alloc::format!( | 1529 | term_println(&alloc::format!( |
| 1519 | "rescan: {} disk(s) added, {} removed · {} volume(s) mounted, {} unmounted · {} node(s) added, {} removed", | 1530 | "rescan: {} disk(s) added, {} removed · {} volume(s) mounted, {} unmounted · {} node(s) added, {} removed", |
| 1520 | da, dr, ma, mu, na, nr | 1531 | da, |
| 1532 | dr, | ||
| 1533 | ma, | ||
| 1534 | mu, | ||
| 1535 | na, | ||
| 1536 | nr | ||
| 1521 | )); | 1537 | )); |
| 1522 | da + dr + ma + mu + na + nr > 0 | 1538 | da + dr + ma + mu + na + nr > 0 |
| 1523 | } | 1539 | } |
| @@ -1530,6 +1546,62 @@ fn cmd_rescan(_p: &Parsed) { | |||
| 1530 | } | 1546 | } |
| 1531 | } | 1547 | } |
| 1532 | 1548 | ||
| 1549 | /// Live mouse event stream: print each decoded packet (dx/dy/buttons) as | ||
| 1550 | /// it arrives until ^C. With an optional task id, events are **posted to | ||
| 1551 | /// that task's message queue** instead of printed — the input-routing | ||
| 1552 | /// path the window manager will use (`mouse <id>` + `msgrecv` proves the | ||
| 1553 | /// whole chain: IRQ12 → ring → decoder → queue → blocked task). Polls | ||
| 1554 | /// the PS/2 ring every 10 ms (via `sleep_ms`, which only blocks the | ||
| 1555 | /// shell task) so background tasks keep running. | ||
| 1556 | fn cmd_mouse(p: &Parsed) { | ||
| 1557 | let target: Option<usize> = p.args.first().and_then(|s| s.parse::<usize>().ok()); | ||
| 1558 | if let Some(id) = target { | ||
| 1559 | if !rsnix::sched::is_alive(id) { | ||
| 1560 | term_err(&alloc::format!("mouse: no such task {}", id)); | ||
| 1561 | return; | ||
| 1562 | } | ||
| 1563 | term_println(&alloc::format!( | ||
| 1564 | "mouse: posting events to task {} — ^C to stop", | ||
| 1565 | id | ||
| 1566 | )); | ||
| 1567 | } else { | ||
| 1568 | if !rsnix::ps2::mouse_present() { | ||
| 1569 | term_err("mouse: no PS/2 mouse at boot — cat /dev/mouse still works"); | ||
| 1570 | return; | ||
| 1571 | } | ||
| 1572 | term_println("mouse: streaming events — ^C to stop"); | ||
| 1573 | } | ||
| 1574 | loop { | ||
| 1575 | if tty::interrupt_pressed() { | ||
| 1576 | term_println("mouse: stopped"); | ||
| 1577 | return; | ||
| 1578 | } | ||
| 1579 | while let Some(ev) = rsnix::ps2::try_poll_mouse() { | ||
| 1580 | match target { | ||
| 1581 | Some(id) => { | ||
| 1582 | if let Err(e) = rsnix::sched::send(id, rsnix::sched::TaskMsg::Mouse(ev)) { | ||
| 1583 | term_err(&alloc::format!("mouse: {}", e)); | ||
| 1584 | return; | ||
| 1585 | } | ||
| 1586 | } | ||
| 1587 | None => { | ||
| 1588 | let mut b = String::new(); | ||
| 1589 | b.push(if ev.left { 'L' } else { '-' }); | ||
| 1590 | b.push(if ev.right { 'R' } else { '-' }); | ||
| 1591 | b.push(if ev.middle { 'M' } else { '-' }); | ||
| 1592 | term_println(&alloc::format!( | ||
| 1593 | "dx={:+} dy={:+} buttons={}", | ||
| 1594 | ev.dx, | ||
| 1595 | ev.dy, | ||
| 1596 | b | ||
| 1597 | )); | ||
| 1598 | } | ||
| 1599 | } | ||
| 1600 | } | ||
| 1601 | rsnix::sched::sleep_ms(10); | ||
| 1602 | } | ||
| 1603 | } | ||
| 1604 | |||
| 1533 | // --------------------------------------------------------------------------- | 1605 | // --------------------------------------------------------------------------- |
| 1534 | // Multitasking: bg / jobs / sleep / kill / demo | 1606 | // Multitasking: bg / jobs / sleep / kill / demo |
| 1535 | // | 1607 | // |
| @@ -1577,6 +1649,141 @@ fn cmd_jobs(p: &Parsed) { | |||
| 1577 | pipe.end(); | 1649 | pipe.end(); |
| 1578 | } | 1650 | } |
| 1579 | 1651 | ||
| 1652 | /// Demo of the scheduler message queues: spawn a task that blocks on | ||
| 1653 | /// `recv_blocking` and logs every message it receives to serial. The | ||
| 1654 | /// shell then posts to it with `send <id> …` (id from `jobs`). | ||
| 1655 | fn cmd_msgrecv(_p: &Parsed) { | ||
| 1656 | match rsnix::sched::spawn(task_msgrecv, "msgrecv") { | ||
| 1657 | Some(id) => term_println(&alloc::format!( | ||
| 1658 | "msgrecv: task {} listening — post with: send {} <type> …", | ||
| 1659 | id, | ||
| 1660 | id | ||
| 1661 | )), | ||
| 1662 | None => term_err("msgrecv: task table full"), | ||
| 1663 | } | ||
| 1664 | } | ||
| 1665 | |||
| 1666 | /// The recv demo body: block forever, log each message. Headless by | ||
| 1667 | /// design (serial only — it never touches tty/vt/fs, per ADR 0003). | ||
| 1668 | fn task_msgrecv() { | ||
| 1669 | loop { | ||
| 1670 | let msg = rsnix::sched::recv_blocking(); | ||
| 1671 | rsnix::serial_println!( | ||
| 1672 | "msgrecv[{}]: {}", | ||
| 1673 | rsnix::sched::current(), | ||
| 1674 | rsnix::sched::msg_describe(&msg) | ||
| 1675 | ); | ||
| 1676 | } | ||
| 1677 | } | ||
| 1678 | |||
| 1679 | /// `send <id> <type> [args]` — post a TaskMsg to a task's queue. This | ||
| 1680 | /// is the shell-side knob for the primitive the window manager will use | ||
| 1681 | /// to route input; types: user <n>, timer, mouse <dx> <dy> <btn> | ||
| 1682 | /// (1=L 2=R 4=M), key <name> (enter esc tab space bksp up down left | ||
| 1683 | /// right home end pgup pgdn del ctrl-c f1..f12, or a single char). | ||
| 1684 | fn cmd_send(p: &Parsed) { | ||
| 1685 | let mut args = p.args.iter(); | ||
| 1686 | let id: usize = match args.next().and_then(|s| s.parse().ok()) { | ||
| 1687 | Some(v) => v, | ||
| 1688 | None => { | ||
| 1689 | term_err("send: needs a task id — jobs lists them, msgrecv spawns a listener"); | ||
| 1690 | return; | ||
| 1691 | } | ||
| 1692 | }; | ||
| 1693 | let kind = match args.next() { | ||
| 1694 | Some(k) => k.as_str(), | ||
| 1695 | None => { | ||
| 1696 | term_err("send: needs a type — user <n> | timer | mouse <dx> <dy> <btn> | key <name>"); | ||
| 1697 | return; | ||
| 1698 | } | ||
| 1699 | }; | ||
| 1700 | let msg = match kind { | ||
| 1701 | "user" => { | ||
| 1702 | let Some(n) = args.next().and_then(|s| s.parse().ok()) else { | ||
| 1703 | term_err("send: user needs a number"); | ||
| 1704 | return; | ||
| 1705 | }; | ||
| 1706 | rsnix::sched::TaskMsg::User(n) | ||
| 1707 | } | ||
| 1708 | "timer" => rsnix::sched::TaskMsg::Timer, | ||
| 1709 | "mouse" => { | ||
| 1710 | let dx: i16 = args.next().and_then(|s| s.parse().ok()).unwrap_or(0); | ||
| 1711 | let dy: i16 = args.next().and_then(|s| s.parse().ok()).unwrap_or(0); | ||
| 1712 | let btn: u8 = args.next().and_then(|s| s.parse().ok()).unwrap_or(0); | ||
| 1713 | rsnix::sched::TaskMsg::Mouse(rsnix::ps2::MouseEvent { | ||
| 1714 | dx, | ||
| 1715 | dy, | ||
| 1716 | left: btn & 1 != 0, | ||
| 1717 | right: btn & 2 != 0, | ||
| 1718 | middle: btn & 4 != 0, | ||
| 1719 | }) | ||
| 1720 | } | ||
| 1721 | "key" => match args.next() { | ||
| 1722 | Some(k) => match key_from_name(k.as_str()) { | ||
| 1723 | Some(ev) => rsnix::sched::TaskMsg::Key(ev), | ||
| 1724 | None => { | ||
| 1725 | term_err("send: unknown key name"); | ||
| 1726 | return; | ||
| 1727 | } | ||
| 1728 | }, | ||
| 1729 | None => { | ||
| 1730 | term_err("send: key needs a name"); | ||
| 1731 | return; | ||
| 1732 | } | ||
| 1733 | }, | ||
| 1734 | _ => { | ||
| 1735 | term_err("send: unknown type — user <n> | timer | mouse <dx> <dy> <btn> | key <name>"); | ||
| 1736 | return; | ||
| 1737 | } | ||
| 1738 | }; | ||
| 1739 | match rsnix::sched::send(id, msg) { | ||
| 1740 | Ok(()) => {} | ||
| 1741 | Err(e) => term_err(&alloc::format!("send: {}", e)), | ||
| 1742 | } | ||
| 1743 | } | ||
| 1744 | |||
| 1745 | /// Map a small key-name vocabulary to a KeyEvent for `send key …`. | ||
| 1746 | fn key_from_name(name: &str) -> Option<rsnix::ps2::KeyEvent> { | ||
| 1747 | use rsnix::ps2::KeyEvent; | ||
| 1748 | Some(match name { | ||
| 1749 | "enter" => KeyEvent::Enter, | ||
| 1750 | "esc" | "escape" => KeyEvent::Escape, | ||
| 1751 | "tab" => KeyEvent::Tab, | ||
| 1752 | "space" => KeyEvent::Char(' '), | ||
| 1753 | "bksp" | "backspace" => KeyEvent::Backspace, | ||
| 1754 | "up" => KeyEvent::ArrowUp, | ||
| 1755 | "down" => KeyEvent::ArrowDown, | ||
| 1756 | "left" => KeyEvent::ArrowLeft, | ||
| 1757 | "right" => KeyEvent::ArrowRight, | ||
| 1758 | "home" => KeyEvent::Home, | ||
| 1759 | "end" => KeyEvent::End, | ||
| 1760 | "pgup" => KeyEvent::PageUp, | ||
| 1761 | "pgdn" => KeyEvent::PageDown, | ||
| 1762 | "del" | "delete" => KeyEvent::Delete, | ||
| 1763 | "ctrl-c" => KeyEvent::Ctrl('c'), | ||
| 1764 | "ctrl-d" => KeyEvent::Ctrl('d'), | ||
| 1765 | "ctrl-s" => KeyEvent::Ctrl('s'), | ||
| 1766 | "ctrl-x" => KeyEvent::Ctrl('x'), | ||
| 1767 | _ => { | ||
| 1768 | if let Some(n) = name.strip_prefix('f') { | ||
| 1769 | if let Ok(n) = n.parse::<u8>() { | ||
| 1770 | if (1..=12).contains(&n) { | ||
| 1771 | KeyEvent::F(n) | ||
| 1772 | } else { | ||
| 1773 | return None; | ||
| 1774 | } | ||
| 1775 | } else { | ||
| 1776 | return None; | ||
| 1777 | } | ||
| 1778 | } else if name.chars().count() == 1 { | ||
| 1779 | KeyEvent::Char(name.chars().next().unwrap()) | ||
| 1780 | } else { | ||
| 1781 | return None; | ||
| 1782 | } | ||
| 1783 | } | ||
| 1784 | }) | ||
| 1785 | } | ||
| 1786 | |||
| 1580 | fn cmd_sleep(p: &Parsed) { | 1787 | fn cmd_sleep(p: &Parsed) { |
| 1581 | let ms: u64 = match p.args.first().and_then(|s| s.parse().ok()) { | 1788 | let ms: u64 = match p.args.first().and_then(|s| s.parse().ok()) { |
| 1582 | Some(m) if m > 0 => m, | 1789 | Some(m) if m > 0 => m, |
