Release to allow opening links in new buffer.
[elpher.git] / elpher.el
index da4d11d..c3c3dc7 100644 (file)
--- a/elpher.el
+++ b/elpher.el
@@ -1,13 +1,14 @@
-;;; elpher.el --- A friendly gopher and gemini client  -*- lexical-binding:t -*-
+;;; elpher.el --- A friendly gopher and gemini client  -*- lexical-binding: t -*-
 
 
-;; Copyright (C) 2019-2020 Tim Vaughan
+;; Copyright (C) 2019-2023 Tim Vaughan <plugd@thelambdalab.xyz>
+;; Copyright (C) 2020-2022 Elpher contributors (See info manual for full list)
 
 ;; Author: Tim Vaughan <plugd@thelambdalab.xyz>
 ;; Created: 11 April 2019
 
 ;; Author: Tim Vaughan <plugd@thelambdalab.xyz>
 ;; Created: 11 April 2019
-;; Version: 2.10.2
-;; Keywords: comm gopher
-;; Homepage: http://thelambdalab.xyz/elpher
-;; Package-Requires: ((emacs "26.2"))
+;; Version: 3.6.0
+;; Keywords: comm gopher gemini
+;; Homepage: https://thelambdalab.xyz/elpher
+;; Package-Requires: ((emacs "27.1"))
 
 ;; This file is not part of GNU Emacs.
 
 
 ;; This file is not part of GNU Emacs.
 
@@ -35,7 +36,6 @@
 ;; - caching of visited sites,
 ;; - pleasant and configurable colouring of Gopher directories,
 ;; - direct visualisation of image files,
 ;; - 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.
 ;; - gopher connections using TLS encryption,
 ;; - the fledgling Gemini protocol,
 ;; - the greybeard Finger protocol.
@@ -47,8 +47,9 @@
 ;; Full instructions can be found in the Elpher info manual.
 
 ;; Elpher is under active development.  Any suggestions for
 ;; 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, gopher://thelambdalab.xyz/1/projects/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:
 
 
 ;;; Code:
 
 ;;
 
 (require 'seq)
 ;;
 
 (require 'seq)
-(require 'pp)
 (require 'shr)
 (require 'url-util)
 (require 'subr-x)
 (require 'shr)
 (require 'url-util)
 (require 'subr-x)
-(require 'dns)
-(require 'ansi-color)
 (require 'nsm)
 (require 'gnutls)
 (require 'socks)
 (require 'nsm)
 (require 'gnutls)
 (require 'socks)
-
+(require 'bookmark)
+(require 'rx)
 
 ;;; Global constants
 ;;
 
 
 ;;; Global constants
 ;;
 
-(defconst elpher-version "2.10.2"
+(defconst elpher-version "3.6.0"
   "Current version of elpher.")
 
 (defconst elpher-margin-width 6
   "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)
     (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))
+    (file elpher-get-file-page nil "~" elpher-gemini)
+    ((about welcome) elpher-get-welcome-page nil "E" elpher-index)
+    ((about bookmarks) elpher-get-bookmarks-page nil "E" elpher-index)
+    ((about history) elpher-get-history-page nil "E" elpher-index)
+    ((about visited-pages) elpher-get-visited-pages-page nil "E" elpher-index))
   "Association list from types to getters, renderers, margin codes and index faces.")
 
 
   "Association list from types to getters, renderers, margin codes and index faces.")
 
 
+;;; Declarations to avoid compiler warnings.
+;;
+
+(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 xterm-color--current-fg)
+  (defvar xterm-color--current-bg)
+  (defvar bookmark-make-record-function)
+  (defvar mu4e~view-beginning-of-url-regexp)
+  (defvar eww-use-browse-url)
+  (defvar thing-at-point-uri-schemes))
+
+
 ;;; Customization group
 ;;
 
 ;;; Customization group
 ;;
 
 
 (defcustom elpher-open-urls-with-eww nil
   "If non-nil, open URL selectors using eww.
 
 (defcustom elpher-open-urls-with-eww nil
   "If non-nil, open URL selectors using eww.
-Otherwise, use the system browser via the BROWSE-URL function."
+Otherwise, use the system browser via the `browse-url' function."
   :type '(boolean))
 
 (defcustom elpher-use-header t
   :type '(boolean))
 
 (defcustom elpher-use-header t
@@ -121,8 +141,9 @@ Otherwise, use the system browser via the BROWSE-URL function."
 
 (defcustom elpher-auto-disengage-TLS nil
   "If non-nil, automatically disengage TLS following an unsuccessful connection.
 
 (defcustom elpher-auto-disengage-TLS nil
   "If non-nil, automatically disengage TLS following an unsuccessful connection.
-While enabling this may seem convenient, it is also potentially dangerous as it
-allows switching from an encrypted channel back to plain text without user input."
+While enabling this may seem convenient, it is also potentially
+dangerous as it allows switching from an encrypted channel back to
+plain text without user input."
   :type '(boolean))
 
 (defcustom elpher-connection-timeout 5
   :type '(boolean))
 
 (defcustom elpher-connection-timeout 5
@@ -131,8 +152,8 @@ allows switching from an encrypted channel back to plain text without user input
 
 (defcustom elpher-filter-ansi-from-text nil
   "If non-nil, filter ANSI escape sequences from text.
 
 (defcustom elpher-filter-ansi-from-text nil
   "If non-nil, filter ANSI escape sequences from text.
-The default behaviour is to use the ansi-color package to interpret these
-sequences."
+The default behaviour is to use the ansi-color package (or xterm-color if it is
+available) to interpret these sequences."
   :type '(boolean))
 
 (defcustom elpher-certificate-directory
   :type '(boolean))
 
 (defcustom elpher-certificate-directory
@@ -146,7 +167,7 @@ These certificates may be used for establishing authenticated TLS connections."
   :type '(file))
 
 (defcustom elpher-default-url-type "gopher"
   :type '(file))
 
 (defcustom elpher-default-url-type "gopher"
-  "Default URL type to assume if not explicitly given."
+  "Default URL type (i.e. scheme) to assume if not explicitly given."
   :type '(choice (const "gopher")
                  (const "gemini")))
 
   :type '(choice (const "gopher")
                  (const "gemini")))
 
@@ -174,10 +195,6 @@ May be empty."
   "Specify the string used for bullets when rendering gemini maps."
   :type '(string))
 
   "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
 (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
@@ -189,6 +206,49 @@ 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))
 
 Otherwise, the SOCKS proxy is only used for connections to onion services."
   :type '(boolean))
 
+(defcustom elpher-use-emacs-bookmark-menu nil
+  "If non-nil, elpher will only use the native Emacs bookmark menu.
+Otherwise, \\[elpher-show-bookmarks] will visit a special elpher bookmark
+page within which all of the standard elpher keybindings are active."
+  :type '(boolean))
+
+(defcustom elpher-start-page-url "about:welcome"
+  "Specify the page displayed initially by elpher.
+The default welcome screen is \"about:welcome\", while the bookmarks list
+is \"about:bookmarks\".  You can also specify local files via \"file:\".
+
+Beware that using \"about:bookmarks\" as a start page in combination with
+the `elpher-use-bookmark-menu' variable set to non-nil will prevent the
+Emacs bookmark menu being accessible via \\[elpher-show-bookmarks] from
+the start page."
+  :type '(string))
+
+(defcustom elpher-gemini-hide-preformatted nil
+  "Cause elpher to hide preformatted gemini text by default.
+When this option is enabled, preformatted text in text/gemini documents
+is replaced with a button which can be used to toggle its display.
+
+This is intended to improve accessibility, as preformatted text often
+includes art which can be difficult for screen readers to interpret
+meaningfully."
+  :type '(boolean))
+
+(defcustom elpher-gemini-preformatted-toggle-bullet "‣ "
+  "Margin symbol used to distinguish the preformatted text toggle."
+  :type '(string))
+
+(defcustom elpher-gemini-preformatted-toggle-label "[Toggle Preformatted Text]"
+  "Label of button used to toggle formatted text."
+  :type '(string))
+
+(defcustom elpher-certificate-map nil
+  "Register client certificates to be used for gemini URLs.
+This variable contains an alist representing a mapping between gemini
+URLs and the names of client certificates which will be automatically
+activated for those URLs.  Beware that the certificates will also be
+active for all subdirectories of the given URLs."
+  :type '(alist :key-type string :value-type string))
+
 ;; Face customizations
 
 (defgroup elpher-faces nil
 ;; Face customizations
 
 (defgroup elpher-faces nil
@@ -259,35 +319,38 @@ Otherwise, the SOCKS proxy is only used for connections to onion services."
   '((t :inherit bold :height 1.2))
   "Face used for gemini heading level 3.")
 
   '((t :inherit bold :height 1.2))
   "Face used for gemini heading level 3.")
 
-(defface elpher-gemini-preformatted
-  '((t :inherit fixed-pitch))
-  "Face used for pre-formatted gemini text blocks.")
-
 (defface elpher-gemini-quoted
   '((t :inherit font-lock-doc-face))
   "Face used for gemini quoted texts.")
 
 (defface elpher-gemini-quoted
   '((t :inherit font-lock-doc-face))
   "Face used for gemini quoted texts.")
 
+(defface elpher-gemini-preformatted
+  '((t :inherit default))
+  "Face used for gemini preformatted text.")
+
+(defface elpher-gemini-preformatted-toggle
+  '((t :inherit button))
+  "Face used for buttons used to toggle display of preformatted text.")
+
 ;;; Model
 ;;
 
 ;; Address
 
 ;; An elpher "address" object is either a url object or a symbol.
 ;;; Model
 ;;
 
 ;; Address
 
 ;; An elpher "address" object is either a url object or a symbol.
-;; Symbol addresses are "special", corresponding to pages generated
+;; Addresses with the "about" type, corresponding to pages generated
 ;; dynamically for and by elpher.  All others represent pages which
 ;; rely on content retrieved over the network.
 
 ;; dynamically for and by elpher.  All others represent pages which
 ;; rely on content retrieved over the network.
 
-(defun elpher-address-from-url (url-string)
-  "Create a ADDRESS object corresponding to the given URL-STRING."
+(defun elpher-address-from-url (url-string &optional default-scheme)
+  "Create a ADDRESS object corresponding to the given URL-STRING.
+If DEFAULT-SCHEME is non-nil, this sets the scheme of the URL when one
+is not explicitly given."
   (let ((data (match-data))) ; Prevent parsing clobbering match data
     (unwind-protect
         (let ((url (url-generic-parse-url url-string)))
           (unless (and (not (url-fullness url)) (url-type url))
   (let ((data (match-data))) ; Prevent parsing clobbering match data
     (unwind-protect
         (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)
             (unless (url-type url)
-              (setf (url-type url) elpher-default-url-type))
+              (setf (url-type url) default-scheme))
             (unless (url-host url)
               (let ((p (split-string (url-filename url) "/" nil nil)))
                 (setf (url-host url) (car p))
             (unless (url-host url)
               (let ((p (split-string (url-filename url) "/" nil nil)))
                 (setf (url-host url) (car p))
@@ -295,6 +358,9 @@ Otherwise, the SOCKS proxy is only used for connections to onion services."
                       (if (cdr p)
                           (concat "/" (mapconcat #'identity (cdr p) "/"))
                         ""))))
                       (if (cdr p)
                           (concat "/" (mapconcat #'identity (cdr p) "/"))
                         ""))))
+            (when (not (string-empty-p (url-host url)))
+              (setf (url-fullness url) t)
+              (setf (url-host url) (puny-encode-domain (url-host url))))
             (when (or (equal "gopher" (url-type url))
                       (equal "gophers" (url-type url)))
               ;; Gopher defaults
             (when (or (equal "gopher" (url-type url))
                       (equal "gophers" (url-type url)))
               ;; Gopher defaults
@@ -310,9 +376,9 @@ Otherwise, the SOCKS proxy is only used for connections to onion services."
 
 (defun elpher-remove-redundant-ports (address)
   "Remove redundant port specifiers from ADDRESS.
 
 (defun elpher-remove-redundant-ports (address)
   "Remove redundant port specifiers from ADDRESS.
-Here 'redundant' means that the specified port matches the default
+Here `redundant' means that the specified port matches the default
 for that protocol, eg 70 for gopher."
 for that protocol, eg 70 for gopher."
-  (if (and (not (elpher-address-special-p address))
+  (if (and (not (elpher-address-about-p address))
            (eq (url-portspec address) ; (url-port) is too slow!
                (pcase (url-type address)
                  ("gemini" 1965)
            (eq (url-portspec address) ; (url-port) is too slow!
                (pcase (url-type address)
                  ("gemini" 1965)
@@ -345,53 +411,66 @@ requiring gopher-over-TLS."
              "/" (string type)
              selector)))))
 
              "/" (string type)
              selector)))))
 
-(defun elpher-make-special-address (type)
-  "Create an ADDRESS object corresponding to the given special address symbol TYPE."
-  type)
+(defun elpher-make-about-address (type)
+  "Create an ADDRESS object corresponding to the given about address TYPE."
+  (elpher-address-from-url (concat "about:" (symbol-name type))))
 
 (defun elpher-address-to-url (address)
 
 (defun elpher-address-to-url (address)
-  "Get string representation of ADDRESS, or nil if ADDRESS is special."
-  (if (elpher-address-special-p address)
-      nil
-    (url-encode-url (url-recreate-url address))))
+  "Get string representation of ADDRESS."
+  (url-encode-url (url-recreate-url address)))
 
 (defun elpher-address-type (address)
   "Retrieve type of ADDRESS object.
 This is used to determine how to retrieve and render the document the
 address refers to, via the table `elpher-type-map'."
 
 (defun elpher-address-type (address)
   "Retrieve type of ADDRESS object.
 This is used to determine how to retrieve and render the document the
 address refers to, via the table `elpher-type-map'."
-  (if (symbolp address)
-      (list 'special address)
-    (let ((protocol (url-type address)))
-      (cond ((or (equal protocol "gopher")
-                 (equal protocol "gophers"))
-             (list 'gopher
-                   (if (member (url-filename address) '("" "/"))
-                       ?1
-                     (string-to-char (substring (url-filename address) 1)))))
-            ((equal protocol "gemini")
-             'gemini)
-            ((equal protocol "telnet")
-             'telnet)
-            ((equal protocol "finger")
-             'finger)
-            (t 'other-url)))))
+  (pcase (url-type address)
+    ("about"
+     (list 'about (intern (url-filename address))))
+    ((or "gopher" "gophers")
+     (list 'gopher
+           (if (member (url-filename address) '("" "/"))
+               ?1
+             (string-to-char (substring (url-filename address) 1)))))
+    ("gemini" 'gemini)
+    ("telnet" 'telnet)
+    ("finger" 'finger)
+    ("file" 'file)
+    (_ 'other-url)))
+
+(defun elpher-address-about-p (address)
+  "Return non-nil if ADDRESS is an about address."
+  (pcase (elpher-address-type address) (`(about ,_) t)))
+
+(defun elpher-address-gopher-p (address)
+  "Return non-nil if ADDRESS object is a gopher address."
+  (pcase (elpher-address-type address) (`(gopher ,_) t)))
 
 (defun elpher-address-protocol (address)
 
 (defun elpher-address-protocol (address)
-  "Retrieve the transport protocol for ADDRESS.  This is nil for special addresses."
-  (if (symbolp address)
-      nil
-    (url-type address)))
+  "Retrieve the transport protocol for ADDRESS."
+  (url-type address))
 
 (defun elpher-address-filename (address)
   "Retrieve the filename component of ADDRESS.
 For gopher addresses this is a combination of the selector type and selector."
 
 (defun elpher-address-filename (address)
   "Retrieve the filename component of ADDRESS.
 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."
 
 (defun elpher-address-host (address)
   "Retrieve host from ADDRESS object."
-  (url-host address))
+  (pcase (url-host address)
+    ;; The following strips out square brackets which sometimes enclose IPv6
+    ;; addresses.  Doing this here rather than at the parsing stage may seem
+    ;; weird, but this lets us way we avoid having to muck with both URL parsing
+    ;; and reconstruction.  It's also more efficient, as this method is not
+    ;; called during page rendering.
+    ((rx (: "[" (let ipv6 (* (not "]"))) "]"))
+     ipv6)
+    ;; The following is a work-around for a parsing bug that causes
+    ;; URLs with empty (but not absent, see RFC 1738) usernames to have
+    ;; @ prepended to the hostname.
+    ((rx (: "@" (let rest (+ anything))))
+     rest)
+    (addr
+     addr)))
 
 (defun elpher-address-user (address)
   "Retrieve user from ADDRESS object."
 
 (defun elpher-address-user (address)
   "Retrieve user from ADDRESS object."
@@ -399,19 +478,10 @@ For gopher addresses this is a combination of the selector type and selector."
 
 (defun elpher-address-port (address)
   "Retrieve port from ADDRESS object.
 
 (defun elpher-address-port (address)
   "Retrieve port from ADDRESS object.
-If no address is defined, returns 0.  (This is for compatibility with the URL library.)"
-  (if (symbolp address)
-      0
-    (url-port address)))
-
-(defun elpher-address-special-p (address)
-  "Return non-nil if ADDRESS object is special (e.g. start page, bookmarks 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"))))
+If no address is defined, returns 0.  (This is for compatibility with
+the URL library.)"
+  (let ((port (url-portspec address))) ; (url-port) is too slow!
+    (if port port 0)))
 
 (defun elpher-gopher-address-selector (address)
   "Retrieve gopher selector from ADDRESS object."
 
 (defun elpher-gopher-address-selector (address)
   "Retrieve gopher selector from ADDRESS object."
@@ -448,6 +518,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))
 
   "Create a page with DISPLAY-STRING and ADDRESS."
   (list display-string address))
 
+(defun elpher-make-start-page ()
+  "Create the start page."
+  (elpher-make-page "Start Page"
+                    (elpher-address-from-url elpher-start-page-url)))
+
 (defun elpher-page-display-string (page)
   "Retrieve the display string corresponding to PAGE."
   (elt page 0))
 (defun elpher-page-display-string (page)
   "Retrieve the display string corresponding to PAGE."
   (elt page 0))
@@ -460,19 +535,62 @@ 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))
 
   "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
+(defun elpher-page-from-url (url &optional default-scheme)
+  "Create a page with address and display string defined by URL.
+The URL is unhexed prior to its use as a display string to improve
+readability.
+
+If DEFAULT-SCHEME is non-nil, this scheme is applied to the URL
+in the instance that URL itself doesn't specify one."
+  (let ((address (elpher-address-from-url url default-scheme)))
+    (elpher-make-page (elpher-address-to-iri address) address)))
+
+(defun elpher-address-to-iri (address)
+  "Return an IRI for ADDRESS.
+Decode percent-escapes and handle punycode in the domain name.
+Drop the password, if any."
+  (let ((data (match-data)) ; Prevent parsing clobbering match data
+        (host (url-host address))
+        (pass (url-password address)))
+    (unwind-protect
+        (let* ((host (url-host address))
+               (pass (url-password address)))
+          (when host
+            (setf (url-host address) (puny-decode-domain host)))
+          (when pass                            ; RFC 3986 says we should not render
+            (setf (url-password address) nil)) ; the password as clear text
+          (elpher-decode (url-unhex-string (url-recreate-url address))))
+      (setf (url-host address) host)
+      (setf (url-password address) pass)
+      (set-match-data data))))
+
+(defvar elpher-current-page nil
+  "The current page for this Elpher buffer.")
+
+(defvar elpher-history nil
+  "The local history stack for this Elpher buffer.
+This variable is used by `elpher-back' and
+`elpher-show-history'.")
+
+(defvar elpher-visited-pages nil
+  "The global history for all Elpher buffers.
+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.
 
 (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)
   (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))
+  (unless no-history
+    (unless (or (not elpher-current-page)
+                 (equal (elpher-page-address elpher-current-page)
+                        (elpher-page-address page)))
+      (push elpher-current-page elpher-history)
+      (unless (or (elpher-address-about-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))
   (setq-local elpher-current-page page)
   (let* ((address (elpher-page-address page))
          (type (elpher-address-type address))
@@ -493,10 +611,9 @@ unless NO-HISTORY is non-nil."
 
 (defun elpher-visit-previous-page ()
   "Visit the previous page in the history."
 
 (defun elpher-visit-previous-page ()
   "Visit the previous page in the history."
-  (let ((previous-page (pop elpher-history)))
-    (if previous-page
-        (elpher-visit-page previous-page nil t)
-      (error "No previous page"))))
+  (if elpher-history
+      (elpher-visit-page (pop elpher-history) nil t)
+    (error "No previous page")))
 
 (defun elpher-reload-current-page ()
   "Reload the current page, discarding any existing cached content."
 
 (defun elpher-reload-current-page ()
   "Reload the current page, discarding any existing cached content."
@@ -515,6 +632,19 @@ unless NO-HISTORY is non-nil."
         (goto-char pos)
       (goto-char (point-min)))))
 
         (goto-char pos)
       (goto-char (point-min)))))
 
+(defun elpher-get-default-url-scheme ()
+  "Suggest default URL scheme for visiting addresses based on the current page."
+  (if elpher-current-page
+      (let* ((address (elpher-page-address elpher-current-page))
+             (current-type (elpher-address-type address)))
+        (pcase current-type
+          ((or (and 'file (guard (not elpher-history)))
+               `(about ,_))
+           elpher-default-url-type)
+          (_
+           (url-type address))))
+      elpher-default-url-type))
+
 
 ;;; Buffer preparation
 ;;
 
 ;;; Buffer preparation
 ;;
@@ -524,25 +654,30 @@ unless NO-HISTORY is non-nil."
 
 (defun elpher-update-header ()
   "If `elpher-use-header' is true, display current page info in window header."
 
 (defun elpher-update-header ()
   "If `elpher-use-header' is true, display current page info in window header."
-  (if elpher-use-header
+  (if (and elpher-use-header elpher-current-page)
       (let* ((display-string (elpher-page-display-string elpher-current-page))
       (let* ((display-string (elpher-page-display-string elpher-current-page))
+             (sanitized-display-string (replace-regexp-in-string "%" "%%" display-string))
              (address (elpher-page-address elpher-current-page))
              (address (elpher-page-address elpher-current-page))
-             (tls-string (if (and (not (elpher-address-special-p address))
+             (tls-string (if (and (not (elpher-address-about-p address))
                                   (member (elpher-address-protocol address)
                                           '("gophers" "gemini")))
                              " [TLS encryption]"
                            ""))
                                   (member (elpher-address-protocol address)
                                           '("gophers" "gemini")))
                              " [TLS encryption]"
                            ""))
-             (header (concat display-string
+             (header (concat sanitized-display-string
                              (propertize tls-string 'face 'bold))))
         (setq header-line-format header))))
 
 (defmacro elpher-with-clean-buffer (&rest args)
   "Evaluate ARGS with a clean *elpher* buffer as current."
                              (propertize tls-string 'face 'bold))))
         (setq header-line-format header))))
 
 (defmacro elpher-with-clean-buffer (&rest args)
   "Evaluate ARGS with a clean *elpher* buffer as current."
+  (declare (debug (body))) ;; Allow edebug to step through body
   `(with-current-buffer elpher-buffer-name
      (unless (eq major-mode 'elpher-mode)
        ;; avoid resetting buffer-local variables
        (elpher-mode))
   `(with-current-buffer elpher-buffer-name
      (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 (also next 2 lines)
+       (setq-local xterm-color--current-fg nil)
+       (setq-local xterm-color--current-bg nil)
        (setq-local network-security-level
                    (default-value 'network-security-level))
        (erase-buffer)
        (setq-local network-security-level
                    (default-value 'network-security-level))
        (erase-buffer)
@@ -564,6 +699,57 @@ If LINE is non-nil, replace that line instead."
               (replace-match string))
           (set-match-data data))))))
 
               (replace-match string))
           (set-match-data data))))))
 
+;;; Link button definitions
+;;
+
+(defvar elpher-link-keymap
+  (let ((map (make-sparse-keymap)))
+    (keymap-set map "S-<down-mouse-1>" 'ignore) ;Prevent buffer face popup
+    (keymap-set map "S-<mouse-1>" #'elpher--open-link-new-buffer-mouse)
+    (keymap-set map "S-<return>" #'elpher--open-link-new-buffer)
+    (set-keymap-parent map button-map)
+    map))
+
+(defun elpher--click-link (button)
+  "Function called when the gopher link BUTTON is activated."
+  (let ((page (button-get button 'elpher-page)))
+    (elpher-visit-page page)))
+
+(defun elpher--open-link-new-buffer ()
+  "Internal function used by Elpher to open links in a new buffer."
+  (interactive)
+  (let ((page (button-get (button-at (point)) 'elpher-page))
+        (new-buf (generate-new-buffer (default-value 'elpher-buffer-name))))
+    (pop-to-buffer new-buf)
+    (elpher-mode)
+    (elpher-visit-page page)))
+
+(defun elpher--open-link-new-buffer-mouse (event)
+  "Internal function used by Elpher to open links in a new buffer.
+The EVENT argument is the mouse event which caused this function to be
+called."
+  (interactive "e")
+  (mouse-set-point event)
+  (elpher--open-link-new-buffer))
+
+(defun elpher--page-button-help (_window buffer pos)
+  "Function called by Emacs to generate mouse-over text.
+The arguments specify the BUFFER and the POS within the buffer of the item
+for which help is required.  The function returns the help to be
+displayed.  The _WINDOW argument is currently unused."
+  (with-current-buffer buffer
+    (let ((button (button-at pos)))
+      (when button
+        (let* ((page (button-get button 'elpher-page))
+               (address (elpher-page-address page)))
+          (format "mouse-1, RET: open '%s'" (elpher-address-to-url address)))))))
+
+(define-button-type 'elpher-link
+  'action #'elpher--click-link
+  'keymap elpher-link-keymap
+  'follow-link t
+  'help-echo #'elpher--page-button-help
+  'face 'button)
 
 ;;; Text Processing
 ;;
 
 ;;; Text Processing
 ;;
@@ -582,11 +768,67 @@ If LINE is non-nil, replace that line instead."
   "Preprocess text selector response contained in STRING.
 This involes decoding the character representation, and clearing
 away CRs and any terminating period."
   "Preprocess text selector response contained in STRING.
 This involes decoding the character representation, and clearing
 away CRs and any terminating period."
-  (elpher-decode (replace-regexp-in-string "\n\.\n$" "\n"
+  (elpher-decode (replace-regexp-in-string "\n\\.\n$" "\n"
                                            (replace-regexp-in-string "\r" "" string))))
 
                                            (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-page-from-url (substring-no-properties (match-string 0)))))
+        (make-text-button (match-beginning 0)
+                          (match-end 0)
+                          'elpher-page page
+                          :type 'elpher-link)))
+    (buffer-string)))
+
+
+;; ANSI colors or XTerm colors (application and filtering)
+
+(or (require 'xterm-color nil t)
+    (require 'ansi-color))
 
 
-;;; Network error reporting
+(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.")
+
+(defun elpher-text-has-ansi-escapes-p (string)
+  "Return non-nil if STRING includes an ANSI escape code."
+  (save-match-data
+    (string-match "\x1b\\[" string)))
+
+
+;; 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-text-has-ansi-escapes-p string)
+                             (if elpher-filter-ansi-from-text
+                                 (elpher-color-filter-apply string)
+                               (elpher-color-apply string))
+                           string)))
+
+
+;;; General network communication
 ;;
 
 (defun elpher-network-error (address error)
 ;;
 
 (defun elpher-network-error (address error)
@@ -600,9 +842,6 @@ ERROR can be either an error object or a string."
            "Press 'u' to return to the previous page.")))
 
 
            "Press 'u' to return to the previous page.")))
 
 
-;;; General network communication
-;;
-
 (defvar elpher-network-timer nil
   "Timer used for network connections.")
 
 (defvar elpher-network-timer nil
   "Timer used for network connections.")
 
@@ -619,6 +858,15 @@ ERROR can be either an error object or a string."
   (if (timerp elpher-network-timer)
       (cancel-timer elpher-network-timer)))
 
   (if (timerp elpher-network-timer)
       (cancel-timer elpher-network-timer)))
 
+(defun elpher-make-network-timer (thunk)
+  "Create 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.
 (defun elpher-get-host-response (address default-port query-string response-processor
                                          &optional use-tls force-ipv4)
   "Generic function for retrieving data from ADDRESS.
@@ -634,7 +882,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
 
 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)
   (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)
@@ -645,18 +893,22 @@ the host operating system and the local network capabilities."
     (condition-case nil
         (let* ((kill-buffer-query-functions nil)
                (port (elpher-address-port address))
     (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))
                (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)
                (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
                                         ; Try again with IPv4
                                    (lambda ()
                                      (elpher-process-cleanup)
                                      (cond
                                         ; Try again with IPv4
-                                      ((not (or force-ipv4 socks))
+                                      ((not (or elpher-ipv4-always force-ipv4 socks))
                                        (message "Connection timed out.  Retrying with IPv4.")
                                        (elpher-get-host-response address default-port
                                                                  query-string
                                        (message "Connection timed out.  Retrying with IPv4.")
                                        (elpher-get-host-response address default-port
                                                                  query-string
@@ -674,12 +926,13 @@ the host operating system and the local network capabilities."
                                                                  nil force-ipv4))
                                       (t
                                        (elpher-network-error address "Connection time-out."))))))
                                                                  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)
+               (proc (if socks
+                         (socks-open-network-stream "elpher-process" nil host service)
                        (make-network-process :name "elpher-process"
                                              :host host
                        (make-network-process :name "elpher-process"
                                              :host host
-                                             :family (and force-ipv4 'ipv4)
+                                             :family (and (or force-ipv4
+                                                              elpher-ipv4-always)
+                                                          'ipv4)
                                              :service service
                                              :buffer nil
                                              :nowait t
                                              :service service
                                              :buffer nil
                                              :nowait t
@@ -688,6 +941,7 @@ the host operating system and the local network capabilities."
                                                   (cons 'gnutls-x509pki
                                                         (apply #'gnutls-boot-parameters
                                                                gnutls-params)))))))
                                                   (cons 'gnutls-x509pki
                                                         (apply #'gnutls-boot-parameters
                                                                gnutls-params)))))))
+          (process-put proc 'elpher-buffer (current-buffer))
           (setq elpher-network-timer timer)
           (set-process-coding-system proc 'binary 'binary)
           (set-process-query-on-exit-flag proc nil)
           (setq elpher-network-timer timer)
           (set-process-coding-system proc 'binary 'binary)
           (set-process-query-on-exit-flag proc nil)
@@ -703,10 +957,10 @@ the host operating system and the local network capabilities."
                                   (when (> new-hkbytes-received hkbytes-received)
                                     (setq hkbytes-received new-hkbytes-received)
                                     (elpher-buffer-message
                                   (when (> new-hkbytes-received hkbytes-received)
                                     (setq hkbytes-received new-hkbytes-received)
                                     (elpher-buffer-message
-                                        (concat "("
-                                                (number-to-string (/ hkbytes-received 10.0))
-                                                " MB read)")
-                                        1)))
+                                     (concat "("
+                                             (number-to-string (/ hkbytes-received 10.0))
+                                             " MB read)")
+                                     1)))
                                 (setq response-string-parts
                                       (cons string response-string-parts))))
           (set-process-sentinel proc
                                 (setq response-string-parts
                                       (cons string response-string-parts))))
           (set-process-sentinel proc
@@ -731,26 +985,30 @@ the host operating system and the local network capabilities."
                                                                   response-processor
                                                                   use-tls t))
                                        (response-string-parts
                                                                   response-processor
                                                                   use-tls t))
                                        (response-string-parts
-                                        (elpher-with-clean-buffer
-                                         (insert "Data received.  Rendering..."))
-                                        (funcall response-processor
-                                                 (apply #'concat (reverse response-string-parts)))
-                                        (elpher-restore-pos))
+                                        (with-current-buffer (process-get proc 'elpher-buffer)
+                                          (elpher-with-clean-buffer
+                                           (insert "Data received.  Rendering..."))
+                                          (funcall response-processor
+                                                   (apply #'concat (reverse response-string-parts)))
+                                          (elpher-restore-pos)))
                                        (t
                                         (error "No response from server")))
                                     (error
                                      (elpher-network-error address the-error)))))
           (when socks
                                        (t
                                         (error "No response from server")))
                                     (error
                                      (elpher-network-error address the-error)))))
           (when socks
-            (if use-tls (apply #'gnutls-negotiate :process proc gnutls-params))
+            (if use-tls
+                (apply #'gnutls-negotiate :process proc gnutls-params))
             (funcall (process-sentinel proc) proc "open\n")))
       (error
             (funcall (process-sentinel proc) proc "open\n")))
       (error
+       (elpher-process-cleanup)
        (error "Error initiating connection to server")))))
 
 
 ;;; Client-side TLS Certificate Management
 ;;
 
        (error "Error initiating connection to server")))))
 
 
 ;;; Client-side TLS Certificate Management
 ;;
 
-(defun elpher-generate-certificate (common-name key-file cert-file &optional temporary)
+(defun elpher-generate-certificate (common-name key-file cert-file url-prefix
+                                                &optional temporary)
   "Generate a key and a self-signed client TLS certificate using openssl.
 
 The Common Name field of the certificate is set to COMMON-NAME.  The
   "Generate a key and a self-signed client TLS certificate using openssl.
 
 The Common Name field of the certificate is set to COMMON-NAME.  The
@@ -764,7 +1022,8 @@ when the certificate is no longer needed for the current session.
 Otherwise, the certificate will be given a 100 year expiration period
 and the files will not be deleted.
 
 Otherwise, the certificate will be given a 100 year expiration period
 and the files will not be deleted.
 
-The function returns a list containing the current host name, the
+The function returns a list containing the URL-PREFIX of addresses
+for which the certificate should be used in this session, the
 temporary flag, and the key and cert file names in the form required
 by `gnutls-boot-parameters`."
   (let ((exp-key-file (expand-file-name key-file))
 temporary flag, and the key and cert file names in the form required
 by `gnutls-boot-parameters`."
   (let ((exp-key-file (expand-file-name key-file))
@@ -778,56 +1037,70 @@ by `gnutls-boot-parameters`."
                         "-subj" (concat "/CN=" common-name)
                         "-keyout" exp-key-file
                         "-out" exp-cert-file)
                         "-subj" (concat "/CN=" common-name)
                         "-keyout" exp-key-file
                         "-out" exp-cert-file)
-          (list (elpher-address-host (elpher-page-address elpher-current-page))
-                temporary exp-key-file exp-cert-file))
+          (list url-prefix temporary exp-key-file exp-cert-file))
       (error
        (message "Check that openssl is installed, or customize `elpher-openssl-command`.")
        (error "Program 'openssl', required for certificate generation, not found")))))
 
       (error
        (message "Check that openssl is installed, or customize `elpher-openssl-command`.")
        (error "Program 'openssl', required for certificate generation, not found")))))
 
-(defun elpher-generate-throwaway-certificate ()
+(defun elpher-generate-throwaway-certificate (url-prefix)
   "Generate and return details of a throwaway certificate.
 The key and certificate files will be deleted when they are no
   "Generate and return details of a throwaway certificate.
 The key and certificate files will be deleted when they are no
-longer needed for this session."
+longer needed for this session.
+
+The certificate will be marked as applying to all addresses with URLs
+starting with URL-PREFIX."
   (let* ((file-base (make-temp-name "elpher"))
          (key-file (concat temporary-file-directory file-base ".key"))
          (cert-file (concat temporary-file-directory file-base ".crt")))
   (let* ((file-base (make-temp-name "elpher"))
          (key-file (concat temporary-file-directory file-base ".key"))
          (cert-file (concat temporary-file-directory file-base ".crt")))
-    (elpher-generate-certificate file-base key-file cert-file t)))
+    (elpher-generate-certificate file-base key-file cert-file url-prefix t)))
 
 
-(defun elpher-generate-persistent-certificate (file-base common-name)
+(defun elpher-generate-persistent-certificate (file-base common-name url-prefix)
   "Generate and return details of a persistent certificate.
 The argument FILE-BASE is used as the base for the key and certificate
 files, while COMMON-NAME specifies the common name field of the
 certificate.
 
   "Generate and return details of a persistent certificate.
 The argument FILE-BASE is used as the base for the key and certificate
 files, while COMMON-NAME specifies the common name field of the
 certificate.
 
-The key and certificate files are written to in `elpher-certificate-directory'."
+The key and certificate files are written to in `elpher-certificate-directory'.
+
+In this session, the certificate will remain active for all addresses
+having URLs starting with URL-PREFIX."
   (let* ((key-file (concat elpher-certificate-directory file-base ".key"))
          (cert-file (concat elpher-certificate-directory file-base ".crt")))
   (let* ((key-file (concat elpher-certificate-directory file-base ".key"))
          (cert-file (concat elpher-certificate-directory file-base ".crt")))
-    (elpher-generate-certificate common-name key-file cert-file)))
+    (elpher-generate-certificate common-name key-file cert-file url-prefix)))
 
 
-(defun elpher-get-existing-certificate (file-base)
+(defun elpher-get-existing-certificate (file-base url-prefix)
   "Return a certificate object corresponding to an existing certificate.
 It is assumed that the key files FILE-BASE.key and FILE-BASE.crt exist in
   "Return a certificate object corresponding to an existing certificate.
 It is assumed that the key files FILE-BASE.key and FILE-BASE.crt exist in
-the directory `elpher-certificate-directory'."
+the directory `elpher-certificate-directory'.
+
+In this session, the certificate will remain active for all addresses
+having URLs starting with URL-PREFIX."
   (let* ((key-file (concat elpher-certificate-directory file-base ".key"))
          (cert-file (concat elpher-certificate-directory file-base ".crt")))
   (let* ((key-file (concat elpher-certificate-directory file-base ".key"))
          (cert-file (concat elpher-certificate-directory file-base ".crt")))
-    (list (elpher-address-host (elpher-page-address elpher-current-page))
+    (list url-prefix
           nil
           (expand-file-name key-file)
           (expand-file-name cert-file))))
 
           nil
           (expand-file-name key-file)
           (expand-file-name cert-file))))
 
-(defun elpher-install-and-use-existing-certificate (key-file-src cert-file-src file-base)
+(defun elpher-install-certificate (key-file-src cert-file-src file-base url-prefix)
   "Install a key+certificate file pair in `elpher-certificate-directory'.
 The strings KEY-FILE-SRC and CERT-FILE-SRC are the existing key and
 certificate files to install.  The argument FILE-BASE is used as the
   "Install a key+certificate file pair in `elpher-certificate-directory'.
 The strings KEY-FILE-SRC and CERT-FILE-SRC are the existing key and
 certificate files to install.  The argument FILE-BASE is used as the
-base for the installed key and certificate files."
+base for the installed key and certificate files.
+
+In this session, the certificate will remain active for all addresses
+having URLs starting with URL-PREFIX."
   (let* ((key-file (concat elpher-certificate-directory file-base ".key"))
          (cert-file (concat elpher-certificate-directory file-base ".crt")))
     (if (or (file-exists-p key-file)
             (file-exists-p cert-file))
         (error "A certificate with base name %s is already installed" file-base))
   (let* ((key-file (concat elpher-certificate-directory file-base ".key"))
          (cert-file (concat elpher-certificate-directory file-base ".crt")))
     (if (or (file-exists-p key-file)
             (file-exists-p cert-file))
         (error "A certificate with base name %s is already installed" file-base))
+    (unless (and (file-exists-p key-file-src)
+                 (file-exists-p cert-file-src))
+      (error "Either of the key or certificate files do not exist"))
     (copy-file key-file-src key-file)
     (copy-file cert-file-src cert-file)
     (copy-file key-file-src key-file)
     (copy-file cert-file-src cert-file)
-    (list (elpher-address-host (elpher-page-address elpher-current-page))
+    (list url-prefix
           nil
           (expand-file-name key-file)
           (expand-file-name cert-file))))
           nil
           (expand-file-name key-file)
           (expand-file-name cert-file))))
@@ -853,7 +1126,7 @@ are also deleted."
       (when (cadr elpher-client-certificate)
         (delete-file (elt elpher-client-certificate 2))
         (delete-file (elt elpher-client-certificate 3)))
       (when (cadr elpher-client-certificate)
         (delete-file (elt elpher-client-certificate 2))
         (delete-file (elt elpher-client-certificate 3)))
-      (setq elpher-client-certificate nil)
+      (setq-local elpher-client-certificate nil)
       (if (called-interactively-p 'any)
           (message "Client certificate forgotten.")))))
 
       (if (called-interactively-p 'any)
           (message "Client certificate forgotten.")))))
 
@@ -861,14 +1134,14 @@ are also deleted."
   "Retrieve the `gnutls-boot-parameters'-compatable keylist.
 
 This is obtained from the client certificate described by
   "Retrieve the `gnutls-boot-parameters'-compatable keylist.
 
 This is obtained from the client certificate described by
-`elpher-current-certificate', if one is available and the host for
-that certificate matches the host in ADDRESS.
+`elpher-current-certificate', if one is available and the
+URL prefix for that certificate matches ADDRESS.
 
 
-If `elpher-current-certificate' is non-nil, and its host name doesn't
+If `elpher-current-certificate' is non-nil, and its URL prefix doesn't
 match that of ADDRESS, the certificate is forgotten."
   (if elpher-client-certificate
 match that of ADDRESS, the certificate is forgotten."
   (if elpher-client-certificate
-      (if (string= (car elpher-client-certificate)
-                   (elpher-address-host address))
+      (if (string-prefix-p (car elpher-client-certificate)
+                           (elpher-address-to-url address))
           (list (cddr elpher-client-certificate))
         (elpher-forget-current-certificate)
         (message "Disabling client certificate for new host")
           (list (cddr elpher-client-certificate))
         (elpher-forget-current-certificate)
         (message "Disabling client certificate for new host")
@@ -904,29 +1177,12 @@ once they are retrieved from the gopher server."
         (error
          (elpher-network-error address the-error))))))
 
         (error
          (elpher-network-error address the-error))))))
 
-;; Index rendering
-
-(defun elpher-insert-index (string)
-  "Insert the index corresponding to STRING into the current buffer."
-  ;; Should be able to split directly on CRLF, but some non-conformant
-  ;; LF-only servers sadly exist, hence the following.
-  (let ((str-processed (elpher-preprocess-text-response string)))
-    (dolist (line (split-string str-processed "\n"))
-      (ignore-errors
-        (unless (= (length line) 0)
-          (let* ((type (elt line 0))
-                 (fields (split-string (substring line 1) "\t"))
-                 (display-string (elt fields 0))
-                 (selector (elt fields 1))
-                 (host (elt fields 2))
-                 (port (if (elt fields 3)
-                           (string-to-number (elt fields 3))
-                         nil))
-                 (address (elpher-make-gopher-address type selector host port)))
-            (elpher-insert-index-record display-string address)))))))
+
+;;; Gopher index rendering
+;;
 
 (defun elpher-insert-margin (&optional type-name)
 
 (defun elpher-insert-margin (&optional type-name)
-  "Insert index margin, optionally containing the TYPE-NAME, into the current buffer."
+  "Insert index margin, optionally containing the TYPE-NAME, into current buffer."
   (if type-name
       (progn
         (insert (format (concat "%" (number-to-string (- elpher-margin-width 1)) "s")
   (if type-name
       (progn
         (insert (format (concat "%" (number-to-string (- elpher-margin-width 1)) "s")
@@ -937,39 +1193,23 @@ once they are retrieved from the gopher server."
         (insert " "))
     (insert (make-string elpher-margin-width ?\s))))
 
         (insert " "))
     (insert (make-string elpher-margin-width ?\s))))
 
-(defun elpher--page-button-help (_window buffer pos)
-  "Function called by Emacs to generate mouse-over text.
-The arguments specify the BUFFER and the POS within the buffer of the item
-for which help is required.  The function returns the help to be
-displayed.  The _WINDOW argument is currently unused."
-  (with-current-buffer buffer
-    (let ((button (button-at pos)))
-      (when button
-        (let* ((page (button-get button 'elpher-page))
-               (address (elpher-page-address page)))
-          (format "mouse-1, RET: open '%s'" (if (elpher-address-special-p address)
-                                                address
-                                              (elpher-address-to-url address))))))))
-
 (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.
 If ADDRESS is not supplied or nil the record is rendered as an
 (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.
 If ADDRESS is not supplied or nil the record is rendered as an
-'information' line."
+`information' line."
   (let* ((type (if address (elpher-address-type address) nil))
          (type-map-entry (cdr (assoc type elpher-type-map))))
     (if type-map-entry
         (let* ((margin-code (elt type-map-entry 2))
                (face (elt type-map-entry 3))
   (let* ((type (if address (elpher-address-type address) nil))
          (type-map-entry (cdr (assoc type elpher-type-map))))
     (if type-map-entry
         (let* ((margin-code (elt type-map-entry 2))
                (face (elt type-map-entry 3))
-               (filtered-display-string (ansi-color-filter-apply display-string))
+               (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
                (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))
+                              :type 'elpher-link))
       (pcase type
         ('nil ;; Information
          (elpher-insert-margin)
       (pcase type
         ('nil ;; Information
          (elpher-insert-margin)
@@ -982,52 +1222,31 @@ If ADDRESS is not supplied or nil the record is rendered as an
                              'face 'elpher-unknown)))))
     (insert "\n")))
 
                              'face 'elpher-unknown)))))
     (insert "\n")))
 
-(defun elpher-click-link (button)
-  "Function called when the gopher link BUTTON is activated (via mouse or keypress)."
-  (let ((page (button-get button 'elpher-page)))
-    (elpher-visit-page page)))
-
 (defun elpher-render-index (data &optional _mime-type-string)
   "Render DATA as an index.  MIME-TYPE-STRING is unused."
   (elpher-with-clean-buffer
    (if (not data)
        t
 (defun elpher-render-index (data &optional _mime-type-string)
   "Render DATA as an index.  MIME-TYPE-STRING is unused."
   (elpher-with-clean-buffer
    (if (not data)
        t
-     (elpher-insert-index data)
+     (let ((data-processed (elpher-preprocess-text-response data)))
+       (dolist (line (split-string data-processed "\n"))
+         (ignore-errors
+           (unless (= (length line) 0)
+             (let* ((type (elt line 0))
+                    (fields (split-string (substring line 1) "\t"))
+                    (display-string (elt fields 0))
+                    (selector (elt fields 1))
+                    (host (elt fields 2))
+                    (port (if (elt fields 3)
+                              (string-to-number (elt fields 3))
+                            nil))
+                    (address (elpher-make-gopher-address type selector host port)))
+               (elpher-insert-index-record display-string address))))))
      (elpher-cache-content (elpher-page-address elpher-current-page)
                            (buffer-string)))))
 
      (elpher-cache-content (elpher-page-address elpher-current-page)
                            (buffer-string)))))
 
-;; 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"
-  "Wildly 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
-                             (ansi-color-filter-apply string)
-                           (ansi-color-apply string))))
+;;; Gopher text rendering
+;;
 
 (defun elpher-render-text (data &optional _mime-type-string)
   "Render DATA as text.  MIME-TYPE-STRING is unused."
 
 (defun elpher-render-text (data &optional _mime-type-string)
   "Render DATA as text.  MIME-TYPE-STRING is unused."
@@ -1039,30 +1258,39 @@ Currently includes buttonifying URLs and processing ANSI escape codes."
       (elpher-page-address elpher-current-page)
       (buffer-string)))))
 
       (elpher-page-address elpher-current-page)
       (buffer-string)))))
 
-;; Image retrieval
+
+;;; Image retrieval
+;;
 
 (defun elpher-render-image (data &optional _mime-type-string)
   "Display DATA as image.  MIME-TYPE-STRING is unused."
   (if (not data)
       nil
     (if (display-images-p)
 
 (defun elpher-render-image (data &optional _mime-type-string)
   "Display DATA as image.  MIME-TYPE-STRING is unused."
   (if (not data)
       nil
     (if (display-images-p)
-        (progn
-          (let ((image (create-image
-                        data
-                        nil t)))
+        (let* ((image (create-image
+                       data
+                       nil t)))
+          (if (not image)
+              (error "Unsupported image format")
+            (let ((window (get-buffer-window elpher-buffer-name)))
+              (when window
+                (setf (image-property image :max-width) (window-body-width window t))
+                (setf (image-property image :max-height) (window-body-height window t))))
             (elpher-with-clean-buffer
              (insert-image image)
              (elpher-restore-pos))))
       (elpher-render-download data))))
 
             (elpher-with-clean-buffer
              (insert-image image)
              (elpher-restore-pos))))
       (elpher-render-download data))))
 
-;; Search retrieval and rendering
+
+;;; Gopher search retrieval and rendering
+;;
 
 (defun elpher-get-gopher-query-page (renderer)
   "Getter for gopher addresses requiring input.
 The response is rendered using the rendering function RENDERER."
 
 (defun elpher-get-gopher-query-page (renderer)
   "Getter for gopher addresses requiring input.
 The response is rendered using the rendering function RENDERER."
-   (let* ((address (elpher-page-address elpher-current-page))
-          (content (elpher-get-cached-content address))
-          (aborted t))
+  (let* ((address (elpher-page-address elpher-current-page))
+         (content (elpher-get-cached-content address))
+         (aborted t))
     (if (and content (funcall renderer nil))
         (elpher-with-clean-buffer
          (insert content)
     (if (and content (funcall renderer nil))
         (elpher-with-clean-buffer
          (insert content)
@@ -1084,7 +1312,9 @@ The response is rendered using the rendering function RENDERER."
         (if aborted
             (elpher-visit-previous-page))))))
 
         (if aborted
             (elpher-visit-previous-page))))))
 
-;; Raw server response rendering
+
+;;; Raw server response rendering
+;;
 
 (defun elpher-render-raw (data &optional mime-type-string)
   "Display raw DATA in buffer.  MIME-TYPE-STRING is also displayed if provided."
 
 (defun elpher-render-raw (data &optional mime-type-string)
   "Display raw DATA in buffer.  MIME-TYPE-STRING is also displayed if provided."
@@ -1097,7 +1327,9 @@ The response is rendered using the rendering function RENDERER."
      (goto-char (point-min)))
     (message "Displaying raw server response.  Reload or redraw to return to standard view.")))
 
      (goto-char (point-min)))
     (message "Displaying raw server response.  Reload or redraw to return to standard view.")))
 
-;; File save "rendering"
+
+;;; File save "rendering"
+;;
 
 (defun elpher-render-download (data &optional _mime-type-string)
   "Save DATA to file.  MIME-TYPE-STRING is unused."
 
 (defun elpher-render-download (data &optional _mime-type-string)
   "Save DATA to file.  MIME-TYPE-STRING is unused."
@@ -1119,7 +1351,9 @@ The response is rendered using the rendering function RENDERER."
             (insert data)))
         (message (format "Saved to file %s." filename))))))
 
             (insert data)))
         (message (format "Saved to file %s." filename))))))
 
-;; HTML rendering
+
+;;; HTML rendering
+;;
 
 (defun elpher-render-html (data &optional _mime-type-string)
   "Render DATA as HTML using shr.  MIME-TYPE-STRING is unused."
 
 (defun elpher-render-html (data &optional _mime-type-string)
   "Render DATA as HTML using shr.  MIME-TYPE-STRING is unused."
@@ -1131,7 +1365,9 @@ The response is rendered using the rendering function RENDERER."
                   (libxml-parse-html-region (point-min) (point-max)))))
        (shr-insert-document dom)))))
 
                   (libxml-parse-html-region (point-min) (point-max)))))
        (shr-insert-document dom)))))
 
-;; Gemini page retrieval
+
+;;; Gemini page retrieval
+;;
 
 (defvar elpher-gemini-redirect-chain)
 
 
 (defvar elpher-gemini-redirect-chain)
 
@@ -1169,14 +1405,17 @@ that the response was malformed."
          (elpher-with-clean-buffer
           (insert "Gemini server is requesting input."))
          (let* ((query-string
          (elpher-with-clean-buffer
           (insert "Gemini server is requesting input."))
          (let* ((query-string
-                 (if (eq (elt response-code 1) ?1)
-                     (read-passwd (concat response-meta ": "))
-                   (read-string (concat response-meta ": "))))
+                 (with-local-quit
+                   (if (eq (elt response-code 1) ?1)
+                       (read-passwd (concat response-meta ": "))
+                     (read-string (concat response-meta ": ")))))
                 (query-address (seq-copy (elpher-page-address elpher-current-page)))
                 (old-fname (url-filename query-address)))
                 (query-address (seq-copy (elpher-page-address elpher-current-page)))
                 (old-fname (url-filename query-address)))
-           (setf (url-filename query-address)
-                 (concat old-fname "?" (url-build-query-string `((,query-string)))))
-           (elpher-get-gemini-response query-address renderer)))
+           (if (not query-string)
+               (elpher-visit-previous-page)
+             (setf (url-filename query-address)
+                   (concat old-fname "?" (url-build-query-string `((,query-string)))))
+             (elpher-get-gemini-response query-address renderer))))
         (?2 ; Normal response
          (funcall renderer response-body response-meta))
         (?3 ; Redirect
         (?2 ; Normal response
          (funcall renderer response-body response-meta))
         (?3 ; Redirect
@@ -1186,8 +1425,7 @@ that the response was malformed."
          (let ((redirect-address (elpher-address-from-gemini-url response-meta)))
            (if (member redirect-address elpher-gemini-redirect-chain)
                (error "Redirect loop detected"))
          (let ((redirect-address (elpher-address-from-gemini-url response-meta)))
            (if (member redirect-address elpher-gemini-redirect-chain)
                (error "Redirect loop detected"))
-           (if (not (string= (elpher-address-protocol redirect-address)
-                             "gemini"))
+           (if (not (eq (elpher-address-type redirect-address) 'gemini))
                (error "Server tried to automatically redirect to non-gemini URL: %s"
                       response-meta))
            (elpher-page-set-address elpher-current-page redirect-address)
                (error "Server tried to automatically redirect to non-gemini URL: %s"
                       response-meta))
            (elpher-page-set-address elpher-current-page redirect-address)
@@ -1206,18 +1444,55 @@ that the response was malformed."
             (insert "Gemini server is requesting a valid TLS certificate:\n\n"))
           (auto-fill-mode 1)
           (elpher-gemini-insert-text response-meta))
             (insert "Gemini server is requesting a valid TLS certificate:\n\n"))
           (auto-fill-mode 1)
           (elpher-gemini-insert-text response-meta))
-         (let ((chosen-certificate (elpher-choose-client-certificate)))
+         (let ((chosen-certificate
+                (with-local-quit
+                  (elpher-acquire-client-certificate
+                   (elpher-address-to-url (elpher-page-address elpher-current-page))))))
            (unless chosen-certificate
              (error "Gemini server requires a client certificate and none was provided"))
            (unless chosen-certificate
              (error "Gemini server requires a client certificate and none was provided"))
-           (setq elpher-client-certificate chosen-certificate))
+           (setq-local elpher-client-certificate chosen-certificate))
          (elpher-with-clean-buffer)
          (elpher-get-gemini-response (elpher-page-address elpher-current-page) renderer))
         (_other
          (error "Gemini server response unknown: %s %s"
                 response-code response-meta))))))
 
          (elpher-with-clean-buffer)
          (elpher-get-gemini-response (elpher-page-address elpher-current-page) renderer))
         (_other
          (error "Gemini server response unknown: %s %s"
                 response-code response-meta))))))
 
-(defun elpher-choose-client-certificate ()
-  "Prompt for a client certificate to use to establish a TLS connection."
+(defun elpher-acquire-client-certificate (url-prefix)
+  "Select a pre-defined client certificate or prompt for one.
+In this case, \"pre-defined\" means a certificate provided by
+the `elpher-certificate-map' variable.
+
+For this session, the certificate will remain active for all addresses
+having URLs begining with URL-PREFIX."
+  (let ((entry (assoc url-prefix
+                      elpher-certificate-map
+                      #'string-prefix-p)))
+    (if entry
+        (let ((cert-url-prefix (car entry))
+              (cert-name (cadr entry)))
+          (message "Using certificate \"%s\" specified in elpher-certificate-map with prefix \"%s\""
+                   cert-name cert-url-prefix)
+          (elpher-get-existing-certificate cert-name cert-url-prefix))
+      (elpher-prompt-for-client-certificate url-prefix))))
+
+(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, or an alist whose keys
+are the possible answers."
+    (completing-read question answers))
+
+(if (fboundp 'read-answer)
+    (defalias 'elpher-read-answer 'read-answer)
+  (defalias 'elpher-read-answer 'elpher--read-answer-polyfill))
+
+
+
+(defun elpher-prompt-for-client-certificate (url-prefix)
+  "Prompt for a client certificate to use to establish a TLS connection.
+
+In this session, the chosen certificate will remain active for all
+addresses with URLs matching URL-PREFIX."
   (let* ((read-answer-short t))
     (pcase (read-answer "What do you want to do? "
                         '(("throwaway" ?t
   (let* ((read-answer-short t))
     (pcase (read-answer "What do you want to do? "
                         '(("throwaway" ?t
@@ -1227,7 +1502,7 @@ that the response was malformed."
                           ("abort" ?a
                            "stop immediately")))
       ("throwaway"
                           ("abort" ?a
                            "stop immediately")))
       ("throwaway"
-       (setq elpher-client-certificate (elpher-generate-throwaway-certificate)))
+       (setq-local elpher-client-certificate (elpher-generate-throwaway-certificate url-prefix)))
       ("persistent"
        (let* ((existing-certificates (elpher-list-existing-certificates))
               (file-base (completing-read
       ("persistent"
        (let* ((existing-certificates (elpher-list-existing-certificates))
               (file-base (completing-read
@@ -1236,8 +1511,8 @@ that the response was malformed."
          (if (string-empty-p (string-trim file-base))
              nil
            (if (member file-base existing-certificates)
          (if (string-empty-p (string-trim file-base))
              nil
            (if (member file-base existing-certificates)
-               (setq elpher-client-certificate
-                     (elpher-get-existing-certificate file-base))
+               (setq-local elpher-client-certificate
+                     (elpher-get-existing-certificate file-base url-prefix))
              (pcase (read-answer "Generate new certificate or install externally-generated one? "
                                  '(("new" ?n
                                     "generate new certificate")
              (pcase (read-answer "Generate new certificate or install externally-generated one? "
                                  '(("new" ?n
                                     "generate new certificate")
@@ -1250,15 +1525,16 @@ that the response was malformed."
                                                 file-base)))
                   (message "New key and self-signed certificate written to %s"
                            elpher-certificate-directory)
                                                 file-base)))
                   (message "New key and self-signed certificate written to %s"
                            elpher-certificate-directory)
-                  (elpher-generate-persistent-certificate file-base common-name)))
+                  (elpher-generate-persistent-certificate file-base
+                                                          common-name
+                                                          url-prefix)))
                ("install"
                 (let* ((cert-file (read-file-name "Certificate file: " nil nil t))
                        (key-file (read-file-name "Key file: " nil nil t)))
                   (message "Key and certificate installed in %s for future use"
                            elpher-certificate-directory)
                ("install"
                 (let* ((cert-file (read-file-name "Certificate file: " nil nil t))
                        (key-file (read-file-name "Key file: " nil nil t)))
                   (message "Key and certificate installed in %s for future use"
                            elpher-certificate-directory)
-                  (elpher-install-and-use-existing-certificate key-file
-                                                               cert-file
-                                                               file-base)))
+                  (elpher-install-certificate key-file cert-file file-base
+                                              url-prefix)))
                ("abort" nil))))))
       ("abort" nil))))
 
                ("abort" nil))))))
       ("abort" nil))))
 
@@ -1269,8 +1545,8 @@ that the response was malformed."
     (condition-case the-error
         (if (and content (funcall renderer nil))
             (elpher-with-clean-buffer
     (condition-case the-error
         (if (and content (funcall renderer nil))
             (elpher-with-clean-buffer
-              (insert content)
-              (elpher-restore-pos))
+             (insert content)
+             (elpher-restore-pos))
           (elpher-with-clean-buffer
            (insert "LOADING GEMINI... (use 'u' to cancel)\n"))
           (setq elpher-gemini-redirect-chain nil)
           (elpher-with-clean-buffer
            (insert "LOADING GEMINI... (use 'u' to cancel)\n"))
           (setq elpher-gemini-redirect-chain nil)
@@ -1278,6 +1554,9 @@ that the response was malformed."
       (error
        (elpher-network-error address the-error)))))
 
       (error
        (elpher-network-error address the-error)))))
 
+;;; Gemini page rendering
+;;
+
 (defun elpher-render-gemini (body &optional mime-type-string)
   "Render gemini response BODY with rendering MIME-TYPE-STRING."
   (if (not body)
 (defun elpher-render-gemini (body &optional mime-type-string)
   "Render gemini response BODY with rendering MIME-TYPE-STRING."
   (if (not body)
@@ -1323,25 +1602,27 @@ Returns nil in the event that the contents of the line following the
 
 (defun elpher-gemini-get-link-display-string (link-line)
   "Extract the display string portion of LINK-LINE, a gemini map file link line.
 
 (defun elpher-gemini-get-link-display-string (link-line)
   "Extract the display string portion of LINK-LINE, a gemini map file link line.
-Returns the url portion in the event that the display-string portion is empty."
+Return nil if this portion is not provided."
   (let* ((rest (string-trim (elt (split-string link-line "=>") 1)))
          (idx (string-match "[ \t]" rest)))
   (let* ((rest (string-trim (elt (split-string link-line "=>") 1)))
          (idx (string-match "[ \t]" rest)))
-    (string-trim (if idx
-                     (substring rest (+ idx 1))
-                   rest))))
+    (and idx
+         (elpher-color-filter-apply (string-trim (substring rest (+ idx 1)))))))
 
 (defun elpher-collapse-dot-sequences (filename)
 
 (defun elpher-collapse-dot-sequences (filename)
-  "Collapse dot sequences in FILENAME.
-For instance, the filename /a/b/../c/./d will reduce to /a/c/d"
-  (let* ((path (split-string filename "/"))
+  "Collapse dot sequences in the (absolute) FILENAME.
+For instance, the filename \"/a/b/../c/./d\" will reduce to \"/a/c/d\""
+  (let* ((path (split-string filename "/" t))
+         (is-directory (string-match-p (rx (: (or "." ".." "/") line-end)) filename))
          (path-reversed-normalized
           (seq-reduce (lambda (a b)
          (path-reversed-normalized
           (seq-reduce (lambda (a b)
-                        (cond ((and a (equal b "..") (cdr a)))
-                              ((and (not a) (equal b "..")) a) ;leading .. are dropped
+                        (cond ((equal b "..") (cdr a))
                               ((equal b ".") a)
                               (t (cons b a))))
                               ((equal b ".") a)
                               (t (cons b a))))
-                      path nil)))
-    (string-join (reverse path-reversed-normalized) "/")))
+                      path nil))
+         (path-normalized (reverse path-reversed-normalized)))
+    (if path-normalized
+        (concat "/" (string-join path-normalized "/") (and is-directory "/"))
+      "/")))
 
 (defun elpher-address-from-gemini-url (url)
   "Extract address from URL with defaults as per gemini map files.
 
 (defun elpher-address-from-gemini-url (url)
   "Extract address from URL with defaults as per gemini map files.
@@ -1351,18 +1632,26 @@ treatment that a separate function is warranted."
   (let ((address (url-generic-parse-url url))
         (current-address (elpher-page-address elpher-current-page)))
     (unless (and (url-type address) (not (url-fullness address))) ;avoid mangling mailto: urls
   (let ((address (url-generic-parse-url url))
         (current-address (elpher-page-address elpher-current-page)))
     (unless (and (url-type address) (not (url-fullness address))) ;avoid mangling mailto: urls
-      (setf (url-fullness address) t)
       (if (url-host address) ;if there is an explicit host, filenames are absolute
           (if (string-empty-p (url-filename address))
               (setf (url-filename address) "/")) ;ensure empty filename is marked as absolute
         (setf (url-host address) (url-host current-address))
       (if (url-host address) ;if there is an explicit host, filenames are absolute
           (if (string-empty-p (url-filename address))
               (setf (url-filename address) "/")) ;ensure empty filename is marked as absolute
         (setf (url-host address) (url-host current-address))
-        (setf (url-port address) (url-port current-address))
-        (unless (string-prefix-p "/" (url-filename address)) ;deal with relative links
+        (setf (url-fullness address) (url-host address)) ;set fullness to t if host is set
+        (setf (url-portspec address) (url-portspec current-address)) ;(url-port) too slow!
+        (cond
+         ((string-prefix-p "/" (url-filename address))) ;do nothing for absolute case
+         ((string-prefix-p "?" (url-filename address)) ;handle query-only links
+          (setf (url-filename address)
+                (concat (url-filename current-address)
+                        (url-filename address))))
+         (t ;deal with relative links
           (setf (url-filename address)
                 (concat (file-name-directory (url-filename current-address))
           (setf (url-filename address)
                 (concat (file-name-directory (url-filename current-address))
-                        (url-filename address)))))
+                        (url-filename address))))))
+      (when (url-host address)
+        (setf (url-host address) (puny-encode-domain (url-host address))))
       (unless (url-type address)
       (unless (url-type address)
-        (setf (url-type address) "gemini"))
+        (setf (url-type address) (url-type current-address)))
       (when (equal (url-type address) "gemini")
         (setf (url-filename address)
               (elpher-collapse-dot-sequences (url-filename address)))))
       (when (equal (url-type address) "gemini")
         (setf (url-filename address)
               (elpher-collapse-dot-sequences (url-filename address)))))
@@ -1370,25 +1659,25 @@ treatment that a separate function is warranted."
 
 (defun elpher-gemini-insert-link (link-line)
   "Insert link described by LINK-LINE into a text/gemini document."
 
 (defun elpher-gemini-insert-link (link-line)
   "Insert link described by LINK-LINE into a text/gemini document."
-  (let* ((url (elpher-gemini-get-link-url link-line))
-         (display-string (elpher-gemini-get-link-display-string link-line))
-         (address (elpher-address-from-gemini-url url))
-         (type (if address (elpher-address-type address) nil))
-         (type-map-entry (cdr (assoc type elpher-type-map))))
-    (when display-string
-      (insert elpher-gemini-link-string)
-      (if type-map-entry
+  (let ((url (elpher-gemini-get-link-url link-line)))
+    (when url
+      (let* ((given-display-string (elpher-gemini-get-link-display-string link-line))
+             (address (elpher-address-from-gemini-url url))
+             (type (if address (elpher-address-type address) nil))
+             (type-map-entry (cdr (assoc type elpher-type-map)))
+             (fill-prefix (make-string (+ 1 (length elpher-gemini-link-string)) ?\s)))
+        (when type-map-entry
+          (insert elpher-gemini-link-string)
           (let* ((face (elt type-map-entry 3))
           (let* ((face (elt type-map-entry 3))
-                 (filtered-display-string (ansi-color-filter-apply display-string))
-                 (page (elpher-make-page filtered-display-string address)))
-            (insert-text-button filtered-display-string
+                 (display-string (or given-display-string
+                                     (elpher-address-to-iri address)))
+                 (page (elpher-make-page display-string
+                                         address)))
+            (insert-text-button display-string
                                 'face face
                                 'elpher-page page
                                 '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"))))
+                                :type 'elpher-link))
+          (newline))))))
 
 (defun elpher-gemini-insert-header (header-line)
   "Insert header described by HEADER-LINE into a text/gemini document.
 
 (defun elpher-gemini-insert-header (header-line)
   "Insert header described by HEADER-LINE into a text/gemini document.
@@ -1402,26 +1691,34 @@ by HEADER-LINE."
                    (2 'elpher-gemini-heading2)
                    (3 'elpher-gemini-heading3)
                    (_ 'default)))
                    (2 'elpher-gemini-heading2)
                    (3 'elpher-gemini-heading3)
                    (_ 'default)))
-          (fill-column (if (display-graphic-p)
-                           (/ (* fill-column
-                                 (font-get (font-spec :name (face-font 'default)) :size))
-                              (font-get (font-spec :name (face-font face)) :size)) fill-column)))
+           (fill-column (if (display-graphic-p)
+                            (/ (* fill-column
+                                  (font-get (font-spec :name (face-font 'default)) :size))
+                               (font-get (font-spec :name (face-font face)) :size)) fill-column)))
       (unless (display-graphic-p)
         (insert (make-string level ?#) " "))
       (unless (display-graphic-p)
         (insert (make-string level ?#) " "))
-      (insert (propertize header 'face face))
+      (insert (propertize header
+                          'face face
+                          'gemini-heading t
+                          'rear-nonsticky t))
       (newline))))
 
 (defun elpher-gemini-insert-text (text-line)
   "Insert a plain non-preformatted TEXT-LINE into a text/gemini document.
 This function uses Emacs' auto-fill to wrap text sensibly to a maximum
       (newline))))
 
 (defun elpher-gemini-insert-text (text-line)
   "Insert a plain non-preformatted TEXT-LINE into a text/gemini document.
 This function uses Emacs' auto-fill to wrap text sensibly to a maximum
-width defined by elpher-gemini-max-fill-width."
-  (string-match "\\(^[ \t]*\\)\\(\*[ \t]+\\|>[ \t]*\\)?" text-line)
-  (let* ((line-prefix (match-string 2 text-line))
+width defined by `elpher-gemini-max-fill-width'."
+  (string-match
+   (rx (: line-start
+          (optional
+           (group (or (: "*" (+ (any " \t")))
+                      (: ">" (* (any " \t"))))))))
+   text-line)
+  (let* ((line-prefix (match-string 1 text-line))
          (processed-text-line
           (if line-prefix
               (cond ((string-prefix-p "*" line-prefix)
                      (concat
          (processed-text-line
           (if line-prefix
               (cond ((string-prefix-p "*" line-prefix)
                      (concat
-                      (replace-regexp-in-string "\*"
+                      (replace-regexp-in-string "\\*"
                                                 elpher-gemini-bullet-string
                                                 (match-string 0 text-line))
                       (substring text-line (match-end 0))))
                                                 elpher-gemini-bullet-string
                                                 (match-string 0 text-line))
                       (substring text-line (match-end 0))))
@@ -1429,28 +1726,77 @@ width defined by elpher-gemini-max-fill-width."
                      (propertize text-line 'face 'elpher-gemini-quoted))
                     (t text-line))
             text-line))
                      (propertize text-line 'face 'elpher-gemini-quoted))
                     (t text-line))
             text-line))
-         (adaptive-fill-mode nil))
+         (fill-prefix (if line-prefix
+                          (make-string (length (match-string 0 text-line)) ?\s)
+                        "")))
     (insert (elpher-process-text-for-display processed-text-line))
     (newline)))
 
     (insert (elpher-process-text-for-display processed-text-line))
     (newline)))
 
+(defun elpher-gemini-pref-expand-collapse (button)
+  "Function called when the preformatted text toggle BUTTON is activated."
+  (let ((id (button-get button 'pref-id)))
+    (if (invisible-p id)
+        (remove-from-invisibility-spec id)
+      (add-to-invisibility-spec id))
+    (redraw-display)))
+
+(defun elpher-gemini-insert-preformatted-toggler (alt-text)
+  "Insert a button for toggling the visibility of preformatted text.
+If non-nil, ALT-TEXT is displayed alongside the button."
+  (let* ((url-string (url-recreate-url (elpher-page-address elpher-current-page)))
+         (pref-id (intern (concat "pref-"
+                                  (number-to-string (point))
+                                  "-"
+                                  url-string))))
+    (insert elpher-gemini-preformatted-toggle-bullet)
+    (when alt-text
+      (insert (propertize (concat alt-text " ")
+                          'face 'elpher-gemin-preformatted)))
+    (insert-text-button elpher-gemini-preformatted-toggle-label
+                        'action #'elpher-gemini-pref-expand-collapse
+                        'pref-id pref-id
+                        'face 'elpher-gemini-preformatted-toggle)
+    (add-to-invisibility-spec pref-id)
+    (newline)
+    pref-id))
+
+(defun elpher-gemini-insert-preformatted-line (line &optional pref-id)
+  "Insert a LINE of preformatted text.
+PREF-ID is the value assigned to the \"invisible\" text attribute, which
+can be used to toggle the display of the preformatted text."
+  (insert (propertize (concat (elpher-process-text-for-display
+                               (propertize line 'face 'elpher-gemini-preformatted))
+                              "\n")
+                      'invisible pref-id
+                      'rear-nonsticky t)))
+
 (defun elpher-render-gemini-map (data _parameters)
   "Render DATA as a gemini map file, PARAMETERS is currently unused."
   (elpher-with-clean-buffer
 (defun elpher-render-gemini-map (data _parameters)
   "Render DATA as a gemini map file, PARAMETERS is currently unused."
   (elpher-with-clean-buffer
-   (let ((preformatted nil))
-     (auto-fill-mode 1)
+   (auto-fill-mode 1)
+   (setq-local buffer-invisibility-spec nil)
+   (let ((preformatted nil)
+         (adaptive-fill-mode nil)) ;Prevent automatic setting of fill-prefix
      (setq-local fill-column (min (window-width) elpher-gemini-max-fill-width))
      (dolist (line (split-string data "\n"))
      (setq-local fill-column (min (window-width) elpher-gemini-max-fill-width))
      (dolist (line (split-string data "\n"))
-       (cond
-        ((string-prefix-p "```" line) (setq preformatted (not preformatted)))
-        (preformatted (insert (elpher-process-text-for-display
-                               (propertize line 'face 'elpher-gemini-preformatted))
-                              "\n"))
-        ((string-prefix-p "=>" line) (elpher-gemini-insert-link line))
-        ((string-prefix-p "#" line) (elpher-gemini-insert-header line))
-        (t (elpher-gemini-insert-text line)))))
+       (pcase line
+         ((rx (: string-start "```" (opt (let alt-text (+ any)))))
+          (setq preformatted
+                (if preformatted
+                    nil
+                  (if elpher-gemini-hide-preformatted
+                      (elpher-gemini-insert-preformatted-toggler alt-text)
+                    t))))
+         ((guard  preformatted)
+          (elpher-gemini-insert-preformatted-line line preformatted))
+         ((pred (string-prefix-p "=>"))
+          (elpher-gemini-insert-link line))
+         ((pred (string-prefix-p "#"))
+          (elpher-gemini-insert-header line))
+         (_ (elpher-gemini-insert-text line))))
    (elpher-cache-content
     (elpher-page-address elpher-current-page)
    (elpher-cache-content
     (elpher-page-address elpher-current-page)
-    (buffer-string))))
+    (buffer-string)))))
 
 (defun elpher-render-gemini-plain-text (data _parameters)
   "Render DATA as plain text file.  PARAMETERS is currently unused."
 
 (defun elpher-render-gemini-plain-text (data _parameters)
   "Render DATA as plain text file.  PARAMETERS is currently unused."
@@ -1460,8 +1806,23 @@ width defined by elpher-gemini-max-fill-width."
     (elpher-page-address elpher-current-page)
     (buffer-string))))
 
     (elpher-page-address elpher-current-page)
     (buffer-string))))
 
-
-;; Finger page connection
+(defun elpher-build-current-imenu-index ()
+  "Build imenu index for current elpher buffer."
+  (save-excursion
+    (goto-char (point-min))
+    (let ((match nil)
+          (headers nil))
+      (while (setq match (text-property-search-forward 'gemini-heading t t))
+        (push (cons
+               (buffer-substring-no-properties (prop-match-beginning match)
+                                               (prop-match-end match))
+               (prop-match-beginning match))
+              headers))
+      (reverse headers))))
+
+
+;;; Finger page connection
+;;
 
 (defun elpher-get-finger-page (renderer)
   "Opens a finger connection to the current page address.
 
 (defun elpher-get-finger-page (renderer)
   "Opens a finger connection to the current page address.
@@ -1487,7 +1848,8 @@ The result is rendered using RENDERER."
          (elpher-network-error address the-error))))))
 
 
          (elpher-network-error address the-error))))))
 
 
-;; Telnet page connection
+;;; Telnet page connection
+;;
 
 (defun elpher-get-telnet-page (renderer)
   "Opens a telnet connection to the current page address (RENDERER must be nil)."
 
 (defun elpher-get-telnet-page (renderer)
   "Opens a telnet connection to the current page address (RENDERER must be nil)."
@@ -1503,30 +1865,69 @@ The result is rendered using RENDERER."
       (telnet host))))
 
 
       (telnet host))))
 
 
-;; Other URL page opening
+;;; Other URL page opening
+;;
 
 (defun elpher-get-other-url-page (renderer)
 
 (defun elpher-get-other-url-page (renderer)
-  "Getter which attempts to open the URL specified by the current page (RENDERER must be nil)."
+  "Getter which attempts to open the URL specified by the current page.
+The RENDERER argument to this getter must be nil."
   (when renderer
     (elpher-visit-previous-page)
     (error "Command not supported for general URLs"))
   (let* ((address (elpher-page-address elpher-current-page))
          (url (elpher-address-to-url address)))
   (when renderer
     (elpher-visit-previous-page)
     (error "Command not supported for general URLs"))
   (let* ((address (elpher-page-address elpher-current-page))
          (url (elpher-address-to-url address)))
-    (progn
-      (elpher-visit-previous-page) ; Do first in case of non-local exits.
-      (message "Opening URL...")
-      (if elpher-open-urls-with-eww
-          (browse-web url)
-        (browse-url url)))))
+    (elpher-visit-previous-page) ; Do first in case of non-local exits.
+    (message "Opening URL...")
+    (if elpher-open-urls-with-eww
+        (browse-web url)
+      (browse-url url))))
 
 
 
 
-;; Start page page retrieval
+;;; File page
+;;
+
+(defun elpher-get-file-page (renderer)
+  "Getter which renders a local file using RENDERER.
+Assumes UTF-8 encoding for all text files."
+  (let* ((address (elpher-page-address elpher-current-page))
+         (filename (elpher-address-filename address)))
+    (unless (file-exists-p filename)
+      (elpher-visit-previous-page)
+      (error "File not found"))
+    (unless (file-readable-p filename)
+      (elpher-visit-previous-page)
+      (error "Could not read from file"))
+    (let ((body (with-temp-buffer
+       (let ((coding-system-for-read 'binary)
+             (coding-system-for-write 'binary))
+         (insert-file-contents-literally filename)
+         (encode-coding-string (buffer-string) 'raw-text)))))
+       (if renderer
+           (funcall renderer body nil)
+         (pcase (file-name-extension filename)
+           ((or  "gmi" "gemini")
+            (elpher-render-gemini-map (decode-coding-string body 'utf-8) nil))
+           ((or "htm" "html")
+            (elpher-render-html (decode-coding-string body 'utf-8)))
+           ((or "txt" "")
+            (elpher-render-text (decode-coding-string body 'utf-8)))
+           ((or "jpg" "jpeg" "gif" "png" "bmp" "tif" "tiff")
+            (elpher-render-image body))
+           ((or "gopher" "gophermap")
+            (elpher-render-index (elpher-decode body)))
+           (_
+            (elpher-render-download body))))
+       (elpher-restore-pos))))
+
+
+;;; Welcome page retrieval
+;;
 
 
-(defun elpher-get-start-page (renderer)
-  "Getter which displays the start page (RENDERER must be nil)."
+(defun elpher-get-welcome-page (renderer)
+  "Getter which displays the welcome page (RENDERER must be nil)."
   (when renderer
     (elpher-visit-previous-page)
   (when renderer
     (elpher-visit-previous-page)
-    (error "Command not supported for start page"))
+    (error "Command not supported for welcome page"))
   (elpher-with-clean-buffer
    (insert "     --------------------------------------------\n"
            "           Elpher Gopher and Gemini Client       \n"
   (elpher-with-clean-buffer
    (insert "     --------------------------------------------\n"
            "           Elpher Gopher and Gemini Client       \n"
@@ -1536,20 +1937,20 @@ The result is rendered using RENDERER."
            "Default bindings:\n"
            "\n"
            " - TAB/Shift-TAB: next/prev item on current page\n"
            "Default bindings:\n"
            "\n"
            " - TAB/Shift-TAB: next/prev item on current page\n"
-           " - RET/mouse-1: open item under cursor\n"
+           " - RET/mouse-1: open item under cursor (with Shift to open in new buffer)\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"
            " - 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"
            " - g: go to a particular address (gopher, gemini, finger)\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"
            " - 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 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"
            " - 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"
            " - T: toggle TLS gopher mode\n"
            " - F: forget/discard current TLS client certificate\n"
            " - .: display the raw server response for the current page\n"
@@ -1558,192 +1959,382 @@ The result is rendered using RENDERER."
    (elpher-insert-index-record "Floodgap Systems Gopher Server"
                                (elpher-make-gopher-address ?1 "" "gopher.floodgap.com" 70))
    (elpher-insert-index-record "Project Gemini home page"
    (elpher-insert-index-record "Floodgap Systems Gopher Server"
                                (elpher-make-gopher-address ?1 "" "gopher.floodgap.com" 70))
    (elpher-insert-index-record "Project Gemini home page"
-                               (elpher-address-from-url "gemini://gemini.circumlunar.space/"))
+                               (elpher-address-from-url "gemini://geminiprotocol.net/"))
    (insert "\n"
            "Alternatively, select a search engine and enter some search terms:\n")
    (elpher-insert-index-record "Gopher Search Engine (Veronica-2)"
                                (elpher-make-gopher-address ?7 "/v2/vs" "gopher.floodgap.com" 70))
    (insert "\n"
            "Alternatively, select a search engine and enter some search terms:\n")
    (elpher-insert-index-record "Gopher Search Engine (Veronica-2)"
                                (elpher-make-gopher-address ?7 "/v2/vs" "gopher.floodgap.com" 70))
-   (elpher-insert-index-record "Gemini Search Engine (GUS)"
-                               (elpher-address-from-url "gemini://gus.guru/search"))
+   (elpher-insert-index-record "Gemini Search Engine (geminispace.info)"
+                               (elpher-address-from-url "gemini://geminispace.info/search"))
    (insert "\n"
    (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 ")
+   (insert-text-button "bookmark list"
+                       'face 'link
+                       'elpher-page
+                       (elpher-make-page "Elpher Bookmarks"
+                                         (elpher-make-about-address 'bookmarks))
+                       :type 'elpher-link)
+   (insert ".\n")
+   (insert (propertize
+            "(Bookmarks from legacy elpher-bookmarks files will be automatically imported.)\n"
+            'face 'shadow))
    (insert "\n"
    (insert "\n"
-           "For Elpher release news or to leave feedback, visit:\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))
    (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)"))
    (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
                          '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
    (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"
+                    " from MELPA or non-GNU ELPA. Otherwise you may have to install the\n"
+                    " manual yourself.)\n")
             'face 'shadow))
    (elpher-restore-pos)))
 
             'face 'shadow))
    (elpher-restore-pos)))
 
-;; Bookmarks page page retrieval
+
+;;; History page retrieval
+;;
+
+(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 "Current History Stack"
+                    (elpher-make-about-address 'history))))
+
+(defun elpher-show-visited-pages ()
+  "Show the all the pages you've visited using Elpher.
+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 Visted Pages"
+                    (elpher-make-about-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-display-history-links elpher-history "Current history stack"))
+
+(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-display-history-links
+   (seq-filter (lambda (page)
+                 (not (elpher-address-about-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 an entry or press 'u' to return to the previous page.")
+     (elpher-restore-pos))))
+
+
+;;; 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)))
+    (unless page
+      (error "Cannot bookmark this link"))
+    (let* ((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-about-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)
+  "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))
+         (page (elpher-page-from-url cleaned-url))
+         (buffer (get-buffer-create elpher-buffer-name)))
+    (elpher-with-clean-buffer
+     (elpher-visit-page page))
+    (set-buffer buffer)
+    nil))
+
+(defun elpher-bookmark-link ()
+  "Bookmark the link at point.
+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 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
+                      (read-file-name "Old Elpher bookmarks: "
+                                      user-emacs-directory nil t
+                                      "elpher-bookmarks"))))
+  (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))
 
 (defun elpher-get-bookmarks-page (renderer)
 
 (defun elpher-get-bookmarks-page (renderer)
-  "Getter to load and display the current bookmark list (RENDERER must be nil)."
+  "Getter which displays the bookmarks (RENDERER must be nil)."
   (when renderer
     (elpher-visit-previous-page)
     (error "Command not supported for bookmarks page"))
   (when renderer
     (elpher-visit-previous-page)
     (error "Command not supported for bookmarks page"))
-  (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
+
+  (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"))))
+
+  (if (and elpher-use-emacs-bookmark-menu
+           elpher-history)
+      (progn
+        (elpher-visit-previous-page)
+        (call-interactively #'bookmark-bmenu-list))
+    (elpher-with-clean-buffer
+     (insert " ---- Elpher Bookmarks ---- \n\n")
+     (bookmark-maybe-load-default-file)
+     (dolist (bookmark (bookmark-maybe-sort-alist))
+       (when (eq #'elpher-bookmark-jump (alist-get 'handler (cdr bookmark)))
+         (let* ((name (car bookmark))
+                (url (alist-get 'location (cdr bookmark)))
+                (address (elpher-address-from-url url)))
+           (elpher-insert-index-record name address))))
+     (when (<= (line-number-at-pos) 3)
+       (insert "No bookmarked pages found.\n"))
+     (insert "\n --------------------------\n\n"
+             "Select an entry or press 'u' to return to the previous page.\n\n"
+             "Bookmarks can be renamed or deleted via the ")
+     (insert-text-button "Emacs bookmark menu"
                          'action (lambda (_)
                                    (interactive)
                          'action (lambda (_)
                                    (interactive)
-                                   (find-file filename))
+                                   (call-interactively #'bookmark-bmenu-list))
                          'follow-link t
                          'follow-link t
-                         'help-echo help-string))
-   (insert "\n")
-   (elpher-restore-pos)))
-
+                         'help-echo "RET,mouse-1: open Emacs bookmark menu")
+     (insert (substitute-command-keys
+              ",\nwhich can also be opened from anywhere using '\\[bookmark-bmenu-list]'."))
+     (elpher-restore-pos))))
 
 
-;;; Bookmarks
-;;
+(defun elpher-show-bookmarks ()
+  "Interactive function to display the current list of elpher bookmarks."
+  (interactive)
+  (elpher-visit-page
+   (elpher-make-page "Elpher Bookmarks"
+                     (elpher-make-about-address '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)))))
 
 ;;; Integrations
 ;;
 
 
 ;;; Integrations
 ;;
 
-(defun elpher-org-link-store ()
-  "Store link to an `elpher' page in org-mode."
+;;; Org
+
+(defun elpher-org-export-link (link description format protocol)
+  "Export a LINK with DESCRIPTION for the given PROTOCOL and FORMAT.
+
+FORMAT is an Org export backend.  DESCRIPTION may be nil.  PROTOCOL may be one
+of gemini, gopher or finger."
+  (let* ((url (if (equal protocol "elpher")
+                  (string-remove-prefix "elpher:" link)
+                (format "%s:%s" protocol link)))
+         (desc (or description url)))
+    (pcase format
+      (`gemini (format "=> %s %s" url desc))
+      (`html (format "<a href=\"%s\">%s</a>" url desc))
+      (`latex (format "\\href{%s}{%s}" url desc))
+      (_ (if (not description)
+             url
+           (format "%s (%s)" desc url))))))
+
+(defun elpher-org-store-link ()
+  "Store link to an `elpher' page in Org."
   (when (eq major-mode 'elpher-mode)
   (when (eq major-mode 'elpher-mode)
-    (let ((link (concat "elpher:" (elpher-info-current)))
-          (desc (car elpher-current-page)))
-      (org-link-store-props :type "elpher"
-                            :link link
-                            :description desc)
+    (let* ((url (elpher-info-current))
+           (desc (car elpher-current-page))
+           (protocol (cond
+                      ((string-prefix-p "gemini:" url) "gemini")
+                      ((string-prefix-p "gopher:" url) "gopher")
+                      ((string-prefix-p "finger:" url) "finger")
+                      (t "elpher"))))
+      (when (equal "elpher" protocol)
+        ;; Weird link. Or special inner link?
+        (setq url (concat "elpher:" url)))
+      (org-link-store-props :type protocol :link url :description desc)
       t)))
 
       t)))
 
-(defun elpher-org-link-follow (link _args)
-  "Follow an `elpher' link in an `org' buffer."
-  (require 'elpher)
-  (message (concat "Got link: " link))
-  (when (or
-         (string-match-p "^gemini://.+" link)
-         (string-match-p "^gopher://.+" link)
-         (string-match-p "^finger://.+" link))
-    (elpher-go (string-remove-prefix "elpher:" link))))
-
-(with-eval-after-load "org"
-  ;; Use `org-link-set-parameters' if defined (org-mode 9+)
-  (if (fboundp 'org-link-set-parameters)
-      (org-link-set-parameters "elpher"
-                               :store #'elpher-org-link-store
-                               :follow #'elpher-org-link-follow)
-    (org-add-link-type "mu4e" 'elpher-org-link-follow)
-    (add-hook 'org-store-link-functions 'elpher-org-link-store)))
-
-(defun browse-url-elpher (url &rest _args)
-  "Browse URL. This function is used by `browse-url'."
+(defun elpher-org-follow-link (link protocol)
+  "Visit a LINK for the given PROTOCOL.
+
+PROTOCOL may be one of gemini, gopher or finger.  This method also
+supports the old protocol elpher, where the link is self-contained."
+  (let ((url (if (equal protocol "elpher")
+                 (string-remove-prefix "elpher:" link)
+               (format "%s:%s" protocol link))))
+    (elpher-go url)))
+
+(defun elpher-org-mode-integration ()
+  "Set up `elpher' integration for `org-mode'."
+  (org-link-set-parameters
+   "elpher"
+   :store #'elpher-org-store-link
+   :export (lambda (link description format _plist)
+             (elpher-org-export-link link description format "elpher"))
+   :follow (lambda (link _arg) (elpher-org-follow-link link "elpher")))
+  (org-link-set-parameters
+   "gemini"
+   :export (lambda (link description format _plist)
+             (elpher-org-export-link link description format "gemini"))
+   :follow (lambda (link _arg) (elpher-org-follow-link link "gemini")))
+  (org-link-set-parameters
+   "gopher"
+   :export (lambda (link description format _plist)
+             (elpher-org-export-link link description format "gopher"))
+   :follow (lambda (link _arg) (elpher-org-follow-link link "gopher")))
+  (org-link-set-parameters
+   "gophers"
+   :export (lambda (link description format _plist)
+             (elpher-org-export-link link description format "gophers"))
+   :follow (lambda (link _arg) (elpher-org-follow-link link "gophers")))
+  (org-link-set-parameters
+   "finger"
+   :export (lambda (link description format _plist)
+             (elpher-org-export-link link description format "finger"))
+   :follow (lambda (link _arg) (elpher-org-follow-link link "finger"))))
+
+(add-hook 'org-mode-hook #'elpher-org-mode-integration)
+
+;; Browse URL
+
+;;;###autoload
+(defun elpher-browse-url-elpher (url &rest _args)
+  "Browse URL using Elpher.  This function is used by `browse-url'."
   (interactive (browse-url-interactive-arg "Elpher URL: "))
   (elpher-go url))
 
   (interactive (browse-url-interactive-arg "Elpher URL: "))
   (elpher-go url))
 
-(with-eval-after-load "browse-url"
-  ;; Use elpher to open gopher, finger and gemini links
-  (add-to-list 'browse-url-default-handlers
-               '("^\\(gopher\\|finger\\|gemini\\)://" . browse-url-elpher))
-  ;; Register "gemini://" as a URI scheme so `browse-url' does the right thing
+;; Use elpher to open gopher, finger and gemini links
+;; For recent version of `browse-url' package
+(if (boundp 'browse-url-default-handlers)
+    (add-to-list
+     'browse-url-default-handlers
+     '("^\\(gopher\\|gophers\\|finger\\|gemini\\)://" . elpher-browse-url-elpher))
+  ;; 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. If the value is an alist,
+  ;; however, we don't know what to do. Better not interfere?
+  (when (and (symbolp browse-url-browser-function)
+             (fboundp browse-url-browser-function))
+    (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" "gophers" "finger"))
+                       ;; `elpher-go' always returns nil, which will stop the
+                       ;; advice chain here in a before-while
+                       (elpher-go url)
+                      ;; chain must continue, then return t.
+                      t))))))
+
+;; Register "gemini://" as a URI scheme so `browse-url' does the right thing
+(with-eval-after-load 'thingatpt
   (add-to-list 'thing-at-point-uri-schemes "gemini://"))
 
   (add-to-list 'thing-at-point-uri-schemes "gemini://"))
 
+;; Mu4e:
+
+;; Make mu4e aware of the gemini world
+(setq mu4e~view-beginning-of-url-regexp
+      "\\(?:https?\\|gopher\\|gophers\\|finger\\|gemini\\)://\\|mailto:")
+
+;; eww:
+
+;; Let elpher handle gemini, gopher links in eww buffer.
+(setq eww-use-browse-url
+      "\\`mailto:\\|\\(\\`gemini\\|\\`gopher\\|\\`gophers\\|\\`finger\\)://")
+
+
 ;;; Interactive procedures
 ;;
 
 ;;; Interactive procedures
 ;;
 
@@ -1760,28 +2351,44 @@ If ADDRESS is already bookmarked, update the label only."
 (defun elpher-follow-current-link ()
   "Open the link or url at point."
   (interactive)
 (defun elpher-follow-current-link ()
   "Open the link or url at point."
   (interactive)
-  (push-button))
+  (elpher--click-link (button-at (point))))
+
+(defun elpher-follow-current-link-new-buffer ()
+  "Open the link or url at point."
+  (interactive)
+  (elpher--open-link-new-buffer))
 
 ;;;###autoload
 (defun elpher-go (host-or-url)
   "Go to a particular gopher site HOST-OR-URL.
 When run interactively HOST-OR-URL is read from the minibuffer."
 
 ;;;###autoload
 (defun elpher-go (host-or-url)
   "Go to a particular gopher site HOST-OR-URL.
 When run interactively HOST-OR-URL is read from the minibuffer."
-  (interactive "sGopher or Gemini URL: ")
-  (let* ((cleaned-host-or-url (string-trim host-or-url))
-         (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)
-    nil))
+  (interactive (list
+                (read-string (format "Visit URL (default scheme %s): "
+                                     (elpher-get-default-url-scheme)))))
+  (let ((trimmed-host-or-url (string-trim host-or-url)))
+    (unless (string-empty-p trimmed-host-or-url)
+      (let ((page (elpher-page-from-url trimmed-host-or-url
+                                        (elpher-get-default-url-scheme))))
+        (unless (get-buffer-window elpher-buffer-name t)
+          (switch-to-buffer elpher-buffer-name))
+        (elpher-with-clean-buffer
+         (elpher-visit-page page))
+        nil)))) ; non-nil value is displayed by eshell
 
 (defun elpher-go-current ()
 
 (defun elpher-go-current ()
-  "Go to a particular site read from the minibuffer, initialized with the current URL."
+  "Go to a particular URL which is read from the minibuffer.
+Unlike `elpher-go', the reader is initialized with the URL of the
+current page."
   (interactive)
   (interactive)
-  (let ((address (elpher-page-address elpher-current-page)))
-    (if (elpher-address-special-p address)
-        (error "Command invalid for this page")
-      (let ((url (read-string "Gopher or Gemini URL: " (elpher-address-to-url address))))
-        (elpher-visit-page (elpher-make-page url (elpher-address-from-url url)))))))
+  (let* ((address (elpher-page-address elpher-current-page))
+         (url (read-string (format "Visit URL (default scheme %s): "
+                                   (elpher-get-default-url-scheme))
+                           (elpher-address-to-url address))))
+    (let ((trimmed-url (string-trim url)))
+      (unless (string-empty-p trimmed-url)
+        (elpher-with-clean-buffer
+         (elpher-visit-page
+          (elpher-page-from-url trimmed-url (elpher-get-default-url-scheme))))))))
 
 (defun elpher-redraw ()
   "Redraw current page."
 
 (defun elpher-redraw ()
   "Redraw current page."
@@ -1807,7 +2414,7 @@ When run interactively HOST-OR-URL is read from the minibuffer."
 (defun elpher-view-raw ()
   "View raw server response for current page."
   (interactive)
 (defun elpher-view-raw ()
   "View raw server response for current page."
   (interactive)
-  (if (elpher-address-special-p (elpher-page-address elpher-current-page))
+  (if (elpher-address-about-p (elpher-page-address elpher-current-page))
       (error "This page was not generated by a server")
     (elpher-visit-page elpher-current-page
                        #'elpher-render-raw)))
       (error "This page was not generated by a server")
     (elpher-visit-page elpher-current-page
                        #'elpher-render-raw)))
@@ -1822,9 +2429,7 @@ When run interactively HOST-OR-URL is read from the minibuffer."
   (interactive)
   (setq-local elpher-current-page nil)
   (setq-local elpher-history nil)
   (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."
 
 (defun elpher-download ()
   "Download the link at point."
@@ -1832,26 +2437,25 @@ When run interactively HOST-OR-URL is read from the minibuffer."
   (let ((button (button-at (point))))
     (if button
         (let ((page (button-get button 'elpher-page)))
   (let ((button (button-at (point))))
     (if button
         (let ((page (button-get button 'elpher-page)))
-          (if (elpher-address-special-p (elpher-page-address page))
-              (error "Cannot download %s"
-                     (elpher-page-display-string page))
-            (elpher-visit-page (button-get button 'elpher-page)
-                               #'elpher-render-download)))
+          (unless page
+            (error "Not an elpher page"))
+          (when (elpher-address-about-p (elpher-page-address page))
+            (error "Cannot download %s" (elpher-page-display-string page)))
+          (elpher-visit-page (button-get button 'elpher-page)
+                             #'elpher-render-download))
       (error "No link selected"))))
 
 (defun elpher-download-current ()
   "Download the current page."
   (interactive)
       (error "No link selected"))))
 
 (defun elpher-download-current ()
   "Download the current page."
   (interactive)
-  (if (elpher-address-special-p (elpher-page-address elpher-current-page))
+  (if (elpher-address-about-p (elpher-page-address elpher-current-page))
       (error "Cannot download %s"
              (elpher-page-display-string elpher-current-page))
       (error "Cannot download %s"
              (elpher-page-display-string elpher-current-page))
-    (elpher-visit-page (elpher-make-page
-                        (elpher-page-display-string elpher-current-page)
-                        (elpher-page-address elpher-current-page))
+    (elpher-visit-page elpher-current-page
                        #'elpher-render-download
                        t)))
 
                        #'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)))
   "Build alist mapping link names to destination pages in current buffer."
   (let ((link-map nil)
         (b (next-button (point-min) t)))
@@ -1863,7 +2467,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)
 (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: "
     (if link-map
         (let ((key (let ((completion-ignore-case t))
                      (completing-read "Directory item/link: "
@@ -1877,7 +2481,7 @@ When run interactively HOST-OR-URL is read from the minibuffer."
   "Visit root of current server."
   (interactive)
   (let ((address (elpher-page-address elpher-current-page)))
   "Visit root of current server."
   (interactive)
   (let ((address (elpher-page-address elpher-current-page)))
-    (if (not (elpher-address-special-p address))
+    (if (not (elpher-address-about-p address))
         (if (or (member (url-filename address) '("/" ""))
                 (and (elpher-address-gopher-p address)
                      (= (length (elpher-gopher-address-selector address)) 0)))
         (if (or (member (url-filename address) '("/" ""))
                 (and (elpher-address-gopher-p address)
                      (= (length (elpher-gopher-address-selector address)) 0)))
@@ -1888,89 +2492,25 @@ 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)))))
 
             (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)
 (defun elpher-info-page (page)
-  "Display information on PAGE."
-  (let ((display-string (elpher-page-display-string page))
-        (address (elpher-page-address page)))
-    (if (elpher-address-special-p address)
-        (message "Special page: %s" display-string)
-      (message "%s" (elpher-address-to-url address)))))
+  "Display URL of PAGE in minibuffer."
+  (let* ((address (elpher-page-address page))
+         (url (elpher-address-to-url address))
+         (iri (elpher-address-to-iri address)))
+    (if (equal url iri)
+        (message "%s" url)
+      (message "%s (Raw: %s)" iri url))))
 
 (defun elpher-info-link ()
   "Display information on page corresponding to link at point."
   (interactive)
   (let ((button (button-at (point))))
 
 (defun elpher-info-link ()
   "Display information on page corresponding to link at point."
   (interactive)
   (let ((button (button-at (point))))
-    (if button
-        (elpher-info-page (button-get button 'elpher-page))
-      (error "No item selected"))))
+    (unless button
+      (error "No item selected"))
+    (let ((page (button-get button 'elpher-page)))
+      (unless page
+        (error "Not an elpher page"))
+      (elpher-info-page page))))
 
 (defun elpher-info-current ()
   "Display information on current page."
 
 (defun elpher-info-current ()
   "Display information on current page."
@@ -1979,20 +2519,21 @@ When run interactively HOST-OR-URL is read from the minibuffer."
 
 (defun elpher-copy-page-url (page)
   "Copy URL representation of address of PAGE to `kill-ring'."
 
 (defun elpher-copy-page-url (page)
   "Copy URL representation of address of PAGE to `kill-ring'."
-  (let ((address (elpher-page-address page)))
-    (if (elpher-address-special-p address)
-        (error (format "Cannot represent %s as URL" (elpher-page-display-string page)))
-      (let ((url (elpher-address-to-url address)))
-        (message "Copied \"%s\" to kill-ring/clipboard." url)
-        (kill-new url)))))
+  (let* ((address (elpher-page-address page))
+         (url (elpher-address-to-url address)))
+    (message "Copied \"%s\" to kill-ring/clipboard." url)
+    (kill-new url)))
 
 (defun elpher-copy-link-url ()
   "Copy URL of item at point to `kill-ring'."
   (interactive)
   (let ((button (button-at (point))))
 
 (defun elpher-copy-link-url ()
   "Copy URL of item at point to `kill-ring'."
   (interactive)
   (let ((button (button-at (point))))
-    (if button
-        (elpher-copy-page-url (button-get button 'elpher-page))
-      (error "No item selected"))))
+    (unless button
+      (error "No item selected"))
+    (let ((page (button-get button 'elpher-page)))
+      (unless page
+        (error "Not an elpher page"))
+      (elpher-copy-page-url page))))
 
 (defun elpher-copy-current-url ()
   "Copy URL of current page to `kill-ring'."
 
 (defun elpher-copy-current-url ()
   "Copy URL of current page to `kill-ring'."
@@ -2018,11 +2559,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 "<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 [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 "g") 'elpher-go)
     (define-key map (kbd "o") 'elpher-go-current)
+    (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)
     (define-key map (kbd "r") 'elpher-redraw)
     (define-key map (kbd "R") 'elpher-reload)
     (define-key map (kbd "T") 'elpher-toggle-tls)
@@ -2036,21 +2581,24 @@ When run interactively HOST-OR-URL is read from the minibuffer."
     (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 "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 "B") 'elpher-show-bookmarks)
+    (define-key map (kbd "!") 'elpher-set-gopher-coding-system)
     (define-key map (kbd "F") 'elpher-forget-current-certificate)
     (when (fboundp 'evil-define-key*)
     (define-key map (kbd "F") 'elpher-forget-current-certificate)
     (when (fboundp 'evil-define-key*)
-      (evil-define-key* 'motion map
+      (evil-define-key*
+        'motion map
         (kbd "TAB") 'elpher-next-link
         (kbd "TAB") 'elpher-next-link
-        (kbd "C-") 'elpher-follow-current-link
         (kbd "C-t") 'elpher-back
         (kbd "u") 'elpher-back
         (kbd "C-t") 'elpher-back
         (kbd "u") 'elpher-back
-        (kbd "U") 'elpher-back-to-start
+        (kbd "-") 'elpher-back
+        (kbd "^") 'elpher-back
         [mouse-3] 'elpher-back
         [mouse-3] 'elpher-back
+        (kbd "U") 'elpher-back-to-start
         (kbd "g") 'elpher-go
         (kbd "o") 'elpher-go-current
         (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 "r") 'elpher-redraw
         (kbd "R") 'elpher-reload
         (kbd "T") 'elpher-toggle-tls
@@ -2064,10 +2612,8 @@ When run interactively HOST-OR-URL is read from the minibuffer."
         (kbd "C") 'elpher-copy-current-url
         (kbd "a") 'elpher-bookmark-link
         (kbd "A") 'elpher-bookmark-current
         (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 "B") 'elpher-show-bookmarks
+        (kbd "!") 'elpher-set-gopher-coding-system
         (kbd "F") 'elpher-forget-current-certificate))
     map)
   "Keymap for gopher client.")
         (kbd "F") 'elpher-forget-current-certificate))
     map)
   "Keymap for gopher client.")
@@ -2076,11 +2622,13 @@ 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
   "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-current-page nil)
   (setq-local elpher-history nil)
   (setq-local elpher-current-page nil)
   (setq-local elpher-history nil)
-  (setq-local elpher-buffer-name (buffer-name)))
+  (setq-local elpher-buffer-name (buffer-name))
+  (setq-local bookmark-make-record-function #'elpher-bookmark-make-record)
+  (setq-local imenu-create-index-function #'elpher-build-current-imenu-index))
 
 (when (fboundp 'evil-set-initial-state)
   (evil-set-initial-state 'elpher-mode 'motion))
 
 (when (fboundp 'evil-set-initial-state)
   (evil-set-initial-state 'elpher-mode 'motion))
@@ -2095,24 +2643,22 @@ functions which initialize the gopher client, namely
 The buffer used for Elpher sessions is determined by the value of
 ‘elpher-buffer-name’.  If there is already an Elpher session active in
 that buffer, Emacs will simply switch to it.  Otherwise, a new session
 The buffer used for Elpher sessions is determined by the value of
 ‘elpher-buffer-name’.  If there is already an Elpher session active in
 that buffer, Emacs will simply switch to it.  Otherwise, a new session
-will begin.  A numeric prefix arg (as in ‘C-u 42 M-x elpher RET’)
-switches to the session with that number, creating it if necessary.  A
-nonnumeric prefix arg means to create a new session.  Returns the
-buffer selected (or created)."
+will begin.  A numeric prefix ARG (as in ‘\\[universal-argument] 42
+\\[execute-extended-command] elpher RET’) switches to the session with
+that number, creating it if necessary.  A non numeric prefix ARG means
+to create a new session.  Returns the buffer selected (or created)."
   (interactive "P")
   (let* ((name (default-value 'elpher-buffer-name))
   (interactive "P")
   (let* ((name (default-value 'elpher-buffer-name))
-        (buf (cond ((numberp arg)
-                    (get-buffer-create (format "%s<%d>" name arg)))
-                   (arg
-                    (generate-new-buffer name))
-                   (t
-                    (get-buffer-create name)))))
+         (buf (cond ((numberp arg)
+                     (get-buffer-create (format "%s<%d>" name arg)))
+                    (arg
+                     (generate-new-buffer name))
+                    (t
+                     (get-buffer-create name)))))
     (pop-to-buffer-same-window buf)
     (unless (buffer-modified-p)
       (elpher-mode)
     (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
       "Started Elpher."))); Otherwise (elpher) evaluates to start page string.
 
 ;;; elpher.el ends here