Create simple terminal and buffer abstractions

This commit is contained in:
Joscha 2022-05-21 19:59:07 +02:00
parent 704bb67c7b
commit bbaea3b5bf
5 changed files with 290 additions and 8 deletions

View file

@ -4,3 +4,6 @@ version = "0.1.0"
edition = "2021"
[dependencies]
crossterm = "0.23.2"
unicode-segmentation = "1.9.0"
unicode-width = "0.1.9"

35
examples/hello_world.rs Normal file
View file

@ -0,0 +1,35 @@
use std::io;
use crossterm::style::{ContentStyle, Stylize};
use toss::buffer::Pos;
use toss::terminal::Terminal;
fn main() -> io::Result<()> {
// Automatically enters alternate screen and enables raw mode
let mut term = Terminal::new(Box::new(io::stdout()))?;
// Must be called before rendering, otherwise the terminal has out-of-date
// size information and will present garbage.
term.autoresize()?;
// Render things to the buffer
let b = term.buffer();
b.write(
Pos::new(0, 0),
"Hello world!",
ContentStyle::default().green(),
);
b.write(
Pos::new(0, 1),
"Press any key to exit",
ContentStyle::default().on_dark_blue(),
);
// Show the buffer's contents on screen
term.present()?;
// Wait for input before exiting
let _ = crossterm::event::read();
Ok(())
}

164
src/buffer.rs Normal file
View file

@ -0,0 +1,164 @@
use crossterm::style::ContentStyle;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Size {
pub width: u16,
pub height: u16,
}
impl Size {
pub const ZERO: Self = Self {
width: 0,
height: 0,
};
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Pos {
pub x: i32,
pub y: i32,
}
impl Pos {
pub const ZERO: Self = Self { x: 0, y: 0 };
pub fn new(x: i32, y: i32) -> Self {
Self { x, y }
}
}
#[derive(Debug, Clone)]
pub struct Cell {
pub content: Box<str>,
pub style: ContentStyle,
pub width: u8,
pub offset: u8,
}
impl Cell {
pub fn empty() -> Self {
Self {
content: " ".to_string().into_boxed_str(),
style: ContentStyle::default(),
width: 1,
offset: 0,
}
}
}
pub struct Buffer {
size: Size,
data: Vec<Cell>,
}
impl Buffer {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
Self {
size: Size::ZERO,
data: vec![],
}
}
fn index(&self, x: u16, y: u16) -> usize {
assert!(x < self.size.width);
assert!(y < self.size.height);
let x: usize = x.into();
let y: usize = y.into();
let width: usize = self.size.width.into();
y * width + x
}
pub fn size(&self) -> Size {
self.size
}
pub fn resize(&mut self, size: Size) {
let width: usize = size.width.into();
let height: usize = size.height.into();
let len = width * height;
self.size = size;
self.data.clear();
self.data.resize_with(len, Cell::empty);
}
/// Reset all cells of the buffer to be empty and have no styling.
pub fn reset(&mut self) {
self.resize(self.size);
}
pub fn write(&mut self, mut pos: Pos, content: &str, style: ContentStyle) {
if pos.y < 0 || pos.y >= self.size.height as i32 {
return;
}
for grapheme in content.graphemes(true) {
let width = grapheme.width().max(1) as u8; // TODO Use actual width
if pos.x >= 0 && pos.x + width as i32 <= self.size.width as i32 {
// Grapheme fits on buffer in its entirety
let grapheme = grapheme.to_string().into_boxed_str();
for offset in 0..width {
let i = self.index(pos.x as u16 + offset as u16, pos.y as u16);
self.data[i] = Cell {
content: grapheme.clone(),
style,
width,
offset,
};
}
}
pos.x += width as i32;
}
}
pub fn cells(&self) -> Cells<'_> {
Cells {
buffer: self,
x: 0,
y: 0,
}
}
}
pub struct Cells<'a> {
buffer: &'a Buffer,
x: u16,
y: u16,
}
impl<'a> Cells<'a> {
fn at(&self, x: u16, y: u16) -> &'a Cell {
assert!(x < self.buffer.size.width);
assert!(y < self.buffer.size.height);
&self.buffer.data[self.buffer.index(x, y)]
}
}
impl<'a> Iterator for Cells<'a> {
type Item = (u16, u16, &'a Cell);
fn next(&mut self) -> Option<Self::Item> {
if self.y >= self.buffer.size.height {
return None;
}
let (x, y) = (self.x, self.y);
let cell = self.at(self.x, self.y);
assert!(cell.offset == 0);
self.x += cell.width as u16;
if self.x >= self.buffer.size.width {
self.x = 0;
self.y += 1;
}
Some((x, y, cell))
}
}

View file

@ -1,8 +1,2 @@
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
let result = 2 + 2;
assert_eq!(result, 4);
}
}
pub mod buffer;
pub mod terminal;

86
src/terminal.rs Normal file
View file

@ -0,0 +1,86 @@
use std::io::Write;
use std::{io, mem};
use crossterm::cursor::MoveTo;
use crossterm::style::{PrintStyledContent, StyledContent};
use crossterm::terminal::{Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen};
use crossterm::{ExecutableCommand, QueueableCommand};
use crate::buffer::{Buffer, Size};
pub struct Terminal {
out: Box<dyn Write>,
/// Currently visible on screen.
prev_buffer: Buffer,
/// Buffer to render to.
curr_buffer: Buffer,
/// When the screen is updated next, it must be cleared and redrawn fully
/// instead of performing an incremental update.
full_redraw: bool,
}
impl Drop for Terminal {
fn drop(&mut self) {
let _ = self.out.execute(LeaveAlternateScreen);
let _ = crossterm::terminal::disable_raw_mode();
}
}
impl Terminal {
pub fn new(out: Box<dyn Write>) -> io::Result<Self> {
let mut result = Self {
out,
prev_buffer: Buffer::new(),
curr_buffer: Buffer::new(),
full_redraw: true,
};
crossterm::terminal::enable_raw_mode()?;
result.out.execute(EnterAlternateScreen)?;
Ok(result)
}
pub fn buffer(&mut self) -> &mut Buffer {
&mut self.curr_buffer
}
pub fn autoresize(&mut self) -> io::Result<()> {
let (width, height) = crossterm::terminal::size()?;
let size = Size { width, height };
if size != self.curr_buffer.size() {
self.prev_buffer.resize(size);
self.curr_buffer.resize(size);
self.full_redraw = true;
}
Ok(())
}
/// Display the contents of the buffer on the screen and prepare rendering
/// the next frame.
pub fn present(&mut self) -> io::Result<()> {
if self.full_redraw {
io::stdout().queue(Clear(ClearType::All))?;
self.prev_buffer.reset();
self.full_redraw = false;
}
self.draw_differences()?;
self.out.flush()?;
mem::swap(&mut self.prev_buffer, &mut self.curr_buffer);
Ok(())
}
fn draw_differences(&mut self) -> io::Result<()> {
// TODO Only draw the differences
for (x, y, cell) in self.curr_buffer.cells() {
let content = StyledContent::new(cell.style, &cell.content as &str);
self.out
.queue(MoveTo(x, y))?
.queue(PrintStyledContent(content))?;
}
Ok(())
}
}