about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2022-12-13 00:04:50 +0100
committerbptato <nincsnevem662@gmail.com>2022-12-13 00:04:50 +0100
commitd9e430c8147c8c2d81b4ca5405786269b2cfc94d (patch)
tree91ddb6edbedc6e8310fedc13106b8b3bdcec2cdc
parent7a3fb32bff44a581d6926bfaf5f070f8ef062338 (diff)
downloadchawan-d9e430c8147c8c2d81b4ca5405786269b2cfc94d.tar.gz
Add all sorts of config options and cookies
-rw-r--r--bonus/config.toml15
-rw-r--r--src/buffer/buffer.nim8
-rw-r--r--src/buffer/container.nim25
-rw-r--r--src/config/config.nim115
-rw-r--r--src/config/toml.nim1
-rw-r--r--src/display/client.nim8
-rw-r--r--src/display/pager.nim59
-rw-r--r--src/display/term.nim35
-rw-r--r--src/io/loader.nim7
-rw-r--r--src/io/request.nim43
-rw-r--r--src/ips/forkserver.nim14
-rw-r--r--src/ips/serialize.nim20
-rw-r--r--src/types/cookie.nim48
13 files changed, 296 insertions, 102 deletions
diff --git a/bonus/config.toml b/bonus/config.toml
new file mode 100644
index 00000000..39e6dc52
--- /dev/null
+++ b/bonus/config.toml
@@ -0,0 +1,15 @@
+[[omnirule]]
+match = '^ddg:'
+substitute-url = '(x) => "https://lite.duckduckgo.com/lite/?kp=-1&kd=-1&q=" + x.substring(4)'
+
+[[omnirule]]
+match = '^wk:'
+substitute-url = '(x) => "https://en.wikipedia.org/wiki/Special:Search?search=" + x.substring(4)'
+
+[[siteconf]]
+url = '/^https?:\/\/(www\.)?npr\.org/'
+rewrite-url = '(x) => x.pathname = x.pathname.replace(/(.*)\/.*/, "$1").replace(/.*\//, "")'
+
+[[siteconf]]
+url = "^https://news.ycombinator.com.*"
+cookie = true
diff --git a/src/buffer/buffer.nim b/src/buffer/buffer.nim
index 9f699ca3..b692e253 100644
--- a/src/buffer/buffer.nim
+++ b/src/buffer/buffer.nim
@@ -36,6 +36,7 @@ import render/renderdocument
 import render/rendertext
 import types/buffersource
 import types/color
+import types/cookie
 import types/url
 import utils/twtstr
 
@@ -528,7 +529,7 @@ proc loadResources(buffer: Buffer, document: Document) =
     for child in elem.children_rev:
       stack.add(child)
 
-type ConnectResult* = tuple[code: int, needsAuth: bool, redirect: Option[URL], contentType: string] 
+type ConnectResult* = tuple[code: int, needsAuth: bool, redirect: Option[URL], contentType: string, cookies: seq[Cookie]] 
 
 proc setupSource(buffer: Buffer): ConnectResult =
   if buffer.loaded:
@@ -564,6 +565,11 @@ proc setupSource(buffer: Buffer): ConnectResult =
     SocketStream(buffer.istream).source.getFd().setBlocking(false)
     result.needsAuth = response.status == 401 # Unauthorized
     result.redirect = response.redirect
+    if "Set-Cookie" in response.headers.table:
+      for s in response.headers.table["Set-Cookie"]:
+        let cookie = newCookie(s)
+        if cookie != nil:
+          result.cookies.add(cookie)
   buffer.istream = newTeeStream(buffer.istream, buffer.sstream, closedest = false)
   if setct:
     result.contentType = buffer.contenttype
diff --git a/src/buffer/container.nim b/src/buffer/container.nim
index 83bb6c8c..a148e0e1 100644
--- a/src/buffer/container.nim
+++ b/src/buffer/container.nim
@@ -1,3 +1,4 @@
+import deques
 import options
 import streams
 import strformat
@@ -17,6 +18,7 @@ import ips/socketstream
 import js/javascript
 import js/regex
 import types/buffersource
+import types/cookie
 import types/dispatcher
 import types/url
 import utils/twtstr
@@ -32,7 +34,8 @@ type
 
   ContainerEventType* = enum
     NO_EVENT, FAIL, SUCCESS, NEEDS_AUTH, REDIRECT, ANCHOR, NO_ANCHOR, UPDATE,
-    READ_LINE, READ_AREA, OPEN, INVALID_COMMAND, STATUS, ALERT, LOADED
+    READ_LINE, READ_AREA, OPEN, INVALID_COMMAND, STATUS, ALERT, LOADED,
+    SET_COOKIE
 
   ContainerEvent* = object
     case t*: ContainerEventType
@@ -52,6 +55,8 @@ type
       msg*: string
     of UPDATE:
       force*: bool
+    of SET_COOKIE:
+      cookies*: seq[Cookie]
     else: discard
 
   Highlight* = ref object
@@ -61,8 +66,9 @@ type
     clear*: bool
 
   Container* = ref object
+    config: BufferConfig
     iface*: BufferInterface
-    attrs*: WindowAttributes
+    attrs: WindowAttributes
     width*: int
     height*: int
     contenttype*: Option[string]
@@ -88,24 +94,25 @@ type
     redraw*: bool
     needslines*: bool
     canceled: bool
-    events*: seq[ContainerEvent]
+    events*: Deque[ContainerEvent]
     startpos: Option[CursorPosition]
     hasstart: bool
 
-proc newBuffer*(dispatcher: Dispatcher, config: Config, source: BufferSource, title = ""): Container =
+proc newBuffer*(dispatcher: Dispatcher, config: Config, source: BufferSource, cookiejar: CookieJar, title = ""): Container =
   let attrs = getWindowAttributes(stdout)
+  let config = config.getBufferConfig(source.location, cookiejar)
   let ostream = dispatcher.forkserver.ostream
   let istream = dispatcher.forkserver.istream
   ostream.swrite(FORK_BUFFER)
   ostream.swrite(source)
-  ostream.swrite(config.getBufferConfig(source.location))
+  ostream.swrite(config)
   ostream.swrite(attrs)
   ostream.swrite(dispatcher.mainproc)
   ostream.flush()
   result = Container(
     source: source, attrs: attrs, width: attrs.width,
     height: attrs.height - 1, contenttype: source.contenttype,
-    title: title
+    title: title, config: config
   )
   istream.sread(result.process)
   result.pos.setx = -1
@@ -250,7 +257,7 @@ func findHighlights*(container: Container, y: int): seq[Highlight] =
       result.add(hl)
 
 proc triggerEvent(container: Container, event: ContainerEvent) =
-  container.events.add(event)
+  container.events.addLast(event)
 
 proc triggerEvent(container: Container, t: ContainerEventType) =
   container.triggerEvent(ContainerEvent(t: t))
@@ -638,6 +645,8 @@ proc load*(container: Container) =
       container.code = res.code
       if res.code == 0:
         container.triggerEvent(SUCCESS)
+        if res.cookies.len > 0 and container.config.cookiejar != nil: # accept cookies
+          container.triggerEvent(ContainerEvent(t: SET_COOKIE, cookies: res.cookies))
         container.setLoadInfo("Connected to " & $container.source.location & ". Downloading...")
         if res.needsAuth:
           container.triggerEvent(NEEDS_AUTH)
@@ -690,7 +699,7 @@ proc dupeBuffer*(dispatcher: Dispatcher, container: Container, config: Config, l
     contenttype: if contenttype.isSome: contenttype else: container.contenttype,
     clonepid: container.process,
   )
-  container.pipeto = dispatcher.newBuffer(config, source, container.title)
+  container.pipeto = dispatcher.newBuffer(config, source, container.config.cookiejar, container.title)
   container.iface.getSource().then(proc() =
     if container.pipeto != nil:
       container.pipeto.load()
diff --git a/src/config/config.nim b/src/config/config.nim
index cb42d6af..73072763 100644
--- a/src/config/config.nim
+++ b/src/config/config.nim
@@ -5,10 +5,12 @@ import streams
 
 import buffer/cell
 import config/toml
+import io/request
 import io/urlfilter
 import js/javascript
 import js/regex
 import types/color
+import types/cookie
 import types/url
 import utils/twtstr
 
@@ -21,15 +23,28 @@ type
   ActionMap = Table[string, string]
 
   StaticSiteConfig = object
-    url: string
+    url: Option[string]
+    host: Option[string]
     subst: Option[string]
+    cookie: bool
+
+  StaticOmniRule = object
+    match: string
+    subst: string
 
   SiteConfig* = object
-    url*: Regex
+    url*: Option[Regex]
+    host*: Option[Regex]
+    subst*: (proc(s: URL): Option[URL])
+    cookie*: bool
+
+  OmniRule* = object
+    match*: Regex
     subst*: (proc(s: string): Option[string])
 
   Config* = ref ConfigObj
   ConfigObj* = object
+    termreload*: bool
     nmap*: ActionMap
     lemap*: ActionMap
     stylesheet*: string
@@ -45,6 +60,7 @@ type
     editor*: string
     tmpdir*: string
     siteconf: seq[StaticSiteConfig]
+    omnirules: seq[StaticOmniRule]
     forceclear*: bool
     emulateoverline*: bool
     visualhome*: string
@@ -52,6 +68,8 @@ type
   BufferConfig* = object
     userstyle*: string
     filter*: URLFilter
+    cookiejar*: CookieJar
+    headers*: HeaderList
 
   ForkServerConfig* = object
     tmpdir*: string
@@ -63,20 +81,36 @@ func getForkServerConfig*(config: Config): ForkServerConfig =
     ambiguous_double: config.ambiguous_double
   )
 
-func getBufferConfig*(config: Config, location: URL): BufferConfig =
+func getBufferConfig*(config: Config, location: URL, cookiejar: CookieJar): BufferConfig =
   result.userstyle = config.stylesheet
   result.filter = newURLFilter(scheme = some(location.scheme))
+  result.cookiejar = cookiejar
 
 proc getSiteConfig*(config: Config, jsctx: JSContext): seq[SiteConfig] =
   for sc in config.siteconf:
+    var conf = SiteConfig(
+      cookie: sc.cookie,
+    )
+    if sc.url.isSome:
+      conf.url = compileRegex(sc.url.get, 0)
+    elif sc.host.isSome:
+      conf.host = compileRegex(sc.host.get, 0)
     if sc.subst.isSome:
-      let re = compileRegex(sc.url, 0)
       let fun = jsctx.eval(sc.subst.get, "<siteconf>", JS_EVAL_TYPE_GLOBAL)
-      let f = getJSFunction[string, string](jsctx, fun.val)
-      result.add(SiteConfig(
-        url: re.get,
-        subst: f.get
-      ))
+      let f = getJSFunction[URL, URL](jsctx, fun.val)
+      conf.subst = f.get
+    result.add(conf)
+
+proc getOmniRules*(config: Config, jsctx: JSContext): seq[OmniRule] =
+  for rule in config.omnirules:
+    let re = compileRegex(rule.match, 0)
+    var conf = OmniRule(
+      match: re.get
+    )
+    let fun = jsctx.eval(rule.subst, "<siteconf>", JS_EVAL_TYPE_GLOBAL)
+    let f = getJSFunction[string, string](jsctx, fun.val)
+    conf.subst = f.get
+    result.add(conf)
 
 func getRealKey(key: string): string =
   var realk: string
@@ -150,9 +184,50 @@ proc readUserStylesheet(dir, file: string): string =
       result = f.readAll()
       f.close()
 
+proc parseConfig(config: Config, dir: string, stream: Stream)
+proc parseConfig*(config: Config, dir: string, s: string)
+
+proc loadConfig*(config: Config, s: string) {.jsfunc.} =
+  let s = if s.len > 0 and s[0] == '/':
+    s
+  else:
+    getCurrentDir() / s
+  if not fileExists(s): return
+  config.parseConfig(parentDir(s), newFileStream(s))
+
+proc bindPagerKey*(config: Config, key, action: string) {.jsfunc.} =
+  let k = getRealKey(key)
+  config.nmap[k] = action
+  var teststr = ""
+  for c in k:
+    teststr &= c
+    if teststr notin config.nmap:
+      config.nmap[teststr] = "client.feedNext()"
+
+proc bindLineKey*(config: Config, key, action: string) {.jsfunc.} =
+  let k = getRealKey(key)
+  config.lemap[k] = action
+  var teststr = ""
+  for c in k:
+    teststr &= c
+    if teststr notin config.nmap:
+      config.lemap[teststr] = "client.feedNext()"
+
 proc parseConfig(config: Config, dir: string, t: TomlValue) =
   for k, v in t:
     case k
+    of "include":
+      if v.vt == VALUE_STRING:
+        when nimvm:
+          config.loadConfig(v.s)
+        else:
+          config.loadConfig(v.s)
+      elif t.vt == VALUE_ARRAY:
+        for v in t.a:
+          when nimvm:
+            config.parseConfig(parentDir(v.s), staticRead(v.s))
+          else:
+            config.parseConfig(parentDir(v.s), newFileStream(v.s))
     of "start":
       for k, v in v:
         case k
@@ -233,10 +308,21 @@ proc parseConfig(config: Config, dir: string, t: TomlValue) =
         var conf = StaticSiteConfig()
         for k, v in v:
           case k
-          of "url": conf.url = v.s
-          of "substitute_url": conf.subst = some(v.s)
-        if conf.url != "":
-          config.siteconf.add(conf)
+          of "url": conf.url = some(v.s)
+          of "host": conf.host = some(v.s)
+          of "rewrite-url": conf.subst = some(v.s)
+          of "cookie": conf.cookie = v.b
+        assert conf.url.isSome != conf.host.isSome
+        config.siteconf.add(conf)
+    of "omnirule":
+      for v in v:
+        var rule = StaticOmniRule()
+        for k, v in v:
+          case k
+          of "match": rule.match = v.s
+          of "substitute-url": rule.subst = v.s
+        if rule.match != "":
+          config.omnirules.add(rule)
 
 proc parseConfig(config: Config, dir: string, stream: Stream) =
   config.parseConfig(dir, parseToml(stream))
@@ -272,3 +358,6 @@ proc readConfig*(): Config =
   when defined(debug):
     result.readConfig(getCurrentDir() / "res")
   result.readConfig(getConfigDir() / "chawan")
+
+proc addConfigModule*(ctx: JSContext) =
+  ctx.registerType(Config)
diff --git a/src/config/toml.nim b/src/config/toml.nim
index c5a93b8a..2c4550b5 100644
--- a/src/config/toml.nim
+++ b/src/config/toml.nim
@@ -447,5 +447,6 @@ proc parseToml*(inputStream: Stream): TomlValue =
         state.consumeComment()
       of '\t', ' ': discard
       else: state.syntaxError(fmt"invalid character after value: {c}")
+  inputStream.close()
 
   return TomlValue(vt: VALUE_TABLE, t: state.root)
diff --git a/src/display/client.nim b/src/display/client.nim
index 7bf9a278..90945968 100644
--- a/src/display/client.nim
+++ b/src/display/client.nim
@@ -48,7 +48,7 @@ type
     pager {.jsget.}: Pager
     line {.jsget.}: LineEdit
     sevent: seq[Container]
-    config: Config
+    config {.jsget.}: Config
     jsrt: JSRuntime
     jsctx: JSContext
     timeoutid: int
@@ -466,7 +466,9 @@ proc newClient*(config: Config, dispatcher: Dispatcher): Client =
   result.jsrt.setInterruptHandler(interruptHandler, cast[pointer](result))
   let ctx = result.jsrt.newJSContext()
   result.jsctx = ctx
-  result.pager = newPager(config, result.attrs, dispatcher, result.config.getSiteConfig(ctx))
+  result.pager = newPager(config, result.attrs, dispatcher,
+                          result.config.getSiteConfig(ctx),
+                          result.config.getOmniRules(ctx))
   var global = ctx.getGlobalObject()
   ctx.registerType(Client, asglobal = true)
   global.setOpaque(result)
@@ -481,5 +483,7 @@ proc newClient*(config: Config, dispatcher: Dispatcher): Client =
   ctx.addHTMLModule()
   ctx.addRequestModule()
   ctx.addLineEditModule()
+  ctx.addConfigModule()
   ctx.addPagerModule()
   ctx.addContainerModule()
+  ctx.addConfigModule()
diff --git a/src/display/pager.nim b/src/display/pager.nim
index 765e1e73..06b28346 100644
--- a/src/display/pager.nim
+++ b/src/display/pager.nim
@@ -1,3 +1,4 @@
+import deques
 import net
 import options
 import os
@@ -22,6 +23,7 @@ import js/javascript
 import js/regex
 import types/buffersource
 import types/color
+import types/cookie
 import types/dispatcher
 import types/url
 import utils/twtstr
@@ -55,6 +57,8 @@ type
     term*: Terminal
     linehist: array[LineMode, LineHistory]
     siteconf: seq[SiteConfig]
+    omnirules: seq[OmniRule]
+    cookiejars: Table[string, CookieJar]
 
 func attrs(pager: Pager): WindowAttributes = pager.term.attrs
 
@@ -147,7 +151,7 @@ proc isearchBackward(pager: Pager) {.jsfunc.} =
   pager.container.pushCursorPos()
   pager.setLineEdit("?", ISEARCH_B)
 
-proc newPager*(config: Config, attrs: WindowAttributes, dispatcher: Dispatcher, siteconf: seq[SiteConfig]): Pager =
+proc newPager*(config: Config, attrs: WindowAttributes, dispatcher: Dispatcher, siteconf: seq[SiteConfig], omnirules: seq[OmniRule]): Pager =
   let pager = Pager(
     dispatcher: dispatcher,
     config: config,
@@ -156,9 +160,13 @@ proc newPager*(config: Config, attrs: WindowAttributes, dispatcher: Dispatcher,
     term: newTerminal(stdout, config, attrs),
   )
   for sc in siteconf:
-    # not sure why but normal copies don't seem to work here...
+    # not sure why but normal copies don't seem to work here... probably
+    # something weird going on with lambdas
     pager.siteconf.add(sc)
     pager.siteconf[^1].subst = sc.subst
+  for rule in omnirules:
+    pager.omnirules.add(rule)
+    pager.omnirules[^1].subst = rule.subst
   return pager
 
 proc launchPager*(pager: Pager, tty: File) =
@@ -304,7 +312,7 @@ proc draw*(pager: Pager) =
     pager.term.writeGrid(pager.statusgrid, 0, pager.attrs.height - 1)
   pager.term.outputGrid()
   if pager.lineedit.isSome:
-    pager.term.setCursor(pager.lineedit.get.getCursorX(), pager.container.attrs.height - 1)
+    pager.term.setCursor(pager.lineedit.get.getCursorX(), pager.attrs.height - 1)
   else:
     pager.term.setCursor(pager.container.acursorx, pager.container.acursory)
   pager.term.showCursor()
@@ -459,21 +467,24 @@ proc windowChange*(pager: Pager, attrs: WindowAttributes) =
   for container in pager.containers:
     container.windowChange(attrs)
 
-# ugh...
-proc substituteUrl(pager: Pager, request: Request) =
-  let surl = $request.url
+proc applySiteconf(pager: Pager, request: Request) =
+  let url = $request.url
+  let host = $request.url.host
   for sc in pager.siteconf:
-    if sc.url.exec(surl).success:
-      let s = sc.subst(surl)
-      if s.isSome:
-        let nurl = parseURL(s.get)
-        if nurl.isSome:
-          request.url = nurl.get
-      break
+    if sc.url.isSome and not sc.url.get.exec(url).success:
+      continue
+    elif sc.host.isSome and not sc.host.get.exec(host).success:
+      continue
+    if sc.subst != nil:
+      let s = sc.subst(request.url)
+      if s.isSome and s.get != nil:
+        request.url = s.get
+    if sc.cookie and request.url.host notin pager.cookiejars:
+      pager.cookiejars[request.url.host] = newCookieJar(request.url)
 
 # Load request in a new buffer.
 proc gotoURL*(pager: Pager, request: Request, prevurl = none(URL), ctype = none(string), replace: Container = nil) =
-  pager.substituteUrl(request)
+  pager.applySiteconf(request)
   if 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
@@ -488,7 +499,8 @@ proc gotoURL*(pager: Pager, request: Request, prevurl = none(URL), ctype = none(
       contenttype: ctype,
       location: request.url
     )
-    let container = pager.dispatcher.newBuffer(pager.config, source)
+    let cookiejar = pager.cookiejars.getOrDefault(request.url.host)
+    let container = pager.dispatcher.newBuffer(pager.config, source, cookiejar)
     if replace != nil:
       container.replace = replace
       container.copyCursorPos(container.replace)
@@ -497,6 +509,12 @@ proc gotoURL*(pager: Pager, request: Request, prevurl = none(URL), ctype = none(
   else:
     pager.container.findAnchor(request.url.anchor)
 
+proc omniRewrite(pager: Pager, s: string): string =
+  for rule in pager.omnirules:
+    if rule.match.exec(s).success:
+      return rule.subst(s).get
+  return s
+
 # When the user has passed a partial URL as an argument, they might've meant
 # either:
 # * file://$PWD/<file>
@@ -504,6 +522,8 @@ proc gotoURL*(pager: Pager, request: Request, prevurl = none(URL), ctype = none(
 # So we attempt to load both, and see what works.
 # (TODO: make this optional)
 proc loadURL*(pager: Pager, url: string, ctype = none(string)) =
+  let url0 = pager.omniRewrite(url)
+  let url = if url[0] == '~': expandPath(url0) else: url0
   let firstparse = parseURL(url)
   if firstparse.issome:
     let prev = if pager.container != nil:
@@ -518,7 +538,6 @@ proc loadURL*(pager: Pager, url: string, ctype = none(string)) =
     if pageurl.isSome: # attempt to load remote page
       urls.add(pageurl.get)
   let cdir = parseURL("file://" & getCurrentDir() & DirSep)
-  let url = if url[0] == '~': expandPath(url) else: url
   let purl = percentEncode(url, LocalPathPercentEncodeSet)
   if purl != url:
     let newurl = parseURL(purl, cdir)
@@ -542,7 +561,7 @@ proc readPipe0*(pager: Pager, ctype: Option[string], fd: FileHandle, location: O
     contenttype: some(ctype.get("text/plain")),
     location: location.get(newURL("file://-"))
   )
-  let container = pager.dispatcher.newBuffer(pager.config, source, title)
+  let container = pager.dispatcher.newBuffer(pager.config, source, nil, title)
   return container
 
 proc readPipe*(pager: Pager, ctype: Option[string], fd: FileHandle) =
@@ -726,12 +745,16 @@ proc handleEvent0(pager: Pager, container: Container, event: ContainerEvent): bo
     if pager.container == container:
       pager.alert(event.msg)
       pager.refreshStatusMsg()
+  of SET_COOKIE:
+    let host = container.source.location.host
+    if host in pager.cookiejars:
+      pager.cookiejars[host].cookies.add(event.cookies)
   of NO_EVENT: discard
   return true
 
 proc handleEvents*(pager: Pager, container: Container): bool =
   while container.events.len > 0:
-    let event = container.events.pop()
+    let event = container.events.popFirst()
     if not pager.handleEvent0(container, event):
       return false
   return true
diff --git a/src/display/term.nim b/src/display/term.nim
index 073b5dfc..247f356d 100644
--- a/src/display/term.nim
+++ b/src/display/term.nim
@@ -386,7 +386,26 @@ proc writeGrid*(term: Terminal, grid: FixedGrid, x = 0, y = 0) =
             cell.format.fgcolor = grid[(ly - y) * grid.width + (lx - x)].format.fgcolor
           j += cell[].width()
 
+proc applyConfig(term: Terminal) =
+  if term.config.colormode.isSome:
+    term.colormode = term.config.colormode.get
+  elif term.isatty():
+    term.colormode = ANSI
+    let colorterm = getEnv("COLORTERM")
+    case colorterm
+    of "24bit", "truecolor": term.colormode = TRUE_COLOR
+  if term.config.formatmode.isSome:
+    term.formatmode = term.config.formatmode.get
+  for fm in FormatFlags:
+    if fm in term.config.noformatmode:
+      term.formatmode.excl(fm)
+  if term.isatty() and term.config.altscreen.isSome:
+    term.smcup = term.config.altscreen.get
+  term.mincontrast = term.config.mincontrast
+
 proc outputGrid*(term: Terminal) =
+  if term.config.termreload:
+    term.applyConfig()
   term.outfile.write(term.resetFormat())
   if term.config.forceclear or not term.cleared:
     term.outfile.write(term.generateFullOutput(term.canvas))
@@ -488,21 +507,7 @@ proc detectTermAttributes(term: Terminal) =
     if term.isatty():
       term.smcup = true
       term.formatmode = {low(FormatFlags)..high(FormatFlags)}
-  if term.config.colormode.isSome:
-    term.colormode = term.config.colormode.get
-  elif term.isatty():
-    term.colormode = ANSI
-    let colorterm = getEnv("COLORTERM")
-    case colorterm
-    of "24bit", "truecolor": term.colormode = TRUE_COLOR
-  if term.config.formatmode.isSome:
-    term.formatmode = term.config.formatmode.get
-  for fm in FormatFlags:
-    if fm in term.config.noformatmode:
-      term.formatmode.excl(fm)
-  if term.isatty() and term.config.altscreen.isSome:
-    term.smcup = term.config.altscreen.get
-  term.mincontrast = term.config.mincontrast
+  term.applyConfig()
 
 proc start*(term: Terminal, infile: File) =
   term.infile = infile
diff --git a/src/io/loader.nim b/src/io/loader.nim
index dbca256b..671b9ca3 100644
--- a/src/io/loader.nim
+++ b/src/io/loader.nim
@@ -26,6 +26,7 @@ import io/urlfilter
 import ips/serialize
 import ips/serversocket
 import ips/socketstream
+import types/cookie
 import types/mime
 import types/url
 import utils/twtstr
@@ -84,7 +85,7 @@ proc loadResource(request: Request, ostream: Stream) =
     ostream.flush()
 
 var ssock: ServerSocket
-proc runFileLoader*(fd: cint, defaultHeaders: HeaderList, filter: URLFilter) =
+proc runFileLoader*(fd: cint, defaultHeaders: HeaderList, filter: URLFilter, cookiejar: CookieJar) =
   if curl_global_init(CURL_GLOBAL_ALL) != CURLE_OK:
     raise newException(Defect, "Failed to initialize libcurl.")
   ssock = initServerSocket()
@@ -116,6 +117,10 @@ proc runFileLoader*(fd: cint, defaultHeaders: HeaderList, filter: URLFilter) =
           for k, v in defaultHeaders.table:
             if k notin request.headers.table:
               request.headers.table[k] = v
+          if cookiejar != nil and cookiejar.cookies.len > 0:
+            if request.url.host == cookiejar.location.host:
+              if "Cookie" notin request.headers.table:
+                request.headers["Cookie"] = $cookiejar
           loadResource(request, stream)
         stream.close()
       of QUIT:
diff --git a/src/io/request.nim b/src/io/request.nim
index 9332f9ff..7587b2e0 100644
--- a/src/io/request.nim
+++ b/src/io/request.nim
@@ -128,30 +128,29 @@ func newHeaderList*(table: Table[string, string]): HeaderList =
     else:
       result.table[k] = @[v]
 
-func newRequest*(url: Url,
-                 httpmethod = HTTP_GET,
-                 headers: seq[(string, string)] = @[],
-                 body = none(string),
+func newRequest*(url: Url, httpmethod: HttpMethod, headers: HeaderList,
+                 body = none(string), multipart = none(MimeData)): Request =
+  return Request(
+    url: url,
+    httpmethod: httpmethod,
+    headers: headers,
+    body: body,
+    multipart: multipart
+  )
+
+func newRequest*(url: Url, httpmethod = HTTP_GET,
+                 headers: seq[(string, string)] = @[], body = none(string),
                  multipart = none(MimeData)): Request {.jsctor.} =
-  new(result)
-  result.httpmethod = httpmethod
-  result.url = url
-  result.headers = newHeaderList()
-  for it in headers:
-    if it[1] != "": #TODO not sure if this is a good idea, options would probably work better
-      result.headers.table[it[0]] = @[it[1]]
-  result.body = body
-  result.multipart = multipart
-
-func newRequest*(url: Url,
-                 httpmethod: HttpMethod,
-                 headers: openarray[(string, string)],
-                 body = none(string),
+  let hl = newHeaderList()
+  for pair in headers:
+    let (k, v) = pair
+    hl.table[k] = @[v]
+  return newRequest(url, httpmethod, hl, body, multipart)
+
+func newRequest*(url: Url, httpmethod: HttpMethod,
+                 headers: openarray[(string, string)], body = none(string),
                  multipart = none(MimeData)): Request =
-  var s: seq[(string, string)]
-  for it in headers:
-    s.add(it)
-  return newRequest(url, httpmethod, s, body, multipart)
+  return newRequest(url, httpmethod, @headers, body, multipart)
 
 proc `[]=`*(multipart: var MimeData, k, v: string) =
   multipart.content.add(MimePart(name: k, content: v))
diff --git a/src/ips/forkserver.nim b/src/ips/forkserver.nim
index eb850ea0..e8904354 100644
--- a/src/ips/forkserver.nim
+++ b/src/ips/forkserver.nim
@@ -12,6 +12,7 @@ import io/window
 import ips/serialize
 import ips/serversocket
 import types/buffersource
+import types/cookie
 import utils/twtstr
 
 type
@@ -28,10 +29,11 @@ type
     ostream: Stream
     children: seq[(Pid, Pid)]
 
-proc newFileLoader*(forkserver: ForkServer, defaultHeaders: HeaderList = DefaultHeaders, filter = newURLFilter()): FileLoader =
+proc newFileLoader*(forkserver: ForkServer, defaultHeaders: HeaderList = DefaultHeaders, filter = newURLFilter(), cookiejar: CookieJar = nil): FileLoader =
   forkserver.ostream.swrite(FORK_LOADER)
   forkserver.ostream.swrite(defaultHeaders)
   forkserver.ostream.swrite(filter)
+  forkserver.ostream.swrite(cookiejar)
   forkserver.ostream.flush()
   forkserver.istream.sread(result)
 
@@ -45,7 +47,7 @@ proc removeChild*(forkserver: Forkserver, pid: Pid) =
   forkserver.ostream.swrite(pid)
   forkserver.ostream.flush()
 
-proc forkLoader(ctx: var ForkServerContext, defaultHeaders: HeaderList, filter: URLFilter): FileLoader =
+proc forkLoader(ctx: var ForkServerContext, defaultHeaders: HeaderList, filter: URLFilter, cookiejar: CookieJar): FileLoader =
   var pipefd: array[2, cint]
   if pipe(pipefd) == -1:
     raise newException(Defect, "Failed to open pipe.")
@@ -56,7 +58,7 @@ proc forkLoader(ctx: var ForkServerContext, defaultHeaders: HeaderList, filter:
     ctx.children.setLen(0)
     zeroMem(addr ctx, sizeof(ctx))
     discard close(pipefd[0]) # close read
-    runFileLoader(pipefd[1], defaultHeaders, filter)
+    runFileLoader(pipefd[1], defaultHeaders, filter, cookiejar)
     assert false
   let readfd = pipefd[0] # get read
   discard close(pipefd[1]) # close write
@@ -77,7 +79,7 @@ proc forkBuffer(ctx: var ForkServerContext): Pid =
   ctx.istream.sread(config)
   ctx.istream.sread(attrs)
   ctx.istream.sread(mainproc)
-  let loader = ctx.forkLoader(DefaultHeaders, config.filter) #TODO make this configurable
+  let loader = ctx.forkLoader(DefaultHeaders, config.filter, config.cookiejar) #TODO make this configurable
   let pid = fork()
   if pid == 0:
     for i in 0 ..< ctx.children.len: ctx.children[i] = (Pid(0), Pid(0))
@@ -109,9 +111,11 @@ proc runForkServer() =
       of FORK_LOADER:
         var defaultHeaders: HeaderList
         var filter: URLFilter
+        var cookiejar: CookieJar
         ctx.istream.sread(defaultHeaders)
         ctx.istream.sread(filter)
-        let loader = ctx.forkLoader(defaultHeaders, filter)
+        ctx.istream.sread(cookiejar)
+        let loader = ctx.forkLoader(defaultHeaders, filter, cookiejar)
         ctx.ostream.swrite(loader)
         ctx.children.add((loader.process, Pid(-1)))
       of LOAD_CONFIG:
diff --git a/src/ips/serialize.nim b/src/ips/serialize.nim
index 73320b27..ed35371a 100644
--- a/src/ips/serialize.nim
+++ b/src/ips/serialize.nim
@@ -139,7 +139,10 @@ func slen*(b: bool): int =
   return sizeof(uint8)
 
 proc swrite*(stream: Stream, url: Url) =
-  stream.swrite(url.serialize())
+  if url != nil:
+    stream.swrite(url.serialize())
+  else:
+    stream.swrite("")
 
 proc sread*(stream: Stream, url: var Url) =
   var s: string
@@ -215,14 +218,21 @@ func slen*(obj: object): int =
     result += slen(f)
 
 proc swrite*(stream: Stream, obj: ref object) =
-  stream.swrite(obj[])
+  stream.swrite(obj != nil)
+  if obj != nil:
+    stream.swrite(obj[])
 
 proc sread*(stream: Stream, obj: var ref object) =
-  new(obj)
-  stream.sread(obj[])
+  var n: bool
+  stream.sread(n)
+  if n:
+    new(obj)
+    stream.sread(obj[])
 
 func slen*(obj: ref object): int =
-  slen(obj[])
+  result = slen(obj != nil)
+  if obj != nil:
+    result += slen(obj[])
 
 proc swrite*(stream: Stream, part: MimePart) =
   stream.swrite(part.isFile)
diff --git a/src/types/cookie.nim b/src/types/cookie.nim
index b0578e44..807c494f 100644
--- a/src/types/cookie.nim
+++ b/src/types/cookie.nim
@@ -3,18 +3,24 @@ import strutils
 import times
 
 import js/javascript
+import types/url
 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
+type
+  Cookie* = ref object
+    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
+
+  CookieJar* = ref object
+    location*: URL
+    cookies*: seq[Cookie]
 
 proc parseCookieDate(val: string): Option[DateTime] =
   # cookie-date
@@ -101,7 +107,19 @@ proc parseCookieDate(val: string): Option[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.} =
+proc `$`*(cookiejar: CookieJar): string =
+  let t = now().toTime().toUnix()
+  for i in countdown(cookiejar.cookies.high, 0):
+    let cookie = cookiejar.cookies[i]
+    if cookie.expires <= t:
+      cookiejar.cookies.delete(i)
+    else:
+      result.percentEncode(cookie.name, UserInfoPercentEncodeSet)
+      result &= "="
+      result.percentEncode(cookie.value, UserInfoPercentEncodeSet)
+      result &= ";"
+
+proc newCookie*(str: string): Cookie {.jsctor.} =
   let cookie = new(Cookie)
   var first = true
   for part in str.split(';'):
@@ -125,7 +143,8 @@ proc newCookie(str: string): Cookie {.jsctor.} =
       let date = parseCookieDate(val)
       if date.issome:
         cookie.expires = date.get.toTime().toUnix()
-    of "max-age": cookie.maxAge = parseInt64(val)
+    of "max-age":
+      cookie.expires = now().toTime().toUnix() + parseInt64(val)
     of "secure": cookie.secure = true
     of "httponly": cookie.httponly = true
     of "samesite": cookie.samesite = true
@@ -133,5 +152,10 @@ proc newCookie(str: string): Cookie {.jsctor.} =
     of "domain": cookie.domain = val
   return cookie
 
+proc newCookieJar*(location: URL): CookieJar =
+  return CookieJar(
+    location: location
+  )
+
 proc addCookieModule*(ctx: JSContext) =
   ctx.registerType(Cookie)