about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2021-01-23 14:20:41 +0100
committerbptato <nincsnevem662@gmail.com>2021-01-23 14:20:41 +0100
commited3886ab51a76f184d023d79e085f928427b8328 (patch)
tree1e95754cb83677fa278be4206f4876e3af835a84
parent94d681b3935a3f9105dc60320230fa9657cbd7b5 (diff)
downloadchawan-ed3886ab51a76f184d023d79e085f928427b8328.tar.gz
unicode kinda broken otherwise better than ever
-rw-r--r--buffer.nim23
-rw-r--r--config58
-rw-r--r--config.nim7
-rw-r--r--display.nim149
-rw-r--r--htmlelement.nim59
-rw-r--r--main.nim2
-rw-r--r--readme.md4
-rw-r--r--twtstr.nim35
8 files changed, 245 insertions, 92 deletions
diff --git a/buffer.nim b/buffer.nim
index 788f8aae..f7cac95b 100644
--- a/buffer.nim
+++ b/buffer.nim
@@ -98,8 +98,8 @@ func onSpace*(buffer: Buffer): bool =
   return buffer.text.len > 0 and buffer.text[^1] == ' '
 
 func cursorOnNode*(buffer: Buffer, node: HtmlNode): bool =
-    return buffer.cursorY >= node.y and buffer.cursorY < node.y + node.height and
-           buffer.cursorX >= node.x and buffer.cursorX <= node.x + node.width
+  return buffer.rawlines[buffer.cursorY - 1] + buffer.cursorX >= node.rawchar and
+         buffer.rawlines[buffer.cursorY - 1] + buffer.cursorX < node.rawend
 
 func findSelectedElement*(buffer: Buffer): Option[HtmlElement] =
   if buffer.selectedlink != nil and buffer.selectedLink.parentNode of HtmlElement:
@@ -200,6 +200,7 @@ proc clearBuffer*(buffer: Buffer) =
   buffer.fromX = 0
   buffer.fromY = 0
   buffer.hovertext = ""
+  buffer.selectedlink = nil
 
 proc scrollTo*(buffer: Buffer, y: int): bool =
   if y == buffer.fromY:
@@ -209,14 +210,16 @@ proc scrollTo*(buffer: Buffer, y: int): bool =
   return true
 
 proc cursorTo*(buffer: Buffer, x: int, y: int): bool =
+  result = false
   buffer.cursorY = min(max(y, 0), buffer.lastLine())
   if buffer.fromY > buffer.cursorY:
-    buffer.fromY = min(buffer.cursorY, buffer.lastLine() - buffer.height)
-  elif buffer.fromY + buffer.height < buffer.cursorY:
-    buffer.fromY = max(buffer.cursorY - buffer.height, 0)
+    buffer.fromY = min(buffer.cursorY - 1, buffer.lastLine() - buffer.height)
+    result = true
+  elif buffer.fromY + buffer.height <= buffer.cursorY:
+    buffer.fromY = max(buffer.cursorY - buffer.height + 1, 0)
+    result = true
   buffer.cursorX = min(max(x, 0), buffer.currentRawLineLength())
   buffer.fromX = min(max(buffer.currentRawLineLength() - buffer.width + 1, 0), 0) #TODO
-  return true
 
 proc cursorDown*(buffer: Buffer): bool =
   if buffer.cursorY < buffer.lastLine():
@@ -409,6 +412,12 @@ proc cursorBottom*(buffer: Buffer): bool =
   buffer.cursorY = min(buffer.fromY + buffer.height - 1, buffer.lastLine())
   return false
 
+proc centerLine*(buffer: Buffer): bool =
+  if min(buffer.cursorY - buffer.height div 2, buffer.lastLine()) == buffer.fromY:
+    return false
+  buffer.fromY = min(buffer.cursorY - buffer.height div 2, buffer.lastLine())
+  return true
+
 proc scrollDown*(buffer: Buffer): bool =
   if buffer.fromY + buffer.height <= buffer.lastLine():
     buffer.fromY += 1
@@ -434,6 +443,7 @@ proc checkLinkSelection*(buffer: Buffer): bool =
     else:
       let anchor = buffer.selectedlink.ancestor(TAG_A)
       anchor.selected = false
+      buffer.selectedlink.fmttext = buffer.selectedlink.getFmtText()
       buffer.selectedlink = nil
       buffer.hovertext = ""
   for node in buffer.links:
@@ -443,6 +453,7 @@ proc checkLinkSelection*(buffer: Buffer): bool =
       assert(anchor != nil)
       anchor.selected = true
       buffer.hovertext = HtmlAnchorElement(anchor).href
+      node.fmttext = node.getFmtText()
       return true
   return false
 
diff --git a/config b/config
new file mode 100644
index 00000000..48e81660
--- /dev/null
+++ b/config
@@ -0,0 +1,58 @@
+#normal mode keybindings
+nmap q ACTION_QUIT
+nmap h ACTION_CURSOR_LEFT
+nmap j ACTION_CURSOR_DOWN
+nmap k ACTION_CURSOR_UP
+nmap l ACTION_CURSOR_RIGHT
+nmap \e[D ACTION_CURSOR_LEFT
+nmap \e[B ACTION_CURSOR_DOWN
+nmap \e[A ACTION_CURSOR_UP
+nmap \e[C ACTION_CURSOR_RIGHT
+nmap ^ ACTION_CURSOR_LINEBEGIN
+nmap $ ACTION_CURSOR_LINEEND
+nmap b ACTION_CURSOR_PREV_WORD
+nmap B ACTION_CURSOR_PREV_NODE
+nmap w ACTION_CURSOR_NEXT_WORD
+nmap W ACTION_CURSOR_NEXT_NODE
+nmap [ ACTION_CURSOR_PREV_LINK
+nmap ] ACTION_CURSOR_NEXT_LINK
+nmap H ACTION_CURSOR_TOP
+nmap M ACTION_CURSOR_MIDDLE
+nmap L ACTION_CURSOR_BOTTOM
+nmap C-d ACTION_HALF_PAGE_DOWN
+nmap C-u ACTION_HALF_PAGE_UP
+nmap C-f ACTION_PAGE_DOWN
+nmap C-b ACTION_PAGE_UP
+nmap \e[6~ ACTION_PAGE_DOWN
+nmap \e[5~ ACTION_PAGE_UP
+nmap C-e ACTION_SCROLL_DOWN
+nmap C-y ACTION_SCROLL_UP
+nmap J ACTION_SCROLL_DOWN
+nmap K ACTION_SCROLL_UP
+nmap C-m ACTION_CLICK
+nmap C-j ACTION_CLICK
+nmap C-l ACTION_CHANGE_LOCATION
+nmap U ACTION_RELOAD
+nmap r ACTION_RESHAPE
+nmap R ACTION_REDRAW
+nmap gg ACTION_CURSOR_FIRST_LINE
+nmap G ACTION_CURSOR_LAST_LINE
+nmap \e[H ACTION_CURSOR_FIRST_LINE
+nmap \e[F ACTION_CURSOR_LAST_LINE
+nmap z ACTION_CENTER_LINE
+nmap C-g ACTION_LINE_INFO
+
+#line editing keybindings
+lemap C-m ACTION_LINED_SUBMIT
+lemap C-j ACTION_LINED_SUBMIT
+lemap C-h ACTION_LINED_BACKSPACE
+lemap C-? ACTION_LINED_BACKSPACE
+lemap C-c ACTION_LINED_CANCEL
+lemap \eb ACTION_LINED_PREV_WORD
+lemap \ef ACTION_LINED_NEXT_WORD
+lemap C-b ACTION_LINED_BACK
+lemap C-f ACTION_LINED_FORWARD
+lemap C-u ACTION_LINED_CLEAR
+lemap C-k ACTION_LINED_KILL
+lemap C-w ACTION_LINED_KILL_WORD
+lemap C-v ACTION_LINED_ESC
diff --git a/config.nim b/config.nim
index f13d172b..8df30caf 100644
--- a/config.nim
+++ b/config.nim
@@ -23,6 +23,7 @@ type
     ACTION_RELOAD, ACTION_RESHAPE, ACTION_REDRAW,
     ACTION_CURSOR_FIRST_LINE, ACTION_CURSOR_LAST_LINE,
     ACTION_CURSOR_TOP, ACTION_CURSOR_MIDDLE, ACTION_CURSOR_BOTTOM,
+    ACTION_CENTER_LINE, ACTION_LINE_INFO,
     ACTION_LINED_SUBMIT, ACTION_LINED_CANCEL,
     ACTION_LINED_BACKSPACE, ACTION_LINED_CLEAR, ACTION_LINED_KILL, ACTION_LINED_KILL_WORD,
     ACTION_LINED_BACK, ACTION_LINED_FORWARD,
@@ -84,10 +85,10 @@ proc constructActionTable*(origTable: var Table[string, TwtAction]): Table[strin
   return newTable
 
 macro staticReadKeymap(): untyped =
-  var keymap = staticRead"keymap"
+  var config = staticRead"config"
   var normalActionMap: Table[string, TwtAction]
   var linedActionMap: Table[string, TwtAction]
-  for line in keymap.split('\n'):
+  for line in config.split('\n'):
     if line.len == 0 or line[0] == '#':
       continue
     let cmd = line.split(' ')
@@ -126,7 +127,7 @@ macro staticReadKeymap(): untyped =
 
 staticReadKeymap()
 
-proc readKeymap*(filename: string): bool =
+proc readConfig*(filename: string): bool =
   var f: File
   let status = f.open(filename, fmRead)
   var normalActionMap: Table[string, TwtAction]
diff --git a/display.nim b/display.nim
index 19876ab5..ec4552de 100644
--- a/display.nim
+++ b/display.nim
@@ -29,7 +29,7 @@ type
     lastwidth: int
     atchar: int
     atrawchar: int
-    centerqueue: int
+    centerqueue: seq[HtmlNode]
     centerlen: int
     blanklines: int
     blankspaces: int
@@ -71,33 +71,49 @@ proc writeWrappedText(buffer: Buffer, state: var RenderState, node: HtmlNode) =
   var fmtword = ""
   var rawword = ""
   var prevl = false
-  for r in node.rawtext.runes:
-    rawword &= r
-    state.x += 1
+  for w in node.fmttext:
+    if w.len > 0 and w[0] == '\e':
+      fmtword &= w
+      continue
+
+    for r in w.runes:
+      fmtword &= r
+      rawword &= r
+
+      state.x += 1
 
-    if state.x > buffer.width:
-      state.lastwidth = max(state.lastwidth, state.x)
-      buffer.flushLine(state)
-      prevl = true
-    else:
-      state.lastwidth = max(state.lastwidth, state.x)
-
-    if r == runeSpace:
-      eprint "x at", rawword, "is", state.x, "."
-      buffer.writefmt(fmtword)
-      buffer.writeraw(rawword)
-      state.atchar += fmtword.len
-      state.atrawchar += rawword.len
       if prevl:
         state.x += rawword.runeLen
         prevl = false
-      fmtword = ""
-      rawword = ""
+
+      if r == runeSpace:
+        buffer.writefmt(fmtword)
+        buffer.writeraw(rawword)
+        state.atchar += fmtword.len
+        state.atrawchar += rawword.runeLen()
+        fmtword = ""
+        rawword = ""
+
+      if state.x > buffer.width:
+        if buffer.rawtext.len > 0 and buffer.rawtext[^1] == ' ':
+          buffer.rawtext = buffer.rawtext.substr(0, buffer.rawtext.len - 2)
+          buffer.text = buffer.text.substr(0, buffer.text.len - 2)
+          state.atchar -= 1
+          state.atrawchar -= 1
+          state.x -= 1
+        state.lastwidth = max(state.lastwidth, state.x)
+        buffer.flushLine(state)
+        state.x = -1
+        prevl = true
+      else:
+        state.lastwidth = max(state.lastwidth, state.x)
+
+      n += 1
 
   buffer.writefmt(fmtword)
   buffer.writeraw(rawword)
   state.atchar += fmtword.len
-  state.atrawchar += rawword.len
+  state.atrawchar += rawword.runeLen()
   state.lastwidth = max(state.lastwidth, state.x)
 
 proc preAlignNode(buffer: Buffer, node: HtmlNode, state: var RenderState) =
@@ -108,8 +124,7 @@ proc preAlignNode(buffer: Buffer, node: HtmlNode, state: var RenderState) =
   if node.openblock:
     while state.blanklines < max(elem.margin, elem.margintop):
       buffer.flushLine(state)
-    if elem.display == DISPLAY_LIST_ITEM:
-      state.indent += 1
+    state.indent += elem.indent
 
   if not buffer.onNewLine() and state.blanklines == 0 and node.displayed():
     buffer.addSpaces(state, state.nextspaces)
@@ -121,11 +136,12 @@ proc preAlignNode(buffer: Buffer, node: HtmlNode, state: var RenderState) =
     buffer.addSpaces(state, max(buffer.width div 2 - state.centerlen div 2, 0))
     state.centerlen = 0
   
-  if elem.display == DISPLAY_LIST_ITEM and state.indent > 0:
+  if node.isElemNode() and elem.display == DISPLAY_LIST_ITEM and state.indent > 0:
+    buffer.flushLine(state)
     var listchar = ""
     case elem.parentElement.tagType
     of TAG_UL:
-      listchar = "*"
+      listchar = "•"
     of TAG_OL:
       state.listval += 1
       listchar = $state.listval & ")"
@@ -133,9 +149,9 @@ proc preAlignNode(buffer: Buffer, node: HtmlNode, state: var RenderState) =
       return
     buffer.addSpaces(state, state.indent)
     buffer.write(listchar)
-    state.x += 1
-    state.atchar += 1
-    state.atrawchar += 1
+    state.x += listchar.runeLen()
+    state.atchar += listchar.len
+    state.atrawchar += listchar.runeLen()
     buffer.addSpaces(state, 1)
 
 proc postAlignNode(buffer: Buffer, node: HtmlNode, state: var RenderState) =
@@ -147,56 +163,61 @@ proc postAlignNode(buffer: Buffer, node: HtmlNode, state: var RenderState) =
 
   if not buffer.onNewLine() and state.blanklines == 0:
     state.nextspaces += max(elem.margin, elem.marginright)
-    if node.closeblock:
+    if node.closeblock and (node.isTextNode() or elem.numChildNodes == 0):
       buffer.flushLine(state)
 
   if node.closeblock:
     while state.blanklines < max(elem.margin, elem.marginbottom):
       buffer.flushLine(state)
-    if elem.display == DISPLAY_LIST_ITEM and node.isTextNode():
-      state.indent -= 1
+    state.indent -= elem.indent
 
   if elem.tagType == TAG_BR and not node.openblock:
     buffer.flushLine(state)
 
-  if elem.display == DISPLAY_LIST_ITEM and node.isElemNode():
-    buffer.flushLine(state)
-
 proc renderNode(buffer: Buffer, node: HtmlNode, state: var RenderState) =
   let elem = node.nodeAttr()
   if elem.tagType == TAG_TITLE:
     if node.isTextNode():
-      buffer.title = $node.rawtext
+      buffer.title = node.rawtext
     return
   else: discard
   if elem.hidden: return
 
   if not state.docenter:
     if elem.centered:
-      if not node.closeblock and elem.tagType != TAG_BR:
-        state.centerqueue += 1
+      state.centerqueue.add(node)
+      if node.closeblock or elem.tagType == TAG_BR:
+        state.docenter = true
+        state.centerlen = 0
+        for node in state.centerqueue:
+          state.centerlen += node.getRawLen()
+        for node in state.centerqueue:
+          buffer.renderNode(node, state)
+        state.centerqueue.setLen(0)
+        state.docenter = false
         return
-    if state.centerqueue > 0:
+      else:
+        return
+    if state.centerqueue.len > 0:
       state.docenter = true
       state.centerlen = 0
-      var i = state.centerqueue
-      while i > 0:
-        state.centerlen += buffer.nodes[^i].getRawLen()
-        i -= 1
-      while state.centerqueue > 0:
-        buffer.renderNode(buffer.nodes[^state.centerqueue], state)
-        state.centerqueue -= 1
+      for node in state.centerqueue:
+        state.centerlen += node.getRawLen()
+      for node in state.centerqueue:
+        buffer.renderNode(node, state)
+      state.centerqueue.setLen(0)
       state.docenter = false
 
   buffer.preAlignNode(node, state)
 
   node.x = state.x
   node.y = state.y
+  node.fmtchar = state.atchar
+  node.rawchar = state.atrawchar
   buffer.writeWrappedText(state, node)
-  #if state.x != node.x:
-  #  eprint node.x, node.y, state.x, state.y, node.nodeAttr().tagType
-  #  eprint "len", state.atrawchar
-  node.width = state.lastwidth - node.x
+  node.fmtend = state.atchar
+  node.rawend = state.atrawchar
+  node.width = state.lastwidth - node.x - 1
   node.height = state.y - node.y + 1
 
   buffer.postAlignNode(node, state)
@@ -229,8 +250,8 @@ proc renderHtml*(buffer: Buffer) =
   var state = newRenderState()
   while stack.len > 0:
     let currElem = stack.pop()
-    buffer.renderNode(currElem.html, state)
     buffer.addNode(currElem.html)
+    buffer.renderNode(currElem.html, state)
     if currElem.xml.len > 0:
       var last = false
       for item in currElem.xml.revItems:
@@ -272,7 +293,7 @@ proc displayBuffer(buffer: Buffer) =
   eraseScreen()
   termGoto(0, 0)
 
-  print(buffer.visibleText())
+  print(buffer.visibleText().ansiReset())
 
 proc inputLoop(attrs: TermAttributes, buffer: Buffer): bool =
   var s = ""
@@ -288,6 +309,7 @@ proc inputLoop(attrs: TermAttributes, buffer: Buffer): bool =
     let action = getNormalAction(s)
     var redraw = false
     var reshape = false
+    var nostatus = false
     case action
     of ACTION_QUIT:
       eraseScreen()
@@ -313,6 +335,7 @@ proc inputLoop(attrs: TermAttributes, buffer: Buffer): bool =
     of ACTION_CURSOR_TOP: redraw = buffer.cursorTop()
     of ACTION_CURSOR_MIDDLE: redraw = buffer.cursorMiddle()
     of ACTION_CURSOR_BOTTOM: redraw = buffer.cursorBottom()
+    of ACTION_CENTER_LINE: redraw = buffer.centerLine()
     of ACTION_SCROLL_DOWN: redraw = buffer.scrollDown()
     of ACTION_SCROLL_UP: redraw = buffer.scrollUp()
     of ACTION_CLICK:
@@ -332,11 +355,15 @@ proc inputLoop(attrs: TermAttributes, buffer: Buffer): bool =
           return true
     of ACTION_CHANGE_LOCATION:
       var url = $buffer.document.location
+
       clearStatusMsg(buffer.height)
       let status = readLine("URL:", url)
       if status:
         buffer.setLocation(parseUri(url))
         return true
+    of ACTION_LINE_INFO:
+      statusMsg("line " & $buffer.cursorY & "/" & $buffer.lastLine() & " col " & $buffer.cursorX & "/" & $buffer.currentLineLength(), buffer.width)
+      nostatus = true
     of ACTION_FEED_NEXT:
       feedNext = true
     of ACTION_RELOAD: return true
@@ -345,13 +372,31 @@ proc inputLoop(attrs: TermAttributes, buffer: Buffer): bool =
       redraw = true
     of ACTION_REDRAW: redraw = true
     else: discard
-    redraw = redraw or buffer.checkLinkSelection()
+
     if reshape:
       buffer.clearText()
       buffer.drawHtml()
     if redraw:
       buffer.displayBuffer()
-    buffer.statusMsgForBuffer()
+
+    let prevlink = buffer.selectedlink
+    let sel = buffer.checkLinkSelection()
+    if prevlink != nil and prevlink != buffer.selectedlink:
+      termGoto(prevlink.x - buffer.fromX, prevlink.y - buffer.fromY - 1)
+      print(buffer.text.substr(prevlink.fmtchar, prevlink.fmtend))
+    if sel:
+      termGoto(buffer.selectedlink.x - buffer.fromX, buffer.selectedlink.y - buffer.fromY - 1)
+      let str = buffer.text.substr(buffer.selectedlink.fmtchar, buffer.selectedlink.fmtend)
+      var i = str.findChar('\n') 
+      while i != -1:
+        print("".ansiStyle(styleUnderscore))
+        i = str.findChar('\n', i + 1)
+      print(str.ansiStyle(styleUnderscore).ansiReset())
+
+    if not nostatus:
+      buffer.statusMsgForBuffer()
+    else:
+      nostatus = false
 
 proc displayPage*(attrs: TermAttributes, buffer: Buffer): bool =
   #buffer.printwrite = true
diff --git a/htmlelement.nim b/htmlelement.nim
index e6dc0ca8..8a61acb7 100644
--- a/htmlelement.nim
+++ b/htmlelement.nim
@@ -25,9 +25,13 @@ type
     parentElement*: HtmlElement
 
     rawtext*: string
-    fmttext*: string
+    fmttext*: seq[string]
     x*: int
     y*: int
+    fmtchar*: int
+    rawchar*: int
+    fmtend*: int
+    rawend*: int
     width*: int
     height*: int
     openblock*: bool
@@ -56,6 +60,8 @@ type
     underscore*: bool
     islink*: bool
     selected*: bool
+    numChildNodes*: int
+    indent*: int
 
   HtmlInputElement* = ref HtmlInputElementObj
   HtmlInputElementObj = object of HtmlElementObj
@@ -125,10 +131,10 @@ func isDocument*(node: HtmlNode): bool =
   return node.nodeType == NODE_DOCUMENT
 
 func getFmtLen*(htmlNode: HtmlNode): int =
-  return htmlNode.fmttext.len
+  return htmlNode.fmttext.join().runeLen()
 
 func getRawLen*(htmlNode: HtmlNode): int =
-  return htmlNode.rawtext.len
+  return htmlNode.rawtext.runeLen()
 
 func toInputType*(str: string): InputType =
   case str
@@ -164,7 +170,7 @@ func toInputSize*(str: string): int =
       return 20
   return str.parseInt()
 
-func getFmtInput(inputElement: HtmlInputElement): string =
+func getFmtInput(inputElement: HtmlInputElement): seq[string] =
   case inputElement.itype
   of INPUT_TEXT, INPUT_SEARCH:
     let valueFit = fitValueToSize(inputElement.value, inputElement.size)
@@ -194,7 +200,7 @@ proc getRawText*(htmlNode: HtmlNode): string =
   elif htmlNode.isTextNode():
     if htmlNode.parentElement != nil and htmlNode.parentElement.tagType != TAG_PRE:
       result = htmlNode.rawtext.remove("\n")
-      if unicode.strip($result).toRunes().len > 0:
+      if unicode.strip(result).runeLen() > 0:
         if htmlNode.nodeAttr().display != DISPLAY_INLINE:
           if htmlNode.previousSibling == nil or htmlNode.previousSibling.nodeAttr().display != DISPLAY_INLINE:
             result = unicode.strip(result, true, false)
@@ -209,28 +215,31 @@ proc getRawText*(htmlNode: HtmlNode): string =
   else:
     assert(false)
 
-func getFmtText*(htmlNode: HtmlNode): string =
+func getFmtText*(htmlNode: HtmlNode): seq[string] =
   if htmlNode.isElemNode():
     case HtmlElement(htmlNode).tagType
     of TAG_INPUT: return HtmlInputElement(htmlNode).getFmtInput()
-    else: return ""
+    else: return @[]
   elif htmlNode.isTextNode():
-    result = htmlNode.rawtext
-    if htmlNode.parentElement != nil and htmlNode.parentElement.islink:
-      result = result.ansiFgColor(fgBlue)
-      let parent = HtmlElement(htmlNode.parentNode).ancestor(TAG_A)
-      if parent != nil and parent.selected:
+    result &= htmlNode.rawtext
+    if htmlNode.parentElement != nil:
+      if htmlNode.parentElement.islink:
+        result = result.ansiFgColor(fgBlue).ansiReset()
+        let anchor = htmlNode.ancestor(TAG_A)
+        if anchor != nil and anchor.selected:
+          result = result.ansiStyle(styleUnderscore).ansiReset()
+
+      if htmlNode.parentElement.tagType == TAG_OPTION:
+        result = result.ansiFgColor(fgRed).ansiReset()
+
+      if htmlNode.parentElement.bold:
+        result = result.ansiStyle(styleBright).ansiReset()
+      if htmlNode.parentElement.italic:
+        result = result.ansiStyle(styleItalic).ansiReset()
+      if htmlNode.parentElement.underscore:
         result = result.ansiStyle(styleUnderscore).ansiReset()
-
-    if HtmlElement(htmlNode.parentNode).tagType == TAG_OPTION:
-      result = result.ansiFgColor(fgRed).ansiReset()
-
-    if HtmlElement(htmlNode.parentNode).bold:
-      result = result.ansiStyle(styleBright).ansiReset()
-    if HtmlElement(htmlNode.parentNode).italic:
-      result = result.ansiStyle(styleItalic).ansiReset()
-    if HtmlElement(htmlNode.parentNode).underscore:
-      result = result.ansiStyle(styleUnderscore).ansiReset()
+    else:
+      assert(false, "Uhhhh I'm pretty sure we should have parent elements for text nodes?" & htmlNode.rawtext)
   else:
     assert(false)
 
@@ -306,6 +315,11 @@ proc getHtmlElement*(xmlElement: XmlNode, parentNode: HtmlNode): HtmlElement =
     result = optionElement
   of TAG_PRE, TAG_TD, TAG_TH:
     result.margin = 1
+  of TAG_UL, TAG_OL:
+    result.indent = 1
+  of TAG_H1, TAG_H2, TAG_H3, TAG_H4, TAG_H5, TAG_H6:
+    result.bold = true
+    result.marginbottom = 1
   else: discard
 
   if parentNode.isElemNode():
@@ -317,6 +331,7 @@ proc getHtmlElement*(xmlElement: XmlNode, parentNode: HtmlNode): HtmlElement =
     result.hidden = result.hidden or parent.hidden
     result.islink = result.islink or parent.islink
   
+  result.numChildNodes = xmlElement.len
 
 proc getHtmlNode*(xmlElement: XmlNode, parent: HtmlNode): HtmlNode =
   case kind(xmlElement)
diff --git a/main.nim b/main.nim
index c98f0db5..db4b0f6d 100644
--- a/main.nim
+++ b/main.nim
@@ -33,7 +33,7 @@ proc main*() =
   if paramCount() != 1:
     eprint "Invalid parameters. Usage:\ntwt <url>"
     quit(1)
-  if not readKeymap("keymap"):
+  if not readConfig("config"):
     eprint "Failed to read keymap, falling back to default"
   let attrs = getTermAttributes()
   let buffer = newBuffer(attrs)
diff --git a/readme.md b/readme.md
index fe58b84d..32c6a7dc 100644
--- a/readme.md
+++ b/readme.md
@@ -5,8 +5,8 @@ A terminal web browser. It displays websites in your terminal and allows you to
 
 ## Why make another web browser?
 I've found other terminal web browsers insufficient for my needs, so I thought it'd be a fun excercise to write one myself.  
-I don't really want a standard-compliant browser, or one that displays pages perfectly - the only way you could do that in a terminal is to work like browsh, which kinda defeats the point of a terminal web browser. I want one that is good enough for daily use - something like lynx or w3m, but better.  
-So the aim is to implement HTML rendering, some degree of JS support, and a very limited subset of CSS. Plus some other things I'd add to w3m if it weren't 50k lines of incomprehensible ancient C code.
+I don't really want a standard-compliant browser, or one that displays pages perfectly - the only way you could do that in a terminal is to work like browsh, which kinda defeats the point of a terminal web browser. I want one that is good enough for daily use, something like lynx or w3m.  
+So the aim is to implement HTML rendering, some degree of JS support, and a very limited subset of CSS. Plus some other things.
 
 ## So what can this do?
 Currently implemented features are:
diff --git a/twtstr.nim b/twtstr.nim
index 85035a61..5f762780 100644
--- a/twtstr.nim
+++ b/twtstr.nim
@@ -4,13 +4,25 @@ import unicode
 
 const runeSpace* = " ".runeAt(0)
 
-func ansiStyle*(str: string, style: Style): string =
-  return ansiStyleCode(style) & str & "\e[0m"
+func ansiStyle*(str: string, style: Style): seq[string] =
+  result &= ansiStyleCode(style)
+  result &= str
 
-func ansiFgColor*(str: string, color: ForegroundColor): string =
-  return ansiForegroundColorCode(color) & str & ansiResetCode
+func ansiFgColor*(str: string, color: ForegroundColor): seq[string] =
+  result &= ansiForegroundColorCode(color)
+  result &= str
 
-func ansiReset*(str: string): string =
+func ansiReset*(str: string): seq[string] =
+  result &= str
+  result &= ansiResetCode
+
+func ansiStyle*(str: seq[string], style: Style): seq[string] =
+  return ansiStyleCode(style) & str
+
+func ansiFgColor*(str: seq[string], color: ForegroundColor): seq[string] =
+  return ansiForegroundColorCode(color) & str
+
+func ansiReset*(str: seq[string]): seq[string] =
   return str & ansiResetCode
 
 func maxString*(str: string, max: int): string =
@@ -23,7 +35,10 @@ func fitValueToSize*(str: string, size: int): string =
     return str & ' '.repeat(size - str.runeLen)
   return str.maxString(size)
 
-func buttonFmt*(str: string): string =
+func buttonFmt*(str: string): seq[string] =
+  return "[".ansiFgColor(fgRed) & str.ansiFgColor(fgRed).ansiReset() & "]".ansiFgColor(fgRed).ansiReset()
+
+func buttonFmt*(str: seq[string]): seq[string] =
   return "[".ansiFgColor(fgRed) & str.ansiFgColor(fgRed).ansiReset() & "]".ansiFgColor(fgRed).ansiReset()
 
 func buttonRaw*(str: string): string =
@@ -51,3 +66,11 @@ func getControlLetter*(c: char): char =
   elif c == '\x7F':
     return '?'
   assert(false)
+
+func findChar*(str: string, c: char, start: int = 0): int =
+  var i = start
+  while i < str.len:
+    if str[i] == c:
+      return i
+    i += 1
+  return -1