Ansi encoder

primitive AnsiEncoder
  """
  Stateless ANSI escape sequence encoder.
  """
  fun move_to(col: USize, row: USize): Array[U8] val =>
    """
    Emit ESC[(row+1);(col+1)H — terminal coordinates are 1-indexed.
    """
    recover val
      let buf = Array[U8]
      buf.push(0x1B); buf.push('[')
      _push_usize(buf, row + 1)
      buf.push(';')
      _push_usize(buf, col + 1)
      buf.push('H')
      buf
    end

  fun set_fg(color: Color): Array[U8] val =>
    """
    Emit ESC[{fg_code}m for the given color.
    """
    let code =
      match color
      | let c: Colorable => c.fg_code()
      end
    _sgr(code.usize())

  fun set_bg(color: Color): Array[U8] val =>
    """
    Emit ESC[{bg_code}m for the given color.
    """
    let code =
      match color
      | let c: Colorable => c.bg_code()
      end
    _sgr(code.usize())

  fun set_attrs(attrs: U8): Array[U8] val =>
    """
    Emit an SGR sequence for each set attribute bit.
    Bold=1, dim=2, underline=4, blink=5, reverse=7.
    """
    recover val
      let buf = Array[U8]
      if (attrs and CellAttrs.bold()) != 0 then
        for b in _sgr(1).values() do buf.push(b) end
      end
      if (attrs and CellAttrs.dim()) != 0 then
        for b in _sgr(2).values() do buf.push(b) end
      end
      if (attrs and CellAttrs.underline()) != 0 then
        for b in _sgr(4).values() do buf.push(b) end
      end
      if (attrs and CellAttrs.blink()) != 0 then
        for b in _sgr(5).values() do buf.push(b) end
      end
      if (attrs and CellAttrs.reverse()) != 0 then
        for b in _sgr(7).values() do buf.push(b) end
      end
      buf
    end

  fun reset(): Array[U8] val =>
    """
    Emit ESC[0m to reset all attributes and colors.
    """
    _sgr(0)

  fun write_char(codepoint: U32): Array[U8] val =>
    """
    Encode a Unicode codepoint as UTF-8 bytes.
    """
    recover val
      let buf = Array[U8]
      if codepoint <= 0x7F then
        buf.push(codepoint.u8())
      elseif codepoint <= 0x7FF then
        buf.push((0xC0 or (codepoint >> 6)).u8())
        buf.push((0x80 or (codepoint and 0x3F)).u8())
      elseif codepoint <= 0xFFFF then
        buf.push((0xE0 or (codepoint >> 12)).u8())
        buf.push((0x80 or ((codepoint >> 6) and 0x3F)).u8())
        buf.push((0x80 or (codepoint and 0x3F)).u8())
      else
        buf.push((0xF0 or (codepoint >> 18)).u8())
        buf.push((0x80 or ((codepoint >> 12) and 0x3F)).u8())
        buf.push((0x80 or ((codepoint >> 6) and 0x3F)).u8())
        buf.push((0x80 or (codepoint and 0x3F)).u8())
      end
      buf
    end

  fun hide_cursor(): Array[U8] val =>
    """
    Emit ESC[?25l to hide the cursor.
    """
    recover val
      [as U8: 0x1B; '['; '?'; '2'; '5'; 'l']
    end

  fun show_cursor(): Array[U8] val =>
    """
    Emit ESC[?25h to show the cursor.
    """
    recover val
      [as U8: 0x1B; '['; '?'; '2'; '5'; 'h']
    end

  fun clear_screen(): Array[U8] val =>
    """
    Emit ESC[2J to clear the entire screen.
    """
    recover val
      [as U8: 0x1B; '['; '2'; 'J']
    end

  fun tag _sgr(code: USize): Array[U8] val =>
    """
    Build ESC[{code}m.
    """
    recover val
      let buf = Array[U8]
      buf.push(0x1B); buf.push('[')
      _push_usize(buf, code)
      buf.push('m')
      buf
    end

  fun tag _push_usize(buf: Array[U8], n: USize) =>
    """
    Append the decimal ASCII representation of n to buf.
    """
    if n >= 10 then
      _push_usize(buf, n / 10)
    end
    buf.push(((n % 10) + 48).u8())