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

              
              
               
               
            
              
 

                    

                      
                
              
                  
                
                 
              
                    
               
                
                   

    
                         

                     

                  

                        
                            
                      
                    

                    

                        

                            
                 
 



                       
 


                                       
 




                                       
 

                                        



                        


                                         
 




                                    
 
                                









                                           
                                      

                                             
 
                                            

                                      

                               
                                            

                                      

                               
                                    



                                               
                        
                                  
                        
                                  









                                               
       


                                              
 
                                  
                            
               



                      
                                                                  











                                                      
                                              
                    
                                                                      

                                              
                                             





                                            
 


































































































































                                                                                                                                        
                               
                    
                                                                                                

                                                                            
                                        

                                                        
                        

                        
                                                                              
                                       



                                                                         




                                                                                         
 
                                                                            
                                                                                           



                                             

                                   
                                                               

                                                                            
         

                      

                                                 
                                                       

                                
                                                                      
       
                                                             
        
                                  
                                            
                     

                                                                
                                                                                        

                                     
                                                                
 
                                                                 
                                            
                         
                                                                                                
                    
 
                                                
                                                
                            
                                           
                     

                                                   
                       
 
                                       


                                                        
 
                                               

                              
                                     
                               

                      
                                                 
                                                       
                                                    
                                     
                                                              
                                                          




                                                 

                        






                                                                      
         








                                                              

                                                                    







                                              
                    







                                              




                                              


                                                                              
                      
                                                                               
                                            






                                                                                                
 











                                                               





                                                             
                                            





                                                             
                                        








                                                       
                                            








                                                       
                                         


                               
                

                       
                                    
 
                                                                                                      
            




                                                          
                                                                         


                                               
                    
           
                



                                               


                 

          
                             
                                   




                                           
                                             


                               
                

                       
                                    
                                                                                                      
            




                                                          
                                                                         


                                               
                    

                



                                               


                 
          





                                           
                                      
                                           

         




































                                                                                   
                            


                        



                           

                                   
               
 
                                        
                                            
 

                                   
                                

















                                                                                    
                                                                           

                           

                                     
                                  



                                                     


                                            
                                                                                   
                      

                        
 

                                
                     
                  

                           
                                 

                                       




                                           










                                                                          
                                                                                   






                                              
                                                         





                                 
                

           



                                
       


                              
 



                             

















                                                            


                                                    









                                                                 

                                           
                          
                                                   



                                                 

                                                       

                    


                        
import options
import os
import streams
import strutils
import terminal
import times
import unicode

import css/sheet
import config/config
import html/dom
import html/htmlparser
import io/buffer
import io/cell
import io/lineedit
import io/loader
import io/request
import io/term
import js/javascript
import js/regex
import types/url
import utils/twtstr

type
  Client* = ref ClientObj
  ClientObj* = object
    buffer*: Buffer
    feednext: bool
    s: string
    iserror: bool
    errormessage: string
    userstyle: CSSStylesheet
    loader: FileLoader
    console: Console
    jsrt: JSRuntime
    jsctx: JSContext
    regex: Option[Regex]
    revsearch: bool
    needsauth: bool
    redirecturl: Option[Url]
    cmdmode: bool

  Console* = ref object
    err*: Stream
    lastbuf*: Buffer
    ibuf: string

  ActionError* = object of IOError
  LoadError* = object of ActionError
  InterruptError* = object of LoadError

proc readChar(console: Console): char =
  if console.ibuf == "":
    return stdin.readChar()
  result = console.ibuf[0]
  console.ibuf = console.ibuf.substr(1)

proc `=destroy`(client: var ClientObj) =
  if client.jsctx != nil:
    free(client.jsctx)
  if client.jsrt != nil:
    free(client.jsrt)

proc statusMode(client: Client) =
  print(HVP(client.buffer.height + 1, 1))
  print(EL())

proc loadError(s: string) =
  raise newException(LoadError, s)

proc actionError(s: string) =
  raise newException(ActionError, s)

proc addBuffer(client: Client) =
  if client.buffer == nil:
    client.buffer = newBuffer()
  else:
    let oldnext = client.buffer.next
    client.buffer.next = newBuffer()
    if oldnext != nil:
      oldnext.prev = client.buffer.next
    client.buffer.next.prev = client.buffer
    client.buffer.next.next = oldnext
    client.buffer = client.buffer.next
  client.buffer.loader = client.loader
  client.buffer.userstyle = client.userstyle
  client.buffer.markcolor = gconfig.markcolor

proc prevBuffer(client: Client) {.jsfunc.} =
  if client.buffer.prev != nil:
    client.buffer = client.buffer.prev
    client.buffer.redraw = true

proc nextBuffer(client: Client) {.jsfunc.} =
  if client.buffer.next != nil:
    client.buffer = client.buffer.next
    client.buffer.redraw = true

proc discardBuffer(buffer: Buffer) =
  if buffer.next == nil and buffer.prev == nil:
    actionError("Cannot discard last buffer!")
  if buffer.sourcepair != nil:
    buffer.sourcepair.sourcepair = nil
  if buffer.next != nil:
    buffer.next.prev = buffer.prev
  if buffer.prev != nil:
    buffer.prev.next = buffer.next
  buffer.sourcepair = nil
  buffer.next = nil
  buffer.prev = nil

proc discardBuffer(client: Client) {.jsfunc.} =
  let old = client.buffer
  if old.next != nil:
    client.buffer = old.next
  elif old.prev != nil:
    client.buffer = old.prev
  else:
    actionError("Cannot discard last buffer!")
  discardBuffer(old)
  client.buffer.redraw = true

proc setupBuffer(client: Client) =
  let buffer = client.buffer
  buffer.load()
  buffer.render()
  buffer.gotoAnchor()
  buffer.redraw = true

proc dupeBuffer(client: Client, location = none(URL)) {.jsfunc.} =
  let prev = client.buffer
  client.addBuffer()
  client.buffer.contenttype = prev.contenttype
  client.buffer.ispipe = prev.ispipe
  client.buffer.istream = newStringStream(prev.source)
  if location.issome:
    client.buffer.location = location.get
  else:
    client.buffer.location = prev.location
  client.buffer.document = prev.document
  client.setupBuffer()

proc readPipe(client: Client, ctype: string) =
  client.addBuffer()
  client.buffer.contenttype = if ctype != "": ctype else: "text/plain"
  client.buffer.ispipe = true
  client.buffer.istream = newFileStream(stdin)
  client.buffer.location = newURL("file://-")
  client.buffer.load()
  #TODO is this portable at all?
  if reopen(stdin, "/dev/tty", fmReadWrite):
    client.setupBuffer()
  else:
    client.buffer.drawBuffer()

type Cookie = ref object of RootObj
  name {.jsget.}: string
  value {.jsget.}: string
  expires {.jsget.}: int64 # unix time
  maxAge {.jsget.}: int64
  secure {.jsget.}: bool
  httponly {.jsget.}: bool
  samesite {.jsget.}: bool
  domain {.jsget.}: string
  path {.jsget.}: string

proc parseCookieDate(val: string): Option[DateTime] =
  # cookie-date
  const Delimiters = {'\t', ' '..'/', ';'..'@', '['..'`', '{'..'~'}
  const NonDigit = Ascii + NonAscii - Digits
  var foundTime = false
  var foundDayOfMonth = false
  var foundMonth = false
  var foundYear = false
  # date-token-list
  var time: array[3, int]
  var dayOfMonth: int
  var month: int
  var year: int
  for dateToken in val.split(Delimiters):
    if dateToken == "": continue # *delimiter
    if not foundTime:
      block timeBlock: # test for time
        let hmsTime = dateToken.until(NonDigit - {':'})
        var i = 0
        for timeField in hmsTime.split(':'):
          if i > 2: break timeBlock # too many time fields
          # 1*2DIGIT
          if timeField.len != 1 and timeField.len != 2: break timeBlock
          var timeFields: array[3, int]
          for c in timeField:
            if c notin Digits: break timeBlock
            timeFields[i] *= 10
            timeFields[i] += c.decValue
          time = timeFields
          inc i
        if i != 3: break timeBlock
        foundTime = true
        continue
    if not foundDayOfMonth:
      block dayOfMonthBlock: # test for day-of-month
        let digits = dateToken.until(NonDigit)
        if digits.len != 1 and digits.len != 2: break dayOfMonthBlock
        var n = 0
        for c in digits:
          if c notin Digits: break dayOfMonthBlock
          n *= 10
          n += c.decValue
        dayOfMonth = n
        foundDayOfMonth = true
        continue
    if not foundMonth:
      block monthBlock: # test for month
        if dateToken.len < 3: break monthBlock
        case dateToken.substr(0, 2).toLower()
        of "jan": month = 1
        of "feb": month = 2
        of "mar": month = 3
        of "apr": month = 4
        of "may": month = 5
        of "jun": month = 6
        of "jul": month = 7
        of "aug": month = 8
        of "sep": month = 9
        of "oct": month = 10
        of "nov": month = 11
        of "dec": month = 12
        else: break monthBlock
        foundMonth = true
        continue
    if not foundYear:
      block yearBlock: # test for year
        let digits = dateToken.until(NonDigit)
        if digits.len != 2 and digits.len != 4: break yearBlock
        var n = 0
        for c in digits:
          if c notin Digits: break yearBlock
          n *= 10
          n += c.decValue
        year = n
        foundYear = true
        continue
  if not (foundDayOfMonth and foundMonth and foundYear and foundTime): return none(DateTime)
  if dayOfMonth notin 0..31: return none(DateTime)
  if year < 1601: return none(DateTime)
  if time[0] > 23: return none(DateTime)
  if time[1] > 59: return none(DateTime)
  if time[2] > 59: return none(DateTime)
  var dateTime = dateTime(year, Month(month), MonthdayRange(dayOfMonth), HourRange(time[0]), MinuteRange(time[1]), SecondRange(time[2]))
  return some(dateTime)

proc parseCookie(client: Client, str: string): Cookie {.jsfunc.} =
  let cookie = new(Cookie)
  var first = true
  for part in str.split(';'):
    if first:
      cookie.name = part.until('=')
      cookie.value = part.after('=')
      first = false
      continue
    let part = percentDecode(part).strip(leading = true, trailing = false, AsciiWhitespace)
    var n = 0
    for i in 0..part.high:
      if part[i] == '=':
        n = i
        break
    if n == 0:
      continue
    let key = part.substr(0, n - 1)
    let val = part.substr(n + 1)
    case key.toLower()
    of "expires":
      let date = parseCookieDate(val)
      if date.issome:
        cookie.expires = date.get.toTime().toUnix()
    of "max-age": cookie.maxAge = parseInt64(val)
    of "secure": cookie.secure = true
    of "httponly": cookie.httponly = true
    of "samesite": cookie.samesite = true
    of "path": cookie.path = val
    of "domain": cookie.domain = val
  return cookie

proc doRequest(client: Client, req: Request): Response {.jsfunc.} =
  client.loader.doRequest(req)

# Load request in a new buffer.
var g_client: Client
proc gotoUrl(client: Client, request: Request, prevurl = none(URL), force = false, ctype = "") =
  if force or prevurl.isnone or not prevurl.get.equals(request.url, true) or
      prevurl.get.equals(request.url) or request.httpmethod != HTTP_GET:
    let page = client.doRequest(request)
    client.needsauth = page.status == 401 # Unauthorized
    client.redirecturl = page.redirect
    if page.body != nil:
      client.addBuffer()
      g_client = client
      client.buffer.contenttype = if ctype != "": ctype else: page.contenttype
      client.buffer.istream = page.body
      client.buffer.location = request.url
      client.setupBuffer()
    else:
      loadError("Couldn't load " & $request.url & " (" & $page.res & ")")
  elif client.buffer != nil and prevurl.issome and prevurl.get.equals(request.url, true):
    if client.buffer.hasAnchor(request.url.anchor):
      client.dupeBuffer(request.url.some)
    else:
      loadError("Couldn't find anchor " & request.url.anchor)

# Relative gotoUrl: either to prevurl, or if that's none, client.buffer.url.
proc gotoUrl(client: Client, url: string, prevurl = none(URL), force = false, ctype = "") =
  var prevurl = prevurl
  if prevurl.isnone and client.buffer != nil:
    prevurl = client.buffer.location.some
  let newurl = parseUrl(url, prevurl)
  if newurl.isnone:
    loadError("Invalid URL " & url)
  client.gotoUrl(newRequest(newurl.get), prevurl, force, ctype)

# When the user has passed a partial URL as an argument, they might've meant
# either:
# * file://$PWD/<file>
# * https://<url>
# So we attempt to load both, and see what works.
# (TODO: make this optional)
proc loadUrl(client: Client, url: string, ctype = "") =
  let firstparse = parseUrl(url)
  if firstparse.issome:
    client.gotoUrl(newRequest(firstparse.get), none(Url), true, ctype)
  else:
    let cdir = parseUrl("file://" & getCurrentDir() & DirSep)
    try:
      # attempt to load local file
      client.gotoUrl(url, cdir, true, ctype)
    except LoadError:
      try:
        # attempt to load local file (this time percent encoded)
        client.gotoUrl(percentEncode(url, LocalPathPercentEncodeSet), cdir, true, ctype)
      except LoadError:
        # attempt to load remote page
        client.gotoUrl("https://" & url, none(Url), true, ctype)

# Reload the page in a new buffer, then kill the previous buffer.
proc reloadPage(client: Client) {.jsfunc.} =
  let buf = client.buffer
  client.gotoUrl(newRequest(client.buffer.location), none(URL), true, client.buffer.contenttype)
  discardBuffer(buf)

# Open a URL prompt and visit the specified URL.
proc changeLocation(client: Client) {.jsfunc.} =
  let buffer = client.buffer
  var url = buffer.location.serialize(true)
  client.statusMode()
  let status = readLine("URL: ", url, buffer.width)
  if status:
    client.loadUrl(url)

proc click(client: Client) {.jsfunc.} =
  let req = client.buffer.click()
  if req.issome:
    client.gotoUrl(req.get, client.buffer.location.some)

proc toggleSource*(client: Client) {.jsfunc.} =
  let buffer = client.buffer
  if buffer.sourcepair != nil:
    client.buffer = buffer.sourcepair
    client.buffer.redraw = true
  else:
    client.addBuffer()
    client.buffer.sourcepair = client.buffer.prev
    client.buffer.sourcepair.sourcepair = client.buffer
    client.buffer.source = client.buffer.prev.source
    client.buffer.streamclosed = true
    client.buffer.location = client.buffer.sourcepair.location
    client.buffer.ispipe = client.buffer.sourcepair.ispipe
    let prevtype = client.buffer.prev.contenttype
    if prevtype == "text/html":
      client.buffer.contenttype = "text/plain"
    else:
      client.buffer.contenttype = "text/html"
    client.setupBuffer()

proc interruptHandler(rt: JSRuntime, opaque: pointer): int {.cdecl.} =
  let client = cast[Client](opaque)
  try:
    let c = stdin.readChar()
    if c == char(3): #C-c
      client.console.ibuf = ""
      return 1
    else:
      client.console.ibuf &= c
  except IOError:
    discard
  return 0

proc evalJS(client: Client, src, filename: string): JSObject =
  unblockStdin()
  return client.jsctx.eval(src, filename, JS_EVAL_TYPE_GLOBAL)

proc command0(client: Client, src: string, filename = "<command>") =
  let ret = client.evalJS(src, filename)
  if ret.isException():
    let ex = client.jsctx.getException()
    let str = ex.toString()
    if str.issome:
      client.console.err.write(str.get & '\n')
    var stack = ex.getProperty("stack")
    if not stack.isUndefined():
      let str = stack.toString()
      if str.issome:
        client.console.err.write(str.get)
    free(stack)
    free(ex)
  else:
    let str = ret.toString()
    if str.issome:
      client.console.err.write(str.get & '\n')
  free(ret)

proc command(client: Client, src: string) =
  restoreStdin()
  let previ = client.console.err.getPosition()
  client.command0(src)
  g_client = client
  client.console.err.setPosition(previ)
  if client.console.lastbuf == nil or client.console.lastbuf != client.buffer:
    client.addBuffer()
    client.buffer.istream = newStringStream(client.console.err.readAll()) #TODO
    client.buffer.contenttype = "text/plain"
    client.buffer.location = parseUrl("javascript:void(0);").get
    client.console.lastbuf = client.buffer
  else:
    client.buffer.istream = newStringStream(client.buffer.source & client.console.err.readAll())
    client.buffer.streamclosed = false
  client.setupBuffer()
  client.buffer.cursorLastLine()

proc command(client: Client): bool {.jsfunc.} =
  var iput: string
  client.statusMode()
  let status = readLine("COMMAND: ", iput, client.buffer.width)
  if status:
    client.command(iput)
  return status

proc commandMode(client: Client) {.jsfunc.} =
  client.cmdmode = client.command()

proc searchNext(client: Client) {.jsfunc.} =
  if client.regex.issome:
    if not client.revsearch:
      discard client.buffer.cursorNextMatch(client.regex.get)
    else:
      discard client.buffer.cursorPrevMatch(client.regex.get)

proc searchPrev(client: Client) {.jsfunc.} =
  if client.regex.issome:
    if not client.revsearch:
      discard client.buffer.cursorPrevMatch(client.regex.get)
    else:
      discard client.buffer.cursorNextMatch(client.regex.get)

proc search(client: Client) {.jsfunc.} =
  client.statusMode()
  var iput: string
  let status = readLine("/", iput, client.buffer.width)
  if status:
    if iput.len != 0:
      client.regex = compileSearchRegex(iput)
    client.revsearch = false
    client.searchNext()

proc searchBack(client: Client) {.jsfunc.} =
  client.statusMode()
  var iput: string
  let status = readLine("?", iput, client.buffer.width)
  if status:
    if iput.len != 0:
      client.regex = compileSearchRegex(iput)
    client.revsearch = true
    client.searchNext()

proc isearch(client: Client) {.jsfunc.} =
  client.statusMode()
  var iput: string
  let cpos = client.buffer.cpos
  var mark: Mark
  template del_mark() =
    if mark != nil:
      client.buffer.removeMark(mark)

  let status = readLine("/", iput, client.buffer.width, {}, false, (proc(state: var LineState): bool =
    del_mark
    let regex = compileSearchRegex($state.news)
    client.buffer.cpos = cpos
    if regex.issome:
      let match = client.buffer.cursorNextMatch(regex.get)
      if match.success:
        mark = client.buffer.addMark(match.x, match.y, match.str.width())
        client.buffer.redraw = true
        client.buffer.refreshBuffer(true)
        print(HVP(client.buffer.height + 1, 2))
        print(SGR())
      else:
        del_mark
        client.buffer.redraw = true
        client.buffer.refreshBuffer(true)
        print(HVP(client.buffer.height + 1, 2))
        print(SGR())
      return true
    false
  ))

  del_mark
  client.buffer.redraw = true
  client.buffer.refreshBuffer(true)
  if status:
    client.regex = compileSearchRegex(iput)
  else:
    client.buffer.cpos = cpos

proc isearchBack(client: Client) {.jsfunc.} =
  client.statusMode()
  var iput: string
  let cpos = client.buffer.cpos
  var mark: Mark
  template del_mark() =
    if mark != nil:
      client.buffer.removeMark(mark)
  let status = readLine("?", iput, client.buffer.width, {}, false, (proc(state: var LineState): bool =
    del_mark
    let regex = compileSearchRegex($state.news)
    client.buffer.cpos = cpos
    if regex.issome:
      let match = client.buffer.cursorPrevMatch(regex.get)
      if match.success:
        mark = client.buffer.addMark(match.x, match.y, match.str.width())
        client.buffer.redraw = true
        client.buffer.refreshBuffer(true)
        print(HVP(client.buffer.height + 1, 2))
        print(SGR())
      else:
        del_mark
        client.buffer.redraw = true
        client.buffer.refreshBuffer(true)
        print(HVP(client.buffer.height + 1, 2))
        print(SGR())
      return true
    false
  ))
  del_mark
  client.buffer.redraw = true
  if status:
    client.regex = compileSearchRegex(iput)
  else:
    client.buffer.cpos = cpos

proc quit(client: Client) {.jsfunc.} =
  print(HVP(getTermAttributes().height, 0))
  quit(0)

proc feedNext(client: Client) {.jsfunc.} =
  client.feednext = true

#TODO move this to a pager module or something
proc cursorLeft(client: Client) {.jsfunc.} = client.buffer.cursorLeft()
proc cursorDown(client: Client) {.jsfunc.} = client.buffer.cursorDown()
proc cursorUp(client: Client) {.jsfunc.} = client.buffer.cursorUp()
proc cursorRight(client: Client) {.jsfunc.} = client.buffer.cursorRight()
proc cursorLineBegin(client: Client) {.jsfunc.} = client.buffer.cursorLineBegin()
proc cursorLineEnd(client: Client) {.jsfunc.} = client.buffer.cursorLineEnd()
proc cursorNextWord(client: Client) {.jsfunc.} = client.buffer.cursorNextWord()
proc cursorPrevWord(client: Client) {.jsfunc.} = client.buffer.cursorPrevWord()
proc cursorNextLink(client: Client) {.jsfunc.} = client.buffer.cursorNextLink()
proc cursorPrevLink(client: Client) {.jsfunc.} = client.buffer.cursorPrevLink()
proc pageDown(client: Client) {.jsfunc.} = client.buffer.pageDown()
proc pageUp(client: Client) {.jsfunc.} = client.buffer.pageUp()
proc pageRight(client: Client) {.jsfunc.} = client.buffer.pageRight()
proc pageLeft(client: Client) {.jsfunc.} = client.buffer.pageLeft()
proc halfPageDown(client: Client) {.jsfunc.} = client.buffer.halfPageDown()
proc halfPageUp(client: Client) {.jsfunc.} = client.buffer.halfPageUp()
proc cursorFirstLine(client: Client) {.jsfunc.} = client.buffer.cursorFirstLine()
proc cursorLastLine(client: Client) {.jsfunc.} = client.buffer.cursorLastLine()
proc cursorTop(client: Client) {.jsfunc.} = client.buffer.cursorTop()
proc cursorMiddle(client: Client) {.jsfunc.} = client.buffer.cursorMiddle()
proc cursorBottom(client: Client) {.jsfunc.} = client.buffer.cursorBottom()
proc cursorLeftEdge(client: Client) {.jsfunc.} = client.buffer.cursorLeftEdge()
proc cursorVertMiddle(client: Client) {.jsfunc.} = client.buffer.cursorVertMiddle()
proc cursorRightEdge(client: Client) {.jsfunc.} = client.buffer.cursorRightEdge()
proc centerLine(client: Client) {.jsfunc.} = client.buffer.centerLine()
proc scrollDown(client: Client) {.jsfunc.} = client.buffer.scrollDown()
proc scrollUp(client: Client) {.jsfunc.} = client.buffer.scrollUp()
proc scrollLeft(client: Client) {.jsfunc.} = client.buffer.scrollLeft()
proc scrollRight(client: Client) {.jsfunc.} = client.buffer.scrollRight()
proc lineInfo(client: Client) {.jsfunc.} = client.buffer.lineInfo()
proc reshape(client: Client) {.jsfunc.} = client.buffer.reshape = true
proc redraw(client: Client) {.jsfunc.} = client.buffer.redraw = true

proc input(client: Client) =
  if client.cmdmode:
    client.commandMode()
    return
  if not client.feednext:
    client.s = ""
  else:
    client.feednext = false
  restoreStdin()
  let c = client.console.readChar()
  client.s &= c

  let action = getNormalAction(client.s)
  discard client.evalJS(action, "<command>")

proc followRedirect(client: Client)

proc checkAuth(client: Client) =
  if client.needsauth:
    client.buffer.refreshBuffer()
    client.statusMode()
    var username = ""
    let ustatus = readLine("Username: ", username, client.buffer.width)
    if not ustatus:
      client.needsauth = false
      return
    client.statusMode()
    var password = ""
    let pstatus = readLine("Password: ", password, client.buffer.width, hide = true)
    if not pstatus:
      client.needsauth = false
      return
    var url = client.buffer.location
    url.username = username
    url.password = password
    var buf = client.buffer
    client.gotoUrl(newRequest(url), prevurl = some(client.buffer.location))
    discardBuffer(buf)
    client.followRedirect()

proc followRedirect(client: Client) =
  while client.redirecturl.issome:
    client.statusMode()
    print("Redirecting to ", $client.redirecturl.get)
    stdout.flushFile()
    client.buffer.refreshBuffer(true)
    var buf = client.buffer
    let redirecturl = client.redirecturl.get
    client.redirecturl = none(Url)
    client.gotoUrl(newRequest(redirecturl), prevurl = some(client.buffer.location))
    discardBuffer(buf)
    if client.needsauth:
      client.checkAuth()

proc inputLoop(client: Client) =
  while true:
    g_client = client
    restoreStdin()
    client.followRedirect()
    client.checkAuth()
    client.buffer.refreshBuffer()
    if client.needsauth: # Unauthorized
      client.checkAuth()
    try:
      client.input()
    except ActionError as e:
      client.buffer.setStatusMessage(e.msg)

#TODO this is dumb
proc readFile(client: Client, path: string): string {.jsfunc.} =
  try:
    return readFile(path)
  except IOError:
    discard

#TODO ditto
proc writeFile(client: Client, path: string, content: string) {.jsfunc.} =
  writeFile(path, content)

proc launchClient*(client: Client, pages: seq[string], ctype: string, dump: bool) =
  if gconfig.startup != "":
    let s = readFile(gconfig.startup)
    #client.command(s)
    client.console.err = newFileStream(stderr)
    client.command0(s, gconfig.startup)
    client.console.err = newStringStream()
    quit()
  client.userstyle = gconfig.stylesheet.parseStylesheet()
  if not stdin.isatty:
    client.readPipe(ctype)
  try:
    for page in pages:
      client.loadUrl(page, ctype)
  except LoadError as e:
    eprint e.msg
    quit(1)

  if stdout.isatty and not dump:
    when defined(posix):
      enableRawMode()
    client.inputLoop()
  else:
    var buffer = client.buffer
    while buffer.next != nil:
      buffer = buffer.next

    buffer.drawBuffer()
    while buffer.prev != nil:
      buffer = buffer.prev
      buffer.drawBuffer()

proc nimGCStats(client: Client): string {.jsfunc.} =
  return GC_getStatistics()

proc jsGCStats(client: Client): string {.jsfunc.} =
  return client.jsrt.getMemoryUsage()

func newConsole(): Console =
  new(result)
  result.err = newStringStream()

proc log(console: Console, ss: varargs[string]) {.jsfunc.} =
  for i in 0..<ss.len:
    console.err.write(ss[i])
    if i != ss.high:
      console.err.write(' ')
  console.err.write('\n')

proc sleep(client: Client, millis: int) {.jsfunc.} =
  sleep millis

proc newClient*(): Client =
  new(result)
  result.loader = newFileLoader()
  result.console = newConsole()
  let rt = newJSRuntime()
  rt.setInterruptHandler(interruptHandler, cast[pointer](result))
  let ctx = rt.newJSContext()
  result.jsrt = rt
  result.jsctx = ctx
  var global = ctx.getGlobalObject()
  ctx.registerType(Cookie)
  ctx.registerType(Client, asglobal = true)
  global.setOpaque(result)
  ctx.setProperty(global.val, "client", global.val)

  let consoleClassId = ctx.registerType(Console)
  let jsConsole = ctx.newJSObject(consoleClassId)
  jsConsole.setOpaque(result.console)
  ctx.setProperty(global.val, "console", jsConsole.val)
  free(global)

  ctx.addUrlModule()
  ctx.addDOMModule()
  ctx.addHTMLModule()
  ctx.addRequestModule()