diff options
-rw-r--r-- | doc/config.md | 10 | ||||
-rw-r--r-- | res/chawan.html | 11 | ||||
-rw-r--r-- | res/config.toml | 16 | ||||
-rw-r--r-- | src/local/client.nim | 4 | ||||
-rw-r--r-- | src/local/container.nim | 66 | ||||
-rw-r--r-- | src/local/pager.nim | 104 |
6 files changed, 139 insertions, 72 deletions
diff --git a/doc/config.md b/doc/config.md index 87f88136..3be07c81 100644 --- a/doc/config.md +++ b/doc/config.md @@ -1172,6 +1172,16 @@ if none were found.)</td> </tr> <tr> +<td>`pager.saveLink()`</td> +<td>Save URL pointed to by the cursor.</td> +</tr> + +<tr> +<td>`pager.saveSource()`</td> +<td>Save the source of the current buffer.</td> +</tr> + +<tr> <td>`pager.extern(cmd, options = {setenv: true, suspend: true, wait: false})` </td> <td>Run an external command `cmd`. The `$CHA_URL` and `$CHA_CHARSET` variables diff --git a/res/chawan.html b/res/chawan.html index 69c45986..169c6800 100644 --- a/res/chawan.html +++ b/res/chawan.html @@ -57,7 +57,7 @@ up/down by one row <li><kbd>y</kbd>: yank (copy) current selection to system clipboard (needs xsel) <li><kbd>U</kbd>: reload page <li><kbd>,</kbd> (comma), <kbd>.</kbd> (period): previous/next buffer -<li><kbd>D</kbd>: discard current buffer +<li><kbd>D</kbd>: discard (delete) current buffer <li><kbd>M-y</kbd>: copy current buffer's URL to clipboard (needs xsel) <li><kbd>yu</kbd>: copy the link currently under the cursor to clipboard (needs <li><kbd>yI</kbd>: copy the image link currently under the cursor to clipboard @@ -67,9 +67,14 @@ up/down by one row <li><kbd>C-d</kbd>, <kbd>C-u</kbd>: scroll up/down by half a page <li><kbd>C-f</kbd>, <kbd>C-b</kbd> (or <kbd>PgDn</kbd>, <kbd>PgUp</kbd>)</kbd>: scroll up/down by an entire page +<li><kbd>{number}G<kbd> (or <kbd>{number}gg</kbd>): jump to {number}'th line +<li><kbd>g0<kbd>: jump to first character of the current line's visible part +<li><kbd>gc<kbd>: jump to center of the current line's visible part +<li><kbd>g$<kbd>: jump to last character of the current line's visible part <li><kbd>{</kbd>, <kbd>}</kbd>: move cursor to the previous/next paragraph -<li><kbd>(</kbd>, <kbd>)</kbd> (or <kbd>zh</kbd>, <kbd>zl</kbd>): shift +<li><kbd>-</kbd>, <kbd>+</kbd> (or <kbd>zh</kbd>, <kbd>zl</kbd>): shift screen to the left/right by one cell +<li><kbd> <li><kbd><</kbd>, <kbd>></kbd>: shift screen to the left/right by one page <li><kbd>/</kbd>, <kbd>?</kbd>: on-page search (or search backwards) <li><kbd>n</kbd>, <kbd>N</kbd>: next/previous match @@ -86,6 +91,8 @@ beginning) <li><kbd>w</kbd>, <kbd>b</kbd>: move cursor to next/previous word <li><kbd>\</kbd>: toggle page source view <li><kbd>sE</kbd>: see source in editor +<li><kbd>sS</kbd>: save current page's source +<li><kbd>s{return/enter key}</kbd>: save page that anchor (link) points to <li><kbd>0</kbd>: cursor to first cell on line <li><kbd>^</kbd>: cursor to first non-whitespace on line <li><kbd>$</kbd>: cursor to last character on line diff --git a/res/config.toml b/res/config.toml index 962d5e22..d7186c24 100644 --- a/res/config.toml +++ b/res/config.toml @@ -106,9 +106,9 @@ W = 'pager.cursorNextBigWord()' H = 'n => pager.cursorTop(n)' M = '() => pager.cursorMiddle()' L = 'n => pager.cursorBottom(n)' -';' = 'pager.cursorLeftEdge()' -'+' = 'pager.cursorMiddleColumn()' -'@' = 'pager.cursorRightEdge()' +g0 = 'pager.cursorLeftEdge()' +gc = 'pager.cursorMiddleColumn()' +'g$' = 'pager.cursorRightEdge()' C-d = 'n => pager.halfPageDown(n)' C-u = 'n => pager.halfPageUp(n)' C-f = 'n => pager.pageDown(n)' @@ -129,6 +129,9 @@ sE = ''' pager.cacheFile)); } ''' +sC-m = 'pager.saveLink()' +sC-j = 'pager.saveLink()' +sS = 'pager.saveSource()' m = ''' async () => { const c = await pager.askChar("m"); @@ -154,8 +157,8 @@ async () => { 'zl' = 'n => pager.scrollRight(n)' J = 'n => pager.scrollDown(n)' K = 'n => pager.scrollUp(n)' -'('= 'n => pager.scrollLeft(n)' -')' = 'n => pager.scrollRight(n)' +'-'= 'n => pager.scrollLeft(n)' +'+' = 'n => pager.scrollRight(n)' C-m = 'pager.click()' C-j = 'pager.click()' I = 'pager.gotoURL(pager.hoverImage)' @@ -218,8 +221,7 @@ pager.alert("Wrap search " + (config.search.wrap ? "on" : "off")); ''' M-y = ''' () => { - if (pager.extern('printf \'%s\' "$CHA_URL" | xsel -bi', - {suspend: false, setenv: true})) + if (pager.externInto('xsel -bi', pager.url)) pager.alert("Copied URL to clipboard."); else pager.alert("Failed to copy URL to clipboard. (Is xsel installed?)"); diff --git a/src/local/client.nim b/src/local/client.nim index 7c5fabb9..c4358c4d 100644 --- a/src/local/client.nim +++ b/src/local/client.nim @@ -648,7 +648,7 @@ proc addConsole(pager: Pager; interactive: bool; clearFun, showFun, hideFun: raise newException(Defect, "Failed to open console pipe.") let url = newURL("stream:console").get let container = pager.readPipe0("text/plain", CHARSET_UNKNOWN, pipefd[0], - url, ConsoleTitle, canReinterpret = false, userRequested = false) + url, ConsoleTitle, {}) let err = newPosixStream(pipefd[1]) err.writeLine("Type (M-c) console.hide() to return to buffer mode.") let console = newConsole(err, clearFun, showFun, hideFun) @@ -664,7 +664,7 @@ proc clearConsole(client: Client) = let url = newURL("stream:console").get let pager = client.pager let replacement = pager.readPipe0("text/plain", CHARSET_UNKNOWN, pipefd[0], - url, ConsoleTitle, canreinterpret = false, userRequested = false) + url, ConsoleTitle, {}) replacement.replace = client.consoleWrapper.container pager.replace(client.consoleWrapper.container, replacement) client.consoleWrapper.container = replacement diff --git a/src/local/container.nim b/src/local/container.nim index d706b77c..41cc2efd 100644 --- a/src/local/container.nim +++ b/src/local/container.nim @@ -56,6 +56,8 @@ type tvalue*: string of cetOpen: request*: Request + url*: URL + save*: bool of cetAnchor, cetNoAnchor: anchor*: string of cetAlert: @@ -90,6 +92,9 @@ type LoadState = enum lsLoading, lsCanceled, lsLoaded + ContainerFlag* = enum + cfCloned, cfUserRequested, cfHasStart, cfCanReinterpret, cfSave, cfIsHTML + Container* = ref object # note: this is not the same as source.request.url (but should be synced # with buffer.url) @@ -134,31 +139,27 @@ type loadState: LoadState events*: Deque[ContainerEvent] startpos: Option[CursorPosition] - hasstart: bool redirectDepth*: int select*: Select - canReinterpret*: bool - cloned: bool currentSelection {.jsget.}: Highlight tmpJumpMark: PagePos jumpMark: PagePos marks: Table[string, PagePos] - ishtml*: bool filter*: BufferFilter bgcolor*: CellColor tailOnLoad*: bool cacheFile* {.jsget.}: string - userRequested*: bool mainConfig*: Config + flags*: set[ContainerFlag] jsDestructor(Highlight) jsDestructor(Container) proc newContainer*(config: BufferConfig; loaderConfig: LoaderClientConfig; url: URL; request: Request; attrs: WindowAttributes; title: string; - redirectDepth: int; canReinterpret: bool; contentType: Option[string]; + redirectDepth: int; flags: set[ContainerFlag]; contentType: Option[string]; charsetStack: seq[Charset]; cacheId: int; cacheFile: string; - userRequested: bool; mainConfig: Config): Container = + mainConfig: Config): Container = return Container( url: url, request: request, @@ -172,13 +173,12 @@ proc newContainer*(config: BufferConfig; loaderConfig: LoaderClientConfig; pos: CursorPosition( setx: -1 ), - canReinterpret: canReinterpret, loadinfo: "Connecting to " & request.url.host & "...", cacheId: cacheId, cacheFile: cacheFile, process: -1, - userRequested: userRequested, - mainConfig: mainConfig + mainConfig: mainConfig, + flags: flags ) func location(container: Container): URL {.jsfget.} = @@ -196,7 +196,7 @@ proc clone*(container: Container; newurl: URL): Promise[Container] = nc[] = container[] nc.url = url nc.process = pid - nc.cloned = true + nc.flags.incl(cfCloned) nc.retry = @[] nc.parent = nil nc.children = @[] @@ -1070,7 +1070,7 @@ proc popCursorPos*(container: Container, nojump = false) = proc copyCursorPos*(container, c2: Container) = container.startpos = some(c2.pos) - container.hasstart = true + container.flags.incl(cfHasStart) proc cursorNextLink*(container: Container, n = 1) {.jsfunc.} = if container.iface == nil: @@ -1375,7 +1375,7 @@ proc onload*(container: Container, res: int) = container.title = title container.triggerEvent(cetTitle) ) - if not container.hasstart and container.url.anchor != "": + if cfHasStart notin container.flags and container.url.anchor != "": container.requestLines().then(proc(): Promise[Opt[tuple[x, y: int]]] = return container.iface.gotoAnchor() ).then(proc(res: Opt[tuple[x, y: int]]) = @@ -1481,22 +1481,26 @@ proc reshape(container: Container): EmptyPromise {.jsfunc.} = return container.requestLines() ) -proc onclick(container: Container, res: ClickResult) +proc onclick(container: Container; res: ClickResult; save: bool) -proc displaySelect(container: Container, selectResult: SelectResult) = +proc displaySelect(container: Container; selectResult: SelectResult) = let submitSelect = proc(selected: seq[int]) = container.iface.select(selected).then(proc(res: ClickResult) = - container.onclick(res)) + container.onclick(res, save = false)) container.select.initSelect(selectResult, container.acursorx, container.acursory, container.height, submitSelect) container.triggerEvent(cetUpdate) -proc onclick(container: Container; res: ClickResult) = +proc onclick(container: Container; res: ClickResult; save: bool) = if res.repaint: container.needslines = true if res.open.isSome: - container.triggerEvent(ContainerEvent(t: cetOpen, request: res.open.get)) - if res.select.isSome: + container.triggerEvent(ContainerEvent( + t: cetOpen, + request: res.open.get, + save: save + )) + if res.select.isSome and not save: container.displaySelect(res.select.get) if res.readline.isSome: let rl = res.readline.get @@ -1521,9 +1525,25 @@ proc click*(container: Container) {.jsfunc.} = if container.iface == nil: return container.iface.click(container.cursorx, container.cursory) - .then(proc(res: ClickResult) = container.onclick(res)) + .then(proc(res: ClickResult) = container.onclick(res, save = false)) + +proc saveLink*(container: Container) {.jsfunc.} = + if container.iface == nil: + return + container.iface.click(container.cursorx, container.cursory) + .then(proc(res: ClickResult) = container.onclick(res, save = true)) -proc windowChange*(container: Container, attrs: WindowAttributes) = +proc saveSource*(container: Container) {.jsfunc.} = + if container.iface == nil: + return + container.triggerEvent(ContainerEvent( + t: cetOpen, + request: newRequest(newURL("cache:" & $container.cacheId).get), + save: true, + url: container.url + )) + +proc windowChange*(container: Container; attrs: WindowAttributes) = if attrs.width != container.width or attrs.height - 1 != container.height: container.width = attrs.width container.height = attrs.height - 1 @@ -1567,7 +1587,7 @@ proc handleCommand(container: Container) = proc setStream*(container: Container; stream: SocketStream; registerFun: proc(fd: int); fd: FileHandle; outCacheId: int) = - assert not container.cloned + assert cfCloned notin container.flags container.iface = newBufferInterface(stream, registerFun) container.iface.passFd(fd, outCacheId) discard close(fd) @@ -1577,7 +1597,7 @@ proc setStream*(container: Container; stream: SocketStream; proc setCloneStream*(container: Container; stream: SocketStream; registerFun: proc(fd: int)) = - assert container.cloned + assert cfCloned in container.flags container.iface = cloneInterface(stream, registerFun) # Maybe we have to resume loading. Let's try. discard container.iface.load().then(proc(res: int) = diff --git a/src/local/pager.nim b/src/local/pager.nim index 3a9c3aa7..49af389b 100644 --- a/src/local/pager.nim +++ b/src/local/pager.nim @@ -502,9 +502,9 @@ proc onSetLoadInfo(pager: Pager; container: Container) = proc newContainer(pager: Pager; bufferConfig: BufferConfig; loaderConfig: LoaderClientConfig; request: Request; title = ""; - redirectDepth = 0; canReinterpret = true; contentType = none(string); - charsetStack: seq[Charset] = @[]; url = request.url; cacheId = -1; - cacheFile = ""; userRequested = true): Container = + redirectDepth = 0; flags = {cfCanReinterpret, cfUserRequested}; + contentType = none(string); charsetStack: seq[Charset] = @[]; + url = request.url; cacheId = -1; cacheFile = ""): Container = request.suspended = true if loaderConfig.cookieJar != nil: # loader stores cookie jars per client, but we have no client yet. @@ -528,12 +528,11 @@ proc newContainer(pager: Pager; bufferConfig: BufferConfig; pager.term.attrs, title, redirectDepth, - canReinterpret, + flags, contentType, charsetStack, cacheId, cacheFile, - userRequested, pager.config ) pager.connectingContainers.add(ConnectingContainerItem( @@ -796,12 +795,12 @@ template myExec(cmd: string) = exitnow(127) proc toggleSource(pager: Pager) {.jsfunc.} = - if not pager.container.canReinterpret: + if cfCanReinterpret notin pager.container.flags: return if pager.container.sourcepair != nil: pager.setContainer(pager.container.sourcepair) else: - let ishtml = not pager.container.ishtml + let ishtml = cfIsHTML notin pager.container.flags #TODO I wish I could set the contentType to whatever I wanted, not just HTML let contentType = if ishtml: "text/html" @@ -944,11 +943,13 @@ proc applySiteconf(pager: Pager; url: var URL; charsetOverride: Charset; ) # Load request in a new buffer. -proc gotoURL(pager: Pager, request: Request, prevurl = none(URL), - contentType = none(string), cs = CHARSET_UNKNOWN, replace: Container = nil, - redirectDepth = 0, referrer: Container = nil) = +proc gotoURL(pager: Pager; request: Request; prevurl = none(URL); + contentType = none(string); cs = CHARSET_UNKNOWN; replace: Container = nil; + redirectDepth = 0; referrer: Container = nil; save = false; + url: URL = nil) = if referrer != nil and referrer.config.referer_from: request.referrer = referrer.url + let url = if url != nil: url else: request.url var loaderConfig: LoaderClientConfig var bufferConfig = pager.applySiteconf(request.url, cs, loaderConfig) if prevurl.isNone or not prevurl.get.equals(request.url, true) or @@ -961,12 +962,17 @@ proc gotoURL(pager: Pager, request: Request, prevurl = none(URL), # feedback on what is actually going to happen when typing a URL; TODO. if referrer != nil: loaderConfig.referrerPolicy = referrer.loaderConfig.referrerPolicy + var flags = {cfCanReinterpret, cfUserRequested} + if save: + flags.incl(cfSave) let container = pager.newContainer( bufferConfig, loaderConfig, request, redirectDepth = redirectDepth, - contentType = contentType + contentType = contentType, + flags = flags, + url = url ) if replace != nil: pager.replace(replace, container) @@ -1026,8 +1032,8 @@ proc loadURL*(pager: Pager, url: string, ctype = none(string), pager.container.retry = urls proc readPipe0*(pager: Pager; contentType: string; cs: Charset; - fd: FileHandle; url: URL, title: string; - canReinterpret, userRequested: bool): Container = + fd: FileHandle; url: URL; title: string; flags: set[ContainerFlag]): + Container = var url = url pager.loader.passFd(url.pathname, fd) safeClose(fd) @@ -1038,16 +1044,15 @@ proc readPipe0*(pager: Pager; contentType: string; cs: Charset; loaderConfig, newRequest(url), title = title, - canReinterpret = canReinterpret, - contentType = some(contentType), - userRequested = userRequested + flags = flags, + contentType = some(contentType) ) proc readPipe*(pager: Pager, contentType: string, cs: Charset, fd: FileHandle, title: string) = let url = newURL("stream:-").get let container = pager.readPipe0(contentType, cs, fd, url, title, - canReinterpret = true, userRequested = true) + {cfCanReinterpret, cfUserRequested}) inc pager.numload pager.addContainer(container) @@ -1246,7 +1251,10 @@ proc externFilterSource(pager: Pager; cmd: string; c: Container = nil; let fallback = pager.container.contentType.get("text/plain") let contentType = contentType.get(fallback) let container = pager.newContainerFrom(fromc, contentType) - container.ishtml = contentType == "text/html" + if contentType == "text/html": + container.flags.incl(cfIsHTML) + else: + container.flags.excl(cfIsHTML) pager.addContainer(container) container.filter = BufferFilter(cmd: cmd) @@ -1453,7 +1461,11 @@ proc filterBuffer(pager: Pager; stream: SocketStream; cmd: string; proc checkMailcap(pager: Pager; container: Container; stream: SocketStream; istreamOutputId: int; contentType: string): CheckMailcapResult = if container.filter != nil: - return pager.filterBuffer(stream, container.filter.cmd, container.ishtml) + return pager.filterBuffer( + stream, + container.filter.cmd, + cfIsHTML in container.flags + ) # contentType must exist, because we set it in applyResponse let shortContentType = container.contentType.get if shortContentType == "text/html": @@ -1562,6 +1574,23 @@ proc redirect(pager: Pager; container: Container; response: Response; pager.alert("Error: maximum redirection depth reached") pager.deleteContainer(container) +proc askDownloadPath(pager: Pager; container: Container; response: Response) = + var buf = pager.config.external.download_dir + let pathname = container.url.pathname + if pathname[^1] == '/': + buf &= "index.html" + else: + buf &= container.url.pathname.afterLast('/') + pager.setLineEdit(lmDownload, buf) + pager.lineData = LineDataDownload( + outputId: response.outputId, + stream: response.body + ) + pager.deleteContainer(container) + pager.redraw = true + 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) @@ -1573,8 +1602,12 @@ proc connected(pager: Pager; container: Container; response: Response) = # This forces client to ask for confirmation before quitting. # (It checks a flag on container, because console buffers must not affect this # variable.) - if container.userRequested: + 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: @@ -1582,35 +1615,28 @@ 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 and container.contentType.get.until('/') != "text": - pager.setLineEdit(lmDownload, - 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 + if not mailcapRes.found and realContentType.startsWithIgnoreCase("text/"): + pager.askDownloadPath(container, response) return if mailcapRes.connect: - container.ishtml = mailcapRes.ishtml + if mailcapRes.ishtml: + container.flags.incl(cfIsHTML) + else: + container.flags.excl(cfIsHTML) # buffer now actually exists; create a process for it container.process = pager.forkserver.forkBuffer( container.config, container.url, container.request, pager.attrs, - container.ishtml, + mailcapRes.ishtml, container.charsetStack ) if mailcapRes.fdout != istream.fd: # istream has been redirected into a filter istream.close() pager.procmap.add(ProcMapItem( - container: container, + container: container, fdout: FileHandle(mailcapRes.fdout), fdin: FileHandle(istream.fd), ostreamOutputId: mailcapRes.ostreamOutputId, @@ -1713,15 +1739,17 @@ proc handleEvent0(pager: Pager; container: Container; event: ContainerEvent): pager.redraw = true of cetOpen: let url = event.request.url - if pager.container == nil or not pager.container.isHoverURL(url): + if not event.save and (pager.container != container or + not container.isHoverURL(url)): pager.ask("Open pop-up? " & $url).then(proc(x: bool) = if x: pager.gotoURL(event.request, some(container.url), - referrer = pager.container) + referrer = pager.container, save = event.save) ) else: + let url = if event.url != nil: event.url else: event.request.url pager.gotoURL(event.request, some(container.url), - referrer = pager.container) + referrer = pager.container, save = event.save, url = url) of cetStatus: if pager.container == container: pager.showAlerts() |