Compare commits

..

3 Commits

Author SHA1 Message Date
andromeda
c6d10069ab stash 2026-01-27 09:33:50 +01:00
andromeda
d2ed0e974b fmt, use waitpid to exit 2026-01-18 12:47:12 +01:00
andromeda
aecca6f229 cool keys, bound check printing, add backspace 2026-01-18 11:45:10 +01:00
9 changed files with 650 additions and 203 deletions

View File

@@ -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)

25
README.md Normal file
View File

@@ -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`

View File

@@ -18,6 +18,10 @@
default = pkgs.callPackage ./package.nix {
naersk = pkgs.callPackage naersk {};
};
noTests = pkgs.callPackage ./package.nix {
naersk = pkgs.callPackage naersk {};
doCheck = false;
};
};
};
}

1
nano.2954.save Normal file
View File

@@ -0,0 +1 @@
help

1
nano.4900.save Normal file
View File

@@ -0,0 +1 @@
help line 2 line 3 line 4

View File

@@ -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";

19
src/config.rs Normal file
View File

@@ -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";

View File

@@ -1,160 +1,25 @@
use nix::fcntl::{fcntl, OFlag, F_GETFL, F_SETFL};
use nix::pty::{forkpty, ForkptyResult, PtyMaster};
use nix::unistd::{execve, read, write};
use nix::fcntl::{F_GETFL, F_SETFL, OFlag, fcntl};
use nix::pty::{ForkptyResult, PtyMaster, forkpty};
use nix::sys::wait::{WaitPidFlag, WaitStatus, waitpid};
use nix::unistd::{Pid, execve, read, write};
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) {
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,
_ => (),
}
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
@@ -162,98 +27,115 @@ 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(
"rust-term",
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);
let pty = spawn_pty().unwrap();
// 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];
while window.is_open() && !window.is_key_down(Key::Escape) {
// mask to render into
let mut mask = vec![0u8; model.screenbuffer.width * model.screenbuffer.height];
// 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()
&& waitpid(child, Some(WaitPidFlag::WNOHANG)) == Ok(WaitStatus::StillAlive)
{
// 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) => (),
};
let keys = window.get_keys_pressed(KeyRepeat::No);
if !keys.is_empty() { println!("{:?}", keys); }
let shift = window.is_key_down(Key::LeftShift);
let ctrl = window.is_key_down(Key::LeftCtrl);
let keys = window.get_keys_pressed(KeyRepeat::Yes);
let shift = window.is_key_down(Key::LeftShift)
|| 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(
.map(|key| {
key_to_u8(
*key, // key
shift,
ctrl
))
shift, ctrl,
)
})
.filter(|v| *v != 0u8)
.collect();
write(&pty, bytes.as_slice());
@@ -262,24 +144,30 @@ fn main() {
}
// forks a new pty and returns file descriptor of the master
fn spawn_pty() -> Option<PtyMaster> {
fn spawn_pty() -> Option<(PtyMaster, Pid)> {
// SAFETY safe unless os out of PTYs; incredibly unlikely
match unsafe { forkpty(None, None) } {
Ok(fork_pty_res) => match fork_pty_res {
ForkptyResult::Parent { child: _, master } => {
ForkptyResult::Parent { child, master } => {
// SAFETY `master` is a valid PtyMaster
return Some(unsafe { PtyMaster::from_owned_fd(master) });
return Some((unsafe { PtyMaster::from_owned_fd(master) }, child));
}
ForkptyResult::Child => {
let _ = execve::<CString, CString>(
&CString::new("/usr/bin/env").unwrap(),
&[ CString::new("/usr/bin/env").unwrap(),
CString::new("-i").unwrap(),
CString::new("sh").unwrap(),
&[
CString::new("/usr/bin/env").unwrap(),
CString::new("bash").unwrap(),
CString::new("--norc").unwrap(),
CString::new("--noprofile").unwrap() ],
&[ CString::new("TERM=dumb").unwrap() ],
);
CString::new("--noprofile").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;
}
},
@@ -287,7 +175,7 @@ fn spawn_pty() -> Option<PtyMaster> {
};
}
// 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',
@@ -449,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);

495
src/structs.rs Normal file
View File

@@ -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");
}
}