summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorandromeda <andromeda@lenovo>2026-01-17 17:31:29 +0100
committerandromeda <andromeda@lenovo>2026-01-17 17:31:29 +0100
commit7ef9d1317b99f3b0693a35246f856462a9bd6ee8 (patch)
tree17f97c03f4ce98b6625570df915778e9c9fba776 /src
parentc42ea43099e56040b6e9908ec8e76ff492362c93 (diff)
add font rendering
Diffstat (limited to 'src')
-rw-r--r--src/main.rs217
1 files changed, 184 insertions, 33 deletions
diff --git a/src/main.rs b/src/main.rs
index fb5f90d..df6e45a 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -5,27 +5,99 @@ use nix::unistd::{execv, read, write};
use minifb::{Key, KeyRepeat, Window, WindowOptions};
use std::vec;
+use std::collections::HashMap;
use std::ffi::CString;
+use std::fmt::Write;
+
+use ttf_parser::{Face, OutlineBuilder, Rect};
use vte::{Params, Parser, Perform};
-use zeno::{Join, Mask, Stroke};
+use zeno::{Mask, Transform};
+
+// params
+const SHELL: &str = "/home/andromeda/.nix-profile/bin/sh";
+const FONT: &str = "/home/andromeda/.nix-profile/share/fonts/truetype/Miracode.ttf";
+
+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;
+ }
+}
-// window stuff
-const WIDTH: usize = 640;
-const HEIGHT: usize = 360;
+struct Cursor {
+ col: usize,
+ row: usize,
+}
-// yoinked from vte docs
-struct Performer;
-impl Perform for Performer {
+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={:?}",
@@ -33,18 +105,23 @@ impl Perform for Performer {
);
}
+ // 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={:?}",
@@ -52,6 +129,7 @@ impl Perform for Performer {
);
}
+ // 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}",
@@ -60,52 +138,103 @@ impl Perform for Performer {
}
}
+// 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();
+ }
+}
+
fn main() {
- let mut buffer: Vec<u32> = vec![0; WIDTH * HEIGHT];
+ // initialize the font
+ let font_data = std::fs::read(FONT).unwrap();
+ let face = Face::parse(&font_data, 0).unwrap();
+ 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,
+ );
+
+ // initialize window and its buffer
let mut window = Window::new(
"rust-term",
- WIDTH,
- HEIGHT,
+ model.screenbuffer.width,
+ model.screenbuffer.height,
WindowOptions::default(),
).unwrap();
-
window.set_target_fps(60);
- let pty = spawn_pty("/home/andromeda/.nix-profile/bin/sh").unwrap();
- println!("{:?}", &pty);
- let pty_flags = fcntl(&pty, F_GETFL).unwrap();
- fcntl(&pty, F_SETFL(OFlag::from_bits_truncate(pty_flags) | OFlag::O_NONBLOCK)).unwrap();
+ let pty = spawn_pty(&SHELL).unwrap();
+ fcntl(&pty, F_SETFL(OFlag::from_bits_truncate(fcntl(&pty, F_GETFL).unwrap()) | OFlag::O_NONBLOCK)).unwrap();
let mut statemachine = Parser::new();
- let mut performer = Performer;
- let mut buf = [0u8; 2048];
+ 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];
- let mask = Mask::new("M 8,56 32,8 56,56 Z")
- .size(WIDTH.try_into().unwrap(), HEIGHT.try_into().unwrap())
- .style(Stroke::new(8.0).join(Join::Round))
- .render()
- .0;
+ // 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
+ .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,
+ )
+ ))
+ .size(model.screenbuffer.width as u32, model.screenbuffer.height as u32)
+ .render_into(&mut mask, None); }
+ };
+ };
- // windowing stuff
- for (p, a) in buffer.iter_mut().zip(mask.iter()) {
- let a0 = *a as u32;
- *p = a0 << 24 | 0x00FF0000;
- *p =
- if a0 > 0
- { a0 << 16 | a0 << 8 | a0 }
- else { 0x00000000 };
+ // 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;
}
- let keys = window.get_keys_pressed(KeyRepeat::No);
- window.update_with_buffer(&buffer, WIDTH, HEIGHT).unwrap();
+
+ // update screen with buffer
+ window.update_with_buffer(&model.screenbuffer.buffer, model.screenbuffer.width, model.screenbuffer.height).unwrap();
+
// other stuff
match read(&pty, &mut buf) {
Ok(0) => (),
- Ok(n) => statemachine.advance(&mut performer, &buf[..n]),
+ Ok(n) => statemachine.advance(&mut model, &buf[..n]),
Err(_e) => (),
};
+
+ let keys = window.get_keys_pressed(KeyRepeat::No);
if !keys.is_empty() {
let bytes: Vec<u8> = keys.iter()
// TODO apply modifiers
@@ -283,3 +412,25 @@ fn key_to_u8(key: Key, shift: bool, ctrl: bool) -> u8 {
return base_shift_ctrl;
}
+// creats a mapping from a `char` to its svg spec for a selection of characters
+fn generate_font(face: &Face) -> HashMap<char, String> {
+ 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(String::new());
+ face.outline_glyph(face.glyph_index(c).unwrap(), &mut builder).unwrap();
+ hm.entry(c).insert_entry(builder.0);
+ }
+
+ hm
+}