about summary refs log tree commit diff stats
path: root/src/local/pager.nim
diff options
context:
space:
mode:
Diffstat (limited to 'src/local/pager.nim')
-rw-r--r--src/local/pager.nim312
1 files changed, 218 insertions, 94 deletions
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)