about summary refs log tree commit diff stats
path: root/src
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2024-03-16 18:24:42 +0100
committerbptato <nincsnevem662@gmail.com>2024-03-16 18:25:24 +0100
commita83c0be8abb07e736297dcc6d6304bfbb243eb99 (patch)
treef118e60b4da3ce625155d1bd1394effe983ba4b4 /src
parent1d2b576e408059f500e97e52e8930d2cb7c38551 (diff)
downloadchawan-a83c0be8abb07e736297dcc6d6304bfbb243eb99.tar.gz
pager, loader: add "Save file to" functionality
As simple as it could be; no download panel yet.

Also, remove the xdg-open default mailcap entry; it's better to just
save by default.
Diffstat (limited to 'src')
-rw-r--r--src/config/config.nim8
-rw-r--r--src/loader/loader.nim72
-rw-r--r--src/local/pager.nim85
3 files changed, 128 insertions, 37 deletions
diff --git a/src/config/config.nim b/src/config/config.nim
index 8f798cd3..e42fe9e4 100644
--- a/src/config/config.nim
+++ b/src/config/config.nim
@@ -92,6 +92,7 @@ type
     mime_types* {.jsgetset.}: seq[ChaPath]
     cgi_dir* {.jsgetset.}: seq[ChaPath]
     urimethodmap* {.jsgetset.}: seq[ChaPath]
+    download_dir* {.jsgetset.}: string
     w3m_cgi_compat* {.jsgetset.}: bool
 
   InputConfig = object
@@ -364,13 +365,6 @@ proc getMailcap*(config: Config): tuple[mailcap: Mailcap, errs: seq[string]] =
     cmd: ansiPath,
     flags: {HTMLOUTPUT}
   ))
-  if not found:
-    mailcap.add(MailcapEntry(
-      mt: "*",
-      subt: "*",
-      cmd: "xdg-open '%s'"
-    ))
-    return (mailcap, errs)
   return (mailcap, errs)
 
 # We try to source mime types declared in config.
diff --git a/src/loader/loader.nim b/src/loader/loader.nim
index 3040c579..fdc87eb2 100644
--- a/src/loader/loader.nim
+++ b/src/loader/loader.nim
@@ -75,6 +75,7 @@ type
     lcAddClient
     lcLoad
     lcPassFd
+    lcRedirectToFile
     lcRemoveCachedItem
     lcRemoveClient
     lcResume
@@ -202,38 +203,45 @@ proc getOutputId(ctx: LoaderContext): int =
   result = ctx.outputNum
   inc ctx.outputNum
 
-type AddCacheFileResult = tuple[outputId: int; cacheFile: string]
-
-proc addCacheFile(ctx: LoaderContext; client: ClientData; output: OutputHandle):
-    AddCacheFileResult =
-  if output.parent != nil and output.parent.cacheId != -1:
-    # may happen e.g. if client tries to cache a `cache:' URL
-    return (output.parent.cacheId, "") #TODO can we get the file name somehow?
-  let tmpf = getTempFile(ctx.config.tmpdir)
-  let ps = newPosixStream(tmpf, O_CREAT or O_WRONLY, 0o600)
-  if unlikely(ps == nil):
-    return (-1, "")
+proc redirectToFile(ctx: LoaderContext; output: OutputHandle;
+    targetPath: string): bool =
+  let ps = newPosixStream(targetPath, O_CREAT or O_WRONLY, 0o600)
+  if ps == nil:
+    return false
   if output.currentBuffer != nil:
     let n = ps.sendData(output.currentBuffer, output.currentBufferIdx)
     if unlikely(n < output.currentBuffer.len - output.currentBufferIdx):
       ps.close()
-      return (-1, "")
+      return false
   for buffer in output.buffers:
     let n = ps.sendData(buffer)
     if unlikely(n < buffer.len):
       ps.close()
-      return (-1, "")
-  let cacheId = output.outputId
+      return false
   if output.parent != nil:
-    output.parent.cacheId = cacheId
     output.parent.outputs.add(OutputHandle(
       parent: output.parent,
       ostream: ps,
       istreamAtEnd: output.istreamAtEnd,
       outputId: ctx.getOutputId()
     ))
-  client.cacheMap.add(CachedItem(id: cacheId, path: tmpf, refc: 1))
-  return (cacheId, tmpf)
+  return true
+
+type AddCacheFileResult = tuple[outputId: int; cacheFile: string]
+
+proc addCacheFile(ctx: LoaderContext; client: ClientData; output: OutputHandle):
+    AddCacheFileResult =
+  if output.parent != nil and output.parent.cacheId != -1:
+    # may happen e.g. if client tries to cache a `cache:' URL
+    return (output.parent.cacheId, "") #TODO can we get the file name somehow?
+  let tmpf = getTempFile(ctx.config.tmpdir)
+  if ctx.redirectToFile(output, tmpf):
+    let cacheId = output.outputId
+    if output.parent != nil:
+      output.parent.cacheId = cacheId
+    client.cacheMap.add(CachedItem(id: cacheId, path: tmpf, refc: 1))
+    return (cacheId, tmpf)
+  return (-1, "")
 
 proc addFd(ctx: LoaderContext; handle: LoaderHandle) =
   let output = handle.output
@@ -493,6 +501,18 @@ proc addCacheFile(ctx: LoaderContext; stream: SocketStream) =
   stream.swrite(file)
   stream.close()
 
+proc redirectToFile(ctx: LoaderContext; stream: SocketStream) =
+  var outputId: int
+  var targetPath: string
+  stream.sread(outputId)
+  stream.sread(targetPath)
+  let output = ctx.findOutput(outputId)
+  var success = false
+  if output != nil:
+    success = ctx.redirectToFile(output, targetPath)
+  stream.swrite(success)
+  stream.close()
+
 proc shareCachedItem(ctx: LoaderContext; stream: SocketStream) =
   # share a cached file with another buffer. this is for newBufferFrom
   # (i.e. view source)
@@ -606,6 +626,9 @@ proc acceptConnection(ctx: LoaderContext) =
     of lcPassFd:
       privileged_command
       ctx.passFd(stream)
+    of lcRedirectToFile:
+      privileged_command
+      ctx.redirectToFile(stream)
     of lcRemoveCachedItem:
       ctx.removeCachedItem(stream, client)
     of lcLoad:
@@ -884,6 +907,17 @@ proc addCacheFile*(loader: FileLoader; outputId, targetPid: int):
   stream.sread(cacheFile)
   return (outputId, cacheFile)
 
+proc redirectToFile*(loader: FileLoader; outputId: int; targetPath: string):
+    bool =
+  let stream = loader.connect()
+  if stream == nil:
+    return false
+  stream.swrite(lcRedirectToFile)
+  stream.swrite(outputId)
+  stream.swrite(targetPath)
+  stream.flush()
+  stream.sread(result)
+
 const BufferSize = 4096
 
 proc handleHeaders(response: Response; request: Request; stream: SocketStream) =
@@ -994,11 +1028,11 @@ proc passFd*(loader: FileLoader; id: string; fd: FileHandle) =
     stream.sendFileHandle(fd)
     stream.close()
 
-proc removeCachedItem*(loader: FileLoader; outputId: int) =
+proc removeCachedItem*(loader: FileLoader; cacheId: int) =
   let stream = loader.connect()
   if stream != nil:
     stream.swrite(lcRemoveCachedItem)
-    stream.swrite(outputId)
+    stream.swrite(cacheId)
     stream.close()
 
 proc addClient*(loader: FileLoader; key: ClientKey; pid: int;
diff --git a/src/local/pager.nim b/src/local/pager.nim
index 5dbb8ae0..80a001e4 100644
--- a/src/local/pager.nim
+++ b/src/local/pager.nim
@@ -53,8 +53,9 @@ import chagashi/charset
 
 type
   LineMode* = enum
-    NO_LINEMODE, LOCATION, USERNAME, PASSWORD, COMMAND, BUFFER, SEARCH_F,
-    SEARCH_B, ISEARCH_F, ISEARCH_B, GOTO_LINE
+    LOCATION, USERNAME, PASSWORD, COMMAND, BUFFER, SEARCH_F, SEARCH_B,
+    ISEARCH_F, ISEARCH_B, GOTO_LINE,
+    DOWNLOAD = "(Download) Save file to"
 
   # fdin is the original fd; fdout may be the same, or different if mailcap
   # is used.
@@ -79,6 +80,12 @@ type
     outputId: int
     status: uint16
 
+  LineData = ref object of RootObj
+
+  LineDataDownload = ref object of LineData
+    outputId: int
+    stream: Stream
+
   Pager* = ref object
     alertState: PagerAlertState
     alerts: seq[string]
@@ -98,6 +105,7 @@ type
     inputBuffer*: string # currently uninterpreted characters
     iregex: Result[Regex, string]
     isearchpromise: EmptyPromise
+    lineData: LineData
     lineedit*: Option[LineEdit]
     linehist: array[LineMode, LineHistory]
     linemode: LineMode
@@ -206,13 +214,13 @@ proc searchPrev(pager: Pager, n = 1) {.jsfunc.} =
       pager.container.cursorNextMatch(pager.regex.get, wrap, true, n)
     pager.container.markPos()
 
-proc getLineHist(pager: Pager, mode: LineMode): LineHistory =
+proc getLineHist(pager: Pager; mode: LineMode): LineHistory =
   if pager.linehist[mode] == nil:
     pager.linehist[mode] = newLineHistory()
   return pager.linehist[mode]
 
-proc setLineEdit(pager: Pager, prompt: string, mode: LineMode,
-    current = "", hide = false) =
+proc setLineEdit(pager: Pager; prompt: string; mode: LineMode; current = "";
+    hide = false) =
   let hist = pager.getLineHist(mode)
   if pager.term.isatty() and pager.config.input.use_mouse:
     pager.term.disableMouse()
@@ -220,6 +228,9 @@ proc setLineEdit(pager: Pager, prompt: string, mode: LineMode,
   pager.lineedit = some(edit)
   pager.linemode = mode
 
+proc setLineEdit(pager: Pager; mode: LineMode; current = "") =
+  pager.setLineEdit($mode, mode, current)
+
 proc clearLineEdit(pager: Pager) =
   pager.lineedit = none(LineEdit)
   if pager.term.isatty() and pager.config.input.use_mouse:
@@ -1118,6 +1129,23 @@ proc updateReadLineISearch(pager: Pager, linemode: LineMode) =
       pager.isearchpromise = nil
   )
 
+proc saveTo(pager: Pager; data: LineDataDownload; path: string) =
+  if pager.loader.redirectToFile(data.outputId, path):
+    pager.alert("Saving file to " & path)
+    pager.loader.resume(@[data.outputId])
+    data.stream.close()
+    pager.lineData = nil
+  else:
+    pager.ask("Failed to save to path " & path & ". Retry?").then(
+      proc(x: bool) =
+        if x:
+          pager.alert("Failed to save to path " & path)
+          pager.setLineEdit(DOWNLOAD, path)
+        else:
+          data.stream.close()
+          pager.lineData = nil
+    )
+
 proc updateReadLine*(pager: Pager) =
   let lineedit = pager.lineedit.get
   if pager.linemode in {ISEARCH_F, ISEARCH_B}:
@@ -1154,7 +1182,18 @@ proc updateReadLine*(pager: Pager) =
         pager.searchNext()
       of GOTO_LINE:
         pager.container.gotoLine(lineedit.news)
-      else: discard
+      of DOWNLOAD:
+        let data = LineDataDownload(pager.lineData)
+        if fileExists(lineedit.news):
+          pager.ask("Override file " & lineedit.news & "?").then(
+            proc(x: bool) =
+              if x:
+                pager.saveTo(data, lineedit.news)
+              else:
+                pager.setLineEdit(DOWNLOAD, lineedit.news)
+          )
+        pager.saveTo(data, lineedit.news)
+      of ISEARCH_F, ISEARCH_B: discard
     of CANCEL:
       case pager.linemode
       of USERNAME: pager.discardBuffer()
@@ -1163,7 +1202,11 @@ proc updateReadLine*(pager: Pager) =
         pager.discardBuffer()
       of BUFFER: pager.container.readCanceled()
       of COMMAND: pager.commandMode = false
+      of DOWNLOAD:
+        let data = LineDataDownload(pager.lineData)
+        data.stream.close()
       else: discard
+      pager.lineData = nil
   if lineedit.state in {LineEditState.CANCEL, LineEditState.FINISH}:
     if pager.lineedit.get == lineedit:
       pager.clearLineEdit()
@@ -1242,6 +1285,7 @@ type CheckMailcapResult = object
   ostreamOutputId: int
   connect: bool
   ishtml: bool
+  found: bool
 
 # Pipe output of an x-ansioutput mailcap command to the text/x-ansi handler.
 proc ansiDecode(pager: Pager; url: URL; ishtml: var bool; fdin: cint): cint =
@@ -1443,17 +1487,22 @@ proc checkMailcap(pager: Pager; container: Container; stream: SocketStream;
   if shortContentType == "text/html":
     # We support text/html natively, so it would make little sense to execute
     # mailcap filters for it.
-    return CheckMailcapResult(connect: true, fdout: stream.fd, ishtml: true)
+    return CheckMailcapResult(
+      connect: true,
+      fdout: stream.fd,
+      ishtml: true,
+      found: true
+    )
   if shortContentType == "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, fdout: stream.fd)
+    return CheckMailcapResult(connect: true, fdout: stream.fd, found: true)
   #TODO callback for outpath or something
   let url = container.url
   let entry = pager.mailcap.getMailcapEntry(contentType, "", url)
   if entry == nil:
-    return CheckMailcapResult(connect: true, fdout: stream.fd)
+    return CheckMailcapResult(connect: true, fdout: stream.fd, found: false)
   let tmpdir = pager.tmpdir
   let ext = url.pathname.afterLast('.')
   let tempfile = getTempFile(tmpdir, ext)
@@ -1499,10 +1548,11 @@ proc checkMailcap(pager: Pager; container: Container; stream: SocketStream;
       connect: true,
       fdout: response.body.fd,
       ostreamOutputId: response.outputId,
-      ishtml: ishtml
+      ishtml: ishtml,
+      found: true
     )
   delEnv("MAILCAP_URL")
-  return CheckMailcapResult(connect: false, fdout: -1)
+  return CheckMailcapResult(connect: false, fdout: -1, found: true)
 
 proc redirectTo(pager: Pager; container: Container; request: Request) =
   pager.gotoURL(request, some(container.url), replace = container,
@@ -1554,6 +1604,19 @@ proc connected(pager: Pager; container: Container; response: Response) =
     container.contentType.get & ";charset=" & $container.charset
   let mailcapRes = pager.checkMailcap(container, istream, response.outputId,
     realContentType)
+  if not mailcapRes.found:
+    pager.setLineEdit(DOWNLOAD,
+      pager.config.external.download_dir &
+      container.url.pathname.afterLast('/'))
+    pager.lineData = LineDataDownload(
+      outputId: response.outputId,
+      stream: istream
+    )
+    pager.deleteContainer(container)
+    pager.redraw = true
+    pager.refreshStatusMsg()
+    dec pager.numload
+    return
   if mailcapRes.connect:
     container.ishtml = mailcapRes.ishtml
     # buffer now actually exists; create a process for it