nandi/freeqsay-nimpublic Fork 0
main
Commits
Clone
git clone https://git.rickub.com/nandi/freeqsay-nim.git
git clone ssh://git@rickub.com/nandi/freeqsay-nim.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

ansi.nim · 124 lines · 3.9 KBNim Blame HistoryRaw
freeqsay: Nim port of the Clojure implementation fa53aa2 nandi 6h ago1## Sprite → truecolor ANSI for terminals (half-block packing).
2## Each terminal cell covers 2 vertical pixels: bg = upper, fg = lower.
3## Sequences are kept compact (single CSI, reset once per line) so tools like
4## termshot/bunt and narrow PTYs do not choke mid-escape.
5
6import std/[strutils, strformat]
7import ./avatar
8
9type Rgba* = object
10 width*, height*: int
11 data*: seq[int] ## w*h*4 bytes
12
13func hexToRgb*(hex: string): (int, int, int) =
14 (parseHexInt(hex[1 .. 2]), parseHexInt(hex[3 .. 4]), parseHexInt(hex[5 .. 6]))
15
16func spriteToRgba*(s: Sprite): Rgba =
17 ## Expand a palette-indexed sprite to RGBA (alpha 0 for transparent).
18 result = Rgba(width: s.width, height: s.height,
19 data: newSeq[int](s.width * s.height * 4))
20 for i, idx in s.pixels:
21 let color = s.palette[idx]
22 if color != "transparent":
23 let (r, g, b) = hexToRgb(color)
24 let o = i * 4
25 result.data[o] = r
26 result.data[o + 1] = g
27 result.data[o + 2] = b
28 result.data[o + 3] = 255
29
30func scaleRgba*(src: Rgba, scale: int): Rgba =
31 ## Nearest-neighbor upscale.
32 if scale <= 1:
33 return src
34 let
35 w = src.width * scale
36 h = src.height * scale
37 result = Rgba(width: w, height: h, data: newSeq[int](w * h * 4))
38 for y in 0 ..< h:
39 let sy = y div scale
40 for x in 0 ..< w:
41 let
42 sx = x div scale
43 si = (sy * src.width + sx) * 4
44 di = (y * w + x) * 4
45 for k in 0 ..< 4:
46 result.data[di + k] = src.data[si + k]
47
48const
49 ESC = "\x1b"
50 RESET = ESC & "[0m"
51 LOWER = ""
52 UPPER = ""
53
54func fg(r, g, b: int): string = &"{ESC}[38;2;{r};{g};{b}m"
55
56func bgFg(ur, ug, ub, lr, lg, lb: int): string =
57 &"{ESC}[48;2;{ur};{ug};{ub};38;2;{lr};{lg};{lb}m"
58
59type Mode = enum mNone, mSpace, mLower, mUpper, mBoth
60
61func rgbaToAnsi*(img: Rgba): string =
62 ## Convert an RGBA raster to a multi-line truecolor ANSI string.
63 let
64 w = img.width
65 h = img.height
66 func px(y, x, k: int): int = img.data[(y * w + x) * 4 + k]
67
68 var lines: seq[string]
69 var y = 0
70 while y < h:
71 let y2 = y + 1
72 var
73 line = ""
74 mode = mNone
75 fr, fgc, fb, br, bg, bb = -1
76 for x in 0 ..< w:
77 let
78 ur = px(y, x, 0)
79 ug = px(y, x, 1)
80 ub = px(y, x, 2)
81 ua = px(y, x, 3)
82 lr = if y2 < h: px(y2, x, 0) else: 0
83 lg = if y2 < h: px(y2, x, 1) else: 0
84 lb = if y2 < h: px(y2, x, 2) else: 0
85 la = if y2 < h: px(y2, x, 3) else: 0
86 if ua == 0 and la == 0:
87 if mode notin {mNone, mSpace}: line.add RESET
88 line.add " "
89 mode = mSpace
90 fr = -1; fgc = -1; fb = -1; br = -1; bg = -1; bb = -1
91 elif ua == 0:
92 if mode != mLower or fr != lr or fgc != lg or fb != lb:
93 line.add fg(lr, lg, lb) & LOWER
94 mode = mLower
95 fr = lr; fgc = lg; fb = lb; br = -1; bg = -1; bb = -1
96 else:
97 line.add LOWER
98 elif la == 0:
99 if mode != mUpper or fr != ur or fgc != ug or fb != ub:
100 line.add fg(ur, ug, ub) & UPPER
101 mode = mUpper
102 fr = ur; fgc = ug; fb = ub; br = -1; bg = -1; bb = -1
103 else:
104 line.add UPPER
105 else:
106 # both: single CSI with bg = upper, fg = lower
107 if mode != mBoth or fr != lr or fgc != lg or fb != lb or
108 br != ur or bg != ug or bb != ub:
109 line.add bgFg(ur, ug, ub, lr, lg, lb) & LOWER
110 mode = mBoth
111 fr = lr; fgc = lg; fb = lb; br = ur; bg = ug; bb = ub
112 else:
113 line.add LOWER
114 if mode notin {mNone, mSpace}: line.add RESET
115 lines.add line
116 y += 2
117 lines.join("\n")
118
119func avatarToAnsi*(av: Avatar, scale = 2, facing = fSouth, frame = 0): string =
120 rgbaToAnsi(scaleRgba(spriteToRgba(renderSpritePixels(av, facing, frame)), scale))
121
122func didToAnsi*(did: string, scale = 2, facing = fSouth, frame = 0): string =
123 ## DID → truecolor ANSI art (no network).
124 avatarToAnsi(deriveAvatar(did), scale, facing, frame)