8000 V4 rewrite by dominikwilkowski · Pull Request #90 · dominikwilkowski/cfonts · GitHub
[go: up one dir, main page]

Skip to content

V4 rewrite - #90

Draft
dominikwilkowski wants to merge 335 commits into
releasedfrom
v4
Draft

dominikwilkowski wants to merge 335 commits into
releasedfrom
v4

Conversation

@dominikwilkowski
@dominikwilkowski dominikwilkowski commented Jun 21, 2026 •
Copy link
Copy Markdown
Owner

cfonts refactor

Motivation

The current Rust implementation (multiple years old) has accumulated technical debt:

  • Took owned String instead of &str, causing unnecessary allocations
  • Excessive clones and heap allocations throughout
  • Not idiomatic Rust (giant Options struct, function-based API)
  • Maintains parallel Node.js and Rust codebases
  • Slow for what it does

The refactor is looking to modernize the codebase while adding major features that have been requested by users.

New features

  • --word-wrap wraps text on whole words at the console width (with break-after support for -, / and )), where v3 always broke lines mid-word (example for a 60 column wide console)
    $ cfonts "hello world friends" --word-wrap -f tiny
    █ █ █▀▀ █   █   █▀█   █ █ █ █▀█ █▀█ █   █▀▄
    █▀█ ██▄ █▄▄ █▄▄ █▄█   ▀▄▀▄▀ █▄█ █▀▄ █▄▄ █▄▀
    
    █▀▀ █▀█ █ █▀▀ █▄ █ █▀▄ █▀▀
    █▀  █▀▄ █ ██▄ █ ▀█ █▄▀ ▄▄█
  • --next allows multiple fonts on a single line: a render is a sequence of blocks, each with its own font, colors, letter spacing, line height and word-wrap
    $ cfonts "hello" -f chrome --next "world" -f tiny
    ╦ ╦ ╔═╗ ╦   ╦   ╔═╗█ █ █ █▀█ █▀█ █   █▀▄
    ╠═╣ ║╣  ║   ║   ║ ║▀▄▀▄▀ █▄█ █▀▄ █▄▄ █▄▀
    ╩ ╩ ╚═╝ ╩═╝ ╩═╝ ╚═╝                    
  • --valign aligns blocks of different font heights on one line (top, middle, bottom)
    $ cfonts "hello" -f chrome --next "world" -f tiny --valign bottom
    ╦ ╦ ╔═╗ ╦   ╦   ╔═╗                     
    ╠═╣ ║╣  ║   ║   ║ ║█ █ █ █▀█ █▀█ █   █▀▄
    ╩ ╩ ╚═╝ ╩═╝ ╩═╝ ╚═╝▀▄▀▄▀ █▄█ █▀▄ █▄▄ █▄▀
  • --stdin enables piped input as first class: echo hi | cfonts works as before but since we now have multiple blocks we need --stdin and --next-stdin to explicitly tell what block should take from the piped input
    $ echo " world" | cfonts hello --next-stdin -f tiny
    
    ██╗  ██╗ ███████╗ ██╗      ██╗       ██████╗                        
    ██║  ██║ ██╔════╝ ██║      ██║      ██╔═══██╗                       
    ███████║ █████╗   ██║      ██║      ██║   ██║  █ █ █ █▀█ █▀█ █   █▀▄
    ██╔══██║ ██╔══╝   ██║      ██║      ██║   ██║  ▀▄▀▄▀ █▄█ █▀▄ █▄▄ █▄▀
    ██║  ██║ ███████╗ ███████╗ ███████╗ ╚██████╔╝                       
    ╚═╝  ╚═╝ ╚══════╝ ╚══════╝ ╚══════╝  ╚═════╝                        
    
  • cfonts now also runs in the browser console: a dedicated browser console target renders %c-styled console.log banners
    // what BrowserHost.say() hands to console.log:
    console.log(
    	"\n\n%c█ █ █%c\n%c█▀█ █%c\n\n",
    	"color:#ea3223", "", "color:#ea3223", "",
    );
  • First-party integrations behind off-by-default feature flags: a ratatui widget (renders straight into the TUI buffer, re-wraps on resize), Leptos and Dioxus components (SSR-safe)
    frame.render_widget(&CfontsWidget { options: &options, overrides: RenderOverrides::default().with_seed(0) }, frame.area());
  • Example for a Topcoat component will serve as a way to use cfonts there until they are stable and I will publish a topcoat component directly
  • Deterministic candy colors: an optional seed makes randomized colors reproducible, for tests, snapshots, and identical SSR/hydration output
    let candy: Options = Cfonts::text("hi").colors(vec![Color::CANDY]).into();
    let host = RustHost::from_overrides(RenderOverrides::default().with_seed(42));
    
    assert_eq!(host.render(&TerminalTarget::default(), &candy).text, host.render(&TerminalTarget::default(), &candy).text);
  • FORCE_SIZE environment override controls output width identically in every terminal host, the CLI, RustHost and the npm Node host (0 = unlimited)
    $ FORCE_SIZE=6 cfonts "HELLO WORLD" -f console
    hello
    world
    $ FORCE_SIZE=0 cfonts "HELLO WORLD" -f console
    hello world
  • CI logs render in real color in the npm package too: GitHub Actions and CircleCI get 24-bit truecolor instead of v3's 16 colors
  • Gradients live in --colors now in the CLI. The delimiter picks the shape:
    • -c red,blue sets one color per font slot
    • -c red-blue is a two stop gradient
    • -c red:blue:green is a transition through every stop
    • -c pride is a gradient of its own
      This works per text block after --next, so blocks can carry their own gradients. --independent-gradient is a global flag. The -g/--gradient and -t/--transition-gradient flags are gone and print a teaching error with the new spellings. Malformed values get their own errors for mixed delimiters, empty segments, a dash gradient with more than two stops, a preset inside a list, and system or candy used as a gradient stop
  • Backgrounds take gradients too: -b red-blue ramps from the top down and -b red:yellow:green and the presets work as well, v3 rejected -b red-blue
  • -c on the first block colors every block after marking it as global: a gradient set there carries on across the blocks and a block opts out with its own -c flag
  • New short flags: -n for --next, -y for --valign, -w for --word-wrap and -d for --demo
  • The help screen teaches more. Every option carries at least one example and a few lines on what it does and when it applies, the usage header explains the | line break, and the examples section is grouped with a note above each group
  • Added --demo command to CLI to show all fonts, with color support
  • Added new bubble font with 1 color support
    $ cfonts bubble --font bubble
     ____   __  __   ____   ____   __     ____ 
    (  _ \ (  )(  ) (  _ \ (  _ \ (  )   (  __)
     ) _ (  )(__)(   ) _ (  ) _ ( / (_/\  ) _) 
    (____/ (______) (____/ (____/ \____/ (____)
  • Added new retro font with 3 color support
    $ cfonts retro --font retro
    █▀▀▄ █▀▀▀ ▀▀█▀▀ █▀▀▄ ▄▀▀▀▄
    █▀▀▄ █▀▀    █   █▀▀▄ █   █
    ▀  ▀ ▀▀▀▀   ▀   ▀  ▀  ▀▀▀ 
  • Added new braille font with 2 color support
    $ cfonts braille --font braille
    ⣏⡱ ⣏⡱ ⣎⣱ ⡇ ⡇  ⡇  ⣏⡉
    ⠧⠜ ⠇⠱ ⠇⠸ ⠇ ⠧⠤ ⠧⠤ ⠧⠤
  • Added new dense font with 3 color support
    $ cfonts dense --font dense
    ┳┓ ┏┓ ┳┓ ┏┓ ┏┓
    ┃┃ ┣  ┃┃ ┗┓ ┣ 
    ┻┛ ┗┛ ┛┗ ┗┛ ┗┛
  • Added new board font with 4 color support
    $ cfonts board --font board
     ┌───────┐  ┌───────┐  ┌───────┐  ┌───────┐  ┌──────┐ 
    ═│∙  ╒═╕∙│══│∙  ╒═╕∙│══│∙  ╒═╕∙│══│∙  ╒═╕∙│══│∙  ╒═╕└┐
    ░│   └─┘╒╛░░│   │░│ │░░│   └─┘ │░░│   └─┘╒╛░░│   │░│ │
    ▒│   ╒═╕└┐▒▒│   │▒│ │▒▒│   ╒═╕ │▒▒│   ╒═╕└┐▒▒│   │▒│ │
    ▓│   │▓│ │▓▓│   └─┘ │▓▓│   │▓│ │▓▓│   │▓│ │▓▓│   └─┘ │
    ═│∙  └─┘∙│══│∙     ∙│══│∙  │═│∙│══│∙  │═│∙│══│∙    ∙╒╛
     ╘═══════╛  ╘═══════╛  ╘═══╛ ╘═╛  ╘═══╛ ╘═╛  ╘══════╛ 
  • Added new edge font with 3 color support
    $ cfonts edge --font edge
    ╔═╝ ╔═  ╔═╝ ╔═╝
    ╔═╝ ║ ║ ║ ║ ╔═╝
    ══╝ ══  ══╝ ══╝
  • Added new thin font with 3 color support
    $ cfonts thin --font thin
    ╶┬╴ ╷ ╷ ╷ ╭╮╷
     │  ├─┤ │ │╰┤
     ╵  ╵ ╵ ╵ ╵ ╵
  • Added new depth font with 2 color support
    $ cfonts depth --font depth
     /███   /█▀▀▀/  /████  /███  /█ /█
    │ ▓_/▓ │ ▓▓▓   │ ▓ /▓ │//▓/ │ ▓▓▓▓
    │ ▒│ ▒ │ ▒_/   │ ▒▒▒/  │ ▒  │ ▒_/▒
    │ ░░░/ │ ░░░░░ │ ░_/   │ ░  │ ░│ ░
    │/__/  │/____/ │//     │//  │//│//
  • Added new neat font with 1 color support
    $ cfonts neat --font neat
    ╒═══╗  ╒═╗ ╒═════╗ ╒══════╗ ╒═════╗
    │   ╚╗ │ ║ │ ╓───╜ │ ╓──┐ ║ └─┐ ╓─╜
    │ ╓┐ ╚╗│ ║ │ ╚═╗   │ ╚══╛ ║   │ ║  
    │ ║└┐ ╚╡ ║ │ ╓─╜   │ ╓──┐ ║   │ ║  
    │ ║ └┐   ║ │ ╚═══╗ │ ║  │ ║   │ ║  
    └─╜  └───╜ └─────╜ └─╜  └─╜   └─╜  
  • Added new bridge font with 2 color support
    $ cfonts bridge --font bridge
    ▄─▄ ▄─▄ ─▄─ ──▄ ▄─▄ ▄─▄
    █─▌ █─▀  █  █ █ █─▄ █─ 
    ▀─▀ ▀ ▀ ─▀─ ──▀ ▀─▀ ▀─▀
  • Added new frost font with 2 color support
    $ cfonts frost --font frost
    ░░░░░░▌ ░░░░░▌    ░░░░░▌  ░░░░░░▌ ░░░░░░▌
    ░░▌     ░░▌ ░░▌  ░░▌  ░░▌ ░░▌       ░░▌  
    ░░░░▌   ░░░░░▌   ░░▌  ░░▌  ░░░░▌    ░░▌  
    ░░▌     ░░▌ ░░▌  ░░▌  ░░▌     ░░▌   ░░▌  
    ░░▌     ░░▌  ░░▌  ░░░░░▌  ░░░░░░▌   ░░▌ 
  • Added new wire font with 1 color support
    $ cfonts wire --font wire
    ┌──┐ ┌──┐ ┌───┐ ┌──────┐  ┌────┐   
    │  │ │  │ ┌───┐ │  ──┘ │┐ │  ───┐  
    │  ┌─┐  │ │   │ │  ┌─┐  │ │  ┌──┴─┐
    │ ─┘ └─ │ │   │ │  │ │  │ │       │
    └───┴───┘ └───┘ └──┘ └──┘ └───────┘
  • Added new vision font with 3 color support
    $ cfonts vision --font vision
    ╥╥╥┬┬┬ ─╥─── ╓──┐┐┐ ─╥─── ╓──┐┐┐ ╓┐┌┌┌
    ╙╖┌┘┘┘  ║║║  ╙──┐┐┐  ║║║  ║║║│││ ║││││
     ╙┘┘┘  ─╨─── ╙──┘┘┘ ─╨─── ╙──┘┘┘ ╨┴┘┴┴

Bug fixes

  • Fixed color markers for the 3d and block fonts
    611982143-70ee58cb-7929-4efe-9e79-2a547246d1e7
  • Fixed various small glyph issues with multiple fonts:
    Before:
    image
    After:
    image
  • Fixed the pallet font shipping ; and , swapped
    $ cfonts ";," -f pallet
    ╔╗───
    ║║───
    ╚╝───
    ╔╗─╔╗
    ╚╣─╚╣
    ─╝──╝
  • Fixed shade and chrome fonts containing empty spaces with color tags
    - "<c1>   </c1>",
    + r"   ",
  • Removed Simple3D font as it was faulty and hard to read
  • Redrew slick with light lines (─│ instead of ━┃) and without its leading ╱ column, and simpleblock went from 7 rows to 5 without its 2 column indent
  • Fixed ANSI-256 conversion: colors now map to the genuinely nearest entry of the real xterm palette (gray ramp included), v3 rounded against a fictional evenly-spaced cube and could land on an entry over 100× further away (#1b1c1c got 59 #5f5f5f instead of 234 #1c1c1c)
  • Fixed broken pipes: closing the reader no longer crashes the render
    $ cfonts "$(printf 'hello world %.0s' {1..60})" | head -1 # exits 0, v3 panicked with 'failed printing to stdout: Broken pipe (os error 32)' and exit code 101
  • Errors and warnings go to stderr (64 usage, 74 I/O) and add the help text for the command that failed, v3 printed errors to stdout
    $ cfonts hi -f nofont > /dev/null
     ERROR  The option "font" was given an invalid value "nofont"
    
      Set the font
      --font, -f
      Applies to the current text block
      Every block can use its own font
      $ cfonts hello --font chrome
      $ cfonts hello --font tiny --next " world" --font block
      Possible arguments:
        block, board, braille, bridge, bubble,
        chrome, dense, depth, edge, 3d,
        frost, grid, huge, neat, pallet,
        retro, shade, simple, simpleblock, slick,
        thin, tiny, vision, wire, console
    $ echo $?
    64
  • Mistyped flags warn loudly instead of being silently ignored and a fatal parse error still reports every warning collected on the way
    $ cfonts hello --nope > /dev/null
     WARNING  An unknown flag "--nope" was used and ignored
  • NO_COLOR follows its spec exactly: an empty NO_COLOR counts as unset, and every possible FORCE_COLOR spelling resolves the way Node reads it
  • Windows consoles get VT escape processing switched on per stream and their real palette dated by build number, v3 assumed every Windows console was 16-color
  • Color detection is per stream: TERM=dumb and piped stderr never receive escape codes unless FORCE_COLOR asks for them
  • The npm package measures the terminal on every render, v3 froze the width at import time, so window resizes were never noticed
  • Raw mode (\r\n) applies to every line break including the closing one, v3 mixed line endings
    $ cfonts hi -f console --raw-mode | od -c | head -1
    0000000   \r  \n  \r  \n   h   i  \r  \n  \r  \n  \r  \n
    # v3: 0000000   \r  \n  \r  \n   h   i  \r  \n  \r  \n  \n
  • Empty input renders one bare row inside the padding instead of a font-height slab of blank lines
  • Running cfonts without text or with an empty pipe errors with exit 64 instead of printing blank lines
  • Invalid hex colors return a typed error instead of panicking the library or silently painting black
    assert!(Rgb::from_hex("#nope").is_err());

Breaking changes

  • The npm package no longer ships a cfonts command
  • The npm package is ESM only and needs Node >=22.12.0
  • v3's say and render functions are gone in Rust and npm: the builder hands its options to a host and a target
  • v3's --env/-e, --debug/-d and --debug-level/-x are gone, -d now means --demo. The old spellings read as an unknown flag followed by stray text
  • -b transparent is gone, the word for no background is system
  • The simple3d font is removed (also under bug fixes)
  • The npm builder refuses letterSpacing, lineHeight, maxLength and canvasWidth above 65535 with a TypeError
  • The leading letter space before the first glyph of a line is gone, so centered text sits up to one column further left and one more glyph fits a line, right alignment is byte-identical
  • --letter-spacing N on the console font inserts N spaces, v3 inserted N−1
  • A soft wrap swallows the space it breaks at, -m 5 "Hello World" is two lines where v3 made three
  • Hex colors may omit the #, so -c fab paints #fab
  • Backgrounds paint as \e[K bands into pipes too, less -R shows them

API improvements

  • One Rust core compiles to both native and WebAssembly
  • Fonts are code, not JSON: glyphs are static Rust data validated by a proc macro while the crate builds, malformed font data is a compile error, not a runtime panic or a silent empty render
    pub(super) static FONT_BLOCK: FontFile = font! {
    	font: Font::Block,
    	name: "block",
    	colors: 2,
    	rows: 6,
    	line_height: 1,
    	letter_spacing: 1,
    	checks: {
    		plain_segments_are_spaces,
    		slots_paint_only: [['█', '▄', '▀'], ['╚', '╝', '╔', '╗', '═', '║']],
    	},
    	// …
    	glyphs: {
    		'A' => [
    			r"<c1> █████</c1><c2>╗ </c2>",
    			r"<c1>██</c1><c2>╔══</c2><c1>██</c1><c2>╗</c2>",
    			r"<c1>███████</c1><c2>║</c2>",
    			r"<c1>██</c1><c2>╔══</c2><c1>██</c1><c2>║</c2>",
    			r"<c1>██</c1><c2>║</c2><c1>  ██</c1><c2>║</c2>",
    			r"<c2>╚═╝  ╚═╝</c2>",
    		],
    		// …
    	},
    };
  • A fluent builder (Cfonts::text("hi").font(Font::Chrome)) replaces the 18-field Options struct and misuse is a compile error with a custom compiler message
    use cfonts::{Cfonts, Font, Host, Options, RustHost, TerminalTarget};
    let options: Options = Cfonts::text("hello").font(Font::Chrome).next("world").font(Font::Tiny).into();
    RustHost::default().say(&TerminalTarget::default(), &options)?;
  • Rendering splits into pure Targets (ANSI, HTML, browser console) and Hosts that resolve terminal capabilities once per render so new output targets need no changes to core render code, the ratatui widget paints the layout rows straight into the buffer
  • render() returns the artifact, say() writes it; the library itself never prints, never panics on input and fallible operations return typed errors
    let rendered = RustHost::default().render(&TerminalTarget::default(), &options); // nothing written anywhere
    assert!(rendered.text.contains("█"));
  • The npm layer is natively typed TypeScript with real enums and the same fluent builder, both the Node and browser entries are type-checked under strict mode
  • Colors are typed by kind (Color<Text>, Color<Gradient>, Color<Background>), so Color::CANDY as a background is a compile error with its own message
  • render_with renders with explicit overrides and no host, for tests and custom targets
  • Every public builder method carries a compiled doc example
  • Every count has one type and the type is the bound: letter_spacing, line_height, max_length, the canvas width and FORCE_SIZE are u16 (0 through 65535), the candy seed is a u32. The command line reports a larger number as too large, the Rust API cannot express one, Node throws a TypeError for it, nothing clamps and nothing is cut on the way across the wasm boundary
    $ cfonts hi --letter-spacing 65536
     ERROR  The option "letter-spacing" was given a value that is too large
    "65536" is above its limit of 65535

Performance

  • render() and say() borrow &Options instead of consuming owned options through every call
  • Glyphs no longer copy through the output during rendering: v3 appended every glyph's rows into the output strings as it walked the input (output[row] += line per row, per character, on freshly painted copies) — v4 stages a line as Copy glyph handles into the static font tables and writes text exactly once, when the target paints the rows
  • Fonts skip parsing entirely: v3 rebuilt a HashMap of all 13 fonts' JSON (164 kB) and re-parsed the chosen font on every single render call while v4 compiles glyphs in as static data with direct array-index lookup
  • Color markers are parsed into typed segments while the crate builds, not while it renders: to paint one row of one character v3 called replace twice per color (once for <cN>, once for </cN>), each call scanning the whole row and allocating a fresh String, so a 2-color font like block did 8 replace calls per row per character on every render (hello world = 576 calls before the first byte reached the terminal) and ran the same loop with empty replacements every time it needed to strip the tags to measure a glyph (360 more). v4's font! macro parses the markers once at compile time and stores the width on the glyph, so the renderer walks a run of integer segments in the font's tables: no marker search, no allocation, the color is an index into the palette and the text is written exactly once
      // what font! compiles r"<c1>██</c1><c2>╔══</c2><c1>██</c1><c2>╗</c2>" into
      Row { first: 3, count: 4 }
      Segment { start: 2, len: 6, slot: Some(0) },  // "██"
      Segment { start: 21, len: 9, slot: Some(1) }, // "╔══"
      Segment { start: 2, len: 6, slot: Some(0) },  // "██", the same bytes
      Segment { start: 17, len: 3, slot: Some(1) }, // "╗"
  • Fewer segments mean less work per row: font! refuses to compile spaces outside a color tag beside one, and visible plain text in fonts that declare plain_segments_are_spaces, so spaces always belong to the neighboring colored run and a row is either entirely plain (whitespace-only rows) or entirely colored. Every segment is a loop iteration and a write, so folding the spaces in removes a segment for every run of spaces, and a color is never closed and reopened around a gap inside a glyph (an extra escape pair, up to 24 bytes for truecolor, to print a single space). The output looks identical because foreground color paints nothing on a space, but the renderer does less and the terminal receives and parses fewer bytes. It's the mirror image of the whitespace-only rows in shade and chrome that carried color tags for nothing
      - r" <c1>█████</c1><c2>╗</c2> ",
      + r"<c1> █████</c1><c2>╗ </c2>",
  • The render hot path allocates almost nothing (45 heap allocations for a short render versus 3,494 in v3): 5 .clone() / 6 .to_string() calls across the v4 library source versus 5 / 30 in v3, tests and doc comments excluded, output is written in one pass into a single growing String
  • Environment and terminal detection runs once per render instead of once per colored fragment
  • Dependencies: the crate went from 9 runtime dependencies (a 27-crate tree) to zero third-party (only platform bindings: libc on unix, windows-sys and windows-version on Windows); the npm package went from a 12-package closure to zero
  • Fonts compile into integer tables over one string of text per font instead of nested references: load-time fixups dropped from 16,875 to 1,564, the binary from 789 kB to 568 kB and each CLI start uses ~0.25 MB less memory
  • A layout row holds 12 bytes per glyph and a run of the same glyph, letter spaces above all, is one entry, so long texts peak below v3's heap
  • Measured against v3 with make compare: the API renders 30 to 7,700× faster, the CLI is faster in every scenario including startup, with 38 to 740× fewer allocations and a lower peak heap and memory everywhere

Quality

  • 80 Rust test functions became 870+ checks: 676 cargo tests plus 58 doc-tests, 91 Node tests, 50 wasm tests, 4 real-browser Playwright tests run against all 3 example pages, and TypeScript consumer type-checks for both entries
  • Width detection is proven against real oracles: tests open actual PTYs at chosen sizes, a run from a terminal also compares against stty size, and a Windows suite allocates a real console and checks against GetConsoleScreenBufferInfo
  • Color detection is a port of Node's own getColorDepth classifier, verified row for row against Node's test matrix
  • End-to-end tests spawn the real binary in a hermetic environment (every detection variable stripped), so the suite passes identically in any shell, v3's e2e suite fails on a clean machine today
  • ~98% line coverage under cargo-llvm-cov, make coverage runs the same coverage CI uploads
  • MSRV declared and CI-enforced (1.95.0), edition 2024, every cargo build, check, clippy, test and coverage call --locked, all GitHub Actions SHA-pinned, each feature flag compile-checked in isolation, wasm targets checked on every push to released and every pull request
  • Packaging is smoke-tested: LICENSE (GPL-3.0-or-later) verified present in both crates and the npm tarball and README in the cfonts crate and the npm tarball, v3.2.0 on npm shipped without any license text
  • One version number across the crate, the npm package and the macro pin, enforced by CI
  • cargo test works from a clean checkout, v3 required a Makefile run before the crate would even compile
  • Fonts follow their rules while the crate builds: a broken glyph fails the build at the exact row, and the macros carry 19 compile-fail cases plus a fixture font that has to compile and read back
  • The Leptos and Dioxus components are tested through their own server renderers against the core render
  • The Rust and Node examples are compared byte for byte in CI
  • CI runs the tests on Linux, macOS and Windows, each on the MSRV and stable
  • perf/ measures speed, allocations and memory on the API and CLI paths: make perf, make perf-save, make compare against v3 and make flamegraph

@dominikwilkowski
dominikwilkowski commented Jun 22, 2026 •
8000 Copy link
Copy Markdown
Owner Author

With the new type-safe fonts in rust I already found two bugs in the 3d font and the block font. Even with all the tests I have in the old code-base. Another win for compile time safety.

image

Reproducible like so:

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

0