about summary refs log tree commit diff stats
path: root/src/local
diff options
context:
space:
mode:
Diffstat (limited to 'src/local')
-rw-r--r--src/local/client.nim114
-rw-r--r--src/local/container.nim383
-rw-r--r--src/local/pager.nim830
3 files changed, 720 insertions, 607 deletions
diff --git a/src/local/client.nim b/src/local/client.nim
index f8e1433d..b707fe84 100644
--- a/src/local/client.nim
+++ b/src/local/client.nim
@@ -24,6 +24,7 @@ import html/event
 import io/bufstream
 import io/posixstream
 import io/promise
+import io/serialize
 import io/socketstream
 import js/base64
 import js/console
@@ -61,7 +62,6 @@ type
     consoleWrapper: ConsoleWrapper
     fdmap: Table[int, Container]
     feednext: bool
-    forkserver: ForkServer
     ibuf: string
     jsctx: JSContext
     jsrt: JSRuntime
@@ -78,6 +78,9 @@ type
 
 jsDestructor(Client)
 
+func forkserver(client: Client): ForkServer {.inline.} =
+  client.pager.forkserver
+
 func console(client: Client): Console {.jsfget.} =
   return client.consoleWrapper.console
 
@@ -454,31 +457,51 @@ proc consoleBuffer(client: Client): Container {.jsfget.} =
   return client.consoleWrapper.container
 
 proc acceptBuffers(client: Client) =
-  while client.pager.unreg.len > 0:
-    let (pid, stream) = client.pager.unreg.pop()
+  let pager = client.pager
+  while pager.unreg.len > 0:
+    let (pid, stream) = pager.unreg.pop()
     let fd = int(stream.fd)
     if fd in client.fdmap:
       client.selector.unregister(fd)
       client.fdmap.del(fd)
     else:
-      client.pager.procmap.del(pid)
+      pager.procmap.del(pid)
     stream.close()
-  var accepted: seq[Pid]
   let registerFun = proc(fd: int) =
     client.selector.unregister(fd)
     client.selector.registerHandle(fd, {Read, Write}, 0)
-  for pid, container in client.pager.procmap:
-    let stream = connectSocketStream(pid, buffered = false, blocking = true)
+  for item in pager.procmap:
+    let container = item.container
+    let stream = connectSocketStream(container.process, buffered = false)
     if stream == nil:
-      client.pager.alert("Error: failed to set up buffer")
+      pager.alert("Error: failed to set up buffer")
       continue
-    container.setStream(stream, registerFun)
+    let key = pager.addLoaderClient(container.process,
+      container.config.loaderConfig)
+    stream.swrite(key)
+    let loader = pager.loader
+    if item.fdin != -1:
+      let outputId = item.istreamOutputId
+      if container.cacheId == -1:
+        container.cacheId = loader.addCacheFile(outputId, loader.clientPid)
+      var outCacheId = container.cacheId
+      let pid = container.process
+      if item.fdout == item.fdin:
+        loader.shareCachedItem(container.cacheId, pid)
+        loader.resume(@[item.istreamOutputId])
+      else:
+        outCacheId = loader.addCacheFile(item.ostreamOutputId, pid)
+        loader.resume(@[item.istreamOutputId, item.ostreamOutputId])
+      # pass down fdout
+      container.setStream(stream, registerFun, item.fdout, outCacheId)
+    else:
+      # buffer is cloned, no need to cache anything
+      container.setCloneStream(stream, registerFun)
     let fd = int(stream.fd)
     client.fdmap[fd] = container
     client.selector.registerHandle(fd, {Read}, 0)
-    client.pager.handleEvents(container)
-    accepted.add(pid)
-  client.pager.procmap.clear()
+    pager.handleEvents(container)
+  pager.procmap.setLen(0)
 
 proc c_setvbuf(f: File, buf: pointer, mode: cint, size: csize_t): cint {.
   importc: "setvbuf", header: "<stdio.h>", tags: [].}
@@ -488,6 +511,19 @@ proc handleRead(client: Client, fd: int) =
     client.input().then(proc() =
       client.handlePagerEvents()
     )
+  elif (let i = client.pager.findConnectingBuffer(fd); i != -1):
+    client.selector.unregister(fd)
+    client.loader.unregistered.add(fd)
+    let (container, stream) = client.pager.connectingBuffers[i]
+    let response = stream.readResponse(container.request)
+    if response.body == nil:
+      client.pager.fail(container, response.getErrorMessage())
+    elif response.redirect != nil:
+      client.pager.redirect(container, response)
+      response.body.close()
+    else:
+      client.pager.connected(container, response)
+    client.pager.connectingBuffers.del(i)
   elif fd == client.forkserver.estream.fd:
     const BufferSize = 4096
     const prefix = "STDERR: "
@@ -560,11 +596,18 @@ proc handleError(client: Client, fd: int) =
     client.loader.onError(fd)
   elif fd in client.loader.unregistered:
     discard # already unregistered...
+  elif (let i = client.pager.findConnectingBuffer(fd); i != -1):
+    # bleh
+    let (container, stream) = client.pager.connectingBuffers[i]
+    client.pager.fail(container, "loader died while loading")
+    client.selector.unregister(fd)
+    stream.close()
+    client.pager.connectingBuffers.del(i)
   else:
     if fd in client.fdmap:
       let container = client.fdmap[fd]
       if container != client.consoleWrapper.container:
-        client.console.log("Error in buffer", $container.location)
+        client.console.log("Error in buffer", $container.url)
       else:
         client.consoleWrapper.container = nil
       client.selector.unregister(fd)
@@ -675,7 +718,7 @@ proc writeFile(client: Client, path: string, content: string) {.jsfunc.} =
 
 const ConsoleTitle = "Browser Console"
 
-proc addConsole(pager: Pager, interactive: bool, clearFun, showFun, hideFun:
+proc addConsole(pager: Pager; interactive: bool; clearFun, showFun, hideFun:
     proc()): ConsoleWrapper =
   if interactive:
     var pipefd: array[0..1, cint]
@@ -683,25 +726,14 @@ proc addConsole(pager: Pager, interactive: bool, clearFun, showFun, hideFun:
       raise newException(Defect, "Failed to open console pipe.")
     let url = newURL("stream:console").get
     let container = pager.readPipe0("text/plain", CHARSET_UNKNOWN, pipefd[0],
-      some(url), ConsoleTitle, canreinterpret = false)
-    pager.registerContainer(container)
+      url, ConsoleTitle, canreinterpret = false)
     let err = newPosixStream(pipefd[1])
     err.writeLine("Type (M-c) console.hide() to return to buffer mode.")
-    let console = newConsole(
-      err,
-      clearFun = clearFun,
-      showFun = showFun,
-      hideFun = hideFun
-    )
-    return ConsoleWrapper(
-      console: console,
-      container: container
-    )
+    let console = newConsole(err, clearFun, showFun, hideFun)
+    return ConsoleWrapper(console: console, container: container)
   else:
-    let err = newFileStream(stderr)
-    return ConsoleWrapper(
-      console: newConsole(err)
-    )
+    let err = newPosixStream(stderr.getFileHandle())
+    return ConsoleWrapper(console: newConsole(err))
 
 proc clearConsole(client: Client) =
   var pipefd: array[0..1, cint]
@@ -710,9 +742,8 @@ proc clearConsole(client: Client) =
   let url = newURL("stream:console").get
   let pager = client.pager
   let replacement = pager.readPipe0("text/plain", CHARSET_UNKNOWN, pipefd[0],
-    some(url), ConsoleTitle, canreinterpret = false)
+    url, ConsoleTitle, canreinterpret = false)
   replacement.replace = client.consoleWrapper.container
-  pager.registerContainer(replacement)
   client.consoleWrapper.container = replacement
   let console = client.consoleWrapper.console
   console.err.close()
@@ -726,7 +757,7 @@ proc dumpBuffers(client: Client) =
       client.pager.drawBuffer(container, ostream)
       client.pager.handleEvents(container)
     except IOError:
-      client.console.log("Error in buffer", $container.location)
+      client.console.log("Error in buffer", $container.url)
       # check for errors
       client.handleRead(client.forkserver.estream.fd)
       quit(1)
@@ -846,17 +877,16 @@ proc newClient*(config: Config, forkserver: ForkServer): Client =
   JS_SetModuleLoaderFunc(jsrt, normalizeModuleName, clientLoadJSModule, nil)
   let jsctx = jsrt.newJSContext()
   let pager = newPager(config, forkserver, jsctx)
+  let loader = forkserver.newFileLoader(LoaderConfig(
+    urimethodmap: config.getURIMethodMap(),
+    w3mCGICompat: config.external.w3m_cgi_compat,
+    cgiDir: pager.cgiDir,
+    tmpdir: pager.tmpdir
+  ))
+  pager.setLoader(loader)
   let client = Client(
     config: config,
-    forkserver: forkserver,
-    loader: forkserver.newFileLoader(
-      defaultHeaders = config.getDefaultHeaders(),
-      proxy = config.getProxy(),
-      urimethodmap = config.getURIMethodMap(),
-      cgiDir = pager.cgiDir,
-      acceptProxy = true,
-      w3mCGICompat = config.external.w3m_cgi_compat
-    ),
+    loader: loader,
     jsrt: jsrt,
     jsctx: jsctx,
     pager: pager
diff --git a/src/local/container.nim b/src/local/container.nim
index 88848ac7..d7ee5c64 100644
--- a/src/local/container.nim
+++ b/src/local/container.nim
@@ -7,22 +7,21 @@ when defined(posix):
 
 import config/config
 import display/term
-import extern/stdio
 import io/promise
 import io/serialize
 import io/socketstream
 import js/javascript
 import js/jstypes
 import js/regex
-import loader/connecterror
+import loader/headers
 import loader/loader
 import loader/request
 import local/select
 import server/buffer
-import server/forkserver
 import types/cell
 import types/color
 import types/cookie
+import types/referrer
 import types/url
 import utils/luwrap
 import utils/mimeguess
@@ -43,25 +42,24 @@ type
     setxsave: bool
 
   ContainerEventType* = enum
-    FAIL, SUCCESS, NEEDS_AUTH, REDIRECT, ANCHOR, NO_ANCHOR, UPDATE, READ_LINE,
-    READ_AREA, OPEN, INVALID_COMMAND, STATUS, ALERT, LOADED, TITLE,
-    CHECK_MAILCAP, QUIT
+    cetAnchor, cetNoAnchor, cetUpdate, cetReadLine, cetReadArea, cetOpen,
+    cetSetLoadInfo, cetStatus, cetAlert, cetLoaded, cetTitle
 
   ContainerEvent* = object
     case t*: ContainerEventType
-    of READ_LINE:
+    of cetReadLine:
       prompt*: string
       value*: string
       password*: bool
-    of READ_AREA:
+    of cetReadArea:
       tvalue*: string
-    of OPEN, REDIRECT:
+    of cetOpen:
       request*: Request
-    of ANCHOR, NO_ANCHOR:
+    of cetAnchor, cetNoAnchor:
       anchor*: string
-    of ALERT:
+    of cetAlert:
       msg*: string
-    of UPDATE:
+    of cetUpdate:
       force*: bool
     else: discard
 
@@ -94,9 +92,15 @@ type
   Container* = ref object
     # note: this is not the same as source.request.url (but should be synced
     # with buffer.url)
-    url: URL
-    #TODO this is inaccurate, because only the network charset is passed through
+    url* {.jsget.}: URL
+    #TODO this is inaccurate, because charsetStack can desync
     charset*: Charset
+    charsetStack*: seq[Charset]
+    # note: this is *not* the same as Buffer.cacheId. buffer has the cache ID of
+    # the output, while container holds that of the input. Thus pager can
+    # re-interpret the original input, and buffer can rewind the (potentially
+    # mailcap) output.
+    cacheId* {.jsget.}: int
     parent* {.jsget.}: Container
     children* {.jsget.}: seq[Container]
     config*: BufferConfig
@@ -113,8 +117,7 @@ type
     pos: CursorPosition
     bpos: seq[CursorPosition]
     highlights: seq[Highlight]
-    process* {.jsget.}: Pid
-    loaderPid* {.jsget.}: Pid
+    process* {.jsget.}: int
     loadinfo*: string
     lines: SimpleFlexibleGrid
     lineshift: int
@@ -146,22 +149,12 @@ type
 jsDestructor(Highlight)
 jsDestructor(Container)
 
-proc newBuffer*(forkserver: ForkServer, config: BufferConfig,
-    request: Request, attrs: WindowAttributes, title: string,
-    redirectdepth: int, canreinterpret: bool, fd: FileHandle,
-    contentType: Option[string]): Container =
-  let (process, loaderPid) = forkserver.forkBuffer(request, config, attrs)
-  if fd != -1:
-    loaderPid.passFd(request.url.host, fd)
-    if fd == 0:
-      # We are passing stdin.
-      closeStdin()
-    else:
-      discard close(fd)
+proc newContainer*(config: BufferConfig; url: URL; request: Request;
+    attrs: WindowAttributes; title: string; redirectdepth: int;
+    canreinterpret: bool; contentType: Option[string];
+    charsetStack: seq[Charset]; cacheId: int): Container =
   return Container(
-    url: request.url,
-    process: process,
-    loaderPid: loaderPid,
+    url: url,
     request: request,
     contentType: contentType,
     width: attrs.width,
@@ -172,76 +165,31 @@ proc newBuffer*(forkserver: ForkServer, config: BufferConfig,
     pos: CursorPosition(
       setx: -1
     ),
-    canreinterpret: canreinterpret
-  )
-
-proc newBufferFrom*(forkserver: ForkServer, attrs: WindowAttributes,
-    container: Container, contentTypeOverride: string): Container =
-  let request = newRequest(container.request.url, fromcache = true)
-  let config = container.config
-  let loaderPid = container.loaderPid
-  let bufferPid = forkserver.forkBufferWithLoader(request, config, attrs,
-    loaderPid)
-  return Container(
-    url: request.url,
-    request: request,
-    width: container.width,
-    height: container.height,
-    title: container.title,
-    config: config,
-    process: bufferPid,
-    loaderPid: loaderPid,
-    pos: CursorPosition(
-      setx: -1
-    ),
-    canreinterpret: true,
-    contentType: some(contentTypeOverride)
+    canreinterpret: canreinterpret,
+    loadinfo: "Connecting to " & request.url.host & "...",
+    cacheId: cacheId
   )
 
-func location*(container: Container): URL {.jsfget.} =
+func location(container: Container): URL {.jsfget.} =
   return container.url
 
-proc clone*(container: Container, newurl: URL): Promise[Container] =
+proc clone*(container: Container; newurl: URL): Promise[Container] =
   let url = if newurl != nil:
     newurl
   else:
-    container.location
-  return container.iface.clone(url).then(proc(pid: Pid): Container =
+    container.url
+  return container.iface.clone(url).then(proc(pid: int): Container =
     if pid == -1:
       return nil
-    return Container(
-      url: url,
-      config: container.config,
-      iface: container.iface, # changed later in setStream
-      width: container.width,
-      height: container.height,
-      title: container.title,
-      hoverText: container.hoverText,
-      lastPeek: container.lastPeek,
-      request: container.request,
-      pos: container.pos,
-      bpos: container.bpos,
-      highlights: container.highlights,
-      process: pid,
-      loaderPid: container.loaderPid,
-      loadinfo: container.loadinfo,
-      lines: container.lines,
-      lineshift: container.lineshift,
-      numLines: container.numLines,
-      code: container.code,
-      retry: container.retry,
-      hlon: container.hlon,
-      #needslines: container.needslines,
-      loadState: container.loadState,
-      events: container.events,
-      startpos: container.startpos,
-      hasstart: container.hasstart,
-      redirectdepth: container.redirectdepth,
-      select: container.select,
-      canreinterpret: container.canreinterpret,
-      ishtml: container.ishtml,
-      cloned: true
-    )
+    let nc = Container()
+    nc[] = container[]
+    nc.url = url
+    nc.process = pid
+    nc.cloned = true
+    nc.retry = @[]
+    nc.parent = nil
+    nc.children = @[]
+    return nc
   )
 
 func lineLoaded(container: Container, y: int): bool =
@@ -355,7 +303,7 @@ func maxScreenWidth(container: Container): int =
 func getTitle*(container: Container): string {.jsfunc.} =
   if container.title != "":
     return container.title
-  return container.location.serialize(excludepassword = true)
+  return container.url.serialize(excludepassword = true)
 
 func currentLineWidth(container: Container): int =
   if container.numLines == 0: return 0
@@ -472,12 +420,9 @@ proc setNumLines(container: Container, lines: int, finish = false) =
 
 proc cursorLastLine*(container: Container)
 
-proc requestLines(container: Container): EmptyPromise
-    {.discardable.} =
+proc requestLines(container: Container): EmptyPromise {.discardable.} =
   if container.iface == nil:
-    let res = EmptyPromise()
-    res.resolve()
-    return res
+    return newResolvedPromise()
   let w = container.lineWindow
   return container.iface.getLines(w).then(proc(res: GetLinesResult) =
     container.lines.setLen(w.len)
@@ -491,7 +436,7 @@ proc requestLines(container: Container): EmptyPromise
     if res.numLines != container.numLines:
       container.setNumLines(res.numLines, true)
       if container.loadState != lsLoading:
-        container.triggerEvent(STATUS)
+        container.triggerEvent(cetStatus)
     if res.numLines > 0:
       container.updateCursor()
       if container.tailOnLoad:
@@ -499,20 +444,22 @@ proc requestLines(container: Container): EmptyPromise
         container.cursorLastLine()
     let cw = container.fromy ..< container.fromy + container.height
     if w.a in cw or w.b in cw or cw.a in w or cw.b in w or isBgNew:
-      container.triggerEvent(UPDATE)
+      container.triggerEvent(cetUpdate)
   )
 
 proc redraw(container: Container) {.jsfunc.} =
-  container.triggerEvent(ContainerEvent(t: UPDATE, force: true))
+  container.triggerEvent(ContainerEvent(t: cetUpdate, force: true))
 
 proc sendCursorPosition*(container: Container) =
+  if container.iface == nil:
+    return
   container.iface.updateHover(container.cursorx, container.cursory)
       .then(proc(res: UpdateHoverResult) =
     if res.hover.len > 0:
       assert res.hover.high <= int(HoverType.high)
       for (ht, s) in res.hover:
         container.hoverText[ht] = s
-      container.triggerEvent(STATUS)
+      container.triggerEvent(cetStatus)
     if res.repaint:
       container.needslines = true
   )
@@ -521,7 +468,7 @@ proc setFromY(container: Container, y: int) {.jsfunc.} =
   if container.pos.fromy != y:
     container.pos.fromy = max(min(y, container.maxfromy), 0)
     container.needslines = true
-    container.triggerEvent(UPDATE)
+    container.triggerEvent(cetUpdate)
 
 proc setFromX(container: Container, x: int, refresh = true) {.jsfunc.} =
   if container.pos.fromx != x:
@@ -530,7 +477,7 @@ proc setFromX(container: Container, x: int, refresh = true) {.jsfunc.} =
       container.pos.cursorx = min(container.pos.fromx, container.currentLineWidth())
       if refresh:
         container.sendCursorPosition()
-    container.triggerEvent(UPDATE)
+    container.triggerEvent(cetUpdate)
 
 proc setFromXY(container: Container, x, y: int) {.jsfunc.} =
   container.setFromY(y)
@@ -580,7 +527,7 @@ proc setCursorX(container: Container, x: int, refresh = true, save = true)
   if container.cursorx == x and container.currentSelection != nil and
       container.currentSelection.x2 != x:
     container.currentSelection.x2 = x
-    container.triggerEvent(UPDATE)
+    container.triggerEvent(cetUpdate)
   if refresh:
     container.sendCursorPosition()
   if save:
@@ -602,7 +549,7 @@ proc setCursorY(container: Container, y: int, refresh = true) {.jsfunc.} =
       container.setFromY(y)
     container.pos.cursory = y
   if container.currentSelection != nil and container.currentSelection.y2 != y:
-    container.triggerEvent(UPDATE)
+    container.triggerEvent(cetUpdate)
     container.currentSelection.y2 = y
   container.restoreCursorX()
   if refresh:
@@ -1053,7 +1000,7 @@ proc scrollLeft*(container: Container, n = 1) {.jsfunc.} =
     container.setFromX(x)
 
 proc alert(container: Container, msg: string) =
-  container.triggerEvent(ContainerEvent(t: ALERT, msg: msg))
+  container.triggerEvent(ContainerEvent(t: cetAlert, msg: msg))
 
 proc lineInfo(container: Container) {.jsfunc.} =
   container.alert("line " & $(container.cursory + 1) & "/" &
@@ -1080,7 +1027,7 @@ proc gotoLine*[T: string|int](container: Container, s: T) =
     elif s[0] == '$':
       container.cursorLastLine()
     else:
-      let i = parseUInt32(s)
+      let i = parseUInt32(s, allowSign = true)
       if i.isSome and i.get > 0:
         container.markPos0()
         container.setCursorY(int(i.get - 1))
@@ -1113,6 +1060,8 @@ proc copyCursorPos*(container, c2: Container) =
   container.hasstart = true
 
 proc cursorNextLink*(container: Container, n = 1) {.jsfunc.} =
+  if container.iface == nil:
+    return
   container.markPos0()
   container.iface
     .findNextLink(container.cursorx, container.cursory, n)
@@ -1123,6 +1072,8 @@ proc cursorNextLink*(container: Container, n = 1) {.jsfunc.} =
     )
 
 proc cursorPrevLink*(container: Container, n = 1) {.jsfunc.} =
+  if container.iface == nil:
+    return
   container.markPos0()
   container.iface
     .findPrevLink(container.cursorx, container.cursory, n)
@@ -1133,6 +1084,8 @@ proc cursorPrevLink*(container: Container, n = 1) {.jsfunc.} =
     )
 
 proc cursorNextParagraph*(container: Container, n = 1) {.jsfunc.} =
+  if container.iface == nil:
+    return
   container.markPos0()
   container.iface
     .findNextParagraph(container.cursory, n)
@@ -1142,6 +1095,8 @@ proc cursorNextParagraph*(container: Container, n = 1) {.jsfunc.} =
     )
 
 proc cursorPrevParagraph*(container: Container, n = 1) {.jsfunc.} =
+  if container.iface == nil:
+    return
   container.markPos0()
   container.iface
     .findPrevParagraph(container.cursory, n)
@@ -1156,17 +1111,17 @@ proc setMark*(container: Container, id: string, x = none(int),
   let y = y.get(container.cursory)
   container.marks.withValue(id, p):
     p[] = (x, y)
-    container.triggerEvent(UPDATE)
+    container.triggerEvent(cetUpdate)
     return false
   do:
     container.marks[id] = (x, y)
-    container.triggerEvent(UPDATE)
+    container.triggerEvent(cetUpdate)
     return true
 
 proc clearMark*(container: Container, id: string): bool {.jsfunc.} =
   result = id in container.marks
   container.marks.del(id)
-  container.triggerEvent(UPDATE)
+  container.triggerEvent(cetUpdate)
 
 proc getMarkPos(container: Container, id: string): Opt[PagePos] {.jsfunc.} =
   if id == "`" or id == "'":
@@ -1226,6 +1181,8 @@ proc findPrevMark*(container: Container, x = none(int), y = none(int)):
   return bestid
 
 proc cursorNthLink*(container: Container, n = 1) {.jsfunc.} =
+  if container.iface == nil:
+    return
   container.iface
     .findNthLink(n)
     .then(proc(res: tuple[x, y: int]) =
@@ -1233,6 +1190,8 @@ proc cursorNthLink*(container: Container, n = 1) {.jsfunc.} =
         container.setCursorXYCenter(res.x, res.y))
 
 proc cursorRevNthLink*(container: Container, n = 1) {.jsfunc.} =
+  if container.iface == nil:
+    return
   container.iface
     .findRevNthLink(n)
     .then(proc(res: tuple[x, y: int]) =
@@ -1258,12 +1217,12 @@ proc onMatch(container: Container, res: BufferMatch, refresh: bool) =
         y2: res.y
       )
       container.highlights.add(hl)
-      container.triggerEvent(UPDATE)
+      container.triggerEvent(cetUpdate)
       container.hlon = false
       container.needslines = true
   elif container.hlon:
     container.clearSearchHighlights()
-    container.triggerEvent(UPDATE)
+    container.triggerEvent(cetUpdate)
     container.needslines = true
     container.hlon = false
 
@@ -1275,6 +1234,8 @@ proc cursorNextMatch*(container: Container, regex: Regex, wrap, refresh: bool,
       container.select.cursorNextMatch(regex, wrap)
     return newResolvedPromise()
   else:
+    if container.iface == nil:
+      return
     return container.iface
       .findNextMatch(regex, container.cursorx, container.cursory, wrap, n)
       .then(proc(res: BufferMatch) =
@@ -1288,6 +1249,8 @@ proc cursorPrevMatch*(container: Container, regex: Regex, wrap, refresh: bool,
       container.select.cursorPrevMatch(regex, wrap)
     return newResolvedPromise()
   else:
+    if container.iface == nil:
+      return
     container.markPos0()
     return container.iface
       .findPrevMatch(regex, container.cursorx, container.cursory, wrap, n)
@@ -1321,13 +1284,15 @@ proc cursorToggleSelection(container: Container, n = 1,
     )
     container.highlights.add(hl)
     container.currentSelection = hl
-  container.triggerEvent(UPDATE)
+  container.triggerEvent(cetUpdate)
   return container.currentSelection
 
 #TODO I don't like this API
 # maybe make selection a subclass of highlight?
 proc getSelectionText(container: Container, hl: Highlight = nil):
     Promise[string] {.jsfunc.} =
+  if container.iface == nil:
+    return
   let hl = if hl == nil: container.currentSelection else: hl
   if hl.t != HL_SELECT:
     let p = newPromise[string]()
@@ -1370,7 +1335,7 @@ proc getSelectionText(container: Container, hl: Highlight = nil):
 
 proc setLoadInfo(container: Container, msg: string) =
   container.loadinfo = msg
-  container.triggerEvent(STATUS)
+  container.triggerEvent(cetSetLoadInfo)
 
 #TODO this should be called with a timeout.
 proc onload*(container: Container, res: int) =
@@ -1382,15 +1347,15 @@ proc onload*(container: Container, res: int) =
   elif res == -1:
     container.loadState = lsLoaded
     container.setLoadInfo("")
-    container.triggerEvent(STATUS)
+    container.triggerEvent(cetStatus)
     container.needslines = true
-    container.triggerEvent(LOADED)
+    container.triggerEvent(cetLoaded)
     container.iface.getTitle().then(proc(title: string) =
       if title != "":
         container.title = title
-        container.triggerEvent(TITLE)
+        container.triggerEvent(cetTitle)
     )
-    if not container.hasstart and container.location.anchor != "":
+    if not container.hasstart and container.url.anchor != "":
       container.iface.gotoAnchor().then(proc(res: Opt[tuple[x, y: int]]) =
         if res.isSome:
           let res = res.get
@@ -1403,72 +1368,62 @@ proc onload*(container: Container, res: int) =
       container.onload(res)
     )
 
-proc load(container: Container) =
-  container.setLoadInfo("Connecting to " & container.location.host & "...")
-  container.iface.connect().then(proc(res: ConnectResult) =
-    let info = container.loadinfo
-    if not res.invalid:
-      container.code = res.code
-      if res.code == 0:
-        container.triggerEvent(SUCCESS)
-        # accept cookies
-        let cookiejar = container.config.loaderConfig.cookiejar
-        if res.cookies.len > 0 and cookiejar != nil:
-          cookiejar.add(res.cookies)
-        # set referrer policy, if any
-        if res.referrerPolicy.isSome and container.config.referer_from:
-          container.config.referrerPolicy = res.referrerPolicy.get
-        container.setLoadInfo("Connected to " & $container.location &
-          ". Downloading...")
-        if res.needsAuth:
-          container.triggerEvent(NEEDS_AUTH)
-        if res.redirect != nil:
-          container.triggerEvent(ContainerEvent(t: REDIRECT, request: res.redirect))
-        container.charset = res.charset
-        if container.contentType.isNone:
-          if res.contentType == "application/octet-stream":
-            let contentType = guessContentType(container.location.pathname,
-              "application/octet-stream", container.config.mimeTypes)
-            if contentType != "application/octet-stream":
-              container.contentType = some(contentType)
-            else:
-              container.contentType = some(res.contentType)
-          else:
-            container.contentType = some(res.contentType)
-        container.ishtml = container.contentType.get == "text/html"
-        container.triggerEvent(CHECK_MAILCAP)
+proc extractCookies(response: Response): seq[Cookie] =
+  result = @[]
+  if "Set-Cookie" in response.headers.table:
+    for s in response.headers.table["Set-Cookie"]:
+      let cookie = newCookie(s, response.url)
+      if cookie.isOk:
+        result.add(cookie.get)
+
+proc extractReferrerPolicy(response: Response): Option[ReferrerPolicy] =
+  if "Referrer-Policy" in response.headers:
+    return getReferrerPolicy(response.headers["Referrer-Policy"])
+  return none(ReferrerPolicy)
+
+# Apply data received in response.
+# Note: pager must call this before checkMailcap.
+proc applyResponse*(container: Container; response: Response) =
+  container.code = response.res
+  # accept cookies
+  let cookieJar = container.config.loaderConfig.cookieJar
+  if cookieJar != nil:
+    cookieJar.add(response.extractCookies())
+  # set referrer policy, if any
+  let referrerPolicy = response.extractReferrerPolicy()
+  if container.config.referer_from:
+    if referrerPolicy.isSome:
+      container.config.referrerPolicy = referrerPolicy.get
+  else:
+    container.config.referrerPolicy = NO_REFERRER
+  container.setLoadInfo("Connected to " & $response.url & ". Downloading...")
+  # setup content type; note that isSome means an override so we skip it
+  if container.contentType.isNone:
+    if response.contentType == "application/octet-stream":
+      let contentType = guessContentType(container.url.pathname,
+        "application/octet-stream", container.config.mimeTypes)
+      if contentType != "application/octet-stream":
+        container.contentType = some(contentType)
       else:
-        if res.errorMessage != "":
-          container.errorMessage = res.errorMessage
-        else:
-          container.errorMessage = getLoaderErrorMessage(res.code)
-        container.setLoadInfo("")
-        container.triggerEvent(FAIL)
+        container.contentType = some(response.contentType)
     else:
-      container.setLoadInfo(info)
-  )
-
-proc startload*(container: Container) =
-  container.iface.load().then(proc(res: int) =
-    container.onload(res)
-  )
-
-proc connect2*(container: Container): EmptyPromise =
-  return container.iface.connect2(container.ishtml)
-
-proc redirectToFd*(container: Container, fdin: FileHandle, wait, cache: bool):
-    EmptyPromise =
-  return container.iface.redirectToFd(fdin, wait, cache)
-
-proc readFromFd*(container: Container, fdout: FileHandle, id: string,
-    ishtml: bool): EmptyPromise =
-  container.ishtml = ishtml
-  let url = newURL("stream:" & id).get
-  container.loaderPid.passFd(url.host, fdout)
-  return container.iface.readFromFd(url, ishtml)
-
-proc quit*(container: Container) =
-  container.triggerEvent(QUIT)
+      container.contentType = some(response.contentType)
+  # setup charsets:
+  # * override charset
+  # * network charset
+  # * default charset guesses
+  # HTML may override the last two (but not the override charset).
+  if container.config.charsetOverride != CHARSET_UNKNOWN:
+    container.charsetStack = @[container.config.charsetOverride]
+  elif response.charset != CHARSET_UNKNOWN:
+    container.charsetStack = @[response.charset]
+  else:
+    container.charsetStack = @[]
+    for i in countdown(container.config.charsets.high, 0):
+      container.charsetStack.add(container.config.charsets[i])
+    if container.charsetStack.len == 0:
+      container.charsetStack.add(DefaultCharset)
+  container.charset = container.charsetStack[^1]
 
 proc cancel*(container: Container) {.jsfunc.} =
   if container.select.open:
@@ -1477,26 +1432,30 @@ proc cancel*(container: Container) {.jsfunc.} =
     container.loadState = lsCanceled
     container.alert("Canceled loading")
 
-proc findAnchor*(container: Container, anchor: string) =
+proc findAnchor*(container: Container; anchor: string) =
   container.iface.findAnchor(anchor).then(proc(found: bool) =
     if found:
-      container.triggerEvent(ContainerEvent(t: ANCHOR, anchor: anchor))
+      container.triggerEvent(ContainerEvent(t: cetAnchor, anchor: anchor))
     else:
-      container.triggerEvent(NO_ANCHOR))
+      container.triggerEvent(ContainerEvent(t: cetNoAnchor, anchor: anchor))
+  )
 
 proc readCanceled*(container: Container) =
   container.iface.readCanceled().then(proc(repaint: bool) =
     if repaint:
       container.needslines = true)
 
-proc readSuccess*(container: Container, s: string) =
+proc readSuccess*(container: Container; s: string) =
   container.iface.readSuccess(s).then(proc(res: ReadSuccessResult) =
     if res.repaint:
       container.needslines = true
     if res.open.isSome:
-      container.triggerEvent(ContainerEvent(t: OPEN, request: res.open.get)))
+      container.triggerEvent(ContainerEvent(t: cetOpen, request: res.open.get))
+  )
 
 proc reshape(container: Container): EmptyPromise {.jsfunc.} =
+  if container.iface == nil:
+    return
   return container.iface.forceRender().then(proc(): EmptyPromise =
     return container.requestLines()
   )
@@ -1509,25 +1468,25 @@ proc displaySelect(container: Container, selectResult: SelectResult) =
       container.onclick(res))
   container.select.initSelect(selectResult, container.acursorx,
     container.acursory, container.height, submitSelect)
-  container.triggerEvent(UPDATE)
+  container.triggerEvent(cetUpdate)
 
-proc onclick(container: Container, res: ClickResult) =
+proc onclick(container: Container; res: ClickResult) =
   if res.repaint:
     container.needslines = true
   if res.open.isSome:
-    container.triggerEvent(ContainerEvent(t: OPEN, request: res.open.get))
+    container.triggerEvent(ContainerEvent(t: cetOpen, request: res.open.get))
   if res.select.isSome:
     container.displaySelect(res.select.get)
   if res.readline.isSome:
     let rl = res.readline.get
     let event = if rl.area:
       ContainerEvent(
-        t: READ_AREA,
+        t: cetReadArea,
         tvalue: rl.value
       )
     else:
       ContainerEvent(
-        t: READ_LINE,
+        t: cetReadLine,
         prompt: rl.prompt,
         value: rl.value,
         password: rl.hide
@@ -1538,6 +1497,8 @@ proc click*(container: Container) {.jsfunc.} =
   if container.select.open:
     container.select.click()
   else:
+    if container.iface == nil:
+      return
     container.iface.click(container.cursorx, container.cursory)
       .then(proc(res: ClickResult) = container.onclick(res))
 
@@ -1545,12 +1506,13 @@ proc windowChange*(container: Container, attrs: WindowAttributes) =
   if attrs.width != container.width or attrs.height - 1 != container.height:
     container.width = attrs.width
     container.height = attrs.height - 1
-    container.iface.windowChange(attrs).then(proc() =
-      container.needslines = true
-    )
+    if container.iface != nil:
+      container.iface.windowChange(attrs).then(proc() =
+        container.needslines = true
+      )
 
 proc peek(container: Container) {.jsfunc.} =
-  container.alert($container.location)
+  container.alert($container.url)
 
 proc clearHover*(container: Container) =
   container.lastPeek = low(HoverType)
@@ -1582,17 +1544,24 @@ proc handleCommand(container: Container) =
   container.iface.stream.sread(packetid)
   container.iface.resolve(packetid, len - slen(packetid))
 
-proc setStream*(container: Container, stream: SocketStream,
+proc setStream*(container: Container; stream: SocketStream;
+    registerFun: proc(fd: int); fd: FileHandle; outCacheId: int) =
+  assert not container.cloned
+  container.iface = newBufferInterface(stream, registerFun)
+  container.iface.passFd(fd, outCacheId)
+  discard close(fd)
+  discard container.iface.load().then(proc(res: int) =
+    container.onload(res)
+  )
+
+proc setCloneStream*(container: Container; stream: SocketStream;
     registerFun: proc(fd: int)) =
-  if not container.cloned:
-    container.iface = newBufferInterface(stream, registerFun)
-    container.load()
-  else:
-    container.iface = cloneInterface(stream, registerFun)
-    # Maybe we have to resume loading. Let's try.
-    discard container.iface.load().then(proc(res: int) =
-      container.onload(res)
-    )
+  assert container.cloned
+  container.iface = cloneInterface(stream, registerFun)
+  # Maybe we have to resume loading. Let's try.
+  discard container.iface.load().then(proc(res: int) =
+    container.onload(res)
+  )
 
 proc onreadline(container: Container, w: Slice[int],
     handle: (proc(line: SimpleFlexibleLine)), res: GetLinesResult) =
diff --git a/src/local/pager.nim b/src/local/pager.nim
index a9d06567..51c2f89f 100644
--- a/src/local/pager.nim
+++ b/src/local/pager.nim
@@ -23,11 +23,14 @@ import extern/stdio
 import extern/tempfile
 import io/posixstream
 import io/promise
+import io/socketstream
+import io/urlfilter
 import js/error
 import js/javascript
 import js/jstypes
 import js/regex
 import js/tojs
+import loader/headers
 import loader/loader
 import loader/request
 import local/container
@@ -38,6 +41,7 @@ import types/cell
 import types/color
 import types/cookie
 import types/opt
+import types/referrer
 import types/urimethodmap
 import types/url
 import utils/strwidth
@@ -50,8 +54,20 @@ type
     NO_LINEMODE, LOCATION, USERNAME, PASSWORD, COMMAND, BUFFER, SEARCH_F,
     SEARCH_B, ISEARCH_F, ISEARCH_B, GOTO_LINE
 
+  # fdin is the original fd; fdout may be the same, or different if mailcap
+  # is used.
+  ProcMapItem = object
+    container*: Container
+    fdin*: FileHandle
+    fdout*: FileHandle
+    istreamOutputId*: int
+    ostreamOutputId*: int
+
+  PagerAlertState = enum
+    pasNormal, pasAlertOn, pasLoadInfo
+
   Pager* = ref object
-    alerton: bool
+    alertState: PagerAlertState
     alerts: seq[string]
     askcharpromise*: Promise[string]
     askcursor: int
@@ -60,23 +76,26 @@ type
     cgiDir*: seq[string]
     commandMode {.jsget.}: bool
     config: Config
+    connectingBuffers*: seq[tuple[container: Container; stream: SocketStream]]
     container*: Container
     cookiejars: Table[string, CookieJar]
+    devRandom: PosixStream
     display: FixedGrid
-    forkserver: ForkServer
+    forkserver*: ForkServer
     inputBuffer*: string # currently uninterpreted characters
     iregex: Result[Regex, string]
     isearchpromise: EmptyPromise
     lineedit*: Option[LineEdit]
     linehist: array[LineMode, LineHistory]
     linemode: LineMode
+    loader*: FileLoader
     mailcap: Mailcap
     mimeTypes: MimeTypes
     notnum*: bool # has a non-numeric character been input already?
     numload*: int
     omnirules: seq[OmniRule]
     precnum*: int32 # current number prefix (when vi-numeric-prefix is true)
-    procmap*: Table[Pid, Container]
+    procmap*: seq[ProcMapItem]
     proxy: URL
     redraw*: bool
     regex: Opt[Regex]
@@ -85,8 +104,8 @@ type
     siteconf: seq[SiteConfig]
     statusgrid*: FixedGrid
     term*: Terminal
-    tmpdir: string
-    unreg*: seq[(Pid, PosixStream)]
+    tmpdir*: string
+    unreg*: seq[tuple[pid: int; stream: PosixStream]]
     urimethodmap: URIMethodMap
     username: string
 
@@ -94,6 +113,9 @@ jsDestructor(Pager)
 
 func attrs(pager: Pager): WindowAttributes = pager.term.attrs
 
+func loaderPid(pager: Pager): int64 {.jsfget.} =
+  int64(pager.loader.process)
+
 func getRoot(container: Container): Container =
   var c = container
   while c.parent != nil: c = c.parent
@@ -237,7 +259,7 @@ proc setPaths(pager: Pager): Err[string] =
   pager.cgiDir = cgiDir
   return ok()
 
-proc newPager*(config: Config, forkserver: ForkServer, ctx: JSContext): Pager =
+proc newPager*(config: Config; forkserver: ForkServer; ctx: JSContext): Pager =
   let (mailcap, errs) = config.getMailcap()
   let pager = Pager(
     config: config,
@@ -261,6 +283,29 @@ proc newPager*(config: Config, forkserver: ForkServer, ctx: JSContext): Pager =
     pager.alert("Error reading mailcap: " & err)
   return pager
 
+proc genClientKey(pager: Pager): ClientKey =
+  var key: ClientKey
+  let n = pager.devRandom.recvData(addr key[0], key.len)
+  doAssert n == key.len
+  return key
+
+proc addLoaderClient*(pager: Pager, pid: int, config: LoaderClientConfig):
+    ClientKey =
+  var key = pager.genClientKey()
+  while unlikely(not pager.loader.addClient(key, pid, config)):
+    key = pager.genClientKey()
+  return key
+
+proc setLoader*(pager: Pager, loader: FileLoader) =
+  pager.devRandom = newPosixStream("/dev/urandom", O_RDONLY, 0)
+  pager.loader = loader
+  let config = LoaderClientConfig(
+    defaultHeaders: pager.config.getDefaultHeaders(),
+    proxy: pager.config.getProxy(),
+    filter: newURLFilter(default = true),
+  )
+  loader.key = pager.addLoaderClient(pager.loader.clientPid, config)
+
 proc launchPager*(pager: Pager, infile: File) =
   case pager.term.start(infile)
   of tsrSuccess: discard
@@ -325,17 +370,13 @@ proc refreshStatusMsg*(pager: Pager) =
     pager.writeStatusMessage($pager.precnum & pager.inputBuffer)
   elif pager.inputBuffer != "":
     pager.writeStatusMessage(pager.inputBuffer)
-  elif container.loadinfo != "":
-    pager.alerton = true
-    pager.writeStatusMessage(container.loadinfo)
-    container.loadinfo = ""
   elif pager.alerts.len > 0:
-    pager.alerton = true
+    pager.alertState = pasAlertOn
     pager.writeStatusMessage(pager.alerts[0])
     pager.alerts.delete(0)
   else:
     var format = Format(flags: {FLAG_REVERSE})
-    pager.alerton = false
+    pager.alertState = pasNormal
     container.clearHover()
     var msg = $(container.cursory + 1) & "/" & $container.numLines & " (" &
               $container.atPercentOf() & "%)"
@@ -351,8 +392,12 @@ proc refreshStatusMsg*(pager: Pager) =
       pager.writeStatusMessage(hover2, format, tw)
 
 # Call refreshStatusMsg if no alert is being displayed on the screen.
+# Alerts take precedence over load info, but load info is preserved when no
+# pending alerts exist.
 proc showAlerts*(pager: Pager) =
-  if not pager.alerton and pager.inputBuffer == "" and pager.precnum == 0:
+  if (pager.alertState == pasNormal or
+      pager.alertState == pasLoadInfo and pager.alerts.len > 0) and
+      pager.inputBuffer == "" and pager.precnum == 0:
     pager.refreshStatusMsg()
 
 proc drawBuffer*(pager: Pager, container: Container, ostream: Stream) =
@@ -444,41 +489,90 @@ proc fulfillCharAsk*(pager: Pager, s: string) =
   pager.askcharpromise = 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, request: Request,
-    title = "", redirectdepth = 0, canreinterpret = true, fd = FileHandle(-1),
-    contentType = none(string)): Container =
-  return newBuffer(
-    pager.forkserver,
+proc onSetLoadInfo(pager: Pager; container: Container) =
+  if pager.alertState != pasAlertOn:
+    if container.loadinfo == "":
+      pager.alertState = pasNormal
+    else:
+      pager.writeStatusMessage(container.loadinfo)
+      pager.alertState = pasLoadInfo
+
+proc newContainer(pager: Pager; bufferConfig: BufferConfig; request: Request;
+    title = ""; redirectdepth = 0; canreinterpret = true;
+    contentType = none(string); charsetStack: seq[Charset] = @[];
+    url: URL = request.url; cacheId = -1): Container =
+  request.suspended = true
+  if bufferConfig.loaderConfig.cookieJar != nil:
+    # loader stores cookie jars per client, but we have no client yet.
+    # therefore we must set cookie here
+    let cookie = bufferConfig.loaderConfig.cookieJar.serialize(request.url)
+    if cookie != "":
+      request.headers["Cookie"] = cookie
+  if request.referrer != nil:
+    # same with referrer
+    let r = request.referrer.getReferrer(request.url,
+      bufferConfig.referrerPolicy)
+    if r != "":
+      request.headers["Referer"] = r
+  let stream = pager.loader.startRequest(request)
+  pager.loader.registerFun(stream.fd)
+  let container = newContainer(
     bufferConfig,
+    url,
     request,
     pager.term.attrs,
     title,
     redirectdepth,
     canreinterpret,
-    fd,
-    contentType
+    contentType,
+    charsetStack,
+    cacheId
   )
+  pager.connectingBuffers.add((container, stream))
+  pager.onSetLoadInfo(container)
+  return container
+
+proc newContainerFrom(pager: Pager; container: Container; contentType: string):
+    Container =
+  let url = newURL("cache:" & $container.cacheId).get
+  return pager.newContainer(
+    container.config,
+    newRequest(url),
+    contentType = some(contentType),
+    charsetStack = container.charsetStack,
+    url = container.url,
+    cacheId = container.cacheId
+  )
+
+func findConnectingBuffer*(pager: Pager; fd: int): int =
+  for i, (_, stream) in pager.connectingBuffers:
+    if stream.fd == fd:
+      return i
+  -1
 
-proc dupeBuffer(pager: Pager, container: Container, location: URL) =
-  container.clone(location).then(proc(container: Container) =
+proc dupeBuffer(pager: Pager, container: Container, url: URL) =
+  container.clone(url).then(proc(container: Container) =
     if container == nil:
       pager.alert("Failed to duplicate buffer.")
     else:
       pager.addContainer(container)
+      pager.procmap.add(ProcMapItem(
+        container: container,
+        fdin: -1,
+        fdout: -1,
+        istreamOutputId: -1,
+        ostreamOutputId: -1
+      ))
   )
 
 proc dupeBuffer(pager: Pager) {.jsfunc.} =
-  pager.dupeBuffer(pager.container, pager.container.location)
+  pager.dupeBuffer(pager.container, pager.container.url)
 
 # The prevBuffer and nextBuffer procedures emulate w3m's PREV and NEXT
 # commands by traversing the container tree in a depth-first order.
@@ -583,8 +677,10 @@ proc deleteContainer(pager: Pager, container: Container) =
       pager.setContainer(nil)
   container.parent = nil
   container.children.setLen(0)
-  pager.unreg.add((container.process, container.iface.stream))
-  pager.forkserver.removeChild(container.process)
+  if container.iface != nil:
+    pager.unreg.add((container.process, container.iface.stream))
+    pager.forkserver.removeChild(container.process)
+    pager.loader.removeClient(container.process)
 
 proc discardBuffer*(pager: Pager, container = none(Container)) {.jsfunc.} =
   let c = container.get(pager.container)
@@ -607,19 +703,17 @@ proc toggleSource(pager: Pager) {.jsfunc.} =
   if pager.container.sourcepair != nil:
     pager.setContainer(pager.container.sourcepair)
   else:
-    let contentType = if pager.container.ishtml:
-      "text/plain"
-    else:
+    let ishtml = not pager.container.ishtml
+    #TODO I wish I could set the contentType to whatever I wanted, not just HTML
+    let contentType = if ishtml:
       "text/html"
-    let container = newBufferFrom(
-      pager.forkserver,
-      pager.attrs,
-      pager.container,
-      contentType
-    )
-    container.sourcepair = pager.container
-    pager.container.sourcepair = container
-    pager.addContainer(container)
+    else:
+      "text/plain"
+    let container = pager.newContainerFrom(pager.container, contentType)
+    if container != nil:
+      container.sourcepair = pager.container
+      pager.container.sourcepair = container
+      pager.addContainer(container)
 
 proc windowChange*(pager: Pager) =
   let oldAttrs = pager.attrs
@@ -639,13 +733,13 @@ proc windowChange*(pager: Pager) =
 
 # Apply siteconf settings to a request.
 # Note that this may modify the URL passed.
-proc applySiteconf(pager: Pager, url: var URL): BufferConfig =
+proc applySiteconf(pager: Pager; url: var URL; cs: Charset): BufferConfig =
   let host = url.host
-  var referer_from: bool
-  var cookiejar: CookieJar
+  var referer_from = false
+  var cookieJar: CookieJar = nil
   var headers = pager.config.getDefaultHeaders()
-  var scripting: bool
-  var images: bool
+  var scripting = false
+  var images = false
   var charsets = pager.config.encoding.document_charset
   var userstyle = pager.config.css.stylesheet
   var proxy = pager.proxy
@@ -667,9 +761,9 @@ proc applySiteconf(pager: Pager, url: var URL): BufferConfig =
         if jarid notin pager.cookiejars:
           pager.cookiejars[jarid] = newCookieJar(url,
             sc.third_party_cookie)
-        cookiejar = pager.cookiejars[jarid]
+        cookieJar = pager.cookiejars[jarid]
       else:
-        cookiejar = nil # override
+        cookieJar = nil # override
     if sc.scripting.isSome:
       scripting = sc.scripting.get
     if sc.referer_from.isSome:
@@ -683,18 +777,17 @@ proc applySiteconf(pager: Pager, url: var URL): BufferConfig =
       userstyle &= sc.stylesheet.get
     if sc.proxy.isSome:
       proxy = sc.proxy.get
-  return pager.config.getBufferConfig(url, cookiejar, headers, referer_from,
+  return pager.config.getBufferConfig(url, cookieJar, headers, referer_from,
     scripting, charsets, images, userstyle, proxy, mimeTypes, urimethodmap,
-    pager.cgiDir, pager.tmpdir)
+    pager.cgiDir, pager.tmpdir, cs)
 
 # Load request in a new buffer.
 proc gotoURL(pager: Pager, request: Request, prevurl = none(URL),
     contentType = none(string), cs = CHARSET_UNKNOWN, replace: Container = nil,
     redirectdepth = 0, referrer: Container = nil) =
   if referrer != nil and referrer.config.referer_from:
-    request.referer = referrer.location
-  var bufferConfig = pager.applySiteconf(request.url)
-  bufferConfig.charsetOverride = cs
+    request.referrer = referrer.url
+  var bufferConfig = pager.applySiteconf(request.url, cs)
   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
@@ -705,7 +798,7 @@ proc gotoURL(pager: Pager, request: Request, prevurl = none(URL),
     # feedback on what is actually going to happen when typing a URL; TODO.
     if referrer != nil:
       bufferConfig.referrerPolicy = referrer.config.referrerPolicy
-    let container = pager.newBuffer(
+    let container = pager.newContainer(
       bufferConfig,
       request,
       redirectdepth = redirectdepth,
@@ -714,7 +807,6 @@ proc gotoURL(pager: Pager, request: Request, prevurl = none(URL),
     if replace != nil:
       container.replace = replace
       container.copyCursorPos(container.replace)
-      pager.registerContainer(container)
     else:
       pager.addContainer(container)
     inc pager.numload
@@ -744,7 +836,7 @@ proc loadURL*(pager: Pager, url: string, ctype = none(string),
   let firstparse = parseURL(url)
   if firstparse.isSome:
     let prev = if pager.container != nil:
-      some(pager.container.location)
+      some(pager.container.url)
     else:
       none(URL)
     pager.gotoURL(newRequest(firstparse.get), prev, ctype, cs)
@@ -769,23 +861,23 @@ proc loadURL*(pager: Pager, url: string, ctype = none(string),
       pager.container.retry = urls
 
 proc readPipe0*(pager: Pager, contentType: string, cs: Charset,
-    fd: FileHandle, location: Option[URL], title: string,
-    canreinterpret: bool): Container =
-  var location = location.get(newURL("stream:-").get)
-  var bufferConfig = pager.applySiteconf(location)
-  bufferConfig.charsetOverride = cs
-  return pager.newBuffer(
+    fd: FileHandle, url: URL, title: string, canreinterpret: bool): Container =
+  var url = url
+  pager.loader.passFd(url.pathname, fd)
+  safeClose(fd)
+  let bufferConfig = pager.applySiteconf(url, cs)
+  return pager.newContainer(
     bufferConfig,
-    newRequest(location),
+    newRequest(url),
     title = title,
     canreinterpret = canreinterpret,
-    fd = fd,
     contentType = some(contentType)
   )
 
 proc readPipe*(pager: Pager, contentType: string, cs: Charset, fd: FileHandle,
     title: string) =
-  let container = pager.readPipe0(contentType, cs, fd, none(URL), title, true)
+  let url = newURL("stream:-").get
+  let container = pager.readPipe0(contentType, cs, fd, url, title, true)
   inc pager.numload
   pager.addContainer(container)
 
@@ -855,12 +947,12 @@ proc updateReadLine*(pager: Pager) =
         pager.username = lineedit.news
         pager.setLineEdit("Password: ", PASSWORD, hide = true)
       of PASSWORD:
-        let url = newURL(pager.container.location)
+        let url = newURL(pager.container.url)
         url.username = pager.username
         url.password = lineedit.news
         pager.username = ""
         pager.gotoURL(
-          newRequest(url), some(pager.container.location),
+          newRequest(url), some(pager.container.url),
           replace = pager.container,
           referrer = pager.container
         )
@@ -901,7 +993,7 @@ proc load(pager: Pager, s = "") {.jsfunc.} =
     if s.len > 1:
       pager.loadURL(s[0..^2])
   elif s == "":
-    pager.setLineEdit("URL: ", LOCATION, $pager.container.location)
+    pager.setLineEdit("URL: ", LOCATION, $pager.container.url)
   else:
     pager.setLineEdit("URL: ", LOCATION, s)
 
@@ -912,12 +1004,12 @@ proc jsGotoURL(pager: Pager, s: string): JSResult[void] {.jsfunc: "gotoURL".} =
 
 # Reload the page in a new buffer, then kill the previous buffer.
 proc reload(pager: Pager) {.jsfunc.} =
-  pager.gotoURL(newRequest(pager.container.location), none(URL),
+  pager.gotoURL(newRequest(pager.container.url), none(URL),
     pager.container.contentType, replace = pager.container)
 
 proc setEnvVars(pager: Pager) {.jsfunc.} =
   try:
-    putEnv("CHA_URL", $pager.container.location)
+    putEnv("CHA_URL", $pager.container.url)
     putEnv("CHA_CHARSET", $pager.container.charset)
   except OSError:
     pager.alert("Warning: failed to set some environment variables")
@@ -948,238 +1040,209 @@ proc externInto(pager: Pager, cmd, ins: string): bool {.jsfunc.} =
   pager.setEnvVars()
   return runProcessInto(cmd, ins)
 
-proc externFilterSource(pager: Pager, cmd: string, c: Container = nil,
+proc externFilterSource(pager: Pager; cmd: string; c: Container = nil;
     contentType = opt(string)) {.jsfunc.} =
-  let container = newBufferFrom(
-    pager.forkserver,
-    pager.attrs,
-    if c != nil: c else: pager.container,
-    contentType.get(pager.container.contentType.get(""))
-  )
+  let fromc = if c != nil: c else: pager.container
+  let contentType = contentType.get(pager.container.contentType.get(""))
+  let container = pager.newContainerFrom(fromc, contentType)
+  container.ishtml = contentType == "text/html"
   pager.addContainer(container)
   container.filter = BufferFilter(cmd: cmd)
 
 proc authorize(pager: Pager) =
   pager.setLineEdit("Username: ", USERNAME)
 
-type CheckMailcapResult = tuple[promise: EmptyPromise, connect: bool]
-
-proc checkMailcap(pager: Pager, container: Container,
-  contentTypeOverride = none(string)): CheckMailcapResult
+type CheckMailcapResult = object
+  fdout: int
+  ostreamOutputId: int
+  connect: bool
+  ishtml: bool
 
 # Pipe output of an x-ansioutput mailcap command to the text/x-ansi handler.
-proc ansiDecode(pager: Pager, container: Container, fdin: cint,
-    ishtml: var bool, fdout: var cint) =
-  let cs = container.charset
-  let url = container.location
-  let entry = pager.mailcap.getMailcapEntry("text/x-ansi", "", url, cs)
+proc ansiDecode(pager: Pager; url: URL; charset: Charset; ishtml: var bool;
+    fdin: cint): cint =
+  let entry = pager.mailcap.getMailcapEntry("text/x-ansi", "", url, charset)
   var canpipe = true
-  let cmd = unquoteCommand(entry.cmd, "text/x-ansi", "", url, cs, canpipe)
+  let cmd = unquoteCommand(entry.cmd, "text/x-ansi", "", url, charset, canpipe)
   if not canpipe:
     pager.alert("Error: could not pipe to text/x-ansi, decoding as text/plain")
+    return -1
+  var pipefdOutAnsi: array[2, cint]
+  if pipe(pipefdOutAnsi) == -1:
+    pager.alert("Error: failed to open pipe")
+    return
+  case fork()
+  of -1:
+    pager.alert("Error: failed to fork ANSI decoder process")
+    discard close(pipefdOutAnsi[0])
+    discard close(pipefdOutAnsi[1])
+    return -1
+  of 0: # child process
+    discard close(pipefdOutAnsi[0])
+    discard dup2(fdin, stdin.getFileHandle())
+    discard close(fdin)
+    discard dup2(pipefdOutAnsi[1], stdout.getFileHandle())
+    discard close(pipefdOutAnsi[1])
+    closeStderr()
+    myExec(cmd)
+    assert false
   else:
-    var pipefdOutAnsi: array[2, cint]
-    if pipe(pipefdOutAnsi) == -1:
-      raise newException(Defect, "Failed to open pipe.")
-    case fork()
-    of -1:
-      pager.alert("Error: failed to fork ANSI decoder process")
-      discard close(pipefdOutAnsi[0])
-      discard close(pipefdOutAnsi[1])
-    of 0: # child process
-      if fdin != -1:
-        discard close(fdin)
-      discard close(pipefdOutAnsi[0])
-      discard dup2(fdout, stdin.getFileHandle())
-      discard close(fdout)
-      discard dup2(pipefdOutAnsi[1], stdout.getFileHandle())
-      discard close(pipefdOutAnsi[1])
-      closeStderr()
-      myExec(cmd)
-      assert false
-    else:
-      discard close(pipefdOutAnsi[1])
-      discard close(fdout)
-      fdout = pipefdOutAnsi[0]
-      ishtml = HTMLOUTPUT in entry.flags
+    discard close(pipefdOutAnsi[1])
+    discard close(fdin)
+    ishtml = HTMLOUTPUT in entry.flags
+    return pipefdOutAnsi[0]
 
 # 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): CheckMailcapResult =
-  var pipefdIn: array[2, cint]
-  var pipefdOut: array[2, cint]
-  if pipe(pipefdIn) == -1 or pipe(pipefdOut) == -1:
-    raise newException(Defect, "Failed to open pipe.")
+proc runMailcapReadPipe(pager: Pager; stream: SocketStream; cmd: string;
+    pipefdOut: array[2, cint]): int =
   let pid = fork()
   if pid == -1:
-    pager.alert("Failed to fork process!")
-    return (nil, false)
-  elif pid == 0: # child process
-    discard close(pipefdIn[1])
+    pager.alert("Error: failed to fork mailcap read process")
+    return -1
+  elif pid == 0:
+    # child process
     discard close(pipefdOut[0])
-    discard dup2(pipefdIn[0], stdin.getFileHandle())
+    discard dup2(stream.fd, stdin.getFileHandle())
+    stream.close()
     discard dup2(pipefdOut[1], stdout.getFileHandle())
     closeStderr()
-    discard close(pipefdIn[0])
     discard close(pipefdOut[1])
     myExec(cmd)
-    assert false
-  else:
-    # parent
-    discard close(pipefdIn[0])
-    discard close(pipefdOut[1])
-    let fdin = pipefdIn[1]
-    var fdout = pipefdOut[0]
-    var ishtml = HTMLOUTPUT in entry.flags
-    if not ishtml and ANSIOUTPUT in entry.flags:
-      # decode ANSI sequence
-      pager.ansiDecode(container, fdin, ishtml, fdout)
-    let p = container.redirectToFd(fdin, wait = false, cache = true)
-    discard close(fdin)
-    let p2 = p.then(proc(): auto =
-      let p = container.readFromFd(fdout, $pid, ishtml)
-      discard close(fdout)
-      return p
-    )
-    return (p2, true)
+    doAssert false
+  # parent
+  pid
 
 # 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): CheckMailcapResult =
-  let needsterminal = NEEDSTERMINAL in entry.flags
-  var pipefd: array[2, cint]
-  if pipe(pipefd) == -1:
-    raise newException(Defect, "Failed to open pipe.")
+proc runMailcapWritePipe(pager: Pager; stream: SocketStream;
+    needsterminal: bool; cmd: string) =
   if needsterminal:
     pager.term.quit()
   let pid = fork()
   if pid == -1:
-    return (nil, false)
+    pager.alert("Error: failed to fork mailcap write process")
   elif pid == 0:
     # child process
-    discard close(pipefd[1])
-    discard dup2(pipefd[0], stdin.getFileHandle())
+    discard dup2(stream.fd, stdin.getFileHandle())
+    stream.close()
     if not needsterminal:
       closeStdout()
       closeStderr()
-    discard close(pipefd[0])
     myExec(cmd)
-    assert false
+    doAssert false
   else:
     # parent
-    discard close(pipefd[0])
-    let fd = pipefd[1]
-    let p = container.redirectToFd(fd, wait = true, cache = false)
-    discard close(fd)
+    stream.close()
     if needsterminal:
       var x: cint
       discard waitpid(pid, x, 0)
       pager.term.restart()
-    return (p, false)
+
+proc writeToFile(istream: SocketStream; outpath: string): bool =
+  let ps = newPosixStream(outpath, O_WRONLY or O_CREAT, 0o600)
+  if ps == nil:
+    return false
+  var buffer: array[4096, uint8]
+  while true:
+    let n = istream.recvData(buffer)
+    if n == 0:
+      break
+    if ps.sendData(buffer.toOpenArray(0, n - 1)) < n:
+      ps.close()
+      return false
+    if n < buffer.len:
+      break
+  ps.close()
+  true
 
 # 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): CheckMailcapResult =
-  let fd = open(outpath, O_WRONLY or O_CREAT, 0o600)
-  if fd == -1:
-    return (nil, false)
-  let p = container.redirectToFd(fd, wait = true, cache = true).then(proc():
-      auto =
-    var pipefd: array[2, cint] # redirect stdout here
-    if pipe(pipefd) == -1:
-      raise newException(Defect, "Failed to open pipe.")
+proc runMailcapReadFile(pager: Pager; stream: SocketStream;
+    cmd, outpath: string; pipefdOut: array[2, cint]): int =
+  let pid = fork()
+  if pid == 0:
+    # child process
+    discard close(pipefdOut[0])
+    discard dup2(pipefdOut[1], stdout.getFileHandle())
+    discard close(pipefdOut[1])
+    closeStderr()
+    if not stream.writeToFile(outpath):
+      #TODO print error message
+      quit(1)
+    stream.close()
+    let ret = execCmd(cmd)
+    discard tryRemoveFile(outpath)
+    quit(ret)
+  # parent
+  pid
+
+# 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; stream: SocketStream;
+    needsterminal: bool; cmd, outpath: string) =
+  if needsterminal:
+    pager.term.quit()
+    if not stream.writeToFile(outpath):
+      pager.term.restart()
+      pager.alert("Error: failed to write file for mailcap process")
+    else:
+      discard execCmd(cmd)
+      discard tryRemoveFile(outpath)
+      pager.term.restart()
+  else:
+    # don't block
     let pid = fork()
     if pid == 0:
       # child process
-      discard close(pipefd[0])
-      discard dup2(pipefd[1], stdout.getFileHandle())
-      discard close(pipefd[1])
+      closeStdin()
+      closeStdout()
       closeStderr()
+      if not stream.writeToFile(outpath):
+        #TODO print error message (maybe in parent?)
+        quit(1)
+      stream.close()
       let ret = execCmd(cmd)
       discard tryRemoveFile(outpath)
       quit(ret)
     # parent
-    discard close(pipefd[1])
-    var fdout = pipefd[0]
-    var ishtml = HTMLOUTPUT in entry.flags
-    if not ishtml and ANSIOUTPUT in entry.flags:
-      pager.ansiDecode(container, -1, ishtml, fdout)
-    let p = container.readFromFd(fdout, $pid, ishtml)
-    discard close(fdout)
-    return p
-  )
-  return (p, true)
+    stream.close()
 
-# 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): CheckMailcapResult =
-  let needsterminal = NEEDSTERMINAL in entry.flags
-  let fd = open(outpath, O_WRONLY or O_CREAT, 0o600)
-  if fd == -1:
-    return (nil, false)
-  let p = container.redirectToFd(fd, wait = true, cache = false).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
-        closeStdin()
-        closeStdout()
-        closeStderr()
-        let ret = execCmd(cmd)
-        discard tryRemoveFile(outpath)
-        quit(ret)
-  )
-  return (p, false)
-
-proc filterBuffer(pager: Pager, container: Container): CheckMailcapResult =
+proc filterBuffer(pager: Pager; stream: SocketStream; cmd: string;
+    ishtml: bool): CheckMailcapResult =
   pager.setEnvVars()
-  let cmd = container.filter.cmd
-  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.")
+    pager.alert("Error: failed to open pipe")
+    return CheckMailcapResult(connect: false, fdout: -1)
   let pid = fork()
   if pid == -1:
-    return (nil, true)
+    pager.alert("Error: failed to fork buffer filter process")
+    return CheckMailcapResult(connect: false, fdout: -1)
   elif pid == 0:
     # child
-    discard close(pipefd_in[1])
     discard close(pipefd_out[0])
-    stdout.flushFile()
-    discard dup2(pipefd_in[0], stdin.getFileHandle())
+    discard dup2(stream.fd, stdin.getFileHandle())
+    stream.close()
     discard dup2(pipefd_out[1], stdout.getFileHandle())
     closeStderr()
-    discard close(pipefd_in[0])
     discard close(pipefd_out[1])
     myExec(cmd)
-    assert false
-  else:
-    # 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, cache = false)
-    let p2 = p.then(proc(): auto =
-      discard close(fdin)
-      return container.readFromFd(fdout, $pid, container.ishtml)
-    ).then(proc() =
-      discard close(fdout)
-    )
-    return (p2, true)
+    doAssert false
+  # parent
+  discard close(pipefd_out[1])
+  let fdout = pipefd_out[0]
+  let url = parseURL("stream:" & $pid).get
+  pager.loader.passFd(url.pathname, FileHandle(fdout))
+  safeClose(fdout)
+  let response = pager.loader.doRequest(newRequest(url, suspended = true))
+  return CheckMailcapResult(
+    connect: true,
+    fdout: response.body.fd,
+    ostreamOutputId: response.outputId,
+    ishtml: ishtml
+  )
 
 # Search for a mailcap entry, and if found, execute the specified command
 # and pipeline the input and output appropriately.
@@ -1190,129 +1253,194 @@ proc filterBuffer(pager: Pager, container: Container): CheckMailcapResult =
 # * 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,
-    contentTypeOverride = none(string)): CheckMailcapResult =
+#TODO add support for edit/compose, better error handling
+proc checkMailcap(pager: Pager; container: Container; stream: SocketStream;
+    istreamOutputId: int): CheckMailcapResult =
   if container.filter != nil:
-    return pager.filterBuffer(container)
-  if container.contentType.isNone:
-    return (nil, true)
-  let contentType = contentTypeOverride.get(container.contentType.get)
+    return pager.filterBuffer(stream, container.filter.cmd, container.ishtml)
+  # contentType must exist, because we set it in applyResponse
+  let contentType = container.contentType.get
   if contentType == "text/html":
-    # We support HTML natively, so it would make little sense to execute
+    # We support text/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)
+    return CheckMailcapResult(connect: true, fdout: stream.fd, ishtml: true)
+  if contentType == "text/plain":
+    # text/plain 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 CheckMailcapResult(connect: true, fdout: stream.fd)
   #TODO callback for outpath or something
-  let url = container.location
+  let url = container.url
   let cs = container.charset
   let entry = pager.mailcap.getMailcapEntry(contentType, "", url, cs)
-  if entry != nil:
-    let tmpdir = pager.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)
-    putEnv("MAILCAP_URL", $url) #TODO delEnv this after command is finished?
-    if {COPIOUSOUTPUT, HTMLOUTPUT, ANSIOUTPUT} * entry.flags == {}:
-      # no output.
+  if entry == nil:
+    return CheckMailcapResult(connect: true, fdout: stream.fd)
+  let tmpdir = pager.tmpdir
+  let ext = url.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)
+  var ishtml = HTMLOUTPUT in entry.flags
+  let needsterminal = NEEDSTERMINAL in entry.flags
+  putEnv("MAILCAP_URL", $url)
+  block needsConnect:
+    if entry.flags * {COPIOUSOUTPUT, HTMLOUTPUT, ANSIOUTPUT} == {}:
+      # No output. Resume here, so that blocking needsterminal filters work.
+      pager.loader.resume(@[istreamOutputId])
       if canpipe:
-        return pager.runMailcapWritePipe(container, entry[], cmd)
+        pager.runMailcapWritePipe(stream, needsterminal, cmd)
       else:
-        return pager.runMailcapWriteFile(container, entry[], cmd, outpath)
+        pager.runMailcapWriteFile(stream, needsterminal, cmd, outpath)
+      # stream is already closed
+      break needsConnect # never connect here, since there's no output
+    var pipefdOut: array[2, cint]
+    if pipe(pipefdOut) == -1:
+      pager.alert("Error: failed to open pipe")
+      stream.close() # connect: false implies that we consumed the stream
+      break needsConnect
+    let pid = if canpipe:
+      pager.runMailcapReadPipe(stream, cmd, pipefdOut)
     else:
-      if canpipe:
-        return pager.runMailcapReadPipe(container, entry[], cmd)
-      else:
-        return pager.runMailcapReadFile(container, entry[], cmd, outpath)
-  return (nil, true)
+      pager.runMailcapReadFile(stream, cmd, outpath, pipefdOut)
+    discard close(pipefdOut[1]) # close write
+    let fdout = if not ishtml and ANSIOUTPUT in entry.flags:
+      pager.ansiDecode(url, cs, ishtml, pipefdOut[0])
+    else:
+      pipefdOut[0]
+    delEnv("MAILCAP_URL")
+    let url = parseURL("stream:" & $pid).get
+    pager.loader.passFd(url.pathname, FileHandle(fdout))
+    safeClose(cint(fdout))
+    let response = pager.loader.doRequest(newRequest(url, suspended = true))
+    return CheckMailcapResult(
+      connect: true,
+      fdout: response.body.fd,
+      ostreamOutputId: response.outputId,
+      ishtml: ishtml
+    )
+  delEnv("MAILCAP_URL")
+  return CheckMailcapResult(connect: false, fdout: -1)
 
-proc redirectTo(pager: Pager, container: Container, request: Request) =
+proc redirectTo(pager: Pager; container: Container; request: Request) =
   pager.alert("Redirecting to " & $request.url)
-  pager.gotoURL(request, some(container.location), replace = container,
+  pager.gotoURL(request, some(container.url), replace = container,
     redirectdepth = container.redirectdepth + 1, referrer = container)
 
-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()),
-        contentType = container.contentType)
+proc fail*(pager: Pager; container: Container; errorMessage: string) =
+  dec pager.numload
+  pager.deleteContainer(container)
+  if container.retry.len > 0:
+    pager.gotoURL(newRequest(container.retry.pop()),
+      contentType = container.contentType)
+  else:
+    pager.alert("Can't load " & $container.url & " (" & errorMessage & ")")
+
+proc redirect*(pager: Pager; container: Container; response: Response) =
+  # still need to apply response, or we lose cookie jars.
+  container.applyResponse(response)
+  let request = response.redirect
+  if container.redirectdepth < pager.config.network.max_redirect:
+    if container.url.scheme == request.url.scheme or
+        container.url.scheme == "cgi-bin" or
+        container.url.scheme == "http" and request.url.scheme == "https" or
+        container.url.scheme == "https" and request.url.scheme == "http":
+      pager.redirectTo(container, request)
     else:
-      pager.alert("Can't load " & $container.location & " (" &
-        container.errorMessage & ")")
-    return false
-  of SUCCESS:
+      let url = request.url
+      pager.ask("Warning: switch protocols? " & $url).then(proc(x: bool) =
+        if x:
+          pager.redirectTo(container, request)
+      )
+  else:
+    pager.alert("Error: maximum redirection depth reached")
+    pager.deleteContainer(container)
+
+proc replace(pager: Pager; container: Container) =
+  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
+
+proc connected*(pager: Pager; container: Container; response: Response) =
+  let istream = response.body
+  container.applyResponse(response)
+  if response.status == 401: # unauthorized
+    pager.authorize()
+    istream.close()
+    return
+  let mailcapRes = pager.checkMailcap(container, istream, response.outputId)
+  if mailcapRes.connect:
+    container.ishtml = mailcapRes.ishtml
+    container.applyResponse(response)
+    # buffer now actually exists; create a process for it
+    container.process = pager.forkserver.forkBuffer(
+      container.config,
+      container.url,
+      container.request,
+      pager.attrs,
+      container.ishtml,
+      container.charsetStack
+    )
+    if mailcapRes.fdout != istream.fd:
+      # istream has been redirected into a filter
+      istream.close()
+    pager.procmap.add(ProcMapItem(
+      container: container, 
+      fdout: FileHandle(mailcapRes.fdout),
+      fdin: FileHandle(istream.fd),
+      ostreamOutputId: mailcapRes.ostreamOutputId,
+      istreamOutputId: response.outputId
+    ))
     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:
+      pager.replace(container)
+  else:
     dec pager.numload
-  of NEEDS_AUTH:
-    if pager.container == container:
-      pager.authorize()
-  of REDIRECT:
-    if container.redirectdepth < pager.config.network.max_redirect:
-      let url = event.request.url
-      if container.location.scheme == url.scheme or
-          container.location.scheme == "cgi-bin" or
-          container.location.scheme == "http" and url.scheme == "https" or
-          container.location.scheme == "https" and url.scheme == "http":
-        pager.redirectTo(container, event.request)
-      else:
-        pager.ask("Warning: switch protocols? " & $url).then(proc(x: bool) =
-          if x:
-            pager.redirectTo(container, event.request)
-        )
-    else:
-      pager.alert("Error: maximum redirection depth reached")
-      pager.deleteContainer(container)
-      return false
-  of ANCHOR:
-    let url2 = newURL(container.location)
+    pager.deleteContainer(container)
+    pager.redraw = true
+    pager.refreshStatusMsg()
+
+proc handleEvent0(pager: Pager; container: Container; event: ContainerEvent):
+    bool =
+  case event.t
+  of cetLoaded:
+    dec pager.numload
+  of cetAnchor:
+    let url2 = newURL(container.url)
     url2.setHash(event.anchor)
     pager.dupeBuffer(container, url2)
-  of NO_ANCHOR:
+  of cetNoAnchor:
     pager.alert("Couldn't find anchor " & event.anchor)
-  of UPDATE:
+  of cetUpdate:
     if container == pager.container:
       pager.redraw = true
-      if event.force: pager.term.clearCanvas()
-  of READ_LINE:
+      if event.force:
+        pager.term.clearCanvas()
+  of cetReadLine:
     if container == pager.container:
       pager.setLineEdit("(BUFFER) " & event.prompt, BUFFER, event.value, hide = event.password)
-  of READ_AREA:
+  of cetReadArea:
     if container == pager.container:
       var s = event.tvalue
       if openInEditor(pager.term, pager.config, pager.tmpdir, s):
@@ -1320,44 +1448,30 @@ proc handleEvent0(pager: Pager, container: Container, event: ContainerEvent): bo
       else:
         pager.container.readCanceled()
       pager.redraw = true
-  of OPEN:
+  of cetOpen:
     let url = event.request.url
     if pager.container == nil or not pager.container.isHoverURL(url):
       pager.ask("Open pop-up? " & $url).then(proc(x: bool) =
         if x:
-          pager.gotoURL(event.request, some(container.location),
+          pager.gotoURL(event.request, some(container.url),
             referrer = pager.container)
       )
     else:
-      pager.gotoURL(event.request, some(container.location),
+      pager.gotoURL(event.request, some(container.url),
         referrer = pager.container)
-  of INVALID_COMMAND: discard
-  of STATUS:
+  of cetStatus:
     if pager.container == container:
       pager.refreshStatusMsg()
-  of TITLE:
+  of cetSetLoadInfo:
+    if pager.container == container:
+      pager.onSetLoadInfo(container)
+  of cetTitle:
     if pager.container == container:
       pager.showAlerts()
       pager.term.setTitle(container.getTitle())
-  of ALERT:
+  of cetAlert:
     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:
-      # remove "connecting..." message left by connect2
-      pager.refreshStatusMsg()
-      cm.then(proc(): auto =
-        container.quit())
-  of QUIT:
-    dec pager.numload
-    pager.deleteContainer(container)
-    return false
   return true
 
 proc handleEvents*(pager: Pager, container: Container) =