Better grouping of text processing procedures.
[elpher.git] / elpher.el
index 883bde8..ef20de0 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
+;; Version: 3.0.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.
@@ -61,8 +61,9 @@
 ;; Full instructions can be found in the Elpher info manual.
 
 ;; 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.
+;; improvements are welcome, and can be made on the official project
+;; page, gopher://thelambdalab.xyz/1/projects/elpher, or via the
+;; project mailing list at https://lists.sr.ht/~michel-slm/elpher.
 
 ;;; Code:
 
 (require 'gnutls)
 (require 'socks)
 
-;;; ANSI colors or XTerm colors
-
-(or (require 'xterm-color nil t)
-    (require 'ansi-color))
-
-(defalias 'elpher-color-filter-apply
-  (if (fboundp 'xterm-color-filter)
-      (lambda (s)
-        (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
-    'ansi-color-apply)
-  "A function to apply ANSI escape sequences.")
-
 ;;; Global constants
 ;;
 
-(defconst elpher-version "2.11.0"
+(defconst elpher-version "3.0.0"
   "Current version of elpher.")
 
 (defconst elpher-margin-width 6
     (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 start) elpher-get-start-page nil)
-    ((special history) elpher-get-history-page nil)
-    ((special history-all) elpher-get-history-all-page nil))
+    ((special start) elpher-get-start-page nil "E" elpher-index)
+    ((special history) elpher-get-history-page nil "E" elpher-index)
+    ((special visited-pages) elpher-get-visited-pages-page nil "E" elpher-index))
   "Association list from types to getters, renderers, margin codes and index faces.")
 
 
-;;; Internal variables
+;;; Declarations to avoid compiler warnings.
 ;;
 
-;; buffer-local
-(defvar elpher--gemini-page-headings nil
-  "List of headings on the page.")
-
-(defvar elpher--gemini-page-links nil
-  "List of links on the page.")
+(eval-when-compile
+  (declare-function ansi-color-filter-apply "ansi-color")
+  (declare-function ansi-color-apply "ansi-color")
+  (declare-function bookmark-store "bookmark")
+  (declare-function org-link-store-props "ol")
+  (declare-function org-link-set-parameters "ol")
+  (defvar ansi-color-context)
+  (defvar bookmark-make-record-function)
+  (defvar mu4e~view-beginning-of-url-regexp)
+  (defvar thing-at-point-uri-schemes))
 
-(defvar elpher--gemini-page-links-cache (make-hash-table :test 'equal)
-  "Hash of addresses and page links.")
 
 ;;; Customization group
 ;;
@@ -230,11 +215,6 @@ 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."
-  :type '(boolean))
-
 ;; Face customizations
 
 (defgroup elpher-faces nil
@@ -330,8 +310,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)
@@ -395,11 +373,6 @@ 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)
@@ -438,7 +411,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."
@@ -462,7 +435,7 @@ If no address is defined, returns 0.  (This is for compatibility with the URL li
 (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."
@@ -499,6 +472,11 @@ If no address is defined, returns 0.  (This is for compatibility with the URL li
   "Create a page with DISPLAY-STRING and ADDRESS."
   (list display-string address))
 
+(defun elpher-make-start-page ()
+  "Create the start page."
+  (elpher-make-page "Elpher Start Page"
+                    (elpher-make-special-address 'start)))
+
 (defun elpher-page-display-string (page)
   "Retrieve the display string corresponding to PAGE."
   (elt page 0))
@@ -515,39 +493,37 @@ If no address is defined, returns 0.  (This is for compatibility with the URL li
   "The current page for this Elpher buffer.")
 
 (defvar elpher-history nil
-  "The local history for this Elpher buffer.
+  "The local history stack for this Elpher buffer.
 This variable is used by `elpher-back' and
 `elpher-show-history'.")
 
-(defvar elpher-history-all nil
+(defvar elpher-visited-pages nil
   "The global history for all Elpher buffers.
-This variable is used by `elpher-show-history-all'.")
+This variable is used by `elpher-show-visited-pages'.")
 
 (defun elpher-visit-page (page &optional renderer no-history)
   "Visit PAGE using its own renderer or RENDERER, if non-nil.
-Additionally, push PAGE onto the stack of previously-visited pages,
-unless NO-HISTORY is non-nil."
+Additionally, push PAGE onto the history stack and the list of
+previously-visited pages,unless NO-HISTORY is non-nil."
   (elpher-save-pos)
   (elpher-process-cleanup)
-  (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-all))
+  (unless no-history
+    (unless (equal (elpher-page-address elpher-current-page)
+                   (elpher-page-address page))
+      (push elpher-current-page elpher-history)
+      (unless (or (elpher-address-special-p (elpher-page-address page))
+                  (and elpher-visited-pages
+                       (equal page (car elpher-visited-pages))))
+        (push page elpher-visited-pages))))
   (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)
@@ -608,7 +584,8 @@ unless NO-HISTORY is non-nil."
      (unless (eq major-mode 'elpher-mode)
        ;; avoid resetting buffer-local variables
        (elpher-mode))
-     (let ((inhibit-read-only t))
+     (let ((inhibit-read-only t)
+           (ansi-color-context nil)) ;; clean ansi interpreter state
        (setq-local network-security-level
                    (default-value 'network-security-level))
        (erase-buffer)
@@ -651,6 +628,57 @@ away CRs and any terminating period."
   (elpher-decode (replace-regexp-in-string "\n\\.\n$" "\n"
                                            (replace-regexp-in-string "\r" "" string))))
 
+;;; Buttonify urls
+
+(defconst elpher-url-regex
+  "\\([a-zA-Z]+\\)://\\([a-zA-Z0-9.-]*[a-zA-Z0-9-]\\|\\[[a-zA-Z0-9:]+\\]\\)\\(:[0-9]+\\)?\\(/\\([0-9a-zA-Z_~?/@|:.%#=&-]*[0-9a-zA-Z_~?/@|#-]\\)?\\)?"
+  "Regexp used to locate and buttonify URLs in text files loaded by elpher.")
+
+(defun elpher-buttonify-urls (string)
+  "Turn substrings which look like urls in STRING into clickable buttons."
+  (with-temp-buffer
+    (insert string)
+    (goto-char (point-min))
+    (while (re-search-forward elpher-url-regex nil t)
+      (let ((page (elpher-make-page (substring-no-properties (match-string 0))
+                                    (elpher-address-from-url (match-string 0)))))
+        (make-text-button (match-beginning 0)
+                          (match-end 0)
+                          'elpher-page  page
+                          'action #'elpher-click-link
+                          'follow-link t
+                          'help-echo #'elpher--page-button-help
+                          'face 'button)))
+    (buffer-string)))
+
+;;; ANSI colors or XTerm colors (application and filtering)
+
+(or (require 'xterm-color nil t)
+    (require 'ansi-color))
+
+(defalias 'elpher-color-filter-apply
+  (if (fboundp 'xterm-color-filter)
+      (lambda (s)
+        (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
+    #'ansi-color-apply)
+  "A function to apply ANSI escape sequences.")
+
+;;; Processing text for display
+
+(defun elpher-process-text-for-display (string)
+  "Perform any desired processing of STRING prior to display as text.
+Currently includes buttonifying URLs and processing ANSI escape codes."
+  (elpher-buttonify-urls (if elpher-filter-ansi-from-text
+                             (elpher-color-filter-apply string)
+                           (elpher-color-apply string))))
+
 
 ;;; Network error reporting
 ;;
@@ -685,6 +713,15 @@ ERROR can be either an error object or a string."
   (if (timerp elpher-network-timer)
       (cancel-timer elpher-network-timer)))
 
+(defun elpher-make-network-timer (thunk)
+  "Creates a timer to run the THUNK after `elpher-connection-timeout' seconds.
+This is just a wraper around `run-at-time' which additionally sets the
+buffer-local variable `elpher-network-timer' to allow
+`elpher-process-cleanup' to also clear the timer."
+  (let ((timer (run-at-time elpher-connection-timeout nil thunk)))
+    (setq-local elpher-network-timer timer)
+    timer))
+
 (defun elpher-get-host-response (address default-port query-string response-processor
                                          &optional use-tls force-ipv4)
   "Generic function for retrieving data from ADDRESS.
@@ -700,7 +737,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)
@@ -711,13 +748,17 @@ the host operating system and the local network capabilities."
     (condition-case nil
         (let* ((kill-buffer-query-functions nil)
                (port (elpher-address-port address))
-               (service (if (> port 0) port default-port))
                (host (elpher-address-host address))
-               (socks (or elpher-socks-always (string-suffix-p ".onion" host)))
+               (service (if (> port 0) port default-port))
                (response-string-parts nil)
                (bytes-received 0)
                (hkbytes-received 0)
-               (timer (run-at-time elpher-connection-timeout nil
+               (socks (or elpher-socks-always (string-suffix-p ".onion" host)))
+               (gnutls-params (list :type 'gnutls-x509pki
+                                    :hostname host
+                                    :keylist
+                                    (elpher-get-current-keylist address)))
+               (timer (elpher-make-network-timer
                                    (lambda ()
                                      (elpher-process-cleanup)
                                      (cond
@@ -740,8 +781,6 @@ the host operating system and the local network capabilities."
                                                                  nil force-ipv4))
                                       (t
                                        (elpher-network-error address "Connection time-out."))))))
-               (gnutls-params (list :type 'gnutls-x509pki :hostname host
-                                    :keylist (elpher-get-current-keylist address)))
                (proc (if socks (socks-open-network-stream "elpher-process" nil host service)
                        (make-network-process :name "elpher-process"
                                              :host host
@@ -810,6 +849,7 @@ the host operating system and the local network capabilities."
             (if use-tls (apply #'gnutls-negotiate :process proc gnutls-params))
             (funcall (process-sentinel proc) proc "open\n")))
       (error
+       (elpher-process-cleanup)
        (error "Error initiating connection to server")))))
 
 
@@ -1064,37 +1104,6 @@ If ADDRESS is not supplied or nil the record is rendered as an
 
 ;; Text rendering
 
-(defconst elpher-url-regex
-  "\\([a-zA-Z]+\\)://\\([a-zA-Z0-9.-]*[a-zA-Z0-9-]\\|\\[[a-zA-Z0-9:]+\\]\\)\\(:[0-9]+\\)?\\(/\\([0-9a-zA-Z_~?/@|:.%#=&-]*[0-9a-zA-Z_~?/@|#-]\\)?\\)?"
-  "Regexp used to locate and buttinofy URLs in text files loaded by elpher.")
-
-(defun elpher-buttonify-urls (string)
-  "Turn substrings which look like urls in STRING into clickable buttons."
-  (with-temp-buffer
-    (insert string)
-    (goto-char (point-min))
-    (while (re-search-forward elpher-url-regex nil t)
-      (let ((page (elpher-make-page (substring-no-properties (match-string 0))
-                                    (elpher-address-from-url (match-string 0)))))
-        (make-text-button (match-beginning 0)
-                          (match-end 0)
-                          'elpher-page  page
-                          'action #'elpher-click-link
-                          'follow-link t
-                          'help-echo #'elpher--page-button-help
-                          'face 'button)))
-    (buffer-string)))
-
-(defconst elpher-ansi-regex "\x1b\\[[^m]*m"
-  "Incomplete regexp used to strip out some troublesome ANSI escape sequences.")
-
-(defun elpher-process-text-for-display (string)
-  "Perform any desired processing of STRING prior to display as text.
-Currently includes buttonifying URLs and processing ANSI escape codes."
-  (elpher-buttonify-urls (if elpher-filter-ansi-from-text
-                             (elpher-color-filter-apply string)
-                           (elpher-color-apply string))))
-
 (defun elpher-render-text (data &optional _mime-type-string)
   "Render DATA as text.  MIME-TYPE-STRING is unused."
   (elpher-with-clean-buffer
@@ -1282,10 +1291,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."
@@ -1446,7 +1460,6 @@ 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
@@ -1462,6 +1475,9 @@ treatment that a separate function is warranted."
         (insert (propertize display-string 'face 'elpher-unknown)))
       (insert "\n"))))
 
+(defvar elpher--gemini-page-headings nil
+  "List of headings on the page.")
+
 (defun elpher-gemini-insert-header (header-line)
   "Insert header described by HEADER-LINE into a text/gemini document.
 The gemini map file line describing the header is given
@@ -1516,11 +1532,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)))
@@ -1528,25 +1542,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."
@@ -1616,7 +1618,7 @@ The result is rendered using RENDERER."
         (browse-url url)))))
 
 
-;; Start page page retrieval
+;; Start page retrieval
 
 (defun elpher-get-start-page (renderer)
   "Getter which displays the start page (RENDERER must be nil)."
@@ -1635,18 +1637,17 @@ The result is rendered using RENDERER."
            " - RET/mouse-1: open item under cursor\n"
            " - m: select an item on current page by name (autocompletes)\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"
+           " - o/O: open a different address selector or the root menu of the current server\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"
            " - B: list all bookmarks\n"
-           " - h/H: show history of current buffer or for all buffers\n"
+           " - s/S: show current history stack or all previously visted pages\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"
@@ -1663,73 +1664,107 @@ 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"
-           "For Elpher release news or to leave feedback, visit:\n")
+           "Your bookmarks are stored in your ")
+   (let ((help-string "RET,mouse-1: Open Emacs bookmark list"))
+     (insert-text-button "Emacs bookmark list"
+                         'face 'link
+                         'action (lambda (_)
+                                   (interactive)
+                                   (call-interactively #'elpher-open-bookmarks))
+                         '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"
+           "The gopher home of the Elpher project is here:\n")
    (elpher-insert-index-record "The Elpher Project Page"
                                (elpher-make-gopher-address ?1
                                                            "/projects/elpher/"
                                                            "thelambdalab.xyz"
                                                            70))
-   (insert "\n"
-           "** Refer to the ")
    (let ((help-string "RET,mouse-1: Open Elpher info manual (if available)"))
-     (insert-text-button "Elpher info manual"
+     (insert "\n"
+             "The following info documentation is available:\n"
+             "   - ")
+     (insert-text-button "Elpher Manual"
                          'face 'link
                          'action (lambda (_)
                                    (interactive)
                                    (info "(elpher)"))
                          'follow-link t
-                         'help-echo help-string))
-   (insert " for the full documentation. **\n")
+                         'help-echo help-string)
+     (insert "\n   - ")
+     (insert-text-button "Changes introduced by the latest release"
+                       'face 'link
+                       'action (lambda (_)
+                                 (interactive)
+                                 (info "(elpher)News"))
+                       'follow-link t
+                       'help-echo help-string))
+   (insert "\n")
    (insert (propertize
-            (concat "  (This should be available if you have installed Elpher using\n"
-                    "   MELPA. Otherwise you will have to install the manual yourself.)\n")
+            (concat "  (These documents should be available if you have installed Elpher \n"
+                    "   using MELPA. Otherwise you may have to install the manual yourself.)\n")
             'face 'shadow))
    (elpher-restore-pos)))
 
 ;; 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.
+(defun elpher-show-history ()
+  "Show the current contents of elpher's history stack.
+Use \\[elpher-show-visited-pages] 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-page "Current History Stack"
                     (elpher-make-special-address 'history))))
 
-(defun elpher-history-all ()
+(defun elpher-show-visited-pages ()
   "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'."
+Use \\[elpher-show-history] to see just the current history stack.
+This is rendered using `elpher-get-visited-pages-page' via `elpher-type-map'."
   (interactive)
-  (elpher-visit-page 
-   (elpher-make-page "Elpher History Of All Seen Pages"
-                    (elpher-make-special-address 'history-all))))
+  (elpher-visit-page
+   (elpher-make-page "Elpher Visted Pages"
+                    (elpher-make-special-address 'visited-pages))))
 
 (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 history page"))
-  (elpher-show-history elpher-history))
+  (elpher-display-history-links elpher-history "Current history stack"))
 
-(defun elpher-get-history-all-page (renderer)
-  "Getter which displays the history page (RENDERER must be nil)."
+(defun elpher-get-visited-pages-page (renderer)
+  "Getter which displays the list of visited pages (RENDERER must be nil)."
   (when renderer
     (elpher-visit-previous-page)
     (error "Command not supported for history page"))
-  (elpher-show-history elpher-history-all))
+  (elpher-display-history-links
+   (seq-filter (lambda (page)
+                 (not (elpher-address-special-p (elpher-page-address page))))
+               elpher-visited-pages)
+   "All visited pages"))
+
+(defun elpher-display-history-links (pages title)
+  "Show all PAGES in an Elpher buffer with a given TITLE."
+  (let* ((title-line (concat "---- " title " ----"))
+         (footer-line (make-string (length title-line) ?-)))
+    (elpher-with-clean-buffer
+     (insert title-line "\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" footer-line "\n"
+             "Select and entry or press 'u' to return to the previous page.")
+     (elpher-restore-pos))))
 
-(defun elpher-show-history (pages)
-  "Show all PAGES in the Elpher buffer."
-  (elpher-with-clean-buffer
-   (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"))))
 
 ;;; Bookmarks
 
@@ -1747,7 +1782,7 @@ 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
+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
@@ -1767,19 +1802,35 @@ record for the current elpher page."
 
 ;;;###autoload
 (defun elpher-bookmark-jump (bookmark)
-  "Go to a particular BOOKMARK."
-  (let* ((url (cdr (assq 'location bookmark))))
-    (elpher-go url)))
+  "Handler used to open a bookmark using elpher.
+The argument BOOKMARK is a bookmark record passed to the function.
+This handler is responsible for loading the bookmark in some buffer,
+then making that buffer the current buffer.  It should not switch
+to the buffer."
+  (let* ((url (cdr (assq 'location bookmark)))
+         (cleaned-url (string-trim url))
+         (address (elpher-address-from-url cleaned-url))
+         (page (elpher-make-page cleaned-url address)))
+    (elpher-with-clean-buffer
+     (elpher-visit-page page))
+    (set-buffer (get-buffer elpher-buffer-name))
+    nil))
 
-(defun elpher-set-bookmark-no-overwrite ()
+(defun elpher-bookmark-link ()
   "Bookmark the link at point.
-To bookmark the current page, use \\[bookmark-set-no-overwrite]."
+To bookmark the current page, use \\[elpher-bookmark-current]."
   (interactive)
   (let ((elpher-bookmark-link t))
     (bookmark-set-no-overwrite)))
 
+(defun elpher-bookmark-current ()
+  "Bookmark the current page.
+To bookmark the link at point use \\[elpher-bookmark-link]."
+  (interactive)
+  (call-interactively #'bookmark-set-no-overwrite))
+
 (defun elpher-bookmark-import (file)
-  "Import Elpher bookmarks into Emacs bookmarks."
+  "Import legacy Elpher bookmarks file FILE into Emacs bookmarks."
   (interactive (list (if (and (boundp 'elpher-bookmarks-file)
                              (file-readable-p elpher-bookmarks-file))
                         elpher-bookmarks-file
@@ -1798,16 +1849,28 @@ To bookmark the current page, use \\[bookmark-set-no-overwrite]."
       (bookmark-store display-string (cdr record) t)))
   (bookmark-save))
 
+(defun elpher-open-bookmarks ()
+  "Display the current list of elpher bookmarks.
+This is just a call to `bookmark-bmenu-list', but we also check for a legacy
+bookmark file and offer to import it."
+  (interactive)
+  (let ((old-bookmarks-file (or (and (boundp 'elpher-bookmarks-file)
+                                     elpher-bookmarks-file)
+                                (locate-user-emacs-file "elpher-bookmarks"))))
+    (when (and (file-readable-p old-bookmarks-file)
+               (y-or-n-p (concat "Legacy elpher-bookmarks file \""
+                                 old-bookmarks-file
+                                 "\" found. Import now?")))
+      (elpher-bookmark-import old-bookmarks-file)
+      (rename-file old-bookmarks-file (concat old-bookmarks-file "-legacy"))))
+  (call-interactively #'bookmark-bmenu-list))
+
+
 ;;; 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.
 
@@ -1851,7 +1914,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
@@ -1874,11 +1938,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)
@@ -1912,13 +1974,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
 ;;
@@ -1960,15 +2018,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)
@@ -2035,7 +2084,7 @@ When run interactively HOST-OR-URL is read from the minibuffer."
                        #'elpher-render-download
                        t)))
 
-(defun elpher-build-link-map ()
+(defun elpher--build-link-map ()
   "Build alist mapping link names to destination pages in current buffer."
   (let ((link-map nil)
         (b (next-button (point-min) t)))
@@ -2047,7 +2096,7 @@ When run interactively HOST-OR-URL is read from the minibuffer."
 (defun elpher-jump ()
   "Select a directory entry by name.  Similar to the info browser (m)enu command."
   (interactive)
-  (let* ((link-map (elpher-build-link-map)))
+  (let* ((link-map (elpher--build-link-map)))
     (if link-map
         (let ((key (let ((completion-ignore-case t))
                      (completing-read "Directory item/link: "
@@ -2134,13 +2183,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 "O") 'elpher-root-dir)
+    (define-key map (kbd "U") 'elpher-back-to-start)
     (define-key map (kbd "g") 'elpher-go)
     (define-key map (kbd "o") 'elpher-go-current)
-    (define-key map (kbd "h") 'elpher-history)
-    (define-key map (kbd "H") 'elpher-history-all)
+    (define-key map (kbd "O") 'elpher-root-dir)
+    (define-key map (kbd "s") 'elpher-show-history)
+    (define-key map (kbd "S") 'elpher-show-visited-pages)
     (define-key map (kbd "r") 'elpher-redraw)
     (define-key map (kbd "R") 'elpher-reload)
     (define-key map (kbd "T") 'elpher-toggle-tls)
@@ -2152,12 +2203,11 @@ When run interactively HOST-OR-URL is read from the minibuffer."
     (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-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 "S") 'elpher-set-gopher-coding-system)
+    (define-key map (kbd "a") 'elpher-bookmark-link)
+    (define-key map (kbd "A") 'elpher-bookmark-current)
+    (define-key map (kbd "B") 'elpher-open-bookmarks)
+    (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
@@ -2167,27 +2217,29 @@ 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 "o") 'elpher-go
-       (kbd "O") 'elpher-go-current
+       (kbd "U") 'elpher-back-to-start
+       (kbd "g") 'elpher-go
+       (kbd "o") 'elpher-go-current
+       (kbd "O") 'elpher-root-dir
+       (kbd "s") 'elpher-show-history
+       (kbd "S") 'elpher-show-visited-pages
        (kbd "r") 'elpher-redraw
        (kbd "R") 'elpher-reload
        (kbd "T") 'elpher-toggle-tls
        (kbd ".") 'elpher-view-raw
        (kbd "d") 'elpher-download
        (kbd "D") 'elpher-download-current
-       (kbd "J") 'elpher-jump
+       (kbd "m") 'elpher-jump
        (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-set-bookmark-no-overwrite
-       (kbd "A") 'bookmark-set-no-overwrite
-       (kbd "B") 'bookmark-bmenu-list
-       (kbd "S") 'elpher-set-gopher-coding-system
-       (kbd "F") 'elpher-forget-current-certificate
-       (kbd "v") 'elpher-visit-gemini-numbered-link))
+       (kbd "a") 'elpher-bookmark-link
+       (kbd "A") 'elpher-bookmark-current
+       (kbd "B") 'elpher-open-bookmarks
+       (kbd "!") 'elpher-set-gopher-coding-system
+       (kbd "F") 'elpher-forget-current-certificate))
     map)
   "Keymap for gopher client.")
 
@@ -2202,7 +2254,8 @@ functions which initialize the client, namely
   (setq-local elpher-history nil)
   (setq-local elpher-buffer-name (buffer-name))
   (setq-local bookmark-make-record-function #'elpher-bookmark-make-record)
-  (setq-local imenu-create-index-function #'elpher--gemini-page-headings))
+  (setq-local imenu-create-index-function (lambda () elpher--gemini-page-headings))
+  (setq-local xterm-color-preserve-properties t))
 
 (when (fboundp 'evil-set-initial-state)
   (evil-set-initial-state 'elpher-mode 'motion))