summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/config.rs19
-rw-r--r--src/main.rs266
-rw-r--r--src/structs.rs495
3 files changed, 578 insertions, 202 deletions
diff --git a/src/config.rs b/src/config.rs
new file mode 100644
index 0000000..bdd3ba3
--- /dev/null
+++ b/src/config.rs
@@ -0,0 +1,19 @@
+// bytes that make up a `.ttf` file
+pub const FONT: &[u8] = std::include_bytes!("../fonts/Miracode.ttf");
+
+// model parameters
+// standard for vte: 80, 24, and whatever happens to work on your screen
+pub const MODEL_WIDTH: usize = 80;
+pub const MODEL_HEIGHT: usize = 24;
+pub const MODEL_SCALE: f32 = 0.01;
+
+// window parameters
+pub const WINDOW_TITLE: &str = "rustty";
+pub const WINDOW_TARGET_FPS: usize = 60;
+
+// input buffer size
+pub const INPUT_BUFFER_SIZE: usize = 2048;
+
+// environment variables
+pub const ENV_TERM: &str = "vt100";
+pub const ENV_PATH: &str = "/run/current-system/sw/bin";
diff --git a/src/main.rs b/src/main.rs
index 8a6a33b..70e0884 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -7,173 +7,19 @@ use minifb::{Key, KeyRepeat, Window, WindowOptions};
use std::collections::HashMap;
use std::ffi::CString;
-use std::fmt::Write;
use std::vec;
-use ttf_parser::{Face, OutlineBuilder, Rect};
+use ttf_parser::Face;
-use vte::{Params, Parser, Perform};
+use vte::Parser;
-use zeno::{Mask, Transform};
+use zeno::{Fill, Mask, Stroke, Style, Transform};
-const FONT: &[u8] = std::include_bytes!("../fonts/Miracode.ttf");
+mod config;
+use config::*;
-struct Buffer<T: std::clone::Clone> {
- buffer: Vec<T>,
- width: usize,
- height: usize,
-}
-impl<T: Clone> Buffer<T> {
- fn new(val: T, width: usize, height: usize) -> Self {
- Buffer {
- buffer: vec![val; width * height],
- width: width,
- height: height,
- }
- }
- fn get(&self, col: usize, row: usize) -> T {
- self.buffer.get(col + row * self.width).unwrap().clone()
- }
- fn set(&mut self, col: usize, row: usize, val: T) {
- self.buffer[col + row * self.width] = val;
- }
-}
-
-struct Cursor {
- col: usize,
- row: usize,
-}
-
-struct Model {
- screenbuffer: Buffer<u32>,
- buffer: Buffer<Option<char>>,
- cell: Rect,
- cursor: Cursor,
-}
-impl Model {
- // `cell` is the bbox of a cell
- // width and height are measured in cells
- fn new(cell: Rect, width: usize, height: usize, scale: f32) -> Self {
- Model {
- screenbuffer: Buffer::new(
- 0,
- (cell.width() as f32 * width as f32 * scale) as usize,
- (cell.height() as f32 * height as f32 * scale) as usize,
- ),
- buffer: Buffer::new(None, width, height),
- cell: cell,
- cursor: Cursor { col: 0, row: 0 },
- }
- }
-
- // returns scale
- fn scale(&self) -> f32 {
- self.screenbuffer.height as f32 / (self.cell.height() as f32 * self.buffer.height as f32)
- }
-}
-impl Perform for Model {
- // draw a character to the screen and update states
- fn print(&mut self, c: char) {
- if self.cursor.col < self.buffer.width {
- // scrolls
- while self.cursor.row >= self.buffer.height {
- for row in 0..(self.buffer.height - 1) {
- for col in 0..self.buffer.width {
- self.buffer.set(col, row, self.buffer.get(col, row + 1));
- }
- }
- for col in 0..self.buffer.width {
- self.buffer.set(col, self.buffer.height - 1, None);
- }
- self.cursor.row -= 1;
- }
- self.buffer.set(self.cursor.col, self.cursor.row, Some(c));
- self.cursor.col += 1;
- }
- println!("[print] {:?}", c);
- }
-
- // execute a C0 or C1 control function
- fn execute(&mut self, byte: u8) {
- match byte {
- 0x0D => self.cursor.col = 0,
- 0x0A => self.cursor.row = self.cursor.row + 1,
- 0x08 => {
- self.cursor.col = self.cursor.col - 1;
- self.buffer.set(self.cursor.col, self.cursor.row, None);
- }
- _ => (),
- }
- println!("[execute] {:02x}", byte);
- }
-
- // invoked when a final character arrives in first part of device control string
- fn hook(&mut self, params: &Params, intermediates: &[u8], ignore: bool, c: char) {
- println!(
- "[hook] params={:?}, intermediates={:?}, ignore={:?}, char={:?}",
- params, intermediates, ignore, c
- );
- }
-
- // pass bytes as part of a device control string to the handle chosen by hook
- // C0 controls are also passed to this handler
- fn put(&mut self, byte: u8) {
- println!("[put] {:02x}", byte);
- }
-
- // called when a device control string is terminated
- fn unhook(&mut self) {
- println!("[unhook]");
- }
-
- // dispatch an operating system command
- fn osc_dispatch(&mut self, params: &[&[u8]], bell_terminated: bool) {
- println!(
- "[osc_dispatch] params={:?} bell_terminated={}",
- params, bell_terminated
- );
- }
-
- // a final character has arrived for a csi sequence
- fn csi_dispatch(&mut self, params: &Params, intermediates: &[u8], ignore: bool, c: char) {
- println!(
- "[csi_dispatch] params={:#?}, intermediates={:?}, ignore={:?}, char={:?}",
- params, intermediates, ignore, c
- );
- }
-
- // the final character of an escape sequence has arrived
- fn esc_dispatch(&mut self, intermediates: &[u8], ignore: bool, byte: u8) {
- println!(
- "[esc_dispatch] intermediates={:?}, ignore={:?}, byte={:02x}",
- intermediates, ignore, byte
- );
- }
-}
-
-// yoinked from tty_parser docs
-struct Builder(String);
-impl OutlineBuilder for Builder {
- fn move_to(&mut self, x: f32, y: f32) {
- write!(&mut self.0, "M {} {} ", x, y).unwrap();
- }
-
- fn line_to(&mut self, x: f32, y: f32) {
- write!(&mut self.0, "L {} {} ", x, y).unwrap();
- }
-
- fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
- write!(&mut self.0, "Q {} {} {} {} ", x1, y1, x, y).unwrap();
- }
-
- fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
- write!(&mut self.0, "C {} {} {} {} {} {} ", x1, y1, x2, y2, x, y).unwrap();
- }
-
- fn close(&mut self) {
- write!(&mut self.0, "Z ").unwrap();
- }
-}
+mod structs;
+use structs::*;
fn main() {
// initialize the font
@@ -181,84 +27,96 @@ fn main() {
let font = generate_font(&face);
let mut model = Model::new(
- Rect {
- x_min: 0,
- y_min: 0,
- x_max: face.height(),
- y_max: face.height(),
- },
- 80,
- 24,
- 0.008,
+ MODEL_WIDTH,
+ MODEL_HEIGHT,
+ MODEL_SCALE,
);
// initialize window and its buffer
let mut window = Window::new(
- "rustty",
- model.screenbuffer.width,
- model.screenbuffer.height,
+ WINDOW_TITLE,
+ model.screenbuffer_width(),
+ model.screenbuffer_height(),
WindowOptions::default(),
)
.unwrap();
- window.set_target_fps(60);
+ window.set_target_fps(WINDOW_TARGET_FPS);
+ // pty and pid of child
let (pty, child) = spawn_pty().unwrap();
+
+ // set pty as non-blocking
fcntl(
&pty,
F_SETFL(OFlag::from_bits_truncate(fcntl(&pty, F_GETFL).unwrap()) | OFlag::O_NONBLOCK),
)
.unwrap();
+ // parser for `vte`
let mut statemachine = Parser::new();
- let mut buf = [0u8; 2048];
+ // buffers for input and writing to the screen
+ let mut buf = [0u8; INPUT_BUFFER_SIZE];
+ let mut mask = vec![0u8; model.screenbuffer_width() * model.screenbuffer_height()];
+
+ // window is open, child is alive
while window.is_open()
- && !window.is_key_down(Key::Escape)
&& waitpid(child, Some(WaitPidFlag::WNOHANG)) == Ok(WaitStatus::StillAlive)
{
- // mask to render into
- let mut mask = vec![0u8; model.screenbuffer.width * model.screenbuffer.height];
+ // clear mask from last frame
+ mask.fill(0u8);
// render each char into mask
- for row in 0..model.buffer.height {
- for col in 0..model.buffer.width {
- if let Some(c) = model.buffer.get(col, row) {
- Mask::new(font.get(&c).map_or("", |v| v)) // get svg
+ // remember that `model.buffer` is always 1-indexed
+ for row in 1..=model.buffer_height() {
+ for col in 1..=model.buffer_width() {
+ if let Some(chr) = model.buffer_at(row, col).chr() {
+ Mask::new(font.get(&chr).map_or("", |v| v)) // get svg
.transform(Some(
Transform::scale(model.scale(), -model.scale()) // scale it
.then_translate(
- col as f32 * model.cell.width() as f32 * model.scale(),
- // shift right by the cell width * the scale
- (1 + row) as f32 * model.scale() * model.cell.height() as f32,
+ // col and row are 1 indexed; we want col to be 0 indexed bc
+ // this refers to the bottom left corner of each cell, or at
+ // least behaves like it
+ (col as f32 - 1.0) * model.cell_width() as f32 * model.scale(),
+ row as f32 * model.scale() * model.cell_height() as f32,
),
))
+ .style(
+ match model.buffer_at(row, col).char_attr() {
+ CharAttr::Normal => Style::Fill(Fill::NonZero),
+ CharAttr::Bold => Style::Stroke(Stroke::new(500.0)),
+ CharAttr::Inverse => Style::Stroke(Stroke::new(50.0)),
+ _ => Style::Stroke(Stroke::new(50.0)),
+ }
+ )
.size(
- model.screenbuffer.width as u32,
- model.screenbuffer.height as u32,
+ // fits transformed svg to screen, or trims excess
+ model.screenbuffer_width() as u32,
+ model.screenbuffer_height() as u32,
)
+ // writes it to mask
.render_into(&mut mask, None);
}
}
}
- // render in white/grayscale to screen
- for (p, m) in model.screenbuffer.buffer.iter_mut().zip(mask.iter()) {
- let m0 = *m as u32;
- *p = m0 << 16 | m0 << 8 | m0;
- }
+ // render mask onto screen
+ model.set_screenbuffer(&mask);
- // update screen with buffer
+ // update screen with new screenbuffer
window
.update_with_buffer(
- &model.screenbuffer.buffer,
- model.screenbuffer.width,
- model.screenbuffer.height,
+ model.screenbuffer_buffer(),
+ model.screenbuffer_width(),
+ model.screenbuffer_height(),
)
.unwrap();
// other stuff
match read(&pty, &mut buf) {
Ok(0) => (),
+ // if there are new chars, feed them to `vte`
Ok(n) => statemachine.advance(&mut model, &buf[..n]),
Err(_e) => (),
};
@@ -268,10 +126,10 @@ fn main() {
|| window.is_key_down(Key::RightShift)
|| window.is_key_down(Key::CapsLock);
let ctrl = window.is_key_down(Key::LeftCtrl) || window.is_key_down(Key::RightCtrl);
+ // writes keystrokes to buffer, will be processed by vte next frame
if !keys.is_empty() {
let bytes: Vec<u8> = keys
.iter()
- // TODO apply modifiers
.map(|key| {
key_to_u8(
*key, // key
@@ -299,12 +157,16 @@ fn spawn_pty() -> Option<(PtyMaster, Pid)> {
&CString::new("/usr/bin/env").unwrap(),
&[
CString::new("/usr/bin/env").unwrap(),
- CString::new("-i").unwrap(),
- CString::new("sh").unwrap(),
+ CString::new("bash").unwrap(),
CString::new("--norc").unwrap(),
CString::new("--noprofile").unwrap(),
],
- &[CString::new("TERM=dumb").unwrap()],
+ &[
+ CString::new("TERM=".to_string() + ENV_TERM).unwrap(),
+ CString::new("PATH=".to_string() + ENV_PATH).unwrap(),
+ CString::new("NIXPKGS_CONFIG=/etc/nix/nixpkgs-config.nix").unwrap(),
+ CString::new("NIX_PATH=nixpkgs=flake:nixpkgs:/nix/var/nix/profiles/per-user/root/channels").unwrap(),
+ ],
);
return None;
}
@@ -313,7 +175,7 @@ fn spawn_pty() -> Option<(PtyMaster, Pid)> {
};
}
-// WARNING not functional; missing some keys and also modifiers
+// returns unrecognised keys as null bytes
fn key_to_u8(key: Key, shift: bool, ctrl: bool) -> u8 {
let mut base = match key {
Key::Key0 => b'0',
@@ -475,7 +337,7 @@ fn generate_font(face: &Face) -> HashMap<char, String> {
let mut hm = HashMap::new();
for c in chars {
- let mut builder = Builder(String::new());
+ let mut builder = Builder::new();
face.outline_glyph(face.glyph_index(c).unwrap(), &mut builder)
.unwrap();
hm.entry(c).insert_entry(builder.0);
diff --git a/src/structs.rs b/src/structs.rs
new file mode 100644
index 0000000..354fe70
--- /dev/null
+++ b/src/structs.rs
@@ -0,0 +1,495 @@
+use std::clone::Clone;
+use std::fmt::Write;
+use std::vec;
+use ttf_parser::{Face, OutlineBuilder, Rect};
+use vte::{Params, Perform};
+
+use crate::config::FONT;
+
+// public structs
+pub struct Model {
+ screenbuffer: Buffer<u32>,
+ buffer: Buffer<Cell>,
+ cell: Rect,
+ cursor: Cursor,
+}
+impl Model {
+ // width and height are measured in cells
+ pub fn new(width: usize, height: usize, scale: f32) -> Self {
+ let face = Face::parse(FONT, 0).unwrap();
+ let cell = Rect {
+ x_min: 0,
+ y_min: 0,
+ x_max: face.height(),
+ y_max: face.height(),
+ };
+ Model {
+ screenbuffer: Buffer::new(
+ 0,
+ (cell.width() as f32 * width as f32 * scale) as usize,
+ (cell.height() as f32 * height as f32 * scale) as usize,
+ ),
+ buffer: Buffer::new(Cell::new(None), width, height),
+ cell: cell,
+ cursor: Cursor::new(),
+ }
+ }
+ // helper
+ pub fn scale(&self) -> f32 {
+ self.screenbuffer.height as f32 / (self.cell.height() as f32 * self.buffer.height as f32)
+ }
+
+ // concerning screenbuffer
+ pub fn screenbuffer_width(&self) -> usize { self.screenbuffer.width }
+ pub fn screenbuffer_height(&self) -> usize { self.screenbuffer.height }
+ pub fn screenbuffer_buffer(&self) -> &Vec<u32> { &self.screenbuffer.buffer }
+ // sets screenbuffer to the contents of `buf`, ignoring extra
+ pub fn set_screenbuffer(&mut self, buf: &[u8]) {
+ for (p, m) in self.screenbuffer.buffer.iter_mut().zip(buf.iter()) {
+ let m0 = *m as u32;
+ *p = m0 << 16 | m0 << 8 | m0;
+ }
+ }
+
+ // concerning buffer
+ pub fn buffer_width(&self) -> usize { self.buffer.width }
+ pub fn buffer_height(&self) -> usize { self.buffer.height }
+ // WARNING: 1-indexed
+ pub fn buffer_at(&self, row: usize, col: usize) -> Cell {
+ self.buffer.get(col - 1, row - 1)
+ }
+ // WARNING: 1-indexed
+ fn set_buffer_at(&mut self, row: usize, col: usize, val: Cell) {
+ self.buffer.set(col - 1, row - 1, val);
+ }
+
+ // concerning cell
+ pub fn cell_width(&self) -> i16 { self.cell.width() }
+ pub fn cell_height(&self) -> i16 { self.cell.height() }
+
+ // concerning cursor
+ fn set_cursor_row(&mut self, row: usize) {
+ self.cursor.row = row;
+ if row < 1 { self.cursor.row = 1; }
+ // notice no lower bounds check; should automatically scroll in print func
+ }
+ fn set_cursor_col(&mut self, col: usize) {
+ self.cursor.col = col;
+ if col < 1 { self.cursor.col = 1; }
+ if col > self.buffer.width{ self.cursor.col = self.buffer.width; }
+ }
+ fn set_cursor(&mut self, row: usize, col: usize) {
+ self.set_cursor_row(row);
+ self.set_cursor_col(col);
+ }
+ fn dec_cursor_row(&mut self, row: usize) {
+ self.set_cursor_row(self.cursor.row - row);
+ }
+ fn dec_cursor_col(&mut self, col: usize) {
+ self.set_cursor_col(self.cursor.col - col);
+ }
+ fn inc_cursor_row(&mut self, row: usize) {
+ self.set_cursor_row(self.cursor.row + row);
+ }
+ fn inc_cursor_col(&mut self, col: usize) {
+ self.set_cursor_col(self.cursor.col + col);
+ }
+ fn set_cursor_char_attr(&mut self, char_attr: CharAttr) {
+ self.cursor.char_attr = char_attr;
+ }
+}
+impl Perform for Model {
+ // draw a character to the screen and update states
+ fn print(&mut self, c: char) {
+ println!("[print] {:?}", c);
+
+ // cycle everything up a line until the cursor is on screen
+ while self.cursor.row > self.buffer.height {
+ println!("cursor off screen, fixing");
+ for row in 1..self.buffer.height {
+ for col in 1..=self.buffer.width {
+ self.set_buffer_at(row, col, self.buffer_at(row + 1, col));
+ }
+ }
+ for col in 1..=self.buffer.width {
+ self.set_buffer_at(self.buffer.height, col, Cell::new(None));
+ }
+ self.dec_cursor_row(1);
+ }
+
+ // draw to the cursor position
+ self.set_buffer_at(self.cursor.row, self.cursor.col, Cell::new_with(Some(c), self.cursor.char_attr));
+
+ // move the cursor further to the right
+ self.inc_cursor_col(1);
+ }
+
+ // execute a C0 or C1 control function
+ fn execute(&mut self, byte: u8) {
+ let mut recognised = true;
+ match byte {
+ // TODO seen in the wild:
+ // 0x07
+
+ 0x00 => (),
+ // BEL
+ 0x07 => (),
+ // BS
+ 0x08 => {
+ self.dec_cursor_col(1);
+ self.set_buffer_at(self.cursor.row, self.cursor.col, Cell::new(None));
+ },
+ // LF
+ 0x0A => self.inc_cursor_row(1),
+ // CR
+ 0x0D => self.set_cursor_col(1),
+ // DEL
+ 0x0F => {
+ self.inc_cursor_col(1);
+ self.set_buffer_at(self.cursor.row, self.cursor.col, Cell::new(None));
+ }
+ _ => recognised = false,
+ }
+ print!("[execute] ");
+ if recognised == false { print!("UNIMPLEMENTED ") };
+ println!("{:02x}", byte);
+ }
+
+ // invoked when a final character arrives in first part of device control string
+ fn hook(&mut self, params: &Params, intermediates: &[u8], ignore: bool, c: char) {
+ println!(
+ "[hook] params={:?}, intermediates={:?}, ignore={:?}, char={:?}",
+ params, intermediates, ignore, c
+ );
+ }
+
+ // pass bytes as part of a device control string to the handle chosen by hook
+ // C0 controls are also passed to this handler
+ fn put(&mut self, byte: u8) {
+ println!("[put] {:02x}", byte);
+ }
+
+ // called when a device control string is terminated
+ fn unhook(&mut self) {
+ println!("[unhook]");
+ }
+
+ // dispatch an operating system command
+ fn osc_dispatch(&mut self, params: &[&[u8]], bell_terminated: bool) {
+ println!(
+ "[osc_dispatch] params={:?} bell_terminated={}",
+ params, bell_terminated
+ );
+ }
+
+ // a final character has arrived for a csi sequence
+ fn csi_dispatch(&mut self, params: &Params, intermediates: &[u8], ignore: bool, c: char) {
+ let mut recognised = true;
+ match (params, intermediates, ignore, c) {
+ // comments come from https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
+ // TODO seen in the wild:
+ // CSI ? 200* h // xterm stuff
+ // CSI ? 200* l // xterm stuff
+ //
+ // priority:
+ // CSI Ps ; Ps f // dk
+ // CSI ? Ps h // random magic number codes
+ // CSI ? Ps l // likewise
+ // CSI Ps ; Ps r // set scrolling region
+
+ // TODO CSI Ps SP A
+ // CSI Ps A
+ // Cursor Up Ps Times (default = 1) (CUU).
+ (_, _, false, 'A') => self.dec_cursor_row(if let Some(val) = params.iter().next() { val[0] as usize } else { 1 }),
+
+ // CSI Ps B
+ // Cursor Down Ps Times (default = 1) (CUD)
+ (_, _, false, 'B') => self.inc_cursor_row(if let Some(val) = params.iter().next() { val[0] as usize } else { 1 }),
+
+ // CSI Ps C
+ // Cursor Forward Ps Times (default = 1) (CUF).
+ (_, _, false, 'C') => self.inc_cursor_col(if let Some(val) = params.iter().next() { val[0] as usize } else { 1 }),
+
+ // CSI Ps D
+ // Cursor Backward Ps Times (default = 1) (CUB).
+ (_, _, false, 'D') => self.dec_cursor_col(if let Some(val) = params.iter().next() { val[0] as usize } else { 1 }),
+
+ // CSI Ps ; Ps H
+ // Cursor Position [row;column] (default [1,1]) (CUP)
+ (_, _, false, 'H') => {
+ let mut iter = params.iter();
+ self.set_cursor(
+ if let Some(row) = iter.next() { row[0] as usize } else { 1 },
+ if let Some(col) = iter.next() { col[0] as usize } else { 1 },
+ );
+ },
+
+ // TODO Ps = {1,2,3}
+ // CSI Ps J
+ // Erase in Display (ED), VT100.
+ // Ps = 0 => Erase Below (default).
+ // Ps = 1 => Erase Above.
+ // Ps = 2 => Erase All.
+ // Ps = 3 => Erase Saved Lines, xterm.
+ (_, _, false, 'J') => {
+ // TODO inline from (_, _, false, 'K') case?
+ // deletes line from cursor to right
+ for col in self.cursor.col..=self.buffer.width {
+ self.set_buffer_at(self.cursor.row, col, Cell::new(None));
+ };
+ // deletes every row below the cursor
+ for row in (self.cursor.row + 1)..=self.buffer.height {
+ for col in 1..=self.buffer.width {
+ self.set_buffer_at(row, col, Cell::new(None));
+ };
+ };
+ },
+
+ // TODO Ps = {1,2}
+ // CSI Ps K
+ // Erase in Line (EL), VT100.
+ // Ps = 0 => Erase to Right (default).
+ // Ps = 1 => Erase to Left.
+ // Ps = 2 => Erase All.
+ (_, _, false, 'K') => {
+ for col in self.cursor.col..=self.buffer.width {
+ self.set_buffer_at(self.cursor.row, col, Cell::new(None));
+ };
+ },
+
+ // CSI
+
+ // TODO a *lot*, color lives here
+ // CSI Pm m
+ // Character Attributes (SGR)
+ // Ps = 0 => Normal
+ // Ps = 1 => Bold
+ // Ps = 4 => Underlined
+ // Ps = 5 => Blink
+ // Ps = 7 => Inverse
+ (_, _, false, 'm') => {
+ let mut iter = params.iter();
+ while let Some(value) = iter.next() {
+ self.set_cursor_char_attr(
+ match value[0] {
+ 0 => CharAttr::Normal,
+ 1 => CharAttr::Bold,
+ 4 => CharAttr::Underlined,
+ 5 => CharAttr::Blink,
+ 7 => CharAttr::Inverse,
+ _ => self.cursor.char_attr,
+ }
+ )
+ }
+ println!("char_attr: {:?}", self.cursor.char_attr);
+ },
+ _ => recognised = false,
+ }
+ print!("[csi_dispatch] ");
+ if recognised == false { print!("UNIMPLEMENTED ") };
+ println!(
+ "params={:#?}, intermediates={:?}, ignore={:?}, char={:?}",
+ params, intermediates, ignore, c
+ );
+ println!("cursor at {:?};{:?}", self.cursor.row, self.cursor.col);
+ }
+
+ // the final character of an escape sequence has arrived
+ fn esc_dispatch(&mut self, intermediates: &[u8], ignore: bool, byte: u8) {
+ // seen in the wild:
+ // in bt ct
+ // 41 30
+ // 3D DECPAM
+ // 3E DECPNM
+ // 40 42
+
+ println!(
+ "[esc_dispatch] intermediates={:?}, ignore={:?}, byte={:02x}",
+ intermediates, ignore, byte
+ );
+ }
+}
+
+// yoinked from tty_parser docs
+pub struct Builder(pub String);
+impl Builder {
+ pub fn new() -> Self { Builder(String::new()) }
+}
+impl OutlineBuilder for Builder {
+ fn move_to(&mut self, x: f32, y: f32) {
+ write!(&mut self.0, "M {} {} ", x, y).unwrap();
+ }
+
+ fn line_to(&mut self, x: f32, y: f32) {
+ write!(&mut self.0, "L {} {} ", x, y).unwrap();
+ }
+
+ fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
+ write!(&mut self.0, "Q {} {} {} {} ", x1, y1, x, y).unwrap();
+ }
+
+ fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
+ write!(&mut self.0, "C {} {} {} {} {} {} ", x1, y1, x2, y2, x, y).unwrap();
+ }
+
+ fn close(&mut self) {
+ write!(&mut self.0, "Z ").unwrap();
+ }
+}
+
+// private structs
+struct Buffer<T: std::clone::Clone> {
+ buffer: Vec<T>,
+ width: usize,
+ height: usize,
+}
+impl<T: Clone> Buffer<T> {
+ // creates new buffer populated with `val`
+ fn new(val: T, width: usize, height: usize) -> Self {
+ Buffer {
+ buffer: vec![val; width * height],
+ width: width,
+ height: height,
+ }
+ }
+
+ fn get(&self, col: usize, row: usize) -> T {
+ // TODO return ref mb? clone bad..
+ // println!("col: {:?}\nrow: {:?}\nget:{:?}", col, row, col + row * self.width);
+ self.buffer.get(col_row_to_index(col, row, self.width)).unwrap().clone()
+ }
+ fn set(&mut self, col: usize, row: usize, val: T) {
+ self.buffer[col_row_to_index(col, row, self.width)] = val;
+ }
+}
+
+#[derive(Clone,Debug,PartialEq)]
+pub struct Cell {
+ chr: Option<char>,
+ char_attr: CharAttr,
+}
+impl Cell {
+ fn new(val: Option<char>) -> Self {
+ Cell {
+ chr: val,
+ char_attr: CharAttr::Normal,
+ }
+ }
+ fn new_with(val: Option<char>, char_attr: CharAttr) -> Self {
+ Cell {
+ chr: val,
+ char_attr: char_attr,
+ }
+ }
+ pub fn chr(&self) -> Option<char> {
+ self.chr
+ }
+ pub fn char_attr(&self) -> CharAttr {
+ self.char_attr.clone()
+ }
+}
+
+struct Cursor {
+ // 1 indexed row number; top to bottom
+ row: usize,
+ // 1 indexed column; left to right
+ col: usize,
+ // current state as for writing characters
+ char_attr: CharAttr,
+}
+
+impl Cursor {
+ fn new() -> Self {
+ Cursor {
+ row: 1,
+ col: 1,
+ char_attr: CharAttr::Normal,
+ }
+ }
+}
+
+// as specified for vt100
+#[derive(Clone,Copy,Debug,PartialEq)]
+pub enum CharAttr {
+ Normal,
+ Bold,
+ Underlined,
+ Blink,
+ Inverse,
+}
+
+// Returns the Vec or Slice index that a 0-indexed column and row correspond to.
+fn col_row_to_index(col: usize, row: usize, width: usize) -> usize {
+ col + row * width
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ mod model {
+ use super::*;
+
+ #[test]
+ fn set_buffer_at_1_1() {
+ let w = 4;
+ let h = 2;
+
+ let mut model = Model::new(w, h, 0.005);
+ model.set_buffer_at(1, 1, Cell::new(Some('A')));
+
+ let mut blank_buffer = Buffer::new(Cell::new(None), w, h);
+ blank_buffer.buffer[0] = Cell::new(Some('A'));
+
+ assert_eq!(blank_buffer.buffer, model.buffer.buffer);
+ }
+
+ #[test]
+ fn set_buffer_at_1_max() {
+ let w = 4;
+ let h = 2;
+
+ let mut model = Model::new(w, h, 0.005);
+ model.set_buffer_at(1, w, Cell::new(Some('A')));
+
+ let mut blank_buffer = Buffer::new(Cell::new(None), w, h);
+ blank_buffer.buffer[w - 1] = Cell::new(Some('A'));
+
+ assert_eq!(blank_buffer.buffer, model.buffer.buffer);
+ }
+
+ #[test]
+ fn set_buffer_at_max_max() {
+ let w = 4;
+ let h = 2;
+
+ let mut model = Model::new(w, h, 0.005);
+ model.set_buffer_at(h, w, Cell::new(Some('A')));
+
+ let mut blank_buffer = Buffer::new(Cell::new(None), w, h);
+ blank_buffer.buffer[h * w - 1] = Cell::new(Some('A'));
+
+ assert_eq!(blank_buffer.buffer, model.buffer.buffer);
+ }
+
+ #[test]
+ fn set_buffer_at_max_1() {
+ let w = 4;
+ let h = 2;
+
+ let mut model = Model::new(w, h, 0.005);
+ model.set_buffer_at(h, 1, Cell::new(Some('A')));
+
+ let mut blank_buffer = Buffer::new(Cell::new(None), w, h);
+ blank_buffer.buffer[(h - 1) * blank_buffer.width] = Cell::new(Some('A'));
+
+ assert_eq!(blank_buffer.buffer, model.buffer.buffer);
+ }
+ }
+
+ #[test]
+ fn r#true() {
+ let result = true;
+ assert!(result, "you can't handle the truth");
+ }
+}