about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2021-12-05 19:55:13 +0100
committerbptato <nincsnevem662@gmail.com>2021-12-05 19:55:13 +0100
commit31117cad22df103f84cfbe97dff08debcde72a66 (patch)
tree5f9e4b0515e63e953758fe10da9fc52d45171e0f
parent0f9c94abf5ff9e4f2ce4d8d49424c9458bbf6229 (diff)
downloadchawan-31117cad22df103f84cfbe97dff08debcde72a66.tar.gz
Change configuration format to toml
-rw-r--r--doc/config.md30
-rw-r--r--nim.cfg1
-rw-r--r--readme.md10
-rw-r--r--res/config64
-rw-r--r--res/config.toml63
-rw-r--r--res/ua.css (renamed from res/default.css)0
-rw-r--r--src/config/config.nim114
-rw-r--r--src/config/toml.nim406
-rw-r--r--src/css/parser.nim48
-rw-r--r--src/io/buffer.nim2
-rw-r--r--src/io/pipe.nim0
-rw-r--r--src/utils/twtstr.nim88
12 files changed, 613 insertions, 213 deletions
diff --git a/doc/config.md b/doc/config.md
index 14a34608..6b3efd17 100644
--- a/doc/config.md
+++ b/doc/config.md
@@ -1,6 +1,7 @@
 # Configuration
 
-Currently keybindings and a user stylesheet can be configured.
+Currently keybindings and a user stylesheet can be configured. The
+configuration format for twt is toml.
 
 twt will look for a config file in the ~/.config/twt/ directory, so you can
 just copy the one from res/ there and customize that to your liking.
@@ -9,26 +10,31 @@ A list of configurable options follows.
 
 ## Stylesheets
 
-To set a user stylesheet, use the format `stylesheet "path-to-user.css"`.
+To set a user stylesheet, use the format `stylesheet = "path-to-user.css"`.
 Relative paths are interpreted as relative to the config directory.
 
-For now, specifying a second stylesheet will override the first one.
+Specifying a second stylesheet will override the first one.
 
 ## Keybindings
-"Normal" (default pager browsing) mode keybindings are configured using the
-syntax
 
-	nmap <keybinding> <action>
+To specify a keybinding, you will first have to specify a mode:
 
-Similarly, "Line-edit" (line editing) mode keybindings are configured
-using the syntax
+* for page browsing: [page]
+* for line editing: [line]
 
-	lemap <keybinding> <action>
+Keybindings are configured using the syntax
+
+	'<keybinding>' = '<action>'
 
 Where `<keybinding>` is a combination of unicode characters with or without
 modifiers. Modifiers are the prefixes `C-` and `M-`, which add control or
 escape to the keybinding respectively (essentially making `M-` the same as
-`C-[`).
+`C-[`). Modifiers can be escaped with the `\` sign.
+
+```Example:
+'C-M-j' = 'CHANGE_LOCATION' # change URL when Control, Escape and j are pressed
+'gg' = 'CURSOR_FIRST_LINE' # go to the first line of the page when g is pressed twice
+```
 
 `<action>` is a valid normal or line-edit mode action. A detailed
 description of these follows.
@@ -37,7 +43,7 @@ description of these follows.
 
 <table>
 <tr><th>**Name**<th>**Function**
-<tr><td>`NULL`<td>Do nothing
+<tr><td>`NULL`<td>Do nothing (used for disabling default keybindings)
 <tr><td>`QUIT`<td>Exit the browser
 <tr><td>`CURSOR_UP`<td>Move the cursor to the previous line
 <tr><td>`CURSOR_DOWN`<td>Move cursor to the next line
@@ -75,7 +81,7 @@ description of these follows.
 <tr><td>`LINE_INFO`<td>Display information about line
 </table>
 
-### Line-edit mode actions
+### Line-editing actions
 
 <table>
 <tr><th>**Name**<th>**Function**
diff --git a/nim.cfg b/nim.cfg
index 5fb94fee..c370ac8b 100644
--- a/nim.cfg
+++ b/nim.cfg
@@ -1,3 +1,4 @@
 -p:"."
 -p:"src/"
+-p:"lib/"
 --import:"utils/eprint"
diff --git a/readme.md b/readme.md
index f1e01aef..7f886430 100644
--- a/readme.md
+++ b/readme.md
@@ -18,13 +18,11 @@ I've found other terminal web browsers insufficient for my needs, so I thought
 it'd be a fun excercise to write one by myself, for myself.
 
 The end result will of course not support nearly as many websites as Firefox or
-Chromium (so forget PWAs I guess), but I'd like it to be at least somewhat more
-functional on the "modern web" than w3m or lynx.
+Chromium, but I'd like it to be at least somewhat more functional on the
+"modern web" than w3m or lynx.
 
-While the original idea was to implement something similar to w3m's rendering
-with JS and minimal CSS support, I've got a bit carried away with my CSS parser
-so the new plan is to mostly implement basic CSS stuff and then JS with the
-most important APIs. Plus some other things.
+The plan is to mostly implement basic CSS stuff and then JS with the most
+important APIs. Plus some other things.
 
 ## So what can this do?
 
diff --git a/res/config b/res/config
deleted file mode 100644
index 0f4e5603..00000000
--- a/res/config
+++ /dev/null
@@ -1,64 +0,0 @@
-#normal mode keybindings
-nmap q QUIT
-nmap h CURSOR_LEFT
-nmap j CURSOR_DOWN
-nmap k CURSOR_UP
-nmap l CURSOR_RIGHT
-nmap M-[D CURSOR_LEFT
-nmap M-[B CURSOR_DOWN
-nmap M-[A CURSOR_UP
-nmap M-[C CURSOR_RIGHT
-nmap ^ CURSOR_LINEBEGIN
-nmap $ CURSOR_LINEEND
-nmap b CURSOR_PREV_WORD
-nmap w CURSOR_NEXT_WORD
-nmap [ CURSOR_PREV_LINK
-nmap ] CURSOR_NEXT_LINK
-nmap H CURSOR_TOP
-nmap M CURSOR_MIDDLE
-nmap L CURSOR_BOTTOM
-nmap C-d HALF_PAGE_DOWN
-nmap C-u HALF_PAGE_UP
-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 J SCROLL_DOWN
-nmap K SCROLL_UP
-nmap ( SCROLL_LEFT
-nmap ) SCROLL_RIGHT
-nmap C-m CLICK
-nmap C-j CLICK
-nmap C-l CHANGE_LOCATION
-nmap U RELOAD
-nmap r RESHAPE
-nmap R REDRAW
-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 C-g LINE_INFO
-nmap gh TOGGLE_SOURCE
-
-#line editing keybindings
-lemap C-m SUBMIT
-lemap C-j SUBMIT
-lemap C-h BACKSPACE
-lemap C-? BACKSPACE
-lemap C-d DELETE
-lemap C-c CANCEL
-lemap M-b PREV_WORD
-lemap M-f NEXT_WORD
-lemap C-b BACK
-lemap C-f FORWARD
-lemap C-u CLEAR
-lemap C-k KILL
-lemap C-w KILL_WORD
-lemap C-a BEGIN
-lemap C-e END
-lemap C-v ESC
diff --git a/res/config.toml b/res/config.toml
new file mode 100644
index 00000000..520d871a
--- /dev/null
+++ b/res/config.toml
@@ -0,0 +1,63 @@
+[page]
+'C-M-j' = 'QUIT'
+q = 'QUIT'
+h = 'CURSOR_LEFT' 
+j = 'CURSOR_DOWN'
+k = 'CURSOR_UP'
+l = 'CURSOR_RIGHT'
+'M-[D' = 'CURSOR_LEFT'
+'M-[B' = 'CURSOR_DOWN'
+'M-[A' = 'CURSOR_UP'
+'M-[C' = 'CURSOR_RIGHT'
+'^' = 'CURSOR_LINEBEGIN'
+'$' = 'CURSOR_LINEEND'
+b = 'CURSOR_PREV_WORD'
+w = 'CURSOR_NEXT_WORD'
+'[' = 'CURSOR_PREV_LINK'
+']' = 'CURSOR_NEXT_LINK'
+H = 'CURSOR_TOP'
+M = 'CURSOR_MIDDLE'
+L = 'CURSOR_BOTTOM'
+C-d = 'HALF_PAGE_DOWN'
+C-u = 'HALF_PAGE_UP'
+C-f = 'PAGE_DOWN'
+C-b = 'PAGE_UP'
+'M-[6~' = 'PAGE_DOWN'
+'M-[5~' = 'PAGE_UP'
+'>' = 'PAGE_RIGHT'
+'<' = 'PAGE_LEFT'
+C-e = 'SCROLL_DOWN'
+C-y = 'SCROLL_UP'
+J = 'SCROLL_DOWN'
+K = 'SCROLL_UP'
+'('= 'SCROLL_LEFT'
+')' = 'SCROLL_RIGHT'
+C-m = 'CLICK'
+C-j = 'CLICK'
+C-l = 'CHANGE_LOCATION'
+U = 'RELOAD'
+r = 'REDRAW'
+R = 'RESHAPE'
+g = 'CURSOR_FIRST_LINE'
+G = 'CURSOR_LAST_LINE'
+z = 'CENTER_LINE'
+C-g = 'LINE_INFO'
+v = 'TOGGLE_SOURCE'
+
+[line]
+C-m = 'SUBMIT'
+C-j = 'SUBMIT'
+C-h = 'BACKSPACE'
+'C-?' = 'BACKSPACE'
+C-d = 'DELETE'
+C-c = 'CANCEL'
+M-b = 'PREV_WORD'
+M-f = 'NEXT_WORD'
+C-b = 'BACK'
+C-f = 'FORWARD'
+C-u = 'CLEAR'
+C-k = 'KILL'
+C-w = 'KILL_WORD'
+C-a = 'BEGIN'
+C-e = 'END'
+C-v = 'ESC'
diff --git a/res/default.css b/res/ua.css
index ac09b495..ac09b495 100644
--- a/res/default.css
+++ b/res/ua.css
diff --git a/src/config/config.nim b/src/config/config.nim
index 91b17dda..98d08442 100644
--- a/src/config/config.nim
+++ b/src/config/config.nim
@@ -1,7 +1,9 @@
 import tables
 import os
 import strutils
+import streams
 
+import config/toml
 import utils/twtstr
 import utils/radixtree
 
@@ -33,19 +35,11 @@ type
     ACTION_LINED_ESC
 
   ActionMap = Table[string, TwtAction]
-  StaticConfig = object
-    nmap: ActionMap
-    lemap: ActionMap
-    stylesheet*: string
-
   Config = object
     nmap*: ActionMap
     lemap*: ActionMap
     stylesheet*: string
 
-func getConfig(s: StaticConfig): Config =
-  return Config(nmap: s.nmap, lemap: s.lemap)
-
 func getRealKey(key: string): string =
   var realk: string
   var control = 0
@@ -106,6 +100,16 @@ func constructActionTable*(origTable: ActionMap): ActionMap =
     newTable[realk] = v
   return newTable
 
+func getAction(s: string): TwtAction =
+  if s == "NULL":
+    return NO_ACTION
+  return parseEnum[TwtAction]("ACTION_" & s)
+
+func getLineAction(s: string): TwtAction =
+  if s == "NULL":
+    return NO_ACTION
+  return parseEnum[TwtAction]("ACTION_LINED_" & s)
+
 proc readUserStylesheet(dir: string, file: string): string =
   if file.len == 0:
     return ""
@@ -120,75 +124,33 @@ proc readUserStylesheet(dir: string, file: string): string =
       result = f.readAll()
       f.close()
 
-proc parseConfigLine[T](line: string, config: var T) =
-  if line.len == 0 or line[0] == '#':
-    return
-  var cmd: seq[string]
-  var s = ""
-  var quote = false
-  var escape = false
-  for c in line:
-    if escape:
-      escape = false
-      s &= c
-      continue
-
-    if not quote and c == ' ' and s.len > 0:
-      cmd.add(s)
-      s = ""
-    elif c == '"':
-      quote = not quote
-    elif c == '\\' and not quote:
-      escape = true
-    else:
-      s &= c
-  if s.len > 0:
-    cmd.add(s)
-
-  if cmd.len == 3:
-    if cmd[0] == "nmap":
-      if cmd[2] == "NULL":
-        config.nmap[getRealKey(cmd[1])] = NO_ACTION
-      else:
-        config.nmap[getRealKey(cmd[1])] = parseEnum[TwtAction]("ACTION_" & cmd[2])
-    elif cmd[0] == "lemap":
-      if cmd[2] == "NULL":
-        config.lemap[getRealKey(cmd[1])] = NO_ACTION
-      else:
-        config.lemap[getRealKey(cmd[1])] = parseEnum[TwtAction]("ACTION_LINED_" & cmd[2])
-  elif cmd.len == 2:
-    if cmd[0] == "stylesheet":
-      config.stylesheet = cmd[1]
-
-proc staticReadConfig(): StaticConfig =
-  let default = staticRead"res/config"
-  for line in default.split('\n'):
-    parseConfigLine(line, result)
-
-  result.nmap = constructActionTable(result.nmap)
-  result.lemap = constructActionTable(result.lemap)
+func parseConfig(t: TomlValue): Config =
+  if "page" in t:
+    for k, v in t["page"].pairs:
+      result.nmap[getRealKey(k)] = getAction(v.s)
+    for k, v in t["line"].pairs:
+      result.nmap[getRealKey(k)] = getLineAction(v.s)
+  if "stylesheet" in t:
+    result.stylesheet = t["stylesheet"].s
+
+proc staticReadConfig(): Config =
+  result = parseConfig(parseToml(newStringStream(staticRead"res/config.toml")))
+  result.stylesheet = readUserStylesheet("res", result.stylesheet)
 
 const defaultConfig = staticReadConfig()
-var gconfig*: Config
+var gconfig* = defaultConfig
 
 proc readConfig(dir: string) =
-  var f: File
-  let status = f.open(dir / "config", fmRead)
-  if status:
-    var line: TaintedString
-    while f.readLine(line):
-      parseConfigLine(line, gconfig)
-
-    gconfig.nmap = constructActionTable(gconfig.nmap)
-    gconfig.lemap = constructActionTable(gconfig.lemap)
-    gconfig.stylesheet = readUserStylesheet(dir, gconfig.stylesheet)
-    f.close()
-
-proc readConfig*() =
-  gconfig = getConfig(defaultConfig)
-  when defined(debug):
-    readConfig(getCurrentDir() / "res")
-  readConfig(getConfigDir() / "twt")
+  let fs = newFileStream(dir / "config.toml")
+  if fs != nil:
+    let t = parseToml(fs)
+    if "page" in t:
+      for k, v in t["page"].pairs:
+        gconfig.nmap[getRealKey(k)] = getAction(v.s)
+      for k, v in t["line"].pairs:
+        gconfig.lemap[getRealKey(k)] = getLineAction(v.s)
+    if "stylesheet" in t:
+      gconfig.stylesheet = readUserStylesheet(dir, t["stylesheet"].s)
 
 proc getNormalAction*(s: string): TwtAction =
   if gconfig.nmap.hasKey(s):
@@ -200,3 +162,9 @@ proc getLinedAction*(s: string): TwtAction =
     return gconfig.lemap[s]
   return NO_ACTION
 
+proc readConfig*() =
+  when defined(debug):
+    readConfig(getCurrentDir() / "res")
+  readConfig(getConfigDir() / "twt")
+  gconfig.nmap = constructActionTable(gconfig.nmap)
+  gconfig.lemap = constructActionTable(gconfig.lemap)
diff --git a/src/config/toml.nim b/src/config/toml.nim
new file mode 100644
index 00000000..cfdb7763
--- /dev/null
+++ b/src/config/toml.nim
@@ -0,0 +1,406 @@
+import streams
+import tables
+import times
+import strutils
+import strformat
+import unicode
+
+import utils/twtstr
+
+type
+  ValueType = enum
+    VALUE_STRING, VALUE_INTEGER, VALUE_FLOAT, VALUE_BOOLEAN, VALUE_DATE_TIME,
+    VALUE_TABLE, VALUE_ARRAY VALUE_TABLE_ARRAY
+
+  SyntaxError = object of ValueError
+
+  TomlParser = object
+    at: int
+    line: int
+    stream: Stream
+    buf: string
+    root: TomlTable
+    node: TomlNode
+    currkey: seq[string]
+
+  TomlValue* = ref object
+    case vt*: ValueType
+    of VALUE_STRING:
+      s*: string
+    of VALUE_INTEGER:
+      i*: int64
+    of VALUE_FLOAT:
+      f*: float64
+    of VALUE_BOOLEAN:
+      b*: bool
+    of VALUE_TABLE:
+      t*: TomlTable
+    of VALUE_DATE_TIME:
+      dt*: DateTime
+    of VALUE_ARRAY:
+      a*: seq[TomlValue]
+    of VALUE_TABLE_ARRAY:
+      ta*: seq[TomlTable]
+
+  TomlNode = ref object of RootObj
+    comment: string
+
+  TomlKVPair = ref object of TomlNode
+    key*: seq[string]
+    value*: TomlValue
+
+  TomlTable = ref object of TomlNode
+    key: seq[string]
+    nodes: seq[TomlNode]
+    map: Table[string, TomlValue]
+
+func `[]`*(val: TomlValue, key: string): TomlValue =
+  return val.t.map[key]
+
+iterator pairs*(val: TomlValue): (string, TomlValue) =
+  for k, v in val.t.map.pairs:
+    yield (k, v)
+
+func contains*(val: TomlValue, key: string): bool =
+  return key in val.t.map
+
+func isBare(c: char): bool =
+  return c == '-' or c == '_' or c.isAlphaNumeric()
+
+func peek(state: TomlParser, i: int): char =
+  return state.buf[state.at + i]
+
+func peek(state: TomlParser, i: int, len: int): string =
+  return state.buf.substr(state.at + i, state.at + i + len)
+
+proc syntaxError(state: TomlParser, msg: string) =
+  raise newException(SyntaxError, fmt"on line {state.line}: {msg}")
+
+proc valueError(state: TomlParser, msg: string) =
+  raise newException(ValueError, fmt"on line {state.line}: {msg}")
+
+proc consume(state: var TomlParser): char =
+  result = state.buf[state.at]
+  inc state.at
+
+proc reconsume(state: var TomlParser) =
+  dec state.at
+
+proc has(state: var TomlParser, i: int = 0): bool =
+  if state.at + i >= state.buf.len and not state.stream.atEnd():
+    state.buf &= state.stream.readLine() & '\n'
+  return state.at + i < state.buf.len
+
+proc consumeEscape(state: var TomlParser, c: char): Rune =
+  var len = 4
+  if c == 'U':
+    len = 8
+  let c = state.consume()
+  var num = hexValue(c)
+  if num != -1:
+    var i = 0
+    while state.has() and i < len:
+      let c = state.peek(0)
+      if hexValue(c) == -1:
+        break
+      discard state.consume()
+      num *= 0x10
+      num += hexValue(c)
+      inc i
+    if i != len - 1:
+      state.syntaxError(fmt"invalid escaped length ({i}, needs {len})")
+    if num > 0x10FFFF or num in {0xD800..0xDFFF}:
+      state.syntaxError(fmt"invalid escaped codepoint {num}")
+    else:
+      return Rune(num)
+  else:
+    state.syntaxError(fmt"invalid escaped codepoint {c}")
+
+proc consumeString(state: var TomlParser, first: char): string =
+  var multiline = false
+
+  if first == '"':
+    if state.has(1):
+      let s = state.peek(0, 2)
+      if s == "\"\"":
+        multiline = true
+  elif first == '\'':
+    if state.has(1):
+      let s = state.peek(0, 2)
+      if s == "''":
+        multiline = true
+
+  if multiline:
+    let c = state.peek(0)
+    if c == '\n':
+      discard state.consume()
+
+  var escape = false
+  var ml_trim = false
+  while state.has():
+    let c = state.consume()
+    if c == '\n' and not multiline:
+      state.syntaxError(fmt"newline in string")
+    elif c == first:
+      if multiline and state.has(1):
+        let c2 = state.peek(0)
+        let c3 = state.peek(1)
+        if c2 == first and c3 == first:
+          break
+      else:
+        break
+    elif first == '"' and c == '\\':
+      escape = true
+    elif escape:
+      case c
+      of 'b': result &= '\b'
+      of 't': result &= '\t'
+      of 'n': result &= '\n'
+      of 'f': result &= '\f'
+      of 'r': result &= '\r'
+      of '"': result &= '"'
+      of '\\': result &= '\\'
+      of 'u', 'U': result &= state.consumeEscape(c)
+      of '\n': ml_trim = true
+      else: state.syntaxError(fmt"invalid escape sequence \{c}")
+      escape = false
+    elif ml_trim:
+      if not (c in {'\n', ' ', '\t'}):
+        result &= c
+        ml_trim = false
+    else:
+      result &= c
+
+proc consumeBare(state: var TomlParser, c: char): string =
+  result &= c
+  while state.has():
+    let c = state.consume()
+    case c
+    of ' ', '\t': break
+    of '.', '=', ']', '\n':
+      state.reconsume()
+      break
+    elif c.isBare():
+      result &= c
+    else:
+      state.syntaxError(fmt"invalid value in token: {c}")
+
+proc flushLine(state: var TomlParser) =
+  if state.node != nil:
+    if state.node of TomlKVPair:
+      var i = 0
+      let keys = state.currkey & TomlKVPair(state.node).key
+      var table = state.root
+      while i < keys.len - 1:
+        if keys[i] in table.map:
+          let node = table.map[keys[i]]
+          if node.vt != VALUE_TABLE:
+            let s = keys.join(".")
+            state.valueError(fmt"re-definition of node {s}")
+          else:
+            table = node.t
+        else:
+          let node = TomlTable()
+          table.map[keys[i]] = TomlValue(vt: VALUE_TABLE, t: node)
+          table = node
+        inc i
+
+      if keys[i] in table.map:
+        let s = keys.join(".")
+        state.valueError(fmt"re-definition of node {s}")
+
+      table.map[keys[i]] = TomlKVPair(state.node).value
+      table.nodes.add(state.node)
+    state.node = nil
+  inc state.line
+
+proc consumeComment(state: var TomlParser) =
+  state.node = TomlNode()
+  while state.has():
+    let c = state.consume()
+    if c == '\n':
+      state.reconsume()
+      break
+    else:
+      state.node.comment &= c
+
+proc consumeKey(state: var TomlParser): seq[string] =
+  var str = ""
+  while state.has():
+    let c = state.consume()
+    case c
+    of '"', '\'':
+      if str.len > 0:
+        state.syntaxError("multiple strings without dot")
+      str = state.consumeString(c)
+    of '=', ']':
+      if str.len != 0:
+        result.add(str)
+        str = ""
+      return result
+    of '.':
+      if str.len == 0: #TODO empty strings are allowed, only empty keys aren't
+        state.syntaxError("redundant dot")
+      else:
+        result.add(str)
+        str = ""
+    of ' ', '\t': discard
+    of '\n':
+      if state.node != nil:
+        state.syntaxError("newline without value")
+      else:
+        state.flushLine()
+    elif c.isBare():
+      if str.len > 0:
+        state.syntaxError(fmt"multiple strings without dot: {str}")
+      str = state.consumeBare(c)
+    else: state.syntaxError(fmt"invalid character in key: {c}")
+
+  state.syntaxError("key without value")
+
+proc consumeTable(state: var TomlParser): TomlTable =
+  new(result)
+  while state.has():
+    let c = state.peek(0)
+    case c
+    of ' ', '\t': discard
+    of '\n':
+      return result
+    of '[':
+      #TODO table array
+      state.syntaxError("arrays of tables are not supported yet")
+    of '"', '\'':
+      result.key = state.consumeKey()
+    elif c.isBare():
+      result.key = state.consumeKey()
+    else: state.syntaxError(fmt"invalid character before key: {c}")
+  state.syntaxError("unexpected end of file")
+
+proc consumeNoState(state: var TomlParser): bool =
+  while state.has():
+    let c = state.peek(0)
+    case c
+    of '#', '\n':
+      return false
+    of ' ', '\t': discard
+    of '[':
+      discard state.consume()
+      let table = state.consumeTable()
+      state.currkey = table.key
+      state.node = table
+      return false
+    elif c == '"' or c == '\'' or c.isBare():
+      let kvpair = TomlKVPair()
+      kvpair.key = state.consumeKey()
+      state.node = kvpair
+      return true
+    else: state.syntaxError(fmt"invalid character before key: {c}")
+  state.syntaxError("unexpected end of file")
+
+proc consumeNumber(state: var TomlParser): TomlValue =
+  var repr: string
+  var isfloat = false
+  if state.has():
+    if state.peek(0) == '+' or state.peek(0) == '-':
+      repr &= state.consume()
+
+  while state.has() and isDigit(state.peek(0)):
+    repr &= state.consume()
+
+  if state.has(1):
+    if state.peek(0) == '.' and isDigit(state.peek(1)):
+      repr &= state.consume()
+      repr &= state.consume()
+      isfloat = true
+      while state.has() and isDigit(state.peek(0)):
+        repr &= state.consume()
+
+  if state.has(1):
+    if state.peek(0) == 'E' or state.peek(0) == 'e':
+      var j = 2
+      if state.peek(1) == '-' or state.peek(1) == '+':
+        inc j
+      if state.has(j) and isDigit(state.peek(j)):
+        while j > 0:
+          repr &= state.consume()
+          dec j
+
+        while state.has() and isDigit(state.peek(0)):
+          repr &= state.consume()
+
+  if isfloat:
+    let val = parseFloat64(repr)
+    return TomlValue(vt: VALUE_FLOAT, f: val)
+
+  let val = parseInt64(repr)
+  return TomlValue(vt: VALUE_INTEGER, i: val)
+
+proc consumeValue(state: var TomlParser): TomlValue
+
+proc consumeArray(state: var TomlParser): TomlValue =
+  result = TomlValue(vt: VALUE_ARRAY)
+  var val: TomlValue
+  while state.has():
+    let c = state.consume()
+    case c
+    of ' ', '\t', '\n': discard
+    of ']':
+      if val != nil:
+        result.a.add(val)
+      break
+    of ',':
+      if val == nil:
+        state.syntaxError("comma without element")
+      result.a.add(val)
+    else:
+      state.reconsume()
+      val = state.consumeValue()
+
+proc consumeValue(state: var TomlParser): TomlValue =
+  while state.has():
+    let c = state.consume()
+    case c
+    of '"', '\'':
+      return TomlValue(vt: VALUE_STRING, s: state.consumeString(c))
+    of ' ', '\t': discard
+    of '\n':
+      state.syntaxError("newline without value")
+    of '#':
+      state.syntaxError("comment without value")
+    of '+', '-', '0'..'9':
+      return state.consumeNumber()
+      #TODO date-time
+    of '[':
+      return state.consumeArray()
+    elif c.isBare():
+      let s = state.consumeBare(c)
+      case s
+      of "true": return TomlValue(vt: VALUE_BOOLEAN, b: true)
+      of "false": return TomlValue(vt: VALUE_BOOLEAN, b: false)
+      else: state.syntaxError(fmt"invalid token {s}")
+    else:
+      state.syntaxError(fmt"invalid character in value: {c}")
+
+proc parseToml*(inputStream: Stream): TomlValue =
+  var state: TomlParser
+  state.stream = inputStream
+  state.line = 1
+  state.root = TomlTable()
+
+  while state.has():
+    if state.consumeNoState():
+      let kvpair = TomlKVPair(state.node)
+      kvpair.value = state.consumeValue()
+
+    while state.has():
+      let c = state.consume()
+      case c
+      of '\n':
+        state.flushLine()
+        break
+      of '#':
+        state.consumeComment()
+      of '\t', ' ': discard
+      else: state.syntaxError(fmt"invalid character after value: {c}")
+
+  return TomlValue(vt: VALUE_TABLE, t: state.root)
diff --git a/src/css/parser.nim b/src/css/parser.nim
index ec2b6591..0c1f91cc 100644
--- a/src/css/parser.nim
+++ b/src/css/parser.nim
@@ -1,6 +1,3 @@
-# CSS tokenizer and parser. It kinda works, though certain less specific
-# details of the specification might have been implemented incorrectly.
-
 import unicode
 import streams
 import math
@@ -73,49 +70,6 @@ type
 func `==`*(a: CSSParsedItem, b: CSSTokenType): bool =
   return a of CSSToken and CSSToken(a).tokenType == b
 
-func toNumber(s: seq[Rune]): float64 =
-  var sign = 1
-  var t = 1
-  var d = 0
-  var integer: float64 = 0
-  var f: float64 = 0
-  var e: float64 = 0
-
-  var i = 0
-  if i < s.len and s[i] == Rune('-'):
-    sign = -1
-    inc i
-  elif i < s.len and s[i] == Rune('+'):
-    inc i
-
-  while i < s.len and isDigitAscii(s[i]):
-    integer *= 10
-    integer += float64(decValue(s[i]))
-    inc i
-
-  if i < s.len and s[i] == Rune('.'):
-    inc i
-    while i < s.len and isDigitAscii(s[i]):
-      f *= 10
-      f += float64(decValue(s[i]))
-      inc i
-      inc d
-
-  if i < s.len and (s[i] == Rune('e') or s[i] == Rune('E')):
-    inc i
-    if i < s.len and s[i] == Rune('-'):
-      t = -1
-      inc i
-    elif i < s.len and s[i] == Rune('+'):
-      inc i
-
-    while i < s.len and isDigitAscii(s[i]):
-      e *= 10
-      e += float64(decValue(s[i]))
-      inc i
-
-  return float64(sign) * (integer + f * pow(10, float64(-d))) * pow(10, (float64(t) * e))
-
 func isNameStartCodePoint*(r: Rune): bool =
   return not isAscii(r) or r == Rune('_') or isAlphaAscii(r)
 
@@ -273,7 +227,7 @@ proc consumeNumber(state: var CSSTokenizerState): tuple[t: tflagb, val: float64]
         while state.has() and isDigitAscii(state.curr()):
           repr &= state.consume()
 
-  let val = toNumber(repr)
+  let val = parseFloat64($repr)
   return (t, val)
 
 proc consumeNumericToken(state: var CSSTokenizerState): CSSToken =
diff --git a/src/io/buffer.nim b/src/io/buffer.nim
index 36700e12..18e4a3d6 100644
--- a/src/io/buffer.nim
+++ b/src/io/buffer.nim
@@ -719,7 +719,7 @@ proc renderPlainText*(buffer: Buffer, text: string) =
   buffer.updateCursor()
 
 
-const css = staticRead"res/default.css"
+const css = staticRead"res/ua.css"
 let ua_stylesheet = newStringStream(css).parseStylesheet()
 
 #TODO refactor
diff --git a/src/io/pipe.nim b/src/io/pipe.nim
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/src/io/pipe.nim
diff --git a/src/utils/twtstr.nim b/src/utils/twtstr.nim
index 2dd8615b..d437be6a 100644
--- a/src/utils/twtstr.nim
+++ b/src/utils/twtstr.nim
@@ -5,6 +5,7 @@ import tables
 import json
 import bitops
 import os
+import math
 
 when defined(posix):
   import posix
@@ -40,15 +41,6 @@ func fitValueToSize*(str: string, size: int): string =
     return str & ' '.repeat(size - str.runeLen)
   return str.maxString(size)
 
-func buttonFmt*(str: string): seq[string] =
-  return "[".ansiFgColor(fgRed) & str.ansiFgColor(fgRed).ansiReset() & "]".ansiFgColor(fgRed).ansiReset()
-
-func buttonFmt*(str: seq[string]): seq[string] =
-  return "[".ansiFgColor(fgRed) & str.ansiFgColor(fgRed).ansiReset() & "]".ansiFgColor(fgRed).ansiReset()
-
-func buttonRaw*(str: string): string =
-  return "[" & str & "]"
-
 func remove*(str: string, c: string): string =
   let rem = c.toRunes()[0]
   for rune in str.runes:
@@ -198,6 +190,83 @@ func skipBlanks*(buf: string, at: int): int =
   while result < buf.len and buf[result].isWhitespace():
     inc result
 
+func parseInt64*(s: string): int64 =
+  var sign = 1
+  var t = 1
+  var d = 0
+  var integer: int64 = 0
+  var e: int64 = 0
+
+  var i = 0
+  if i < s.len and s[i] == '-':
+    sign = -1
+    inc i
+  elif i < s.len and s[i] == '+':
+    inc i
+
+  while i < s.len and isDigit(s[i]):
+    integer *= 10
+    integer += decValue(s[i])
+    inc i
+
+  if i < s.len and (s[i] == 'e' or s[i] == 'E'):
+    inc i
+    if i < s.len and s[i] == '-':
+      t = -1
+      inc i
+    elif i < s.len and s[i] == '+':
+      inc i
+
+    while i < s.len and isDigit(s[i]):
+      e *= 10
+      e += decValue(s[i])
+      inc i
+
+  return sign * integer * 10 ^ t * e
+
+func parseFloat64*(s: string): float64 =
+  var sign = 1
+  var t = 1
+  var d = 0
+  var integer: float64 = 0
+  var f: float64 = 0
+  var e: float64 = 0
+
+  var i = 0
+  if i < s.len and s[i] == '-':
+    sign = -1
+    inc i
+  elif i < s.len and s[i] == '+':
+    inc i
+
+  while i < s.len and isDigit(s[i]):
+    integer *= 10
+    integer += float64(decValue(s[i]))
+    inc i
+
+  if i < s.len and s[i] == '.':
+    inc i
+    while i < s.len and isDigit(s[i]):
+      f *= 10
+      f += float64(decValue(s[i]))
+      inc i
+      inc d
+
+  if i < s.len and (s[i] == 'e' or s[i] == 'E'):
+    inc i
+    if i < s.len and s[i] == '-':
+      t = -1
+      inc i
+    elif i < s.len and s[i] == '+':
+      inc i
+
+    while i < s.len and isDigit(s[i]):
+      e *= 10
+      e += float64(decValue(s[i]))
+      inc i
+
+  return float64(sign) * (integer + f * pow(10, float64(-d))) * pow(10, (float64(t) * e))
+
 proc expandPath*(path: string): string =
   if path.len == 0:
     return path
@@ -599,4 +668,3 @@ proc fullwidth*(s: seq[Rune]): seq[Rune] =
 
 proc fullwidth*(s: string): string =
   return $fullwidth(s.toRunes())
-