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 | |
| 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.
| -rw-r--r-- | rsdos/src/main.rs | 69 | ||||
| -rw-r--r-- | rsnix/src/tty.rs | 62 | ||||
| -rw-r--r-- | rsnix/src/vt.rs | 96 | ||||
| -rw-r--r-- | rsnix/src/wm.rs | 16 | ||||
| -rw-r--r-- | shell/src/lib.rs | 46 |
5 files changed, 273 insertions, 16 deletions
diff --git a/rsdos/src/main.rs b/rsdos/src/main.rs index 6d2fa4a..e5fb6dd 100644 --- a/rsdos/src/main.rs +++ b/rsdos/src/main.rs | |||
| @@ -420,10 +420,79 @@ pub unsafe extern "efiapi" fn efi_main(image_handle: Handle, system_table: *mut | |||
| 420 | t.set_window(id); | 420 | t.set_window(id); |
| 421 | } | 421 | } |
| 422 | rsnix::wm::spawn_input_pump(); | 422 | rsnix::wm::spawn_input_pump(); |
| 423 | // Register the terminal-spawn callback so F1 in the WM creates a | ||
| 424 | // windowed shell. The task lives here in rsdos (which depends on | ||
| 425 | // both rsnix and shell). | ||
| 426 | unsafe { | ||
| 427 | rsnix::wm::SPAWN_TERMINAL_CB = Some(terminal_task); | ||
| 428 | } | ||
| 423 | 429 | ||
| 424 | shell::run(); | 430 | shell::run(); |
| 425 | } | 431 | } |
| 426 | 432 | ||
| 433 | /// Callback registered with the WM for F1. Stores the spawn arguments | ||
| 434 | /// and spawns the terminal task (can't block the input pump). | ||
| 435 | static mut TERM_TITLE_BUF: [u8; 64] = [0u8; 64]; | ||
| 436 | static mut TERM_TITLE_LEN: usize = 0; | ||
| 437 | static mut TERM_W: usize = 0; | ||
| 438 | static mut TERM_H: usize = 0; | ||
| 439 | |||
| 440 | fn terminal_task(title: &str, w: usize, h: usize) { | ||
| 441 | rsnix::serial_println!("term: callback invoked title='{}' {}x{}", title, w, h); | ||
| 442 | // Copy title into static buffer so the spawned task can see it. | ||
| 443 | let bytes = title.as_bytes(); | ||
| 444 | let n = bytes.len().min(63); | ||
| 445 | unsafe { | ||
| 446 | TERM_TITLE_BUF[..n].copy_from_slice(&bytes[..n]); | ||
| 447 | TERM_TITLE_BUF[n] = 0; | ||
| 448 | TERM_TITLE_LEN = n; | ||
| 449 | TERM_W = w; | ||
| 450 | TERM_H = h; | ||
| 451 | } | ||
| 452 | match rsnix::sched::spawn(terminal_entry, "term") { | ||
| 453 | Some(id) => rsnix::serial_println!("term: spawned task {}", id), | ||
| 454 | None => rsnix::serial_println!("term: spawn FAILED"), | ||
| 455 | } | ||
| 456 | } | ||
| 457 | |||
| 458 | /// The actual terminal task — reads args from statics, creates a window, | ||
| 459 | /// and runs the shell. | ||
| 460 | fn terminal_entry() { | ||
| 461 | use rsnix::vt::Terminal; | ||
| 462 | let (w, h) = unsafe { (TERM_W, TERM_H) }; | ||
| 463 | let title = unsafe { core::str::from_utf8_unchecked(&TERM_TITLE_BUF[..TERM_TITLE_LEN]) }; | ||
| 464 | rsnix::serial_println!("term: entry title='{}' {}x{}", title, w, h); | ||
| 465 | let Some(win_id) = rsnix::wm::create_window(title, w, h) else { | ||
| 466 | rsnix::serial_println!("term: create_window FAILED"); | ||
| 467 | return; | ||
| 468 | }; | ||
| 469 | rsnix::serial_println!("term: win_id={}", win_id); | ||
| 470 | let Some((buf, ww, hh)) = rsnix::wm::buffer(win_id) else { | ||
| 471 | rsnix::serial_println!("term: buffer FAILED"); | ||
| 472 | rsnix::wm::destroy(win_id); | ||
| 473 | return; | ||
| 474 | }; | ||
| 475 | rsnix::serial_println!("term: buffer {:?} {}x{}", buf as *mut u32, ww, hh); | ||
| 476 | |||
| 477 | // Build a Terminal that renders into the WM-allocated buffer. | ||
| 478 | let mut term = Terminal::from_buffer(buf as *mut u32, ww, hh); | ||
| 479 | term.set_window(win_id); | ||
| 480 | rsnix::serial_println!("term: terminal created, entering shell"); | ||
| 481 | |||
| 482 | // Redirect all vt output (gop_print!, put_char, …) to this terminal. | ||
| 483 | rsnix::vt::set_active(&mut term as *mut Terminal); | ||
| 484 | |||
| 485 | // Run the shell — it owns input, line editing, and command execution. | ||
| 486 | shell::run_windowed(); | ||
| 487 | |||
| 488 | // Shell exited (^D or window close). Clean up. | ||
| 489 | rsnix::serial_println!("term: shell exited, cleaning up"); | ||
| 490 | rsnix::vt::set_active(core::ptr::null_mut()); | ||
| 491 | drop(term); | ||
| 492 | rsnix::wm::destroy(win_id); | ||
| 493 | rsnix::serial_println!("term: done"); | ||
| 494 | } | ||
| 495 | |||
| 427 | // --------------------------------------------------------------------------- | 496 | // --------------------------------------------------------------------------- |
| 428 | // Print a memory summary from the stashed memory map | 497 | // Print a memory summary from the stashed memory map |
| 429 | // --------------------------------------------------------------------------- | 498 | // --------------------------------------------------------------------------- |
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) { |
diff --git a/shell/src/lib.rs b/shell/src/lib.rs index f1addd9..6077f35 100644 --- a/shell/src/lib.rs +++ b/shell/src/lib.rs | |||
| @@ -93,6 +93,30 @@ pub fn run() -> ! { | |||
| 93 | } | 93 | } |
| 94 | } | 94 | } |
| 95 | 95 | ||
| 96 | /// Run a shell instance inside a windowed terminal. Same as `run()` but | ||
| 97 | /// reads from the task's message queue (Fed by the WM input pump) and | ||
| 98 | /// writes to the active terminal (set by the caller via `vt::set_active`). | ||
| 99 | /// Returns when the window is closed (Close message or ^D on empty line). | ||
| 100 | pub fn run_windowed() { | ||
| 101 | term_println(&alloc::format!("rs-dos v{}", env!("CARGO_PKG_VERSION"))); | ||
| 102 | loop { | ||
| 103 | rsnix::vt::sync_scroll(); | ||
| 104 | term_print("> "); | ||
| 105 | match tty::read_line_from_queue() { | ||
| 106 | None => return, // window closed | ||
| 107 | Some(LineInput::Interrupted) => continue, | ||
| 108 | Some(LineInput::Eof) => return, // ^D in windowed mode = close | ||
| 109 | Some(LineInput::Line(line)) => { | ||
| 110 | let trimmed = line.trim(); | ||
| 111 | if trimmed.is_empty() { | ||
| 112 | continue; | ||
| 113 | } | ||
| 114 | execute(trimmed); | ||
| 115 | } | ||
| 116 | } | ||
| 117 | } | ||
| 118 | } | ||
| 119 | |||
| 96 | // --------------------------------------------------------------------------- | 120 | // --------------------------------------------------------------------------- |
| 97 | // Diagnostics and command output go straight to terminal + serial, never | 121 | // Diagnostics and command output go straight to terminal + serial, never |
| 98 | // through the filter pipeline and never into a file sink. These helpers | 122 | // through the filter pipeline and never into a file sink. These helpers |
| @@ -741,6 +765,7 @@ write `{} <path> TIL <marker>`", | |||
| 741 | "rm" => cmd_rm(&parsed), | 765 | "rm" => cmd_rm(&parsed), |
| 742 | "edit" => cmd_edit(&parsed), | 766 | "edit" => cmd_edit(&parsed), |
| 743 | "draw" => cmd_draw(&parsed), | 767 | "draw" => cmd_draw(&parsed), |
| 768 | "term" => cmd_term(&parsed), | ||
| 744 | "windows" => cmd_windows(&parsed), | 769 | "windows" => cmd_windows(&parsed), |
| 745 | "restore" => cmd_restore(&parsed), | 770 | "restore" => cmd_restore(&parsed), |
| 746 | "bg" => cmd_bg(&parsed), | 771 | "bg" => cmd_bg(&parsed), |
| @@ -800,6 +825,7 @@ fn cmd_help(_p: &Parsed) { | |||
| 800 | touch - create an empty file | 825 | touch - create an empty file |
| 801 | rm - remove a file or empty dir | 826 | rm - remove a file or empty dir |
| 802 | edit - full-screen editor (^S save, ^X exit, ^W search) | 827 | edit - full-screen editor (^S save, ^X exit, ^W search) |
| 828 | term - spawn a windowed terminal (F1 also works) | ||
| 803 | halt - halt the system (hlt loop) | 829 | halt - halt the system (hlt loop) |
| 804 | poweroff - shut down via ACPI (ResetSystem) | 830 | poweroff - shut down via ACPI (ResetSystem) |
| 805 | exit - alias for poweroff | 831 | exit - alias for poweroff |
| @@ -1401,6 +1427,26 @@ fn cmd_draw(_p: &Parsed) { | |||
| 1401 | } | 1427 | } |
| 1402 | } | 1428 | } |
| 1403 | 1429 | ||
| 1430 | fn cmd_term(_p: &Parsed) { | ||
| 1431 | // Debug: check if the callback is registered. | ||
| 1432 | let cb_set = unsafe { core::ptr::addr_of!(rsnix::wm::SPAWN_TERMINAL_CB).read().is_some() }; | ||
| 1433 | term_println(&alloc::format!("term: SPAWN_TERMINAL_CB set = {}", cb_set)); | ||
| 1434 | if !cb_set { | ||
| 1435 | term_err("term: callback not registered — cannot spawn"); | ||
| 1436 | return; | ||
| 1437 | } | ||
| 1438 | // Invoke the callback directly (same as F1). | ||
| 1439 | let (cw, ch) = rsnix::gop::pixel_dims(); | ||
| 1440 | let w = 640usize.min(cw); | ||
| 1441 | let h = 400usize.min(ch); | ||
| 1442 | term_println(&alloc::format!("term: requesting window {}x{}", w, h)); | ||
| 1443 | unsafe { | ||
| 1444 | let cb = core::ptr::addr_of!(rsnix::wm::SPAWN_TERMINAL_CB).read().unwrap(); | ||
| 1445 | cb("term dbg", w, h); | ||
| 1446 | } | ||
| 1447 | term_println("term: callback returned (task should be spawning)"); | ||
| 1448 | } | ||
| 1449 | |||
| 1404 | fn cmd_windows(_p: &Parsed) { | 1450 | fn cmd_windows(_p: &Parsed) { |
| 1405 | let list = rsnix::wm::list(); | 1451 | let list = rsnix::wm::list(); |
| 1406 | if list.is_empty() { | 1452 | if list.is_empty() { |
