#![no_std] //! Command shell — reads from PS/2 keyboard, writes to GOP + serial. //! //! One canonical grammar for every command (ADR 0002): //! //! ```text //! cmd args [USE src] [TIL marker] [filters…] [output kw dst] //! ``` //! //! - **Keywords** (uppercase-only, reserved operators): `WRITING`/`WRITE` //! overwrite a file, `TO` appends, `USE`/`USING` names an input source, //! `TIL`/`UNTIL` ends input at an exact full-line marker. //! - **Filters** are trailing flags applied left to right as stages over a //! line stream: `-h N` (first N lines, then kill the upstream), //! `-t N` (hold back a rolling window of the last N lines, emit only at //! end of stream), `-g PAT` (smart-case literal substring), `-p` (pause //! at each screenful). Each may repeat. //! - **Quoting** (`"..."` groups + literalizes, `\x` escapes one char) //! makes quoted/escaped text literal — never an operator, never split. //! - `>` and friends are gone; `>` is ordinary text. //! - ^D is EOF everywhere and never powers off; `poweroff`/`exit` are the //! only shutdown verbs. extern crate alloc; use alloc::collections::VecDeque; use alloc::string::{String, ToString}; use alloc::vec::Vec; use rsnix::tty::{self, LineInput}; use rsnix::{gop_print, gop_println}; /// Run the shell forever. pub fn run() -> ! { gop_println!( "rs-dos v{} — type 'help' for commands", env!("CARGO_PKG_VERSION") ); term_println(&alloc::format!( "========== rs-dos v{} ==========", env!("CARGO_PKG_VERSION") )); // A crash report from a previous run survives on the disk (the fault // handler writes crash.log on the mounted volume before halting; real // hardware has no serial console). Surface it so the user can report it. if let Some(h) = rsnix::fs::read_crash_log() { term_println("!!! PREVIOUS CRASH REPORT:"); let mut buf = [0u8; 256]; if let Ok(n) = rsnix::fs::read(h, &mut buf, 0) && n > 0 { let s = core::str::from_utf8(&buf[..n]).unwrap_or("?"); for line in s.split('\n') { if !line.is_empty() { term_println(line); } } } term_println("!!! end of crash report"); } loop { // Push any deferred scrolls so the prompt renders on a current // framebuffer (e.g. after a streaming command's batch of output). rsnix::vt::sync_scroll(); // Cheap hotplug probe: if any AHCI port's device presence changed // since the last scan, re-run discovery (mounts new volumes, // unmounts gone ones) before showing the prompt. if rsnix::ahci::poll_dirty() { report_rescan(rescan_all()); } // Print prompt term_print("> "); // Read one line from keyboard (tty canonical mode) match tty::read_line() { LineInput::Interrupted => continue, // ^C — fresh prompt LineInput::Eof => continue, // ^D on an empty line — fresh prompt (no poweroff) LineInput::Line(line) => { let trimmed = line.trim(); if trimmed.is_empty() { continue; } execute(trimmed); } } } } // --------------------------------------------------------------------------- // Diagnostics and command output go straight to terminal + serial, never // through the filter pipeline and never into a file sink. These helpers // write to the serial port directly — NOT via `serial_println!`, which // feeds the klog ring — so user-facing shell output (prompts, echoes, // command results) never lands in the boot log. // --------------------------------------------------------------------------- fn term_print(s: &str) { gop_print!("{}", s); rsnix::serial::write_str(s); } fn term_println(s: &str) { gop_print!("{}", s); gop_println!(); // One locked call (line + CRLF) so a tick can't split the pair and // let a background task's serial line interleave mid-line. rsnix::serial::write_line(s); } fn term_err(msg: &str) { term_println(msg); } // --------------------------------------------------------------------------- // Parsing // --------------------------------------------------------------------------- /// One lexical token: its text plus whether any quoting/escaping was used /// (a token containing quoted/escaped text is never an operator or flag). struct Token { text: String, literal: bool, } /// Split a command line into words, handling `"..."` groups and `\x` /// escapes. Quotes are delimiters, not content; escapes prevent splitting /// and literalize one character (`\"` → `"`, `\ ` → space). fn tokenize(s: &str) -> Result, &'static str> { let mut toks: Vec = Vec::new(); let mut cur = String::new(); let mut cur_literal = false; let mut in_quote = false; let mut chars = s.chars(); while let Some(c) = chars.next() { match c { '"' => { cur_literal = true; in_quote = !in_quote; } '\\' => { cur_literal = true; match chars.next() { Some(e) => cur.push(e), None => cur.push('\\'), // trailing backslash: literal } } c if c.is_whitespace() && !in_quote => { if !cur.is_empty() { toks.push(Token { text: core::mem::take(&mut cur), literal: cur_literal, }); cur_literal = false; } } c => cur.push(c), } } if in_quote { return Err("unterminated quote"); } if !cur.is_empty() { toks.push(Token { text: cur, literal: cur_literal, }); } Ok(toks) } fn is_keyword(s: &str) -> bool { matches!( s, "WRITING" | "WRITE" | "TO" | "USE" | "USING" | "TIL" | "UNTIL" ) } /// How the command's input is fed. Only `cat`/`hexdump`, and only when /// path-less, may carry an input keyword. #[derive(Clone)] enum Input { None, /// `USE f` — read from a file. Use(String), /// `TIL m` — read from the keyboard until an exact line equals `m`. Til(String), /// `USE f TIL m` — read `f` until an exact line equals `m`. UseTil(String, String), } impl Input { fn present(&self) -> bool { !matches!(self, Input::None) } } /// A post-filter stage. Applied left to right over the line stream. #[derive(Clone)] enum Filter { Head(usize), Tail(usize), Grep(String), Pause, } /// Output destination. #[derive(Clone)] enum Sink { Write(String), Append(String), } struct Parsed { cmd: String, args: Vec, input: Input, filters: Vec, output: Option, } /// Parse one command line into the canonical grammar. Errors are the /// syntax-error message to show the user. fn parse(s: &str) -> Result { let toks = tokenize(s).map_err(|e| e.to_string())?; if toks.is_empty() { return Err("empty command".into()); } let cmd = toks[0].text.clone(); let mut i = 1; let mut args: Vec = Vec::new(); let mut filters: Vec = Vec::new(); let mut output: Option = None; let mut trailing = false; // a filter flag or output keyword has been seen let mut src: Option = None; let mut til: Option = None; let mut til_seen = false; while i < toks.len() { let t = &toks[i]; let text = t.text.as_str(); if !t.literal { match text { "USE" | "USING" => { if output.is_some() { return Err("USE must come before the output keyword".into()); } if trailing { return Err("USE must come before the filters".into()); } if src.is_some() || til_seen { return Err("duplicate input keyword".into()); } i += 1; let path = toks.get(i).ok_or("USE needs a source path")?; if !path.literal && is_keyword(&path.text) { return Err("USE needs a source path".into()); } src = Some(path.text.clone()); i += 1; continue; } "TIL" | "UNTIL" => { if output.is_some() { return Err("TIL must come before the output keyword".into()); } if trailing { return Err("TIL must come before the filters".into()); } if til_seen { return Err("duplicate TIL".into()); } i += 1; let m = toks.get(i).ok_or("TIL needs a marker")?; if !m.literal && is_keyword(&m.text) { return Err("TIL needs a marker".into()); } til = Some(m.text.clone()); til_seen = true; i += 1; continue; } "WRITING" | "WRITE" | "TO" => { if output.is_some() { return Err("duplicate output keyword".into()); } i += 1; let dst = toks.get(i).ok_or("output keyword needs a destination")?; if !dst.literal && is_keyword(&dst.text) { return Err("output keyword needs a destination".into()); } output = Some(if text == "TO" { Sink::Append(dst.text.clone()) } else { Sink::Write(dst.text.clone()) }); trailing = true; i += 1; continue; } "-h" | "-t" => { if output.is_some() { return Err("filters must come before the output keyword".into()); } trailing = true; i += 1; let n = toks.get(i).ok_or("flag needs a count")?; let n: usize = n.text.parse().map_err(|_| "count must be a number ≥ 1")?; if n < 1 { return Err("count must be ≥ 1".into()); } filters.push(if text == "-h" { Filter::Head(n) } else { Filter::Tail(n) }); i += 1; continue; } "-g" => { if output.is_some() { return Err("filters must come before the output keyword".into()); } trailing = true; i += 1; let pat = toks.get(i).ok_or("-g needs a pattern")?; filters.push(Filter::Grep(pat.text.clone())); i += 1; continue; } "-p" => { if output.is_some() { return Err("filters must come before the output keyword".into()); } trailing = true; filters.push(Filter::Pause); i += 1; continue; } _ => {} } } // Ordinary token. Once the trailing region has started (a filter // flag or the output keyword), stray arguments are syntax errors. if trailing { return Err(alloc::format!("unexpected argument '{}'", text)); } args.push(text.to_string()); i += 1; } let input = match (src, til) { (Some(s), Some(t)) => Input::UseTil(s, t), (Some(s), None) => Input::Use(s), (None, Some(t)) => Input::Til(t), (None, None) => Input::None, }; Ok(Parsed { cmd, args, input, filters, output, }) } // --------------------------------------------------------------------------- // Filter pipeline + output sink // --------------------------------------------------------------------------- /// Per-stage mutable state. enum StageState { /// Lines still allowed through before the upstream is killed. Head(usize), /// Rolling window of the last N lines. Tail(VecDeque), Grep, Pause { shown: usize, screen: usize, cols: usize, }, } /// Where the filtered line stream ends up. enum OutSink { Term, File { path: String, h: Option, pos: u64, overwrite: bool, }, } /// Smart-case literal substring match: case-insensitive unless the pattern /// contains an uppercase letter. fn smart_match(pat: &str, line: &str) -> bool { if pat.chars().any(|c| c.is_uppercase()) { line.contains(pat) } else { let lp = pat.to_ascii_lowercase(); let ll = line.to_ascii_lowercase(); ll.contains(&lp) } } /// Pager pause: terminal only, waits for a keypress; ^C aborts the stream. fn pager_wait() -> bool { gop_print!("\n— more —"); match tty::read_byte() { 0x03 => { // ^C — abort the stream (stream-abort role). gop_println!(); false } _ => { // Any key continues; swallow queued tail bytes (e.g. the // rest of an arrow-key escape sequence). while tty::try_read_byte().is_some() {} true } } } /// The filter chain plus the output sink. Producers push one line at a /// time; `line()` returns false when the upstream must stop (head limit or /// pager ^C). At end of stream call `end()` to flush held-back tails. struct Pipeline { stages: Vec<(Filter, StageState)>, sink: OutSink, } impl Pipeline { fn new(filters: &[Filter], output: &Option) -> Self { let file_sink = output.is_some(); let mut stages: Vec<(Filter, StageState)> = Vec::new(); for f in filters { // With a file sink the pager is a no-op. if file_sink && matches!(f, Filter::Pause) { continue; } let st = match f { Filter::Head(n) => StageState::Head(*n), Filter::Tail(n) => StageState::Tail(VecDeque::with_capacity(*n)), Filter::Grep(_) => StageState::Grep, Filter::Pause => StageState::Pause { shown: 0, screen: rsnix::vt::size().0.saturating_sub(1).max(1), // Rows a screenful of *wrapped* output occupies: a // line wider than the terminal takes more than one // row, so the pager counts rows, not lines. cols: rsnix::vt::size().1.max(1), }, }; stages.push((f.clone(), st)); } let sink = match output { Some(Sink::Write(p)) => OutSink::File { path: p.clone(), h: None, pos: 0, overwrite: true, }, Some(Sink::Append(p)) => OutSink::File { path: p.clone(), h: None, pos: 0, overwrite: false, }, None => OutSink::Term, }; Pipeline { stages, sink } } /// Push one line through the stages. Returns false when the producer /// must stop (head killed the stream, or ^C at the pager). fn line(&mut self, l: &str) -> bool { self.push_through(0, l) } /// Run one line through the stages starting at `start`, delivering to /// the sink. `start` lets `finish()` replay a drained tail window /// through only the stages after it. fn push_through(&mut self, start: usize, l: &str) -> bool { let mut cur = alloc::vec![l.to_string()]; for (i, (f, st)) in self.stages.iter_mut().enumerate() { if i < start { continue; } let mut out = Vec::new(); match f { Filter::Head(_) => { let StageState::Head(rem) = st else { unreachable!("stage state does not match filter") }; if *rem == 0 { // Head limit reached: kill the upstream. The line // that would have overflowed is dropped. return false; } for l in cur.drain(..) { *rem -= 1; out.push(l); } } Filter::Tail(n) => { let StageState::Tail(buf) = st else { unreachable!("stage state does not match filter") }; for l in cur.drain(..) { if buf.len() == *n { buf.pop_front(); } buf.push_back(l); } // Held back — nothing reaches the downstream yet. return true; } Filter::Grep(pat) => { let StageState::Grep = st else { unreachable!("stage state does not match filter") }; for l in cur.drain(..) { if smart_match(pat, &l) { out.push(l); } } } Filter::Pause => { let StageState::Pause { shown, screen, cols, } = st else { unreachable!("stage state does not match filter") }; for l in cur.drain(..) { // Rows this line occupies after terminal wrapping // (at least 1; an empty line is one row). let rows = l.len().div_ceil(*cols); if *shown > 0 && *shown % *screen == 0 && !pager_wait() { return false; } *shown += rows; out.push(l); } } } cur = out; } for l in &cur { self.emit(l); } true } /// Flush held-back tail windows through the remaining stages. Called /// at end of stream: EOF, ^C, or after a head kill. fn finish(&mut self) { loop { let mut any = false; for i in 0..self.stages.len() { let drained: Vec = match &mut self.stages[i] { (Filter::Tail(_), StageState::Tail(buf)) if !buf.is_empty() => { buf.drain(..).collect() } _ => continue, }; any = true; for l in drained { self.push_through(i + 1, &l); } } if !any { break; } } } /// End of stream: flush tails, done. File writes are synchronous. fn end(&mut self) { self.finish(); } fn emit(&mut self, l: &str) { match &self.sink { OutSink::Term => { term_println(l); } OutSink::File { .. } => {} } if let OutSink::File { h: None, .. } = &self.sink { self.open_sink(); } if let OutSink::File { h: Some(h), pos, .. } = &mut self.sink { let mut bytes = l.as_bytes().to_vec(); bytes.push(b'\n'); if let Ok(n) = rsnix::fs::write(*h, &bytes, *pos) { *pos += n as u64; } } } /// Lazily open the file sink on first output (a command that emits /// nothing touches no file). WRITING truncates and writes from offset /// 0 (the node's cached size is stale after truncation); TO appends at /// the file's current size. fn open_sink(&mut self) { let (path, overwrite) = match &self.sink { OutSink::File { path, overwrite, .. } => (path.clone(), *overwrite), _ => return, }; match rsnix::fs::ensure_file(&path) { Ok(h) => { let pos = if overwrite { let _ = rsnix::fs::set_len(h, 0); 0 } else { rsnix::fs::size(h) }; self.sink = OutSink::File { path, h: Some(h), pos, overwrite, }; } Err(()) => { term_err(&alloc::format!("cannot write '{}'", path)); self.sink = OutSink::Term; } } } } /// Emit every line of a pre-built multi-line string (interior blank lines /// preserved; a trailing newline does not produce an extra empty line). fn emit_string(pipe: &mut Pipeline, s: &str) { let mut rest = s; loop { match rest.find('\n') { Some(idx) => { if !pipe.line(&rest[..idx]) { return; } rest = &rest[idx + 1..]; } None => { if !rest.is_empty() { let _ = pipe.line(rest); } return; } } } } // --------------------------------------------------------------------------- // Execution // --------------------------------------------------------------------------- /// Commands that may consume an input stream (`USE`/`TIL`). fn accepts_input(cmd: &str) -> bool { matches!(cmd, "cat" | "hexdump") } fn execute(s: &str) { let parsed = match parse(s) { Ok(p) => p, Err(e) => { term_err(&e); return; } }; let cmd = parsed.cmd.as_str(); if parsed.input.present() { if !accepts_input(cmd) { term_err(&alloc::format!("{}: does not accept input", cmd)); return; } // `cat TIL ` is sugar for `cat USE TIL // `: the paths are the input source, truncated at the // marker. An explicit source keyword (USE / USE..TIL) alongside // path arguments is genuinely two input sources. if !parsed.args.is_empty() && !matches!(parsed.input, Input::Til(_)) { term_err(&alloc::format!( "{}: input source keyword with path arguments is ambiguous — \ the paths are the input; drop the keyword, or to truncate them at a marker \ write `{} TIL `", cmd, cmd )); return; } } match cmd { "help" => cmd_help(&parsed), "echo" => cmd_echo(&parsed), "clear" => cmd_clear(), "halt" => cmd_halt(), "poweroff" | "exit" => cmd_poweroff(), "hello" => cmd_hello(&parsed), "info" => cmd_info(&parsed), "mem" => cmd_mem(&parsed), "mmap" => cmd_mmap(&parsed), "stress" => cmd_stress(&parsed), "date" => cmd_date(&parsed), "pci" => cmd_pci(&parsed), "rescan" => cmd_rescan(&parsed), "mouse" => cmd_mouse(&parsed), "cursor" => cmd_cursor(&parsed), "gfx" => cmd_gfx(&parsed), "ls" => cmd_ls(&parsed), "cat" => cmd_cat(&parsed), "klog" => cmd_klog(&parsed), "hexdump" => cmd_hexdump(&parsed), "mkdir" => cmd_mkdir(&parsed), "touch" => cmd_touch(&parsed), "rm" => cmd_rm(&parsed), "edit" => cmd_edit(&parsed), "bg" => cmd_bg(&parsed), "jobs" => cmd_jobs(&parsed), "msgrecv" => cmd_msgrecv(&parsed), "send" => cmd_send(&parsed), "sleep" => cmd_sleep(&parsed), "kill" => cmd_kill(&parsed), "demo" => cmd_demo(&parsed), "fault" => cmd_fault(), "corrupt" => cmd_corrupt(), "crashlog" => cmd_crashlog(), "stall" => cmd_stall(&parsed), _ => term_err(&alloc::format!("unknown command: {}", cmd)), } } // --------------------------------------------------------------------------- // Built-in commands // --------------------------------------------------------------------------- fn cmd_help(_p: &Parsed) { let mut pipe = Pipeline::new(&_p.filters, &_p.output); let text = "available commands: help — show this message echo — echo arguments clear — clear screen hello — print 'World!' info — show system info mem — show memory info mmap — dump the UEFI memory map stress — stress-test heap/page allocators date — show current date/time (RTC) pci — list PCI devices rescan — re-scan for hotplugged/removed disks (also runs automatically) cursor — report the on-screen mouse cursor (x, y, buttons) mouse — watch the cursor live (x/y/buttons); ^C stops mouse — route mouse events to task 's queue (cursor still tracks) gfx — demo: draw a status bar + RGB boxes (bottom strip) gfx check — pixel self-test: shadow, display push, cursor overlay bg — spawn a background task (spinner, ticker, counter, boom, overflow) msgrecv — spawn a task that blocks on its message queue and logs every message to serial (ids from `jobs`) send [args] — post a message to a task's queue: user · timer · mouse (1=L 2=R 4=M) key (enter esc tab space bksp up down left right home end pgup pgdn del ctrl-c f1..f12, or a single char) jobs — list tasks (id, name, state, ticks, q=pending messages) sleep — block the shell for ms; background tasks keep running kill — stop a background task (id from jobs) demo — spawn spinner + ticker + counter ls — list a directory cat — print files (cat USE a TO b copies a→b; cat TIL END is a heredoc) klog — kernel boot log (cat /dev/klog; WRITING persists it) hexdump — hex-dump a file or input stream mkdir — create a directory touch — create an empty file rm — remove a file or empty dir edit — full-screen editor (^S save, ^X exit, ^W search) halt — halt the system (hlt loop) poweroff — shut down via ACPI (ResetSystem) exit — alias for poweroff redirection keywords (uppercase, after args): USE/USING read command input from a file (cat/hexdump only) TIL/UNTIL read input until an exact marker line (heredoc) cat TIL reads the path to the marker WRITING/WRITE write output to a file (overwrite) TO append output to a file order: USE/TIL first, then the -flags, then WRITING/TO last a file sink replaces the screen; > >> < << are gone (> is plain text) post-filter flags (trailing, applied left to right): -h N first N lines (kills the command after N) -t N last N lines (shows nothing until the stream ends; ^C flushes) -g PAT smart-case literal substring match (repeatable) -p pause at each screenful (more-style; ^C aborts) quoting: \"...\" groups text; \\x escapes one char. Quoted text is never an operator: echo \"WRITING\" prints WRITING. ^D = EOF; ^C cancels a line or aborts a stream. poweroff/exit shut down."; emit_string(&mut pipe, text); pipe.end(); } fn cmd_echo(p: &Parsed) { let mut pipe = Pipeline::new(&p.filters, &p.output); let line = p.args.join(" "); let _ = pipe.line(&line); pipe.end(); } fn cmd_hello(p: &Parsed) { let mut pipe = Pipeline::new(&p.filters, &p.output); let _ = pipe.line("World!"); pipe.end(); } /// Deliberately fault on an unmapped address — test hook that verifies /// the fault-dump path (screen + serial) halts cleanly instead of /// triple-faulting. The system stops; the log must show the dump. /// Canonical-but-unmapped address (below 2^47, above any RAM/MMIO): the /// page walk terminates at a not-present entry → #PF with CR2 = address. fn cmd_fault() { term_println("fault: writing to 0x0000_4000_0000_0000..."); unsafe { core::ptr::write_volatile(0x0000_4000_0000_0000usize as *mut u32, 0x1234u32); } } /// Deliberately clobber a live heap block's back-pointer, then free it — /// test hook that proves the allocator's corruption guard fires with a /// readable report (instead of a #GP inside `__rust_dealloc`). The /// system stops; the log must show `!!! heap corruption in dealloc`. fn cmd_corrupt() { term_println("corrupt: clobbering a live block's back-pointer..."); let s = String::from("hello heap guard"); let ptr = s.as_ptr() as usize; unsafe { *((ptr - 8) as *mut usize) = 0xdeed_beef_deed_beefusize; } drop(s); term_println("corrupt: guard did NOT fire (unexpected)"); } /// Debug hook: run the crash-log FAT write from the *shell* context (not /// the fault ISR) to compare behaviour. fn cmd_crashlog() { let lines = ["!!! test crashlog write", "second line", "third line"]; rsnix::fs::write_crash_log(&lines); term_println("crashlog: done"); } /// Set the display-store stall (cycles per word) — diagnostics knob that /// simulates the per-write cost of an uncached real-GPU framebuffer so /// the preemptive scheduler races long scroll pushes like on real /// hardware. `stall 0` disables (default; QEMU RAM framebuffers are /// instant). fn cmd_stall(p: &Parsed) { let n: u32 = p.args.first().and_then(|a| a.parse().ok()).unwrap_or(0); rsnix::gop::DISPLAY_STALL.store(n, core::sync::atomic::Ordering::Relaxed); term_println(&alloc::format!("stall: display store stall = {}", n)); } fn cmd_clear() { rsnix::gop::clear_screen(); } fn cmd_info(p: &Parsed) { let mut pipe = Pipeline::new(&p.filters, &p.output); let mut s = String::from("rs-dos v"); s.push_str(env!("CARGO_PKG_VERSION")); s.push_str(" — rsnix kernel"); let _ = pipe.line(&s); let _ = pipe.line("arch: x86_64, freestanding, no userspace"); pipe.end(); } fn cmd_ls(p: &Parsed) { let path = p.args.first().map(|s| s.as_str()).unwrap_or("/"); let mut pipe = Pipeline::new(&p.filters, &p.output); match rsnix::fs::resolve(path) { None => term_err(&alloc::format!("ls: {}: no such file or directory", path)), Some(h) => { let list = if rsnix::fs::kind(h) == rsnix::fs::FileType::Dir { rsnix::fs::children(h) } else { alloc::vec![( path.rsplit('/').next().unwrap_or(path).to_string(), rsnix::fs::kind(h), rsnix::fs::size(h) )] }; for (name, kind, size) in list { let l = match kind { rsnix::fs::FileType::Dir => alloc::format!("{}/", name), rsnix::fs::FileType::Char => alloc::format!("{}@", name), rsnix::fs::FileType::Regular => alloc::format!("{} {} B", name, size), }; if !pipe.line(&l) { break; } } } } pipe.end(); } // --------------------------------------------------------------------------- // klog — kernel log (thin alias for `cat /dev/klog`) // --------------------------------------------------------------------------- fn cmd_klog(p: &Parsed) { if !p.args.is_empty() { term_err("klog: usage: klog [-h N] [-t N] [-g PAT] [-p] [WRITING | TO ]"); return; } // Alias for `cat /dev/klog` — same filters and redirection, so // `klog -t 5` shows the last 5 log lines and `klog WRITING // /disk/.../boot.log` persists the boot log. Input keywords were // already rejected (klog does not accept input). let q = Parsed { cmd: alloc::string::String::from("klog"), args: alloc::vec!["/dev/klog".into()], input: Input::None, filters: p.filters.clone(), output: p.output.clone(), }; cmd_cat(&q); } // --------------------------------------------------------------------------- // cat — file and stream consumers // --------------------------------------------------------------------------- /// How a file read ended: read to EOF, stopped at the TIL marker, or /// aborted (^C / head kill) — the caller should stop processing paths. enum ReadResult { Done, Marker, Aborted, } fn cmd_cat(p: &Parsed) { let mut pipe = Pipeline::new(&p.filters, &p.output); match &p.input { Input::None => { if p.args.is_empty() { // Path-less `cat` with no input keyword reads the keyboard // until ^D. cat_stdin(&mut pipe, None); } else { for path in &p.args { if !cat_file(&mut pipe, path) { break; } } } } Input::Use(path) => { cat_file(&mut pipe, path); } Input::Til(marker) => { if p.args.is_empty() { cat_stdin(&mut pipe, Some(marker)); } else { // `cat TIL ` — the paths are the input // source, truncated at the first marker line. for path in &p.args { match cat_file_until(&mut pipe, path, marker) { ReadResult::Done => {} ReadResult::Marker | ReadResult::Aborted => break, } } } } Input::UseTil(path, marker) => { cat_file_until(&mut pipe, path, marker); } } pipe.end(); } /// Read lines from the tty until ^C/^D (or an exact marker line, when /// `marker` is Some — the marker itself is not part of the input). fn cat_stdin(pipe: &mut Pipeline, marker: Option<&str>) -> bool { loop { match tty::read_line() { LineInput::Line(l) => { if marker.is_some() && l.as_str() == marker.unwrap() { return true; } if !pipe.line(l.as_str()) { return false; } } LineInput::Interrupted => return false, // ^C aborts the stream LineInput::Eof => return true, // ^D = EOF } } } fn cat_file(pipe: &mut Pipeline, path: &str) -> bool { let h = match rsnix::fs::resolve(path) { Some(h) => h, None => { term_err(&alloc::format!("cat: {}: no such file", path)); return true; // keep going with the remaining paths } }; let mut buf = [0u8; 512]; let mut pos = 0u64; let mut pending = String::new(); loop { if tty::interrupt_pressed() { return false; // ^C aborts everything } match rsnix::fs::read(h, &mut buf, pos) { Ok(0) | Err(_) => break, Ok(n) => { pos += n as u64; pending.push_str(&String::from_utf8_lossy(&buf[..n])); while let Some(idx) = pending.find('\n') { let line: String = pending.drain(..=idx).collect(); let line = line.trim_end_matches('\n'); if !pipe.line(line) { return false; } } } } } if !pending.is_empty() && !pipe.line(&pending) { return false; } true } fn cat_file_until(pipe: &mut Pipeline, path: &str, marker: &str) -> ReadResult { let h = match rsnix::fs::resolve(path) { Some(h) => h, None => { term_err(&alloc::format!("cat: {}: no such file", path)); return ReadResult::Done; } }; let mut buf = [0u8; 512]; let mut pos = 0u64; let mut pending = String::new(); let mut stopped = false; 'outer: loop { if tty::interrupt_pressed() { return ReadResult::Aborted; } match rsnix::fs::read(h, &mut buf, pos) { Ok(0) | Err(_) => break, Ok(n) => { pos += n as u64; pending.push_str(&String::from_utf8_lossy(&buf[..n])); while let Some(idx) = pending.find('\n') { let line: String = pending.drain(..=idx).collect(); let line = line.trim_end_matches('\n'); // Marker line and everything after is discarded. if line == marker { stopped = true; break 'outer; } if !pipe.line(line) { return ReadResult::Aborted; } } } } } if !stopped && !pending.is_empty() && pending != marker && !pipe.line(&pending) { return ReadResult::Aborted; } if stopped { ReadResult::Marker } else { ReadResult::Done } } // --------------------------------------------------------------------------- // hexdump — hex-dump files and input streams // --------------------------------------------------------------------------- struct HexCtx<'a> { pipe: &'a mut Pipeline, pos: u64, row: [u8; 16], row_len: usize, alive: bool, } fn format_partial(addr: u64, row: &[u8]) -> String { let mut out = String::new(); use core::fmt::Write as _; let _ = core::write!(out, "{:08x} ", addr); for i in 0..16 { if i < row.len() { let _ = core::write!(out, "{:02x} ", row[i]); } else { out.push_str(" "); } } out.push_str(" |"); for &b in row { out.push(if b.is_ascii_graphic() || b == b' ' { b as char } else { '.' }); } out.push('|'); out } impl HexCtx<'_> { fn feed(&mut self, data: &[u8]) { if !self.alive { return; } for &b in data { self.row[self.row_len] = b; self.row_len += 1; if self.row_len == 16 { let line = format_partial(self.pos, &self.row); self.alive = self.pipe.line(&line); self.pos += 16; self.row_len = 0; if !self.alive { return; } } } } /// Emit the trailing partial row, if any. fn flush(&mut self) { if self.alive && self.row_len > 0 { let line = format_partial(self.pos, &self.row[..self.row_len]); let _ = self.pipe.line(&line); } } } fn cmd_hexdump(p: &Parsed) { let mut pipe = Pipeline::new(&p.filters, &p.output); match &p.input { Input::None => { if p.args.is_empty() { hexdump_stdin(&mut pipe, None); } else { for path in &p.args { if !hexdump_file(&mut pipe, path) { break; } } } } Input::Use(path) => { hexdump_file(&mut pipe, path); } Input::Til(marker) => { if p.args.is_empty() { hexdump_stdin(&mut pipe, Some(marker)); } else { // `hexdump TIL ` — the paths are the input // source, truncated at the first marker line. for path in &p.args { match hexdump_file_until(&mut pipe, path, marker) { ReadResult::Done => {} ReadResult::Marker | ReadResult::Aborted => break, } } } } Input::UseTil(path, marker) => { hexdump_file_until(&mut pipe, path, marker); } } pipe.end(); } fn hexdump_file(pipe: &mut Pipeline, path: &str) -> bool { let h = match rsnix::fs::resolve(path) { Some(h) => h, None => { term_err(&alloc::format!("hexdump: {}: no such file", path)); return true; } }; let mut ctx = HexCtx { pipe, pos: 0, row: [0u8; 16], row_len: 0, alive: true, }; let mut buf = [0u8; 512]; let mut pos = 0u64; loop { if tty::interrupt_pressed() { ctx.flush(); return false; } match rsnix::fs::read(h, &mut buf, pos) { Ok(0) | Err(_) => break, Ok(n) => { pos += n as u64; ctx.feed(&buf[..n]); if !ctx.alive { break; } } } } ctx.flush(); ctx.alive } /// Hex-dump lines from the tty until ^C/^D (or an exact marker line, /// when `marker` is Some — the marker itself is not dumped). fn hexdump_stdin(pipe: &mut Pipeline, marker: Option<&str>) { let mut ctx = HexCtx { pipe, pos: 0, row: [0u8; 16], row_len: 0, alive: true, }; loop { match tty::read_line() { LineInput::Line(l) => { if marker.is_some() && l.as_str() == marker.unwrap() { break; } ctx.feed(l.as_str().as_bytes()); if !ctx.alive { break; } ctx.feed(b"\n"); if !ctx.alive { break; } } LineInput::Interrupted => break, LineInput::Eof => break, } } ctx.flush(); } fn hexdump_file_until(pipe: &mut Pipeline, path: &str, marker: &str) -> ReadResult { let h = match rsnix::fs::resolve(path) { Some(h) => h, None => { term_err(&alloc::format!("hexdump: {}: no such file", path)); return ReadResult::Done; } }; let mut ctx = HexCtx { pipe, pos: 0, row: [0u8; 16], row_len: 0, alive: true, }; let mut buf = [0u8; 512]; let mut pos = 0u64; let mut pending = String::new(); let mut stopped = false; 'outer: loop { if tty::interrupt_pressed() { break; } match rsnix::fs::read(h, &mut buf, pos) { Ok(0) | Err(_) => break, Ok(n) => { pos += n as u64; pending.push_str(&String::from_utf8_lossy(&buf[..n])); while let Some(idx) = pending.find('\n') { let line: String = pending.drain(..=idx).collect(); let line = line.trim_end_matches('\n'); if line == marker { stopped = true; break 'outer; } ctx.feed(line.as_bytes()); ctx.feed(b"\n"); if !ctx.alive { break 'outer; } } } } } if ctx.alive && !stopped && !pending.is_empty() && pending != marker { ctx.feed(pending.as_bytes()); } ctx.flush(); if !ctx.alive { ReadResult::Aborted } else if stopped { ReadResult::Marker } else { ReadResult::Done } } // --------------------------------------------------------------------------- // Other commands // --------------------------------------------------------------------------- fn cmd_mkdir(p: &Parsed) { let path = p.args.first().map(|s| s.as_str()).unwrap_or(""); if path.is_empty() { term_err("usage: mkdir "); return; } match rsnix::fs::mkdir(path) { Ok(()) => {} Err(()) => term_err(&alloc::format!("mkdir: cannot create directory '{}'", path)), } } fn cmd_touch(p: &Parsed) { let path = p.args.first().map(|s| s.as_str()).unwrap_or(""); if path.is_empty() { term_err("usage: touch "); return; } match rsnix::fs::touch(path) { Ok(()) => {} Err(()) => term_err(&alloc::format!("touch: cannot create '{}'", path)), } } fn cmd_rm(p: &Parsed) { let path = p.args.first().map(|s| s.as_str()).unwrap_or(""); if path.is_empty() { term_err("usage: rm "); return; } match rsnix::fs::remove_path(path) { Ok(()) => {} Err(()) => term_err(&alloc::format!( "rm: cannot remove '{}' (missing, dir not empty, or busy)", path )), } } fn cmd_edit(p: &Parsed) { let path = p.args.first().map(|s| s.as_str()).unwrap_or(""); if path.is_empty() { term_err("usage: edit "); return; } if rsnix::fs::ensure_file(path).is_err() { term_err(&alloc::format!("edit: cannot create {}", path)); return; } rsnix::gop::clear_screen(); rsdosedit::run(path); rsnix::gop::clear_screen(); } fn cmd_mem(p: &Parsed) { let (map, key) = rsnix::memory::get_map(); let mut total_kb = 0u64; let mut conventional_kb = 0u64; let mut loader_kb = 0u64; let mut runtime_kb = 0u64; let mut boot_code_kb = 0u64; let mut boot_data_kb = 0u64; let mut reserved_kb = 0u64; let mut acpi_kb = 0u64; let mut mmio_kb = 0u64; for desc in map { let pages = desc.number_of_pages; let bytes_4k = pages * 4; total_kb += bytes_4k; match desc.kind { 1 | 2 => loader_kb += bytes_4k, // LoaderCode + LoaderData 3 => boot_code_kb += bytes_4k, 4 => boot_data_kb += bytes_4k, 5 | 6 => runtime_kb += bytes_4k, // RuntimeCode + RuntimeData 7 => conventional_kb += bytes_4k, 9 | 10 => acpi_kb += bytes_4k, // ACPIReclaim + ACPINVS 11 | 12 => mmio_kb += bytes_4k, _ => reserved_kb += bytes_4k, // Reserved, Unusable, Pal, ... } } let h = rsnix::memory::heap_stats(); let ps = rsnix::memory::page_stats(); let mut pipe = Pipeline::new(&p.filters, &p.output); let _ = pipe.line(&alloc::format!( "memory map (key=0x{:x}, {} entries)", key, map.len() )); let _ = pipe.line(&alloc::format!( " usable {:4} MB Conventional", conventional_kb / 1024 )); let _ = pipe.line(&alloc::format!( " boot code {:4} MB BootServicesCode", boot_code_kb / 1024 )); let _ = pipe.line(&alloc::format!( " boot data {:4} MB BootServicesData", boot_data_kb / 1024 )); let _ = pipe.line(&alloc::format!( " loader {:4} MB LoaderCode + LoaderData", loader_kb / 1024 )); let _ = pipe.line(&alloc::format!( " runtime {:4} MB RuntimeServices", runtime_kb / 1024 )); let _ = pipe.line(&alloc::format!(" ACPI {:4} MB", acpi_kb / 1024)); let _ = pipe.line(&alloc::format!(" MMIO {:4} MB", mmio_kb / 1024)); let _ = pipe.line(&alloc::format!(" reserved {:4} MB", reserved_kb / 1024)); let _ = pipe.line(&alloc::format!( " raw total {:4} MB (incl. PCIe MMIO)", total_kb / 1024 )); let _ = pipe.line("heap:"); let _ = pipe.line(&alloc::format!( " range {:#x}..{:#x} {} KB total", h.heap_start, h.heap_end, h.heap_size / 1024 )); let _ = pipe.line(&alloc::format!( " free {} KB in {} block(s), allocated {} KB", h.free_bytes / 1024, h.free_blocks, h.heap_size.saturating_sub(h.free_bytes) / 1024 )); let _ = pipe.line("pages:"); let _ = pipe.line(&alloc::format!( " span base {:#x}, {} MB covered", ps.base_page * rsnix::memory::PAGE_SIZE, ps.span_bytes / 1024 / 1024 )); let _ = pipe.line(&alloc::format!( " free {} pages ({} MB), {} used", ps.free_pages, ps.free_pages * rsnix::memory::PAGE_SIZE / 1024 / 1024, ps.used_pages )); pipe.end(); } // --------------------------------------------------------------------------- // mmap — dump every entry of the UEFI memory map with type names // --------------------------------------------------------------------------- fn cmd_mmap(p: &Parsed) { let (map, key) = rsnix::memory::get_map(); let mut pipe = Pipeline::new(&p.filters, &p.output); let _ = pipe.line(&alloc::format!( "memory map (key=0x{:x}, {} entries):", key, map.len() )); let _ = pipe.line(" # type start pages size"); for (i, d) in map.iter().enumerate() { let l = alloc::format!( " {:>2} {:<16} {:#014x} {:>9} {:>8} KB", i, rsnix::uefi::memory_type_name(d.kind), d.physical_start, d.number_of_pages, d.number_of_pages * 4 ); if !pipe.line(&l) { break; } } pipe.end(); } // --------------------------------------------------------------------------- // stress — allocate blocks of KB, hold them, then free all. // Pushes the heap past its base region so growth into the page allocator // is exercised; also verifies a clean return to the free-list after. // --------------------------------------------------------------------------- fn cmd_stress(p: &Parsed) { let mut it = p.args.iter(); let chunks: usize = match it.next().and_then(|s| s.parse().ok()) { Some(n) if (1..=64).contains(&n) => n, _ => { term_err("usage: stress "); return; } }; let kb: usize = match it.next().and_then(|s| s.parse().ok()) { Some(n) if n > 0 && n <= 1024 * 1024 => n, _ => { term_err("usage: stress "); return; } }; let mut pipe = Pipeline::new(&p.filters, &p.output); let mut allocated = 0usize; { let mut blocks: [Option>; 64] = core::array::from_fn(|_| None); for i in 0..chunks { let mut b = alloc::vec::Vec::::new(); match b.try_reserve_exact(kb * 1024) { Ok(()) => { b.resize(kb * 1024, 0x42); blocks[i] = Some(b); allocated += 1; } Err(_) => { let _ = pipe.line(&alloc::format!(" block {}: out of memory, stopping", i)); break; } } } let h = rsnix::memory::heap_stats(); let ps = rsnix::memory::page_stats(); let _ = pipe.line(&alloc::format!( " held {} x {} KB = {} KB", allocated, kb, allocated * kb )); let _ = pipe.line(&alloc::format!( " heap free {} KB ({} blocks), pages free {} MB", h.free_bytes / 1024, h.free_blocks, ps.free_pages * 4 / 1024 )); // blocks dropped here → everything freed } let h = rsnix::memory::heap_stats(); let _ = pipe.line(&alloc::format!( " freed: heap free {} KB in {} blocks", h.free_bytes / 1024, h.free_blocks )); pipe.end(); } fn cmd_halt() -> ! { term_println("halting..."); rsnix::power::halt() } fn cmd_date(p: &Parsed) { let t = rsnix::rtc::now(); let mut pipe = Pipeline::new(&p.filters, &p.output); let line = alloc::format!( "{}-{:02}-{:02} {:02}:{:02}:{:02}", t.year, t.month, t.day, t.hour, t.minute, t.second ); let _ = pipe.line(&line); pipe.end(); } /// Deep device re-scan: AHCI ports → FAT volumes → VFS mount table. /// Returns (disks added, disks removed, vols mounted, vols unmounted, /// nodes added, nodes removed) so callers can report the result. fn rescan_all() -> (usize, usize, usize, usize, usize, usize) { let (da, dr) = rsnix::ahci::rescan(); let (ma, mu) = rsnix::fat::rescan(); let (na, nr) = rsnix::fs::rescan_disks(); (da, dr, ma, mu, na, nr) } /// Print the one-line change summary; returns true if anything changed. fn report_rescan((da, dr, ma, mu, na, nr): (usize, usize, usize, usize, usize, usize)) -> bool { term_println(&alloc::format!( "rescan: {} disk(s) added, {} removed · {} volume(s) mounted, {} unmounted · {} node(s) added, {} removed", da, dr, ma, mu, na, nr )); da + dr + ma + mu + na + nr > 0 } fn cmd_rescan(_p: &Parsed) { if report_rescan(rescan_all()) { term_println("rescan done — /disk updated"); } else { term_println("rescan done — nothing changed"); } } /// Mouse front-end. The cursor task owns the PS/2 ring, so this /// command *watches the on-screen cursor* (position + buttons, printed /// only on change) until ^C. With a task id it also routes every event /// to that task's queue — the window-manager input path — clearing the /// route on exit. fn cmd_mouse(p: &Parsed) { let target: Option = p.args.first().and_then(|s| s.parse::().ok()); if let Some(id) = target { if !rsnix::sched::is_alive(id) { term_err(&alloc::format!("mouse: no such task {}", id)); return; } rsnix::cursor::set_forward(Some(id)); term_println(&alloc::format!( "mouse: routing events to task {} (cursor still tracks) — ^C to stop", id )); } else if !rsnix::ps2::mouse_present() { term_err("mouse: no PS/2 mouse at boot — cat /dev/mouse still works"); return; } else { term_println("mouse: watching cursor — move the mouse, ^C to stop"); } let mut last = (usize::MAX, usize::MAX, u8::MAX); loop { if tty::interrupt_pressed() { rsnix::cursor::set_forward(None); term_println("mouse: stopped"); return; } let (x, y, b) = ( rsnix::gop::cursor_pos().0, rsnix::gop::cursor_pos().1, rsnix::gop::cursor_buttons(), ); if (x, y, b) != last { last = (x, y, b); term_println(&alloc::format!( "mouse: x={} y={} buttons={}{}{}", x, y, if b & 1 != 0 { 'L' } else { '-' }, if b & 2 != 0 { 'R' } else { '-' }, if b & 4 != 0 { 'M' } else { '-' } )); } rsnix::sched::sleep_ms(10); } } /// One-shot cursor state (parseable; used by the test harness). fn cmd_cursor(_p: &Parsed) { let (x, y) = rsnix::gop::cursor_pos(); let b = rsnix::gop::cursor_buttons(); term_println(&alloc::format!( "cursor: x={} y={} buttons={}{}{}", x, y, if b & 1 != 0 { 'L' } else { '-' }, if b & 2 != 0 { 'R' } else { '-' }, if b & 4 != 0 { 'M' } else { '-' } )); } /// Graphics primitives demo + pixel self-test (shadow + display + cursor /// overlay readbacks). `gfx` / `gfx demo` draws a status-bar scene; /// `gfx check` runs the readback assertions (run after `clear` for a /// predictable background). fn cmd_gfx(p: &Parsed) { match p.args.first().map(|s| s.as_str()) { Some("check") => gfx_check(), _ => gfx_demo(), } } fn gfx_demo() { let (w, h) = rsnix::gop::pixel_dims(); if w == 0 { term_err("gfx: no framebuffer"); return; } // Status-bar strip across the bottom, with three RGB boxes. rsnix::gop::fill_rect(0, h - 40, w, 40, rsnix::gop::rgb(0, 0, 128)); rsnix::gop::fill_rect(20, h - 32, 60, 24, rsnix::gop::rgb(255, 0, 0)); rsnix::gop::fill_rect(90, h - 32, 60, 24, rsnix::gop::rgb(0, 255, 0)); rsnix::gop::fill_rect(160, h - 32, 60, 24, rsnix::gop::rgb(255, 255, 0)); term_println(&alloc::format!( "gfx: drew status bar + RGB boxes ({}x{} px bottom strip)", w, h )); } fn gfx_check() { let (w, _h) = rsnix::gop::pixel_dims(); if w == 0 { term_err("gfx: no framebuffer"); return; } let mut ok = true; let mut report = String::new(); // 1. fill_rect lands in the shadow. let (rx, ry, rw, rh) = (8usize, 8usize, 40usize, 16usize); let red = rsnix::gop::rgb(255, 0, 0); rsnix::gop::fill_rect(rx, ry, rw, rh, red); match rsnix::gop::shadow_pixel(rx + rw / 2, ry + rh / 2) { Some(p) if p == red => report.push_str("fill_rect/shadow: OK\n"), Some(p) => { ok = false; report.push_str(&alloc::format!("fill_rect/shadow: FAIL ({:#x})\n", p)); } None => { ok = false; report.push_str("fill_rect/shadow: FAIL (no shadow)\n"); } } // 2. the push reached the display. match rsnix::gop::display_pixel(rx + rw / 2, ry + rh / 2) { Some(p) if p == red => report.push_str("display push: OK\n"), Some(p) => { ok = false; report.push_str(&alloc::format!("display push: FAIL ({:#x})\n", p)); } None => { ok = false; report.push_str("display push: FAIL (readback)\n"); } } // 3. cursor overlay: the arrow tip is white on the display… let (cx, cy) = rsnix::gop::cursor_pos(); let (dw, dh) = rsnix::gop::pixel_dims(); let white = rsnix::gop::rgb(255, 255, 255); let tip = rsnix::gop::display_pixel(cx + 2, cy); let out = rsnix::gop::display_pixel(cx, cy); match (tip, out) { (Some(t), Some(o)) if t == white && o != white => report.push_str("cursor overlay: OK\n"), (Some(t), Some(o)) => { ok = false; report.push_str(&alloc::format!( "cursor overlay: FAIL (tip={:#x} outside={:#x})\n", t, o )); } _ => { ok = false; report.push_str(&alloc::format!( "cursor overlay: FAIL (readback cx={} cy={} dims={}x{})\n", cx, cy, dw, dh )); } } // …and it must NOT be in the shadow (pure overlay). match rsnix::gop::shadow_pixel(cx + 2, cy) { Some(p) if p != white => report.push_str("cursor not in shadow: OK\n"), p => { ok = false; report.push_str(&alloc::format!("cursor in shadow: FAIL ({:?})\n", p)); } } term_println(&report); term_println(if ok { "gfx check: PASS" } else { "gfx check: FAIL" }); } // --------------------------------------------------------------------------- // Multitasking: bg / jobs / sleep / kill / demo // // Background tasks are headless: they may sleep, yield, and log to // serial/klog only. Only the shell touches the disk, filesystem, and // terminal (the ADR 0003 invariant). // --------------------------------------------------------------------------- fn cmd_bg(p: &Parsed) { let Some(name) = p.args.first() else { term_err("bg: needs a task name — spinner, ticker, counter, boom, overflow"); return; }; let entry: fn() = match name.as_str() { "spinner" => task_spinner, "ticker" => task_ticker, "counter" => task_counter, "boom" => task_boom, "overflow" => task_overflow, _ => { term_err(&alloc::format!( "bg: unknown task '{}' (spinner, ticker, counter, boom, overflow)", name )); return; } }; match rsnix::sched::spawn(entry, name) { Some(id) => term_println(&alloc::format!("bg: spawned task {} ({})", id, name)), None => term_err("bg: task table full"), } } fn cmd_jobs(p: &Parsed) { let mut pipe = Pipeline::new(&p.filters, &p.output); let lines = rsnix::sched::jobs(); if lines.is_empty() { let _ = pipe.line("jobs: no tasks"); } for l in lines { if !pipe.line(&l) { break; } } pipe.end(); } /// Demo of the scheduler message queues: spawn a task that blocks on /// `recv_blocking` and logs every message it receives to serial. The /// shell then posts to it with `send …` (id from `jobs`). fn cmd_msgrecv(_p: &Parsed) { match rsnix::sched::spawn(task_msgrecv, "msgrecv") { Some(id) => term_println(&alloc::format!( "msgrecv: task {} listening — post with: send {} …", id, id )), None => term_err("msgrecv: task table full"), } } /// The recv demo body: block forever, log each message. Headless by /// design (serial only — it never touches tty/vt/fs, per ADR 0003). fn task_msgrecv() { loop { let msg = rsnix::sched::recv_blocking(); rsnix::serial_println!( "msgrecv[{}]: {}", rsnix::sched::current(), rsnix::sched::msg_describe(&msg) ); } } /// `send [args]` — post a TaskMsg to a task's queue. This /// is the shell-side knob for the primitive the window manager will use /// to route input; types: user , timer, mouse /// (1=L 2=R 4=M), key (enter esc tab space bksp up down left /// right home end pgup pgdn del ctrl-c f1..f12, or a single char). fn cmd_send(p: &Parsed) { let mut args = p.args.iter(); let id: usize = match args.next().and_then(|s| s.parse().ok()) { Some(v) => v, None => { term_err("send: needs a task id — jobs lists them, msgrecv spawns a listener"); return; } }; let kind = match args.next() { Some(k) => k.as_str(), None => { term_err("send: needs a type — user | timer | mouse | key "); return; } }; let msg = match kind { "user" => { let Some(n) = args.next().and_then(|s| s.parse().ok()) else { term_err("send: user needs a number"); return; }; rsnix::sched::TaskMsg::User(n) } "timer" => rsnix::sched::TaskMsg::Timer, "mouse" => { let dx: i16 = args.next().and_then(|s| s.parse().ok()).unwrap_or(0); let dy: i16 = args.next().and_then(|s| s.parse().ok()).unwrap_or(0); let btn: u8 = args.next().and_then(|s| s.parse().ok()).unwrap_or(0); rsnix::sched::TaskMsg::Mouse(rsnix::ps2::MouseEvent { dx, dy, left: btn & 1 != 0, right: btn & 2 != 0, middle: btn & 4 != 0, }) } "key" => match args.next() { Some(k) => match key_from_name(k.as_str()) { Some(ev) => rsnix::sched::TaskMsg::Key(ev), None => { term_err("send: unknown key name"); return; } }, None => { term_err("send: key needs a name"); return; } }, _ => { term_err("send: unknown type — user | timer | mouse | key "); return; } }; match rsnix::sched::send(id, msg) { Ok(()) => {} Err(e) => term_err(&alloc::format!("send: {}", e)), } } /// Map a small key-name vocabulary to a KeyEvent for `send key …`. fn key_from_name(name: &str) -> Option { use rsnix::ps2::KeyEvent; Some(match name { "enter" => KeyEvent::Enter, "esc" | "escape" => KeyEvent::Escape, "tab" => KeyEvent::Tab, "space" => KeyEvent::Char(' '), "bksp" | "backspace" => KeyEvent::Backspace, "up" => KeyEvent::ArrowUp, "down" => KeyEvent::ArrowDown, "left" => KeyEvent::ArrowLeft, "right" => KeyEvent::ArrowRight, "home" => KeyEvent::Home, "end" => KeyEvent::End, "pgup" => KeyEvent::PageUp, "pgdn" => KeyEvent::PageDown, "del" | "delete" => KeyEvent::Delete, "ctrl-c" => KeyEvent::Ctrl('c'), "ctrl-d" => KeyEvent::Ctrl('d'), "ctrl-s" => KeyEvent::Ctrl('s'), "ctrl-x" => KeyEvent::Ctrl('x'), _ => { if let Some(n) = name.strip_prefix('f') { if let Ok(n) = n.parse::() { if (1..=12).contains(&n) { KeyEvent::F(n) } else { return None; } } else { return None; } } else if name.chars().count() == 1 { KeyEvent::Char(name.chars().next().unwrap()) } else { return None; } } }) } fn cmd_sleep(p: &Parsed) { let ms: u64 = match p.args.first().and_then(|s| s.parse().ok()) { Some(m) if m > 0 => m, _ => { term_err("sleep: needs a duration in ms (e.g. sleep 2000)"); return; } }; term_println(&alloc::format!( "sleep: blocking shell for {} ms — background tasks keep running", ms )); rsnix::sched::sleep_ms(ms); term_println(&alloc::format!("sleep: woke after {} ms", ms)); } fn cmd_kill(p: &Parsed) { let id: usize = match p.args.first().and_then(|s| s.parse().ok()) { Some(id) => id, None => { term_err("kill: needs a task id (see jobs)"); return; } }; match rsnix::sched::kill(id) { Ok(()) => term_println(&alloc::format!("kill: task {} marked done", id)), Err(e) => term_err(&alloc::format!("kill: {}", e)), } } fn cmd_demo(_p: &Parsed) { let mut n = 0; for (entry, name) in [ (task_spinner as fn(), "spinner"), (task_ticker as fn(), "ticker"), (task_counter as fn(), "counter"), ] { if rsnix::sched::spawn(entry, name).is_some() { n += 1; } } term_println(&alloc::format!( "demo: spawned {} task(s) — spinner 250ms, ticker 500ms, counter 1000ms (try jobs)", n )); } // --- Demo task bodies (headless: sleep + serial/klog only) --- fn task_spinner() { let mut n: u64 = 0; loop { rsnix::serial_println!("spinner: {}", n); n += 1; rsnix::sched::sleep_ms(250); } } fn task_ticker() { let mut n: u64 = 0; loop { rsnix::serial_println!("ticker: {}", n); n += 1; rsnix::sched::sleep_ms(500); } } fn task_counter() { let mut n: u64 = 0; loop { rsnix::serial_println!("counter: {}", n); n += 1; rsnix::sched::sleep_ms(1000); } } fn task_boom() { panic!("boom: deliberate task panic"); } /// Recurses until the guard page faults — the task should die with a /// "stack overflow" panic while the shell keeps running. fn task_overflow() { #[allow(unconditional_recursion)] fn recurse(d: u64) { let keep = [0u8; 512]; core::hint::black_box(&keep); recurse(d + 1); } recurse(0); } fn cmd_pci(p: &Parsed) { let devices = rsnix::pci::probe(); let mut pipe = Pipeline::new(&p.filters, &p.output); let _ = pipe.line(&alloc::format!("pci: {} device(s)", devices.len())); let _ = pipe.line(" bus dev fn class vendor:device"); for d in &devices { let l = alloc::format!( " {:02x}:{:02x}.{} {:<16} {:04x}:{:04x}", d.bus, d.device, d.func, rsnix::pci::class_name(d.class, d.subclass), d.vendor, d.device_id ); if !pipe.line(&l) { break; } } pipe.end(); } fn cmd_poweroff() -> ! { term_println("shutting down..."); rsnix::power::poweroff() }