From c6d10069ab98052ec4d1cf2cdafca7888457264d Mon Sep 17 00:00:00 2001 From: andromeda Date: Tue, 27 Jan 2026 09:33:50 +0100 Subject: [PATCH] stash --- HACKING.md | 9 + README.md | 25 +++ flake.nix | 4 + nano.2954.save | 1 + nano.4900.save | 1 + package.nix | 5 + src/config.rs | 19 ++ src/main.rs | 268 +++++++------------------- src/structs.rs | 495 +++++++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 624 insertions(+), 203 deletions(-) create mode 100644 README.md create mode 100644 nano.2954.save create mode 100644 nano.4900.save create mode 100644 src/config.rs create mode 100644 src/structs.rs diff --git a/HACKING.md b/HACKING.md index a2610f7..5297da5 100644 --- a/HACKING.md +++ b/HACKING.md @@ -1,3 +1,12 @@ helpful to remember: `infocmp` prints terminfo in a readable way. `infocmp dumb` zB prints dumb terminal info +[control sequence docs, xterm](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html) + +contributions should not do any of the following things: + +- add needless dependencies or features +- add support for a proprietary platform + +if a depedency is a 'convenience' but not a necesity, look at how they implement the functionality and write it yourself. + Miracode is from [here](https://github.com/IdreesInc/Miracode) and is [freely licensed](https://github.com/IdreesInc/Miracode/blob/main/LICENSE) diff --git a/README.md b/README.md new file mode 100644 index 0000000..f784c24 --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +this project is for me. Here are the priorities + +- provide a complaint terminal emulator +- minimise application start time; time from `rustty` to a window appearing +- minimise build time; preferably builds from scratch in less than 30s + +long term goals: + +- emulate vt100 +- run nvim + +non-goals; never going to happen: + +- non-\*nix support +- non-compiletime configuration + +rationale for the above: + +alacritty is nice but it takes forever to start. I like st's philosophy. I am writing something with st's philosophy in rust, because I know rust already. + +see `HACKING.md` for development tips + +run with `nix run --flake git+https://git.mtgmonkey.net/andromeda/rustty` + +build with cargo by cloning it and running `cargo build --release` diff --git a/flake.nix b/flake.nix index 6a49e38..cd5583d 100644 --- a/flake.nix +++ b/flake.nix @@ -18,6 +18,10 @@ default = pkgs.callPackage ./package.nix { naersk = pkgs.callPackage naersk {}; }; + noTests = pkgs.callPackage ./package.nix { + naersk = pkgs.callPackage naersk {}; + doCheck = false; + }; }; }; } diff --git a/nano.2954.save b/nano.2954.save new file mode 100644 index 0000000..a87bf43 --- /dev/null +++ b/nano.2954.save @@ -0,0 +1 @@ +help diff --git a/nano.4900.save b/nano.4900.save new file mode 100644 index 0000000..a99c532 --- /dev/null +++ b/nano.4900.save @@ -0,0 +1 @@ +help line 2 line 3 line 4 diff --git a/package.nix b/package.nix index d862ef7..60dcd2f 100644 --- a/package.nix +++ b/package.nix @@ -1,4 +1,5 @@ { + doCheck ? true, lib, libGL, libxkbcommon, @@ -25,6 +26,10 @@ naersk.buildPackage rec { postInstall = '' wrapProgram "$out/bin/${meta.mainProgram}" --prefix LD_LIBRARY_PATH : "${lib.makeLibraryPath buildInputs}" ''; + + # enables test suite + inherit doCheck; + meta = { mainProgram = "rustty"; homepage = "https://mtgmonkey.net"; 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 { - buffer: Vec, - width: usize, - height: usize, -} -impl Buffer { - 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, - buffer: Buffer>, - 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, ), )) - .size( - model.screenbuffer.width as u32, - model.screenbuffer.height as u32, + .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( + // 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 = 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 { 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, + buffer: Buffer, + 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 { &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 { + buffer: Vec, + width: usize, + height: usize, +} +impl Buffer { + // 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_attr: CharAttr, +} +impl Cell { + fn new(val: Option) -> Self { + Cell { + chr: val, + char_attr: CharAttr::Normal, + } + } + fn new_with(val: Option, char_attr: CharAttr) -> Self { + Cell { + chr: val, + char_attr: char_attr, + } + } + pub fn chr(&self) -> Option { + 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"); + } +}