about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--adapter/protocol/http.nim2
-rw-r--r--doc/api.md5
-rw-r--r--doc/config.md43
-rw-r--r--res/config.toml1
-rw-r--r--src/config/config.nim79
-rw-r--r--src/css/cssparser.nim6
-rw-r--r--src/html/catom.nim4
-rw-r--r--src/html/dom.nim6
-rw-r--r--src/html/env.nim12
-rw-r--r--src/js/timeout.nim20
-rw-r--r--src/layout/renderdocument.nim2
-rw-r--r--src/local/client.nim1
-rw-r--r--src/local/container.nim18
-rw-r--r--src/local/pager.nim92
-rw-r--r--src/local/term.nim12
-rw-r--r--src/server/buffer.nim54
-rw-r--r--src/types/cell.nim20
-rw-r--r--src/utils/twtstr.nim30
18 files changed, 266 insertions, 141 deletions
diff --git a/adapter/protocol/http.nim b/adapter/protocol/http.nim
index b51d6349..d00479e4 100644
--- a/adapter/protocol/http.nim
+++ b/adapter/protocol/http.nim
@@ -135,7 +135,7 @@ proc main() =
   else: discard #TODO
   let headers = getEnv("REQUEST_HEADERS")
   for line in headers.split("\r\n"):
-    if line.startsWithNoCase("Accept-Encoding: "):
+    if line.startsWithIgnoreCase("Accept-Encoding: "):
       let s = line.after(' ')
       # From the CURLOPT_ACCEPT_ENCODING manpage:
       # > The application does not have to keep the string around after
diff --git a/doc/api.md b/doc/api.md
index ac928baf..0480e901 100644
--- a/doc/api.md
+++ b/doc/api.md
@@ -110,9 +110,12 @@ Same as `pager.load(url + "\n")`.</td>
 </tr>
 
 <tr>
-<td>`gotoURL(url)`</td>
+<td>`gotoURL(url, options = {replace: null, contentType: null})`</td>
 <td>Go to the specified URL immediately (without a prompt). This differs from
 `load` and `loadSubmit` in that it *does not* try to correct the URL.<br>
+When `replace` is set, the new buffer may replace the old one if it loads
+successfully. When `contentType` is set, the new buffer's content type is
+forcefully set to that string.<br>
 Use this for loading automatically retrieved (i.e. non-user-provided) URLs.</td>
 </tr>
 
diff --git a/doc/config.md b/doc/config.md
index 9b9bcae1..3ccb70f4 100644
--- a/doc/config.md
+++ b/doc/config.md
@@ -144,10 +144,10 @@ Defaults to false.</td>
 <td>boolean</td>
 <td>Enable/disable cookies on sites.<br>
 Defaults to false.<br>
-Note that in Chawan, each website gets a separate cookie jar, so some websites
-relying on cross-site cookies may not work as expected. You may use the
-`[[siteconf]]` "cookie-jar" and "third-party-cookie" settings to adjust this
-behavior for specific sites.</td>
+Note: in Chawan, each website gets a separate cookie jar, so websites relying on
+cross-site cookies may not work as expected. You may use the `[[siteconf]]`
+"cookie-jar" and "third-party-cookie" settings to adjust this behavior for
+specific sites.</td>
 </tr>
 
 <tr>
@@ -167,6 +167,16 @@ automatically after the buffer is loaded.<br>
 Defaults to false</td>
 </tr>
 
+<tr>
+<td>meta-refresh</td>
+<td>"never" / "always" / "ask"</td>
+<td>Whether or not `http-equiv=refresh` meta tags should be respected. "never"
+completely disables them, "always" automatically accepts all of them, "ask"
+brings up a pop-up menu.<br>
+Defaults to "ask".
+</td>
+</tr>
+
 </table>
 
 ## Search
@@ -702,7 +712,8 @@ returned, it will replace the old one.</td>
 <td>cookie</td>
 <td>boolean</td>
 <td>Whether loading cookies should be allowed for this URL. By default, this is
-false for all websites.</td>
+false for all websites.<br>
+Overrides `buffer.cookie`.</td>
 </tr>
 
 <tr>
@@ -728,7 +739,7 @@ subdomains.</td>
 originating from this domain. Simplified example: if you click a link on a.com
 that refers to b.com, and referer-from is true, b.com is sent "a.com" as the
 Referer header.<br>
-Overrides `buffer.referer-from`. Defaults to false.
+Overrides `buffer.referer-from`.
 </td>
 </tr>
 
@@ -755,8 +766,8 @@ Overrides `buffer.referer-from`. Defaults to false.
 <tr>
 <td>document-charset</td>
 <td>charset label string</td>
-<td>Specify the default encoding for this site. Overrides `document-charset`
-in `[encoding]`.</td>
+<td>Specify the default encoding for this site. Overrides
+`encoding.document-charset`. </td>
 </tr>
 
 <tr>
@@ -772,14 +783,14 @@ with this stylesheet to get the final user stylesheet.)</td>
 <td>proxy</td>
 <td>URL</td>
 <td>Specify a proxy for network requests fetching contents of this buffer.
-Overrides `proxy` in `[network]`.</td>
+Overrides `network.proxy`.</td>
 </tr>
 
 <tr>
 <td>default-headers</td>
 <td>table</td>
 <td>Specify a list of default headers for HTTP(S) network requests to this
-buffer. Overrides `default-headers` in `[network]`.</td>
+buffer. Overrides `network.default-headers`.</td>
 </tr>
 
 <tr>
@@ -796,7 +807,17 @@ are doing.</td>
 <td>autofocus</td>
 <td>boolean</td>
 <td>When set to true, elements with an "autofocus" attribute are focused on
-automatically after the buffer is loaded. Overrides buffer.autofocus.</td>
+automatically after the buffer is loaded. Overrides `buffer.autofocus`.</td>
+</tr>
+
+<tr>
+<td>meta-refresh</td>
+<td>"never" / "always" / "ask"</td>
+<td>Whether or not `http-equiv=refresh` meta tags should be respected. "never"
+completely disables them, "always" automatically accepts all of them, "ask"
+brings up a pop-up menu.<br>
+Overrides `buffer.meta-refresh`.
+</td>
 </tr>
 
 </table>
diff --git a/res/config.toml b/res/config.toml
index 6e05b816..b74b54f8 100644
--- a/res/config.toml
+++ b/res/config.toml
@@ -237,6 +237,7 @@ images = false
 scripting = false
 referer-from = false
 cookie = false
+meta-refresh = "ask"
 
 [search]
 wrap = true
diff --git a/src/config/config.nim b/src/config/config.nim
index 3d4a0217..3c733bc5 100644
--- a/src/config/config.nim
+++ b/src/config/config.nim
@@ -30,12 +30,20 @@ import utils/twtstr
 
 type
   ColorMode* = enum
-    cmMonochrome, cmANSI, cmEightBit, cmTrueColor
+    cmMonochrome = "monochrome"
+    cmANSI = "ansi"
+    cmEightBit = "eight-bit"
+    cmTrueColor = "true-color"
 
-  FormatMode* = set[FormatFlags]
+  MetaRefresh* = enum
+    mrNever = "never"
+    mrAlways = "always"
+    mrAsk = "ask"
 
   ImageMode* = enum
-    imNone = "none", imSixel = "sixel", imKitty = "kitty"
+    imNone = "none"
+    imSixel = "sixel"
+    imKitty = "kitty"
 
   ChaPathResolved* = distinct string
 
@@ -65,6 +73,7 @@ type
     default_headers*: TableRef[string, string]
     insecure_ssl_no_verify*: Option[bool]
     autofocus*: Option[bool]
+    meta_refresh*: Option[MetaRefresh]
 
   OmniRule* = ref object
     match*: Regex
@@ -116,8 +125,8 @@ type
 
   DisplayConfig = object
     color_mode* {.jsgetset.}: Option[ColorMode]
-    format_mode* {.jsgetset.}: Option[FormatMode]
-    no_format_mode* {.jsgetset.}: FormatMode
+    format_mode* {.jsgetset.}: Option[set[FormatFlag]]
+    no_format_mode* {.jsgetset.}: set[FormatFlag]
     image_mode* {.jsgetset.}: Option[ImageMode]
     alt_screen* {.jsgetset.}: Option[bool]
     highlight_color* {.jsgetset.}: ARGBColor
@@ -148,6 +157,7 @@ type
     cookie* {.jsgetset.}: bool
     referer_from* {.jsgetset.}: bool
     autofocus* {.jsgetset.}: bool
+    meta_refresh* {.jsgetset.}: MetaRefresh
 
   Config* = ref object
     jsctx: JSContext
@@ -326,11 +336,9 @@ proc parseConfigValue(ctx: var ConfigParser; x: var int32; v: TomlValue;
   k: string)
 proc parseConfigValue(ctx: var ConfigParser; x: var int64; v: TomlValue;
   k: string)
-proc parseConfigValue(ctx: var ConfigParser; x: var Option[ColorMode];
-  v: TomlValue; k: string)
-proc parseConfigValue(ctx: var ConfigParser; x: var Option[FormatMode];
-  v: TomlValue; k: string)
-proc parseConfigValue(ctx: var ConfigParser; x: var FormatMode; v: TomlValue;
+proc parseConfigValue(ctx: var ConfigParser; x: var ColorMode; v: TomlValue;
+  k: string)
+proc parseConfigValue[T](ctx: var ConfigParser; x: var Option[T]; v: TomlValue;
   k: string)
 proc parseConfigValue(ctx: var ConfigParser; x: var ARGBColor; v: TomlValue;
   k: string)
@@ -466,48 +474,21 @@ proc parseConfigValue(ctx: var ConfigParser; x: var int64; v: TomlValue;
   typeCheck(v, tvtInteger, k)
   x = v.i
 
-proc parseConfigValue(ctx: var ConfigParser; x: var Option[ColorMode];
-    v: TomlValue; k: string) =
+proc parseConfigValue(ctx: var ConfigParser; x: var ColorMode; v: TomlValue;
+    k: string) =
   typeCheck(v, tvtString, k)
-  case v.s
-  of "auto": x = none(ColorMode)
-  of "monochrome": x = some(cmMonochrome)
-  of "ansi": x = some(cmANSI)
-  of "8bit", "eight-bit": x = some(cmEightBit)
-  of "24bit", "true-color": x = some(cmTrueColor)
+  let y = strictParseEnum[ColorMode](v.s)
+  if y.isSome:
+    x = y.get
+  # backwards compat
+  elif v.s == "8bit":
+    x = cmEightBit
+  elif v.s == "24bit":
+    x = cmTrueColor
   else:
     raise newException(ValueError, "unknown color mode '" & v.s &
       "' for key " & k)
 
-proc parseConfigValue(ctx: var ConfigParser; x: var Option[FormatMode];
-    v: TomlValue; k: string) =
-  typeCheck(v, {tvtString, tvtArray}, k)
-  if v.t == tvtString and v.s == "auto":
-    x = none(FormatMode)
-  else:
-    var y: FormatMode
-    ctx.parseConfigValue(y, v, k)
-    x = some(y)
-
-proc parseConfigValue(ctx: var ConfigParser; x: var FormatMode; v: TomlValue;
-    k: string) =
-  typeCheck(v, tvtArray, k)
-  for i in 0 ..< v.a.len:
-    let kk = k & "[" & $i & "]"
-    let vv = v.a[i]
-    typeCheck(vv, tvtString, kk)
-    case vv.s
-    of "bold": x.incl(ffBold)
-    of "italic": x.incl(ffItalic)
-    of "underline": x.incl(ffUnderline)
-    of "reverse": x.incl(ffReverse)
-    of "strike": x.incl(ffStrike)
-    of "overline": x.incl(ffOverline)
-    of "blink": x.incl(ffBlink)
-    else:
-      raise newException(ValueError, "unknown format mode '" & vv.s &
-        "' for key " & kk)
-
 proc parseConfigValue(ctx: var ConfigParser; x: var ARGBColor; v: TomlValue;
     k: string) =
   typeCheck(v, tvtString, k)
@@ -560,14 +541,14 @@ proc parseConfigValue[T](ctx: var ConfigParser; x: var set[T]; v: TomlValue;
   typeCheck(v, {tvtString, tvtArray}, k)
   if v.t == tvtString:
     var xx: T
-    xx.parseConfigValue(v, k)
+    ctx.parseConfigValue(xx, v, k)
     x = {xx}
   else:
     x = {}
     for i in 0 ..< v.a.len:
       let kk = k & "[" & $i & "]"
       var xx: T
-      xx.parseConfigValue(v.a[i], kk)
+      ctx.parseConfigValue(xx, v.a[i], kk)
       x.incl(xx)
 
 proc parseConfigValue(ctx: var ConfigParser; x: var CSSConfig; v: TomlValue;
diff --git a/src/css/cssparser.nim b/src/css/cssparser.nim
index 0d71e6d0..1e56d11f 100644
--- a/src/css/cssparser.nim
+++ b/src/css/cssparser.nim
@@ -908,9 +908,9 @@ proc parseAnB*(state: var CSSParseState): Option[CSSAnB] =
       let tok2 = get_tok
       fail_non_signless_integer tok2, none(CSSAnB)
       return some((-1, -int(tok2.nvalue)))
-    elif tok.value.startsWithNoCase("n-"):
+    elif tok.value.startsWithIgnoreCase("n-"):
       return some((1, -parse_sub_int(tok.value, "n-".len)))
-    elif tok.value.startsWithNoCase("-n-"):
+    elif tok.value.startsWithIgnoreCase("-n-"):
       fail_plus
       return some((-1, -parse_sub_int(tok.value, "n-".len)))
     else:
@@ -952,7 +952,7 @@ proc parseAnB*(state: var CSSParseState): Option[CSSAnB] =
       let tok2 = get_tok
       fail_non_signless_integer tok2, none(CSSAnB)
       return some((int(tok.nvalue), -int(tok2.nvalue)))
-    elif tok.unit.startsWithNoCase("n-"):
+    elif tok.unit.startsWithIgnoreCase("n-"):
       # <ndashdigit-dimension>
       return some((int(tok.nvalue), -parse_sub_int(tok.unit, "n-".len)))
     else:
diff --git a/src/html/catom.nim b/src/html/catom.nim
index f22d74ef..0ff7b949 100644
--- a/src/html/catom.nim
+++ b/src/html/catom.nim
@@ -9,6 +9,7 @@ import monoucha/javascript
 import monoucha/jserror
 import monoucha/tojs
 import types/opt
+import utils/twtstr
 
 # create a static enum compatible with chame/tags
 
@@ -33,6 +34,7 @@ macro makeStaticAtom =
       satColor = "color"
       satCols = "cols"
       satColspan = "colspan"
+      satContent = "content"
       satCrossorigin = "crossorigin"
       satDOMContentLoaded = "DOMContentLoaded"
       satDefer = "defer"
@@ -102,7 +104,7 @@ macro makeStaticAtom =
     if t == TAG_UNKNOWN:
       continue
     let tn = $t
-    let name = "sat" & tn[0].toUpperAscii() & tn.substr(1)
+    let name = "sat" & tn[0].toUpperAscii() & tn.substr(1).kebabToCamelCase()
     seen.incl(tn)
     decl0.add(newNimNode(nnkEnumFieldDef).add(ident(name), newStrLitNode(tn)))
   for i, f in StaticAtom0.getType():
diff --git a/src/html/dom.nim b/src/html/dom.nim
index 7491e56b..a2153acb 100644
--- a/src/html/dom.nim
+++ b/src/html/dom.nim
@@ -2414,6 +2414,12 @@ func findAnchor*(document: Document; id: string): Element =
       return child
   return nil
 
+proc findMetaRefresh*(document: Document): Element =
+  for child in document.elements(TAG_META):
+    if child.attr(satHttpEquiv) == "refresh":
+      return child
+  return nil
+
 func focus*(document: Document): Element =
   return document.internalFocus
 
diff --git a/src/html/env.nim b/src/html/env.nim
index 131d7b4d..187968e3 100644
--- a/src/html/env.nim
+++ b/src/html/env.nim
@@ -112,13 +112,13 @@ proc fetch(window: Window; input: JSValue; init = none(RequestInit)):
 # Forward declaration hack
 windowFetch = fetch
 
-proc setTimeout(window: Window; handler: JSValue; timeout = 0i32): int32
-    {.jsfunc.} =
-  return window.timeouts.setTimeout(ttTimeout, handler, timeout)
+proc setTimeout(window: Window; handler: JSValue; timeout = 0i32;
+    args: varargs[JSValue]): int32 {.jsfunc.} =
+  return window.timeouts.setTimeout(ttTimeout, handler, timeout, args)
 
-proc setInterval(window: Window; handler: JSValue; interval = 0i32): int32
-    {.jsfunc.} =
-  return window.timeouts.setTimeout(ttInterval, handler, interval)
+proc setInterval(window: Window; handler: JSValue; interval = 0i32;
+    args: varargs[JSValue]): int32 {.jsfunc.} =
+  return window.timeouts.setTimeout(ttInterval, handler, interval, args)
 
 proc clearTimeout(window: Window; id: int32) {.jsfunc.} =
   window.timeouts.clearTimeout(id)
diff --git a/src/js/timeout.nim b/src/js/timeout.nim
index 6c4b157f..f8b8ed8a 100644
--- a/src/js/timeout.nim
+++ b/src/js/timeout.nim
@@ -5,6 +5,7 @@ import io/dynstream
 import js/console
 import monoucha/fromjs
 import monoucha/javascript
+import monoucha/jsutils
 import types/opt
 
 type
@@ -16,8 +17,9 @@ type
     t: TimeoutType
     fd: int
     val: JSValue
+    args: seq[JSValue]
 
-  TimeoutState* = object
+  TimeoutState* = ref object
     timeoutid: int32
     timeouts: Table[int32, TimeoutEntry]
     timeoutFds: Table[int, int32]
@@ -43,33 +45,39 @@ proc clearTimeout*(state: var TimeoutState; id: int32) =
     let entry = state.timeouts[id]
     state.selector.unregister(entry.fd)
     JS_FreeValue(state.jsctx, entry.val)
+    for arg in entry.args:
+      JS_FreeValue(state.jsctx, arg)
     state.timeoutFds.del(entry.fd)
     state.timeouts.del(id)
 
 #TODO varargs
 proc setTimeout*(state: var TimeoutState; t: TimeoutType; handler: JSValue;
-    timeout = 0i32): int32 =
+    timeout: int32; args: openArray[JSValue]): int32 =
   let id = state.timeoutid
   inc state.timeoutid
   let fd = state.selector.registerTimer(max(timeout, 1), t == ttTimeout, 0)
   state.timeoutFds[fd] = id
-  state.timeouts[id] = TimeoutEntry(
+  let entry = TimeoutEntry(
     t: t,
     fd: fd,
     val: JS_DupValue(state.jsctx, handler)
   )
+  for arg in args:
+    entry.args.add(JS_DupValue(state.jsctx, arg))
+  state.timeouts[id] = entry
   return id
 
 proc runEntry(state: var TimeoutState; entry: TimeoutEntry; name: string) =
   if JS_IsFunction(state.jsctx, entry.val):
-    let ret = JS_Call(state.jsctx, entry.val, JS_UNDEFINED, 0, nil)
+    let ret = JS_Call(state.jsctx, entry.val, JS_UNDEFINED,
+      cint(entry.args.len), entry.args.toJSValueArray())
     if JS_IsException(ret):
       state.jsctx.writeException(state.err)
     JS_FreeValue(state.jsctx, ret)
   else:
     let s = fromJS[string](state.jsctx, entry.val)
     if s.isSome:
-      state.evalJSFree(s.get, "setInterval handler")
+      state.evalJSFree(s.get, name)
 
 proc runTimeoutFd*(state: var TimeoutState; fd: int): bool =
   if fd notin state.timeoutFds:
@@ -85,5 +93,7 @@ proc clearAll*(state: var TimeoutState) =
   for entry in state.timeouts.values:
     state.selector.unregister(entry.fd)
     JS_FreeValue(state.jsctx, entry.val)
+    for arg in entry.args:
+      JS_FreeValue(state.jsctx, arg)
   state.timeouts.clear()
   state.timeoutFds.clear()
diff --git a/src/layout/renderdocument.nim b/src/layout/renderdocument.nim
index 125e53a6..a407cdfe 100644
--- a/src/layout/renderdocument.nim
+++ b/src/layout/renderdocument.nim
@@ -55,7 +55,7 @@ proc addFormat(line: var FlexibleLine; pos: int; format: Format;
 func toFormat(computed: CSSComputedValues): Format =
   if computed == nil:
     return Format()
-  var flags: set[FormatFlags]
+  var flags: set[FormatFlag] = {}
   if computed{"font-style"} in {FontStyleItalic, FontStyleOblique}:
     flags.incl(ffItalic)
   if computed{"font-weight"} > 500:
diff --git a/src/local/client.nim b/src/local/client.nim
index 1fc5a7f1..eae079d9 100644
--- a/src/local/client.nim
+++ b/src/local/client.nim
@@ -738,6 +738,7 @@ proc launchClient*(client: Client; pages: seq[string];
   # better associate it with jsctx
   client.timeouts = newTimeoutState(client.selector, client.jsctx,
     client.console.err, proc(src, file: string) = client.evalJSFree(src, file))
+  client.pager.timeouts = client.timeouts
   addExitProc((proc() = client.cleanup()))
   if client.config.start.startup_script != "":
     let s = if fileExists(client.config.start.startup_script):
diff --git a/src/local/container.nim b/src/local/container.nim
index 805c1b80..62bda3f2 100644
--- a/src/local/container.nim
+++ b/src/local/container.nim
@@ -49,7 +49,8 @@ type
 
   ContainerEventType* = enum
     cetAnchor, cetNoAnchor, cetReadLine, cetReadArea, cetReadFile, cetOpen,
-    cetSetLoadInfo, cetStatus, cetAlert, cetLoaded, cetTitle, cetCancel
+    cetSetLoadInfo, cetStatus, cetAlert, cetLoaded, cetTitle, cetCancel,
+    cetMetaRefresh
 
   ContainerEvent* = object
     case t*: ContainerEventType
@@ -67,6 +68,9 @@ type
       anchor*: string
     of cetAlert:
       msg*: string
+    of cetMetaRefresh:
+      refreshIn*: int
+      refreshURL*: URL
     else: discard
 
   HighlightType = enum
@@ -144,6 +148,7 @@ type
     replaceBackup*: Container # for redirection; when set, we get discarded
     # if we are referenced by another container, replaceRef is set so that we
     # can clear ourselves on discard
+    #TODO this is a mess :(
     replaceRef*: Container
     code*: int # note: this is not the status code, but the ConnectErrorCode.
     errorMessage*: string
@@ -1713,6 +1718,15 @@ proc onload(container: Container; res: int) =
       )
     else:
       container.needslines = true
+    if container.config.metaRefresh != mrNever:
+      container.iface.checkRefresh().then(proc(res: CheckRefreshResult) =
+        if res.n >= 0:
+          container.triggerEvent(ContainerEvent(
+            t: cetMetaRefresh,
+            refreshIn: res.n,
+            refreshURL: if res.url != nil: res.url else: container.url
+          ))
+      )
   else:
     container.needslines = true
     container.setLoadInfo(convertSize(res) & " loaded")
@@ -1744,7 +1758,7 @@ proc applyResponse*(container: Container; response: Response;
     cookieJar.add(response.extractCookies())
   # set referrer policy, if any
   let referrerPolicy = response.extractReferrerPolicy()
-  if container.config.referer_from:
+  if container.config.refererFrom:
     if referrerPolicy.isSome:
       container.loaderConfig.referrerPolicy = referrerPolicy.get
   else:
diff --git a/src/local/pager.nim b/src/local/pager.nim
index e150507c..50a07ce8 100644
--- a/src/local/pager.nim
+++ b/src/local/pager.nim
@@ -5,6 +5,7 @@ import std/os
 import std/osproc
 import std/posix
 import std/selectors
+import std/sets
 import std/tables
 import std/unicode
 
@@ -21,6 +22,7 @@ import io/socketstream
 import io/stdio
 import io/tempfile
 import io/urlfilter
+import js/timeout
 import layout/renderdocument
 import loader/connecterror
 import loader/headers
@@ -146,12 +148,14 @@ type
     numload*: int # number of pages currently being loaded
     precnum*: int32 # current number prefix (when vi-numeric-prefix is true)
     procmap*: seq[ProcMapItem]
+    refreshAllowed: HashSet[string]
     regex: Opt[Regex]
     reverseSearch: bool
     scommand*: string
     selector*: Selector[int]
     status: Surface
     term*: Terminal
+    timeouts*: TimeoutState
     unreg*: seq[Container]
 
 jsDestructor(Pager)
@@ -1119,7 +1123,7 @@ proc applySiteconf(pager: Pager; url: var URL; charsetOverride: Charset;
   let ctx = pager.jsctx
   var res = BufferConfig(
     userstyle: pager.config.css.stylesheet,
-    referer_from: pager.config.buffer.referer_from,
+    refererFrom: pager.config.buffer.referer_from,
     scripting: pager.config.buffer.scripting,
     charsets: pager.config.encoding.document_charset,
     images: pager.config.buffer.images,
@@ -1127,7 +1131,8 @@ proc applySiteconf(pager: Pager; url: var URL; charsetOverride: Charset;
     autofocus: pager.config.buffer.autofocus,
     isdump: pager.config.start.headless,
     charsetOverride: charsetOverride,
-    protocol: pager.config.protocol
+    protocol: pager.config.protocol,
+    metaRefresh: pager.config.buffer.meta_refresh
   )
   loaderConfig = LoaderClientConfig(
     defaultHeaders: newHeaders(pager.config.network.default_headers),
@@ -1171,7 +1176,7 @@ proc applySiteconf(pager: Pager; url: var URL; charsetOverride: Charset;
     if sc.scripting.isSome:
       res.scripting = sc.scripting.get
     if sc.referer_from.isSome:
-      res.referer_from = sc.referer_from.get
+      res.refererFrom = sc.referer_from.get
     if sc.document_charset.len > 0:
       res.charsets = sc.document_charset
     if sc.images.isSome:
@@ -1187,6 +1192,8 @@ proc applySiteconf(pager: Pager; url: var URL; charsetOverride: Charset;
       loaderConfig.insecureSSLNoVerify = sc.insecure_ssl_no_verify.get
     if sc.autofocus.isSome:
       res.autofocus = sc.autofocus.get
+    if sc.meta_refresh.isSome:
+      res.metaRefresh = sc.meta_refresh.get
   if res.images:
     loaderConfig.filter.allowschemes
       .add(pager.config.external.urimethodmap.imageProtos)
@@ -1196,9 +1203,9 @@ proc applySiteconf(pager: Pager; url: var URL; charsetOverride: Charset;
 proc gotoURL(pager: Pager; request: Request; prevurl = none(URL);
     contentType = none(string); cs = CHARSET_UNKNOWN; replace: Container = nil;
     replaceBackup: Container = nil; redirectDepth = 0;
-    referrer: Container = nil; save = false; url: URL = nil) =
+    referrer: Container = nil; save = false; url: URL = nil): Container =
   pager.navDirection = ndNext
-  if referrer != nil and referrer.config.referer_from:
+  if referrer != nil and referrer.config.refererFrom:
     request.referrer = referrer.url
   let url = if url != nil: url else: request.url
   var loaderConfig: LoaderClientConfig
@@ -1237,8 +1244,10 @@ proc gotoURL(pager: Pager; request: Request; prevurl = none(URL);
     else:
       pager.addContainer(container)
     inc pager.numload
+    return container
   else:
     pager.container.findAnchor(request.url.anchor)
+    return nil
 
 proc omniRewrite(pager: Pager; s: string): string =
   for rule in pager.config.omnirule:
@@ -1271,7 +1280,7 @@ proc loadURL*(pager: Pager; url: string; ctype = none(string);
       some(pager.container.url)
     else:
       none(URL)
-    pager.gotoURL(newRequest(firstparse.get), prev, ctype, cs)
+    discard pager.gotoURL(newRequest(firstparse.get), prev, ctype, cs)
     return
   var urls: seq[URL]
   if pager.config.network.prepend_https and
@@ -1288,10 +1297,10 @@ proc loadURL*(pager: Pager; url: string; ctype = none(string);
   if urls.len == 0:
     pager.alert("Invalid URL " & url)
   else:
-    let prevc = pager.container
-    pager.gotoURL(newRequest(urls.pop()), contentType = ctype, cs = cs)
-    if pager.container != prevc:
-      pager.container.retry = urls
+    let container = pager.gotoURL(newRequest(urls.pop()), contentType = ctype,
+      cs = cs)
+    if container != nil:
+      container.retry = urls
 
 proc readPipe0*(pager: Pager; contentType: string; cs: Charset;
     fd: FileHandle; url: URL; title: string; flags: set[ContainerFlag]):
@@ -1402,7 +1411,7 @@ proc updateReadLine*(pager: Pager) =
       of lmPassword:
         let url = LineDataAuth(pager.lineData).url
         url.password = lineedit.news
-        pager.gotoURL(newRequest(url), some(pager.container.url),
+        discard pager.gotoURL(newRequest(url), some(pager.container.url),
           replace = pager.container, referrer = pager.container)
         pager.lineData = nil
       of lmCommand:
@@ -1472,18 +1481,26 @@ proc load(pager: Pager; s = "") {.jsfunc.} =
     pager.setLineEdit(lmLocation, s)
 
 # Go to specific URL (for JS)
-proc jsGotoURL(pager: Pager; v: JSValue): JSResult[void] {.jsfunc: "gotoURL".} =
-  let req = fromJS[JSRequest](pager.jsctx, v)
-  if req.isSome:
-    pager.gotoURL(req.get.request)
+type GotoURLDict = object of JSDict
+  contentType: Option[string]
+  replace: Container
+
+proc jsGotoURL(pager: Pager; v: JSValue; t = GotoURLDict()): JSResult[void]
+    {.jsfunc: "gotoURL".} =
+  let request = if (let x = fromJS[JSRequest](pager.jsctx, v); x.isSome):
+    x.get.request
+  elif (let x = fromJS[URL](pager.jsctx, v); x.isSome):
+    newRequest(x.get)
   else:
     let s = ?fromJS[string](pager.jsctx, v)
-    pager.gotoURL(newRequest(?newURL(s)))
-  ok()
+    newRequest(?newURL(s))
+  discard pager.gotoURL(request, contentType = t.contentType,
+    replace = t.replace)
+  return ok()
 
 # Reload the page in a new buffer, then kill the previous buffer.
 proc reload(pager: Pager) {.jsfunc.} =
-  pager.gotoURL(newRequest(pager.container.url), none(URL),
+  discard pager.gotoURL(newRequest(pager.container.url), none(URL),
     pager.container.contentType, replace = pager.container)
 
 proc setEnvVars(pager: Pager) {.jsfunc.} =
@@ -1818,18 +1835,18 @@ proc redirectTo(pager: Pager; container: Container; request: Request) =
     container.replaceBackup
   else:
     container.find(ndAny)
-  pager.gotoURL(request, some(container.url), replace = container,
+  let nc = pager.gotoURL(request, some(container.url), replace = container,
     replaceBackup = replaceBackup, redirectDepth = container.redirectDepth + 1,
     referrer = container)
-  pager.container.loadinfo = "Redirecting to " & $request.url
-  pager.onSetLoadInfo(pager.container)
+  nc.loadinfo = "Redirecting to " & $request.url
+  pager.onSetLoadInfo(nc)
   dec pager.numload
 
 proc fail(pager: Pager; container: Container; errorMessage: string) =
   dec pager.numload
   pager.deleteContainer(container, container.find(ndAny))
   if container.retry.len > 0:
-    pager.gotoURL(newRequest(container.retry.pop()),
+    discard pager.gotoURL(newRequest(container.retry.pop()),
       contentType = container.contentType)
   else:
     pager.alert("Can't load " & $container.url & " (" & errorMessage & ")")
@@ -1997,6 +2014,16 @@ proc handleConnectingContainerError*(pager: Pager; i: int) =
   item.stream.sclose()
   pager.connectingContainers.del(i)
 
+proc metaRefresh(pager: Pager; container: Container; n: int; url: URL) =
+  let ctx = pager.jsctx
+  let fun = ctx.newFunction(["url", "replace"],
+    "pager.gotoURL(url, {replace: replace})")
+  let args = [ctx.toJS(url), ctx.toJS(container)]
+  discard pager.timeouts.setTimeout(ttTimeout, fun, int32(n), args)
+  JS_FreeValue(ctx, fun)
+  for arg in args:
+    JS_FreeValue(ctx, arg)
+
 proc handleEvent0(pager: Pager; container: Container; event: ContainerEvent):
     bool =
   case event.t
@@ -2035,12 +2062,12 @@ proc handleEvent0(pager: Pager; container: Container; event: ContainerEvent):
         not event.save and not container.isHoverURL(url):
       pager.ask("Open pop-up? " & $url).then(proc(x: bool) =
         if x:
-          pager.gotoURL(event.request, some(container.url),
+          discard pager.gotoURL(event.request, some(container.url),
             referrer = pager.container, save = event.save)
       )
     else:
       let url = if event.url != nil: event.url else: event.request.url
-      pager.gotoURL(event.request, some(container.url),
+      discard pager.gotoURL(event.request, some(container.url),
         referrer = pager.container, save = event.save, url = url)
   of cetStatus:
     if pager.container == container:
@@ -2068,6 +2095,23 @@ proc handleEvent0(pager: Pager; container: Container; event: ContainerEvent):
       pager.connectingContainers.del(i)
       pager.unregisterFd(int(item.stream.fd))
       item.stream.sclose()
+  of cetMetaRefresh:
+    let url = event.refreshURL
+    let n = event.refreshIn
+    case container.config.metaRefresh
+    of mrNever: assert false
+    of mrAlways: pager.metaRefresh(container, n, url)
+    of mrAsk:
+      let surl = $url
+      if surl in pager.refreshAllowed:
+        pager.metaRefresh(container, n, url)
+      else:
+        pager.ask("Redirect to " & $url & " (in " & $n & "ms)?")
+          .then(proc(x: bool) =
+            if x:
+              pager.refreshAllowed.incl($url)
+              pager.metaRefresh(container, n, url)
+          )
   return true
 
 proc handleEvents*(pager: Pager; container: Container) =
diff --git a/src/local/term.nim b/src/local/term.nim
index 7391a17e..758f0fe3 100644
--- a/src/local/term.nim
+++ b/src/local/term.nim
@@ -84,7 +84,7 @@ type
     lineDamage: seq[int]
     attrs*: WindowAttributes
     colorMode: ColorMode
-    formatMode: FormatMode
+    formatMode: set[FormatFlag]
     imageMode*: ImageMode
     smcup: bool
     tc: Termcap
@@ -257,7 +257,7 @@ proc resetFormat(term: Terminal): string =
       return term.cap me
   return SGR()
 
-proc startFormat(term: Terminal; flag: FormatFlags): string =
+proc startFormat(term: Terminal; flag: FormatFlag): string =
   when termcap_found:
     if term.isatty():
       case flag
@@ -269,7 +269,7 @@ proc startFormat(term: Terminal; flag: FormatFlags): string =
       else: discard
   return SGR(FormatCodes[flag].s)
 
-proc endFormat(term: Terminal; flag: FormatFlags): string =
+proc endFormat(term: Terminal; flag: FormatFlag): string =
   when termcap_found:
     if term.isatty():
       case flag
@@ -389,7 +389,7 @@ template rgbSGR(rgb: RGBColor; bgmod: int): string =
   SGR(38 + bgmod, 2, rgb.r, rgb.g, rgb.b)
 
 proc processFormat*(term: Terminal; format: var Format; cellf: Format): string =
-  for flag in FormatFlags:
+  for flag in FormatFlag:
     if flag in term.formatMode:
       if flag in format.flags and flag notin cellf.flags:
         result &= term.endFormat(flag)
@@ -587,7 +587,7 @@ proc applyConfig(term: Terminal) =
     term.colorMode = term.config.display.color_mode.get
   if term.config.display.format_mode.isSome:
     term.formatMode = term.config.display.format_mode.get
-  for fm in FormatFlags:
+  for fm in FormatFlag:
     if fm in term.config.display.no_format_mode:
       term.formatMode.excl(fm)
   if term.config.display.image_mode.isSome:
@@ -1219,7 +1219,7 @@ proc detectTermAttributes(term: Terminal; windowOnly: bool): TermStartResult =
         term.formatMode.incl(ffBlink)
   else:
     term.smcup = true
-    term.formatMode = {low(FormatFlags)..high(FormatFlags)}
+    term.formatMode = {FormatFlag.low..FormatFlag.high}
 
 type
   MouseInputType* = enum
diff --git a/src/server/buffer.nim b/src/server/buffer.nim
index 984ed826..e27e42e0 100644
--- a/src/server/buffer.nim
+++ b/src/server/buffer.nim
@@ -1,4 +1,4 @@
-from std/strutils import split, toUpperAscii, find
+from std/strutils import split, toUpperAscii, find, AllChars
 
 import std/macros
 import std/nativesockets
@@ -62,7 +62,8 @@ type
     bcReadCanceled, bcClick, bcFindNextLink, bcFindPrevLink, bcFindNthLink,
     bcFindRevNthLink, bcFindNextMatch, bcFindPrevMatch, bcGetLines,
     bcUpdateHover, bcGotoAnchor, bcCancel, bcGetTitle, bcSelect, bcClone,
-    bcFindPrevParagraph, bcFindNextParagraph, bcMarkURL, bcToggleImages
+    bcFindPrevParagraph, bcFindNextParagraph, bcMarkURL, bcToggleImages,
+    bcCheckRefresh
 
   BufferState = enum
     bsLoadingPage, bsLoadingResources, bsLoaded
@@ -131,7 +132,7 @@ type
 
   BufferConfig* = object
     userstyle*: string
-    referer_from*: bool
+    refererFrom*: bool
     styling*: bool
     scripting*: bool
     images*: bool
@@ -140,6 +141,7 @@ type
     charsetOverride*: Charset
     protocol*: Table[string, ProtocolConfig]
     autofocus*: bool
+    metaRefresh*: MetaRefresh
 
 proc getFromOpaque[T](opaque: pointer; res: var T) =
   let opaque = cast[InterfaceOpaque](opaque)
@@ -704,6 +706,52 @@ proc gotoAnchor*(buffer: Buffer): GotoAnchorResult {.proxy.} =
         )
   return GotoAnchorResult(found: false)
 
+type CheckRefreshResult* = object
+  # n is timeout in millis. -1 => not found
+  n*: int
+  # url == nil => self
+  url*: URL
+
+proc checkRefresh*(buffer: Buffer): CheckRefreshResult {.proxy.} =
+  if buffer.document == nil:
+    return CheckRefreshResult(n: -1)
+  let element = buffer.document.findMetaRefresh()
+  if element == nil:
+    return CheckRefreshResult(n: -1)
+  let s = element.attr(satContent)
+  var i = s.skipBlanks(0)
+  let s0 = s.until(AllChars - AsciiDigit, i)
+  let x = parseUInt32(s0, allowSign = false)
+  if s0 != "":
+    if x.isNone and (i >= s.len or s[i] != '.'):
+      return CheckRefreshResult(n: -1)
+  var n = int(x.get(0) * 1000)
+  i = s.skipBlanks(i + s0.len)
+  if i < s.len and s[i] == '.':
+    inc i
+    let s1 = s.until(AllChars - AsciiDigit, i)
+    if s1 != "":
+      n += int(parseUInt32(s1, allowSign = false).get(0))
+      i = s.skipBlanks(i + s1.len)
+  if i >= s.len: # just reload this page
+    return CheckRefreshResult(n: n)
+  if s[i] notin {',', ';'}:
+    return CheckRefreshResult(n: -1)
+  i = s.skipBlanks(i + 1)
+  if s.startsWithIgnoreCase("url=", i):
+    i = s.skipBlanks(i + "url=".len)
+  var q = false
+  if i < s.len and s[i] in {'"', '\''}:
+    q = true
+    inc i
+  var s2 = s.substr(i)
+  if q and s2.len > 0 and s[^1] in {'"', '\''}:
+    s2.setLen(s2.high)
+  let url = buffer.document.parseURL(s2)
+  if url.isNone:
+    return CheckRefreshResult(n: -1)
+  return CheckRefreshResult(n: n, url: url.get)
+
 proc do_reshape(buffer: Buffer) =
   if buffer.document == nil:
     return # not parsed yet, nothing to render
diff --git a/src/types/cell.nim b/src/types/cell.nim
index 8820f351..bc2f6924 100644
--- a/src/types/cell.nim
+++ b/src/types/cell.nim
@@ -2,19 +2,19 @@ import types/color
 import utils/strwidth
 
 type
-  FormatFlags* = enum
-    ffBold
-    ffItalic
-    ffUnderline
-    ffReverse
-    ffStrike
-    ffOverline
-    ffBlink
+  FormatFlag* = enum
+    ffBold = "bold"
+    ffItalic = "italic"
+    ffUnderline = "underline"
+    ffReverse = "reverse"
+    ffStrike = "strike"
+    ffOverline = "overline"
+    ffBlink = "blink"
 
   Format* = object
     fgcolor*: CellColor
     bgcolor*: CellColor
-    flags*: set[FormatFlags]
+    flags*: set[FormatFlag]
 
   SimpleFormatCell* = object
     format*: Format
@@ -50,7 +50,7 @@ iterator items*(grid: FixedGrid): FixedCell {.inline.} =
   for cell in grid.cells:
     yield cell
 
-const FormatCodes*: array[FormatFlags, tuple[s, e: uint8]] = [
+const FormatCodes*: array[FormatFlag, tuple[s, e: uint8]] = [
   ffBold: (1u8, 22u8),
   ffItalic: (3u8, 23u8),
   ffUnderline: (4u8, 24u8),
diff --git a/src/utils/twtstr.nim b/src/utils/twtstr.nim
index 14e93bda..44c6ea3f 100644
--- a/src/utils/twtstr.nim
+++ b/src/utils/twtstr.nim
@@ -45,12 +45,17 @@ func snakeToKebabCase*(s: string): string =
       c = '-'
 
 func kebabToCamelCase*(s: string): string =
-  result = s
+  result = ""
   var flip = false
-  for c in result.mitems:
-    if flip:
-      c = c.toUpperAscii()
-    flip = c == '-'
+  for c in s:
+    if c == '-':
+      flip = true
+    else:
+      if flip:
+        result &= c.toUpperAscii()
+      else:
+        result &= c
+      flip = false
 
 func camelToKebabCase*(s: string): string =
   result = ""
@@ -61,17 +66,6 @@ func camelToKebabCase*(s: string): string =
     else:
       result &= c
 
-func startsWithNoCase*(s, prefix: string): bool =
-  if s.len < prefix.len:
-    return false
-  # prefix.len is always lower
-  var i = 0
-  while true:
-    if i == prefix.len: return true
-    if s[i].toLowerAscii() != prefix[i].toLowerAscii():
-      return false
-    inc i
-
 func hexValue*(c: char): int =
   if c in AsciiDigit:
     return int(c) - int('0')
@@ -114,9 +108,9 @@ func toHexLower*(u: uint16): string =
 func equalsIgnoreCase*(s1, s2: string): bool {.inline.} =
   return s1.cmpIgnoreCase(s2) == 0
 
-func startsWithIgnoreCase*(s1, s2: string): bool =
+func startsWithIgnoreCase*(s1, s2: string; si = 0): bool =
   if s1.len < s2.len: return false
-  for i in 0 ..< s2.len:
+  for i in si ..< s2.len:
     if s1[i].toLowerAscii() != s2[i].toLowerAscii():
       return false
   return true