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
  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
115
116
117
118
119
120
121
122
123
124
## Sprite → truecolor ANSI for terminals (half-block packing).
## Each terminal cell covers 2 vertical pixels: bg = upper, fg = lower.
## Sequences are kept compact (single CSI, reset once per line) so tools like
## termshot/bunt and narrow PTYs do not choke mid-escape.

import std/[strutils, strformat]
import ./avatar

type Rgba* = object
  width*, height*: int
  data*: seq[int]     ## w*h*4 bytes

func hexToRgb*(hex: string): (int, int, int) =
  (parseHexInt(hex[1 .. 2]), parseHexInt(hex[3 .. 4]), parseHexInt(hex[5 .. 6]))

func spriteToRgba*(s: Sprite): Rgba =
  ## Expand a palette-indexed sprite to RGBA (alpha 0 for transparent).
  result = Rgba(width: s.width, height: s.height,
                data: newSeq[int](s.width * s.height * 4))
  for i, idx in s.pixels:
    let color = s.palette[idx]
    if color != "transparent":
      let (r, g, b) = hexToRgb(color)
      let o = i * 4
      result.data[o] = r
      result.data[o + 1] = g
      result.data[o + 2] = b
      result.data[o + 3] = 255

func scaleRgba*(src: Rgba, scale: int): Rgba =
  ## Nearest-neighbor upscale.
  if scale <= 1:
    return src
  let
    w = src.width * scale
    h = src.height * scale
  result = Rgba(width: w, height: h, data: newSeq[int](w * h * 4))
  for y in 0 ..< h:
    let sy = y div scale
    for x in 0 ..< w:
      let
        sx = x div scale
        si = (sy * src.width + sx) * 4
        di = (y * w + x) * 4
      for k in 0 ..< 4:
        result.data[di + k] = src.data[si + k]

const
  ESC = "\x1b"
  RESET = ESC & "[0m"
  LOWER = ""
  UPPER = ""

func fg(r, g, b: int): string = &"{ESC}[38;2;{r};{g};{b}m"

func bgFg(ur, ug, ub, lr, lg, lb: int): string =
  &"{ESC}[48;2;{ur};{ug};{ub};38;2;{lr};{lg};{lb}m"

type Mode = enum mNone, mSpace, mLower, mUpper, mBoth

func rgbaToAnsi*(img: Rgba): string =
  ## Convert an RGBA raster to a multi-line truecolor ANSI string.
  let
    w = img.width
    h = img.height
  func px(y, x, k: int): int = img.data[(y * w + x) * 4 + k]

  var lines: seq[string]
  var y = 0
  while y < h:
    let y2 = y + 1
    var
      line = ""
      mode = mNone
      fr, fgc, fb, br, bg, bb = -1
    for x in 0 ..< w:
      let
        ur = px(y, x, 0)
        ug = px(y, x, 1)
        ub = px(y, x, 2)
        ua = px(y, x, 3)
        lr = if y2 < h: px(y2, x, 0) else: 0
        lg = if y2 < h: px(y2, x, 1) else: 0
        lb = if y2 < h: px(y2, x, 2) else: 0
        la = if y2 < h: px(y2, x, 3) else: 0
      if ua == 0 and la == 0:
        if mode notin {mNone, mSpace}: line.add RESET
        line.add " "
        mode = mSpace
        fr = -1; fgc = -1; fb = -1; br = -1; bg = -1; bb = -1
      elif ua == 0:
        if mode != mLower or fr != lr or fgc != lg or fb != lb:
          line.add fg(lr, lg, lb) & LOWER
          mode = mLower
          fr = lr; fgc = lg; fb = lb; br = -1; bg = -1; bb = -1
        else:
          line.add LOWER
      elif la == 0:
        if mode != mUpper or fr != ur or fgc != ug or fb != ub:
          line.add fg(ur, ug, ub) & UPPER
          mode = mUpper
          fr = ur; fgc = ug; fb = ub; br = -1; bg = -1; bb = -1
        else:
          line.add UPPER
      else:
        # both: single CSI with bg = upper, fg = lower
        if mode != mBoth or fr != lr or fgc != lg or fb != lb or
           br != ur or bg != ug or bb != ub:
          line.add bgFg(ur, ug, ub, lr, lg, lb) & LOWER
          mode = mBoth
          fr = lr; fgc = lg; fb = lb; br = ur; bg = ug; bb = ub
        else:
          line.add LOWER
    if mode notin {mNone, mSpace}: line.add RESET
    lines.add line
    y += 2
  lines.join("\n")

func avatarToAnsi*(av: Avatar, scale = 2, facing = fSouth, frame = 0): string =
  rgbaToAnsi(scaleRgba(spriteToRgba(renderSpritePixels(av, facing, frame)), scale))

func didToAnsi*(did: string, scale = 2, facing = fSouth, frame = 0): string =
  ## DID → truecolor ANSI art (no network).
  avatarToAnsi(deriveAvatar(did), scale, facing, frame)