Repository navigation
V4 rewrite - #90
Draft
dominikwilkowski wants to merge 335 commits into
Draft
V4 rewrite#90dominikwilkowski wants to merge 335 commits into
dominikwilkowski wants to merge 335 commits into
Conversation
This was referenced Jun 21, 2026
Owner
Author
…tline of cli parser, fixed ANSI_RESET,
…and try_into trait
…rops its unread context
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
cfonts refactor
Motivation
The current Rust implementation (multiple years old) has accumulated technical debt:
Stringinstead of&str, causing unnecessary allocationsOptionsstruct, function-based API)The refactor is looking to modernize the codebase while adding major features that have been requested by users.
New features
--word-wrapwraps 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 █ █ █▀▀ █ █ █▀█ █ █ █ █▀█ █▀█ █ █▀▄ █▀█ ██▄ █▄▄ █▄▄ █▄█ ▀▄▀▄▀ █▄█ █▀▄ █▄▄ █▄▀ █▀▀ █▀█ █ █▀▀ █▄ █ █▀▄ █▀▀ █▀ █▀▄ █ ██▄ █ ▀█ █▄▀ ▄▄█--nextallows 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--valignaligns blocks of different font heights on one line (top,middle,bottom)--stdinenables piped input as first class:echo hi | cfontsworks as before but since we now have multiple blocks we need--stdinand--next-stdinto explicitly tell what block should take from the piped input%c-styledconsole.logbannersFORCE_SIZEenvironment override controls output width identically in every terminal host, the CLI,RustHostand the npm Node host (0= unlimited)--colorsnow in the CLI. The delimiter picks the shape:-c red,bluesets one color per font slot-c red-blueis a two stop gradient-c red:blue:greenis a transition through every stop-c prideis a gradient of its ownThis works per text block after
--next, so blocks can carry their own gradients.--independent-gradientis a global flag. The-g/--gradientand-t/--transition-gradientflags 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, andsystemorcandyused as a gradient stop-b red-blueramps from the top down and-b red:yellow:greenand the presets work as well, v3 rejected-b red-blue-con 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-cflag-nfor--next,-yfor--valign,-wfor--word-wrapand-dfor--demo|line break, and the examples section is grouped with a note above each group--democommand to CLI to show all fonts, with color supportbubblefont with1color supportretrofont with3color supportbraillefont with2color supportdensefont with3color supportboardfont with4color supportedgefont with3color supportthinfont with3color supportdepthfont with2color supportneatfont with1color supportbridgefont with2color supportfrostfont with2color supportwirefont with1color supportvisionfont with3color supportBug fixes
3dandblockfontsBefore:
After:
palletfont shipping;and,swapped$ cfonts ";," -f pallet ╔╗─── ║║─── ╚╝─── ╔╗─╔╗ ╚╣─╚╣ ─╝──╝shadeandchromefonts containing empty spaces with color tagsSimple3Dfont as it was faulty and hard to readslickwith light lines (─│instead of━┃) and without its leading╱column, andsimpleblockwent from 7 rows to 5 without its 2 column indent#1b1c1cgot 59#5f5f5finstead of 234#1c1c1c)64usage,74I/O) and add the help text for the command that failed, v3 printed errors to stdoutNO_COLORfollows its spec exactly: an emptyNO_COLORcounts as unset, and every possibleFORCE_COLORspelling resolves the way Node reads itTERM=dumband piped stderr never receive escape codes unlessFORCE_COLORasks for them\r\n) applies to every line break including the closing one, v3 mixed line endingscfontswithout text or with an empty pipe errors with exit64instead of printing blank linesBreaking changes
cfontscommand>=22.12.0sayandrenderfunctions are gone in Rust and npm: the builder hands its options to a host and a target--env/-e,--debug/-dand--debug-level/-xare gone,-dnow means--demo. The old spellings read as an unknown flag followed by stray text-b transparentis gone, the word for no background issystemsimple3dfont is removed (also under bug fixes)letterSpacing,lineHeight,maxLengthandcanvasWidthabove 65535 with aTypeError--letter-spacing Non theconsolefont inserts N spaces, v3 inserted N−1-m 5 "Hello World"is two lines where v3 made three#, so-c fabpaints#fab\e[Kbands into pipes too,less -Rshows themAPI improvements
Cfonts::text("hi").font(Font::Chrome)) replaces the 18-fieldOptionsstruct and misuse is a compile error with a custom compiler messagerender()returns the artifact,say()writes it; the library itself never prints, never panics on input and fallible operations return typed errorsColor<Text>,Color<Gradient>,Color<Background>), soColor::CANDYas a background is a compile error with its own messagerender_withrenders with explicit overrides and no host, for tests and custom targetsletter_spacing,line_height,max_length, the canvas width andFORCE_SIZEareu16(0 through 65535), the candy seed is au32. The command line reports a larger number as too large, the Rust API cannot express one, Node throws aTypeErrorfor it, nothing clamps and nothing is cut on the way across the wasm boundaryPerformance
render()andsay()borrow&Optionsinstead of consuming owned options through every calloutput[row] += lineper row, per character, on freshly painted copies) — v4 stages a line asCopyglyph handles into the static font tables and writes text exactly once, when the target paints the rowsreplacetwice per color (once for<cN>, once for</cN>), each call scanning the whole row and allocating a freshString, so a 2-color font likeblockdid 8replacecalls 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'sfont!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 oncefont!refuses to compile spaces outside a color tag beside one, and visible plain text in fonts that declareplain_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 inshadeandchromethat carried color tags for nothing.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 Stringmake 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 everywhereQuality
stty size, and a Windows suite allocates a real console and checks againstGetConsoleScreenBufferInfogetColorDepthclassifier, verified row for row against Node's test matrixmake coverageruns the same coverage CI uploads--locked, all GitHub Actions SHA-pinned, each feature flag compile-checked in isolation, wasm targets checked on every push toreleasedand every pull requestcargo testworks from a clean checkout, v3 required a Makefile run before the crate would even compileperf/measures speed, allocations and memory on the API and CLI paths:make perf,make perf-save,make compareagainst v3 andmake flamegraph