feat(userspace): terminal server + shell desktop; US keymap w/ shift; libkos allocator multi-page fix; panics print
This commit is contained in:
13
crates/apps/shell/Cargo.toml
Normal file
13
crates/apps/shell/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "shell"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[[bin]]
|
||||
name = "shell"
|
||||
test = false
|
||||
bench = false
|
||||
|
||||
[dependencies]
|
||||
types = { workspace = true }
|
||||
libkos = { workspace = true }
|
||||
48
crates/apps/shell/src/main.rs
Normal file
48
crates/apps/shell/src/main.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
//! The shell, for now built-ins only; running programs comes next.
|
||||
|
||||
#![no_std]
|
||||
#![no_main]
|
||||
|
||||
extern crate libkos;
|
||||
|
||||
use libkos::{print, println};
|
||||
use types::bounded::CapSlot;
|
||||
|
||||
/// Init's manifest gives it the terminal first.
|
||||
const TERMINAL: CapSlot = CapSlot::const_new(0);
|
||||
|
||||
fn run(line: &str) {
|
||||
let mut words = line.split_ascii_whitespace();
|
||||
match words.next() {
|
||||
None => {}
|
||||
Some("help") => {
|
||||
println!("built-ins: help, echo <text>, clear");
|
||||
}
|
||||
Some("echo") => {
|
||||
let mut first = true;
|
||||
for w in words {
|
||||
print!("{}{}", if first { "" } else { " " }, w);
|
||||
first = false;
|
||||
}
|
||||
println!();
|
||||
}
|
||||
Some("clear") => libkos::terminal::clear(TERMINAL),
|
||||
Some(other) => println!("shell: {}: not found", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
extern "C" fn main() {
|
||||
// Everything this shell prints goes to the terminal.
|
||||
libkos::start::set_log(Some(TERMINAL));
|
||||
println!("k-os shell. Type 'help'.");
|
||||
let mut buf = [0u8; 256];
|
||||
loop {
|
||||
print!("$ ");
|
||||
let n = libkos::terminal::read_line(TERMINAL, &mut buf);
|
||||
match core::str::from_utf8(&buf[..n]) {
|
||||
Ok(line) => run(line.trim_end_matches('\n')),
|
||||
Err(_) => println!("shell: not text"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,16 +67,17 @@ extern "C" fn main() {
|
||||
fn handle_input(snake: &mut Snake) {
|
||||
// Drain all pending events, apply the latest direction
|
||||
loop {
|
||||
let Ok([tag, kind, _, _]) = syscall::request(DISPLAY_EP, [types::display::input::POLL, 0, 0, 0]) else {
|
||||
let Ok([tag, kind, ch, _]) = syscall::request(DISPLAY_EP, [types::display::input::POLL, 0, 0, 0]) else {
|
||||
break;
|
||||
};
|
||||
if tag == 0 { break; } // NoEvent — queue drained
|
||||
if tag == 1 {
|
||||
match kind {
|
||||
0 => snake.set_direction(Direction::Up),
|
||||
1 => snake.set_direction(Direction::Down),
|
||||
2 => snake.set_direction(Direction::Left),
|
||||
3 => snake.set_direction(Direction::Right),
|
||||
// Arrows, or WASD (kind 9 is a character).
|
||||
match (kind, ch as u8) {
|
||||
(0, _) | (9, b'w') => snake.set_direction(Direction::Up),
|
||||
(1, _) | (9, b's') => snake.set_direction(Direction::Down),
|
||||
(2, _) | (9, b'a') => snake.set_direction(Direction::Left),
|
||||
(3, _) | (9, b'd') => snake.set_direction(Direction::Right),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
56
crates/drivers/input/virtio/src/keymap.rs
Normal file
56
crates/drivers/input/virtio/src/keymap.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
//! Linux key codes (what virtio-input reports) to keys, US layout.
|
||||
|
||||
use port_input_device::Key;
|
||||
|
||||
pub const LEFT_SHIFT: u16 = 42;
|
||||
pub const RIGHT_SHIFT: u16 = 54;
|
||||
|
||||
/// Rows of the main block: the first key's code, then each key unshifted
|
||||
/// and shifted.
|
||||
const ROWS: [(u16, &[u8], &[u8]); 4] = [
|
||||
(2, b"1234567890-=", b"!@#$%^&*()_+"),
|
||||
(16, b"qwertyuiop[]", b"QWERTYUIOP{}"),
|
||||
(30, b"asdfghjkl;'`", b"ASDFGHJKL:\"~"),
|
||||
(44, b"zxcvbnm,./", b"ZXCVBNM<>?"),
|
||||
];
|
||||
|
||||
pub fn key(code: u16, shift: bool) -> Option<Key> {
|
||||
let named = match code {
|
||||
1 => Some(Key::Escape),
|
||||
14 => Some(Key::Backspace),
|
||||
15 => Some(Key::Tab),
|
||||
28 => Some(Key::Enter),
|
||||
57 => Some(Key::Space),
|
||||
103 => Some(Key::Up),
|
||||
108 => Some(Key::Down),
|
||||
105 => Some(Key::Left),
|
||||
106 => Some(Key::Right),
|
||||
43 => Some(Key::Char(if shift { b'|' } else { b'\\' })),
|
||||
_ => None,
|
||||
};
|
||||
named.or_else(|| {
|
||||
ROWS.iter().find_map(|(first, plain, shifted)| {
|
||||
let i = usize::from(code.checked_sub(*first)?);
|
||||
let row = if shift { shifted } else { plain };
|
||||
row.get(i).map(|c| Key::Char(*c))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn letters_digits_and_shift() {
|
||||
assert_eq!(key(35, false), Some(Key::Char(b'h')));
|
||||
assert_eq!(key(35, true), Some(Key::Char(b'H')));
|
||||
assert_eq!(key(11, false), Some(Key::Char(b'0')));
|
||||
assert_eq!(key(12, true), Some(Key::Char(b'_')));
|
||||
assert_eq!(key(53, false), Some(Key::Char(b'/')));
|
||||
assert_eq!(key(41, true), Some(Key::Char(b'~')));
|
||||
assert_eq!(key(28, false), Some(Key::Enter));
|
||||
assert_eq!(key(103, false), Some(Key::Up));
|
||||
assert_eq!(key(29, false), None, "left ctrl is not a character");
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
#![no_std]
|
||||
|
||||
mod keymap;
|
||||
|
||||
use hw_virtio::mmio::MmioAccess;
|
||||
use port_input_device::{InputDevice, InputEvent, Key};
|
||||
|
||||
@@ -28,6 +30,7 @@ pub struct VirtioInputDevice {
|
||||
last_used: u16,
|
||||
next_avail: u16,
|
||||
evbuf_pa: usize,
|
||||
shift: bool,
|
||||
}
|
||||
|
||||
impl VirtioInputDevice {
|
||||
@@ -45,7 +48,7 @@ impl VirtioInputDevice {
|
||||
let evbuf_pa = libkos::device::dma(1, EVBUF_VA).ok()?;
|
||||
hw_virtio::mmio::init_legacy_device(®s, vq_pa, QUEUE_SIZE as u32);
|
||||
|
||||
let mut dev = Self { last_used: 0, next_avail: 0, evbuf_pa };
|
||||
let mut dev = Self { last_used: 0, next_avail: 0, evbuf_pa, shift: false };
|
||||
dev.fill_queue();
|
||||
Some(dev)
|
||||
}
|
||||
@@ -118,25 +121,6 @@ impl VirtioInputDevice {
|
||||
}
|
||||
}
|
||||
|
||||
fn linux_keycode_to_key(code: u16) -> Option<Key> {
|
||||
match code {
|
||||
17 => Some(Key::Up), // KEY_W
|
||||
31 => Some(Key::Down), // KEY_S
|
||||
30 => Some(Key::Left), // KEY_A
|
||||
32 => Some(Key::Right), // KEY_D
|
||||
103 => Some(Key::Up), // KEY_UP
|
||||
108 => Some(Key::Down), // KEY_DOWN
|
||||
105 => Some(Key::Left), // KEY_LEFT
|
||||
106 => Some(Key::Right),// KEY_RIGHT
|
||||
28 => Some(Key::Enter), // KEY_ENTER
|
||||
1 => Some(Key::Escape), // KEY_ESC
|
||||
57 => Some(Key::Space), // KEY_SPACE
|
||||
14 => Some(Key::Backspace), // KEY_BACKSPACE
|
||||
15 => Some(Key::Tab), // KEY_TAB
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
impl InputDevice for VirtioInputDevice {
|
||||
fn poll(&mut self) -> Option<InputEvent> {
|
||||
loop {
|
||||
@@ -151,8 +135,12 @@ impl InputDevice for VirtioInputDevice {
|
||||
let (ev_type, ev_code, ev_value) = self.read_event(desc_idx);
|
||||
self.resubmit(desc_idx);
|
||||
|
||||
if ev_type == EV_KEY && matches!(ev_code, keymap::LEFT_SHIFT | keymap::RIGHT_SHIFT) {
|
||||
self.shift = ev_value != 0;
|
||||
continue;
|
||||
}
|
||||
if ev_type == EV_KEY {
|
||||
if let Some(key) = linux_keycode_to_key(ev_code) {
|
||||
if let Some(key) = keymap::key(ev_code, self.shift) {
|
||||
if ev_value == KEY_PRESS {
|
||||
return Some(InputEvent::KeyDown(key));
|
||||
} else if ev_value == 0 {
|
||||
|
||||
@@ -60,6 +60,8 @@ pub const SERVERS: &[Program] = &[
|
||||
// The framebuffer alone is ~1000 pages at 1280x800.
|
||||
..server("display", b"display.elf", Limits { pages: 1280, regions: 4, ..SMALL })
|
||||
},
|
||||
// Text on the display, lines from the keyboard (it takes input focus).
|
||||
Program { uses: &["display"], ..server("terminal", b"terminal.elf", SMALL) },
|
||||
server("permis", b"permis.elf", SMALL),
|
||||
server("session", b"session.elf", SMALL),
|
||||
];
|
||||
@@ -76,6 +78,6 @@ pub const TESTS: &[Program] = &[Program {
|
||||
/// The desktop; the test image runs the SMP self-test instead.
|
||||
pub const APPS: &[Program] = &[Program {
|
||||
serves: false,
|
||||
uses: &["display"],
|
||||
..server("snake", b"snake.elf", Limits { pages: 128, ..SMALL })
|
||||
uses: &["terminal"],
|
||||
..server("shell", b"shell.elf", SMALL)
|
||||
}];
|
||||
|
||||
@@ -39,22 +39,21 @@ impl KosAllocator {
|
||||
}
|
||||
|
||||
impl AllocatorInner {
|
||||
fn request_page(&mut self) -> *mut u8 {
|
||||
if self.page_count >= MAX_PAGES {
|
||||
/// `n` pages in one piece, so an allocation larger than a page fits.
|
||||
fn request_pages(&mut self, n: usize) -> *mut u8 {
|
||||
if self.page_count + n > MAX_PAGES {
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
let count = match types::bounded::PageCount::new(1) {
|
||||
Some(c) => c,
|
||||
None => return ptr::null_mut(),
|
||||
let Some(count) = u16::try_from(n).ok().and_then(types::bounded::PageCount::new) else {
|
||||
return ptr::null_mut();
|
||||
};
|
||||
let va = match crate::syscall::request_pages(count) {
|
||||
Some(va) => va,
|
||||
None => return ptr::null_mut(),
|
||||
let Some(va) = crate::syscall::request_pages(count) else {
|
||||
return ptr::null_mut();
|
||||
};
|
||||
|
||||
self.pages[self.page_count] = va.raw();
|
||||
self.page_count += 1;
|
||||
if let Some(slot) = self.pages.get_mut(self.page_count) {
|
||||
*slot = va.raw();
|
||||
}
|
||||
self.page_count += n;
|
||||
va.raw() as *mut u8
|
||||
}
|
||||
|
||||
@@ -125,14 +124,13 @@ unsafe impl GlobalAlloc for KosAllocator {
|
||||
return ptr;
|
||||
}
|
||||
|
||||
let pages_needed = (size + PAGE_SIZE - 1) / PAGE_SIZE;
|
||||
for _ in 0..pages_needed {
|
||||
let page = inner.request_page();
|
||||
if page.is_null() {
|
||||
return ptr::null_mut();
|
||||
}
|
||||
inner.add_to_free_list(page, PAGE_SIZE);
|
||||
// Alignment can cost up to `align` bytes of the block.
|
||||
let pages_needed = (size + align + PAGE_SIZE - 1) / PAGE_SIZE;
|
||||
let block = inner.request_pages(pages_needed);
|
||||
if block.is_null() {
|
||||
return ptr::null_mut();
|
||||
}
|
||||
inner.add_to_free_list(block, pages_needed * PAGE_SIZE);
|
||||
|
||||
inner.alloc_from_free_list(size, align)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ pub mod io;
|
||||
mod rt;
|
||||
pub mod spawn;
|
||||
pub mod start;
|
||||
pub mod terminal;
|
||||
pub mod syscall;
|
||||
|
||||
#[cfg(target_os = "none")]
|
||||
|
||||
@@ -18,6 +18,7 @@ _start:
|
||||
|
||||
#[cfg(target_os = "none")]
|
||||
#[panic_handler]
|
||||
fn panic(_: &core::panic::PanicInfo) -> ! {
|
||||
fn panic(info: &core::panic::PanicInfo) -> ! {
|
||||
crate::println!("panic: {}", info);
|
||||
crate::syscall::exit();
|
||||
}
|
||||
|
||||
29
crates/libkos/src/terminal.rs
Normal file
29
crates/libkos/src/terminal.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
//! The terminal (`types::terminal`). Output is the log protocol, so a
|
||||
//! process prints to a terminal by making it its log (`start::set_log`).
|
||||
|
||||
use crate::syscall;
|
||||
use types::bounded::CapSlot;
|
||||
|
||||
/// Reads a typed line into `buf`, newline included if it fits; how much.
|
||||
pub fn read_line(terminal: CapSlot, buf: &mut [u8]) -> usize {
|
||||
let mut n = 0;
|
||||
while n < buf.len() {
|
||||
let Ok(words) = syscall::request(terminal, [types::terminal::READ, 0, 0, 0]) else {
|
||||
break;
|
||||
};
|
||||
let Some((bytes, len)) = types::log::text_of(words) else {
|
||||
break;
|
||||
};
|
||||
let take = len.min(buf.len() - n);
|
||||
buf[n..n + take].copy_from_slice(&bytes[..take]);
|
||||
n += take;
|
||||
if bytes[..len].last() == Some(&b'\n') || len == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
n
|
||||
}
|
||||
|
||||
pub fn clear(terminal: CapSlot) {
|
||||
let _ = syscall::request(terminal, [types::terminal::CLEAR, 0, 0, 0]);
|
||||
}
|
||||
14
crates/servers/terminal-server/Cargo.toml
Normal file
14
crates/servers/terminal-server/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "terminal-server"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[[bin]]
|
||||
name = "terminal-server"
|
||||
test = false
|
||||
bench = false
|
||||
|
||||
[dependencies]
|
||||
types = { workspace = true }
|
||||
libkos = { workspace = true }
|
||||
server-framework = { workspace = true }
|
||||
89
crates/servers/terminal-server/src/grid.rs
Normal file
89
crates/servers/terminal-server/src/grid.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
90
crates/servers/terminal-server/src/lib.rs
Normal file
90
crates/servers/terminal-server/src/lib.rs
Normal 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)
|
||||
}
|
||||
9
crates/servers/terminal-server/src/main.rs
Normal file
9
crates/servers/terminal-server/src/main.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
#![no_std]
|
||||
#![no_main]
|
||||
|
||||
extern crate libkos;
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
extern "C" fn main() {
|
||||
terminal_server::run();
|
||||
}
|
||||
184
crates/servers/terminal-server/src/server.rs
Normal file
184
crates/servers/terminal-server/src/server.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
38
crates/servers/terminal-server/src/tests/grid.rs
Normal file
38
crates/servers/terminal-server/src/tests/grid.rs
Normal 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);
|
||||
}
|
||||
2
crates/servers/terminal-server/src/tests/mod.rs
Normal file
2
crates/servers/terminal-server/src/tests/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
mod grid;
|
||||
mod server;
|
||||
70
crates/servers/terminal-server/src/tests/server.rs
Normal file
70
crates/servers/terminal-server/src/tests/server.rs
Normal 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");
|
||||
}
|
||||
@@ -74,7 +74,14 @@ pub mod text {
|
||||
let len = s.len().min(TEXT_BYTES);
|
||||
let mut bytes = [0u8; TEXT_BYTES];
|
||||
bytes[..len].copy_from_slice(&s[..len]);
|
||||
Self { x, y, fg, bg, bytes, len }
|
||||
Self {
|
||||
x,
|
||||
y,
|
||||
fg,
|
||||
bg,
|
||||
bytes,
|
||||
len,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn chars(&self) -> &[u8] {
|
||||
@@ -85,7 +92,9 @@ pub mod text {
|
||||
let (lo, hi) = self.bytes.as_chunks::<8>().0.split_at(1);
|
||||
[
|
||||
TEXT | self.len << 8 | (self.fg.raw() as usize) << 16,
|
||||
self.x.raw() as usize | (self.y.raw() as usize) << 16 | (self.bg.raw() as usize) << 32,
|
||||
self.x.raw() as usize
|
||||
| (self.y.raw() as usize) << 16
|
||||
| (self.bg.raw() as usize) << 32,
|
||||
u64::from_le_bytes(lo[0]) as usize,
|
||||
u64::from_le_bytes(hi[0]) as usize,
|
||||
]
|
||||
@@ -117,7 +126,13 @@ pub mod text {
|
||||
|
||||
#[test]
|
||||
fn text_round_trips() {
|
||||
let t = Text::new(ScreenCoord::new(1272), ScreenCoord::new(784), Color::rgb(1, 2, 3), Color::rgb(4, 5, 6), b"$ ls -la /home/x");
|
||||
let t = Text::new(
|
||||
ScreenCoord::new(1272),
|
||||
ScreenCoord::new(784),
|
||||
Color::rgb(1, 2, 3),
|
||||
Color::rgb(4, 5, 6),
|
||||
b"$ ls -la /home/x",
|
||||
);
|
||||
assert_eq!(Text::from_words(t.words()), Some(t));
|
||||
assert_eq!(Text::new(t.x, t.y, t.fg, t.bg, &[b'a'; 40]).len, TEXT_BYTES);
|
||||
}
|
||||
|
||||
@@ -19,4 +19,5 @@ pub mod spawn;
|
||||
pub mod start_arg;
|
||||
pub mod storage;
|
||||
pub mod syscall_abi;
|
||||
pub mod terminal;
|
||||
pub mod time;
|
||||
|
||||
7
crates/types/src/terminal.rs
Normal file
7
crates/types/src/terminal.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
//! The terminal's requests beyond text output, which is the log protocol
|
||||
//! (`types::log`): a program given the terminal as its log prints there.
|
||||
|
||||
/// Reply: the next chunk of a typed line as a text message (`types::log`);
|
||||
/// the line's last chunk ends with `\n`. Blocks until a line is entered.
|
||||
pub const READ: usize = 10;
|
||||
pub const CLEAR: usize = 11;
|
||||
Reference in New Issue
Block a user