about summary refs log tree commit diff stats
path: root/src/local
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2024-03-14 20:57:45 +0100
committerbptato <nincsnevem662@gmail.com>2024-03-14 21:05:16 +0100
commitd26766c4c4015990703e84e8136f96d222edbc97 (patch)
tree7f412f8ca98d2b04323da5cf2fd607efbd6c408d /src/local
parenta8f05f18fdd64485c26b453e62e8073b50e271ef (diff)
downloadchawan-d26766c4c4015990703e84e8136f96d222edbc97.tar.gz
Move around some modules
* extern -> gone, runproc absorbed by pager, others moved into io/
* display -> local/ (where else would we display?)
* xhr -> html/
* move out WindowAttributes from term, so we don't depend on local
  from server
Diffstat (limited to 'src/local')
-rw-r--r--src/local/client.nim8
-rw-r--r--src/local/container.nim2
-rw-r--r--src/local/lineedit.nim328
-rw-r--r--src/local/pager.nim70
-rw-r--r--src/local/term.nim899
5 files changed, 1292 insertions, 15 deletions
diff --git a/src/local/client.nim b/src/local/client.nim
index 4f7d9f2b..bc52f704 100644
--- a/src/local/client.nim
+++ b/src/local/client.nim
@@ -15,11 +15,11 @@ when defined(posix):
 import bindings/constcharp
 import bindings/quickjs
 import config/config
-import display/lineedit
-import display/term
 import html/chadombuilder
 import html/dom
 import html/event
+import html/formdata
+import html/xmlhttprequest
 import io/bufstream
 import io/posixstream
 import io/promise
@@ -41,7 +41,9 @@ import loader/headers
 import loader/loader
 import loader/request
 import local/container
+import local/lineedit
 import local/pager
+import local/term
 import server/buffer
 import server/forkserver
 import types/blob
@@ -49,8 +51,6 @@ import types/cookie
 import types/opt
 import types/url
 import utils/twtstr
-import xhr/formdata
-import xhr/xmlhttprequest
 
 import chagashi/charset
 
diff --git a/src/local/container.nim b/src/local/container.nim
index 7946200a..e2d84ac0 100644
--- a/src/local/container.nim
+++ b/src/local/container.nim
@@ -6,7 +6,6 @@ when defined(posix):
   import std/posix
 
 import config/config
-import display/term
 import io/promise
 import io/serialize
 import io/socketstream
@@ -23,6 +22,7 @@ import types/color
 import types/cookie
 import types/referrer
 import types/url
+import types/winattrs
 import utils/luwrap
 import utils/mimeguess
 import utils/strwidth
diff --git a/src/local/lineedit.nim b/src/local/lineedit.nim
new file mode 100644
index 00000000..0f048586
--- /dev/null
+++ b/src/local/lineedit.nim
@@ -0,0 +1,328 @@
+import std/strutils
+import std/unicode
+
+import bindings/quickjs
+import js/javascript
+import types/cell
+import types/opt
+import types/winattrs
+import utils/strwidth
+import utils/twtstr
+
+import chagashi/charset
+import chagashi/validator
+import chagashi/decoder
+
+type
+  LineEditState* = enum
+    EDIT, FINISH, CANCEL
+
+  LineHistory* = ref object
+    lines: seq[string]
+
+  LineEdit* = ref object
+    news*: string
+    prompt: string
+    promptw: int
+    state*: LineEditState
+    escNext*: bool
+    cursorx: int # 0 ..< news.notwidth
+    cursori: int # 0 ..< news.len
+    shiftx: int # 0 ..< news.notwidth
+    shifti: int # 0 ..< news.len
+    padding: int # 0 or 1
+    maxwidth: int
+    disallowed: set[char]
+    hide: bool
+    hist: LineHistory
+    histindex: int
+    histtmp: string
+    invalid*: bool
+
+jsDestructor(LineEdit)
+
+func newLineHistory*(): LineHistory =
+  return LineHistory()
+
+# Note: capped at edit.maxwidth.
+func getDisplayWidth(edit: LineEdit): int =
+  var dispw = 0
+  var i = edit.shifti
+  var r: Rune
+  while i < edit.news.len and dispw < edit.maxwidth:
+    fastRuneAt(edit.news, i, r)
+    dispw += r.width()
+  return dispw
+
+proc shiftView(edit: LineEdit) =
+  # Shift view so it contains the cursor.
+  if edit.cursorx < edit.shiftx:
+    edit.shiftx = edit.cursorx
+    edit.shifti = edit.cursori
+  # Shift view so it is completely filled.
+  if edit.shiftx > 0:
+    let dispw = edit.getDisplayWidth()
+    if dispw < edit.maxwidth:
+      let targetx = edit.shiftx - edit.maxwidth + dispw
+      if targetx <= 0:
+        edit.shiftx = 0
+        edit.shifti = 0
+      else:
+        while edit.shiftx > targetx:
+          let (r, len) = edit.news.lastRune(edit.shifti - 1)
+          edit.shiftx -= r.width()
+          edit.shifti -= len
+  edit.padding = 0
+  # Shift view so it contains the cursor. (act 2)
+  if edit.shiftx < edit.cursorx - edit.maxwidth:
+    while edit.shiftx < edit.cursorx - edit.maxwidth and
+        edit.shifti < edit.news.len:
+      var r: Rune
+      fastRuneAt(edit.news, edit.shifti, r)
+      edit.shiftx += r.width()
+    if edit.shiftx > edit.cursorx - edit.maxwidth:
+      # skipped over a cell because of a double-width char
+      edit.padding = 1
+
+proc generateOutput*(edit: LineEdit): FixedGrid =
+  edit.shiftView()
+  # Make the output grid +1 cell wide, so it covers the whole input area.
+  result = newFixedGrid(edit.promptw + edit.maxwidth + 1)
+  var x = 0
+  for r in edit.prompt.runes:
+    result[x].str &= $r
+    x += r.width()
+    if x >= result.width: break
+  for i in 0 ..< edit.padding:
+    if x < result.width:
+      result[x].str = " "
+      inc x
+  var i = edit.shifti
+  while i < edit.news.len:
+    var r: Rune
+    fastRuneAt(edit.news, i, r)
+    if not edit.hide:
+      let w = r.width()
+      if x + w > result.width: break
+      if r.isControlChar():
+        result[x].str &= '^'
+        inc x
+        result[x].str &= char(r).getControlLetter()
+        inc x
+      else:
+        result[x].str &= $r
+        x += w
+    else:
+      if x + 1 > result.width: break
+      result[x].str &= '*'
+      inc x
+
+proc getCursorX*(edit: LineEdit): int =
+  return edit.promptw + edit.cursorx + edit.padding - edit.shiftx
+
+proc insertCharseq(edit: LineEdit, s: string) =
+  let s = if edit.escNext:
+    s
+  else:
+    deleteChars(s, edit.disallowed)
+  edit.escNext = false
+  if s.len == 0:
+    return
+  let rem = edit.news.substr(edit.cursori)
+  edit.news.setLen(edit.cursori)
+  edit.news &= s
+  edit.news &= rem
+  edit.cursori += s.len
+  edit.cursorx += s.notwidth()
+  edit.invalid = true
+
+proc cancel(edit: LineEdit) {.jsfunc.} =
+  edit.state = CANCEL
+
+proc submit(edit: LineEdit) {.jsfunc.} =
+  if edit.hist.lines.len == 0 or edit.news != edit.hist.lines[^1]:
+    edit.hist.lines.add(edit.news)
+  edit.state = FINISH
+
+proc backspace(edit: LineEdit) {.jsfunc.} =
+  if edit.cursori > 0:
+    let (r, len) = edit.news.lastRune(edit.cursori - 1)
+    edit.news.delete(edit.cursori - len .. edit.cursori - 1)
+    edit.cursori -= len
+    edit.cursorx -= r.width()
+    edit.invalid = true
+
+proc write*(edit: LineEdit, s: string, cs: Charset): bool =
+  if cs == CHARSET_UTF_8:
+    if s.validateUTF8Surr() != -1:
+      return false
+    edit.insertCharseq(s)
+  else:
+    let td = newTextDecoder(cs)
+    var success = false
+    let s = td.decodeAll(s, success)
+    if not success:
+      return false
+    edit.insertCharseq(s)
+  return true
+
+proc write(edit: LineEdit, s: string): bool {.jsfunc.} =
+  edit.write(s, CHARSET_UTF_8)
+
+proc delete(edit: LineEdit) {.jsfunc.} =
+  if edit.cursori < edit.news.len:
+    let len = edit.news.runeLenAt(edit.cursori)
+    edit.news.delete(edit.cursori ..< edit.cursori + len)
+    edit.invalid = true
+
+proc escape(edit: LineEdit) {.jsfunc.} =
+  edit.escNext = true
+
+proc clear(edit: LineEdit) {.jsfunc.} =
+  if edit.cursori > 0:
+    edit.news.delete(0..edit.cursori - 1)
+    edit.cursori = 0
+    edit.cursorx = 0
+    edit.invalid = true
+
+proc kill(edit: LineEdit) {.jsfunc.} =
+  if edit.cursori < edit.news.len:
+    edit.news.setLen(edit.cursori)
+    edit.invalid = true
+
+proc backward(edit: LineEdit) {.jsfunc.} =
+  if edit.cursori > 0:
+    let (r, len) = edit.news.lastRune(edit.cursori - 1)
+    edit.cursori -= len
+    edit.cursorx -= r.width()
+    if edit.cursorx < edit.shiftx:
+      edit.invalid = true
+
+proc forward(edit: LineEdit) {.jsfunc.} =
+  if edit.cursori < edit.news.len:
+    var r: Rune
+    fastRuneAt(edit.news, edit.cursori, r)
+    edit.cursorx += r.width()
+    if edit.cursorx >= edit.shiftx + edit.maxwidth:
+      edit.invalid = true
+
+proc prevWord(edit: LineEdit) {.jsfunc.} =
+  if edit.cursori == 0:
+    return
+  let (r, len) = edit.news.lastRune(edit.cursori - 1)
+  if r.breaksWord():
+    edit.cursori -= len
+    edit.cursorx -= r.width()
+  while edit.cursori > 0:
+    let (r, len) = edit.news.lastRune(edit.cursori - 1)
+    if r.breaksWord():
+      break
+    edit.cursori -= len
+    edit.cursorx -= r.width()
+  if edit.cursorx < edit.shiftx:
+    edit.invalid = true
+
+proc nextWord(edit: LineEdit) {.jsfunc.} =
+  if edit.cursori >= edit.news.len:
+    return
+  let oc = edit.cursori
+  var r: Rune
+  fastRuneAt(edit.news, edit.cursori, r)
+  if r.breaksWord():
+    edit.cursorx += r.width()
+  else:
+    edit.cursori = oc
+  while edit.cursori < edit.news.len:
+    let pc = edit.cursori
+    fastRuneAt(edit.news, edit.cursori, r)
+    if r.breaksWord():
+      edit.cursori = pc
+      break
+    edit.cursorx += r.width()
+  if edit.cursorx >= edit.shiftx + edit.maxwidth:
+    edit.invalid = true
+
+proc clearWord(edit: LineEdit) {.jsfunc.} =
+  let oc = edit.cursori
+  edit.prevWord()
+  if oc != edit.cursori:
+    edit.news.delete(edit.cursori .. oc - 1)
+    edit.invalid = true
+
+proc killWord(edit: LineEdit) {.jsfunc.} =
+  if edit.cursori >= edit.news.len:
+    return
+  let oc = edit.cursori
+  let ox = edit.cursorx
+  edit.nextWord()
+  if edit.cursori != oc:
+    if edit.cursori < edit.news.len:
+      let len = edit.news.runeLenAt(edit.cursori)
+      edit.news.delete(oc ..< edit.cursori + len)
+    else:
+      edit.news.delete(oc ..< edit.cursori)
+    edit.cursori = oc
+    edit.cursorx = ox
+    edit.invalid = true
+
+proc begin(edit: LineEdit) {.jsfunc.} =
+  edit.cursori = 0
+  edit.cursorx = 0
+  if edit.shiftx > 0:
+    edit.invalid = true
+
+proc `end`(edit: LineEdit) {.jsfunc.} =
+  if edit.cursori < edit.news.len:
+    edit.cursori = edit.news.len
+    edit.cursorx = edit.news.notwidth()
+    if edit.cursorx >= edit.shiftx + edit.maxwidth:
+      edit.invalid = true
+
+proc prevHist(edit: LineEdit) {.jsfunc.} =
+  if edit.histindex > 0:
+    if edit.news.len > 0:
+      edit.histtmp = $edit.news
+    dec edit.histindex
+    edit.news = edit.hist.lines[edit.histindex]
+    # The begin call is needed so the cursor doesn't get lost outside
+    # the string.
+    edit.begin()
+    edit.end()
+    edit.invalid = true
+
+proc nextHist(edit: LineEdit) {.jsfunc.} =
+  if edit.histindex + 1 < edit.hist.lines.len:
+    inc edit.histindex
+    edit.news = edit.hist.lines[edit.histindex]
+    edit.begin()
+    edit.end()
+    edit.invalid = true
+  elif edit.histindex < edit.hist.lines.len:
+    inc edit.histindex
+    edit.news = edit.histtmp
+    edit.begin()
+    edit.end()
+    edit.histtmp = ""
+
+proc windowChange*(edit: LineEdit, attrs: WindowAttributes) =
+  edit.maxwidth = attrs.width - edit.promptw - 1
+
+proc readLine*(prompt: string, termwidth: int, current = "",
+    disallowed: set[char] = {}, hide = false, hist: LineHistory): LineEdit =
+  result = LineEdit(
+    prompt: prompt,
+    promptw: prompt.width(),
+    news: current,
+    disallowed: disallowed,
+    hide: hide,
+    invalid: true
+  )
+  result.cursori = result.news.len
+  result.cursorx = result.news.notwidth()
+  # - 1, so that the cursor always has place
+  result.maxwidth = termwidth - result.promptw - 1
+  result.hist = hist
+  result.histindex = result.hist.lines.len
+
+proc addLineEditModule*(ctx: JSContext) =
+  ctx.registerType(LineEdit)
diff --git a/src/local/pager.nim b/src/local/pager.nim
index bf9d3168..7502cb00 100644
--- a/src/local/pager.nim
+++ b/src/local/pager.nim
@@ -16,16 +16,13 @@ import config/chapath
 import config/config
 import config/mailcap
 import config/mimetypes
-import display/lineedit
-import display/term
-import extern/runproc
-import extern/stdio
-import extern/tempfile
 import io/bufstream
 import io/posixstream
 import io/promise
 import io/serialize
 import io/socketstream
+import io/stdio
+import io/tempfile
 import io/urlfilter
 import js/error
 import js/javascript
@@ -37,7 +34,9 @@ import loader/headers
 import loader/loader
 import loader/request
 import local/container
+import local/lineedit
 import local/select
+import local/term
 import server/buffer
 import server/forkserver
 import types/cell
@@ -47,6 +46,7 @@ import types/opt
 import types/referrer
 import types/urimethodmap
 import types/url
+import types/winattrs
 import utils/strwidth
 import utils/twtstr
 
@@ -126,7 +126,8 @@ type
 
 jsDestructor(Pager)
 
-func attrs(pager: Pager): WindowAttributes = pager.term.attrs
+template attrs(pager: Pager): WindowAttributes =
+  pager.term.attrs
 
 func loaderPid(pager: Pager): int64 {.jsfget.} =
   int64(pager.loader.process)
@@ -745,6 +746,59 @@ proc discardTree(pager: Pager, container = none(Container)) {.jsfunc.} =
   else:
     pager.alert("Buffer has no children!")
 
+proc c_system(cmd: cstring): cint {.importc: "system", header: "<stdlib.h>".}
+
+# Run process (without suspending the terminal controller).
+proc runProcess(cmd: string): bool =
+  let wstatus = c_system(cstring(cmd))
+  if wstatus == -1:
+    result = false
+  else:
+    result = WIFEXITED(wstatus) and WEXITSTATUS(wstatus) == 0
+    if not result:
+      # Hack.
+      #TODO this is a very bad idea, e.g. say the editor is writing into the
+      # file, then receives SIGINT, now the file is corrupted but Chawan will
+      # happily read it as if nothing happened.
+      # We should find a proper solution for this.
+      result = WIFSIGNALED(wstatus) and WTERMSIG(wstatus) == SIGINT
+
+# Run process (and suspend the terminal controller).
+proc runProcess(term: Terminal, cmd: string, wait = false): bool =
+  term.quit()
+  result = runProcess(cmd)
+  if wait:
+    term.anyKey()
+  term.restart()
+
+# Run process, and capture its output.
+proc runProcessCapture(cmd: string, outs: var string): bool =
+  let file = popen(cmd, "r")
+  if file == nil:
+    return false
+  let fs = newFileStream(file)
+  outs = fs.readAll()
+  let rv = pclose(file)
+  if rv == -1:
+    return false
+  return rv == 0
+
+# Run process, and write an arbitrary string into its standard input.
+proc runProcessInto(cmd, ins: string): bool =
+  let file = popen(cmd, "w")
+  if file == nil:
+    return false
+  let fs = newFileStream(file)
+  fs.write(ins)
+  let rv = pclose(file)
+  if rv == -1:
+    return false
+  return rv == 0
+
+template myExec(cmd: string) =
+  discard execl("/bin/sh", "sh", "-c", cstring(cmd), nil)
+  exitnow(127)
+
 proc toggleSource(pager: Pager) {.jsfunc.} =
   if not pager.container.canreinterpret:
     return
@@ -1182,7 +1236,6 @@ proc ansiDecode(pager: Pager; url: URL; ishtml: var bool; fdin: cint): cint =
     discard close(pipefdOutAnsi[1])
     closeStderr()
     myExec(cmd)
-    assert false
   else:
     discard close(pipefdOutAnsi[1])
     discard close(fdin)
@@ -1206,7 +1259,6 @@ proc runMailcapReadPipe(pager: Pager; stream: SocketStream; cmd: string;
     closeStderr()
     discard close(pipefdOut[1])
     myExec(cmd)
-    doAssert false
   # parent
   pid
 
@@ -1227,7 +1279,6 @@ proc runMailcapWritePipe(pager: Pager; stream: SocketStream;
       closeStdout()
       closeStderr()
     myExec(cmd)
-    doAssert false
   else:
     # parent
     stream.close()
@@ -1326,7 +1377,6 @@ proc filterBuffer(pager: Pager; stream: SocketStream; cmd: string;
     closeStderr()
     discard close(pipefd_out[1])
     myExec(cmd)
-    doAssert false
   # parent
   discard close(pipefd_out[1])
   let fdout = pipefd_out[0]
diff --git a/src/local/term.nim b/src/local/term.nim
new file mode 100644
index 00000000..050edf3b
--- /dev/null
+++ b/src/local/term.nim
@@ -0,0 +1,899 @@
+import std/options
+import std/os
+import std/posix
+import std/streams
+import std/strutils
+import std/tables
+import std/termios
+import std/unicode
+
+import bindings/termcap
+import config/config
+import types/cell
+import types/color
+import types/opt
+import types/winattrs
+import utils/strwidth
+import utils/twtstr
+
+import chagashi/charset
+import chagashi/encoder
+import chagashi/validator
+
+#TODO switch from termcap...
+
+type
+  TermcapCap = enum
+    ce # clear till end of line
+    cd # clear display
+    cm # cursor move
+    ti # terminal init (=smcup)
+    te # terminal end (=rmcup)
+    so # start standout mode
+    md # start bold mode
+    us # start underline mode
+    mr # start reverse mode
+    mb # start blink mode
+    ZH # start italic mode
+    ue # end underline mode
+    se # end standout mode
+    me # end all formatting modes
+    vs # enhance cursor
+    vi # make cursor invisible
+    ve # reset cursor to normal
+
+  Termcap = ref object
+    bp: array[1024, uint8]
+    funcstr: array[256, uint8]
+    caps: array[TermcapCap, cstring]
+
+  Terminal* = ref TerminalObj
+  TerminalObj = object
+    cs*: Charset
+    config: Config
+    infile*: File
+    outfile: File
+    cleared: bool
+    canvas: FixedGrid
+    pcanvas: FixedGrid
+    attrs*: WindowAttributes
+    colormode: ColorMode
+    formatmode: FormatMode
+    smcup: bool
+    tc: Termcap
+    tname: string
+    set_title: bool
+    stdin_unblocked: bool
+    orig_flags: cint
+    orig_flags2: cint
+    orig_termios: Termios
+    defaultBackground: RGBColor
+    defaultForeground: RGBColor
+
+# control sequence introducer
+template CSI(s: varargs[string, `$`]): string =
+  "\e[" & s.join(';')
+
+# 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")
+
+# allow shift-key to override mouse protocol
+const XTSHIFTESCAPE = CSI(">0s")
+
+# device control string
+template DCS(a, b: char, s: varargs[string]): string =
+  "\eP" & a & b & s.join(';') & "\e\\"
+
+template XTGETTCAP(s: varargs[string, `$`]): string =
+  DCS('+', 'q', s)
+
+# OS command
+template OSC(s: varargs[string, `$`]): string =
+  "\e]" & s.join(';') & '\a'
+
+template XTERM_TITLE(s: string): string =
+  OSC(0, s)
+
+const XTGETFG = OSC(10, "?") # get foreground color
+const XTGETBG = OSC(11, "?") # get background color
+
+# DEC set
+template DECSET(s: varargs[string, `$`]): string =
+  "\e[?" & s.join(';') & 'h'
+
+# DEC reset
+template DECRST(s: varargs[string, `$`]): string =
+  "\e[?" & s.join(';') & 'l'
+
+# alt screen
+const SMCUP = DECSET(1049)
+const RMCUP = DECRST(1049)
+
+# mouse tracking
+const SGRMOUSEBTNON = DECSET(1002, 1006)
+const SGRMOUSEBTNOFF = DECRST(1002, 1006)
+
+when not termcap_found:
+  const CNORM = DECSET(25)
+  const CIVIS = DECRST(25)
+  template HVP(s: varargs[string, `$`]): string =
+    CSI(s) & "f"
+  template EL(): string =
+    CSI() & "K"
+  template ED(): string =
+    CSI() & "J"
+
+  proc write(term: Terminal, s: string) =
+    term.outfile.write(s)
+else:
+  func hascap(term: Terminal, c: TermcapCap): bool = term.tc.caps[c] != nil
+  func cap(term: Terminal, c: TermcapCap): string = $term.tc.caps[c]
+  func ccap(term: Terminal, c: TermcapCap): cstring = term.tc.caps[c]
+
+  var goutfile: File
+  proc putc(c: char): cint {.cdecl.} =
+    goutfile.write(c)
+
+  proc write(term: Terminal, s: cstring) =
+    discard tputs(s, 1, putc)
+
+  proc write(term: Terminal, s: string) =
+    term.write(cstring(s))
+
+template SGR*(s: varargs[string, `$`]): string =
+  CSI(s) & "m"
+
+#TODO a) this should be customizable b) these defaults sucks
+const ANSIColorMap = [
+  rgb(0, 0, 0),
+  rgb(205, 0, 0),
+  rgb(0, 205, 0),
+  rgb(205, 205, 0),
+  rgb(0, 0, 238),
+  rgb(205, 0, 205),
+  rgb(0, 205, 205),
+  rgb(229, 229, 229),
+  rgb(127, 127, 127),
+  rgb(255, 0, 0),
+  rgb(0, 255, 0),
+  rgb(255, 255, 0),
+  rgb(92, 92, 255),
+  rgb(255, 0, 255),
+  rgb(0, 255, 255),
+  rgb(255, 255, 255)
+]
+
+proc flush*(term: Terminal) =
+  term.outfile.flushFile()
+
+proc cursorGoto(term: Terminal, x, y: int): string =
+  when termcap_found:
+    return $tgoto(term.ccap cm, cint(x), cint(y))
+  else:
+    return HVP(y + 1, x + 1)
+
+proc clearEnd(term: Terminal): string =
+  when termcap_found:
+    return term.cap ce
+  else:
+    return EL()
+
+proc clearDisplay(term: Terminal): string =
+  when termcap_found:
+    return term.cap cd
+  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()
+
+proc anyKey*(term: Terminal) =
+  if term.isatty():
+    term.outfile.write("[Hit any key]")
+    discard term.infile.readChar()
+
+proc resetFormat(term: Terminal): string =
+  when termcap_found:
+    if term.isatty():
+      return term.cap me
+  return SGR()
+
+proc startFormat(term: Terminal, flag: FormatFlags): string =
+  when termcap_found:
+    if term.isatty():
+      case flag
+      of FLAG_BOLD: return term.cap md
+      of FLAG_UNDERLINE: return term.cap us
+      of FLAG_REVERSE: return term.cap mr
+      of FLAG_BLINK: return term.cap mb
+      of FLAG_ITALIC: return term.cap ZH
+      else: discard
+  return SGR(FormatCodes[flag].s)
+
+proc endFormat(term: Terminal, flag: FormatFlags): string =
+  when termcap_found:
+    if flag == FLAG_UNDERLINE and term.isatty():
+      return term.cap ue
+  return SGR(FormatCodes[flag].e)
+
+proc setCursor*(term: Terminal, x, y: int) =
+  term.write(term.cursorGoto(x, y))
+
+proc enableAltScreen(term: Terminal): string =
+  when termcap_found:
+    if term.hascap ti:
+      return term.cap ti
+  return SMCUP
+
+proc disableAltScreen(term: Terminal): string =
+  when termcap_found:
+    if term.hascap te:
+      return term.cap te
+  return RMCUP
+
+func mincontrast(term: Terminal): int32 =
+  return term.config.display.minimum_contrast
+
+proc getRGB(a: CellColor, termDefault: RGBColor): RGBColor =
+  case a.t
+  of ctNone:
+    return termDefault
+  of ctANSI:
+    if a.color >= 16:
+      return EightBitColor(a.color).toRGB()
+    return ANSIColorMap[a.color]
+  of ctRGB:
+    return a.rgbcolor
+
+# Use euclidian distance to quantize RGB colors.
+proc approximateANSIColor(rgb, termDefault: RGBColor): CellColor =
+  var a = 0i32
+  var n = -1
+  for i in -1 .. ANSIColorMap.high:
+    let color = if i >= 0:
+      ANSIColorMap[i]
+    else:
+      termDefault
+    if color == rgb:
+      return if i == -1: defaultColor else: ANSIColor(i).cellColor()
+    let x = int32(color.r) - int32(rgb.r)
+    let y = int32(color.g) - int32(rgb.g)
+    let z = int32(color.b) - int32(rgb.b)
+    let xx = x * x
+    let yy = y * y
+    let zz = z * z
+    let b = xx + yy + zz
+    if i == -1 or b < a:
+      n = i
+      a = b
+  return if n == -1: defaultColor else: ANSIColor(n).cellColor()
+
+# Return a fgcolor contrasted to the background by term.mincontrast.
+proc correctContrast(term: Terminal, bgcolor, fgcolor: CellColor): CellColor =
+  let contrast = term.mincontrast
+  let cfgcolor = fgcolor
+  let bgcolor = getRGB(bgcolor, term.defaultBackground)
+  let fgcolor = getRGB(fgcolor, term.defaultForeground)
+  let bgY = int(bgcolor.Y)
+  var fgY = int(fgcolor.Y)
+  let diff = abs(bgY - fgY)
+  if diff < contrast:
+    if bgY > fgY:
+      fgY = bgY - contrast
+      if fgY < 0:
+        fgY = bgY + contrast
+        if fgY > 255:
+          fgY = 0
+    else:
+      fgY = bgY + contrast
+      if fgY > 255:
+        fgY = bgY - contrast
+        if fgY < 0:
+          fgY = 255
+    let newrgb = YUV(cast[uint8](fgY), fgcolor.U, fgcolor.V)
+    case term.colormode
+    of TRUE_COLOR:
+      return cellColor(newrgb)
+    of ANSI:
+      return approximateANSIColor(newrgb, term.defaultForeground)
+    of EIGHT_BIT:
+      return cellColor(newrgb.toEightBit())
+    of MONOCHROME:
+      doAssert false
+  return cfgcolor
+
+template ansiSGR(n: uint8, bgmod: int): string =
+  if n < 8:
+    SGR(30 + bgmod + n)
+  else:
+    SGR(82 + bgmod + n)
+
+template eightBitSGR(n: uint8, bgmod: int): string =
+  if n < 16:
+    ansiSGR(n, bgmod)
+  else:
+    SGR(38 + bgmod, 5, n)
+
+template rgbSGR(rgb: RGBColor, bgmod: int): string =
+  SGR(38 + bgmod, 2, rgb.r, rgb.g, rgb.b)
+
+proc processFormat*(term: Terminal, format: var Format, cellf: Format): string =
+  for flag in FormatFlags:
+    if flag in term.formatmode:
+      if flag in format.flags and flag notin cellf.flags:
+        result &= term.endFormat(flag)
+      if flag notin format.flags and flag in cellf.flags:
+        result &= term.startFormat(flag)
+  var cellf = cellf
+  case term.colormode
+  of ANSI:
+    # quantize
+    if cellf.bgcolor.t == ctANSI and cellf.bgcolor.color > 15:
+      cellf.bgcolor = cellf.fgcolor.eightbit.toRGB().cellColor()
+    if cellf.bgcolor.t == ctRGB:
+      cellf.bgcolor = approximateANSIColor(cellf.bgcolor.rgbcolor,
+        term.defaultBackground)
+    if cellf.fgcolor.t == ctANSI and cellf.fgcolor.color > 15:
+      cellf.fgcolor = cellf.fgcolor.eightbit.toRGB().cellColor()
+    if cellf.fgcolor.t == ctRGB:
+      if cellf.bgcolor.t == ctNone:
+        cellf.fgcolor = approximateANSIColor(cellf.fgcolor.rgbcolor,
+          term.defaultForeground)
+      else:
+        # ANSI fgcolor + bgcolor at the same time is broken
+        cellf.fgcolor = defaultColor
+    # correct
+    cellf.fgcolor = term.correctContrast(cellf.bgcolor, cellf.fgcolor)
+    # print
+    case cellf.fgcolor.t
+    of ctNone: result &= SGR(39)
+    of ctANSI: result &= ansiSGR(cellf.fgcolor.color, 0)
+    else: assert false
+    case cellf.bgcolor.t
+    of ctNone: result &= SGR(49)
+    of ctANSI: result &= ansiSGR(cellf.bgcolor.color, 10)
+    else: assert false
+  of EIGHT_BIT:
+    # quantize
+    if cellf.bgcolor.t == ctRGB:
+      cellf.bgcolor = cellf.bgcolor.rgbcolor.toEightBit().cellColor()
+    if cellf.fgcolor.t == ctRGB:
+      cellf.fgcolor = cellf.fgcolor.rgbcolor.toEightBit().cellColor()
+    # correct
+    cellf.fgcolor = term.correctContrast(cellf.bgcolor, cellf.fgcolor)
+    # print
+    case cellf.fgcolor.t
+    of ctNone: result &= SGR(39)
+    of ctANSI: result &= eightBitSGR(cellf.fgcolor.color, 0)
+    of ctRGB: assert false
+    case cellf.bgcolor.t
+    of ctNone: result &= SGR(49)
+    of ctANSI: result &= eightBitSGR(cellf.bgcolor.color, 10)
+    of ctRGB: assert false
+  of TRUE_COLOR:
+    # correct
+    cellf.fgcolor = term.correctContrast(cellf.bgcolor, cellf.fgcolor)
+    # print
+    if cellf.fgcolor != format.fgcolor:
+      case cellf.fgcolor.t
+      of ctNone: result &= SGR(39)
+      of ctANSI: result &= eightBitSGR(cellf.fgcolor.color, 0)
+      of ctRGB: result &= rgbSGR(cellf.fgcolor.rgbcolor, 0)
+    if cellf.bgcolor != format.bgcolor:
+      case cellf.bgcolor.t
+      of ctNone: result &= SGR(49)
+      of ctANSI: result &= eightBitSGR(cellf.bgcolor.color, 10)
+      of ctRGB: result &= rgbSGR(cellf.bgcolor.rgbcolor, 10)
+  of MONOCHROME:
+    discard # nothing to do
+  format = cellf
+
+proc setTitle*(term: Terminal, title: string) =
+  if term.set_title:
+    term.outfile.write(XTERM_TITLE(title.replaceControls()))
+
+proc enableMouse*(term: Terminal) =
+  term.write(XTSHIFTESCAPE & SGRMOUSEBTNON)
+
+proc disableMouse*(term: Terminal) =
+  term.write(SGRMOUSEBTNOFF)
+
+proc processOutputString*(term: Terminal, str: string, w: var int): string =
+  if str.validateUTF8Surr() != -1:
+    return "?"
+  # twidth wouldn't work here, the view may start at the nth character.
+  # pager must ensure tabs are converted beforehand.
+  w += str.notwidth()
+  let str = if Controls in str:
+    str.replaceControls()
+  else:
+    str
+  if term.cs == CHARSET_UTF_8:
+    # The output encoding matches the internal representation.
+    return str
+  else:
+    # Output is not utf-8, so we must encode it first.
+    var success = false
+    return newTextEncoder(term.cs).encodeAll(str, success)
+
+proc generateFullOutput(term: Terminal, grid: FixedGrid): string =
+  var format = Format()
+  result &= term.cursorGoto(0, 0)
+  result &= term.resetFormat()
+  result &= term.clearDisplay()
+  for y in 0 ..< grid.height:
+    if y != 0:
+      result &= "\r\n"
+    var w = 0
+    for x in 0 ..< grid.width:
+      while w < x:
+        result &= " "
+        inc w
+      let cell = grid[y * grid.width + x]
+      result &= term.processFormat(format, cell.format)
+      result &= term.processOutputString(cell.str, w)
+
+proc generateSwapOutput(term: Terminal, grid, prev: FixedGrid): string =
+  var vy = -1
+  for y in 0 ..< grid.height:
+    var w = 0
+    var change = false
+    # scan for changes, and set cx to x of the first change
+    var cx = 0
+    # if there is a change, we have to start from the last x with
+    # a string (otherwise we might overwrite a double-width char)
+    var lastx = 0
+    for x in 0 ..< grid.width:
+      let i = y * grid.width + x
+      if grid[i].str != "":
+        lastx = x
+      if grid[i] != prev[i]:
+        change = true
+        cx = lastx
+        w = lastx
+        break
+    if change:
+      if cx == 0 and vy != -1:
+        while vy < y:
+          result &= "\r\n"
+          inc vy
+      else:
+        result &= term.cursorGoto(cx, y)
+        vy = y
+      result &= term.resetFormat()
+      var format = Format()
+      for x in cx ..< grid.width:
+        while w < x: # if previous cell had no width, catch up with x
+          result &= ' '
+          inc w
+        let cell = grid[y * grid.width + x]
+        result &= term.processFormat(format, cell.format)
+        result &= term.processOutputString(cell.str, w)
+      if w < grid.width:
+        result &= term.clearEnd()
+
+proc hideCursor*(term: Terminal) =
+  when termcap_found:
+    term.write(term.ccap vi)
+  else:
+    term.write(CIVIS)
+
+proc showCursor*(term: Terminal) =
+  when termcap_found:
+    term.write(term.ccap ve)
+  else:
+    term.write(CNORM)
+
+func emulateOverline(term: Terminal): bool =
+  term.config.display.emulate_overline and
+    FLAG_OVERLINE notin term.formatmode and FLAG_UNDERLINE in term.formatmode
+
+proc writeGrid*(term: Terminal, grid: FixedGrid, x = 0, y = 0) =
+  for ly in y ..< y + grid.height:
+    for lx in x ..< x + grid.width:
+      let i = ly * term.canvas.width + lx
+      term.canvas[i] = grid[(ly - y) * grid.width + (lx - x)]
+      let isol = FLAG_OVERLINE in term.canvas[i].format.flags
+      if i >= term.canvas.width and isol and term.emulateOverline:
+        let w = grid[(ly - y) * grid.width + (lx - x)].width()
+        let s = i - term.canvas.width
+        var j = s
+        while j < term.canvas.len and j < s + w:
+          let cell = addr term.canvas[j]
+          cell.format.flags.incl(FLAG_UNDERLINE)
+          if cell.str == "":
+            cell.str = " "
+          if cell.str == " ":
+            let i = (ly - y) * grid.width + (lx - x)
+            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():
+    let colorterm = getEnv("COLORTERM")
+    if colorterm in ["24bit", "truecolor"]:
+      term.colormode = TRUE_COLOR
+  if term.config.display.format_mode.isSome:
+    term.formatmode = term.config.display.format_mode.get
+  for fm in FormatFlags:
+    if fm in term.config.display.no_format_mode:
+      term.formatmode.excl(fm)
+  if term.isatty():
+    if term.config.display.alt_screen.isSome:
+      term.smcup = term.config.display.alt_screen.get
+    term.set_title = term.config.display.set_title
+  if term.config.display.default_background_color.isSome:
+    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:
+    term.cs = DefaultCharset
+    for s in ["LC_ALL", "LC_CTYPE", "LANG"]:
+      let env = getEnv(s)
+      if env == "":
+        continue
+      let cs = getLocaleCharset(env)
+      if cs != CHARSET_UNKNOWN:
+        term.cs = cs
+        break
+  term.applyConfigDimensions()
+
+proc outputGrid*(term: Terminal) =
+  term.outfile.write(term.resetFormat())
+  let samesize = term.canvas.width == term.pcanvas.width and
+    term.canvas.height == term.pcanvas.height
+  if term.config.display.force_clear or not term.cleared or not samesize:
+    term.outfile.write(term.generateFullOutput(term.canvas))
+    term.cleared = true
+  else:
+    term.outfile.write(term.generateSwapOutput(term.canvas, term.pcanvas))
+  if not samesize:
+    term.pcanvas.width = term.canvas.width
+    term.pcanvas.height = term.canvas.height
+    term.pcanvas.cells.setLen(term.canvas.cells.len)
+  for i in 0 ..< term.canvas.cells.len:
+    term.pcanvas[i] = term.canvas[i]
+
+proc clearCanvas*(term: Terminal) =
+  term.cleared = false
+
+# see https://viewsourcecode.org/snaptoken/kilo/02.enteringRawMode.html
+proc disableRawMode(term: Terminal) =
+  let fd = term.infile.getFileHandle()
+  discard tcSetAttr(fd, TCSAFLUSH, addr term.orig_termios)
+
+proc enableRawMode(term: Terminal) =
+  let fd = term.infile.getFileHandle()
+  discard tcGetAttr(fd, addr term.orig_termios)
+  var raw = term.orig_termios
+  raw.c_iflag = raw.c_iflag and not (BRKINT or ICRNL or INPCK or ISTRIP or IXON)
+  raw.c_oflag = raw.c_oflag and not (OPOST)
+  raw.c_cflag = raw.c_cflag or CS8
+  raw.c_lflag = raw.c_lflag and not (ECHO or ICANON or ISIG or IEXTEN)
+  discard tcSetAttr(fd, TCSAFLUSH, addr raw)
+
+proc unblockStdin*(term: Terminal) =
+  if term.isatty():
+    let fd = term.infile.getFileHandle()
+    term.orig_flags = fcntl(fd, F_GETFL, 0)
+    let flags = term.orig_flags or O_NONBLOCK
+    discard fcntl(fd, F_SETFL, flags)
+    term.stdin_unblocked = true
+
+proc restoreStdin*(term: Terminal) =
+  if term.stdin_unblocked:
+    let fd = term.infile.getFileHandle()
+    discard fcntl(fd, F_SETFL, term.orig_flags)
+    term.stdin_unblocked = false
+
+proc quit*(term: Terminal) =
+  if term.isatty():
+    term.disableRawMode()
+    if term.smcup:
+      term.write(term.disableAltScreen())
+    else:
+      term.write(term.cursorGoto(0, term.attrs.height - 1) &
+        term.resetFormat() & "\n")
+    if term.config.input.use_mouse:
+      term.disableMouse()
+    term.showCursor()
+    term.cleared = false
+    if term.stdin_unblocked:
+      let fd = term.infile.getFileHandle()
+      term.orig_flags2 = fcntl(fd, F_GETFL, 0)
+      discard fcntl(fd, F_SETFL, term.orig_flags2 and (not O_NONBLOCK))
+      term.stdin_unblocked = false
+    else:
+      term.orig_flags2 = -1
+  term.flush()
+
+when termcap_found:
+  proc loadTermcap(term: Terminal) =
+    assert goutfile == nil
+    goutfile = term.outfile
+    let tc = new Termcap
+    if tgetent(cast[cstring](addr tc.bp), cstring(term.tname)) == 1:
+      term.tc = tc
+      for id in TermcapCap:
+        tc.caps[id] = tgetstr(cstring($id), cast[ptr cstring](addr tc.funcstr))
+    else:
+      raise newException(Defect, "Failed to load termcap description for terminal " & term.tname)
+
+type
+  QueryAttrs = enum
+    qaAnsiColor, qaRGB, qaSixel
+
+  QueryResult = object
+    success: bool
+    attrs: set[QueryAttrs]
+    fgcolor: Option[RGBColor]
+    bgcolor: Option[RGBColor]
+    widthPx: int
+    heightPx: int
+    width: int
+    height: int
+
+proc queryAttrs(term: Terminal, windowOnly: bool): QueryResult =
+  const tcapRGB = 0x524742 # RGB supported?
+  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 = return
+    template expect(term: Terminal, c: char) =
+      if term.consume != c:
+        fail
+    template expect(term: Terminal, s: string) =
+      for c in s:
+        term.expect c
+    template skip_until(term: Terminal, c: char) =
+      while (let cc = term.consume; cc != c):
+        discard
+    term.expect '\e'
+    case term.consume
+    of '[':
+      # CSI
+      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'
+      let c = term.consume
+      if c notin {'0', '1'}: fail
+      term.expect ';'
+      if term.consume == 'r' and term.consume == 'g' and term.consume == 'b':
+        term.expect ':'
+        template eat_color(tc: char): uint8 =
+          var val = 0u8
+          var i = 0
+          while (let c = term.consume; c != tc):
+            let v0 = hexValue(c)
+            if i > 4 or v0 == -1: fail # wat
+            let v = uint8(v0)
+            if i == 0: # 1st place
+              val = (v shl 4) or v
+            elif i == 1: # 2nd place
+              val = (val xor 0xF) or v
+            # all other places are irrelevant
+            inc i
+          val
+        let r = eat_color '/'
+        let g = eat_color '/'
+        let b = eat_color '\a'
+        if c == '0':
+          result.fgcolor = some(rgb(r, g, b))
+        else:
+          result.bgcolor = some(rgb(r, g, b))
+      else:
+        # not RGB, give up
+        term.skip_until '\a'
+    of 'P':
+      # DCS
+      let c = term.consume
+      if c notin {'0', '1'}:
+        fail
+      term.expect "+r"
+      if c == '1':
+        var id = 0
+        while (let c = term.consume; c != '='):
+          if c notin AsciiHexDigit:
+            fail
+          id *= 0x10
+          id += hexValue(c)
+        term.skip_until '\e' # ST (1)
+        if id == tcapRGB:
+          result.attrs.incl(qaRGB)
+      else: # 0
+        term.expect '\e' # ST (1)
+      term.expect '\\' # ST (2)
+    else:
+      fail
+
+type TermStartResult* = enum
+  tsrSuccess, tsrDA1Fail
+
+# 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 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
+      # 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 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(windowOnly = false)
+  if result == tsrDA1Fail:
+    term.config.display.query_da1 = false
+  if term.isatty() and term.config.input.use_mouse:
+    term.enableMouse()
+  term.applyConfig()
+  term.canvas = newFixedGrid(term.attrs.width, term.attrs.height)
+  if term.smcup:
+    term.write(term.enableAltScreen())
+
+proc restart*(term: Terminal) =
+  if term.isatty():
+    term.enableRawMode()
+    if term.orig_flags2 != -1:
+      let fd = term.infile.getFileHandle()
+      discard fcntl(fd, F_SETFL, term.orig_flags2)
+      term.orig_flags2 = 0
+      term.stdin_unblocked = true
+    if term.config.input.use_mouse:
+      term.enableMouse()
+  if term.smcup:
+    term.write(term.enableAltScreen())
+
+proc newTerminal*(outfile: File, config: Config): Terminal =
+  return Terminal(
+    outfile: outfile,
+    config: config,
+    defaultBackground: ColorsRGB["black"],
+    defaultForeground: ColorsRGB["white"]
+  )