More delinting.
[elpher.git] / elpher.el
index 75c9a7b..29d2e2b 100644 (file)
--- a/elpher.el
+++ b/elpher.el
 ;; Copyright (C) 2020 Koushk Roy <kroy@twilio.com>
 ;; Copyright (C) 2020 Vee <vee@vnsf.xyz>
 ;; Copyright (C) 2020 Simon South <simon@simonsouth.net>
-;; Copyright (C) 2019-2020 Tim Vaughan <plugd@thelambdalab.xyz>
+;; Copyright (C) 2019-2021 Tim Vaughan <plugd@thelambdalab.xyz>
 
 ;; Author: Tim Vaughan <plugd@thelambdalab.xyz>
 ;; Created: 11 April 2019
 ;; Version: 2.11.0
 ;; Keywords: comm gopher
-;; Homepage: https://alexschroeder.ch/cgit/elpher
+;; Homepage: https://thelambdalab.xyz/elpher
 ;; Package-Requires: ((emacs "27.1"))
 
 ;; This file is not part of GNU Emacs.
@@ -50,7 +50,6 @@
 ;; - caching of visited sites,
 ;; - pleasant and configurable colouring of Gopher directories,
 ;; - direct visualisation of image files,
-;; - a simple bookmark management system,
 ;; - gopher connections using TLS encryption,
 ;; - the fledgling Gemini protocol,
 ;; - the greybeard Finger protocol.
@@ -63,7 +62,8 @@
 
 ;; Elpher is under active development.  Any suggestions for
 ;; improvements are welcome, and can be made on the official
-;; project page, https://alexschroeder.ch/cgit/elpher.
+;; project page, gopher://thelambdalab.xyz/elpher, or via the
+;; project mailing list at https://lists.sr.ht/~michel-slm/elpher.
 
 ;;; Code:
 
 (defalias 'elpher-color-filter-apply
   (if (fboundp 'xterm-color-filter)
       (lambda (s)
-        (let ((xterm-color-render nil))
+        (let ((_xterm-color-render nil))
           (xterm-color-filter s)))
     'ansi-color-filter-apply)
   "A function to filter out ANSI escape sequences.")
+
 (defalias 'elpher-color-apply
   (if (fboundp 'xterm-color-filter)
       'xterm-color-filter
     (finger elpher-get-finger-page elpher-render-text "txt" elpher-text)
     (telnet elpher-get-telnet-page nil "tel" elpher-telnet)
     (other-url elpher-get-other-url-page nil "url" elpher-other-url)
-    ((special bookmarks) elpher-get-bookmarks-page nil "/" elpher-index)
-    ((special start) elpher-get-start-page nil))
+    ((special start) elpher-get-start-page nil)
+    ((special history) elpher-get-history-page nil)
+    ((special history-all) elpher-get-history-all-page nil))
   "Association list from types to getters, renderers, margin codes and index faces.")
 
 
 (defvar elpher--gemini-page-headings nil
   "List of headings on the page.")
 
-(defvar elpher--gemini-page-links nil
-  "List of links on the page.")
+;;; Declarations to avoid compiler warnings.
+;;
+
+(eval-when-compile
+  (defvar bookmark-make-record-function)
+  (declare-function bookmark-store "bookmark")
+  (declare-function org-link-store-props "ol")
+  (declare-function org-link-set-parameters "ol")
+  (defvar thing-at-point-uri-schemes)
+  (defvar mu4e~view-beginning-of-url-regexp))
 
-(defvar elpher--gemini-page-links-cache (make-hash-table :test 'equal)
-  "Hash of addresses and page links.")
 
 ;;; Customization group
 ;;
@@ -219,10 +227,6 @@ May be empty."
   "Specify the string used for bullets when rendering gemini maps."
   :type '(string))
 
-(defcustom elpher-bookmarks-file (locate-user-emacs-file "elpher-bookmarks")
-  "Specify the name of the file where elpher bookmarks will be saved."
-  :type '(file))
-
 (defcustom elpher-ipv4-always nil
   "If non-nil, elpher will always use IPv4 to establish network connections.
 This can be useful when browsing from a computer that supports IPv6, because
@@ -234,9 +238,9 @@ some servers which do not support IPv6 can take a long time to time-out."
 Otherwise, the SOCKS proxy is only used for connections to onion services."
   :type '(boolean))
 
-(defcustom elpher-gemini-number-links nil
-  "If non-nil, number links in gemini pages when rendering.
-Links can be accessed by pressing `v' ('visit') followed by the link number."
+(defcustom elpher-number-links nil
+  "If non-nil, number links in pages when rendering.
+Links can be followed numerically by pressing `M' and entering by the link number."
   :type '(boolean))
 
 ;; Face customizations
@@ -334,8 +338,6 @@ Links can be accessed by pressing `v' ('visit') followed by the link number."
         (let ((url (url-generic-parse-url url-string)))
           (unless (and (not (url-fullness url)) (url-type url))
             (setf (url-fullness url) t)
-            (setf (url-filename url)
-                  (url-unhex-string (url-filename url)))
             (unless (url-type url)
               (setf (url-type url) elpher-default-url-type))
             (unless (url-host url)
@@ -399,6 +401,11 @@ requiring gopher-over-TLS."
   "Create an ADDRESS object corresponding to the given special address symbol TYPE."
   type)
 
+(defun elpher-make-start-page ()
+  "Create the start page."
+  (elpher-make-page "Elpher Start Page"
+                    (elpher-make-special-address 'start)))
+
 (defun elpher-address-to-url (address)
   "Get string representation of ADDRESS, or nil if ADDRESS is special."
   (if (elpher-address-special-p address)
@@ -437,7 +444,7 @@ address refers to, via the table `elpher-type-map'."
 For gopher addresses this is a combination of the selector type and selector."
   (if (symbolp address)
       nil
-    (url-filename address)))
+    (url-unhex-string (url-filename address))))
 
 (defun elpher-address-host (address)
   "Retrieve host from ADDRESS object."
@@ -455,13 +462,13 @@ If no address is defined, returns 0.  (This is for compatibility with the URL li
     (url-port address)))
 
 (defun elpher-address-special-p (address)
-  "Return non-nil if ADDRESS object is special (e.g. start page, bookmarks page)."
+  "Return non-nil if ADDRESS object is special (e.g. start page page)."
   (symbolp address))
 
 (defun elpher-address-gopher-p (address)
   "Return non-nill if ADDRESS object is a gopher address."
   (and (not (elpher-address-special-p address))
-       (member (elpher-address-protocol address) '("gopher gophers"))))
+       (member (elpher-address-protocol address) '("gopher" "gophers"))))
 
 (defun elpher-gopher-address-selector (address)
   "Retrieve gopher selector from ADDRESS object."
@@ -510,8 +517,17 @@ If no address is defined, returns 0.  (This is for compatibility with the URL li
   "Set the address corresponding to PAGE to NEW-ADDRESS."
   (setcar (cdr page) new-address))
 
-(defvar elpher-current-page nil) ; buffer local
-(defvar elpher-history nil)      ; buffer local
+(defvar elpher-current-page nil
+  "The current page for this Elpher buffer.")
+
+(defvar elpher-history nil
+  "The local history for this Elpher buffer.
+This variable is used by `elpher-back' and
+`elpher-show-history'.")
+
+(defvar elpher-history-all nil
+  "The global history for all Elpher buffers.
+This variable is used by `elpher-show-history-all'.")
 
 (defun elpher-visit-page (page &optional renderer no-history)
   "Visit PAGE using its own renderer or RENDERER, if non-nil.
@@ -519,24 +535,21 @@ Additionally, push PAGE onto the stack of previously-visited pages,
 unless NO-HISTORY is non-nil."
   (elpher-save-pos)
   (elpher-process-cleanup)
+  (elpher-reset-link-number-counter)
   (unless (or no-history
               (equal (elpher-page-address elpher-current-page)
                      (elpher-page-address page)))
-    (push elpher-current-page elpher-history))
+    (push elpher-current-page elpher-history)
+    (push elpher-current-page elpher-history-all))
   (setq-local elpher-current-page page)
   (let* ((address (elpher-page-address page))
          (type (elpher-address-type address))
-         (type-record (cdr (assoc type elpher-type-map)))
-         (page-links nil))
+         (type-record (cdr (assoc type elpher-type-map))))
     (if type-record
-        (progn
-          (funcall (car type-record)
-                   (if renderer
-                       renderer
-                     (cadr type-record)))
-          (setq page-links (gethash address elpher--gemini-page-links-cache))
-          (if page-links
-              (setq elpher--gemini-page-links page-links)))
+        (funcall (car type-record)
+                 (if renderer
+                     renderer
+                   (cadr type-record)))
       (elpher-visit-previous-page)
       (pcase type
         (`(gopher ,type-char)
@@ -689,7 +702,7 @@ unless `elpher-gemini-TLS-cert-checks' is non-nil.
 
 If non-nil, FORCE-IPV4 causes the network connection to be made over
 ipv4 only.  (The default behaviour when this is not set depends on
-the host operating system and the local network capabilities."
+the host operating system and the local network capabilities.)"
   (if (and use-tls (not (gnutls-available-p)))
       (error "Use of TLS requires Emacs to be compiled with GNU TLS support")
     (unless (< (elpher-address-port address) 65536)
@@ -1006,6 +1019,24 @@ displayed.  The _WINDOW argument is currently unused."
                                                 address
                                               (elpher-address-to-url address))))))))
 
+(defvar elpher--link-number-counter 0
+  "Used to number links on elpher pages.")
+(defun elpher-reset-link-number-counter ()
+  "Reset the link number counter."
+  (setq-local elpher--link-number-counter 0))
+
+(defun elpher--insert-text-button (label &rest properties)
+  "Insert a potentially-numbered button into the current buffer.
+The text for the button is provided by LABEL, while the button
+properties in PROPERTIES are as per `insert-text-button'."
+
+  (if elpher-number-links
+      (setq-local elpher--link-number-counter (+ elpher--link-number-counter 1)))
+  (let ((pref (if elpher-number-links
+                  (concat "[" (number-to-string elpher--link-number-counter) "] ")
+                "")))
+    (apply #'insert-text-button (cons (concat pref label) properties))))
+
 (defun elpher-insert-index-record (display-string &optional address)
   "Function to insert an index record into the current buffer.
 The contents of the record are dictated by DISPLAY-STRING and ADDRESS.
@@ -1019,12 +1050,12 @@ If ADDRESS is not supplied or nil the record is rendered as an
                (filtered-display-string (elpher-color-filter-apply display-string))
                (page (elpher-make-page filtered-display-string address)))
           (elpher-insert-margin margin-code)
-          (insert-text-button filtered-display-string
-                              'face face
-                              'elpher-page page
-                              'action #'elpher-click-link
-                              'follow-link t
-                              'help-echo #'elpher--page-button-help))
+          (elpher--insert-text-button filtered-display-string
+                                      'face face
+                                      'elpher-page page
+                                      'action #'elpher-click-link
+                                      'follow-link t
+                                      'help-echo #'elpher--page-button-help))
       (pcase type
         ('nil ;; Information
          (elpher-insert-margin)
@@ -1271,10 +1302,15 @@ that the response was malformed."
          (error "Gemini server response unknown: %s %s"
                 response-code response-meta))))))
 
-(unless (fboundp 'read-answer)
-  (defun read-answer (question answers)
-    "Backfill for the new read-answer code."
-    (completing-read question (mapcar 'identity answers))))
+(defun elpher--read-answer-polyfill (question answers)
+  "Polyfill for `read-answer' in Emacs 26.1.
+QUESTION is a string containing a question, and ANSWERS
+is a list of possible answers."
+    (completing-read question (mapcar 'identity answers)))
+
+(if (fboundp 'read-answer)
+    (defalias 'elpher-read-answer 'read-answer)
+  (defalias 'elpher-read-answer 'elpher--read-answer-polyfill))
 
 (defun elpher-choose-client-certificate ()
   "Prompt for a client certificate to use to establish a TLS connection."
@@ -1435,19 +1471,18 @@ treatment that a separate function is warranted."
          (address (elpher-address-from-gemini-url url))
          (type (if address (elpher-address-type address) nil))
          (type-map-entry (cdr (assoc type elpher-type-map))))
-    (setq elpher--gemini-page-links (append elpher--gemini-page-links `(,address)))
     (when display-string
       (insert elpher-gemini-link-string)
       (if type-map-entry
           (let* ((face (elt type-map-entry 3))
                  (filtered-display-string (elpher-color-filter-apply display-string))
                  (page (elpher-make-page filtered-display-string address)))
-            (insert-text-button filtered-display-string
-                                'face face
-                                'elpher-page page
-                                'action #'elpher-click-link
-                                'follow-link t
-                                'help-echo #'elpher--page-button-help))
+            (elpher--insert-text-button filtered-display-string
+                                        'face face
+                                        'elpher-page page
+                                        'action #'elpher-click-link
+                                        'follow-link t
+                                        'help-echo #'elpher--page-button-help))
         (insert (propertize display-string 'face 'elpher-unknown)))
       (insert "\n"))))
 
@@ -1492,7 +1527,12 @@ width defined by `elpher-gemini-max-fill-width'."
                      (propertize text-line 'face 'elpher-gemini-quoted))
                     (t text-line))
             text-line))
-         (adaptive-fill-mode t))
+         (adaptive-fill-mode t)
+        ;; fill-prefix is important for adaptive-fill-mode: without
+        ;; it, multi-line list items are not indented correct
+         (fill-prefix (if (match-string 2 text-line)
+                          (replace-regexp-in-string "[>\*]" " " (match-string 0 text-line))
+                        nil)))
     (insert (elpher-process-text-for-display processed-text-line))
     (newline)))
 
@@ -1500,11 +1540,9 @@ width defined by `elpher-gemini-max-fill-width'."
   "Render DATA as a gemini map file, PARAMETERS is currently unused."
   (elpher-with-clean-buffer
    (setq elpher--gemini-page-headings nil)
-   (let ((preformatted nil)
-         (link-counter 1))
+   (let ((preformatted nil))
      (auto-fill-mode 1)
      (setq-local fill-column (min (window-width) elpher-gemini-max-fill-width))
-     (setq elpher--gemini-page-links '())
      (dolist (line (split-string data "\n"))
        (cond
         ((string-prefix-p "```" line) (setq preformatted (not preformatted)))
@@ -1512,25 +1550,13 @@ width defined by `elpher-gemini-max-fill-width'."
                                (propertize line 'face 'elpher-gemini-preformatted))
                               "\n"))
         ((string-prefix-p "=>" line)
-         (progn
-           (if elpher-gemini-number-links
-               (insert
-                (concat
-                 "["
-                 (number-to-string link-counter)
-                 "] ")))
-           (setq link-counter (1+ link-counter))
-           (elpher-gemini-insert-link line)))
+         (elpher-gemini-insert-link line))
         ((string-prefix-p "#" line) (elpher-gemini-insert-header line))
         (t (elpher-gemini-insert-text line)))))
    (setq elpher--gemini-page-headings (nreverse elpher--gemini-page-headings))
    (elpher-cache-content
     (elpher-page-address elpher-current-page)
-    (buffer-string))
-   (puthash
-    (elpher-page-address elpher-current-page)
-    elpher--gemini-page-links
-    elpher--gemini-page-links-cache)))
+    (buffer-string))))
 
 (defun elpher-render-gemini-plain-text (data _parameters)
   "Render DATA as plain text file.  PARAMETERS is currently unused."
@@ -1617,20 +1643,19 @@ The result is rendered using RENDERER."
            "\n"
            " - TAB/Shift-TAB: next/prev item on current page\n"
            " - RET/mouse-1: open item under cursor\n"
-           " - m: select an item on current page by name (autocompletes)\n"
+           " - m/M: select an item on current page by name (autocompletes) or number\n"
            " - u/mouse-3/U: return to previous page or to the start page\n"
            " - o/O: visit different selector or the root menu of the current server\n"
            " - g: go to a particular address (gopher, gemini, finger)\n"
-           " - v: visit a numbered link on a gemini page\n"
            " - d/D: download item under cursor or current page\n"
            " - i/I: info on item under cursor or current page\n"
            " - c/C: copy URL representation of item under cursor or current page\n"
            " - a/A: bookmark the item under cursor or current page\n"
-           " - x/X: remove bookmark for item under cursor or current page\n"
-           " - B: visit the bookmarks page\n"
+           " - B: list all bookmarks\n"
+           " - s/S: show history of current buffer or for all buffers\n"
            " - r: redraw current page (using cached contents if available)\n"
            " - R: reload current page (regenerates cache)\n"
-           " - S: set character coding system for gopher (default is to autodetect)\n"
+           " - !: set character coding system for gopher (default is to autodetect)\n"
            " - T: toggle TLS gopher mode\n"
            " - F: forget/discard current TLS client certificate\n"
            " - .: display the raw server response for the current page\n"
@@ -1647,8 +1672,19 @@ The result is rendered using RENDERER."
    (elpher-insert-index-record "Gemini Search Engine (geminispace.info)"
                                (elpher-address-from-url "gemini://geminispace.info/search"))
    (insert "\n"
-           "This page contains your bookmarked sites (also visit with B):\n")
-   (elpher-insert-index-record "Your Bookmarks" 'bookmarks)
+           "Your bookmarks are stored in your ")
+   (let ((help-string "RET,mouse-1: Open Emacs bookmark list"))
+     (elpher--insert-text-button "Emacs bookmark list"
+                                'face 'link
+                                'action (lambda (_)
+                                          (interactive)
+                                          (call-interactively #'bookmark-bmenu-list))
+                                'follow-link t
+                                'help-echo help-string))
+   (insert ".\n")
+   (insert (propertize
+            "(Bookmarks from legacy elpher-bookmarks files will be automatically imported.)\n"
+            'face 'shadow))
    (insert "\n"
            "For Elpher release news or to leave feedback, visit:\n")
    (elpher-insert-index-record "The Elpher Project Page"
@@ -1659,13 +1695,13 @@ The result is rendered using RENDERER."
    (insert "\n"
            "** Refer to the ")
    (let ((help-string "RET,mouse-1: Open Elpher info manual (if available)"))
-     (insert-text-button "Elpher info manual"
-                         'face 'link
-                         'action (lambda (_)
-                                   (interactive)
-                                   (info "(elpher)"))
-                         'follow-link t
-                         'help-echo help-string))
+     (elpher--insert-text-button "Elpher info manual"
+                                 'face 'link
+                                 'action (lambda (_)
+                                           (interactive)
+                                           (info "(elpher)"))
+                                 'follow-link t
+                                 'help-echo help-string))
    (insert " for the full documentation. **\n")
    (insert (propertize
             (concat "  (This should be available if you have installed Elpher using\n"
@@ -1673,124 +1709,126 @@ The result is rendered using RENDERER."
             'face 'shadow))
    (elpher-restore-pos)))
 
-;; Bookmarks page page retrieval
+;; History page retrieval
+
+(defun elpher-history ()
+  "Show the history of pages leading to the current page in this buffer.
+Use \\[elpher-history-all] to see the entire history.
+This is rendered using `elpher-get-history-page' via `elpher-type-map'."
+  (interactive)
+  (elpher-visit-page
+   (elpher-make-page "Elpher History Page"
+                    (elpher-make-special-address 'history))))
 
-(defun elpher-get-bookmarks-page (renderer)
-  "Getter to load and display the current bookmark list (RENDERER must be nil)."
+(defun elpher-history-all ()
+  "Show the all the pages you've visited using Elpher.
+Use \\[elpher-history] to see just the history for the current buffer.
+This is rendered using `elpher-get-history-all-page' via `elpher-type-map'."
+  (interactive)
+  (elpher-visit-page
+   (elpher-make-page "Elpher History Of All Seen Pages"
+                    (elpher-make-special-address 'history-all))))
+
+(defun elpher-get-history-page (renderer)
+  "Getter which displays the history page (RENDERER must be nil)."
   (when renderer
     (elpher-visit-previous-page)
-    (error "Command not supported for bookmarks page"))
+    (error "Command not supported for history page"))
+  (elpher-show-history elpher-history))
+
+(defun elpher-get-history-all-page (renderer)
+  "Getter which displays the history page (RENDERER must be nil)."
+  (when renderer
+    (elpher-visit-previous-page)
+    (error "Command not supported for history page"))
+  (elpher-show-history elpher-history-all))
+
+(defun elpher-show-history (pages)
+  "Show all PAGES in the Elpher buffer."
   (elpher-with-clean-buffer
-   (insert "---- Bookmark list ----\n\n")
-   (let ((bookmarks (elpher-load-bookmarks)))
-     (if bookmarks
-         (dolist (bookmark bookmarks)
-           (let ((display-string (elpher-bookmark-display-string bookmark))
-                 (address (elpher-address-from-url (elpher-bookmark-url bookmark))))
-             (elpher-insert-index-record display-string address)))
-       (insert "No bookmarks found.\n")))
-   (insert "\n-----------------------\n"
-           "\n"
-           "- u: return to previous page\n"
-           "- x: delete selected bookmark\n"
-           "- a: rename selected bookmark\n"
-           "\n"
-           "Bookmarks are stored in the file ")
-   (let ((filename elpher-bookmarks-file)
-         (help-string "RET,mouse-1: Open bookmarks file in new buffer for editing."))
-     (insert-text-button filename
-                         'face 'link
-                         'action (lambda (_)
-                                   (interactive)
-                                   (find-file filename))
-                         'follow-link t
-                         'help-echo help-string))
-   (insert "\n")
-   (elpher-restore-pos)))
+   (insert "---- Visited link history ----\n\n")
+   (if pages
+       (dolist (page pages)
+        (when page
+           (let ((display-string (elpher-page-display-string page))
+                (address (elpher-page-address page)))
+             (elpher-insert-index-record display-string address))))
+     (insert "No history items found.\n"))
+   (insert "\n----------------------------")))
 
 
 ;;; Bookmarks
-;;
 
-(defun elpher-make-bookmark (display-string url)
-  "Make an elpher bookmark.
-DISPLAY-STRING determines how the bookmark will appear in the
-bookmark list, while URL is the url of the entry."
-  (list display-string url))
-
-(defun elpher-bookmark-display-string (bookmark)
-  "Get the display string of BOOKMARK."
-  (elt bookmark 0))
-
-(defun elpher-set-bookmark-display-string (bookmark display-string)
-  "Set the display string of BOOKMARK to DISPLAY-STRING."
-  (setcar bookmark display-string))
-
-(defun elpher-bookmark-url (bookmark)
-  "Get the address for BOOKMARK."
-  (elt bookmark 1))
-
-(defun elpher-save-bookmarks (bookmarks)
-  "Record the bookmark list BOOKMARKS to the user's bookmark file.
-Beware that this completely replaces the existing contents of the file."
-  (let ((bookmark-dir (file-name-directory elpher-bookmarks-file)))
-    (unless (file-directory-p bookmark-dir)
-      (make-directory bookmark-dir)))
-  (with-temp-file elpher-bookmarks-file
-    (erase-buffer)
-    (insert "; Elpher bookmarks file\n\n"
-            "; Bookmarks are stored as a list of (label URL) items.\n"
-            "; Feel free to edit by hand, but take care to ensure\n"
-            "; the list structure remains intact.\n\n")
-    (pp bookmarks (current-buffer))))
-
-(defun elpher-load-bookmarks ()
-  "Get the list of bookmarks from the users's bookmark file."
-  (let ((bookmarks
-         (with-temp-buffer
-           (ignore-errors
-             (insert-file-contents elpher-bookmarks-file)
-             (goto-char (point-min))
-             (read (current-buffer))))))
-    (if (and bookmarks (listp (cadar bookmarks)))
-        (progn
-          (message "Reading old bookmark file. (Will be updated on write.)")
-          (mapcar (lambda (old-bm)
-                    (list (car old-bm)
-                          (elpher-address-to-url (apply #'elpher-make-gopher-address
-                                                        (cadr old-bm)))))
-                  bookmarks))
-      bookmarks)))
-
-(defun elpher-add-address-bookmark (address display-string)
-  "Save a bookmark for ADDRESS with label DISPLAY-STRING.)))
-If ADDRESS is already bookmarked, update the label only."
-  (let ((bookmarks (elpher-load-bookmarks))
-        (url (elpher-address-to-url address)))
-    (let ((existing-bookmark (rassoc (list url) bookmarks)))
-      (if existing-bookmark
-          (elpher-set-bookmark-display-string existing-bookmark display-string)
-        (push (elpher-make-bookmark display-string url) bookmarks)))
-    (elpher-save-bookmarks bookmarks)))
-
-(defun elpher-remove-address-bookmark (address)
-  "Remove any bookmark to ADDRESS."
-  (let ((url (elpher-address-to-url address)))
-    (elpher-save-bookmarks
-     (seq-filter (lambda (bookmark)
-                   (not (equal (elpher-bookmark-url bookmark) url)))
-                 (elpher-load-bookmarks)))))
+;; This code allows Elpher to use the standard Emacs bookmarks: `C-x r
+;; m' to add a bookmark, `C-x r l' to list bookmarks (which is where
+;; you can anotate bookmarks!), `C-x r b' to jump to a bookmark, and
+;; so on. See the Bookmarks section in the Emacs info manual for more.
+
+(defvar elpher-bookmark-link nil
+  "Prefer bookmarking a link or the current page.
+Bind this variable dynamically, or set it to t.
+If you set it to t, the commands \\[bookmark-set-no-overwrite]
+and \\[elpher-set-bookmark-no-overwrite] do the same thing.")
+
+(defun elpher-bookmark-make-record ()
+  "Return a bookmark record.
+If `elpher-bookmark-link' is non-nil and point is on a link button,
+return a bookmark record for that link.  Otherwise, return a bookmark
+record for the current elpher page."
+  (let* ((button (and elpher-bookmark-link (button-at (point))))
+        (page (if button
+                  (button-get button 'elpher-page)
+                elpher-current-page))
+        (address (elpher-page-address page))
+        (url (elpher-address-to-url address))
+        (display-string (elpher-page-display-string page))
+        (pos (if button nil (point))))
+    (if (elpher-address-special-p address)
+       (error "Cannot bookmark %s" display-string)
+      `(,display-string
+       (defaults . (,display-string))
+       (position . ,pos)
+       (location . ,url)
+       (handler . elpher-bookmark-jump)))))
+
+;;;###autoload
+(defun elpher-bookmark-jump (bookmark)
+  "Go to a particular BOOKMARK."
+  (let* ((url (cdr (assq 'location bookmark))))
+    (elpher-go url)))
+
+(defun elpher-set-bookmark-no-overwrite ()
+  "Bookmark the link at point.
+To bookmark the current page, use \\[bookmark-set-no-overwrite]."
+  (interactive)
+  (let ((elpher-bookmark-link t))
+    (bookmark-set-no-overwrite)))
+
+(defun elpher-bookmark-import (file)
+  "Import Elpher bookmarks file FILE into Emacs bookmarks."
+  (interactive (list (if (and (boundp 'elpher-bookmarks-file)
+                             (file-readable-p elpher-bookmarks-file))
+                        elpher-bookmarks-file
+                      (read-file-name "Old Elpher bookmarks: "
+                                      user-emacs-directory nil t
+                                      "elpher-bookmarks"))))
+  (require 'bookmark)
+  (dolist (bookmark (with-temp-buffer
+                     (insert-file-contents file)
+                     (read (current-buffer))))
+    (let* ((display-string (car bookmark))
+           (url (cadr bookmark))
+          (record `(,display-string
+                    (location . ,url)
+                    (handler . elpher-bookmark-jump))))
+      (bookmark-store display-string (cdr record) t)))
+  (bookmark-save))
 
 ;;; Integrations
 ;;
 
 ;;; Org
 
-;; Avoid byte compilation warnings.
-(eval-when-compile
-  (declare-function org-link-store-props "ol")
-  (declare-function org-link-set-parameters "ol"))
-
 (defun elpher-org-export-link (link description format protocol)
   "Export a LINK with DESCRIPTION for the given PROTOCOL and FORMAT.
 
@@ -1834,7 +1872,8 @@ supports the old protocol elpher, where the link is self-contained."
                (format "%s:%s" protocol link))))
     (elpher-go url)))
 
-(with-eval-after-load 'org
+(defun elpher-org-mode-integration ()
+  "Set up `elpher' integration for `org-mode'."
   (org-link-set-parameters
    "elpher"
    :store #'elpher-org-store-link
@@ -1857,11 +1896,9 @@ supports the old protocol elpher, where the link is self-contained."
              (elpher-org-export-link link description format "finger"))
    :follow (lambda (link _arg) (elpher-org-follow-link link "finger"))))
 
-;;; Browse URL
+(add-hook 'org-mode-hook #'elpher-org-mode-integration)
 
-;; Avoid byte compilation warnings.
-(eval-when-compile
-  (defvar thing-at-point-uri-schemes))
+;;; Browse URL
 
 ;;;###autoload
 (defun elpher-browse-url-elpher (url &rest _args)
@@ -1875,9 +1912,12 @@ supports the old protocol elpher, where the link is self-contained."
     (add-to-list
      'browse-url-default-handlers
      '("^\\(gopher\\|finger\\|gemini\\)://" . elpher-browse-url-elpher))
-  ;; Patch browse-url-default-browser for older ones
-  (advice-add 'browse-url-default-browser :before-while
+  ;; Patch `browse-url-browser-function' for older ones. The value of
+  ;; that variable is `browse-url-default-browser' by default, so
+  ;; that's the function that gets advised.
+  (advice-add browse-url-browser-function :before-while
               (lambda (url &rest _args)
+               "Handle gemini, gopher, and finger schemes using Elpher."
                 (let ((scheme (downcase (car (split-string url ":" t)))))
                   (if (member scheme '("gemini" "gopher" "finger"))
                       ;; `elpher-go' always returns nil, which will stop the
@@ -1892,13 +1932,9 @@ supports the old protocol elpher, where the link is self-contained."
 
 ;;; Mu4e:
 
-(eval-when-compile
-  (defvar mu4e~view-beginning-of-url-regexp))
-
-(with-eval-after-load 'mu4e-view
-  ;; Make mu4e aware of the gemini world
-  (setq mu4e~view-beginning-of-url-regexp
-        "\\(?:https?\\|gopher\\|finger\\|gemini\\)://\\|mailto:"))
+;; Make mu4e aware of the gemini world
+(setq mu4e~view-beginning-of-url-regexp
+      "\\(?:https?\\|gopher\\|finger\\|gemini\\)://\\|mailto:")
 
 ;;; Interactive procedures
 ;;
@@ -1927,7 +1963,8 @@ When run interactively HOST-OR-URL is read from the minibuffer."
          (address (elpher-address-from-url cleaned-host-or-url))
          (page (elpher-make-page cleaned-host-or-url address)))
     (switch-to-buffer elpher-buffer-name)
-    (elpher-visit-page page)
+    (elpher-with-clean-buffer
+     (elpher-visit-page page))
     nil))
 
 (defun elpher-go-current ()
@@ -1939,15 +1976,6 @@ When run interactively HOST-OR-URL is read from the minibuffer."
                               (elpher-address-to-url address)))))
       (elpher-visit-page (elpher-make-page url (elpher-address-from-url url))))))
 
-(defun elpher-visit-gemini-numbered-link (n)
-  "Visit link designated by a number N."
-  (interactive "nLink number: ")
-  (if (or (> n (length elpher--gemini-page-links))
-          (< n 1))
-      (user-error "Invalid link number"))
-  (let ((address (nth (1- n) elpher--gemini-page-links)))
-    (elpher-go (url-recreate-url address))))
-
 (defun elpher-redraw ()
   "Redraw current page."
   (interactive)
@@ -1987,9 +2015,7 @@ When run interactively HOST-OR-URL is read from the minibuffer."
   (interactive)
   (setq-local elpher-current-page nil)
   (setq-local elpher-history nil)
-  (let ((start-page (elpher-make-page "Elpher Start Page"
-                                      (elpher-make-special-address 'start))))
-    (elpher-visit-page start-page)))
+  (elpher-visit-page (elpher-make-start-page)))
 
 (defun elpher-download ()
   "Download the link at point."
@@ -2038,6 +2064,17 @@ When run interactively HOST-OR-URL is read from the minibuffer."
                 (goto-char (button-start b))
                 (button-activate b)))))))
 
+(defun elpher-jump-to-number (n)
+  "Jump to directory entry number N."
+  (interactive "nDirectory item/link number: ")
+  (let* ((link-map (reverse (elpher-build-link-map))))
+    (if link-map
+        (if (<= 1 n (length link-map))
+            (let ((b (cdr (elt link-map (- n 1)))))
+              (goto-char (button-start b))
+              (button-activate b))
+          (error "No link with that number")))))
+
 (defun elpher-root-dir ()
   "Visit root of current server."
   (interactive)
@@ -2053,74 +2090,6 @@ When run interactively HOST-OR-URL is read from the minibuffer."
             (elpher-go (elpher-address-to-url address-copy))))
       (error "Command invalid for %s" (elpher-page-display-string elpher-current-page)))))
 
-(defun elpher-bookmarks-current-p ()
-  "Return non-nil if current page is a bookmarks page."
-  (equal (elpher-address-type (elpher-page-address elpher-current-page))
-         '(special bookmarks)))
-
-(defun elpher-reload-bookmarks ()
-  "Reload bookmarks if current page is a bookmarks page."
-  (if (elpher-bookmarks-current-p)
-      (elpher-reload-current-page)))
-
-(defun elpher-bookmark-current ()
-  "Bookmark the current page."
-  (interactive)
-  (let ((address (elpher-page-address elpher-current-page))
-        (display-string (elpher-page-display-string elpher-current-page)))
-    (if (not (elpher-address-special-p address))
-        (let ((bookmark-display-string (read-string "Bookmark display string: "
-                                                    display-string)))
-          (elpher-add-address-bookmark address bookmark-display-string)
-          (message "Bookmark added."))
-      (error "Cannot bookmark %s" display-string))))
-
-(defun elpher-bookmark-link ()
-  "Bookmark the link at point."
-  (interactive)
-  (let ((button (button-at (point))))
-    (if button
-        (let* ((page (button-get button 'elpher-page))
-               (address (elpher-page-address page))
-               (display-string (elpher-page-display-string page)))
-          (if (not (elpher-address-special-p address))
-              (let ((bookmark-display-string (read-string "Bookmark display string: "
-                                                          display-string)))
-                (elpher-add-address-bookmark address bookmark-display-string)
-                (elpher-reload-bookmarks)
-                (message "Bookmark added."))
-            (error "Cannot bookmark %s" display-string)))
-      (error "No link selected"))))
-
-(defun elpher-unbookmark-current ()
-  "Remove bookmark for the current page."
-  (interactive)
-  (let ((address (elpher-page-address elpher-current-page)))
-    (when (and (not (elpher-address-special-p address))
-               (y-or-n-p "Really remove bookmark for the current page? "))
-      (elpher-remove-address-bookmark address)
-      (message "Bookmark removed."))))
-
-(defun elpher-unbookmark-link ()
-  "Remove bookmark for the link at point."
-  (interactive)
-  (let ((button (button-at (point))))
-    (if button
-        (when (y-or-n-p "Really remove bookmark for this link? ")
-          (let ((page (button-get button 'elpher-page)))
-            (elpher-remove-address-bookmark (elpher-page-address page))
-            (elpher-reload-bookmarks)
-            (message "Bookmark removed.")))
-      (error "No link selected"))))
-
-;;;###autoload
-(defun elpher-bookmarks ()
-  "Visit bookmarks page."
-  (interactive)
-  (switch-to-buffer elpher-buffer-name)
-  (elpher-visit-page
-   (elpher-make-page "Bookmarks Page" (elpher-make-special-address 'bookmarks))))
-
 (defun elpher-info-page (page)
   "Display information on PAGE."
   (let ((display-string (elpher-page-display-string page))
@@ -2183,11 +2152,15 @@ When run interactively HOST-OR-URL is read from the minibuffer."
     (define-key map (kbd "<backtab>") 'elpher-prev-link)
     (define-key map (kbd "C-M-i") 'elpher-prev-link)
     (define-key map (kbd "u") 'elpher-back)
-    (define-key map (kbd "U") 'elpher-back-to-start)
+    (define-key map (kbd "-") 'elpher-back)
+    (define-key map (kbd "^") 'elpher-back)
     (define-key map [mouse-3] 'elpher-back)
+    (define-key map (kbd "U") 'elpher-back-to-start)
     (define-key map (kbd "O") 'elpher-root-dir)
     (define-key map (kbd "g") 'elpher-go)
     (define-key map (kbd "o") 'elpher-go-current)
+    (define-key map (kbd "s") 'elpher-history)
+    (define-key map (kbd "S") 'elpher-history-all)
     (define-key map (kbd "r") 'elpher-redraw)
     (define-key map (kbd "R") 'elpher-reload)
     (define-key map (kbd "T") 'elpher-toggle-tls)
@@ -2195,18 +2168,16 @@ When run interactively HOST-OR-URL is read from the minibuffer."
     (define-key map (kbd "d") 'elpher-download)
     (define-key map (kbd "D") 'elpher-download-current)
     (define-key map (kbd "m") 'elpher-jump)
+    (define-key map (kbd "M") 'elpher-jump-to-number)
     (define-key map (kbd "i") 'elpher-info-link)
     (define-key map (kbd "I") 'elpher-info-current)
     (define-key map (kbd "c") 'elpher-copy-link-url)
     (define-key map (kbd "C") 'elpher-copy-current-url)
-    (define-key map (kbd "a") 'elpher-bookmark-link)
-    (define-key map (kbd "A") 'elpher-bookmark-current)
-    (define-key map (kbd "x") 'elpher-unbookmark-link)
-    (define-key map (kbd "X") 'elpher-unbookmark-current)
-    (define-key map (kbd "B") 'elpher-bookmarks)
-    (define-key map (kbd "S") 'elpher-set-gopher-coding-system)
+    (define-key map (kbd "a") 'elpher-set-bookmark-no-overwrite)
+    (define-key map (kbd "A") 'bookmark-set-no-overwrite)
+    (define-key map (kbd "B") 'bookmark-bmenu-list)
+    (define-key map (kbd "!") 'elpher-set-gopher-coding-system)
     (define-key map (kbd "F") 'elpher-forget-current-certificate)
-    (define-key map (kbd "v") 'elpher-visit-gemini-numbered-link)
     (when (fboundp 'evil-define-key*)
       (evil-define-key*
        'motion map
@@ -2216,8 +2187,8 @@ When run interactively HOST-OR-URL is read from the minibuffer."
        (kbd "u") 'elpher-back
        (kbd "-") 'elpher-back
        (kbd "^") 'elpher-back
-       (kbd "U") 'elpher-back-to-start
        [mouse-3] 'elpher-back
+       (kbd "U") 'elpher-back-to-start
        (kbd "o") 'elpher-go
        (kbd "O") 'elpher-go-current
        (kbd "r") 'elpher-redraw
@@ -2226,19 +2197,17 @@ When run interactively HOST-OR-URL is read from the minibuffer."
        (kbd ".") 'elpher-view-raw
        (kbd "d") 'elpher-download
        (kbd "D") 'elpher-download-current
-       (kbd "J") 'elpher-jump
+       (kbd "m") 'elpher-jump
+       (kbd "M") 'elpher-jump-to-number
        (kbd "i") 'elpher-info-link
        (kbd "I") 'elpher-info-current
        (kbd "c") 'elpher-copy-link-url
        (kbd "C") 'elpher-copy-current-url
-       (kbd "a") 'elpher-bookmark-link
-       (kbd "A") 'elpher-bookmark-current
-       (kbd "x") 'elpher-unbookmark-link
-       (kbd "X") 'elpher-unbookmark-current
-       (kbd "B") 'elpher-bookmarks
-       (kbd "S") 'elpher-set-gopher-coding-system
-       (kbd "F") 'elpher-forget-current-certificate
-       (kbd "v") 'elpher-visit-gemini-numbered-link))
+       (kbd "a") 'elpher-set-bookmark-no-overwrite
+       (kbd "A") 'bookmark-set-no-overwrite
+       (kbd "B") 'bookmark-bmenu-list
+       (kbd "!") 'elpher-set-gopher-coding-system
+       (kbd "F") 'elpher-forget-current-certificate))
     map)
   "Keymap for gopher client.")
 
@@ -2246,16 +2215,14 @@ When run interactively HOST-OR-URL is read from the minibuffer."
   "Major mode for elpher, an elisp gopher client.
 
 This mode is automatically enabled by the interactive
-functions which initialize the gopher client, namely
-`elpher', `elpher-go' and `elpher-bookmarks'."
+functions which initialize the client, namely
+`elpher', and `elpher-go'."
   (setq-local elpher--gemini-page-headings nil)
   (setq-local elpher-current-page nil)
   (setq-local elpher-history nil)
   (setq-local elpher-buffer-name (buffer-name))
-
-  (setq-local imenu-create-index-function
-              (lambda ()
-                elpher--gemini-page-headings)))
+  (setq-local bookmark-make-record-function #'elpher-bookmark-make-record)
+  (setq-local imenu-create-index-function (lambda () elpher--gemini-page-headings)))
 
 (when (fboundp 'evil-set-initial-state)
   (evil-set-initial-state 'elpher-mode 'motion))
@@ -2285,10 +2252,7 @@ to create a new session.  Returns the buffer selected (or created)."
     (pop-to-buffer-same-window buf)
     (unless (buffer-modified-p)
       (elpher-mode)
-      (let ((start-page (elpher-make-page
-                         "Elpher Start Page"
-                         (elpher-make-special-address 'start))))
-        (elpher-visit-page start-page))
+      (elpher-visit-page (elpher-make-start-page))
       "Started Elpher."))); Otherwise (elpher) evaluates to start page string.
 
 ;;; elpher.el ends here