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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
|
## freeqsay — cowsay with your FreeqWorld face.
##
## freeqsay <handle> <message...>
## echo "hi" | freeqsay <handle>
## freeqsay did:plc:… "offline, no resolve"
##
## As a library, this module re-exports the whole port: `freeqsay.say` (balloon
## + face), `avatar` (traits and the 16×24 sprite), `ansi` (truecolor
## half-block rendering), `png` (RGBA → PNG), `resolve` (handle → DID),
## `balloon`, `sha256`.
import freeqsay/[ansi, avatar, balloon, png, resolve, say, sha256]
export ansi, avatar, balloon, png, resolve, say, sha256
when isMainModule:
import std/[os, strutils]
const usageText = """
freeqsay — cowsay with a FreeqWorld character
Usage:
freeqsay <handle> <message...>
freeqsay <handle> # message from stdin
echo "hello" | freeqsay <handle>
Resolves a Bluesky handle to a DID, derives the deterministic avatar,
and prints a cowsay balloon above the character. A raw did:… works too.
Options:
-W, --width <n> Balloon wrap width (default 40)
--scale <n> Character pixel scale (default 2)
--facing <dir> south | north | east | west (default south)
--think Thought bubble instead of speech
-o, --output <path> Write to file (else stdout)
-h, --help Show help
Examples:
freeqsay jay.bsky.team hello from the timeline
freeqsay @pfrazee.com "ships free software"
freeqsay someone.bsky.social -W 20 short lines work
echo "piped in" | freeqsay jay.bsky.team
"""
proc usage(code = 1) =
stderr.writeLine usageText
quit(code)
proc positiveInt(s: string): int =
try:
result = parseInt(s.strip())
except ValueError:
usage()
if result < 1: usage()
type Args = object
handle, output: string
messageParts: seq[string]
width, scale: int
facing: Facing
think: bool
proc parseArgs(argv: seq[string]): Args =
result = Args(width: 40, scale: 2, facing: fSouth)
var i = 0
while i < argv.len:
let a = argv[i]
proc value(): string =
# A flag with no value left is a usage error, not an index crash.
if i + 1 >= argv.len: usage()
inc i
argv[i]
case a
of "-h", "--help": usage(0)
of "--think": result.think = true
of "-W", "--width": result.width = positiveInt(value())
of "--scale": result.scale = positiveInt(value())
of "--facing":
let f = value()
if f notin ["south", "north", "east", "west"]: usage()
result.facing = parseEnum[Facing](f)
of "-o", "--output": result.output = value()
else:
if a.startsWith("-"):
stderr.writeLine "unknown flag: " & a
usage()
elif result.handle.len == 0:
result.handle = a
else:
result.messageParts.add a
inc i
proc main() =
let args = parseArgs(commandLineParams())
if args.handle.len == 0: usage()
var message = args.messageParts.join(" ").strip()
if message.len == 0:
message = readAll(stdin).strip()
if message.len == 0: usage()
let result = freeqsay(args.handle, message, width = args.width,
think = args.think, scale = args.scale,
facing = args.facing)
let text = result.output & "\n"
if args.output.len > 0:
writeFile(args.output, text)
else:
stdout.write text
stdout.flushFile()
try:
main()
except CatchableError as e:
stderr.writeLine e.msg
quit(1)
|