about summary refs log tree commit diff stats
path: root/src
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2022-11-20 17:36:21 +0100
committerbptato <nincsnevem662@gmail.com>2022-11-20 17:36:21 +0100
commit7d6c75e4c737e51f997f2ac02001fad1a5627ca2 (patch)
treef315fb6e576e1b68e254be26d30d61473c91b3c8 /src
parent06fbb9bc967e11151fdd1311437248cf9bc25706 (diff)
downloadchawan-7d6c75e4c737e51f997f2ac02001fad1a5627ca2.tar.gz
Terminal refactorings
Diffstat (limited to 'src')
-rw-r--r--src/bindings/notcurses.nim12
-rw-r--r--src/buffer/buffer.nim48
-rw-r--r--src/buffer/cell.nim53
-rw-r--r--src/buffer/container.nim79
-rw-r--r--src/css/values.nim36
-rw-r--r--src/display/client.nim45
-rw-r--r--src/display/pager.nim131
-rw-r--r--src/io/term.nim294
-rw-r--r--src/io/window.nim52
-rw-r--r--src/layout/box.nim4
-rw-r--r--src/layout/engine.nim10
-rw-r--r--src/render/renderdocument.nim56
-rw-r--r--src/utils/twtstr.nim19
13 files changed, 533 insertions, 306 deletions
diff --git a/src/bindings/notcurses.nim b/src/bindings/notcurses.nim
index bb422a54..8e1cd718 100644
--- a/src/bindings/notcurses.nim
+++ b/src/bindings/notcurses.nim
@@ -18,6 +18,13 @@ const
   NCOPTION_DRAIN_INPUT* = 0x0100u64
   NCOPTION_SCROLLING* = 0x0200u64
 
+const
+  NCDIRECT_OPTION_INHIBIT_SETLOCALE* = 0x0001u64
+  NCDIRECT_OPTION_INHIBIT_CBREAK* = 0x0002u64
+  NCDIRECT_OPTION_NO_QUIT_SIGHANDLERS* = 0x0008u64
+  NCDIRECT_OPTION_VERBOSE* = 0x0010u64
+  NCDIRECT_OPTION_VERY_VERBOSE* = 0x0020u64
+
 const NCOPTION_CLI_MODE = NCOPTION_NO_ALTERNATE_SCREEN or
   NCOPTION_NO_CLEAR_BITMAPS or
   NCOPTION_PRESERVE_CURSOR or
@@ -48,9 +55,12 @@ type
 
   notcurses* = pointer
 
+  ncdirect* = pointer
+
 {.push importc.}
 
-proc notcurses_core_init*(opts: notcurses_options, fp: File): notcurses
+proc ncdirect_core_init*(termtype: cstring, fp: File, flags: uint64): ncdirect
+proc ncdirect_stop*(nc: ncdirect): cint
 
 {.pop.}
 {.pop.}
diff --git a/src/buffer/buffer.nim b/src/buffer/buffer.nim
index 9bd1d20a..11daa763 100644
--- a/src/buffer/buffer.nim
+++ b/src/buffer/buffer.nim
@@ -25,8 +25,8 @@ import io/process
 import io/request
 import io/serialize
 import io/socketstream
-import io/term
 import js/regex
+import io/window
 import layout/box
 import render/renderdocument
 import render/rendertext
@@ -36,9 +36,9 @@ import utils/twtstr
 
 type
   BufferCommand* = enum
-    LOAD, RENDER, DRAW_BUFFER, 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
+    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
 
   ContainerCommand* = enum
     SET_LINES, SET_NEEDS_AUTH, SET_CONTENT_TYPE, SET_REDIRECT, SET_TITLE,
@@ -73,12 +73,11 @@ type
     bsource: BufferSource
     width: int
     height: int
-    attrs: TermAttributes
+    attrs: WindowAttributes
     document: Document
     viewport: Viewport
     prevstyled: StyledNode
     reshape: bool
-    nostatus: bool
     location: Url
     selector: Selector[int]
     istream: Stream
@@ -286,7 +285,7 @@ proc gotoAnchor(buffer: Buffer) =
       inc i
 
 proc windowChange(buffer: Buffer) =
-  buffer.width = buffer.attrs.width - 1
+  buffer.width = buffer.attrs.width
   buffer.height = buffer.attrs.height - 1
   buffer.reshape = true
 
@@ -421,7 +420,7 @@ proc render(buffer: Buffer) =
   case buffer.contenttype
   of "text/html":
     if buffer.viewport == nil:
-      buffer.viewport = Viewport(term: buffer.attrs)
+      buffer.viewport = Viewport(window: buffer.attrs)
     let ret = renderDocument(buffer.document, buffer.attrs, buffer.config.userstyle, buffer.viewport, buffer.prevstyled)
     buffer.lines = ret[0]
     buffer.prevstyled = ret[1]
@@ -720,31 +719,6 @@ proc click(buffer: Buffer, cursorx, cursory: int) =
     else:
       restore_focus
 
-proc drawBuffer(buffer: Buffer, ostream: Stream) =
-  var format = newFormat()
-  for line in buffer.lines:
-    if line.formats.len == 0:
-      ostream.swrite(line.str & "\n")
-    else:
-      var x = 0
-      var i = 0
-      var s = ""
-      for f in line.formats:
-        var outstr = ""
-        #assert f.pos < line.str.width(), "fpos " & $f.pos & "\nstr" & line.str & "\n"
-        while x < f.pos:
-          var r: Rune
-          fastRuneAt(line.str, i, r)
-          outstr &= r
-          x += r.width()
-        s &= outstr
-        s &= format.processFormat(f.format)
-      s &= line.str.substr(i) & format.processFormat(newFormat()) & "\n"
-      ostream.swrite(s)
-    ostream.flush()
-  ostream.swrite("")
-  ostream.flush()
-
 proc readCommand(buffer: Buffer) =
   let istream = buffer.pistream
   let ostream = buffer.postream
@@ -768,16 +742,14 @@ proc readCommand(buffer: Buffer) =
   of GET_LINES:
     var w: Slice[int]
     istream.sread(w)
+    if w.b < 0 or w.b > buffer.lines.high:
+      w.b = buffer.lines.high
     ostream.swrite(SET_LINES)
     ostream.swrite(buffer.lines.len)
-    w.b = min(buffer.lines.high, w.b)
     ostream.swrite(w)
     for y in w:
       ostream.swrite(buffer.lines[y])
-      ostream.flush()
     ostream.flush()
-  of DRAW_BUFFER:
-    buffer.drawBuffer(ostream)
   of WINDOW_CHANGE:
     istream.sread(buffer.attrs)
     buffer.windowChange()
@@ -877,7 +849,7 @@ proc runBuffer(buffer: Buffer, readf, writef: File) =
   quit(0)
 
 proc launchBuffer*(config: BufferConfig, source: BufferSource,
-                   attrs: TermAttributes, readf, writef: File) =
+                   attrs: WindowAttributes, readf, writef: File) =
   let buffer = new Buffer
   buffer.attrs = attrs
   buffer.windowChange()
diff --git a/src/buffer/cell.nim b/src/buffer/cell.nim
index 84efef46..7c6ec00a 100644
--- a/src/buffer/cell.nim
+++ b/src/buffer/cell.nim
@@ -51,9 +51,21 @@ type
     str*: string
     format*: Format
 
-  FixedGrid* = seq[FixedCell]
-
-const FormatCodes: array[FormatFlags, tuple[s: int, e: int]] = [
+  FixedGrid* = object
+    width*, height*: int
+    cells*: seq[FixedCell]
+
+proc `[]=`*(grid: var FixedGrid, i: int, cell: FixedCell) = grid.cells[i] = cell
+proc `[]=`*(grid: var FixedGrid, i: BackwardsIndex, cell: FixedCell) = grid.cells[i] = cell
+proc `[]`*(grid: var FixedGrid, i: int): var FixedCell = grid.cells[i]
+proc `[]`*(grid: var FixedGrid, i: BackwardsIndex): var FixedCell = grid.cells[i]
+proc `[]`*(grid: FixedGrid, i: int): FixedCell = grid.cells[i]
+proc `[]`*(grid: FixedGrid, i: BackwardsIndex): FixedCell = grid.cells[i]
+iterator items*(grid: FixedGrid): FixedCell {.inline.} =
+  for cell in grid.cells: yield cell
+proc len*(grid: FixedGrid): int = grid.cells.len
+
+const FormatCodes*: array[FormatFlags, tuple[s: int, e: int]] = [
   FLAG_BOLD: (1, 22),
   FLAG_ITALIC: (3, 23),
   FLAG_UNDERLINE: (4, 24),
@@ -80,7 +92,7 @@ func `==`*(a: FixedCell, b: FixedCell): bool =
     a.str == b.str
 
 func newFixedGrid*(w: int, h: int = 1): FixedGrid =
-  return newSeq[FixedCell](w * h)
+  return FixedGrid(width: w, height: h, cells: newSeq[FixedCell](w * h))
 
 func width*(line: FlexibleLine): int =
   return line.str.width()
@@ -286,36 +298,3 @@ proc parseAnsiCode*(format: var Format, stream: Stream) =
   if 0x40 <= int(c) and int(c) <= 0x7E:
     let final = c
     format.handleAnsiCode(final, params)
-
-proc processFormat*(format: var Format, cellf: Format): string =
-  for flag in FormatFlags:
-    if flag in format.flags and flag notin cellf.flags:
-      result &= SGR(FormatCodes[flag].e)
-
-  if cellf.fgcolor != format.fgcolor:
-    var color = cellf.fgcolor
-    if color.rgb:
-      let rgb = color.rgbcolor
-      result &= SGR(38, 2, rgb.r, rgb.g, rgb.b)
-    elif color == defaultColor:
-      result &= SGR()
-      format = newFormat()
-    else:
-      result &= SGR(color.color)
-
-  if cellf.bgcolor != format.bgcolor:
-    var color = cellf.bgcolor
-    if color.rgb:
-      let rgb = color.rgbcolor
-      result &= SGR(48, 2, rgb.r, rgb.g, rgb.b)
-    elif color == defaultColor:
-      result &= SGR()
-      format = newFormat()
-    else:
-      result &= SGR(color.color)
-
-  for flag in FormatFlags:
-    if flag notin format.flags and flag in cellf.flags:
-      result &= SGR(FormatCodes[flag].s)
-
-  format = cellf
diff --git a/src/buffer/container.nim b/src/buffer/container.nim
index 35071f1e..e8908986 100644
--- a/src/buffer/container.nim
+++ b/src/buffer/container.nim
@@ -13,7 +13,7 @@ import config/bufferconfig
 import config/config
 import io/request
 import io/serialize
-import io/term
+import io/window
 import js/regex
 import types/url
 import utils/twtstr
@@ -29,7 +29,7 @@ type
 
   ContainerEventType* = enum
     NO_EVENT, FAIL, SUCCESS, NEEDS_AUTH, REDIRECT, ANCHOR, NO_ANCHOR, UPDATE,
-    STATUS, JUMP, READ_LINE, OPEN
+    READ_LINE, OPEN
 
   ContainerEvent* = object
     case t*: ContainerEventType
@@ -48,7 +48,7 @@ type
     clear*: bool
 
   Container* = ref object
-    attrs*: TermAttributes
+    attrs*: WindowAttributes
     width*: int
     height*: int
     contenttype*: Option[string]
@@ -83,7 +83,7 @@ proc c_setvbuf(f: File, buf: pointer, mode: cint, size: csize_t): cint {.
   importc: "setvbuf", header: "<stdio.h>", tags: [].}
 
 proc newBuffer*(config: Config, source: BufferSource, tty: FileHandle, ispipe = false): Container =
-  let attrs = getTermAttributes(stdout)
+  let attrs = getWindowAttributes(stdout)
   when defined(posix):
     var pipefd_in, pipefd_out: array[0..1, cint]
     if pipe(pipefd_in) == -1:
@@ -274,10 +274,13 @@ 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 requestLines*(container: Container, w = container.lineWindow) =
+  container.writeCommand(GET_LINES, w)
+
 proc setFromY*(container: Container, y: int) =
   if container.pos.fromy != y:
     container.pos.fromy = max(min(y, container.maxfromy), 0)
-    container.writeCommand(GET_LINES, container.lineWindow)
+    container.requestLines()
     container.redraw = true
 
 proc setFromX*(container: Container, x: int) =
@@ -515,11 +518,16 @@ proc updateCursor(container: Container) =
 proc pushCursorPos*(container: Container) =
   container.bpos.add(container.pos)
 
-proc popCursorPos*(container: Container) =
+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()
-  container.writeCommand(MOVE_CURSOR, container.cursorx, container.cursory)
-  container.writeCommand(GET_LINES, container.lineWindow)
+  if not nojump:
+    container.writeCommand(MOVE_CURSOR, container.cursorx, container.cursory)
+    container.requestLines()
 
 macro proxy(fun: typed) =
   let name = fun[0] # sym
@@ -568,10 +576,11 @@ proc gotoAnchor*(container: Container, anchor: string) {.proxy.} = discard
 proc readCanceled*(container: Container) {.proxy.} = discard
 proc readSuccess*(container: Container, s: string) {.proxy.} = discard
 
-proc render*(container: Container) =
+proc render*(container: Container, noreq = false) =
   container.writeCommand(RENDER)
   container.jump = true # may jump to anchor
-  container.writeCommand(GET_LINES, container.lineWindow)
+  if not noreq:
+    container.requestLines()
 
 proc dupeBuffer*(container: Container, config: Config, location = none(URL), contenttype = none(string)): Container =
   let source = BufferSource(
@@ -587,21 +596,9 @@ proc dupeBuffer*(container: Container, config: Config, location = none(URL), con
 proc click*(container: Container) =
   container.writeCommand(CLICK, container.cursorx, container.cursory)
 
-proc drawBuffer*(container: Container) =
-  container.writeCommand(DRAW_BUFFER)
-  while true:
-    var s: string
-    container.istream.sread(s)
-    if s == "": break
-    try:
-      stdout.write(s)
-    except IOError: # couldn't write to stdout; it's probably just a broken pipe.
-      quit(1)
-    stdout.flushFile()
-
-proc windowChange*(container: Container, attrs: TermAttributes) =
+proc windowChange*(container: Container, attrs: WindowAttributes) =
   container.attrs = attrs
-  container.width = attrs.width - 1
+  container.width = attrs.width
   container.height = attrs.height - 1
   container.writeCommand(WINDOW_CHANGE, attrs)
 
@@ -610,9 +607,7 @@ proc clearSearchHighlights*(container: Container) =
     if container.highlights[i].clear:
       container.highlights.del(i)
 
-proc handleEvent*(container: Container): ContainerEvent =
-  var cmd: ContainerCommand
-  container.istream.sread(cmd)
+proc handleCommand(container: Container, cmd: ContainerCommand): ContainerEvent =
   case cmd
   of SET_LINES:
     var w: Slice[int]
@@ -639,10 +634,8 @@ proc handleEvent*(container: Container): ContainerEvent =
     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 != 0:
@@ -664,18 +657,40 @@ proc handleEvent*(container: Container): ContainerEvent =
     container.istream.sread(x)
     container.istream.sread(y)
     container.istream.sread(ex)
-    if container.jump:
+    if x != -1 and y != -1 and container.jump:
       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
-      return ContainerEvent(t: UPDATE)
   of OPEN:
     return ContainerEvent(t: OPEN, request: container.istream.readRequest())
   of SOURCE_READY:
     if container.pipeto != nil:
       container.pipeto.load()
   of RESHAPE:
-    container.writeCommand(GET_LINES, container.lineWindow)
+    container.requestLines()
+
+# Synchronously read all lines in the buffer.
+iterator readLines*(container: Container): SimpleFlexibleLine {.inline.} =
+  var cmd: ContainerCommand
+  container.requestLines(0 .. -1)
+  container.istream.sread(cmd)
+  while cmd != SET_LINES:
+    discard container.handleCommand(cmd)
+    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 cmd: ContainerCommand
+  container.istream.sread(cmd)
+  return container.handleCommand(cmd)
diff --git a/src/css/values.nim b/src/css/values.nim
index f323ba8b..249bfa37 100644
--- a/src/css/values.nim
+++ b/src/css/values.nim
@@ -4,7 +4,7 @@ import strutils
 
 import css/cssparser
 import css/selectorparser
-import io/term
+import io/window
 import types/color
 import utils/twtstr
 
@@ -247,26 +247,26 @@ macro `{}=`*(vals: CSSComputedValues, s: string, v: typed): untyped =
 func inherited(t: CSSPropertyType): bool =
   return InheritedArray[t]
 
-func em_to_px(em: float64, term: TermAttributes): int =
-  int(em * float64(term.ppl))
+func em_to_px(em: float64, window: WindowAttributes): int =
+  int(em * float64(window.ppl))
 
-func ch_to_px(ch: float64, term: TermAttributes): int =
-  int(ch * float64(term.ppc))
+func ch_to_px(ch: float64, window: WindowAttributes): int =
+  int(ch * float64(window.ppc))
 
 # 水 width, we assume it's 2 chars
-func ic_to_px(ic: float64, term: TermAttributes): int =
-  int(ic * float64(term.ppc) * 2)
+func ic_to_px(ic: float64, window: WindowAttributes): int =
+  int(ic * float64(window.ppc) * 2)
 
 # x-letter height, we assume it's em/2
-func ex_to_px(ex: float64, term: TermAttributes): int =
-  int(ex * float64(term.ppc) / 2)
+func ex_to_px(ex: float64, window: WindowAttributes): int =
+  int(ex * float64(window.ppc) / 2)
 
-func px*(l: CSSLength, term: TermAttributes, p: int): int {.inline.} =
+func px*(l: CSSLength, window: WindowAttributes, p: int): int {.inline.} =
   case l.unit
-  of UNIT_EM, UNIT_REM: em_to_px(l.num, term)
-  of UNIT_CH: ch_to_px(l.num, term)
-  of UNIT_IC: ic_to_px(l.num, term)
-  of UNIT_EX: ex_to_px(l.num, term)
+  of UNIT_EM, UNIT_REM: em_to_px(l.num, window)
+  of UNIT_CH: ch_to_px(l.num, window)
+  of UNIT_IC: ic_to_px(l.num, window)
+  of UNIT_EX: ex_to_px(l.num, window)
   of UNIT_PERC: int(p / 100 * l.num)
   of UNIT_PX: int(l.num)
   of UNIT_CM: int(l.num * 37.8)
@@ -274,10 +274,10 @@ func px*(l: CSSLength, term: TermAttributes, p: int): int {.inline.} =
   of UNIT_IN: int(l.num * 96)
   of UNIT_PC: int(l.num * 96 / 6)
   of UNIT_PT: int(l.num * 96 / 72)
-  of UNIT_VW: int(term.width_px / 100 * l.num)
-  of UNIT_VH: int(term.height_px / 100 * l.num)
-  of UNIT_VMIN: int(min(term.width_px, term.width_px) / 100 * l.num)
-  of UNIT_VMAX: int(max(term.width_px, term.width_px) / 100 * l.num)
+  of UNIT_VW: int(window.width_px / 100 * l.num)
+  of UNIT_VH: int(window.height_px / 100 * l.num)
+  of UNIT_VMIN: int(min(window.width_px, window.width_px) / 100 * l.num)
+  of UNIT_VMAX: int(max(window.width_px, window.width_px) / 100 * l.num)
 
 func listMarker*(t: CSSListStyleType, i: int): string =
   case t
diff --git a/src/display/client.nim b/src/display/client.nim
index 54fca3c1..e94a4d4f 100644
--- a/src/display/client.nim
+++ b/src/display/client.nim
@@ -8,6 +8,8 @@ import terminal
 when defined(posix):
   import posix
 
+import std/exitprocs
+
 import bindings/quickjs
 import buffer/container
 import css/sheet
@@ -19,15 +21,15 @@ import io/lineedit
 import io/loader
 import io/request
 import io/term
+import io/window
 import js/javascript
 import types/cookie
 import types/url
-import utils/twtstr
 
 type
   Client* = ref ClientObj
   ClientObj* = object
-    attrs: TermAttributes
+    attrs: WindowAttributes
     feednext: bool
     s: string
     errormessage: string
@@ -107,11 +109,7 @@ proc command(client: Client, src: string) =
   client.console.container.cursorLastLine()
 
 proc quit(client: Client, code = 0) {.jsfunc.} =
-  if stdout.isatty():
-    print(HVP(getTermAttributes(stdout).height, 1))
-    print('\n')
-    print(EL())
-    stdout.showCursor()
+  client.pager.quit()
   when defined(posix):
     assert kill(client.loader.process, cint(SIGTERM)) == 0
   quit(code)
@@ -150,11 +148,6 @@ proc input(client: Client) =
     client.s = ""
   else:
     client.feedNext = false
-  if client.pager.container != nil:
-    if client.pager.lineedit.isNone:
-      client.pager.refreshStatusMsg()
-      client.pager.displayStatus()
-      client.pager.displayCursor()
 
 proc setTimeout[T: JSObject|string](client: Client, handler: T, timeout = 0): int {.jsfunc.} =
   let id = client.timeoutid
@@ -211,8 +204,12 @@ proc clearInterval(client: Client, id: int) {.jsfunc.} =
     client.interval_fdis.del(interval.fdi)
     client.intervals.del(id)
 
+let SIGWINCH {.importc, header: "<signal.h>", nodecl.}: cint
+
 proc inputLoop(client: Client) =
-  client.pager.selector.registerHandle(int(client.console.tty.getFileHandle()), {Read}, nil)
+  let selector = client.pager.selector
+  selector.registerHandle(int(client.console.tty.getFileHandle()), {Read}, nil)
+  let sigwinch = selector.registerSignal(int(SIGWINCH), nil)
   while true:
     let events = client.pager.selector.select(-1)
     for event in events:
@@ -226,6 +223,9 @@ proc inputLoop(client: Client) =
         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.pager.fdmap[FileHandle(event.fd)]
         if not client.pager.handleEvent(container):
@@ -236,11 +236,7 @@ proc inputLoop(client: Client) =
     if client.pager.scommand != "":
       client.command(client.pager.scommand)
       client.pager.scommand = ""
-    if client.pager.lineedit.isNone and client.pager.redraw or client.pager.container.redraw:
-      client.pager.refreshDisplay(client.pager.container)
-      client.pager.draw()
-      client.pager.redraw = false
-      client.pager.container.redraw = false
+    client.pager.draw()
 
 #TODO this is dumb
 proc readFile(client: Client, path: string): string {.jsfunc.} =
@@ -254,6 +250,10 @@ proc writeFile(client: Client, path: string, content: string) {.jsfunc.} =
   writeFile(path, content)
 
 proc launchClient*(client: Client, pages: seq[string], ctype: Option[string], dump: bool) =
+  let pid = getpid()
+  addExitProc((proc() =
+    if pid == getpid():
+      client.quit()))
   if client.config.startup != "":
     let s = readFile(client.config.startup)
     client.console.err = newFileStream(stderr)
@@ -272,15 +272,14 @@ proc launchClient*(client: Client, pages: seq[string], ctype: Option[string], du
     client.pager.loadURL(page, ctype = ctype)
 
   if stdout.isatty() and not dump:
-    when defined(posix):
-      enableRawMode(client.console.tty.getFileHandle())
     client.inputLoop()
   else:
     for msg in client.pager.status:
       eprint msg
+    let ostream = newFileStream(stdout)
     for container in client.pager.containers:
-      container.render()
-      container.drawBuffer()
+      container.render(true)
+      client.pager.drawBuffer(container, ostream)
     stdout.close()
   client.quit()
 
@@ -328,7 +327,7 @@ proc sleep(client: Client, millis: int) {.jsfunc.} =
 proc newClient*(config: Config): Client =
   new(result)
   result.config = config
-  result.attrs = getTermAttributes(stdout)
+  result.attrs = getWindowAttributes(stdout)
   result.loader = newFileLoader()
   var tty: File
   if stdin.isatty():
diff --git a/src/display/pager.nim b/src/display/pager.nim
index d4a2dc99..456e6474 100644
--- a/src/display/pager.nim
+++ b/src/display/pager.nim
@@ -3,7 +3,6 @@ import os
 import selectors
 import streams
 import tables
-import terminal
 import unicode
 
 import buffer/buffer
@@ -13,6 +12,7 @@ import config/config
 import io/lineedit
 import io/request
 import io/term
+import io/window
 import js/javascript
 import js/regex
 import types/url
@@ -24,7 +24,7 @@ type
     SEARCH_B, ISEARCH_F, ISEARCH_B
 
   Pager* = ref object
-    attrs: TermAttributes
+    attrs: WindowAttributes
     commandMode*: bool
     container*: Container
     lineedit*: Option[LineEdit]
@@ -42,9 +42,8 @@ type
     fdmap*: Table[FileHandle, Container]
     icpos: CursorPosition
     display: FixedGrid
-    bheight*: int
-    bwidth*: int
     redraw*: bool
+    term*: Terminal
 
 iterator containers*(pager: Pager): Container =
   if pager.container != nil:
@@ -135,15 +134,21 @@ proc isearchBackward(pager: Pager) {.jsfunc.} =
   pager.container.pushCursorPos()
   pager.setLineEdit(readLine("?", pager.attrs.width, config = pager.config, tty = pager.tty), ISEARCH_B)
 
-proc newPager*(config: Config, attrs: TermAttributes, tty: File): Pager =
+func attrs*(pager: Pager): WindowAttributes = pager.term.attrs
+func bwidth(pager: Pager): int = pager.attrs.width
+func bheight(pager: Pager): int = pager.attrs.height - 1
+
+proc newPager*(config: Config, attrs: WindowAttributes, tty: File): Pager =
   new(result)
   result.config = config
   result.attrs = attrs
   result.tty = tty
   result.selector = newSelector[Container]()
-  result.bwidth = attrs.width - 1 # writing to the last column is a bad idea it seems
-  result.bheight = attrs.height - 1
   result.display = newFixedGrid(result.bwidth, result.bheight)
+  result.term = newTerminal(tty, stdout)
+
+proc quit*(pager: Pager, code = 0) =
+  pager.term.quit()
 
 proc clearDisplay(pager: Pager) =
   pager.display = newFixedGrid(pager.bwidth, pager.bheight)
@@ -177,7 +182,7 @@ proc refreshDisplay*(pager: Pager, container = pager.container) =
       let pw = w
       fastRuneAt(line.str, i, r)
       w += r.width()
-      if w > container.fromx + pager.bwidth:
+      if w > container.fromx + pager.display.width:
         break # die on exceeding the width limit
       if nf.pos != -1 and nf.pos <= pw:
         cf = nf
@@ -186,13 +191,13 @@ proc refreshDisplay*(pager: Pager, container = pager.container) =
       if cf.pos != -1:
         pager.display[dls + k].format = cf.format
       let tk = k + r.width()
-      while k < tk and k < pager.bwidth - 1:
+      while k < tk and k < pager.display.width - 1:
         inc k
     # Finally, override cell formatting for highlighted cells.
-    let hls = container.findHighlights(by)
+    let hls = container.findHighlights(container.fromy + by)
     let aw = container.width - (startw - container.fromx) # actual width
     for hl in hls:
-      let area = hl.colorArea(by, startw .. startw + aw)
+      let area = hl.colorArea(container.fromy + by, startw .. startw + aw)
       for i in area:
         pager.display[dls + i - startw].format = hlformat
     inc by
@@ -201,7 +206,7 @@ func generateStatusMessage*(pager: Pager): string =
   var format = newFormat()
   var w = 0
   for cell in pager.statusmsg:
-    result &= format.processFormat(cell.format)
+    result &= pager.term.processFormat(format, cell.format)
     result &= cell.str
     w += cell.width()
   if w < pager.bwidth:
@@ -239,55 +244,55 @@ func generateStatusOutput(pager: Pager): string =
   else:
     return pager.generateStatusMessage()
 
-func generateFullOutput(pager: Pager): string =
-  var x = 0
-  var w = 0
-  var format = newFormat()
-  result &= HVP(1, 1)
-  for cell in pager.display:
-    if x >= pager.bwidth:
-      result &= EL()
-      result &= "\r\n"
-      x = 0
-      w = 0
-    result &= format.processFormat(cell.format)
-    result &= cell.str
-    w += cell.width()
-    inc x
-  result &= EL()
-  result &= "\r\n"
-
-proc displayCursor*(pager: Pager) =
-  if pager.container == nil: return
-  print(HVP(pager.container.acursory + 1, pager.container.acursorx + 1))
-  stdout.flushFile()
-
 proc displayStatus*(pager: Pager) =
   if pager.lineedit.isNone:
     pager.statusMode()
     print(pager.generateStatusOutput())
     stdout.flushFile()
 
-proc displayPage*(pager: Pager) =
-  stdout.hideCursor()
-  print(SGR())
-  print(pager.generateFullOutput())
-  pager.displayStatus()
-  pager.displayCursor()
-  stdout.showCursor()
-  if pager.lineedit.isSome:
-    pager.statusMode()
-    pager.lineedit.get.writePrompt()
-    pager.lineedit.get.fullRedraw()
-  stdout.flushFile()
+proc drawBuffer*(pager: Pager, container: Container, ostream: Stream) =
+  var format = newFormat()
+  for line in container.readLines:
+    if line.formats.len == 0:
+      ostream.write(line.str & "\n")
+    else:
+      var x = 0
+      var i = 0
+      var s = ""
+      for f in line.formats:
+        var outstr = ""
+        #assert f.pos < line.str.width(), "fpos " & $f.pos & "\nstr" & line.str & "\n"
+        while x < f.pos:
+          var r: Rune
+          fastRuneAt(line.str, i, r)
+          outstr &= r
+          x += r.width()
+        s &= outstr
+        s &= pager.term.processFormat(format, f.format)
+      s &= line.str.substr(i) & pager.term.processFormat(format, newFormat()) & "\n"
+      ostream.write(s)
+  ostream.flush()
 
 proc redraw(pager: Pager) {.jsfunc.} =
   pager.redraw = true
 
 proc draw*(pager: Pager) =
-  pager.refreshDisplay()
+  pager.term.hideCursor()
+  if pager.redraw or pager.container != nil and pager.container.redraw:
+    pager.refreshDisplay()
+    pager.term.outputGrid(pager.display)
   pager.refreshStatusMsg()
-  pager.displayPage()
+  pager.displayStatus()
+  if pager.lineedit.isSome:
+    pager.statusMode()
+    pager.lineedit.get.writePrompt()
+    pager.lineedit.get.fullRedraw()
+  elif pager.container != nil:
+    pager.term.setCursor(pager.container.acursorx, pager.container.acursory)
+  pager.term.showCursor()
+  pager.term.flush()
+  pager.redraw = false
+  pager.container.redraw = false
 
 proc registerContainer*(pager: Pager, container: Container) =
   pager.fdmap[container.ifd] = container
@@ -398,6 +403,11 @@ proc toggleSource*(pager: Pager) {.jsfunc.} =
     pager.container.sourcepair = container
     pager.container.children.add(container)
 
+proc windowChange*(pager: Pager, attrs: WindowAttributes) =
+  pager.attrs = attrs
+  for container in pager.containers:
+    container.windowChange(attrs)
+
 # Load request in a new buffer.
 proc gotoURL*(pager: Pager, request: Request, prevurl = none(URL), ctype = none(string), replace: Container = nil) =
   if prevurl.isnone or not prevurl.get.equals(request.url, true) or
@@ -490,8 +500,7 @@ proc updateReadLineISearch(pager: Pager, linemode: LineMode) =
   of EDIT:
     let x = $lineedit.news
     if x != "": pager.iregex = compileSearchRegex(x)
-    pager.container.clearSearchHighlights()
-    pager.container.popCursorPos()
+    pager.container.popCursorPos(true)
     if pager.iregex.isSome:
       if linemode == ISEARCH_F:
         pager.container.cursorNextMatch(pager.iregex.get, true)
@@ -499,9 +508,7 @@ proc updateReadLineISearch(pager: Pager, linemode: LineMode) =
         pager.container.cursorPrevMatch(pager.iregex.get, true)
       pager.container.hlon = true
     pager.container.pushCursorPos()
-    pager.displayPage()
-    pager.statusMode()
-    pager.lineedit.get.fullRedraw()
+    pager.draw()
   of FINISH:
     if pager.iregex.isSome:
       pager.regex = pager.iregex
@@ -582,8 +589,6 @@ proc handleEvent*(pager: Pager, container: Container): bool =
       pager.gotoURL(newRequest(container.retry.pop()), ctype = container.contenttype)
     else:
       pager.setStatusMessage("Couldn't load " & $container.source.location & " (error code " & $container.code & ")")
-      pager.displayStatus()
-      pager.displayCursor()
     if pager.container == nil:
       return false
   of SUCCESS:
@@ -606,28 +611,14 @@ proc handleEvent*(pager: Pager, container: Container): bool =
   of REDIRECT:
     let redirect = container.redirect.get
     pager.setStatusMessage("Redirecting to " & $redirect)
-    pager.displayStatus()
-    pager.displayCursor()
     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.displayStatus()
-    pager.displayCursor()
   of UPDATE:
     if container == pager.container:
       pager.redraw = true
-  of JUMP:
-    if container == pager.container:
-      pager.refreshStatusMsg()
-      pager.displayStatus()
-      pager.displayCursor()
-  of STATUS:
-    if container == pager.container:
-      pager.refreshStatusMsg()
-      pager.displayStatus()
-      pager.displayCursor()
   of READ_LINE:
     if container == pager.container:
       pager.setLineEdit(readLine(event.prompt, pager.bwidth, current = event.value, hide = event.password, config = pager.config, tty = pager.tty), BUFFER)
diff --git a/src/io/term.nim b/src/io/term.nim
index 1eacd237..d39273d0 100644
--- a/src/io/term.nim
+++ b/src/io/term.nim
@@ -1,16 +1,243 @@
+import os
+import streams
+import strutils
 import terminal
 
-import std/exitprocs
+import bindings/notcurses
+import buffer/cell
+import io/window
+import types/color
+import utils/twtstr
 
 type
-  TermAttributes* = object
-    width*: int
-    height*: int
-    ppc*: int # cell width
-    ppl*: int # cell height
-    cell_ratio*: float64 # ppl / ppc
-    width_px*: int
-    height_px*: int
+  ColorMode = enum
+    MONOCHROME, ANSI, EIGHT_BIT, TRUE_COLOR
+
+  FormatMode = set[FormatFlags]
+
+  Terminal* = ref TerminalObj
+  TerminalObj = object
+    infile: File
+    outfile: File
+    nc*: ncdirect
+    cleared: bool
+    prevgrid: FixedGrid
+    attrs*: WindowAttributes
+    colormode: ColorMode
+    formatmode: FormatMode
+    smcup: bool
+
+  TermInfo = ref object
+
+proc `=destroy`(term: var TerminalObj) =
+  if term.nc != nil:
+    #discard ncdirect_stop(term.nc)
+    term.nc = nil
+
+template CSI*(s: varargs[string, `$`]): string =
+  var r = "\e["
+  var first = true
+  for x in s:
+    if not first:
+      r &= ";"
+    first = false
+    r &= x
+  r
+
+template DECSET(s: varargs[string, `$`]): string =
+  var r = "\e[?"
+  var first = true
+  for x in s:
+    if not first:
+      r &= ";"
+    first = false
+    r &= x
+  r & "h"
+
+template DECRST(s: varargs[string, `$`]): string =
+  var r = "\e[?"
+  var first = true
+  for x in s:
+    if not first:
+      r &= ";"
+    first = false
+    r &= x
+  r & "l"
+
+template SMCUP(): string = DECSET(1049)
+template RMCUP(): string = DECRST(1049)
+
+template SGR*(s: varargs[string, `$`]): string =
+  CSI(s) & "m"
+
+template HVP*(s: varargs[string, `$`]): string =
+  CSI(s) & "f"
+
+template EL*(s: varargs[string, `$`]): string =
+  CSI(s) & "K"
+
+proc processFormat*(term: Terminal, format: var Format, cellf: Format): string =
+  for flag in FormatFlags:
+    if flag in format.flags and flag notin cellf.flags:
+      result &= SGR(FormatCodes[flag].e)
+
+  if cellf.fgcolor != format.fgcolor:
+    var color = cellf.fgcolor
+    if color.rgb:
+      let rgb = color.rgbcolor
+      result &= SGR(38, 2, rgb.r, rgb.g, rgb.b)
+    elif color == defaultColor:
+      result &= SGR()
+      format = newFormat()
+    else:
+      result &= SGR(color.color)
+
+  if cellf.bgcolor != format.bgcolor:
+    var color = cellf.bgcolor
+    if color.rgb:
+      let rgb = color.rgbcolor
+      result &= SGR(48, 2, rgb.r, rgb.g, rgb.b)
+    elif color == defaultColor:
+      result &= SGR()
+      format = newFormat()
+    else:
+      result &= SGR(color.color)
+
+  for flag in FormatFlags:
+    if flag notin format.flags and flag in cellf.flags:
+      result &= SGR(FormatCodes[flag].s)
+
+  format = cellf
+
+proc updateWindow*(term: Terminal) =
+  term.attrs = getWindowAttributes(term.outfile)
+
+proc findTermInfoDirs(termenv: string): seq[string] =
+  let tienv = getEnv("TERMINFO")
+  if tienv != "":
+    if dirExists(tienv):
+      return @[tienv]
+  else:
+    let home = getEnv("HOME")
+    if home != "":
+      result.add(home & '/' & ".terminfo")
+  let tidirsenv = getEnv("TERMINFO_DIRS")
+  if tidirsenv != "":
+    for s in tidirsenv.split({':'}):
+      if s == "":
+        result.add("/usr/share/terminfo")
+      else:
+        result.add(s)
+    return result
+  result.add("/usr/share/terminfo")
+
+proc findFile(dir: string, file: string): string =
+  var stack = dir
+  for f in walkDirRec(dir, followFilter = {pcDir, pcLinkToDir}):
+    if f == file:
+      return f
+
+proc parseTermInfo(s: Stream): TermInfo =
+  let magic = s.readInt16()
+  #TODO do we really want this?
+  s.close()
+
+proc getTermInfo(termenv: string): TermInfo =
+  let tipaths = findTermInfoDirs(termenv)
+  for tipath in tipaths:
+    let f = findFile(tipath, termenv)
+    if f != "":
+      return parseTermInfo(newFileStream(f))
+
+proc getCursorPos(term: Terminal): (int, int) =
+  term.outfile.write(CSI("6n"))
+  term.outfile.flushFile()
+  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)
+
+proc detectTermAttributes*(term: Terminal) =
+  term.colormode = ANSI
+  let colorterm = getEnv("COLORTERM")
+  case colorterm
+  of "24bit", "truecolor": term.colormode = TRUE_COLOR
+  #TODO terminfo/termcap?
+
+func generateFullOutput(term: Terminal, grid: FixedGrid): string =
+  var x = 0
+  var format = newFormat()
+  result &= HVP(1, 1)
+  for cell in grid.cells:
+    if x >= grid.width - 1:
+      result &= EL()
+      result &= "\r\n"
+      x = 0
+    result &= term.processFormat(format, cell.format)
+    result &= cell.str
+    inc x
+  result &= EL()
+
+func generateSwapOutput(term: Terminal, grid: FixedGrid): string =
+  var format = newFormat()
+  let curr = grid.cells
+  let prev = term.prevgrid.cells
+  var i = 0
+  var x = 0
+  var y = 0
+  var line = ""
+  var lr = false
+  while i < curr.len:
+    if x >= grid.width - 1:
+      if lr:
+        result &= HVP(y + 1, 1)
+        result &= EL()
+        result &= line
+        lr = false
+      x = 0
+      inc y
+      line = ""
+    lr = lr or (curr[i] != prev[i])
+    line &= term.processFormat(format, curr[i].format)
+    line &= curr[i].str
+    inc i
+    inc x
+  if lr:
+    result &= HVP(y + 1, 1)
+    result &= EL()
+    result &= line
+    lr = false
+
+proc hideCursor*(term: Terminal) =
+  term.outfile.hideCursor()
+
+proc showCursor*(term: Terminal) =
+  term.outfile.showCursor()
+
+proc flush*(term: Terminal) =
+  term.outfile.flushFile()
+
+proc outputGrid*(term: Terminal, grid: FixedGrid) =
+  term.outfile.write(SGR())
+  if not term.cleared:
+    term.outfile.write(term.generateFullOutput(grid))
+    term.cleared = true
+  else:
+    term.outfile.write(term.generateSwapOutput(grid))
+  term.prevgrid = grid
+
+proc setCursor*(term: Terminal, x, y: int) =
+  term.outfile.write(HVP(y + 1, x + 1))
 
 when defined(posix):
   import posix
@@ -24,7 +251,6 @@ when defined(posix):
 
   proc enableRawMode*(fileno: FileHandle) =
     stdin_fileno = fileno
-    addExitProc(disableRawMode)
     discard tcGetAttr(fileno, addr orig_termios)
     var raw = orig_termios
     raw.c_iflag = raw.c_iflag and not (BRKINT or ICRNL or INPCK or ISTRIP or IXON)
@@ -52,27 +278,29 @@ else:
   proc restoreStdin*(flags: cint) =
     discard
 
-proc getTermAttributes*(tty: File): TermAttributes =
-  if tty.isatty():
+proc isatty*(term: Terminal): bool =
+  term.infile.isatty() and term.outfile.isatty()
+
+proc quit*(term: Terminal) =
+  if term.isatty():
     when defined(posix):
-      var win: IOctl_WinSize
-      if ioctl(cint(getOsFileHandle(tty)), TIOCGWINSZ, addr win) != -1:
-        result.ppc = int(win.ws_xpixel) div int(win.ws_col)
-        result.ppl = int(win.ws_ypixel) div int(win.ws_row)
-        # some terminals don't like it when we fill the last cell. #TODO make this optional
-        result.width = int(win.ws_col) - 1
-        result.height = int(win.ws_row)
-        result.width_px = result.width * result.ppc
-        result.height_px = result.height * result.ppl
-        result.cell_ratio = result.ppl / result.ppc
-        return
-  #fail
-  result.width = terminalWidth() - 1
-  result.height = terminalHeight()
-  if result.height == 0:
-    result.height = 24
-  result.ppc = 9
-  result.ppl = 18
-  result.cell_ratio = result.ppl / result.ppc
-  result.width_px = result.ppc * result.width
-  result.height_px = result.ppl * result.height
+      disableRawMode()
+    if term.smcup:
+      term.outfile.write(RMCUP())
+    else:
+      term.outfile.write(HVP(term.attrs.height, 1) & '\n')
+    term.outfile.showCursor()
+  term.outfile.flushFile()
+
+proc newTerminal*(infile, outfile: File, force_minimal = false): Terminal =
+  let term = new Terminal
+  term.infile = infile
+  term.outfile = outfile
+  when defined(posix):
+    if term.isatty():
+      enableRawMode(infile.getFileHandle())
+  if not force_minimal:
+    term.detectTermAttributes()
+    term.smcup = true
+    term.outfile.write(SMCUP())
+  return term
diff --git a/src/io/window.nim b/src/io/window.nim
new file mode 100644
index 00000000..95da9e70
--- /dev/null
+++ b/src/io/window.nim
@@ -0,0 +1,52 @@
+import terminal
+
+when defined(posix):
+  import termios
+
+
+type
+  WindowAttributes* = object
+    width*: int
+    height*: int
+    ppc*: int # cell width
+    ppl*: int # cell height
+    cell_ratio*: float64 # ppl / ppc
+    width_px*: int
+    height_px*: int
+
+proc getWindowAttributes*(tty: File): WindowAttributes =
+  if tty.isatty():
+    when defined(posix):
+      var win: IOctl_WinSize
+      if ioctl(cint(getOsFileHandle(tty)), TIOCGWINSZ, addr win) != -1:
+        var cols = win.ws_col
+        var rows = win.ws_row
+        if cols == 0:
+          cols = 80
+        if rows == 0:
+          rows = 24
+        # some terminals don't like it when we fill the last cell. #TODO make this optional
+        result.width = int(cols) - 1
+        result.height = int(rows)
+        result.ppc = int(win.ws_xpixel) div result.width
+        result.ppl = int(win.ws_ypixel) div result.height
+        # some terminal emulators (aka vte) don't set ws_xpixel or ws_ypixel.
+        # solution: use xterm.
+        if result.ppc == 0:
+          result.ppc = 9
+        if result.ppl == 0:
+          result.ppl = 18
+        result.width_px = result.width * result.ppc
+        result.height_px = result.height * result.ppl
+        result.cell_ratio = result.ppl / result.ppc
+        return
+  #fail
+  result.width = terminalWidth() - 1
+  result.height = terminalHeight()
+  if result.height == 0:
+    result.height = 24
+  result.ppc = 9
+  result.ppl = 18
+  result.cell_ratio = result.ppl / result.ppc
+  result.width_px = result.ppc * result.width
+  result.height_px = result.ppl * result.height
diff --git a/src/layout/box.nim b/src/layout/box.nim
index 204abc00..4875ffda 100644
--- a/src/layout/box.nim
+++ b/src/layout/box.nim
@@ -3,7 +3,7 @@ import options
 import css/stylednode
 import css/values
 import html/dom
-import io/term
+import io/window
 
 type
   #LayoutUnit* = distinct int32
@@ -22,7 +22,7 @@ type
     neg*: int
 
   Viewport* = ref object
-    term*: TermAttributes
+    window*: WindowAttributes
     root*: BlockBox
 
   BoxBuilder* = ref object of RootObj
diff --git a/src/layout/engine.nim b/src/layout/engine.nim
index 7b651e4f..09103129 100644
--- a/src/layout/engine.nim
+++ b/src/layout/engine.nim
@@ -5,13 +5,13 @@ import css/stylednode
 import css/values
 import html/tags
 import html/dom
-import io/term
+import io/window
 import layout/box
 import utils/twtstr
 
 # Build phase
 func px(l: CSSLength, viewport: Viewport, p = 0): int {.inline.} =
-  return px(l, viewport.term, p)
+  return px(l, viewport.window, p)
 
 type InlineState = object
   ictx: InlineContext
@@ -25,13 +25,13 @@ func whitespacepre(computed: CSSComputedValues): bool {.inline.} =
   computed{"white-space"} in {WHITESPACE_PRE, WHITESPACE_PRE_WRAP}
 
 func cellwidth(viewport: Viewport): int {.inline.} =
-  viewport.term.ppc
+  viewport.window.ppc
 
 func cellwidth(ictx: InlineContext): int {.inline.} =
   ictx.viewport.cellwidth
 
 func cellheight(viewport: Viewport): int {.inline.} =
-  viewport.term.ppl
+  viewport.window.ppl
 
 func cellheight(ictx: InlineContext): int {.inline.} =
   ictx.viewport.cellheight
@@ -411,7 +411,7 @@ proc newListItem(parent: BlockBox, builder: ListItemBoxBuilder): ListItemBox =
   result.shrink = result.computed{"width"}.auto and parent.shrink
 
 proc newBlockBox(viewport: Viewport, box: BlockBoxBuilder): BlockBox =
-  result = newFlowRootBox(viewport, box, viewport.term.width_px)
+  result = newFlowRootBox(viewport, box, viewport.window.width_px)
 
 proc newInlineBlock(viewport: Viewport, builder: InlineBlockBoxBuilder, parentWidth: int, parentHeight = none(int)): InlineBlockBox =
   new(result)
diff --git a/src/render/renderdocument.nim b/src/render/renderdocument.nim
index 44b369f7..06901ba6 100644
--- a/src/render/renderdocument.nim
+++ b/src/render/renderdocument.nim
@@ -7,7 +7,7 @@ import css/sheet
 import css/stylednode
 import css/values
 import html/dom
-import io/term
+import io/window
 import layout/box
 import layout/engine
 import types/color
@@ -128,13 +128,13 @@ proc setText(lines: var FlexibleGrid, linestr: string, cformat: ComputedFormat,
   assert lines[y].formats[fi].pos <= nx
   # That's it!
 
-proc setRowWord(lines: var FlexibleGrid, word: InlineWord, x, y: int, term: TermAttributes) =
+proc setRowWord(lines: var FlexibleGrid, word: InlineWord, x, y: int, window: WindowAttributes) =
   var r: Rune
 
-  var y = (y + word.offset.y) div term.ppl # y cell
+  var y = (y + word.offset.y) div window.ppl # y cell
   if y < 0: return # y is outside the canvas, no need to draw
 
-  var x = (x + word.offset.x) div term.ppc # x cell
+  var x = (x + word.offset.x) div window.ppc # x cell
   var i = 0
   while x < 0 and i < word.str.len:
     fastRuneAt(word.str, i, r)
@@ -144,12 +144,12 @@ proc setRowWord(lines: var FlexibleGrid, word: InlineWord, x, y: int, term: Term
 
   lines.setText(linestr, word.format, x, y)
 
-proc setSpacing(lines: var FlexibleGrid, spacing: InlineSpacing, x, y: int, term: TermAttributes) =
-  var y = (y + spacing.offset.y) div term.ppl # y cell
+proc setSpacing(lines: var FlexibleGrid, spacing: InlineSpacing, x, y: int, window: WindowAttributes) =
+  var y = (y + spacing.offset.y) div window.ppl # y cell
   if y < 0: return # y is outside the canvas, no need to draw
 
-  var x = (x + spacing.offset.x) div term.ppc # x cell
-  let width = spacing.width div term.ppc # cell width
+  var x = (x + spacing.offset.x) div window.ppc # x cell
+  let width = spacing.width div window.ppc # cell width
 
   if x + width < 0: return # highest x is outside the canvas, no need to draw
   var i = 0
@@ -160,11 +160,11 @@ proc setSpacing(lines: var FlexibleGrid, spacing: InlineSpacing, x, y: int, term
 
   lines.setText(linestr, spacing.format, x, y)
 
-proc paintBackground(lines: var FlexibleGrid, color: CSSColor, startx, starty, endx, endy: int, term: TermAttributes) =
+proc paintBackground(lines: var FlexibleGrid, color: CSSColor, startx, starty, endx, endy: int, window: WindowAttributes) =
   let color = color.cellColor()
 
-  var starty = starty div term.ppl
-  var endy = endy div term.ppl
+  var starty = starty div window.ppl
+  var endy = endy div window.ppl
 
   if starty > endy:
     swap(starty, endy)
@@ -173,9 +173,9 @@ proc paintBackground(lines: var FlexibleGrid, color: CSSColor, startx, starty, e
   if starty < 0: starty = 0
   if starty == endy: return # height is 0, no need to paint
 
-  var startx = startx div term.ppc
+  var startx = startx div window.ppc
 
-  var endx = endx div term.ppc
+  var endx = endx div window.ppc
   if endy < 0: endy = 0
 
   if startx > endx:
@@ -237,40 +237,40 @@ proc paintBackground(lines: var FlexibleGrid, color: CSSColor, startx, starty, e
       if lines[y].formats[fi].pos >= startx:
         lines[y].formats[fi].format.bgcolor = color
 
-proc renderBlockContext(grid: var FlexibleGrid, ctx: BlockBox, x, y: int, term: TermAttributes)
+proc renderBlockContext(grid: var FlexibleGrid, ctx: BlockBox, x, y: int, window: WindowAttributes)
 
-proc renderInlineContext(grid: var FlexibleGrid, ctx: InlineContext, x, y: int, term: TermAttributes) =
+proc renderInlineContext(grid: var FlexibleGrid, ctx: InlineContext, x, y: int, window: WindowAttributes) =
   let x = x + ctx.offset.x
   let y = y + ctx.offset.y
   for line in ctx.lines:
     let x = x + line.offset.x
     let y = y + line.offset.y
 
-    let r = y div term.ppl
+    let r = y div window.ppl
     while grid.len <= r:
       grid.addLine()
 
     for atom in line.atoms:
       if atom of InlineBlockBox:
         let iblock = InlineBlockBox(atom)
-        grid.renderBlockContext(iblock.bctx, x + iblock.offset.x, y + iblock.offset.y, term)
+        grid.renderBlockContext(iblock.bctx, x + iblock.offset.x, y + iblock.offset.y, window)
       elif atom of InlineWord:
         let word = InlineWord(atom)
-        grid.setRowWord(word, x, y, term)
+        grid.setRowWord(word, x, y, window)
       elif atom of InlineSpacing:
         let spacing = InlineSpacing(atom)
-        grid.setSpacing(spacing, x, y, term)
+        grid.setSpacing(spacing, x, y, window)
 
-proc renderTable(grid: var FlexibleGrid, ctx: TableBox, x, y: int, term: TermAttributes) =
+proc renderTable(grid: var FlexibleGrid, ctx: TableBox, x, y: int, window: WindowAttributes) =
   for row in ctx.nested:
     assert row.computed{"display"} == DISPLAY_TABLE_ROW
     for cell in row.nested:
       let x = x + row.offset.x
       let y = y + row.offset.y
       assert cell.computed{"display"} == DISPLAY_TABLE_CELL
-      grid.renderBlockContext(cell, x, y, term)
+      grid.renderBlockContext(cell, x, y, window)
 
-proc renderBlockContext(grid: var FlexibleGrid, ctx: BlockBox, x, y: int, term: TermAttributes) =
+proc renderBlockContext(grid: var FlexibleGrid, ctx: BlockBox, x, y: int, window: WindowAttributes) =
   var stack = newSeqOfCap[(BlockBox, int, int)](100)
   stack.add((ctx, x, y))
 
@@ -280,29 +280,29 @@ proc renderBlockContext(grid: var FlexibleGrid, ctx: BlockBox, x, y: int, term:
     y += ctx.offset.y
 
     if ctx.computed{"background-color"}.rgba.a != 0: #TODO color blending
-      grid.paintBackground(ctx.computed{"background-color"}, x, y, x + ctx.width, y + ctx.height, term)
+      grid.paintBackground(ctx.computed{"background-color"}, x, y, x + ctx.width, y + ctx.height, window)
 
     if ctx of ListItemBox:
       let ctx = ListItemBox(ctx)
       if ctx.marker != nil:
-        grid.renderInlineContext(ctx.marker, x - ctx.marker.maxwidth, y, term)
+        grid.renderInlineContext(ctx.marker, x - ctx.marker.maxwidth, y, window)
 
     if ctx.inline != nil:
       assert ctx.nested.len == 0
-      grid.renderInlineContext(ctx.inline, x, y, term)
+      grid.renderInlineContext(ctx.inline, x, y, window)
     elif ctx.computed{"display"} == DISPLAY_TABLE: #TODO INLINE_TABLE
-      grid.renderTable(TableBox(ctx), x, y, term)
+      grid.renderTable(TableBox(ctx), x, y, window)
     else:
       for i in countdown(ctx.nested.high, 0):
         stack.add((ctx.nested[i], x, y))
 
 const css = staticRead"res/ua.css"
 let uastyle = css.parseStylesheet()
-proc renderDocument*(document: Document, term: TermAttributes, userstyle: CSSStylesheet, layout: var Viewport, previousStyled: StyledNode): (FlexibleGrid, StyledNode) =
+proc renderDocument*(document: Document, window: WindowAttributes, userstyle: CSSStylesheet, layout: var Viewport, previousStyled: StyledNode): (FlexibleGrid, StyledNode) =
   let styledNode = document.applyStylesheets(uastyle, userstyle, previousStyled)
   result[1] = styledNode
   layout.renderLayout(document, styledNode)
   result[0].setLen(0)
-  result[0].renderBlockContext(layout.root, 0, 0, term)
+  result[0].renderBlockContext(layout.root, 0, 0, window)
   if result[0].len == 0:
     result[0].addLine()
diff --git a/src/utils/twtstr.nim b/src/utils/twtstr.nim
index 3636da14..2123daeb 100644
--- a/src/utils/twtstr.nim
+++ b/src/utils/twtstr.nim
@@ -628,25 +628,6 @@ proc expandPath*(path: string): string =
         if p != nil:
           result = $p.pw_dir / path.substr(i)
 
-template CSI*(s: varargs[string, `$`]): string =
-  var r = "\e["
-  var first = true
-  for x in s:
-    if not first:
-      r &= ";"
-    first = false
-    r &= x
-  r
-
-template SGR*(s: varargs[string, `$`]): string =
-  CSI(s) & "m"
-
-template HVP*(s: varargs[string, `$`]): string =
-  CSI(s) & "f"
-
-template EL*(s: varargs[string, `$`]): string =
-  CSI(s) & "K"
-
 iterator split*(s: seq[Rune], sep: Rune): seq[Rune] =
   var i = 0
   var prev = 0