about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2024-02-25 20:25:04 +0100
committerbptato <nincsnevem662@gmail.com>2024-02-25 20:25:04 +0100
commit92fbf479f8bf6db6ad0655e5b71a0b58a45a2213 (patch)
tree47b7f2c23ebabbf94e48476615014b52762542f3
parent4df71520f39ed7992f78484701467e513a34c5dc (diff)
downloadchawan-92fbf479f8bf6db6ad0655e5b71a0b58a45a2213.tar.gz
term: improve pixels-per-column/line detection
Some terminal emulators (AKA vte) refuse to set ws_xpixel and ws_ypixel
in the TIOCGWINSZ ioctl, so we now query for CSI 14 t as well. (Also CSI
18 t for good measure, just in case we can't ioctl for some reason.)

Also added some fallback (optionally forced) config values for width,
height, ppc, and ppl. (This is especially useful in dump mode.)
-rw-r--r--doc/config.md16
-rw-r--r--res/config.toml8
-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
17 files changed, 280 insertions, 370 deletions
diff --git a/doc/config.md b/doc/config.md
index 7e43300c..e0208e16 100644
--- a/doc/config.md
+++ b/doc/config.md
@@ -432,6 +432,22 @@ output will most likely look horrible. (Except, obviously, if your terminal does
 not support Primary Device Attributes.)</td>
 </tr>
 
+<tr>
+<td>columns, lines, pixels-per-column, pixels-per-line</td>
+<td>number</td>
+<td>Fallback values for the number of columns, lines, pixels per
+column, and pixels per line for the cases where it cannot be determined
+automatically. (For example, these values are used in dump mode.)</td>
+</tr>
+
+<tr>
+<td>force-columns, force-lines, force-pixels-per-column,
+force-pixels-per-line</td>
+<td>boolean</td>
+<td>Force-set columns, lines, pixels per column, or pixels per line to the
+fallback values provided above.</td>
+</tr>
+
 </table>
 
 ## Omnirule
diff --git a/res/config.toml b/res/config.toml
index e8f651db..d95b8b2a 100644
--- a/res/config.toml
+++ b/res/config.toml
@@ -66,6 +66,14 @@ set-title = true
 default-background-color = "auto"
 default-foreground-color = "auto"
 query-da1 = true
+columns = 80
+lines = 24
+pixels-per-column = 9
+pixels-per-line = 18
+force-columns = false
+force-lines = false
+force-pixels-per-column = false
+force-pixels-per-line = false
 
 [[omnirule]]
 match = '^ddg:'
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