about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2022-11-25 00:32:54 +0100
committerbptato <nincsnevem662@gmail.com>2022-11-25 00:33:39 +0100
commitb086e346afeded51c94c9b77280dcea6f6b3ce8a (patch)
treee2fd9f29e77ab787f960bbd8ba190a14a6d5d7a0
parent896489a6c500e28f13d0237ab691622cb5c5114f (diff)
downloadchawan-b086e346afeded51c94c9b77280dcea6f6b3ce8a.tar.gz
Buffer improvements
-rw-r--r--src/buffer/buffer.nim33
-rw-r--r--src/buffer/container.nim112
-rw-r--r--src/config/bufferconfig.nim7
-rw-r--r--src/display/client.nim47
-rw-r--r--src/display/pager.nim4
-rw-r--r--src/ips/serialize.nim84
-rw-r--r--src/ips/socketstream.nim42
-rw-r--r--src/render/rendertext.nim24
-rw-r--r--src/types/color.nim2
9 files changed, 274 insertions, 81 deletions
diff --git a/src/buffer/buffer.nim b/src/buffer/buffer.nim
index 260fc8ea..0c30ea92 100644
--- a/src/buffer/buffer.nim
+++ b/src/buffer/buffer.nim
@@ -39,7 +39,7 @@ type
   BufferCommand* = enum
     LOAD, RENDER, WINDOW_CHANGE, GOTO_ANCHOR, READ_SUCCESS, READ_CANCELED,
     CLICK, FIND_NEXT_LINK, FIND_PREV_LINK, FIND_NEXT_MATCH, FIND_PREV_MATCH,
-    GET_SOURCE, GET_LINES, MOVE_CURSOR
+    GET_SOURCE, GET_LINES, MOVE_CURSOR, PASS_FD
 
   ContainerCommand* = enum
     SET_LINES, SET_NEEDS_AUTH, SET_CONTENT_TYPE, SET_REDIRECT, SET_TITLE,
@@ -68,6 +68,8 @@ type
     location: Url
     selector: Selector[int]
     istream: Stream
+    sstream: Stream
+    available: int
     pistream: Stream # for input pipe
     postream: Stream # for output pipe
     streamclosed: bool
@@ -76,9 +78,17 @@ type
     prevnode: StyledNode
     loader: FileLoader
     config: BufferConfig
+    userstyle: CSSStylesheet
 
 macro writeCommand(buffer: Buffer, cmd: ContainerCommand, args: varargs[typed]) =
   result = newStmtList()
+  let lens = ident("lens")
+  result.add(quote do:
+    var `lens` = slen(`cmd`))
+  for arg in args:
+    result.add(quote do: `lens` += slen(`arg`))
+  result.add(quote do:
+    `buffer`.postream.swrite(`lens`))
   result.add(quote do: `buffer`.postream.swrite(`cmd`))
   for arg in args:
     result.add(quote do: `buffer`.postream.swrite(`arg`))
@@ -413,7 +423,7 @@ proc render(buffer: Buffer) =
   of "text/html":
     if buffer.viewport == nil:
       buffer.viewport = Viewport(window: buffer.attrs)
-    let ret = renderDocument(buffer.document, buffer.attrs, buffer.config.userstyle, buffer.viewport, buffer.prevstyled)
+    let ret = renderDocument(buffer.document, buffer.attrs, buffer.userstyle, buffer.viewport, buffer.prevstyled)
     buffer.lines = ret[0]
     buffer.prevstyled = ret[1]
   else:
@@ -428,14 +438,15 @@ proc load2(buffer: Buffer) =
     # (We're basically recv'ing single bytes, but nim std/net does buffering
     # for us so we should be ok?)
     if not buffer.streamclosed:
-      let c = buffer.istream.readChar()
-      buffer.source &= c
+      buffer.source &= buffer.istream.readChar()
       buffer.reshape = true
 
 proc finishLoad(buffer: Buffer) =
   if not buffer.streamclosed:
     if not buffer.istream.atEnd:
-      buffer.source &= buffer.istream.readAll()
+      let a = buffer.istream.readAll()
+      buffer.sstream.write(a)
+      buffer.available += a.len
       buffer.reshape = true
     buffer.selector.unregister(int(buffer.getFd()))
     buffer.istream.close()
@@ -717,6 +728,9 @@ proc readCommand(buffer: Buffer) =
   var cmd: BufferCommand
   istream.sread(cmd)
   case cmd
+  of PASS_FD:
+    let fd = SocketStream(istream).recvFileHandle()
+    buffer.bsource.fd = fd
   of LOAD:
     let code = buffer.setupSource()
     buffer.load()
@@ -736,6 +750,10 @@ proc readCommand(buffer: Buffer) =
     istream.sread(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)
+    for y in w:
+      lens += slen(buffer.lines[y])
+    ostream.swrite(lens)
     ostream.swrite(SET_LINES)
     ostream.swrite(buffer.lines.len)
     ostream.swrite(w)
@@ -837,18 +855,19 @@ proc launchBuffer*(config: BufferConfig, source: BufferSource,
                    attrs: WindowAttributes, loader: FileLoader,
                    mainproc: Pid) =
   let buffer = new Buffer
+  buffer.userstyle = parseStylesheet(config.userstyle)
   buffer.attrs = attrs
   buffer.windowChange()
+  buffer.sstream = newStringStream()
   buffer.config = config
   buffer.loader = loader
   buffer.bsource = source
   buffer.selector = newSelector[int]()
   let sstream = connectSocketStream(mainproc, false)
   sstream.swrite(getpid())
-  sstream.swrite(BUFFER_READY)
-  sstream.flush()
   buffer.pistream = sstream
   buffer.postream = sstream
+  buffer.writeCommand(BUFFER_READY)
   let rfd = int(sstream.source.getFd())
   buffer.selector.registerHandle(rfd, {Read}, 0)
   buffer.runBuffer(rfd)
diff --git a/src/buffer/container.nim b/src/buffer/container.nim
index 4241e886..cd321b48 100644
--- a/src/buffer/container.nim
+++ b/src/buffer/container.nim
@@ -15,6 +15,7 @@ import io/request
 import io/window
 import ips/forkserver
 import ips/serialize
+import ips/socketstream
 import js/javascript
 import js/regex
 import types/buffersource
@@ -33,7 +34,7 @@ type
 
   ContainerEventType* = enum
     NO_EVENT, FAIL, SUCCESS, NEEDS_AUTH, REDIRECT, ANCHOR, NO_ANCHOR, UPDATE,
-    READ_LINE, OPEN
+    READ_LINE, OPEN, INVALID_COMMAND
 
   ContainerEvent* = object
     case t*: ContainerEventType
@@ -76,12 +77,11 @@ type
     retry*: seq[URL]
     redirect*: Option[URL]
     ispipe: bool
-    jump: bool
     hlon*: bool
-    waitfor: bool
     pipeto: Container
     redraw*: bool
     sourceready*: bool
+    cmdvalid: array[ContainerCommand, bool]
 
 proc newBuffer*(dispatcher: Dispatcher, config: Config, source: BufferSource, ispipe = false, autoload = true): Container =
   let attrs = getWindowAttributes(stdout)
@@ -96,8 +96,9 @@ proc newBuffer*(dispatcher: Dispatcher, config: Config, source: BufferSource, is
   result = Container(
     source: source, attrs: attrs, width: attrs.width,
     height: attrs.height - 1, contenttype: source.contenttype,
-    ispipe: ispipe, waitfor: true
+    ispipe: ispipe
   )
+  result.cmdvalid[BUFFER_READY] = true
   istream.sread(result.process)
   result.pos.setx = -1
 
@@ -249,12 +250,21 @@ macro writeCommand(container: Container, cmd: BufferCommand, args: varargs[typed
     result.add(quote do: `container`.ostream.swrite(`arg`))
   result.add(quote do: `container`.ostream.flush())
 
+proc expect(container: Container, cmd: ContainerCommand) =
+  container.cmdvalid[cmd] = true
+
 proc requestLines*(container: Container, w = container.lineWindow) =
   container.writeCommand(GET_LINES, w)
+  container.expect(SET_LINES)
 
 proc redraw*(container: Container) {.jsfunc.} =
   container.redraw = true
 
+proc sendCursorPosition*(container: Container) =
+  container.writeCommand(MOVE_CURSOR, container.cursorx, container.cursory)
+  container.expect(SET_HOVER)
+  container.expect(RESHAPE)
+
 proc setFromY*(container: Container, y: int) {.jsfunc.} =
   if container.pos.fromy != y:
     container.pos.fromy = max(min(y, container.maxfromy), 0)
@@ -266,7 +276,7 @@ proc setFromX*(container: Container, x: int) {.jsfunc.} =
     container.pos.fromx = max(min(x, container.maxfromx), 0)
     if container.pos.fromx > container.cursorx:
       container.pos.cursorx = min(container.pos.fromx, container.currentLineWidth())
-      container.writeCommand(MOVE_CURSOR, container.cursorx, container.cursory)
+      container.sendCursorPosition()
     container.redraw = true
 
 proc setFromXY*(container: Container, x, y: int) {.jsfunc.} =
@@ -293,7 +303,7 @@ proc setCursorX*(container: Container, x: int, refresh = true, save = true) {.js
   elif x < container.cursorx:
     container.setFromX(x)
     container.pos.cursorx = x
-  container.writeCommand(MOVE_CURSOR, container.cursorx, container.cursory)
+  container.sendCursorPosition()
   if save:
     container.pos.xend = container.cursorx
 
@@ -311,7 +321,7 @@ proc setCursorY*(container: Container, y: int) {.jsfunc.} =
     else:
       container.setFromY(y)
     container.pos.cursory = y
-  container.writeCommand(MOVE_CURSOR, container.cursorx, container.cursory)
+  container.sendCursorPosition()
   container.restoreCursorX()
 
 proc centerLine*(container: Container) {.jsfunc.} =
@@ -496,15 +506,11 @@ proc updateCursor(container: Container) =
 proc pushCursorPos*(container: Container) =
   container.bpos.add(container.pos)
 
-proc sendCursorPosition*(container: Container) =
-  container.writeCommand(MOVE_CURSOR, container.cursorx, container.cursory)
-  container.requestLines()
-
 proc popCursorPos*(container: Container, nojump = false) =
   container.pos = container.bpos.pop()
   container.updateCursor()
   if not nojump:
-    container.writeCommand(MOVE_CURSOR, container.cursorx, container.cursory)
+    container.sendCursorPosition()
     container.requestLines()
 
 macro proxy(fun: typed) =
@@ -535,28 +541,40 @@ macro proxy(fun: typed) =
 
 proc cursorNextLink*(container: Container) {.jsfunc.} =
   container.writeCommand(FIND_NEXT_LINK, container.cursorx, container.cursory)
-  container.jump = true
+  container.expect(JUMP)
 
 proc cursorPrevLink*(container: Container) {.jsfunc.} =
   container.writeCommand(FIND_PREV_LINK, container.cursorx, container.cursory)
-  container.jump = true
+  container.expect(JUMP)
 
 proc cursorNextMatch*(container: Container, regex: Regex, wrap: bool) {.jsfunc.} =
   container.writeCommand(FIND_NEXT_MATCH, container.cursorx, container.cursory, regex, wrap)
-  container.jump = true
+  container.expect(JUMP)
 
 proc cursorPrevMatch*(container: Container, regex: Regex, wrap: bool) {.jsfunc.} =
   container.writeCommand(FIND_PREV_MATCH, container.cursorx, container.cursory, regex, wrap)
-  container.jump = true
+  container.expect(JUMP)
+
+proc load*(container: Container) =
+  container.writeCommand(LOAD)
+  container.expect(LOAD_DONE)
+  container.expect(SET_NEEDS_AUTH)
+  container.expect(SET_REDIRECT)
+  container.expect(SET_CONTENT_TYPE)
+  container.expect(SET_TITLE)
+
+proc gotoAnchor*(container: Container, anchor: string) =
+  container.writeCommand(GOTO_ANCHOR, anchor)
+  container.expect(ANCHOR_FOUND)
+  container.expect(ANCHOR_FAIL)
 
-proc load*(container: Container) {.proxy.} = discard
-proc gotoAnchor*(container: Container, anchor: string) {.proxy.} = discard
 proc readCanceled*(container: Container) {.proxy.} = discard
 proc readSuccess*(container: Container, s: string) {.proxy.} = discard
 
 proc reshape*(container: Container, noreq = false) {.jsfunc.} =
   container.writeCommand(RENDER)
-  container.jump = true # may jump to anchor
+  container.expect(RESHAPE)
+  container.expect(JUMP)
   if not noreq:
     container.requestLines()
 
@@ -569,23 +587,35 @@ proc dupeBuffer*(dispatcher: Dispatcher, container: Container, config: Config, l
   )
   container.pipeto = dispatcher.newBuffer(config, source, container.ispipe)
   container.writeCommand(GET_SOURCE)
+  container.expect(SOURCE_READY)
   return container.pipeto
 
 proc click*(container: Container) {.jsfunc.} =
   container.writeCommand(CLICK, container.cursorx, container.cursory)
+  container.expect(OPEN)
+  container.expect(READ_LINE)
+  container.expect(RESHAPE)
 
 proc windowChange*(container: Container, attrs: WindowAttributes) =
   container.attrs = attrs
   container.width = attrs.width
   container.height = attrs.height - 1
   container.writeCommand(WINDOW_CHANGE, 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)
 
-proc handleCommand(container: Container, cmd: ContainerCommand): ContainerEvent =
+proc handleCommand(container: Container, cmd: ContainerCommand, len: int): ContainerEvent =
+  if not container.cmdvalid[cmd]:
+    let len = len - sizeof(cmd)
+    #TODO TODO TODO this is very dumb
+    for i in 0 ..< len:
+      discard container.istream.readChar()
+    return ContainerEvent(t: INVALID_COMMAND)
+  container.cmdvalid[cmd] = false
   case cmd
   of SET_LINES:
     var w: Slice[int]
@@ -603,13 +633,14 @@ proc handleCommand(container: Container, cmd: ContainerCommand): ContainerEvent
     return ContainerEvent(t: NEEDS_AUTH)
   of SET_CONTENT_TYPE:
     var ctype: string
-    container.istream.sread(ctype)
+    container.istream.sread(ctype, 128)
     container.contenttype = some(ctype)
   of SET_REDIRECT:
     var redirect: URL
     container.istream.sread(redirect)
-    container.redirect = some(redirect)
-    return ContainerEvent(t: REDIRECT)
+    if redirect != nil:
+      container.redirect = some(redirect)
+      return ContainerEvent(t: REDIRECT)
   of SET_TITLE:
     container.istream.sread(container.title)
   of SET_HOVER:
@@ -621,40 +652,49 @@ proc handleCommand(container: Container, cmd: ContainerCommand): ContainerEvent
       return ContainerEvent(t: FAIL)
     return ContainerEvent(t: SUCCESS)
   of ANCHOR_FOUND:
+    container.cmdvalid[ANCHOR_FAIL] = false
     return ContainerEvent(t: ANCHOR)
   of ANCHOR_FAIL:
+    container.cmdvalid[ANCHOR_FOUND] = false
     return ContainerEvent(t: FAIL)
   of READ_LINE:
     var prompt, str: string
     var pwd: bool
-    container.istream.sread(prompt)
-    container.istream.sread(str)
+    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 and container.jump:
+    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)
-      container.jump = false
   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.waitfor:
-      container.waitfor = false
-      container.load()
+    if container.source.t == LOAD_PIPE:
+      container.ostream.swrite(PASS_FD)
+      container.ostream.flush()
+      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.requestLines()
 
@@ -662,9 +702,12 @@ proc handleCommand(container: Container, cmd: ContainerCommand): ContainerEvent
 iterator readLines*(container: Container): SimpleFlexibleLine {.inline.} =
   var cmd: ContainerCommand
   container.requestLines(0 .. -1)
+  var len: int
+  container.istream.sread(len)
   container.istream.sread(cmd)
   while cmd != SET_LINES:
-    discard container.handleCommand(cmd)
+    discard container.handleCommand(cmd, len)
+    container.istream.sread(len)
     container.istream.sread(cmd)
   assert cmd == SET_LINES
   var w: Slice[int]
@@ -676,9 +719,14 @@ iterator readLines*(container: Container): SimpleFlexibleLine {.inline.} =
     yield line
 
 proc handleEvent*(container: Container): ContainerEvent =
+  var len: int
+  container.istream.sread(len)
   var cmd: ContainerCommand
   container.istream.sread(cmd)
-  return container.handleCommand(cmd)
+  if cmd > high(ContainerCommand):
+    return ContainerEvent(t: INVALID_COMMAND)
+  else:
+    return container.handleCommand(cmd, len)
 
 proc addContainerModule*(ctx: JSContext) =
   ctx.registerType(Container, name = "Buffer")
diff --git a/src/config/bufferconfig.nim b/src/config/bufferconfig.nim
index ee776363..6e8d19e6 100644
--- a/src/config/bufferconfig.nim
+++ b/src/config/bufferconfig.nim
@@ -1,9 +1,8 @@
 import config/config
 import css/sheet
 
-type BufferConfig* = ref object
-  userstyle*: CSSStylesheet
+type BufferConfig* = object
+  userstyle*: string
 
 proc loadBufferConfig*(config: Config): BufferConfig =
-  new(result)
-  result.userstyle = parseStylesheet(config.stylesheet)
+  result.userstyle = config.stylesheet
diff --git a/src/display/client.nim b/src/display/client.nim
index bd7f3977..f4acaee4 100644
--- a/src/display/client.nim
+++ b/src/display/client.nim
@@ -242,26 +242,31 @@ proc inputLoop(client: Client) =
     client.acceptBuffers()
     let events = client.selector.select(-1)
     for event in events:
-      if event.fd == client.console.tty.getFileHandle():
-        client.input()
-        stdout.flushFile()
-      elif event.fd in client.interval_fdis:
-        client.intervals[client.interval_fdis[event.fd]].handler()
-      elif event.fd in client.timeout_fdis:
-        let id = client.timeout_fdis[event.fd]
-        let timeout = client.timeouts[id]
-        timeout.handler()
-        client.clearTimeout(id)
-      elif event.fd == sigwinch:
-        client.attrs = getWindowAttributes(client.console.tty)
-        client.pager.windowChange(client.attrs)
-      else:
-        let container = client.fdmap[event.fd]
-        if not client.pager.handleEvent(container):
-          disableRawMode()
-          for msg in client.pager.status:
-            eprint msg
-          client.quit(1)
+      if Read in event.events:
+        if event.fd == client.console.tty.getFileHandle():
+          client.input()
+          stdout.flushFile()
+        elif event.fd in client.interval_fdis:
+          client.intervals[client.interval_fdis[event.fd]].handler()
+        elif event.fd in client.timeout_fdis:
+          let id = client.timeout_fdis[event.fd]
+          let timeout = client.timeouts[id]
+          timeout.handler()
+          client.clearTimeout(id)
+        elif event.fd == sigwinch:
+          client.attrs = getWindowAttributes(client.console.tty)
+          client.pager.windowChange(client.attrs)
+        else:
+          let container = client.fdmap[event.fd]
+          if not client.pager.handleEvent(container):
+            disableRawMode()
+            for msg in client.pager.status:
+              eprint msg
+            client.quit(1)
+      elif Error in event.events:
+        eprint "Error", event
+        #TODO handle errors
+      else: assert false
     if client.pager.scommand != "":
       client.command(client.pager.scommand)
       client.pager.scommand = ""
@@ -286,7 +291,6 @@ proc newConsole(pager: Pager, tty: File): Console =
       raise newException(Defect, "Failed to open console pipe.")
     let url = newURL("javascript:console.show()")
     result.container = pager.readPipe0(some("text/plain"), pipefd[0], option(url))
-    discard close(pipefd[0])
     var f: File
     if not open(f, pipefd[1], fmWrite):
       raise newException(Defect, "Failed to open file for console pipe.")
@@ -333,7 +337,6 @@ proc launchClient*(client: Client, pages: seq[string], ctype: Option[string], du
   client.userstyle = client.config.stylesheet.parseStylesheet()
   if not stdin.isatty:
     client.pager.readPipe(ctype, stdin.getFileHandle())
-    stdin.close()
   else:
     client.console.tty = stdin
 
diff --git a/src/display/pager.nim b/src/display/pager.nim
index 3fdcd504..d04981a0 100644
--- a/src/display/pager.nim
+++ b/src/display/pager.nim
@@ -648,6 +648,10 @@ proc handleEvent*(pager: Pager, container: Container): bool =
       pager.setLineEdit(readLine(event.prompt, pager.statusmsg.width, current = event.value, hide = event.password, config = pager.config, tty = pager.tty), BUFFER)
   of OPEN:
     pager.gotoURL(event.request, some(container.source.location))
+  of INVALID_COMMAND:
+    if container == pager.container:
+      if pager.status.len == 0:
+        pager.setStatusMessage("Invalid command from buffer")
   of NO_EVENT: discard
   return true
 
diff --git a/src/ips/serialize.nim b/src/ips/serialize.nim
index 82003715..2114cec0 100644
--- a/src/ips/serialize.nim
+++ b/src/ips/serialize.nim
@@ -11,10 +11,84 @@ import types/buffersource
 import types/color
 import types/url
 
+proc slen*[T](o: T): int =
+  when T is string:
+    return sizeof(o.len) + o.len
+  elif T is bool:
+    return sizeof(char)
+  elif T is URL:
+    return ($o).slen
+  elif T is seq:
+    result = slen(o.len)
+    for x in o:
+      result += slen(x)
+  elif T is Option:
+    result = slen(o.isSome)
+    if o.isSome:
+      result += slen(o.get)
+  elif T is HeaderList:
+    result += slen(o.table.len)
+    for k, v in o.table:
+      result += slen(k)
+      result += slen(v.len)
+      for s in v:
+        result += slen(s)
+  elif T is MimePart:
+    result += slen(o.isFile)
+    result += slen(o.name)
+    result += slen(o.content)
+    if o.isFile:
+      result += slen(o.filename)
+      result += slen(o.contentType)
+      result += slen(o.fileSize)
+      result += slen(o.isStream)
+  elif T is Request:
+    result += slen(o.httpmethod)
+    result += slen(o.url)
+    result += slen(o.headers)
+    result += slen(o.body)
+    result += slen(o.multipart)
+  elif T is CellColor:
+    result += slen(o.rgb)
+    if o.rgb:
+      result += slen(o.rgbcolor)
+    else:
+      result += slen(o.color)
+  elif T is Format:
+    result += slen(o.fgcolor)
+    result += slen(o.bgcolor)
+    result += slen(o.flags)
+  elif T is SimpleFormatCell:
+    result += slen(o.format)
+    result += slen(o.pos)
+  elif T is SimpleFlexibleLine:
+    result += slen(o.str)
+    result += slen(o.formats)
+  elif T is FormatCell:
+    result += slen(o.format)
+    result += slen(o.pos)
+  elif T is FlexibleLine:
+    result += slen(o.str)
+    result += slen(o.formats)
+  elif T is Regex:
+    result += slen(o.plen)
+    result += o.plen
+    result += slen(o.buf)
+  elif T is BufferSource:
+    result += slen(o.t)
+    case o.t
+    of CLONE: result += slen(o.clonepid)
+    of LOAD_REQUEST: result += slen(o.request)
+    of LOAD_PIPE: result += slen(o.fd)
+    result += slen(o.location)
+    result += slen(o.contenttype)
+  else:
+    result += sizeof(o)
+
 template swrite*[T](stream: Stream, o: T) =
   stream.write(o)
 
-proc swrite*(stream: Stream, s: string) =
+proc swrite*(stream: Stream, s: string, maxlen = 8192) =
   stream.swrite(s.len)
   stream.write(s)
 
@@ -107,9 +181,11 @@ proc swrite*(stream: Stream, source: BufferSource) =
 template sread*[T](stream: Stream, o: T) =
   stream.read(o)
 
-proc sread*(stream: Stream, s: var string) =
+proc sread*(stream: Stream, s: var string, maxlen = 8192) =
   var len: int
   stream.sread(len)
+  if maxlen != -1:
+    len = min(maxlen, len)
   stream.readStr(len, s)
 
 proc sread*(stream: Stream, b: var bool) =
@@ -123,8 +199,8 @@ proc sread*(stream: Stream, b: var bool) =
 
 proc sread*(stream: Stream, url: var Url) =
   var s: string
-  stream.sread(s)
-  url = parseUrl(s).get
+  stream.sread(s, 2048)
+  url = newURL(s)
 
 proc sread*(stream: Stream, headers: var HeaderList) =
   new(headers)
diff --git a/src/ips/socketstream.nim b/src/ips/socketstream.nim
index efc226bd..3918fe2c 100644
--- a/src/ips/socketstream.nim
+++ b/src/ips/socketstream.nim
@@ -30,6 +30,48 @@ proc sockClose(s: Stream) = {.cast(tags: []).}: #...sigh
   let s = SocketStream(s)
   s.source.close()
 
+# See https://stackoverflow.com/a/4491203
+proc sendFileHandle*(s: SocketStream, fd: FileHandle) =
+  var hdr: Tmsghdr
+  var iov: IOVec
+  var cmsgbuf = alloc(CMSG_SPACE(csize_t(sizeof(FileHandle))))
+  var buf = char(0)
+  iov.iov_base = addr buf
+  iov.iov_len = csize_t(1)
+  zeroMem(addr hdr, sizeof(hdr))
+  hdr.msg_iov = addr iov
+  hdr.msg_iovlen = 1
+  hdr.msg_control = cmsgbuf
+  hdr.msg_controllen = CMSG_LEN(csize_t(sizeof(FileHandle)))
+  let cmsg = CMSG_FIRSTHDR(addr hdr)
+  cmsg.cmsg_len = CMSG_LEN(csize_t(sizeof(FileHandle)))
+  cmsg.cmsg_level = SOL_SOCKET
+  cmsg.cmsg_type = SCM_RIGHTS
+  cast[ptr FileHandle](CMSG_DATA(cmsg))[] = fd
+  let n = sendmsg(s.source.getFd(), addr hdr, 0)
+  dealloc(cmsgbuf)
+  assert n == int(iov.iov_len) #TODO remove this
+
+proc recvFileHandle*(s: SocketStream): FileHandle =
+  var iov: IOVec
+  var hdr: Tmsghdr
+  let space = CMSG_SPACE(csize_t(sizeof(FileHandle)))
+  var buf: char
+  var cmsgbuf = alloc(space)
+  iov.iov_base = addr buf
+  iov.iov_len = 1
+  zeroMem(addr hdr, sizeof(hdr))
+  hdr.msg_iov = addr iov
+  hdr.msg_iovlen = 1
+  hdr.msg_control = cmsgbuf
+  hdr.msg_controllen = space
+  let n = recvmsg(s.source.getFd(), addr hdr, 0)
+  assert n != 0, "Unexpected EOF" #TODO remove this
+  assert n > 0, "Failed to receive message " & $osLastError() #TODO remove this
+  var cmsg = CMSG_FIRSTHDR(addr hdr)
+  result = cast[ptr FileHandle](CMSG_DATA(cmsg))[]
+  dealloc(cmsgbuf)
+
 func newSocketStream*(): SocketStream =
   new(result)
   result.readDataImpl = cast[proc (s: Stream, buffer: pointer, bufLen: int): int
diff --git a/src/render/rendertext.nim b/src/render/rendertext.nim
index 7a1c4417..d9cc4dfc 100644
--- a/src/render/rendertext.nim
+++ b/src/render/rendertext.nim
@@ -46,32 +46,33 @@ proc renderPlainText*(text: string): FlexibleGrid =
   if result.len > 1 and result[^1].str.len == 0 and result[^1].formats.len == 0:
     discard result.pop()
 
-proc renderStream*(stream: Stream): FlexibleGrid =
+proc renderStream*(grid: var FlexibleGrid, stream: Stream, len: int) =
   var format = newFormat()
   template add_format() =
     if af:
       af = false
-      result[result.high].addFormat(result[^1].str.len, format)
+      grid[grid.high].addFormat(grid[^1].str.len, format)
 
-  result.addLine()
+  if grid.len == 0: grid.addLine()
   const tabwidth = 8
   var spaces = 0
   var af = false
-  while not stream.atEnd():
+  var i = 0
+  while i < len:
     let c = stream.readChar()
     case c
     of '\n':
       add_format
-      result.addLine()
+      grid.addLine()
     of '\r': discard
     of '\t':
       add_format
       for i in 0 ..< tabwidth - spaces:
-        result[^1].str &= ' '
+        grid[^1].str &= ' '
         spaces = 0
     of ' ':
       add_format
-      result[^1].str &= c
+      grid[^1].str &= c
       inc spaces
       if spaces == 8:
         spaces = 0
@@ -80,10 +81,11 @@ proc renderStream*(stream: Stream): FlexibleGrid =
       af = true
     elif c.isControlChar():
       add_format
-      result[^1].str &= '^' & c.getControlLetter()
+      grid[^1].str &= '^' & c.getControlLetter()
     else:
       add_format
-      result[^1].str &= c
+      grid[^1].str &= c
+    inc i
 
-  if result.len > 1 and result[^1].str.len == 0 and result[^1].formats.len == 0:
-    discard result.pop()
+  #if grid.len > 1 and grid[^1].str.len == 0 and grid[^1].formats.len == 0:
+  #  discard grid.pop()
diff --git a/src/types/color.nim b/src/types/color.nim
index c33e2b5c..9fc00edc 100644
--- a/src/types/color.nim
+++ b/src/types/color.nim
@@ -212,7 +212,7 @@ func parseHexColor*(s: string): Option[RGBAColor] =
   case s.len
   of 6:
     let c = (hexValue(s[0]) shl 20) or (hexValue(s[1]) shl 16) or
-            (hexValue(s[1]) shl 12) or (hexValue(s[3]) shl 8) or
+            (hexValue(s[2]) shl 12) or (hexValue(s[3]) shl 8) or
             (hexValue(s[4]) shl 4) or hexValue(s[5])
     return some(RGBAColor(c))
   of 8: