//! 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 { 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 TerminalServer { 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 Server for TerminalServer { 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) -> ([usize; 4], Option) { 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 { (words[0] == types::terminal::CLEAR).then_some(Request::Clear) } }