about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2021-01-20 13:26:00 +0100
committerbptato <nincsnevem662@gmail.com>2021-01-20 13:26:00 +0100
commit5268aada71dd554dca29c5bc273702d39d14bb5c (patch)
tree18b4a41ffaf464a8c0df0222a248f29bc570e369
downloadchawan-5268aada71dd554dca29c5bc273702d39d14bb5c.tar.gz
Some things work, most things don't
-rw-r--r--Makefile3
-rw-r--r--buffer.nim444
-rw-r--r--config.nim138
-rw-r--r--display.nim365
-rw-r--r--htmlelement.nim340
-rw-r--r--keymap53
-rw-r--r--main.nim59
-rw-r--r--readme.md29
-rw-r--r--search.html1586
-rw-r--r--termattrs.nim12
-rw-r--r--twtio.nim125
-rw-r--r--twtstr.nim28
12 files changed, 3182 insertions, 0 deletions
diff --git a/Makefile b/Makefile
new file mode 100644
index 00000000..68cb1ff5
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,3 @@
+all: build
+build:
+	nim compile -d:ssl -o:twt main.nim
diff --git a/buffer.nim b/buffer.nim
new file mode 100644
index 00000000..5f4cf227
--- /dev/null
+++ b/buffer.nim
@@ -0,0 +1,444 @@
+import options
+import uri
+import tables
+
+import fusion/htmlparser/xmltree
+import fusion/htmlparser
+
+import termattrs
+import htmlelement
+import twtio
+
+type
+  Buffer* = ref BufferObj
+  BufferObj = object
+    text*: string
+    rawText*: string
+    lines*: seq[int]
+    rawlines*: seq[int]
+    title*: string
+    hovertext*: string
+    htmlSource*: XmlNode
+    width*: int
+    height*: int
+    cursorX*: int
+    cursorY*: int
+    xend*: int
+    fromX*: int
+    fromY*: int
+    nodes*: seq[HtmlNode]
+    links*: seq[HtmlNode]
+    elements*: seq[HtmlNode]
+    idelements*: Table[string, HtmlNode]
+    selectedlink*: HtmlNode
+    location*: Uri
+    printwrite*: bool
+    attrs*: TermAttributes
+
+proc newBuffer*(attrs: TermAttributes): Buffer =
+  return Buffer(lines: @[0],
+                rawlines: @[0],
+                width: attrs.termWidth,
+                height: attrs.termHeight,
+                cursorY: 1)
+
+
+
+func lastLine*(buffer: Buffer): int =
+  assert buffer.rawlines.len == buffer.lines.len
+  return buffer.lines.len - 1
+
+func lastVisibleLine*(buffer: Buffer): int =
+  return min(buffer.fromY + buffer.height, buffer.lastLine() + 1) - 1
+
+#doesn't include newline
+func lineLength*(buffer: Buffer, line: int): int =
+  assert buffer.lines.len > line
+  let len = buffer.lines[line] - buffer.lines[line - 1] - 2
+  if len >= 0:
+    return len
+  else:
+    return 0
+
+func currentLine*(buffer: Buffer): int =
+  return buffer.cursorY - 1
+
+func rawLineLength*(buffer: Buffer, line: int): int =
+  assert buffer.rawlines.len > line
+  let len = buffer.rawlines[line] - buffer.rawlines[line - 1] - 2
+  if len >= 0:
+    return len
+  else:
+    return 0
+
+func currentLineLength*(buffer: Buffer): int =
+  return buffer.lineLength(buffer.cursorY)
+
+func currentRawLineLength*(buffer: Buffer): int =
+  return buffer.rawLineLength(buffer.cursorY)
+
+func cursorAtLineEnd*(buffer: Buffer): bool =
+  return buffer.cursorX == buffer.currentRawLineLength()
+
+func atPercentOf*(buffer: Buffer): int =
+  return (100 * buffer.cursorY) div buffer.lastLine()
+
+func visibleText*(buffer: Buffer): string = 
+  return buffer.text.substr(buffer.lines[buffer.fromY], buffer.lines[buffer.lastVisibleLine() - 1])
+
+func lastNode*(buffer: Buffer): HtmlNode =
+  return buffer.nodes[^1]
+
+func onNewLine*(buffer: Buffer): bool =
+  return buffer.text.len == 0 or buffer.text[^1] == '\n'
+
+func onSpace*(buffer: Buffer): bool =
+  return buffer.text.len > 0 and buffer.text[^1] == ' '
+
+func findSelectedElement*(buffer: Buffer): Option[HtmlElement] =
+  if buffer.selectedlink != nil:
+    return some(buffer.selectedlink.text.parent)
+  for node in buffer.nodes:
+    if node.nodeType == NODE_ELEMENT:
+      if node.element.formattedElem.len > 0:
+        if buffer.cursorY >= node.y and buffer.cursorY <= node.y + node.height and buffer.cursorX >= node.x and buffer.cursorX <= node.x + node.width: return some(node.element)
+  return none(HtmlElement)
+
+func cursorAt*(buffer: Buffer): int =
+  return buffer.rawlines[buffer.currentLine()] + buffer.cursorX
+
+func cursorChar*(buffer: Buffer): char =
+  return buffer.text[buffer.cursorAt()]
+
+func canScroll*(buffer: Buffer): bool =
+  return buffer.lastLine() > buffer.height
+
+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
+
+func getElementById*(buffer: Buffer, id: string): HtmlNode =
+  if buffer.idelements.hasKey(id):
+    return buffer.idelements[id]
+  return nil
+
+proc findSelectedNode*(buffer: Buffer): Option[HtmlNode] =
+  for node in buffer.nodes:
+    if node.getFormattedLen() > 0 and node.displayed():
+      if buffer.cursorY >= node.y and buffer.cursorY <= node.y + node.height and buffer.cursorX >= node.x and buffer.cursorX <= node.x + node.width:
+        return some(node)
+  return none(HtmlNode)
+
+proc addNode*(buffer: Buffer, htmlNode: HtmlNode) =
+  buffer.nodes.add(htmlNode)
+  if htmlNode.isTextNode() and htmlNode.text.parent.htmlTag == tagA:
+    buffer.links.add(htmlNode)
+  if htmlNode.isElemNode():
+    buffer.elements.add(htmlNode)
+    if htmlNode.element.id != "":
+      buffer.idelements[htmlNode.element.id] = htmlNode
+
+proc writefmt*(buffer: Buffer, str: string) =
+  buffer.text &= str
+  if buffer.printwrite:
+    stdout.write(str)
+
+proc writefmt*(buffer: Buffer, c: char) =
+  buffer.text &= c
+  if buffer.printwrite:
+    stdout.write(c)
+
+proc writeraw*(buffer: Buffer, str: string) =
+  buffer.rawtext &= str
+
+proc writeraw*(buffer: Buffer, c: char) =
+  buffer.rawtext &= c
+
+proc write*(buffer: Buffer, str: string) =
+  buffer.writefmt(str)
+  buffer.writeraw(str)
+
+proc write*(buffer: Buffer, c: char) =
+  buffer.writefmt(c)
+  buffer.writeraw(c)
+
+proc clearText*(buffer: Buffer) =
+  buffer.text = ""
+  buffer.rawtext = ""
+  buffer.lines = @[0]
+  buffer.rawlines = @[0]
+
+proc clearNodes*(buffer: Buffer) =
+  buffer.nodes.setLen(0)
+  buffer.links.setLen(0)
+  buffer.elements.setLen(0)
+  buffer.idelements.clear()
+
+proc clearBuffer*(buffer: Buffer) =
+  buffer.clearText()
+  buffer.clearNodes()
+  buffer.cursorX = 0
+  buffer.cursorY = 1
+  buffer.fromX = 0
+  buffer.fromY = 0
+  buffer.hovertext = ""
+
+proc cursorDown*(buffer: Buffer): bool =
+  if buffer.cursorY < buffer.lastLine():
+    buffer.cursorY += 1
+    if buffer.cursorX > buffer.currentRawLineLength():
+      if buffer.xend == 0:
+        buffer.xend = buffer.cursorX
+      buffer.cursorX = buffer.currentRawLineLength()
+    elif buffer.xend > 0:
+      buffer.cursorX = min(buffer.currentRawLineLength(), buffer.xend)
+    if buffer.cursorY > buffer.lastVisibleLine():
+      buffer.fromY += 1
+      return true
+  return false
+
+proc cursorUp*(buffer: Buffer): bool =
+  if buffer.cursorY > 1:
+    buffer.cursorY -= 1
+    if buffer.cursorX > buffer.currentRawLineLength():
+      if buffer.xend == 0:
+        buffer.xend = buffer.cursorX
+      buffer.cursorX = buffer.currentRawLineLength()
+    elif buffer.xend > 0:
+      buffer.cursorX = min(buffer.currentRawLineLength(), buffer.xend)
+    if buffer.cursorY <= buffer.fromY:
+      buffer.fromY -= 1
+      return true
+  return false
+
+proc cursorRight*(buffer: Buffer): bool =
+  if buffer.cursorX < buffer.currentRawLineLength():
+    buffer.cursorX += 1
+    buffer.xend = 0
+  else:
+    buffer.xend = buffer.cursorX
+  return false
+
+proc cursorLeft*(buffer: Buffer): bool =
+  if buffer.cursorX > 0:
+    buffer.cursorX -= 1
+  buffer.xend = 0
+  return false
+
+proc cursorLineBegin*(buffer: Buffer) =
+  buffer.cursorX = 0
+  buffer.xend = 0
+
+proc cursorLineEnd*(buffer: Buffer) =
+  buffer.cursorX = buffer.currentRawLineLength()
+  buffer.xend = buffer.cursorX
+
+proc cursorNextNode*(buffer: Buffer):  bool =
+  if buffer.cursorAtLineEnd():
+    if buffer.cursorY < buffer.lastLine():
+      let ret = buffer.cursorDown()
+      buffer.cursorLineEnd()
+      return ret
+    else:
+      buffer.cursorLineBegin()
+      return false
+
+  let selectedNode = buffer.findSelectedNode()
+  var res = buffer.cursorRight()
+  if selectedNode.isNone:
+    return res
+  while buffer.findSelectedNode().isSome and buffer.findSelectedNode().get() == selectedNode.get():
+    if buffer.cursorAtLineEnd():
+      return res
+    res = buffer.cursorRight()
+
+proc cursorNextWord*(buffer: Buffer): bool =
+  if buffer.cursorAtLineEnd():
+    if buffer.cursorY < buffer.lastLine():
+      let ret = buffer.cursorDown()
+      buffer.cursorLineBegin()
+      return ret
+    else:
+      buffer.cursorLineEnd()
+      return false
+
+  var res = buffer.cursorRight()
+  while buffer.rawtext[buffer.rawlines[buffer.currentLine()] + buffer.cursorX] != ' ':
+    if buffer.cursorAtLineEnd():
+      return res
+    res = res or buffer.cursorRight()
+
+proc cursorPrevNode*(buffer: Buffer): bool =
+  if buffer.cursorX <= 1:
+    if buffer.cursorY > 1:
+      let res = buffer.cursorUp()
+      buffer.cursorLineEnd()
+      return res
+    else:
+      buffer.cursorLineBegin()
+      return false
+
+  let selectedNode = buffer.findSelectedNode()
+  var res = buffer.cursorLeft()
+  if selectedNode.isNone:
+    return res
+  while buffer.findSelectedNode().isSome and buffer.findSelectedNode().get() == selectedNode.get():
+    if buffer.cursorX == 0:
+      return res
+    res = res or buffer.cursorLeft()
+
+proc cursorPrevWord*(buffer: Buffer): bool =
+  if buffer.cursorX <= 1:
+    if buffer.cursorY > 1:
+      let ret = buffer.cursorUp()
+      buffer.cursorLineEnd()
+      return ret
+    else:
+      buffer.cursorLineBegin()
+      return false
+
+  discard buffer.cursorLeft()
+  while buffer.rawtext[buffer.rawlines[buffer.currentLine()] + buffer.cursorX] != ' ':
+    if buffer.cursorX == 0:
+      return false
+    discard buffer.cursorLeft()
+
+iterator revNodes*(buffer: Buffer): HtmlNode {.inline.} =
+  var i = buffer.nodes.len - 1
+  while i >= 0:
+    yield buffer.nodes[i]
+    i -= 1
+
+proc cursorNextLink*(buffer: Buffer): bool =
+  var next = false
+  for node in buffer.nodes:
+    if node.nodeType == NODE_ELEMENT:
+      case node.element.htmlTag
+      of tagInput, tagA:
+        if next:
+          var res = false
+          while buffer.cursorY < node.y:
+            res = res or buffer.cursorDown()
+          buffer.cursorLineBegin()
+          while buffer.cursorX < node.x:
+            res = res or buffer.cursorRight()
+          return res
+        if buffer.cursorY >= node.y and buffer.cursorY <= node.y + node.height and buffer.cursorX >= node.x and buffer.cursorX <= node.x + node.width:
+          next = true
+      else: discard
+
+proc cursorPrevLink*(buffer: Buffer): bool =
+  var next = false
+  for node in buffer.revNodes:
+    if node.nodeType == NODE_ELEMENT and node.element.htmlTag == tagInput:
+      if next:
+        var res = false
+        while buffer.cursorY < node.y:
+          res = res or buffer.cursorDown()
+        buffer.cursorLineBegin()
+        while buffer.cursorX < node.x:
+          res = res or buffer.cursorRight()
+        return true
+      if buffer.cursorY >= node.y and buffer.cursorY <= node.y + node.height and buffer.cursorX >= node.x and buffer.cursorX <= node.x + node.width:
+        next = true
+
+proc cursorFirstLine*(buffer: Buffer): bool =
+  if buffer.fromY > 0:
+    buffer.fromY = 0
+    result = true
+  else:
+    result = false
+
+  buffer.cursorY = 1
+  buffer.cursorLineBegin()
+
+proc cursorLastLine*(buffer: Buffer): bool =
+  if buffer.fromY < buffer.lastLine() - buffer.height:
+    buffer.fromY = buffer.lastLine() - (buffer.height - 1)
+    result = true
+  else:
+    result = false
+  buffer.cursorY = buffer.lastLine()
+  buffer.cursorLineBegin()
+
+proc halfPageUp*(buffer: Buffer): bool =
+  buffer.cursorY = max(buffer.cursorY - buffer.height div 2 + 1, 1)
+  if buffer.fromY - 1 > buffer.cursorY or true:
+    buffer.fromY = max(0, buffer.fromY - buffer.height div 2 + 1)
+    return true
+  return false
+
+proc halfPageDown*(buffer: Buffer): bool =
+  buffer.cursorY = min(buffer.cursorY + buffer.height div 2 - 1, buffer.lastLine())
+  buffer.fromY = min(max(buffer.lastLine() - buffer.height + 1, 0), buffer.fromY + buffer.height div 2 - 1)
+  return true
+
+proc pageUp*(buffer: Buffer): bool =
+  buffer.cursorY = max(buffer.cursorY - buffer.height + 1, 1)
+  buffer.fromY = max(0, buffer.fromY - buffer.height)
+  return true
+
+proc pageDown*(buffer: Buffer): bool =
+  buffer.cursorY = min(buffer.cursorY + buffer.height div 2 - 1, buffer.lastLine())
+  buffer.fromY = min(max(buffer.lastLine() - buffer.height + 1, 0), buffer.fromY + buffer.height div 2)
+  return true
+
+proc cursorTop*(buffer: Buffer): bool =
+  buffer.cursorY = buffer.fromY + 1
+  return false
+
+proc cursorMiddle*(buffer: Buffer): bool =
+  buffer.cursorY = min(buffer.fromY + buffer.height div 2, buffer.lastLine())
+  return false
+
+proc cursorBottom*(buffer: Buffer): bool =
+  buffer.cursorY = min(buffer.fromY + buffer.height - 1, buffer.lastLine())
+  return false
+
+proc scrollTo*(buffer: Buffer, y: int): bool =
+  if y == buffer.fromY:
+    return false
+  buffer.fromY = min(max(buffer.lastLine() - buffer.height + 1, 0), y - buffer.height)
+  return true
+
+proc scrollDown*(buffer: Buffer): bool =
+  if buffer.fromY + buffer.height <= buffer.lastLine():
+    buffer.fromY += 1
+    if buffer.fromY >= buffer.cursorY:
+      discard buffer.cursorDown()
+    return true
+  discard buffer.cursorDown()
+  return false
+
+proc scrollUp*(buffer: Buffer): bool =
+  if buffer.fromY > 0:
+    buffer.fromY -= 1
+    if buffer.fromY + buffer.height <= buffer.cursorY:
+      discard buffer.cursorUp()
+    return true
+  discard buffer.cursorUp()
+  return false
+
+proc checkLinkSelection*(buffer: Buffer): bool =
+  if buffer.selectedlink != nil:
+    if buffer.cursorOnNode(buffer.selectedlink):
+      return false
+    else:
+      buffer.selectedlink = nil
+      buffer.hovertext = ""
+  for node in buffer.links:
+    if buffer.cursorOnNode(node):
+      buffer.selectedlink = node
+      node.text.parent.selected = true
+      buffer.hovertext = node.text.parent.href
+      return true
+  return false
+
+proc gotoAnchor*(buffer: Buffer): bool =
+  if buffer.location.anchor != "":
+    let node =  buffer.getElementById(buffer.location.anchor)
+    if node != nil:
+      return buffer.scrollTo(node.y)
+  return false
+
+proc setLocation*(buffer: Buffer, uri: Uri) =
+  buffer.location = buffer.location.combine(uri)
diff --git a/config.nim b/config.nim
new file mode 100644
index 00000000..ca136b9e
--- /dev/null
+++ b/config.nim
@@ -0,0 +1,138 @@
+import tables
+import strutils
+import macros
+
+type
+  TwtAction* =
+    enum
+    NO_ACTION,
+    ACTION_FEED_NEXT,
+    ACTION_QUIT,
+    ACTION_CURSOR_UP, ACTION_CURSOR_DOWN, ACTION_CURSOR_LEFT, ACTION_CURSOR_RIGHT,
+    ACTION_CURSOR_LINEEND, ACTION_CURSOR_LINEBEGIN,
+    ACTION_CURSOR_NEXT_WORD, ACTION_CURSOR_PREV_WORD,
+    ACTION_CURSOR_NEXT_NODE, ACTION_CURSOR_PREV_NODE,
+    ACTION_CURSOR_NEXT_LINK, ACTION_CURSOR_PREV_LINK,
+    ACTION_PAGE_DOWN, ACTION_PAGE_UP,
+    ACTION_HALF_PAGE_DOWN, ACTION_HALF_PAGE_UP,
+    ACTION_SCROLL_DOWN, ACTION_SCROLL_UP,
+    ACTION_CLICK,
+    ACTION_CHANGE_LOCATION,
+    ACTION_RELOAD, ACTION_RESHAPE, ACTION_REDRAW,
+    ACTION_CURSOR_FIRST_LINE, ACTION_CURSOR_LAST_LINE,
+    ACTION_CURSOR_TOP, ACTION_CURSOR_MIDDLE, ACTION_CURSOR_BOTTOM,
+    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,
+    ACTION_LINED_PREV_WORD, ACTION_LINED_NEXT_WORD,
+    ACTION_LINED_ESC
+
+var normalActionRemap*: Table[string, TwtAction]
+var linedActionRemap*: Table[string, TwtAction]
+
+func getControlChar(c: char): char =
+  if int(c) >= int('a'):
+    return char(int(c) - int('a') + 1)
+  elif c == '?':
+    return char(127)
+  assert(false)
+
+proc getRealKey(key: string): string =
+  var realk: string
+  var currchar: char
+  var control = 0
+  var skip = false
+  for c in key:
+    if c == '\\':
+      skip = true
+    elif skip:
+      if c == 'e':
+        realk &= '\e'
+      else:
+        realk &= c
+      skip = false
+    elif c == 'C':
+      control += 1
+      currchar = c
+    elif c == '-' and control == 1:
+      control += 1
+    elif control == 1:
+      realk &= 'C' & c
+      control = 0
+    elif control == 2:
+      realk &= getControlChar(c)
+      control = 0
+    else:
+      realk &= c
+  if control == 1:
+    realk &= 'C'
+  return realk
+
+proc constructActionTable*(origTable: var Table[string, TwtAction]): Table[string, TwtAction] =
+  var newTable: Table[string, TwtAction]
+  var strs = newSeq[string](0)
+  for k in origTable.keys:
+    let realk = getRealKey(k)
+    var teststr = ""
+    for c in realk:
+      teststr &= c
+      strs.add(teststr)
+
+  for k, v in origTable.mpairs:
+    let realk = getRealKey(k)
+    var teststr = ""
+    for c in realk:
+      teststr &= c
+      if strs.contains(teststr):
+        newTable[teststr] = ACTION_FEED_NEXT
+    newTable[realk] = v
+  return newTable
+
+var keymapStr*: string
+macro staticReadKeymap(): untyped =
+  var keymap = staticRead"keymap"
+
+  let keymapLit = newLit(keymap)
+  result = quote do:
+    keymapStr = `keymapLit`
+
+staticReadKeymap()
+
+proc readKeymap*(filename: string): bool =
+  var f: File
+  let status = f.open(filename, fmRead)
+  var normalActionMap: Table[string, TwtAction]
+  var linedActionMap: Table[string, TwtAction]
+  if status:
+    var line: TaintedString
+    while f.readLine(line):
+      if line.string.len == 0 or line.string[0] == '#':
+        continue
+      let cmd = line.split(' ')
+      if cmd.len == 3:
+        if cmd[0] == "nmap":
+          normalActionMap[getRealKey(cmd[1])] = parseEnum[TwtAction](cmd[2])
+        elif cmd[0] == "lemap":
+          linedActionMap[getRealKey(cmd[1])] = parseEnum[TwtAction](cmd[2])
+
+    normalActionRemap = constructActionTable(normalActionMap)
+    linedActionRemap = constructActionTable(linedActionMap)
+    return true
+  else:
+    return false
+
+proc parseKeymap*(keymap: string) =
+  var normalActionMap: Table[string, TwtAction]
+  var linedActionMap: Table[string, TwtAction]
+  for line in keymap.split('\n'):
+    if line.len == 0 or line[0] == '#':
+      continue
+    let cmd = line.split(' ')
+    if cmd.len == 3:
+      if cmd[0] == "nmap":
+        normalActionMap[getRealKey(cmd[1])] = parseEnum[TwtAction](cmd[2])
+      elif cmd[0] == "lemap":
+        linedActionMap[getRealKey(cmd[1])] = parseEnum[TwtAction](cmd[2])
+
+  normalActionRemap = constructActionTable(normalActionMap)
+  linedActionRemap = constructActionTable(linedActionMap)
diff --git a/display.nim b/display.nim
new file mode 100644
index 00000000..f4eaa806
--- /dev/null
+++ b/display.nim
@@ -0,0 +1,365 @@
+import terminal
+import options
+import uri
+import strutils
+
+import fusion/htmlparser/xmltree
+import fusion/htmlparser
+
+import buffer
+import termattrs
+import htmlelement
+import twtstr
+import twtio
+import config
+
+proc clearStatusMsg*(at: int) =
+  setCursorPos(0, at)
+  eraseLine()
+
+proc statusMsg*(str: string, at: int) =
+  clearStatusMsg(at)
+  print(str.addAnsiStyle(styleReverse))
+
+type
+  RenderState = ref RenderStateObj
+  RenderStateObj = object
+    x: int
+    y: int
+    atchar: int
+    atrawchar: int
+    centerqueue: int
+    centerlen: int
+    blanklines: int
+    blankspaces: int
+    nextspaces: int
+    docenter: bool
+
+func newRenderState(): RenderState =
+  return RenderState()
+
+func nodeAttr(node: HtmlNode): HtmlElement =
+  case node.nodeType
+  of NODE_TEXT: return node.text.parent
+  of NODE_ELEMENT: return node.element
+  else: assert(false)
+
+proc flushLine(buffer: Buffer, state: RenderState) =
+  if buffer.onNewLine():
+    state.blanklines += 1
+  buffer.write('\n')
+  state.x = 0
+  state.y += 1
+  state.atchar += 1
+  state.atrawchar += 1
+  state.nextspaces = 0
+  buffer.lines.add(state.atchar)
+  buffer.rawlines.add(state.atrawchar)
+  assert(buffer.onNewLine())
+
+proc addSpaces(buffer: Buffer, state: RenderState, n: int) =
+  if state.x + n > buffer.width:
+    buffer.flushLine(state)
+    return
+  state.blankspaces += n
+  buffer.write(' '.repeat(n))
+  state.x += n
+  state.atchar += n
+  state.atrawchar += n
+
+proc addSpace(buffer: Buffer, state: RenderState) =
+  buffer.addSpaces(state, 1)
+
+proc assignCoords(node: HtmlNode, state: RenderState) =
+  node.x = state.x
+  node.y = state.y
+
+proc addSpacePadding(buffer: Buffer, state: RenderState) =
+  if not buffer.onSpace():
+    buffer.addSpace(state)
+
+proc writeWrappedText(buffer: Buffer, state: RenderState, fmttext: string, rawtext: string) =
+  var n = 0
+  var fmtword = ""
+  var rawword = ""
+  var prevl = false
+  for c in fmttext:
+    fmtword &= c
+    if n >= rawtext.len or rawtext[n] != c:
+      continue
+
+    state.x += 1
+    rawword &= c
+
+    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
+      buffer.flushLine(state)
+      prevl = true
+
+    if c == ' ':
+      buffer.writefmt(fmtword)
+      buffer.writeraw(rawword)
+      state.atchar += fmtword.len
+      state.atrawchar += rawword.len
+      if prevl:
+        state.x += fmtword.len
+        prevl = false
+      fmtword = ""
+      rawword = ""
+    n += 1
+  buffer.writefmt(fmtword)
+  buffer.writeraw(rawword)
+  state.atchar += fmtword.len
+  state.atrawchar += rawword.len
+
+proc preAlignNode(buffer: Buffer, node: HtmlNode, state: RenderState) =
+  let elem = node.nodeAttr()
+  if not buffer.onNewLine() and node.openblock and state.blanklines == 0:
+    buffer.flushLine(state)
+
+  if node.openblock:
+    while state.blanklines < max(elem.margin, elem.margintop):
+      buffer.flushLine(state)
+
+  if not buffer.onNewLine() and state.blanklines == 0 and node.displayed():
+    buffer.addSpaces(state, state.nextspaces)
+    state.nextspaces = 0
+    if elem.pad:
+      buffer.addSpacePadding(state)
+    
+    if state.blankspaces < max(elem.margin, elem.marginleft):
+      buffer.addSpaces(state, max(elem.margin, elem.marginleft) - state.blankspaces)
+
+  if elem.centered and buffer.onNewLine() and node.displayed():
+    buffer.addSpaces(state, max(buffer.width div 2 - state.centerlen div 2, 0))
+    state.centerlen = 0
+
+proc postAlignNode(buffer: Buffer, node: HtmlNode, state: RenderState) =
+  let elem = node.nodeAttr()
+
+  if node.getRawLen() > 0:
+    state.blanklines = 0
+    state.blankspaces = 0
+
+  if not buffer.onNewLine() and state.blanklines == 0:
+    if elem.pad:
+      state.nextspaces = 1
+    state.nextspaces += max(elem.margin, elem.marginright)
+    if node.closeblock:
+      buffer.flushLine(state)
+
+  if node.closeblock:
+    while state.blanklines < max(elem.margin, elem.marginbottom):
+      buffer.flushLine(state)
+
+  if elem.htmlTag == tagBr and not node.openblock:
+    buffer.flushLine(state)
+
+proc renderNode(buffer: Buffer, node: HtmlNode, state: RenderState) =
+  if not node.visibleNode():
+    return
+  if node.isElemNode():
+    node.element.formattedElem = node.element.getFormattedElem()
+  let elem = node.nodeAttr()
+  if elem.htmlTag == tagTitle:
+    if isTextNode(node):
+      buffer.title = node.text.text
+    return
+  else: discard
+  if elem.hidden: return
+
+  node.height = 1
+  node.width = node.getRawLen()
+
+  if not state.docenter:
+    if elem.centered:
+      if not node.closeblock and elem.htmlTag != tagBr:
+        state.centerqueue += 1
+        return
+    if state.centerqueue > 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
+      state.docenter = false
+
+  buffer.preAlignNode(node, state)
+
+  node.assignCoords(state)
+  if isTextNode(node):
+    buffer.writeWrappedText(state, node.text.formattedText, node.text.text)
+  elif isElemNode(node):
+    buffer.writeWrappedText(state, node.element.formattedElem, node.element.rawElem)
+
+  buffer.postAlignNode(node, state)
+
+iterator revItems*(n: XmlNode): XmlNode {.inline.} =
+  var i = n.len - 1
+  while i >= 0:
+    yield n[i]
+    i -= 1
+
+type
+  XmlHtmlNode* = ref XmlHtmlNodeObj
+  XmlHtmlNodeObj = object
+    xml*: XmlNode
+    html*: HtmlNode
+
+proc setLastHtmlLine(buffer: Buffer, state: RenderState) =
+  if buffer.text.len != buffer.lines[^1]:
+    state.atchar = buffer.text.len + 1
+    state.atrawchar = buffer.rawtext.len + 1
+  buffer.flushLine(state)
+
+proc renderHtml(buffer: Buffer) =
+  var stack: seq[XmlHtmlNode]
+  let first = XmlHtmlNode(xml: buffer.htmlSource,
+                         html: getHtmlNode(buffer.htmlSource, none(HtmlElement)))
+  stack.add(first)
+
+  var state = newRenderState()
+  while stack.len > 0:
+    let currElem = stack.pop()
+    if currElem.html.nodeType != NODE_COMMENT:
+      buffer.renderNode(currElem.html, state)
+      if currElem.html.isElemNode():
+        if currElem.html.element.id != "":
+          eprint currElem.html.element.id
+      buffer.addNode(currElem.html)
+    var last = false
+    for item in currElem.xml.revItems:
+      let child = XmlHtmlNode(xml: item,
+                              html: getHtmlNode(item, some(currElem.html.element)))
+      stack.add(child)
+      if not last and child.html.visibleNode():
+        last = true
+        if currElem.html.element.display == DISPLAY_BLOCK:
+          stack[^1].html.closeblock = true
+    if last:
+      if currElem.html.element.display == DISPLAY_BLOCK:
+        stack[^1].html.openblock = true
+  buffer.setLastHtmlLine(state)
+
+proc drawHtml(buffer: Buffer) =
+  var state = newRenderState()
+  for node in buffer.nodes:
+    buffer.renderNode(node, state)
+  buffer.setLastHtmlLine(state)
+
+proc statusMsgForBuffer(buffer: Buffer) =
+  let msg = $buffer.cursorY & "/" & $buffer.lastLine() & " (" &
+            $buffer.atPercentOf() & "%) " &
+            "<" & buffer.title & buffer.hovertext
+  statusMsg(msg.maxString(buffer.width), buffer.height)
+
+proc cursorBufferPos(buffer: Buffer) =
+  var x = buffer.cursorX
+  if x > buffer.currentRawLineLength():
+    x = buffer.currentRawLineLength()
+  var y = buffer.cursorY - 1 - buffer.fromY
+  termGoto(x, y)
+
+proc displayBuffer(buffer: Buffer) =
+  eraseScreen()
+  termGoto(0, 0)
+
+  print(buffer.visibleText())
+
+proc inputLoop(attrs: TermAttributes, buffer: Buffer): bool =
+  var s = ""
+  var feedNext = false
+  while true:
+    cursorBufferPos(buffer)
+    if not feedNext:
+      s = ""
+    else:
+      feedNext = false
+    let c = getch()
+    s &= c
+    let action = getNormalAction(s)
+    var redraw = false
+    var reshape = false
+    case action
+    of ACTION_QUIT:
+      eraseScreen()
+      return false
+    of ACTION_CURSOR_LEFT: redraw = buffer.cursorLeft()
+    of ACTION_CURSOR_DOWN: redraw = buffer.cursorDown()
+    of ACTION_CURSOR_UP: redraw = buffer.cursorUp()
+    of ACTION_CURSOR_RIGHT: redraw = buffer.cursorRight()
+    of ACTION_CURSOR_LINEBEGIN: buffer.cursorLineBegin()
+    of ACTION_CURSOR_LINEEND: buffer.cursorLineEnd()
+    of ACTION_CURSOR_NEXT_WORD: redraw = buffer.cursorNextWord()
+    of ACTION_CURSOR_NEXT_NODE: redraw = buffer.cursorNextNode()
+    of ACTION_CURSOR_PREV_WORD: redraw = buffer.cursorPrevWord()
+    of ACTION_CURSOR_PREV_NODE: redraw = buffer.cursorPrevNode()
+    of ACTION_CURSOR_NEXT_LINK: redraw = buffer.cursorNextLink()
+    of ACTION_CURSOR_PREV_LINK: redraw = buffer.cursorPrevLink()
+    of ACTION_PAGE_DOWN: redraw = buffer.pageDown()
+    of ACTION_PAGE_UP: redraw = buffer.pageUp()
+    of ACTION_HALF_PAGE_DOWN: redraw = buffer.halfPageDown()
+    of ACTION_HALF_PAGE_UP: redraw = buffer.halfPageUp()
+    of ACTION_CURSOR_FIRST_LINE: redraw = buffer.cursorFirstLine()
+    of ACTION_CURSOR_LAST_LINE: redraw = buffer.cursorLastLine()
+    of ACTION_CURSOR_TOP: redraw = buffer.cursorTop()
+    of ACTION_CURSOR_MIDDLE: redraw = buffer.cursorMiddle()
+    of ACTION_CURSOR_BOTTOM: redraw = buffer.cursorBottom()
+    of ACTION_SCROLL_DOWN: redraw = buffer.scrollDown()
+    of ACTION_SCROLL_UP: redraw = buffer.scrollUp()
+    of ACTION_CLICK:
+      let selectedElem = buffer.findSelectedElement()
+      if selectedElem.isSome:
+        case selectedElem.get().htmlTag
+        of tagInput:
+          clearStatusMsg(buffer.height)
+          let status = readLine("TEXT:", selectedElem.get().value)
+          if status:
+            reshape = true
+            redraw = true
+        of tagA:
+          buffer.setLocation(parseUri(buffer.selectedlink.text.parent.href))
+          return true
+        else: discard
+    of ACTION_CHANGE_LOCATION:
+      var url = $buffer.location
+      clearStatusMsg(buffer.height)
+      let status = readLine("URL:", url)
+      if status:
+        buffer.setLocation(parseUri(url))
+        return true
+    of ACTION_FEED_NEXT:
+      feedNext = true
+    of ACTION_RELOAD: return true
+    of ACTION_RESHAPE:
+      reshape = true
+      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()
+
+proc displayPage*(attrs: TermAttributes, buffer: Buffer): bool =
+  eraseScreen()
+  termGoto(0, 0)
+  #buffer.printwrite = true
+  discard buffer.gotoAnchor()
+  buffer.renderHtml()
+  buffer.displayBuffer()
+  buffer.statusMsgForBuffer()
+  return inputLoop(attrs, buffer)
+
diff --git a/htmlelement.nim b/htmlelement.nim
new file mode 100644
index 00000000..4cf2e02e
--- /dev/null
+++ b/htmlelement.nim
@@ -0,0 +1,340 @@
+import strutils
+import re
+import terminal
+import options
+
+import fusion/htmlparser
+import fusion/htmlparser/xmltree
+
+import twtstr
+import twtio
+
+type
+  NodeType* =
+    enum
+    NODE_ELEMENT, NODE_TEXT, NODE_COMMENT
+  DisplayType* =
+    enum
+    DISPLAY_INLINE, DISPLAY_BLOCK, DISPLAY_SINGLE, DISPLAY_NONE
+  InputType* =
+    enum
+    INPUT_BUTTON, INPUT_CHECKBOX, INPUT_COLOR, INPUT_DATE, INPUT_DATETIME_LOCAL,
+    INPUT_EMAIL, INPUT_FILE, INPUT_HIDDEN, INPUT_IMAGE, INPUT_MONTH,
+    INPUT_NUMBER, INPUT_PASSWORD, INPUT_RADIO, INPUT_RANGE, INPUT_RESET,
+    INPUT_SEARCH, INPUT_SUBMIT, INPUT_TEL, INPUT_TEXT, INPUT_TIME, INPUT_URL,
+    INPUT_WEEK, INPUT_UNKNOWN
+  WhitespaceType* =
+    enum
+    WHITESPACE_NORMAL, WHITESPACE_NOWRAP,
+    WHITESPACE_PRE, WHITESPACE_PRE_LINE, WHITESPACE_PRE_WRAP,
+    WHITESPACE_INITIAL, WHITESPACE_INHERIT
+
+type
+  HtmlText* = ref HtmlTextObj
+  HtmlTextObj = object
+    parent*: HtmlElement
+    text*: string
+    formattedText*: string
+  HtmlElement* = ref HtmlElementObj
+  HtmlElementObj = object
+    id*: string
+    name*: string
+    value*: string
+    centered*: bool
+    hidden*: bool
+    display*: DisplayType
+    innerText*: string
+    formattedElem*: string
+    rawElem*: string
+    textNodes*: int
+    margintop*: int
+    marginbottom*: int
+    marginleft*: int
+    marginright*: int
+    margin*: int
+    pad*: bool
+    bold*: bool
+    italic*: bool
+    underscore*: bool
+    parentElement*: HtmlElement
+    case htmlTag*: HtmlTag
+    of tagInput:
+      itype*: InputType
+      size*: int
+    of tagA:
+      href*: string
+      selected*: bool
+    else:
+      discard
+
+type
+  HtmlNode* = ref HtmlNodeObj
+  HtmlNodeObj = object
+    case nodeType*: NodeType
+    of NODE_ELEMENT:
+      element*: HtmlElement
+    of NODE_TEXT:
+      text*: HtmlText
+    of NODE_COMMENT:
+      comment*: string
+    x*: int
+    y*: int
+    width*: int
+    height*: int
+    openblock*: bool
+    closeblock*: bool
+
+func isTextNode*(node: HtmlNode): bool =
+  return node.nodeType == NODE_TEXT
+
+func isElemNode*(node: HtmlNode): bool =
+  return node.nodeType == NODE_ELEMENT
+
+func getFormattedLen*(htmlText: HtmlText): int =
+  return htmlText.formattedText.strip().len
+
+func getFormattedLen*(htmlElem: HtmlElement): int =
+  return htmlElem.formattedElem.len
+
+func getFormattedLen*(htmlNode: HtmlNode): int =
+  case htmlNode.nodeType
+  of NODE_TEXT: return htmlNode.text.getFormattedLen()
+  of NODE_ELEMENT: return htmlNode.element.getFormattedLen()
+  else:
+    assert(false)
+    return 0
+
+func getRawLen*(htmlText: HtmlText): int =
+  return htmlText.text.len
+
+func getRawLen*(htmlElem: HtmlElement): int =
+  return htmlElem.rawElem.len
+
+func getRawLen*(htmlNode: HtmlNode): int =
+  case htmlNode.nodeType
+  of NODE_TEXT: return htmlNode.text.getRawLen()
+  of NODE_ELEMENT: return htmlNode.element.getRawLen()
+  else:
+    assert(false)
+    return 0
+
+func visibleNode*(node: HtmlNode): bool =
+  case node.nodeType
+  of NODE_TEXT: return true
+  of NODE_ELEMENT: return true
+  else: return false
+
+func displayed*(elem: HtmlElement): bool =
+  return elem.display != DISPLAY_NONE and (elem.getFormattedLen() > 0 or elem.htmlTag == tagBr) and not elem.hidden
+
+func displayed*(node: HtmlNode): bool =
+  if node.isTextNode():
+    return node.getRawLen() > 0
+  elif node.isElemNode():
+    return node.element.displayed()
+
+func empty*(elem: HtmlElement): bool =
+  return elem.textNodes == 0 or not elem.displayed()
+
+func toInputType*(str: string): InputType =
+  case str
+  of "button": INPUT_BUTTON
+  of "checkbox": INPUT_CHECKBOX
+  of "color": INPUT_COLOR
+  of "date": INPUT_DATE
+  of "datetime_local": INPUT_DATETIME_LOCAL
+  of "email": INPUT_EMAIL
+  of "file": INPUT_FILE
+  of "hidden": INPUT_HIDDEN
+  of "image": INPUT_IMAGE
+  of "month": INPUT_MONTH
+  of "number": INPUT_NUMBER
+  of "password": INPUT_PASSWORD
+  of "radio": INPUT_RADIO
+  of "range": INPUT_RANGE
+  of "reset": INPUT_RESET
+  of "search": INPUT_SEARCH
+  of "submit": INPUT_SUBMIT
+  of "tel": INPUT_TEL
+  of "text": INPUT_TEXT
+  of "time": INPUT_TIME
+  of "url": INPUT_URL
+  of "week": INPUT_WEEK
+  else: INPUT_UNKNOWN
+
+func toInputSize*(str: string): int =
+  if str.len == 0:
+    return 20
+  return str.parseInt()
+
+func getInputElement(xmlElement: XmlNode, htmlElement: HtmlElement): HtmlElement =
+  assert(htmlElement.htmlTag == tagInput)
+  htmlElement.itype = xmlElement.attr("type").toInputType()
+  htmlElement.size = xmlElement.attr("size").toInputSize()
+  htmlElement.value = xmlElement.attr("value")
+  htmlElement.pad = true
+  return htmlElement
+
+func getAnchorElement(xmlElement: XmlNode, htmlElement: HtmlElement): HtmlElement =
+  assert(htmlElement.htmlTag == tagA)
+  htmlElement.href = xmlElement.attr("href")
+  return htmlElement
+
+func getSelectElement(xmlElement: XmlNode, htmlElement: HtmlElement): HtmlElement =
+  assert(htmlElement.htmlTag == tagSelect)
+  for item in xmlElement.items:
+    if item.kind == xnElement:
+      if item.tag == "option" and item.attr("value") != "":
+        htmlElement.value = item.attr("value")
+        break
+  htmlElement.name = xmlElement.attr("name")
+  return htmlElement
+
+func getOptionElement(xmlElement: XmlNode, htmlElement: HtmlElement): HtmlElement =
+  assert(htmlElement.htmlTag == tagOption)
+  htmlElement.value = xmlElement.attr("value")
+  if htmlElement.parentElement.value != htmlElement.value:
+    htmlElement.hidden = true
+  return htmlElement
+
+func getFormattedInput(htmlElement: HtmlElement): string =
+  case htmlElement.itype
+  of INPUT_TEXT, INPUT_SEARCH:
+    let valueFit = fitValueToSize(htmlElement.value, htmlElement.size)
+    return "[" & valueFit.addAnsiStyle(styleUnderscore).addAnsiFgColor(fgRed) & "]"
+  of INPUT_SUBMIT: return ("[" & htmlElement.value & "]").addAnsiFgColor(fgRed)
+  else: discard
+
+func getRawInput(htmlElement: HtmlElement): string =
+  case htmlElement.itype
+  of INPUT_TEXT, INPUT_SEARCH:
+    return "[" & htmlElement.value.fitValueToSize(htmlElement.size) & "]"
+  of INPUT_SUBMIT: return "[" & htmlElement.value & "]"
+  else: discard
+
+func getRawElem*(htmlElement: HtmlElement): string =
+  case htmlElement.htmlTag
+  of tagInput: return htmlElement.getRawInput()
+  of tagOption: return "[]"
+  else: return ""
+
+func getFormattedElem*(htmlElement: HtmlElement): string =
+  case htmlElement.htmlTag
+  of tagInput: return htmlElement.getFormattedInput()
+  else: return ""
+
+func getRawText*(htmlText: HtmlText): string =
+  if htmlText.parent.htmlTag != tagPre:
+    result = htmlText.text.replace(re"\n").strip()
+  else:
+    result = htmlText.text
+
+  if htmlText.parent.htmlTag == tagOption:
+    result = "[" & result & "]"
+
+func getFormattedText*(htmlText: HtmlText): string =
+  result = htmlText.text
+  case htmlText.parent.htmlTag
+  of tagA:
+    result = result.addAnsiFgColor(fgBlue)
+    if htmlText.parent.selected:
+      result = result.addAnsiStyle(styleUnderscore)
+  of tagOption: result = result.addAnsiFgColor(fgRed)
+  else: discard
+
+  if htmlText.parent.bold:
+    result = result.addAnsiStyle(styleBright)
+  if htmlText.parent.italic:
+    result = result.addAnsiStyle(styleItalic)
+  if htmlText.parent.underscore:
+    result = result.addAnsiStyle(styleUnderscore)
+
+proc newElemFromParent(elem: HtmlElement, parentOpt: Option[HtmlElement]): HtmlElement =
+  if parentOpt.isSome:
+    let parent = parentOpt.get()
+    elem.centered = parent.centered
+    elem.bold = parent.bold
+    elem.italic = parent.italic
+    elem.underscore = parent.underscore
+    elem.hidden = parent.hidden
+    elem.display = parent.display
+    #elem.margin = parent.margin
+    #elem.margintop = parent.margintop
+    #elem.marginbottom = parent.marginbottom
+    #elem.marginleft = parent.marginleft
+    #elem.marginright = parent.marginright
+    elem.parentElement = parent
+  elem.pad = false
+
+  return elem
+
+proc getHtmlElement*(xmlElement: XmlNode, inherit: Option[HtmlElement]): HtmlElement =
+  assert kind(xmlElement) == xnElement
+  var htmlElement: HtmlElement
+  htmlElement = newElemFromParent(HtmlElement(htmlTag: htmlTag(xmlElement)), inherit)
+  htmlElement.id = xmlElement.attr("id")
+
+  if htmlElement.htmlTag in InlineTags:
+    htmlElement.display = DISPLAY_INLINE
+  elif htmlElement.htmlTag in BlockTags:
+    htmlElement.display = DISPLAY_BLOCK
+    htmlElement.pad = true
+  elif htmlElement.htmlTag in SingleTags:
+    htmlElement.display = DISPLAY_SINGLE
+  else:
+    htmlElement.display = DISPLAY_NONE
+
+  case htmlElement.htmlTag
+  of tagCenter:
+    htmlElement.centered = true
+  of tagB:
+    htmlElement.bold = true
+  of tagI:
+    htmlElement.italic = true
+  of tagU:
+    htmlElement.underscore = true
+  of tagHead:
+    htmlElement.hidden = true
+  of tagStyle:
+    htmlElement.hidden = true
+  of tagScript:
+    htmlElement.hidden = true
+  of tagInput:
+    htmlElement = getInputElement(xmlElement, htmlElement)
+  of tagA:
+    htmlElement = getAnchorElement(xmlElement, htmlElement)
+  of tagSelect:
+    htmlElement = getSelectElement(xmlElement, htmlElement)
+  of tagOption:
+    htmlElement = getOptionElement(xmlElement, htmlElement)
+  of tagPre, tagTd, tagTh:
+    htmlElement.margin = 1
+  else:
+    discard
+
+  for child in xmlElement.items:
+    if child.kind == xnText and child.text.strip().len > 0:
+      htmlElement.textNodes += 1
+  
+  htmlElement.rawElem = htmlElement.getRawElem()
+  htmlElement.formattedElem = htmlElement.getFormattedElem()
+  return htmlElement
+
+proc getHtmlText*(text: string, parent: HtmlElement): HtmlText =
+  var textNode = HtmlText(parent: parent, text: text)
+  textNode.text = textNode.getRawText()
+  textNode.formattedText = textNode.getFormattedText()
+  return textNode
+
+proc getHtmlNode*(xmlElement: XmlNode, parent: Option[HtmlElement]): HtmlNode =
+  case kind(xmlElement)
+  of xnElement:
+    return HtmlNode(nodeType: NODE_ELEMENT, element: getHtmlElement(xmlElement, parent))
+  of xnText:
+    assert(parent.isSome)
+    return HtmlNode(nodeType: NODE_TEXT, text: getHtmlText(xmlElement.text, parent.get()))
+  of xnComment:
+    return HtmlNode(nodeType: NODE_COMMENT, comment: xmlElement.text)
+  of xnCData:
+    return HtmlNode(nodeType: NODE_TEXT, text: getHtmlText(xmlElement.text, parent.get()))
+  else: assert(false)
diff --git a/keymap b/keymap
new file mode 100644
index 00000000..218cabd3
--- /dev/null
+++ b/keymap
@@ -0,0 +1,53 @@
+#"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 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
+
+#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
diff --git a/main.nim b/main.nim
new file mode 100644
index 00000000..bbfffd58
--- /dev/null
+++ b/main.nim
@@ -0,0 +1,59 @@
+import httpClient
+import uri
+import os
+
+import fusion/htmlparser
+import fusion/htmlparser/xmltree
+
+import display
+import termattrs
+import buffer
+import twtio
+import config
+
+proc loadRemotePage*(url: string): string =
+  return newHttpClient().getContent(url)
+
+proc loadLocalPage*(url: string): string =
+  return readFile(url)
+
+proc loadPageUri(uri: Uri, currentcontent: XmlNode): XmlNode =
+  var moduri = uri
+  var page: XmlNode
+  moduri.anchor = ""
+  if uri.scheme == "" and uri.path == "" and uri.anchor != "" and currentcontent != nil:
+    return currentcontent
+  elif uri.scheme == "" or uri.scheme == "file":
+    return parseHtml(loadLocalPage($moduri))
+  else:
+    return parseHtml(loadRemotePage($moduri))
+
+var buffers: seq[Buffer]
+
+proc main*() =
+  if paramCount() != 1:
+    eprint "Invalid parameters. Usage:\ntwt <url>"
+    quit(1)
+  if not readKeymap("keymap"):
+    eprint "Failed to read keymap, falling back to default"
+    parseKeymap(keymapStr)
+  let attrs = getTermAttributes()
+  var buffer = newBuffer(attrs)
+  var url = parseUri(paramStr(1))
+  buffers.add(buffer)
+  buffer.setLocation(uri)
+  buffer.htmlSource = loadPageUri(uri, buffer.htmlSource)
+  var lastUri = uri
+  while displayPage(attrs, buffer):
+    statusMsg("Loading...", buffer.height)
+    var newUri = buffer.location
+    lastUri.anchor = ""
+    newUri.anchor = ""
+    if $lastUri != $newUri:
+      buffer.htmlSource = loadPageUri(buffer.location, buffer.htmlSource)
+      buffer.clearBuffer()
+    else
+    lastUri = newUri
+
+#waitFor loadPage("https://lite.duckduckgo.com/lite/?q=hello%20world")
+main()
diff --git a/readme.md b/readme.md
new file mode 100644
index 00000000..62c97ce6
--- /dev/null
+++ b/readme.md
@@ -0,0 +1,29 @@
+# twt - a web browser in your terminal
+
+## What is this?
+A terminal web browser. It displays websites in your terminal.
+
+## Why make another web browser?
+I've found other terminal web browsers insufficient for my needs. In fact, I started working on this after failing to add JavaScript support to w3m.
+
+## So what can this do?
+Currently implemented features are:
+* basic html rendering (WIP)
+* custom keybindings
+
+Planned features:
+* image (sixel/kitty)
+* video (sixel/kitty)
+* audio
+* table
+* cookie
+* form
+* JavaScript
+* SOCKS proxy
+* extension API (adblock support?)
+* markdown? (with pandoc?)
+* gopher?
+* gemini?
+
+## How do I configure stuff?
+Currently only keybindings can be configured. See the keymap file for the default (built-in) configuration.
diff --git a/search.html b/search.html
new file mode 100644
index 00000000..d4c3facd
--- /dev/null
+++ b/search.html
@@ -0,0 +1,1586 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+<head>
+  <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=3.0, user-scalable=1;">
+  <meta name="referrer" content="origin">
+  <meta name="HandheldFriendly" content="true" />
+  <meta name="robots" content="noindex, nofollow" />
+  <title>rhetorische at DuckDuckGo</title>
+  <link title="DuckDuckGo (Lite)" type="application/opensearchdescription+xml" rel="search" href="//duckduckgo.com/opensearch_lite_v2.xml">
+  <link href="//duckduckgo.com/favicon.ico" rel="shortcut icon" />
+  <link rel="icon" href="//duckduckgo.com/favicon.ico" type="image/x-icon" />
+  <link rel="apple-touch-icon" href="//duckduckgo.com/assets/logo_icon128.v101.png">
+  <link rel="image_src" href="//duckduckgo.com/assets/logo_homepage.normal.v101.png"/>
+
+  <style type="text/css">
+    html {
+      font-family: 'Helvetica Neue','Segoe UI', Arial, sans-serif;
+    }
+    .header {
+      font-size: 32px;
+      font-weight: bold;
+      color: #dc5e47;
+      height: 32px;
+      line-height: 32px;
+    }
+    .extra {
+      margin-top: 0px;
+      margin-bottom: 0px;
+      height: 1.3em;
+    }
+    #end-spacer {
+      height: 10em;
+    }
+    body {
+      padding-left: 20px;
+      width: 90%;
+      margin-top: 0;
+    }
+    table {
+      border-collapse: collapse;
+      border-spacing: 0;
+    }
+    th, td {
+      margin: 0;
+      padding: 0;
+      font-size: 107.1%;
+    }
+    
+    span.no-results {
+      font-size: 110%;
+    }
+    .query {
+      height: 28px;
+      border-color: #dc5e47;
+      border-style: solid solid solid solid;
+      border-width: 1px 1px 1px 1px;
+      -moz-border-radius: 3px;
+      border-radius: 3px;
+      font-size: 20px;
+      padding: 5px 6px;
+      text-align: left;
+      width: 55%; 
+      max-width: 600px;
+    }
+    .submit {
+      padding-left: 7px;
+      padding-right: 7px;
+      height: 40px;
+      font-size: 16px;
+    /*
+      color: white;
+      background-color: #019801;
+      border-style: none none none;
+      -moz-border-radius: 3px;
+      border-radius: 3px;
+      padding: 5px 6px;
+    */
+      cursor: pointer;
+    }
+    .link-text {
+      color: #662200;
+      font-size: 77.4%;
+    }
+    .result-link {
+      font-size: 107.1%;
+    }
+    .result-snippet {
+      font-size: 77.4%;
+    }
+
+    .result-sponsored:hover {
+      background: #eae3ad !important;
+    }
+
+    .result-sponsored {
+      background: #fff7d0;
+      border: 1px solid #fef9eb !important;
+    }
+
+    .did-you-mean {
+      color: #EE4444;
+      font-size: 107.1%;
+    }
+    a {
+      text-decoration: none;
+      color: #1168CC;
+    }
+    a:hover {
+      text-decoration: underline;
+    }
+    a:visited {
+      color: #6830BB;
+    }
+    .timestamp {
+      color:#555555;
+      font-size: 77.4%;
+    }
+    .navbutton {
+      background-color:transparent;
+      border: 0px none;
+      cursor:pointer;
+      font-size:16px;
+      font-weight:bold;
+      text-decoration: underline;
+      color: #1168CC;
+      border-bottom: 1px solid transparent;
+      padding: 0px;
+    }
+
+    .results--powered {
+      color: #888;
+      display: inline;
+      margin-left: 10px;
+      position: absolute;
+      bottom: 10px;
+      right: 10px;
+    }
+    .results--powered a {
+      color: #888;
+      font-size: 13px;
+    }
+    .results--powered__badge {
+      vertical-align: baseline;
+    }
+    .badge--yahoo {
+      background-image: url("//duckduckgo.com/assets/attribution/yahoo.v103.png");
+      background-size: 55px 13px;
+      width: 55px;
+      height: 13px;
+    }
+    .badge, .results--powered__badge {
+      text-indent: -999999px;
+      display: inline-block;
+      vertical-align: middle;
+      position: relative;
+      background-position: 50% 50%;
+      background-repeat: no-repeat;
+    }
+
+    .next_form {
+      float: left;
+    }
+    .prev_form {
+      float: left;
+    }
+
+    .filters {
+      margin-top: 10px;
+    }
+
+    @media only screen and (max-device-width: 700px) {
+      body {
+        width: 95%;
+        margin-left: 10px;
+        padding: 0;
+      }
+      .query {
+        width: 180px;
+        font-size: 12px;
+      }
+      .submit {
+        font-size: 12px;
+      }
+    }
+
+
+    @media only screen and (max-device-width: 701px) and (orientation:landscape) 
+    {
+      .query {
+        width: 273px;
+      }
+    }
+
+  </style>
+</head>
+
+<body>
+<p class='extra'>&nbsp;</p>
+<div class="header">DuckDuckGo</div>
+<p class='extra'>&nbsp;</p>
+
+<form action="/lite/" method="post">
+  <input class='query' type="text" size="40" name="q" value="rhetorische" >
+  <input class='submit' type="submit" value="Search" >
+    
+    
+    
+    
+
+<div class="filters">
+
+<select class="submit" name="kl">
+
+  <option value="" >All Regions</option>
+
+  <option value="ar-es" >Argentina</option>
+
+  <option value="au-en" >Australia</option>
+
+  <option value="at-de" >Austria</option>
+
+  <option value="be-fr" >Belgium (fr)</option>
+
+  <option value="be-nl" >Belgium (nl)</option>
+
+  <option value="br-pt" >Brazil</option>
+
+  <option value="bg-bg" >Bulgaria</option>
+
+  <option value="ca-en" >Canada (en)</option>
+
+  <option value="ca-fr" >Canada (fr)</option>
+
+  <option value="ct-ca" >Catalonia</option>
+
+  <option value="cl-es" >Chile</option>
+
+  <option value="cn-zh" >China</option>
+
+  <option value="co-es" >Colombia</option>
+
+  <option value="hr-hr" >Croatia</option>
+
+  <option value="cz-cs" >Czech Republic</option>
+
+  <option value="dk-da" >Denmark</option>
+
+  <option value="ee-et" >Estonia</option>
+
+  <option value="fi-fi" >Finland</option>
+
+  <option value="fr-fr" >France</option>
+
+  <option value="de-de" >Germany</option>
+
+  <option value="gr-el" >Greece</option>
+
+  <option value="hk-tzh" >Hong Kong</option>
+
+  <option value="hu-hu" >Hungary</option>
+
+  <option value="in-en" >India (en)</option>
+
+  <option value="id-en" >Indonesia (en)</option>
+
+  <option value="ie-en" >Ireland</option>
+
+  <option value="il-en" >Israel (en)</option>
+
+  <option value="it-it" >Italy</option>
+
+  <option value="jp-jp" >Japan</option>
+
+  <option value="kr-kr" >Korea</option>
+
+  <option value="lv-lv" >Latvia</option>
+
+  <option value="lt-lt" >Lithuania</option>
+
+  <option value="my-en" >Malaysia (en)</option>
+
+  <option value="mx-es" >Mexico</option>
+
+  <option value="nl-nl" >Netherlands</option>
+
+  <option value="nz-en" >New Zealand</option>
+
+  <option value="no-no" >Norway</option>
+
+  <option value="pk-en" >Pakistan (en)</option>
+
+  <option value="pe-es" >Peru</option>
+
+  <option value="ph-en" >Philippines (en)</option>
+
+  <option value="pl-pl" >Poland</option>
+
+  <option value="pt-pt" >Portugal</option>
+
+  <option value="ro-ro" >Romania</option>
+
+  <option value="ru-ru" >Russia</option>
+
+  <option value="xa-ar" >Saudi Arabia</option>
+
+  <option value="sg-en" >Singapore</option>
+
+  <option value="sk-sk" >Slovakia</option>
+
+  <option value="sl-sl" >Slovenia</option>
+
+  <option value="za-en" >South Africa</option>
+
+  <option value="es-ca" >Spain (ca)</option>
+
+  <option value="es-es" >Spain (es)</option>
+
+  <option value="se-sv" >Sweden</option>
+
+  <option value="ch-de" >Switzerland (de)</option>
+
+  <option value="ch-fr" >Switzerland (fr)</option>
+
+  <option value="tw-tzh" >Taiwan</option>
+
+  <option value="th-en" >Thailand (en)</option>
+
+  <option value="tr-tr" >Turkey</option>
+
+  <option value="us-en" >US (English)</option>
+
+  <option value="us-es" >US (Spanish)</option>
+
+  <option value="ua-uk" >Ukraine</option>
+
+  <option value="uk-en" >United Kingdom</option>
+
+  <option value="vn-en" >Vietnam (en)</option>
+
+</select>
+
+<select class="submit" name="df">
+
+  <option value="" selected>Any Time</option>
+
+  <option value="d" >Past Day</option>
+
+  <option value="w" >Past Week</option>
+
+  <option value="m" >Past Month</option>
+
+  <option value="y" >Past Year</option>
+
+</select>
+
+</form>
+
+
+  <p class="extra">&nbsp;</p>
+    <table border="0"><tr>
+    <td>
+      
+    </td>
+    <td>
+      
+        <form action="/lite/" method="post">
+          <!-- <a rel="next" href="/lite/?q=rhetorische&amp;v=l&amp;l=us-en&amp;p=-1&amp;s=30&amp;ex=-1&amp;o=json&amp;dl=en&amp;ct=AT&amp;ss_mkt=us&amp;sp=0&amp;vqd=3-317185285141429630887821832006479517976-203666168757858907862519566843911872030&amp;dc=27&amp;api=%2Fd.js">Next Page &gt;</a> //-->
+          <input type="submit" class='navbutton' value="Next Page &gt;">
+          <input type="hidden" name="q" value="rhetorische">
+          <input type="hidden" name="s" value="30">
+          <input type="hidden" name="nextParams" value="">
+          <input type="hidden" name="v" value="l">
+          <input type="hidden" name="o" value="json">
+          <input type="hidden" name="dc" value="27">
+          <input type="hidden" name="api" value="/d.js">
+          <input type="hidden" name="vqd" value="3-317185285141429630887821832006479517976-203666168757858907862519566843911872030">
+              
+              
+          
+          <input name="kl" value="wt-wt" type="hidden">
+              
+              
+              
+              
+        </form>
+      
+    </td>
+    </tr></table>
+
+
+
+
+<p class='extra'>&nbsp;</p>
+
+<table border="0">
+
+</table>
+
+<table border="0">
+
+<!-- Web results are present -->
+
+
+
+
+
+ <tr>
+
+    <td valign="top">1.&nbsp;</td>
+    <td>
+
+    
+        <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fde.wikipedia.org%2Fwiki%2FRhetorische_Frage" class='result-link'><b>Rhetorische</b> Frage - Wikipedia</a>
+    
+    </td>
+  </tr>
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td class='result-snippet'>
+      Die <b>rhetorische</b> Frage gilt als Stilmittel der Rhetorik. <b>Rhetorische</b> Fragen dienen nicht dem Informationsgewinn, sondern sind sprachliche Mittel der Beeinflussung. Semantisch stehen <b>rhetorische</b> Fragen den Behauptungen nahe.
+    </td>
+  </tr>
+  
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td>
+      <span class='link-text'>de.wikipedia.org/wiki/Rhetorische_Frage</span>
+      
+    </td>
+  </tr>
+  
+
+  <tr>
+    <td>&nbsp;</td>
+    <td>&nbsp;</td>
+  </tr>
+
+
+
+
+
+
+
+
+
+ <tr>
+
+    <td valign="top">2.&nbsp;</td>
+    <td>
+
+    
+        <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwortwuchs.net%2Fstilmittel%2Frhetorische%2Dfrage%2F" class='result-link'><b>Rhetorische</b> Frage | Definition, Wirkung und Beispiel</a>
+    
+    </td>
+  </tr>
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td class='result-snippet'>
+      Die <b>rhetorische</b> Frage ist ein Stilmittel der Rhetorik. Äußerlich betrachtet, unterscheidet sich eine <b>rhetorische</b> Wer eine <b>rhetorische</b> Frage stellt, fragt nicht nach Informationen, sondern versucht...
+    </td>
+  </tr>
+  
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td>
+      <span class='link-text'>wortwuchs.net/stilmittel/rhetorische-frage/</span>
+      
+    </td>
+  </tr>
+  
+
+  <tr>
+    <td>&nbsp;</td>
+    <td>&nbsp;</td>
+  </tr>
+
+
+
+
+
+
+
+
+
+ <tr>
+
+    <td valign="top">3.&nbsp;</td>
+    <td>
+
+    
+        <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.lingvolive.com%2Fru%2Dru%2Ftranslate%2Fde%2Dru%2FRhetorische" class='result-link'>Переводы «<b>Rhetorische</b>» (De-Ru) на ABBYY Lingvo Live</a>
+    
+    </td>
+  </tr>
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td class='result-snippet'>
+      Jahrzehnte später, beim Klassentreffen erinnern sich Frauen selten an die <b>rhetorische</b> Gewandtheit, die Forschheit oder die Bereitschaft der Mitschülerinnen, ungestüm eigene Interessen zu vertreten.
+    </td>
+  </tr>
+  
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td>
+      <span class='link-text'>www.lingvolive.com/ru-ru/translate/de-ru/Rhetorische</span>
+      
+    </td>
+  </tr>
+  
+
+  <tr>
+    <td>&nbsp;</td>
+    <td>&nbsp;</td>
+  </tr>
+
+
+
+
+
+
+
+
+
+ <tr>
+
+    <td valign="top">4.&nbsp;</td>
+    <td>
+
+    
+        <a rel="nofollow" href="//duckduckgo.com/l/?uddg=http%3A%2F%2Fwww.abipedia.de%2Frhetorische%2Dmittel.php" class='result-link'><b>Rhetorische</b> Mittel</a>
+    
+    </td>
+  </tr>
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td class='result-snippet'>
+      <b>Rhetorische</b> Mittel sind allgegenwärtig. Kaum ein Werbespruch kommt heute noch ohne eine <b>rhetorische</b> Figur aus. Auch wir selbst benutzen sie ständig, ohne uns dabei bewusst zu sein.
+    </td>
+  </tr>
+  
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td>
+      <span class='link-text'>www.abipedia.de/rhetorische-mittel.php</span>
+      
+    </td>
+  </tr>
+  
+
+  <tr>
+    <td>&nbsp;</td>
+    <td>&nbsp;</td>
+  </tr>
+
+
+
+
+
+
+
+
+
+ <tr>
+
+    <td valign="top">5.&nbsp;</td>
+    <td>
+
+    
+        <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fcontext.reverso.net%2Ftranslation%2Fgerman%2Denglish%2Frhetorische" class='result-link'><b>rhetorische</b> - Translation into English - examples... | Reverso Context</a>
+    
+    </td>
+  </tr>
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td class='result-snippet'>
+      Translations in context of &quot;<b>rhetorische</b>&quot; in German-English from Reverso Context: <b>rhetorische</b> Frage, <b>rhetorische</b> Figur. Das darf nicht nur eine <b>rhetorische</b> Verpflichtung bleiben.
+    </td>
+  </tr>
+  
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td>
+      <span class='link-text'>context.reverso.net/translation/german-english/rhetorische</span>
+      
+    </td>
+  </tr>
+  
+
+  <tr>
+    <td>&nbsp;</td>
+    <td>&nbsp;</td>
+  </tr>
+
+
+
+
+
+
+
+
+
+ <tr>
+
+    <td valign="top">6.&nbsp;</td>
+    <td>
+
+    
+        <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fen.glosbe.com%2Fde%2Fen%2Frhetorische" class='result-link'><b>rhetorische</b> - translation - German-English Dictionary - Glosbe</a>
+    
+    </td>
+  </tr>
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td class='result-snippet'>
+      translation and definition &quot;<b>rhetorische</b>&quot;, German-English Dictionary online. Inflected form of rhetorisch. Automatic translation: <b>rhetorische</b>.
+    </td>
+  </tr>
+  
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td>
+      <span class='link-text'>en.glosbe.com/de/en/rhetorische</span>
+      
+    </td>
+  </tr>
+  
+
+  <tr>
+    <td>&nbsp;</td>
+    <td>&nbsp;</td>
+  </tr>
+
+
+
+
+
+
+
+
+
+ <tr>
+
+    <td valign="top">7.&nbsp;</td>
+    <td>
+
+    
+        <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.dw.com%2Fde%2Frhetorische%2Dfiguren%2Fa%2D1606602" class='result-link'><b>Rhetorische</b> Figuren | Alltagsdeutsch - Podcast | DW | 08.11.2006</a>
+    
+    </td>
+  </tr>
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td class='result-snippet'>
+      <b>Rhetorische</b> Figuren sind doch keine Menschen! Diese <b>rhetorische</b> Figur zählt übrigens zu den grammatischen Figuren, die bewusst vom korrekten grammatischen Sprachgebrauch abweichen.
+    </td>
+  </tr>
+  
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td>
+      <span class='link-text'>www.dw.com/de/rhetorische-figuren/a-1606602</span>
+      
+    </td>
+  </tr>
+  
+
+  <tr>
+    <td>&nbsp;</td>
+    <td>&nbsp;</td>
+  </tr>
+
+
+
+
+
+
+
+
+
+ <tr>
+
+    <td valign="top">8.&nbsp;</td>
+    <td>
+
+    
+        <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fkarrierebibel.de%2Frhetorische%2Dfragen%2F" class='result-link'><b>Rhetorische</b> Fragen: 25 Beispiele - und ihre Subbotschaften</a>
+    
+    </td>
+  </tr>
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td class='result-snippet'>
+      Auf den ersten Blick ist die Frage völlig harmlos: „Wie geht es dir?&quot; Oft handelt es sich dabei nur um eine Plattitüde, eine Art Eisbrecher beim Smalltalk, um ins Gespräch zu finden.
+    </td>
+  </tr>
+  
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td>
+      <span class='link-text'>karrierebibel.de/rhetorische-fragen/</span>
+      
+    </td>
+  </tr>
+  
+
+  <tr>
+    <td>&nbsp;</td>
+    <td>&nbsp;</td>
+  </tr>
+
+
+
+
+
+
+
+
+
+ <tr>
+
+    <td valign="top">9.&nbsp;</td>
+    <td>
+
+    
+        <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.schreiben.net%2Fartikel%2Frhetorische%2Dfrage%2D3620%2F" class='result-link'><b>Rhetorische</b> Frage: Definition, Wirkung &amp; 16 typische Beispiele</a>
+    
+    </td>
+  </tr>
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td class='result-snippet'>
+      Alles Wissenswerte über die <b>rhetorische</b> Frage: Definition, Wirkung, typische Beispiele und wie sie sich von der Suggestivfrage unterscheidet.
+    </td>
+  </tr>
+  
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td>
+      <span class='link-text'>www.schreiben.net/artikel/rhetorische-frage-3620/</span>
+      
+    </td>
+  </tr>
+  
+
+  <tr>
+    <td>&nbsp;</td>
+    <td>&nbsp;</td>
+  </tr>
+
+
+
+
+
+
+
+
+
+ <tr>
+
+    <td valign="top">10.&nbsp;</td>
+    <td>
+
+    
+        <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Flearnattack.de%2Fjournal%2F40%2Dwichtige%2Drhetorische%2Dmittel%2Dtextanalyse%2F" class='result-link'>40 wichtige <b>rhetorische</b> Mittel für deine nächste Textanalyse</a>
+    
+    </td>
+  </tr>
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td class='result-snippet'>
+      ©iStockphoto.com/avosb. <b>Rhetorische</b> Mittel sind heute in jedermanns Sprachgebrauch zu finden, doch sind wir uns dessen selten bewusst.
+    </td>
+  </tr>
+  
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td>
+      <span class='link-text'>learnattack.de/journal/40-wichtige-rhetorische-mittel-textanalyse/</span>
+      
+    </td>
+  </tr>
+  
+
+  <tr>
+    <td>&nbsp;</td>
+    <td>&nbsp;</td>
+  </tr>
+
+
+
+
+
+
+
+
+
+ <tr>
+
+    <td valign="top">11.&nbsp;</td>
+    <td>
+
+    
+        <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.multitran.com%2Fm.exe%3Fl1%3D3%26l2%3D2%26s%3Drhetorische" class='result-link'><b>rhetorische</b></a>
+    
+    </td>
+  </tr>
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td class='result-snippet'>
+      <b>rhetorische</b>: 5 фраз в 2 тематиках. Лингвистика.
+    </td>
+  </tr>
+  
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td>
+      <span class='link-text'>www.multitran.com/m.exe?l1=3&amp;l2=2&amp;s=rhetorische</span>
+      
+    </td>
+  </tr>
+  
+
+  <tr>
+    <td>&nbsp;</td>
+    <td>&nbsp;</td>
+  </tr>
+
+
+
+
+
+
+
+
+
+ <tr>
+
+    <td valign="top">12.&nbsp;</td>
+    <td>
+
+    
+        <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Funiversal_de_ru.academic.ru%2F755953%2Frhetorisch" class='result-link'>rhetorisch - это... Что такое rhetorisch?</a>
+    
+    </td>
+  </tr>
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td class='result-snippet'>
+      Rhetorisch — Rhetorisch, 1) was nach Anleitung u. Zweck der Redekunst gefertigt ist od. in Verbindung mit derselben steht, z.B. Rhetorischer Ausdruck, <b>Rhetorische</b> Figuren (s.u. Figur 2) B); 2)...
+    </td>
+  </tr>
+  
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td>
+      <span class='link-text'>universal_de_ru.academic.ru/755953/rhetorisch</span>
+      
+    </td>
+  </tr>
+  
+
+  <tr>
+    <td>&nbsp;</td>
+    <td>&nbsp;</td>
+  </tr>
+
+
+
+
+
+
+
+
+
+ <tr>
+
+    <td valign="top">13.&nbsp;</td>
+    <td>
+
+    
+        <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.facebook.com%2Fafdnpd%2F" class='result-link'><b>Rhetorische</b> Perlen von AfD- und NPD-Anhängern und... | Facebook</a>
+    
+    </td>
+  </tr>
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td class='result-snippet'>
+      See more of <b>Rhetorische</b> Perlen von AfD- und NPD-Anhängern und Verschwörungstheoretikern on Facebook.
+    </td>
+  </tr>
+  
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td>
+      <span class='link-text'>www.facebook.com/afdnpd/</span>
+      
+    </td>
+  </tr>
+  
+
+  <tr>
+    <td>&nbsp;</td>
+    <td>&nbsp;</td>
+  </tr>
+
+
+
+
+
+
+
+
+
+ <tr>
+
+    <td valign="top">14.&nbsp;</td>
+    <td>
+
+    
+        <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Ftwitter.com%2Frhetorischea" class='result-link'><b>Rhetorische</b> Antwort (@RhetorischeA) | Твиттер</a>
+    
+    </td>
+  </tr>
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td class='result-snippet'>
+      Последние твиты от <b>Rhetorische</b> Antwort (@RhetorischeA). Es gibt keine dummen Antworten! #primatechangenotclimatechange.
+    </td>
+  </tr>
+  
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td>
+      <span class='link-text'>twitter.com/rhetorischea</span>
+      
+    </td>
+  </tr>
+  
+
+  <tr>
+    <td>&nbsp;</td>
+    <td>&nbsp;</td>
+  </tr>
+
+
+
+
+
+
+
+
+
+ <tr>
+
+    <td valign="top">15.&nbsp;</td>
+    <td>
+
+    
+        <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fde.thefreedictionary.com%2Frhetorisch" class='result-link'>Rhetorisch Übersetzung rhetorisch Definition auf TheFreeDictionary</a>
+    
+    </td>
+  </tr>
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td class='result-snippet'>
+      rhetorisch nicht steig. geh. die Rhetorik betreffend <b>rhetorische</b> Fähigkeiten haben/erwerben, eine rhetorisch gute Rede halten eine <b>rhetorische</b> Frage eine Frage, auf die man keine ernsthafte Antwort...
+    </td>
+  </tr>
+  
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td>
+      <span class='link-text'>de.thefreedictionary.com/rhetorisch</span>
+      
+    </td>
+  </tr>
+  
+
+  <tr>
+    <td>&nbsp;</td>
+    <td>&nbsp;</td>
+  </tr>
+
+
+
+
+
+
+
+
+
+ <tr>
+
+    <td valign="top">16.&nbsp;</td>
+    <td>
+
+    
+        <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.inhaltsangabe.de%2Fwissen%2Fstilmittel%2Frhetorische%2Dfrage%2F" class='result-link'><b>Rhetorische</b> Frage (Stilmittel) - Definition, Merkmale und Beispiele</a>
+    
+    </td>
+  </tr>
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td class='result-snippet'>
+      Die <b>rhetorische</b> Frage gehört zu den ältesten und meist genutzten Stilmitteln der Rhetorik. Zu finden sind <b>rhetorische</b> Fragen in der Politik, der Literatur, in der Werbung und im ganz alltäglichen...
+    </td>
+  </tr>
+  
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td>
+      <span class='link-text'>www.inhaltsangabe.de/wissen/stilmittel/rhetorische-frage/</span>
+      
+    </td>
+  </tr>
+  
+
+  <tr>
+    <td>&nbsp;</td>
+    <td>&nbsp;</td>
+  </tr>
+
+
+
+
+
+
+
+
+
+ <tr>
+
+    <td valign="top">17.&nbsp;</td>
+    <td>
+
+    
+        <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fplay.google.com%2Fstore%2Fapps%2Fdetails%3Fid%3Dclick.itkeller.lars.lernen%26hl%3Dsv" class='result-link'><b>Rhetorische</b>-Mittel-Deutsch - Appar på Google Play</a>
+    
+    </td>
+  </tr>
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td class='result-snippet'>
+      <b>Rhetorische</b>-Mittel-Deutsch. IT-KellerUtbildning. Ingen åldersgräns.
+    </td>
+  </tr>
+  
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td>
+      <span class='link-text'>play.google.com/store/apps/details?id=click.itkeller.lars.lernen&amp;hl=sv</span>
+      
+    </td>
+  </tr>
+  
+
+  <tr>
+    <td>&nbsp;</td>
+    <td>&nbsp;</td>
+  </tr>
+
+
+
+
+
+
+
+
+
+ <tr>
+
+    <td valign="top">18.&nbsp;</td>
+    <td>
+
+    
+        <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.frustfrei%2Dlernen.de%2Fdeutsch%2Frhetorik%2Dsprachliche%2Dmittel%2Dstillistische%2Dfiguren.html" class='result-link'>Rhetorik: Sprachliche Mittel und stilistische Figuren</a>
+    
+    </td>
+  </tr>
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td class='result-snippet'>
+      <b>Rhetorische</b> Frage Eine <b>rhetorische</b> Frage ist eine Frage, deren Antwort schon bekannt ist und welche durch die Fragestellung schon hervorgegangen ist. Hier wird durch die Wirkung der Frage die...
+    </td>
+  </tr>
+  
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td>
+      <span class='link-text'>www.frustfrei-lernen.de/deutsch/rhetorik-sprachliche-mittel-stillistische-figuren.html</span>
+      
+    </td>
+  </tr>
+  
+
+  <tr>
+    <td>&nbsp;</td>
+    <td>&nbsp;</td>
+  </tr>
+
+
+
+
+
+
+
+
+
+ <tr>
+
+    <td valign="top">19.&nbsp;</td>
+    <td>
+
+    
+        <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fvimeo.com%2Fgroups%2F156428" class='result-link'><b>Rhetorische</b> Stilmittel / sylistic devices of rhetoric on Vimeo</a>
+    
+    </td>
+  </tr>
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td class='result-snippet'>
+      1 Moderator. Related RSS Feeds. <b>Rhetorische</b> Stilmittel / sylistic devices of rhetoric. This is a Vimeo Group. Groups allow you to create mini communities around the things you like.
+    </td>
+  </tr>
+  
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td>
+      <span class='link-text'>vimeo.com/groups/156428</span>
+      
+    </td>
+  </tr>
+  
+
+  <tr>
+    <td>&nbsp;</td>
+    <td>&nbsp;</td>
+  </tr>
+
+
+
+
+
+
+
+
+
+ <tr>
+
+    <td valign="top">20.&nbsp;</td>
+    <td>
+
+    
+        <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.spektrum.de%2Fastrowissen%2Frhetorik.html" class='result-link'>Andreas Müller - Rhetorik | <b>Rhetorische</b> Figuren</a>
+    
+    </td>
+  </tr>
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td class='result-snippet'>
+      <b>Rhetorische</b> Mittel leisten diese Zielsetzung bei richtigem Gebrauch. Der Inhalt kann durch <b>rhetorische</b> Stilmittel Man differenziert <b>rhetorische</b> Figuren weiterhin in Figuren und Bilder: Die...
+    </td>
+  </tr>
+  
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td>
+      <span class='link-text'>www.spektrum.de/astrowissen/rhetorik.html</span>
+      
+    </td>
+  </tr>
+  
+
+  <tr>
+    <td>&nbsp;</td>
+    <td>&nbsp;</td>
+  </tr>
+
+
+
+
+
+
+
+
+
+ <tr>
+
+    <td valign="top">21.&nbsp;</td>
+    <td>
+
+    
+        <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.storyboardthat.com%2Fde%2Farticles%2Fe%2Fethos%2Dpathos%2Dlogos" class='result-link'>Ethos Pathos Logos | <b>Rhetorisches</b> Dreieck | Überredendes Schreiben</a>
+    
+    </td>
+  </tr>
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td class='result-snippet'>
+      Lernen Sie das <b>rhetorische</b> Dreieck von Ethos Pathos Logos mit lustigen und leicht verständlichen Storyboards. Ethos, Pathos und Logos sind wichtige Fähigkeiten für das Sprechen und...
+    </td>
+  </tr>
+  
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td>
+      <span class='link-text'>www.storyboardthat.com/de/articles/e/ethos-pathos-logos</span>
+      
+    </td>
+  </tr>
+  
+
+  <tr>
+    <td>&nbsp;</td>
+    <td>&nbsp;</td>
+  </tr>
+
+
+
+
+
+
+
+
+
+ <tr>
+
+    <td valign="top">22.&nbsp;</td>
+    <td>
+
+    
+        <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.bachelorprint.at%2Fwissenschaftliches%2Dschreiben%2Frhetorische%2Dmittel%2F" class='result-link'><b>Rhetorische</b> Mittel | Übersicht | Erklärungen | Beispiele</a>
+    
+    </td>
+  </tr>
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td class='result-snippet'>
+      <b>Rhetorische</b> Mittel: Erklärungen &amp; Beispiele | Übersicht: die wichtigsten rhetorischen Übersichts-PDF <b>rhetorische</b> Mittel. Noch nicht genug? Hier stellen wir dir eine ausführlichere Liste mit rhetorischen...
+    </td>
+  </tr>
+  
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td>
+      <span class='link-text'>www.bachelorprint.at/wissenschaftliches-schreiben/rhetorische-mittel/</span>
+      
+    </td>
+  </tr>
+  
+
+  <tr>
+    <td>&nbsp;</td>
+    <td>&nbsp;</td>
+  </tr>
+
+
+
+
+
+
+
+
+
+ <tr>
+
+    <td valign="top">23.&nbsp;</td>
+    <td>
+
+    
+        <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fquizlet.com%2F30344516%2Frhetorische%2Dfigurenstilmittel%2Dflash%2Dcards%2F" class='result-link'><b>Rhetorische</b> Figuren/Stilmittel Flashcards | Quizlet</a>
+    
+    </td>
+  </tr>
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td class='result-snippet'>
+      Start studying <b>Rhetorische</b> Figuren/Stilmittel. Learn vocabulary, terms and more with flashcards, games and other study tools. <b>Rhetorische</b> Figuren/Stilmittel. STUDY. Flashcards.
+    </td>
+  </tr>
+  
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td>
+      <span class='link-text'>quizlet.com/30344516/rhetorische-figurenstilmittel-flash-cards/</span>
+      
+    </td>
+  </tr>
+  
+
+  <tr>
+    <td>&nbsp;</td>
+    <td>&nbsp;</td>
+  </tr>
+
+
+
+
+
+
+
+
+
+ <tr>
+
+    <td valign="top">24.&nbsp;</td>
+    <td>
+
+    
+        <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.dict.cc%2F%3Fs%3Drhetorische" class='result-link'><b>rhetorische</b> | Übersetzung Englisch-Deutsch</a>
+    
+    </td>
+  </tr>
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td class='result-snippet'>
+      Deutsch-Englisch-Übersetzung für: <b>rhetorische</b>. ling. lit. rhetorical means. <b>rhetorische</b> Mittel {pl}. » Weitere 1 Übersetzungen für <b>rhetorische</b> innerhalb von Kommentaren.
+    </td>
+  </tr>
+  
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td>
+      <span class='link-text'>www.dict.cc/?s=rhetorische</span>
+      
+    </td>
+  </tr>
+  
+
+  <tr>
+    <td>&nbsp;</td>
+    <td>&nbsp;</td>
+  </tr>
+
+
+
+
+
+
+
+
+
+ <tr>
+
+    <td valign="top">25.&nbsp;</td>
+    <td>
+
+    
+        <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fdoclecture.net%2F1%2D48272.html" class='result-link'><b>Rhetorische</b> Figuren 94</a>
+    
+    </td>
+  </tr>
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td class='result-snippet'>
+      <b>Rhetorische</b> Figuren 94. (4) die Aneignung der Rede durch Auswendiglernen (Memoria f), (5) die Kunst der <b>rhetorische</b> Frage: 1.im eigentlichen Sinn (als eine ↑ Immutatio syntactica) Aussage in...
+    </td>
+  </tr>
+  
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td>
+      <span class='link-text'>doclecture.net/1-48272.html</span>
+      
+    </td>
+  </tr>
+  
+
+  <tr>
+    <td>&nbsp;</td>
+    <td>&nbsp;</td>
+  </tr>
+
+
+
+
+
+
+
+
+
+ <tr>
+
+    <td valign="top">26.&nbsp;</td>
+    <td>
+
+    
+        <a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.soft%2Dskills.com%2Frhetorische%2Dkompetenz%2F" class='result-link'><b>Rhetorische</b> Kompetenz im Soft Skills Würfel (Rhetorik)</a>
+    
+    </td>
+  </tr>
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td class='result-snippet'>
+      <b>Rhetorische</b> Kompetenz als Soft Skill ist die Summe von Redegewandtheit (Wortgewandtheit, Eloquenz) und Teilen der Soft Skills Präsentationskompetenz und Überzeugungsvermögen.
+    </td>
+  </tr>
+  
+
+  
+  <tr>
+    <td>&nbsp;&nbsp;&nbsp;</td>
+    <td>
+      <span class='link-text'>www.soft-skills.com/rhetorische-kompetenz/</span>
+      
+    </td>
+  </tr>
+  
+
+  <tr>
+    <td>&nbsp;</td>
+    <td>&nbsp;</td>
+  </tr>
+
+
+
+
+
+
+
+
+
+  
+    <tr>
+    <td colspan=2>
+      
+      
+        <form class="next_form" action="/lite/" method="post">
+          <!-- <a rel="next" href="/lite/?q=rhetorische&amp;v=l&amp;l=us-en&amp;p=-1&amp;s=30&amp;ex=-1&amp;o=json&amp;dl=en&amp;ct=AT&amp;ss_mkt=us&amp;sp=0&amp;vqd=3-317185285141429630887821832006479517976-203666168757858907862519566843911872030&amp;dc=27&amp;api=%2Fd.js">Next Page &gt;</a> //-->
+          <input type="submit" class='navbutton' value="Next Page &gt;">
+          <input type="hidden" name="q" value="rhetorische">
+          <input type="hidden" name="s" value="30">
+          <input type="hidden" name="o" value="json">
+          <input type="hidden" name="dc" value="27">
+          <input type="hidden" name="api" value="/d.js">
+          &nbsp;&nbsp;&nbsp;&nbsp;
+                
+                
+          
+          <input name="kl" value="wt-wt" type="hidden">
+                
+              
+                
+                
+        </form>
+      
+      
+    </td>
+  </tr>
+
+</table>
+
+<p class='extra'>&nbsp;</p>
+
+<form action="/lite/" method="post">
+  <input class='query' type="text" size="40" name="q" value="rhetorische" >
+  <input class='submit' type="submit" value="Search" >
+    
+    
+    
+    
+
+
+  <input name="kl" value="wt-wt" type="hidden">
+    
+    
+</form>
+
+<p class='extra'>&nbsp;</p>
+
+<!--
+<a href="#top">Top</a>
+//-->
+
+
+  <img src="//duckduckgo.com/t/sl_l"/>
+
+<div id='end-spacer'>&nbsp;</div>
+
+
+
+</body>
+</html>
diff --git a/termattrs.nim b/termattrs.nim
new file mode 100644
index 00000000..21fd3b04
--- /dev/null
+++ b/termattrs.nim
@@ -0,0 +1,12 @@
+import terminal
+
+type
+  TermAttributes* = object
+    termWidth*: int
+    termHeight*: int
+
+proc getTermAttributes*(): TermAttributes =
+  var t = TermAttributes()
+  t.termWidth = terminalWidth()
+  t.termHeight = terminalHeight()
+  return t
diff --git a/twtio.nim b/twtio.nim
new file mode 100644
index 00000000..cb59686c
--- /dev/null
+++ b/twtio.nim
@@ -0,0 +1,125 @@
+import terminal
+import tables
+import strutils
+
+import twtstr
+import config
+
+template print*(s: varargs[string, `$`]) =
+  for x in s:
+    stdout.write(x)
+
+template eprint*(s: varargs[string, `$`]) =
+  var a = false
+  for x in s:
+    if not a:
+      a = true
+    else:
+      stderr.write(' ')
+    stderr.write(x)
+  stderr.write('\n')
+
+proc termGoto*(x: int, y: int) =
+  setCursorPos(stdout, x, y)
+
+proc getNormalAction*(s: string): TwtAction =
+  if normalActionRemap.hasKey(s):
+    return normalActionRemap[s]
+  return NO_ACTION
+
+proc getLinedAction*(s: string): TwtAction =
+  if linedActionRemap.hasKey(s):
+    return linedActionRemap[s]
+  return NO_ACTION
+
+proc readLine*(prompt: string, current: var string): bool =
+  var new = current
+  print(prompt)
+  print(' ')
+  print(new)
+  var s = ""
+  var feedNext = false
+  var cursor = new.len
+  while true:
+    if not feedNext:
+      s = ""
+    else:
+      feedNext = false
+    let c = getch()
+    s &= c
+    let action = getLinedAction(s)
+    case action
+    of ACTION_LINED_CANCEL:
+      return false
+    of ACTION_LINED_SUBMIT:
+      current = new
+      return true
+    of ACTION_LINED_BACKSPACE:
+      if cursor > 0:
+        print(' '.repeat(new.len - cursor + 1))
+        print('\b'.repeat(new.len - cursor + 1))
+        print("\b \b")
+        new = new.substr(0, cursor - 2) & new.substr(cursor, new.len)
+        cursor -= 1
+        print(new.substr(cursor, new.len))
+        print('\b'.repeat(new.len - cursor))
+    of ACTION_LINED_ESC:
+      new &= c
+      print("^[".addAnsiFgColor(fgBlue).addAnsiStyle(styleBright))
+    of ACTION_LINED_CLEAR:
+      print(' '.repeat(new.len - cursor + 1))
+      print('\b'.repeat(new.len - cursor + 1))
+      print('\b'.repeat(cursor))
+      print(' '.repeat(cursor))
+      print('\b'.repeat(cursor))
+      new = new.substr(cursor, new.len)
+      print(new)
+      print('\b'.repeat(new.len))
+      cursor = 0
+    of ACTION_LINED_KILL:
+      print(' '.repeat(new.len - cursor + 1))
+      print('\b'.repeat(new.len - cursor + 1))
+      new = new.substr(0, cursor - 1)
+    of ACTION_LINED_BACK:
+      if cursor > 0:
+        cursor -= 1
+        print("\b")
+    of ACTION_LINED_FORWARD:
+      if cursor < new.len:
+        print(new[cursor])
+        cursor += 1
+    of ACTION_LINED_PREV_WORD:
+      while cursor > 0:
+        print('\b')
+        cursor -= 1
+        if new[cursor] == ' ':
+          break
+    of ACTION_LINED_NEXT_WORD:
+      while cursor < new.len:
+        print(new[cursor])
+        cursor += 1
+        if cursor < new.len and new[cursor] == ' ':
+          break
+    of ACTION_LINED_KILL_WORD:
+      var chars = 0
+      while cursor > chars:
+        chars += 1
+        if new[cursor - chars] == ' ':
+          break
+      if chars > 0:
+        print(' '.repeat(new.len - cursor + 1))
+        print('\b'.repeat(new.len - cursor + 1))
+        print("\b \b".repeat(chars))
+        new = new.substr(0, cursor - 1 - chars) & new.substr(cursor, new.len)
+        cursor -= chars
+        print(new.substr(cursor, new.len))
+        print('\b'.repeat(new.len - cursor))
+    of ACTION_FEED_NEXT:
+      feedNext = true
+    else:
+      print(' '.repeat(new.len - cursor + 1))
+      print('\b'.repeat(new.len - cursor + 1))
+      new = new.substr(0, cursor - 1) & c & new.substr(cursor, new.len)
+      print(new.substr(cursor, new.len))
+      print('\b'.repeat(new.len - cursor - 1))
+      cursor += 1
diff --git a/twtstr.nim b/twtstr.nim
new file mode 100644
index 00000000..5a41f3e3
--- /dev/null
+++ b/twtstr.nim
@@ -0,0 +1,28 @@
+import terminal
+import strutils
+
+func stripNewline*(str: string): string =
+  if str.len == 0:
+    result = str
+    return
+
+  case str[^1]
+  of '\n':
+    result = str.substr(0, str.len - 2)
+  else: discard
+
+func addAnsiStyle*(str: string, style: Style): string =
+  return ansiStyleCode(style) & str & "\e[0m"
+
+func addAnsiFgColor*(str: string, color: ForegroundColor): string =
+  return ansiForegroundColorCode(color) & str & "\e[0m"
+
+func maxString*(str: string, max: int): string =
+  if max < str.len:
+    return str.substr(0, max - 2) & "$"
+  return str
+
+func fitValueToSize*(str: string, size: int): string =
+  if str.len < size:
+    return str & ' '.repeat(size - str.len)
+  return str.maxString(size)