Create simple terminal and buffer abstractions
This commit is contained in:
parent
704bb67c7b
commit
bbaea3b5bf
5 changed files with 290 additions and 8 deletions
|
|
@ -4,3 +4,6 @@ version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
crossterm = "0.23.2"
|
||||||
|
unicode-segmentation = "1.9.0"
|
||||||
|
unicode-width = "0.1.9"
|
||||||
|
|
|
||||||
35
examples/hello_world.rs
Normal file
35
examples/hello_world.rs
Normal 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
164
src/buffer.rs
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/lib.rs
10
src/lib.rs
|
|
@ -1,8 +1,2 @@
|
||||||
#[cfg(test)]
|
pub mod buffer;
|
||||||
mod tests {
|
pub mod terminal;
|
||||||
#[test]
|
|
||||||
fn it_works() {
|
|
||||||
let result = 2 + 2;
|
|
||||||
assert_eq!(result, 4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
86
src/terminal.rs
Normal file
86
src/terminal.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue