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::vec; use ttf_parser::Face; use vte::Parser; use zeno::{Fill, Mask, Stroke, Style, Transform}; mod config; use config::*; mod structs; use structs::*; fn main() { // initialize the font let face = Face::parse(FONT, 0).unwrap(); let font = generate_font(&face); let mut model = Model::new( MODEL_WIDTH, MODEL_HEIGHT, MODEL_SCALE, ); // initialize window and its buffer let mut window = Window::new( WINDOW_TITLE, model.screenbuffer_width(), model.screenbuffer_height(), WindowOptions::default(), ) .unwrap(); 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(); // 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 // 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 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( // 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 mask onto screen model.set_screenbuffer(&mask); // update screen with new screenbuffer window .update_with_buffer( 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::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 = keys .iter() .map(|key| { key_to_u8( *key, // key shift, ctrl, ) }) .filter(|v| *v != 0u8) .collect(); write(&pty, bytes.as_slice()); }; } } // forks a new pty and returns file descriptor of the master 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 } => { // SAFETY `master` is a valid PtyMaster return Some((unsafe { PtyMaster::from_owned_fd(master) }, child)); } ForkptyResult::Child => { let _ = execve::( &CString::new("/usr/bin/env").unwrap(), &[ CString::new("/usr/bin/env").unwrap(), CString::new("bash").unwrap(), CString::new("--norc").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; } }, Err(_e) => return None, }; } // 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', Key::Key1 => b'1', Key::Key2 => b'2', Key::Key3 => b'3', Key::Key4 => b'4', Key::Key5 => b'5', Key::Key6 => b'6', Key::Key7 => b'7', Key::Key8 => b'8', Key::Key9 => b'9', Key::A => b'a', Key::B => b'b', Key::C => b'c', Key::D => b'd', Key::E => b'e', Key::F => b'f', Key::G => b'g', Key::H => b'h', Key::I => b'i', Key::J => b'j', Key::K => b'k', Key::L => b'l', Key::M => b'm', Key::N => b'n', Key::O => b'o', Key::P => b'p', Key::Q => b'q', Key::R => b'r', Key::S => b's', Key::T => b't', Key::U => b'u', Key::V => b'v', Key::W => b'w', Key::X => b'x', Key::Y => b'y', Key::Z => b'z', Key::F1 => 0, Key::F2 => 0, Key::F3 => 0, Key::F4 => 0, Key::F5 => 0, Key::F6 => 0, Key::F7 => 0, Key::F8 => 0, Key::F9 => 0, Key::F10 => 0, Key::F11 => 0, Key::F12 => 0, Key::F13 => 0, Key::F14 => 0, Key::F15 => 0, Key::Down => 0, Key::Left => 0, Key::Right => 0, Key::Up => 0, Key::Apostrophe => b'\'', Key::Backquote => b'`', Key::Backslash => b'\\', Key::Comma => b',', Key::Equal => b'=', Key::LeftBracket => b'[', Key::Minus => b'-', Key::Period => b'.', Key::RightBracket => b']', Key::Semicolon => b';', Key::Slash => b'/', Key::Backspace => 8, Key::Delete => 127, Key::End => 0, Key::Enter => b'\n', Key::Escape => 27, Key::Home => 0, Key::Insert => 0, Key::Menu => 0, Key::PageDown => 0, Key::PageUp => 0, Key::Pause => 0, Key::Space => b' ', Key::Tab => b'\t', Key::NumLock => 0, Key::CapsLock => 0, Key::ScrollLock => 0, Key::LeftShift => 0, Key::RightShift => 0, Key::LeftCtrl => 0, Key::RightCtrl => 0, Key::NumPad0 => 0, Key::NumPad1 => 0, Key::NumPad2 => 0, Key::NumPad3 => 0, Key::NumPad4 => 0, Key::NumPad5 => 0, Key::NumPad6 => 0, Key::NumPad7 => 0, Key::NumPad8 => 0, Key::NumPad9 => 0, Key::NumPadDot => 0, Key::NumPadSlash => 0, Key::NumPadAsterisk => 0, Key::NumPadMinus => 0, Key::NumPadPlus => 0, Key::NumPadEnter => 0, Key::LeftAlt => 0, Key::RightAlt => 0, Key::LeftSuper => 0, Key::RightSuper => 0, Key::Unknown => 0, Key::Count => 0, }; if shift { base = match base { b'0' => b')', b'1' => b'!', b'2' => b'@', b'3' => b'#', b'4' => b'$', b'5' => b'%', b'6' => b'^', b'7' => b'&', b'8' => b'*', b'9' => b'(', b'a'..=b'z' => base - 32, b'\'' => b'"', b'`' => b'~', b'\\' => b'|', b',' => b'<', b'=' => b'+', b'[' => b'{', b'-' => b'_', b'.' => b'>', b']' => b'}', b';' => b':', b'/' => b'?', _ => base, }; } if ctrl { base = base & 0x1F; } return base; } // creats a mapping from a `char` to its svg spec for a selection of characters fn generate_font(face: &Face) -> HashMap { let chars = vec![ '\'', '`', '\\', ',', '=', '[', '-', '.', ']', ';', '/', ')', '!', '@', '#', '$', '%', '^', '&', '*', '(', '"', '~', '|', '<', '+', '{', '_', '>', '}', ':', '?', ] .into_iter() .chain('a'..='z') .chain('A'..='Z') .chain('0'..='9'); let mut hm = HashMap::new(); for c in chars { let mut builder = Builder::new(); face.outline_glyph(face.glyph_index(c).unwrap(), &mut builder) .unwrap(); hm.entry(c).insert_entry(builder.0); } hm }