freeqsay
Cowsay with your FreeqWorld face. Written in Nim — a port of the
Clojure/Jolt implementation, which
is itself a port of the original TypeScript.
freeqsay <handle> <message>
Resolves a Bluesky handle → DID, derives the deterministic avatar-v1 character,
and prints a cowsay balloon above truecolor ANSI art.
Usage
freeqsay jay.bsky.team hello from the timeline
freeqsay @pfrazee.com "ships free software"
echo "piped in" | freeqsay jay.bsky.team
# wrap width, thought bubble, scale
freeqsay someone.bsky.social -W 20 short lines
freeqsay jay.bsky.team --think hmm
freeqsay jay.bsky.team --scale 3 big face says hi
# raw DID (no network)
freeqsay did:plc:ewvi7nmzuoqusbcablsm7c4h offline ok
Without a built binary, run it straight from the source tree:
nim r -d:ssl src/freeqsay.nim jay.bsky.team hello world
Library
import freeqsay
echo freeqsay("jay.bsky.team", "hello").output
Modules: freeqsay/say (balloon + face), freeqsay/avatar (traits and the
16×24 sprite), freeqsay/ansi (truecolor half-block rendering), freeqsay/png
(RGBA → PNG), freeqsay/resolve (handle → DID), freeqsay/balloon,
freeqsay/sha256 (SHA-256 / HMAC / HKDF). Importing freeqsay re-exports all
of them.
Development
just build # optimized binary → ./freeqsay
just static # fully static binary → ./freeqsay-static
just test # test suite
just run <handle> <message> # run from source, no build
just shot <handle> <message> # gapless termshot png
just --list # everything else
just static produces a binary with no dynamic dependencies at all — musl
libc and OpenSSL linked in, nothing dlopen'd — so it runs on any Linux. It
builds through nimstatic, pinned in
tools/nimstatic as a DotSlash file: the first run
fetches that exact build, checks its hash and caches it, and the URL names a
commit so it cannot move. A static binary carries no CA store, so set
SSL_CERT_FILE when resolving handles on a host without one at the default
path.
nimble build and nimble test work too. -d:ssl is set in config.nims,
since handle resolution goes over HTTPS through the stdlib client.
Everything but the HTTP call and JSON parsing is pure Nim with no dependencies:
SHA-256, HMAC and HKDF live in freeqsay/sha256, and the PNG encoder writes
stored deflate blocks.
How it works
handle → resolveHandle → did
did → HKDF-SHA256(avatar-v1) → traits → 16×24 sprite → ANSI
message → cowsay balloon
fixtures/avatar-conformance.json pins the cross-implementation contract:
canonical seed, traits, and a sprite hash over all four facings. The suite
replays it, so this port derives byte-identical characters to the Clojure and
TypeScript implementations — including the sfc32 PRNG, reproduced on uint32
lanes so trait selection matches the JavaScript original bit for bit.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
|