From cb17c57452ccd2ccca77e99098402d4b9f61c0a5 Mon Sep 17 00:00:00 2001 From: salmonik Date: Thu, 20 Aug 2026 13:47:10 +0200 Subject: wm: F1 spawns a windowed shell — real shell, not a duplicate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- rsnix/src/tty.rs | 62 ++++++++++++++++++++++++++++++++++++ rsnix/src/vt.rs | 96 +++++++++++++++++++++++++++++++++++++++++++++++--------- rsnix/src/wm.rs | 16 +++++++++- 3 files changed, 158 insertions(+), 16 deletions(-) (limited to 'rsnix/src') 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 { } } +/// Read one line in canonical mode from the task's message queue (for +/// windowed shells). Same semantics as `read_line` but keys arrive as +/// `TaskMsg::Key` instead of from the global PS/2 queue. Returns `None` +/// on `TaskMsg::Close` (window was closed). +pub fn read_line_from_queue() -> Option { + crate::vt::sync_scroll(); + let mut buf = strbuf::String::::new(); + loop { + match crate::sched::recv_blocking() { + crate::sched::TaskMsg::Key(ev) => match ev { + KeyEvent::Enter => { + gop::put_char('\n'); + serial::write_str("\r\n"); + return Some(LineInput::Line(buf)); + } + KeyEvent::Ctrl('c') => { + buf.clear(); + gop::put_char('^'); + gop::put_char('C'); + gop::put_char('\n'); + serial::write_str("^C\r\n"); + return Some(LineInput::Interrupted); + } + KeyEvent::Ctrl('d') => { + if buf.is_empty() { + gop::put_char('^'); + gop::put_char('D'); + gop::put_char('\n'); + serial::write_str("^D\r\n"); + return Some(LineInput::Eof); + } + } + KeyEvent::Char(c) if c.is_ascii_graphic() || c == ' ' => { + if buf.push(c).is_ok() { + gop::put_char(c); + let mut s = [0u8; 4]; + let s = c.encode_utf8(&mut s); + serial::write_str(s); + } + } + KeyEvent::Backspace => { + if buf.pop().is_some() { + gop::put_char('\x08'); + serial::write_str("\x08 \x08"); + } + } + KeyEvent::Tab => { + for _ in 0..4 { + if buf.push(' ').is_ok() { + gop::put_char(' '); + serial::write_str(" "); + } + } + } + _ => {} + }, + crate::sched::TaskMsg::Close => return None, + _ => {} + } + } +} + /// Non-blocking drain of the keyboard: true if a ^C is pending. Used by /// streaming commands (cat/hexdump) so a long dump can be interrupted. 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 { last_sync_tick: u64, dirty_all: bool, buffer: Vec, + ext_buf: Option<*mut u32>, w: usize, h: usize, window_id: Option, @@ -131,20 +132,63 @@ impl Terminal { last_sync_tick: 0, dirty_all: true, buffer: vec![0; w * h], + ext_buf: None, w, h, window_id: None, } } + /// Build a terminal that renders into an externally-owned pixel buffer + /// (e.g. one allocated by the window manager). The caller must free + /// the buffer when the window is destroyed. + pub fn from_buffer(buf: *mut u32, w: usize, h: usize) -> Terminal { + let cols = w / 8; + let rows = h / 16; + Terminal { + cells: vec![BLANK; rows * cols], + rendered: vec![BLANK; rows * cols], + rows, + cols, + cursor_r: 1, + cursor_c: 0, + last_cursor: (1, 0), + cursor_visible: true, + sgr_fg: 7, + sgr_bg: 0, + parser: Parser::Ground, + private: false, + params: [0; 4], + nparams: 0, + cur: 0, + pending_scrolls: 0, + last_sync_tick: 0, + dirty_all: true, + buffer: Vec::new(), + ext_buf: Some(buf), + w, + h, + window_id: None, + } + } + + /// Raw pointer to the pixel buffer (owned or external). + fn buf_ptr(&mut self) -> *mut u32 { + if let Some(p) = self.ext_buf { + p + } else { + self.buffer.as_mut_ptr() + } + } + /// Attach this terminal to a WM window (so `flush` composites it). pub fn set_window(&mut self, id: usize) { self.window_id = Some(id); } /// Buffer pointer and pixel dimensions (for the WM to composite). - pub fn window_params(&self) -> (*const u32, usize, usize) { - (self.buffer.as_ptr(), self.w, self.h) + pub fn window_params(&mut self) -> (*const u32, usize, usize) { + (self.buf_ptr() as *const u32, self.w, self.h) } pub fn ready(&self) -> bool { @@ -191,7 +235,7 @@ impl Terminal { } let rows = self.rows; let cols = self.cols; - gop::buffer_shift_up(self.buffer.as_mut_ptr(), self.w, self.h, n * 16); + gop::buffer_shift_up(self.buf_ptr(), self.w, self.h, n * 16); // Mirror the pushed content in RENDERED (it describes the buffer). let total = rows * cols; self.rendered.copy_within(n * cols..total, 0); @@ -206,8 +250,8 @@ impl Terminal { /// Re-blit a region of this terminal's buffer into the shadow (used by /// the window manager to restore the console layer beneath a window). - pub fn repaint_region(&self, x: usize, y: usize, w: usize, h: usize) { - let src = unsafe { self.buffer.as_ptr().add(y * self.w + x) }; + pub fn repaint_region(&mut self, x: usize, y: usize, w: usize, h: usize) { + let src = unsafe { self.buf_ptr().add(y * self.w + x) }; gop::shadow_blit(x, y, w, h, src, self.w); } @@ -537,7 +581,7 @@ impl Terminal { let fg = PALETTE[c.fg as usize]; let bg = PALETTE[c.bg as usize]; gop::buffer_glyph_px( - self.buffer.as_mut_ptr(), + self.buf_ptr(), self.w, self.h, col * 8, @@ -560,7 +604,7 @@ impl Terminal { let fg = PALETTE[c.fg as usize]; let bg = PALETTE[c.bg as usize]; gop::buffer_glyph_px( - self.buffer.as_mut_ptr(), + self.buf_ptr(), self.w, self.h, lc * 8, @@ -578,7 +622,7 @@ impl Terminal { let fg = PALETTE[c.bg as usize]; let bg = PALETTE[c.fg as usize]; gop::buffer_glyph_px( - self.buffer.as_mut_ptr(), + self.buf_ptr(), self.w, self.h, cc * 8, @@ -602,7 +646,7 @@ impl Terminal { Some(id) => crate::wm::flush(id), None => { if let Some((x, y, w, h)) = dirty { - let src = unsafe { self.buffer.as_ptr().add(y * self.w + x) }; + let src = unsafe { self.buf_ptr().add(y * self.w + x) }; gop::shadow_blit(x, y, w, h, src, self.w); gop::push_rect(x, y, w, h); } @@ -616,6 +660,20 @@ impl Terminal { // --------------------------------------------------------------------------- static mut HOME: Option = None; +/// When set, all vt output goes here instead of HOME. Used by windowed +/// shells so that gop_print!/put_char/write_str write to the active +/// terminal's pixel buffer. +static mut ACTIVE: *mut Terminal = core::ptr::null_mut(); + +fn active() -> &'static mut Terminal { + unsafe { + if !ACTIVE.is_null() { + &mut *ACTIVE + } else { + home() + } + } +} fn home() -> &'static mut Terminal { unsafe { @@ -624,6 +682,14 @@ fn home() -> &'static mut Terminal { } } +/// Set the active terminal for output (pass null to restore the home +/// terminal). +pub fn set_active(term: *mut Terminal) { + unsafe { + ACTIVE = term; + } +} + /// Initialise the home terminal over a full-screen pixel buffer. Call once /// after `gop::init()`. pub fn init() { @@ -675,27 +741,27 @@ pub fn set_cursor_visible(v: bool) { } pub fn sync_scroll() { - home().sync_scroll(); + active().sync_scroll(); } pub fn repaint_region(x: usize, y: usize, w: usize, h: usize) { - home().repaint_region(x, y, w, h); + active().repaint_region(x, y, w, h); } pub fn write_bytes(b: &[u8]) { - home().write_bytes(b); + active().write_bytes(b); } pub fn write_str(s: &str) { - home().write_str(s); + active().write_str(s); } pub fn put_char(b: u8) { - home().put_char(b); + active().put_char(b); } pub fn clear_all() { - home().clear_all(); + active().clear_all(); } // --------------------------------------------------------------------------- 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 { } } +static TERM_COUNT: core::sync::atomic::AtomicUsize = core::sync::atomic::AtomicUsize::new(0); + +/// Callback set by rsdos to spawn a terminal task. Takes (title, w, h). +pub static mut SPAWN_TERMINAL_CB: Option = None; + fn spawn_terminal() { - crate::serial_println!("wm: F1 (spawn terminal) not implemented yet"); + let Some(cb) = (unsafe { SPAWN_TERMINAL_CB }) else { + crate::serial_println!("wm: SPAWN_TERMINAL_CB not set"); + return; + }; + let n = TERM_COUNT.fetch_add(1, Ordering::Relaxed) + 1; + let title = alloc::format!("term {}", n); + let (cw, ch) = gop::pixel_dims(); + let w = 640.min(cw); + let h = 400.min(ch); + cb(&title, w, h); } fn remove_from_zorder(id: usize) { -- cgit v1.2.3