about summary refs log tree commit diff stats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/config/config.nim8
-rw-r--r--src/css/values.nim2
-rw-r--r--src/display/lineedit.nim2
-rw-r--r--src/display/term.nim240
-rw-r--r--src/display/winattrs.nim58
-rw-r--r--src/html/dom.nim2
-rw-r--r--src/html/env.nim2
-rw-r--r--src/layout/engine.nim2
-rw-r--r--src/layout/renderdocument.nim61
-rw-r--r--src/local/client.nim7
-rw-r--r--src/local/container.nim2
-rw-r--r--src/local/pager.nim28
-rw-r--r--src/server/buffer.nim2
-rw-r--r--src/server/forkserver.nim2
-rw-r--r--src/types/cell.nim208
15 files changed, 256 insertions, 370 deletions
diff --git a/src/config/config.nim b/src/config/config.nim
index 1ae0a727..e7c0d0aa 100644
--- a/src/config/config.nim
+++ b/src/config/config.nim
@@ -121,6 +121,14 @@ type
     default_background_color* {.jsgetset.}: Opt[RGBColor]
     default_foreground_color* {.jsgetset.}: Opt[RGBColor]
     query_da1* {.jsgetset.}: bool
+    columns* {.jsgetset.}: int32
+    lines* {.jsgetset.}: int32
+    pixels_per_column* {.jsgetset.}: int32
+    pixels_per_line* {.jsgetset.}: int32
+    force_columns* {.jsgetset.}: bool
+    force_lines* {.jsgetset.}: bool
+    force_pixels_per_column* {.jsgetset.}: bool
+    force_pixels_per_line* {.jsgetset.}: bool
 
   Config* = ref ConfigObj
   ConfigObj* = object
diff --git a/src/css/values.nim b/src/css/values.nim
index c53825ee..e1282f57 100644
--- a/src/css/values.nim
+++ b/src/css/values.nim
@@ -6,7 +6,7 @@ import std/unicode
 
 import css/cssparser
 import css/selectorparser
-import display/winattrs
+import display/term
 import img/bitmap
 import layout/layoutunit
 import types/color
diff --git a/src/display/lineedit.nim b/src/display/lineedit.nim
index 6969171e..a69465a6 100644
--- a/src/display/lineedit.nim
+++ b/src/display/lineedit.nim
@@ -2,7 +2,7 @@ import std/strutils
 import std/unicode
 
 import bindings/quickjs
-import display/winattrs
+import display/term
 import js/javascript
 import types/cell
 import types/opt
diff --git a/src/display/term.nim b/src/display/term.nim
index 588951cd..a782fe99 100644
--- a/src/display/term.nim
+++ b/src/display/term.nim
@@ -9,7 +9,6 @@ import std/unicode
 
 import bindings/termcap
 import config/config
-import display/winattrs
 import types/cell
 import types/color
 import types/opt
@@ -20,8 +19,6 @@ import chagashi/charset
 import chagashi/encoder
 import chagashi/validator
 
-export isatty
-
 #TODO switch from termcap...
 
 type
@@ -49,6 +46,14 @@ type
     funcstr: array[256, uint8]
     caps: array[TermcapCap, cstring]
 
+  WindowAttributes* = object
+    width*: int
+    height*: int
+    ppc*: int # cell width
+    ppl*: int # cell height
+    width_px*: int
+    height_px*: int
+
   Terminal* = ref TerminalObj
   TerminalObj = object
     cs*: Charset
@@ -79,6 +84,12 @@ template CSI(s: varargs[string, `$`]): string =
 # primary device attributes
 const DA1 = CSI("c")
 
+# report xterm text area size in pixels
+const GEOMPIXEL = CSI(14, "t")
+
+# report window size in chars
+const GEOMCELL = CSI(18, "t")
+
 # device control string
 template DCS(a, b: char, s: varargs[string]): string =
   "\eP" & a & b & s.join(';') & "\e\\"
@@ -175,6 +186,10 @@ proc clearDisplay(term: Terminal): string =
   else:
     return ED()
 
+proc isatty(fd: FileHandle): cint {.importc: "isatty", header: "<unistd.h>".}
+proc isatty*(f: File): bool =
+  return isatty(f.getFileHandle()) != 0
+
 proc isatty(term: Terminal): bool =
   term.infile != nil and term.infile.isatty() and term.outfile.isatty()
 
@@ -381,11 +396,6 @@ proc processFormat*(term: Terminal, format: var Format, cellf: Format): string =
     discard # nothing to do
   format = cellf
 
-proc windowChange*(term: Terminal, attrs: WindowAttributes) =
-  term.attrs = attrs
-  term.canvas = newFixedGrid(attrs.width, attrs.height)
-  term.cleared = false
-
 proc setTitle*(term: Terminal, title: string) =
   if term.set_title:
     let title = if Controls in title:
@@ -504,7 +514,21 @@ proc writeGrid*(term: Terminal, grid: FixedGrid, x = 0, y = 0) =
             cell.format.fgcolor = grid[i].format.fgcolor
           j += cell[].width()
 
+proc applyConfigDimensions(term: Terminal) =
+  # screen dimensions
+  if term.attrs.width == 0 or term.config.display.force_columns:
+    term.attrs.width = int(term.config.display.columns)
+  if term.attrs.height == 0 or term.config.display.force_lines:
+    term.attrs.height = int(term.config.display.lines)
+  if term.attrs.ppc == 0 or term.config.display.force_pixels_per_column:
+    term.attrs.ppc = int(term.config.display.pixels_per_column)
+  if term.attrs.ppl == 0 or term.config.display.force_pixels_per_line:
+    term.attrs.ppl = int(term.config.display.pixels_per_line)
+  term.attrs.width_px = term.attrs.ppc * term.attrs.width
+  term.attrs.height_px = term.attrs.ppl * term.attrs.height
+
 proc applyConfig(term: Terminal) =
+  # colors, formatting
   if term.config.display.color_mode.isSome:
     term.colormode = term.config.display.color_mode.get
   elif term.isatty():
@@ -524,6 +548,7 @@ proc applyConfig(term: Terminal) =
     term.defaultBackground = term.config.display.default_background_color.get
   if term.config.display.default_foreground_color.isSome:
     term.defaultForeground = term.config.display.default_foreground_color.get
+  # charsets
   if term.config.encoding.display_charset.isSome:
     term.cs = term.config.encoding.display_charset.get
   else:
@@ -536,10 +561,9 @@ proc applyConfig(term: Terminal) =
       if cs != CHARSET_UNKNOWN:
         term.cs = cs
         break
+  term.applyConfigDimensions()
 
 proc outputGrid*(term: Terminal) =
-  if term.config.display.force_clear:
-    term.applyConfig()
   term.outfile.write(term.resetFormat())
   let samesize = term.canvas.width == term.pcanvas.width and
     term.canvas.height == term.pcanvas.height
@@ -626,19 +650,32 @@ type
     attrs: set[QueryAttrs]
     fgcolor: Option[RGBColor]
     bgcolor: Option[RGBColor]
+    widthPx: int
+    heightPx: int
+    width: int
+    height: int
 
-proc queryAttrs(term: Terminal): QueryResult =
+proc queryAttrs(term: Terminal, windowOnly: bool): QueryResult =
   const tcapRGB = 0x524742 # RGB supported?
-  const outs =
-    XTGETFG &
-    XTGETBG &
-    XTGETTCAP("524742") &
-    DA1
-  term.outfile.write(outs)
+  if not windowOnly:
+    const outs =
+      XTGETFG &
+      XTGETBG &
+      GEOMPIXEL &
+      GEOMCELL &
+      XTGETTCAP("524742") &
+      DA1
+    term.outfile.write(outs)
+  else:
+    const outs =
+      GEOMPIXEL &
+      GEOMCELL &
+      DA1
+    term.outfile.write(outs)
   result = QueryResult(success: false, attrs: {})
   while true:
     template consume(term: Terminal): char = term.infile.readChar()
-    template fail = break
+    template fail = return
     template expect(term: Terminal, c: char) =
       if term.consume != c:
         fail
@@ -652,20 +689,44 @@ proc queryAttrs(term: Terminal): QueryResult =
     case term.consume
     of '[':
       # CSI
-      term.expect '?'
-      var n = 0
-      while (let c = term.consume; c != 'c'):
-        if c == ';':
-          case n
-          of 4: result.attrs.incl(qaSixel)
-          of 22: result.attrs.incl(qaAnsiColor)
-          else: discard
-          n = 0
-        else:
-          n *= 10
-          n += decValue(c)
-      result.success = true
-      break # DA1 returned; done
+      case (let c = term.consume; c)
+      of '?': # DA1
+        var n = 0
+        while (let c = term.consume; c != 'c'):
+          if c == ';':
+            case n
+            of 4: result.attrs.incl(qaSixel)
+            of 22: result.attrs.incl(qaAnsiColor)
+            else: discard
+            n = 0
+          else:
+            n *= 10
+            n += decValue(c)
+        result.success = true
+        break # DA1 returned; done
+      of '4', '8': # GEOMPIXEL, GEOMCELL
+        term.expect ';'
+        var height = 0
+        var width = 0
+        while (let c = term.consume; c != ';'):
+          if (let x = decValue(c); x != -1):
+            height *= 10
+            height += x
+          else:
+            fail
+        while (let c = term.consume; c != 't'):
+          if (let x = decValue(c); x != -1):
+            width *= 10
+            width += x
+          else:
+            fail
+        if c == '4': # GEOMSIZE
+          result.widthPx = width
+          result.heightPx = height
+        if c == '8': # GEOMCELL
+          result.width = width
+          result.height = height
+      else: fail
     of ']':
       # OSC
       term.expect '1'
@@ -723,57 +784,87 @@ proc queryAttrs(term: Terminal): QueryResult =
 type TermStartResult* = enum
   tsrSuccess, tsrDA1Fail
 
-proc detectTermAttributes(term: Terminal): TermStartResult =
+# when windowOnly, only refresh window size.
+proc detectTermAttributes(term: Terminal, windowOnly: bool): TermStartResult =
   result = tsrSuccess
   term.tname = getEnv("TERM")
   if term.tname == "":
     term.tname = "dosansi"
-  if term.isatty():
-    if term.config.display.query_da1:
-      let r = term.queryAttrs()
-      if r.success: # DA1 success
-        if qaAnsiColor in r.attrs:
-          term.colormode = ANSI
-        if qaRGB in r.attrs:
-          term.colormode = TRUE_COLOR
-        # just assume the terminal doesn't choke on these.
-        term.formatmode = {FLAG_STRIKE, FLAG_OVERLINE}
-        if r.bgcolor.isSome:
-          term.defaultBackground = r.bgcolor.get
-        if r.fgcolor.isSome:
-          term.defaultForeground = r.fgcolor.get
-      else:
-        # something went horribly wrong. set result to DA1 fail, pager will
-        # alert the user
-        result = tsrDA1Fail
-    if term.colormode != TRUE_COLOR:
-      let colorterm = getEnv("COLORTERM")
-      if colorterm in ["24bit", "truecolor"]:
+  if not term.isatty():
+    return
+  let fd = term.infile.getFileHandle()
+  var win: IOctl_WinSize
+  if ioctl(cint(fd), TIOCGWINSZ, addr win) != -1:
+    term.attrs.width = int(win.ws_col)
+    term.attrs.height = int(win.ws_row)
+    term.attrs.ppc = int(win.ws_xpixel) div term.attrs.width
+    term.attrs.ppl = int(win.ws_ypixel) div term.attrs.height
+  if term.config.display.query_da1:
+    let r = term.queryAttrs(windowOnly)
+    if r.success: # DA1 success
+      if r.width != 0:
+        term.attrs.width = r.width
+        if r.widthPx != 0:
+          term.attrs.ppc = r.widthPx div r.width
+      if r.height != 0:
+        term.attrs.height = r.height
+        if r.heightPx != 0:
+          term.attrs.ppl = r.heightPx div r.height
+      if windowOnly:
+        return
+      if qaAnsiColor in r.attrs:
+        term.colormode = ANSI
+      if qaRGB in r.attrs:
         term.colormode = TRUE_COLOR
-    when termcap_found:
-      term.loadTermcap()
-      if term.tc != nil:
-        term.smcup = term.hascap(ti)
-        if term.hascap(ZH):
-          term.formatmode.incl(FLAG_ITALIC)
-        if term.hascap(us):
-          term.formatmode.incl(FLAG_UNDERLINE)
-        if term.hascap(md):
-          term.formatmode.incl(FLAG_BOLD)
-        if term.hascap(mr):
-          term.formatmode.incl(FLAG_REVERSE)
-        if term.hascap(mb):
-          term.formatmode.incl(FLAG_BLINK)
+      # just assume the terminal doesn't choke on these.
+      term.formatmode = {FLAG_STRIKE, FLAG_OVERLINE}
+      if r.bgcolor.isSome:
+        term.defaultBackground = r.bgcolor.get
+      if r.fgcolor.isSome:
+        term.defaultForeground = r.fgcolor.get
     else:
-      term.smcup = true
-      term.formatmode = {low(FormatFlags)..high(FormatFlags)}
+      # something went horribly wrong. set result to DA1 fail, pager will
+      # alert the user
+      result = tsrDA1Fail
+  if windowOnly:
+    return
+  if term.colormode != TRUE_COLOR:
+    let colorterm = getEnv("COLORTERM")
+    if colorterm in ["24bit", "truecolor"]:
+      term.colormode = TRUE_COLOR
+  when termcap_found:
+    term.loadTermcap()
+    if term.tc != nil:
+      term.smcup = term.hascap(ti)
+      if term.hascap(ZH):
+        term.formatmode.incl(FLAG_ITALIC)
+      if term.hascap(us):
+        term.formatmode.incl(FLAG_UNDERLINE)
+      if term.hascap(md):
+        term.formatmode.incl(FLAG_BOLD)
+      if term.hascap(mr):
+        term.formatmode.incl(FLAG_REVERSE)
+      if term.hascap(mb):
+        term.formatmode.incl(FLAG_BLINK)
+  else:
+    term.smcup = true
+    term.formatmode = {low(FormatFlags)..high(FormatFlags)}
+
+proc windowChange*(term: Terminal) =
+  discard term.detectTermAttributes(windowOnly = true)
+  term.applyConfigDimensions()
+  term.canvas = newFixedGrid(term.attrs.width, term.attrs.height)
+  term.cleared = false
 
 proc start*(term: Terminal, infile: File): TermStartResult =
   term.infile = infile
   if term.isatty():
     term.enableRawMode()
-  result = term.detectTermAttributes()
+  result = term.detectTermAttributes(windowOnly = false)
+  if result == tsrDA1Fail:
+    term.config.display.query_da1 = false
   term.applyConfig()
+  term.canvas = newFixedGrid(term.attrs.width, term.attrs.height)
   if term.smcup:
     term.write(term.enableAltScreen())
 
@@ -788,13 +879,10 @@ proc restart*(term: Terminal) =
   if term.smcup:
     term.write(term.enableAltScreen())
 
-proc newTerminal*(outfile: File, config: Config, attrs: WindowAttributes):
-    Terminal =
-  let term = Terminal(
+proc newTerminal*(outfile: File, config: Config): Terminal =
+  return Terminal(
     outfile: outfile,
     config: config,
     defaultBackground: ColorsRGB["black"],
     defaultForeground: ColorsRGB["white"]
   )
-  term.windowChange(attrs)
-  return term
diff --git a/src/display/winattrs.nim b/src/display/winattrs.nim
deleted file mode 100644
index 1fe07080..00000000
--- a/src/display/winattrs.nim
+++ /dev/null
@@ -1,58 +0,0 @@
-when defined(posix):
-  import std/termios
-
-type
-  WindowAttributes* = object
-    width*: int
-    height*: int
-    ppc*: int # cell width
-    ppl*: int # cell height
-    width_px*: int
-    height_px*: int
-
-proc isatty(fd: FileHandle): cint {.importc: "isatty", header: "<unistd.h>".}
-proc isatty*(f: File): bool =
-  return isatty(f.getFileHandle()) != 0
-
-proc getWindowAttributes*(tty: File): WindowAttributes =
-  if tty.isatty():
-    var win: IOctl_WinSize
-    if ioctl(cint(getOsFileHandle(tty)), TIOCGWINSZ, addr win) != -1:
-      var cols = int(win.ws_col)
-      var rows = int(win.ws_row)
-      if cols == 0:
-        cols = 80
-      if rows == 0:
-        rows = 24
-      var ppc = int(win.ws_xpixel) div cols
-      var ppl = int(win.ws_ypixel) div rows
-      # some terminal emulators (aka vte) don't set ws_xpixel or ws_ypixel.
-      # solution: use xterm.
-      if ppc == 0:
-        ppc = 9
-      if ppl == 0:
-        ppl = 18
-      # Filling the last row without raw mode breaks things. However,
-      # not supporting Windows means we can always have raw mode, so we can
-      # use all available columns.
-      return WindowAttributes(
-        width: cols,
-        height: rows,
-        ppc: ppc,
-        ppl: ppl,
-        width_px: cols * ppc,
-        height_px: rows * ppl
-      )
-  # Fallback for ioctl failure
-  let height = 24
-  let width = 80
-  let ppc = 9
-  let ppl = 18
-  return WindowAttributes(
-    width: width,
-    height: height,
-    ppc: ppc,
-    ppl: ppl,
-    width_px: ppc * width,
-    height_px: ppl * height
-  )
diff --git a/src/html/dom.nim b/src/html/dom.nim
index 1104ee90..d322049c 100644
--- a/src/html/dom.nim
+++ b/src/html/dom.nim
@@ -10,7 +10,7 @@ import css/cssparser
 import css/mediaquery
 import css/sheet
 import css/values
-import display/winattrs
+import display/term
 import html/catom
 import html/enums
 import html/event
diff --git a/src/html/env.nim b/src/html/env.nim
index 8a570b93..9e817ea0 100644
--- a/src/html/env.nim
+++ b/src/html/env.nim
@@ -2,7 +2,7 @@ import std/selectors
 import std/streams
 
 import bindings/quickjs
-import display/winattrs
+import display/term
 import html/catom
 import html/chadombuilder
 import html/dom
diff --git a/src/layout/engine.nim b/src/layout/engine.nim
index 613cd9c3..93a324b3 100644
--- a/src/layout/engine.nim
+++ b/src/layout/engine.nim
@@ -5,7 +5,7 @@ import std/unicode
 
 import css/stylednode
 import css/values
-import display/winattrs
+import display/term
 import layout/box
 import layout/layoutunit
 import utils/luwrap
diff --git a/src/layout/renderdocument.nim b/src/layout/renderdocument.nim
index 7526b111..0313ca2b 100644
--- a/src/layout/renderdocument.nim
+++ b/src/layout/renderdocument.nim
@@ -3,7 +3,7 @@ import std/unicode
 
 import css/stylednode
 import css/values
-import display/winattrs
+import display/term
 import layout/box
 import layout/engine
 import layout/layoutunit
@@ -11,6 +11,63 @@ import types/cell
 import types/color
 import utils/strwidth
 
+type
+  # A FormatCell *starts* a new terminal formatting context.
+  # If no FormatCell exists before a given cell, the default formatting is used.
+  FormatCell* = object
+    format*: Format
+    pos*: int
+    node*: StyledNode
+
+  # Following properties should hold for `formats':
+  # * Position should be >= 0, <= str.width().
+  # * The position of every FormatCell should be greater than the position
+  #   of the previous FormatCell.
+  FlexibleLine* = object
+    str*: string
+    formats*: seq[FormatCell]
+
+  FlexibleGrid* = seq[FlexibleLine]
+
+func findFormatN*(line: FlexibleLine, pos: int): int =
+  var i = 0
+  while i < line.formats.len:
+    if line.formats[i].pos > pos:
+      break
+    inc i
+  return i
+
+func findFormat*(line: FlexibleLine, pos: int): FormatCell =
+  let i = line.findFormatN(pos) - 1
+  if i != -1:
+    result = line.formats[i]
+  else:
+    result.pos = -1
+
+func findNextFormat*(line: FlexibleLine, pos: int): FormatCell =
+  let i = line.findFormatN(pos)
+  if i < line.formats.len:
+    result = line.formats[i]
+  else:
+    result.pos = -1
+
+proc addLine*(grid: var FlexibleGrid) =
+  grid.add(FlexibleLine())
+
+proc addLines*(grid: var FlexibleGrid, n: int) =
+  grid.setLen(grid.len + n)
+
+proc insertFormat*(line: var FlexibleLine, i: int, cell: FormatCell) =
+  line.formats.insert(cell, i)
+
+proc insertFormat*(line: var FlexibleLine, pos, i: int, format: Format,
+    node: StyledNode = nil) =
+  line.insertFormat(i, FormatCell(format: format, node: node, pos: pos))
+
+proc addFormat*(line: var FlexibleLine, pos: int, format: Format,
+    node: StyledNode = nil) =
+  line.formats.add(FormatCell(format: format, node: node, pos: pos))
+
 func toFormat(computed: CSSComputedValues): Format =
   if computed == nil:
     return Format()
@@ -233,7 +290,7 @@ proc paintBackground(grid: var FlexibleGrid; color: CellColor; startx,
 
   for y in starty..<endy:
     # Make sure line.width() >= endx
-    let linewidth = grid[y].width()
+    let linewidth = grid[y].str.width()
     if linewidth < endx:
       grid[y].str &= ' '.repeat(endx - linewidth)
 
diff --git a/src/local/client.nim b/src/local/client.nim
index 71aaac8c..d088982f 100644
--- a/src/local/client.nim
+++ b/src/local/client.nim
@@ -18,7 +18,6 @@ import bindings/quickjs
 import config/config
 import display/lineedit
 import display/term
-import display/winattrs
 import html/chadombuilder
 import html/dom
 import html/event
@@ -437,8 +436,7 @@ proc inputLoop(client: Client) =
         client.handleError(event.fd)
       if Signal in event.events:
         assert event.fd == sigwinch
-        let attrs = getWindowAttributes(client.pager.infile)
-        client.pager.windowChange(attrs)
+        client.pager.windowChange()
       if selectors.Event.Timer in event.events:
         let r = client.timeouts.runTimeoutFd(event.fd)
         assert r
@@ -690,8 +688,7 @@ proc newClient*(config: Config, forkserver: ForkServer): Client =
   let jsrt = newJSRuntime()
   JS_SetModuleLoaderFunc(jsrt, normalizeModuleName, clientLoadJSModule, nil)
   let jsctx = jsrt.newJSContext()
-  let attrs = getWindowAttributes(stdout)
-  let pager = newPager(config, attrs, forkserver, jsctx)
+  let pager = newPager(config, forkserver, jsctx)
   let client = Client(
     config: config,
     forkserver: forkserver,
diff --git a/src/local/container.nim b/src/local/container.nim
index 3bab3130..8008a896 100644
--- a/src/local/container.nim
+++ b/src/local/container.nim
@@ -7,7 +7,7 @@ when defined(posix):
   import std/posix
 
 import config/config
-import display/winattrs
+import display/term
 import extern/stdio
 import io/promise
 import io/serialize
diff --git a/src/local/pager.nim b/src/local/pager.nim
index 62746a4e..40b093ac 100644
--- a/src/local/pager.nim
+++ b/src/local/pager.nim
@@ -17,7 +17,6 @@ import config/mailcap
 import config/mimetypes
 import display/lineedit
 import display/term
-import display/winattrs
 import extern/editor
 import extern/runproc
 import extern/stdio
@@ -233,20 +232,17 @@ proc setPaths(pager: Pager): Err[string] =
   pager.cgiDir = cgiDir
   return ok()
 
-proc newPager*(config: Config, attrs: WindowAttributes, forkserver: ForkServer,
-    ctx: JSContext): Pager =
+proc newPager*(config: Config, forkserver: ForkServer, ctx: JSContext): Pager =
   let (mailcap, errs) = config.getMailcap()
   let pager = Pager(
     config: config,
-    display: newFixedGrid(attrs.width, attrs.height - 1),
     forkserver: forkserver,
     mailcap: mailcap,
     mimeTypes: config.getMimeTypes(),
     omnirules: config.getOmniRules(ctx),
     proxy: config.getProxy(),
     siteconf: config.getSiteConfig(ctx),
-    statusgrid: newFixedGrid(attrs.width),
-    term: newTerminal(stdout, config, attrs),
+    term: newTerminal(stdout, config),
     urimethodmap: config.getURIMethodMap()
   )
   let r = pager.setPaths()
@@ -265,6 +261,8 @@ proc launchPager*(pager: Pager, infile: File) =
   of tsrSuccess: discard
   of tsrDA1Fail:
     pager.alert("Failed to query DA1, please set display.query-da1 = false")
+  pager.display = newFixedGrid(pager.attrs.width, pager.attrs.height - 1)
+  pager.statusgrid = newFixedGrid(pager.attrs.width)
 
 func infile*(pager: Pager): File =
   return pager.term.infile
@@ -458,7 +456,7 @@ proc newBuffer(pager: Pager, bufferConfig: BufferConfig, request: Request,
     pager.forkserver,
     bufferConfig,
     request,
-    pager.attrs,
+    pager.term.attrs,
     title,
     redirectdepth,
     canreinterpret,
@@ -618,16 +616,18 @@ proc toggleSource(pager: Pager) {.jsfunc.} =
     pager.container.sourcepair = container
     pager.addContainer(container)
 
-proc windowChange*(pager: Pager, attrs: WindowAttributes) =
-  if attrs == pager.attrs:
+proc windowChange*(pager: Pager) =
+  let oldAttrs = pager.attrs
+  pager.term.windowChange()
+  if pager.attrs == oldAttrs:
+    #TODO maybe it's more efficient to let false positives through?
     return
   if pager.lineedit.isSome:
-    pager.lineedit.get.windowChange(attrs)
-  pager.term.windowChange(attrs)
-  pager.display = newFixedGrid(attrs.width, attrs.height - 1)
-  pager.statusgrid = newFixedGrid(attrs.width)
+    pager.lineedit.get.windowChange(pager.attrs)
+  pager.display = newFixedGrid(pager.attrs.width, pager.attrs.height - 1)
+  pager.statusgrid = newFixedGrid(pager.attrs.width)
   for container in pager.containers:
-    container.windowChange(attrs)
+    container.windowChange(pager.attrs)
   if pager.askprompt != "":
     pager.writeAskPrompt()
   pager.showAlerts()
diff --git a/src/server/buffer.nim b/src/server/buffer.nim
index aef50329..6f8beb34 100644
--- a/src/server/buffer.nim
+++ b/src/server/buffer.nim
@@ -19,7 +19,7 @@ import css/mediaquery
 import css/sheet
 import css/stylednode
 import css/values
-import display/winattrs
+import display/term
 import html/catom
 import html/chadombuilder
 import html/dom
diff --git a/src/server/forkserver.nim b/src/server/forkserver.nim
index a62c94df..3e9124d0 100644
--- a/src/server/forkserver.nim
+++ b/src/server/forkserver.nim
@@ -5,7 +5,7 @@ import std/streams
 import std/tables
 
 import config/config
-import display/winattrs
+import display/term
 import io/posixstream
 import io/serialize
 import io/serversocket
diff --git a/src/types/cell.nim b/src/types/cell.nim
index 2f2e5203..d66743c0 100644
--- a/src/types/cell.nim
+++ b/src/types/cell.nim
@@ -1,10 +1,5 @@
-import std/options
-import std/tables
-
-import css/stylednode
 import types/color
 import utils/strwidth
-import utils/twtstr
 
 type
   FormatFlags* = enum
@@ -21,31 +16,14 @@ type
     bgcolor*: CellColor
     flags*: set[FormatFlags]
 
-  # A FormatCell *starts* a new terminal formatting context.
-  # If no FormatCell exists before a given cell, the default formatting is used.
-  FormatCell* = object
-    format*: Format
-    pos*: int
-    node*: StyledNode
-
   SimpleFormatCell* = object
     format*: Format
     pos*: int
 
-  # Following properties should hold for `formats':
-  # * Position should be >= 0, <= str.width().
-  # * The position of every FormatCell should be greater than the position
-  #   of the previous FormatCell.
-  FlexibleLine* = object
-    str*: string
-    formats*: seq[FormatCell]
-
   SimpleFlexibleLine* = object
     str*: string
     formats*: seq[SimpleFormatCell]
 
-  FlexibleGrid* = seq[FlexibleLine]
-
   SimpleFlexibleGrid* = seq[SimpleFlexibleLine]
 
   FixedCell* = object
@@ -77,13 +55,6 @@ const FormatCodes*: array[FormatFlags, tuple[s, e: uint8]] = [
   FLAG_BLINK: (5u8, 25u8),
 ]
 
-const FormatCodeMap = block:
-  var res: Table[uint8, tuple[flag: FormatFlags, reverse: bool]]
-  for x in FormatFlags:
-    res[FormatCodes[x][0]] = (x, false)
-    res[FormatCodes[x][1]] = (x, true)
-  res
-
 template flag_template(format: Format, val: bool, flag: FormatFlags) =
   if val: format.flags.incl(flag)
   else: format.flags.excl(flag)
@@ -99,14 +70,11 @@ template `blink=`*(f: var Format, b: bool) = flag_template f, b, FLAG_BLINK
 func newFixedGrid*(w: int, h: int = 1): FixedGrid =
   return FixedGrid(width: w, height: h, cells: newSeq[FixedCell](w * h))
 
-func width*(line: FlexibleLine): int =
-  return line.str.width()
-
 func width*(cell: FixedCell): int =
   return cell.str.width()
 
 # Get the first format cell after pos, if any.
-func findFormatN*(line: FlexibleLine|SimpleFlexibleLine, pos: int): int =
+func findFormatN*(line: SimpleFlexibleLine, pos: int): int =
   var i = 0
   while i < line.formats.len:
     if line.formats[i].pos > pos:
@@ -114,13 +82,6 @@ func findFormatN*(line: FlexibleLine|SimpleFlexibleLine, pos: int): int =
     inc i
   return i
 
-func findFormat*(line: FlexibleLine, pos: int): FormatCell =
-  let i = line.findFormatN(pos) - 1
-  if i != -1:
-    result = line.formats[i]
-  else:
-    result.pos = -1
-
 func findFormat*(line: SimpleFlexibleLine, pos: int): SimpleFormatCell =
   let i = line.findFormatN(pos) - 1
   if i != -1:
@@ -128,176 +89,9 @@ func findFormat*(line: SimpleFlexibleLine, pos: int): SimpleFormatCell =
   else:
     result.pos = -1
 
-func findNextFormat*(line: FlexibleLine, pos: int): FormatCell =
-  let i = line.findFormatN(pos)
-  if i < line.formats.len:
-    result = line.formats[i]
-  else:
-    result.pos = -1
-
 func findNextFormat*(line: SimpleFlexibleLine, pos: int): SimpleFormatCell =
   let i = line.findFormatN(pos)
   if i < line.formats.len:
     result = line.formats[i]
   else:
     result.pos = -1
-
-proc addLine*(grid: var FlexibleGrid) =
-  grid.add(FlexibleLine())
-
-proc addLines*(grid: var FlexibleGrid, n: int) =
-  grid.setLen(grid.len + n)
-
-proc insertFormat*(line: var FlexibleLine, i: int, cell: FormatCell) =
-  line.formats.insert(cell, i)
-
-proc insertFormat*(line: var FlexibleLine, pos, i: int, format: Format,
-    node: StyledNode = nil) =
-  line.insertFormat(i, FormatCell(format: format, node: node, pos: pos))
-
-proc addFormat*(line: var FlexibleLine, pos: int, format: Format,
-    node: StyledNode = nil) =
-  line.formats.add(FormatCell(format: format, node: node, pos: pos))
-
-# https://www.ecma-international.org/wp-content/uploads/ECMA-48_5th_edition_june_1991.pdf
-type
-  AnsiCodeParseState* = enum
-    PARSE_START, PARSE_PARAMS, PARSE_INTERM, PARSE_FINAL, PARSE_DONE
-
-  AnsiCodeParser* = object
-    state*: AnsiCodeParseState
-    params: string
-
-proc getParam(parser: AnsiCodeParser, i: var int, colon = false): string =
-  while i < parser.params.len and
-      not (parser.params[i] == ';' or colon and parser.params[i] == ':'):
-    result &= parser.params[i]
-    inc i
-  if i < parser.params.len:
-    inc i
-
-template getParamU8(parser: AnsiCodeParser, i: var int,
-    colon = false): uint8 =
-  if i >= parser.params.len:
-    return false
-  let u = parseUInt8(parser.getParam(i))
-  if u.isNone:
-    return false
-  u.get
-
-proc parseSGRDefColor(parser: AnsiCodeParser, format: var Format,
-    i: var int, isfg: bool): bool =
-  let u = parser.getParamU8(i, colon = true)
-  template set_color(c: CellColor) =
-    if isfg:
-      format.fgcolor = c
-    else:
-      format.bgcolor = c
-  if u == 2:
-    let param0 = parser.getParamU8(i, colon = true)
-    if i < parser.params.len:
-      let r = param0
-      let g = parser.getParamU8(i, colon = true)
-      let b = parser.getParamU8(i, colon = true)
-      set_color cellColor(rgb(r, g, b))
-    else:
-      set_color cellColor(gray(param0))
-  elif u == 5:
-    let param0 = parser.getParamU8(i, colon = true)
-    if param0 in 0u8..15u8:
-      set_color cellColor(ANSIColor(param0))
-    elif param0 in 16u8..255u8:
-      set_color cellColor(EightBitColor(param0))
-  else:
-    return false
-
-proc parseSGRColor(parser: AnsiCodeParser, format: var Format,
-    i: var int, u: uint8): bool =
-  if u in 30u8..37u8:
-    format.fgcolor = cellColor(ANSIColor(u - 30))
-  elif u == 38:
-    return parser.parseSGRDefColor(format, i, isfg = true)
-  elif u == 39:
-    format.fgcolor = defaultColor
-  elif u in 40u8..47u8:
-    format.bgcolor = cellColor(ANSIColor(u - 40))
-  elif u == 48:
-    return parser.parseSGRDefColor(format, i, isfg = false)
-  elif u == 49:
-    format.bgcolor = defaultColor
-  elif u in 90u8..97u8:
-    format.fgcolor = cellColor(ANSIColor(u - 82))
-  elif u in 100u8..107u8:
-    format.bgcolor = cellColor(ANSIColor(u - 92))
-  else:
-    return false
-  return true
-
-proc parseSGRAspect(parser: AnsiCodeParser, format: var Format,
-    i: var int): bool =
-  let u = parser.getParamU8(i)
-  if u in FormatCodeMap:
-    let entry = FormatCodeMap[u]
-    if entry.reverse:
-      format.flags.excl(entry.flag)
-    else:
-      format.flags.incl(entry.flag)
-    return true
-  elif u == 0:
-    format = Format()
-    return true
-  else:
-    return parser.parseSGRColor(format, i, u)
-
-proc parseSGR(parser: AnsiCodeParser, format: var Format) =
-  if parser.params.len == 0:
-    format = Format()
-  else:
-    var i = 0
-    while i < parser.params.len:
-      if not parser.parseSGRAspect(format, i):
-        break
-
-proc parseControlFunction(parser: var AnsiCodeParser, format: var Format,
-    f: char) =
-  case f
-  of 'm':
-    parser.parseSGR(format)
-  else: discard # unknown
-
-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:
-      parser.parseControlFunction(format, c)
-    else:
-      return true
-  of PARSE_DONE: discard