about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--doc/config.md10
-rw-r--r--res/chawan.html11
-rw-r--r--res/config.toml16
-rw-r--r--src/local/client.nim4
-rw-r--r--src/local/container.nim66
-rw-r--r--src/local/pager.nim104
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>&lt;</kbd>, <kbd>&gt;</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()