summaryrefslogtreecommitdiffstats
path: root/shell/src
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-08-01 19:52:05 +0200
committersalmonik <salmonik@tuta.io>2026-08-01 19:52:05 +0200
commit819778f292ee4e1294603b98662e083e49e23463 (patch)
tree8df31b678432d2a89cac8e0d50f271a75a3882b5 /shell/src
parentrsnix v0.4.0: GUI input foundation — PS/2 mouse + scheduler message queues (diff)
downloadrs-dos-819778f292ee4e1294603b98662e083e49e23463.tar.gz
rs-dos-819778f292ee4e1294603b98662e083e49e23463.zip
rsnix v0.5.0: graphics layer + cursor compositor
The first visible GUI surface: real rect primitives, a single shadow→display push path, and an on-screen mouse cursor that can never race the text surface. gop: - fill_rect + push_rect(x,y,w,h): every raster op rasterises into the RAM shadow, then push_rect copies the rect to the display (write-only, still never read) and composites the cursor on top. blit_glyph and scroll shifts now route through it. - cursor overlay: 12x16 pointer (const-fn built from column lists) drawn white with a black offset outline; position in one AtomicU64 (x:high 32, y:low 32) — unpack masks the low word (a bare 'as usize' cast is a no-op on 64-bit). The cursor never enters the shadow, so erasing is just a shadow push of the old rect. - cursor_redraw after scroll shifts and once at spawn (the vt diff never re-blits idle cells). cursor task (task 2, spawned at boot when a mouse is present): - owns the PS/2 ring; polls every 10 ms, moves the cursor (clamped), tracks buttons, and forwards every event to a task's queue when 'mouse <id>' armed it (the window-manager input path). - /dev/mouse is now a position device (x/y/buttons snapshot); 'mouse' watches the cursor live (prints on change); 'cursor' prints one-shot state; 'gfx' draws a status-bar demo; 'gfx check' is an in-kernel pixel self-test (shadow readback, display readback, cursor overlay purity) — PASS/FAIL per stage. Tests (headless QEMU + monitor injection): - make test-gfx (new): all four compositor checks PASS. - make test-mouse (rewritten): cursor tracks mouse_move deltas, buttons ride along, /dev/mouse snapshot, live watch, real-event queue routing. - make test-msg: task numbering shifted (cursor is task 2; msgrecv is task 3) — updated, still PASS. - make test-disk regression re-passes. 0 build warnings.
Diffstat (limited to 'shell/src')
-rw-r--r--shell/src/lib.rs194
1 files changed, 158 insertions, 36 deletions
diff --git a/shell/src/lib.rs b/shell/src/lib.rs
index c18d319..e1d54c8 100644
--- a/shell/src/lib.rs
+++ b/shell/src/lib.rs
@@ -704,6 +704,8 @@ write `{} <path> TIL <marker>`",
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 "mouse" => cmd_mouse(&parsed),
707 "cursor" => cmd_cursor(&parsed),
708 "gfx" => cmd_gfx(&parsed),
707 "ls" => cmd_ls(&parsed), 709 "ls" => cmd_ls(&parsed),
708 "cat" => cmd_cat(&parsed), 710 "cat" => cmd_cat(&parsed),
709 "klog" => cmd_klog(&parsed), 711 "klog" => cmd_klog(&parsed),
@@ -741,8 +743,11 @@ fn cmd_help(_p: &Parsed) {
741 date — show current date/time (RTC) 743 date — show current date/time (RTC)
742 pci — list PCI devices 744 pci — list PCI devices
743 rescan — re-scan for hotplugged/removed disks (also runs automatically) 745 rescan — re-scan for hotplugged/removed disks (also runs automatically)
744 mouse — live mouse events (dx/dy/buttons); ^C stops 746 cursor — report the on-screen mouse cursor (x, y, buttons)
745 mouse <id> — post decoded mouse events to task <id>'s queue (input routing) 747 mouse — watch the cursor live (x/y/buttons); ^C stops
748 mouse <id> — route mouse events to task <id>'s queue (cursor still tracks)
749 gfx — demo: draw a status bar + RGB boxes (bottom strip)
750 gfx check — pixel self-test: shadow, display push, cursor overlay
746 bg <name> — spawn a background task (spinner, ticker, counter, boom, overflow) 751 bg <name> — spawn a background task (spinner, ticker, counter, boom, overflow)
747 msgrecv — spawn a task that blocks on its message queue and logs every 752 msgrecv — spawn a task that blocks on its message queue and logs every
748 message to serial (ids from `jobs`) 753 message to serial (ids from `jobs`)
@@ -1546,13 +1551,11 @@ fn cmd_rescan(_p: &Parsed) {
1546 } 1551 }
1547} 1552}
1548 1553
1549/// Live mouse event stream: print each decoded packet (dx/dy/buttons) as 1554/// Mouse front-end. The cursor task owns the PS/2 ring, so this
1550/// it arrives until ^C. With an optional task id, events are **posted to 1555/// command *watches the on-screen cursor* (position + buttons, printed
1551/// that task's message queue** instead of printed — the input-routing 1556/// only on change) until ^C. With a task id it also routes every event
1552/// path the window manager will use (`mouse <id>` + `msgrecv` proves the 1557/// to that task's queue — the window-manager input path — clearing the
1553/// whole chain: IRQ12 → ring → decoder → queue → blocked task). Polls 1558/// route on exit.
1554/// the PS/2 ring every 10 ms (via `sleep_ms`, which only blocks the
1555/// shell task) so background tasks keep running.
1556fn cmd_mouse(p: &Parsed) { 1559fn cmd_mouse(p: &Parsed) {
1557 let target: Option<usize> = p.args.first().and_then(|s| s.parse::<usize>().ok()); 1560 let target: Option<usize> = p.args.first().and_then(|s| s.parse::<usize>().ok());
1558 if let Some(id) = target { 1561 if let Some(id) = target {
@@ -1560,48 +1563,167 @@ fn cmd_mouse(p: &Parsed) {
1560 term_err(&alloc::format!("mouse: no such task {}", id)); 1563 term_err(&alloc::format!("mouse: no such task {}", id));
1561 return; 1564 return;
1562 } 1565 }
1566 rsnix::cursor::set_forward(Some(id));
1563 term_println(&alloc::format!( 1567 term_println(&alloc::format!(
1564 "mouse: posting events to task {} — ^C to stop", 1568 "mouse: routing events to task {} (cursor still tracks) — ^C to stop",
1565 id 1569 id
1566 )); 1570 ));
1571 } else if !rsnix::ps2::mouse_present() {
1572 term_err("mouse: no PS/2 mouse at boot — cat /dev/mouse still works");
1573 return;
1567 } else { 1574 } else {
1568 if !rsnix::ps2::mouse_present() { 1575 term_println("mouse: watching cursor — move the mouse, ^C to stop");
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 } 1576 }
1577 let mut last = (usize::MAX, usize::MAX, u8::MAX);
1574 loop { 1578 loop {
1575 if tty::interrupt_pressed() { 1579 if tty::interrupt_pressed() {
1580 rsnix::cursor::set_forward(None);
1576 term_println("mouse: stopped"); 1581 term_println("mouse: stopped");
1577 return; 1582 return;
1578 } 1583 }
1579 while let Some(ev) = rsnix::ps2::try_poll_mouse() { 1584 let (x, y, b) = (
1580 match target { 1585 rsnix::gop::cursor_pos().0,
1581 Some(id) => { 1586 rsnix::gop::cursor_pos().1,
1582 if let Err(e) = rsnix::sched::send(id, rsnix::sched::TaskMsg::Mouse(ev)) { 1587 rsnix::gop::cursor_buttons(),
1583 term_err(&alloc::format!("mouse: {}", e)); 1588 );
1584 return; 1589 if (x, y, b) != last {
1585 } 1590 last = (x, y, b);
1586 } 1591 term_println(&alloc::format!(
1587 None => { 1592 "mouse: x={} y={} buttons={}{}{}",
1588 let mut b = String::new(); 1593 x,
1589 b.push(if ev.left { 'L' } else { '-' }); 1594 y,
1590 b.push(if ev.right { 'R' } else { '-' }); 1595 if b & 1 != 0 { 'L' } else { '-' },
1591 b.push(if ev.middle { 'M' } else { '-' }); 1596 if b & 2 != 0 { 'R' } else { '-' },
1592 term_println(&alloc::format!( 1597 if b & 4 != 0 { 'M' } else { '-' }
1593 "dx={:+} dy={:+} buttons={}", 1598 ));
1594 ev.dx,
1595 ev.dy,
1596 b
1597 ));
1598 }
1599 }
1600 } 1599 }
1601 rsnix::sched::sleep_ms(10); 1600 rsnix::sched::sleep_ms(10);
1602 } 1601 }
1603} 1602}
1604 1603
1604/// One-shot cursor state (parseable; used by the test harness).
1605fn cmd_cursor(_p: &Parsed) {
1606 let (x, y) = rsnix::gop::cursor_pos();
1607 let b = rsnix::gop::cursor_buttons();
1608 term_println(&alloc::format!(
1609 "cursor: x={} y={} buttons={}{}{}",
1610 x,
1611 y,
1612 if b & 1 != 0 { 'L' } else { '-' },
1613 if b & 2 != 0 { 'R' } else { '-' },
1614 if b & 4 != 0 { 'M' } else { '-' }
1615 ));
1616}
1617
1618/// Graphics primitives demo + pixel self-test (shadow + display + cursor
1619/// overlay readbacks). `gfx` / `gfx demo` draws a status-bar scene;
1620/// `gfx check` runs the readback assertions (run after `clear` for a
1621/// predictable background).
1622fn cmd_gfx(p: &Parsed) {
1623 match p.args.first().map(|s| s.as_str()) {
1624 Some("check") => gfx_check(),
1625 _ => gfx_demo(),
1626 }
1627}
1628
1629fn gfx_demo() {
1630 let (w, h) = rsnix::gop::pixel_dims();
1631 if w == 0 {
1632 term_err("gfx: no framebuffer");
1633 return;
1634 }
1635 // Status-bar strip across the bottom, with three RGB boxes.
1636 rsnix::gop::fill_rect(0, h - 40, w, 40, rsnix::gop::rgb(0, 0, 128));
1637 rsnix::gop::fill_rect(20, h - 32, 60, 24, rsnix::gop::rgb(255, 0, 0));
1638 rsnix::gop::fill_rect(90, h - 32, 60, 24, rsnix::gop::rgb(0, 255, 0));
1639 rsnix::gop::fill_rect(160, h - 32, 60, 24, rsnix::gop::rgb(255, 255, 0));
1640 term_println(&alloc::format!(
1641 "gfx: drew status bar + RGB boxes ({}x{} px bottom strip)",
1642 w,
1643 h
1644 ));
1645}
1646
1647fn gfx_check() {
1648 let (w, _h) = rsnix::gop::pixel_dims();
1649 if w == 0 {
1650 term_err("gfx: no framebuffer");
1651 return;
1652 }
1653 let mut ok = true;
1654 let mut report = String::new();
1655
1656 // 1. fill_rect lands in the shadow.
1657 let (rx, ry, rw, rh) = (8usize, 8usize, 40usize, 16usize);
1658 let red = rsnix::gop::rgb(255, 0, 0);
1659 rsnix::gop::fill_rect(rx, ry, rw, rh, red);
1660 match rsnix::gop::shadow_pixel(rx + rw / 2, ry + rh / 2) {
1661 Some(p) if p == red => report.push_str("fill_rect/shadow: OK\n"),
1662 Some(p) => {
1663 ok = false;
1664 report.push_str(&alloc::format!("fill_rect/shadow: FAIL ({:#x})\n", p));
1665 }
1666 None => {
1667 ok = false;
1668 report.push_str("fill_rect/shadow: FAIL (no shadow)\n");
1669 }
1670 }
1671 // 2. the push reached the display.
1672 match rsnix::gop::display_pixel(rx + rw / 2, ry + rh / 2) {
1673 Some(p) if p == red => report.push_str("display push: OK\n"),
1674 Some(p) => {
1675 ok = false;
1676 report.push_str(&alloc::format!("display push: FAIL ({:#x})\n", p));
1677 }
1678 None => {
1679 ok = false;
1680 report.push_str("display push: FAIL (readback)\n");
1681 }
1682 }
1683 // 3. cursor overlay: the arrow tip is white on the display…
1684 let (cx, cy) = rsnix::gop::cursor_pos();
1685 let (dw, dh) = rsnix::gop::pixel_dims();
1686 let white = rsnix::gop::rgb(255, 255, 255);
1687 let tip = rsnix::gop::display_pixel(cx + 2, cy);
1688 let out = rsnix::gop::display_pixel(cx, cy);
1689 match (tip, out) {
1690 (Some(t), Some(o)) if t == white && o != white => report.push_str("cursor overlay: OK\n"),
1691 (Some(t), Some(o)) => {
1692 ok = false;
1693 report.push_str(&alloc::format!(
1694 "cursor overlay: FAIL (tip={:#x} outside={:#x})\n",
1695 t,
1696 o
1697 ));
1698 }
1699 _ => {
1700 ok = false;
1701 report.push_str(&alloc::format!(
1702 "cursor overlay: FAIL (readback cx={} cy={} dims={}x{})\n",
1703 cx,
1704 cy,
1705 dw,
1706 dh
1707 ));
1708 }
1709 }
1710 // …and it must NOT be in the shadow (pure overlay).
1711 match rsnix::gop::shadow_pixel(cx + 2, cy) {
1712 Some(p) if p != white => report.push_str("cursor not in shadow: OK\n"),
1713 p => {
1714 ok = false;
1715 report.push_str(&alloc::format!("cursor in shadow: FAIL ({:?})\n", p));
1716 }
1717 }
1718
1719 term_println(&report);
1720 term_println(if ok {
1721 "gfx check: PASS"
1722 } else {
1723 "gfx check: FAIL"
1724 });
1725}
1726
1605// --------------------------------------------------------------------------- 1727// ---------------------------------------------------------------------------
1606// Multitasking: bg / jobs / sleep / kill / demo 1728// Multitasking: bg / jobs / sleep / kill / demo
1607// 1729//