The Shell — NPS (Not a POSIX Shell)
With keyboard input working, we build a simple interactive shell. The name is honest about its limitations.
Design
The shell maintains a command buffer. Each keystroke either appends to the buffer, handles a special key (backspace, enter), or is ignored. On Enter, the buffer is parsed and executed.
#![allow(unused)]
fn main() {
const MAX_COMMAND_LEN: usize = 64;
pub struct NPShell {
buffer: [u8; MAX_COMMAND_LEN],
pos: usize,
}
}
Fixed-size buffer on the stack — no heap allocation needed.
Input Handling
#![allow(unused)]
fn main() {
pub fn handle_char(&mut self, ch: u8) {
match ch {
b'\n' => {
self.execute();
self.clear();
println!();
self.show_prompt();
}
0x08 => { // Backspace
if self.pos > 0 {
self.pos -= 1;
writer().backspace();
}
}
0x20..=0x7E => { // Printable ASCII
if self.pos < MAX_COMMAND_LEN - 1 {
self.buffer[self.pos] = ch;
self.pos += 1;
print!("{}", ch as char);
}
}
_ => {} // Ignore everything else
}
}
}
Command Dispatch
#![allow(unused)]
fn main() {
fn execute(&self) {
let cmd = core::str::from_utf8(&self.buffer[..self.pos]).unwrap_or("");
match cmd.trim() {
"help" => self.cmd_help(),
"stack" => self.cmd_stack(),
"gdt" => self.cmd_gdt(),
"clear" => self.cmd_clear(),
"about" => self.cmd_about(),
"reboot" => self.cmd_reboot(),
"halt" => self.cmd_halt(),
"42" => self.cmd_42(),
"" => {}
_ => println!("Unknown command: '{}'. Type 'help' for commands.", cmd),
}
}
}
core::str::from_utf8 — Convert byte buffer to string slice. Can fail if the buffer contains invalid UTF-8, hence the unwrap_or("") fallback.
Commands
| Command | Function |
|---|---|
help | List available commands |
stack | Print kernel stack contents (ESP, EBP, top 16 dwords) |
gdt | Dump all GDT entries |
clear | Clear screen |
about | Kernel information |
halt | cli; hlt — stop everything |
reboot | Pulse keyboard controller reset line |
42 | Print the obligatory 42 logo |
Reboot Implementation
#![allow(unused)]
fn main() {
fn cmd_reboot(&self) {
unsafe {
// Wait for keyboard controller to be ready
let mut port: u8;
loop {
core::arch::asm!("in al, dx", out("al") port, in("dx") 0x64u16);
if (port & 0x02) == 0 { break; }
}
// Pulse the CPU reset line via keyboard controller port
core::arch::asm!("out dx, al", in("dx") 0x64u16, in("al") 0xFEu8);
// Fallback: triple fault
core::arch::asm!("cli; hlt", options(noreturn));
}
}
}
The keyboard controller (port 0x64) has a line connected to the CPU reset pin. Writing 0xFE pulses it low, resetting the CPU. This is the standard software reboot method on x86.
Global Instance
#![allow(unused)]
fn main() {
static mut NPSHELL: NPShell = NPShell::new();
pub fn handle_input(ch: u8) {
unsafe {
let shell = &mut *core::ptr::addr_of_mut!(NPSHELL);
shell.handle_char(ch);
}
}
}
The shell is a static global, accessed via addr_of_mut! for the same reasons as the VGA writer.
The keyboard handler now calls nps::handle_input(ascii) instead of printing directly — the shell owns all display of typed characters.