summaryrefslogtreecommitdiff
path: root/src/structs.rs
diff options
context:
space:
mode:
authorandromeda <andromeda@lenovo>2026-01-27 09:33:50 +0100
committerandromeda <andromeda@lenovo>2026-01-27 09:33:50 +0100
commitc6d10069ab98052ec4d1cf2cdafca7888457264d (patch)
tree9244f85432bd956564eb2b0ed3324368eef3b7c6 /src/structs.rs
parentd2ed0e974bb28d4588405153ce443f807e484e76 (diff)
Diffstat (limited to 'src/structs.rs')
-rw-r--r--src/structs.rs495
1 files changed, 495 insertions, 0 deletions
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");
+ }
+}