about summary refs log tree commit diff stats
path: root/src
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2022-10-19 11:24:45 +0200
committerbptato <nincsnevem662@gmail.com>2022-10-19 11:25:56 +0200
commit08a758ed7a06e1bff8994c01d6c5d317d400ccf9 (patch)
treecb53cf39ebd323491715eb569cc509c00edcae91 /src
parentc4e2de9cd8cad7e28b33e68b1b76f9044fe510be (diff)
downloadchawan-08a758ed7a06e1bff8994c01d6c5d317d400ccf9.tar.gz
Implement tree buffers, fix a js bug, refactor
Diffstat (limited to 'src')
-rw-r--r--src/display/client.nim593
-rw-r--r--src/display/pager.nim380
-rw-r--r--src/io/buffer.nim36
-rw-r--r--src/io/loader.nim23
-rw-r--r--src/js/javascript.nim1
-rw-r--r--src/types/cookie.nim137
-rw-r--r--src/utils/twtstr.nim1
7 files changed, 597 insertions, 574 deletions
diff --git a/src/display/client.nim b/src/display/client.nim
index 82fc2299..1f15e907 100644
--- a/src/display/client.nim
+++ b/src/display/client.nim
@@ -1,11 +1,9 @@
 import options
 import os
 import streams
-import strutils
 import tables
 import terminal
 import times
-import unicode
 
 import std/monotimes
 
@@ -15,23 +13,21 @@ import display/pager
 import html/dom
 import html/htmlparser
 import io/buffer
-import io/cell
 import io/lineedit
 import io/loader
 import io/request
 import io/term
 import js/javascript
-import js/regex
+import types/cookie
 import types/url
 import utils/twtstr
 
 type
   Client* = ref ClientObj
   ClientObj* = object
-    buffer*: Buffer
+    attrs: TermAttributes
     feednext: bool
     s: string
-    iserror: bool
     errormessage: string
     userstyle: CSSStylesheet
     loader: FileLoader
@@ -40,11 +36,6 @@ type
     config: Config
     jsrt: JSRuntime
     jsctx: JSContext
-    regex: Option[Regex]
-    revsearch: bool
-    needsauth: bool
-    redirecturl: Option[Url]
-    cmdmode: bool
     timeoutid: int
     timeouts: Table[int, tuple[handler: proc(), time: int64]]
     added_timeouts: Table[int, tuple[handler: proc(), time: int64]]
@@ -58,10 +49,6 @@ type
     lastbuf*: Buffer
     ibuf: string
 
-  ActionError* = object of IOError
-  LoadError* = object of ActionError
-  InterruptError* = object of LoadError
-
 proc readChar(console: Console): char =
   if console.ibuf == "":
     return stdin.readChar()
@@ -75,323 +62,26 @@ proc `=destroy`(client: var ClientObj) =
     free(client.jsrt)
 
 proc statusMode(client: Client) =
-  print(HVP(client.buffer.height + 1, 1))
+  print(HVP(client.attrs.height + 1, 1))
   print(EL())
 
-proc loadError(s: string) =
-  raise newException(LoadError, s)
-
-proc actionError(s: string) =
-  raise newException(ActionError, s)
-
-proc addBuffer(client: Client) =
-  if client.buffer == nil:
-    client.buffer = newBuffer()
-  else:
-    let oldnext = client.buffer.next
-    client.buffer.next = newBuffer()
-    if oldnext != nil:
-      oldnext.prev = client.buffer.next
-    client.buffer.next.prev = client.buffer
-    client.buffer.next.next = oldnext
-    client.buffer = client.buffer.next
-  client.buffer.loader = client.loader
-  client.buffer.userstyle = client.userstyle
-
-proc prevBuffer(client: Client) {.jsfunc.} =
-  if client.buffer.prev != nil:
-    client.buffer = client.buffer.prev
-    client.buffer.redraw = true
-
-proc nextBuffer(client: Client) {.jsfunc.} =
-  if client.buffer.next != nil:
-    client.buffer = client.buffer.next
-    client.buffer.redraw = true
-
-proc discardBuffer(buffer: Buffer) =
-  if buffer.next == nil and buffer.prev == nil:
-    actionError("Cannot discard last buffer!")
-  if buffer.sourcepair != nil:
-    buffer.sourcepair.sourcepair = nil
-  if buffer.next != nil:
-    buffer.next.prev = buffer.prev
-  if buffer.prev != nil:
-    buffer.prev.next = buffer.next
-  buffer.sourcepair = nil
-  buffer.next = nil
-  buffer.prev = nil
-
-proc discardBuffer(client: Client) {.jsfunc.} =
-  let old = client.buffer
-  if old.next != nil:
-    client.buffer = old.next
-  elif old.prev != nil:
-    client.buffer = old.prev
-  else:
-    actionError("Cannot discard last buffer!")
-  discardBuffer(old)
-  client.buffer.redraw = true
-
-proc setupBuffer(client: Client) =
-  let buffer = client.buffer
-  buffer.load()
-  buffer.render()
-  buffer.gotoAnchor()
-  buffer.redraw = true
-
-proc dupeBuffer(client: Client, location = none(URL)) {.jsfunc.} =
-  let prev = client.buffer
-  client.addBuffer()
-  client.buffer.contenttype = prev.contenttype
-  client.buffer.ispipe = prev.ispipe
-  client.buffer.istream = newStringStream(prev.source)
-  if location.issome:
-    client.buffer.location = location.get
-  else:
-    client.buffer.location = prev.location
-  client.buffer.document = prev.document
-  client.setupBuffer()
-
 proc readPipe(client: Client, ctype: string) =
-  client.addBuffer()
-  client.buffer.contenttype = if ctype != "": ctype else: "text/plain"
-  client.buffer.ispipe = true
-  client.buffer.istream = newFileStream(stdin)
-  client.buffer.location = newURL("file://-")
-  client.buffer.load()
+  let buffer = newBuffer(client.config, client.loader)
+  buffer.contenttype = if ctype != "": ctype else: "text/plain"
+  buffer.ispipe = true
+  buffer.istream = newFileStream(stdin)
+  buffer.location = newURL("file://-")
+  client.pager.addBuffer(buffer)
   #TODO is this portable at all?
   if reopen(stdin, "/dev/tty", fmReadWrite):
-    client.setupBuffer()
+    buffer.setupBuffer()
   else:
-    client.buffer.drawBuffer()
-
-type Cookie = ref object of RootObj
-  name {.jsget.}: string
-  value {.jsget.}: string
-  expires {.jsget.}: int64 # unix time
-  maxAge {.jsget.}: int64
-  secure {.jsget.}: bool
-  httponly {.jsget.}: bool
-  samesite {.jsget.}: bool
-  domain {.jsget.}: string
-  path {.jsget.}: string
-
-proc parseCookieDate(val: string): Option[DateTime] =
-  # cookie-date
-  const Delimiters = {'\t', ' '..'/', ';'..'@', '['..'`', '{'..'~'}
-  const NonDigit = Ascii + NonAscii - Digits
-  var foundTime = false
-  var foundDayOfMonth = false
-  var foundMonth = false
-  var foundYear = false
-  # date-token-list
-  var time: array[3, int]
-  var dayOfMonth: int
-  var month: int
-  var year: int
-  for dateToken in val.split(Delimiters):
-    if dateToken == "": continue # *delimiter
-    if not foundTime:
-      block timeBlock: # test for time
-        let hmsTime = dateToken.until(NonDigit - {':'})
-        var i = 0
-        for timeField in hmsTime.split(':'):
-          if i > 2: break timeBlock # too many time fields
-          # 1*2DIGIT
-          if timeField.len != 1 and timeField.len != 2: break timeBlock
-          var timeFields: array[3, int]
-          for c in timeField:
-            if c notin Digits: break timeBlock
-            timeFields[i] *= 10
-            timeFields[i] += c.decValue
-          time = timeFields
-          inc i
-        if i != 3: break timeBlock
-        foundTime = true
-        continue
-    if not foundDayOfMonth:
-      block dayOfMonthBlock: # test for day-of-month
-        let digits = dateToken.until(NonDigit)
-        if digits.len != 1 and digits.len != 2: break dayOfMonthBlock
-        var n = 0
-        for c in digits:
-          if c notin Digits: break dayOfMonthBlock
-          n *= 10
-          n += c.decValue
-        dayOfMonth = n
-        foundDayOfMonth = true
-        continue
-    if not foundMonth:
-      block monthBlock: # test for month
-        if dateToken.len < 3: break monthBlock
-        case dateToken.substr(0, 2).toLower()
-        of "jan": month = 1
-        of "feb": month = 2
-        of "mar": month = 3
-        of "apr": month = 4
-        of "may": month = 5
-        of "jun": month = 6
-        of "jul": month = 7
-        of "aug": month = 8
-        of "sep": month = 9
-        of "oct": month = 10
-        of "nov": month = 11
-        of "dec": month = 12
-        else: break monthBlock
-        foundMonth = true
-        continue
-    if not foundYear:
-      block yearBlock: # test for year
-        let digits = dateToken.until(NonDigit)
-        if digits.len != 2 and digits.len != 4: break yearBlock
-        var n = 0
-        for c in digits:
-          if c notin Digits: break yearBlock
-          n *= 10
-          n += c.decValue
-        year = n
-        foundYear = true
-        continue
-  if not (foundDayOfMonth and foundMonth and foundYear and foundTime): return none(DateTime)
-  if dayOfMonth notin 0..31: return none(DateTime)
-  if year < 1601: return none(DateTime)
-  if time[0] > 23: return none(DateTime)
-  if time[1] > 59: return none(DateTime)
-  if time[2] > 59: return none(DateTime)
-  var dateTime = dateTime(year, Month(month), MonthdayRange(dayOfMonth), HourRange(time[0]), MinuteRange(time[1]), SecondRange(time[2]))
-  return some(dateTime)
-
-proc parseCookie(client: Client, str: string): Cookie {.jsfunc.} =
-  let cookie = new(Cookie)
-  var first = true
-  for part in str.split(';'):
-    if first:
-      cookie.name = part.until('=')
-      cookie.value = part.after('=')
-      first = false
-      continue
-    let part = percentDecode(part).strip(leading = true, trailing = false, AsciiWhitespace)
-    var n = 0
-    for i in 0..part.high:
-      if part[i] == '=':
-        n = i
-        break
-    if n == 0:
-      continue
-    let key = part.substr(0, n - 1)
-    let val = part.substr(n + 1)
-    case key.toLower()
-    of "expires":
-      let date = parseCookieDate(val)
-      if date.issome:
-        cookie.expires = date.get.toTime().toUnix()
-    of "max-age": cookie.maxAge = parseInt64(val)
-    of "secure": cookie.secure = true
-    of "httponly": cookie.httponly = true
-    of "samesite": cookie.samesite = true
-    of "path": cookie.path = val
-    of "domain": cookie.domain = val
-  return cookie
+    buffer.load()
+    buffer.drawBuffer()
 
 proc doRequest(client: Client, req: Request): Response {.jsfunc.} =
   client.loader.doRequest(req)
 
-# Load request in a new buffer.
-var g_client: Client
-proc gotoUrl(client: Client, request: Request, prevurl = none(URL), force = false, ctype = "") =
-  if force or prevurl.isnone or not prevurl.get.equals(request.url, true) or
-      prevurl.get.equals(request.url) or request.httpmethod != HTTP_GET:
-    let page = client.doRequest(request)
-    client.needsauth = page.status == 401 # Unauthorized
-    client.redirecturl = page.redirect
-    if page.body != nil:
-      client.addBuffer()
-      g_client = client
-      client.buffer.contenttype = if ctype != "": ctype else: page.contenttype
-      client.buffer.istream = page.body
-      client.buffer.location = request.url
-      client.setupBuffer()
-    else:
-      loadError("Couldn't load " & $request.url & " (" & $page.res & ")")
-  elif client.buffer != nil and prevurl.issome and prevurl.get.equals(request.url, true):
-    if client.buffer.hasAnchor(request.url.anchor):
-      client.dupeBuffer(request.url.some)
-    else:
-      loadError("Couldn't find anchor " & request.url.anchor)
-
-# Relative gotoUrl: either to prevurl, or if that's none, client.buffer.url.
-proc gotoUrl(client: Client, url: string, prevurl = none(URL), force = false, ctype = "") =
-  var prevurl = prevurl
-  if prevurl.isnone and client.buffer != nil:
-    prevurl = client.buffer.location.some
-  let newurl = parseUrl(url, prevurl)
-  if newurl.isnone:
-    loadError("Invalid URL " & url)
-  client.gotoUrl(newRequest(newurl.get), prevurl, force, ctype)
-
-# When the user has passed a partial URL as an argument, they might've meant
-# either:
-# * file://$PWD/<file>
-# * https://<url>
-# So we attempt to load both, and see what works.
-# (TODO: make this optional)
-proc loadUrl(client: Client, url: string, ctype = "") =
-  let firstparse = parseUrl(url)
-  if firstparse.issome:
-    client.gotoUrl(newRequest(firstparse.get), none(Url), true, ctype)
-  else:
-    let cdir = parseUrl("file://" & getCurrentDir() & DirSep)
-    try:
-      # attempt to load local file
-      client.gotoUrl(url, cdir, true, ctype)
-    except LoadError:
-      try:
-        # attempt to load local file (this time percent encoded)
-        client.gotoUrl(percentEncode(url, LocalPathPercentEncodeSet), cdir, true, ctype)
-      except LoadError:
-        # attempt to load remote page
-        client.gotoUrl("https://" & url, none(Url), true, ctype)
-
-# Reload the page in a new buffer, then kill the previous buffer.
-proc reloadPage(client: Client) {.jsfunc.} =
-  let buf = client.buffer
-  client.gotoUrl(newRequest(client.buffer.location), none(URL), true, client.buffer.contenttype)
-  discardBuffer(buf)
-
-# Open a URL prompt and visit the specified URL.
-proc changeLocation(client: Client) {.jsfunc.} =
-  let buffer = client.buffer
-  var url = buffer.location.serialize(true)
-  client.statusMode()
-  let status = readLine("URL: ", url, buffer.width, config = client.config)
-  if status:
-    client.loadUrl(url)
-
-proc click(client: Client) {.jsfunc.} =
-  let req = client.buffer.click()
-  if req.issome:
-    client.gotoUrl(req.get, client.buffer.location.some)
-
-proc toggleSource*(client: Client) {.jsfunc.} =
-  let buffer = client.buffer
-  if buffer.sourcepair != nil:
-    client.buffer = buffer.sourcepair
-    client.buffer.redraw = true
-  else:
-    client.addBuffer()
-    client.buffer.sourcepair = client.buffer.prev
-    client.buffer.sourcepair.sourcepair = client.buffer
-    client.buffer.source = client.buffer.prev.source
-    client.buffer.streamclosed = true
-    client.buffer.location = client.buffer.sourcepair.location
-    client.buffer.ispipe = client.buffer.sourcepair.ispipe
-    let prevtype = client.buffer.prev.contenttype
-    if prevtype == "text/html":
-      client.buffer.contenttype = "text/plain"
-    else:
-      client.buffer.contenttype = "text/html"
-    client.setupBuffer()
-
 proc interruptHandler(rt: JSRuntime, opaque: pointer): int {.cdecl.} =
   let client = cast[Client](opaque)
   try:
@@ -433,184 +123,41 @@ proc command(client: Client, src: string) =
   restoreStdin()
   let previ = client.console.err.getPosition()
   client.command0(src)
-  g_client = client
   client.console.err.setPosition(previ)
-  if client.console.lastbuf == nil or client.console.lastbuf != client.buffer:
-    client.addBuffer()
-    client.buffer.istream = newStringStream(client.console.err.readAll()) #TODO
-    client.buffer.contenttype = "text/plain"
-    client.buffer.location = parseUrl("javascript:void(0);").get
-    client.console.lastbuf = client.buffer
+  if client.console.lastbuf == nil:
+    let buffer = newBuffer(client.config, client.loader)
+    buffer.istream = newStringStream(client.console.err.readAll()) #TODO
+    buffer.contenttype = "text/plain"
+    buffer.location = parseUrl("javascript:void(0);").get
+    client.console.lastbuf = buffer
+    client.pager.addBuffer(buffer)
   else:
-    client.buffer.istream = newStringStream(client.buffer.source & client.console.err.readAll())
-    client.buffer.streamclosed = false
-  client.setupBuffer()
-  client.buffer.cursorLastLine()
+    client.console.lastbuf.istream = newStringStream(client.console.lastbuf.source & client.console.err.readAll())
+    client.console.lastbuf.streamclosed = false
+  client.console.lastbuf.setupBuffer()
+  client.console.lastbuf.cursorLastLine()
 
 proc command(client: Client): bool {.jsfunc.} =
   var iput: string
   client.statusMode()
-  let status = readLine("COMMAND: ", iput, client.buffer.width, config = client.config)
+  let status = readLine("COMMAND: ", iput, client.attrs.width, config = client.config)
   if status:
     client.command(iput)
   return status
 
 proc commandMode(client: Client) {.jsfunc.} =
-  client.cmdmode = client.command()
-
-proc searchNext(client: Client) {.jsfunc.} =
-  if client.regex.issome:
-    if not client.revsearch:
-      discard client.buffer.cursorNextMatch(client.regex.get)
-    else:
-      discard client.buffer.cursorPrevMatch(client.regex.get)
+  client.pager.commandMode = client.command()
 
-proc searchPrev(client: Client) {.jsfunc.} =
-  if client.regex.issome:
-    if not client.revsearch:
-      discard client.buffer.cursorPrevMatch(client.regex.get)
-    else:
-      discard client.buffer.cursorNextMatch(client.regex.get)
-
-proc search(client: Client) {.jsfunc.} =
-  client.statusMode()
-  var iput: string
-  let status = readLine("/", iput, client.buffer.width, config = client.config)
-  if status:
-    if iput.len != 0:
-      client.regex = compileSearchRegex(iput)
-    client.revsearch = false
-    client.searchNext()
-
-proc searchBack(client: Client) {.jsfunc.} =
-  client.statusMode()
-  var iput: string
-  let status = readLine("?", iput, client.buffer.width, config = client.config)
-  if status:
-    if iput.len != 0:
-      client.regex = compileSearchRegex(iput)
-    client.revsearch = true
-    client.searchNext()
-
-proc isearch(client: Client) {.jsfunc.} =
-  client.statusMode()
-  var iput: string
-  let cpos = client.buffer.cpos
-  var mark: Mark
-  template del_mark() =
-    if mark != nil:
-      client.buffer.removeMark(mark)
-
-  let status = readLine("/", iput, client.buffer.width, {}, false, client.config, (proc(state: var LineState): bool =
-    del_mark
-    let regex = compileSearchRegex($state.news)
-    client.buffer.cpos = cpos
-    if regex.issome:
-      let match = client.buffer.cursorNextMatch(regex.get)
-      if match.success:
-        mark = client.buffer.addMark(match.x, match.y, match.str.width())
-        client.buffer.redraw = true
-        client.buffer.refreshBuffer(true)
-        print(HVP(client.buffer.height + 1, 2))
-        print(SGR())
-      else:
-        del_mark
-        client.buffer.redraw = true
-        client.buffer.refreshBuffer(true)
-        print(HVP(client.buffer.height + 1, 2))
-        print(SGR())
-      return true
-    false
-  ))
-
-  del_mark
-  client.buffer.redraw = true
-  client.buffer.refreshBuffer(true)
-  if status:
-    client.regex = compileSearchRegex(iput)
-  else:
-    client.buffer.cpos = cpos
-
-proc isearchBack(client: Client) {.jsfunc.} =
-  client.statusMode()
-  var iput: string
-  let cpos = client.buffer.cpos
-  var mark: Mark
-  template del_mark() =
-    if mark != nil:
-      client.buffer.removeMark(mark)
-  let status = readLine("?", iput, client.buffer.width, {}, false, client.config, (proc(state: var LineState): bool =
-    del_mark
-    let regex = compileSearchRegex($state.news)
-    client.buffer.cpos = cpos
-    if regex.issome:
-      let match = client.buffer.cursorPrevMatch(regex.get)
-      if match.success:
-        mark = client.buffer.addMark(match.x, match.y, match.str.width())
-        client.buffer.redraw = true
-        client.buffer.refreshBuffer(true)
-        print(HVP(client.buffer.height + 1, 2))
-        print(SGR())
-      else:
-        del_mark
-        client.buffer.redraw = true
-        client.buffer.refreshBuffer(true)
-        print(HVP(client.buffer.height + 1, 2))
-        print(SGR())
-      return true
-    false
-  ))
-  del_mark
-  client.buffer.redraw = true
-  if status:
-    client.regex = compileSearchRegex(iput)
-  else:
-    client.buffer.cpos = cpos
-
-proc quit(client: Client) {.jsfunc.} =
+proc quit(client: Client, code = 0) {.jsfunc.} =
   print(HVP(getTermAttributes().height, 0))
   print(EL())
-  quit(0)
+  quit(code)
 
 proc feedNext(client: Client) {.jsfunc.} =
   client.feednext = true
 
-#TODO move this to a pager module or something
-proc cursorLeft(client: Client) {.jsfunc.} = client.buffer.cursorLeft()
-proc cursorDown(client: Client) {.jsfunc.} = client.buffer.cursorDown()
-proc cursorUp(client: Client) {.jsfunc.} = client.buffer.cursorUp()
-proc cursorRight(client: Client) {.jsfunc.} = client.buffer.cursorRight()
-proc cursorLineBegin(client: Client) {.jsfunc.} = client.buffer.cursorLineBegin()
-proc cursorLineEnd(client: Client) {.jsfunc.} = client.buffer.cursorLineEnd()
-proc cursorNextWord(client: Client) {.jsfunc.} = client.buffer.cursorNextWord()
-proc cursorPrevWord(client: Client) {.jsfunc.} = client.buffer.cursorPrevWord()
-proc cursorNextLink(client: Client) {.jsfunc.} = client.buffer.cursorNextLink()
-proc cursorPrevLink(client: Client) {.jsfunc.} = client.buffer.cursorPrevLink()
-proc pageDown(client: Client) {.jsfunc.} = client.buffer.pageDown()
-proc pageUp(client: Client) {.jsfunc.} = client.buffer.pageUp()
-proc pageRight(client: Client) {.jsfunc.} = client.buffer.pageRight()
-proc pageLeft(client: Client) {.jsfunc.} = client.buffer.pageLeft()
-proc halfPageDown(client: Client) {.jsfunc.} = client.buffer.halfPageDown()
-proc halfPageUp(client: Client) {.jsfunc.} = client.buffer.halfPageUp()
-proc cursorFirstLine(client: Client) {.jsfunc.} = client.buffer.cursorFirstLine()
-proc cursorLastLine(client: Client) {.jsfunc.} = client.buffer.cursorLastLine()
-proc cursorTop(client: Client) {.jsfunc.} = client.buffer.cursorTop()
-proc cursorMiddle(client: Client) {.jsfunc.} = client.buffer.cursorMiddle()
-proc cursorBottom(client: Client) {.jsfunc.} = client.buffer.cursorBottom()
-proc cursorLeftEdge(client: Client) {.jsfunc.} = client.buffer.cursorLeftEdge()
-proc cursorVertMiddle(client: Client) {.jsfunc.} = client.buffer.cursorVertMiddle()
-proc cursorRightEdge(client: Client) {.jsfunc.} = client.buffer.cursorRightEdge()
-proc centerLine(client: Client) {.jsfunc.} = client.buffer.centerLine()
-proc scrollDown(client: Client) {.jsfunc.} = client.buffer.scrollDown()
-proc scrollUp(client: Client) {.jsfunc.} = client.buffer.scrollUp()
-proc scrollLeft(client: Client) {.jsfunc.} = client.buffer.scrollLeft()
-proc scrollRight(client: Client) {.jsfunc.} = client.buffer.scrollRight()
-proc lineInfo(client: Client) {.jsfunc.} = client.buffer.lineInfo()
-proc reshape(client: Client) {.jsfunc.} = client.buffer.reshape = true
-proc redraw(client: Client) {.jsfunc.} = client.buffer.redraw = true
-
 proc input(client: Client) =
-  if client.cmdmode:
+  if client.pager.commandMode:
     client.commandMode()
     return
   if not client.feednext:
@@ -624,58 +171,16 @@ proc input(client: Client) =
   let action = getNormalAction(client.config, client.s)
   client.evalJSFree(action, "<command>")
 
-proc followRedirect(client: Client)
-
-proc checkAuth(client: Client) =
-  if client.needsauth:
-    client.buffer.refreshBuffer()
-    client.statusMode()
-    var username = ""
-    let ustatus = readLine("Username: ", username, client.buffer.width, config = client.config)
-    if not ustatus:
-      client.needsauth = false
-      return
-    client.statusMode()
-    var password = ""
-    let pstatus = readLine("Password: ", password, client.buffer.width, hide = true, config = client.config)
-    if not pstatus:
-      client.needsauth = false
-      return
-    var url = client.buffer.location
-    url.username = username
-    url.password = password
-    var buf = client.buffer
-    client.gotoUrl(newRequest(url), prevurl = some(client.buffer.location))
-    discardBuffer(buf)
-    client.followRedirect()
-
-proc followRedirect(client: Client) =
-  while client.redirecturl.issome:
-    client.statusMode()
-    print("Redirecting to ", $client.redirecturl.get)
-    stdout.flushFile()
-    client.buffer.refreshBuffer(true)
-    var buf = client.buffer
-    let redirecturl = client.redirecturl.get
-    client.redirecturl = none(Url)
-    client.gotoUrl(newRequest(redirecturl), prevurl = some(client.buffer.location))
-    discardBuffer(buf)
-    if client.needsauth:
-      client.checkAuth()
-
 proc inputLoop(client: Client) =
   while true:
-    g_client = client
     restoreStdin()
-    client.followRedirect()
-    client.checkAuth()
-    client.buffer.refreshBuffer()
-    if client.needsauth: # Unauthorized
-      client.checkAuth()
-    try:
-      client.input()
-    except ActionError as e:
-      client.buffer.setStatusMessage(e.msg)
+    client.pager.displayPage()
+    client.pager.followRedirect()
+    if client.pager.container != nil:
+      client.pager.container.buffer.refreshBuffer()
+    if client.pager.container.needsauth: # Unauthorized
+      client.pager.checkAuth()
+    client.input()
 
 #TODO this is dumb
 proc readFile(client: Client, path: string): string {.jsfunc.} =
@@ -781,26 +286,22 @@ proc launchClient*(client: Client, pages: seq[string], ctype: string, dump: bool
   client.userstyle = client.config.stylesheet.parseStylesheet()
   if not stdin.isatty:
     client.readPipe(ctype)
-  try:
-    for page in pages:
-      client.loadUrl(page, ctype)
-  except LoadError as e:
-    eprint e.msg
-    quit(1)
+  for page in pages:
+    client.pager.loadURL(page, force = true, ctype = ctype)
 
   if stdout.isatty and not dump:
     when defined(posix):
       enableRawMode()
     client.inputLoop()
   else:
-    var buffer = client.buffer
-    while buffer.next != nil:
-      buffer = buffer.next
-
-    buffer.drawBuffer()
-    while buffer.prev != nil:
-      buffer = buffer.prev
-      buffer.drawBuffer()
+    for msg in client.pager.status:
+      eprint msg
+    while client.pager.nextBuffer():
+      discard
+    if client.pager.container != nil:
+      client.pager.container.buffer.drawBuffer()
+    while client.pager.prevBuffer():
+      client.pager.container.buffer.drawBuffer()
 
 proc nimGCStats(client: Client): string {.jsfunc.} =
   return GC_getStatistics()
@@ -827,13 +328,14 @@ proc newClient*(config: Config): Client =
   result.config = config
   result.loader = newFileLoader()
   result.console = newConsole()
+  result.attrs = getTermAttributes()
+  result.pager = newPager(config, result.attrs, result.loader)
   let rt = newJSRuntime()
   rt.setInterruptHandler(interruptHandler, cast[pointer](result))
   let ctx = rt.newJSContext()
   result.jsrt = rt
   result.jsctx = ctx
   var global = ctx.getGlobalObject()
-  ctx.registerType(Cookie)
   ctx.registerType(Client, asglobal = true)
   global.setOpaque(result)
   ctx.setProperty(global.val, "client", global.val)
@@ -841,6 +343,7 @@ proc newClient*(config: Config): Client =
 
   ctx.registerType(Console)
 
+  ctx.addCookieModule()
   ctx.addUrlModule()
   ctx.addDOMModule()
   ctx.addHTMLModule()
diff --git a/src/display/pager.nim b/src/display/pager.nim
index 9fc8957d..22574998 100644
--- a/src/display/pager.nim
+++ b/src/display/pager.nim
@@ -1,16 +1,39 @@
+import options
+import os
+import terminal
+import unicode
+
 import config/config
 import io/buffer
+import io/cell
+import io/lineedit
+import io/loader
+import io/request
+import io/term
 import js/javascript
+import js/regex
+import types/url
+import utils/twtstr
 
 type
   Container = ref object
-    buffer: Buffer
+    buffer*: Buffer
     children: seq[Container]
+    pos: CursorPosition
+    parent: Container
+    sourcepair: Container
+    needsauth*: bool #TODO move to buffer?
+    redirecturl: Option[URL]
 
   Pager* = ref object
-    rootContainer: Container
-    container: Container
+    attrs: TermAttributes
+    commandMode*: bool
+    container*: Container
     config: Config
+    loader: FileLoader
+    regex: Option[Regex]
+    reverseSearch: bool
+    status*: seq[string]
 
 proc cursorLeft(pager: Pager) {.jsfunc.} = pager.container.buffer.cursorLeft()
 proc cursorDown(pager: Pager) {.jsfunc.} = pager.container.buffer.cursorDown()
@@ -45,18 +68,357 @@ proc lineInfo(pager: Pager) {.jsfunc.} = pager.container.buffer.lineInfo()
 proc reshape(pager: Pager) {.jsfunc.} = pager.container.buffer.reshape = true
 proc redraw(pager: Pager) {.jsfunc.} = pager.container.buffer.redraw = true
 
-proc newContainer(): Container =
+proc searchNext(pager: Pager) {.jsfunc.} =
+  if pager.regex.issome:
+    if not pager.reverseSearch:
+      discard pager.container.buffer.cursorNextMatch(pager.regex.get)
+    else:
+      discard pager.container.buffer.cursorPrevMatch(pager.regex.get)
+
+proc searchPrev(pager: Pager) {.jsfunc.} =
+  if pager.regex.issome:
+    if not pager.reverseSearch:
+      discard pager.container.buffer.cursorPrevMatch(pager.regex.get)
+    else:
+      discard pager.container.buffer.cursorNextMatch(pager.regex.get)
+
+proc statusMode(pager: Pager) =
+  print(HVP(pager.attrs.height + 1, 1))
+  print(EL())
+
+proc search(pager: Pager) {.jsfunc.} =
+  pager.statusMode()
+  var iput: string
+  let status = readLine("/", iput, pager.attrs.width, config = pager.config)
+  if status:
+    if iput.len != 0:
+      pager.regex = compileSearchRegex(iput)
+    pager.reverseSearch = false
+    pager.searchNext()
+
+proc searchBack(pager: Pager) {.jsfunc.} =
+  pager.statusMode()
+  var iput: string
+  let status = readLine("?", iput, pager.attrs.width, config = pager.config)
+  if status:
+    if iput.len != 0:
+      pager.regex = compileSearchRegex(iput)
+    pager.reverseSearch = true
+    pager.searchNext()
+
+proc displayPage*(pager: Pager) =
+  let buffer = pager.container.buffer
+  if buffer.refreshBuffer():
+    stdout.hideCursor()
+    print(buffer.generateFullOutput())
+    stdout.showCursor()
+
+proc isearch(pager: Pager) {.jsfunc.} =
+  pager.statusMode()
+  var iput: string
+  let cpos = pager.container.buffer.cpos
+  var mark: Mark
+  template del_mark() =
+    if mark != nil:
+      pager.container.buffer.removeMark(mark)
+
+  let status = readLine("/", iput, pager.attrs.width, {}, false, pager.config, (proc(state: var LineState): bool =
+    del_mark
+    let regex = compileSearchRegex($state.news)
+    pager.container.buffer.cpos = cpos
+    if regex.issome:
+      let match = pager.container.buffer.cursorNextMatch(regex.get)
+      if match.success:
+        mark = pager.container.buffer.addMark(match.x, match.y, match.str.width())
+        pager.container.buffer.redraw = true
+        pager.container.buffer.refreshBuffer(true)
+        pager.displayPage()
+        print(HVP(pager.attrs.height + 1, 2))
+        print(SGR())
+      else:
+        del_mark
+        pager.container.buffer.redraw = true
+        pager.container.buffer.refreshBuffer(true)
+        pager.displayPage()
+        print(HVP(pager.attrs.height + 1, 2))
+        print(SGR())
+      return true
+    false
+  ))
+
+  del_mark
+  pager.container.buffer.redraw = true
+  pager.container.buffer.refreshBuffer(true)
+  if status:
+    pager.regex = compileSearchRegex(iput)
+  else:
+    pager.container.buffer.cpos = cpos
+
+proc isearchBack(pager: Pager) {.jsfunc.} =
+  pager.statusMode()
+  var iput: string
+  let cpos = pager.container.buffer.cpos
+  var mark: Mark
+  template del_mark() =
+    if mark != nil:
+      pager.container.buffer.removeMark(mark)
+  let status = readLine("?", iput, pager.container.buffer.width, {}, false, pager.config, (proc(state: var LineState): bool =
+    del_mark
+    let regex = compileSearchRegex($state.news)
+    pager.container.buffer.cpos = cpos
+    if regex.issome:
+      let match = pager.container.buffer.cursorPrevMatch(regex.get)
+      if match.success:
+        mark = pager.container.buffer.addMark(match.x, match.y, match.str.width())
+        pager.container.buffer.redraw = true
+        pager.container.buffer.refreshBuffer(true)
+        pager.displayPage()
+        print(HVP(pager.attrs.height + 1, 2))
+        print(SGR())
+      else:
+        del_mark
+        pager.container.buffer.redraw = true
+        pager.container.buffer.refreshBuffer(true)
+        pager.displayPage()
+        print(HVP(pager.attrs.height + 1, 2))
+        print(SGR())
+      return true
+    false
+  ))
+  del_mark
+  pager.container.buffer.redraw = true
+  if status:
+    pager.regex = compileSearchRegex(iput)
+  else:
+    pager.container.buffer.cpos = cpos
+
+proc newContainer(buffer: Buffer, parent: Container): Container =
   new(result)
+  result.buffer = buffer
+  result.parent = parent
 
-proc newPager*(config: Config, buffer: Buffer): Pager =
+proc newPager*(config: Config, attrs: TermAttributes, loader: FileLoader): Pager =
+  new(result)
   result.config = config
-  result.rootContainer = newContainer()
+  result.attrs = attrs
+  result.loader = loader
 
 proc addBuffer*(pager: Pager, buffer: Buffer) =
-  var ncontainer = newContainer()
-  ncontainer.buffer = buffer
-  pager.container.children.add(ncontainer)
+  var ncontainer = newContainer(buffer, pager.container)
+  if pager.container != nil:
+    pager.container.children.add(ncontainer)
   pager.container = ncontainer
 
+proc dupeBuffer*(pager: Pager, location = none(URL)) {.jsfunc.} =
+  var clone: Buffer
+  clone = pager.container.buffer.dupeBuffer(location)
+  pager.addBuffer(clone)
+
+# The prevBuffer and nextBuffer procedures emulate w3m's PREV and NEXT
+# commands by traversing the container tree in a depth-first order.
+proc prevBuffer*(pager: Pager): bool {.jsfunc.} =
+  if pager.container == nil:
+    return false
+  if pager.container.parent == nil:
+    return false
+  for i in 0..pager.container.parent.children.high:
+    let child = pager.container.parent.children[i]
+    if child == pager.container:
+      if i > 0:
+        pager.container = pager.container.parent.children[i - 1]
+      else:
+        pager.container = pager.container.parent
+      return true
+  assert false, "Container not a child of its parent"
+
+proc nextBuffer*(pager: Pager): bool {.jsfunc.} =
+  if pager.container == nil:
+    return false
+  if pager.container.children.len > 0:
+    pager.container = pager.container.children[0]
+    return true
+  if pager.container.parent == nil:
+    return false
+  for i in countdown(pager.container.parent.children.high, 0):
+    let child = pager.container.parent.children[i]
+    if child == pager.container:
+      if i < pager.container.parent.children.high:
+        pager.container = pager.container.parent.children[i + 1]
+        return true
+      return false
+  assert false, "Container not a child of its parent"
+
+#TODO we should have a separate status message stack for all buffers AND the
+# pager.
+proc setStatusMessage(pager: Pager, msg: string) =
+  if pager.container != nil:
+    pager.container.buffer.setStatusMessage(msg)
+  else:
+    pager.status.add(msg)
+
+proc discardBuffer*(pager: Pager) {.jsfunc.} =
+  if pager.container.parent == nil and pager.container.children.len == 0:
+    pager.setStatusMessage("Cannot discard last buffer!")
+  else:
+    if pager.container.parent != nil:
+      let parent = pager.container.parent
+      let n = parent.children.find(pager.container)
+      assert n != -1, "Container not a child of its parent"
+      for i in countdown(pager.container.children.high, 0):
+        let child = pager.container.children[i]
+        child.parent = pager.container.parent
+        parent.children.insert(child, n + 1)
+      parent.children.delete(n)
+      pager.container = parent
+    else:
+      pager.container = pager.container.children[0]
+      pager.container.parent = nil
+
+proc drawBuffer*(pager: Pager) {.jsfunc.} =
+  pager.container.buffer.drawBuffer() #TODO move this to pager
+
+proc toggleSource*(pager: Pager) {.jsfunc.} =
+  if pager.container.sourcepair != nil:
+    pager.container = pager.container.sourcepair
+  else:
+    let buffer = newBuffer(pager.config, pager.loader)
+    buffer.source = pager.container.buffer.source
+    buffer.streamclosed = true
+    buffer.location = pager.container.buffer.location
+    buffer.ispipe = pager.container.buffer.ispipe
+    if pager.container.buffer.contenttype == "text/plain":
+      buffer.contenttype = "text/html"
+    else:
+      buffer.contenttype = "text/plain"
+    buffer.setupBuffer()
+    let container = newContainer(buffer, pager.container)
+    container.sourcepair = pager.container
+    pager.container.sourcepair = container
+    pager.container.children.add(container)
+
+# Load request in a new buffer.
+proc gotoURL*(pager: Pager, request: Request, prevurl = none(URL), force = false, ctype = "", replace = false): bool {.discardable.} =
+  if force or prevurl.isnone or not prevurl.get.equals(request.url, true) or
+      request.url.hash == "" or request.httpmethod != HTTP_GET:
+    # Basically, we want to reload the page *only* when
+    # a) force == true
+    # b) or the new URL isn't just the old URL + an anchor
+    # I think this makes navigation pretty natural, or at least very close to
+    # what other browsers do. Still, it would be nice if we got some visual
+    # feedback on what is actually going to happen when typing a URL; TODO.
+    let response = pager.loader.doRequest(request)
+    if response.body != nil:
+      let buffer = newBuffer(pager.config, pager.loader)
+      buffer.contenttype = if ctype != "": ctype else: response.contenttype
+      buffer.istream = response.body
+      buffer.location = request.url
+      buffer.setupBuffer()
+      if replace:
+        pager.discardBuffer()
+      pager.addBuffer(buffer)
+      pager.container.needsauth = response.status == 401 # Unauthorized
+      pager.container.redirecturl = response.redirect
+    else:
+      pager.setStatusMessage("Couldn't load " & $request.url & " (" & $response.res & ")")
+      return false
+  else:
+    if pager.container.buffer.hasAnchor(request.url.anchor):
+      pager.dupeBuffer(request.url.some)
+    else:
+      pager.setStatusMessage("Couldn't find anchor " & request.url.anchor)
+      return false
+  return true
+
+# When the user has passed a partial URL as an argument, they might've meant
+# either:
+# * file://$PWD/<file>
+# * https://<url>
+# So we attempt to load both, and see what works.
+# (TODO: make this optional)
+proc loadURL*(pager: Pager, url: string, force = false, ctype = "") =
+  let firstparse = parseURL(url)
+  if firstparse.issome:
+    let prev = if pager.container != nil:
+      some(pager.container.buffer.location)
+    else:
+      none(URL)
+    pager.gotoURL(newRequest(firstparse.get), prev, force, ctype)
+    return
+  let cdir = parseURL("file://" & getCurrentDir() & DirSep)
+  let newurl = parseURL(url, cdir)
+  if newurl.isSome:
+    # attempt to load local file
+    if pager.gotoURL(newRequest(newurl.get), force = force, ctype = ctype):
+      return
+  block:
+    let purl = percentEncode(url, LocalPathPercentEncodeSet)
+    if purl != url:
+      let newurl = parseURL(purl, cdir)
+      if newurl.isSome:
+        if pager.gotoURL(newRequest(newurl.get), force = force, ctype = ctype):
+          pager.status.setLen(0)
+          return
+  block:
+    let newurl = parseURL("https://" & url)
+    if newurl.isSome:
+      # attempt to load remote page
+      if pager.gotoURL(newRequest(newurl.get), force = force, ctype = ctype):
+        pager.status.setLen(0)
+        return
+  pager.setStatusMessage("Invalid URL " & url)
+
+# Open a URL prompt and visit the specified URL.
+proc changeLocation(pager: Pager) {.jsfunc.} =
+  var url = pager.container.buffer.location.serialize()
+  pager.statusMode()
+  let status = readLine("URL: ", url, pager.attrs.width, config = pager.config)
+  if status:
+    pager.loadURL(url)
+
+# Reload the page in a new buffer, then kill the previous buffer.
+proc reloadPage(pager: Pager) {.jsfunc.} =
+  pager.gotoURL(newRequest(pager.container.buffer.location), none(URL), true, pager.container.buffer.contenttype, true)
+
+proc click(pager: Pager) {.jsfunc.} =
+  #TODO this conflicts with the planned event loop
+  let req = pager.container.buffer.click()
+  if req.issome:
+    pager.gotoURL(req.get, pager.container.buffer.location.some)
+
+proc followRedirect*(pager: Pager)
+
+proc checkAuth*(pager: Pager) =
+  if pager.container != nil and pager.container.needsauth:
+    pager.container.buffer.refreshBuffer()
+    pager.statusMode()
+    var username = ""
+    let ustatus = readLine("Username: ", username, pager.attrs.width, config = pager.config)
+    if not ustatus:
+      pager.container.needsauth = false
+      return
+    pager.statusMode()
+    var password = ""
+    let pstatus = readLine("Password: ", password, pager.attrs.width, hide = true, config = pager.config)
+    if not pstatus:
+      pager.container.needsauth = false
+      return
+    var url = pager.container.buffer.location
+    url.username = username
+    url.password = password
+    pager.gotoURL(newRequest(url), prevurl = some(pager.container.buffer.location), replace = true)
+    pager.followRedirect()
+
+proc followRedirect*(pager: Pager) =
+  while pager.container != nil and pager.container.redirecturl.issome:
+    pager.statusMode()
+    print("Redirecting to ", $pager.container.redirecturl.get)
+    stdout.flushFile()
+    pager.container.buffer.refreshBuffer(true)
+    let redirecturl = pager.container.redirecturl.get
+    pager.container.redirecturl = none(URL)
+    pager.gotoURL(newRequest(redirecturl), prevurl = some(pager.container.buffer.location), replace = true)
+    if pager.container.needsauth:
+      pager.checkAuth()
+
 proc addPagerModule*(ctx: JSContext) =
   ctx.registerType(Pager)
diff --git a/src/io/buffer.nim b/src/io/buffer.nim
index b5e46297..1e0a1c29 100644
--- a/src/io/buffer.nim
+++ b/src/io/buffer.nim
@@ -63,19 +63,18 @@ type
     streamclosed*: bool
     source*: string
     prevnode*: StyledNode
-    sourcepair*: Buffer
-    prev*: Buffer
-    next*: Buffer
     userstyle*: CSSStylesheet
     loader*: FileLoader
     marks*: seq[Mark]
     config*: Config
 
-proc newBuffer*(): Buffer =
+proc newBuffer*(config: Config, loader: FileLoader): Buffer =
   new(result)
   result.attrs = getTermAttributes()
   result.width = result.attrs.width
   result.height = result.attrs.height - 1
+  result.config = config
+  result.loader = loader
 
   result.display = newFixedGrid(result.width, result.height)
   result.prevdisplay = newFixedGrid(result.width, result.height)
@@ -87,7 +86,7 @@ func fromx*(buffer: Buffer): int {.inline.} = buffer.cpos.fromx
 func fromy*(buffer: Buffer): int {.inline.} = buffer.cpos.fromy
 func xend*(buffer: Buffer): int {.inline.} = buffer.cpos.xend
 
-func generateFullOutput(buffer: Buffer): string =
+func generateFullOutput*(buffer: Buffer): string =
   var x = 0
   var w = 0
   var format = newFormat()
@@ -878,6 +877,8 @@ proc render*(buffer: Buffer) =
   of "text/html":
     if buffer.viewport == nil:
       buffer.viewport = Viewport(term: buffer.attrs)
+    if buffer.userstyle == nil:
+      buffer.userstyle = buffer.config.stylesheet.parseStylesheet()
     let ret = renderDocument(buffer.document, buffer.attrs, buffer.userstyle, buffer.viewport, buffer.prevstyled)
     buffer.lines = ret[0]
     buffer.prevstyled = ret[1]
@@ -1196,6 +1197,24 @@ proc click*(buffer: Buffer): Option[Request] =
     else:
       restore_focus
 
+proc setupBuffer*(buffer: Buffer) =
+  buffer.load()
+  buffer.render()
+  buffer.gotoAnchor()
+  buffer.redraw = true
+
+proc dupeBuffer*(buffer: Buffer, location = none(URL)): Buffer =
+  let clone = newBuffer(buffer.config, buffer.loader)
+  clone.contenttype = buffer.contenttype
+  clone.ispipe = buffer.ispipe
+  if location.isSome:
+    clone.location = location.get
+  else:
+    clone.location = buffer.location
+  clone.istream = newStringStream(buffer.source)
+  clone.setupBuffer()
+  return clone
+
 proc drawBuffer*(buffer: Buffer) =
   var format = newFormat()
   for line in buffer.lines:
@@ -1218,8 +1237,11 @@ proc drawBuffer*(buffer: Buffer) =
       print(format.processFormat(newFormat()))
       print("\n")
 
-proc refreshBuffer*(buffer: Buffer, peek = false) =
+proc refreshTitle*(buffer: Buffer) =
   buffer.title = buffer.getTitle()
+
+proc refreshBuffer*(buffer: Buffer, peek = false): bool {.discardable.} =
+  buffer.refreshTitle()
   stdout.hideCursor()
 
   if buffer.refreshTermAttrs():
@@ -1229,6 +1251,7 @@ proc refreshBuffer*(buffer: Buffer, peek = false) =
   if buffer.redraw:
     buffer.refreshDisplay()
     buffer.displayBuffer()
+    #result = true
     buffer.redraw = false
 
   if not peek:
@@ -1239,6 +1262,7 @@ proc refreshBuffer*(buffer: Buffer, peek = false) =
     buffer.reshape = false
     buffer.refreshDisplay()
     buffer.displayBufferSwapOutput()
+    #result = true
 
   if not peek:
     if not buffer.nostatus:
diff --git a/src/io/loader.nim b/src/io/loader.nim
index 1530bf5b..7e6faab4 100644
--- a/src/io/loader.nim
+++ b/src/io/loader.nim
@@ -76,12 +76,18 @@ proc loadResource(loader: FileLoader, request: Request, ostream: Stream) =
     ostream.swrite(-1) # error
     ostream.flush()
 
-proc runFileLoader(loader: FileLoader, loadcb: proc()) =
+proc runFileLoader(loader: FileLoader, fd: cint) =
   if curl_global_init(CURL_GLOBAL_ALL) != CURLE_OK:
     raise newException(Defect, "Failed to initialize libcurl.")
   let ssock = initServerSocket(getpid())
   # The server has been initialized, so the main process can resume execution.
-  loadcb()
+  var writef: File
+  if not open(writef, FileHandle(fd), fmWrite):
+    raise newException(Defect, "Failed to open input handle.")
+  writef.write(char(0u8))
+  writef.flushFile()
+  close(writef)
+  discard close(fd)
   while true:
     let stream = ssock.acceptSocketStream()
     try:
@@ -111,7 +117,7 @@ proc doRequest*(loader: FileLoader, request: Request): Response =
     if "Content-Type" in result.headers.table:
       result.contenttype = result.headers.table["Content-Type"][0].until(';')
     else:
-      result.contenttype = guessContentType($request.url)
+      result.contenttype = guessContentType($request.url.path)
     if "Location" in result.headers.table:
       let location = result.headers.table["Location"][0]
       result.redirect = parseUrl(location, some(request.url))
@@ -131,15 +137,7 @@ proc newFileLoader*(defaultHeaders: HeaderList): FileLoader =
     elif pid == 0:
       # child process
       discard close(pipefd[0]) # close read
-      var writef: File
-      if not open(writef, FileHandle(pipefd[1]), fmWrite):
-        raise newException(Defect, "Failed to open input handle.")
-      result.runFileLoader((proc() =
-        writef.write(char(0u8))
-        writef.flushFile()
-        close(writef)
-        discard close(pipefd[1])
-      ))
+      result.runFileLoader(pipefd[1])
     else:
       result.process = pid
       let readfd = pipefd[0] # get read
@@ -150,7 +148,6 @@ proc newFileLoader*(defaultHeaders: HeaderList): FileLoader =
       assert readf.readChar() == char(0u8)
       close(readf)
       discard close(pipefd[0])
-      
 
 proc newFileLoader*(): FileLoader =
   newFileLoader(DefaultHeaders)
diff --git a/src/js/javascript.nim b/src/js/javascript.nim
index 3047a9de..b453cb13 100644
--- a/src/js/javascript.nim
+++ b/src/js/javascript.nim
@@ -1302,6 +1302,7 @@ macro registerType*(ctx: typed, t: typed, parent: JSClassID = 0, asglobal = fals
       elif f1.strVal.startsWith("js_set"):
         setters[f0] = f1
       else:
+        f0 = fun.name
         tabList.add(quote do:
           JS_CFUNC_DEF(`f0`, 0, cast[JSCFunction](`f1`)))
 
diff --git a/src/types/cookie.nim b/src/types/cookie.nim
new file mode 100644
index 00000000..b0578e44
--- /dev/null
+++ b/src/types/cookie.nim
@@ -0,0 +1,137 @@
+import options
+import strutils
+import times
+
+import js/javascript
+import utils/twtstr
+
+type Cookie = ref object of RootObj
+  name {.jsget.}: string
+  value {.jsget.}: string
+  expires {.jsget.}: int64 # unix time
+  maxAge {.jsget.}: int64
+  secure {.jsget.}: bool
+  httponly {.jsget.}: bool
+  samesite {.jsget.}: bool
+  domain {.jsget.}: string
+  path {.jsget.}: string
+
+proc parseCookieDate(val: string): Option[DateTime] =
+  # cookie-date
+  const Delimiters = {'\t', ' '..'/', ';'..'@', '['..'`', '{'..'~'}
+  const NonDigit = AllChars - AsciiDigit
+  var foundTime = false
+  var foundDayOfMonth = false
+  var foundMonth = false
+  var foundYear = false
+  # date-token-list
+  var time: array[3, int]
+  var dayOfMonth: int
+  var month: int
+  var year: int
+  for dateToken in val.split(Delimiters):
+    if dateToken == "": continue # *delimiter
+    if not foundTime:
+      block timeBlock: # test for time
+        let hmsTime = dateToken.until(NonDigit - {':'})
+        var i = 0
+        for timeField in hmsTime.split(':'):
+          if i > 2: break timeBlock # too many time fields
+          # 1*2DIGIT
+          if timeField.len != 1 and timeField.len != 2: break timeBlock
+          var timeFields: array[3, int]
+          for c in timeField:
+            if c notin AsciiDigit: break timeBlock
+            timeFields[i] *= 10
+            timeFields[i] += c.decValue
+          time = timeFields
+          inc i
+        if i != 3: break timeBlock
+        foundTime = true
+        continue
+    if not foundDayOfMonth:
+      block dayOfMonthBlock: # test for day-of-month
+        let digits = dateToken.until(NonDigit)
+        if digits.len != 1 and digits.len != 2: break dayOfMonthBlock
+        var n = 0
+        for c in digits:
+          if c notin AsciiDigit: break dayOfMonthBlock
+          n *= 10
+          n += c.decValue
+        dayOfMonth = n
+        foundDayOfMonth = true
+        continue
+    if not foundMonth:
+      block monthBlock: # test for month
+        if dateToken.len < 3: break monthBlock
+        case dateToken.substr(0, 2).toLower()
+        of "jan": month = 1
+        of "feb": month = 2
+        of "mar": month = 3
+        of "apr": month = 4
+        of "may": month = 5
+        of "jun": month = 6
+        of "jul": month = 7
+        of "aug": month = 8
+        of "sep": month = 9
+        of "oct": month = 10
+        of "nov": month = 11
+        of "dec": month = 12
+        else: break monthBlock
+        foundMonth = true
+        continue
+    if not foundYear:
+      block yearBlock: # test for year
+        let digits = dateToken.until(NonDigit)
+        if digits.len != 2 and digits.len != 4: break yearBlock
+        var n = 0
+        for c in digits:
+          if c notin AsciiDigit: break yearBlock
+          n *= 10
+          n += c.decValue
+        year = n
+        foundYear = true
+        continue
+  if not (foundDayOfMonth and foundMonth and foundYear and foundTime): return none(DateTime)
+  if dayOfMonth notin 0..31: return none(DateTime)
+  if year < 1601: return none(DateTime)
+  if time[0] > 23: return none(DateTime)
+  if time[1] > 59: return none(DateTime)
+  if time[2] > 59: return none(DateTime)
+  var dateTime = dateTime(year, Month(month), MonthdayRange(dayOfMonth), HourRange(time[0]), MinuteRange(time[1]), SecondRange(time[2]))
+  return some(dateTime)
+
+proc newCookie(str: string): Cookie {.jsctor.} =
+  let cookie = new(Cookie)
+  var first = true
+  for part in str.split(';'):
+    if first:
+      cookie.name = part.until('=')
+      cookie.value = part.after('=')
+      first = false
+      continue
+    let part = percentDecode(part).strip(leading = true, trailing = false, AsciiWhitespace)
+    var n = 0
+    for i in 0..part.high:
+      if part[i] == '=':
+        n = i
+        break
+    if n == 0:
+      continue
+    let key = part.substr(0, n - 1)
+    let val = part.substr(n + 1)
+    case key.toLower()
+    of "expires":
+      let date = parseCookieDate(val)
+      if date.issome:
+        cookie.expires = date.get.toTime().toUnix()
+    of "max-age": cookie.maxAge = parseInt64(val)
+    of "secure": cookie.secure = true
+    of "httponly": cookie.httponly = true
+    of "samesite": cookie.samesite = true
+    of "path": cookie.path = val
+    of "domain": cookie.domain = val
+  return cookie
+
+proc addCookieModule*(ctx: JSContext) =
+  ctx.registerType(Cookie)
diff --git a/src/utils/twtstr.nim b/src/utils/twtstr.nim
index 41ffd09f..3132510e 100644
--- a/src/utils/twtstr.nim
+++ b/src/utils/twtstr.nim
@@ -32,7 +32,6 @@ const Ascii* = {chr(0x00)..chr(0x7F)}
 const AsciiUpperAlpha* = {'A'..'Z'}
 const AsciiLowerAlpha* = {'a'..'z'}
 const AsciiAlpha* = (AsciiUpperAlpha + AsciiLowerAlpha)
-const AllChars = {chr(0x00)..chr(0xFF)}
 const NonAscii* = (AllChars - Ascii)
 const AsciiDigit* = {'0'..'9'}
 const AsciiHexDigit* = (AsciiDigit + {'a'..'f', 'A'..'F'})