feat(userspace): terminal server + shell desktop; US keymap w/ shift; libkos allocator multi-page fix; panics print

This commit is contained in:
2026-10-27 16:50:35 +01:00
parent 2845d190f0
commit b80303f553
26 changed files with 739 additions and 53 deletions

View File

@@ -0,0 +1,89 @@
//! The terminal's text: a grid of cells, a cursor, and which rows changed.
use alloc::vec;
use alloc::vec::Vec;
pub const MAX_COLS: usize = 160;
pub const MAX_ROWS: usize = 64;
/// On the heap: a full screen is more than a program stack holds.
pub struct Grid {
cells: Vec<u8>,
cols: usize,
rows: usize,
pub col: usize,
pub row: usize,
/// Rows to redraw.
dirty: Vec<bool>,
}
impl Grid {
pub fn new(cols: usize, rows: usize) -> Self {
let (cols, rows) = (cols.clamp(1, MAX_COLS), rows.clamp(1, MAX_ROWS));
Self { cells: vec![b' '; cols * rows], cols, rows, col: 0, row: 0, dirty: vec![true; rows] }
}
pub fn row(&self, r: usize) -> &[u8] {
&self.cells[r * self.cols..(r + 1) * self.cols]
}
pub fn rows(&self) -> usize {
self.rows
}
/// The rows changed since the last call, each once.
pub fn take_dirty(&mut self) -> impl Iterator<Item = usize> + '_ {
let rows = self.rows;
self.dirty[..rows].iter_mut().enumerate().filter_map(|(r, d)| core::mem::take(d).then_some(r))
}
fn mark(&mut self, r: usize) {
self.dirty[r] = true;
}
fn newline(&mut self) {
// The row the cursor leaves, so its cursor cell is redrawn.
self.mark(self.row);
self.col = 0;
if self.row + 1 < self.rows {
self.row += 1;
return;
}
// At the bottom: everything moves up a row.
self.cells.copy_within(self.cols.., 0);
let last = (self.rows - 1) * self.cols;
self.cells[last..].fill(b' ');
self.dirty.fill(true);
}
/// Writes one byte at the cursor: `\n` moves to the next line, other
/// bytes wrap at the right edge.
pub fn put(&mut self, b: u8) {
if b == b'\n' {
self.newline();
return;
}
if self.col == self.cols {
self.newline();
}
self.cells[self.row * self.cols + self.col] = b;
self.mark(self.row);
self.col += 1;
}
/// Erases the cell before the cursor, on this row only.
pub fn back(&mut self) {
if self.col > 0 {
self.col -= 1;
self.cells[self.row * self.cols + self.col] = b' ';
self.mark(self.row);
}
}
pub fn clear(&mut self) {
self.cells.fill(b' ');
self.col = 0;
self.row = 0;
self.dirty.fill(true);
}
}

View File

@@ -0,0 +1,90 @@
#![no_std]
extern crate alloc;
pub mod grid;
pub mod server;
#[cfg(test)]
#[path = "tests/mod.rs"]
mod tests;
use libkos::display::{self, CELL_HEIGHT, CELL_WIDTH};
use libkos::input::{InputEvent, Key};
use libkos::syscall;
use types::bounded::CapSlot;
use types::display::{Color, ScreenCoord};
/// Where init put them (its manifest: own endpoint, then `uses`).
const EP: CapSlot = CapSlot::const_new(0);
const DISPLAY: CapSlot = CapSlot::const_new(1);
const FG: Color = Color::rgb(220, 220, 220);
const BG: Color = Color::rgb(16, 16, 24);
/// Output is shown at most this long after it is written. The timebase is
/// QEMU virt's 10 MHz.
const FLUSH_DELAY: u64 = 20 * 10_000;
struct Screen {
keys: CapSlot,
/// Bound to the serving thread; the flush timer signals it.
timer: CapSlot,
}
fn at(col: usize, row: usize) -> (ScreenCoord, ScreenCoord) {
(ScreenCoord::new((col * CELL_WIDTH) as u16), ScreenCoord::new((row * CELL_HEIGHT) as u16))
}
impl server::Console for Screen {
fn draw_row(&mut self, row: usize, text: &[u8]) {
let (x, y) = at(0, row);
display::draw_text(DISPLAY, x, y, FG, BG, text);
}
fn draw_cursor(&mut self, col: usize, row: usize, under: u8) {
let (x, y) = at(col, row);
display::draw_text(DISPLAY, x, y, BG, FG, &[under]);
}
fn flush(&mut self) {
display::flush(DISPLAY);
}
fn flush_later(&mut self) {
let at = syscall::get_time().raw() + FLUSH_DELAY;
let _ = syscall::set_timer(self.timer, types::time::Ticks::from_raw(at));
}
fn next_key(&mut self) -> u8 {
loop {
while let Some(event) = display::poll_input(DISPLAY) {
let InputEvent::KeyDown(key) = event else { continue };
match key {
Key::Char(c) => return c,
Key::Space | Key::Tab => return b' ',
Key::Enter => return b'\n',
Key::Backspace => return server::BACKSPACE,
_ => {}
}
}
let _ = syscall::wait(self.keys);
}
}
}
pub fn run() -> ! {
let Some(keys) = display::subscribe_input(DISPLAY) else {
libkos::println!("terminal: no input focus, exiting");
syscall::exit();
};
let Some(timer) = syscall::create_notification().ok().filter(|t| syscall::bind_notification(*t).is_ok()) else {
libkos::println!("terminal: no timer notification, exiting");
syscall::exit();
};
let (w, h) = display::get_dimensions(DISPLAY);
display::clear(DISPLAY, BG);
let cols = w.raw() as usize / CELL_WIDTH;
let rows = h.raw() as usize / CELL_HEIGHT;
let mut srv = server::TerminalServer::new(Screen { keys, timer }, cols, rows);
server_framework::serve(&mut srv, EP)
}

View File

@@ -0,0 +1,9 @@
#![no_std]
#![no_main]
extern crate libkos;
#[unsafe(no_mangle)]
extern "C" fn main() {
terminal_server::run();
}

View File

@@ -0,0 +1,184 @@
//! Text out (the log protocol) and lines in, on a screen and keyboard
//! behind `Console`.
use server_framework::{Encode, MALFORMED, Server};
use types::bounded::CapSlot;
use types::log::{TEXT_BYTES, text_of, text_words};
use crate::grid::Grid;
/// What the terminal draws on and reads from.
pub trait Console {
fn draw_row(&mut self, row: usize, text: &[u8]);
/// Shows the cursor at a cell, over what `draw_row` drew there.
fn draw_cursor(&mut self, col: usize, row: usize, under: u8);
fn flush(&mut self);
/// Soon calls `TerminalServer::signal`, to flush a burst of output once.
fn flush_later(&mut self);
/// Blocks until a key is typed: a byte, `\n` for Enter, 8 for Backspace.
fn next_key(&mut self) -> u8;
}
pub const BACKSPACE: u8 = 8;
const MAX_LINE: usize = 256;
pub struct TerminalServer<C: Console> {
console: C,
grid: Grid,
/// A typed line not yet handed out in full, and how much of it was.
line: [u8; MAX_LINE],
line_len: usize,
line_read: usize,
flush_pending: bool,
}
/// An empty reply, whose tag is not text.
pub struct Done;
impl Encode for Done {
fn encode(&self) -> [usize; 4] {
[0; 4]
}
}
impl<C: Console> TerminalServer<C> {
pub fn new(console: C, cols: usize, rows: usize) -> Self {
let mut s = Self {
console,
grid: Grid::new(cols, rows),
line: [0; MAX_LINE],
line_len: 0,
line_read: 0,
flush_pending: false,
};
s.redraw();
s.console.flush();
s
}
pub fn console(&self) -> &C {
&self.console
}
pub fn grid(&self) -> &Grid {
&self.grid
}
fn redraw(&mut self) {
let mut dirty = [false; crate::grid::MAX_ROWS];
for r in self.grid.take_dirty() {
dirty[r] = true;
}
for (r, _) in dirty.iter().enumerate().filter(|(_, d)| **d) {
self.console.draw_row(r, self.grid.row(r));
}
let (col, row) = (self.grid.col, self.grid.row);
let under = self.grid.row(row).get(col).copied().unwrap_or(b' ');
self.console.draw_cursor(col, row, under);
}
fn write(&mut self, bytes: &[u8]) {
for &b in bytes {
self.grid.put(b);
}
self.redraw();
if !self.flush_pending {
self.flush_pending = true;
self.console.flush_later();
}
}
/// Edits a line from the keyboard, echoing it, until Enter.
fn read_line(&mut self) {
self.console.flush();
self.line_len = 0;
self.line_read = 0;
loop {
match self.console.next_key() {
b'\n' => {
self.line[self.line_len] = b'\n';
self.line_len += 1;
self.grid.put(b'\n');
break;
}
BACKSPACE if self.line_len > 0 => {
self.line_len -= 1;
self.grid.back();
}
BACKSPACE => continue,
// Room is kept for the closing newline.
b if self.line_len + 1 < MAX_LINE && (b' '..=b'~').contains(&b) => {
self.line[self.line_len] = b;
self.line_len += 1;
self.grid.put(b);
}
_ => continue,
}
self.redraw();
self.console.flush();
}
self.redraw();
self.console.flush();
}
/// The next chunk of the typed line, reading one first if none is left.
fn read(&mut self) -> [usize; 4] {
if self.line_read == self.line_len {
self.read_line();
}
let end = (self.line_read + TEXT_BYTES).min(self.line_len);
let chunk = &self.line[self.line_read..end];
self.line_read = end;
text_words(chunk)
}
}
impl<C: Console> Server for TerminalServer<C> {
type Request = Request;
type Response = Done;
fn handle(&mut self, request: Request) -> Done {
match request {
Request::Clear => {
self.grid.clear();
self.redraw();
self.console.flush();
}
}
Done
}
fn handle_call(&mut self, _badge: usize, words: [usize; 4], _cap: Option<CapSlot>) -> ([usize; 4], Option<CapSlot>) {
let reply = match words[0] {
types::terminal::READ => self.read(),
types::terminal::CLEAR => {
self.handle(Request::Clear);
[0; 4]
}
_ => match text_of(words) {
Some((bytes, len)) => {
self.write(&bytes[..len]);
[0; 4]
}
None => [MALFORMED, 0, 0, 0],
},
};
(reply, None)
}
/// The flush timer: show a burst of output in one frame.
fn signal(&mut self, _bits: usize) {
self.flush_pending = false;
self.console.flush();
}
}
pub enum Request {
Clear,
}
impl server_framework::Decode for Request {
fn decode(words: [usize; 4]) -> Option<Self> {
(words[0] == types::terminal::CLEAR).then_some(Request::Clear)
}
}

View File

@@ -0,0 +1,38 @@
use crate::grid::Grid;
fn text(g: &Grid, r: usize) -> &str {
core::str::from_utf8(g.row(r)).unwrap().trim_end()
}
#[test]
fn text_wraps_at_the_edge_and_newlines_move_down() {
let mut g = Grid::new(4, 3);
for b in b"abcdef\nx" {
g.put(*b);
}
assert_eq!((text(&g, 0), text(&g, 1), text(&g, 2)), ("abcd", "ef", "x"));
assert_eq!((g.col, g.row), (1, 2));
}
#[test]
fn the_bottom_line_scrolls_everything_up() {
let mut g = Grid::new(4, 2);
for b in b"one\ntwo\nsix" {
g.put(*b);
}
assert_eq!((text(&g, 0), text(&g, 1)), ("two", "six"));
let _ = g.take_dirty().count();
g.put(b'\n');
assert_eq!(g.take_dirty().count(), 2, "a scroll redraws every row");
}
#[test]
fn backspace_stops_at_the_row_start_and_dirty_rows_come_once() {
let mut g = Grid::new(4, 2);
g.put(b'a');
g.back();
g.back();
assert_eq!((text(&g, 0), g.col), ("", 0));
let _ = g.take_dirty().count();
assert_eq!(g.take_dirty().count(), 0);
}

View File

@@ -0,0 +1,2 @@
mod grid;
mod server;

View File

@@ -0,0 +1,70 @@
extern crate std;
use crate::server::{BACKSPACE, Console, TerminalServer};
use server_framework::Server;
use std::vec::Vec;
use types::log::{text_of, text_words};
#[derive(Default)]
struct Fake {
keys: Vec<u8>,
flushes: usize,
timers: usize,
}
impl Console for Fake {
fn draw_row(&mut self, _row: usize, _text: &[u8]) {}
fn draw_cursor(&mut self, _col: usize, _row: usize, _under: u8) {}
fn flush(&mut self) {
self.flushes += 1;
}
fn flush_later(&mut self) {
self.timers += 1;
}
fn next_key(&mut self) -> u8 {
self.keys.remove(0)
}
}
fn term(keys: &[u8]) -> TerminalServer<Fake> {
TerminalServer::new(Fake { keys: keys.to_vec(), ..Fake::default() }, 40, 10)
}
fn read(t: &mut TerminalServer<Fake>) -> Vec<u8> {
let (words, _) = t.handle_call(1, [types::terminal::READ, 0, 0, 0], None);
let (bytes, len) = text_of(words).unwrap();
bytes[..len].to_vec()
}
#[test]
fn writes_land_on_the_grid_and_flush_once_per_burst() {
let mut t = term(&[]);
let before = t.console().flushes;
for chunk in [&b"hello "[..], b"world"] {
t.handle_call(1, text_words(chunk), None);
}
assert_eq!(&t.grid().row(0)[..11], b"hello world");
assert_eq!((t.console().timers, t.console().flushes), (1, before));
t.signal(1);
assert_eq!(t.console().flushes, before + 1);
}
#[test]
fn a_read_returns_the_edited_line_in_chunks() {
let mut keys = b"ls -lx".to_vec();
keys.push(BACKSPACE);
keys.extend_from_slice(b"a /home/someone/very/long\n");
let mut t = term(&keys);
let mut line = read(&mut t);
while line.last() != Some(&b'\n') {
line.extend(read(&mut t));
}
assert_eq!(line, b"ls -la /home/someone/very/long\n");
assert_eq!(&t.grid().row(0)[..6], b"ls -la");
}
#[test]
fn backspace_on_an_empty_line_does_nothing() {
let mut t = term(&[BACKSPACE, b'x', b'\n']);
assert_eq!(read(&mut t), b"x\n");
}