diff options
author | bptato <nincsnevem662@gmail.com> | 2024-03-16 18:24:42 +0100 |
---|---|---|
committer | bptato <nincsnevem662@gmail.com> | 2024-03-16 18:25:24 +0100 |
commit | a83c0be8abb07e736297dcc6d6304bfbb243eb99 (patch) | |
tree | f118e60b4da3ce625155d1bd1394effe983ba4b4 /src | |
parent | 1d2b576e408059f500e97e52e8930d2cb7c38551 (diff) | |
download | chawan-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.nim | 8 | ||||
-rw-r--r-- | src/loader/loader.nim | 72 | ||||
-rw-r--r-- | src/local/pager.nim | 85 |
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 |