about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--src/buffer/buffer.nim358
-rw-r--r--src/buffer/container.nim285
-rw-r--r--src/display/client.nim6
-rw-r--r--src/display/pager.nim24
-rw-r--r--src/ips/serialize.nim21
-rw-r--r--src/utils/eprint.nim9
6 files changed, 420 insertions, 283 deletions
diff --git a/src/buffer/buffer.nim b/src/buffer/buffer.nim
index 9c016c0f..458b5b17 100644
--- a/src/buffer/buffer.nim
+++ b/src/buffer/buffer.nim
@@ -42,12 +42,10 @@ type
   BufferCommand* = enum
     LOAD, RENDER, WINDOW_CHANGE, FIND_ANCHOR, READ_SUCCESS, READ_CANCELED,
     CLICK, FIND_NEXT_LINK, FIND_PREV_LINK, FIND_NEXT_MATCH, FIND_PREV_MATCH,
-    GET_SOURCE, GET_LINES, MOVE_CURSOR, PASS_FD
+    GET_SOURCE, GET_LINES, UPDATE_HOVER, PASS_FD, CONNECT, GOTO_ANCHOR
 
   ContainerCommand* = enum
-    SET_LINES, SET_NEEDS_AUTH, SET_CONTENT_TYPE, SET_REDIRECT, SET_TITLE,
-    SET_HOVER, SET_LOAD_INFO, SET_NUM_LINES, READ_LINE, LOAD_DONE,
-    ANCHOR_FOUND, ANCHOR_FAIL, JUMP, OPEN, BUFFER_READY, SOURCE_READY, RESHAPE
+    BUFFER_READY, RESHAPE, FULFILL_PROMISE
 
   BufferMatch* = object
     success*: bool
@@ -84,6 +82,60 @@ type
     config: BufferConfig
     userstyle: CSSStylesheet
 
+  # async, but worse
+  EmptyPromise = ref object of RootObj
+    cb: (proc())
+    next: EmptyPromise
+    stream: Stream
+
+  Promise*[T] = ref object of EmptyPromise
+    res: T
+
+  BufferInterface* = ref object
+    stream: Stream
+    packetid: int
+    promises: Table[int, EmptyPromise]
+
+proc newBufferInterface*(ostream: Stream): BufferInterface =
+  result = BufferInterface(
+    stream: ostream
+  )
+
+proc fulfill*(iface: BufferInterface, packetid, len: int) =
+  var promise: EmptyPromise
+  if iface.promises.pop(packetid, promise):
+    if promise.stream != nil and promise.cb == nil and len != 0:
+      var abc = alloc(len)
+      var x = 0
+      while x < len:
+        x += promise.stream.readData(abc, len)
+      dealloc(abc)
+    while promise != nil:
+      if promise.cb != nil:
+        promise.cb()
+      promise = promise.next
+
+proc then*(promise: EmptyPromise, cb: (proc())): EmptyPromise {.discardable.} =
+  promise.cb = cb
+  promise.next = EmptyPromise()
+  return promise.next
+
+proc then*[T](promise: Promise[T], cb: (proc(x: T))): EmptyPromise {.discardable.} =
+  return promise.then(proc() =
+    if promise.stream != nil:
+      promise.stream.sread(promise.res)
+    cb(promise.res))
+
+proc then*[T, U](promise: Promise[T], cb: (proc(x: T): Promise[U])): Promise[U] {.discardable.} =
+  let next = Promise[U]()
+  promise.then(proc(x: T) =
+    let p2 = cb(x)
+    if p2 != nil:
+      p2.then(proc(y: U) =
+        next.res = y
+        next.cb()))
+  return next
+
 macro writeCommand(buffer: Buffer, cmd: ContainerCommand, args: varargs[typed]) =
   let cmdblock = newStmtList()
   var idlist: seq[NimNode]
@@ -96,14 +148,15 @@ macro writeCommand(buffer: Buffer, cmd: ContainerCommand, args: varargs[typed])
     inc i
   let lens = ident("lens")
   cmdblock.add(quote do:
+    if `cmd` != BUFFER_READY: return
     var `lens` = slen(`cmd`))
   for id in idlist:
     cmdblock.add(quote do: `lens` += slen(`id`))
-  cmdblock.add(quote do:
-    `buffer`.postream.swrite(`lens`))
+  cmdblock.add(quote do: `buffer`.postream.swrite(`lens`))
   cmdblock.add(quote do: `buffer`.postream.swrite(`cmd`))
   for id in idlist:
-    cmdblock.add(quote do: `buffer`.postream.swrite(`id`))
+    cmdblock.add(quote do:
+      `buffer`.postream.swrite(`id`))
   cmdblock.add(quote do: `buffer`.postream.flush())
   return newBlockStmt(cmdblock)
 
@@ -117,28 +170,45 @@ proc buildInterfaceProc(fun: NimNode): tuple[fun, name: NimNode] =
   if x[^1] == '=':
     x = "SET_" & x[0..^2]
   let nup = ident(x) # add this to enums
-  let this2 = newIdentDefs(ident("stream"), ident("Stream"))
+  let this2 = newIdentDefs(ident("iface"), ident("BufferInterface"))
   let thisval = this2[0]
   body.add(quote do:
-    `thisval`.swrite(BufferCommand.`nup`))
+    `thisval`.stream.swrite(BufferCommand.`nup`)
+    `thisval`.stream.swrite(`thisval`.packetid))
   var params2: seq[NimNode]
-  params2.add(retval)
+  var retval2: NimNode
+  if retval.kind == nnkEmpty:
+    retval2 = ident("EmptyPromise")
+  else:
+    retval2 = newNimNode(nnkBracketExpr).add(
+      ident("Promise"),
+      retval)
+  params2.add(retval2)
   params2.add(this2)
   for i in 2 ..< params.len:
     let param = params[i]
     for i in 0 ..< param.len - 2:
       let id2 = newIdentDefs(ident(param[i].strVal), param[^2])
       params2.add(id2)
-  for c in params2[2..^1]:
-    let s = c[0] # sym e.g. url
+  for i in 2 ..< params2.len:
+    let s = params2[i][0] # sym e.g. url
     body.add(quote do:
-      `thisval`.swrite(`s`))
+      `thisval`.stream.swrite(`s`))
+  body.add(quote do:
+    `thisval`.stream.flush())
   body.add(quote do:
-    `thisval`.flush())
-  if retval.kind != nnkEmpty:
+    `thisval`.promises[`thisval`.packetid] = `retval2`(stream: `thisval`.stream)
+    inc `thisval`.packetid)
+  var pragmas: NimNode
+  if retval.kind == nnkEmpty:
+    body.add(quote do:
+      return `thisval`.promises[`thisval`.packetid - 1])
+    pragmas = newNimNode(nnkPragma).add(ident("discardable"))
+  else:
     body.add(quote do:
-      `thisval`.sread(result))
-  return (newProc(name, params2, body), nup)
+      return `retval2`(`thisval`.promises[`thisval`.packetid - 1]))
+    pragmas = newEmptyNode()
+  return (newProc(name, params2, body, pragmas = pragmas), nup)
 
 type
   ProxyFunction = object
@@ -150,17 +220,31 @@ type
 # Name -> ProxyFunction
 var ProxyFunctions {.compileTime.}: ProxyMap
 
-macro proxy(fun: typed) =
+macro proxy0(fun: untyped) =
+  fun[0] = ident(fun[0].strVal & "_internal")
+  return fun
+
+macro proxy1(fun: typed) =
   let iproc = buildInterfaceProc(fun)
   var pfun: ProxyFunction
   pfun.iname = ident(fun[0].strVal & "_internal")
   pfun.ename = iproc[1]
-  for x in fun[3]: pfun.params.add(x)
+  pfun.params.add(fun[3][0])
+  var params2: seq[NimNode]
+  params2.add(fun[3][0])
+  for i in 1 ..< fun[3].len:
+    let param = fun[3][i]
+    pfun.params.add(param)
+    for i in 0 ..< param.len - 2:
+      let id2 = newIdentDefs(ident(param[i].strVal), param[^2])
+      params2.add(id2)
   ProxyFunctions[fun[0].strVal] = pfun
-  let ifun = newProc(pfun.iname, pfun.params, fun[6])
-  result = newStmtList()
-  result.add(iproc[0])
-  result.add(ifun)
+  return iproc[0]
+
+macro proxy(fun: typed) =
+  quote do:
+    proxy0(`fun`)
+    proxy1(`fun`)
 
 func getLink(node: StyledNode): HTMLAnchorElement =
   if node == nil:
@@ -199,7 +283,7 @@ func cursorBytes(buffer: Buffer, y: int, cc: int): int =
     w += r.width()
   return i
 
-func findPrevLink0(buffer: Buffer, cursorx, cursory: int): tuple[x, y: int] =
+proc findPrevLink*(buffer: Buffer, cursorx, cursory: int): tuple[x, y: int] {.proxy.} =
   let line = buffer.lines[cursory]
   var i = line.findFormatN(cursorx) - 1
   var link: Element = nil
@@ -256,7 +340,7 @@ func findPrevLink0(buffer: Buffer, cursorx, cursory: int): tuple[x, y: int] =
       dec i
   return (-1, -1)
 
-func findNextLink0(buffer: Buffer, cursorx, cursory: int): tuple[x, y: int] =
+proc findNextLink*(buffer: Buffer, cursorx, cursory: int): tuple[x, y: int] {.proxy.} =
   let line = buffer.lines[cursory]
   var i = line.findFormatN(cursorx) - 1
   var link: Element = nil
@@ -282,7 +366,7 @@ func findNextLink0(buffer: Buffer, cursorx, cursory: int): tuple[x, y: int] =
       inc i
   return (-1, -1)
 
-proc findPrevMatch0(buffer: Buffer, regex: Regex, cursorx, cursory: int, wrap: bool): BufferMatch =
+proc findPrevMatch*(buffer: Buffer, regex: Regex, cursorx, cursory: int, wrap: bool): BufferMatch {.proxy.} =
   template return_if_match =
     if res.success and res.captures.len > 0:
       let cap = res.captures[^1]
@@ -309,7 +393,7 @@ proc findPrevMatch0(buffer: Buffer, regex: Regex, cursorx, cursory: int, wrap: b
     return_if_match
     dec y
 
-proc findNextMatch0(buffer: Buffer, regex: Regex, cursorx, cursory: int, wrap: bool): BufferMatch =
+proc findNextMatch*(buffer: Buffer, regex: Regex, cursorx, cursory: int, wrap: bool): BufferMatch {.proxy.} =
   template return_if_match =
     if res.success and res.captures.len > 0:
       let cap = res.captures[0]
@@ -336,25 +420,7 @@ proc findNextMatch0(buffer: Buffer, regex: Regex, cursorx, cursory: int, wrap: b
     return_if_match
     inc y
 
-proc findPrevLink*(buffer: Buffer, cursorx, cursory: int) {.proxy.} =
-  let pl = buffer.findPrevLink0(cursorx, cursory)
-  buffer.writeCommand(JUMP, pl.x, pl.y, 0)
-
-proc findNextLink*(buffer: Buffer, cursorx, cursory: int) {.proxy.} =
-  let pl = buffer.findNextLink0(cursorx, cursory)
-  buffer.writeCommand(JUMP, pl.x, pl.y, 0)
-
-proc findPrevMatch*(buffer: Buffer, cursorx, cursory: int, regex: Regex, wrap: bool) {.proxy.} =
-  let match = buffer.findPrevMatch0(regex, cursorx, cursory, wrap)
-  if match.success:
-    buffer.writeCommand(JUMP, match.x, match.y, match.x + match.str.width() - 1)
-
-proc findNextMatch*(buffer: Buffer, cursorx, cursory: int, regex: Regex, wrap: bool) {.proxy.} =
-  let match = buffer.findNextMatch0(regex, cursorx, cursory, wrap)
-  if match.success:
-    buffer.writeCommand(JUMP, match.x, match.y, match.x + match.str.width() - 1)
-
-proc gotoAnchor(buffer: Buffer) =
+proc gotoAnchor*(buffer: Buffer): tuple[x, y: int] {.proxy.} =
   if buffer.document == nil: return
   let anchor = buffer.document.getElementById(buffer.location.anchor)
   if anchor == nil: return
@@ -363,17 +429,30 @@ proc gotoAnchor(buffer: Buffer) =
     for i in 0 ..< line.formats.len:
       let format = line.formats[i]
       if format.node != nil and anchor in format.node.node:
-        buffer.writeCommand(JUMP, format.pos, y, 0)
-        return
+        return (format.pos, y)
+
+proc do_reshape(buffer: Buffer) =
+  case buffer.contenttype
+  of "text/html":
+    if buffer.viewport == nil:
+      buffer.viewport = Viewport(window: buffer.attrs)
+    let ret = renderDocument(buffer.document, buffer.attrs, buffer.userstyle, buffer.viewport, buffer.prevstyled)
+    buffer.lines = ret[0]
+    buffer.prevstyled = ret[1]
+  else:
+    buffer.lines = renderPlainText(buffer.source)
 
 proc windowChange*(buffer: Buffer, attrs: WindowAttributes) {.proxy.} =
   buffer.attrs = attrs
   buffer.viewport = Viewport(window: buffer.attrs)
   buffer.width = buffer.attrs.width
   buffer.height = buffer.attrs.height - 1
-  buffer.reshape = true
 
-proc updateHover(buffer: Buffer, cursorx, cursory: int) =
+type UpdateHoverResult* = object
+  hover*: Option[string]
+  repaint*: bool
+
+proc updateHover*(buffer: Buffer, cursorx, cursory: int): UpdateHoverResult {.proxy.} =
   var thisnode: StyledNode
   let i = buffer.lines[cursory].findFormatN(cursorx) - 1
   if i >= 0:
@@ -387,12 +466,13 @@ proc updateHover(buffer: Buffer, cursorx, cursory: int) =
         if not elem.hover:
           elem.hover = true
           buffer.reshape = true
+          result.repaint = true
 
     let link = thisnode.getLink()
     if link != nil:
-      buffer.writeCommand(SET_HOVER, link.href)
+      result.hover = some(link.href)
     else:
-      buffer.writeCommand(SET_HOVER, "")
+      result.hover = some("")
 
     for styledNode in prevnode.branch:
       if styledNode.t == STYLED_ELEMENT and styledNode.node != nil:
@@ -400,12 +480,10 @@ proc updateHover(buffer: Buffer, cursorx, cursory: int) =
         if elem.hover:
           elem.hover = false
           buffer.reshape = true
+          result.repaint = true
 
   buffer.prevnode = thisnode
 
-proc moveCursor*(buffer: Buffer, cursorx, cursory: int) {.proxy.} =
-  buffer.updateHover(cursorx, cursory)
-
 proc loadResource(buffer: Buffer, document: Document, elem: HTMLLinkElement) =
   let url = parseUrl(elem.href, document.location.some)
   if url.isSome:
@@ -437,7 +515,8 @@ proc loadResources(buffer: Buffer, document: Document) =
 proc c_setvbuf(f: File, buf: pointer, mode: cint, size: csize_t): cint {.
   importc: "setvbuf", header: "<stdio.h>", tags: [].}
 
-func getFd(buffer: Buffer): FileHandle =
+func getFd(buffer: Buffer): int =
+  if buffer.streamclosed: return -1
   let source = buffer.bsource
   case source.t
   of CLONE, LOAD_REQUEST:
@@ -446,8 +525,12 @@ func getFd(buffer: Buffer): FileHandle =
   of LOAD_PIPE:
     return buffer.bsource.fd
 
-proc setupSource(buffer: Buffer): int =
-  if buffer.loaded: return -2
+type ConnectResult* = tuple[code: int, needsAuth: bool, redirect: Option[URL], contentType: string] 
+
+proc setupSource(buffer: Buffer): ConnectResult =
+  if buffer.loaded:
+    result.code = -2
+    return
   let source = buffer.bsource
   let setct = source.contenttype.isNone
   if not setct:
@@ -456,13 +539,16 @@ proc setupSource(buffer: Buffer): int =
   case source.t
   of CLONE:
     buffer.istream = connectSocketStream(source.clonepid)
-    if buffer.istream == nil: return -2
+    if buffer.istream == nil:
+      result.code = -2
+      return
     if setct:
       buffer.contenttype = "text/plain"
   of LOAD_PIPE:
     var f: File
     if not open(f, source.fd, fmRead):
-      return 1
+      result.code = 1
+      return
     discard c_setvbuf(f, nil, IONBF, 0)
     buffer.istream = newFileStream(f)
     if setct:
@@ -471,20 +557,19 @@ proc setupSource(buffer: Buffer): int =
     let request = source.request
     let response = buffer.loader.doRequest(request)
     if response.body == nil:
-      return response.res
+      result.code = response.res
+      return
     if setct:
       buffer.contenttype = response.contenttype
     buffer.istream = response.body
-    if response.status == 401: # Unauthorized
-      buffer.writeCommand(SET_NEEDS_AUTH)
-    if response.redirect.isSome:
-      buffer.writeCommand(SET_REDIRECT, response.redirect.get)
+    result.needsAuth = response.status == 401 # Unauthorized
+    result.redirect = response.redirect
   if setct:
-    buffer.writeCommand(SET_CONTENT_TYPE, buffer.contenttype)
+    result.contentType = buffer.contenttype
   buffer.selector.registerHandle(cast[int](buffer.getFd()), {Read}, 1)
   buffer.loaded = true
 
-proc load0(buffer: Buffer) =
+proc load0(buffer: Buffer): auto =
   if buffer.contenttype == "text/html":
     if not buffer.streamclosed:
       buffer.source = buffer.istream.readAll()
@@ -494,40 +579,27 @@ proc load0(buffer: Buffer) =
       buffer.streamclosed = true
     else:
       buffer.document = parseHTML5(newStringStream(buffer.source))
-    buffer.writeCommand(SET_TITLE, buffer.document.title)
     buffer.document.location = buffer.location
     buffer.loadResources(buffer.document)
+    return (true, buffer.document.title)
+  return (false, "")
 
-proc load*(buffer: Buffer) {.proxy.} =
-  buffer.writeCommand(SET_LOAD_INFO, CONNECT)
+proc connect*(buffer: Buffer): ConnectResult {.proxy.} =
   let code = buffer.setupSource()
-  if code != -2:
-    buffer.writeCommand(SET_LOAD_INFO, DOWNLOAD)
-    buffer.load0()
-  buffer.writeCommand(LOAD_DONE, code)
+  return code
 
-proc do_reshape(buffer: Buffer) =
-  case buffer.contenttype
-  of "text/html":
-    if buffer.viewport == nil:
-      buffer.viewport = Viewport(window: buffer.attrs)
-    let ret = renderDocument(buffer.document, buffer.attrs, buffer.userstyle, buffer.viewport, buffer.prevstyled)
-    buffer.lines = ret[0]
-    buffer.prevstyled = ret[1]
-  else:
-    buffer.lines = renderPlainText(buffer.source)
+proc load*(buffer: Buffer): tuple[success: bool, title: string] {.proxy.} =
+  return buffer.load0()
 
-proc render*(buffer: Buffer) {.proxy.} =
-  buffer.writeCommand(SET_LOAD_INFO, LoadInfo.RENDER)
+proc render*(buffer: Buffer): int {.proxy.} =
   buffer.do_reshape()
-  buffer.writeCommand(SET_LOAD_INFO, DONE)
-  buffer.writeCommand(SET_NUM_LINES, buffer.lines.len)
-  buffer.gotoAnchor()
+  return buffer.lines.len
 
 proc load2(buffer: Buffer) =
   case buffer.contenttype
   of "text/html":
-    assert false, "Not implemented yet..."
+    #assert false, "Not implemented yet..."
+    discard
   else:
     # This is incredibly stupid but it works so whatever.
     # (We're basically recv'ing single bytes, but nim std/net does buffering
@@ -535,10 +607,10 @@ proc load2(buffer: Buffer) =
     if not buffer.streamclosed:
       let c = buffer.istream.readChar()
       buffer.source &= c
-      buffer.reshape = true
+      buffer.do_reshape()
 
 proc finishLoad(buffer: Buffer) =
-  if not buffer.streamclosed:
+  if buffer.contenttype != "text/html" and not buffer.streamclosed:
     if not buffer.istream.atEnd:
       let a = buffer.istream.readAll()
       buffer.sstream.write(a)
@@ -726,21 +798,25 @@ template restore_focus =
     buffer.document.focus = nil
     buffer.reshape = true
 
-proc readSuccess*(buffer: Buffer, s: string) {.proxy.} =
+type ReadSuccessResult* = tuple[open: Option[Request], reshape: bool]
+
+proc readSuccess*(buffer: Buffer, s: string): ReadSuccessResult {.proxy.} =
   if buffer.input != nil:
     let input = buffer.input
     case input.inputType
     of INPUT_SEARCH:
       input.value = s
       input.invalid = true
+      result.reshape = true
       buffer.reshape = true
       if input.form != nil:
         let submitaction = submitForm(input.form, input)
         if submitaction.isSome:
-          buffer.writeCommand(OPEN, submitaction.get)
+          result.open = submitaction
     of INPUT_TEXT, INPUT_PASSWORD:
       input.value = s
       input.invalid = true
+      result.reshape = true
       buffer.reshape = true
     of INPUT_FILE:
       let cdir = parseUrl("file://" & getCurrentDir() & DirSep)
@@ -748,11 +824,22 @@ proc readSuccess*(buffer: Buffer, s: string) {.proxy.} =
       if path.issome:
         input.file = path
         input.invalid = true
+        result.reshape = true
         buffer.reshape = true
     else: discard
     buffer.input = nil
 
-proc click*(buffer: Buffer, cursorx, cursory: int) {.proxy.} =
+type ReadLineResult* = object
+  prompt*: string
+  value*: string
+  hide*: bool
+
+type ClickResult* = object
+  open*: Option[Request]
+  readline*: Option[ReadLineResult]
+  repaint*: bool
+
+proc click*(buffer: Buffer, cursorx, cursory: int): ClickResult {.proxy.} =
   let clickable = buffer.getCursorClickable(cursorx, cursory)
   if clickable != nil:
     case clickable.tagType
@@ -762,7 +849,7 @@ proc click*(buffer: Buffer, cursorx, cursory: int) {.proxy.} =
       restore_focus
       let url = parseUrl(HTMLAnchorElement(clickable).href, clickable.document.baseUrl.some)
       if url.issome:
-        buffer.writeCommand(OPEN, newRequest(url.get, HTTP_GET))
+        result.open = some(newRequest(url.get, HTTP_GET))
     of TAG_OPTION:
       let option = HTMLOptionElement(clickable)
       let select = option.select
@@ -783,19 +870,30 @@ proc click*(buffer: Buffer, cursorx, cursory: int) {.proxy.} =
       case input.inputType
       of INPUT_SEARCH:
         buffer.input = input
-        buffer.writeCommand(READ_LINE, "SEARCH: ", input.value, false)
+        result.readline = some(ReadLineResult(
+          prompt: "SEARCH: ",
+          value: input.value
+        ))
       of INPUT_TEXT, INPUT_PASSWORD:
         buffer.input = input
-        buffer.writeCommand(READ_LINE, "TEXT: ", input.value, input.inputType == INPUT_PASSWORD)
+        result.readline = some(ReadLineResult(
+          prompt: "TEXT: ",
+          value: input.value,
+          hide: input.inputType == INPUT_PASSWORD
+        ))
       of INPUT_FILE:
         var path = if input.file.issome:
           input.file.get.path.serialize_unicode()
         else:
           ""
-        buffer.writeCommand(READ_LINE, "Filename: ", path, false)
+        result.readline = some(ReadLineResult(
+          prompt: "Filename: ",
+          value: path
+        ))
       of INPUT_CHECKBOX:
         input.checked = not input.checked
         input.invalid = true
+        result.repaint = true
         buffer.reshape = true
       of INPUT_RADIO:
         for radio in input.radiogroup:
@@ -803,16 +901,16 @@ proc click*(buffer: Buffer, cursorx, cursory: int) {.proxy.} =
           radio.invalid = true
         input.checked = true
         input.invalid = true
+        result.repaint = true
         buffer.reshape = true
       of INPUT_RESET:
         if input.form != nil:
           input.form.reset()
+          result.repaint = true
           buffer.reshape = true
       of INPUT_SUBMIT, INPUT_BUTTON:
         if input.form != nil:
-          let submitaction = submitForm(input.form, input)
-          if submitaction.isSome:
-            buffer.writeCommand(OPEN, submitaction.get)
+          result.open = submitForm(input.form, input)
       else:
         restore_focus
     else:
@@ -821,26 +919,19 @@ proc click*(buffer: Buffer, cursorx, cursory: int) {.proxy.} =
 proc readCanceled*(buffer: Buffer) {.proxy.} =
   buffer.input = nil
 
-proc findAnchor*(buffer: Buffer, anchor: string) {.proxy.} =
-  if buffer.document != nil and buffer.document.getElementById(anchor) != nil:
-    buffer.writeCommand(ANCHOR_FOUND)
-  else:
-    buffer.writeCommand(ANCHOR_FAIL)
+proc findAnchor*(buffer: Buffer, anchor: string): bool {.proxy.} =
+  return buffer.document != nil and buffer.document.getElementById(anchor) != nil
 
-proc getLines*(buffer: Buffer, w: Slice[int]) {.proxy.} =
+proc getLines*(buffer: Buffer, w: Slice[int]): seq[SimpleFlexibleLine] {.proxy.} =
   var w = w
   if w.b < 0 or w.b > buffer.lines.high:
     w.b = buffer.lines.high
-  var lens = sizeof(SET_LINES) + sizeof(buffer.lines.len) + sizeof(w)
+  #TODO this is horribly inefficient
   for y in w:
-    lens += slen(buffer.lines[y])
-  buffer.postream.swrite(lens)
-  buffer.postream.swrite(SET_LINES)
-  buffer.postream.swrite(buffer.lines.len)
-  buffer.postream.swrite(w)
-  for y in w:
-    buffer.postream.swrite(buffer.lines[y])
-  buffer.postream.flush()
+    var line = SimpleFlexibleLine(str: buffer.lines[y].str)
+    for f in buffer.lines[y].formats:
+      line.formats.add(SimpleFormatCell(format: f.format, pos: f.pos))
+    result.add(line)
 
 proc passFd*(buffer: Buffer) {.proxy.} =
   let fd = SocketStream(buffer.pistream).recvFileHandle()
@@ -848,7 +939,6 @@ proc passFd*(buffer: Buffer) {.proxy.} =
 
 proc getSource*(buffer: Buffer) {.proxy.} =
   let ssock = initServerSocket()
-  buffer.writeCommand(SOURCE_READY)
   let stream = ssock.acceptSocketStream()
   if not buffer.streamclosed:
     buffer.source = buffer.istream.readAll()
@@ -857,7 +947,7 @@ proc getSource*(buffer: Buffer) {.proxy.} =
   stream.close()
   ssock.close()
 
-macro bufferDispatcher(funs: static ProxyMap, buffer: Buffer, cmd: BufferCommand) =
+macro bufferDispatcher(funs: static ProxyMap, buffer: Buffer, cmd: BufferCommand, packetid: int) =
   let switch = newNimNode(nnkCaseStmt)
   switch.add(ident("cmd"))
   for k, v in funs:
@@ -874,7 +964,28 @@ macro bufferDispatcher(funs: static ProxyMap, buffer: Buffer, cmd: BufferCommand
           var `id`: `typ`
           `buffer`.pistream.sread(`id`))
         call.add(id)
-    stmts.add(call)
+    var rval: NimNode
+    if v.params[0].kind == nnkEmpty:
+      stmts.add(call)
+    else:
+      rval = ident("retval")
+      stmts.add(quote do:
+        let `rval` = `call`)
+    if rval == nil:
+      stmts.add(quote do:
+        let len = slen(FULFILL_PROMISE) + slen(`packetid`)
+        buffer.postream.swrite(len)
+        buffer.postream.swrite(FULFILL_PROMISE)
+        buffer.postream.swrite(`packetid`)
+        buffer.postream.flush())
+    else:
+      stmts.add(quote do:
+        let len = slen(FULFILL_PROMISE) + slen(`packetid`) + slen(`rval`)
+        buffer.postream.swrite(len)
+        buffer.postream.swrite(FULFILL_PROMISE)
+        buffer.postream.swrite(`packetid`)
+        buffer.postream.swrite(`rval`)
+        buffer.postream.flush())
     ofbranch.add(stmts)
     switch.add(ofbranch)
   return switch
@@ -883,7 +994,9 @@ proc readCommand(buffer: Buffer) =
   let istream = buffer.pistream
   var cmd: BufferCommand
   istream.sread(cmd)
-  bufferDispatcher(ProxyFunctions, buffer, cmd)
+  var packetid: int
+  istream.sread(packetid)
+  bufferDispatcher(ProxyFunctions, buffer, cmd, packetid)
 
 proc runBuffer(buffer: Buffer, rfd: int) =
   block loop:
@@ -905,10 +1018,9 @@ proc runBuffer(buffer: Buffer, rfd: int) =
             buffer.finishLoad()
       if not buffer.alive:
         break loop
-      if buffer.reshape:
+      if buffer.reshape and buffer.document != nil: #TODO null check shouldn't be needed?
         buffer.reshape = false
         buffer.do_reshape()
-        buffer.writeCommand(RESHAPE)
   buffer.pistream.close()
   buffer.postream.close()
   buffer.loader.quit()
diff --git a/src/buffer/container.nim b/src/buffer/container.nim
index 2e9d9618..f17267c3 100644
--- a/src/buffer/container.nim
+++ b/src/buffer/container.nim
@@ -52,6 +52,7 @@ type
     clear*: bool
 
   Container* = ref object
+    iface: BufferInterface
     attrs*: WindowAttributes
     width*: int
     height*: int
@@ -81,6 +82,7 @@ type
     redraw*: bool
     cmdvalid: array[ContainerCommand, bool]
     needslines*: bool
+    events*: seq[ContainerEvent]
 
 proc newBuffer*(dispatcher: Dispatcher, config: Config, source: BufferSource, title = ""): Container =
   let attrs = getWindowAttributes(stdout)
@@ -240,20 +242,35 @@ func findHighlights*(container: Container, y: int): seq[Highlight] =
     if y in hl:
       result.add(hl)
 
-proc expect(container: Container, cmd: ContainerCommand) =
-  container.cmdvalid[cmd] = true
+proc triggerEvent(container: Container, event: ContainerEvent) =
+  container.events.add(event)
+
+proc triggerEvent(container: Container, t: ContainerEventType) =
+  container.triggerEvent(ContainerEvent(t: t))
+
+proc updateCursor(container: Container)
 
 proc requestLines*(container: Container, w = container.lineWindow) =
-  container.ostream.getLines(w)
-  container.expect(SET_LINES)
+  container.iface.getLines(w).then(proc(res: seq[SimpleFlexibleLine]) =
+    container.lines.setLen(w.len)
+    container.lineshift = w.a
+    for y in 0 ..< min(res.len, w.len):
+      container.lines[y] = res[y]
+    container.updateCursor()
+    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:
+      container.triggerEvent(UPDATE))
 
 proc redraw*(container: Container) {.jsfunc.} =
   container.redraw = true
 
 proc sendCursorPosition*(container: Container) =
-  container.ostream.moveCursor(container.cursorx, container.cursory)
-  container.expect(SET_HOVER)
-  container.expect(RESHAPE)
+  container.iface.updateHover(container.cursorx, container.cursory).then(proc(res: UpdateHoverResult) =
+    if res.hover.isSome:
+      container.hovertext = res.hover.get
+      container.triggerEvent(STATUS)
+    if res.repaint:
+      container.needslines = true)
 
 proc setFromY*(container: Container, y: int) {.jsfunc.} =
   if container.pos.fromy != y:
@@ -504,49 +521,100 @@ proc popCursorPos*(container: Container, nojump = false) =
     container.needslines = true
 
 proc cursorNextLink*(container: Container) {.jsfunc.} =
-  container.ostream.findNextLink(container.cursorx, container.cursory)
-  container.expect(JUMP)
+  container.iface
+    .findNextLink(container.cursorx, container.cursory)
+    .then(proc(res: tuple[x, y: int]) =
+      container.setCursorXY(res.x, res.y))
 
 proc cursorPrevLink*(container: Container) {.jsfunc.} =
-  container.ostream.findPrevLink(container.cursorx, container.cursory)
-  container.expect(JUMP)
+  container.iface
+    .findPrevLink(container.cursorx, container.cursory)
+    .then(proc(res: tuple[x, y: int]) =
+      container.setCursorXY(res.x, res.y))
+
+proc clearSearchHighlights*(container: Container) =
+  for i in countdown(container.highlights.high, 0):
+    if container.highlights[i].clear:
+      container.highlights.del(i)
 
 proc cursorNextMatch*(container: Container, regex: Regex, wrap: bool) {.jsfunc.} =
-  container.ostream.findNextMatch(container.cursorx, container.cursory, regex, wrap)
-  container.expect(JUMP)
+  container.iface
+    .findNextMatch(regex, container.cursorx, container.cursory, wrap)
+    .then(proc(res: BufferMatch) =
+      if container.hlon:
+        container.setCursorXY(res.x, res.y)
+        container.clearSearchHighlights()
+        let ex = res.x + res.str.width() - 1
+        let hl = Highlight(x: res.x, y: res.y, endx: ex, endy: res.y, clear: true)
+        container.highlights.add(hl)
+        container.hlon = false)
 
 proc cursorPrevMatch*(container: Container, regex: Regex, wrap: bool) {.jsfunc.} =
-  container.ostream.findPrevMatch(container.cursorx, container.cursory, regex, wrap)
-  container.expect(JUMP)
+  container.iface
+    .findPrevMatch(regex, container.cursorx, container.cursory, wrap)
+    .then(proc(res: BufferMatch) =
+      if container.hlon:
+        container.setCursorXY(res.x, res.y)
+        container.clearSearchHighlights()
+        let ex = res.x + res.str.width() - 1
+        let hl = Highlight(x: res.x, y: res.y, endx: ex, endy: res.y, clear: true)
+        container.highlights.add(hl)
+        container.hlon = false)
+
+proc setLoadInfo(container: Container, msg: string) =
+  container.loadinfo = msg
+  container.triggerEvent(STATUS)
 
 proc load*(container: Container) =
-  container.ostream.load()
-  container.expect(LOAD_DONE)
-  container.expect(SET_LOAD_INFO)
-  container.expect(SET_NEEDS_AUTH)
-  container.expect(SET_REDIRECT)
-  container.expect(SET_CONTENT_TYPE)
-  container.expect(SET_TITLE)
-  if container.source.location.anchor != "":
-    container.expect(JUMP)
-
-proc gotoAnchor*(container: Container, anchor: string) =
-  container.ostream.findAnchor(anchor)
-  container.expect(ANCHOR_FOUND)
-  container.expect(ANCHOR_FAIL)
+  container.loadinfo = "Connecting to " & $container.source.location
+  container.iface.connect().then(proc(res: ConnectResult): auto =
+    container.code = res.code
+    if res.code != -2:
+      if res.code == 0:
+        if res.needsAuth:
+          container.triggerEvent(NEEDS_AUTH)
+        if res.redirect.isSome:
+          container.triggerEvent(REDIRECT)
+        if res.contentType != "":
+          container.contenttype = some(res.contentType)
+        container.setLoadInfo("Downloading " & $container.source.location)
+        return container.iface.load()
+      else:
+        container.triggerEvent(FAIL)
+  ).then(proc(res: tuple[success: bool, title: string]): auto =
+    if res.success:
+      container.title = res.title
+      container.setLoadInfo("Rendering " & $container.source.location)
+      return container.iface.render()
+  ).then(proc(lines: int): auto =
+    container.numLines = lines
+    container.setLoadInfo("")
+    container.needslines = true
+    return container.iface.gotoAnchor()
+  ).then(proc(res: tuple[x, y: int]) =
+    container.setCursorXY(res.x, res.y)
+  )
+
+proc findAnchor*(container: Container, anchor: string) =
+  container.iface.findAnchor(anchor).then(proc(found: bool) =
+    if found:
+      container.triggerEvent(ANCHOR)
+    else:
+      container.triggerEvent(NO_ANCHOR))
 
 proc readCanceled*(container: Container) =
-  container.ostream.readCanceled()
+  container.iface.readCanceled()
 
 proc readSuccess*(container: Container, s: string) =
-  container.ostream.readSuccess(s)
-  container.expect(OPEN)
-  container.expect(RESHAPE)
+  container.iface.readSuccess(s).then(proc(res: ReadSuccessResult) =
+    if res.reshape:
+      container.needslines = true
+    if res.open.isSome:
+      container.triggerEvent(ContainerEvent(t: OPEN, request: res.open.get)))
 
 proc reshape*(container: Container, noreq = false) {.jsfunc.} =
-  container.ostream.render()
-  container.expect(SET_NUM_LINES)
-  container.expect(JUMP)
+  container.iface.render().then(proc(lines: int) =
+    container.numLines = lines)
   if not noreq:
     container.needslines = true
 
@@ -558,30 +626,36 @@ proc dupeBuffer*(dispatcher: Dispatcher, container: Container, config: Config, l
     clonepid: container.process,
   )
   container.pipeto = dispatcher.newBuffer(config, source, container.title)
-  container.ostream.getSource()
-  container.expect(SOURCE_READY)
+  container.iface.getSource().then(proc() =
+    if container.pipeto != nil:
+      container.pipeto.load()
+      container.pipeto = nil)
   return container.pipeto
 
 proc click*(container: Container) {.jsfunc.} =
-  container.ostream.click(container.cursorx, container.cursory)
-  container.expect(OPEN)
-  container.expect(READ_LINE)
-  container.expect(RESHAPE)
+  container.iface.click(container.cursorx, container.cursory).then(proc(res: ClickResult) =
+    if res.repaint:
+      container.needslines = true
+    if res.open.isSome:
+      container.triggerEvent(ContainerEvent(t: OPEN, request: res.open.get))
+    if res.readline.isSome:
+      let rl = res.readline.get
+      container.triggerEvent(
+        ContainerEvent(
+          t: READ_LINE,
+          prompt: rl.prompt,
+          value: rl.value,
+          password: rl.hide)))
 
 proc windowChange*(container: Container, attrs: WindowAttributes) =
   container.attrs = attrs
   container.width = attrs.width
   container.height = attrs.height - 1
-  container.ostream.windowChange(attrs)
-  container.expect(RESHAPE)
-
-proc clearSearchHighlights*(container: Container) =
-  for i in countdown(container.highlights.high, 0):
-    if container.highlights[i].clear:
-      container.highlights.del(i)
+  container.iface.windowChange(attrs).then(proc() =
+    container.needslines = true)
 
 proc handleCommand(container: Container, cmd: ContainerCommand, len: int): ContainerEvent =
-  if not container.cmdvalid[cmd] and cmd != SET_LINES:
+  if not container.cmdvalid[cmd] and false or cmd notin {FULFILL_PROMISE, BUFFER_READY}:
     let len = len - sizeof(cmd)
     #TODO TODO TODO
     for i in 0 ..< len:
@@ -590,106 +664,28 @@ proc handleCommand(container: Container, cmd: ContainerCommand, len: int): Conta
       return ContainerEvent(t: INVALID_COMMAND)
   container.cmdvalid[cmd] = false
   case cmd
-  of SET_LOAD_INFO:
-    var li: LoadInfo
-    container.istream.sread(li)
-    case li
-    of CONNECT:
-      container.loadinfo = "Connecting to " & $container.source.location
-      container.expect(SET_LOAD_INFO)
-    of DOWNLOAD:
-      container.loadinfo = "Downloading " & $container.source.location
-      container.expect(SET_LOAD_INFO)
-    of RENDER:
-      container.loadinfo = "Rendering " & $container.source.location
-      container.expect(SET_LOAD_INFO)
-    of DONE:
-      container.loadinfo = ""
-    return ContainerEvent(t: STATUS)
-  of SET_LINES:
-    var w: Slice[int]
-    container.istream.sread(container.numLines)
-    container.istream.sread(w)
-    container.lines.setLen(w.len)
-    container.lineshift = w.a
-    for y in 0 ..< w.len:
-      container.istream.sread(container.lines[y])
-    container.updateCursor()
-    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:
-      return ContainerEvent(t: UPDATE)
-  of SET_NUM_LINES:
-    container.istream.sread(container.numLines)
-  of SET_NEEDS_AUTH:
-    return ContainerEvent(t: NEEDS_AUTH)
-  of SET_CONTENT_TYPE:
-    var ctype: string
-    container.istream.sread(ctype, 128)
-    container.contenttype = some(ctype)
-  of SET_REDIRECT:
-    var redirect: URL
-    container.istream.sread(redirect)
-    if redirect != nil:
-      container.redirect = some(redirect)
-      return ContainerEvent(t: REDIRECT)
-  of SET_TITLE:
-    container.istream.sread(container.title)
-    return ContainerEvent(t: STATUS)
-  of SET_HOVER:
-    container.istream.sread(container.hovertext)
-    return ContainerEvent(t: STATUS)
-  of LOAD_DONE:
-    container.istream.sread(container.code)
-    if container.code == -2: return
-    if container.code != 0:
-      return ContainerEvent(t: FAIL)
-    return ContainerEvent(t: SUCCESS)
-  of ANCHOR_FOUND:
-    return ContainerEvent(t: ANCHOR)
-  of ANCHOR_FAIL:
-    return ContainerEvent(t: FAIL)
-  of READ_LINE:
-    var prompt, str: string
-    var pwd: bool
-    container.istream.sread(prompt, 1024)
-    container.istream.sread(str, 1024)
-    container.istream.sread(pwd)
-    container.cmdvalid[OPEN] = false
-    return ContainerEvent(t: READ_LINE, prompt: prompt, value: str, password: pwd)
-  of JUMP:
-    var x, y, ex: int
-    container.istream.sread(x)
-    container.istream.sread(y)
-    container.istream.sread(ex)
-    if x != -1 and y != -1:
-      if container.hlon:
-        container.clearSearchHighlights()
-        let hl = Highlight(x: x, y: y, endx: ex, endy: y, clear: true)
-        container.highlights.add(hl)
-        container.hlon = false
-      container.setCursorXY(x, y)
-  of OPEN:
-    var request: Request
-    container.istream.sread(request)
-    container.cmdvalid[READ_LINE] = false
-    return ContainerEvent(t: OPEN, request: request)
   of BUFFER_READY:
     if container.source.t == LOAD_PIPE:
-      container.ostream.passFd()
+      container.iface.passFd()
       let s = SocketStream(container.ostream)
       s.sendFileHandle(container.source.fd)
       discard close(container.source.fd)
       container.ostream.flush()
     container.load()
-  of SOURCE_READY:
-    if container.pipeto != nil:
-      container.pipeto.load()
-      container.pipeto = nil
   of RESHAPE:
     container.needslines = true
+  of FULFILL_PROMISE:
+    var packetid: int
+    container.istream.sread(packetid)
+    container.iface.fulfill(packetid, len - slen(packetid) - slen(FULFILL_PROMISE))
   if container.needslines:
-    container.expect(SET_LINES)
     container.requestLines()
+    container.needslines = false
+
+proc setStream*(container: Container, stream: Stream) =
+  container.istream = stream
+  container.ostream = stream
+  container.iface = newBufferInterface(stream)
 
 # Synchronously read all lines in the buffer.
 iterator readLines*(container: Container): SimpleFlexibleLine {.inline.} =
@@ -698,11 +694,12 @@ iterator readLines*(container: Container): SimpleFlexibleLine {.inline.} =
   var len: int
   container.istream.sread(len)
   container.istream.sread(cmd)
-  while cmd != SET_LINES:
-    discard container.handleCommand(cmd, len)
-    container.istream.sread(len)
-    container.istream.sread(cmd)
-  assert cmd == SET_LINES
+  #TODO TODO TODO
+  #while cmd != SET_LINES:
+  #  discard container.handleCommand(cmd, len)
+  #  container.istream.sread(len)
+  #  container.istream.sread(cmd)
+  #assert cmd == SET_LINES
   var w: Slice[int]
   container.istream.sread(container.numLines)
   container.istream.sread(w)
diff --git a/src/display/client.nim b/src/display/client.nim
index 36761d56..24ea8183 100644
--- a/src/display/client.nim
+++ b/src/display/client.nim
@@ -243,8 +243,7 @@ proc acceptBuffers(client: Client) =
     if pid in client.pager.procmap:
       let container = client.pager.procmap[pid]
       client.pager.procmap.del(pid)
-      container.istream = stream
-      container.ostream = stream
+      container.setStream(stream)
       let fd = stream.source.getFd()
       client.fdmap[int(fd)] = container
       client.selector.registerHandle(fd, {Read}, nil)
@@ -314,7 +313,8 @@ proc writeFile(client: Client, path: string, content: string) {.jsfunc.} =
 
 proc newConsole(pager: Pager, tty: File): Console =
   new(result)
-  if tty != nil:
+  result.tty = tty
+  if tty != nil and false:
     var pipefd: array[0..1, cint]
     if pipe(pipefd) == -1:
       raise newException(Defect, "Failed to open console pipe.")
diff --git a/src/display/pager.nim b/src/display/pager.nim
index 119b5002..7bd50e6f 100644
--- a/src/display/pager.nim
+++ b/src/display/pager.nim
@@ -8,7 +8,6 @@ import unicode
 when defined(posix):
   import posix
 
-import buffer/buffer
 import buffer/cell
 import buffer/container
 import config/config
@@ -428,7 +427,7 @@ proc gotoURL*(pager: Pager, request: Request, prevurl = none(URL), ctype = none(
     pager.addContainer(container)
   else:
     pager.container.redirect = some(request.url)
-    pager.container.gotoAnchor(request.url.anchor)
+    pager.container.findAnchor(request.url.anchor)
 
 # When the user has passed a partial URL as an argument, they might've meant
 # either:
@@ -580,12 +579,7 @@ proc click(pager: Pager) {.jsfunc.} =
 proc authorize*(pager: Pager) =
   pager.setLineEdit(readLine("Username: ", pager.attrs.width, term = pager.term), USERNAME)
 
-proc handleEvent*(pager: Pager, container: Container): bool =
-  var event: ContainerEvent
-  try:
-    event = container.handleEvent()
-  except IOError:
-    return false
+proc handleEvent0*(pager: Pager, container: Container, event: ContainerEvent): bool =
   case event.t
   of FAIL:
     pager.deleteContainer(container)
@@ -637,5 +631,19 @@ proc handleEvent*(pager: Pager, container: Container): bool =
   of NO_EVENT: discard
   return true
 
+proc handleEvent*(pager: Pager, container: Container): bool =
+  var event: ContainerEvent
+  try:
+    event = container.handleEvent()
+  except IOError:
+    return false
+  if not pager.handleEvent0(container, event):
+    return false
+  while container.events.len > 0:
+    let event = container.events.pop()
+    if not pager.handleEvent0(container, event):
+      return false
+  return true
+
 proc addPagerModule*(ctx: JSContext) =
   ctx.registerType(Pager)
diff --git a/src/ips/serialize.nim b/src/ips/serialize.nim
index 9de0e374..2647db2f 100644
--- a/src/ips/serialize.nim
+++ b/src/ips/serialize.nim
@@ -14,7 +14,7 @@ import types/url
 
 proc slen*[T](o: T): int =
   when T is string:
-    return sizeof(o.len) + o.len
+    return slen(o.len) + o.len
   elif T is bool:
     return sizeof(char)
   elif T is URL:
@@ -83,6 +83,9 @@ proc slen*[T](o: T): int =
     of LOAD_PIPE: result += slen(o.fd)
     result += slen(o.location)
     result += slen(o.contenttype)
+  elif T is tuple:
+    for f in o.fields:
+      result += slen(f)
   else:
     result += sizeof(o)
 
@@ -182,6 +185,14 @@ proc swrite*(stream: Stream, source: BufferSource) =
 proc swrite*(stream: Stream, bconfig: BufferConfig) =
   stream.swrite(bconfig.userstyle)
 
+proc swrite*(stream: Stream, tup: tuple) =
+  for f in tup.fields:
+    stream.swrite(f)
+
+proc swrite*(stream: Stream, obj: object) =
+  for f in obj.fields:
+    stream.swrite(f)
+
 template sread*[T](stream: Stream, o: T) =
   stream.read(o)
 
@@ -314,3 +325,11 @@ proc sread*(stream: Stream, source: var BufferSource) =
 
 proc sread*(stream: Stream, bconfig: var BufferConfig) =
   stream.sread(bconfig.userstyle)
+
+proc sread*(stream: Stream, obj: var object) =
+  for f in obj.fields:
+    stream.sread(f)
+
+proc sread*(stream: Stream, tup: var tuple) =
+  for f in tup.fields:
+    stream.sread(f)
diff --git a/src/utils/eprint.nim b/src/utils/eprint.nim
index c5936d2a..6692ccf5 100644
--- a/src/utils/eprint.nim
+++ b/src/utils/eprint.nim
@@ -2,8 +2,8 @@
 
 template eprint*(s: varargs[string, `$`]) = {.cast(noSideEffect), cast(tags: []), cast(raises: []).}:
   var a = false
+  var o = ""
   when nimVm:
-    var o = ""
     for x in s:
       if not a:
         a = true
@@ -16,6 +16,7 @@ template eprint*(s: varargs[string, `$`]) = {.cast(noSideEffect), cast(tags: [])
       if not a:
         a = true
       else:
-        stderr.write(' ')
-      stderr.write(x)
-    stderr.write('\n')
+        o &= ' '
+      o &= x
+    o &= '\n'
+    stderr.write(o)