about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--res/config.toml3
-rw-r--r--src/buffer/buffer.nim201
-rw-r--r--src/buffer/cell.nim44
-rw-r--r--src/buffer/container.nim138
-rw-r--r--src/display/client.nim37
-rw-r--r--src/display/pager.nim46
-rw-r--r--src/ips/socketstream.nim9
-rw-r--r--src/layout/engine.nim8
-rw-r--r--src/main.nim4
-rw-r--r--src/render/rendertext.nim52
-rw-r--r--src/utils/twtstr.nim13
11 files changed, 302 insertions, 253 deletions
diff --git a/res/config.toml b/res/config.toml
index 5fb2dfdf..b9a37d75 100644
--- a/res/config.toml
+++ b/res/config.toml
@@ -44,11 +44,12 @@ K = 'pager.scrollUp()'
 ')' = 'pager.scrollRight()'
 C-m = 'pager.click()'
 C-j = 'pager.click()'
-C-l = 'pager.changeLocation()'
 M-u = 'pager.dupeBuffer()'
+C-l = 'pager.load()'
 U = 'pager.reload()'
 r = 'pager.redraw()'
 R = 'pager.reshape()'
+C-cC-c = 'pager.cancel()'
 g = 'pager.cursorFirstLine()'
 G = 'pager.cursorLastLine()'
 z = 'pager.centerLine()'
diff --git a/src/buffer/buffer.nim b/src/buffer/buffer.nim
index 458b5b17..a2860952 100644
--- a/src/buffer/buffer.nim
+++ b/src/buffer/buffer.nim
@@ -44,9 +44,6 @@ type
     CLICK, FIND_NEXT_LINK, FIND_PREV_LINK, FIND_NEXT_MATCH, FIND_PREV_MATCH,
     GET_SOURCE, GET_LINES, UPDATE_HOVER, PASS_FD, CONNECT, GOTO_ANCHOR
 
-  ContainerCommand* = enum
-    BUFFER_READY, RESHAPE, FULFILL_PROMISE
-
   BufferMatch* = object
     success*: bool
     x*: int
@@ -59,14 +56,13 @@ type
     contenttype: string
     lines: FlexibleGrid
     rendered: bool
-    bsource: BufferSource
+    source: BufferSource
     width: int
     height: int
     attrs: WindowAttributes
     document: Document
     viewport: Viewport
     prevstyled: StyledNode
-    reshape: bool
     location: Url
     selector: Selector[int]
     istream: Stream
@@ -74,9 +70,9 @@ type
     available: int
     pistream: Stream # for input pipe
     postream: Stream # for output pipe
+    srenderer: StreamRenderer
     streamclosed: bool
     loaded: bool
-    source: string
     prevnode: StyledNode
     loader: FileLoader
     config: BufferConfig
@@ -115,18 +111,24 @@ proc fulfill*(iface: BufferInterface, packetid, len: int) =
         promise.cb()
       promise = promise.next
 
+proc hasPromises*(iface: BufferInterface): bool =
+  return iface.promises.len > 0
+
 proc then*(promise: EmptyPromise, cb: (proc())): EmptyPromise {.discardable.} =
+  if promise == nil: return
   promise.cb = cb
   promise.next = EmptyPromise()
   return promise.next
 
 proc then*[T](promise: Promise[T], cb: (proc(x: T))): EmptyPromise {.discardable.} =
+  if promise == nil: return
   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.} =
+  if promise == nil: return
   let next = Promise[U]()
   promise.then(proc(x: T) =
     let p2 = cb(x)
@@ -136,30 +138,6 @@ proc then*[T, U](promise: Promise[T], cb: (proc(x: T): Promise[U])): Promise[U]
         next.cb()))
   return next
 
-macro writeCommand(buffer: Buffer, cmd: ContainerCommand, args: varargs[typed]) =
-  let cmdblock = newStmtList()
-  var idlist: seq[NimNode]
-  var i = 0
-  for arg in args:
-    let id = ident("arg_" & $i)
-    idlist.add(id)
-    cmdblock.add(quote do:
-      let `id` = `arg`)
-    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(`cmd`))
-  for id in idlist:
-    cmdblock.add(quote do:
-      `buffer`.postream.swrite(`id`))
-  cmdblock.add(quote do: `buffer`.postream.flush())
-  return newBlockStmt(cmdblock)
-
 proc buildInterfaceProc(fun: NimNode): tuple[fun, name: NimNode] =
   let name = fun[0] # sym
   let params = fun[3] # formalparams
@@ -172,6 +150,7 @@ proc buildInterfaceProc(fun: NimNode): tuple[fun, name: NimNode] =
   let nup = ident(x) # add this to enums
   let this2 = newIdentDefs(ident("iface"), ident("BufferInterface"))
   let thisval = this2[0]
+  let n = name.strVal
   body.add(quote do:
     `thisval`.stream.swrite(BufferCommand.`nup`)
     `thisval`.stream.swrite(`thisval`.packetid))
@@ -430,6 +409,7 @@ proc gotoAnchor*(buffer: Buffer): tuple[x, y: int] {.proxy.} =
       let format = line.formats[i]
       if format.node != nil and anchor in format.node.node:
         return (format.pos, y)
+  return (-1, -1)
 
 proc do_reshape(buffer: Buffer) =
   case buffer.contenttype
@@ -440,7 +420,8 @@ proc do_reshape(buffer: Buffer) =
     buffer.lines = ret[0]
     buffer.prevstyled = ret[1]
   else:
-    buffer.lines = renderPlainText(buffer.source)
+    buffer.lines.renderStream(buffer.srenderer, buffer.available)
+    buffer.available = 0
 
 proc windowChange*(buffer: Buffer, attrs: WindowAttributes) {.proxy.} =
   buffer.attrs = attrs
@@ -453,6 +434,7 @@ type UpdateHoverResult* = object
   repaint*: bool
 
 proc updateHover*(buffer: Buffer, cursorx, cursory: int): UpdateHoverResult {.proxy.} =
+  if buffer.lines.len == 0: return
   var thisnode: StyledNode
   let i = buffer.lines[cursory].findFormatN(cursorx) - 1
   if i >= 0:
@@ -465,7 +447,6 @@ proc updateHover*(buffer: Buffer, cursorx, cursory: int): UpdateHoverResult {.pr
         let elem = Element(styledNode.node)
         if not elem.hover:
           elem.hover = true
-          buffer.reshape = true
           result.repaint = true
 
     let link = thisnode.getLink()
@@ -479,8 +460,9 @@ proc updateHover*(buffer: Buffer, cursorx, cursory: int): UpdateHoverResult {.pr
         let elem = Element(styledNode.node)
         if elem.hover:
           elem.hover = false
-          buffer.reshape = true
           result.repaint = true
+  if result.repaint:
+    buffer.do_reshape()
 
   buffer.prevnode = thisnode
 
@@ -517,13 +499,13 @@ proc c_setvbuf(f: File, buf: pointer, mode: cint, size: csize_t): cint {.
 
 func getFd(buffer: Buffer): int =
   if buffer.streamclosed: return -1
-  let source = buffer.bsource
+  let source = buffer.source
   case source.t
   of CLONE, LOAD_REQUEST:
     let istream = SocketStream(buffer.istream)
     return cast[FileHandle](istream.source.getFd())
   of LOAD_PIPE:
-    return buffer.bsource.fd
+    return buffer.source.fd
 
 type ConnectResult* = tuple[code: int, needsAuth: bool, redirect: Option[URL], contentType: string] 
 
@@ -531,7 +513,7 @@ proc setupSource(buffer: Buffer): ConnectResult =
   if buffer.loaded:
     result.code = -2
     return
-  let source = buffer.bsource
+  let source = buffer.source
   let setct = source.contenttype.isNone
   if not setct:
     buffer.contenttype = source.contenttype.get
@@ -562,6 +544,7 @@ proc setupSource(buffer: Buffer): ConnectResult =
     if setct:
       buffer.contenttype = response.contenttype
     buffer.istream = response.body
+    SocketStream(buffer.istream).recvw = true
     result.needsAuth = response.status == 401 # Unauthorized
     result.redirect = response.redirect
   if setct:
@@ -569,56 +552,55 @@ proc setupSource(buffer: Buffer): ConnectResult =
   buffer.selector.registerHandle(cast[int](buffer.getFd()), {Read}, 1)
   buffer.loaded = true
 
-proc load0(buffer: Buffer): auto =
-  if buffer.contenttype == "text/html":
-    if not buffer.streamclosed:
-      buffer.source = buffer.istream.readAll()
-      buffer.istream.close()
-      buffer.istream = newStringStream(buffer.source)
-      buffer.document = parseHTML5(buffer.istream)
-      buffer.streamclosed = true
-    else:
-      buffer.document = parseHTML5(newStringStream(buffer.source))
-    buffer.document.location = buffer.location
-    buffer.loadResources(buffer.document)
-    return (true, buffer.document.title)
-  return (false, "")
-
 proc connect*(buffer: Buffer): ConnectResult {.proxy.} =
   let code = buffer.setupSource()
   return code
 
-proc load*(buffer: Buffer): tuple[success: bool, title: string] {.proxy.} =
-  return buffer.load0()
+const BufferSize = 4096
+
+proc load*(buffer: Buffer): tuple[atend: bool, lines, bytes: int] {.proxy.} =
+  var bytes = -1
+  if buffer.streamclosed: return (true, buffer.lines.len, bytes)
+  let op = buffer.sstream.getPosition()
+  let s = buffer.istream.readStr(BufferSize)
+  buffer.sstream.setPosition(op + buffer.available)
+  buffer.sstream.write(s)
+  buffer.sstream.setPosition(op)
+  buffer.available += s.len
+  case buffer.contenttype
+  of "text/html":
+    bytes = buffer.available
+  else:
+    buffer.do_reshape()
+  return (buffer.istream.atEnd, buffer.lines.len, bytes)
 
 proc render*(buffer: Buffer): int {.proxy.} =
   buffer.do_reshape()
   return buffer.lines.len
 
-proc load2(buffer: Buffer) =
-  case buffer.contenttype
-  of "text/html":
-    #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
-    # for us so we should be ok?)
-    if not buffer.streamclosed:
-      let c = buffer.istream.readChar()
-      buffer.source &= c
-      buffer.do_reshape()
-
 proc finishLoad(buffer: Buffer) =
-  if buffer.contenttype != "text/html" and not buffer.streamclosed:
-    if not buffer.istream.atEnd:
-      let a = buffer.istream.readAll()
+  if buffer.streamclosed: return
+  if not buffer.istream.atEnd:
+    let op = buffer.sstream.getPosition()
+    buffer.sstream.setPosition(op + buffer.available)
+    while not buffer.istream.atEnd:
+      let a = buffer.istream.readStr(BufferSize)
       buffer.sstream.write(a)
       buffer.available += a.len
-      buffer.reshape = true
-    buffer.selector.unregister(int(buffer.getFd()))
-    buffer.istream.close()
-    buffer.streamclosed = true
+    buffer.sstream.setPosition(op)
+  case buffer.contenttype
+  of "text/html":
+    buffer.sstream.setPosition(0)
+    buffer.available = 0
+    buffer.document = parseHTML5(buffer.sstream)
+    buffer.document.location = buffer.location
+    buffer.loadResources(buffer.document)
+    buffer.do_reshape()
+  else:
+    buffer.do_reshape()
+  buffer.selector.unregister(int(buffer.getFd()))
+  buffer.istream.close()
+  buffer.streamclosed = true
 
 # https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#constructing-the-form-data-set
 proc constructEntryList(form: HTMLFormElement, submitter: Element = nil, encoding: string = ""): seq[tuple[name, value: string]] =
@@ -788,17 +770,19 @@ proc submitForm(form: HTMLFormElement, submitter: Element): Option[Request] =
       assert formmethod == FORM_METHOD_POST
       getActionUrl
 
-template set_focus(e: Element) =
+template set_focus(buffer: Buffer, e: Element) =
   if buffer.document.focus != e:
     buffer.document.focus = e
-    buffer.reshape = true
+    buffer.do_reshape()
 
-template restore_focus =
+template restore_focus(buffer: Buffer) =
   if buffer.document.focus != nil:
     buffer.document.focus = nil
-    buffer.reshape = true
+    buffer.do_reshape()
 
-type ReadSuccessResult* = tuple[open: Option[Request], reshape: bool]
+type ReadSuccessResult* = object
+  open*: Option[Request]
+  repaint*: bool
 
 proc readSuccess*(buffer: Buffer, s: string): ReadSuccessResult {.proxy.} =
   if buffer.input != nil:
@@ -807,8 +791,8 @@ proc readSuccess*(buffer: Buffer, s: string): ReadSuccessResult {.proxy.} =
     of INPUT_SEARCH:
       input.value = s
       input.invalid = true
-      result.reshape = true
-      buffer.reshape = true
+      buffer.do_reshape()
+      result.repaint = true
       if input.form != nil:
         let submitaction = submitForm(input.form, input)
         if submitaction.isSome:
@@ -816,16 +800,16 @@ proc readSuccess*(buffer: Buffer, s: string): ReadSuccessResult {.proxy.} =
     of INPUT_TEXT, INPUT_PASSWORD:
       input.value = s
       input.invalid = true
-      result.reshape = true
-      buffer.reshape = true
+      buffer.do_reshape()
+      result.repaint = true
     of INPUT_FILE:
       let cdir = parseUrl("file://" & getCurrentDir() & DirSep)
       let path = parseUrl(s, cdir)
       if path.issome:
         input.file = path
         input.invalid = true
-        result.reshape = true
-        buffer.reshape = true
+        buffer.do_reshape()
+        result.repaint = true
     else: discard
     buffer.input = nil
 
@@ -844,9 +828,9 @@ proc click*(buffer: Buffer, cursorx, cursory: int): ClickResult {.proxy.} =
   if clickable != nil:
     case clickable.tagType
     of TAG_SELECT:
-      set_focus clickable
+      buffer.set_focus clickable
     of TAG_A:
-      restore_focus
+      buffer.restore_focus
       let url = parseUrl(HTMLAnchorElement(clickable).href, clickable.document.baseUrl.some)
       if url.issome:
         result.open = some(newRequest(url.get, HTTP_GET))
@@ -860,12 +844,12 @@ proc click*(buffer: Buffer, cursorx, cursory: int): ClickResult {.proxy.} =
             for option in select.options:
               option.selected = false
           option.selected = true
-          restore_focus
+          buffer.restore_focus
         else:
           # focus on select
-          set_focus select
+          buffer.set_focus select
     of TAG_INPUT:
-      restore_focus
+      buffer.restore_focus
       let input = HTMLInputElement(clickable)
       case input.inputType
       of INPUT_SEARCH:
@@ -894,7 +878,7 @@ proc click*(buffer: Buffer, cursorx, cursory: int): ClickResult {.proxy.} =
         input.checked = not input.checked
         input.invalid = true
         result.repaint = true
-        buffer.reshape = true
+        buffer.do_reshape()
       of INPUT_RADIO:
         for radio in input.radiogroup:
           radio.checked = false
@@ -902,19 +886,19 @@ proc click*(buffer: Buffer, cursorx, cursory: int): ClickResult {.proxy.} =
         input.checked = true
         input.invalid = true
         result.repaint = true
-        buffer.reshape = true
+        buffer.do_reshape()
       of INPUT_RESET:
         if input.form != nil:
           input.form.reset()
           result.repaint = true
-          buffer.reshape = true
+          buffer.do_reshape()
       of INPUT_SUBMIT, INPUT_BUTTON:
         if input.form != nil:
           result.open = submitForm(input.form, input)
       else:
-        restore_focus
+        buffer.restore_focus
     else:
-      restore_focus
+      buffer.restore_focus
 
 proc readCanceled*(buffer: Buffer) {.proxy.} =
   buffer.input = nil
@@ -935,15 +919,14 @@ proc getLines*(buffer: Buffer, w: Slice[int]): seq[SimpleFlexibleLine] {.proxy.}
 
 proc passFd*(buffer: Buffer) {.proxy.} =
   let fd = SocketStream(buffer.pistream).recvFileHandle()
-  buffer.bsource.fd = fd
+  buffer.source.fd = fd
 
 proc getSource*(buffer: Buffer) {.proxy.} =
   let ssock = initServerSocket()
   let stream = ssock.acceptSocketStream()
-  if not buffer.streamclosed:
-    buffer.source = buffer.istream.readAll()
-    buffer.streamclosed = true
-  stream.write(buffer.source)
+  buffer.finishLoad()
+  buffer.sstream.setPosition(0)
+  stream.write(buffer.sstream.readAll())
   stream.close()
   ssock.close()
 
@@ -973,16 +956,14 @@ macro bufferDispatcher(funs: static ProxyMap, buffer: Buffer, cmd: BufferCommand
         let `rval` = `call`)
     if rval == nil:
       stmts.add(quote do:
-        let len = slen(FULFILL_PROMISE) + slen(`packetid`)
+        let len = 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`)
+        let len = slen(`packetid`) + slen(`rval`)
         buffer.postream.swrite(len)
-        buffer.postream.swrite(FULFILL_PROMISE)
         buffer.postream.swrite(`packetid`)
         buffer.postream.swrite(`rval`)
         buffer.postream.flush())
@@ -1009,18 +990,11 @@ proc runBuffer(buffer: Buffer, rfd: int) =
               buffer.readCommand()
             except IOError:
               break loop
-          else:
-            buffer.load2()
         if Error in event.events:
           if event.fd == rfd:
             break loop
           elif event.fd == buffer.getFd():
             buffer.finishLoad()
-      if not buffer.alive:
-        break loop
-      if buffer.reshape and buffer.document != nil: #TODO null check shouldn't be needed?
-        buffer.reshape = false
-        buffer.do_reshape()
   buffer.pistream.close()
   buffer.postream.close()
   buffer.loader.quit()
@@ -1035,18 +1009,19 @@ proc launchBuffer*(config: BufferConfig, source: BufferSource,
     attrs: attrs,
     config: config,
     loader: loader,
-    bsource: source,
+    source: source,
     sstream: newStringStream(),
     viewport: Viewport(window: attrs),
     width: attrs.width,
     height: attrs.height - 1
   )
   buffer.selector = newSelector[int]()
+  buffer.sstream = newStringStream()
+  buffer.srenderer = newStreamRenderer(buffer.sstream)
   let sstream = connectSocketStream(mainproc, false)
   sstream.swrite(getpid())
   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/cell.nim b/src/buffer/cell.nim
index 7c6ec00a..d7de064f 100644
--- a/src/buffer/cell.nim
+++ b/src/buffer/cell.nim
@@ -270,6 +270,50 @@ proc parseAnsiCode*(format: var Format, buf: string, fi: int): int =
 
   return i
 
+type
+  AnsiCodeParseState* = enum
+    PARSE_START, PARSE_PARAMS, PARSE_INTERM, PARSE_FINAL, PARSE_DONE
+
+  AnsiCodeParser* = object
+    state*: AnsiCodeParseState
+    params: string
+
+proc reset*(parser: var AnsiCodeParser) =
+  parser.state = PARSE_START
+  parser.params = ""
+
+proc parseAnsiCode*(parser: var AnsiCodeParser, format: var Format, c: char): bool =
+  case parser.state
+  of PARSE_START:
+    if 0x40 <= int(c) and int(c) <= 0x5F:
+      if c != '[':
+        #C1, TODO?
+        parser.state = PARSE_DONE
+      else:
+        parser.state = PARSE_PARAMS
+    else:
+      parser.state = PARSE_DONE
+      return true
+  of PARSE_PARAMS:
+    if 0x30 <= int(c) and int(c) <= 0x3F:
+      parser.params &= c
+    else:
+      parser.state = PARSE_INTERM
+      return parser.parseAnsiCode(format, c)
+  of PARSE_INTERM:
+    if 0x20 <= int(c) and int(c) <= 0x2F:
+      discard
+    else:
+      parser.state = PARSE_FINAL
+      return parser.parseAnsiCode(format, c)
+  of PARSE_FINAL:
+    parser.state = PARSE_DONE
+    if 0x40 <= int(c) and int(c) <= 0x7E:
+      format.handleAnsiCode(c, parser.params)
+    else:
+      return true
+  of PARSE_DONE: discard
+
 proc parseAnsiCode*(format: var Format, stream: Stream) =
   if stream.atEnd(): return
   var c = stream.readChar()
diff --git a/src/buffer/container.nim b/src/buffer/container.nim
index f17267c3..58f93f42 100644
--- a/src/buffer/container.nim
+++ b/src/buffer/container.nim
@@ -80,8 +80,8 @@ type
     sourcepair*: Container
     pipeto: Container
     redraw*: bool
-    cmdvalid: array[ContainerCommand, bool]
     needslines*: bool
+    canceled: bool
     events*: seq[ContainerEvent]
 
 proc newBuffer*(dispatcher: Dispatcher, config: Config, source: BufferSource, title = ""): Container =
@@ -99,7 +99,6 @@ proc newBuffer*(dispatcher: Dispatcher, config: Config, source: BufferSource, ti
     height: attrs.height - 1, contenttype: source.contenttype,
     title: title
   )
-  result.cmdvalid[BUFFER_READY] = true
   istream.sread(result.process)
   result.pos.setx = -1
 
@@ -566,34 +565,45 @@ proc setLoadInfo(container: Container, msg: string) =
   container.triggerEvent(STATUS)
 
 proc load*(container: Container) =
-  container.loadinfo = "Connecting to " & $container.source.location
+  container.setLoadInfo("Connecting to " & $container.source.location & "...")
+  var onload: (proc(res: tuple[atend: bool, lines, bytes: int]))
+  onload = (proc(res: tuple[atend: bool, lines, bytes: int]) =
+    if res.bytes == -1:
+      container.setLoadInfo("")
+    elif not res.atend:
+      container.setLoadInfo(convert_size(res.bytes) & " loaded")
+    if res.lines > container.numLines:
+      container.numLines = res.lines
+      container.triggerEvent(STATUS)
+      container.requestLines()
+    if not res.atend and not container.canceled:
+      discard container.iface.load().then(onload)
+    elif not container.canceled:
+      container.iface.gotoAnchor().then(proc(res: tuple[x, y: int]) =
+        if res.x != -1 and res.y != -1:
+          container.setCursorXY(res.x, res.y)
+      )
+  )
   container.iface.connect().then(proc(res: ConnectResult): auto =
-    container.code = res.code
     if res.code != -2:
+      container.code = res.code
       if res.code == 0:
+        container.setLoadInfo("Connected to " & $container.source.location & ". Downloading...")
         if res.needsAuth:
           container.triggerEvent(NEEDS_AUTH)
         if res.redirect.isSome:
+          container.redirect = res.redirect
           container.triggerEvent(REDIRECT)
         if res.contentType != "":
           container.contenttype = some(res.contentType)
-        container.setLoadInfo("Downloading " & $container.source.location)
         return container.iface.load()
       else:
+        container.setLoadInfo("")
         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)
-  )
+  ).then(onload)
+
+proc cancel*(container: Container) =
+  container.canceled = true
 
 proc findAnchor*(container: Container, anchor: string) =
   container.iface.findAnchor(anchor).then(proc(found: bool) =
@@ -607,14 +617,15 @@ proc readCanceled*(container: Container) =
 
 proc readSuccess*(container: Container, s: string) =
   container.iface.readSuccess(s).then(proc(res: ReadSuccessResult) =
-    if res.reshape:
+    if res.repaint:
       container.needslines = true
     if res.open.isSome:
       container.triggerEvent(ContainerEvent(t: OPEN, request: res.open.get)))
 
 proc reshape*(container: Container, noreq = false) {.jsfunc.} =
   container.iface.render().then(proc(lines: int) =
-    container.numLines = lines)
+    container.numLines = lines
+    container.updateCursor())
   if not noreq:
     container.needslines = true
 
@@ -654,69 +665,46 @@ proc windowChange*(container: Container, attrs: WindowAttributes) =
   container.iface.windowChange(attrs).then(proc() =
     container.needslines = true)
 
-proc handleCommand(container: Container, cmd: ContainerCommand, len: int): ContainerEvent =
-  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:
-      discard container.istream.readChar()
-    if cmd != RESHAPE:
-      return ContainerEvent(t: INVALID_COMMAND)
-  container.cmdvalid[cmd] = false
-  case cmd
-  of BUFFER_READY:
-    if container.source.t == LOAD_PIPE:
-      container.iface.passFd()
-      let s = SocketStream(container.ostream)
-      s.sendFileHandle(container.source.fd)
-      discard close(container.source.fd)
-      container.ostream.flush()
-    container.load()
-  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.requestLines()
-    container.needslines = false
+proc handleCommand(container: Container) =
+  var packetid, len: int
+  container.istream.sread(len)
+  container.istream.sread(packetid)
+  container.iface.fulfill(packetid, len - slen(packetid))
 
 proc setStream*(container: Container, stream: Stream) =
   container.istream = stream
   container.ostream = stream
   container.iface = newBufferInterface(stream)
+  if container.source.t == LOAD_PIPE:
+    container.iface.passFd()
+    let s = SocketStream(container.ostream)
+    s.sendFileHandle(container.source.fd)
+    discard close(container.source.fd)
+    container.ostream.flush()
+  container.load()
 
 # Synchronously read all lines in the buffer.
 iterator readLines*(container: Container): SimpleFlexibleLine {.inline.} =
-  var cmd: ContainerCommand
-  container.requestLines(0 .. -1)
-  var len: int
-  container.istream.sread(len)
-  container.istream.sread(cmd)
-  #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)
-  var line: SimpleFlexibleLine
-  for y in 0 ..< w.len:
-    container.istream.sread(line)
-    yield line
-
-proc handleEvent*(container: Container): ContainerEvent =
-  var len: int
-  container.istream.sread(len)
-  var cmd: ContainerCommand
-  container.istream.sread(cmd)
-  if cmd > high(ContainerCommand):
-    return ContainerEvent(t: INVALID_COMMAND)
-  else:
-    return container.handleCommand(cmd, len)
+  while container.iface.hasPromises:
+    # Spin event loop till container has been loaded
+    container.handleCommand()
+  if container.code == 0:
+    # load succeded
+    discard container.iface.getLines(0 .. -1)
+    var plen, len, packetid: int
+    container.istream.sread(plen)
+    container.istream.sread(packetid)
+    container.istream.sread(len)
+    var line: SimpleFlexibleLine
+    for y in 0 ..< len:
+      container.istream.sread(line)
+      yield line
+
+proc handleEvent*(container: Container) =
+  container.handleCommand()
+  if container.needslines:
+    container.requestLines()
+    container.needslines = false
 
 proc addContainerModule*(ctx: JSContext) =
   ctx.registerType(Container, name = "Buffer")
diff --git a/src/display/client.nim b/src/display/client.nim
index 24ea8183..3b56373e 100644
--- a/src/display/client.nim
+++ b/src/display/client.nim
@@ -47,6 +47,7 @@ type
     console {.jsget.}: Console
     pager {.jsget.}: Pager
     line {.jsget.}: LineEdit
+    sevent: seq[Container]
     config: Config
     jsrt: JSRuntime
     jsctx: JSContext
@@ -84,6 +85,7 @@ proc doRequest(client: Client, req: Request): Response {.jsfunc.} =
 
 proc interruptHandler(rt: JSRuntime, opaque: pointer): int {.cdecl.} =
   let client = cast[Client](opaque)
+  if client.console.tty == nil: return
   try:
     let c = client.console.tty.readChar()
     if c == char(3): #C-c
@@ -96,9 +98,11 @@ proc interruptHandler(rt: JSRuntime, opaque: pointer): int {.cdecl.} =
   return 0
 
 proc evalJS(client: Client, src, filename: string): JSObject =
-  unblockStdin(client.console.tty.getFileHandle())
+  if client.console.tty != nil:
+    unblockStdin(client.console.tty.getFileHandle())
   result = client.jsctx.eval(src, filename, JS_EVAL_TYPE_GLOBAL)
-  restoreStdin(client.console.tty.getFileHandle())
+  if client.console.tty != nil:
+    restoreStdin(client.console.tty.getFileHandle())
 
 proc evalJSFree(client: Client, src, filename: string) =
   free(client.evalJS(src, filename))
@@ -235,8 +239,7 @@ proc acceptBuffers(client: Client) =
     else:
       client.pager.procmap.del(pid)
     stream.close()
-  var i = 0
-  while i < client.pager.procmap.len:
+  while client.pager.procmap.len > 0:
     let stream = client.ssock.acceptSocketStream()
     var pid: Pid
     stream.sread(pid)
@@ -247,9 +250,10 @@ proc acceptBuffers(client: Client) =
       let fd = stream.source.getFd()
       client.fdmap[int(fd)] = container
       client.selector.registerHandle(fd, {Read}, nil)
-      inc i
+      client.sevent.add(container)
     else:
-      #TODO print an error?
+      #TODO uh what?
+      eprint "???"
       stream.close()
 
 proc log(console: Console, ss: varargs[string]) {.jsfunc.} =
@@ -264,7 +268,7 @@ proc inputLoop(client: Client) =
   let selector = client.selector
   selector.registerHandle(int(client.console.tty.getFileHandle()), {Read}, nil)
   let sigwinch = selector.registerSignal(int(SIGWINCH), nil)
-  client.acceptBuffers()
+  let redrawtimer = client.selector.registerTimer(1000, false, nil)
   while true:
     let events = client.selector.select(-1)
     for event in events:
@@ -286,9 +290,12 @@ proc inputLoop(client: Client) =
           client.pager.windowChange(client.attrs)
         else: assert false
       if Event.Timer in event.events:
-        if event.fd in client.interval_fdis:
+        if event.fd == redrawtimer:
+          if client.pager.container != nil:
+            client.pager.container.requestLines()
+        elif event.fd in client.interval_fdis:
           client.intervals[client.interval_fdis[event.fd]].handler()
-        if event.fd in client.timeout_fdis:
+        elif event.fd in client.timeout_fdis:
           let id = client.timeout_fdis[event.fd]
           let timeout = client.timeouts[id]
           timeout.handler()
@@ -313,8 +320,7 @@ proc writeFile(client: Client, path: string, content: string) {.jsfunc.} =
 
 proc newConsole(pager: Pager, tty: File): Console =
   new(result)
-  result.tty = tty
-  if tty != nil and false:
+  if tty != nil:
     var pipefd: array[0..1, cint]
     if pipe(pipefd) == -1:
       raise newException(Defect, "Failed to open console pipe.")
@@ -332,14 +338,10 @@ proc newConsole(pager: Pager, tty: File): Console =
     result.err = newFileStream(stderr)
 
 proc dumpBuffers(client: Client) =
-  client.acceptBuffers()
-  for container in client.pager.containers:
-    container.load()
   let ostream = newFileStream(stdout)
   for container in client.pager.containers:
-    container.reshape(true)
     client.pager.drawBuffer(container, ostream)
-  client.pager.dumpAlerts()
+    discard client.pager.handleEvents(container)
   stdout.close()
 
 proc launchClient*(client: Client, pages: seq[string], ctype: Option[string], dump: bool) =
@@ -372,7 +374,8 @@ proc launchClient*(client: Client, pages: seq[string], ctype: Option[string], du
 
   for page in pages:
     client.pager.loadURL(page, ctype = ctype)
-
+  client.acceptBuffers()
+  client.pager.refreshStatusMsg()
   if not dump:
     client.inputLoop()
   else:
diff --git a/src/display/pager.nim b/src/display/pager.nim
index 7bd50e6f..a50eb9a8 100644
--- a/src/display/pager.nim
+++ b/src/display/pager.nim
@@ -59,7 +59,8 @@ iterator containers*(pager: Pager): Container =
     var stack: seq[Container]
     stack.add(c)
     while stack.len > 0:
-      yield stack.pop()
+      c = stack.pop()
+      yield c
       for i in countdown(c.children.high, 0):
         stack.add(c.children[i])
 
@@ -152,7 +153,7 @@ proc launchPager*(pager: Pager, tty: File) =
 
 proc dumpAlerts*(pager: Pager) =
   for msg in pager.alerts:
-    eprint msg
+    eprint "cha: " & msg
 
 proc quit*(pager: Pager, code = 0) =
   pager.term.quit()
@@ -231,6 +232,7 @@ proc writeStatusMessage(pager: Pager, str: string, format: Format = Format()) =
 proc refreshStatusMsg*(pager: Pager) =
   let container = pager.container
   if container == nil: return
+  if pager.tty == nil: return
   if container.loadinfo != "":
     pager.writeStatusMessage(container.loadinfo)
   elif pager.alerts.len > 0:
@@ -343,10 +345,7 @@ proc lineInfo(pager: Pager) {.jsfunc.} =
   pager.alert(pager.container.lineInfo())
 
 proc deleteContainer(pager: Pager, container: Container) =
-  if container.parent == nil and
-      container.children.len == 0 and
-      container != pager.container:
-    return
+  container.cancel()
   if container.sourcepair != nil:
     container.sourcepair.sourcepair = nil
     container.sourcepair = nil
@@ -565,28 +564,37 @@ proc updateReadLine*(pager: Pager) =
       pager.clearLineEdit()
 
 # Open a URL prompt and visit the specified URL.
-proc changeLocation(pager: Pager) {.jsfunc.} =
-  var url = pager.container.source.location.serialize()
-  pager.setLineEdit(readLine("URL: ", pager.attrs.width, current = url, term = pager.term), LOCATION)
+proc load(pager: Pager, s = "") {.jsfunc.} =
+  if s.len > 0 and s[^1] == '\n':
+    pager.loadURL(s)
+  else:
+    var url = s
+    if url == "":
+      url = pager.container.source.location.serialize()
+    pager.setLineEdit(readLine("URL: ", pager.attrs.width, current = url, term = pager.term), LOCATION)
 
 # Reload the page in a new buffer, then kill the previous buffer.
 proc reload(pager: Pager) {.jsfunc.} =
   pager.gotoURL(newRequest(pager.container.source.location), none(URL), pager.container.contenttype, pager.container)
 
+# Cancel loading current page (if exists).
+proc cancel(pager: Pager) {.jsfunc.} =
+  pager.container.cancel()
+
 proc click(pager: Pager) {.jsfunc.} =
   pager.container.click()
 
 proc authorize*(pager: Pager) =
   pager.setLineEdit(readLine("Username: ", pager.attrs.width, term = pager.term), USERNAME)
 
-proc handleEvent0*(pager: Pager, container: Container, event: ContainerEvent): bool =
+proc handleEvent0(pager: Pager, container: Container, event: ContainerEvent): bool =
   case event.t
   of FAIL:
     pager.deleteContainer(container)
     if container.retry.len > 0:
       pager.gotoURL(newRequest(container.retry.pop()), ctype = container.contenttype)
     else:
-      pager.alert("Couldn't load " & $container.source.location & " (error code " & $container.code & ")")
+      pager.alert("Can't load " & $container.source.location & " (error code " & $container.code & ")")
       pager.refreshStatusMsg()
     if pager.container == nil:
       return false
@@ -631,19 +639,19 @@ proc handleEvent0*(pager: Pager, container: Container, event: ContainerEvent): b
   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
+proc handleEvents*(pager: Pager, container: Container): bool =
   while container.events.len > 0:
     let event = container.events.pop()
     if not pager.handleEvent0(container, event):
       return false
   return true
 
+proc handleEvent*(pager: Pager, container: Container): bool =
+  try:
+    container.handleEvent()
+  except IOError:
+    return false
+  return pager.handleEvents(container)
+
 proc addPagerModule*(ctx: JSContext) =
   ctx.registerType(Pager)
diff --git a/src/ips/socketstream.nim b/src/ips/socketstream.nim
index 6ce9bd88..6df9f09b 100644
--- a/src/ips/socketstream.nim
+++ b/src/ips/socketstream.nim
@@ -10,11 +10,18 @@ import ips/serversocket
 
 type SocketStream* = ref object of Stream
   source*: Socket
+  recvw*: bool
   isend: bool
 
 proc sockReadData(s: Stream, buffer: pointer, len: int): int =
   let s = SocketStream(s)
-  result = s.source.recv(buffer, len)
+  try:
+    if s.recvw:
+      result = s.source.recv(buffer, len, 100)
+    else:
+      result = s.source.recv(buffer, len)
+  except TimeoutError:
+    return
   if result < 0:
     raise newException(IOError, "Failed to read data (code " & $osLastError() & ")")
   elif result < len:
diff --git a/src/layout/engine.nim b/src/layout/engine.nim
index d1ce9f27..408c3b70 100644
--- a/src/layout/engine.nim
+++ b/src/layout/engine.nim
@@ -83,7 +83,7 @@ proc horizontalAlignLine(ictx: InlineContext, line: LineBox, computed: CSSComput
     let x = max(maxwidth, line.width) - line.width
     for atom in line.atoms:
       atom.offset.x += x
-  of TEXT_ALIGN_CENTER:
+  of TEXT_ALIGN_CENTER, TEXT_ALIGN_CHA_CENTER:
     let x = max((max(maxwidth - line.offset.x, line.width)) div 2 - line.width div 2, 0)
     for atom in line.atoms:
       atom.offset.x += x
@@ -107,8 +107,6 @@ proc horizontalAlignLine(ictx: InlineContext, line: LineBox, computed: CSSComput
             let atom = InlineSpacing(atom)
             atom.width = spacingwidth
           line.width += atom.width
-  else:
-    discard
 
 # Align atoms (inline boxes, text, etc.) vertically inside the line.
 proc verticalAlignLine(ictx: InlineContext) =
@@ -686,7 +684,6 @@ iterator rows(builder: TableBoxBuilder): BoxBuilder {.inline.} =
   var body: seq[TableRowBoxBuilder]
   var footer: seq[TableRowBoxBuilder]
   var caption: TableCaptionBoxBuilder
-  #TODO this should be done 
   for child in builder.children:
     assert child.computed{"display"} in ProperTableChild, $child.computed{"display"}
     case child.computed{"display"}
@@ -708,7 +705,8 @@ iterator rows(builder: TableBoxBuilder): BoxBuilder {.inline.} =
       if caption == nil:
         caption = TableCaptionBoxBuilder(child)
     else: discard
-  yield caption
+  if caption != nil:
+    yield caption
   for child in header:
     yield child
   for child in body:
diff --git a/src/main.nim b/src/main.nim
index 63c52947..ad978fa6 100644
--- a/src/main.nim
+++ b/src/main.nim
@@ -40,6 +40,7 @@ Options:
     -T, --type <type>           Specify content mime type
     -v, --version               Print version information
     -h, --help                  Print this page
+    -r, --run <script/file>     Run passed script or file
     --                          Interpret all following arguments as URLs"""
   if i == 0:
     echo s
@@ -90,6 +91,7 @@ while i < params.len:
     inc i
     if i < params.len:
       conf.startup = params[i]
+      conf.headless = true
       dump = true
     else:
       help(1)
@@ -102,7 +104,7 @@ while i < params.len:
       pages.add(param)
   inc i
 
-if pages.len == 0 and conf.startup == "":
+if pages.len == 0 and not conf.headless:
   if stdin.isatty:
     help(1)
 
diff --git a/src/render/rendertext.nim b/src/render/rendertext.nim
index d9cc4dfc..675c4ddb 100644
--- a/src/render/rendertext.nim
+++ b/src/render/rendertext.nim
@@ -3,6 +3,7 @@ import streams
 import buffer/cell
 import utils/twtstr
 
+const tabwidth = 8
 proc renderPlainText*(text: string): FlexibleGrid =
   var format = newFormat()
   template add_format() =
@@ -11,7 +12,6 @@ proc renderPlainText*(text: string): FlexibleGrid =
       result[result.high].addFormat(result[^1].str.len, format)
 
   result.addLine()
-  const tabwidth = 8
   var spaces = 0
   var i = 0
   var af = false
@@ -46,20 +46,34 @@ 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*(grid: var FlexibleGrid, stream: Stream, len: int) =
-  var format = newFormat()
+type StreamRenderer* = object
+  spaces: int
+  stream*: Stream
+  ansiparser: AnsiCodeParser
+  format: Format
+  af: bool
+
+proc newStreamRenderer*(stream: Stream): StreamRenderer =
+  result.format = newFormat()
+  result.ansiparser.state = PARSE_DONE
+  result.stream = stream
+
+proc renderStream*(grid: var FlexibleGrid, renderer: var StreamRenderer, len: int) =
   template add_format() =
-    if af:
-      af = false
-      grid[grid.high].addFormat(grid[^1].str.len, format)
+    if renderer.af:
+      renderer.af = false
+      grid[grid.high].addFormat(grid[^1].str.len, renderer.format)
 
   if grid.len == 0: grid.addLine()
-  const tabwidth = 8
-  var spaces = 0
-  var af = false
   var i = 0
-  while i < len:
-    let c = stream.readChar()
+  while i < len and not renderer.stream.atEnd:
+    let c = renderer.stream.readChar()
+    if renderer.ansiparser.state != PARSE_DONE:
+      let cancel = renderer.ansiparser.parseAnsiCode(renderer.format, c)
+      if not cancel:
+        if renderer.ansiparser.state == PARSE_DONE:
+          renderer.af = true
+        continue
     case c
     of '\n':
       add_format
@@ -67,18 +81,17 @@ proc renderStream*(grid: var FlexibleGrid, stream: Stream, len: int) =
     of '\r': discard
     of '\t':
       add_format
-      for i in 0 ..< tabwidth - spaces:
+      for i in 0 ..< tabwidth - renderer.spaces:
         grid[^1].str &= ' '
-        spaces = 0
+        renderer.spaces = 0
     of ' ':
       add_format
       grid[^1].str &= c
-      inc spaces
-      if spaces == 8:
-        spaces = 0
+      inc renderer.spaces
+      if renderer.spaces == 8:
+        renderer.spaces = 0
     of '\e':
-      format.parseAnsiCode(stream)
-      af = true
+      renderer.ansiparser.reset()
     elif c.isControlChar():
       add_format
       grid[^1].str &= '^' & c.getControlLetter()
@@ -86,6 +99,3 @@ proc renderStream*(grid: var FlexibleGrid, stream: Stream, len: int) =
       add_format
       grid[^1].str &= c
     inc i
-
-  #if grid.len > 1 and grid[^1].str.len == 0 and grid[^1].formats.len == 0:
-  #  discard grid.pop()
diff --git a/src/utils/twtstr.nim b/src/utils/twtstr.nim
index 2123daeb..3d33f8d1 100644
--- a/src/utils/twtstr.nim
+++ b/src/utils/twtstr.nim
@@ -224,6 +224,19 @@ func skipBlanks*(buf: string, at: int): int =
   while result < buf.len and buf[result].isWhitespace():
     inc result
 
+# From w3m
+const SizeUnit = [
+  "b", "kb", "Mb", "Gb", "Tb", "Pb", "Eb", "Zb", "Bb", "Yb"
+]
+func convert_size*(len: int): string =
+  var sizepos = 0
+  var csize = float(len)
+  while csize >= 999.495 and sizepos < SizeUnit.len:
+    csize = csize / 1024.0
+    inc sizepos
+  result = $(floor(csize * 100 + 0.5) / 100)
+  result &= SizeUnit[sizepos]
+
 func until*(s: string, c: set[char]): string =
   var i = 0
   while i < s.len: