| freeqsay: Nim port of the Clojure implementation fa53aa2 nandi 12h ago | 1 | ## freeqsay — cowsay with your FreeqWorld face. |
| 2 | ## |
| 3 | ## freeqsay <handle> <message...> |
| 4 | ## echo "hi" | freeqsay <handle> |
| 5 | ## freeqsay did:plc:… "offline, no resolve" |
| 6 | ## |
| 7 | ## As a library, this module re-exports the whole port: `freeqsay.say` (balloon |
| 8 | ## + face), `avatar` (traits and the 16×24 sprite), `ansi` (truecolor |
| 9 | ## half-block rendering), `png` (RGBA → PNG), `resolve` (handle → DID), |
| 10 | ## `balloon`, `sha256`. |
| 11 | |
| 12 | import freeqsay/[ansi, avatar, balloon, png, resolve, say, sha256] |
| 13 | export ansi, avatar, balloon, png, resolve, say, sha256 |
| 14 | |
| 15 | when isMainModule: |
| 16 | import std/[os, strutils] |
| 17 | |
| 18 | const usageText = """ |
| 19 | freeqsay — cowsay with a FreeqWorld character |
| 20 | |
| 21 | Usage: |
| 22 | freeqsay <handle> <message...> |
| 23 | freeqsay <handle> # message from stdin |
| 24 | echo "hello" | freeqsay <handle> |
| 25 | |
| 26 | Resolves a Bluesky handle to a DID, derives the deterministic avatar, |
| 27 | and prints a cowsay balloon above the character. A raw did:… works too. |
| 28 | |
| 29 | Options: |
| 30 | -W, --width <n> Balloon wrap width (default 40) |
| 31 | --scale <n> Character pixel scale (default 2) |
| 32 | --facing <dir> south | north | east | west (default south) |
| 33 | --think Thought bubble instead of speech |
| 34 | -o, --output <path> Write to file (else stdout) |
| 35 | -h, --help Show help |
| 36 | |
| 37 | Examples: |
| 38 | freeqsay jay.bsky.team hello from the timeline |
| 39 | freeqsay @pfrazee.com "ships free software" |
| 40 | freeqsay someone.bsky.social -W 20 short lines work |
| 41 | echo "piped in" | freeqsay jay.bsky.team |
| 42 | """ |
| 43 | |
| 44 | proc usage(code = 1) = |
| 45 | stderr.writeLine usageText |
| 46 | quit(code) |
| 47 | |
| 48 | proc positiveInt(s: string): int = |
| 49 | try: |
| 50 | result = parseInt(s.strip()) |
| 51 | except ValueError: |
| 52 | usage() |
| 53 | if result < 1: usage() |
| 54 | |
| 55 | type Args = object |
| 56 | handle, output: string |
| 57 | messageParts: seq[string] |
| 58 | width, scale: int |
| 59 | facing: Facing |
| 60 | think: bool |
| 61 | |
| 62 | proc parseArgs(argv: seq[string]): Args = |
| 63 | result = Args(width: 40, scale: 2, facing: fSouth) |
| 64 | var i = 0 |
| 65 | while i < argv.len: |
| 66 | let a = argv[i] |
| 67 | proc value(): string = |
| 68 | # A flag with no value left is a usage error, not an index crash. |
| 69 | if i + 1 >= argv.len: usage() |
| 70 | inc i |
| 71 | argv[i] |
| 72 | case a |
| 73 | of "-h", "--help": usage(0) |
| 74 | of "--think": result.think = true |
| 75 | of "-W", "--width": result.width = positiveInt(value()) |
| 76 | of "--scale": result.scale = positiveInt(value()) |
| 77 | of "--facing": |
| 78 | let f = value() |
| 79 | if f notin ["south", "north", "east", "west"]: usage() |
| 80 | result.facing = parseEnum[Facing](f) |
| 81 | of "-o", "--output": result.output = value() |
| 82 | else: |
| 83 | if a.startsWith("-"): |
| 84 | stderr.writeLine "unknown flag: " & a |
| 85 | usage() |
| 86 | elif result.handle.len == 0: |
| 87 | result.handle = a |
| 88 | else: |
| 89 | result.messageParts.add a |
| 90 | inc i |
| 91 | |
| 92 | proc main() = |
| 93 | let args = parseArgs(commandLineParams()) |
| 94 | if args.handle.len == 0: usage() |
| 95 | var message = args.messageParts.join(" ").strip() |
| 96 | if message.len == 0: |
| 97 | message = readAll(stdin).strip() |
| 98 | if message.len == 0: usage() |
| 99 | |
| 100 | let result = freeqsay(args.handle, message, width = args.width, |
| 101 | think = args.think, scale = args.scale, |
| 102 | facing = args.facing) |
| 103 | let text = result.output & "\n" |
| 104 | if args.output.len > 0: |
| 105 | writeFile(args.output, text) |
| 106 | else: |
| 107 | stdout.write text |
| 108 | stdout.flushFile() |
| 109 | |
| 110 | try: |
| 111 | main() |
| 112 | except CatchableError as e: |
| 113 | stderr.writeLine e.msg |
| 114 | quit(1) |