summaryrefslogtreecommitdiffstats
path: root/shell
diff options
context:
space:
mode:
Diffstat (limited to 'shell')
-rw-r--r--shell/Cargo.toml7
-rw-r--r--shell/src/lib.rs285
2 files changed, 292 insertions, 0 deletions
diff --git a/shell/Cargo.toml b/shell/Cargo.toml
new file mode 100644
index 0000000..9fd10bc
--- /dev/null
+++ b/shell/Cargo.toml
@@ -0,0 +1,7 @@
1[package]
2name = "shell"
3version.workspace = true
4edition.workspace = true
5
6[dependencies]
7rsnix = { path = "../rsnix" }
diff --git a/shell/src/lib.rs b/shell/src/lib.rs
new file mode 100644
index 0000000..bdd79c6
--- /dev/null
+++ b/shell/src/lib.rs
@@ -0,0 +1,285 @@
1#![no_std]
2
3extern crate alloc;
4
5/// Minimal command shell — reads from PS/2 keyboard, writes to GOP + serial.
6use rsnix::ps2::{self, KeyEvent};
7use rsnix::{gop_print, gop_println, serial_print, serial_println};
8
9/// Maximum input line length.
10const LINE_BUF_SIZE: usize = 256;
11
12/// Run the shell forever.
13pub fn run() -> ! {
14 gop_println!(
15 "rs-dos v{} — type 'help' for commands",
16 env!("CARGO_PKG_VERSION")
17 );
18 serial_println!(
19 "========== rs-dos v{} ==========",
20 env!("CARGO_PKG_VERSION")
21 );
22
23 loop {
24 // Print prompt
25 gop_print!("> ");
26 serial_print!("> ");
27
28 // Read one line from keyboard
29 let line = read_line();
30
31 // Parse and execute
32 let trimmed = line.trim();
33 if trimmed.is_empty() {
34 continue;
35 }
36
37 let (cmd, args) = split_first_word(trimmed);
38
39 match cmd {
40 "help" => cmd_help(),
41 "echo" => cmd_echo(args),
42 "clear" => cmd_clear(),
43 "halt" | "poweroff" | "exit" => cmd_halt(),
44 "hello" => {
45 gop_println!("World!");
46 serial_println!("World!");
47 }
48 "info" => cmd_info(),
49 "mem" => cmd_mem(),
50 _ => {
51 gop_println!("unknown command: {}", cmd);
52 serial_println!("unknown command: {}", cmd);
53 }
54 }
55 }
56}
57
58// ---------------------------------------------------------------------------
59// Line input (PS/2 keyboard, no heap)
60// ---------------------------------------------------------------------------
61fn read_line() -> heapless_str::String<LINE_BUF_SIZE> {
62 let mut buf = heapless_str::String::<LINE_BUF_SIZE>::new();
63 loop {
64 match ps2::poll_key() {
65 KeyEvent::Enter => {
66 rsnix::gop::put_char('\n');
67 rsnix::serial::write_str("\r\n");
68 return buf;
69 }
70 KeyEvent::Char(c) if c.is_ascii_graphic() || c == ' ' => {
71 if buf.push(c).is_ok() {
72 rsnix::gop::put_char(c);
73 // Echo to serial too
74 let mut s = [0u8; 4];
75 let s = c.encode_utf8(&mut s);
76 rsnix::serial::write_str(s);
77 }
78 }
79 KeyEvent::Backspace => {
80 if buf.pop().is_some() {
81 // GOP backspace: move cursor back and erase
82 rsnix::gop::put_char('\x08');
83 // Serial backspace: send BS SP BS (standard terminal protocol)
84 rsnix::serial::write_str("\x08 \x08");
85 }
86 }
87 KeyEvent::Tab => {
88 for _ in 0..4 {
89 if buf.push(' ').is_ok() {
90 rsnix::gop::put_char(' ');
91 rsnix::serial::write_str(" ");
92 }
93 }
94 }
95 _ => {}
96 }
97 }
98}
99
100/// Split a command line into the first word and the rest.
101fn split_first_word(s: &str) -> (&str, &str) {
102 let s = s.trim_start();
103 if let Some(i) = s.find(char::is_whitespace) {
104 (&s[..i], s[i..].trim_start())
105 } else {
106 (s, "")
107 }
108}
109
110// ---------------------------------------------------------------------------
111// Built-in commands
112// ---------------------------------------------------------------------------
113
114fn cmd_help() {
115 gop_println!("available commands:");
116 gop_println!(" help — show this message");
117 gop_println!(" echo — echo arguments");
118 gop_println!(" clear — clear screen");
119 gop_println!(" hello — print 'World!'");
120 gop_println!(" info — show system info");
121 gop_println!(" mem — show memory info");
122 gop_println!(" halt — halt the system");
123 gop_println!(" exit — alias for halt");
124 serial_println!("available commands:");
125 serial_println!(" help, echo, clear, hello, info, mem, halt, exit");
126}
127
128fn cmd_echo(args: &str) {
129 gop_println!("{}", args);
130}
131
132fn cmd_clear() {
133 rsnix::gop::clear_screen();
134}
135
136fn cmd_info() {
137 // Test that the global allocator works: build a String on the heap
138 let mut s = alloc::string::String::from("rs-dos v");
139 s.push_str(env!("CARGO_PKG_VERSION"));
140 s.push_str(" — rsnix kernel");
141 gop_println!("{}", s);
142 gop_println!("arch: x86_64, freestanding, no userspace");
143 serial_println!("{}", s);
144 serial_println!("arch: x86_64, freestanding, no userspace");
145 // String is freed (dropped) when this scope ends — tests dealloc too
146}
147
148fn cmd_mem() {
149 let (map, key) = rsnix::memory::get_map();
150
151 let mut total_kb = 0u64;
152 let mut conventional_kb = 0u64;
153 let mut loader_kb = 0u64;
154 let mut boot_code_kb = 0u64;
155 let mut boot_data_kb = 0u64;
156 let mut reserved_kb = 0u64;
157 let mut acpi_kb = 0u64;
158 let mut mmio_kb = 0u64;
159
160 for desc in map {
161 let pages = desc.number_of_pages;
162 let bytes_4k = pages * 4;
163 total_kb += bytes_4k;
164 match desc.kind {
165 1 => reserved_kb += bytes_4k,
166 3 => boot_code_kb += bytes_4k,
167 4 => boot_data_kb += bytes_4k,
168 5 | 10 => acpi_kb += bytes_4k,
169 6 => loader_kb += bytes_4k,
170 7 => conventional_kb += bytes_4k,
171 11 | 12 => mmio_kb += bytes_4k,
172 _ => reserved_kb += bytes_4k,
173 }
174 }
175
176 // Build a big heap string with format! to test the allocator
177 let mut out = alloc::string::String::new();
178 use alloc::fmt::Write;
179 let _ = writeln!(out, "memory map (key=0x{:x}, {} entries)", key, map.len());
180 let _ = writeln!(
181 out,
182 " usable {:4} MB Conventional",
183 conventional_kb / 1024
184 );
185 let _ = writeln!(
186 out,
187 " boot code {:4} MB BootServicesCode",
188 boot_code_kb / 1024
189 );
190 let _ = writeln!(
191 out,
192 " boot data {:4} MB BootServicesData",
193 boot_data_kb / 1024
194 );
195 let _ = writeln!(out, " loader {:4} MB LoaderData", loader_kb / 1024);
196 let _ = writeln!(out, " ACPI {:4} MB", acpi_kb / 1024);
197 let _ = writeln!(out, " MMIO {:4} MB", mmio_kb / 1024);
198 let _ = writeln!(out, " reserved {:4} MB", reserved_kb / 1024);
199 let _ = writeln!(
200 out,
201 " raw total {:4} MB (incl. PCIe MMIO)",
202 total_kb / 1024
203 );
204
205 gop_print!("{}", out);
206 serial_print!("{}", out);
207}
208
209fn cmd_halt() -> ! {
210 gop_println!("halting...");
211 serial_println!("halting...");
212 loop {
213 unsafe {
214 core::arch::asm!("cli; hlt");
215 }
216 }
217}
218
219// ---------------------------------------------------------------------------
220// Minimal fixed-capacity string for no-stack use (avoids alloc dependency)
221// ---------------------------------------------------------------------------
222mod heapless_str {
223 use core::fmt;
224
225 /// A fixed-capacity string allocated entirely on the stack.
226 pub struct String<const N: usize> {
227 buf: [u8; N],
228 len: usize,
229 }
230
231 impl<const N: usize> String<N> {
232 pub const fn new() -> Self {
233 String {
234 buf: [0u8; N],
235 len: 0,
236 }
237 }
238
239 pub fn as_str(&self) -> &str {
240 // SAFETY: we only write valid ASCII bytes.
241 unsafe { core::str::from_utf8_unchecked(&self.buf[..self.len]) }
242 }
243
244 pub fn push(&mut self, c: char) -> Result<(), ()> {
245 if self.len >= N {
246 return Err(());
247 }
248 self.buf[self.len] = c as u8;
249 self.len += 1;
250 Ok(())
251 }
252
253 pub fn pop(&mut self) -> Option<char> {
254 if self.len == 0 {
255 return None;
256 }
257 self.len -= 1;
258 Some(self.buf[self.len] as char)
259 }
260
261 pub fn is_empty(&self) -> bool {
262 self.len == 0
263 }
264
265 pub fn clear(&mut self) {
266 self.len = 0;
267 }
268
269 pub fn trim(&self) -> &str {
270 self.as_str().trim()
271 }
272 }
273
274 impl<const N: usize> fmt::Display for String<N> {
275 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
276 write!(f, "{}", self.as_str())
277 }
278 }
279
280 impl<const N: usize> fmt::Debug for String<N> {
281 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
282 write!(f, "{:?}", self.as_str())
283 }
284 }
285}