about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2022-11-28 19:52:10 +0100
committerbptato <nincsnevem662@gmail.com>2022-11-28 23:00:06 +0100
commiteb2e57c97eb67eec19f068e294a8f6d1375c82f5 (patch)
tree87156c515f6ee9a63f58dc080184bd3127ce6836
parent8af10b8b74fd29fe4c9debcd5cbecfaddf53a7b5 (diff)
downloadchawan-eb2e57c97eb67eec19f068e294a8f6d1375c82f5.tar.gz
Add textarea
Editing is implemented using an external editor (like vi).
-rw-r--r--res/config.toml4
-rw-r--r--res/ua.css5
-rw-r--r--src/buffer/buffer.nim98
-rw-r--r--src/buffer/container.nim49
-rw-r--r--src/config/bufferconfig.nim7
-rw-r--r--src/config/config.nim26
-rw-r--r--src/css/cascade.nim23
-rw-r--r--src/css/selectorparser.nim2
-rw-r--r--src/display/client.nim2
-rw-r--r--src/display/pager.nim11
-rw-r--r--src/display/term.nim (renamed from src/io/term.nim)13
-rw-r--r--src/html/dom.nim157
-rw-r--r--src/html/htmlparser.nim74
-rw-r--r--src/html/tags.nim2
-rw-r--r--src/io/lineedit.nim2
-rw-r--r--src/ips/editor.nim59
-rw-r--r--src/ips/forkserver.nim16
-rw-r--r--src/ips/serialize.nim17
-rw-r--r--src/ips/serversocket.nim8
-rw-r--r--src/ips/socketstream.nim2
-rw-r--r--src/main.nim24
21 files changed, 411 insertions, 190 deletions
diff --git a/res/config.toml b/res/config.toml
index b9a37d75..f4cbba2a 100644
--- a/res/config.toml
+++ b/res/config.toml
@@ -1,3 +1,7 @@
+[external]
+tmpdir = "/tmp/cha"
+editor = "vi %s +%d"
+
 [display]
 color-mode = "auto"
 format-mode = "auto"
diff --git a/res/ua.css b/res/ua.css
index 6284f855..1e5acd84 100644
--- a/res/ua.css
+++ b/res/ua.css
@@ -125,6 +125,11 @@ button {
 	color: red;
 }
 
+textarea {
+	color: red;
+	white-space: pre;
+}
+
 li {
 	display: list-item;
 }
diff --git a/src/buffer/buffer.nim b/src/buffer/buffer.nim
index 2bc67d7c..d7061ce8 100644
--- a/src/buffer/buffer.nim
+++ b/src/buffer/buffer.nim
@@ -17,7 +17,7 @@ import css/cssparser
 import css/mediaquery
 import css/sheet
 import css/stylednode
-import config/bufferconfig
+import config/config
 import html/dom
 import html/tags
 import html/htmlparser
@@ -56,7 +56,6 @@ type
     lasttimeout: int
     timeout: int
     readbufsize: int
-    input: HTMLInputElement
     contenttype: string
     lines: FlexibleGrid
     rendered: bool
@@ -239,7 +238,7 @@ func getLink(node: StyledNode): HTMLAnchorElement =
   #TODO ::before links?
 
 const ClickableElements = {
-  TAG_A, TAG_INPUT, TAG_OPTION, TAG_BUTTON
+  TAG_A, TAG_INPUT, TAG_OPTION, TAG_BUTTON, TAG_TEXTAREA
 }
 
 func getClickable(styledNode: StyledNode): Element =
@@ -267,6 +266,7 @@ func cursorBytes(buffer: Buffer, y: int, cc: int): int =
   return i
 
 proc findPrevLink*(buffer: Buffer, cursorx, cursory: int): tuple[x, y: int] {.proxy.} =
+  if cursory >= buffer.lines.len: return (-1, -1)
   let line = buffer.lines[cursory]
   var i = line.findFormatN(cursorx) - 1
   var link: Element = nil
@@ -324,6 +324,7 @@ proc findPrevLink*(buffer: Buffer, cursorx, cursory: int): tuple[x, y: int] {.pr
   return (-1, -1)
 
 proc findNextLink*(buffer: Buffer, cursorx, cursory: int): tuple[x, y: int] {.proxy.} =
+  if cursory >= buffer.lines.len: return (-1, -1)
   let line = buffer.lines[cursory]
   var i = line.findFormatN(cursorx) - 1
   var link: Element = nil
@@ -350,6 +351,7 @@ proc findNextLink*(buffer: Buffer, cursorx, cursory: int): tuple[x, y: int] {.pr
   return (-1, -1)
 
 proc findPrevMatch*(buffer: Buffer, regex: Regex, cursorx, cursory: int, wrap: bool): BufferMatch {.proxy.} =
+  if cursory >= buffer.lines.len: return
   template return_if_match =
     if res.success and res.captures.len > 0:
       let cap = res.captures[^1]
@@ -377,6 +379,7 @@ proc findPrevMatch*(buffer: Buffer, regex: Regex, cursorx, cursory: int, wrap: b
     dec y
 
 proc findNextMatch*(buffer: Buffer, regex: Regex, cursorx, cursory: int, wrap: bool): BufferMatch {.proxy.} =
+  if cursory >= buffer.lines.len: return
   template return_if_match =
     if res.success and res.captures.len > 0:
       let cap = res.captures[0]
@@ -471,6 +474,7 @@ proc updateHover*(buffer: Buffer, cursorx, cursory: int): UpdateHoverResult {.pr
   buffer.prevnode = thisnode
 
 proc loadResource(buffer: Buffer, document: Document, elem: HTMLLinkElement) =
+  if elem.href == "": return
   let url = parseUrl(elem.href, document.location.some)
   if url.isSome:
     let url = url.get
@@ -514,8 +518,7 @@ proc setupSource(buffer: Buffer): ConnectResult =
   buffer.location = source.location
   case source.t
   of CLONE:
-    buffer.istream = connectSocketStream(source.clonepid)
-    SocketStream(buffer.istream).source.getFd().setBlocking(false)
+    buffer.istream = connectSocketStream(source.clonepid, blocking = false)
     if buffer.istream == nil:
       result.code = -2
       return
@@ -574,7 +577,7 @@ proc load*(buffer: Buffer): tuple[atend: bool, lines, bytes: int] {.proxy.} =
   var s = newString(buffer.readbufsize)
   try:
     buffer.istream.readStr(buffer.readbufsize, s)
-    result = (s.len < buffer.readbufsize, buffer.lines.len, bytes)
+    result = (s.len == 0, buffer.lines.len, bytes)
     if buffer.readbufsize < BufferSize:
       buffer.readbufsize = min(BufferSize, buffer.readbufsize * 2)
   except IOError:
@@ -671,12 +674,14 @@ proc constructEntryList(form: HTMLFormElement, submitter: Element = nil, encodin
         "UTF-8"
       entrylist.add((name, charset))
     else:
-      if field.tagType == TAG_INPUT:
+      case field.tagType
+      of TAG_INPUT:
         entrylist.add((name, HTMLInputElement(field).value))
-      elif field.tagType == TAG_BUTTON:
+      of TAG_BUTTON:
         entrylist.add((name, HTMLButtonElement(field).value))
-      else:
-        assert false
+      of TAG_TEXTAREA:
+        entrylist.add((name, HTMLTextAreaElement(field).value))
+      else: assert false, "Tag type " & $field.tagType & " not accounted for in constructEntryList"
     if field.tagType == TAG_TEXTAREA or
         field.tagType == TAG_INPUT and HTMLInputElement(field).inputType in {INPUT_TEXT, INPUT_SEARCH}:
       if field.attr("dirname") != "":
@@ -803,39 +808,46 @@ type ReadSuccessResult* = object
   open*: Option[Request]
   repaint*: bool
 
+proc implicitSubmit(buffer: Buffer, input: HTMLInputElement): Option[Request] =
+  if input.form != nil and input.form.canSubmitImplicitly():
+    return submitForm(input.form, input.form)
+
 proc readSuccess*(buffer: Buffer, s: string): ReadSuccessResult {.proxy.} =
-  if buffer.input != nil:
-    let input = buffer.input
-    case input.inputType
-    of INPUT_SEARCH:
-      input.value = s
-      input.invalid = true
-      buffer.do_reshape()
-      result.repaint = true
-      if input.form != nil:
-        let submitaction = submitForm(input.form, input)
-        if submitaction.isSome:
-          result.open = submitaction
-    of INPUT_TEXT, INPUT_PASSWORD:
-      input.value = s
-      input.invalid = true
-      buffer.do_reshape()
-      result.repaint = true
-    of INPUT_FILE:
-      let cdir = parseUrl("file://" & getCurrentDir() & DirSep)
-      let path = parseUrl(s, cdir)
-      if path.issome:
-        input.file = path
+  if buffer.document.focus != nil:
+    case buffer.document.focus.tagType
+    of TAG_INPUT:
+      let input = HTMLInputElement(buffer.document.focus)
+      case input.inputType
+      of INPUT_SEARCH, INPUT_TEXT, INPUT_PASSWORD:
+        input.value = s
         input.invalid = true
         buffer.do_reshape()
         result.repaint = true
+        result.open = buffer.implicitSubmit(input)
+      of INPUT_FILE:
+        let cdir = parseUrl("file://" & getCurrentDir() & DirSep)
+        let path = parseUrl(s, cdir)
+        if path.issome:
+          input.file = path
+          input.invalid = true
+          buffer.do_reshape()
+          result.repaint = true
+          result.open = buffer.implicitSubmit(input)
+      else: discard
+    of TAG_TEXTAREA:
+      let textarea = HTMLTextAreaElement(buffer.document.focus)
+      textarea.value = s
+      textarea.invalid = true
+      buffer.do_reshape()
+      result.repaint = true
     else: discard
-    buffer.input = nil
+    buffer.restore_focus
 
 type ReadLineResult* = object
   prompt*: string
   value*: string
   hide*: bool
+  area*: bool
 
 type ClickResult* = object
   open*: Option[Request]
@@ -848,6 +860,7 @@ proc click*(buffer: Buffer, cursorx, cursory: int): ClickResult {.proxy.} =
     case clickable.tagType
     of TAG_SELECT:
       buffer.set_focus clickable
+      result.repaint = true
     of TAG_A:
       buffer.restore_focus
       let url = parseUrl(HTMLAnchorElement(clickable).href, clickable.document.baseUrl.some)
@@ -877,24 +890,32 @@ proc click*(buffer: Buffer, cursorx, cursory: int): ClickResult {.proxy.} =
           result.repaint = true
           buffer.do_reshape()
         of BUTTON_BUTTON: discard
+    of TAG_TEXTAREA:
+      buffer.set_focus clickable
+      let textarea = HTMLTextAreaElement(clickable)
+      result.readline = some(ReadLineResult(
+        value: textarea.value,
+        area: true
+      ))
     of TAG_INPUT:
       buffer.restore_focus
       let input = HTMLInputElement(clickable)
       case input.inputType
       of INPUT_SEARCH:
-        buffer.input = input
+        buffer.set_focus input
         result.readline = some(ReadLineResult(
           prompt: "SEARCH: ",
           value: input.value
         ))
       of INPUT_TEXT, INPUT_PASSWORD:
-        buffer.input = input
+        buffer.set_focus input
         result.readline = some(ReadLineResult(
           prompt: "TEXT: ",
           value: input.value,
           hide: input.inputType == INPUT_PASSWORD
         ))
       of INPUT_FILE:
+        buffer.set_focus input
         var path = if input.file.issome:
           input.file.get.path.serialize_unicode()
         else:
@@ -930,12 +951,12 @@ proc click*(buffer: Buffer, cursorx, cursory: int): ClickResult {.proxy.} =
       buffer.restore_focus
 
 proc readCanceled*(buffer: Buffer) {.proxy.} =
-  buffer.input = nil
+  buffer.restore_focus
 
 proc findAnchor*(buffer: Buffer, anchor: string): bool {.proxy.} =
   return buffer.document != nil and buffer.document.getElementById(anchor) != nil
 
-proc getLines*(buffer: Buffer, w: Slice[int]): seq[SimpleFlexibleLine] {.proxy.} =
+proc getLines*(buffer: Buffer, w: Slice[int]): tuple[numLines: int, lines: seq[SimpleFlexibleLine]] {.proxy.} =
   var w = w
   if w.b < 0 or w.b > buffer.lines.high:
     w.b = buffer.lines.high
@@ -944,7 +965,8 @@ proc getLines*(buffer: Buffer, w: Slice[int]): seq[SimpleFlexibleLine] {.proxy.}
     var line = SimpleFlexibleLine(str: buffer.lines[y].str)
     for f in buffer.lines[y].formats:
       line.formats.add(SimpleFormatCell(format: f.format, pos: f.pos))
-    result.add(line)
+    result.lines.add(line)
+  result.numLines = buffer.lines.len
 
 proc passFd*(buffer: Buffer) {.proxy.} =
   let fd = SocketStream(buffer.pistream).recvFileHandle()
diff --git a/src/buffer/container.nim b/src/buffer/container.nim
index 65b1ae59..9a0ff802 100644
--- a/src/buffer/container.nim
+++ b/src/buffer/container.nim
@@ -8,7 +8,6 @@ when defined(posix):
 
 import buffer/buffer
 import buffer/cell
-import config/bufferconfig
 import config/config
 import io/request
 import io/window
@@ -33,7 +32,7 @@ type
 
   ContainerEventType* = enum
     NO_EVENT, FAIL, SUCCESS, NEEDS_AUTH, REDIRECT, ANCHOR, NO_ANCHOR, UPDATE,
-    READ_LINE, OPEN, INVALID_COMMAND, STATUS, ALERT
+    READ_LINE, READ_AREA, OPEN, INVALID_COMMAND, STATUS, ALERT
 
   ContainerEvent* = object
     case t*: ContainerEventType
@@ -41,6 +40,8 @@ type
       prompt*: string
       value*: string
       password*: bool
+    of READ_AREA:
+      tvalue*: string
     of OPEN:
       request*: Request
     of ANCHOR, NO_ANCHOR:
@@ -71,7 +72,7 @@ type
     bpos: seq[CursorPosition]
     highlights: seq[Highlight]
     parent*: Container
-    process*: Pid
+    process* {.jsget.}: Pid
     loadinfo*: string
     lines: SimpleFlexibleGrid
     lineshift: int
@@ -93,7 +94,7 @@ proc newBuffer*(dispatcher: Dispatcher, config: Config, source: BufferSource, ti
   let istream = dispatcher.forkserver.istream
   ostream.swrite(FORK_BUFFER)
   ostream.swrite(source)
-  ostream.swrite(config.loadBufferConfig())
+  ostream.swrite(config.getBufferConfig())
   ostream.swrite(attrs)
   ostream.swrite(dispatcher.mainproc)
   ostream.flush()
@@ -252,13 +253,17 @@ proc triggerEvent(container: Container, t: ContainerEventType) =
 
 proc updateCursor(container: Container)
 
+proc setNumLines(container: Container, lines: int) =
+  container.numLines = lines
+  container.updateCursor()
+
 proc requestLines*(container: Container, w = container.lineWindow) =
-  container.iface.getLines(w).then(proc(res: seq[SimpleFlexibleLine]) =
+  container.iface.getLines(w).then(proc(res: tuple[numLines: int, lines: seq[SimpleFlexibleLine]]) =
     container.lines.setLen(w.len)
     container.lineshift = w.a
-    for y in 0 ..< min(res.len, w.len):
-      container.lines[y] = res[y]
-    container.updateCursor()
+    for y in 0 ..< min(res.lines.len, w.len):
+      container.lines[y] = res.lines[y]
+    container.setNumLines(res.numLines)
     container.redraw = true
     let cw = container.fromy ..< container.fromy + container.height
     if w.a in cw or w.b in cw or cw.a in w or cw.b in w:
@@ -544,8 +549,8 @@ proc cursorNextMatch*(container: Container, regex: Regex, wrap: bool) {.jsfunc.}
   container.iface
     .findNextMatch(regex, container.cursorx, container.cursory, wrap)
     .then(proc(res: BufferMatch) =
+      container.setCursorXY(res.x, res.y)
       if container.hlon:
-        container.setCursorXY(res.x, res.y)
         container.clearSearchHighlights()
         let ex = res.x + res.str.width() - 1
         let hl = Highlight(x: res.x, y: res.y, endx: ex, endy: res.y, clear: true)
@@ -556,8 +561,8 @@ proc cursorPrevMatch*(container: Container, regex: Regex, wrap: bool) {.jsfunc.}
   container.iface
     .findPrevMatch(regex, container.cursorx, container.cursory, wrap)
     .then(proc(res: BufferMatch) =
+      container.setCursorXY(res.x, res.y)
       if container.hlon:
-        container.setCursorXY(res.x, res.y)
         container.clearSearchHighlights()
         let ex = res.x + res.str.width() - 1
         let hl = Highlight(x: res.x, y: res.y, endx: ex, endy: res.y, clear: true)
@@ -568,10 +573,6 @@ proc setLoadInfo(container: Container, msg: string) =
   container.loadinfo = msg
   container.triggerEvent(STATUS)
 
-proc setNumLines(container: Container, lines: int) =
-  container.numLines = lines
-  container.updateCursor()
-
 proc alert(container: Container, msg: string) =
   container.triggerEvent(ContainerEvent(t: ALERT, msg: msg))
 
@@ -677,12 +678,20 @@ proc click*(container: Container) {.jsfunc.} =
       container.triggerEvent(ContainerEvent(t: OPEN, request: res.open.get))
     if res.readline.isSome:
       let rl = res.readline.get
-      container.triggerEvent(
-        ContainerEvent(
-          t: READ_LINE,
-          prompt: rl.prompt,
-          value: rl.value,
-          password: rl.hide)))
+      if rl.area:
+        container.triggerEvent(
+          ContainerEvent(
+            t: READ_AREA,
+            tvalue: rl.value
+          ))
+      else:
+        container.triggerEvent(
+          ContainerEvent(
+            t: READ_LINE,
+            prompt: rl.prompt,
+            value: rl.value,
+            password: rl.hide
+          )))
 
 proc windowChange*(container: Container, attrs: WindowAttributes) =
   container.attrs = attrs
diff --git a/src/config/bufferconfig.nim b/src/config/bufferconfig.nim
deleted file mode 100644
index 78f3b87e..00000000
--- a/src/config/bufferconfig.nim
+++ /dev/null
@@ -1,7 +0,0 @@
-import config/config
-
-type BufferConfig* = object
-  userstyle*: string
-
-proc loadBufferConfig*(config: Config): BufferConfig =
-  result.userstyle = config.stylesheet
diff --git a/src/config/config.nim b/src/config/config.nim
index ff4f0b45..316e6d33 100644
--- a/src/config/config.nim
+++ b/src/config/config.nim
@@ -11,8 +11,11 @@ import utils/twtstr
 type
   ColorMode* = enum
     MONOCHROME, ANSI, EIGHT_BIT, TRUE_COLOR
+
   FormatMode* = set[FormatFlags]
+
   ActionMap = Table[string, string]
+
   Config* = ref ConfigObj
   ConfigObj* = object
     nmap*: ActionMap
@@ -26,6 +29,24 @@ type
     formatmode*: Option[FormatMode]
     altscreen*: Option[bool]
     mincontrast*: float
+    editor*: string
+    tmpdir*: string
+
+  BufferConfig* = object
+    userstyle*: string
+
+  ForkServerConfig* = object
+    tmpdir*: string
+    ambiguous_double*: bool
+
+func getForkServerConfig*(config: Config): ForkServerConfig =
+  return ForkServerConfig(
+    tmpdir: config.tmpdir,
+    ambiguous_double: config.ambiguous_double
+  )
+
+func getBufferConfig*(config: Config): BufferConfig =
+  result.userstyle = config.stylesheet
 
 func getRealKey(key: string): string =
   var realk: string
@@ -164,6 +185,11 @@ proc parseConfig(config: Config, dir: string, t: TomlValue) =
             config.mincontrast = float(v.i)
           else:
             config.mincontrast = float(v.f)
+    of "external":
+      for k, v in v:
+        case k
+        of "editor": config.editor = v.s
+        of "tmpdir": config.tmpdir = v.s
 
 proc parseConfig(config: Config, dir: string, stream: Stream) =
   config.parseConfig(dir, parseToml(stream))
diff --git a/src/css/cascade.nim b/src/css/cascade.nim
index 851e6f83..273ba4cb 100644
--- a/src/css/cascade.nim
+++ b/src/css/cascade.nim
@@ -109,6 +109,10 @@ func calcPresentationalHints(element: Element): CSSComputedValues =
     map_width
   of TAG_BODY:
     map_bgcolor
+  of TAG_TEXTAREA:
+    let textarea = HTMLTextAreaElement(element)
+    set_cv(PROPERTY_WIDTH, length, CSSLength(unit: UNIT_CH, num: float64(textarea.cols)))
+    set_cv(PROPERTY_HEIGHT, length, CSSLength(unit: UNIT_EM, num: float64(textarea.rows)))
   else: discard
  
 proc applyDeclarations(styledNode: StyledNode, parent: CSSComputedValues, ua, user: DeclarationList, author: seq[DeclarationList]) =
@@ -225,6 +229,12 @@ proc applyRules(document: Document, ua, user: CSSStylesheet, cachedTree: StyledN
             let styledText = styledParent.newStyledText(content)
             styledText.pseudo = pseudo
             styledParent.children.add(styledText)
+        of PSEUDO_TEXTAREA_TEXT:
+          let content = HTMLTextAreaElement(styledParent.node).textAreaString()
+          if content.len > 0:
+            let styledText = styledParent.newStyledText(content)
+            styledText.pseudo = pseudo
+            styledParent.children.add(styledText)
         of PSEUDO_NONE: discard
       else:
         assert child != nil
@@ -299,12 +309,15 @@ proc applyRules(document: Document, ua, user: CSSStylesheet, cachedTree: StyledN
 
       stack_append styledChild, PSEUDO_AFTER
 
-      for i in countdown(elem.childNodes.high, 0):
-        if elem.childNodes[i].nodeType in {ELEMENT_NODE, TEXT_NODE}:
-          stack_append styledChild, elem.childNodes[i]
+      if elem.tagType != TAG_TEXTAREA:
+        for i in countdown(elem.childNodes.high, 0):
+          if elem.childNodes[i].nodeType in {ELEMENT_NODE, TEXT_NODE}:
+            stack_append styledChild, elem.childNodes[i]
+        if elem.tagType == TAG_INPUT:
+          stack_append styledChild, PSEUDO_INPUT_TEXT
+      else:
+        stack_append styledChild, PSEUDO_TEXTAREA_TEXT
 
-      if elem.tagType == TAG_INPUT:
-        stack_append styledChild, PSEUDO_INPUT_TEXT
       stack_append styledChild, PSEUDO_BEFORE
 
 proc applyStylesheets*(document: Document, uass, userss: CSSStylesheet, previousStyled: StyledNode): StyledNode =
diff --git a/src/css/selectorparser.nim b/src/css/selectorparser.nim
index f8abc7f7..97c6f029 100644
--- a/src/css/selectorparser.nim
+++ b/src/css/selectorparser.nim
@@ -19,7 +19,7 @@ type
   PseudoElem* = enum
     PSEUDO_NONE, PSEUDO_BEFORE, PSEUDO_AFTER,
     # internal
-    PSEUDO_INPUT_TEXT
+    PSEUDO_INPUT_TEXT, PSEUDO_TEXTAREA_TEXT
 
   PseudoClass* = enum
     PSEUDO_FIRST_CHILD, PSEUDO_LAST_CHILD, PSEUDO_ONLY_CHILD, PSEUDO_HOVER,
diff --git a/src/display/client.nim b/src/display/client.nim
index 3b56373e..34477c1a 100644
--- a/src/display/client.nim
+++ b/src/display/client.nim
@@ -17,12 +17,12 @@ import buffer/container
 import css/sheet
 import config/config
 import display/pager
+import display/term
 import html/dom
 import html/htmlparser
 import io/lineedit
 import io/loader
 import io/request
-import io/term
 import io/window
 import ips/forkserver
 import ips/serialize
diff --git a/src/display/pager.nim b/src/display/pager.nim
index b280d906..6d684b26 100644
--- a/src/display/pager.nim
+++ b/src/display/pager.nim
@@ -11,10 +11,11 @@ when defined(posix):
 import buffer/cell
 import buffer/container
 import config/config
+import display/term
 import io/lineedit
 import io/request
-import io/term
 import io/window
+import ips/editor
 import ips/forkserver
 import ips/socketstream
 import js/javascript
@@ -629,6 +630,14 @@ proc handleEvent0(pager: Pager, container: Container, event: ContainerEvent): bo
   of READ_LINE:
     if container == pager.container:
       pager.setLineEdit(readLine(event.prompt, pager.attrs.width, current = event.value, hide = event.password, term = pager.term), BUFFER)
+  of READ_AREA:
+    if container == pager.container:
+      var s = event.tvalue
+      if openInEditor(pager.term, pager.config, s):
+        pager.container.readSuccess(s)
+      else:
+        pager.container.readCanceled()
+      pager.redraw = true
   of OPEN:
     pager.gotoURL(event.request, some(container.source.location))
   of INVALID_COMMAND: discard
diff --git a/src/io/term.nim b/src/display/term.nim
index 7d3786e0..ad9368d3 100644
--- a/src/io/term.nim
+++ b/src/display/term.nim
@@ -401,8 +401,9 @@ proc quit*(term: Terminal) =
       term.write(term.disableAltScreen())
     else:
       term.write(term.cursorGoto(0, term.attrs.height - 1))
-    term.outfile.showCursor()
-  term.outfile.flushFile()
+    term.showCursor()
+    term.cleared = false
+  term.flush()
 
 when termcap_found:
   proc loadTermcap(term: Terminal) =
@@ -451,14 +452,18 @@ proc detectTermAttributes(term: Terminal) =
 
 proc start*(term: Terminal, infile: File) =
   term.infile = infile
-  assert term.outfile.getFileHandle().setInheritable(false)
-  assert term.infile.getFileHandle().setInheritable(false)
   if term.isatty():
     enableRawMode(infile.getFileHandle())
   term.detectTermAttributes()
   if term.smcup:
     term.write(term.enableAltScreen())
 
+proc restart*(term: Terminal) =
+  if term.isatty():
+    enableRawMode(term.infile.getFileHandle())
+  if term.smcup:
+    term.write(term.enableAltScreen())
+
 proc newTerminal*(outfile: File, config: Config, attrs: WindowAttributes): Terminal =
   let term = new Terminal
   term.outfile = outfile
diff --git a/src/html/dom.nim b/src/html/dom.nim
index be66dc51..ab3b95be 100644
--- a/src/html/dom.nim
+++ b/src/html/dom.nim
@@ -1,3 +1,4 @@
+import macros
 import options
 import streams
 import strutils
@@ -91,10 +92,10 @@ type
   HTMLElement* = ref object of Element
 
   FormAssociatedElement* = ref object of HTMLElement
-    form*: HTMLFormElement
     parserInserted*: bool
 
   HTMLInputElement* = ref object of FormAssociatedElement
+    form* {.jsget.}: HTMLFormElement
     inputType*: InputType
     autofocus*: bool
     required*: bool
@@ -108,6 +109,7 @@ type
   HTMLAnchorElement* = ref object of HTMLElement
 
   HTMLSelectElement* = ref object of FormAssociatedElement
+    form* {.jsget.}: HTMLFormElement
     size*: int
 
   HTMLSpanElement* = ref object of HTMLElement
@@ -169,8 +171,15 @@ type
   HTMLAreaElement* = ref object of HTMLElement
 
   HTMLButtonElement* = ref object of FormAssociatedElement
+    form* {.jsget.}: HTMLFormElement
     ctype*: ButtonType
-    value*: string
+    value* {.jsget, jsset.}: string
+
+  HTMLTextAreaElement* = ref object of FormAssociatedElement
+    form* {.jsget.}: HTMLFormElement
+    rows*: int
+    cols*: int
+    value* {.jsget.}: string
 
 proc tostr(ftype: enum): string =
   return ($ftype).split('_')[1..^1].join("-").tolower()
@@ -312,6 +321,39 @@ iterator options*(select: HTMLSelectElement): HTMLOptionElement {.inline.} =
         if opt.tagType == TAG_OPTION:
           yield HTMLOptionElement(child)
 
+func form*(element: FormAssociatedElement): HTMLFormElement =
+  case element.tagType
+  of TAG_INPUT: return HTMLInputElement(element).form
+  of TAG_SELECT: return HTMLSelectElement(element).form
+  of TAG_BUTTON: return HTMLButtonElement(element).form
+  of TAG_TEXTAREA: return HTMLTextAreaElement(element).form
+  else: assert false
+
+func `form=`*(element: FormAssociatedElement, form: HTMLFormElement) =
+  case element.tagType
+  of TAG_INPUT: HTMLInputElement(element).form = form
+  of TAG_SELECT:  HTMLSelectElement(element).form = form
+  of TAG_BUTTON: HTMLButtonElement(element).form = form
+  of TAG_TEXTAREA: HTMLTextAreaElement(element).form = form
+  else: assert false
+
+func canSubmitImplicitly*(form: HTMLFormElement): bool =
+  const BlocksImplicitSubmission = {
+    INPUT_TEXT, INPUT_SEARCH, INPUT_URL, INPUT_TEL, INPUT_EMAIL, INPUT_PASSWORD,
+    INPUT_DATE, INPUT_MONTH, INPUT_WEEK, INPUT_TIME, INPUT_DATETIME_LOCAL,
+    INPUT_NUMBER
+  }
+  var found = false
+  for control in form.controls:
+    if control.tagType == TAG_INPUT:
+      let input = HTMLInputElement(control)
+      if input.inputType in BlocksImplicitSubmission:
+        if found:
+          return false
+        else:
+          found = true
+  return true
+
 func qualifiedName*(element: Element): string =
   if element.namespacePrefix.issome: element.namespacePrefix.get & ':' & element.localName
   else: element.localName
@@ -441,7 +483,7 @@ func nextElementSibling*(elem: Element): Element =
     inc i
   return nil
 
-func attr*(element: Element, s: string): string =
+func attr*(element: Element, s: string): string {.inline.} =
   return element.attributes.getOrDefault(s, "")
 
 func attri*(element: Element, s: string): Option[int] =
@@ -492,7 +534,7 @@ proc sheets*(element: Element): seq[CSSStylesheet] =
         result.add(child.sheet)
 
 func inputString*(input: HTMLInputElement): string =
-  var text = case input.inputType
+  case input.inputType
   of INPUT_CHECKBOX, INPUT_RADIO:
     if input.checked: "*"
     else: " "
@@ -510,7 +552,17 @@ func inputString*(input: HTMLInputElement): string =
     if input.file.isnone: "".padToWidth(input.size)
     else: input.file.get.path.serialize_unicode().padToWidth(input.size)
   else: input.value
-  return text
+
+func textAreaString*(textarea: HTMLTextAreaElement): string =
+  let split = textarea.value.split('\n')
+  for i in 0 ..< textarea.rows:
+    if textarea.cols > 2:
+      if i < split.len:
+        result &= '[' & split[i].padToWidth(textarea.cols - 2) & "]\n"
+      else:
+        result &= '[' & ' '.repeat(textarea.cols - 2) & "]\n"
+    else:
+      result &= "[]\n"
 
 func isButton*(element: Element): bool =
   if element.tagType == TAG_BUTTON:
@@ -537,6 +589,8 @@ func action*(element: Element): string =
     if element.form != nil:
       if element.form.attrb("action"):
         return element.form.attr("action")
+  if element.tagType == TAG_FORM:
+    return element.attr("action")
   return ""
 
 func enctype*(element: Element): FormEncodingType =
@@ -655,6 +709,8 @@ func newHTMLElement*(document: Document, tagType: TagType, namespace = Namespace
     result = new(HTMLBaseElement)
   of TAG_BUTTON:
     result = new(HTMLButtonElement)
+  of TAG_TEXTAREA:
+    result = new(HTMLTextAreaElement)
   else:
     result = new(HTMLElement)
 
@@ -886,6 +942,10 @@ proc resetElement*(element: Element) =
             if option.selected:
               option.selected = false
               inc j
+  of TAG_TEXTAREA:
+    let textarea = HTMLTextAreaElement(element)
+    textarea.value = textarea.childTextContent()
+    textarea.invalid = true
   else: discard
 
 proc setForm*(element: FormAssociatedElement, form: HTMLFormElement) =
@@ -902,7 +962,11 @@ proc setForm*(element: FormAssociatedElement, form: HTMLFormElement) =
     let button = HTMLButtonElement(element)
     button.form = form
     form.controls.add(button)
-  of TAG_FIELDSET, TAG_OBJECT, TAG_OUTPUT, TAG_TEXTAREA, TAG_IMG:
+  of TAG_TEXTAREA:
+    let textarea = HTMLTextAreaElement(element)
+    textarea.form = form
+    form.controls.add(textarea)
+  of TAG_FIELDSET, TAG_OBJECT, TAG_OUTPUT, TAG_IMG:
     discard #TODO
   else: assert false
 
@@ -935,9 +999,7 @@ proc insertionSteps(insertedNode: Node) =
         if select != nil:
           select.resetElement()
     else: discard
-    if tagType in FormAssociatedElements:
-      if tagType notin SupportedFormAssociatedElements:
-        return #TODO TODO TODO implement others too
+    if tagType in SupportedFormAssociatedElements:
       let element = FormAssociatedElement(element)
       if element.parserInserted:
         return
@@ -987,52 +1049,57 @@ proc reset*(form: HTMLFormElement) =
     control.resetElement()
     control.invalid = true
 
-proc appendAttribute*(element: Element, k, v: string) =
-  case k
-  of "id": element.id = v
-  of "class":
-    let classes = v.split(' ')
-    for class in classes:
-      if class != "" and class notin element.classList:
-        element.classList.add(class)
+proc appendAttributes*(element: Element, attrs: Table[string, string]) =
+  for k, v in attrs:
+    element.attributes[k] = v
+  template reflect_str(element: Element, name: static string, val: untyped) =
+    element.attributes.withValue(name, val):
+      element.val = val[]
+  template reflect_str(element: Element, name: static string, val, fun: untyped) =
+    element.attributes.withValue(name, val):
+      element.val = fun(val[])
+  template reflect_nonzero_int(element: Element, name: static string, val: untyped, default: int) =
+    element.attributes.withValue(name, val):
+      if val[].isValidNonZeroInt():
+        element.val = parseInt(val[])
+      else:
+        element.val = default
+    do:
+      element.val = default
+  template reflect_bool(element: Element, name: static string, val: untyped) =
+    if name in element.attributes:
+      element.val = true
+  element.reflect_str "id", id
+  element.attributes.withValue("class", val):
+    let classList = val[].split(' ')
+    for x in classList:
+      if x != "" and x notin element.classList:
+        element.classList.add(x)
   case element.tagType
   of TAG_INPUT:
     let input = HTMLInputElement(element)
-    case k
-    of "value": input.value = v
-    of "type": input.inputType = inputType(v)
-    of "size":
-      if v.isValidNonZeroInt():
-        input.size = parseInt(v)
-      else:
-        input.size = 20
-    of "checked": input.checked = true
+    input.reflect_str "value", value
+    input.reflect_str "type", inputType, inputType
+    input.reflect_nonzero_int "size", size, 20
+    input.reflect_bool "checked", checked
   of TAG_OPTION:
     let option = HTMLOptionElement(element)
-    if k == "selected":
-      option.selected = true
+    option.reflect_bool "selected", selected
   of TAG_SELECT:
     let select = HTMLSelectElement(element)
-    case k
-    of "multiple":
-      if not select.attributes["size"].isValidNonZeroInt():
-        select.size = 4
-    of "size":
-      if v.isValidNonZeroInt():
-        select.size = parseInt(v)
-      elif "multiple" in select.attributes:
-        select.size = 4
+    select.reflect_nonzero_int "size", size, (if "multiple" in element.attributes: 4 else: 1)
   of TAG_BUTTON:
     let button = HTMLButtonElement(element)
-    if k == "type":
-      case v
-      of "submit": button.ctype = BUTTON_SUBMIT
-      of "reset": button.ctype = BUTTON_RESET
-      of "button": button.ctype = BUTTON_BUTTON
-    elif k == "value":
-      button.value = v
+    button.reflect_str "type", ctype, (func(s: string): ButtonType =
+      case s
+      of "submit": return BUTTON_SUBMIT
+      of "reset": return BUTTON_RESET
+      of "button": return BUTTON_BUTTON)
+  of TAG_TEXTAREA:
+    let textarea = HTMLTextAreaElement(element)
+    textarea.reflect_nonzero_int "cols", cols, 20
+    textarea.reflect_nonzero_int "rows", rows, 1
   else: discard
-  element.attributes[k] = v
 
 # Forward definition hack (these are set in selectors.nim)
 var doqsa*: proc (node: Node, q: string): seq[Element]
diff --git a/src/html/htmlparser.nim b/src/html/htmlparser.nim
index 2b48c59d..e1359201 100644
--- a/src/html/htmlparser.nim
+++ b/src/html/htmlparser.nim
@@ -16,11 +16,13 @@ import utils/twtstr
 type
   DOMParser = ref object # JS interface
 
+  OpenElements = seq[Element]
+
   HTML5Parser = object
     case fragment: bool
     of true: ctx: Element
     else: discard
-    openElements: seq[Element]
+    openElements: OpenElements
     insertionMode: InsertionMode
     oldInsertionMode: InsertionMode
     templateModes: seq[InsertionMode]
@@ -196,8 +198,9 @@ func createElement(parser: HTML5Parser, token: Token, namespace: Namespace, inte
   let document = intendedParent.document
   let localName = token.tagname
   let element = document.newHTMLElement(localName, namespace, tagType = token.tagtype)
-  for k, v in token.attrs:
-    element.appendAttribute(k, v)
+  element.appendAttributes(token.attrs)
+  #for k, v in token.attrs:
+  #  element.appendAttribute(k, v)
   if element.isResettable():
     element.resetElement()
 
@@ -450,18 +453,23 @@ proc genericRCDATAElementParsingAlgorithm(parser: var HTML5Parser, token: Token)
   parser.oldInsertionMode = parser.insertionMode
   parser.insertionMode = TEXT
 
+proc popElement(parser: var HTML5Parser): Element =
+  result = parser.openElements.pop()
+  if result.tagType == TAG_TEXTAREA:
+    result.resetElement()
+
 # 13.2.6.3
 proc generateImpliedEndTags(parser: var HTML5Parser) =
   const tags = {TAG_DD, TAG_DT, TAG_LI, TAG_OPTGROUP, TAG_OPTION, TAG_P,
                 TAG_RB, TAG_RP, TAG_RT, TAG_RTC}
   while parser.currentNode.tagType in tags:
-    discard parser.openElements.pop()
+    discard parser.popElement()
 
 proc generateImpliedEndTags(parser: var HTML5Parser, exclude: TagType) =
   let tags = {TAG_DD, TAG_DT, TAG_LI, TAG_OPTGROUP, TAG_OPTION, TAG_P,
                 TAG_RB, TAG_RP, TAG_RT, TAG_RTC} - {exclude}
   while parser.currentNode.tagType in tags:
-    discard parser.openElements.pop()
+    discard parser.popElement()
 
 proc generateImpliedEndTagsThoroughly(parser: var HTML5Parser) =
   const tags = {TAG_CAPTION, TAG_COLGROUP, TAG_DD, TAG_DT, TAG_LI,
@@ -469,7 +477,7 @@ proc generateImpliedEndTagsThoroughly(parser: var HTML5Parser) =
                 TAG_RTC, TAG_TBODY, TAG_TD, TAG_TFOOT, TAG_TH, TAG_THEAD,
                 TAG_TR}
   while parser.currentNode.tagType in tags:
-    discard parser.openElements.pop()
+    discard parser.popElement()
 
 # 13.2.4.3
 proc pushOntoActiveFormatting(parser: var HTML5Parser, element: Element, token: Token) =
@@ -535,7 +543,7 @@ proc reconstructActiveFormatting(parser: var HTML5Parser) =
 proc clearActiveFormattingTillMarker(parser: var HTML5Parser) =
   while parser.activeFormatting.len > 0 and parser.activeFormatting.pop()[0] != nil: discard
 
-template pop_current_node = discard parser.openElements.pop()
+template pop_current_node = discard parser.popElement()
 
 func isHTMLIntegrationPoint(node: Element): bool =
   return false #TODO SVG (NOTE MathML not implemented)
@@ -849,7 +857,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
           parser.generateImpliedEndTagsThoroughly()
           if parser.currentNode.tagType != TAG_TEMPLATE:
             parse_error
-          while parser.openElements.pop().tagType != TAG_TEMPLATE: discard
+          while parser.popElement().tagType != TAG_TEMPLATE: discard
           parser.clearActiveFormattingTillMarker()
           discard parser.templateModes.pop()
           parser.resetInsertionMode()
@@ -918,7 +926,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
     proc closeP(parser: var HTML5Parser) =
       parser.generateImpliedEndTags(TAG_P)
       if parser.currentNode.tagType != TAG_P: parse_error
-      while parser.openElements.pop().tagType != TAG_P: discard
+      while parser.popElement().tagType != TAG_P: discard
 
     proc adoptionAgencyAlgorithm(parser: var HTML5Parser, token: Token): bool =
       if parser.currentNode.tagType != TAG_UNKNOWN and parser.currentNode.tagtype == token.tagtype or parser.currentNode.localName == token.tagname:
@@ -965,7 +973,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
             furthestBlockIndex = j
             break
         if furthestBlock == nil:
-          while parser.openElements.pop() != formatting: discard
+          while parser.popElement() != formatting: discard
           parser.activeFormatting.delete(formattingIndex)
           return false
         let commonAncestor = parser.openElements[stackIndex - 1]
@@ -1031,7 +1039,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
         if node.tagType != TAG_UNKNOWN and node.tagType == token.tagtype or node.localName == token.tagname:
           parser.generateImpliedEndTags(token.tagtype)
           if node != parser.currentNode: parse_error
-          while parser.openElements.pop() != node: discard
+          while parser.popElement() != node: discard
           break
         elif node.tagType in SpecialElements:
           parse_error
@@ -1149,7 +1157,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
           of TAG_LI:
             parser.generateImpliedEndTags(TAG_LI)
             if parser.currentNode.tagType != TAG_LI: parse_error
-            while parser.openElements.pop().tagType != TAG_LI: discard
+            while parser.popElement().tagType != TAG_LI: discard
             break
           of SpecialElements - {TAG_ADDRESS, TAG_DIV, TAG_P, TAG_LI}:
             break
@@ -1166,12 +1174,12 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
           of TAG_DD:
             parser.generateImpliedEndTags(TAG_DD)
             if parser.currentNode.tagType != TAG_DD: parse_error
-            while parser.openElements.pop().tagType != TAG_DD: discard
+            while parser.popElement().tagType != TAG_DD: discard
             break
           of TAG_DT:
             parser.generateImpliedEndTags(TAG_DT)
             if parser.currentNode.tagType != TAG_DT: parse_error
-            while parser.openElements.pop().tagType != TAG_DT: discard
+            while parser.popElement().tagType != TAG_DT: discard
             break
           of SpecialElements - {TAG_ADDRESS, TAG_DIV, TAG_P, TAG_DD, TAG_DT}:
             break
@@ -1190,7 +1198,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
         if parser.openElements.hasElementInScope(TAG_BUTTON):
           parse_error
           parser.generateImpliedEndTags()
-          while parser.openElements.pop().tagType != TAG_BUTTON: discard
+          while parser.popElement().tagType != TAG_BUTTON: discard
         parser.reconstructActiveFormatting()
         discard parser.insertHTMLElement(token)
         parser.framesetOk = false
@@ -1205,7 +1213,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
         else:
           parser.generateImpliedEndTags()
           if parser.currentNode.tagType != token.tagtype: parse_error
-          while parser.openElements.pop().tagType != token.tagtype: discard
+          while parser.popElement().tagType != token.tagtype: discard
       )
       "</form>" => (block:
         if not parser.openElements.hasElement(TAG_TEMPLATE):
@@ -1223,7 +1231,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
             return
           parser.generateImpliedEndTags()
           if parser.currentNode.tagType != TAG_FORM: parse_error
-          while parser.openElements.pop().tagType != TAG_FORM: discard
+          while parser.popElement().tagType != TAG_FORM: discard
       )
       "</p>" => (block:
         if not parser.openElements.hasElementInButtonScope(TAG_P):
@@ -1237,7 +1245,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
         else:
           parser.generateImpliedEndTags(TAG_LI)
           if parser.currentNode.tagType != TAG_LI: parse_error
-          while parser.openElements.pop().tagType != TAG_LI: discard
+          while parser.popElement().tagType != TAG_LI: discard
       )
       ("</dd>", "</dt>") => (block:
         if not parser.openElements.hasElementInScope(token.tagtype):
@@ -1245,7 +1253,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
         else:
           parser.generateImpliedEndTags(token.tagtype)
           if parser.currentNode.tagType != token.tagtype: parse_error
-          while parser.openElements.pop().tagType != token.tagtype: discard
+          while parser.popElement().tagType != token.tagtype: discard
       )
       ("</h1>", "</h2>", "</h3>", "</h4>", "</h5>", "</h6>") => (block:
         if not parser.openElements.hasElementInScope(HTagTypes):
@@ -1253,7 +1261,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
         else:
           parser.generateImpliedEndTags()
           if parser.currentNode.tagType != token.tagtype: parse_error
-          while parser.openElements.pop().tagType notin HTagTypes: discard
+          while parser.popElement().tagType notin HTagTypes: discard
       )
       "</sarcasm>" => (block:
         #*deep breath*
@@ -1321,7 +1329,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
         else:
           parser.generateImpliedEndTags()
           if parser.currentNode.tagType != token.tagtype: parse_error
-          while parser.openElements.pop().tagType != token.tagtype: discard
+          while parser.popElement().tagType != token.tagtype: discard
           parser.clearActiveFormattingTillMarker()
       )
       "<table>" => (block:
@@ -1504,7 +1512,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
         if not parser.openElements.hasElementInScope(TAG_TABLE):
           discard
         else:
-          while parser.openElements.pop().tagType != TAG_TABLE: discard
+          while parser.popElement().tagType != TAG_TABLE: discard
           parser.resetInsertionMode()
           reprocess token
       )
@@ -1512,7 +1520,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
         if not parser.openElements.hasElementInScope(TAG_TABLE):
           parse_error
         else:
-          while parser.openElements.pop().tagType != TAG_TABLE: discard
+          while parser.popElement().tagType != TAG_TABLE: discard
           parser.resetInsertionMode()
       )
       ("</body>", "</caption>", "</col>", "</colgroup>", "</html>", "</tbody>",
@@ -1587,7 +1595,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
         else:
           parser.generateImpliedEndTags()
           if parser.currentNode.tagType != TAG_CAPTION: parse_error
-          while parser.openElements.pop().tagType != TAG_CAPTION: discard
+          while parser.popElement().tagType != TAG_CAPTION: discard
           parser.clearActiveFormattingTillMarker()
           parser.insertionMode = IN_TABLE
       )
@@ -1728,7 +1736,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
     template close_cell() =
       parser.generateImpliedEndTags()
       if parser.currentNode.tagType notin {TAG_TD, TAG_TH}: parse_error
-      while parser.openElements.pop().tagType notin {TAG_TD, TAG_TH}: discard
+      while parser.popElement().tagType notin {TAG_TD, TAG_TH}: discard
       parser.clearActiveFormattingTillMarker()
       parser.insertionMode = IN_ROW
 
@@ -1739,7 +1747,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
         else:
           parser.generateImpliedEndTags()
           if parser.currentNode.tagType != token.tagtype: parse_error
-          while parser.openElements.pop().tagType != token.tagtype: discard
+          while parser.popElement().tagType != token.tagtype: discard
           parser.clearActiveFormattingTillMarker()
           parser.insertionMode = IN_ROW
       )
@@ -1799,13 +1807,13 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
         if not parser.openElements.hasElementInSelectScope(TAG_SELECT):
           parse_error
         else:
-          while parser.openElements.pop().tagType != TAG_SELECT: discard
+          while parser.popElement().tagType != TAG_SELECT: discard
           parser.resetInsertionMode()
       )
       "<select>" => (block:
         parse_error
         if parser.openElements.hasElementInSelectScope(TAG_SELECT):
-          while parser.openElements.pop().tagType != TAG_SELECT: discard
+          while parser.popElement().tagType != TAG_SELECT: discard
           parser.resetInsertionMode()
       )
       ("<input>", "<keygen>", "<textarea>") => (block:
@@ -1813,7 +1821,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
         if not parser.openElements.hasElementInSelectScope(TAG_SELECT):
           discard
         else:
-          while parser.openElements.pop().tagType != TAG_SELECT: discard
+          while parser.popElement().tagType != TAG_SELECT: discard
           parser.resetInsertionMode()
           reprocess token
       )
@@ -1826,7 +1834,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
       ("<caption>", "<table>", "<tbody>", "<tfoot>", "<thead>", "<tr>", "<td>",
        "<th>") => (block:
         parse_error
-        while parser.openElements.pop().tagType != TAG_SELECT: discard
+        while parser.popElement().tagType != TAG_SELECT: discard
         parser.resetInsertionMode()
         reprocess token
       )
@@ -1836,7 +1844,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
         if not parser.openElements.hasElementInTableScope(token.tagtype):
           discard
         else:
-          while parser.openElements.pop().tagType != TAG_SELECT: discard
+          while parser.popElement().tagType != TAG_SELECT: discard
           parser.resetInsertionMode()
           reprocess token
       )
@@ -1887,7 +1895,7 @@ proc processInHTMLContent(parser: var HTML5Parser, token: Token, insertionMode =
           discard # stop
         else:
           parse_error
-          while parser.openElements.pop().tagType != TAG_TEMPLATE: discard
+          while parser.popElement().tagType != TAG_TEMPLATE: discard
           parser.clearActiveFormattingTillMarker()
           discard parser.templateModes.pop()
           parser.resetInsertionMode()
@@ -1982,7 +1990,7 @@ proc processInForeignContent(parser: var HTML5Parser, token: Token) =
     for i in countdown(parser.openElements.high, 1):
       let node = parser.openElements[i]
       if node.localName == token.tagname:
-        while parser.openElements.pop() != node: discard
+        while parser.popElement() != node: discard
         break
       if node.namespace == Namespace.HTML: break
       parser.processInHTMLContent(token)
diff --git a/src/html/tags.nim b/src/html/tags.nim
index fbec9164..ff6a3a30 100644
--- a/src/html/tags.nim
+++ b/src/html/tags.nim
@@ -118,7 +118,7 @@ const FormAssociatedElements* = {
 
 #TODO support all the other ones
 const SupportedFormAssociatedElements* = {
-  TAG_SELECT, TAG_INPUT, TAG_BUTTON
+  TAG_BUTTON, TAG_INPUT, TAG_SELECT, TAG_TEXTAREA
 }
 
 const ListedElements* = {
diff --git a/src/io/lineedit.nim b/src/io/lineedit.nim
index 6673676d..2cd76fff 100644
--- a/src/io/lineedit.nim
+++ b/src/io/lineedit.nim
@@ -5,7 +5,7 @@ import sugar
 
 import bindings/quickjs
 import buffer/cell
-import io/term
+import display/term
 import js/javascript
 import types/color
 import utils/twtstr
diff --git a/src/ips/editor.nim b/src/ips/editor.nim
new file mode 100644
index 00000000..3bce0fa9
--- /dev/null
+++ b/src/ips/editor.nim
@@ -0,0 +1,59 @@
+import os
+
+import config/config
+import display/term
+
+func formatEditorName(editor, file: string, line: int): string =
+  result = newStringOfCap(editor.len + file.len)
+  var i = 0
+  var filefound = false
+  while i < editor.len:
+    if editor[i] == '%' and i < editor.high:
+      if editor[i + 1] == 's':
+        result &= file
+        filefound = true
+        i += 2
+        continue
+      elif editor[i + 1] == 'd':
+        result &= $line
+        i += 2
+        continue
+      elif editor[i + 1] == '%':
+        result &= '%'
+        i += 2
+        continue
+    result &= editor[i]
+    inc i
+  if editor.len == 0:
+    result = "vi"
+  if not filefound:
+    if result[^1] != ' ':
+      result &= ' '
+    result &= file
+
+proc openEditor*(term: Terminal, config: Config, file: string, line = 0): bool =
+  var editor = config.editor
+  if editor == "":
+    editor = getEnv("EDITOR")
+    if editor == "":
+      editor = "vi %s +%d"
+  let cmd = formatEditorName(editor, file, line)
+  term.quit()
+  result = execShellCmd(cmd) == 0
+  term.restart()
+
+var tmpf_seq: int
+proc openInEditor*(term: Terminal, config: Config, input: var string): bool =
+  try:
+    let tmpdir = config.tmpdir
+    if not dirExists(tmpdir):
+      createDir(tmpdir)
+    let tmpf = tmpdir / "chatmp" & $tmpf_seq
+    inc tmpf_seq
+    writeFile(tmpf, input)
+    if openEditor(term, config, tmpf):
+      input = readFile(tmpf)
+      removeFile(tmpf)
+      return true
+  except IOError:
+    discard
diff --git a/src/ips/forkserver.nim b/src/ips/forkserver.nim
index 1c29b120..6e95fc8e 100644
--- a/src/ips/forkserver.nim
+++ b/src/ips/forkserver.nim
@@ -4,16 +4,18 @@ when defined(posix):
   import posix
 
 import buffer/buffer
-import config/bufferconfig
+import config/config
 import io/loader
 import io/request
 import io/window
 import ips/serialize
+import ips/serversocket
 import types/buffersource
+import utils/twtstr
 
 type
   ForkCommand* = enum
-    FORK_BUFFER, FORK_LOADER, REMOVE_CHILD
+    FORK_BUFFER, FORK_LOADER, REMOVE_CHILD, LOAD_CONFIG
 
   ForkServer* = ref object
     process*: Pid
@@ -31,6 +33,11 @@ proc newFileLoader*(forkserver: ForkServer, defaultHeaders: HeaderList = Default
   forkserver.ostream.flush()
   forkserver.istream.sread(result)
 
+proc loadForkServerConfig*(forkserver: ForkServer, config: Config) =
+  forkserver.ostream.swrite(LOAD_CONFIG)
+  forkserver.ostream.swrite(config.getForkServerConfig())
+  forkserver.ostream.flush()
+
 proc removeChild*(forkserver: Forkserver, pid: Pid) =
   forkserver.ostream.swrite(REMOVE_CHILD)
   forkserver.ostream.swrite(pid)
@@ -103,6 +110,11 @@ proc runForkServer() =
         let loader = ctx.forkLoader(defaultHeaders)
         ctx.ostream.swrite(loader)
         ctx.children.add((loader.process, Pid(-1)))
+      of LOAD_CONFIG:
+        var config: ForkServerConfig
+        ctx.istream.sread(config)
+        width_table = makewidthtable(config.ambiguous_double)
+        SocketDirectory = config.tmpdir
       ctx.ostream.flush()
     except IOError:
       # EOF
diff --git a/src/ips/serialize.nim b/src/ips/serialize.nim
index 2647db2f..6eeaaca9 100644
--- a/src/ips/serialize.nim
+++ b/src/ips/serialize.nim
@@ -5,7 +5,6 @@ import streams
 import tables
 
 import buffer/cell
-import config/bufferconfig
 import io/request
 import js/regex
 import types/buffersource
@@ -92,7 +91,7 @@ proc slen*[T](o: T): int =
 template swrite*[T](stream: Stream, o: T) =
   stream.write(o)
 
-proc swrite*(stream: Stream, s: string, maxlen = 8192) =
+proc swrite*(stream: Stream, s: string) =
   stream.swrite(s.len)
   stream.write(s)
 
@@ -182,9 +181,6 @@ proc swrite*(stream: Stream, source: BufferSource) =
   stream.swrite(source.location)
   stream.swrite(source.contenttype)
 
-proc swrite*(stream: Stream, bconfig: BufferConfig) =
-  stream.swrite(bconfig.userstyle)
-
 proc swrite*(stream: Stream, tup: tuple) =
   for f in tup.fields:
     stream.swrite(f)
@@ -196,11 +192,11 @@ proc swrite*(stream: Stream, obj: object) =
 template sread*[T](stream: Stream, o: T) =
   stream.read(o)
 
-proc sread*(stream: Stream, s: var string, maxlen = 8192) =
+proc sread*(stream: Stream, s: var string) =
   var len: int
   stream.sread(len)
-  if maxlen != -1:
-    len = min(maxlen, len)
+  #if maxlen != -1:
+  #  len = min(maxlen, len)
   stream.readStr(len, s)
 
 proc sread*(stream: Stream, b: var bool) =
@@ -214,7 +210,7 @@ proc sread*(stream: Stream, b: var bool) =
 
 proc sread*(stream: Stream, url: var Url) =
   var s: string
-  stream.sread(s, 2048)
+  stream.sread(s)
   url = newURL(s)
 
 proc sread*(stream: Stream, headers: var HeaderList) =
@@ -323,9 +319,6 @@ proc sread*(stream: Stream, source: var BufferSource) =
   stream.sread(source.location)
   stream.sread(source.contenttype)
 
-proc sread*(stream: Stream, bconfig: var BufferConfig) =
-  stream.sread(bconfig.userstyle)
-
 proc sread*(stream: Stream, obj: var object) =
   for f in obj.fields:
     stream.sread(f)
diff --git a/src/ips/serversocket.nim b/src/ips/serversocket.nim
index 8ed79ab3..b476320e 100644
--- a/src/ips/serversocket.nim
+++ b/src/ips/serversocket.nim
@@ -7,10 +7,10 @@ type ServerSocket* = object
   sock*: Socket
   path*: string
 
-const SocketDirectory = "/tmp/cha/"
-const SocketPathPrefix = SocketDirectory & "cha_sock_"
-func getSocketPath*(pid: Pid): string =
-  SocketPathPrefix & $pid
+var SocketDirectory* = "/tmp/cha"
+const SocketPathPrefix = "cha_sock_"
+proc getSocketPath*(pid: Pid): string =
+  SocketDirectory / SocketPathPrefix & $pid
 
 proc initServerSocket*(buffered = true): ServerSocket =
   createDir(SocketDirectory)
diff --git a/src/ips/socketstream.nim b/src/ips/socketstream.nim
index df4bbf09..ff2d189e 100644
--- a/src/ips/socketstream.nim
+++ b/src/ips/socketstream.nim
@@ -18,7 +18,7 @@ proc sockReadData(s: Stream, buffer: pointer, len: int): int =
   result = s.source.recv(buffer, len)
   if result < 0:
     raise newException(IOError, "Failed to read data (code " & $osLastError() & ")")
-  elif result < len:
+  elif result == 0:
     s.isend = true
 
 proc sockWriteData(s: Stream, buffer: pointer, len: int) =
diff --git a/src/main.nim b/src/main.nim
index ad978fa6..fdec3081 100644
--- a/src/main.nim
+++ b/src/main.nim
@@ -1,13 +1,4 @@
-import config/config
 import types/dispatcher
-import utils/twtstr
-
-# Inherited memory
-var conf = readConfig()
-width_table = makewidthtable(conf.ambiguous_double)
-# We don't actually want to inherit the entire config, so zero it out here.
-zeroMem(addr conf[], sizeof(conf[]))
-
 let disp = newDispatcher()
 
 import options
@@ -17,9 +8,14 @@ import terminal
 when defined(profile):
   import nimprof
 
+import config/config
 import display/client
+import ips/forkserver
+import utils/twtstr
 
-conf = readConfig()
+let conf = readConfig()
+widthtable = makewidthtable(conf.ambiguous_double)
+disp.forkserver.loadForkServerConfig(conf)
 let params = commandLineParams()
 
 proc version(long: static bool = false): string =
@@ -34,14 +30,14 @@ proc help(i: int) =
 
 Usage: cha [options] [URL(s) or file(s)...]
 Options:
+    --                          Interpret all following arguments as URLs
     -d, --dump                  Print page to stdout
     -c, --css <stylesheet>      Pass stylesheet (e.g. -c 'a{color: blue}')
     -o, --opt <config>          Pass config options (e.g. -o 'page.q="QUIT"')
     -T, --type <type>           Specify content mime type
-    -v, --version               Print version information
-    -h, --help                  Print this page
     -r, --run <script/file>     Run passed script or file
-    --                          Interpret all following arguments as URLs"""
+    -h, --help                  Print this usage message
+    -v, --version               Print version information"""
   if i == 0:
     echo s
   else:
@@ -71,7 +67,7 @@ while i < params.len:
       help(1)
   of "-":
     discard # emulate programs that accept - as stdin
-  of "-d", "--dump":
+  of "-d", "-dump", "--dump":
     dump = true
   of "-c", "--css":
     inc i