305 lines
9.9 KiB
Rust
305 lines
9.9 KiB
Rust
//! 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, SavedReply};
|
|
use types::log::{text_of, text_words};
|
|
|
|
use crate::grid::Grid;
|
|
use crate::line::Line;
|
|
|
|
/// 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);
|
|
/// Moves everything drawn up by `rows` text rows, blank below.
|
|
fn scroll(&mut self, rows: usize);
|
|
/// Soon calls `TerminalServer::signal`, to flush a burst of output once.
|
|
fn flush_later(&mut self);
|
|
/// A key already typed, without waiting: a byte, `\n` for Enter, 8
|
|
/// for Backspace.
|
|
fn poll_key(&mut self) -> Option<u8>;
|
|
/// Keeps the caller of the request being handled, to answer later.
|
|
fn save_reply(&mut self) -> Option<SavedReply>;
|
|
fn answer(&mut self, reply: SavedReply, words: [usize; 4]);
|
|
fn interrupt(&mut self, notification: CapSlot);
|
|
/// Lets go of a notification it was given.
|
|
fn forget(&mut self, notification: CapSlot);
|
|
}
|
|
|
|
pub const BACKSPACE: u8 = 8;
|
|
/// Keys typed while no line is being read, kept for the next one.
|
|
const TYPEAHEAD: usize = 64;
|
|
/// Callers waiting for a line at once, at most.
|
|
const READERS: usize = 4;
|
|
|
|
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: Line,
|
|
/// Callers waiting for a line, oldest first, and whether theirs is
|
|
/// secret: the oldest gets the line being typed.
|
|
waiting: [Option<(SavedReply, bool)>; READERS],
|
|
flush_pending: bool,
|
|
/// How many cells of each row are drawn on the screen.
|
|
drawn: [usize; crate::grid::MAX_ROWS],
|
|
typeahead: [u8; TYPEAHEAD],
|
|
typeahead_len: usize,
|
|
/// Told about Ctrl+C typed while no line is being read.
|
|
on_interrupt: Option<CapSlot>,
|
|
}
|
|
|
|
/// 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: Line::new(),
|
|
waiting: [None; READERS],
|
|
flush_pending: false,
|
|
drawn: [0; crate::grid::MAX_ROWS],
|
|
typeahead: [0; TYPEAHEAD],
|
|
typeahead_len: 0,
|
|
on_interrupt: None,
|
|
};
|
|
s.redraw();
|
|
s.console.flush();
|
|
s
|
|
}
|
|
|
|
pub fn console(&self) -> &C {
|
|
&self.console
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub fn console_mut_for_test(&mut self) -> &mut C {
|
|
&mut self.console
|
|
}
|
|
|
|
pub fn grid(&self) -> &Grid {
|
|
&self.grid
|
|
}
|
|
|
|
fn redraw(&mut self) {
|
|
let scrolled = self.grid.take_scrolled();
|
|
if scrolled > 0 {
|
|
self.console.scroll(scrolled);
|
|
let rows = self.grid.rows();
|
|
self.drawn.copy_within(scrolled.min(rows)..rows, 0);
|
|
self.drawn[rows - scrolled.min(rows)..rows].fill(0);
|
|
}
|
|
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) {
|
|
// As far as there is text, or as far as was drawn before (so
|
|
// what is gone is erased): rarely the whole width.
|
|
let row = self.grid.row(r);
|
|
let text = row.iter().rposition(|b| *b != b' ').map_or(0, |i| i + 1);
|
|
let width = text.max(self.drawn[r]).min(row.len());
|
|
if width > 0 {
|
|
self.console.draw_row(r, &row[..width]);
|
|
}
|
|
self.drawn[r] = text;
|
|
}
|
|
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);
|
|
// The cursor cell is drawn too, so it is erased when it moves.
|
|
self.drawn[row] = self.drawn[row].max(col + 1);
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
/// A key typed ahead, or one just typed.
|
|
fn take_key(&mut self) -> Option<u8> {
|
|
if self.typeahead_len == 0 {
|
|
return self.console.poll_key();
|
|
}
|
|
let key = self.typeahead[0];
|
|
self.typeahead.copy_within(1..self.typeahead_len, 0);
|
|
self.typeahead_len -= 1;
|
|
Some(key)
|
|
}
|
|
|
|
/// Keys typed while nobody reads a line: Ctrl+C interrupts, the rest
|
|
/// wait for the next line.
|
|
fn keys_arrived(&mut self) {
|
|
if self.line.typing() {
|
|
if self.type_keys() {
|
|
self.serve_waiting();
|
|
}
|
|
return;
|
|
}
|
|
while let Some(key) = self.console.poll_key() {
|
|
if key == types::terminal::CTRL_C {
|
|
self.typeahead_len = 0;
|
|
self.write(b"^C\n");
|
|
if let Some(n) = self.on_interrupt {
|
|
self.console.interrupt(n);
|
|
}
|
|
} else if self.typeahead_len < TYPEAHEAD {
|
|
self.typeahead[self.typeahead_len] = key;
|
|
self.typeahead_len += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn begin_line(&mut self, secret: bool) {
|
|
self.console.flush();
|
|
// Flushed now; a flush timer still running is harmless.
|
|
self.flush_pending = false;
|
|
self.line.start(secret);
|
|
}
|
|
|
|
/// Feeds the keys there are into the line; whether it is done.
|
|
fn type_keys(&mut self) -> bool {
|
|
let mut done = false;
|
|
while !done && let Some(key) = self.take_key() {
|
|
done = self.line.key(key, &mut self.grid);
|
|
self.redraw();
|
|
self.console.flush();
|
|
}
|
|
done
|
|
}
|
|
|
|
/// The oldest waiting caller gets the finished line; once it is all
|
|
/// read, the next waiting caller's line starts.
|
|
fn serve_waiting(&mut self) {
|
|
loop {
|
|
if self.line.unread() {
|
|
let Some((reply, _)) = self.waiting[0] else { return };
|
|
self.waiting.rotate_left(1);
|
|
self.waiting[READERS - 1] = None;
|
|
let chunk = self.line.chunk();
|
|
self.console.answer(reply, chunk);
|
|
// More of it comes when it asks again.
|
|
if self.line.unread() {
|
|
return;
|
|
}
|
|
}
|
|
let Some((_, secret)) = self.waiting[0] else { return };
|
|
self.begin_line(secret);
|
|
if !self.type_keys() {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The next chunk of the typed line now, if there is one; otherwise
|
|
/// the caller is kept and answered once the line is typed.
|
|
fn read(&mut self, secret: bool) -> Option<[usize; 4]> {
|
|
if self.line.unread() {
|
|
let chunk = self.line.chunk();
|
|
if !self.line.unread() {
|
|
self.serve_waiting();
|
|
}
|
|
return Some(chunk);
|
|
}
|
|
let nobody_waits = self.waiting[0].is_none();
|
|
if nobody_waits && !self.line.typing() {
|
|
self.begin_line(secret);
|
|
}
|
|
if nobody_waits && self.type_keys() {
|
|
return Some(self.line.chunk());
|
|
}
|
|
let free = self.waiting.iter().position(Option::is_none);
|
|
match (free, free.and_then(|_| self.console.save_reply())) {
|
|
(Some(i), Some(reply)) => {
|
|
self.waiting[i] = Some((reply, secret));
|
|
None
|
|
}
|
|
// Too many waiting: an empty line.
|
|
_ => Some(text_words(b"\n")),
|
|
}
|
|
}
|
|
}
|
|
|
|
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<libkos::syscall::Give>) {
|
|
let reply = match words[0] {
|
|
// Kept for later: what goes back now is not sent.
|
|
types::terminal::READ => self.read(false).unwrap_or([0; 4]),
|
|
types::terminal::READ_SECRET => self.read(true).unwrap_or([0; 4]),
|
|
types::terminal::ON_INTERRUPT => match cap {
|
|
Some(n) if badge & types::terminal::CONTROL != 0 => {
|
|
if let Some(old) = self.on_interrupt.replace(n) {
|
|
self.console.forget(old);
|
|
}
|
|
[0; 4]
|
|
}
|
|
_ => [MALFORMED, 0, 0, 0],
|
|
},
|
|
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)
|
|
}
|
|
|
|
/// Keys, or the flush timer: show a burst of output in one frame.
|
|
fn signal(&mut self, bits: usize) {
|
|
if bits & types::terminal::KEYS_BIT != 0 {
|
|
self.keys_arrived();
|
|
}
|
|
if bits & types::terminal::TIMER_BIT != 0 {
|
|
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)
|
|
}
|
|
}
|