import sequtils
import streams
import strutils
import sugar
import css/stylednode
import layout/box
import types/color
import utils/twtstr
type
FormatFlags* = enum
FLAG_BOLD
FLAG_ITALIC
FLAG_UNDERLINE
FLAG_REVERSE
FLAG_STRIKE
FLAG_OVERLINE
FLAG_BLINK
Format* = object
fgcolor*: CellColor
bgcolor*: CellColor
flags*: set[FormatFlags]
# A FormatCell *starts* a new terminal formatting context.
# If no FormatCell exists before a given cell, the default formatting is used.
FormatCell* = object
format*: Format
pos*: int
computed*: ComputedFormat
node*: StyledNode
SimpleFormatCell* = object
format*: Format
pos*: int
FlexibleLine* = object
str*: string
formats*: seq[FormatCell]
SimpleFlexibleLine* = object
str*: string
formats*: seq[SimpleFormatCell]
FlexibleGrid* = seq[FlexibleLine]
SimpleFlexibleGrid* = seq[SimpleFlexibleLine]
FixedCell* = object
str*: string
format*: Format
FixedGrid* = object
width*, height*: int
cells*: seq[FixedCell]
proc `[]=`*(grid: var FixedGrid, i: int, cell: FixedCell) = grid.cells[i] = cell
proc `[]=`*(grid: var FixedGrid, i: BackwardsIndex, cell: FixedCell) = grid.cells[i] = cell
proc `[]`*(grid: var FixedGrid, i: int): var FixedCell = grid.cells[i]
proc `[]`*(grid: var FixedGrid, i: BackwardsIndex): var FixedCell = grid.cells[i]
proc `[]`*(grid: FixedGrid, i: int): FixedCell = grid.cells[i]
proc `[]`*(grid: FixedGrid, i: BackwardsIndex): FixedCell = grid.cells[i]
iterator items*(grid: FixedGrid): FixedCell {.inline.} =
for cell in grid.cells: yield cell
proc len*(grid: FixedGrid): int = grid.cells.len
const FormatCodes*: array[FormatFlags, tuple[s: int, e: int]] = [
FLAG_BOLD: (1, 22),
FLAG_ITALIC: (3, 23),
FLAG_UNDERLINE: (4, 24),
FLAG_REVERSE: (7, 27),
FLAG_STRIKE: (9, 29),
FLAG_OVERLINE: (53, 55),
FLAG_BLINK: (5, 25),
]
template flag_template(format: Format, val: bool, flag: FormatFlags) =
if val: format.flags.incl(flag)
else: format.flags.excl(flag)
template `italic=`*(f: var Format, b: bool) = flag_template f, b, FLAG_ITALIC
template `bold=`*(f: var Format, b: bool) = flag_template f, b, FLAG_BOLD
template `underline=`*(f: var Format, b: bool) = flag_template f, b, FLAG_UNDERLINE
template `reverse=`*(f: var Format, b: bool) = flag_template f, b, FLAG_REVERSE
template `strike=`*(f: var Format, b: bool) = flag_template f, b, FLAG_STRIKE
template `overline=`*(f: var Format, b: bool) = flag_template f, b, FLAG_OVERLINE
template `blink=`*(f: var Format, b: bool) = flag_template f, b, FLAG_BLINK
func `==`*(a: FixedCell, b: FixedCell): bool =
return a.format == b.format and
a.str == b.str
func newFixedGrid*(w: int, h: int = 1): FixedGrid =
return FixedGrid(width: w, height: h, cells: newSeq[FixedCell](w * h))
func width*(line: FlexibleLine): int =
return line.str.width()
func width*(cell: FixedCell): int =
return cell.str.width()
func newFormat*(): Format =
return Format(fgcolor: defaultColor, bgcolor: defaultColor)
# Get the first format cell after pos, if any.
func findFormatN*(line: FlexibleLine|SimpleFlexibleLine, pos: int): int =
var i = 0
while i < line.formats.len:
if line.formats[i].pos > pos:
break
inc i
return i
func findFormat*(line: FlexibleLine, pos: int): FormatCell =
let i = line.findFormatN(pos) - 1
if i != -1:
result = line.formats[i]
else:
result.pos = -1
func findFormat*(line: SimpleFlexibleLine, pos: int): SimpleFormatCell =
let i = line.findFormatN(pos) - 1
if i != -1:
result = line.formats[i]
else:
result.pos = -1
func findNextFormat*(line: FlexibleLine, pos: int): FormatCell =
let i = line.findFormatN(pos)
if i < line.formats.len:
result = line.formats[i]
else:
result.pos = -1
func findNextFormat*(line: SimpleFlexibleLine, pos: int): SimpleFormatCell =
let i = line.findFormatN(pos)
if i < line.formats.len:
result = line.formats[i]
else:
result.pos = -1
proc addLine*(grid: var FlexibleGrid) =
grid.add(FlexibleLine())
proc insertFormat*(line: var FlexibleLine, pos, i: int, format: Format, computed: ComputedFormat = nil) =
if computed == nil:
line.formats.insert(FormatCell(format: format, pos: pos), i)
else:
line.formats.insert(FormatCell(format: format, computed: computed, node: computed.node, pos: pos), i)
proc addFormat*(line: var FlexibleLine, pos: int, format: Format) =
line.formats.add(FormatCell(format: format, pos: pos))
proc addFormat*(line: var FlexibleLine, pos: int, format: Format, computed: ComputedFormat) =
#if computed != nil and line.formats.len > 0 and line.formats[^1].computed == computed and line.formats[^1].format.bgcolor != format.bgcolor:
# return
if computed == nil:
line.formats.add(FormatCell(format: format, pos: pos))
else:
line.formats.add(FormatCell(format: format, computed: computed, node: computed.node, pos: pos))
template inc_check(i: int) =
inc i
if i >= buf.len:
return i
proc handleAnsiCode(format: var Format, final: char, params: string) =
case final
of 'm':
if params.len == 0:
format = newFormat()
else:
let sparams = params.split(';')
try:
let ip = sparams.map((x) => parseInt(x))
var pi = 0
while pi < ip.len:
case ip[pi]
of 0:
format = newFormat()
of 1: format.bold = true
of 3: format.italic = true
of 4: format.underline = true
of 5: format.blink = true
of 7: format.reverse = true
of 9: format.strike = true
of 22: format.bold = false
of 23: format.italic = false
of 25: format.blink = false
of 27: format.reverse = false
of 29: format.strike = false
of 30..37: format.fgcolor = uint8(ip[pi]).cellColor()
of 38:
inc pi
if pi < ip.len:
if ip[pi] == 2:
inc pi
if pi + 2 < ip.len:
let r = ip[pi]
inc pi
let g = ip[pi]
inc pi
let b = ip[pi]
format.fgcolor = rgb(r, g, b).cellColor()
else:
#TODO
inc pi
continue
else:
break
of 39:
format.fgcolor = defaultColor
of 40..47:
format.bgcolor = uint8(ip[0]).cellColor()
of 48:
inc pi
if pi < ip.len:
if ip[pi] == 2:
inc pi
if pi + 2 < ip.len:
let r = ip[pi]
inc pi
let g = ip[pi]
inc pi
let b = ip[pi]
format.bgcolor = rgb(r, g, b).cellColor()
else:
#TODO
inc pi
continue
else:
break
of 49: format.bgcolor = defaultColor
of 53: format.overline = true
of 55: format.overline = false
else: discard
inc pi
except ValueError: discard
else: discard
proc parseAnsiCode*(format: var Format, buf: string, fi: int): int =
var i = fi
if buf[i] != '\e':
return i
inc_check i
if 0x40 <= int(buf[i]) and int(buf[i]) <= 0x5F:
if buf[i] != '[':
#C1, TODO?
return
inc_check i
let sp = i
#parameter bytes
while 0x30 <= int(buf[i]) and int(buf[i]) <= 0x3F:
inc_check i
let params = buf.substr(sp, i - 1)
#let si = i
#intermediate bytes
while 0x20 <= int(buf[i]) and int(buf[i]) <= 0x2F:
inc_check i
#let interm = buf.substr(si, i)
let final = buf[i]
#final byte
if 0x40 <= int(buf[i]) and int(buf[i]) <= 0x7E:
format.handleAnsiCode(final, params)
return i
type
AnsiCodeParseState* = enum
PARSE_START, PARSE_PARAMS, PARSE_INTERM, PARSE_FINAL, PARSE_DONE
AnsiCodeParser* = object
state*: AnsiCodeParseState
params: string
proc reset*(parser: var AnsiCodeParser) =
parser.state = PARSE_START
parser.params = ""
proc parseAnsiCode*(parser: var AnsiCodeParser, format: var Format, c: char): bool =
case parser.state
of PARSE_START:
if 0x40 <= int(c) and int(c) <= 0x5F:
if c != '[':
#C1, TODO?
parser.state = PARSE_DONE
else:
parser.state = PARSE_PARAMS
else:
parser.state = PARSE_DONE
return true
of PARSE_PARAMS:
if 0x30 <= int(c) and int(c) <= 0x3F:
parser.params &= c
else:
parser.state = PARSE_INTERM
return parser.parseAnsiCode(format, c)
of PARSE_INTERM:
if 0x20 <= int(c) and int(c) <= 0x2F:
discard
else:
parser.state = PARSE_FINAL
return parser.parseAnsiCode(format, c)
of PARSE_FINAL:
parser.state = PARSE_DONE
if 0x40 <= int(c) and int(c) <= 0x7E:
format.handleAnsiCode(c, parser.params)
else:
return true
of PARSE_DONE: discard
proc parseAnsiCode*(format: var Format, stream: Stream) =
if stream.atEnd(): return
var c = stream.readChar()
if 0x40 <= int(c) and int(c) <= 0x5F:
if c != '[':
#C1, TODO?
return
if stream.atEnd(): return
c = stream.readChar()
var params = $c
#parameter bytes
while 0x30 <= int(c) and int(c) <= 0x3F:
params &= c
if stream.atEnd(): return
c = stream.readChar()
#intermediate bytes
#var interm = $c
while 0x20 <= int(c) and int(c) <= 0x2F:
#interm &= c
if stream.atEnd(): return
c = stream.readChar()
#final byte
if 0x40 <= int(c) and int(c) <= 0x7E:
let final = c
format.handleAnsiCode(final, params)