about summary refs log tree commit diff stats
path: root/src/display/client.nim
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2022-09-12 00:30:21 +0200
committerbptato <nincsnevem662@gmail.com>2022-09-12 00:30:21 +0200
commit51ea622d58bfca19212fac1800cfb033bb85ec39 (patch)
treeb75891690f67b190c60584751f2a30c96f342fdc /src/display/client.nim
parente38402dfa1bbc33db6b9d9736517eb45533d595c (diff)
downloadchawan-51ea622d58bfca19212fac1800cfb033bb85ec39.tar.gz
Add JS binding generation
Diffstat (limited to 'src/display/client.nim')
-rw-r--r--src/display/client.nim591
1 files changed, 591 insertions, 0 deletions
diff --git a/src/display/client.nim b/src/display/client.nim
new file mode 100644
index 00000000..6e3c8c45
--- /dev/null
+++ b/src/display/client.nim
@@ -0,0 +1,591 @@
+import options
+import os
+import streams
+import terminal
+import unicode
+
+import css/sheet
+import config/config
+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()
+
+# 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.loader.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 command(client: Client, src: string) =
+  restoreStdin()
+  let previ = client.console.err.getPosition()
+  let ret = client.evalJS(src, "<command>")
+  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)
+  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.} =
+  eraseScreen()
+  print(HVP(0, 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)
+
+proc launchClient*(client: Client, pages: seq[string], ctype: string, dump: bool) =
+  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 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()
+  discard ctx.registerType(Client, asglobal = true, addto = some(global))
+  global.setOpaque(result)
+  global.setProperty("client", global)
+
+  let consoleClassId = ctx.registerType(Console)
+  let jsConsole = ctx.newJSObject(consoleClassId)
+  jsConsole.setOpaque(result.console)
+  global.setProperty("console", jsConsole)
+
+  ctx.addUrlModule()