about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--doc/cha-config.59
-rw-r--r--doc/cha-mailcap.5109
-rw-r--r--doc/config.md7
-rw-r--r--doc/mailcap.md134
-rw-r--r--res/config.toml6
-rw-r--r--src/config/config.nim32
-rw-r--r--src/config/mailcap.nim133
-rw-r--r--src/io/dynstream.nim7
-rw-r--r--src/local/pager.nim312
-rw-r--r--src/utils/twtstr.nim12
-rw-r--r--todo11
11 files changed, 482 insertions, 290 deletions
diff --git a/doc/cha-config.5 b/doc/cha-config.5
index 101c11e6..e22ee754 100644
--- a/doc/cha-config.5
+++ b/doc/cha-config.5
@@ -350,6 +350,15 @@ Search path for mime.types files.
 T}@T{
 T}
 T{
+auto\-mailcap
+T}@T{
+path
+T}@T{
+Mailcap file for entries that are automatically executed.
+The \[lq]Open as\[rq] prompt also saves entries in this file.
+T}@T{
+T}
+T{
 cgi\-dir
 T}@T{
 array of paths
diff --git a/doc/cha-mailcap.5 b/doc/cha-mailcap.5
index 9b364c51..60e97b44 100644
--- a/doc/cha-mailcap.5
+++ b/doc/cha-mailcap.5
@@ -2,14 +2,17 @@
 .\"
 .TH "cha\-mailcap" "5" "" "" "Mailcap support in Chawan"
 .SH Mailcap
-Chawan\[cq]s buffers can only handle HTML and plain text.
-To make Chawan recognize other file formats, the mailcap file format can
-be used.
+By default, Chawan\[cq]s buffers only handle HTML and plain text.
+The \f[CR]mailcap\f[R] file can be used to view other file formats using
+external commands, or to convert them to HTML/plain text before
+displaying them in Chawan.
 .PP
 Note that Chawan\[cq]s default mime.types file only recognizes a few
-file extensions, which may result in your entries not being executed.
-Please consult the \f[B]cha\-mime.types\f[R](5) documentation for
-details.
+file extensions, which may result in your entries not being executed if
+your system lacks an /etc/mime.types file.
+Please consult the
+.PP
+\f[B]cha\-mime.types\f[R](5) documentation for details.
 .PP
 For an exact description of the mailcap format, see \c
 .UR https://www.rfc-editor.org/rfc/rfc1524
@@ -17,62 +20,63 @@ RFC 1524
 .UE \c
 \&.
 .SS Search path
-The search path for mailcap files can be overridden using the
-configuration variable \f[CR]external.mailcap\f[R].
-.PP
-By default, the only file checked by Chawan is
-\f[CR]$HOME/.mailcap\f[R].
-.PP
-In the past, the full path from the specification was used.
-This was changed because mailcap files shipped with various systems are
-usually incompatible with the assumptions Chawan makes about mailcap
-file contents.
-You can restore the old/standard\-recommended behavior by adding this to
-your config.toml:
+The search path for mailcap files is set by the configuration variable
+\f[CR]external.mailcap\f[R].
+This matches the recommended path in the RFC:
 .IP
 .EX
-\f[B][external]\f[R]
-mailcap = [
-\[dq]\[ti]/.mailcap\[dq],
-\[dq]/etc/mailcap\[dq],
-\[dq]/usr/etc/mailcap\[dq],
-\[dq]/usr/local/etc/mailcap\[dq]
-]
+$HOME/.mailcap:/etc/mailcap:/usr/etc/mailcap:/usr/local/etc/mailcap
 .EE
+.PP
+By default, mailcap entries are only executed if the user types
+\f[CR]r\f[R] (run) after the prompt.
+Other options are to view the file with \f[CR]t\f[R] (text), or to save
+the file with \f[CR]s\f[R].
+.PP
+If a capital letter is typed (e.g.\ press shift and type \f[CR]R\f[R]),
+then a corresponding entry is appended to
+\f[CR]external.auto\-mailcap\f[R] (default:
+\f[CR]\[ti]/.chawan/auto.mailcap\f[R], or
+\f[CR]\[ti]/.config/chawan/config.toml\f[R] with XDG basedirs).
+\f[CR](T)ext\f[R] and \f[CR](S)ave\f[R] may also be used to append
+entries corresponding to the other display options.
+.PP
+Entries in auto\-mailcap are automatically executed, so it is
+recommended to add your Chawan\-specific entries there (or just set it
+to your personal mailcap file).
 .SS Format
-Chawan tries to adhere to the format described in RFC 1524, with a few
+Chawan adheres to the format described in RFC 1524, with a few
 extensions.
+.PP
+Note that text/html and text/plain entries are ignored.
 .SS Templating
 \f[CR]%s\f[R], \f[CR]%t\f[R], and named content type fields like
 \f[CR]%{charset}\f[R] work as described in the standard.
 .PP
-If no quoting is applied, Chawan will quote the templates automatically.
+If no quoting is applied, Chawan quotes the templates automatically.
 (This works with $(command substitutions) as well.)
 .PP
-DEPRECATED
-.PP
 The non\-standard template %u may be specified to get the original URL
 of the resource.
-(As far as I can tell, this is a Netscape extension that may or may not
-be compatible with other implementations.)
-.PP
-Use of this is not recommended; instead, use the \f[CR]$MAILCAP_URL\f[R]
-environment variable which is set to the same value before the execution
-of every mailcap command.
+This is a Netscape extension that may not be compatible with other
+implementations.
+As an alternative, the \f[CR]$MAILCAP_URL\f[R] environment variable is
+set to the same value.
 .SS Fields
 The \f[CR]test\f[R], \f[CR]nametemplate\f[R], \f[CR]needsterminal\f[R]
 and \f[CR]copiousoutput\f[R] fields are recognized.
-Additionally, the non\-standard \f[CR]x\-htmloutput\f[R] and
-\f[CR]x\-ansioutput\f[R] extension fields are recognized too.
+The non\-standard \f[CR]x\-htmloutput\f[R], \f[CR]x\-ansioutput\f[R],
+\f[CR]x\-saveoutput\f[R] and \f[CR]x\-needsstyle\f[R] extension fields
+are also recognized.
 .IP \[bu] 2
 When the \f[CR]test\f[R] named field is specified, the mailcap entry is
 only used if the test command returns 0.
 Warning: as of now, \f[CR]%s\f[R] does not work with \f[CR]test\f[R];
-\f[CR]test\f[R] named fields with a \f[CR]%s\f[R] template are skipped.
-Additionally, no data is piped into \f[CR]test\f[R] either.
+\f[CR]test\f[R] named fields with a \f[CR]%s\f[R] template are skipped,
+and no data is piped into \f[CR]test\f[R] commands.
 .IP \[bu] 2
 \f[CR]copiousoutput\f[R] makes Chawan redirect the output of the
-external command into a new buffer.
+external command\[cq]s output into a new buffer.
 If either x\-htmloutput or x\-ansioutput is defined too, then it is
 ignored.
 .IP \[bu] 2
@@ -85,6 +89,9 @@ output as HTML.
 formatting, etc.
 are displayed correctly.
 .IP \[bu] 2
+\f[CR]x\-saveoutput\f[R] prompts the user to save the entry\[cq]s output
+in a file.
+.IP \[bu] 2
 \f[CR]x\-needsstyle\f[R] forces CSS to be processed for the specific
 type, even if styling is disabled in the config.
 Only useful when combined with \f[CR]x\-htmloutput\f[R].
@@ -95,25 +102,9 @@ Note: as of now, \f[CR]needsterminal\f[R] does nothing if either
 \f[CR]copiousoutput\f[R] or \f[CR]x\-htmloutput\f[R] is specified.
 .IP \[bu] 2
 For a description of \f[CR]nametemplate\f[R], see the RFC.
-Note however, that it does not work with test (since %s is not supported
-there).
-.SS Environment variables
-As noted above, the \f[CR]$MAILCAP_URL\f[R] variable is set to the URL
-of the target resource before the execution of the mailcap command.
-Backwards compatibility with mailcap agents that do not support this
-variable can be achieved through shell substitution,
-e.g.\ \f[CR]${MAILCAP_URL:\-string for when it is unsupported}\f[R].
-.PP
-Note that it is not recommended to set \f[CR]%s\f[R] as the fallback,
-because it will force Chawan to download the entire file before
-displaying it even if it could have been piped into the command.
-.SS Note
-Entries with a content type of text/html or text/plain are ignored.
-.PP
-Content types that do not appear in mailcap files are handled as text
-files in case they start with \f[CR]text/\f[R].
-Otherwise, they prompt the user to save the file to the disk.
 .SS Examples
+I recommend placing entries in \f[CR]\[ti]/.chawan/auto.mailcap\f[R] (or
+\f[CR]\[ti]/.config/chawan/auto.mailcap\f[R] if you use XDG basedirs).
 .IP
 .EX
 # Note: these examples require an entry in mime.types that sets e.g. md as
@@ -123,7 +114,7 @@ Otherwise, they prompt the user to save the file to the disk.
 text/markdown; pandoc \- \-f markdown \-t html \-o \-; x\-htmloutput
 
 # Show syntax highlighting for JavaScript source files using bat.
-text/javascript; bat \-f \-l es6 \-\-file\-name ${MAILCAP_URL:\-STDIN} \-; x\-ansioutput
+text/javascript; bat \-f \-l es6 \-\-file\-name \[dq]${MAILCAP_URL:\-STDIN}\[dq] \-; x\-ansioutput
 
 # Play music using mpv, and hand over control of the terminal until mpv exits.
 audio/*; mpv \-; needsterminal
diff --git a/doc/config.md b/doc/config.md
index a8ecf919..1597d374 100644
--- a/doc/config.md
+++ b/doc/config.md
@@ -318,6 +318,13 @@ the line number.</td>
 </tr>
 
 <tr>
+<td>auto-mailcap</td>
+<td>path</td>
+<td>Mailcap file for entries that are automatically executed.<br>
+The "Open as" prompt also saves entries in this file.</td>
+</tr>
+
+<tr>
 <td>cgi-dir</td>
 <td>array of paths</td>
 <td>Search path for <!-- MANOFF -->[local CGI](localcgi.md) scripts.<!-- MANON -->
diff --git a/doc/mailcap.md b/doc/mailcap.md
index d24f9b06..ed35ef40 100644
--- a/doc/mailcap.md
+++ b/doc/mailcap.md
@@ -4,12 +4,15 @@ MANOFF -->
 
 # Mailcap
 
-Chawan's buffers can only handle HTML and plain text. To make Chawan recognize
-other file formats, the mailcap file format can be used.
+By default, Chawan's buffers only handle HTML and plain text. The
+`mailcap` file can be used to view other file formats using external
+commands, or to convert them to HTML/plain text before displaying them
+in Chawan.
 
 Note that Chawan's default mime.types file only recognizes a few file
-extensions, which may result in your entries not being executed.
-Please consult the <!-- MANOFF -->[mime.types](mime.types.md)<!-- MANON -->
+extensions, which may result in your entries not being executed if your
+system lacks an /etc/mime.types file. Please consult the
+<!-- MANOFF -->[mime.types](mime.types.md)<!-- MANON -->
 <!-- MANON **cha-mime.types**(5) MANOFF --> documentation for details.
 
 For an exact description of the mailcap format, see
@@ -17,96 +20,81 @@ For an exact description of the mailcap format, see
 
 ## Search path
 
-The search path for mailcap files can be overridden using the configuration
-variable `external.mailcap`.
+The search path for mailcap files is set by the configuration variable
+`external.mailcap`. This matches the recommended path in the RFC:
 
-By default, the only file checked by Chawan is `$HOME/.mailcap`.
+```
+$HOME/.mailcap:/etc/mailcap:/usr/etc/mailcap:/usr/local/etc/mailcap
+```
 
-In the past, the full path from the specification was used. This was changed
-because mailcap files shipped with various systems are usually incompatible
-with the assumptions Chawan makes about mailcap file contents. You can restore
-the old/standard-recommended behavior by adding this to your config.toml:
+By default, mailcap entries are only executed if the user types `r`
+(run) after the prompt. Other options are to view the file with `t`
+(text), or to save the file with `s`.
 
-```toml
-[external]
-mailcap = [
-	"~/.mailcap",
-	"/etc/mailcap",
-	"/usr/etc/mailcap",
-	"/usr/local/etc/mailcap"
-]
-```
+If a capital letter is typed (e.g. press shift and type `R`), then a
+corresponding entry is appended to `external.auto-mailcap` (default:
+`~/.chawan/auto.mailcap`, or `~/.config/chawan/config.toml` with XDG
+basedirs). `(T)ext` and `(S)ave` may also be used to append entries
+corresponding to the other display options.
+
+Entries in auto-mailcap are automatically executed, so it is recommended
+to add your Chawan-specific entries there (or just set it to your
+personal mailcap file).
 
 ## Format
 
-Chawan tries to adhere to the format described in RFC 1524, with a few
+Chawan adheres to the format described in RFC 1524, with a few
 extensions.
 
-### Templating
-
-`%s`, `%t`, and named content type fields like `%{charset}` work as described in
-the standard.
+Note that text/html and text/plain entries are ignored.
 
-If no quoting is applied, Chawan will quote the templates automatically. (This
-works with $(command substitutions) as well.)
+### Templating
 
-DEPRECATED
+`%s`, `%t`, and named content type fields like `%{charset}` work as
+described in the standard.
 
-The non-standard template %u may be specified to get the original URL of the
-resource. (As far as I can tell, this is a Netscape extension that may or may
-not be compatible with other implementations.)
+If no quoting is applied, Chawan quotes the templates automatically.
+(This works with $(command substitutions) as well.)
 
-Use of this is not recommended; instead, use the `$MAILCAP_URL` environment
-variable which is set to the same value before the execution of every mailcap
-command.
+The non-standard template %u may be specified to get the original URL of
+the resource. This is a Netscape extension that may not be compatible
+with other implementations. As an alternative, the `$MAILCAP_URL`
+environment variable is set to the same value.
 
 ### Fields
 
-The `test`, `nametemplate`, `needsterminal` and `copiousoutput` fields are
-recognized. Additionally, the non-standard `x-htmloutput` and `x-ansioutput`
-extension fields are recognized too.
+The `test`, `nametemplate`, `needsterminal` and `copiousoutput` fields
+are recognized. The non-standard `x-htmloutput`, `x-ansioutput`,
+`x-saveoutput` and `x-needsstyle` extension fields are also recognized.
 
 * When the `test` named field is specified, the mailcap entry is only used
   if the test command returns 0.  
-  Warning: as of now, `%s` does not work with `test`; `test` named fields with a
-  `%s` template are skipped. Additionally, no data is piped into `test` either.
-* `copiousoutput` makes Chawan redirect the output of the external command
-  into a new buffer. If either x-htmloutput or x-ansioutput is defined too, then
-  it is ignored.
-* The `x-htmloutput` extension field behaves the same as `copiousoutput`,
-  but makes Chawan interpret the command's output as HTML.
-* `x-ansioutput` pipes the output through the "text/x-ansi" content type
-  handler, so that ANSI colors, formatting, etc. are displayed correctly.
-* `x-needsstyle` forces CSS to be processed for the specific type, even if
-  styling is disabled in the config. Only useful when combined with
+  Warning: as of now, `%s` does not work with `test`; `test` named
+  fields with a `%s` template are skipped, and no data is piped into
+  `test` commands.
+* `copiousoutput` makes Chawan redirect the output of the external
+  command's output into a new buffer. If either x-htmloutput or
+  x-ansioutput is defined too, then it is ignored.
+* The `x-htmloutput` extension field behaves the same as
+  `copiousoutput`, but makes Chawan interpret the command's output as
+  HTML.
+* `x-ansioutput` pipes the output through the "text/x-ansi" content
+  type handler, so that ANSI colors, formatting, etc. are displayed
+  correctly.
+* `x-saveoutput` prompts the user to save the entry's output in a file.
+* `x-needsstyle` forces CSS to be processed for the specific type, even
+  if styling is disabled in the config. Only useful when combined with
   `x-htmloutput`.
-* `needsterminal` hands over control of the terminal to the command while
-  it is running. Note: as of now, `needsterminal` does nothing if either
-  `copiousoutput` or `x-htmloutput` is specified.
-* For a description of `nametemplate`, see the RFC. Note however, that it does
-  not work with test (since %s is not supported there).
-
-### Environment variables
-
-As noted above, the `$MAILCAP_URL` variable is set to the URL of the target
-resource before the execution of the mailcap command. Backwards compatibility
-with mailcap agents that do not support this variable can be achieved through
-shell substitution, e.g. `${MAILCAP_URL:-string for when it is unsupported}`.
-
-Note that it is not recommended to set `%s` as the fallback, because it
-will force Chawan to download the entire file before displaying it even if
-it could have been piped into the command.
-
-## Note
-
-Entries with a content type of text/html or text/plain are ignored.
-
-Content types that do not appear in mailcap files are handled as text files in
-case they start with `text/`. Otherwise, they prompt the user to save the file
-to the disk.
+* `needsterminal` hands over control of the terminal to the command
+  while it is running. Note: as of now, `needsterminal` does nothing if
+  either `copiousoutput` or `x-htmloutput` is specified.
+* For a description of `nametemplate`, see the RFC.
 
 ## Examples
 
+I recommend placing entries in `~/.chawan/auto.mailcap` (or
+`~/.config/chawan/auto.mailcap` if you use XDG basedirs).
+
 ```
 # Note: these examples require an entry in mime.types that sets e.g. md as
 # the markdown content type.
@@ -115,7 +103,7 @@ to the disk.
 text/markdown; pandoc - -f markdown -t html -o -; x-htmloutput
 
 # Show syntax highlighting for JavaScript source files using bat.
-text/javascript; bat -f -l es6 --file-name ${MAILCAP_URL:-STDIN} -; x-ansioutput
+text/javascript; bat -f -l es6 --file-name "${MAILCAP_URL:-STDIN}" -; x-ansioutput
 
 # Play music using mpv, and hand over control of the terminal until mpv exits.
 audio/*; mpv -; needsterminal
diff --git a/res/config.toml b/res/config.toml
index 0909c9cc..36661a71 100644
--- a/res/config.toml
+++ b/res/config.toml
@@ -258,8 +258,12 @@ display-charset = "auto"
 
 [external]
 mailcap = [
-	"~/.mailcap"
+	"~/.mailcap",
+	"/etc/mailcap",
+	"/usr/etc/mailcap",
+	"/usr/local/etc/mailcap"
 ]
+auto-mailcap = "$CHA_CONFIG_DIR/auto.mailcap"
 mime-types = [
 	"~/.mime.types",
 	"/etc/mime.types",
diff --git a/src/config/config.nim b/src/config/config.nim
index 8177dab5..97bf461c 100644
--- a/src/config/config.nim
+++ b/src/config/config.nim
@@ -105,6 +105,7 @@ type
     sockdir* {.jsgetset.}: ChaPathResolved
     editor* {.jsgetset.}: string
     mailcap*: Mailcap
+    auto_mailcap*: AutoMailcap
     mime_types*: MimeTypes
     cgi_dir* {.jsgetset.}: seq[ChaPathResolved]
     urimethodmap*: URIMethodMap
@@ -366,6 +367,8 @@ proc parseConfigValue(ctx: var ConfigParser; x: var MimeTypes; v: TomlValue;
   k: string)
 proc parseConfigValue(ctx: var ConfigParser; x: var Mailcap; v: TomlValue;
   k: string)
+proc parseConfigValue(ctx: var ConfigParser; x: var AutoMailcap; v: TomlValue;
+  k: string)
 proc parseConfigValue(ctx: var ConfigParser; x: var URIMethodMap; v: TomlValue;
   k: string)
 proc parseConfigValue(ctx: var ConfigParser; x: var CommandConfig; v: TomlValue;
@@ -620,8 +623,6 @@ proc parseConfigValue(ctx: var ConfigParser; x: var MimeTypes; v: TomlValue;
       deallocMem(src)
       ps.sclose()
 
-const DefaultMailcap = parseMailcap(staticRead"res/mailcap").get
-
 proc parseConfigValue(ctx: var ConfigParser; x: var Mailcap; v: TomlValue;
     k: string) =
   var paths: seq[ChaPathResolved]
@@ -631,14 +632,31 @@ proc parseConfigValue(ctx: var ConfigParser; x: var Mailcap; v: TomlValue;
     let ps = openFileExpand(ctx.dir, p)
     if ps != nil:
       let src = ps.recvAllOrMmap()
-      let res = parseMailcap(src.toOpenArray())
+      let res = x.parseMailcap(src.toOpenArray())
       deallocMem(src)
       ps.sclose()
-      if res.isSome:
-        x.add(res.get)
-      else:
+      if res.isNone:
         ctx.warnings.add("Error reading mailcap: " & res.error)
-  x.add(DefaultMailcap)
+
+const DefaultMailcap = block:
+  var mailcap: Mailcap
+  doAssert mailcap.parseMailcap(staticRead"res/mailcap").isSome
+  mailcap
+
+proc parseConfigValue(ctx: var ConfigParser; x: var AutoMailcap;
+    v: TomlValue; k: string) =
+  var path: ChaPathResolved
+  ctx.parseConfigValue(path, v, k)
+  x = AutoMailcap(path: path)
+  let ps = openFileExpand(ctx.dir, path)
+  if ps != nil:
+    let src = ps.recvAllOrMmap()
+    let res = x.entries.parseMailcap(src.toOpenArray())
+    deallocMem(src)
+    ps.sclose()
+    if res.isNone:
+      ctx.warnings.add("Error reading auto-mailcap: " & res.error)
+  x.entries.add(DefaultMailcap)
 
 const DefaultURIMethodMap = parseURIMethodMap(staticRead"res/urimethodmap")
 
diff --git a/src/config/mailcap.nim b/src/config/mailcap.nim
index 38e18d7b..9e07c61d 100644
--- a/src/config/mailcap.nim
+++ b/src/config/mailcap.nim
@@ -1,10 +1,13 @@
 # See https://www.rfc-editor.org/rfc/rfc1524
 
+import std/os
 import std/osproc
+import std/posix
 import std/strutils
 
-import types/url
+import io/dynstream
 import types/opt
+import types/url
 import utils/twtstr
 
 type
@@ -19,11 +22,11 @@ type
     mfCopiousoutput = "copiousoutput"
     mfHtmloutput = "x-htmloutput" # from w3m
     mfAnsioutput = "x-ansioutput" # Chawan extension
+    mfSaveoutput = "x-saveoutput" # Chawan extension
     mfNeedsstyle = "x-needsstyle" # Chawan extension
 
   MailcapEntry* = object
-    mt*: string
-    subt*: string
+    t*: string
     cmd*: string
     flags*: set[MailcapFlag]
     nametemplate*: string
@@ -32,6 +35,36 @@ type
 
   Mailcap* = seq[MailcapEntry]
 
+  AutoMailcap* = object
+    path*: string
+    entries*: Mailcap
+
+proc serializeCommand(s: var string; cmd: string) =
+  for c in cmd:
+    if c == ';':
+      s &= '\\'
+    s &= c
+
+proc `$`*(entry: MailcapEntry): string =
+  var s = ""
+  s.serializeCommand(entry.t)
+  s &= ';'
+  s.serializeCommand(entry.cmd)
+  for flag in MailcapFlag:
+    if flag in entry.flags:
+      s &= ';' & $flag
+  if entry.nametemplate != "":
+    s &= ";nametemplate="
+    s.serializeCommand(entry.nametemplate)
+  if entry.edit != "":
+    s &= ";edit="
+    s.serializeCommand(entry.edit)
+  if entry.test != "":
+    s &= ";test="
+    s.serializeCommand(entry.test)
+  s &= '\n'
+  return s
+
 proc has(state: MailcapParser; buf: openArray[char]): bool {.inline.} =
   return state.at < buf.len
 
@@ -75,41 +108,34 @@ proc skipLine(state: var MailcapParser; buf: openArray[char]) =
     if c == '\n':
       break
 
-proc consumeTypeField(state: var MailcapParser; buf: openArray[char]):
-    Result[string, string] =
-  var s = ""
-  # type
-  while state.has(buf):
-    let c = state.consume(buf)
-    if c == '/':
-      s &= c
-      break
-    if c notin AsciiAlphaNumeric + {'-', '*'}:
-      return err("line " & $state.line & ": invalid character in type field: " &
-        c)
-    s &= c.toLowerAscii()
-  if not state.has(buf):
-    return err("Missing subtype")
-  # subtype
+proc consumeTypeField(state: var MailcapParser; buf: openArray[char];
+    outs: var string): Err[string] =
+  var nslash = 0
   while state.has(buf):
     let c = state.consume(buf)
     if c in AsciiWhitespace + {';'}:
       state.reconsume(c)
       break
-    if c notin AsciiAlphaNumeric + {'-', '.', '*', '_', '+'}:
+    if c == '/':
+      inc nslash
+    elif c notin AsciiAlphaNumeric + {'-', '.', '*', '_', '+'}:
       return err("line " & $state.line &
-        ": invalid character in subtype field: " & c)
-    s &= c.toLowerAscii()
+        ": invalid character in type field: " & c)
+    outs &= c.toLowerAscii()
+  if nslash == 0:
+    # Accept types without a subtype - RFC calls this "implicit-wild".
+    outs &= "/*"
+  if nslash > 1:
+    return err("line " & $state.line & ": too many slash characters")
   var c: char
   if not state.skipBlanks(buf, c) or c != ';':
     return err("Semicolon not found")
-  return ok(s)
+  return ok()
 
-proc consumeCommand(state: var MailcapParser; buf: openArray[char]):
-    Result[string, string] =
+proc consumeCommand(state: var MailcapParser; buf: openArray[char];
+    outs: var string): Err[string] =
   state.skipBlanks(buf)
   var quoted = false
-  var s = ""
   while state.has(buf):
     let c = state.consume(buf)
     if not quoted:
@@ -117,7 +143,7 @@ proc consumeCommand(state: var MailcapParser; buf: openArray[char]):
         continue
       if c == ';' or c == '\n':
         state.reconsume(c)
-        return ok(s)
+        return ok()
       if c == '\\':
         quoted = true
         continue
@@ -126,8 +152,8 @@ proc consumeCommand(state: var MailcapParser; buf: openArray[char]):
           c)
     else:
       quoted = false
-    s &= c
-  return ok(s)
+    outs &= c
+  return ok()
 
 type NamedField = enum
   nmTest = "test"
@@ -147,7 +173,8 @@ proc consumeField(state: var MailcapParser; buf: openArray[char];
     of '\r':
       continue
     of '=':
-      let cmd = ?state.consumeCommand(s)
+      var cmd = ""
+      ?state.consumeCommand(buf, cmd)
       while s.len > 0 and s[^1] in AsciiWhitespace:
         s.setLen(s.len - 1)
       if (let x = parseEnumNoCase[NamedField](s); x.isSome):
@@ -155,7 +182,7 @@ proc consumeField(state: var MailcapParser; buf: openArray[char];
         of nmTest: entry.test = cmd
         of nmNametemplate: entry.nametemplate = cmd
         of nmEdit: entry.edit = cmd
-      return ok(state.consume(s) == ';')
+      return ok(state.has(buf) and state.consume(buf) == ';')
     elif c in Controls:
       return err("line " & $state.line & ": invalid character in field: " & c)
     else:
@@ -166,9 +193,9 @@ proc consumeField(state: var MailcapParser; buf: openArray[char];
     entry.flags.incl(x.get)
   return ok(res)
 
-proc parseMailcap*(buf: openArray[char]): Result[Mailcap, string] =
+proc parseMailcap*(omailcap: var Mailcap; buf: openArray[char]): Err[string] =
   var state = MailcapParser(line: 1)
-  var mailcap: Mailcap
+  var mailcap = default(Mailcap)
   while state.has(buf):
     let c = state.consume(buf)
     if c == '#':
@@ -180,19 +207,15 @@ proc parseMailcap*(buf: openArray[char]): Result[Mailcap, string] =
     if c2 == '\n' or c2 == '\r':
       continue
     state.reconsume(c2)
-    let t = ?state.consumeTypeField(buf)
-    let mt = t.until('/') #TODO this could be more efficient
-    let subt = t[mt.len + 1 .. ^1]
-    var entry = MailcapEntry(
-      mt: mt,
-      subt: subt,
-      cmd: ?state.consumeCommand(buf)
-    )
-    if state.consume(buf) == ';':
+    var entry = MailcapEntry()
+    ?state.consumeTypeField(buf, entry.t)
+    ?state.consumeCommand(buf, entry.cmd)
+    if state.has(buf) and state.consume(buf) == ';':
       while ?state.consumeField(buf, entry):
         discard
     mailcap.add(entry)
-  return ok(mailcap)
+  omailcap.add(mailcap)
+  return ok()
 
 # Mostly based on w3m's mailcap quote/unquote
 type UnquoteState = enum
@@ -315,12 +338,12 @@ proc unquoteCommand*(ecmd, contentType, outpath: string; url: URL): string =
 
 proc findMailcapEntry*(mailcap: var Mailcap; contentType, outpath: string;
     url: URL): int =
-  let mt = contentType.until('/')
-  let st = contentType.until(AsciiWhitespace + {';'}, mt.len + 1)
+  let mt = contentType.until('/') & '/'
+  let st = contentType.until(AsciiWhitespace + {';'}, mt.len - 1)
   for i, entry in mailcap.mypairs:
-    if entry.mt != "*" and not entry.mt.equalsIgnoreCase(mt):
+    if not entry.t.startsWith("*/") and not entry.t.startsWithIgnoreCase(mt):
       continue
-    if entry.subt != "*" and not entry.subt.equalsIgnoreCase(st):
+    if not entry.t.endsWith("/*") and not entry.t.endsWithIgnoreCase(st):
       continue
     if entry.test != "":
       var canpipe = true
@@ -331,3 +354,19 @@ proc findMailcapEntry*(mailcap: var Mailcap; contentType, outpath: string;
         continue
     return i
   return -1
+
+proc saveEntry*(mailcap: var AutoMailcap; entry: MailcapEntry): bool =
+  let s = $entry
+  try:
+    let pdir = mailcap.path.parentDir()
+    if not dirExists(pdir):
+      createDir(pdir)
+    let ps = newPosixStream(mailcap.path, O_WRONLY or O_APPEND or O_CREAT, 0o644)
+    if ps == nil:
+      return false
+    ps.sendDataLoop(s)
+    ps.sclose()
+  except IOError, OSError:
+    return false
+  mailcap.entries.add(entry)
+  return true
diff --git a/src/io/dynstream.nim b/src/io/dynstream.nim
index 8b917b98..8117d22b 100644
--- a/src/io/dynstream.nim
+++ b/src/io/dynstream.nim
@@ -320,15 +320,12 @@ proc maybeMmapForSend*(ps: PosixStream; len: int): MaybeMappedMemory =
   return res
 
 template toOpenArray*(mem: MaybeMappedMemory): openArray[char] =
-  if mem.len > 0:
-    cast[ptr UncheckedArray[char]](mem.p).toOpenArray(0, mem.len - 1)
-  else:
-    []
+  cast[ptr UncheckedArray[char]](mem.p).toOpenArray(0, mem.len - 1)
 
 proc sendDataLoop*(ps: PosixStream; mem: MaybeMappedMemory) =
   # only send if not mmapped; otherwise everything is already where it should be
   if mem.t != mmmtMmap:
-    ps.sendDataLoop(mem.p, mem.len)
+    ps.sendDataLoop(mem.toOpenArray())
 
 template dealloc*(mem: MaybeMappedMemory) {.error: "use deallocMem".} = discard
 
diff --git a/src/local/pager.nim b/src/local/pager.nim
index 9fd0f4d8..c6059fdb 100644
--- a/src/local/pager.nim
+++ b/src/local/pager.nim
@@ -72,6 +72,7 @@ type
     lmDownload = "(Download)Save file to: "
     lmBufferFile = "(Upload)Filename: "
     lmAlert = "Alert: "
+    lmMailcap = "Mailcap: "
 
   ProcMapItem = object
     container*: Container
@@ -102,6 +103,14 @@ type
   LineDataAuth = ref object of LineData
     url: URL
 
+  LineDataMailcap = ref object of LineData
+    container: Container
+    ostream: PosixStream
+    contentType: string
+    i: int
+    response: Response
+    sx: int
+
   NavDirection = enum
     ndPrev = "prev"
     ndNext = "next"
@@ -173,6 +182,17 @@ type
   ContainerData* = ref object of MapData
     container*: Container
 
+  CheckMailcapFlag = enum
+    cmfConnect, cmfHTML, cmfFound, cmfRedirected, cmfPrompt, cmfNeedsstyle,
+    cmfSaveoutput
+
+  MailcapResult = object
+    entry: MailcapEntry
+    flags: set[CheckMailcapFlag]
+    ostream: PosixStream
+    ostreamOutputId: int
+    cmd: string
+
 jsDestructor(Pager)
 
 # Forward declarations
@@ -181,6 +201,10 @@ proc addConsole(pager: Pager; interactive: bool): ConsoleWrapper
 proc addLoaderClient(pager: Pager; pid: int; config: LoaderClientConfig;
   clonedFrom = -1): ClientKey
 proc alert*(pager: Pager; msg: string)
+proc askMailcap(pager: Pager; container: Container; ostream: PosixStream;
+  contentType: string; i: int; response: Response; sx: int)
+proc connected2(pager: Pager; container: Container; res: MailcapResult;
+  response: Response)
 proc draw(pager: Pager)
 proc dumpBuffers(pager: Pager)
 proc evalJS(pager: Pager; src, filename: string; module = false): JSValue
@@ -197,6 +221,9 @@ proc openMenu(pager: Pager; x = -1; y = -1)
 proc readPipe(pager: Pager; contentType: string; cs: Charset; ps: PosixStream;
   title: string)
 proc refreshStatusMsg(pager: Pager)
+proc runMailcap(pager: Pager; url: URL; stream: PosixStream;
+  istreamOutputId: int; contentType: string; entry: MailcapEntry):
+  MailcapResult
 proc showAlerts(pager: Pager)
 proc updateReadLine(pager: Pager)
 
@@ -1229,7 +1256,7 @@ proc draw(pager: Pager) =
   pager.term.flush()
 
 proc writeAskPrompt(pager: Pager; s = "") =
-  let maxwidth = pager.status.grid.width - s.len
+  let maxwidth = pager.status.grid.width - s.width()
   let i = pager.writeStatusMessage(pager.askprompt, maxwidth = maxwidth)
   pager.askcursor = pager.writeStatusMessage(s, start = i)
 
@@ -1246,14 +1273,16 @@ proc askChar(pager: Pager; prompt: string): Promise[string] {.jsfunc.} =
   return pager.askcharpromise
 
 proc fulfillAsk(pager: Pager; y: bool) =
-  pager.askpromise.resolve(y)
+  let p = pager.askpromise
   pager.askpromise = nil
   pager.askprompt = ""
+  p.resolve(y)
 
 proc fulfillCharAsk(pager: Pager; s: string) =
-  pager.askcharpromise.resolve(s)
+  let p = pager.askcharpromise
   pager.askcharpromise = nil
   pager.askprompt = ""
+  p.resolve(s)
 
 proc addContainer*(pager: Pager; container: Container) =
   container.parent = pager.container
@@ -2177,6 +2206,19 @@ proc updateReadLine(pager: Pager) =
           )
         else:
           pager.saveTo(data, lineedit.news)
+      of lmMailcap:
+        var mailcap = default(Mailcap)
+        let res = mailcap.parseMailcap(lineedit.news)
+        let data = LineDataMailcap(pager.lineData)
+        if res.isSome and mailcap.len == 1:
+          let res = pager.runMailcap(data.container.url, data.ostream,
+            data.response.outputId, data.contentType, mailcap[0])
+          pager.connected2(data.container, res, data.response)
+        else:
+          if res.isNone:
+            pager.alert(res.error)
+          pager.askMailcap(data.container, data.ostream, data.contentType,
+            data.i, data.response, data.sx)
       of lmISearchF, lmISearchB, lmAlert: discard
     of lesCancel:
       case pager.linemode
@@ -2186,6 +2228,10 @@ proc updateReadLine(pager: Pager) =
       of lmDownload:
         let data = LineDataDownload(pager.lineData)
         data.stream.sclose()
+      of lmMailcap:
+        let data = LineDataMailcap(pager.lineData)
+        pager.askMailcap(data.container, data.ostream, data.contentType,
+          data.i, data.response, data.sx)
       else: discard
       pager.lineData = nil
   if lineedit.state in {lesCancel, lesFinish} and pager.lineedit == lineedit:
@@ -2265,15 +2311,6 @@ proc externFilterSource(pager: Pager; cmd: string; c: Container = nil;
   pager.addContainer(container)
   container.filter = BufferFilter(cmd: cmd)
 
-type CheckMailcapResult = object
-  ostream: PosixStream
-  ostreamOutputId: int
-  connect: bool
-  ishtml: bool
-  found: bool
-  redirected: bool # whether or not ostream is the same as istream
-  needsstyle: bool
-
 proc execPipe(pager: Pager; cmd: string; ps, os, closeme: PosixStream): int =
   case (let pid = myFork(); pid)
   of -1:
@@ -2430,9 +2467,9 @@ proc filterBuffer(pager: Pager; ps: PosixStream; cmd: string): PosixStream =
 # If needsterminal is specified, and stdout is not being read, then the
 # pager is suspended until the command exits.
 #TODO add support for edit/compose, better error handling
-proc checkMailcap0(pager: Pager; url: URL; stream: PosixStream;
+proc runMailcap(pager: Pager; url: URL; stream: PosixStream;
     istreamOutputId: int; contentType: string; entry: MailcapEntry):
-    CheckMailcapResult =
+    MailcapResult =
   let ext = url.pathname.afterLast('.')
   var outpath = pager.getTempFile(ext)
   if entry.nametemplate != "":
@@ -2443,7 +2480,8 @@ proc checkMailcap0(pager: Pager; url: URL; stream: PosixStream;
   let needsterminal = mfNeedsterminal in entry.flags
   putEnv("MAILCAP_URL", $url)
   block needsConnect:
-    if entry.flags * {mfCopiousoutput, mfHtmloutput, mfAnsioutput} == {}:
+    if entry.flags * {mfCopiousoutput, mfHtmloutput, mfAnsioutput,
+        mfSaveoutput} == {}:
       # No output. Resume here, so that blocking needsterminal filters work.
       pager.loader.resume(istreamOutputId)
       if canpipe:
@@ -2472,46 +2510,21 @@ proc checkMailcap0(pager: Pager; url: URL; stream: PosixStream;
     pager.loader.passFd(url.pathname, pins.fd)
     pins.safeClose()
     let response = pager.loader.doRequest(newRequest(url))
-    return CheckMailcapResult(
-      connect: true,
-      ostream: response.body,
-      ostreamOutputId: response.outputId,
-      ishtml: ishtml,
+    var flags = {cmfConnect, cmfFound, cmfRedirected}
+    if mfNeedsstyle in entry.flags or mfAnsioutput in entry.flags:
       # ansi always needs styles
-      needsstyle: mfNeedsstyle in entry.flags or mfAnsioutput in entry.flags,
-      found: true,
-      redirected: true
+      flags.incl(cmfNeedsstyle)
+    if mfSaveoutput in entry.flags:
+      flags.incl(cmfSaveoutput)
+    if ishtml:
+      flags.incl(cmfHTML)
+    return MailcapResult(
+      flags: flags,
+      ostream: response.body,
+      ostreamOutputId: response.outputId
     )
   delEnv("MAILCAP_URL")
-  return CheckMailcapResult(connect: false, found: true)
-
-proc checkMailcap(pager: Pager; container: Container; stream: PosixStream;
-    istreamOutputId: int; contentType: string): CheckMailcapResult =
-  # contentType must exist, because we set it in applyResponse
-  let shortContentType = container.contentType.get
-  var stream = stream
-  if container.filter != nil:
-    stream = pager.filterBuffer(stream, container.filter.cmd)
-  if shortContentType.equalsIgnoreCase("text/html"):
-    # We support text/html natively, so it would make little sense to execute
-    # mailcap filters for it.
-    return CheckMailcapResult(
-      connect: true,
-      ostream: stream,
-      ishtml: true,
-      found: true
-    )
-  if shortContentType.equalsIgnoreCase("text/plain"):
-    # text/plain could potentially be useful. Unfortunately, many mailcaps
-    # include a text/plain entry with less by default, so it's probably better
-    # to ignore this.
-    return CheckMailcapResult(connect: true, ostream: stream, found: true)
-  let url = container.url
-  let i = pager.config.external.mailcap.findMailcapEntry(contentType, "", url)
-  if i == -1:
-    return CheckMailcapResult(connect: true, ostream: stream, found: false)
-  return pager.checkMailcap0(url, stream, istreamOutputId, contentType,
-    pager.config.external.mailcap[i])
+  return MailcapResult(flags: {cmfFound})
 
 proc redirectTo(pager: Pager; container: Container; request: Request) =
   let replaceBackup = if container.replaceBackup != nil:
@@ -2559,7 +2572,8 @@ proc redirect(pager: Pager; container: Container; response: Response;
     pager.alert("Error: maximum redirection depth reached")
     pager.deleteContainer(container, failTarget)
 
-proc askDownloadPath(pager: Pager; container: Container; response: Response) =
+proc askDownloadPath(pager: Pager; container: Container; stream: PosixStream;
+    response: Response) =
   var buf = string(pager.config.external.download_dir)
   let pathname = container.url.pathname
   if buf.len == 0 or buf[^1] != '/':
@@ -2569,50 +2583,22 @@ proc askDownloadPath(pager: Pager; container: Container; response: Response) =
   else:
     buf &= container.url.pathname.afterLast('/').percentDecode()
   pager.setLineEdit(lmDownload, buf)
-  pager.lineData = LineDataDownload(
-    outputId: response.outputId,
-    stream: response.body
-  )
+  pager.lineData = LineDataDownload(outputId: response.outputId, stream: stream)
   pager.deleteContainer(container, container.find(ndAny))
   pager.refreshStatusMsg()
   dec pager.numload
 
-proc connected(pager: Pager; container: Container; response: Response) =
-  let istream = response.body
-  container.applyResponse(response, pager.config.external.mime_types)
-  if response.status == 401: # unauthorized
-    pager.setLineEdit(lmUsername, container.url.username)
-    pager.lineData = LineDataAuth(url: newURL(container.url))
-    istream.sclose()
-    return
-  # This forces client to ask for confirmation before quitting.
-  # (It checks a flag on container, because console buffers must not affect this
-  # variable.)
-  if cfUserRequested in container.flags:
-    pager.hasload = true
-  if cfSave in container.flags:
-    # download queried by user
-    pager.askDownloadPath(container, response)
-    return
-  let realContentType = if "Content-Type" in response.headers:
-    response.headers["Content-Type"]
-  else:
-    # both contentType and charset must be set by applyResponse.
-    container.contentType.get & ";charset=" & $container.charset
-  let mailcapRes = pager.checkMailcap(container, istream, response.outputId,
-    realContentType)
-  let shortContentType = container.contentType.get
-  if not mailcapRes.found and
-      not shortContentType.startsWithIgnoreCase("text/") and
-      not shortContentType.isJavaScriptType():
-    pager.askDownloadPath(container, response)
-    return
-  if mailcapRes.connect:
-    if mailcapRes.ishtml:
+proc connected2(pager: Pager; container: Container; res: MailcapResult;
+    response: Response) =
+  if cfSave in container.flags or cmfSaveoutput in res.flags:
+    container.flags.incl(cfSave) # saveoutput doesn't include it before
+    pager.askDownloadPath(container, res.ostream, response)
+  elif cmfConnect in res.flags:
+    if cmfHTML in res.flags:
       container.flags.incl(cfIsHTML)
     else:
       container.flags.excl(cfIsHTML)
-    if mailcapRes.needsstyle: # override
+    if cmfNeedsstyle in res.flags: # override
       container.config.styling = true
     # buffer now actually exists; create a process for it
     var attrs = pager.attrs
@@ -2623,14 +2609,14 @@ proc connected(pager: Pager; container: Container; response: Response) =
       container.config,
       container.url,
       attrs,
-      mailcapRes.ishtml,
+      cmfHTML in res.flags,
       container.charsetStack
     )
     pager.procmap.add(ProcMapItem(
       container: container,
-      ostream: mailcapRes.ostream,
-      redirected: mailcapRes.redirected,
-      ostreamOutputId: mailcapRes.ostreamOutputId,
+      ostream: res.ostream,
+      redirected: cmfRedirected in res.flags,
+      ostreamOutputId: res.ostreamOutputId,
       istreamOutputId: response.outputId
     ))
     if container.replace != nil:
@@ -2641,6 +2627,144 @@ proc connected(pager: Pager; container: Container; response: Response) =
     pager.deleteContainer(container, container.find(ndAny))
     pager.refreshStatusMsg()
 
+proc saveEntry(pager: Pager; entry: MailcapEntry) =
+  if not pager.config.external.auto_mailcap.saveEntry(entry):
+    pager.alert("Could not write to " & pager.config.external.auto_mailcap.path)
+
+proc askMailcapMsg(pager: Pager; shortContentType: string; i: int; sx: var int):
+    string =
+  var msg = "Open " & shortContentType & " as (shift=always): (t)ext, (s)ave"
+  if i != -1:
+    msg &= ", (r)un \"" & pager.config.external.mailcap[i].cmd.strip() & '"'
+  msg &= ", (e)dit entry, (C-c)ancel"
+  msg = msg.toValidUTF8()
+  var mw = msg.width()
+  var j = 0
+  var x = 0
+  while j < msg.len:
+    let pj = j
+    let px = x
+    x += msg.nextUTF8(j).width()
+    if mw - px <= pager.attrs.width or x > sx:
+      j = pj
+      sx = px
+      break
+  msg = msg.substr(j)
+  return msg
+
+proc askMailcap(pager: Pager; container: Container; ostream: PosixStream;
+    contentType: string; i: int; response: Response; sx: int) =
+  var sx = sx
+  let msg = pager.askMailcapMsg(container.contentType.get, i, sx)
+  pager.askChar(msg).then(proc(s: string) =
+    if s.len != 1:
+      pager.askMailcap(container, ostream, contentType, i, response, sx)
+      return
+    let c = s[0]
+    if c in {'\3', 'q'}:
+      pager.alert("Canceled")
+      ostream.sclose()
+      pager.connected2(container, MailcapResult(), response)
+    elif c == 'e':
+      #TODO no idea how to implement save :/
+      # probably it should run use a custom reader that runs through
+      # auto.mailcap clearing any other entry. but maybe it's better to
+      # add a full blown editor like w3m has at that point...
+      var s = container.contentType.get & ';'
+      if i != -1:
+        s = $pager.config.external.mailcap[i]
+      pager.setLineEdit(lmMailcap, s)
+      pager.lineData = LineDataMailcap(
+        container: container,
+        ostream: ostream,
+        contentType: contentType,
+        i: i,
+        response: response,
+        sx: sx
+      )
+    elif c in {'t', 'T'}:
+      pager.connected2(container, MailcapResult(
+        flags: {cmfConnect},
+        ostream: ostream
+      ), response)
+      if c == 'T':
+        pager.saveEntry(MailcapEntry(
+          t: container.contentType.get,
+          cmd: "cat",
+          flags: {mfCopiousoutput}
+        ))
+    elif c in {'s', 'S'}:
+      container.flags.incl(cfSave)
+      pager.connected2(container, MailcapResult(
+        flags: {cmfConnect},
+        ostream: ostream
+      ), response)
+      if c == 'S':
+        pager.saveEntry(MailcapEntry(
+          t: container.contentType.get,
+          cmd: "cat",
+          flags: {mfSaveoutput}
+        ))
+    elif i != -1 and c in {'r', 'R'}:
+      let res = pager.runMailcap(container.url, ostream, response.outputId,
+        contentType, pager.config.external.mailcap[i])
+      pager.connected2(container, res, response)
+      if c == 'R':
+        pager.saveEntry(pager.config.external.mailcap[i])
+    else:
+      var sx = sx
+      if c == 'h':
+        dec sx
+      if c == 'l':
+        inc sx
+      pager.askMailcap(container, ostream, contentType, i, response, max(sx, 0))
+  )
+
+proc connected(pager: Pager; container: Container; response: Response) =
+  var istream = PosixStream(response.body)
+  container.applyResponse(response, pager.config.external.mime_types)
+  if response.status == 401: # unauthorized
+    pager.setLineEdit(lmUsername, container.url.username)
+    pager.lineData = LineDataAuth(url: newURL(container.url))
+    istream.sclose()
+    return
+  # This forces client to ask for confirmation before quitting.
+  # (It checks a flag on container, because console buffers must not affect this
+  # variable.)
+  if cfUserRequested in container.flags:
+    pager.hasload = true
+  var contentType = if "Content-Type" in response.headers:
+    response.headers["Content-Type"]
+  else:
+    # both contentType and charset must be set by applyResponse.
+    container.contentType.get & ";charset=" & $container.charset
+  contentType = contentType.toValidUTF8()
+  # contentType must exist, because we set it in applyResponse
+  let shortContentType = container.contentType.get
+  if container.filter != nil:
+    istream = pager.filterBuffer(istream, container.filter.cmd)
+  if shortContentType.equalsIgnoreCase("text/html"):
+    pager.connected2(container, MailcapResult(
+      flags: {cmfConnect, cmfHTML, cmfFound},
+      ostream: istream
+    ), response)
+  elif shortContentType.equalsIgnoreCase("text/plain"):
+    pager.connected2(container, MailcapResult(
+      flags: {cmfConnect, cmfFound},
+      ostream: istream
+    ), response)
+  else:
+    let i = pager.config.external.auto_mailcap.entries
+      .findMailcapEntry(contentType, "", container.url)
+    if i != -1:
+      let res = pager.runMailcap(container.url, istream, response.outputId,
+        contentType, pager.config.external.auto_mailcap.entries[i])
+      pager.connected2(container, res, response)
+    else:
+      let i = pager.config.external.mailcap.findMailcapEntry(contentType, "",
+        container.url)
+      pager.askMailcap(container, istream, contentType, i, response, 0)
+
 proc unregisterFd(pager: Pager; fd: int) =
   pager.pollData.unregister(fd)
   pager.loader.unregistered.add(fd)
diff --git a/src/utils/twtstr.nim b/src/utils/twtstr.nim
index 2734e5ce..5fe93507 100644
--- a/src/utils/twtstr.nim
+++ b/src/utils/twtstr.nim
@@ -217,16 +217,20 @@ func equalsIgnoreCase*(s1, s2: string): bool {.inline.} =
   return s1.cmpIgnoreCase(s2) == 0
 
 func startsWithIgnoreCase*(s1, s2: openArray[char]): bool =
-  if s1.len < s2.len: return false
+  if s1.len < s2.len:
+    return false
   for i in 0 ..< s2.len:
     if s1[i].toLowerAscii() != s2[i].toLowerAscii():
       return false
   return true
 
 func endsWithIgnoreCase*(s1, s2: openArray[char]): bool =
-  if s1.len < s2.len: return false
-  for i in countdown(s2.high, 0):
-    if s1[i].toLowerAscii() != s2[i].toLowerAscii():
+  if s1.len < s2.len:
+    return false
+  let h1 = s1.high
+  let h2 = s2.high
+  for i in 0 ..< s2.len:
+    if s1[h1 - i].toLowerAscii() != s2[h2 - i].toLowerAscii():
       return false
   return true
 
diff --git a/todo b/todo
index 3585d5cf..bfe5b10a 100644
--- a/todo
+++ b/todo
@@ -21,6 +21,17 @@ config:
   proto.gemini.known-hosts = '/some/path'; maybe also with inline JS?)
 - add RPC for CGI scripts e.g. toggle settings/issue downloads/etc
 	* also some way to set permissions for RPC calls
+mailcap:
+- save custom command?
+- w3mmee extensions?
+	* x-cgioutput would allow for easy implementation of an
+	  in-browser image viewer
+		- open question: how to decide if I want cgioutput or
+		  feh?
+			* maybe an internal mime type attribute?
+	* browsecap looks cleaner than urimethodmap too, and would be
+	  useful for mailto
+		- it overlaps with siteconf quite a bit...
 buffer:
 - important: validate returned values
 	* do not block container when receiving buffer data; if invalid, kill