summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-08-20 13:47:10 +0200
committersalmonik <salmonik@tuta.io>2026-08-20 13:47:10 +0200
commitcb17c57452ccd2ccca77e99098402d4b9f61c0a5 (patch)
tree6566e0ddaa764c6675e9a56f03772de345170992
parentwm: integrate home terminal as window 0 + fix console focus after close (diff)
downloadrs-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.rs69
-rw-r--r--rsnix/src/tty.rs62
-rw-r--r--rsnix/src/vt.rs96
-rw-r--r--rsnix/src/wm.rs16
-rw-r--r--shell/src/lib.rs46
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).
435static mut TERM_TITLE_BUF: [u8; 64] = [0u8; 64];
436static mut TERM_TITLE_LEN: usize = 0;
437static mut TERM_W: usize = 0;
438static mut TERM_H: usize = 0;
439
440fn 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.
460fn 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).
142pub 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.
140pub fn interrupt_pressed() -> bool { 202pub 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
618static mut HOME: Option<Terminal> = None; 662static 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.
666static mut ACTIVE: *mut Terminal = core::ptr::null_mut();
667
668fn active() -> &'static mut Terminal {
669 unsafe {
670 if !ACTIVE.is_null() {
671 &mut *ACTIVE
672 } else {
673 home()
674 }
675 }
676}
619 677
620fn home() -> &'static mut Terminal { 678fn 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).
687pub 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()`.
629pub fn init() { 695pub fn init() {
@@ -675,27 +741,27 @@ pub fn set_cursor_visible(v: bool) {
675} 741}
676 742
677pub fn sync_scroll() { 743pub fn sync_scroll() {
678 home().sync_scroll(); 744 active().sync_scroll();
679} 745}
680 746
681pub fn repaint_region(x: usize, y: usize, w: usize, h: usize) { 747pub 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
685pub fn write_bytes(b: &[u8]) { 751pub fn write_bytes(b: &[u8]) {
686 home().write_bytes(b); 752 active().write_bytes(b);
687} 753}
688 754
689pub fn write_str(s: &str) { 755pub fn write_str(s: &str) {
690 home().write_str(s); 756 active().write_str(s);
691} 757}
692 758
693pub fn put_char(b: u8) { 759pub fn put_char(b: u8) {
694 home().put_char(b); 760 active().put_char(b);
695} 761}
696 762
697pub fn clear_all() { 763pub 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
397static 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).
400pub static mut SPAWN_TERMINAL_CB: Option<fn(&str, usize, usize)> = None;
401
397fn spawn_terminal() { 402fn 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
401fn remove_from_zorder(id: usize) { 415fn 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).
100pub 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
1430fn 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
1404fn cmd_windows(_p: &Parsed) { 1450fn 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() {