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

              
              
             
               
            
 

                    

                    
                    

                      

                  
                
                 
              
                    
                   
                
                   

    
                         
                     
                         

                  
                        
                            
                      


                              

                    






                                                                                             
 



                       
 




                                       
 

                                        



                        
                                 
                                        
             
 
                                              





                                                               

                                            
                        
       

                       



                                                                   






                                                                      
         








                                                              



                                                                                     
                                        
                       
                                                   
       



                                                
           





                                             




                                              
                                       






                                                                        
       



                                                                                                                  
 


                                               
                                                                                      




                                             
                                             
 
                                                
                                           
             
            
 


                                          
                            
                              

                        



                           

                                   
               
 
                                                       
                                        
 

                                
                  






                                                       
 










                                                                          

















































































                                                                                                
                                                                                   

                                           
                                              
                                                             
                        

                                          
                                                               

                          

                                                           
 



                                
       







                                                

















                                                            


                                                    
                                         
             
                        

                                 

                                                              





                                                                 
                                           
                          
                                                   
              
 

                           
                       
                    


                        
                      
import options
import os
import streams
import tables
import terminal
import times

import std/monotimes

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

type
  Client* = ref ClientObj
  ClientObj* = object
    attrs: TermAttributes
    feednext: bool
    s: string
    errormessage: string
    userstyle: CSSStylesheet
    loader: FileLoader
    console {.jsget.}: Console
    pager {.jsget.}: Pager
    config: Config
    jsrt: JSRuntime
    jsctx: JSContext
    timeoutid: int
    timeouts: Table[int, tuple[handler: proc(), time: int64]]
    added_timeouts: Table[int, tuple[handler: proc(), time: int64]]
    removed_timeouts: seq[int]
    intervals: Table[int, tuple[handler: proc(), time: int64, wait: int, del: JSValue]]
    added_intervals: Table[int, tuple[handler: proc(), time: int64, wait: int, del: JSValue]]
    removed_intervals: seq[int]

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

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.attrs.height + 1, 1))
  print(EL())

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

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

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 evalJSFree(client: Client, src, filename: string) =
  free(client.evalJS(src, filename))

proc command0(client: Client, src: string, filename = "<command>", silence = false) =
  let ret = client.evalJS(src, filename)
  if ret.isException():
    client.jsctx.writeException(client.console.err)
  else:
    if not silence:
      let str = ret.toString()
      if str.issome:
        client.console.err.write(str.get & '\n')
  free(ret)
  for k, v in client.added_timeouts:
    client.timeouts[k] = v
  client.added_timeouts.clear()
  for k, v in client.added_intervals:
    client.intervals = client.added_intervals
  client.added_intervals.clear()

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

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

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

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

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

proc input(client: Client) =
  if client.pager.commandMode:
    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.config, client.s)
  client.evalJSFree(action, "<command>")

proc inputLoop(client: Client) =
  while true:
    restoreStdin()
    client.pager.displayPage()
    client.pager.followRedirect()
    if client.pager.container != nil:
      client.pager.container.buffer.refreshBuffer()
    if client.pager.container.needsauth: # Unauthorized
      client.pager.checkAuth()
    client.input()

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

import bindings/quickjs

proc setTimeout[T: JSObject|string](client: Client, handler: T, timeout = 0): int {.jsfunc.} =
  let id = client.timeoutid
  inc client.timeoutid
  when T is string:
    client.added_timeouts[id] = ((proc() =
      client.evalJSFree(handler, "setTimeout handler")
    ), getMonoTime().ticks div 1_000_000 + timeout)
  else:
    let fun = JS_DupValue(handler.ctx, handler.val)
    client.added_timeouts[id] = ((proc() =
      let ret = JSObject(ctx: handler.ctx, val: fun).callFunction()
      if ret.isException():
        ret.ctx.writeException(client.console.err)
      JS_FreeValue(ret.ctx, ret.val)
      JS_FreeValue(ret.ctx, fun)
    ), getMonoTime().ticks div 1_000_000 + timeout)
  return id

proc setInterval[T: JSObject|string](client: Client, handler: T, interval = 0): int {.jsfunc.} =
  let id = client.timeoutid
  inc client.timeoutid
  when T is string:
    client.added_intervals[id] = ((proc() =
      client.evalJSFree(handler, "setInterval handler")
    ), getMonoTime().ticks div 1_000_000 + interval, interval, JS_NULL)
  else:
    let fun = JS_DupValue(handler.ctx, handler.val)
    client.added_intervals[id] = ((proc() =
      let ret = JSObject(ctx: handler.ctx, val: fun).callFunction()
      if ret.isException():
        ret.ctx.writeException(client.console.err)
      JS_FreeValue(ret.ctx, ret.val)
    ), getMonoTime().ticks div 1_000_000 + interval, interval, fun)
  return id

proc clearTimeout(client: Client, id: int) {.jsfunc.} =
  client.removed_timeouts.add(id)

proc clearInterval(client: Client, id: int) {.jsfunc.} =
  client.removed_intervals.add(id)

proc jsEventLoop(client: Client) =
  while client.timeouts.len > 0 or client.intervals.len > 0:
    var wait = -1
    let curr = getMonoTime().ticks div 1_000_000
    for k, v in client.timeouts:
      if v.time <= curr:
        v.handler()
        client.removed_timeouts.add(k)
    for k, v in client.intervals.mpairs:
      if v.time <= curr:
        v.handler()
        v.time = curr + v.wait
    for k, v in client.added_timeouts:
      client.timeouts[k] = v
    client.added_timeouts.clear()
    for k, v in client.added_intervals:
      client.intervals[k] = v
    client.added_intervals.clear()
    for k in client.removed_timeouts:
      client.timeouts.del(k)
    for k in client.removed_intervals:
      if k in client.intervals and client.intervals[k].del != JS_NULL:
        JS_FreeValue(client.jsctx, client.intervals[k].del)
      client.intervals.del(k)
    client.removed_timeouts.setLen(0)
    client.removed_intervals.setLen(0)
    for k, v in client.timeouts:
      if wait != -1:
        wait = min(wait, int(v.time - curr))
      else:
        wait = int(v.time - curr)
    for k, v in client.intervals:
      if wait != -1:
        wait = min(wait, int(v.time - curr))
      else:
        wait = int(v.time - curr)
    if wait > 0:
      sleep(wait)

proc launchClient*(client: Client, pages: seq[string], ctype: string, dump: bool) =
  if client.config.startup != "":
    let s = readFile(client.config.startup)
    client.console.err = newFileStream(stderr)
    client.command0(s, client.config.startup, silence = true)
    client.jsEventLoop()
    client.console.err = newStringStream()
    quit()
  client.userstyle = client.config.stylesheet.parseStylesheet()
  if not stdin.isatty:
    client.readPipe(ctype)
  for page in pages:
    client.pager.loadURL(page, force = true, ctype = ctype)

  if stdout.isatty and not dump:
    when defined(posix):
      enableRawMode()
    client.inputLoop()
  else:
    for msg in client.pager.status:
      eprint msg
    while client.pager.nextBuffer():
      discard
    if client.pager.container != nil:
      client.pager.container.buffer.drawBuffer()
    while client.pager.prevBuffer():
      client.pager.container.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*(config: Config): Client =
  new(result)
  result.config = config
  result.loader = newFileLoader()
  result.console = newConsole()
  result.attrs = getTermAttributes()
  result.pager = newPager(config, result.attrs, result.loader)
  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(Client, asglobal = true)
  global.setOpaque(result)
  ctx.setProperty(global.val, "client", global.val)
  free(global)

  ctx.registerType(Console)

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