A C# / .NET 10 Native-AOT parity port of ttfx — a single-binary terminal text-effects CLI with 37 effects. Pipe text in, pick an effect:
printf 'Hello\n' | ./artifacts/ttfx wipe
fortune | ./artifacts/ttfx --random-effectEvery clip is hypa-logo.txt piped through that effect with --seed 42.
Regenerate with ./bin/build and tools/render_examples.py (Pillow + ffmpeg).
This is a port of a port. Every effect, the animation engine, and the CLI are ChrisBuilds' design in TerminalTextEffects (TTE). ttfx translated that work to Rust; this project translates ttfx to C# and adds nothing to the art. If you like what you see here, star the upstream projects.
Attribution details are in LICENSE and NOTICE. Effect ideas belong upstream.
| Topic | Behavior here | Why |
|---|---|---|
| Random number generator | xoshiro256++ (same as ttfx) | Inherited from ttfx; --seed matches ttfx and this port, not Python TTE's Mersenne Twister |
| Broken-pipe exit status | 0 | Upstream Python swallows EPIPE; we match that contract |
| SIGTERM exit status | Matches ttfx (signal 15 via WIFSIGNALED) |
Not a divergence |
| Shell completions | Hand-written templates | Zero NuGet packages — no clap_complete; text differs from ttfx's generated scripts |
| Plugin effects | Not supported | No Python interpreter to load them |
| Cell width | One codepoint = one cell (Rune) |
Faithfully reproduces upstream; no wcwidth
|
| Byte-exact parity | Verified on tested RIDs only | See Fidelity below |
This is a parity port: given the same input, config, and seed, hypa-ttfx aims for
byte-identical frame output to the pinned ttfx binary (REFERENCE.md).
Verified on this project's CI / local testing:
| RID | Status |
|---|---|
osx-x64 |
Verified locally (AOT publish on this developer host) |
osx-arm64 |
Expected on Apple Silicon hosts (same gate; build with ./bin/build osx-arm64) |
linux-x64 |
Verified in CI (oracle suites behind Linux gate) |
linux-arm64 |
Expected but unverified until a matching CI runner executes the oracle suites |
Publishing four RIDs under a claim tested on one machine would overclaim — maths-library behavior, AOT codegen, and signal delivery can differ by architecture. The README states what has actually been measured.
./bin/test runs the full gate: unit goldens, AOT publish, CLI corpus, signal tests, and
(on Linux or when reference/ttfx is present on macOS) byte-exact parity suites.
Other .NET 10+ programs consume the engine as Hypa.Ttfx:
dotnet add package Hypa.Ttfxusing Ttfx;
IReadOnlyList<string> frames = TextEffects.Render(
"wipe",
"Hello from .NET\n",
new TextEffectOptions { Seed = 42 });
foreach (string frame in TextEffects.EnumerateFrames("smoke", "Hello\n", new TextEffectOptions
{
Seed = 42,
EffectArguments = ["--final-gradient-stops", "8A008A", "00D1FF"],
}))
{
Console.Write(frame);
}Render / EnumerateFrames use a virtual clock and ignore the process TTY
(canvas sizes to the input). TextEffects.Run writes a live animation to a
stream. Per-effect flags are the same tokens as the CLI.
The CLI is also a dotnet tool (framework-dependent; Native-AOT builds are
the GitHub Release assets):
dotnet tool install -g Hypa.Ttfx.Tool
printf 'Hello\n' | ttfx wipe./bin/build # AOT publish to artifacts/ttfx (host RID)
./bin/build linux-x64 # cross-RID publish when toolchain is available
./bin/test
./bin/pack # Hypa.Ttfx + Hypa.Ttfx.Tool into artifacts/nugetThis repo consumes zero NuGet packages — everything comes from
Microsoft.NETCore.App. Downstream apps add Hypa.Ttfx as a package
reference; that is the supported way to reuse the engine.
Version lives in Directory.Build.props. A tag vX.Y.Z (semver, optional
prerelease suffix) runs .github/workflows/release.yml: macOS + Linux test
gates, Native-AOT binaries for osx-arm64 / osx-x64 / linux-x64 /
linux-arm64, both nupkgs, a GitHub Release, nuget.org, and GitHub Packages.
# bump <Version> in Directory.Build.props, commit, then:
git tag v0.3.1
git push origin v0.3.1One-time nuget.org setup (Trusted Publishing — no long-lived API key):
- On nuget.org → Trusted Publishing, add a policy for this repo, workflow
file
release.yml, environmentrelease. - Store your nuget.org username (profile name, not email) as the
NUGET_USERActions secret. - Optional fallback:
NUGET_API_KEYif OIDC is not available yet.
workflow_dispatch stamps a version and uploads artifacts; turn on
publish only when you intend to cut the release.
<producer> | ttfx [terminal options] <effect> [effect options]
ttfx --help
ttfx <effect> --help
ttfx --random-effect # --include-effects / --exclude-effects to filter
ttfx --print-completion bash|zsh
Terminal options go before the effect name; effect options after it. Names and defaults match ttfx / TTE.
See REFERENCE.md for the pinned ttfx and upstream TTE commits.




































