about summary refs log blame commit diff stats
path: root/src/types/cell.nim
blob: 2f2e520364062fd153b8bc89303ee2e62706ff85 (plain) (tree)
1
2
3
4
5
6
7
8
9

                  
 
                     
                  
                     
                   

    
                     
             
               
                  
                

                 
              
 
                  

                       
                            
 

                                                                                

                      
             




                            
 



                                                                          

                        
                             
 


                                   

                                   
 


                                               
                
                   
 












                                                                                           
                                                  
 







                                                              

 






                                                                


                                                                      
 





                                                                                   
                                                                           
 
                                                   
                                                                        

                                      
                         
 
                                   
                         
 
                                              
                                                                         



                                 
         

          
                                                            
                                   
             
                            
       
                   
 






                                                                        
                                                                
                               



                            
 






                                                                            
                                       

                          


                                                

                                                                      
 





                                                                        
                                                                    
 
                                                                                         







                                                                    









                                                                          
                            








                                                                 




                                    





                                                   
                                       
         


                                                   
                           
                                            



                                                



                                                              
                                                 




                                                          
                                                 




                                                           
                                                 
                         
                                                 














                                                               
                     
               




                                                           
                     




                                              







                                                                         



                                         

                                                                   

























                                            
                                            


                        
import std/options
import std/tables

import css/stylednode
import types/color
import utils/strwidth
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
    node*: StyledNode

  SimpleFormatCell* = object
    format*: Format
    pos*: int

  # Following properties should hold for `formats':
  # * Position should be >= 0, <= str.width().
  # * The position of every FormatCell should be greater than the position
  #   of the previous FormatCell.
  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
proc high*(grid: FixedGrid): int = grid.cells.high

const FormatCodes*: array[FormatFlags, tuple[s, e: uint8]] = [
  FLAG_BOLD: (1u8, 22u8),
  FLAG_ITALIC: (3u8, 23u8),
  FLAG_UNDERLINE: (4u8, 24u8),
  FLAG_REVERSE: (7u8, 27u8),
  FLAG_STRIKE: (9u8, 29u8),
  FLAG_OVERLINE: (53u8, 55u8),
  FLAG_BLINK: (5u8, 25u8),
]

const FormatCodeMap = block:
  var res: Table[uint8, tuple[flag: FormatFlags, reverse: bool]]
  for x in FormatFlags:
    res[FormatCodes[x][0]] = (x, false)
    res[FormatCodes[x][1]] = (x, true)
  res

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 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()

# 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 addLines*(grid: var FlexibleGrid, n: int) =
  grid.setLen(grid.len + n)

proc insertFormat*(line: var FlexibleLine, i: int, cell: FormatCell) =
  line.formats.insert(cell, i)

proc insertFormat*(line: var FlexibleLine, pos, i: int, format: Format,
    node: StyledNode = nil) =
  line.insertFormat(i, FormatCell(format: format, node: node, pos: pos))

proc addFormat*(line: var FlexibleLine, pos: int, format: Format,
    node: StyledNode = nil) =
  line.formats.add(FormatCell(format: format, node: node, pos: pos))

# https://www.ecma-international.org/wp-content/uploads/ECMA-48_5th_edition_june_1991.pdf
type
  AnsiCodeParseState* = enum
    PARSE_START, PARSE_PARAMS, PARSE_INTERM, PARSE_FINAL, PARSE_DONE

  AnsiCodeParser* = object
    state*: AnsiCodeParseState
    params: string

proc getParam(parser: AnsiCodeParser, i: var int, colon = false): string =
  while i < parser.params.len and
      not (parser.params[i] == ';' or colon and parser.params[i] == ':'):
    result &= parser.params[i]
    inc i
  if i < parser.params.len:
    inc i

template getParamU8(parser: AnsiCodeParser, i: var int,
    colon = false): uint8 =
  if i >= parser.params.len:
    return false
  let u = parseUInt8(parser.getParam(i))
  if u.isNone:
    return false
  u.get

proc parseSGRDefColor(parser: AnsiCodeParser, format: var Format,
    i: var int, isfg: bool): bool =
  let u = parser.getParamU8(i, colon = true)
  template set_color(c: CellColor) =
    if isfg:
      format.fgcolor = c
    else:
      format.bgcolor = c
  if u == 2:
    let param0 = parser.getParamU8(i, colon = true)
    if i < parser.params.len:
      let r = param0
      let g = parser.getParamU8(i, colon = true)
      let b = parser.getParamU8(i, colon = true)
      set_color cellColor(rgb(r, g, b))
    else:
      set_color cellColor(gray(param0))
  elif u == 5:
    let param0 = parser.getParamU8(i, colon = true)
    if param0 in 0u8..15u8:
      set_color cellColor(ANSIColor(param0))
    elif param0 in 16u8..255u8:
      set_color cellColor(EightBitColor(param0))
  else:
    return false

proc parseSGRColor(parser: AnsiCodeParser, format: var Format,
    i: var int, u: uint8): bool =
  if u in 30u8..37u8:
    format.fgcolor = cellColor(ANSIColor(u - 30))
  elif u == 38:
    return parser.parseSGRDefColor(format, i, isfg = true)
  elif u == 39:
    format.fgcolor = defaultColor
  elif u in 40u8..47u8:
    format.bgcolor = cellColor(ANSIColor(u - 40))
  elif u == 48:
    return parser.parseSGRDefColor(format, i, isfg = false)
  elif u == 49:
    format.bgcolor = defaultColor
  elif u in 90u8..97u8:
    format.fgcolor = cellColor(ANSIColor(u - 82))
  elif u in 100u8..107u8:
    format.bgcolor = cellColor(ANSIColor(u - 92))
  else:
    return false
  return true

proc parseSGRAspect(parser: AnsiCodeParser, format: var Format,
    i: var int): bool =
  let u = parser.getParamU8(i)
  if u in FormatCodeMap:
    let entry = FormatCodeMap[u]
    if entry.reverse:
      format.flags.excl(entry.flag)
    else:
      format.flags.incl(entry.flag)
    return true
  elif u == 0:
    format = Format()
    return true
  else:
    return parser.parseSGRColor(format, i, u)

proc parseSGR(parser: AnsiCodeParser, format: var Format) =
  if parser.params.len == 0:
    format = Format()
  else:
    var i = 0
    while i < parser.params.len:
      if not parser.parseSGRAspect(format, i):
        break

proc parseControlFunction(parser: var AnsiCodeParser, format: var Format,
    f: char) =
  case f
  of 'm':
    parser.parseSGR(format)
  else: discard # unknown

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:
      parser.parseControlFunction(format, c)
    else:
      return true
  of PARSE_DONE: discard