enerator' content='cgit 1.4.1-2-gfad0'/>
about summary refs log blame commit diff stats
path: root/display.nim
blob: c7cd4bb47ec0c0584e9f40f728946bfff2eb5691 (plain) (tree)
1
2
3
4
5
6
7



               
              

                                






                  
            






                                       
                                                

    
                      

          
                  

                   
                              




                    

                

                                    








                                                            
 
                                                        




                                                                                    
             
             
                      

                    
 
                                                                



                                
                            
              
 
                                                                               
                     
           

                  
                   





                                  
                        





                                                                                  


                    


                  
                                  
 
                                 

                                                       
                                          



                                                       





                                       
 
                                                 
 
                                                                           
                            
                                                                        




                                                              
                               
 
                                                                          

                                             


                                                                                    
                                                                   

                                                                               
  

                                                                                  
                     

                                   
                      
              
                       



                                         
                         
                                 
                              
 
                                                                            





                            
                                                     
                                                          
                                                                          
                                           




                                                                 

                                 
 
                                                   

                           
                                                                         
                       
          
                            
                               
                         
                                 



                        

                        









                                                   
              


                                 

                           




                                           



                                  

                  
                                      

                   
                                           
                                    





                                                    

                              







                                   
                                                              

                            
 
                                  

                                                 
                                                                               




                              
                                 
                                           






                                                                       
                                              

                                                                 
                                                                                                       

                                            
                                                                                                      

                                                               

                               









                                      



                                       


                               






                                         
                                                                           
                                           


                                    


                                                       


                                           




                                    
                                         




                                                             

                            








                                   
                        










                                                                
                                                                










                                                                  
                                                       




                                                       

                                       
                                       
                                                                                    


                          
                     
                                     

                                                                                  
                     
                              
                                         
 




                                         
                        
                                                                                                                                                       
                     







                                   






























                                                             
 




                            
 



                                 

                                                                

                             



                                 
import terminal
import options
import uri
import strutils
import unicode

import fusion/htmlparser/xmltree

import buffer
import termattrs
import htmlelement
import twtstr
import twtio
import config
import enums

proc clearStatusMsg*(at: int) =
  setCursorPos(0, at)
  eraseLine()

proc statusMsg*(str: string, at: int) =
  clearStatusMsg(at)
  print(str.ansiStyle(styleReverse).ansiReset())

type
  RenderState = object
    x: int
    y: int
    lastwidth: int
    fmtline: string
    rawline: string
    centerqueue: seq[HtmlNode]
    centerlen: int
    blanklines: int
    blankspaces: int
    nextspaces: int
    docenter: bool
    indent: int
    listval: int

func newRenderState(): RenderState =
  return RenderState()

proc write(state: var RenderState, s: string) =
  state.fmtline &= s
  state.rawline &= s

proc write(state: var RenderState, fs: string, rs: string) =
  state.fmtline &= fs
  state.rawline &= rs

proc flushLine(buffer: Buffer, state: var RenderState) =
  if state.rawline.len == 0:
    inc state.blanklines
  assert(state.rawline.runeLen() < buffer.width, "line too long:\n" & state.rawline)
  buffer.writefmt(state.fmtline)
  buffer.writeraw(state.rawline)
  state.x = 0
  inc state.y
  state.nextspaces = 0
  state.fmtline = ""
  state.rawline = ""

proc addSpaces(buffer: Buffer, state: var RenderState, n: int) =
  if state.x + n > buffer.width:
    buffer.flushLine(state)
    return
  state.blankspaces += n
  state.write(' '.repeat(n))
  state.x += n

proc writeWrappedText(buffer: Buffer, state: var RenderState, node: HtmlNode) =
  state.lastwidth = 0
  var n = 0
  var fmtword = ""
  var rawword = ""
  var prevl = false
  for w in node.fmttext:
    if w.len > 0 and w[0] == '\e':
      fmtword &= w
      continue

    for r in w.runes:
      if r == Rune(' '):
        if rawword[0] == ' ' and prevl: #first byte can't fool comparison to ascii
          fmtword = fmtword.substr(1)
          rawword = rawword.substr(1)
          state.x -= 1
          prevl = false
        state.write(fmtword, rawword)
        fmtword = ""
        rawword = ""

      fmtword &= r
      rawword &= r

      state.x += mk_wcwidth_cjk(r)

      if state.x >= buffer.width:
        state.lastwidth = max(state.lastwidth, state.x)
        buffer.flushLine(state)
        state.x = mk_wcswidth_cjk(rawword)
        prevl = true
      else:
        state.lastwidth = max(state.lastwidth, state.x)

      inc n

  state.write(fmtword, rawword)
  if prevl:
    state.x += mk_wcswidth_cjk(rawword)
    prevl = false

  state.lastwidth = max(state.lastwidth, state.x)

proc preAlignNode(buffer: Buffer, node: HtmlNode, state: var RenderState) =
  let elem = node.nodeAttr()
  if state.rawline.len > 0 and node.openblock and state.blanklines == 0:
    buffer.flushLine(state)

  if node.openblock:
    while state.blanklines < max(elem.margin, elem.margintop):
      buffer.flushLine(state)
    state.indent += elem.indent

  if state.rawline.len > 0 and state.blanklines == 0 and node.displayed():
    buffer.addSpaces(state, state.nextspaces)
    state.nextspaces = 0
    if state.blankspaces < max(elem.margin, elem.marginleft):
      buffer.addSpaces(state, max(elem.margin, elem.marginleft) - state.blankspaces)

  if elem.centered and state.rawline.len == 0 and node.displayed():
    buffer.addSpaces(state, max(buffer.width div 2 - state.centerlen div 2, 0))
    state.centerlen = 0
  
  if node.isElemNode() and elem.display == DISPLAY_LIST_ITEM and state.indent > 0:
    buffer.flushLine(state)
    var listchar = ""
    case elem.parentElement.tagType
    of TAG_UL:
      listchar = "•"
    of TAG_OL:
      inc state.listval
      listchar = $state.listval & ")"
    else:
      return
    buffer.addSpaces(state, state.indent)
    state.write(listchar)
    state.x += listchar.runeLen()
    buffer.addSpaces(state, 1)

proc postAlignNode(buffer: Buffer, node: HtmlNode, state: var RenderState) =
  let elem = node.nodeAttr()

  if node.getRawLen() > 0:
    state.blanklines = 0
    state.blankspaces = 0

  if state.rawline.len > 0 and state.blanklines == 0:
    state.nextspaces += max(elem.margin, elem.marginright)
    if node.closeblock and (node.isTextNode() or elem.numChildNodes == 0):
      state.write($node.nodeAttr().tagType)
      buffer.flushLine(state)

  if node.closeblock:
    while state.blanklines < max(elem.margin, elem.marginbottom):
      buffer.flushLine(state)
    if node.isElemNode():
      state.indent -= elem.indent

  if elem.tagType == TAG_BR and not node.openblock:
    buffer.flushLine(state)

proc renderNode(buffer: Buffer, node: HtmlNode, state: var RenderState) =
  if node.isDocument():
    return
  let elem = node.nodeAttr()
  if elem.tagType == TAG_TITLE:
    if node.isTextNode():
      buffer.title = node.rawtext
    return
  else: discard
  if elem.hidden: return

  if not state.docenter:
    if elem.centered:
      state.centerqueue.add(node)
      if node.closeblock or elem.tagType == TAG_BR:
        state.docenter = true
        state.centerlen = 0
        for node in state.centerqueue:
          state.centerlen += node.getRawLen()
        for node in state.centerqueue:
          buffer.renderNode(node, state)
        state.centerqueue.setLen(0)
        state.docenter = false
        return
      else:
        return
    if state.centerqueue.len > 0:
      state.docenter = true
      state.centerlen = 0
      for node in state.centerqueue:
        state.centerlen += node.getRawLen()
      for node in state.centerqueue:
        buffer.renderNode(node, state)
      state.centerqueue.setLen(0)
      state.docenter = false

  buffer.preAlignNode(node, state)

  node.x = state.x
  node.y = state.y
  buffer.writeWrappedText(state, node)
  node.ex = state.x
  node.ey = state.y
  node.width = state.lastwidth - node.x - 1
  node.height = state.y - node.y + 1

  buffer.postAlignNode(node, state)

iterator revItems*(n: XmlNode): XmlNode {.inline.} =
  var i = n.len - 1
  while i >= 0:
    if n[i].kind != xnComment:
      yield n[i]
    i -= 1

type
  XmlHtmlNode* = ref XmlHtmlNodeObj
  XmlHtmlNodeObj = object
    xml*: XmlNode
    html*: HtmlNode

proc setLastHtmlLine(buffer: Buffer, state: var RenderState) =
  if state.rawline.len != 0:
    buffer.flushLine(state)

proc renderHtml*(buffer: Buffer) =
  var stack: seq[XmlHtmlNode]
  let first = XmlHtmlNode(xml: buffer.htmlSource,
                         html: getHtmlNode(buffer.htmlSource, buffer.document))
  stack.add(first)

  var state = newRenderState()
  while stack.len > 0:
    let currElem = stack.pop()
    buffer.addNode(currElem.html)
    buffer.renderNode(currElem.html, state)
    if currElem.xml.len > 0:
      var last = false
      for item in currElem.xml.revItems:
        let child = XmlHtmlNode(xml: item,
                                html: getHtmlNode(item, currElem.html))
        stack.add(child)
        currElem.html.childNodes.add(child.html)
        if not last and not child.html.hidden:
          last = true
          if HtmlElement(currElem.html).display == DISPLAY_BLOCK:
            eprint "elem", HtmlElement(currElem.html).tagType, "close @", child.html.nodeAttr().tagType
            stack[^1].html.closeblock = true
      if last:
        eprint "elem", HtmlElement(currElem.html).tagType, "open @", stack[^1].html.nodeAttr().tagType
        if HtmlElement(currElem.html).display == DISPLAY_BLOCK:
          stack[^1].html.openblock = true
  buffer.setLastHtmlLine(state)

proc nrenderHtml*(buffer: Buffer) =
  var stack: seq[HtmlNode]
  let first = buffer.document
  stack.add(first)

  var state = newRenderState()
  while stack.len > 0:
    let currElem = stack.pop()
    buffer.addNode(currElem)
    buffer.renderNode(currElem, state)
    var i = currElem.childNodes.len - 1
    while i >= 0:
      stack.add(currElem.childNodes[i])
      i -= 1

  buffer.setLastHtmlLine(state)

proc drawHtml(buffer: Buffer) =
  var state = newRenderState()
  for node in buffer.nodes:
    buffer.renderNode(node, state)
  buffer.setLastHtmlLine(state)

proc statusMsgForBuffer(buffer: Buffer) =
  var msg = $(buffer.cursory + 1) & "/" & $(buffer.lastLine() + 1) & " (" &
            $buffer.atPercentOf() & "%) " &
            "<" & buffer.title & ">"
  if buffer.hovertext.len > 0:
    msg &= " " & buffer.hovertext
  statusMsg(msg.maxString(buffer.width), buffer.height)

proc cursorBufferPos(buffer: Buffer) =
  var x = buffer.cursorx
  var y = buffer.cursory - 1 - buffer.fromY
  termGoto(x, y + 1)

proc displayBuffer(buffer: Buffer) =
  eraseScreen()
  termGoto(0, 0)

  print(buffer.visibleText().ansiReset())

proc inputLoop(attrs: TermAttributes, buffer: Buffer): bool =
  var s = ""
  var feedNext = false
  while true:
    stdout.showCursor()
    buffer.cursorBufferPos()
    if not feedNext:
      s = ""
    else:
      feedNext = false
    let c = getch()
    s &= c
    let action = getNormalAction(s)
    var redraw = false
    var reshape = false
    var nostatus = false
    case action
    of ACTION_QUIT:
      eraseScreen()
      return false
    of ACTION_CURSOR_LEFT: redraw = buffer.cursorLeft()
    of ACTION_CURSOR_DOWN: redraw = buffer.cursorDown()
    of ACTION_CURSOR_UP: redraw = buffer.cursorUp()
    of ACTION_CURSOR_RIGHT: redraw = buffer.cursorRight()
    of ACTION_CURSOR_LINEBEGIN: buffer.cursorLineBegin()
    of ACTION_CURSOR_LINEEND: buffer.cursorLineEnd()
    of ACTION_CURSOR_NEXT_WORD: redraw = buffer.cursorNextWord()
    of ACTION_CURSOR_PREV_WORD: redraw = buffer.cursorPrevWord()
    of ACTION_CURSOR_NEXT_LINK: redraw = buffer.cursorNextLink()
    of ACTION_CURSOR_PREV_LINK: redraw = buffer.cursorPrevLink()
    of ACTION_PAGE_DOWN: redraw = buffer.pageDown()
    of ACTION_PAGE_UP: redraw = buffer.pageUp()
    of ACTION_HALF_PAGE_DOWN: redraw = buffer.halfPageDown()
    of ACTION_HALF_PAGE_UP: redraw = buffer.halfPageUp()
    of ACTION_CURSOR_FIRST_LINE: redraw = buffer.cursorFirstLine()
    of ACTION_CURSOR_LAST_LINE: redraw = buffer.cursorLastLine()
    of ACTION_CURSOR_TOP: redraw = buffer.cursorTop()
    of ACTION_CURSOR_MIDDLE: redraw = buffer.cursorMiddle()
    of ACTION_CURSOR_BOTTOM: redraw = buffer.cursorBottom()
    of ACTION_CENTER_LINE: redraw = buffer.centerLine()
    of ACTION_SCROLL_DOWN: redraw = buffer.scrollDown()
    of ACTION_SCROLL_UP: redraw = buffer.scrollUp()
    of ACTION_CLICK:
      let selectedElem = buffer.findSelectedElement()
      if selectedElem.isSome:
        case selectedElem.get().tagType
        of TAG_INPUT:
          clearStatusMsg(buffer.height)
          let status = readLine("TEXT:", HtmlInputElement(selectedElem.get()).value)
          if status:
            reshape = true
            redraw = true
        else: discard
        if selectedElem.get().islink:
          let anchor = HtmlAnchorElement(buffer.selectedlink.ancestor(TAG_A)).href
          buffer.gotoLocation(parseUri(anchor))
          return true
    of ACTION_CHANGE_LOCATION:
      var url = $buffer.document.location

      clearStatusMsg(buffer.height)
      let status = readLine("URL:", url)
      if status:
        buffer.setLocation(parseUri(url))
        return true
    of ACTION_LINE_INFO:
      statusMsg("line " & $buffer.cursory & "/" & $buffer.lastLine() & " col " & $buffer.cursorx & "/" & $buffer.realCurrentLineLength(), buffer.width)
      nostatus = true
    of ACTION_FEED_NEXT:
      feedNext = true
    of ACTION_RELOAD: return true
    of ACTION_RESHAPE:
      reshape = true
      redraw = true
    of ACTION_REDRAW: redraw = true
    else: discard
    stdout.hideCursor()

    let prevlink = buffer.selectedlink
    let sel = buffer.checkLinkSelection()
    if sel:
      buffer.clearText()
      buffer.drawHtml()
      termGoto(0, buffer.selectedlink.y - buffer.fromy)
      stdout.eraseLine()
      for i in buffer.selectedlink.y..buffer.selectedlink.ey:
        if i < buffer.fromy + buffer.height - 1:
          let line = buffer.fmttext[i]
          print(line)
          print('\n')
      print("".ansiReset())
    
    if prevlink != nil:
      buffer.clearText()
      buffer.drawHtml()
      termGoto(0, prevlink.y - buffer.fromy)
      for i in prevlink.y..prevlink.ey:
        if i < buffer.fromy + buffer.height - 1:
          let line = buffer.fmttext[i]
          stdout.eraseLine()
          print(line)
          print('\n')
      print("".ansiReset())

    if buffer.refreshTermAttrs():
      redraw = true
      reshape = true

    if reshape:
      buffer.clearText()
      buffer.drawHtml()
    if redraw:
      buffer.displayBuffer()

    if not nostatus:
      buffer.statusMsgForBuffer()
    else:
      nostatus = false

proc displayPage*(attrs: TermAttributes, buffer: Buffer): bool =
  #buffer.printwrite = true
  discard buffer.gotoAnchor()
  buffer.displayBuffer()
  buffer.statusMsgForBuffer()
  return inputLoop(attrs, buffer)