diff options
-rw-r--r-- | doc/cha-config.5 | 9 | ||||
-rw-r--r-- | doc/cha-mailcap.5 | 109 | ||||
-rw-r--r-- | doc/config.md | 7 | ||||
-rw-r--r-- | doc/mailcap.md | 134 | ||||
-rw-r--r-- | res/config.toml | 6 | ||||
-rw-r--r-- | src/config/config.nim | 32 | ||||
-rw-r--r-- | src/config/mailcap.nim | 133 | ||||
-rw-r--r-- | src/io/dynstream.nim | 7 | ||||
-rw-r--r-- | src/local/pager.nim | 312 | ||||
-rw-r--r-- | src/utils/twtstr.nim | 12 | ||||
-rw-r--r-- | todo | 11 |
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 |