diff options
| author | salmonik <salmonik@tuta.io> | 2026-08-17 18:56:04 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-08-17 18:56:04 +0200 |
| commit | 5558e3509d020a0e021a77f0617044497b2f2aee (patch) | |
| tree | cd7b6cee9f7c327f9fae7ad9ac27602d1a97b222 | |
| parent | USB2/1.1 (EHCI/UHCI) drivers + real-hardware bring-up hardening (diff) | |
| download | rs-dos-5558e3509d020a0e021a77f0617044497b2f2aee.tar.gz rs-dos-5558e3509d020a0e021a77f0617044497b2f2aee.zip | |
Windowed GUI: wm module, input routing, and the paint app
- Add the wm window-manager facility per ADR 0004: overlapping
pixel-surface windows, damage-rectangle compositing, WM-drawn title
bar, focus/minimize/close, and F1-F4 keybindings
- Add gop shadow compositing primitives and vt::repaint_region so the
console stays layer 0 beneath windows
- Route mouse (absolute coords, press/release edges) and keyboard
(focus-aware input pump) to the focused window
- Add the rspaint drawing-program crate (pen, 8 colors, clear) and shell
draw/windows/restore commands
- Extend TaskMsg with AppMouse/Focus/Close and document the design in
CONTEXT.md + ADR 0004
| -rw-r--r-- | CONTEXT.md | 52 | ||||
| -rw-r--r-- | Cargo.lock | 8 | ||||
| -rw-r--r-- | Cargo.toml | 2 | ||||
| -rw-r--r-- | docs/adr/0004-windowed-gui-and-compositor.md | 78 | ||||
| -rw-r--r-- | rsdos/src/main.rs | 5 | ||||
| -rw-r--r-- | rsnix/src/cursor.rs | 19 | ||||
| -rw-r--r-- | rsnix/src/gop.rs | 103 | ||||
| -rw-r--r-- | rsnix/src/lib.rs | 9 | ||||
| -rw-r--r-- | rsnix/src/ps2.rs | 20 | ||||
| -rw-r--r-- | rsnix/src/sched.rs | 59 | ||||
| -rw-r--r-- | rsnix/src/tty.rs | 25 | ||||
| -rw-r--r-- | rsnix/src/vt.rs | 25 | ||||
| -rw-r--r-- | rsnix/src/wm.rs | 592 | ||||
| -rw-r--r-- | rspaint/Cargo.toml | 10 | ||||
| -rw-r--r-- | rspaint/src/lib.rs | 122 | ||||
| -rw-r--r-- | shell/Cargo.toml | 1 | ||||
| -rw-r--r-- | shell/src/lib.rs | 67 |
17 files changed, 1152 insertions, 45 deletions
| @@ -77,6 +77,58 @@ Two-phase model matching output: | |||
| 77 | The framebuffer address stays valid after `ExitBootServices` — the GPU's | 77 | The framebuffer address stays valid after `ExitBootServices` — the GPU's |
| 78 | linear frame buffer is system memory that we own once boot services are gone. | 78 | linear frame buffer is system memory that we own once boot services are gone. |
| 79 | 79 | ||
| 80 | ## Window | ||
| 81 | |||
| 82 | A rectangular display surface owned by exactly one task and composited | ||
| 83 | onto the 800×600 canvas by the window manager. The global console (vt) | ||
| 84 | becomes the first window, owned by the shell; further windows are spawned | ||
| 85 | per task (a terminal, a drawing canvas, …). Windows may overlap, so they | ||
| 86 | carry a z-order. Every window is a pixel surface: a terminal window is a | ||
| 87 | text grid rendered into its own pixels, a drawing canvas draws pixels | ||
| 88 | directly. The ruling constraint is "stupid simple". | ||
| 89 | |||
| 90 | ## Chrome | ||
| 91 | |||
| 92 | Window-manager-owned UI drawn on the canvas but not a window: the | ||
| 93 | background/desktop fill, per-window title bars, and (later) a | ||
| 94 | launcher/menu. Chrome is not focusable and does not take input as a | ||
| 95 | window. | ||
| 96 | |||
| 97 | ## Title bar | ||
| 98 | |||
| 99 | Per-window chrome strip at the top of a window showing the window's title | ||
| 100 | and its minimize and close buttons. Drawn by the window manager, not the | ||
| 101 | app. | ||
| 102 | |||
| 103 | ## Minimized | ||
| 104 | |||
| 105 | A window state in which the window is not rendered (hidden); the app keeps | ||
| 106 | running. The inverse of the normal visible state. | ||
| 107 | |||
| 108 | ## Close | ||
| 109 | |||
| 110 | The action that destroys a window and ends its app. | ||
| 111 | |||
| 112 | ## App | ||
| 113 | |||
| 114 | A task that owns exactly one window and receives input events through its | ||
| 115 | message queue. A GUI program is an app; there is no separate "process" | ||
| 116 | concept — the app is its task plus its window. The console is app 0 | ||
| 117 | (window 0). | ||
| 118 | |||
| 119 | ## Focus | ||
| 120 | |||
| 121 | The single window (if any) that receives keyboard and mouse input. Focus | ||
| 122 | is set by clicking a window, which also raises it; input is delivered to | ||
| 123 | the focused app's message queue. With no focused window, a click only | ||
| 124 | sets focus. | ||
| 125 | |||
| 126 | ## Keybinding | ||
| 127 | |||
| 128 | A global keyboard shortcut handled by the window manager before input is | ||
| 129 | routed to the focused window. It triggers a WM action (spawn a terminal, | ||
| 130 | focus/restore the next window, close, …) and is never delivered to an app. | ||
| 131 | |||
| 80 | ## Keyboard layout | 132 | ## Keyboard layout |
| 81 | 133 | ||
| 82 | Scancode set 1, US QWERTY layout. Hardcoded scancode→ASCII table. | 134 | Scancode set 1, US QWERTY layout. Hardcoded scancode→ASCII table. |
| @@ -22,9 +22,17 @@ name = "rsnix" | |||
| 22 | version = "0.5.10" | 22 | version = "0.5.10" |
| 23 | 23 | ||
| 24 | [[package]] | 24 | [[package]] |
| 25 | name = "rspaint" | ||
| 26 | version = "0.1.0" | ||
| 27 | dependencies = [ | ||
| 28 | "rsnix", | ||
| 29 | ] | ||
| 30 | |||
| 31 | [[package]] | ||
| 25 | name = "shell" | 32 | name = "shell" |
| 26 | version = "0.5.10" | 33 | version = "0.5.10" |
| 27 | dependencies = [ | 34 | dependencies = [ |
| 28 | "rsdosedit", | 35 | "rsdosedit", |
| 29 | "rsnix", | 36 | "rsnix", |
| 37 | "rspaint", | ||
| 30 | ] | 38 | ] |
| @@ -1,6 +1,6 @@ | |||
| 1 | [workspace] | 1 | [workspace] |
| 2 | resolver = "2" | 2 | resolver = "2" |
| 3 | members = ["rsnix", "shell", "rsdos", "rsdosedit"] | 3 | members = ["rsnix", "shell", "rsdos", "rsdosedit", "rspaint"] |
| 4 | 4 | ||
| 5 | [workspace.package] | 5 | [workspace.package] |
| 6 | version = "0.5.10" | 6 | version = "0.5.10" |
diff --git a/docs/adr/0004-windowed-gui-and-compositor.md b/docs/adr/0004-windowed-gui-and-compositor.md new file mode 100644 index 0000000..86e8b30 --- /dev/null +++ b/docs/adr/0004-windowed-gui-and-compositor.md | |||
| @@ -0,0 +1,78 @@ | |||
| 1 | # ADR 0004 — Overlapping-window GUI with damage-rectangle compositing and a shared-facility window manager | ||
| 2 | |||
| 3 | ## Status | ||
| 4 | |||
| 5 | Accepted. | ||
| 6 | |||
| 7 | ## Context | ||
| 8 | |||
| 9 | The kernel's only display surface was a single global text terminal (`vt`), | ||
| 10 | shared by the shell, klog, and fault dumps. We want a "proper GUI" with | ||
| 11 | programs (a terminal, a drawing program, …). The kernel is | ||
| 12 | TempleOS-inspired: one identity-mapped address space, no userspace | ||
| 13 | separation, preemptive round-robin tasks. The display must stay viable on | ||
| 14 | real hardware (uncached GOP framebuffers), not just fast on QEMU. The | ||
| 15 | existing cursor task already routed mouse events to a single "forward | ||
| 16 | target" in anticipation of a window manager. | ||
| 17 | |||
| 18 | ## Decision | ||
| 19 | |||
| 20 | Introduce an overlapping-window GUI: | ||
| 21 | |||
| 22 | - **Window** is the fundamental unit: a rectangular, task-owned **pixel | ||
| 23 | surface** composited onto the fixed 800×600 canvas, with z-order. | ||
| 24 | - **Every window is a raster surface.** A terminal window renders its text | ||
| 25 | grid into its own pixels; a drawing program draws pixels directly. One | ||
| 26 | content model. | ||
| 27 | - **Damage-rectangle compositing** (not full-frame redraw) to stay | ||
| 28 | real-hardware-viable. Damage granularity is **window-level first** | ||
| 29 | (repaint a dirty window's whole bounds) — revisit sub-window rectangles | ||
| 30 | if the console proves slow. | ||
| 31 | - **Repaint** = blit the dirty window, then re-overlay any occluders above | ||
| 32 | it (rectangle intersection, not subtraction). | ||
| 33 | - **The window manager is a shared facility, not a task** — in a single | ||
| 34 | address space there is no isolation to enforce, so a dedicated task only | ||
| 35 | adds round-trips. | ||
| 36 | - **App = a task owning one window** (no separate process concept). The | ||
| 37 | console is app 0. | ||
| 38 | - **Focus = click-to-focus + raise**; keyboard and mouse go to the focused | ||
| 39 | window. | ||
| 40 | - **The console is the launcher**; no taskbar/menu in the first pass. | ||
| 41 | - **Chrome** (background, per-window title bars, later a menu) is | ||
| 42 | window-manager-drawn, not windows. A title bar is a strip above a | ||
| 43 | **content-only** app buffer, with title, minimize, and close. | ||
| 44 | - **Restore** of minimized windows is via a console command | ||
| 45 | (`windows`/`restore`) plus global **keybindings** F1–F4 (spawn terminal, | ||
| 46 | focus next, minimize, close); F1–F4 are reserved and invisible to apps. | ||
| 47 | - **Window buffers** are dedicated `w×h×4` buffers allocated from the page | ||
| 48 | allocator (the same mechanism as the shadow backbuffer); full-screen | ||
| 49 | (800×600) content is allowed. | ||
| 50 | - **Drawing is explicit** — the app writes into its buffer and calls | ||
| 51 | `flush()` to request a composite; there is no automatic per-tick | ||
| 52 | compositing. | ||
| 53 | - **App event contract** — an app's queue delivers `Key`, `Mouse { x, y, | ||
| 54 | buttons, kind }` (absolute window-relative; `kind` = Move/Press/Release), | ||
| 55 | `Focus(bool)`, `Close`, `Timer`, and `User(u64)`. Mouse events outside | ||
| 56 | the focused window's content bounds are suppressed. | ||
| 57 | - **Minimum window size** is 64×64 content. | ||
| 58 | - **Window lifecycle** — a window is created by and bound to its task: it | ||
| 59 | is freed (buffer returned) when the task exits, via a reap hook. Close | ||
| 60 | is cooperative: `Close` is delivered and the app exits; three F4 presses | ||
| 61 | in quick succession force-teardown (free the window, kill the task). The | ||
| 62 | console (app 0) never closes. | ||
| 63 | - **Placement** — new windows cascade (+24px each, wrapping) and are | ||
| 64 | focused and raised on creation; the console is full-canvas at (0,0). | ||
| 65 | Windows are fixed-position and fixed-size in v1: move is the first | ||
| 66 | post-v1 feature and resize is deferred. | ||
| 67 | |||
| 68 | ## Consequences | ||
| 69 | |||
| 70 | - Positive: one content model, one compositing path, minimal concepts. | ||
| 71 | - Negative: window-level damage means a full-screen console repaints its | ||
| 72 | whole rectangle per keystroke — accepted for now, revisitable. | ||
| 73 | - Negative: windows are fixed-size initially (no resize). | ||
| 74 | - The existing `vt` dirty-cell path is demoted to a per-terminal-window | ||
| 75 | trigger; the window manager becomes the only writer of the framebuffer | ||
| 76 | (plus the cursor overlay). | ||
| 77 | - Open (not yet decided): the terminal-app design (per-instance vt + shell), | ||
| 78 | and the save/image-format + image-viewer design (both deferred). | ||
diff --git a/rsdos/src/main.rs b/rsdos/src/main.rs index 0e159e6..89df5ea 100644 --- a/rsdos/src/main.rs +++ b/rsdos/src/main.rs | |||
| @@ -418,6 +418,11 @@ pub unsafe extern "efiapi" fn efi_main(image_handle: Handle, system_table: *mut | |||
| 418 | // later, and prints the exact field values when it is. | 418 | // later, and prints the exact field values when it is. |
| 419 | rsnix::vt::debug_verify(); | 419 | rsnix::vt::debug_verify(); |
| 420 | 420 | ||
| 421 | // Windowed GUI: initialise the window manager and its input pump. | ||
| 422 | // The console (vt) stays layer 0; GUI apps are windows composited on top. | ||
| 423 | rsnix::wm::init(); | ||
| 424 | rsnix::wm::spawn_input_pump(); | ||
| 425 | |||
| 421 | shell::run(); | 426 | shell::run(); |
| 422 | } | 427 | } |
| 423 | 428 | ||
diff --git a/rsnix/src/cursor.rs b/rsnix/src/cursor.rs index 31b919e..b986b23 100644 --- a/rsnix/src/cursor.rs +++ b/rsnix/src/cursor.rs | |||
| @@ -49,14 +49,29 @@ fn forward_target() -> Option<usize> { | |||
| 49 | } | 49 | } |
| 50 | 50 | ||
| 51 | fn task_main() { | 51 | fn task_main() { |
| 52 | let mut prev_buttons: u8 = 0; | ||
| 52 | loop { | 53 | loop { |
| 53 | while let Some(ev) = crate::usb::poll_mouse() | 54 | while let Some(ev) = crate::usb::poll_mouse() |
| 54 | .or_else(crate::ehci::poll_mouse) | 55 | .or_else(crate::ehci::poll_mouse) |
| 55 | .or_else(crate::uhci::poll_mouse) | 56 | .or_else(crate::uhci::poll_mouse) |
| 56 | .or_else(crate::ps2::try_poll_mouse) | 57 | .or_else(crate::ps2::try_poll_mouse) |
| 57 | { | 58 | { |
| 58 | crate::gop::cursor_move(ev.dx, ev.dy); | 59 | let (cx, cy) = crate::gop::cursor_move(ev.dx, ev.dy); |
| 59 | crate::gop::cursor_set_buttons(btn_bits(&ev)); | 60 | let buttons = btn_bits(&ev); |
| 61 | crate::gop::cursor_set_buttons(buttons); | ||
| 62 | // Left-button edge detection → press/release. | ||
| 63 | let kind = if buttons & 1 != 0 && prev_buttons & 1 == 0 { | ||
| 64 | crate::ps2::MouseKind::Press | ||
| 65 | } else if buttons & 1 == 0 && prev_buttons & 1 != 0 { | ||
| 66 | crate::ps2::MouseKind::Release | ||
| 67 | } else { | ||
| 68 | crate::ps2::MouseKind::Move | ||
| 69 | }; | ||
| 70 | prev_buttons = buttons; | ||
| 71 | // Route to the window manager (focus + deliver to the focused | ||
| 72 | // window with window-relative coordinates). | ||
| 73 | crate::wm::route_mouse(cx as i32, cy as i32, buttons, kind); | ||
| 74 | // Legacy single-target debug routing (`mouse <id>`). | ||
| 60 | if let Some(id) = forward_target() | 75 | if let Some(id) = forward_target() |
| 61 | && crate::sched::send(id, TaskMsg::Mouse(ev)).is_err() | 76 | && crate::sched::send(id, TaskMsg::Mouse(ev)).is_err() |
| 62 | { | 77 | { |
diff --git a/rsnix/src/gop.rs b/rsnix/src/gop.rs index 676814e..deff226 100644 --- a/rsnix/src/gop.rs +++ b/rsnix/src/gop.rs | |||
| @@ -4,10 +4,10 @@ | |||
| 4 | /// module is the low-level rasteriser: glyph blits and pixel shifts on the | 4 | /// module is the low-level rasteriser: glyph blits and pixel shifts on the |
| 5 | /// linear framebuffer. | 5 | /// linear framebuffer. |
| 6 | use crate::font; | 6 | use crate::font; |
| 7 | use crate::uefi::{GopModeInfo, GraphicsOutputProtocol, EFI_SUCCESS, PIXEL_BGR, PIXEL_RGB}; | 7 | use crate::uefi::{EFI_SUCCESS, GopModeInfo, GraphicsOutputProtocol, PIXEL_BGR, PIXEL_RGB}; |
| 8 | use core::fmt; | 8 | use core::fmt; |
| 9 | use core::ptr; | 9 | use core::ptr; |
| 10 | use core::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicU8, Ordering}; | 10 | use core::sync::atomic::{AtomicBool, AtomicU8, AtomicU32, AtomicU64, Ordering}; |
| 11 | 11 | ||
| 12 | /// Framebuffer state captured before ExitBootServices. | 12 | /// Framebuffer state captured before ExitBootServices. |
| 13 | pub struct Framebuffer { | 13 | pub struct Framebuffer { |
| @@ -512,6 +512,105 @@ pub fn fill_rect(x: usize, y: usize, w: usize, h: usize, color: u32) { | |||
| 512 | push_rect(x, y, w, h); | 512 | push_rect(x, y, w, h); |
| 513 | } | 513 | } |
| 514 | 514 | ||
| 515 | /// Fill a shadow rectangle WITHOUT pushing to the display. The window | ||
| 516 | /// manager composites several surfaces into the shadow and pushes once at | ||
| 517 | /// the end. | ||
| 518 | pub(crate) fn shadow_fill_rect(x: usize, y: usize, w: usize, h: usize, color: u32) { | ||
| 519 | let Some(fb) = fb().as_ref() else { | ||
| 520 | return; | ||
| 521 | }; | ||
| 522 | if unsafe { SHADOW.is_null() } || x >= fb.width || y >= fb.height { | ||
| 523 | return; | ||
| 524 | } | ||
| 525 | let w = w.min(fb.width - x); | ||
| 526 | let h = h.min(fb.height - y); | ||
| 527 | let row_words = fb.pitch / 4; | ||
| 528 | unsafe { | ||
| 529 | for dy in 0..h { | ||
| 530 | let off = (y + dy) * row_words + x; | ||
| 531 | for dx in 0..w { | ||
| 532 | *SHADOW.add(off + dx) = color; | ||
| 533 | } | ||
| 534 | } | ||
| 535 | } | ||
| 536 | } | ||
| 537 | |||
| 538 | /// Copy a row-major `u32` source buffer (stride in pixels) into the shadow | ||
| 539 | /// WITHOUT pushing. Clipped to the framebuffer. | ||
| 540 | pub(crate) fn shadow_blit( | ||
| 541 | x: usize, | ||
| 542 | y: usize, | ||
| 543 | w: usize, | ||
| 544 | h: usize, | ||
| 545 | src: *const u32, | ||
| 546 | src_stride: usize, | ||
| 547 | ) { | ||
| 548 | let Some(fb) = fb().as_ref() else { | ||
| 549 | return; | ||
| 550 | }; | ||
| 551 | if unsafe { SHADOW.is_null() } || src.is_null() || x >= fb.width || y >= fb.height { | ||
| 552 | return; | ||
| 553 | } | ||
| 554 | let w = w.min(fb.width - x); | ||
| 555 | let h = h.min(fb.height - y); | ||
| 556 | let row_words = fb.pitch / 4; | ||
| 557 | unsafe { | ||
| 558 | for dy in 0..h { | ||
| 559 | let doff = (y + dy) * row_words + x; | ||
| 560 | let soff = dy * src_stride; | ||
| 561 | for dx in 0..w { | ||
| 562 | *SHADOW.add(doff + dx) = *src.add(soff + dx); | ||
| 563 | } | ||
| 564 | } | ||
| 565 | } | ||
| 566 | } | ||
| 567 | |||
| 568 | /// Render one 8×16 glyph into the shadow at a pixel origin WITHOUT pushing | ||
| 569 | /// (for window chrome and console repaint). Same glyph lookup as | ||
| 570 | /// `blit_glyph`, but caller-controlled push. | ||
| 571 | pub(crate) fn shadow_glyph_px(x: usize, y: usize, ch: u16, fg: (u8, u8, u8), bg: (u8, u8, u8)) { | ||
| 572 | let Some(fb) = fb().as_ref() else { | ||
| 573 | return; | ||
| 574 | }; | ||
| 575 | if unsafe { SHADOW.is_null() } { | ||
| 576 | return; | ||
| 577 | } | ||
| 578 | let glyph = font::GLYPH_8X16; | ||
| 579 | let data = if ch < 256 { | ||
| 580 | match glyph.get(ch as usize) { | ||
| 581 | Some(d) => *d, | ||
| 582 | None => return, | ||
| 583 | } | ||
| 584 | } else if let Some(bits) = unicode_glyph_bits(ch) { | ||
| 585 | bits | ||
| 586 | } else { | ||
| 587 | match glyph.get(32) { | ||
| 588 | Some(d) => *d, | ||
| 589 | None => return, | ||
| 590 | } | ||
| 591 | }; | ||
| 592 | let fgc = rgb(fg.0, fg.1, fg.2); | ||
| 593 | let bgc = rgb(bg.0, bg.1, bg.2); | ||
| 594 | let row_words = fb.pitch / 4; | ||
| 595 | unsafe { | ||
| 596 | for cy in 0..16 { | ||
| 597 | let py = y + cy; | ||
| 598 | if py >= fb.height { | ||
| 599 | break; | ||
| 600 | } | ||
| 601 | let row_bits = data[cy]; | ||
| 602 | for cx in 0..8 { | ||
| 603 | let px = x + cx; | ||
| 604 | if px >= fb.width { | ||
| 605 | break; | ||
| 606 | } | ||
| 607 | let on = row_bits & (0x80 >> cx) != 0; | ||
| 608 | *SHADOW.add(py * row_words + px) = if on { fgc } else { bgc }; | ||
| 609 | } | ||
| 610 | } | ||
| 611 | } | ||
| 612 | } | ||
| 613 | |||
| 515 | /// Print crash/fault lines directly to the framebuffer, bypassing the vt | 614 | /// Print crash/fault lines directly to the framebuffer, bypassing the vt |
| 516 | /// entirely (the vt state may be corrupt when an exception fires). | 615 | /// entirely (the vt state may be corrupt when an exception fires). |
| 517 | /// | 616 | /// |
diff --git a/rsnix/src/lib.rs b/rsnix/src/lib.rs index f5c3419..a422057 100644 --- a/rsnix/src/lib.rs +++ b/rsnix/src/lib.rs | |||
| @@ -4,6 +4,7 @@ extern crate alloc; | |||
| 4 | 4 | ||
| 5 | pub mod ahci; | 5 | pub mod ahci; |
| 6 | pub mod cursor; | 6 | pub mod cursor; |
| 7 | pub mod ehci; | ||
| 7 | pub mod fat; | 8 | pub mod fat; |
| 8 | pub mod font; | 9 | pub mod font; |
| 9 | pub mod fs; | 10 | pub mod fs; |
| @@ -14,16 +15,16 @@ pub mod klog; | |||
| 14 | pub mod memory; | 15 | pub mod memory; |
| 15 | pub mod paging; | 16 | pub mod paging; |
| 16 | pub mod pci; | 17 | pub mod pci; |
| 17 | pub mod qr; | ||
| 18 | pub mod power; | 18 | pub mod power; |
| 19 | pub mod ps2; | 19 | pub mod ps2; |
| 20 | pub mod uhci; | 20 | pub mod qr; |
| 21 | pub mod ehci; | ||
| 22 | pub mod usb; | ||
| 23 | pub mod rtc; | 21 | pub mod rtc; |
| 24 | pub mod sched; | 22 | pub mod sched; |
| 25 | pub mod serial; | 23 | pub mod serial; |
| 26 | pub mod strbuf; | 24 | pub mod strbuf; |
| 27 | pub mod tty; | 25 | pub mod tty; |
| 28 | pub mod uefi; | 26 | pub mod uefi; |
| 27 | pub mod uhci; | ||
| 28 | pub mod usb; | ||
| 29 | pub mod vt; | 29 | pub mod vt; |
| 30 | pub mod wm; | ||
diff --git a/rsnix/src/ps2.rs b/rsnix/src/ps2.rs index 965e4a0..cac476f 100644 --- a/rsnix/src/ps2.rs +++ b/rsnix/src/ps2.rs | |||
| @@ -463,6 +463,26 @@ pub struct MouseEvent { | |||
| 463 | pub middle: bool, | 463 | pub middle: bool, |
| 464 | } | 464 | } |
| 465 | 465 | ||
| 466 | /// What happened in a window-relative (GUI) mouse event. | ||
| 467 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] | ||
| 468 | pub enum MouseKind { | ||
| 469 | Move, | ||
| 470 | Press, | ||
| 471 | Release, | ||
| 472 | } | ||
| 473 | |||
| 474 | /// A mouse event delivered to a focused GUI window: absolute coordinates | ||
| 475 | /// relative to the window's content top-left, the current button bitmask, | ||
| 476 | /// and the action (move / press / release). Unlike `MouseEvent` (relative | ||
| 477 | /// deltas), this is what an app's drawing loop consumes. | ||
| 478 | #[derive(Clone, Copy, Debug)] | ||
| 479 | pub struct AppMouseEvent { | ||
| 480 | pub x: i32, | ||
| 481 | pub y: i32, | ||
| 482 | pub buttons: u8, | ||
| 483 | pub kind: MouseKind, | ||
| 484 | } | ||
| 485 | |||
| 466 | /// Packet assembly state — lives in main context (consumer side), fed by | 486 | /// Packet assembly state — lives in main context (consumer side), fed by |
| 467 | /// the raw byte ring that the IRQ12 handler fills. | 487 | /// the raw byte ring that the IRQ12 handler fills. |
| 468 | struct MouseState { | 488 | struct MouseState { |
diff --git a/rsnix/src/sched.rs b/rsnix/src/sched.rs index 259ddf6..39ecb65 100644 --- a/rsnix/src/sched.rs +++ b/rsnix/src/sched.rs | |||
| @@ -59,6 +59,9 @@ const MSG_CAP: usize = 16; | |||
| 59 | pub enum TaskMsg { | 59 | pub enum TaskMsg { |
| 60 | Key(crate::ps2::KeyEvent), | 60 | Key(crate::ps2::KeyEvent), |
| 61 | Mouse(crate::ps2::MouseEvent), | 61 | Mouse(crate::ps2::MouseEvent), |
| 62 | AppMouse(crate::ps2::AppMouseEvent), | ||
| 63 | Focus(bool), | ||
| 64 | Close, | ||
| 62 | Timer, | 65 | Timer, |
| 63 | User(u64), | 66 | User(u64), |
| 64 | } | 67 | } |
| @@ -94,6 +97,11 @@ pub fn msg_describe(msg: &TaskMsg) -> alloc::string::String { | |||
| 94 | if m.left { "L" } else { "-" }, | 97 | if m.left { "L" } else { "-" }, |
| 95 | if m.right { "R" } else { "-" } | 98 | if m.right { "R" } else { "-" } |
| 96 | ), | 99 | ), |
| 100 | TaskMsg::AppMouse(m) => { | ||
| 101 | alloc::format!("AppMouse({},{} {:?} b={:#x})", m.x, m.y, m.kind, m.buttons) | ||
| 102 | } | ||
| 103 | TaskMsg::Focus(f) => alloc::format!("Focus({})", f), | ||
| 104 | TaskMsg::Close => "Close".into(), | ||
| 97 | TaskMsg::Timer => "Timer".into(), | 105 | TaskMsg::Timer => "Timer".into(), |
| 98 | TaskMsg::User(n) => alloc::format!("User({})", n), | 106 | TaskMsg::User(n) => alloc::format!("User({})", n), |
| 99 | } | 107 | } |
| @@ -179,7 +187,10 @@ struct SpinWatch { | |||
| 179 | last_rsp: usize, | 187 | last_rsp: usize, |
| 180 | same: u32, | 188 | same: u32, |
| 181 | } | 189 | } |
| 182 | const SPIN_WATCH_INIT: SpinWatch = SpinWatch { last_rsp: 0, same: 0 }; | 190 | const SPIN_WATCH_INIT: SpinWatch = SpinWatch { |
| 191 | last_rsp: 0, | ||
| 192 | same: 0, | ||
| 193 | }; | ||
| 183 | static mut SPIN_WATCH: [SpinWatch; MAX_TASKS] = [SPIN_WATCH_INIT; MAX_TASKS]; | 194 | static mut SPIN_WATCH: [SpinWatch; MAX_TASKS] = [SPIN_WATCH_INIT; MAX_TASKS]; |
| 184 | /// A spinning background task is interrupted at roughly half the tick rate | 195 | /// A spinning background task is interrupted at roughly half the tick rate |
| 185 | /// (it alternates with idle), so 1500 hits is ~30 s of wall time. | 196 | /// (it alternates with idle), so 1500 hits is ~30 s of wall time. |
| @@ -381,11 +392,7 @@ pub fn spawn(entry: fn(), name: &str) -> Option<usize> { | |||
| 381 | }; | 392 | }; |
| 382 | let ok = spawn_at(id, entry, name); | 393 | let ok = spawn_at(id, entry, name); |
| 383 | unlock(); | 394 | unlock(); |
| 384 | if ok { | 395 | if ok { Some(id) } else { None } |
| 385 | Some(id) | ||
| 386 | } else { | ||
| 387 | None | ||
| 388 | } | ||
| 389 | } | 396 | } |
| 390 | 397 | ||
| 391 | unsafe fn reap(id: usize) { | 398 | unsafe fn reap(id: usize) { |
| @@ -698,10 +705,12 @@ pub(crate) fn fault_dump(kind: &str, cr2: u64, frame: &crate::interrupts::IntFra | |||
| 698 | } | 705 | } |
| 699 | let ra = *(fp as *const u64).add(1); // [rbp+8] = return address | 706 | let ra = *(fp as *const u64).add(1); // [rbp+8] = return address |
| 700 | if let Some((start, end)) = filter | 707 | if let Some((start, end)) = filter |
| 701 | && ra >= start as u64 && ra < end as u64 { | 708 | && ra >= start as u64 |
| 702 | bt[bn] = ra; | 709 | && ra < end as u64 |
| 703 | bn += 1; | 710 | { |
| 704 | } | 711 | bt[bn] = ra; |
| 712 | bn += 1; | ||
| 713 | } | ||
| 705 | let nfp = *(fp as *const u64); | 714 | let nfp = *(fp as *const u64); |
| 706 | if nfp <= fp { | 715 | if nfp <= fp { |
| 707 | break; // chain must grow upward | 716 | break; // chain must grow upward |
| @@ -715,19 +724,21 @@ pub(crate) fn fault_dump(kind: &str, cr2: u64, frame: &crate::interrupts::IntFra | |||
| 715 | while p + 8 <= limit && bn < bt.len() { | 724 | while p + 8 <= limit && bn < bt.len() { |
| 716 | let v = *(p as *const u64); | 725 | let v = *(p as *const u64); |
| 717 | if let Some((start, end)) = filter | 726 | if let Some((start, end)) = filter |
| 718 | && v >= start as u64 && v < end as u64 { | 727 | && v >= start as u64 |
| 719 | let mut dup = false; | 728 | && v < end as u64 |
| 720 | for i in 0..bn { | 729 | { |
| 721 | if bt[i] == v { | 730 | let mut dup = false; |
| 722 | dup = true; | 731 | for i in 0..bn { |
| 723 | break; | 732 | if bt[i] == v { |
| 724 | } | 733 | dup = true; |
| 725 | } | 734 | break; |
| 726 | if !dup { | ||
| 727 | bt[bn] = v; | ||
| 728 | bn += 1; | ||
| 729 | } | 735 | } |
| 730 | } | 736 | } |
| 737 | if !dup { | ||
| 738 | bt[bn] = v; | ||
| 739 | bn += 1; | ||
| 740 | } | ||
| 741 | } | ||
| 731 | p += 8; | 742 | p += 8; |
| 732 | } | 743 | } |
| 733 | } | 744 | } |
| @@ -842,7 +853,11 @@ pub(crate) fn fault_dump(kind: &str, cr2: u64, frame: &crate::interrupts::IntFra | |||
| 842 | &mut w, | 853 | &mut w, |
| 843 | core::format_args!( | 854 | core::format_args!( |
| 844 | "rax={:#x} rcx={:#x} rdx={:#x} rsi={:#x} rdi={:#x}", | 855 | "rax={:#x} rcx={:#x} rdx={:#x} rsi={:#x} rdi={:#x}", |
| 845 | frame.rax, frame.rcx, frame.rdx, frame.rsi, frame.rdi | 856 | frame.rax, |
| 857 | frame.rcx, | ||
| 858 | frame.rdx, | ||
| 859 | frame.rsi, | ||
| 860 | frame.rdi | ||
| 846 | ), | 861 | ), |
| 847 | ); | 862 | ); |
| 848 | w.len() | 863 | w.len() |
diff --git a/rsnix/src/tty.rs b/rsnix/src/tty.rs index 0783521..6102fb5 100644 --- a/rsnix/src/tty.rs +++ b/rsnix/src/tty.rs | |||
| @@ -41,14 +41,31 @@ static mut RAW_LEN: usize = 0; | |||
| 41 | /// Combined blocking key poll: USB HID keyboard first (polled UHCI), then | 41 | /// Combined blocking key poll: USB HID keyboard first (polled UHCI), then |
| 42 | /// the PS/2 path (which parks via `hlt` until an IRQ-fed scancode lands). | 42 | /// the PS/2 path (which parks via `hlt` until an IRQ-fed scancode lands). |
| 43 | fn poll_key() -> KeyEvent { | 43 | fn poll_key() -> KeyEvent { |
| 44 | if let Some(ev) = crate::uhci::try_poll_key() { | 44 | loop { |
| 45 | return ev; | 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); | ||
| 46 | } | 60 | } |
| 47 | ps2::poll_key() | ||
| 48 | } | 61 | } |
| 49 | 62 | ||
| 50 | /// Combined non-blocking key poll (USB first, then PS/2). | 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). | ||
| 51 | fn try_poll_key() -> Option<KeyEvent> { | 65 | fn try_poll_key() -> Option<KeyEvent> { |
| 66 | if crate::wm::focused().is_some() { | ||
| 67 | return None; | ||
| 68 | } | ||
| 52 | crate::uhci::try_poll_key().or_else(ps2::try_poll_key) | 69 | crate::uhci::try_poll_key().or_else(ps2::try_poll_key) |
| 53 | } | 70 | } |
| 54 | 71 | ||
diff --git a/rsnix/src/vt.rs b/rsnix/src/vt.rs index 9444063..c5fc5fd 100644 --- a/rsnix/src/vt.rs +++ b/rsnix/src/vt.rs | |||
| @@ -147,6 +147,31 @@ pub fn size() -> (usize, usize) { | |||
| 147 | unsafe { (ROWS, COLS) } | 147 | unsafe { (ROWS, COLS) } |
| 148 | } | 148 | } |
| 149 | 149 | ||
| 150 | /// Re-render the console's cells into the shadow for a pixel region, so the | ||
| 151 | /// window manager can restore the console (layer 0) beneath a window that | ||
| 152 | /// moved, closed, or was minimized. Does not push — the caller pushes once. | ||
| 153 | pub fn repaint_region(x: usize, y: usize, w: usize, h: usize) { | ||
| 154 | unsafe { | ||
| 155 | if CELLS.is_null() { | ||
| 156 | return; | ||
| 157 | } | ||
| 158 | let (cols, rows) = (COLS, ROWS); | ||
| 159 | let c0 = x / 8; | ||
| 160 | let c1 = (x + w).div_ceil(8).min(cols); | ||
| 161 | let r0 = y / 16; | ||
| 162 | let r1 = (y + h).div_ceil(16).min(rows); | ||
| 163 | let cells = &*CELLS; | ||
| 164 | for r in r0..r1 { | ||
| 165 | for c in c0..c1 { | ||
| 166 | let cell = cells[r * cols + c]; | ||
| 167 | let fg = PALETTE[cell.fg as usize]; | ||
| 168 | let bg = PALETTE[cell.bg as usize]; | ||
| 169 | crate::gop::shadow_glyph_px(c * 8, r * 16, cell.ch, fg, bg); | ||
| 170 | } | ||
| 171 | } | ||
| 172 | } | ||
| 173 | } | ||
| 174 | |||
| 150 | /// DEBUG (crash-family): read one cell's raw word (ch | fg<<16 | bg<<24) | 175 | /// DEBUG (crash-family): read one cell's raw word (ch | fg<<16 | bg<<24) |
| 151 | /// from the live cells grid and the rendered grid, for the given text | 176 | /// from the live cells grid and the rendered grid, for the given text |
| 152 | /// row/col. Returns (raw_cell, raw_rendered). | 177 | /// row/col. Returns (raw_cell, raw_rendered). |
diff --git a/rsnix/src/wm.rs b/rsnix/src/wm.rs new file mode 100644 index 0000000..a39702d --- /dev/null +++ b/rsnix/src/wm.rs | |||
| @@ -0,0 +1,592 @@ | |||
| 1 | //! Window manager — a shared facility (no dedicated task). | ||
| 2 | //! | ||
| 3 | //! Implements ADR 0004: overlapping windows backed by per-window pixel | ||
| 4 | //! buffers, composited onto the canvas with window-granularity damage | ||
| 5 | //! rectangles. The console (`vt`) is layer 0 and is re-rendered (never | ||
| 6 | //! re-allocated) when a region is exposed; GUI windows are composited | ||
| 7 | //! above it. Chrome (title bar) is WM-drawn; each window's buffer is its | ||
| 8 | //! content only. | ||
| 9 | //! | ||
| 10 | //! Apps call in directly (`create_window`, `flush`, …). Input routing and | ||
| 11 | //! keybindings are handled here so `handle_key` is the single entry point | ||
| 12 | //! for the GUI keyboard path. | ||
| 13 | |||
| 14 | extern crate alloc; | ||
| 15 | |||
| 16 | use core::sync::atomic::{AtomicUsize, Ordering}; | ||
| 17 | |||
| 18 | use crate::gop; | ||
| 19 | use crate::memory; | ||
| 20 | use crate::ps2::{AppMouseEvent, KeyEvent, MouseKind}; | ||
| 21 | use crate::sched::TaskMsg; | ||
| 22 | |||
| 23 | /// Height of a window's title bar in pixels (one 8×16 glyph row). | ||
| 24 | pub const TITLE_BAR_H: usize = 16; | ||
| 25 | |||
| 26 | /// Sentinel: `FOCUSED == CONSOLE` means the console (vt) has focus. | ||
| 27 | pub const CONSOLE: usize = usize::MAX; | ||
| 28 | |||
| 29 | const MAX_WINDOWS: usize = 16; | ||
| 30 | const MIN_W: usize = 64; | ||
| 31 | const MIN_H: usize = 64; | ||
| 32 | const CASCADE: usize = 24; | ||
| 33 | const TITLE_MAX: usize = 32; | ||
| 34 | |||
| 35 | // Palette (RGB; `gop::rgb` packs for the current pixel format). | ||
| 36 | const TITLE_BG: (u8, u8, u8) = (0x3a, 0x3c, 0x42); | ||
| 37 | const TITLE_BG_FOCUS: (u8, u8, u8) = (0x28, 0x4a, 0x8c); | ||
| 38 | const TITLE_FG: (u8, u8, u8) = (0xff, 0xff, 0xff); | ||
| 39 | const CLOSE_BG: (u8, u8, u8) = (0xb0, 0x30, 0x30); | ||
| 40 | const MIN_BG: (u8, u8, u8) = (0x50, 0x50, 0x50); | ||
| 41 | |||
| 42 | #[derive(Clone, Copy, PartialEq, Eq)] | ||
| 43 | enum WinState { | ||
| 44 | Visible, | ||
| 45 | Minimized, | ||
| 46 | } | ||
| 47 | |||
| 48 | #[derive(Clone, Copy)] | ||
| 49 | struct Window { | ||
| 50 | used: bool, | ||
| 51 | state: WinState, | ||
| 52 | title: [u8; TITLE_MAX], | ||
| 53 | title_len: usize, | ||
| 54 | #[allow(dead_code)] // read once input routing delivers to the owner's queue | ||
| 55 | owner: usize, | ||
| 56 | x: usize, | ||
| 57 | y: usize, | ||
| 58 | w: usize, | ||
| 59 | h: usize, | ||
| 60 | buf: *mut u32, | ||
| 61 | } | ||
| 62 | |||
| 63 | const EMPTY: Window = Window { | ||
| 64 | used: false, | ||
| 65 | state: WinState::Visible, | ||
| 66 | title: [0; TITLE_MAX], | ||
| 67 | title_len: 0, | ||
| 68 | owner: usize::MAX, | ||
| 69 | x: 0, | ||
| 70 | y: 0, | ||
| 71 | w: 0, | ||
| 72 | h: 0, | ||
| 73 | buf: core::ptr::null_mut(), | ||
| 74 | }; | ||
| 75 | |||
| 76 | static mut WINDOWS: [Window; MAX_WINDOWS] = [EMPTY; MAX_WINDOWS]; | ||
| 77 | /// Window ids in bottom→top z-order. | ||
| 78 | static mut ZORDER: [usize; MAX_WINDOWS] = [usize::MAX; MAX_WINDOWS]; | ||
| 79 | static mut ZLEN: usize = 0; | ||
| 80 | static FOCUSED: AtomicUsize = AtomicUsize::new(CONSOLE); | ||
| 81 | static mut CASCADE_X: usize = 0; | ||
| 82 | static mut CASCADE_Y: usize = 0; | ||
| 83 | |||
| 84 | /// Initialise window state. The console (vt) remains the visible layer 0; | ||
| 85 | /// no GUI windows exist yet. | ||
| 86 | pub fn init() { | ||
| 87 | unsafe { | ||
| 88 | ZLEN = 0; | ||
| 89 | CASCADE_X = 0; | ||
| 90 | CASCADE_Y = 0; | ||
| 91 | } | ||
| 92 | FOCUSED.store(CONSOLE, Ordering::Relaxed); | ||
| 93 | } | ||
| 94 | |||
| 95 | /// The focused window, or `None` if the console has focus. | ||
| 96 | pub fn focused() -> Option<usize> { | ||
| 97 | let v = FOCUSED.load(Ordering::Relaxed); | ||
| 98 | if v == CONSOLE { None } else { Some(v) } | ||
| 99 | } | ||
| 100 | |||
| 101 | fn get(id: usize) -> Option<Window> { | ||
| 102 | if id >= MAX_WINDOWS { | ||
| 103 | return None; | ||
| 104 | } | ||
| 105 | let w = unsafe { WINDOWS[id] }; | ||
| 106 | if w.used { Some(w) } else { None } | ||
| 107 | } | ||
| 108 | |||
| 109 | /// Create a window owned by the current task. `w`×`h` is the *content* | ||
| 110 | /// size (title bar excluded). Returns the window id, or `None` on a full | ||
| 111 | /// table or an allocation failure. | ||
| 112 | pub fn create_window(title: &str, w: usize, h: usize) -> Option<usize> { | ||
| 113 | let (cw, ch) = gop::pixel_dims(); | ||
| 114 | let w = w.max(MIN_W).min(cw.saturating_sub(8)); | ||
| 115 | let h = h.max(MIN_H).min(ch.saturating_sub(TITLE_BAR_H + 8)); | ||
| 116 | if w < MIN_W || h < MIN_H || cw == 0 || ch == 0 { | ||
| 117 | return None; | ||
| 118 | } | ||
| 119 | |||
| 120 | let pages = (w * h * 4).div_ceil(4096); | ||
| 121 | let buf = memory::alloc_pages(pages).map(|a| a as *mut u32)?; | ||
| 122 | // Zero-fill so a fresh canvas reads black, not garbage. | ||
| 123 | unsafe { | ||
| 124 | for i in 0..(w * h) { | ||
| 125 | *buf.add(i) = 0; | ||
| 126 | } | ||
| 127 | } | ||
| 128 | |||
| 129 | let id = unsafe { | ||
| 130 | let mut slot = None; | ||
| 131 | for i in 0..MAX_WINDOWS { | ||
| 132 | if !WINDOWS[i].used { | ||
| 133 | slot = Some(i); | ||
| 134 | break; | ||
| 135 | } | ||
| 136 | } | ||
| 137 | match slot { | ||
| 138 | Some(i) => i, | ||
| 139 | None => { | ||
| 140 | memory::free_pages(buf as usize, pages); | ||
| 141 | return None; | ||
| 142 | } | ||
| 143 | } | ||
| 144 | }; | ||
| 145 | |||
| 146 | let (x, y) = unsafe { | ||
| 147 | let mut px = CASCADE_X; | ||
| 148 | let mut py = CASCADE_Y; | ||
| 149 | if px + w > cw { | ||
| 150 | px = 0; | ||
| 151 | } | ||
| 152 | if py + TITLE_BAR_H + h > ch { | ||
| 153 | py = 0; | ||
| 154 | } | ||
| 155 | CASCADE_X = px + CASCADE; | ||
| 156 | CASCADE_Y = py + CASCADE; | ||
| 157 | (px, py) | ||
| 158 | }; | ||
| 159 | |||
| 160 | let mut tl = [0u8; TITLE_MAX]; | ||
| 161 | let n = title.len().min(TITLE_MAX); | ||
| 162 | tl[..n].copy_from_slice(&title.as_bytes()[..n]); | ||
| 163 | |||
| 164 | unsafe { | ||
| 165 | WINDOWS[id] = Window { | ||
| 166 | used: true, | ||
| 167 | state: WinState::Visible, | ||
| 168 | title: tl, | ||
| 169 | title_len: n, | ||
| 170 | owner: crate::sched::current(), | ||
| 171 | x, | ||
| 172 | y, | ||
| 173 | w, | ||
| 174 | h, | ||
| 175 | buf, | ||
| 176 | }; | ||
| 177 | ZORDER[ZLEN] = id; | ||
| 178 | ZLEN += 1; | ||
| 179 | } | ||
| 180 | focus(id); | ||
| 181 | Some(id) | ||
| 182 | } | ||
| 183 | |||
| 184 | /// Composite a window's full outer bounds (bottom→top), restoring the | ||
| 185 | /// console beneath it. Called by an app after it writes into its buffer. | ||
| 186 | pub fn flush(id: usize) { | ||
| 187 | let Some(win) = get(id) else { | ||
| 188 | return; | ||
| 189 | }; | ||
| 190 | composite_rect(win.x, win.y, win.w, win.h + TITLE_BAR_H); | ||
| 191 | } | ||
| 192 | |||
| 193 | /// Destroy a window: free its buffer, remove it, restore the exposed region. | ||
| 194 | pub fn destroy(id: usize) { | ||
| 195 | let Some(win) = get(id) else { | ||
| 196 | return; | ||
| 197 | }; | ||
| 198 | let (x, y, w, h) = (win.x, win.y, win.w, win.h + TITLE_BAR_H); | ||
| 199 | if !win.buf.is_null() { | ||
| 200 | let pages = (win.w * win.h * 4).div_ceil(4096); | ||
| 201 | memory::free_pages(win.buf as usize, pages); | ||
| 202 | } | ||
| 203 | remove_from_zorder(id); | ||
| 204 | unsafe { | ||
| 205 | WINDOWS[id] = EMPTY; | ||
| 206 | } | ||
| 207 | if focused() == Some(id) { | ||
| 208 | if unsafe { ZLEN } > 0 { | ||
| 209 | focus(unsafe { ZORDER[ZLEN - 1] }); | ||
| 210 | } else { | ||
| 211 | focus_console(); | ||
| 212 | } | ||
| 213 | } | ||
| 214 | composite_rect(x, y, w, h); | ||
| 215 | } | ||
| 216 | |||
| 217 | /// Minimize a window: it is no longer rendered (the app keeps running). | ||
| 218 | pub fn minimize(id: usize) { | ||
| 219 | let Some(win) = get(id) else { | ||
| 220 | return; | ||
| 221 | }; | ||
| 222 | if win.state == WinState::Minimized { | ||
| 223 | return; | ||
| 224 | } | ||
| 225 | let (x, y, w, h) = (win.x, win.y, win.w, win.h + TITLE_BAR_H); | ||
| 226 | unsafe { | ||
| 227 | WINDOWS[id].state = WinState::Minimized; | ||
| 228 | } | ||
| 229 | remove_from_zorder(id); | ||
| 230 | if focused() == Some(id) { | ||
| 231 | focus_console(); | ||
| 232 | } | ||
| 233 | composite_rect(x, y, w, h); | ||
| 234 | } | ||
| 235 | |||
| 236 | /// Restore a minimized window (back to the top, focused). | ||
| 237 | pub fn restore(id: usize) { | ||
| 238 | let Some(win) = get(id) else { | ||
| 239 | return; | ||
| 240 | }; | ||
| 241 | if win.state == WinState::Visible { | ||
| 242 | return; | ||
| 243 | } | ||
| 244 | unsafe { | ||
| 245 | WINDOWS[id].state = WinState::Visible; | ||
| 246 | ZORDER[ZLEN] = id; | ||
| 247 | ZLEN += 1; | ||
| 248 | } | ||
| 249 | focus(id); | ||
| 250 | composite_rect(win.x, win.y, win.w, win.h + TITLE_BAR_H); | ||
| 251 | } | ||
| 252 | |||
| 253 | /// Focus a window (raising it). Repaints the newly-focused window (so it | ||
| 254 | /// appears above former occluders) and un-focuses the previous title bar. | ||
| 255 | pub fn focus(id: usize) { | ||
| 256 | let Some(win) = get(id) else { | ||
| 257 | return; | ||
| 258 | }; | ||
| 259 | if win.state != WinState::Visible { | ||
| 260 | return; | ||
| 261 | } | ||
| 262 | // Raise to the top of the z-order. | ||
| 263 | remove_from_zorder(id); | ||
| 264 | unsafe { | ||
| 265 | ZORDER[ZLEN] = id; | ||
| 266 | ZLEN += 1; | ||
| 267 | } | ||
| 268 | let prev = focused(); | ||
| 269 | FOCUSED.store(id, Ordering::Relaxed); | ||
| 270 | if let Some(p) = prev | ||
| 271 | && p != id | ||
| 272 | && let Some(pw) = get(p) | ||
| 273 | { | ||
| 274 | let _ = crate::sched::send(pw.owner, TaskMsg::Focus(false)); | ||
| 275 | composite_rect(pw.x, pw.y, pw.w, TITLE_BAR_H); | ||
| 276 | } | ||
| 277 | let _ = crate::sched::send(win.owner, TaskMsg::Focus(true)); | ||
| 278 | composite_rect(win.x, win.y, win.w, win.h + TITLE_BAR_H); | ||
| 279 | } | ||
| 280 | |||
| 281 | /// Move focus to the console. | ||
| 282 | pub fn focus_console() { | ||
| 283 | let prev = focused(); | ||
| 284 | FOCUSED.store(CONSOLE, Ordering::Relaxed); | ||
| 285 | if let Some(p) = prev | ||
| 286 | && let Some(pw) = get(p) | ||
| 287 | { | ||
| 288 | let _ = crate::sched::send(pw.owner, TaskMsg::Focus(false)); | ||
| 289 | composite_rect(pw.x, pw.y, pw.w, TITLE_BAR_H); | ||
| 290 | } | ||
| 291 | } | ||
| 292 | |||
| 293 | /// Cycle focus: console → top window → … → bottom window → console. | ||
| 294 | pub fn focus_next() { | ||
| 295 | unsafe { | ||
| 296 | if ZLEN == 0 { | ||
| 297 | focus_console(); | ||
| 298 | return; | ||
| 299 | } | ||
| 300 | match focused() { | ||
| 301 | None => focus(ZORDER[ZLEN - 1]), | ||
| 302 | Some(cur) => { | ||
| 303 | let mut idx = usize::MAX; | ||
| 304 | for i in 0..ZLEN { | ||
| 305 | if ZORDER[i] == cur { | ||
| 306 | idx = i; | ||
| 307 | break; | ||
| 308 | } | ||
| 309 | } | ||
| 310 | if idx == usize::MAX || idx == 0 { | ||
| 311 | focus_console(); | ||
| 312 | } else { | ||
| 313 | focus(ZORDER[idx - 1]); | ||
| 314 | } | ||
| 315 | } | ||
| 316 | } | ||
| 317 | } | ||
| 318 | } | ||
| 319 | |||
| 320 | /// Handle a global WM keybinding. Returns `true` if the key was consumed | ||
| 321 | /// (F1–F4), `false` if it should be delivered to the focused app. | ||
| 322 | pub fn handle_key(ev: KeyEvent) -> bool { | ||
| 323 | match ev { | ||
| 324 | KeyEvent::F(1) => { | ||
| 325 | spawn_terminal(); | ||
| 326 | true | ||
| 327 | } | ||
| 328 | KeyEvent::F(2) => { | ||
| 329 | focus_next(); | ||
| 330 | true | ||
| 331 | } | ||
| 332 | KeyEvent::F(3) => { | ||
| 333 | if let Some(id) = focused() { | ||
| 334 | minimize(id); | ||
| 335 | } | ||
| 336 | true | ||
| 337 | } | ||
| 338 | KeyEvent::F(4) => { | ||
| 339 | if let Some(id) = focused() { | ||
| 340 | request_close(id); | ||
| 341 | } | ||
| 342 | true | ||
| 343 | } | ||
| 344 | _ => false, | ||
| 345 | } | ||
| 346 | } | ||
| 347 | |||
| 348 | fn spawn_terminal() { | ||
| 349 | // Terminal app deferred (needs per-instance vt + per-instance shell). | ||
| 350 | crate::serial_println!("wm: F1 (spawn terminal) not implemented yet"); | ||
| 351 | } | ||
| 352 | |||
| 353 | fn remove_from_zorder(id: usize) { | ||
| 354 | unsafe { | ||
| 355 | let mut j = 0; | ||
| 356 | for i in 0..ZLEN { | ||
| 357 | if ZORDER[i] != id { | ||
| 358 | ZORDER[j] = ZORDER[i]; | ||
| 359 | j += 1; | ||
| 360 | } | ||
| 361 | } | ||
| 362 | ZLEN = j; | ||
| 363 | } | ||
| 364 | } | ||
| 365 | |||
| 366 | /// Repaint a canvas region bottom→top: restore the console (layer 0), then | ||
| 367 | /// blit every visible window that intersects, then push once. | ||
| 368 | fn composite_rect(x: usize, y: usize, w: usize, h: usize) { | ||
| 369 | crate::vt::repaint_region(x, y, w, h); | ||
| 370 | unsafe { | ||
| 371 | for i in 0..ZLEN { | ||
| 372 | draw_window_intersect(ZORDER[i], x, y, w, h); | ||
| 373 | } | ||
| 374 | } | ||
| 375 | gop::push_rect(x, y, w, h); | ||
| 376 | } | ||
| 377 | |||
| 378 | /// Blit the portion of a window that intersects `(rx, ry, rw, rh)`. | ||
| 379 | fn draw_window_intersect(id: usize, rx: usize, ry: usize, rw: usize, rh: usize) { | ||
| 380 | let win = unsafe { WINDOWS[id] }; | ||
| 381 | if !win.used || win.state != WinState::Visible { | ||
| 382 | return; | ||
| 383 | } | ||
| 384 | let wx = win.x; | ||
| 385 | let wy = win.y; | ||
| 386 | let ww = win.w; | ||
| 387 | let whh = win.h + TITLE_BAR_H; | ||
| 388 | let ix = wx.max(rx); | ||
| 389 | let iy = wy.max(ry); | ||
| 390 | let ix2 = (wx + ww).min(rx + rw); | ||
| 391 | let iy2 = (wy + whh).min(ry + rh); | ||
| 392 | if ix >= ix2 || iy >= iy2 { | ||
| 393 | return; | ||
| 394 | } | ||
| 395 | // Title bar strip. | ||
| 396 | let tb0 = wy; | ||
| 397 | let tb1 = wy + TITLE_BAR_H; | ||
| 398 | if iy < tb1 && iy2 > tb0 { | ||
| 399 | draw_title_bar(id); | ||
| 400 | } | ||
| 401 | // Content strip. | ||
| 402 | let c0 = wy + TITLE_BAR_H; | ||
| 403 | let c1 = wy + TITLE_BAR_H + win.h; | ||
| 404 | if iy2 > c0 && iy < c1 { | ||
| 405 | let cy0 = iy.max(c0); | ||
| 406 | let cy1 = iy2.min(c1); | ||
| 407 | let sx = ix - wx; | ||
| 408 | let sy = cy0 - c0; | ||
| 409 | let cw = ix2 - ix; | ||
| 410 | let chh = cy1 - cy0; | ||
| 411 | let src = unsafe { win.buf.add(sy * win.w + sx) }; | ||
| 412 | gop::shadow_blit(ix, cy0, cw, chh, src, win.w); | ||
| 413 | } | ||
| 414 | } | ||
| 415 | |||
| 416 | /// Draw a window's full title bar (background, title text, min/close). | ||
| 417 | fn draw_title_bar(id: usize) { | ||
| 418 | let win = unsafe { WINDOWS[id] }; | ||
| 419 | let bg = if focused() == Some(id) { | ||
| 420 | TITLE_BG_FOCUS | ||
| 421 | } else { | ||
| 422 | TITLE_BG | ||
| 423 | }; | ||
| 424 | let bgc = gop::rgb(bg.0, bg.1, bg.2); | ||
| 425 | gop::shadow_fill_rect(win.x, win.y, win.w, TITLE_BAR_H, bgc); | ||
| 426 | |||
| 427 | // Title text, clipped short of the two buttons. | ||
| 428 | let btn_w = 16; | ||
| 429 | let cols = win.w.saturating_sub(btn_w + 4) / 8; | ||
| 430 | for (i, &b) in win.title[..win.title_len].iter().enumerate().take(cols) { | ||
| 431 | gop::shadow_glyph_px(win.x + 4 + i * 8, win.y, b as u16, TITLE_FG, bg); | ||
| 432 | } | ||
| 433 | |||
| 434 | // Close button (right edge, red) and minimize button (grey). | ||
| 435 | let close_x = win.x + win.w - 8; | ||
| 436 | let min_x = win.x + win.w - 16; | ||
| 437 | gop::shadow_fill_rect( | ||
| 438 | min_x, | ||
| 439 | win.y, | ||
| 440 | 8, | ||
| 441 | TITLE_BAR_H, | ||
| 442 | gop::rgb(MIN_BG.0, MIN_BG.1, MIN_BG.2), | ||
| 443 | ); | ||
| 444 | gop::shadow_fill_rect( | ||
| 445 | close_x, | ||
| 446 | win.y, | ||
| 447 | 8, | ||
| 448 | TITLE_BAR_H, | ||
| 449 | gop::rgb(CLOSE_BG.0, CLOSE_BG.1, CLOSE_BG.2), | ||
| 450 | ); | ||
| 451 | } | ||
| 452 | |||
| 453 | /// The task that owns a window (its input destination). | ||
| 454 | pub fn owner(id: usize) -> Option<usize> { | ||
| 455 | get(id).map(|w| w.owner) | ||
| 456 | } | ||
| 457 | |||
| 458 | /// A window's content buffer (pointer, width, height) for direct drawing. | ||
| 459 | pub fn buffer(id: usize) -> Option<(*mut u32, usize, usize)> { | ||
| 460 | get(id).map(|w| (w.buf, w.w, w.h)) | ||
| 461 | } | ||
| 462 | |||
| 463 | /// List windows as `(id, title, minimized)` for the shell `windows` command. | ||
| 464 | pub fn list() -> alloc::vec::Vec<(usize, alloc::string::String, bool)> { | ||
| 465 | let mut out = alloc::vec::Vec::new(); | ||
| 466 | for i in 0..MAX_WINDOWS { | ||
| 467 | let w = unsafe { WINDOWS[i] }; | ||
| 468 | if w.used { | ||
| 469 | let title = alloc::string::String::from( | ||
| 470 | core::str::from_utf8(&w.title[..w.title_len]).unwrap_or("?"), | ||
| 471 | ); | ||
| 472 | out.push((i, title, w.state == WinState::Minimized)); | ||
| 473 | } | ||
| 474 | } | ||
| 475 | out | ||
| 476 | } | ||
| 477 | |||
| 478 | /// Request a cooperative close: deliver `Close` to the owning app (which is | ||
| 479 | /// expected to call `destroy` and exit). | ||
| 480 | pub fn request_close(id: usize) { | ||
| 481 | if let Some(w) = get(id) { | ||
| 482 | let _ = crate::sched::send(w.owner, TaskMsg::Close); | ||
| 483 | } | ||
| 484 | } | ||
| 485 | |||
| 486 | #[derive(Clone, Copy, PartialEq, Eq)] | ||
| 487 | enum Hit { | ||
| 488 | None, | ||
| 489 | Close, | ||
| 490 | Minimize, | ||
| 491 | Title, | ||
| 492 | Content, | ||
| 493 | } | ||
| 494 | |||
| 495 | /// Topmost visible window whose *outer* bounds contain the point. | ||
| 496 | pub fn window_at(x: usize, y: usize) -> Option<usize> { | ||
| 497 | unsafe { | ||
| 498 | for i in (0..ZLEN).rev() { | ||
| 499 | let id = ZORDER[i]; | ||
| 500 | let w = WINDOWS[id]; | ||
| 501 | if !w.used || w.state != WinState::Visible { | ||
| 502 | continue; | ||
| 503 | } | ||
| 504 | if x >= w.x && x < w.x + w.w && y >= w.y && y < w.y + w.h + TITLE_BAR_H { | ||
| 505 | return Some(id); | ||
| 506 | } | ||
| 507 | } | ||
| 508 | } | ||
| 509 | None | ||
| 510 | } | ||
| 511 | |||
| 512 | fn hit_test(id: usize, x: usize, y: usize) -> Hit { | ||
| 513 | let Some(w) = get(id) else { | ||
| 514 | return Hit::None; | ||
| 515 | }; | ||
| 516 | if y >= w.y + TITLE_BAR_H { | ||
| 517 | return Hit::Content; | ||
| 518 | } | ||
| 519 | let right = w.x + w.w; | ||
| 520 | if x >= right.saturating_sub(8) { | ||
| 521 | return Hit::Close; | ||
| 522 | } | ||
| 523 | if x >= right.saturating_sub(16) { | ||
| 524 | return Hit::Minimize; | ||
| 525 | } | ||
| 526 | Hit::Title | ||
| 527 | } | ||
| 528 | |||
| 529 | /// Route one mouse event (absolute canvas coords, button bitmask, kind). | ||
| 530 | pub fn route_mouse(x: i32, y: i32, buttons: u8, kind: MouseKind) { | ||
| 531 | if kind == MouseKind::Press { | ||
| 532 | if x < 0 || y < 0 { | ||
| 533 | return; | ||
| 534 | } | ||
| 535 | let (cx, cy) = (x as usize, y as usize); | ||
| 536 | if let Some(id) = window_at(cx, cy) { | ||
| 537 | match hit_test(id, cx, cy) { | ||
| 538 | Hit::Close => request_close(id), | ||
| 539 | Hit::Minimize => minimize(id), | ||
| 540 | Hit::Title => focus(id), | ||
| 541 | Hit::Content => { | ||
| 542 | focus(id); | ||
| 543 | deliver_mouse(id, cx, cy, buttons, kind); | ||
| 544 | } | ||
| 545 | Hit::None => {} | ||
| 546 | } | ||
| 547 | } else { | ||
| 548 | focus_console(); | ||
| 549 | } | ||
| 550 | } else if let Some(id) = focused() { | ||
| 551 | deliver_mouse(id, x as usize, y as usize, buttons, kind); | ||
| 552 | } | ||
| 553 | } | ||
| 554 | |||
| 555 | fn deliver_mouse(id: usize, cx: usize, cy: usize, buttons: u8, kind: MouseKind) { | ||
| 556 | let Some(w) = get(id) else { | ||
| 557 | return; | ||
| 558 | }; | ||
| 559 | let rx = cx as i32 - w.x as i32; | ||
| 560 | let ry = cy as i32 - (w.y + TITLE_BAR_H) as i32; | ||
| 561 | if rx < 0 || ry < 0 || rx >= w.w as i32 || ry >= w.h as i32 { | ||
| 562 | return; // out of bounds — suppressed | ||
| 563 | } | ||
| 564 | let ev = AppMouseEvent { | ||
| 565 | x: rx, | ||
| 566 | y: ry, | ||
| 567 | buttons, | ||
| 568 | kind, | ||
| 569 | }; | ||
| 570 | let _ = crate::sched::send(w.owner, TaskMsg::AppMouse(ev)); | ||
| 571 | } | ||
| 572 | |||
| 573 | /// Spawn the keyboard pump task (routes keys to the focused GUI window). | ||
| 574 | pub fn spawn_input_pump() -> Option<usize> { | ||
| 575 | crate::sched::spawn(pump_main, "wm-input") | ||
| 576 | } | ||
| 577 | |||
| 578 | fn pump_main() { | ||
| 579 | loop { | ||
| 580 | if focused().is_some() { | ||
| 581 | while let Some(ev) = crate::ps2::try_poll_key().or_else(crate::uhci::try_poll_key) { | ||
| 582 | if !handle_key(ev) | ||
| 583 | && let Some(id) = focused() | ||
| 584 | && let Some(o) = owner(id) | ||
| 585 | { | ||
| 586 | let _ = crate::sched::send(o, TaskMsg::Key(ev)); | ||
| 587 | } | ||
| 588 | } | ||
| 589 | } | ||
| 590 | crate::sched::sleep_ms(5); | ||
| 591 | } | ||
| 592 | } | ||
diff --git a/rspaint/Cargo.toml b/rspaint/Cargo.toml new file mode 100644 index 0000000..66364a2 --- /dev/null +++ b/rspaint/Cargo.toml | |||
| @@ -0,0 +1,10 @@ | |||
| 1 | [package] | ||
| 2 | name = "rspaint" | ||
| 3 | version = "0.1.0" | ||
| 4 | edition = "2024" | ||
| 5 | |||
| 6 | [lints] | ||
| 7 | workspace = true | ||
| 8 | |||
| 9 | [dependencies] | ||
| 10 | rsnix = { path = "../rsnix" } | ||
diff --git a/rspaint/src/lib.rs b/rspaint/src/lib.rs new file mode 100644 index 0000000..7c2e2d2 --- /dev/null +++ b/rspaint/src/lib.rs | |||
| @@ -0,0 +1,122 @@ | |||
| 1 | #![no_std] | ||
| 2 | |||
| 3 | //! Paint — the first GUI app: a freehand pen on a raster canvas. | ||
| 4 | |||
| 5 | use rsnix::ps2::{KeyEvent, MouseKind}; | ||
| 6 | use rsnix::{sched, wm}; | ||
| 7 | |||
| 8 | const W: usize = 640; | ||
| 9 | const H: usize = 480; | ||
| 10 | |||
| 11 | /// 8-colour palette selected by keys 1–8. | ||
| 12 | const PALETTE: [(u8, u8, u8); 8] = [ | ||
| 13 | (255, 255, 255), // 1 white | ||
| 14 | (0, 0, 0), // 2 black | ||
| 15 | (170, 0, 0), // 3 red | ||
| 16 | (0, 170, 0), // 4 green | ||
| 17 | (0, 0, 170), // 5 blue | ||
| 18 | (170, 170, 0), // 6 yellow | ||
| 19 | (0, 170, 170), // 7 cyan | ||
| 20 | (170, 0, 170), // 8 magenta | ||
| 21 | ]; | ||
| 22 | |||
| 23 | /// Run the paint program as a spawned task: create a window, then loop on | ||
| 24 | /// events (pen strokes from the mouse, colour/clear from keys) until `Close`. | ||
| 25 | pub fn run() { | ||
| 26 | let Some(id) = wm::create_window("paint", W, H) else { | ||
| 27 | return; | ||
| 28 | }; | ||
| 29 | let Some((buf, _, _)) = wm::buffer(id) else { | ||
| 30 | return; | ||
| 31 | }; | ||
| 32 | |||
| 33 | let mut color: usize = 0; // white | ||
| 34 | let mut last: Option<(i32, i32)> = None; | ||
| 35 | |||
| 36 | loop { | ||
| 37 | match sched::recv_blocking() { | ||
| 38 | sched::TaskMsg::Close => break, | ||
| 39 | sched::TaskMsg::AppMouse(m) => match m.kind { | ||
| 40 | MouseKind::Press => { | ||
| 41 | plot(buf, m.x, m.y, color); | ||
| 42 | last = Some((m.x, m.y)); | ||
| 43 | wm::flush(id); | ||
| 44 | } | ||
| 45 | MouseKind::Move => { | ||
| 46 | if m.buttons & 1 != 0 { | ||
| 47 | if let Some((lx, ly)) = last { | ||
| 48 | line(buf, lx, ly, m.x, m.y, color); | ||
| 49 | wm::flush(id); | ||
| 50 | } | ||
| 51 | } | ||
| 52 | last = Some((m.x, m.y)); | ||
| 53 | } | ||
| 54 | MouseKind::Release => { | ||
| 55 | last = None; | ||
| 56 | } | ||
| 57 | }, | ||
| 58 | sched::TaskMsg::Key(k) => { | ||
| 59 | if let KeyEvent::Char(c) = k { | ||
| 60 | match c { | ||
| 61 | '1'..='8' => color = (c as u8 - b'1') as usize, | ||
| 62 | 'c' => { | ||
| 63 | fill(buf, 1); // clear to black | ||
| 64 | wm::flush(id); | ||
| 65 | } | ||
| 66 | _ => {} | ||
| 67 | } | ||
| 68 | } | ||
| 69 | } | ||
| 70 | _ => {} | ||
| 71 | } | ||
| 72 | } | ||
| 73 | wm::destroy(id); | ||
| 74 | } | ||
| 75 | |||
| 76 | fn pack(idx: usize) -> u32 { | ||
| 77 | let (r, g, b) = PALETTE[idx.min(PALETTE.len() - 1)]; | ||
| 78 | rsnix::gop::rgb(r, g, b) | ||
| 79 | } | ||
| 80 | |||
| 81 | fn plot(buf: *mut u32, x: i32, y: i32, color: usize) { | ||
| 82 | if x < 0 || y < 0 || x >= W as i32 || y >= H as i32 { | ||
| 83 | return; | ||
| 84 | } | ||
| 85 | unsafe { | ||
| 86 | *buf.add(y as usize * W + x as usize) = pack(color); | ||
| 87 | } | ||
| 88 | } | ||
| 89 | |||
| 90 | /// Bresenham line between two points. | ||
| 91 | fn line(buf: *mut u32, x0: i32, y0: i32, x1: i32, y1: i32, color: usize) { | ||
| 92 | let mut x = x0; | ||
| 93 | let mut y = y0; | ||
| 94 | let dx = (x1 - x0).abs(); | ||
| 95 | let sx = if x0 < x1 { 1 } else { -1 }; | ||
| 96 | let dy = -(y1 - y0).abs(); | ||
| 97 | let sy = if y0 < y1 { 1 } else { -1 }; | ||
| 98 | let mut err = dx + dy; | ||
| 99 | loop { | ||
| 100 | plot(buf, x, y, color); | ||
| 101 | if x == x1 && y == y1 { | ||
| 102 | break; | ||
| 103 | } | ||
| 104 | let e2 = 2 * err; | ||
| 105 | if e2 >= dy { | ||
| 106 | err += dy; | ||
| 107 | x += sx; | ||
| 108 | } | ||
| 109 | if e2 <= dx { | ||
| 110 | err += dx; | ||
| 111 | y += sy; | ||
| 112 | } | ||
| 113 | } | ||
| 114 | } | ||
| 115 | |||
| 116 | fn fill(buf: *mut u32, color: usize) { | ||
| 117 | unsafe { | ||
| 118 | for i in 0..(W * H) { | ||
| 119 | *buf.add(i) = pack(color); | ||
| 120 | } | ||
| 121 | } | ||
| 122 | } | ||
diff --git a/shell/Cargo.toml b/shell/Cargo.toml index bc35524..f95b574 100644 --- a/shell/Cargo.toml +++ b/shell/Cargo.toml | |||
| @@ -9,3 +9,4 @@ workspace = true | |||
| 9 | [dependencies] | 9 | [dependencies] |
| 10 | rsnix = { path = "../rsnix" } | 10 | rsnix = { path = "../rsnix" } |
| 11 | rsdosedit = { path = "../rsdosedit" } | 11 | rsdosedit = { path = "../rsdosedit" } |
| 12 | rspaint = { path = "../rspaint" } | ||
diff --git a/shell/src/lib.rs b/shell/src/lib.rs index bb048d8..f1addd9 100644 --- a/shell/src/lib.rs +++ b/shell/src/lib.rs | |||
| @@ -53,14 +53,15 @@ pub fn run() -> ! { | |||
| 53 | term_println("!!! PREVIOUS CRASH REPORT:"); | 53 | term_println("!!! PREVIOUS CRASH REPORT:"); |
| 54 | let mut buf = [0u8; 256]; | 54 | let mut buf = [0u8; 256]; |
| 55 | if let Ok(n) = rsnix::fs::read(h, &mut buf, 0) | 55 | if let Ok(n) = rsnix::fs::read(h, &mut buf, 0) |
| 56 | && n > 0 { | 56 | && n > 0 |
| 57 | let s = core::str::from_utf8(&buf[..n]).unwrap_or("?"); | 57 | { |
| 58 | for line in s.split('\n') { | 58 | let s = core::str::from_utf8(&buf[..n]).unwrap_or("?"); |
| 59 | if !line.is_empty() { | 59 | for line in s.split('\n') { |
| 60 | term_println(line); | 60 | if !line.is_empty() { |
| 61 | } | 61 | term_println(line); |
| 62 | } | 62 | } |
| 63 | } | 63 | } |
| 64 | } | ||
| 64 | term_println("!!! end of crash report"); | 65 | term_println("!!! end of crash report"); |
| 65 | } | 66 | } |
| 66 | 67 | ||
| @@ -739,6 +740,9 @@ write `{} <path> TIL <marker>`", | |||
| 739 | "touch" => cmd_touch(&parsed), | 740 | "touch" => cmd_touch(&parsed), |
| 740 | "rm" => cmd_rm(&parsed), | 741 | "rm" => cmd_rm(&parsed), |
| 741 | "edit" => cmd_edit(&parsed), | 742 | "edit" => cmd_edit(&parsed), |
| 743 | "draw" => cmd_draw(&parsed), | ||
| 744 | "windows" => cmd_windows(&parsed), | ||
| 745 | "restore" => cmd_restore(&parsed), | ||
| 742 | "bg" => cmd_bg(&parsed), | 746 | "bg" => cmd_bg(&parsed), |
| 743 | "jobs" => cmd_jobs(&parsed), | 747 | "jobs" => cmd_jobs(&parsed), |
| 744 | "msgrecv" => cmd_msgrecv(&parsed), | 748 | "msgrecv" => cmd_msgrecv(&parsed), |
| @@ -1387,6 +1391,40 @@ fn cmd_edit(p: &Parsed) { | |||
| 1387 | rsnix::gop::clear_screen(); | 1391 | rsnix::gop::clear_screen(); |
| 1388 | } | 1392 | } |
| 1389 | 1393 | ||
| 1394 | fn cmd_draw(_p: &Parsed) { | ||
| 1395 | match rsnix::sched::spawn(rspaint::run, "paint") { | ||
| 1396 | Some(id) => term_println(&alloc::format!( | ||
| 1397 | "draw: paint task {} — F4 or the close button to close", | ||
| 1398 | id | ||
| 1399 | )), | ||
| 1400 | None => term_err("draw: task table full"), | ||
| 1401 | } | ||
| 1402 | } | ||
| 1403 | |||
| 1404 | fn cmd_windows(_p: &Parsed) { | ||
| 1405 | let list = rsnix::wm::list(); | ||
| 1406 | if list.is_empty() { | ||
| 1407 | term_println("windows: none (console only)"); | ||
| 1408 | return; | ||
| 1409 | } | ||
| 1410 | for (id, title, minimized) in list { | ||
| 1411 | term_println(&alloc::format!( | ||
| 1412 | " {} {}{}", | ||
| 1413 | id, | ||
| 1414 | title, | ||
| 1415 | if minimized { " (minimized)" } else { "" } | ||
| 1416 | )); | ||
| 1417 | } | ||
| 1418 | } | ||
| 1419 | |||
| 1420 | fn cmd_restore(p: &Parsed) { | ||
| 1421 | let Some(id) = p.args.first().and_then(|s| s.parse::<usize>().ok()) else { | ||
| 1422 | term_err("usage: restore <id>"); | ||
| 1423 | return; | ||
| 1424 | }; | ||
| 1425 | rsnix::wm::restore(id); | ||
| 1426 | } | ||
| 1427 | |||
| 1390 | fn cmd_mem(p: &Parsed) { | 1428 | fn cmd_mem(p: &Parsed) { |
| 1391 | let (map, key) = rsnix::memory::get_map(); | 1429 | let (map, key) = rsnix::memory::get_map(); |
| 1392 | 1430 | ||
| @@ -1602,14 +1640,19 @@ fn cmd_heapdbg(_p: &Parsed) { | |||
| 1602 | let ov = h.heap_start < fb.saturating_add(fsz) && fb < h.heap_end; | 1640 | let ov = h.heap_start < fb.saturating_add(fsz) && fb < h.heap_end; |
| 1603 | term_println(&alloc::format!( | 1641 | term_println(&alloc::format!( |
| 1604 | "fb {:#x} size={:#x} overlap-heap={}", | 1642 | "fb {:#x} size={:#x} overlap-heap={}", |
| 1605 | fb, fsz, ov | 1643 | fb, |
| 1644 | fsz, | ||
| 1645 | ov | ||
| 1606 | )); | 1646 | )); |
| 1607 | } | 1647 | } |
| 1608 | let probe = h.heap_start.wrapping_add(0x443e0); // historical crash offset | 1648 | let probe = h.heap_start.wrapping_add(0x443e0); // historical crash offset |
| 1609 | match rsnix::memory::freelist_contains(probe) { | 1649 | match rsnix::memory::freelist_contains(probe) { |
| 1610 | Some((b, s, off)) => term_println(&alloc::format!( | 1650 | Some((b, s, off)) => term_println(&alloc::format!( |
| 1611 | "probe {:#x} (+0x443e0) IN free blk {:#x} size={:#x} off={:#x}", | 1651 | "probe {:#x} (+0x443e0) IN free blk {:#x} size={:#x} off={:#x}", |
| 1612 | probe, b, s, off | 1652 | probe, |
| 1653 | b, | ||
| 1654 | s, | ||
| 1655 | off | ||
| 1613 | )), | 1656 | )), |
| 1614 | None => term_println(&alloc::format!("probe {:#x} (+0x443e0) NOT free", probe)), | 1657 | None => term_println(&alloc::format!("probe {:#x} (+0x443e0) NOT free", probe)), |
| 1615 | } | 1658 | } |
| @@ -1682,7 +1725,9 @@ fn cmd_heapdbg(_p: &Parsed) { | |||
| 1682 | let (rc, rr) = rsnix::vt::cell_raw(tr, tc); | 1725 | let (rc, rr) = rsnix::vt::cell_raw(tr, tc); |
| 1683 | term_println(&alloc::format!( | 1726 | term_println(&alloc::format!( |
| 1684 | "shadow@{:x} cell={:08x} rendered={:08x}", | 1727 | "shadow@{:x} cell={:08x} rendered={:08x}", |
| 1685 | f, rc, rr | 1728 | f, |
| 1729 | rc, | ||
| 1730 | rr | ||
| 1686 | )); | 1731 | )); |
| 1687 | } | 1732 | } |
| 1688 | } | 1733 | } |
| @@ -1697,7 +1742,9 @@ fn cmd_heapdbg(_p: &Parsed) { | |||
| 1697 | match rsnix::memory::freelist_contains(b) { | 1742 | match rsnix::memory::freelist_contains(b) { |
| 1698 | Some((fb, fs, fo)) => term_println(&alloc::format!( | 1743 | Some((fb, fs, fo)) => term_println(&alloc::format!( |
| 1699 | "shadow base IN free list: blk {:#x} size={:#x} off={:#x}", | 1744 | "shadow base IN free list: blk {:#x} size={:#x} off={:#x}", |
| 1700 | fb, fs, fo | 1745 | fb, |
| 1746 | fs, | ||
| 1747 | fo | ||
| 1701 | )), | 1748 | )), |
| 1702 | None => term_println("shadow base NOT in free list"), | 1749 | None => term_println("shadow base NOT in free list"), |
| 1703 | } | 1750 | } |
