diff options
| author | salmonik <salmonik@tuta.io> | 2026-08-20 13:47:10 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-08-20 13:47:10 +0200 |
| commit | cb17c57452ccd2ccca77e99098402d4b9f61c0a5 (patch) | |
| tree | 6566e0ddaa764c6675e9a56f03772de345170992 /rsnix/src | |
| parent | wm: integrate home terminal as window 0 + fix console focus after close (diff) | |
| download | rs-dos-cb17c57452ccd2ccca77e99098402d4b9f61c0a5.tar.gz rs-dos-cb17c57452ccd2ccca77e99098402d4b9f61c0a5.zip | |
wm: F1 spawns a windowed shell — real shell, not a duplicate
Architecture:
- vt: ACTIVE terminal redirect — when set, all gop_print!/put_char/write_str
go there instead of HOME. Terminal::from_buffer() renders into a
WM-owned pixel buffer.
- tty: read_line_from_queue() — canonical-mode line input from the task's
own message queue (TaskMsg::Key). Returns None on Close.
- shell: run_windowed() — same shell as the console but reads from the
message queue. Returns on ^D or window close.
- wm: F1 calls SPAWN_TERMINAL_CB (set by rsdos). No shell logic in the WM.
- rsdos: terminal_task callback + terminal_entry task — creates a 640x400
chrome window, wires up Terminal::from_buffer, sets ACTIVE, runs
shell::run_windowed(), cleans up on exit.
- shell: 'term' debug command that invokes the callback directly and
logs each step to serial.
Diffstat (limited to 'rsnix/src')
| -rw-r--r-- | rsnix/src/tty.rs | 62 | ||||
| -rw-r--r-- | rsnix/src/vt.rs | 96 | ||||
| -rw-r--r-- | rsnix/src/wm.rs | 16 |
3 files changed, 158 insertions, 16 deletions
diff --git a/rsnix/src/tty.rs b/rsnix/src/tty.rs index 6102fb5..918f6e0 100644 --- a/rsnix/src/tty.rs +++ b/rsnix/src/tty.rs | |||
| @@ -135,6 +135,68 @@ pub fn read_line() -> LineInput { | |||
| 135 | } | 135 | } |
| 136 | } | 136 | } |
| 137 | 137 | ||
| 138 | /// Read one line in canonical mode from the task's message queue (for | ||
| 139 | /// windowed shells). Same semantics as `read_line` but keys arrive as | ||
| 140 | /// `TaskMsg::Key` instead of from the global PS/2 queue. Returns `None` | ||
| 141 | /// on `TaskMsg::Close` (window was closed). | ||
| 142 | pub fn read_line_from_queue() -> Option<LineInput> { | ||
| 143 | crate::vt::sync_scroll(); | ||
| 144 | let mut buf = strbuf::String::<LINE_BUF_SIZE>::new(); | ||
| 145 | loop { | ||
| 146 | match crate::sched::recv_blocking() { | ||
| 147 | crate::sched::TaskMsg::Key(ev) => match ev { | ||
| 148 | KeyEvent::Enter => { | ||
| 149 | gop::put_char('\n'); | ||
| 150 | serial::write_str("\r\n"); | ||
| 151 | return Some(LineInput::Line(buf)); | ||
| 152 | } | ||
| 153 | KeyEvent::Ctrl('c') => { | ||
| 154 | buf.clear(); | ||
| 155 | gop::put_char('^'); | ||
| 156 | gop::put_char('C'); | ||
| 157 | gop::put_char('\n'); | ||
| 158 | serial::write_str("^C\r\n"); | ||
| 159 | return Some(LineInput::Interrupted); | ||
| 160 | } | ||
| 161 | KeyEvent::Ctrl('d') => { | ||
| 162 | if buf.is_empty() { | ||
| 163 | gop::put_char('^'); | ||
| 164 | gop::put_char('D'); | ||
| 165 | gop::put_char('\n'); | ||
| 166 | serial::write_str("^D\r\n"); | ||
| 167 | return Some(LineInput::Eof); | ||
| 168 | } | ||
| 169 | } | ||
| 170 | KeyEvent::Char(c) if c.is_ascii_graphic() || c == ' ' => { | ||
| 171 | if buf.push(c).is_ok() { | ||
| 172 | gop::put_char(c); | ||
| 173 | let mut s = [0u8; 4]; | ||
| 174 | let s = c.encode_utf8(&mut s); | ||
| 175 | serial::write_str(s); | ||
| 176 | } | ||
| 177 | } | ||
| 178 | KeyEvent::Backspace => { | ||
| 179 | if buf.pop().is_some() { | ||
| 180 | gop::put_char('\x08'); | ||
| 181 | serial::write_str("\x08 \x08"); | ||
| 182 | } | ||
| 183 | } | ||
| 184 | KeyEvent::Tab => { | ||
| 185 | for _ in 0..4 { | ||
| 186 | if buf.push(' ').is_ok() { | ||
| 187 | gop::put_char(' '); | ||
| 188 | serial::write_str(" "); | ||
| 189 | } | ||
| 190 | } | ||
| 191 | } | ||
| 192 | _ => {} | ||
| 193 | }, | ||
| 194 | crate::sched::TaskMsg::Close => return None, | ||
| 195 | _ => {} | ||
| 196 | } | ||
| 197 | } | ||
| 198 | } | ||
| 199 | |||
| 138 | /// Non-blocking drain of the keyboard: true if a ^C is pending. Used by | 200 | /// Non-blocking drain of the keyboard: true if a ^C is pending. Used by |
| 139 | /// streaming commands (cat/hexdump) so a long dump can be interrupted. | 201 | /// streaming commands (cat/hexdump) so a long dump can be interrupted. |
| 140 | pub fn interrupt_pressed() -> bool { | 202 | pub fn interrupt_pressed() -> bool { |
diff --git a/rsnix/src/vt.rs b/rsnix/src/vt.rs index 12e2cdc..0d7cf9b 100644 --- a/rsnix/src/vt.rs +++ b/rsnix/src/vt.rs | |||
| @@ -82,6 +82,7 @@ pub struct Terminal { | |||
| 82 | last_sync_tick: u64, | 82 | last_sync_tick: u64, |
| 83 | dirty_all: bool, | 83 | dirty_all: bool, |
| 84 | buffer: Vec<u32>, | 84 | buffer: Vec<u32>, |
| 85 | ext_buf: Option<*mut u32>, | ||
| 85 | w: usize, | 86 | w: usize, |
| 86 | h: usize, | 87 | h: usize, |
| 87 | window_id: Option<usize>, | 88 | window_id: Option<usize>, |
| @@ -131,20 +132,63 @@ impl Terminal { | |||
| 131 | last_sync_tick: 0, | 132 | last_sync_tick: 0, |
| 132 | dirty_all: true, | 133 | dirty_all: true, |
| 133 | buffer: vec![0; w * h], | 134 | buffer: vec![0; w * h], |
| 135 | ext_buf: None, | ||
| 134 | w, | 136 | w, |
| 135 | h, | 137 | h, |
| 136 | window_id: None, | 138 | window_id: None, |
| 137 | } | 139 | } |
| 138 | } | 140 | } |
| 139 | 141 | ||
| 142 | /// Build a terminal that renders into an externally-owned pixel buffer | ||
| 143 | /// (e.g. one allocated by the window manager). The caller must free | ||
| 144 | /// the buffer when the window is destroyed. | ||
| 145 | pub fn from_buffer(buf: *mut u32, w: usize, h: usize) -> Terminal { | ||
| 146 | let cols = w / 8; | ||
| 147 | let rows = h / 16; | ||
| 148 | Terminal { | ||
| 149 | cells: vec![BLANK; rows * cols], | ||
| 150 | rendered: vec![BLANK; rows * cols], | ||
| 151 | rows, | ||
| 152 | cols, | ||
| 153 | cursor_r: 1, | ||
| 154 | cursor_c: 0, | ||
| 155 | last_cursor: (1, 0), | ||
| 156 | cursor_visible: true, | ||
| 157 | sgr_fg: 7, | ||
| 158 | sgr_bg: 0, | ||
| 159 | parser: Parser::Ground, | ||
| 160 | private: false, | ||
| 161 | params: [0; 4], | ||
| 162 | nparams: 0, | ||
| 163 | cur: 0, | ||
| 164 | pending_scrolls: 0, | ||
| 165 | last_sync_tick: 0, | ||
| 166 | dirty_all: true, | ||
| 167 | buffer: Vec::new(), | ||
| 168 | ext_buf: Some(buf), | ||
| 169 | w, | ||
| 170 | h, | ||
| 171 | window_id: None, | ||
| 172 | } | ||
| 173 | } | ||
| 174 | |||
| 175 | /// Raw pointer to the pixel buffer (owned or external). | ||
| 176 | fn buf_ptr(&mut self) -> *mut u32 { | ||
| 177 | if let Some(p) = self.ext_buf { | ||
| 178 | p | ||
| 179 | } else { | ||
| 180 | self.buffer.as_mut_ptr() | ||
| 181 | } | ||
| 182 | } | ||
| 183 | |||
| 140 | /// Attach this terminal to a WM window (so `flush` composites it). | 184 | /// Attach this terminal to a WM window (so `flush` composites it). |
| 141 | pub fn set_window(&mut self, id: usize) { | 185 | pub fn set_window(&mut self, id: usize) { |
| 142 | self.window_id = Some(id); | 186 | self.window_id = Some(id); |
| 143 | } | 187 | } |
| 144 | 188 | ||
| 145 | /// Buffer pointer and pixel dimensions (for the WM to composite). | 189 | /// Buffer pointer and pixel dimensions (for the WM to composite). |
| 146 | pub fn window_params(&self) -> (*const u32, usize, usize) { | 190 | pub fn window_params(&mut self) -> (*const u32, usize, usize) { |
| 147 | (self.buffer.as_ptr(), self.w, self.h) | 191 | (self.buf_ptr() as *const u32, self.w, self.h) |
| 148 | } | 192 | } |
| 149 | 193 | ||
| 150 | pub fn ready(&self) -> bool { | 194 | pub fn ready(&self) -> bool { |
| @@ -191,7 +235,7 @@ impl Terminal { | |||
| 191 | } | 235 | } |
| 192 | let rows = self.rows; | 236 | let rows = self.rows; |
| 193 | let cols = self.cols; | 237 | let cols = self.cols; |
| 194 | gop::buffer_shift_up(self.buffer.as_mut_ptr(), self.w, self.h, n * 16); | 238 | gop::buffer_shift_up(self.buf_ptr(), self.w, self.h, n * 16); |
| 195 | // Mirror the pushed content in RENDERED (it describes the buffer). | 239 | // Mirror the pushed content in RENDERED (it describes the buffer). |
| 196 | let total = rows * cols; | 240 | let total = rows * cols; |
| 197 | self.rendered.copy_within(n * cols..total, 0); | 241 | self.rendered.copy_within(n * cols..total, 0); |
| @@ -206,8 +250,8 @@ impl Terminal { | |||
| 206 | 250 | ||
| 207 | /// Re-blit a region of this terminal's buffer into the shadow (used by | 251 | /// Re-blit a region of this terminal's buffer into the shadow (used by |
| 208 | /// the window manager to restore the console layer beneath a window). | 252 | /// the window manager to restore the console layer beneath a window). |
| 209 | pub fn repaint_region(&self, x: usize, y: usize, w: usize, h: usize) { | 253 | pub fn repaint_region(&mut self, x: usize, y: usize, w: usize, h: usize) { |
| 210 | let src = unsafe { self.buffer.as_ptr().add(y * self.w + x) }; | 254 | let src = unsafe { self.buf_ptr().add(y * self.w + x) }; |
| 211 | gop::shadow_blit(x, y, w, h, src, self.w); | 255 | gop::shadow_blit(x, y, w, h, src, self.w); |
| 212 | } | 256 | } |
| 213 | 257 | ||
| @@ -537,7 +581,7 @@ impl Terminal { | |||
| 537 | let fg = PALETTE[c.fg as usize]; | 581 | let fg = PALETTE[c.fg as usize]; |
| 538 | let bg = PALETTE[c.bg as usize]; | 582 | let bg = PALETTE[c.bg as usize]; |
| 539 | gop::buffer_glyph_px( | 583 | gop::buffer_glyph_px( |
| 540 | self.buffer.as_mut_ptr(), | 584 | self.buf_ptr(), |
| 541 | self.w, | 585 | self.w, |
| 542 | self.h, | 586 | self.h, |
| 543 | col * 8, | 587 | col * 8, |
| @@ -560,7 +604,7 @@ impl Terminal { | |||
| 560 | let fg = PALETTE[c.fg as usize]; | 604 | let fg = PALETTE[c.fg as usize]; |
| 561 | let bg = PALETTE[c.bg as usize]; | 605 | let bg = PALETTE[c.bg as usize]; |
| 562 | gop::buffer_glyph_px( | 606 | gop::buffer_glyph_px( |
| 563 | self.buffer.as_mut_ptr(), | 607 | self.buf_ptr(), |
| 564 | self.w, | 608 | self.w, |
| 565 | self.h, | 609 | self.h, |
| 566 | lc * 8, | 610 | lc * 8, |
| @@ -578,7 +622,7 @@ impl Terminal { | |||
| 578 | let fg = PALETTE[c.bg as usize]; | 622 | let fg = PALETTE[c.bg as usize]; |
| 579 | let bg = PALETTE[c.fg as usize]; | 623 | let bg = PALETTE[c.fg as usize]; |
| 580 | gop::buffer_glyph_px( | 624 | gop::buffer_glyph_px( |
| 581 | self.buffer.as_mut_ptr(), | 625 | self.buf_ptr(), |
| 582 | self.w, | 626 | self.w, |
| 583 | self.h, | 627 | self.h, |
| 584 | cc * 8, | 628 | cc * 8, |
| @@ -602,7 +646,7 @@ impl Terminal { | |||
| 602 | Some(id) => crate::wm::flush(id), | 646 | Some(id) => crate::wm::flush(id), |
| 603 | None => { | 647 | None => { |
| 604 | if let Some((x, y, w, h)) = dirty { | 648 | if let Some((x, y, w, h)) = dirty { |
| 605 | let src = unsafe { self.buffer.as_ptr().add(y * self.w + x) }; | 649 | let src = unsafe { self.buf_ptr().add(y * self.w + x) }; |
| 606 | gop::shadow_blit(x, y, w, h, src, self.w); | 650 | gop::shadow_blit(x, y, w, h, src, self.w); |
| 607 | gop::push_rect(x, y, w, h); | 651 | gop::push_rect(x, y, w, h); |
| 608 | } | 652 | } |
| @@ -616,6 +660,20 @@ impl Terminal { | |||
| 616 | // --------------------------------------------------------------------------- | 660 | // --------------------------------------------------------------------------- |
| 617 | 661 | ||
| 618 | static mut HOME: Option<Terminal> = None; | 662 | static mut HOME: Option<Terminal> = None; |
| 663 | /// When set, all vt output goes here instead of HOME. Used by windowed | ||
| 664 | /// shells so that gop_print!/put_char/write_str write to the active | ||
| 665 | /// terminal's pixel buffer. | ||
| 666 | static mut ACTIVE: *mut Terminal = core::ptr::null_mut(); | ||
| 667 | |||
| 668 | fn active() -> &'static mut Terminal { | ||
| 669 | unsafe { | ||
| 670 | if !ACTIVE.is_null() { | ||
| 671 | &mut *ACTIVE | ||
| 672 | } else { | ||
| 673 | home() | ||
| 674 | } | ||
| 675 | } | ||
| 676 | } | ||
| 619 | 677 | ||
| 620 | fn home() -> &'static mut Terminal { | 678 | fn home() -> &'static mut Terminal { |
| 621 | unsafe { | 679 | unsafe { |
| @@ -624,6 +682,14 @@ fn home() -> &'static mut Terminal { | |||
| 624 | } | 682 | } |
| 625 | } | 683 | } |
| 626 | 684 | ||
| 685 | /// Set the active terminal for output (pass null to restore the home | ||
| 686 | /// terminal). | ||
| 687 | pub fn set_active(term: *mut Terminal) { | ||
| 688 | unsafe { | ||
| 689 | ACTIVE = term; | ||
| 690 | } | ||
| 691 | } | ||
| 692 | |||
| 627 | /// Initialise the home terminal over a full-screen pixel buffer. Call once | 693 | /// Initialise the home terminal over a full-screen pixel buffer. Call once |
| 628 | /// after `gop::init()`. | 694 | /// after `gop::init()`. |
| 629 | pub fn init() { | 695 | pub fn init() { |
| @@ -675,27 +741,27 @@ pub fn set_cursor_visible(v: bool) { | |||
| 675 | } | 741 | } |
| 676 | 742 | ||
| 677 | pub fn sync_scroll() { | 743 | pub fn sync_scroll() { |
| 678 | home().sync_scroll(); | 744 | active().sync_scroll(); |
| 679 | } | 745 | } |
| 680 | 746 | ||
| 681 | pub fn repaint_region(x: usize, y: usize, w: usize, h: usize) { | 747 | pub fn repaint_region(x: usize, y: usize, w: usize, h: usize) { |
| 682 | home().repaint_region(x, y, w, h); | 748 | active().repaint_region(x, y, w, h); |
| 683 | } | 749 | } |
| 684 | 750 | ||
| 685 | pub fn write_bytes(b: &[u8]) { | 751 | pub fn write_bytes(b: &[u8]) { |
| 686 | home().write_bytes(b); | 752 | active().write_bytes(b); |
| 687 | } | 753 | } |
| 688 | 754 | ||
| 689 | pub fn write_str(s: &str) { | 755 | pub fn write_str(s: &str) { |
| 690 | home().write_str(s); | 756 | active().write_str(s); |
| 691 | } | 757 | } |
| 692 | 758 | ||
| 693 | pub fn put_char(b: u8) { | 759 | pub fn put_char(b: u8) { |
| 694 | home().put_char(b); | 760 | active().put_char(b); |
| 695 | } | 761 | } |
| 696 | 762 | ||
| 697 | pub fn clear_all() { | 763 | pub fn clear_all() { |
| 698 | home().clear_all(); | 764 | active().clear_all(); |
| 699 | } | 765 | } |
| 700 | 766 | ||
| 701 | // --------------------------------------------------------------------------- | 767 | // --------------------------------------------------------------------------- |
diff --git a/rsnix/src/wm.rs b/rsnix/src/wm.rs index 2641b69..05781e8 100644 --- a/rsnix/src/wm.rs +++ b/rsnix/src/wm.rs | |||
| @@ -394,8 +394,22 @@ pub fn handle_key(ev: KeyEvent) -> bool { | |||
| 394 | } | 394 | } |
| 395 | } | 395 | } |
| 396 | 396 | ||
| 397 | static TERM_COUNT: core::sync::atomic::AtomicUsize = core::sync::atomic::AtomicUsize::new(0); | ||
| 398 | |||
| 399 | /// Callback set by rsdos to spawn a terminal task. Takes (title, w, h). | ||
| 400 | pub static mut SPAWN_TERMINAL_CB: Option<fn(&str, usize, usize)> = None; | ||
| 401 | |||
| 397 | fn spawn_terminal() { | 402 | fn spawn_terminal() { |
| 398 | crate::serial_println!("wm: F1 (spawn terminal) not implemented yet"); | 403 | let Some(cb) = (unsafe { SPAWN_TERMINAL_CB }) else { |
| 404 | crate::serial_println!("wm: SPAWN_TERMINAL_CB not set"); | ||
| 405 | return; | ||
| 406 | }; | ||
| 407 | let n = TERM_COUNT.fetch_add(1, Ordering::Relaxed) + 1; | ||
| 408 | let title = alloc::format!("term {}", n); | ||
| 409 | let (cw, ch) = gop::pixel_dims(); | ||
| 410 | let w = 640.min(cw); | ||
| 411 | let h = 400.min(ch); | ||
| 412 | cb(&title, w, h); | ||
| 399 | } | 413 | } |
| 400 | 414 | ||
| 401 | fn remove_from_zorder(id: usize) { | 415 | fn remove_from_zorder(id: usize) { |
