about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2021-08-05 22:57:38 +0200
committerbptato <nincsnevem662@gmail.com>2021-08-05 23:13:55 +0200
commitb94597a68eb8572cf8f521ee9c39cc7d9d310827 (patch)
treea487471466f80a4e95cfefb4d7084f29a840eacc
parent087f830528b41b00d0bf7a501f7b0472f75ffb18 (diff)
downloadchawan-b94597a68eb8572cf8f521ee9c39cc7d9d310827.tar.gz
Implement new buffer model which supports X coords
...also, one can kinda use this as a file viewer now I guess
-rw-r--r--res/config6
-rw-r--r--src/config.nim8
-rw-r--r--src/css/cssparser.nim3
-rw-r--r--src/css/style.nim2
-rw-r--r--src/io/buffer.nim511
-rw-r--r--src/io/display.nim109
-rw-r--r--src/io/term.nim (renamed from src/utils/termattrs.nim)3
-rw-r--r--src/io/twtio.nim17
-rw-r--r--src/main.nim20
-rw-r--r--src/utils/eprint.nim11
-rw-r--r--src/utils/radixtree.nim3
-rw-r--r--src/utils/twtstr.nim8
12 files changed, 334 insertions, 367 deletions
diff --git a/res/config b/res/config
index a4f5ebd0..c9a7c099 100644
--- a/res/config
+++ b/res/config
@@ -23,8 +23,12 @@ nmap C-f PAGE_DOWN
 nmap C-b PAGE_UP
 nmap M-[6~ PAGE_DOWN
 nmap M-[5~ PAGE_UP
+nmap > PAGE_RIGHT
+nmap < PAGE_LEFT
 nmap C-e SCROLL_DOWN
 nmap C-y SCROLL_UP
+nmap zh SCROLL_LEFT
+nmap zl SCROLL_RIGHT
 nmap J SCROLL_DOWN
 nmap K SCROLL_UP
 nmap C-m CLICK
@@ -37,7 +41,7 @@ nmap gg CURSOR_FIRST_LINE
 nmap G CURSOR_LAST_LINE
 nmap M-[H CURSOR_FIRST_LINE
 nmap M-[F CURSOR_LAST_LINE
-nmap z CENTER_LINE
+nmap zz CENTER_LINE
 nmap C-g LINE_INFO
 
 #line editing keybindings
diff --git a/src/config.nim b/src/config.nim
index 002532cc..41d4a6d4 100644
--- a/src/config.nim
+++ b/src/config.nim
@@ -15,9 +15,9 @@ type
     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_PAGE_DOWN, ACTION_PAGE_UP, ACTION_PAGE_LEFT, ACTION_PAGE_RIGHT,
     ACTION_HALF_PAGE_DOWN, ACTION_HALF_PAGE_UP,
-    ACTION_SCROLL_DOWN, ACTION_SCROLL_UP,
+    ACTION_SCROLL_DOWN, ACTION_SCROLL_UP, ACTION_SCROLL_LEFT, ACTION_SCROLL_RIGHT,
     ACTION_CLICK,
     ACTION_CHANGE_LOCATION,
     ACTION_RELOAD, ACTION_RESHAPE, ACTION_REDRAW,
@@ -62,7 +62,7 @@ func getRealKey(key: string): string =
     elif c == '-' and meta == 1:
       inc meta
     elif meta == 1:
-      realk &= 'C' & c
+      realk &= 'M' & c
       meta = 0
     elif meta == 2:
       realk &= '\e'
@@ -77,6 +77,8 @@ func getRealKey(key: string): string =
       realk &= c
   if control == 1:
     realk &= 'C'
+  if meta == 1:
+    realk &= 'M'
   return realk
 
 func constructActionTable*(origTable: ActionMap): ActionMap =
diff --git a/src/css/cssparser.nim b/src/css/cssparser.nim
index 5e76bcc0..732a7da2 100644
--- a/src/css/cssparser.nim
+++ b/src/css/cssparser.nim
@@ -9,9 +9,8 @@ import options
 import sequtils
 import sugar
 
-import ../io/twtio
-
 import ../utils/twtstr
+import ../utils/eprint
 
 import ../types/enums
 
diff --git a/src/css/style.nim b/src/css/style.nim
index d98c1f6c..6df59f22 100644
--- a/src/css/style.nim
+++ b/src/css/style.nim
@@ -2,9 +2,9 @@ import unicode
 import terminal
 import tables
 
-import ../io/twtio
 
 import ../utils/twtstr
+import ../utils/eprint
 
 import ../types/enums
 
diff --git a/src/io/buffer.nim b/src/io/buffer.nim
index 6d79700f..31c02aa9 100644
--- a/src/io/buffer.nim
+++ b/src/io/buffer.nim
@@ -7,27 +7,35 @@ import unicode
 import ../types/enums
 import ../types/color
 
-import ../utils/termattrs
 import ../utils/twtstr
+import ../utils/eprint
 
 import ../html/dom
 
 import ./twtio
+import ./term
 
 type
-  BufferCell = object
-    rune*: Rune
+  Cell = object of RootObj
     fgcolor*: CellColor
     bgcolor*: CellColor
     italic: bool
     bold: bool
     underline: bool
 
+  BufferCell = object of Cell
+    rune*: Rune
+
+# xterm supports max 2 characters per cell by default. might make the tuple a
+# seq in the future but for now it's fine like this
+  DisplayCell = object of Cell
+    runes*: tuple[a: Rune, b: Rune]
+
   Buffer* = ref BufferObj
   BufferObj = object
     title*: string
     lines*: seq[seq[BufferCell]]
-    display*: seq[BufferCell]
+    display*: seq[DisplayCell]
     hovertext*: string
     width*: int
     height*: int
@@ -38,7 +46,6 @@ type
     fromy*: int
     nodes*: seq[Node]
     links*: seq[Node]
-    clickables*: seq[Node]
     elements*: seq[Element]
     idelements*: Table[string, Element]
     selectedlink*: Node
@@ -46,82 +53,52 @@ type
     attrs*: TermAttributes
     document*: Document
     displaycontrols*: bool
+    redraw*: bool
+    location*: Uri
 
-    #TODO remove these
-    fmttext*: seq[string]
-    rawtext*: seq[string]
-
-proc newBuffer*(attrs: TermAttributes): Buffer =
+func newBuffer*(attrs: TermAttributes): Buffer =
   new(result)
   result.width = attrs.termWidth
   result.height = attrs.termHeight
   result.attrs = attrs
 
-  let cells = result.width * result.height
-  result.display = newSeq[BufferCell](cells)
-
-proc setText*(buffer: Buffer, x: int, y: int, text: seq[Rune]) =
-  discard
-
-proc setDisplayText(buffer: Buffer, x: int, y: int, text: seq[Rune]) =
-  let pos = y * buffer.width + x
-  var i = 0
-  while i < text.len:
-    buffer.display[pos + i].rune = text[i]
-
-proc refreshDisplay*(buffer: Buffer) =
-  var y = 0
-  for line in buffer.lines[buffer.fromy..buffer.fromy+buffer.height]:
-    var w = 0
-    var i = 0
-    while w < buffer.fromx and i < line.len:
-      w += line[i].rune.width()
-      inc i
-
-    let dls = y * buffer.width
-    var j = 0
-    while w < buffer.fromx + buffer.width and i < line.len:
-      w += line[i].rune.width()
-      buffer.display[dls + j] = line[i]
-      inc i
-
-    inc y
+  result.display = newSeq[DisplayCell](result.width * result.height)
 
 func generateFullOutput*(buffer: Buffer): string =
   var x = 0
   var y = 0
   for cell in buffer.display:
-    
-    discard
+    if x >= buffer.width:
+      inc y
+      result &= '\n'
+      x = 0
+
+    if cell.runes.a != Rune(0):
+      result &= $cell.runes.a
+    if cell.runes.b != Rune(0):
+      result &= $cell.runes.b
+    inc x
 
-#TODO go through these and remove ones that don't make sense in the new model
 func lastLine*(buffer: Buffer): int =
-  assert(buffer.fmttext.len == buffer.rawtext.len)
-  return buffer.fmttext.len - 1
+  return buffer.lines.len - 1
 
 func lastVisibleLine*(buffer: Buffer): int =
   return min(buffer.fromy + buffer.height - 1, buffer.lastLine())
 
-func currentLineLength*(buffer: Buffer): int =
-  return buffer.rawtext[buffer.cursory].width()
+func width(line: seq[BufferCell]): int =
+  for c in line:
+    result += c.rune.width()
 
-func atPercentOf*(buffer: Buffer): int =
-  if buffer.fmttext.len == 0: return 100
-  return (100 * (buffer.cursory + 1)) div (buffer.lastLine() + 1)
+func currentLineWidth*(buffer: Buffer): int =
+  return buffer.lines[buffer.cursory].width()
 
-func fmtBetween*(buffer: Buffer, sx: int, sy: int, ex: int, ey: int): string =
-  if sy < ey:
-    result &= buffer.rawtext[sy].runeSubstr(sx)
-    var i = sy + 1
-    while i < ey - 1:
-      result &= buffer.rawtext[i]
-      inc i
-    result &= buffer.rawtext[i].runeSubstr(0, ex - sx)
-  else:
-    result &= buffer.rawtext[sy].runeSubstr(sx, ex - sx)
+func maxScreenWidth*(buffer: Buffer): int =
+  for line in buffer.lines[buffer.fromy..buffer.lastVisibleLine()]:
+    result = max(line.width(), result)
 
-func visibleText*(buffer: Buffer): string = 
-  return buffer.fmttext[buffer.fromy..buffer.lastVisibleLine()].join("\n")
+func atPercentOf*(buffer: Buffer): int =
+  if buffer.lines.len == 0: return 100
+  return (100 * (buffer.cursory + 1)) div (buffer.lastLine() + 1)
 
 func lastNode*(buffer: Buffer): Node =
   return buffer.nodes[^1]
@@ -158,45 +135,17 @@ proc findSelectedNode*(buffer: Buffer): Option[Node] =
         return some(node)
   return none(Node)
 
-proc addNode*(buffer: Buffer, node: Node) =
-  buffer.nodes.add(node)
-
-  if node.isTextNode() and node.parentElement != nil and node.parentElement.getStyle().islink:
-    buffer.links.add(node)
-
-  if node.isElemNode():
-    case Element(node).tagType
-    of TAG_INPUT, TAG_OPTION:
-      if not Element(node).hidden:
-        buffer.clickables.add(node)
-    else: discard
-  elif node.isTextNode():
-    if node.parentElement != nil and node.getStyle().islink:
-      let anchor = node.ancestor(TAG_A)
-      assert(anchor != nil)
-      buffer.clickables.add(anchor)
-
-  if node.isElemNode():
-    let elem = Element(node)
-    buffer.elements.add(elem)
-    if elem.id != "" and not buffer.idelements.hasKey(elem.id):
-      buffer.idelements[elem.id] = elem
-
 proc writefmt*(buffer: Buffer, str: string) =
-  buffer.fmttext &= str
-  if buffer.printwrite:
-    stdout.write(str)
+  discard
 
 proc writefmt*(buffer: Buffer, c: char) =
-  buffer.rawtext &= $c
-  if buffer.printwrite:
-    stdout.write(c)
+  discard
 
 proc writeraw*(buffer: Buffer, str: string) =
-  buffer.rawtext &= str
+  discard
 
 proc writeraw*(buffer: Buffer, c: char) =
-  buffer.rawtext &= $c
+  discard
 
 proc write*(buffer: Buffer, str: string) =
   buffer.writefmt(str)
@@ -207,13 +156,11 @@ proc write*(buffer: Buffer, c: char) =
   buffer.writeraw(c)
 
 proc clearText*(buffer: Buffer) =
-  buffer.fmttext.setLen(0)
-  buffer.rawtext.setLen(0)
+  buffer.lines.setLen(0)
 
 proc clearNodes*(buffer: Buffer) =
   buffer.nodes.setLen(0)
   buffer.links.setLen(0)
-  buffer.clickables.setLen(0)
   buffer.elements.setLen(0)
   buffer.idelements.clear()
 
@@ -227,72 +174,77 @@ proc clearBuffer*(buffer: Buffer) =
   buffer.hovertext = ""
   buffer.selectedlink = nil
 
-proc scrollTo*(buffer: Buffer, y: int): bool =
+proc restoreCursorX(buffer: Buffer) =
+  buffer.cursorx = min(buffer.currentLineWidth() - 1, buffer.xend)
+
+proc scrollTo*(buffer: Buffer, y: int) =
   if y == buffer.fromy:
-    return false
+    return
   buffer.fromy = min(max(buffer.lastLine() - buffer.height + 1, 0), y)
   buffer.cursory = min(max(buffer.fromy, buffer.cursory), buffer.fromy + buffer.height)
-  return true
+  buffer.redraw = true
+  buffer.restoreCursorX()
 
-proc cursorTo*(buffer: Buffer, x: int, y: int): bool =
-  result = false
+proc cursorTo*(buffer: Buffer, x: int, y: int) =
+  buffer.redraw = false
   buffer.cursory = min(max(y, 0), buffer.lastLine())
   if buffer.fromy > buffer.cursory:
     buffer.fromy = max(buffer.cursory, 0)
-    result = true
+    buffer.redraw = true
   elif buffer.fromy + buffer.height - 1 <= buffer.cursory:
     buffer.fromy = max(buffer.cursory - buffer.height + 2, 0)
-    result = true
-  buffer.cursorx = min(max(x, 0), buffer.currentLineLength())
-  #buffer.fromX = min(max(buffer.currentLineLength() - buffer.width + 1, 0), 0) #TODO
+    buffer.redraw = true
+  buffer.cursorx = min(max(x, 0), buffer.currentLineWidth())
+  #buffer.fromX = min(max(buffer.currentLineWidth() - buffer.width + 1, 0), 0) #TODO
 
-proc cursorDown*(buffer: Buffer): bool =
+proc cursorDown*(buffer: Buffer) =
   if buffer.cursory < buffer.lastLine():
     inc buffer.cursory
-    if buffer.cursorx >= buffer.currentLineLength():
-      buffer.cursorx = max(buffer.currentLineLength() - 1, 0)
-    elif buffer.xend > 0:
-      buffer.cursorx = min(buffer.currentLineLength() - 1, buffer.xend)
+    buffer.restoreCursorX()
     if buffer.cursory >= buffer.lastVisibleLine() and buffer.lastVisibleLine() != buffer.lastLine():
       inc buffer.fromy
-      return true
-  return false
+      buffer.redraw = true
 
-proc cursorUp*(buffer: Buffer): bool =
+proc cursorUp*(buffer: Buffer) =
   if buffer.cursory > 0:
     dec buffer.cursory
-    if buffer.cursorx > buffer.currentLineLength():
-      if buffer.cursorx == 0:
-        buffer.xend = buffer.cursorx
-      buffer.cursorx = max(buffer.currentLineLength() - 1, 0)
-    elif buffer.xend > 0:
-      buffer.cursorx = min(buffer.currentLineLength() - 1, buffer.xend)
+    buffer.restoreCursorX()
     if buffer.cursory < buffer.fromy:
       dec buffer.fromy
-      return true
-  return false
+      buffer.redraw = true
 
-proc cursorRight*(buffer: Buffer): bool =
-  if buffer.cursorx < buffer.currentLineLength() - 1:
+proc cursorRight*(buffer: Buffer) =
+  if buffer.cursorx < buffer.currentLineWidth() - 1:
     inc buffer.cursorx
-    buffer.xend = 0
-  else:
     buffer.xend = buffer.cursorx
-  return false
-
-proc cursorLeft*(buffer: Buffer): bool =
-  if buffer.cursorx > 0:
+    if buffer.cursorx - buffer.width >= buffer.fromx:
+      inc buffer.fromx
+      buffer.redraw = true
+
+proc cursorLeft*(buffer: Buffer) =
+  if buffer.fromx > buffer.cursorx:
+    buffer.fromx = buffer.cursorx
+    buffer.redraw = true
+  elif buffer.cursorx > 0:
     dec buffer.cursorx
-  buffer.xend = 0
-  return false
+    if buffer.fromx > buffer.cursorx:
+      buffer.fromx = buffer.cursorx
+      buffer.redraw = true
+
+  buffer.xend = buffer.cursorx
 
 proc cursorLineBegin*(buffer: Buffer) =
   buffer.cursorx = 0
   buffer.xend = 0
+  if buffer.fromx > 0:
+    buffer.fromx = 0
+    buffer.redraw = true
 
 proc cursorLineEnd*(buffer: Buffer) =
-  buffer.cursorx = buffer.currentLineLength() - 1
+  buffer.cursorx = buffer.currentLineWidth() - 1
   buffer.xend = buffer.cursorx
+  buffer.fromx = max(buffer.cursorx - buffer.width + 1, 0)
+  buffer.redraw = buffer.fromx > 0
 
 iterator revnodes*(buffer: Buffer): Node {.inline.} =
   var i = buffer.nodes.len - 1
@@ -300,207 +252,174 @@ iterator revnodes*(buffer: Buffer): Node {.inline.} =
     yield buffer.nodes[i]
     dec i
 
-proc cursorNextWord*(buffer: Buffer): bool =
-  let llen = buffer.currentLineLength() - 1
-  var r: Rune
+proc cursorNextWord*(buffer: Buffer) =
+  let llen = buffer.currentLineWidth() - 1
   var x = buffer.cursorx
   var y = buffer.cursory
   if llen >= 0:
-    fastRuneAt(buffer.rawtext[y], x, r, false)
 
-    while r != Rune(' '):
+    while buffer.lines[y][x].rune != Rune(' '):
       if x >= llen:
         break
       inc x
-      fastRuneAt(buffer.rawtext[y], x, r, false)
 
-    while r == Rune(' '):
+    while buffer.lines[y][x].rune == Rune(' '):
       if x >= llen:
         break
       inc x
-      fastRuneAt(buffer.rawtext[y], x, r, false)
 
   if x >= llen:
     if y < buffer.lastLine():
       inc y
       x = 0
-  return buffer.cursorTo(x, y)
+  buffer.cursorTo(x, y)
 
-proc cursorPrevWord*(buffer: Buffer): bool =
-  var r: Rune
+proc cursorPrevWord*(buffer: Buffer) =
   var x = buffer.cursorx
   var y = buffer.cursory
-  if buffer.currentLineLength() > 0:
-    fastRuneAt(buffer.rawtext[y], x, r, false)
-
-    while r != Rune(' '):
+  if buffer.currentLineWidth() > 0:
+    while buffer.lines[y][x].rune != Rune(' '):
       if x == 0:
         break
       dec x
-      fastRuneAt(buffer.rawtext[y], x, r, false)
 
-    while r == Rune(' '):
+    while buffer.lines[y][x].rune == Rune(' '):
       if x == 0:
         break
       dec x
-      fastRuneAt(buffer.rawtext[y], x, r, false)
 
   if x == 0:
     if y > 0:
       dec y
-      x = buffer.rawtext[y].runeLen() - 1
-  return buffer.cursorTo(x, y)
+      x = buffer.lines[y].len - 1
+  buffer.cursorTo(x, y)
 
-iterator revclickables*(buffer: Buffer): Node {.inline.} =
-  var i = buffer.clickables.len - 1
-  while i >= 0:
-    yield buffer.clickables[i]
-    dec i
+proc cursorNextLink*(buffer: Buffer) =
+  #TODO
+  return
 
-proc cursorNextLink*(buffer: Buffer): bool =
-  for node in buffer.clickables:
-    if node.y > buffer.cursory or (node.y == buffer.cursorY and node.x > buffer.cursorx):
-      result = buffer.cursorTo(node.x, node.y)
-      if buffer.cursorx < buffer.currentLineLength():
-        var r: Rune
-        fastRuneAt(buffer.rawtext[buffer.cursory], buffer.cursorx, r, false)
-        if r == Rune(' '):
-          return result or buffer.cursorNextWord()
-      return result
-  return false
-
-proc cursorPrevLink*(buffer: Buffer): bool =
-  for node in buffer.revclickables:
-    if node.y < buffer.cursorY or (node.y == buffer.cursorY and node.x < buffer.cursorx):
-      return buffer.cursorTo(node.x, node.y)
-  return false
+proc cursorPrevLink*(buffer: Buffer) =
+  #TODO
+  return
 
-proc cursorFirstLine*(buffer: Buffer): bool =
+proc cursorFirstLine*(buffer: Buffer) =
   if buffer.fromy > 0:
     buffer.fromy = 0
-    result = true
+    buffer.redraw = true
   else:
-    result = false
+    buffer.redraw = false
 
-  buffer.cursorY = 0
-  buffer.cursorLineBegin()
+  buffer.cursory = 0
+  buffer.restoreCursorX()
 
-proc cursorLastLine*(buffer: Buffer): bool =
+proc cursorLastLine*(buffer: Buffer) =
   if buffer.fromy < buffer.lastLine() - buffer.height:
     buffer.fromy = buffer.lastLine() - (buffer.height - 2)
-    result = true
+    buffer.redraw = true
   else:
-    result = false
+    buffer.redraw = false
   buffer.cursory = buffer.lastLine()
-  buffer.cursorLineBegin()
+  buffer.restoreCursorX()
 
-proc cursorTop*(buffer: Buffer): bool =
-  buffer.cursorY = buffer.fromy
-  return false
+proc cursorTop*(buffer: Buffer) =
+  buffer.cursory = buffer.fromy
+  buffer.restoreCursorX()
 
-proc cursorMiddle*(buffer: Buffer): bool =
-  buffer.cursorY = min(buffer.fromy + (buffer.height - 2) div 2, buffer.lastLine())
-  return false
+proc cursorMiddle*(buffer: Buffer) =
+  buffer.cursory = min(buffer.fromy + (buffer.height - 2) div 2, buffer.lastLine())
+  buffer.restoreCursorX()
 
-proc cursorBottom*(buffer: Buffer): bool =
-  buffer.cursorY = min(buffer.fromy + buffer.height - 2, buffer.lastLine())
-  return false
+proc cursorBottom*(buffer: Buffer) =
+  buffer.cursory = min(buffer.fromy + buffer.height - 2, buffer.lastLine())
+  buffer.restoreCursorX()
 
-proc centerLine*(buffer: Buffer): bool =
+proc centerLine*(buffer: Buffer) =
   let ny = max(min(buffer.cursory - buffer.height div 2, buffer.lastLine() - buffer.height + 2), 0)
   if ny != buffer.fromy:
     buffer.fromy = ny
-    return true
-  return false
+    buffer.redraw = true
 
-proc halfPageUp*(buffer: Buffer): bool =
-  buffer.cursory = max(buffer.cursorY - buffer.height div 2 + 1, 0)
+proc halfPageUp*(buffer: Buffer) =
+  buffer.cursory = max(buffer.cursory - buffer.height div 2 + 1, 0)
   let nfy = max(0, buffer.fromy - buffer.height div 2 + 1)
   if nfy != buffer.fromy:
     buffer.fromy = nfy
-    return true
-  return false
+    buffer.redraw = true
+  buffer.restoreCursorX()
 
-proc halfPageDown*(buffer: Buffer): bool =
-  buffer.cursory = min(buffer.cursorY + buffer.height div 2 - 1, buffer.lastLine())
+proc halfPageDown*(buffer: Buffer) =
+  buffer.cursory = min(buffer.cursory + buffer.height div 2 - 1, buffer.lastLine())
   let nfy = min(max(buffer.lastLine() - buffer.height + 2, 0), buffer.fromy + buffer.height div 2 - 1)
   if nfy != buffer.fromy:
     buffer.fromy = nfy
-    return true
-  return false
+    buffer.redraw = true
+  buffer.restoreCursorX()
 
-proc pageUp*(buffer: Buffer): bool =
-  buffer.cursorY = max(buffer.cursorY - buffer.height + 1, 1)
+proc pageUp*(buffer: Buffer) =
+  buffer.cursory = max(buffer.cursory - buffer.height + 1, 1)
   buffer.fromy = max(0, buffer.fromy - buffer.height)
-  return true
+  buffer.redraw = true
+  buffer.restoreCursorX()
 
-proc pageDown*(buffer: Buffer): bool =
-  buffer.cursorY = min(buffer.cursorY + buffer.height div 2 - 1, buffer.lastLine())
+proc pageDown*(buffer: Buffer) =
+  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
+  buffer.redraw = true
+  buffer.restoreCursorX()
+
+proc pageLeft*(buffer: Buffer) =
+  buffer.cursorx = max(buffer.cursorx - buffer.width, 0)
+  buffer.fromx = max(0, buffer.fromx - buffer.width)
+  buffer.redraw = true
+
+proc pageRight*(buffer: Buffer) =
+  buffer.cursorx = min(buffer.fromx, buffer.currentLineWidth())
+  buffer.fromx = min(max(buffer.maxScreenWidth() - buffer.width, 0), buffer.fromx + buffer.width)
+  buffer.redraw = true
 
-proc scrollDown*(buffer: Buffer): bool =
+proc scrollDown*(buffer: Buffer) =
   if buffer.fromy + buffer.height - 1 <= buffer.lastLine():
     inc buffer.fromy
-    if buffer.fromy >= buffer.cursory:
-      discard buffer.cursorDown()
-    return true
-  discard buffer.cursorDown()
-  return false
+    if buffer.fromy > buffer.cursory:
+      buffer.cursorDown()
+    buffer.redraw = true
+  else:
+    buffer.cursorDown()
 
-proc scrollUp*(buffer: Buffer): bool =
+proc scrollUp*(buffer: Buffer) =
   if buffer.fromy > 0:
     dec buffer.fromy
-    if buffer.fromy + buffer.height - 1 <= 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:
-      let anchor = buffer.selectedlink.ancestor(TAG_A)
-      buffer.selectedlink.fmttext = buffer.selectedlink.getFmtText()
-      buffer.selectedlink = nil
-      buffer.hovertext = ""
-      var stack: seq[Node]
-      stack.add(anchor)
-      while stack.len > 0:
-        let elem = stack.pop()
-        elem.fmttext = elem.getFmtText()
-        for child in elem.childNodes:
-          stack.add(child)
-  for node in buffer.links:
-    if buffer.cursorOnNode(node):
-      buffer.selectedlink = node
-      let anchor = node.ancestor(TAG_A)
-      assert(anchor != nil)
-      buffer.hovertext = HtmlAnchorElement(anchor).href
-      var stack: seq[Node]
-      stack.add(anchor)
-      while stack.len > 0:
-        let elem = stack.pop()
-        elem.fmttext = elem.getFmtText()
-        for child in elem.childNodes:
-          stack.add(child)
-      return true
-  return false
+    if buffer.fromy + buffer.height - 1 <= buffer.cursory:
+      buffer.cursorUp()
+    buffer.redraw = true
+  else:
+    buffer.cursorUp()
+
+proc scrollRight*(buffer: Buffer) =
+  if buffer.fromx + buffer.width < buffer.maxScreenWidth():
+    inc buffer.fromx
+    if buffer.fromx >= buffer.cursorx:
+      buffer.cursorRight()
+    buffer.redraw = true
+
+proc scrollLeft*(buffer: Buffer) =
+  if buffer.fromx > 0:
+    dec buffer.fromx
+    if buffer.fromx + buffer.height <= buffer.cursorx:
+      buffer.cursorLeft()
+    buffer.redraw = true
 
 proc gotoAnchor*(buffer: Buffer): bool =
-  if buffer.document.location.anchor != "":
-    let node =  buffer.getElementById(buffer.document.location.anchor)
+  if buffer.location.anchor != "":
+    let node =  buffer.getElementById(buffer.location.anchor)
     if node != nil:
-      return buffer.scrollTo(max(node.y - buffer.height div 2, 0))
-  return false
+      buffer.scrollTo(max(node.y - buffer.height div 2, 0))
 
 proc setLocation*(buffer: Buffer, uri: Uri) =
-  buffer.document.location = uri
+  buffer.location = uri
 
 proc gotoLocation*(buffer: Buffer, uri: Uri) =
-  buffer.document.location = buffer.document.location.combine(uri)
+  buffer.location = buffer.location.combine(uri)
 
 proc refreshTermAttrs*(buffer: Buffer): bool =
   let newAttrs = getTermAttributes()
@@ -510,3 +429,77 @@ proc refreshTermAttrs*(buffer: Buffer): bool =
     buffer.height = newAttrs.termHeight
     return true
   return false
+
+proc setText*(buffer: Buffer, x: int, y: int, text: seq[Rune]) =
+  while buffer.lines.len <= y:
+    buffer.lines.add(newSeq[BufferCell]())
+
+  while buffer.lines[y].len < x + text.len:
+    buffer.lines[y].add(BufferCell())
+  
+  var i = 0
+  while i < text.len:
+    buffer.lines[y][i].rune = text[i]
+    inc i
+
+proc setDisplayText(buffer: Buffer, x: int, y: int, text: seq[Rune]) =
+  let pos = y * buffer.width + x
+  var i = 0
+  var n = 0
+  while i < text.len:
+    if text[i].width() == 0:
+      buffer.display[pos + i - n].runes.b = text[i]
+      inc n
+    else:
+      buffer.display[pos + i - n].runes.a = text[i]
+    inc i
+
+proc reshape*(buffer: Buffer) =
+  buffer.display = newSeq[DisplayCell](buffer.width * buffer.height)
+
+proc refreshDisplay*(buffer: Buffer) =
+  var y = 0
+  for line in buffer.lines[buffer.fromy..buffer.lastVisibleLine()]:
+    var w = 0
+    var i = 0
+    var n = 0
+    while w < buffer.fromx and i < line.len:
+      w += line[i].rune.width()
+      inc i
+
+    let dls = y * buffer.width
+    var j = 0
+    while w < buffer.fromx + buffer.width and i < line.len:
+      w += line[i].rune.width()
+      if line[i].rune.width() == 0:
+        buffer.display[dls + j].runes.b = line[i].rune
+      else:
+        buffer.display[dls + j].runes.a = line[i].rune
+      inc i
+      inc j
+
+    while j < buffer.width:
+      buffer.display[dls + j].runes.a = Rune(0)
+      buffer.display[dls + j].runes.b = Rune(0)
+      inc j
+
+    inc y
+
+proc renderPlainText*(buffer: Buffer, text: string) =
+  var i = 0
+  var y = 0
+  var line = ""
+  while i < text.len:
+    if text[i] == '\n':
+      buffer.setText(0, y, line.toRunes())
+      inc y
+      line = ""
+    elif text[i] == '\t':
+      line &= ' '.repeat(8)
+    else:
+      line &= text[i]
+    inc i
+  if line.len > 0:
+    buffer.setText(0, y, line.toRunes())
+
+  buffer.refreshDisplay()
diff --git a/src/io/display.nim b/src/io/display.nim
index d4c772d6..6f7be014 100644
--- a/src/io/display.nim
+++ b/src/io/display.nim
@@ -6,7 +6,6 @@ import unicode
 
 import ../types/enums
 
-import ../utils/termattrs
 import ../utils/twtstr
 
 import ../html/dom
@@ -15,6 +14,7 @@ import ../config
 
 import ./buffer
 import ./twtio
+import ./term
 
 proc clearStatusMsg*(at: int) =
   setCursorPos(0, at)
@@ -243,7 +243,6 @@ proc renderHtml*(buffer: Buffer) =
   var state = newRenderState()
   while stack.len > 0:
     let currElem = stack.pop()
-    buffer.addNode(currElem)
     buffer.renderNode(currElem, state)
     var i = currElem.childNodes.len - 1
     while i >= 0:
@@ -267,20 +266,21 @@ proc statusMsgForBuffer(buffer: Buffer) =
   statusMsg(msg.maxString(buffer.width), buffer.height)
 
 proc cursorBufferPos(buffer: Buffer) =
-  var x = buffer.cursorx
-  var y = buffer.cursory - 1 - buffer.fromY
-  termGoto(x, y + 1)
+  var x = max(buffer.cursorx - buffer.fromx, 0)
+  var y = buffer.cursory - buffer.fromy
+  termGoto(x, y)
 
 proc displayBuffer(buffer: Buffer) =
   eraseScreen()
   termGoto(0, 0)
 
-  print(buffer.visibleText().ansiReset())
+  print(buffer.generateFullOutput().ansiReset())
 
 proc inputLoop(attrs: TermAttributes, buffer: Buffer): bool =
   var s = ""
   var feedNext = false
   while true:
+    buffer.redraw = false
     stdout.showCursor()
     buffer.cursorBufferPos()
     if not feedNext:
@@ -296,46 +296,38 @@ proc inputLoop(attrs: TermAttributes, buffer: Buffer): bool =
     case action
     of ACTION_QUIT:
       eraseScreen()
+      setCursorPos(0, 0)
       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_LEFT: buffer.cursorLeft()
+    of ACTION_CURSOR_DOWN: buffer.cursorDown()
+    of ACTION_CURSOR_UP: buffer.cursorUp()
+    of ACTION_CURSOR_RIGHT: 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_PREV_WORD: redraw = buffer.cursorPrevWord()
-    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_CENTER_LINE: redraw = buffer.centerLine()
-    of ACTION_SCROLL_DOWN: redraw = buffer.scrollDown()
-    of ACTION_SCROLL_UP: redraw = buffer.scrollUp()
+    of ACTION_CURSOR_NEXT_WORD: buffer.cursorNextWord()
+    of ACTION_CURSOR_PREV_WORD: buffer.cursorPrevWord()
+    of ACTION_CURSOR_NEXT_LINK: buffer.cursorNextLink()
+    of ACTION_CURSOR_PREV_LINK: buffer.cursorPrevLink()
+    of ACTION_PAGE_DOWN: buffer.pageDown()
+    of ACTION_PAGE_UP: buffer.pageUp()
+    of ACTION_PAGE_RIGHT: buffer.pageRight()
+    of ACTION_PAGE_LEFT: buffer.pageLeft()
+    of ACTION_HALF_PAGE_DOWN: buffer.halfPageDown()
+    of ACTION_HALF_PAGE_UP: buffer.halfPageUp()
+    of ACTION_CURSOR_FIRST_LINE: buffer.cursorFirstLine()
+    of ACTION_CURSOR_LAST_LINE: buffer.cursorLastLine()
+    of ACTION_CURSOR_TOP: buffer.cursorTop()
+    of ACTION_CURSOR_MIDDLE: buffer.cursorMiddle()
+    of ACTION_CURSOR_BOTTOM: buffer.cursorBottom()
+    of ACTION_CENTER_LINE: buffer.centerLine()
+    of ACTION_SCROLL_DOWN: buffer.scrollDown()
+    of ACTION_SCROLL_UP: buffer.scrollUp()
+    of ACTION_SCROLL_LEFT: buffer.scrollLeft()
+    of ACTION_SCROLL_RIGHT: buffer.scrollRight()
     of ACTION_CLICK:
-      let selectedElem = buffer.findSelectedElement()
-      if selectedElem.isSome:
-        case selectedElem.get().tagType
-        of TAG_INPUT:
-          clearStatusMsg(buffer.height)
-          let status = readLine("TEXT: ", HtmlInputElement(selectedElem.get()).value, buffer.width)
-          if status:
-            reshape = true
-            redraw = true
-        else: discard
-        if selectedElem.get().getStyle().islink:
-          let anchor = HtmlAnchorElement(buffer.selectedlink.ancestor(TAG_A)).href
-          buffer.gotoLocation(parseUri(anchor))
-          return true
+      discard
     of ACTION_CHANGE_LOCATION:
-      var url = $buffer.document.location
+      var url = $buffer.location
 
       clearStatusMsg(buffer.height)
       let status = readLine("URL: ", url, buffer.width)
@@ -343,7 +335,7 @@ proc inputLoop(attrs: TermAttributes, buffer: Buffer): bool =
         buffer.setLocation(parseUri(url))
         return true
     of ACTION_LINE_INFO:
-      statusMsg("line " & $buffer.cursory & "/" & $buffer.lastLine() & " col " & $(buffer.cursorx + 1) & "/" & $buffer.currentLineLength(), buffer.width)
+      statusMsg("line " & $buffer.cursory & "/" & $buffer.lastLine() & " col " & $(buffer.cursorx + 1) & "/" & $buffer.currentLineWidth(), buffer.width)
       nostatus = true
     of ACTION_FEED_NEXT:
       feedNext = true
@@ -355,40 +347,17 @@ proc inputLoop(attrs: TermAttributes, buffer: Buffer): bool =
     else: discard
     stdout.hideCursor()
 
-    let prevlink = buffer.selectedlink
-    let sel = buffer.checkLinkSelection()
-    if sel:
-      buffer.clearText()
-      buffer.drawHtml()
-      termGoto(0, buffer.selectedlink.y - buffer.fromy)
-      stdout.eraseLine()
-      for i in buffer.selectedlink.y..buffer.selectedlink.ey:
-        if i < buffer.fromy + buffer.height - 1:
-          let line = buffer.fmttext[i]
-          print(line)
-          print('\n')
-      print("".ansiReset())
-    
-    if prevlink != nil:
-      buffer.clearText()
-      buffer.drawHtml()
-      termGoto(0, prevlink.y - buffer.fromy)
-      for i in prevlink.y..prevlink.ey:
-        if i < buffer.fromy + buffer.height - 1:
-          let line = buffer.fmttext[i]
-          stdout.eraseLine()
-          print(line)
-          print('\n')
-      print("".ansiReset())
-
     if buffer.refreshTermAttrs():
       redraw = true
       reshape = true
 
+    if buffer.redraw:
+      redraw = true
+
     if reshape:
-      buffer.clearText()
-      buffer.drawHtml()
+      buffer.reshape()
     if redraw:
+      buffer.refreshDisplay()
       buffer.displayBuffer()
 
     if not nostatus:
diff --git a/src/utils/termattrs.nim b/src/io/term.nim
index d49800ae..5103c153 100644
--- a/src/utils/termattrs.nim
+++ b/src/io/term.nim
@@ -9,3 +9,6 @@ proc getTermAttributes*(): TermAttributes =
   let attrs = TermAttributes(termWidth: terminalWidth(),
                              termHeight: terminalHeight())
   return attrs
+
+proc termGoto*(x: int, y: int) =
+  setCursorPos(stdout, x, y)
diff --git a/src/io/twtio.nim b/src/io/twtio.nim
index 4e2789b0..8a8bef1c 100644
--- a/src/io/twtio.nim
+++ b/src/io/twtio.nim
@@ -1,4 +1,4 @@
-import terminal
+import std/terminal
 import tables
 import unicode
 import strutils
@@ -10,6 +10,8 @@ import ../utils/radixtree
 
 import ../config
 
+import ./terminal
+
 template print*(s: varargs[string, `$`]) =
   for x in s:
     stdout.write(x)
@@ -22,19 +24,6 @@ template printesc*(s: string) =
     else:
       stdout.write($r)
 
-template eprint*(s: varargs[string, `$`]) = {.cast(noSideEffect).}:
-  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]
diff --git a/src/main.nim b/src/main.nim
index 7912a431..78d24e8d 100644
--- a/src/main.nim
+++ b/src/main.nim
@@ -3,14 +3,14 @@ import uri
 import os
 import streams
 
-import utils/termattrs
+import utils/eprint
 
 import html/dom
 import html/htmlparser
 
 import io/display
-import io/twtio
 import io/buffer
+import io/term
 
 import config
 
@@ -43,19 +43,21 @@ proc main*() =
     eprint "Invalid parameters. Usage:\ntwt <url>"
     quit(1)
   if not readConfig("res/config"):
-    eprint "Failed to read keymap, fallback to default"
+    #eprint "Failed to read keymap, fallback to default"
+    discard
   let attrs = getTermAttributes()
   let buffer = newBuffer(attrs)
   let uri = parseUri(paramStr(1))
   buffers.add(buffer)
-  buffer.document = parseHtml(getPageUri(uri))
-  buffer.document.applyDefaultStylesheet()
-  buffer.setLocation(uri)
-  buffer.renderHtml()
+  buffer.renderPlainText(getPageUri(uri).readAll())
+  #buffer.document = parseHtml(getPageUri(uri))
+  #buffer.setLocation(uri)
+  #buffer.document.applyDefaultStylesheet()
+  #buffer.renderHtml()
   var lastUri = uri
   while displayPage(attrs, buffer):
     statusMsg("Loading...", buffer.height)
-    var newUri = buffer.document.location
+    var newUri = buffer.location
     lastUri.anchor = ""
     newUri.anchor = ""
     if $lastUri != $newUri:
@@ -63,7 +65,7 @@ proc main*() =
       if uri.scheme == "" and uri.path == "" and uri.anchor != "":
         discard
       else:
-        buffer.document = parseHtml(getPageUri(buffer.document.location))
+        buffer.document = parseHtml(getPageUri(buffer.location))
       buffer.renderHtml()
     lastUri = newUri
 main()
diff --git a/src/utils/eprint.nim b/src/utils/eprint.nim
new file mode 100644
index 00000000..23248363
--- /dev/null
+++ b/src/utils/eprint.nim
@@ -0,0 +1,11 @@
+{.used.}
+
+template eprint*(s: varargs[string, `$`]) = {.cast(noSideEffect).}:
+  var a = false
+  for x in s:
+    if not a:
+      a = true
+    else:
+      stderr.write(' ')
+    stderr.write(x)
+  stderr.write('\n')
diff --git a/src/utils/radixtree.nim b/src/utils/radixtree.nim
index dd0a6a1d..b361d367 100644
--- a/src/utils/radixtree.nim
+++ b/src/utils/radixtree.nim
@@ -1,6 +1,5 @@
 # Radix tree implementation. It isn't that much faster than a hash table,
-# however it *is* faster. Use StaticRadixTree for saving trees in the
-# executable and RadixNode otherwise (which needs less bounds checking).
+# however it *is* faster.
 
 import json
 import tables
diff --git a/src/utils/twtstr.nim b/src/utils/twtstr.nim
index 730f8b32..22207e2c 100644
--- a/src/utils/twtstr.nim
+++ b/src/utils/twtstr.nim
@@ -211,8 +211,7 @@ iterator split*(s: seq[Rune], sep: Rune): seq[Rune] =
 #
 #   - The null character (U+0000) has a column width of 0.
 #
-#   - Other C0/C1 control characters and DEL will lead to a return value of 2
-#     (changed from 0 b/c we normally display control chars like ^H - TODO?).
+#   - Other C0/C1 control characters and DEL will lead to a return value of 0
 #
 #   - Non-spacing and enclosing combining characters (general category code Mn
 #     or Me in the Unicode database) have a column width of 0.
@@ -305,7 +304,7 @@ func is_dwidth(r: Rune): bool =
 func makewidthtable(): array[0..0x10FFFF, byte] =
   for r in low(char)..high(char):
     if r.isControlChar():
-      result[int(r)] = 2
+      result[int(r)] = 2 #TODO this should be 0
     else:
       result[int(r)] = 1
 
@@ -545,6 +544,3 @@ proc fullwidth*(s: seq[Rune]): seq[Rune] =
 
 proc fullwidth*(s: string): string =
   return $fullwidth(s.toRunes())
-
-echo (halfwidth("とうギょう"))
-echo "東京"