about summary refs log tree commit diff stats
path: root/src/display
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2023-09-14 01:08:10 +0200
committerbptato <nincsnevem662@gmail.com>2023-09-14 01:08:10 +0200
commitdb0798acccbedcef4b16737f6be0cf7388cc0528 (patch)
tree208616de68fe9729e09e4084557f27f5b7204387 /src/display
parentc2004e4aba182473e79100759a2d58e1bd7d184c (diff)
downloadchawan-db0798acccbedcef4b16737f6be0cf7388cc0528.tar.gz
move some modules to local/
makes a bit more sense than the previous arrangement
Diffstat (limited to 'src/display')
-rw-r--r--src/display/client.nim643
-rw-r--r--src/display/pager.nim1189
2 files changed, 0 insertions, 1832 deletions
diff --git a/src/display/client.nim b/src/display/client.nim
deleted file mode 100644
index 3236732d..00000000
--- a/src/display/client.nim
+++ /dev/null
@@ -1,643 +0,0 @@
-import cstrutils
-import nativesockets
-import net
-import options
-import os
-import selectors
-import streams
-import strutils
-import tables
-import terminal
-
-when defined(posix):
-  import posix
-
-import std/exitprocs
-
-import bindings/quickjs
-import buffer/container
-import config/config
-import css/sheet
-import display/pager
-import display/term
-import html/chadombuilder
-import html/dom
-import html/event
-import io/headers
-import io/lineedit
-import io/loader
-import io/posixstream
-import io/promise
-import io/request
-import io/window
-import ips/forkserver
-import ips/socketstream
-import js/base64
-import js/domexception
-import js/error
-import js/fromjs
-import js/intl
-import js/javascript
-import js/module
-import js/timeout
-import js/tojs
-import types/blob
-import types/cookie
-import types/url
-import utils/opt
-import utils/twtstr
-import xhr/formdata
-import xhr/xmlhttprequest
-
-import chakasu/charset
-
-type
-  Client* = ref object
-    alive: bool
-    attrs: WindowAttributes
-    config {.jsget.}: Config
-    console {.jsget.}: Console
-    errormessage: string
-    fd: int
-    fdmap: Table[int, Container]
-    feednext: bool
-    forkserver: ForkServer
-    notnum: bool # has a non-numeric character been input already?
-    jsctx: JSContext
-    jsrt: JSRuntime
-    line {.jsget.}: LineEdit
-    loader: FileLoader
-    mainproc: Pid
-    pager {.jsget.}: Pager
-    precnum: int32 # current number prefix (when vi-numeric-prefix is true)
-    s: string # current input buffer
-    selector: Selector[Container]
-    store {.jsget, jsset.}: Document
-    timeouts: TimeoutState[Container]
-    userstyle: CSSStylesheet
-
-  Console = ref object
-    err: Stream
-    pager: Pager
-    container: Container
-    prev: Container
-    ibuf: string
-    tty: File
-
-jsDestructor(Client)
-jsDestructor(Console)
-
-proc readChar(console: Console): char =
-  if console.ibuf == "":
-    try:
-      return console.tty.readChar()
-    except EOFError:
-      quit(1)
-  result = console.ibuf[0]
-  console.ibuf = console.ibuf.substr(1)
-
-proc finalize(client: Client) {.jsfin.} =
-  if client.jsctx != nil:
-    free(client.jsctx)
-  if client.jsrt != nil:
-    free(client.jsrt)
-
-proc doRequest(client: Client, req: Request): Response {.jsfunc.} =
-  return client.loader.doRequest(req)
-
-proc fetch[T: Request|string](client: Client, req: T,
-    init = none(RequestInit)): JSResult[FetchPromise] {.jsfunc.} =
-  let req = ?newRequest(client.jsctx, req, init)
-  return ok(client.loader.fetch(req))
-
-proc interruptHandler(rt: JSRuntime, opaque: pointer): int {.cdecl.} =
-  let client = cast[Client](opaque)
-  if client.console == nil or client.console.tty == nil: return
-  try:
-    let c = client.console.tty.readChar()
-    if c == char(3): #C-c
-      client.console.ibuf = ""
-      return 1
-    else:
-      client.console.ibuf &= c
-  except IOError:
-    discard
-  return 0
-
-proc runJSJobs(client: Client) =
-  client.jsrt.runJSJobs(client.console.err)
-
-proc evalJS(client: Client, src, filename: string, module = false): JSValue =
-  client.pager.term.unblockStdin()
-  let flags = if module:
-    JS_EVAL_TYPE_MODULE
-  else:
-    JS_EVAL_TYPE_GLOBAL
-  result = client.jsctx.eval(src, filename, flags)
-  client.runJSJobs()
-  client.pager.term.restoreStdin()
-
-proc evalJSFree(client: Client, src, filename: string) =
-  JS_FreeValue(client.jsctx, client.evalJS(src, filename))
-
-proc command0(client: Client, src: string, filename = "<command>",
-    silence = false, module = false) =
-  let ret = client.evalJS(src, filename, module = module)
-  if JS_IsException(ret):
-    client.jsctx.writeException(client.console.err)
-  else:
-    if not silence:
-      let str = fromJS[string](client.jsctx, ret)
-      if str.isSome:
-        client.console.err.write(str.get & '\n')
-        client.console.err.flush()
-  JS_FreeValue(client.jsctx, ret)
-
-proc command(client: Client, src: string) =
-  client.command0(src)
-  client.console.container.requestLines().then(proc() =
-    client.console.container.cursorLastLine())
-
-proc suspend(client: Client) {.jsfunc.} =
-  client.pager.term.quit()
-  discard kill(client.mainproc, cint(SIGSTOP))
-  client.pager.term.restart()
-
-proc quit(client: Client, code = 0) {.jsfunc.} =
-  if client.alive:
-    client.alive = false
-    client.pager.quit()
-    let ctx = client.jsctx
-    var global = JS_GetGlobalObject(ctx)
-    JS_FreeValue(ctx, global)
-    if client.jsctx != nil:
-      free(client.jsctx)
-    #TODO
-    #if client.jsrt != nil:
-    #  free(client.jsrt)
-  quit(code)
-
-proc feedNext(client: Client) {.jsfunc.} =
-  client.feednext = true
-
-proc alert(client: Client, msg: string) {.jsfunc.} =
-  client.pager.alert(msg)
-
-proc handlePagerEvents(client: Client) =
-  let container = client.pager.container
-  if container != nil:
-    client.pager.handleEvents(container)
-
-proc evalAction(client: Client, action: string, arg0: int32) =
-  let ret = client.evalJS(action, "<command>")
-  let ctx = client.jsctx
-  if JS_IsFunction(ctx, ret):
-    if arg0 != 0:
-      var arg0 = toJS(ctx, arg0)
-      JS_FreeValue(ctx, JS_Call(ctx, ret, JS_UNDEFINED, 1, addr arg0))
-      JS_FreeValue(ctx, arg0)
-    else: # no precnum
-      JS_FreeValue(ctx, JS_Call(ctx, ret, JS_UNDEFINED, 0, nil))
-  JS_FreeValue(ctx, ret)
-
-# The maximum number we are willing to accept.
-# This should be fine for 32-bit signed ints (which precnum currently is).
-# We can always increase it further (e.g. by switching to uint32, uint64...) if
-# it proves to be too low.
-const MaxPrecNum = 100000000
-
-proc handleCommandInput(client: Client, c: char) =
-  if client.config.input.vi_numeric_prefix and not client.notnum:
-    if client.precnum != 0 and c == '0' or c in '1' .. '9':
-      if client.precnum < MaxPrecNum: # better ignore than eval...
-        client.precnum *= 10
-        client.precnum += cast[int32](decValue(c))
-      return
-    else:
-      client.notnum = true
-  client.s &= c
-  let action = getNormalAction(client.config, client.s)
-  client.evalAction(action, client.precnum)
-  if not client.feedNext:
-    client.precnum = 0
-    client.notnum = false
-    client.handlePagerEvents()
-    client.pager.refreshStatusMsg()
-
-proc input(client: Client) =
-  client.pager.term.restoreStdin()
-  while true:
-    let c = client.console.readChar()
-    if client.pager.askpromise != nil:
-      if c == 'y':
-        client.pager.fulfillAsk(true)
-        client.runJSJobs()
-      elif c == 'n':
-        client.pager.fulfillAsk(false)
-        client.runJSJobs()
-    elif client.pager.lineedit.isSome:
-      client.s &= c
-      let edit = client.pager.lineedit.get
-      client.line = edit
-      if edit.escNext:
-        edit.escNext = false
-        if edit.write(client.s, client.pager.term.cs):
-          client.s = ""
-      else:
-        let action = getLinedAction(client.config, client.s)
-        if action == "":
-          if edit.write(client.s, client.pager.term.cs):
-            client.s = ""
-          else:
-            client.feedNext = true
-        elif not client.feednext:
-          client.evalAction(action, 0)
-        if client.pager.lineedit.isNone:
-          client.line = nil
-        if not client.feedNext:
-          client.pager.updateReadLine()
-    else:
-      client.handleCommandInput(c)
-    if not client.feednext:
-      client.s = ""
-      break
-    else:
-      client.feednext = false
-  client.s = ""
-
-proc setTimeout[T: JSValue|string](client: Client, handler: T,
-    timeout = 0i32): int32 {.jsfunc.} =
-  return client.timeouts.setTimeout(handler, timeout)
-
-proc setInterval[T: JSValue|string](client: Client, handler: T,
-    interval = 0i32): int32 {.jsfunc.} =
-  return client.timeouts.setInterval(handler, interval)
-
-proc clearTimeout(client: Client, id: int32) {.jsfunc.} =
-  client.timeouts.clearTimeout(id)
-
-proc clearInterval(client: Client, id: int32) {.jsfunc.} =
-  client.timeouts.clearInterval(id)
-
-let SIGWINCH {.importc, header: "<signal.h>", nodecl.}: cint
-
-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')
-  console.err.flush()
-
-proc show(console: Console) {.jsfunc.} =
-  if console.pager.container != console.container:
-    console.prev = console.pager.container
-    console.pager.setContainer(console.container)
-    console.container.requestLines()
-
-proc hide(console: Console) {.jsfunc.} =
-  if console.pager.container == console.container:
-    console.pager.setContainer(console.prev)
-
-proc buffer(console: Console): Container {.jsfget.} =
-  return console.container
-
-proc acceptBuffers(client: Client) =
-  while client.pager.unreg.len > 0:
-    let (pid, stream) = client.pager.unreg.pop()
-    let fd = stream.source.getFd()
-    if int(fd) in client.fdmap:
-      client.selector.unregister(fd)
-      client.fdmap.del(int(fd))
-    else:
-      client.pager.procmap.del(pid)
-    stream.close()
-  for pid, container in client.pager.procmap:
-    let stream = connectSocketStream(pid, buffered = false, blocking = true)
-    container.setStream(stream)
-    let fd = stream.source.getFd()
-    client.fdmap[int(fd)] = container
-    client.selector.registerHandle(fd, {Read}, nil)
-    client.pager.handleEvents(container)
-  client.pager.procmap.clear()
-
-proc c_setvbuf(f: File, buf: pointer, mode: cint, size: csize_t): cint {.
-  importc: "setvbuf", header: "<stdio.h>", tags: [].}
-
-proc handleRead(client: Client, fd: int) =
-  if client.console.tty != nil and fd == client.console.tty.getFileHandle():
-    client.input()
-    client.handlePagerEvents()
-  elif fd == client.forkserver.estream.fd:
-    var nl = false
-    const prefix = "STDERR: "
-    var s = prefix
-    while true:
-      try:
-        let c = client.forkserver.estream.readChar()
-        if nl and s.len > prefix.len:
-          client.console.err.write(s)
-          s = prefix
-          nl = false
-        s &= c
-        nl = c == '\n'
-      except IOError:
-        break
-    if s.len > prefix.len:
-      client.console.err.write(s)
-    client.console.err.flush()
-  elif fd in client.loader.connecting:
-    client.loader.onConnected(fd)
-    client.runJSJobs()
-  elif fd in client.loader.ongoing:
-    client.loader.onRead(fd)
-  elif fd in client.loader.unregistered:
-    discard # ignore
-  else:
-    let container = client.fdmap[fd]
-    client.pager.handleEvent(container)
-
-proc flushConsole*(client: Client) {.jsfunc.} =
-  client.handleRead(client.forkserver.estream.fd)
-
-proc handleError(client: Client, fd: int) =
-  if client.console.tty != nil and fd == client.console.tty.getFileHandle():
-    #TODO do something here...
-    stderr.write("Error in tty\n")
-    quit(1)
-  elif fd == client.forkserver.estream.fd:
-    #TODO do something here...
-    stderr.write("Fork server crashed :(\n")
-    quit(1)
-  elif fd in client.loader.connecting:
-    #TODO handle error?
-    discard
-  elif fd in client.loader.ongoing:
-    client.loader.onError(fd)
-  elif fd in client.loader.unregistered:
-    discard # already unregistered...
-  else:
-    if fd in client.fdmap:
-      let container = client.fdmap[fd]
-      if container != client.console.container:
-        client.console.log("Error in buffer", $container.location)
-      else:
-        client.console.container = nil
-      client.selector.unregister(fd)
-      client.fdmap.del(fd)
-    if client.console.container != nil:
-      client.console.show()
-    else:
-      doAssert false
-
-proc inputLoop(client: Client) =
-  let selector = client.selector
-  discard c_setvbuf(client.console.tty, nil, IONBF, 0)
-  selector.registerHandle(int(client.console.tty.getFileHandle()), {Read}, nil)
-  let sigwinch = selector.registerSignal(int(SIGWINCH), nil)
-  while true:
-    let events = client.selector.select(-1)
-    for event in events:
-      if Read in event.events:
-        client.handleRead(event.fd)
-      if Error in event.events:
-        client.handleError(event.fd)
-      if Signal in event.events: 
-        assert event.fd == sigwinch
-        client.attrs = getWindowAttributes(client.console.tty)
-        client.pager.windowChange(client.attrs)
-      if selectors.Event.Timer in event.events:
-        assert client.timeouts.runTimeoutFd(event.fd)
-        client.runJSJobs()
-        client.console.container.requestLines().then(proc() =
-          client.console.container.cursorLastLine())
-    client.loader.unregistered.setLen(0)
-    client.acceptBuffers()
-    if client.pager.scommand != "":
-      client.command(client.pager.scommand)
-      client.pager.scommand = ""
-      client.handlePagerEvents()
-    if client.pager.container == nil:
-      # No buffer to display.
-      quit(1)
-    client.pager.showAlerts()
-    client.pager.draw()
-
-func hasSelectFds(client: Client): bool =
-  return not client.timeouts.empty or
-    client.pager.numload > 0 or
-    client.loader.connecting.len > 0 or
-    client.loader.ongoing.len > 0 or
-    client.pager.procmap.len > 0
-
-proc headlessLoop(client: Client) =
-  while client.hasSelectFds():
-    let events = client.selector.select(-1)
-    for event in events:
-      if Read in event.events:
-        client.handleRead(event.fd)
-      if Error in event.events:
-        client.handleError(event.fd)
-      if selectors.Event.Timer in event.events:
-        assert client.timeouts.runTimeoutFd(event.fd)
-    client.runJSJobs()
-    client.loader.unregistered.setLen(0)
-    client.acceptBuffers()
-
-proc clientLoadJSModule(ctx: JSContext, module_name: cstring,
-    opaque: pointer): JSModuleDef {.cdecl.} =
-  let global = JS_GetGlobalObject(ctx)
-  JS_FreeValue(ctx, global)
-  var x: Option[URL]
-  if module_name.startsWith("/") or module_name.startsWith("./") or
-      module_name.startsWith("../"):
-    let cur = getCurrentDir()
-    x = parseURL($module_name, parseURL("file://" & cur & "/"))
-  else:
-    x = parseURL($module_name)
-  if x.isNone or x.get.scheme != "file":
-    JS_ThrowTypeError(ctx, "Invalid URL: %s", module_name)
-    return nil
-  try:
-    let f = readFile($x.get.path)
-    return finishLoadModule(ctx, f, module_name)
-  except IOError:
-    JS_ThrowTypeError(ctx, "Failed to open file %s", module_name)
-    return nil
-
-proc readBlob(client: Client, path: string): Option[WebFile] {.jsfunc.} =
-  try:
-    return some(newWebFile(path))
-  except IOError:
-    discard
-
-#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 newConsole(pager: Pager, tty: File): Console =
-  new(result)
-  if tty != nil:
-    var pipefd: array[0..1, cint]
-    if pipe(pipefd) == -1:
-      raise newException(Defect, "Failed to open console pipe.")
-    let url = newURL("javascript:console.show()")
-    result.container = pager.readPipe0(some("text/plain"), CHARSET_UNKNOWN,
-      pipefd[0], option(url.get(nil)), "Browser console")
-    var f: File
-    if not open(f, pipefd[1], fmWrite):
-      raise newException(Defect, "Failed to open file for console pipe.")
-    result.err = newFileStream(f)
-    result.err.writeLine("Type (M-c) console.hide() to return to buffer mode.")
-    result.err.flush()
-    result.pager = pager
-    result.tty = tty
-    pager.registerContainer(result.container)
-  else:
-    result.err = newFileStream(stderr)
-
-proc dumpBuffers(client: Client) =
-  client.headlessLoop()
-  let ostream = newFileStream(stdout)
-  for container in client.pager.containers:
-    try:
-      client.pager.drawBuffer(container, ostream)
-      client.pager.handleEvents(container)
-    except IOError:
-      client.console.log("Error in buffer", $container.location)
-      # check for errors
-      client.handleRead(client.forkserver.estream.fd)
-      quit(1)
-  stdout.close()
-
-proc launchClient*(client: Client, pages: seq[string],
-    contentType: Option[string], cs: Charset, dump: bool) =
-  var tty: File
-  var dump = dump
-  if not dump:
-    if stdin.isatty():
-      tty = stdin
-    if stdout.isatty():
-      if tty == nil:
-        dump = not open(tty, "/dev/tty", fmRead)
-    else:
-      dump = true
-  let selector = newSelector[Container]()
-  let efd = int(client.forkserver.estream.fd)
-  selector.registerHandle(efd, {Read}, nil)
-  client.loader.registerFun = proc(fd: int) =
-    selector.registerHandle(fd, {Read}, nil)
-  client.loader.unregisterFun = proc(fd: int) =
-    selector.unregister(fd)
-  client.selector = selector
-  client.pager.launchPager(tty)
-  client.console = newConsole(client.pager, tty)
-  #TODO passing console.err here makes it impossible to change it later. maybe
-  # better associate it with jsctx
-  client.timeouts = newTimeoutState(client.selector, client.jsctx,
-    client.console.err, proc(src, file: string) = client.evalJSFree(src, file))
-  client.alive = true
-  addExitProc((proc() = client.quit()))
-  if client.config.start.startup_script != "":
-    let s = if fileExists(client.config.start.startup_script):
-      readFile(client.config.start.startup_script)
-    else:
-      client.config.start.startup_script
-    let ismodule = client.config.start.startup_script.endsWith(".mjs")
-    client.command0(s, client.config.start.startup_script, silence = true,
-      module = ismodule)
-  client.userstyle = client.config.css.stylesheet.parseStylesheet()
-
-  if not stdin.isatty():
-    client.pager.readPipe(contentType, cs, stdin.getFileHandle())
-
-  for page in pages:
-    client.pager.loadURL(page, ctype = contentType, cs = cs)
-  client.pager.showAlerts()
-  client.acceptBuffers()
-  if not dump:
-    client.inputLoop()
-  else:
-    client.dumpBuffers()
-  if client.config.start.headless:
-    client.headlessLoop()
-  client.quit()
-
-proc nimGCStats(client: Client): string {.jsfunc.} =
-  return GC_getStatistics()
-
-proc jsGCStats(client: Client): string {.jsfunc.} =
-  return client.jsrt.getMemoryUsage()
-
-proc nimCollect(client: Client) {.jsfunc.} =
-  GC_fullCollect()
-
-proc jsCollect(client: Client) {.jsfunc.} =
-  JS_RunGC(client.jsrt)
-
-proc sleep(client: Client, millis: int) {.jsfunc.} =
-  sleep millis
-
-proc atob(client: Client, data: string): DOMResult[string] {.jsfunc.} =
-  return atob(data)
-
-proc btoa(client: Client, data: string): DOMResult[string] {.jsfunc.} =
-  return btoa(data)
-
-proc addJSModules(client: Client, ctx: JSContext) =
-  ctx.addDOMExceptionModule()
-  ctx.addCookieModule()
-  ctx.addURLModule()
-  ctx.addEventModule()
-  ctx.addDOMModule()
-  ctx.addHTMLModule()
-  ctx.addIntlModule()
-  ctx.addBlobModule()
-  ctx.addFormDataModule()
-  ctx.addXMLHttpRequestModule()
-  ctx.addHeadersModule()
-  ctx.addRequestModule()
-  ctx.addResponseModule()
-  ctx.addLineEditModule()
-  ctx.addConfigModule()
-  ctx.addPagerModule()
-  ctx.addContainerModule()
-
-func getClient(client: Client): Client {.jsfget: "client".} =
-  return client
-
-proc newClient*(config: Config, forkserver: ForkServer, mainproc: Pid): Client =
-  setControlCHook(proc() {.noconv.} = quit(1))
-  let jsrt = newJSRuntime()
-  JS_SetModuleLoaderFunc(jsrt, normalizeModuleName, clientLoadJSModule, nil)
-  let jsctx = jsrt.newJSContext()
-  let attrs = getWindowAttributes(stdout)
-  let client = Client(
-    config: config,
-    forkserver: forkserver,
-    mainproc: mainproc,
-    attrs: attrs,
-    loader: forkserver.newFileLoader(
-      defaultHeaders = config.getDefaultHeaders(),
-      proxy = config.getProxy(),
-      acceptProxy = true
-    ),
-    jsrt: jsrt,
-    jsctx: jsctx,
-    pager: newPager(config, attrs, forkserver, mainproc, jsctx)
-  )
-  jsrt.setInterruptHandler(interruptHandler, cast[pointer](client))
-  var global = JS_GetGlobalObject(jsctx)
-  jsctx.registerType(Client, asglobal = true)
-  setGlobal(jsctx, global, client)
-  JS_FreeValue(jsctx, global)
-  jsctx.registerType(Console)
-  client.addJSModules(jsctx)
-  return client
diff --git a/src/display/pager.nim b/src/display/pager.nim
deleted file mode 100644
index 5c6d1852..00000000
--- a/src/display/pager.nim
+++ /dev/null
@@ -1,1189 +0,0 @@
-import deques
-import net
-import options
-import os
-import osproc
-import streams
-import tables
-import unicode
-
-when defined(posix):
-  import posix
-
-import buffer/cell
-import buffer/container
-import buffer/select
-import config/config
-import config/mailcap
-import config/mimetypes
-import display/term
-import extern/editor
-import extern/runproc
-import io/connecterror
-import io/lineedit
-import io/loader
-import io/promise
-import io/request
-import io/tempfile
-import io/window
-import ips/forkserver
-import ips/socketstream
-import js/dict
-import js/javascript
-import js/regex
-import js/tojs
-import types/buffersource
-import types/color
-import types/cookie
-import types/url
-import utils/opt
-import utils/twtstr
-
-import chakasu/charset
-
-type
-  LineMode* = enum
-    NO_LINEMODE, LOCATION, USERNAME, PASSWORD, COMMAND, BUFFER, SEARCH_F,
-    SEARCH_B, ISEARCH_F, ISEARCH_B, GOTO_LINE
-
-  Pager* = ref object
-    alerton: bool
-    alerts: seq[string]
-    askcursor: int
-    askpromise*: Promise[bool]
-    askprompt: string
-    commandMode* {.jsget.}: bool
-    config: Config
-    container*: Container
-    cookiejars: Table[string, CookieJar]
-    display: FixedGrid
-    forkserver: ForkServer
-    iregex: Result[Regex, string]
-    isearchpromise: EmptyPromise
-    lineedit*: Option[LineEdit]
-    linehist: array[LineMode, LineHistory]
-    linemode*: LineMode
-    mailcap: Mailcap
-    mainproc: Pid
-    mimeTypes: MimeTypes
-    numload*: int
-    omnirules: seq[OmniRule]
-    procmap*: Table[Pid, Container]
-    proxy: URL
-    redraw*: bool
-    regex: Opt[Regex]
-    reverseSearch: bool
-    scommand*: string
-    siteconf: seq[SiteConfig]
-    statusgrid*: FixedGrid
-    term*: Terminal
-    tty: File
-    unreg*: seq[(Pid, SocketStream)]
-    username: string
-
-jsDestructor(Pager)
-
-func attrs(pager: Pager): WindowAttributes = pager.term.attrs
-
-func getRoot(container: Container): Container =
-  var c = container
-  while c.parent != nil: c = c.parent
-  return c
-
-iterator all_children(parent: Container): Container {.inline.} =
-  var stack = newSeqOfCap[Container](parent.children.len)
-  for i in countdown(parent.children.high, 0):
-    stack.add(parent.children[i])
-  while stack.len > 0:
-    let c = stack.pop()
-    yield c
-    for i in countdown(c.children.high, 0):
-      stack.add(c.children[i])
-
-iterator containers*(pager: Pager): Container {.inline.} =
-  if pager.container != nil:
-    let root = getRoot(pager.container)
-    yield root
-    for c in root.all_children:
-      yield c
-
-proc setContainer*(pager: Pager, c: Container) {.jsfunc.} =
-  pager.container = c
-  pager.redraw = true
-  if c != nil:
-    pager.term.setTitle(c.getTitle())
-
-proc hasprop(ctx: JSContext, pager: Pager, s: string): bool {.jshasprop.} =
-  if pager.container != nil:
-    let cval = toJS(ctx, pager.container)
-    let val = JS_GetPropertyStr(ctx, cval, s)
-    if val != JS_UNDEFINED:
-      result = true
-    JS_FreeValue(ctx, val)
-
-proc reflect(ctx: JSContext, this_val: JSValue, argc: cint, argv: ptr JSValue,
-             magic: cint, func_data: ptr JSValue): JSValue {.cdecl.} =
-  let fun = cast[ptr JSValue](cast[int](func_data) + sizeof(JSValue))[]
-  return JS_Call(ctx, fun, func_data[], argc, argv)
-
-proc getter(ctx: JSContext, pager: Pager, s: string): Option[JSValue]
-    {.jsgetprop.} =
-  if pager.container != nil:
-    let cval = toJS(ctx, pager.container)
-    let val = JS_GetPropertyStr(ctx, cval, s)
-    if val != JS_UNDEFINED:
-      if JS_IsFunction(ctx, val):
-        var func_data = @[cval, val]
-        let fun = JS_NewCFunctionData(ctx, reflect, 1, 0, 2, addr func_data[0])
-        return some(fun)
-      return some(val)
-
-proc searchNext(pager: Pager) {.jsfunc.} =
-  if pager.regex.isSome:
-    let wrap = pager.config.search.wrap
-    if not pager.reverseSearch:
-      pager.container.cursorNextMatch(pager.regex.get, wrap, true)
-    else:
-      pager.container.cursorPrevMatch(pager.regex.get, wrap, true)
-
-proc searchPrev(pager: Pager) {.jsfunc.} =
-  if pager.regex.isSome:
-    let wrap = pager.config.search.wrap
-    if not pager.reverseSearch:
-      pager.container.cursorPrevMatch(pager.regex.get, wrap, true)
-    else:
-      pager.container.cursorNextMatch(pager.regex.get, wrap, true)
-
-proc getLineHist(pager: Pager, mode: LineMode): LineHistory =
-  if pager.linehist[mode] == nil:
-    pager.linehist[mode] = newLineHistory()
-  return pager.linehist[mode]
-
-proc setLineEdit(pager: Pager, prompt: string, mode: LineMode, current = "", hide = false) =
-  pager.lineedit = some(readLine(prompt, pager.attrs.width, current = current, term = pager.term, hide = hide, hist = pager.getLineHist(mode)))
-  pager.linemode = mode
-
-proc clearLineEdit(pager: Pager) =
-  pager.lineedit = none(LineEdit)
-
-proc searchForward(pager: Pager) {.jsfunc.} =
-  pager.setLineEdit("/", SEARCH_F)
-
-proc searchBackward(pager: Pager) {.jsfunc.} =
-  pager.setLineEdit("?", SEARCH_B)
-
-proc isearchForward(pager: Pager) {.jsfunc.} =
-  pager.container.pushCursorPos()
-  pager.isearchpromise = newResolvedPromise()
-  pager.setLineEdit("/", ISEARCH_F)
-
-proc isearchBackward(pager: Pager) {.jsfunc.} =
-  pager.container.pushCursorPos()
-  pager.isearchpromise = newResolvedPromise()
-  pager.setLineEdit("?", ISEARCH_B)
-
-proc gotoLine[T: string|int](pager: Pager, s: T = "") {.jsfunc.} =
-  when s is string:
-    if s == "":
-      pager.setLineEdit("Goto line: ", GOTO_LINE)
-      return
-  pager.container.gotoLine(s)
-
-proc alert*(pager: Pager, msg: string)
-
-proc newPager*(config: Config, attrs: WindowAttributes,
-    forkserver: ForkServer, mainproc: Pid, ctx: JSContext): Pager =
-  let pager = Pager(
-    config: config,
-    display: newFixedGrid(attrs.width, attrs.height - 1),
-    forkserver: forkserver,
-    mainproc: mainproc,
-    omnirules: config.getOmniRules(ctx),
-    proxy: config.getProxy(),
-    siteconf: config.getSiteConfig(ctx),
-    statusgrid: newFixedGrid(attrs.width),
-    term: newTerminal(stdout, config, attrs),
-    mimeTypes: config.getMimeTypes()
-  )
-  let (mcap, errs) = config.getMailcap()
-  pager.mailcap = mcap
-  for err in errs:
-    pager.alert("Error reading mailcap: " & err)
-  return pager
-
-proc launchPager*(pager: Pager, tty: File) =
-  pager.tty = tty
-  pager.term.start(tty)
-
-proc dumpAlerts*(pager: Pager) =
-  for msg in pager.alerts:
-    stderr.write("cha: " & msg & '\n')
-
-proc quit*(pager: Pager, code = 0) =
-  pager.term.quit()
-  pager.dumpAlerts()
-
-proc clearDisplay(pager: Pager) =
-  pager.display = newFixedGrid(pager.display.width, pager.display.height)
-
-proc buffer(pager: Pager): Container {.jsfget, inline.} = pager.container
-
-proc refreshDisplay(pager: Pager, container = pager.container) =
-  pager.clearDisplay()
-  container.drawLines(pager.display,
-    cellColor(pager.config.display.highlight_color))
-
-# Note: this function doesn't work if start < i of last written char
-proc writeStatusMessage(pager: Pager, str: string,
-                        format: Format = newFormat(), start = 0,
-                        maxwidth = -1, clip = '$'): int {.discardable.} =
-  var maxwidth = maxwidth
-  if maxwidth == -1:
-    maxwidth = pager.statusgrid.len
-  var i = start
-  let e = min(start + maxwidth, pager.statusgrid.width)
-  if i >= e:
-    return i
-  for r in str.runes:
-    let pi = i
-    i += r.twidth(i)
-    if i >= e:
-      if i >= pager.statusgrid.width:
-        i = pi
-      pager.statusgrid[i].format = format
-      pager.statusgrid[i].str = $clip
-      inc i
-      break
-    if r.isControlChar():
-      pager.statusgrid[pi].str = "^" & getControlLetter(char(r))
-    else:
-      pager.statusgrid[pi].str = $r
-    pager.statusgrid[pi].format = format
-  result = i
-  var def = newFormat()
-  while i < e:
-    pager.statusgrid[i].str = ""
-    pager.statusgrid[i].format = def
-    inc i
-
-# Note: should only be called directly after user interaction.
-proc refreshStatusMsg*(pager: Pager) =
-  let container = pager.container
-  if container == nil: return
-  if pager.tty == nil: return
-  if pager.askpromise != nil: return
-  if container.loadinfo != "":
-    pager.alerton = false
-    pager.writeStatusMessage(container.loadinfo)
-  elif pager.alerts.len > 0:
-    pager.alerton = true
-    pager.writeStatusMessage(pager.alerts[0])
-    pager.alerts.delete(0)
-  else:
-    var format = newFormat()
-    format.reverse = true
-    pager.alerton = false
-    container.clearHover()
-    var msg = $(container.cursory + 1) & "/" & $container.numLines & " (" &
-              $container.atPercentOf() & "%)"
-    let mw = pager.writeStatusMessage(msg, format)
-    let title = " <" & container.getTitle() & ">"
-    let hover = container.getHoverText()
-    if hover.len == 0:
-      pager.writeStatusMessage(title, format, mw)
-    else:
-      let hover2 = " " & hover
-      let maxwidth = pager.statusgrid.width - hover2.width() - mw
-      let tw = pager.writeStatusMessage(title, format, mw, maxwidth, '>')
-      pager.writeStatusMessage(hover2, format, tw)
-
-# Call refreshStatusMsg if no alert is being displayed on the screen.
-proc showAlerts*(pager: Pager) =
-  if not pager.alerton:
-    pager.refreshStatusMsg()
-
-proc drawBuffer*(pager: Pager, container: Container, ostream: Stream) =
-  var format = newFormat()
-  container.readLines(proc(line: SimpleFlexibleLine) =
-    if line.formats.len == 0:
-      ostream.write(line.str & "\n")
-    else:
-      var x = 0
-      var w = 0
-      var i = 0
-      var s = ""
-      for f in line.formats:
-        var outstr = ""
-        while x < f.pos:
-          var r: Rune
-          fastRuneAt(line.str, i, r)
-          outstr &= r
-          x += r.width()
-        s &= pager.term.processOutputString(outstr, w)
-        s &= pager.term.processFormat(format, f.format)
-      if i < line.str.len:
-        s &= pager.term.processOutputString(line.str.substr(i), w)
-      s &= pager.term.processFormat(format, newFormat()) & "\n"
-      ostream.write(s))
-  ostream.flush()
-
-proc redraw(pager: Pager) {.jsfunc.} =
-  pager.redraw = true
-  pager.term.clearCanvas()
-
-proc draw*(pager: Pager) =
-  let container = pager.container
-  if container == nil: return
-  pager.term.hideCursor()
-  if pager.redraw:
-    pager.refreshDisplay()
-    pager.term.writeGrid(pager.display)
-  if container.select.open and container.select.redraw:
-    container.select.drawSelect(pager.display)
-    pager.term.writeGrid(pager.display)
-  if pager.askpromise != nil:
-    discard
-  elif pager.lineedit.isSome:
-    if pager.lineedit.get.isnew:
-      #TODO hack
-      # make term notice that it must redraw when status is restored
-      let x = newFixedGrid(pager.attrs.width)
-      pager.term.writeGrid(x, 0, pager.attrs.height - 1)
-  else:
-    pager.term.writeGrid(pager.statusgrid, 0, pager.attrs.height - 1)
-  pager.term.outputGrid()
-  if pager.askpromise != nil:
-    pager.term.setCursor(pager.askcursor, pager.attrs.height - 1)
-  elif pager.lineedit.isSome:
-    if pager.lineedit.get.isnew:
-      #TODO hack
-      pager.term.setCursor(0, pager.attrs.height - 1)
-      pager.lineedit.get.drawPrompt()
-      pager.term.setCursor(pager.lineedit.get.getCursorX(), pager.attrs.height - 1)
-      pager.lineedit.get.fullRedraw()
-      pager.lineedit.get.isnew = false
-    pager.term.setCursor(pager.lineedit.get.getCursorX(), pager.attrs.height - 1)
-  elif container.select.open:
-    pager.term.setCursor(container.select.getCursorX(),
-      container.select.getCursorY())
-  else:
-    pager.term.setCursor(pager.container.acursorx, pager.container.acursory)
-  pager.term.showCursor()
-  pager.term.flush()
-  pager.redraw = false
-
-proc writeAskPrompt(pager: Pager) =
-  let yn = " (y/n)"
-  let maxwidth = pager.statusgrid.width - yn.len
-  let i = pager.writeStatusMessage(pager.askprompt, maxwidth = maxwidth)
-  pager.askcursor = pager.writeStatusMessage(yn, start = i)
-  pager.term.writeGrid(pager.statusgrid, 0, pager.attrs.height - 1)
-
-proc ask(pager: Pager, prompt: string): Promise[bool] {.jsfunc.} =
-  pager.askprompt = prompt
-  pager.writeAskPrompt()
-  pager.askpromise = Promise[bool]()
-  return pager.askpromise
-
-proc fulfillAsk*(pager: Pager, y: bool) =
-  pager.askpromise.resolve(y)
-  pager.askpromise = nil
-  pager.askprompt = ""
-
-proc registerContainer*(pager: Pager, container: Container) =
-  pager.procmap[container.process] = container
-
-proc addContainer*(pager: Pager, container: Container) =
-  container.parent = pager.container
-  if pager.container != nil:
-    pager.container.children.insert(container, 0)
-  pager.registerContainer(container)
-  pager.setContainer(container)
-
-proc newBuffer(pager: Pager, bufferConfig: BufferConfig, source: BufferSource,
-    title = "", redirectdepth = 0): Container =
-  return newBuffer(
-    pager.forkserver,
-    pager.mainproc,
-    bufferConfig,
-    source,
-    title,
-    redirectdepth
-  )
-
-proc dupeBuffer(pager: Pager, container: Container, location: URL,
-    contentType = ""): Container =
-  let contentType = if contentType != "":
-    some(contentType)
-  else:
-    container.contenttype
-  let location = if location != nil:
-    location
-  else:
-    container.source.location
-  let source = BufferSource(
-    t: CLONE,
-    location: location,
-    contenttype: contentType,
-    clonepid: container.process,
-  )
-  let pipeTo = pager.newBuffer(container.config, source, container.title)
-  container.pipeBuffer(pipeTo)
-  return pipeTo
-
-proc dupeBuffer(pager: Pager, location: URL = nil) {.jsfunc.} =
-  pager.addContainer(pager.dupeBuffer(pager.container, location))
-
-# The prevBuffer and nextBuffer procedures emulate w3m's PREV and NEXT
-# commands by traversing the container tree in a depth-first order.
-proc prevBuffer(pager: Pager): bool {.jsfunc.} =
-  if pager.container == nil:
-    return false
-  if pager.container.parent == nil:
-    return false
-  let n = pager.container.parent.children.find(pager.container)
-  assert n != -1, "Container not a child of its parent"
-  if n > 0:
-    var container = pager.container.parent.children[n - 1]
-    while container.children.len > 0:
-      container = container.children[^1]
-    pager.setContainer(container)
-  else:
-    pager.setContainer(pager.container.parent)
-  return true
-
-proc nextBuffer(pager: Pager): bool {.jsfunc.} =
-  if pager.container == nil:
-    return false
-  if pager.container.children.len > 0:
-    pager.setContainer(pager.container.children[0])
-    return true
-  var container = pager.container
-  while container.parent != nil:
-    let n = container.parent.children.find(container)
-    assert n != -1, "Container not a child of its parent"
-    if n < container.parent.children.high:
-      pager.setContainer(container.parent.children[n + 1])
-      return true
-    container = container.parent
-  return false
-
-proc parentBuffer(pager: Pager): bool {.jsfunc.} =
-  if pager.container == nil:
-    return false
-  if pager.container.parent == nil:
-    return false
-  pager.setContainer(pager.container.parent)
-  return true
-
-proc prevSiblingBuffer(pager: Pager): bool {.jsfunc.} =
-  if pager.container == nil:
-    return false
-  if pager.container.parent == nil:
-    return false
-  var n = pager.container.parent.children.find(pager.container)
-  assert n != -1, "Container not a child of its parent"
-  if n == 0:
-    n = pager.container.parent.children.len
-  pager.setContainer(pager.container.parent.children[n - 1])
-  return true
-
-proc nextSiblingBuffer(pager: Pager): bool {.jsfunc.} =
-  if pager.container == nil:
-    return false
-  if pager.container.parent == nil:
-    return false
-  var n = pager.container.parent.children.find(pager.container)
-  assert n != -1, "Container not a child of its parent"
-  if n == pager.container.parent.children.high:
-    n = -1
-  pager.setContainer(pager.container.parent.children[n + 1])
-  return true
-
-proc alert*(pager: Pager, msg: string) {.jsfunc.} =
-  pager.alerts.add(msg)
-
-proc deleteContainer(pager: Pager, container: Container) =
-  container.cancel()
-  if container.sourcepair != nil:
-    container.sourcepair.sourcepair = nil
-    container.sourcepair = nil
-  if container.parent != nil:
-    let parent = container.parent
-    let n = parent.children.find(container)
-    assert n != -1, "Container not a child of its parent"
-    for i in countdown(container.children.high, 0):
-      let child = container.children[i]
-      child.parent = container.parent
-      parent.children.insert(child, n + 1)
-    parent.children.delete(n)
-    if container == pager.container:
-      if n == 0:
-        pager.setContainer(parent)
-      else:
-        pager.setContainer(parent.children[n - 1])
-  elif container.children.len > 0:
-    let parent = container.children[0]
-    parent.parent = nil
-    for i in 1..container.children.high:
-      container.children[i].parent = parent
-      parent.children.add(container.children[i])
-    if container == pager.container:
-      pager.setContainer(parent)
-  else:
-    for child in container.children:
-      child.parent = nil
-    if container == pager.container:
-      pager.setContainer(nil)
-  container.parent = nil
-  container.children.setLen(0)
-  pager.unreg.add((container.process, SocketStream(container.iface.stream)))
-  pager.forkserver.removeChild(container.process)
-
-proc discardBuffer(pager: Pager, container = none(Container)) {.jsfunc.} =
-  let c = container.get(pager.container)
-  if c == nil or c.parent == nil and c.children.len == 0:
-    pager.alert("Cannot discard last buffer!")
-  else:
-    pager.deleteContainer(c)
-
-proc discardTree(pager: Pager, container = none(Container)) {.jsfunc.} =
-  let container = container.get(pager.container)
-  if container != nil:
-    for c in container.all_children:
-      pager.deleteContainer(c)
-  else:
-    pager.alert("Buffer has no children!")
-
-proc toggleSource(pager: Pager) {.jsfunc.} =
-  if pager.container.sourcepair != nil:
-    pager.setContainer(pager.container.sourcepair)
-  else:
-    let contenttype = if pager.container.contentType.get("") == "text/html":
-      "text/plain"
-    else:
-      "text/html"
-    let container = pager.dupeBuffer(pager.container, nil, contenttype)
-    container.sourcepair = pager.container
-    pager.container.sourcepair = container
-    pager.addContainer(container)
-
-proc windowChange*(pager: Pager, attrs: WindowAttributes) =
-  pager.term.windowChange(attrs)
-  pager.display = newFixedGrid(attrs.width, attrs.height - 1)
-  pager.statusgrid = newFixedGrid(attrs.width)
-  for container in pager.containers:
-    container.windowChange(attrs)
-  if pager.askprompt != "":
-    pager.writeAskPrompt()
-  pager.showAlerts()
-
-# Apply siteconf settings to a request.
-# Note that this may modify the URL passed.
-proc applySiteconf(pager: Pager, url: var URL): BufferConfig =
-  let host = url.host
-  var referer_from: bool
-  var cookiejar: CookieJar
-  var headers = pager.config.getDefaultHeaders()
-  var scripting: bool
-  var images: bool
-  var charsets = pager.config.encoding.document_charset
-  var userstyle = pager.config.css.stylesheet
-  var proxy = pager.proxy
-  let mimeTypes = pager.mimeTypes
-  for sc in pager.siteconf:
-    if sc.url.isSome and not sc.url.get.match($url):
-      continue
-    elif sc.host.isSome and not sc.host.get.match(host):
-      continue
-    if sc.rewrite_url != nil:
-      let s = sc.rewrite_url(url)
-      if s.isSome and s.get != nil:
-        url = s.get
-    if sc.cookie.isSome:
-      if sc.cookie.get:
-        # host/url might have changed by now
-        let jarid = sc.share_cookiejar.get(url.host)
-        if jarid notin pager.cookiejars:
-          pager.cookiejars[jarid] = newCookieJar(url,
-            sc.third_party_cookie)
-        cookiejar = pager.cookiejars[jarid]
-      else:
-        cookiejar = nil # override
-    if sc.scripting.isSome:
-      scripting = sc.scripting.get
-    if sc.referer_from.isSome:
-      referer_from = sc.referer_from.get
-    if sc.document_charset.len > 0:
-      charsets = sc.document_charset
-    if sc.images.isSome:
-      images = sc.images.get
-    if sc.stylesheet.isSome:
-      userstyle &= "\n"
-      userstyle &= sc.stylesheet.get
-    if sc.proxy.isSome:
-      proxy = sc.proxy.get
-  return pager.config.getBufferConfig(url, cookiejar, headers, referer_from,
-    scripting, charsets, images, userstyle, proxy, mimeTypes)
-
-# Load request in a new buffer.
-proc gotoURL(pager: Pager, request: Request, prevurl = none(URL),
-    ctype = none(string), cs = CHARSET_UNKNOWN, replace: Container = nil,
-    redirectdepth = 0, referrer: Container = nil) =
-  if referrer != nil and referrer.config.referer_from:
-    request.referer = referrer.source.location
-  var bufferconfig = pager.applySiteconf(request.url)
-  if prevurl.isnone or not prevurl.get.equals(request.url, true) or
-      request.url.hash == "" or request.httpmethod != HTTP_GET:
-    # Basically, we want to reload the page *only* when
-    # a) we force a reload (by setting prevurl to none)
-    # b) or the new URL isn't just the old URL + an anchor
-    # I think this makes navigation pretty natural, or at least very close to
-    # what other browsers do. Still, it would be nice if we got some visual
-    # feedback on what is actually going to happen when typing a URL; TODO.
-    let source = BufferSource(
-      t: LOAD_REQUEST,
-      request: request,
-      contenttype: ctype,
-      charset: cs,
-      location: request.url
-    )
-    if referrer != nil:
-      bufferconfig.referrerpolicy = referrer.config.referrerpolicy
-    let container = pager.newBuffer(
-      bufferconfig,
-      source,
-      redirectdepth = redirectdepth
-    )
-    if replace != nil:
-      container.replace = replace
-      container.copyCursorPos(container.replace)
-    pager.addContainer(container)
-    inc pager.numload
-  else:
-    pager.container.findAnchor(request.url.anchor)
-
-proc omniRewrite(pager: Pager, s: string): string =
-  for rule in pager.omnirules:
-    if rule.match.match(s):
-      let sub = rule.substitute_url(s)
-      if sub.isSome:
-        return sub.get
-      else:
-        pager.alert("Error in substitution of rule " & rule.match.buf & " for " & s)
-  return s
-
-# 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.
-proc loadURL*(pager: Pager, url: string, ctype = none(string),
-    cs = CHARSET_UNKNOWN) =
-  let url0 = pager.omniRewrite(url)
-  let url = if url[0] == '~': expandPath(url0) else: url0
-  let firstparse = parseURL(url)
-  if firstparse.issome:
-    let prev = if pager.container != nil:
-      some(pager.container.source.location)
-    else:
-      none(URL)
-    pager.gotoURL(newRequest(firstparse.get), prev, ctype, cs)
-    return
-  var urls: seq[URL]
-  if pager.config.network.prepend_https and url[0] != '/':
-    let pageurl = parseURL("https://" & url)
-    if pageurl.isSome: # attempt to load remote page
-      urls.add(pageurl.get)
-  let cdir = parseURL("file://" & percentEncode(getCurrentDir(), LocalPathPercentEncodeSet) & DirSep)
-  let localurl = percentEncode(url, LocalPathPercentEncodeSet)
-  let newurl = parseURL(localurl, cdir)
-  if newurl.isSome:
-    urls.add(newurl.get) # attempt to load local file
-  if urls.len == 0:
-    pager.alert("Invalid URL " & url)
-  else:
-    let prevc = pager.container
-    pager.gotoURL(newRequest(urls.pop()), ctype = ctype, cs = cs)
-    if pager.container != prevc:
-      pager.container.retry = urls
-
-proc readPipe0*(pager: Pager, ctype: Option[string], cs: Charset,
-    fd: FileHandle, location: Option[URL], title: string): Container =
-  var location = location.get(newURL("file://-").get)
-  let bufferconfig = pager.applySiteconf(location)
-  let source = BufferSource(
-    t: LOAD_PIPE,
-    fd: fd,
-    contenttype: some(ctype.get("text/plain")),
-    charset: cs,
-    location: location
-  )
-  return pager.newBuffer(bufferconfig, source, title = title)
-
-proc readPipe*(pager: Pager, ctype: Option[string], cs: Charset,
-    fd: FileHandle) =
-  let container = pager.readPipe0(ctype, cs, fd, none(URL), "*pipe*")
-  pager.addContainer(container)
-
-proc command(pager: Pager) {.jsfunc.} =
-  pager.setLineEdit("COMMAND: ", COMMAND)
-
-proc commandMode(pager: Pager, val: bool) {.jsfset.} =
-  pager.commandMode = val
-  if val:
-    pager.command()
-
-proc checkRegex(pager: Pager, regex: Result[Regex, string]): Opt[Regex] =
-  if regex.isErr:
-    pager.alert("Invalid regex: " & regex.error)
-    return err()
-  return ok(regex.get)
-
-proc updateReadLineISearch(pager: Pager, linemode: LineMode) =
-  let lineedit = pager.lineedit.get
-  pager.isearchpromise = pager.isearchpromise.then(proc(): EmptyPromise =
-    case lineedit.state
-    of CANCEL:
-      pager.iregex.err()
-      pager.container.popCursorPos()
-      pager.container.clearSearchHighlights()
-      pager.redraw = true
-      pager.isearchpromise = nil
-    of EDIT:
-      let x = $lineedit.news
-      if x != "": pager.iregex = compileSearchRegex(x)
-      pager.container.popCursorPos(true)
-      pager.container.pushCursorPos()
-      if pager.iregex.isSome:
-        pager.container.hlon = true
-        let wrap = pager.config.search.wrap
-        return if linemode == ISEARCH_F:
-          pager.container.cursorNextMatch(pager.iregex.get, wrap, false)
-        else:
-          pager.container.cursorPrevMatch(pager.iregex.get, wrap, false)
-    of FINISH:
-      pager.regex = pager.checkRegex(pager.iregex)
-      pager.reverseSearch = linemode == ISEARCH_B
-      pager.container.clearSearchHighlights()
-      pager.container.sendCursorPosition()
-      pager.redraw = true
-      pager.isearchpromise = nil
-  )
-
-proc updateReadLine*(pager: Pager) =
-  let lineedit = pager.lineedit.get
-  template s: string = $lineedit.news
-  if pager.linemode in {ISEARCH_F, ISEARCH_B}:
-    pager.updateReadLineISearch(pager.linemode)
-  else:
-    case lineedit.state
-    of EDIT: return
-    of FINISH:
-      case pager.linemode
-      of LOCATION: pager.loadURL(s)
-      of USERNAME:
-        pager.username = s
-        pager.setLineEdit("Password: ", PASSWORD, hide = true)
-      of PASSWORD:
-        let url = newURL(pager.container.source.location)
-        url.username = pager.username
-        url.password = s
-        pager.username = ""
-        pager.gotoURL(newRequest(url), some(pager.container.source.location), replace = pager.container, referrer = pager.container)
-      of COMMAND:
-        pager.scommand = s
-        if pager.commandMode:
-          pager.command()
-      of BUFFER: pager.container.readSuccess(s)
-      of SEARCH_F, SEARCH_B:
-        let x = s
-        if x != "":
-          pager.regex = pager.checkRegex(compileSearchRegex(x))
-        pager.reverseSearch = pager.linemode == SEARCH_B
-        pager.searchNext()
-      of GOTO_LINE:
-        pager.container.gotoLine(s)
-      else: discard
-    of CANCEL:
-      case pager.linemode
-      of USERNAME: pager.discardBuffer()
-      of PASSWORD:
-        pager.username = ""
-        pager.discardBuffer()
-      of BUFFER: pager.container.readCanceled()
-      of COMMAND: pager.commandMode = false
-      else: discard
-  if lineedit.state in {CANCEL, FINISH}:
-    if pager.lineedit.get == lineedit:
-      pager.clearLineEdit()
-
-# Open a URL prompt and visit the specified URL.
-proc load(pager: Pager, s = "") {.jsfunc.} =
-  if s.len > 0 and s[^1] == '\n':
-    pager.loadURL(s[0..^2])
-  else:
-    var url = s
-    if url == "":
-      url = pager.container.source.location.serialize()
-    pager.setLineEdit("URL: ", LOCATION, url)
-
-# Reload the page in a new buffer, then kill the previous buffer.
-proc reload(pager: Pager) {.jsfunc.} =
-  pager.gotoURL(newRequest(pager.container.source.location), none(URL),
-    pager.container.contenttype, replace = pager.container)
-
-proc setEnvVars(pager: Pager) {.jsfunc.} =
-  try:
-    putEnv("CHA_URL", $pager.container.location)
-    putEnv("CHA_CHARSET", $pager.container.charset)
-  except OSError:
-    pager.alert("Warning: failed to set some environment variables")
-
-#TODO use default values instead...
-type ExternDict = object of JSDict
-  setenv: Opt[bool]
-  suspend: Opt[bool]
-  wait: bool
-
-#TODO this could be handled much better.
-# * suspend, setenv, wait as dict flags
-# * retval as int?
-proc extern(pager: Pager, cmd: string, t = ExternDict()): bool {.jsfunc.} =
-  if t.setenv.get(true):
-    pager.setEnvVars()
-  if t.suspend.get(true):
-    return runProcess(pager.term, cmd, t.wait)
-  else:
-    return runProcess(cmd)
-
-proc authorize(pager: Pager) =
-  pager.setLineEdit("Username: ", USERNAME)
-
-# Pipe input into the mailcap command, then read its output into a buffer.
-# needsterminal is ignored.
-proc runMailcapReadPipe(pager: Pager, container: Container,
-    entry: MailcapEntry, cmd: string): (EmptyPromise, bool) =
-  var pipefd_in: array[2, cint]
-  if pipe(pipefd_in) == -1:
-    raise newException(Defect, "Failed to open pipe.")
-  var pipefd_out: array[2, cint]
-  if pipe(pipefd_out) == -1:
-    raise newException(Defect, "Failed to open pipe.")
-  let pid = fork()
-  if pid == -1:
-    return (nil, false)
-  elif pid == 0:
-    # child process
-    discard close(pipefd_in[1])
-    discard close(pipefd_out[0])
-    stdout.flushFile()
-    discard dup2(pipefd_in[0], stdin.getFileHandle())
-    discard dup2(pipefd_out[1], stdout.getFileHandle())
-    let devnull = open("/dev/null", O_WRONLY)
-    discard dup2(devnull, stderr.getFileHandle())
-    discard close(devnull)
-    discard close(pipefd_in[0])
-    discard close(pipefd_out[1])
-    discard execCmd(cmd)
-    discard close(stdin.getFileHandle())
-    discard close(stdout.getFileHandle())
-    quit(0)
-  # parent
-  discard close(pipefd_in[0])
-  discard close(pipefd_out[1])
-  let fdin = pipefd_in[1]
-  let fdout = pipefd_out[0]
-  let p = container.redirectToFd(fdin, wait = false)
-  let p2 = p.then(proc(): auto =
-    discard close(fdin)
-    let ishtml = HTMLOUTPUT in entry.flags
-    if ishtml:
-      #TODO this is a hack for dupe buffer and should be reconsidered.
-      container.source.contenttype = some("text/html")
-    return container.readFromFd(fdout, ishtml)
-  ).then(proc() =
-    discard close(fdout)
-  )
-  return (p2, true)
-
-# Pipe input into the mailcap command, and discard its output.
-# If needsterminal, leave stderr and stdout open and wait for the process.
-proc runMailcapWritePipe(pager: Pager, container: Container,
-    entry: MailcapEntry, cmd: string): (EmptyPromise, bool) =
-  let needsterminal = NEEDSTERMINAL in entry.flags
-  var pipefd: array[2, cint]
-  if pipe(pipefd) == -1:
-    raise newException(Defect, "Failed to open pipe.")
-  if needsterminal:
-    pager.term.quit()
-  let pid = fork()
-  if pid == -1:
-    return (nil, false)
-  elif pid == 0:
-    # child process
-    discard close(pipefd[1])
-    discard dup2(pipefd[0], stdin.getFileHandle())
-    if not needsterminal:
-      let devnull = open("/dev/null", O_WRONLY)
-      discard dup2(devnull, stdout.getFileHandle())
-      discard dup2(devnull, stderr.getFileHandle())
-      discard close(devnull)
-    discard close(pipefd[0])
-    discard execCmd(cmd)
-    discard close(stdin.getFileHandle())
-    quit(0)
-  else:
-    # parent
-    discard close(pipefd[0])
-    let fd = pipefd[1]
-    let p = container.redirectToFd(fd, wait = false)
-    discard close(fd)
-    if needsterminal:
-      var x: cint
-      discard waitpid(pid, x, 0)
-      pager.term.restart()
-    return (p, false)
-
-# Save input in a file, run the command, and redirect its output to a
-# new buffer.
-# needsterminal is ignored.
-proc runMailcapReadFile(pager: Pager, container: Container,
-    entry: MailcapEntry, cmd, outpath: string): (EmptyPromise, bool) =
-  let fd = open(outpath, O_WRONLY or O_CREAT, 0o666)
-  if fd == -1:
-    return (nil, false)
-  let p = container.redirectToFd(fd, wait = true).then(proc(): auto =
-    var pipefd: array[2, cint] # redirect stdout here
-    if pipe(pipefd) == -1:
-      raise newException(Defect, "Failed to open pipe.")
-    let pid = fork()
-    if pid == 0:
-      # child process
-      discard close(pipefd[0])
-      discard dup2(pipefd[1], stdout.getFileHandle())
-      discard close(pipefd[1])
-      let devnull = open("/dev/null", O_WRONLY)
-      discard dup2(devnull, stderr.getFileHandle())
-      discard close(devnull)
-      discard execCmd(cmd)
-      discard tryRemoveFile(outpath)
-      quit(0)
-    # parent
-    discard close(pipefd[1])
-    let fdout = pipefd[0]
-    let ishtml = HTMLOUTPUT in entry.flags
-    if ishtml:
-      #TODO this is a hack for dupe buffer and should be reconsidered.
-      container.source.contenttype = some("text/html")
-    return container.readFromFd(fdout, ishtml).then(proc() =
-      discard close(fdout)
-    )
-  )
-  return (p, true)
-
-# Save input in a file, run the command, and discard its output.
-# If needsterminal, leave stderr and stdout open and wait for the process.
-proc runMailcapWriteFile(pager: Pager, container: Container,
-    entry: MailcapEntry, cmd, outpath: string): (EmptyPromise, bool) =
-  let needsterminal = NEEDSTERMINAL in entry.flags
-  let fd = open(outpath, O_WRONLY or O_CREAT, 0o666)
-  if fd == -1:
-    return (nil, false)
-  let p = container.redirectToFd(fd, wait = true).then(proc() =
-    if needsterminal:
-      pager.term.quit()
-      discard execCmd(cmd)
-      discard tryRemoveFile(outpath)
-      pager.term.restart()
-    else:
-      # don't block
-      let pid = fork()
-      if pid == 0:
-        # child process
-        let devnull = open("/dev/null", O_WRONLY)
-        discard dup2(devnull, stdin.getFileHandle())
-        discard dup2(devnull, stdout.getFileHandle())
-        discard dup2(devnull, stderr.getFileHandle())
-        discard close(devnull)
-        discard execCmd(cmd)
-        discard tryRemoveFile(outpath)
-        quit(0)
-  )
-  return (p, false)
-
-# Search for a mailcap entry, and if found, execute the specified command
-# and pipeline the input and output appropriately.
-# There is four possible outcomes:
-# * pipe stdin, discard stdout
-# * pipe stdin, read stdout
-# * write to file, run, discard stdout
-# * write to file, run, read stdout
-# If needsterminal is specified, and stdout is not being read, then the
-# pager is suspended until the command exits.
-#TODO add support for edit/compose, better error handling (use Promise[bool]
-# instead of tuple[EmptyPromise, bool])
-proc checkMailcap(pager: Pager, container: Container): (EmptyPromise, bool) =
-  if container.contenttype.isNone:
-    return (nil, true)
-  if container.source.t == CLONE:
-    return (nil, true) # clone cannot use mailcap
-  let contentType = container.contenttype.get
-  if contentType == "text/html":
-    # We support HTML natively, so it would make little sense to execute
-    # mailcap filters for it.
-    return (nil, true)
-  elif contentType == "text/plain":
-    # This could potentially be useful. Unfortunately, many mailcaps include
-    # a text/plain entry with less by default, so it's probably better to
-    # ignore this.
-    return (nil, true)
-  #TODO callback for outpath or something
-  let url = container.location
-  let cs = container.source.charset
-  let entry = pager.mailcap.getMailcapEntry(contentType, "", url, cs)
-  if entry != nil:
-    let tmpdir = pager.config.external.tmpdir
-    let ext = container.location.pathname.afterLast('.')
-    let tempfile = getTempfile(tmpdir, ext)
-    let outpath = if entry.nametemplate != "":
-      unquoteCommand(entry.nametemplate, contentType, tempfile, url, cs)
-    else:
-      tempfile
-    var canpipe = true
-    let cmd = unquoteCommand(entry.cmd, contentType, outpath, url, cs, canpipe)
-    if {COPIOUSOUTPUT, HTMLOUTPUT} * entry.flags == {}:
-      # no output.
-      if canpipe:
-        return pager.runMailcapWritePipe(container, entry[], cmd)
-      else:
-        return pager.runMailcapWriteFile(container, entry[], cmd, outpath)
-    else:
-      if canpipe:
-        return pager.runMailcapReadPipe(container, entry[], cmd)
-      else:
-        return pager.runMailcapReadFile(container, entry[], cmd, outpath)
-  return (nil, true)
-
-proc handleEvent0(pager: Pager, container: Container, event: ContainerEvent): bool =
-  case event.t
-  of FAIL:
-    dec pager.numload
-    pager.deleteContainer(container)
-    if container.retry.len > 0:
-      pager.gotoURL(newRequest(container.retry.pop()), ctype = container.contenttype)
-    else:
-      let errorMessage = getLoaderErrorMessage(container.code)
-      pager.alert("Can't load " & $container.source.location & " (" &
-        errorMessage & ")")
-    return false
-  of SUCCESS:
-    if container.replace != nil:
-      let n = container.replace.children.find(container)
-      if n != -1:
-        container.replace.children.delete(n)
-        container.parent = nil
-      let n2 = container.children.find(container.replace)
-      if n2 != -1:
-        container.children.delete(n2)
-        container.replace.parent = nil
-      container.children.add(container.replace.children)
-      for child in container.children:
-        child.parent = container
-      container.replace.children.setLen(0)
-      if container.replace.parent != nil:
-        container.parent = container.replace.parent
-        let n = container.replace.parent.children.find(container.replace)
-        assert n != -1, "Container not a child of its parent"
-        container.parent.children[n] = container
-        container.replace.parent = nil
-      if pager.container == container.replace:
-        pager.setContainer(container)
-      pager.deleteContainer(container.replace)
-      container.replace = nil
-  of LOADED:
-    dec pager.numload
-  of NEEDS_AUTH:
-    if pager.container == container:
-      pager.authorize()
-  of REDIRECT:
-    if container.redirectdepth < pager.config.network.max_redirect:
-      pager.alert("Redirecting to " & $event.request.url)
-      pager.gotoURL(event.request, some(container.source.location),
-        replace = container, redirectdepth = container.redirectdepth + 1,
-        referrer = pager.container)
-    else:
-      pager.alert("Error: maximum redirection depth reached")
-      pager.deleteContainer(container)
-      return false
-  of ANCHOR:
-    var url2 = newURL(container.source.location)
-    url2.setHash(event.anchor)
-    pager.addContainer(pager.dupeBuffer(container, url2))
-  of NO_ANCHOR:
-    pager.alert("Couldn't find anchor " & event.anchor)
-  of UPDATE:
-    if container == pager.container:
-      pager.redraw = true
-      if event.force: pager.term.clearCanvas()
-  of READ_LINE:
-    if container == pager.container:
-      pager.setLineEdit("(BUFFER) " & event.prompt, BUFFER, event.value, hide = event.password)
-  of READ_AREA:
-    if container == pager.container:
-      var s = event.tvalue
-      if openInEditor(pager.term, pager.config, s):
-        pager.container.readSuccess(s)
-      else:
-        pager.container.readCanceled()
-      pager.redraw = true
-  of OPEN:
-    if pager.container == nil or not pager.container.isHoverURL(event.request.url):
-      pager.ask("Open pop-up? " & $event.request.url).then(proc(x: bool) =
-        if x:
-          pager.gotoURL(event.request, some(container.source.location), referrer = pager.container))
-    else:
-      pager.gotoURL(event.request, some(container.source.location), referrer = pager.container)
-  of INVALID_COMMAND: discard
-  of STATUS:
-    if pager.container == container:
-      pager.showAlerts()
-  of TITLE:
-    if pager.container == container:
-      pager.showAlerts()
-      pager.term.setTitle(container.getTitle())
-  of ALERT:
-    if pager.container == container:
-      pager.alert(event.msg)
-  of CHECK_MAILCAP:
-    var (cm, connect) = pager.checkMailcap(container)
-    if cm == nil:
-      cm = container.connect2()
-    if connect:
-      cm.then(proc() =
-        container.startload())
-    else:
-      cm.then(proc(): auto =
-        container.quit())
-  of QUIT:
-    dec pager.numload
-    pager.deleteContainer(container)
-    return false
-  of NO_EVENT: discard
-  return true
-
-proc handleEvents*(pager: Pager, container: Container) =
-  while container.events.len > 0:
-    let event = container.events.popFirst()
-    if not pager.handleEvent0(container, event):
-      break
-
-proc handleEvent*(pager: Pager, container: Container) =
-  try:
-    container.handleEvent()
-    pager.handleEvents(container)
-  except IOError:
-    discard
-
-proc addPagerModule*(ctx: JSContext) =
-  ctx.registerType(Pager)