about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--src/buffer/buffer.nim52
-rw-r--r--src/buffer/container.nim55
-rw-r--r--src/config/bufferconfig.nim1
-rw-r--r--src/config/config.nim2
-rw-r--r--src/config/toml.nim1
-rw-r--r--src/display/client.nim106
-rw-r--r--src/display/pager.nim135
-rw-r--r--src/io/lineedit.nim22
-rw-r--r--src/io/term.nim58
-rw-r--r--src/ips/forkserver.nim1
-rw-r--r--src/ips/socketstream.nim2
11 files changed, 243 insertions, 192 deletions
diff --git a/src/buffer/buffer.nim b/src/buffer/buffer.nim
index 0c30ea92..2acf0e6c 100644
--- a/src/buffer/buffer.nim
+++ b/src/buffer/buffer.nim
@@ -36,6 +36,9 @@ import types/url
 import utils/twtstr
 
 type
+  LoadInfo* = enum
+    CONNECT, DOWNLOAD, RENDER, DONE
+
   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,
@@ -43,8 +46,8 @@ type
 
   ContainerCommand* = enum
     SET_LINES, SET_NEEDS_AUTH, SET_CONTENT_TYPE, SET_REDIRECT, SET_TITLE,
-    SET_HOVER, READ_LINE, LOAD_DONE, ANCHOR_FOUND, ANCHOR_FAIL, JUMP, OPEN,
-    BUFFER_READY, SOURCE_READY, RESHAPE
+    SET_HOVER, SET_LOAD_INFO, SET_NUM_LINES, READ_LINE, LOAD_DONE,
+    ANCHOR_FOUND, ANCHOR_FAIL, JUMP, OPEN, BUFFER_READY, SOURCE_READY, RESHAPE
 
   BufferMatch* = object
     success*: bool
@@ -53,6 +56,7 @@ type
     str*: string
 
   Buffer* = ref object
+    alive: bool
     input: HTMLInputElement
     contenttype: string
     lines: FlexibleGrid
@@ -83,12 +87,14 @@ type
 macro writeCommand(buffer: Buffer, cmd: ContainerCommand, args: varargs[typed]) =
   result = newStmtList()
   let lens = ident("lens")
-  result.add(quote do:
+  let calclens = newStmtList()
+  calclens.add(quote do:
     var `lens` = slen(`cmd`))
   for arg in args:
-    result.add(quote do: `lens` += slen(`arg`))
-  result.add(quote do:
+    calclens.add(quote do: `lens` += slen(`arg`))
+  calclens.add(quote do:
     `buffer`.postream.swrite(`lens`))
+  result.add(newBlockStmt(calclens))
   result.add(quote do: `buffer`.postream.swrite(`cmd`))
   for arg in args:
     result.add(quote do: `buffer`.postream.swrite(`arg`))
@@ -272,15 +278,13 @@ proc gotoAnchor(buffer: Buffer) =
   if buffer.document == nil: return
   let anchor = buffer.document.getElementById(buffer.location.anchor)
   if anchor == nil: return
-  for y in 0..<buffer.lines.len:
+  for y in 0 ..< buffer.lines.len:
     let line = buffer.lines[y]
-    var i = 0
-    while i < line.formats.len:
+    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)
+        buffer.writeCommand(JUMP, format.pos, y, 0)
         return
-      inc i
 
 proc windowChange(buffer: Buffer) =
   buffer.viewport = Viewport(window: buffer.attrs)
@@ -732,8 +736,11 @@ proc readCommand(buffer: Buffer) =
     let fd = SocketStream(istream).recvFileHandle()
     buffer.bsource.fd = fd
   of LOAD:
+    buffer.writeCommand(SET_LOAD_INFO, CONNECT)
     let code = buffer.setupSource()
-    buffer.load()
+    if code != -2:
+      buffer.writeCommand(SET_LOAD_INFO, DOWNLOAD)
+      buffer.load()
     buffer.writeCommand(LOAD_DONE, code)
   of GOTO_ANCHOR:
     var anchor: string
@@ -743,7 +750,10 @@ proc readCommand(buffer: Buffer) =
     else:
       buffer.writeCommand(ANCHOR_FAIL)
   of RENDER:
+    buffer.writeCommand(SET_LOAD_INFO, LoadInfo.RENDER)
     buffer.render()
+    buffer.writeCommand(SET_LOAD_INFO, DONE)
+    buffer.writeCommand(SET_NUM_LINES, buffer.lines.len)
     buffer.gotoAnchor()
   of GET_LINES:
     var w: Slice[int]
@@ -826,7 +836,7 @@ proc readCommand(buffer: Buffer) =
 
 proc runBuffer(buffer: Buffer, rfd: int) =
   block loop:
-    while true:
+    while buffer.alive:
       let events = buffer.selector.select(-1)
       for event in events:
         if Read in event.events:
@@ -842,6 +852,8 @@ proc runBuffer(buffer: Buffer, rfd: int) =
             break loop
           elif event.fd == buffer.getFd():
             buffer.finishLoad()
+      if not buffer.alive:
+        break loop
       if buffer.reshape:
         buffer.reshape = false
         buffer.render()
@@ -854,14 +866,16 @@ proc runBuffer(buffer: Buffer, rfd: int) =
 proc launchBuffer*(config: BufferConfig, source: BufferSource,
                    attrs: WindowAttributes, loader: FileLoader,
                    mainproc: Pid) =
-  let buffer = new Buffer
-  buffer.userstyle = parseStylesheet(config.userstyle)
-  buffer.attrs = attrs
+  let buffer = Buffer(
+    alive: true,
+    userstyle: parseStylesheet(config.userstyle),
+    attrs: attrs,
+    config: config,
+    loader: loader,
+    bsource: source,
+    sstream: newStringStream()
+  )
   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())
diff --git a/src/buffer/container.nim b/src/buffer/container.nim
index cd321b48..b1f73629 100644
--- a/src/buffer/container.nim
+++ b/src/buffer/container.nim
@@ -34,7 +34,7 @@ type
 
   ContainerEventType* = enum
     NO_EVENT, FAIL, SUCCESS, NEEDS_AUTH, REDIRECT, ANCHOR, NO_ANCHOR, UPDATE,
-    READ_LINE, OPEN, INVALID_COMMAND
+    READ_LINE, OPEN, INVALID_COMMAND, STATUS
 
   ContainerEvent* = object
     case t*: ContainerEventType
@@ -65,10 +65,10 @@ type
     bpos: seq[CursorPosition]
     highlights: seq[Highlight]
     parent*: Container
-    sourcepair*: Container
     istream*: Stream
     ostream*: Stream
     process*: Pid
+    loadinfo*: string
     lines: SimpleFlexibleGrid
     lineshift: int
     numLines*: int
@@ -76,14 +76,14 @@ type
     code*: int
     retry*: seq[URL]
     redirect*: Option[URL]
-    ispipe: bool
     hlon*: bool
+    sourcepair*: Container
     pipeto: Container
     redraw*: bool
-    sourceready*: bool
     cmdvalid: array[ContainerCommand, bool]
+    needslines*: bool
 
-proc newBuffer*(dispatcher: Dispatcher, config: Config, source: BufferSource, ispipe = false, autoload = true): Container =
+proc newBuffer*(dispatcher: Dispatcher, config: Config, source: BufferSource, title = ""): Container =
   let attrs = getWindowAttributes(stdout)
   let ostream = dispatcher.forkserver.ostream
   let istream = dispatcher.forkserver.istream
@@ -96,7 +96,7 @@ 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
+    title: title
   )
   result.cmdvalid[BUFFER_READY] = true
   istream.sread(result.process)
@@ -179,8 +179,6 @@ func maxScreenWidth(container: Container): int =
 func getTitle*(container: Container): string =
   if container.title != "":
     return container.title
-  if container.ispipe:
-    return "*pipe*"
   return container.source.location.serialize(excludepassword = true)
 
 func currentLineWidth(container: Container): int =
@@ -268,7 +266,7 @@ proc sendCursorPosition*(container: Container) =
 proc setFromY*(container: Container, y: int) {.jsfunc.} =
   if container.pos.fromy != y:
     container.pos.fromy = max(min(y, container.maxfromy), 0)
-    container.requestLines()
+    container.needslines = true
     container.redraw = true
 
 proc setFromX*(container: Container, x: int) {.jsfunc.} =
@@ -511,7 +509,7 @@ proc popCursorPos*(container: Container, nojump = false) =
   container.updateCursor()
   if not nojump:
     container.sendCursorPosition()
-    container.requestLines()
+    container.needslines = true
 
 macro proxy(fun: typed) =
   let name = fun[0] # sym
@@ -558,10 +556,13 @@ proc cursorPrevMatch*(container: Container, regex: Regex, wrap: bool) {.jsfunc.}
 proc load*(container: Container) =
   container.writeCommand(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.writeCommand(GOTO_ANCHOR, anchor)
@@ -574,9 +575,10 @@ proc readSuccess*(container: Container, s: string) {.proxy.} = discard
 proc reshape*(container: Container, noreq = false) {.jsfunc.} =
   container.writeCommand(RENDER)
   container.expect(RESHAPE)
+  container.expect(SET_NUM_LINES)
   container.expect(JUMP)
   if not noreq:
-    container.requestLines()
+    container.needslines = true
 
 proc dupeBuffer*(dispatcher: Dispatcher, container: Container, config: Config, location = none(URL), contenttype = none(string)): Container =
   let source = BufferSource(
@@ -585,7 +587,7 @@ proc dupeBuffer*(dispatcher: Dispatcher, container: Container, config: Config, l
     contenttype: if contenttype.isSome: contenttype else: container.contenttype,
     clonepid: container.process,
   )
-  container.pipeto = dispatcher.newBuffer(config, source, container.ispipe)
+  container.pipeto = dispatcher.newBuffer(config, source, container.title)
   container.writeCommand(GET_SOURCE)
   container.expect(SOURCE_READY)
   return container.pipeto
@@ -611,12 +613,29 @@ proc clearSearchHighlights*(container: Container) =
 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
+    #TODO TODO TODO
     for i in 0 ..< len:
       discard container.istream.readChar()
-    return ContainerEvent(t: INVALID_COMMAND)
+    if cmd != RESHAPE:
+      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)
@@ -629,6 +648,8 @@ proc handleCommand(container: Container, cmd: ContainerCommand, len: int): Conta
     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:
@@ -643,8 +664,10 @@ proc handleCommand(container: Container, cmd: ContainerCommand, len: int): Conta
       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
@@ -652,10 +675,8 @@ proc handleCommand(container: Container, cmd: ContainerCommand, len: int): Conta
       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
@@ -696,6 +717,8 @@ proc handleCommand(container: Container, cmd: ContainerCommand, len: int): Conta
       container.pipeto.load()
       container.pipeto = nil
   of RESHAPE:
+    container.needslines = true
+  if container.needslines:
     container.requestLines()
 
 # Synchronously read all lines in the buffer.
diff --git a/src/config/bufferconfig.nim b/src/config/bufferconfig.nim
index 6e8d19e6..78f3b87e 100644
--- a/src/config/bufferconfig.nim
+++ b/src/config/bufferconfig.nim
@@ -1,5 +1,4 @@
 import config/config
-import css/sheet
 
 type BufferConfig* = object
   userstyle*: string
diff --git a/src/config/config.nim b/src/config/config.nim
index c8002289..ff4f0b45 100644
--- a/src/config/config.nim
+++ b/src/config/config.nim
@@ -102,6 +102,8 @@ proc readUserStylesheet(dir, file: string): string =
 proc parseConfig(config: Config, dir: string, t: TomlValue) =
   for k, v in t:
     case k
+    of "startup":
+      config.startup = v.s
     of "headless":
       config.headless = v.b
     of "page":
diff --git a/src/config/toml.nim b/src/config/toml.nim
index 7f743f09..dd0b4fe1 100644
--- a/src/config/toml.nim
+++ b/src/config/toml.nim
@@ -334,7 +334,6 @@ proc consumeNumber(state: var TomlParser, c: char): TomlValue =
     let val = parseFloat64(repr)
     return TomlValue(vt: VALUE_FLOAT, f: val)
 
-  eprint repr
   let val = parseInt64(repr)
   return TomlValue(vt: VALUE_INTEGER, i: val)
 
diff --git a/src/display/client.nim b/src/display/client.nim
index 05a5a0fc..36761d56 100644
--- a/src/display/client.nim
+++ b/src/display/client.nim
@@ -36,6 +36,7 @@ import types/url
 type
   Client* = ref ClientObj
   ClientObj* = object
+    alive: bool
     attrs: WindowAttributes
     dispatcher: Dispatcher
     feednext: bool
@@ -120,43 +121,52 @@ proc command(client: Client, src: string) =
   client.console.container.cursorLastLine()
 
 proc quit(client: Client, code = 0) {.jsfunc.} =
-  client.pager.quit()
+  if client.alive:
+    client.alive = false
+    client.pager.quit()
   quit(code)
 
 proc feedNext(client: Client) {.jsfunc.} =
   client.feednext = true
 
+proc alert(client: Client, msg: string) {.jsfunc.} =
+  client.pager.alert(msg)
+
 proc input(client: Client) =
   restoreStdin(client.console.tty.getFileHandle())
-  let c = client.console.readChar()
-  client.s &= c
-  if client.pager.lineedit.isSome:
-    let edit = client.pager.lineedit.get
-    client.line = edit
-    if edit.escNext:
-      edit.escNext = false
-      if edit.write(client.s):
-        client.s = ""
-    else:
-      let action = getLinedAction(client.config, client.s)
-      if action == "":
+  while true:
+    let c = client.console.readChar()
+    client.s &= c
+    if client.pager.lineedit.isSome:
+      let edit = client.pager.lineedit.get
+      client.line = edit
+      if edit.escNext:
+        edit.escNext = false
         if edit.write(client.s):
           client.s = ""
-        else:
-          client.feedNext = true
-      elif not client.feedNext:
-        client.evalJSFree(action, "<command>")
-      if client.pager.lineedit.isNone:
-        client.line = nil
+      else:
+        let action = getLinedAction(client.config, client.s)
+        if action == "":
+          if edit.write(client.s):
+            client.s = ""
+          else:
+            client.feedNext = true
+        elif not client.feedNext:
+          client.evalJSFree(action, "<command>")
+        if client.pager.lineedit.isNone:
+          client.line = nil
+        if not client.feedNext:
+          client.pager.updateReadLine()
+    else:
+      let action = getNormalAction(client.config, client.s)
+      client.evalJSFree(action, "<command>")
       if not client.feedNext:
-        client.pager.updateReadLine()
-  else:
-    let action = getNormalAction(client.config, client.s)
-    client.evalJSFree(action, "<command>")
-  if not client.feedNext:
-    client.s = ""
-  else:
-    client.feedNext = false
+        client.pager.refreshStatusMsg()
+    if not client.feedNext:
+      client.s = ""
+      break
+    else:
+      client.feedNext = false
 
 proc setTimeout[T: JSObject|string](client: Client, handler: T, timeout = 0): int {.jsfunc.} =
   let id = client.timeoutid
@@ -216,6 +226,15 @@ proc clearInterval(client: Client, id: int) {.jsfunc.} =
 let SIGWINCH {.importc, header: "<signal.h>", nodecl.}: cint
 
 proc acceptBuffers(client: Client) =
+  while client.pager.unreg.len > 0:
+    let (pid, stream) = client.pager.unreg.pop()
+    let fd = stream.source.getFd()
+    if int(fd) in client.fdmap:
+      client.selector.unregister(fd)
+      client.fdmap.del(int(fd))
+    else:
+      client.pager.procmap.del(pid)
+    stream.close()
   var i = 0
   while i < client.pager.procmap.len:
     let stream = client.ssock.acceptSocketStream()
@@ -234,12 +253,20 @@ proc acceptBuffers(client: Client) =
       #TODO print an error?
       stream.close()
 
+proc log(console: Console, ss: varargs[string]) {.jsfunc.} =
+  for i in 0..<ss.len:
+    console.err.write(ss[i])
+    if i != ss.high:
+      console.err.write(' ')
+  console.err.write('\n')
+  console.err.flush()
+
 proc 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()
   while true:
-    client.acceptBuffers()
     let events = client.selector.select(-1)
     for event in events:
       if Read in event.events:
@@ -249,13 +276,11 @@ proc inputLoop(client: Client) =
         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 Error in event.events:
-        eprint "Error", event
         #TODO handle errors
+        client.alert("Error in selected fds, check console")
+        client.console.log($event)
       if Signal in event.events: 
         if event.fd == sigwinch:
           client.attrs = getWindowAttributes(client.console.tty)
@@ -272,7 +297,9 @@ proc inputLoop(client: Client) =
     if client.pager.scommand != "":
       client.command(client.pager.scommand)
       client.pager.scommand = ""
+      client.pager.refreshStatusMsg()
     client.pager.draw()
+    client.acceptBuffers()
 
 #TODO this is dumb
 proc readFile(client: Client, path: string): string {.jsfunc.} =
@@ -292,11 +319,12 @@ proc newConsole(pager: Pager, tty: File): Console =
     if pipe(pipefd) == -1:
       raise newException(Defect, "Failed to open console pipe.")
     let url = newURL("javascript:console.show()")
-    result.container = pager.readPipe0(some("text/plain"), pipefd[0], option(url))
+    result.container = pager.readPipe0(some("text/plain"), pipefd[0], option(url), "Browser console")
     var f: File
     if not open(f, pipefd[1], fmWrite):
       raise newException(Defect, "Failed to open file for console pipe.")
     result.err = newFileStream(f)
+    result.err.writeLine("Type (M-c) console.hide() to return to buffer mode.")
     result.pager = pager
     result.tty = tty
     pager.registerContainer(result.container)
@@ -307,12 +335,11 @@ proc dumpBuffers(client: Client) =
   client.acceptBuffers()
   for container in client.pager.containers:
     container.load()
-  for msg in client.pager.status:
-    eprint msg
   let ostream = newFileStream(stdout)
   for container in client.pager.containers:
     container.reshape(true)
     client.pager.drawBuffer(container, ostream)
+  client.pager.dumpAlerts()
   stdout.close()
 
 proc launchClient*(client: Client, pages: seq[string], ctype: Option[string], dump: bool) =
@@ -329,6 +356,7 @@ proc launchClient*(client: Client, pages: seq[string], ctype: Option[string], du
   client.selector = newSelector[Container]()
   client.pager.launchPager(tty)
   client.console = newConsole(client.pager, tty)
+  client.alive = true
   addExitProc((proc() = client.quit()))
   if client.config.startup != "":
     let s = if fileExists(client.config.startup):
@@ -357,14 +385,6 @@ proc nimGCStats(client: Client): string {.jsfunc.} =
 proc jsGCStats(client: Client): string {.jsfunc.} =
   return client.jsrt.getMemoryUsage()
 
-proc log(console: Console, ss: varargs[string]) {.jsfunc.} =
-  for i in 0..<ss.len:
-    console.err.write(ss[i])
-    if i != ss.high:
-      console.err.write(' ')
-  console.err.write('\n')
-  console.err.flush()
-
 proc show(console: Console) {.jsfunc.} =
   if console.pager.container != console.container:
     console.prev = console.pager.container
diff --git a/src/display/pager.nim b/src/display/pager.nim
index d04981a0..fdce5cc0 100644
--- a/src/display/pager.nim
+++ b/src/display/pager.nim
@@ -1,3 +1,4 @@
+import net
 import options
 import os
 import streams
@@ -16,6 +17,7 @@ import io/request
 import io/term
 import io/window
 import ips/forkserver
+import ips/socketstream
 import js/javascript
 import js/regex
 import types/buffersource
@@ -30,7 +32,7 @@ type
     SEARCH_B, ISEARCH_F, ISEARCH_B
 
   Pager* = ref object
-    attrs: WindowAttributes
+    alerts: seq[string]
     commandMode*: bool
     container*: Container
     dispatcher*: Dispatcher
@@ -42,10 +44,10 @@ type
     regex: Option[Regex]
     iregex: Option[Regex]
     reverseSearch: bool
-    status*: seq[string]
-    statusmsg*: FixedGrid
+    statusgrid*: FixedGrid
     tty: File
     procmap*: Table[Pid, Container]
+    unreg*: seq[(Pid, SocketStream)]
     icpos: CursorPosition
     display: FixedGrid
     redraw*: bool
@@ -111,23 +113,15 @@ proc searchPrev(pager: Pager) {.jsfunc.} =
     else:
       pager.container.cursorNextMatch(pager.regex.get, true)
 
-#TODO get rid of this
-proc statusMode(pager: Pager) =
-  pager.term.setCursor(0, pager.attrs.height - 1)
-  pager.term.resetFormat2()
-  pager.term.eraseLine()
-
-#TODO ditto
-proc setLineEdit*(pager: Pager, edit: LineEdit, mode: LineMode) =
-  pager.statusMode()
-  edit.writeStart()
-  pager.term.flush()
+proc setLineEdit(pager: Pager, edit: LineEdit, mode: LineMode) =
   pager.lineedit = some(edit)
   pager.linemode = mode
 
 proc clearLineEdit(pager: Pager) =
   pager.lineedit = none(LineEdit)
 
+func attrs(pager: Pager): WindowAttributes = pager.term.attrs
+
 proc searchForward(pager: Pager) {.jsfunc.} =
   pager.setLineEdit(readLine("/", pager.attrs.width, config = pager.config, tty = pager.tty), SEARCH_F)
 
@@ -142,16 +136,13 @@ proc isearchBackward(pager: Pager) {.jsfunc.} =
   pager.container.pushCursorPos()
   pager.setLineEdit(readLine("?", pager.attrs.width, config = pager.config, tty = pager.tty), ISEARCH_B)
 
-func attrs*(pager: Pager): WindowAttributes = pager.term.attrs
-
 proc newPager*(config: Config, attrs: WindowAttributes, dispatcher: Dispatcher): Pager =
   let pager = Pager(
     dispatcher: dispatcher,
     config: config,
-    attrs: attrs,
     display: newFixedGrid(attrs.width, attrs.height - 1),
-    statusmsg: newFixedGrid(attrs.width),
-    term: newTerminal(stdout, config)
+    statusgrid: newFixedGrid(attrs.width),
+    term: newTerminal(stdout, config, attrs)
   )
   return pager
 
@@ -160,15 +151,20 @@ proc launchPager*(pager: Pager, tty: File) =
   if tty != nil:
     pager.term.start(tty)
 
+proc dumpAlerts*(pager: Pager) =
+  for msg in pager.alerts:
+    eprint msg
+
 proc quit*(pager: Pager, code = 0) =
   pager.term.quit()
+  pager.dumpAlerts()
 
 proc clearDisplay(pager: Pager) =
   pager.display = newFixedGrid(pager.display.width, pager.display.height)
 
 proc buffer(pager: Pager): Container {.jsfget, inline.} = pager.container
 
-proc refreshDisplay*(pager: Pager, container = pager.container) =
+proc refreshDisplay(pager: Pager, container = pager.container) =
   var r: Rune
   var by = 0
   pager.clearDisplay()
@@ -219,36 +215,29 @@ proc refreshDisplay*(pager: Pager, container = pager.container) =
         pager.display[dls + i - startw].format = hlformat
     inc by
 
-func generateStatusMessage*(pager: Pager): string =
-  var format = newFormat()
-  var w = 0
-  for cell in pager.statusmsg:
-    result &= pager.term.processFormat(format, cell.format)
-    result &= cell.str
-    w += cell.width()
-  if w < pager.statusmsg.width:
-    result &= EL()
-
 proc clearStatusMessage(pager: Pager) =
-  pager.statusmsg = newFixedGrid(pager.statusmsg.width)
+  pager.statusgrid = newFixedGrid(pager.statusgrid.width)
 
 proc writeStatusMessage(pager: Pager, str: string, format: Format = Format()) =
   pager.clearStatusMessage()
   var i = 0
   for r in str.runes:
     i += r.width()
-    if i >= pager.statusmsg.len:
-      pager.statusmsg[^1].str = "$"
+    if i >= pager.statusgrid.len:
+      pager.statusgrid[^1].str = "$"
       break
-    pager.statusmsg[i].str &= r
-    pager.statusmsg[i].format = format
+    pager.statusgrid[i].str &= r
+    pager.statusgrid[i].format = format
 
 proc refreshStatusMsg*(pager: Pager) =
   let container = pager.container
-  if pager.status.len > 0:
-    pager.writeStatusMessage(pager.status[0])
-    pager.status.delete(0)
-  elif container != nil:
+  if container == nil: return
+  if container.loadinfo != "":
+    pager.writeStatusMessage(container.loadinfo)
+  elif pager.alerts.len > 0:
+    pager.writeStatusMessage(pager.alerts[0])
+    pager.alerts.delete(0)
+  else:
     var msg = $(container.cursory + 1) & "/" & $container.numLines & " (" &
               $container.atPercentOf() & "%) " & "<" & container.getTitle() & ">"
     if container.hovertext.len > 0:
@@ -257,17 +246,6 @@ proc refreshStatusMsg*(pager: Pager) =
     format.reverse = true
     pager.writeStatusMessage(msg, format)
 
-#TODO get rid of this
-func generateStatusOutput(pager: Pager): string =
-  return pager.generateStatusMessage()
-
-#TODO ditto
-proc displayStatus*(pager: Pager) =
-  if pager.lineedit.isNone:
-    pager.statusMode()
-    print(pager.generateStatusOutput())
-    stdout.flushFile()
-
 proc drawBuffer*(pager: Pager, container: Container, ostream: Stream) =
   var format = newFormat()
   for line in container.readLines:
@@ -298,14 +276,15 @@ proc draw*(pager: Pager) =
   pager.term.hideCursor()
   if pager.redraw or pager.container != nil and pager.container.redraw:
     pager.refreshDisplay()
-    pager.term.outputGrid(pager.display)
-  pager.refreshStatusMsg()
-  pager.displayStatus()
+    pager.term.writeGrid(pager.display)
+  if pager.lineedit.isSome:
+    pager.term.writeGrid(pager.lineedit.get.generateOutput(), 0, pager.attrs.height - 1)
+  else:
+    pager.term.writeGrid(pager.statusgrid, 0, pager.attrs.height - 1)
+  pager.term.outputGrid()
   if pager.lineedit.isSome:
-    pager.statusMode()
-    pager.lineedit.get.writePrompt()
-    pager.lineedit.get.fullRedraw()
-  elif pager.container != nil:
+    pager.term.setCursor(pager.lineedit.get.getCursorX(), pager.container.attrs.height - 1)
+  else:
     pager.term.setCursor(pager.container.acursorx, pager.container.acursory)
   pager.term.showCursor()
   pager.term.flush()
@@ -358,12 +337,11 @@ proc nextBuffer*(pager: Pager): bool {.jsfunc.} =
     return true
   return false
 
-proc setStatusMessage*(pager: Pager, msg: string) =
-  pager.status.add(msg)
-  pager.refreshStatusMsg()
+proc alert*(pager: Pager, msg: string) {.jsfunc.} =
+  pager.alerts.add(msg)
 
 proc lineInfo(pager: Pager) {.jsfunc.} =
-  pager.setStatusMessage(pager.container.lineInfo())
+  pager.alert(pager.container.lineInfo())
 
 proc deleteContainer(pager: Pager, container: Container) =
   if container.parent == nil and
@@ -399,14 +377,13 @@ proc deleteContainer(pager: Pager, container: Container) =
       pager.setContainer(nil)
   container.parent = nil
   container.children.setLen(0)
-  container.istream.close()
-  container.ostream.close()
+  pager.unreg.add((container.process, SocketStream(container.istream)))
   pager.dispatcher.forkserver.removeChild(container.process)
 
 proc discardBuffer*(pager: Pager) {.jsfunc.} =
   if pager.container == nil or pager.container.parent == nil and
       pager.container.children.len == 0:
-    pager.setStatusMessage("Cannot discard last buffer!")
+    pager.alert("Cannot discard last buffer!")
   else:
     pager.deleteContainer(pager.container)
 
@@ -424,10 +401,9 @@ proc toggleSource*(pager: Pager) {.jsfunc.} =
     pager.addContainer(container)
 
 proc windowChange*(pager: Pager, attrs: WindowAttributes) =
-  pager.attrs = attrs
-  pager.display = newFixedGrid(attrs.width, attrs.height - 1)
-  pager.statusmsg = newFixedGrid(attrs.width)
   pager.term.windowChange(attrs)
+  pager.display = newFixedGrid(attrs.width, attrs.height - 1)
+  pager.statusgrid = newFixedGrid(attrs.width)
   for container in pager.containers:
     container.windowChange(attrs)
 
@@ -483,25 +459,25 @@ proc loadURL*(pager: Pager, url: string, ctype = none(string)) =
   if localurl.isSome: # attempt to load local file
     urls.add(localurl.get)
   if urls.len == 0:
-    pager.setStatusMessage("Invalid URL " & url)
+    pager.alert("Invalid URL " & url)
   else:
     let prevc = pager.container
     pager.gotoURL(newRequest(urls.pop()), ctype = ctype)
     if pager.container != prevc:
       pager.container.retry = urls
 
-proc readPipe0*(pager: Pager, ctype: Option[string], fd: FileHandle, location: Option[URL]): Container =
+proc readPipe0*(pager: Pager, ctype: Option[string], fd: FileHandle, location: Option[URL], title: string): Container =
   let source = BufferSource(
     t: LOAD_PIPE,
     fd: fd,
     contenttype: some(ctype.get("text/plain")),
     location: location.get(newURL("file://-"))
   )
-  let container = pager.dispatcher.newBuffer(pager.config, source, ispipe = true)
+  let container = pager.dispatcher.newBuffer(pager.config, source, title)
   return container
 
 proc readPipe*(pager: Pager, ctype: Option[string], fd: FileHandle) =
-  let container = pager.readPipe0(ctype, fd, none(URL))
+  let container = pager.readPipe0(ctype, fd, none(URL), "*pipe*")
   pager.addContainer(container)
 
 proc command(pager: Pager) {.jsfunc.} =
@@ -612,11 +588,12 @@ proc handleEvent*(pager: Pager, container: Container): bool =
     if container.retry.len > 0:
       pager.gotoURL(newRequest(container.retry.pop()), ctype = container.contenttype)
     else:
-      pager.setStatusMessage("Couldn't load " & $container.source.location & " (error code " & $container.code & ")")
+      pager.alert("Couldn't load " & $container.source.location & " (error code " & $container.code & ")")
     if pager.container == nil:
       return false
   of SUCCESS:
     container.reshape()
+    pager.container.loadinfo = ""
     if container.replace != nil:
       container.children.add(container.replace.children)
       for child in container.children:
@@ -634,24 +611,24 @@ proc handleEvent*(pager: Pager, container: Container): bool =
       pager.authorize()
   of REDIRECT:
     let redirect = container.redirect.get
-    pager.setStatusMessage("Redirecting to " & $redirect)
+    pager.alert("Redirecting to " & $redirect)
     pager.gotoURL(newRequest(redirect), some(pager.container.source.location), replace = pager.container)
   of ANCHOR:
     pager.addContainer(pager.dupeContainer(container, container.redirect))
   of NO_ANCHOR:
-    pager.setStatusMessage("Couldn't find anchor " & container.redirect.get.anchor)
+    pager.alert("Couldn't find anchor " & container.redirect.get.anchor)
   of UPDATE:
     if container == pager.container:
       pager.redraw = true
   of READ_LINE:
     if container == pager.container:
-      pager.setLineEdit(readLine(event.prompt, pager.statusmsg.width, current = event.value, hide = event.password, config = pager.config, tty = pager.tty), BUFFER)
+      pager.setLineEdit(readLine(event.prompt, pager.attrs.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 INVALID_COMMAND: discard
+  of STATUS:
+    if pager.container == container:
+      pager.refreshStatusMsg()
   of NO_EVENT: discard
   return true
 
diff --git a/src/io/lineedit.nim b/src/io/lineedit.nim
index 2d7da512..eb812122 100644
--- a/src/io/lineedit.nim
+++ b/src/io/lineedit.nim
@@ -5,6 +5,7 @@ import sequtils
 import sugar
 
 import bindings/quickjs
+import buffer/cell
 import config/config
 import js/javascript
 import utils/twtstr
@@ -16,6 +17,7 @@ type
   LineEdit* = ref object
     news*: seq[Rune]
     prompt*: string
+    promptw: int
     current: string
     state*: LineEditState
     escNext*: bool
@@ -81,6 +83,25 @@ proc begin0(state: LineEdit) =
 proc space(edit: LineEdit, i: int) =
   print(' '.repeat(i))
 
+proc generateOutput*(edit: LineEdit): FixedGrid =
+  result = newFixedGrid(edit.maxlen)
+  let os = edit.news.substr(edit.shift, edit.shift + edit.displen)
+  var x = 0
+  for r in edit.prompt.runes():
+    result[x].str &= $r
+    x += r.lwidth()
+  if edit.hide:
+    for r in os:
+      result[x].str = "*"
+      x += r.lwidth()
+  else:
+    for r in os:
+      result[x].str &= $r
+      x += r.lwidth()
+
+proc getCursorX*(edit: LineEdit): int =
+  return edit.promptw + edit.news.lwidth(edit.shift, edit.cursor)
+
 proc redraw(state: LineEdit) =
   var dispw = state.news.lwidth(state.shift, state.shift + state.displen)
   if state.shift + state.displen > state.news.len:
@@ -282,6 +303,7 @@ proc readLine*(prompt: string, termwidth: int, current = "",
                tty: File): LineEdit =
   new(result)
   result.prompt = prompt
+  result.promptw = prompt.lwidth()
   result.current = current
   result.news = current.toRunes()
   result.cursor = result.news.len
diff --git a/src/io/term.nim b/src/io/term.nim
index 518990e3..f892761b 100644
--- a/src/io/term.nim
+++ b/src/io/term.nim
@@ -9,7 +9,6 @@ import buffer/cell
 import config/config
 import io/window
 import types/color
-import utils/twtstr
 
 #TODO switch from termcap...
 
@@ -40,6 +39,7 @@ type
     infile: File
     outfile: File
     cleared: bool
+    canvas: FixedGrid
     prevgrid: FixedGrid
     attrs*: WindowAttributes
     mincontrast: float
@@ -273,26 +273,9 @@ proc processFormat*(term: Terminal, format: var Format, cellf: Format): string =
 
 proc windowChange*(term: Terminal, attrs: WindowAttributes) =
   term.attrs = attrs
+  term.canvas = newFixedGrid(attrs.width, attrs.height)
   term.cleared = false
 
-proc getCursorPos(term: Terminal): (int, int) =
-  term.write(CSI("6n"))
-  term.flush()
-  var c = term.infile.readChar()
-  while true:
-    while c != '\e':
-      c = term.infile.readChar()
-    c = term.infile.readChar()
-    if c == '[': break
-  var tmp = ""
-  while (let c = term.infile.readChar(); c != ';'):
-    tmp &= c
-  result[1] = parseInt32(tmp)
-  tmp = ""
-  while (let c = term.infile.readChar(); c != 'R'):
-    tmp &= c
-  result[0] = parseInt32(tmp)
-
 func generateFullOutput(term: Terminal, grid: FixedGrid): string =
   var format = newFormat()
   result &= term.cursorGoto(0, 0)
@@ -304,7 +287,8 @@ func generateFullOutput(term: Terminal, grid: FixedGrid): string =
       result &= cell.str
     result &= SGR()
     result &= term.clearEnd()
-    result &= "\r\n"
+    if y != grid.height - 1:
+      result &= "\r\n"
 
 func generateSwapOutput(term: Terminal, grid: FixedGrid): string =
   var format = newFormat()
@@ -343,14 +327,19 @@ proc hideCursor*(term: Terminal) =
 proc showCursor*(term: Terminal) =
   term.outfile.showCursor()
 
-proc outputGrid*(term: Terminal, grid: FixedGrid) =
+proc writeGrid*(term: Terminal, grid: FixedGrid, x = 0, y = 0) =
+  for ly in y ..< y + grid.height:
+    for lx in x ..< x + grid.width:
+      term.canvas[ly * term.canvas.width + lx] = grid[(ly - y) * grid.width + (lx - x)]
+
+proc outputGrid*(term: Terminal) =
   term.outfile.write(SGR())
   if not term.cleared:
-    term.outfile.write(term.generateFullOutput(grid))
+    term.outfile.write(term.generateFullOutput(term.canvas))
     term.cleared = true
   else:
-    term.outfile.write(term.generateSwapOutput(grid))
-  term.prevgrid = grid
+    term.outfile.write(term.generateSwapOutput(term.canvas))
+  term.prevgrid = term.canvas
 
 when defined(posix):
   import posix
@@ -359,10 +348,10 @@ when defined(posix):
   # see https://viewsourcecode.org/snaptoken/kilo/02.enteringRawMode.html
   var orig_termios: Termios
   var stdin_fileno: FileHandle
-  proc disableRawMode*() {.noconv.} =
+  proc disableRawMode() {.noconv.} =
     discard tcSetAttr(stdin_fileno, TCSAFLUSH, addr orig_termios)
 
-  proc enableRawMode*(fileno: FileHandle) =
+  proc enableRawMode(fileno: FileHandle) =
     stdin_fileno = fileno
     discard tcGetAttr(fileno, addr orig_termios)
     var raw = orig_termios
@@ -385,6 +374,12 @@ when defined(posix):
       discard fcntl(fileno, F_SETFL, orig_flags)
       stdin_unblocked = false
 else:
+  proc disableRawMode() =
+    discard
+
+  proc enableRawMode(fileno: FileHandle) =
+    discard
+
   proc unblockStdin*(): cint =
     discard
 
@@ -396,8 +391,7 @@ proc isatty*(term: Terminal): bool =
 
 proc quit*(term: Terminal) =
   if term.infile != nil and term.isatty():
-    when defined(posix):
-      disableRawMode()
+    disableRawMode()
     if term.smcup:
       term.write(term.disableAltScreen())
     else:
@@ -454,15 +448,15 @@ proc start*(term: Terminal, infile: File) =
   term.infile = infile
   assert term.outfile.getFileHandle().setInheritable(false)
   assert term.infile.getFileHandle().setInheritable(false)
-  when defined(posix):
-    if term.isatty():
-      enableRawMode(infile.getFileHandle())
+  if term.isatty():
+    enableRawMode(infile.getFileHandle())
   term.detectTermAttributes()
   if term.smcup:
     term.write(term.enableAltScreen())
 
-proc newTerminal*(outfile: File, config: Config): Terminal =
+proc newTerminal*(outfile: File, config: Config, attrs: WindowAttributes): Terminal =
   let term = new Terminal
   term.outfile = outfile
   term.config = config
+  term.windowChange(attrs)
   return term
diff --git a/src/ips/forkserver.nim b/src/ips/forkserver.nim
index 3e93402d..1c29b120 100644
--- a/src/ips/forkserver.nim
+++ b/src/ips/forkserver.nim
@@ -33,6 +33,7 @@ proc newFileLoader*(forkserver: ForkServer, defaultHeaders: HeaderList = Default
 
 proc removeChild*(forkserver: Forkserver, pid: Pid) =
   forkserver.ostream.swrite(REMOVE_CHILD)
+  forkserver.ostream.swrite(pid)
   forkserver.ostream.flush()
 
 proc forkLoader(ctx: var ForkServerContext, defaultHeaders: HeaderList): FileLoader =
diff --git a/src/ips/socketstream.nim b/src/ips/socketstream.nim
index 3918fe2c..6ce9bd88 100644
--- a/src/ips/socketstream.nim
+++ b/src/ips/socketstream.nim
@@ -16,7 +16,7 @@ proc sockReadData(s: Stream, buffer: pointer, len: int): int =
   let s = SocketStream(s)
   result = s.source.recv(buffer, len)
   if result < 0:
-    raise newException(Defect, "Failed to read data (code " & $osLastError() & ")")
+    raise newException(IOError, "Failed to read data (code " & $osLastError() & ")")
   elif result < len:
     s.isend = true