Sanitize display strings for use in header.
[elpher.git] / elpher.el
index da4d11d..b9c43f8 100644 (file)
--- a/elpher.el
+++ b/elpher.el
@@ -1,13 +1,29 @@
-;;; elpher.el --- A friendly gopher and gemini client  -*- lexical-binding:t -*-
-
-;; Copyright (C) 2019-2020 Tim Vaughan
+;;; elpher.el --- A friendly gopher and gemini client  -*- lexical-binding: t -*-
+
+;; Copyright (C) 2021 Jens Östlund <jostlund@gmail.com>
+;; Copyright (C) 2021 F. Jason Park <jp@neverwas.me>
+;; Copyright (C) 2021 Christopher Brannon <chris@the-brannons.com>
+;; Copyright (C) 2021 Omar Polo <op@omarpolo.com>
+;; Copyright (C) 2021 Noodles! <nnoodle@chiru.no>
+;; Copyright (C) 2021 Abhiseck Paira <abhiseckpaira@disroot.org>
+;; Copyright (C) 2020-2021 Alex Schroeder <alex@gnu.org>
+;; Copyright (C) 2020 Zhiwei Chen <chenzhiwei03@kuaishou.com>
+;; Copyright (C) 2020 condy0919 <condy0919@gmail.com>
+;; Copyright (C) 2020 Alexis <flexibeast@gmail.com>
+;; Copyright (C) 2020 Étienne Deparis <etienne@depar.is>
+;; Copyright (C) 2020 Simon Nicolussi <sinic@sinic.name>
+;; Copyright (C) 2020 Michel Alexandre Salim <michel@michel-slm.name>
+;; Copyright (C) 2020 Koushk Roy <kroy@twilio.com>
+;; Copyright (C) 2020 Vee <vee@vnsf.xyz>
+;; Copyright (C) 2020 Simon South <simon@simonsouth.net>
+;; Copyright (C) 2019-2021 Tim Vaughan <plugd@thelambdalab.xyz>
 
 ;; Author: Tim Vaughan <plugd@thelambdalab.xyz>
 ;; Created: 11 April 2019
-;; Version: 2.10.2
+;; Version: 3.2.1
 ;; Keywords: comm gopher
-;; Homepage: http://thelambdalab.xyz/elpher
-;; Package-Requires: ((emacs "26.2"))
+;; Homepage: https://thelambdalab.xyz/elpher
+;; Package-Requires: ((emacs "27.1"))
 
 ;; This file is not part of GNU Emacs.
 
@@ -35,7 +51,6 @@
 ;; - caching of visited sites,
 ;; - pleasant and configurable colouring of Gopher directories,
 ;; - direct visualisation of image files,
-;; - a simple bookmark management system,
 ;; - gopher connections using TLS encryption,
 ;; - the fledgling Gemini protocol,
 ;; - the greybeard Finger protocol.
@@ -47,8 +62,9 @@
 ;; Full instructions can be found in the Elpher info manual.
 
 ;; Elpher is under active development.  Any suggestions for
-;; improvements are welcome, and can be made on the official
-;; project page, 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:
 
 ;;
 
 (require 'seq)
-(require 'pp)
 (require 'shr)
 (require 'url-util)
 (require 'subr-x)
-(require 'dns)
-(require 'ansi-color)
 (require 'nsm)
 (require 'gnutls)
 (require 'socks)
-
+(require 'bookmark)
 
 ;;; Global constants
 ;;
 
-(defconst elpher-version "2.10.2"
+(defconst elpher-version "3.2.1"
   "Current version of elpher.")
 
 (defconst elpher-margin-width 6
     (finger elpher-get-finger-page elpher-render-text "txt" elpher-text)
     (telnet elpher-get-telnet-page nil "tel" elpher-telnet)
     (other-url elpher-get-other-url-page nil "url" elpher-other-url)
-    ((special 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.")
 
 
+;;; 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 bookmark-make-record-function)
+  (defvar mu4e~view-beginning-of-url-regexp)
+  (defvar eww-use-browse-url)
+  (defvar thing-at-point-uri-schemes)
+  (defvar xterm-color-preserve-properties))
+
+
 ;;; Customization group
 ;;
 
 
 (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
@@ -121,8 +154,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.
-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
@@ -146,7 +180,7 @@ These certificates may be used for establishing authenticated TLS connections."
   :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")))
 
@@ -174,10 +208,6 @@ May be empty."
   "Specify the string used for bullets when rendering gemini maps."
   :type '(string))
 
-(defcustom elpher-bookmarks-file (locate-user-emacs-file "elpher-bookmarks")
-  "Specify the name of the file where elpher bookmarks will be saved."
-  :type '(file))
-
 (defcustom elpher-ipv4-always nil
   "If non-nil, elpher will always use IPv4 to establish network connections.
 This can be useful when browsing from a computer that supports IPv6, because
@@ -189,6 +219,23 @@ some servers which do not support IPv6 can take a long time to time-out."
 Otherwise, the SOCKS proxy is only used for connections to onion services."
   :type '(boolean))
 
+(defcustom elpher-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 "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))
+
 ;; Face customizations
 
 (defgroup elpher-faces nil
@@ -273,7 +320,7 @@ Otherwise, the SOCKS proxy is only used for connections to onion services."
 ;; 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.
 
@@ -284,8 +331,6 @@ Otherwise, the SOCKS proxy is only used for connections to onion services."
         (let ((url (url-generic-parse-url url-string)))
           (unless (and (not (url-fullness url)) (url-type url))
             (setf (url-fullness url) t)
-            (setf (url-filename url)
-                  (url-unhex-string (url-filename url)))
             (unless (url-type url)
               (setf (url-type url) elpher-default-url-type))
             (unless (url-host url)
@@ -312,7 +357,7 @@ Otherwise, the SOCKS proxy is only used for connections to onion services."
   "Remove redundant port specifiers from ADDRESS.
 Here 'redundant' means that the specified port matches the default
 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)
@@ -345,49 +390,48 @@ requiring gopher-over-TLS."
              "/" (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)
-  "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'."
-  (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-nill if ADDRESS object is a gopher address."
+  (eq 'gopher (elpher-address-type 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."
-  (if (symbolp address)
-      nil
-    (url-filename address)))
+  (url-unhex-string (url-filename address)))
 
 (defun elpher-address-host (address)
   "Retrieve host from ADDRESS object."
@@ -400,18 +444,7 @@ For gopher addresses this is a combination of the selector type and selector."
 (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"))))
+  (url-port address))
 
 (defun elpher-gopher-address-selector (address)
   "Retrieve gopher selector from ADDRESS object."
@@ -448,6 +481,11 @@ If no address is defined, returns 0.  (This is for compatibility with the URL li
   "Create a page with DISPLAY-STRING and ADDRESS."
   (list display-string address))
 
+(defun elpher-make-start-page ()
+  "Create the welcome page."
+  (elpher-make-page "Start Page"
+                    (elpher-address-from-url elpher-start-page)))
+
 (defun elpher-page-display-string (page)
   "Retrieve the display string corresponding to PAGE."
   (elt page 0))
@@ -460,19 +498,33 @@ If no address is defined, returns 0.  (This is for compatibility with the URL li
   "Set the address corresponding to PAGE to NEW-ADDRESS."
   (setcar (cdr page) new-address))
 
-(defvar elpher-current-page nil)       ; buffer local
-(defvar elpher-history nil)            ; buffer local
+(defvar elpher-current-page nil
+  "The current page for this Elpher buffer.")
+
+(defvar elpher-history nil
+  "The local history 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.
-Additionally, push PAGE onto the stack of previously-visited pages,
-unless NO-HISTORY is non-nil."
+Additionally, push PAGE onto the history stack and the list of
+previously-visited pages,unless NO-HISTORY is non-nil."
   (elpher-save-pos)
   (elpher-process-cleanup)
-  (unless (or no-history
-              (equal (elpher-page-address elpher-current-page)
-                     (elpher-page-address page)))
-    (push elpher-current-page elpher-history))
+  (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))
@@ -493,10 +545,9 @@ unless NO-HISTORY is non-nil."
 
 (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."
@@ -526,23 +577,26 @@ unless NO-HISTORY is non-nil."
   "If `elpher-use-header' is true, display current page info in window header."
   (if elpher-use-header
       (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))
-             (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]"
                            ""))
-             (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."
+  (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))
-     (let ((inhibit-read-only t))
+     (let ((inhibit-read-only t)
+           (ansi-color-context nil)) ;; clean ansi interpreter state
        (setq-local network-security-level
                    (default-value 'network-security-level))
        (erase-buffer)
@@ -582,9 +636,60 @@ 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."
-  (elpher-decode (replace-regexp-in-string "\n\.\n$" "\n"
+  (elpher-decode (replace-regexp-in-string "\n\\.\n$" "\n"
                                            (replace-regexp-in-string "\r" "" string))))
 
+;;; Buttonify urls
+
+(defconst elpher-url-regex
+  "\\([a-zA-Z]+\\)://\\([a-zA-Z0-9.-]*[a-zA-Z0-9-]\\|\\[[a-zA-Z0-9:]+\\]\\)\\(:[0-9]+\\)?\\(/\\([0-9a-zA-Z_~?/@|:.%#=&-]*[0-9a-zA-Z_~?/@|#-]\\)?\\)?"
+  "Regexp used to locate and buttonify URLs in text files loaded by elpher.")
+
+(defun elpher-buttonify-urls (string)
+  "Turn substrings which look like urls in STRING into clickable buttons."
+  (with-temp-buffer
+    (insert string)
+    (goto-char (point-min))
+    (while (re-search-forward elpher-url-regex nil t)
+      (let ((page (elpher-make-page (substring-no-properties (match-string 0))
+                                    (elpher-address-from-url (match-string 0)))))
+        (make-text-button (match-beginning 0)
+                          (match-end 0)
+                          'elpher-page  page
+                          'action #'elpher-click-link
+                          'follow-link t
+                          'help-echo #'elpher--page-button-help
+                          'face 'button)))
+    (buffer-string)))
+
+;;; ANSI colors or XTerm colors (application and filtering)
+
+(or (require 'xterm-color nil t)
+    (require 'ansi-color))
+
+(defalias 'elpher-color-filter-apply
+  (if (fboundp 'xterm-color-filter)
+      (lambda (s)
+        (let ((_xterm-color-render nil))
+          (xterm-color-filter s)))
+    #'ansi-color-filter-apply)
+  "A function to filter out ANSI escape sequences.")
+
+(defalias 'elpher-color-apply
+  (if (fboundp 'xterm-color-filter)
+      #'xterm-color-filter
+    #'ansi-color-apply)
+  "A function to apply ANSI escape sequences.")
+
+;;; Processing text for display
+
+(defun elpher-process-text-for-display (string)
+  "Perform any desired processing of STRING prior to display as text.
+Currently includes buttonifying URLs and processing ANSI escape codes."
+  (elpher-buttonify-urls (if elpher-filter-ansi-from-text
+                             (elpher-color-filter-apply string)
+                           (elpher-color-apply string))))
+
 
 ;;; Network error reporting
 ;;
@@ -619,6 +724,15 @@ ERROR can be either an error object or a string."
   (if (timerp elpher-network-timer)
       (cancel-timer elpher-network-timer)))
 
+(defun elpher-make-network-timer (thunk)
+  "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.
@@ -634,7 +748,7 @@ unless `elpher-gemini-TLS-cert-checks' is non-nil.
 
 If non-nil, FORCE-IPV4 causes the network connection to be made over
 ipv4 only.  (The default behaviour when this is not set depends on
-the host operating system and the local network capabilities."
+the host operating system and the local network capabilities.)"
   (if (and use-tls (not (gnutls-available-p)))
       (error "Use of TLS requires Emacs to be compiled with GNU TLS support")
     (unless (< (elpher-address-port address) 65536)
@@ -645,18 +759,22 @@ the host operating system and the local network capabilities."
     (condition-case nil
         (let* ((kill-buffer-query-functions nil)
                (port (elpher-address-port address))
-               (service (if (> port 0) port default-port))
                (host (elpher-address-host address))
-               (socks (or elpher-socks-always (string-suffix-p ".onion" host)))
+               (service (if (> port 0) port default-port))
                (response-string-parts nil)
                (bytes-received 0)
                (hkbytes-received 0)
-               (timer (run-at-time elpher-connection-timeout nil
+               (socks (or elpher-socks-always (string-suffix-p ".onion" host)))
+               (gnutls-params (list :type 'gnutls-x509pki
+                                    :hostname host
+                                    :keylist
+                                    (elpher-get-current-keylist address)))
+               (timer (elpher-make-network-timer
                                    (lambda ()
                                      (elpher-process-cleanup)
                                      (cond
                                         ; 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
@@ -674,12 +792,12 @@ the host operating system and the local network capabilities."
                                                                  nil force-ipv4))
                                       (t
                                        (elpher-network-error address "Connection time-out."))))))
-               (gnutls-params (list :type 'gnutls-x509pki :hostname host
-                                    :keylist (elpher-get-current-keylist address)))
                (proc (if socks (socks-open-network-stream "elpher-process" nil host service)
                        (make-network-process :name "elpher-process"
                                              :host host
-                                             :family (and force-ipv4 'ipv4)
+                                             :family (and (or force-ipv4
+                                                              elpher-ipv4-always)
+                                                          'ipv4)
                                              :service service
                                              :buffer nil
                                              :nowait t
@@ -703,10 +821,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
-                                        (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
@@ -744,6 +862,7 @@ the host operating system and the local network capabilities."
             (if use-tls (apply #'gnutls-negotiate :process proc gnutls-params))
             (funcall (process-sentinel proc) proc "open\n")))
       (error
+       (elpher-process-cleanup)
        (error "Error initiating connection to server")))))
 
 
@@ -906,25 +1025,6 @@ once they are retrieved from the gopher server."
 
 ;; 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)))))))
-
 (defun elpher-insert-margin (&optional type-name)
   "Insert index margin, optionally containing the TYPE-NAME, into the current buffer."
   (if type-name
@@ -947,7 +1047,7 @@ displayed.  The _WINDOW argument is currently unused."
       (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)
+          (format "mouse-1, RET: open '%s'" (if (elpher-address-about-p address)
                                                 address
                                               (elpher-address-to-url address))))))))
 
@@ -961,7 +1061,7 @@ If ADDRESS is not supplied or nil the record is rendered as an
     (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
@@ -992,43 +1092,25 @@ If ADDRESS is not supplied or nil the record is rendered as an
   (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)))))
 
 ;; 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))))
-
 (defun elpher-render-text (data &optional _mime-type-string)
   "Render DATA as text.  MIME-TYPE-STRING is unused."
   (elpher-with-clean-buffer
@@ -1046,13 +1128,16 @@ Currently includes buttonifying URLs and processing ANSI escape codes."
   (if (not data)
       nil
     (if (display-images-p)
-        (progn
-          (let ((image (create-image
-                        data
-                        nil t)))
-            (elpher-with-clean-buffer
-             (insert-image image)
-             (elpher-restore-pos))))
+        (let* ((image (create-image
+                       data
+                       nil t))
+               (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))))
 
 ;; Search retrieval and rendering
@@ -1060,9 +1145,9 @@ Currently includes buttonifying URLs and processing ANSI escape codes."
 (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)
@@ -1186,8 +1271,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"))
-           (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)
@@ -1216,6 +1300,16 @@ that the response was malformed."
          (error "Gemini server response unknown: %s %s"
                 response-code response-meta))))))
 
+(defun elpher--read-answer-polyfill (question answers)
+  "Polyfill for `read-answer' in Emacs 26.1.
+QUESTION is a string containing a question, and ANSWERS
+is a list of possible answers."
+    (completing-read question (mapcar 'identity answers)))
+
+(if (fboundp 'read-answer)
+    (defalias 'elpher-read-answer 'read-answer)
+  (defalias 'elpher-read-answer 'elpher--read-answer-polyfill))
+
 (defun elpher-choose-client-certificate ()
   "Prompt for a client certificate to use to establish a TLS connection."
   (let* ((read-answer-short t))
@@ -1269,8 +1363,8 @@ that the response was malformed."
     (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)
@@ -1356,13 +1450,13 @@ treatment that a separate function is warranted."
           (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))
+        (setf (url-portspec address) (url-portspec current-address)) ; (url-port) too slow!
         (unless (string-prefix-p "/" (url-filename address)) ;deal with relative links
           (setf (url-filename address)
                 (concat (file-name-directory (url-filename current-address))
                         (url-filename 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)))))
@@ -1379,7 +1473,7 @@ treatment that a separate function is warranted."
       (insert elpher-gemini-link-string)
       (if type-map-entry
           (let* ((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)))
             (insert-text-button filtered-display-string
                                 'face face
@@ -1390,6 +1484,9 @@ treatment that a separate function is warranted."
         (insert (propertize display-string 'face 'elpher-unknown)))
       (insert "\n"))))
 
+(defvar elpher--gemini-page-headings nil
+  "List of headings on the page.")
+
 (defun elpher-gemini-insert-header (header-line)
   "Insert header described by HEADER-LINE into a text/gemini document.
 The gemini map file line describing the header is given
@@ -1402,10 +1499,12 @@ by HEADER-LINE."
                    (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)))
+      (setq elpher--gemini-page-headings (cons (cons header (point))
+                                               elpher--gemini-page-headings))
       (unless (display-graphic-p)
         (insert (make-string level ?#) " "))
       (insert (propertize header 'face face))
@@ -1414,28 +1513,42 @@ by HEADER-LINE."
 (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))
-         (processed-text-line
-          (if line-prefix
-              (cond ((string-prefix-p "*" line-prefix)
-                     (concat
-                      (replace-regexp-in-string "\*"
-                                                elpher-gemini-bullet-string
-                                                (match-string 0 text-line))
-                      (substring text-line (match-end 0))))
-                    ((string-prefix-p ">" line-prefix)
-                     (propertize text-line 'face 'elpher-gemini-quoted))
-                    (t text-line))
-            text-line))
-         (adaptive-fill-mode nil))
-    (insert (elpher-process-text-for-display processed-text-line))
-    (newline)))
+width defined by `elpher-gemini-max-fill-width'."
+  (if (string-empty-p text-line)
+      (insert "\n")
+    (string-match
+     (rx (: line-start
+            (* (any " \t"))
+            (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
+                        (replace-regexp-in-string "\\*"
+                                                  elpher-gemini-bullet-string
+                                                  (match-string 0 text-line))
+                        (substring text-line (match-end 0))))
+                      ((string-prefix-p ">" line-prefix)
+                       (propertize text-line 'face 'elpher-gemini-quoted))
+                      (t text-line))
+              text-line))
+           (adaptive-fill-mode t)
+          ;; fill-prefix is important for adaptive-fill-mode: without
+          ;; it, multi-line list items are not indented correct
+           (fill-prefix (if (match-string 1 text-line)
+                            (make-string (length (match-string 0 text-line)) ?\s)
+                          nil)))
+      (insert (elpher-process-text-for-display processed-text-line))
+      (newline))))
 
 (defun elpher-render-gemini-map (data _parameters)
   "Render DATA as a gemini map file, PARAMETERS is currently unused."
   (elpher-with-clean-buffer
+   (setq elpher--gemini-page-headings nil)
    (let ((preformatted nil))
      (auto-fill-mode 1)
      (setq-local fill-column (min (window-width) elpher-gemini-max-fill-width))
@@ -1445,9 +1558,11 @@ width defined by elpher-gemini-max-fill-width."
         (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-link line))
         ((string-prefix-p "#" line) (elpher-gemini-insert-header line))
         (t (elpher-gemini-insert-text line)))))
+   (setq elpher--gemini-page-headings (nreverse elpher--gemini-page-headings))
    (elpher-cache-content
     (elpher-page-address elpher-current-page)
     (buffer-string))))
@@ -1512,21 +1627,53 @@ The result is rendered using RENDERER."
     (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-start-page (renderer)
-  "Getter which displays the start page (RENDERER must be nil)."
+(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))
+           (_
+            (elpher-render-download body))))
+       (elpher-restore-pos))))
+
+
+;; Welcome page retrieval
+
+(defun elpher-get-welcome-page (renderer)
+  "Getter which displays the welcome page (RENDERER must be nil)."
   (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"
@@ -1539,17 +1686,17 @@ The result is rendered using RENDERER."
            " - RET/mouse-1: open item under cursor\n"
            " - m: select an item on current page by name (autocompletes)\n"
            " - u/mouse-3/U: return to previous page or to the start page\n"
-           " - o/O: visit different selector or the root menu of the current server\n"
            " - g: go to a particular address (gopher, gemini, finger)\n"
+           " - 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"
-           " - 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"
-           " - 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"
@@ -1563,187 +1710,367 @@ The result is rendered using RENDERER."
            "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"
-           "This page contains your bookmarked sites (also visit with B):\n")
-   (elpher-insert-index-record "Your Bookmarks" 'bookmarks)
+           "Your bookmarks are stored in your ")
+   (let ((help-string "RET,mouse-1: Open bookmark list"))
+     (insert-text-button "bookmark list"
+                         'face 'link
+                         'action (lambda (_)
+                                   (interactive)
+                                   (call-interactively #'elpher-show-bookmarks))
+                         'follow-link t
+                         'help-echo help-string))
+   (insert ".\n")
+   (insert (propertize
+            "(Bookmarks from legacy elpher-bookmarks files will be automatically imported.)\n"
+            'face 'shadow))
    (insert "\n"
-           "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))
-   (insert "\n"
-           "** Refer to the ")
    (let ((help-string "RET,mouse-1: Open Elpher info manual (if available)"))
-     (insert-text-button "Elpher info manual"
+     (insert "\n"
+             "The following info documentation is available:\n"
+             "   - ")
+     (insert-text-button "Elpher Manual"
                          'face 'link
                          'action (lambda (_)
                                    (interactive)
                                    (info "(elpher)"))
                          'follow-link t
-                         'help-echo help-string))
-   (insert " for the full documentation. **\n")
+                         'help-echo help-string)
+     (insert "\n   - ")
+     (insert-text-button "Changes introduced by the latest release"
+                       'face 'link
+                       'action (lambda (_)
+                                 (interactive)
+                                 (info "(elpher)News"))
+                       'follow-link t
+                       'help-echo help-string))
+   (insert "\n")
    (insert (propertize
-            (concat "  (This should be available if you have installed Elpher using\n"
-                    "   MELPA. Otherwise you will have to install the manual yourself.)\n")
+            (concat "(These documents should be available if you have installed Elpher \n"
+                    " using MELPA. Otherwise you may have to install the manual yourself.)\n")
             'face 'shadow))
    (elpher-restore-pos)))
 
-;; 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))
+         (address (elpher-address-from-url cleaned-url))
+         (page (elpher-make-page cleaned-url address)))
+    (elpher-with-clean-buffer
+     (elpher-visit-page page))
+    (set-buffer (get-buffer elpher-buffer-name))
+    nil))
+
+(defun elpher-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)
-  "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"))
-  (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)
-                                   (find-file filename))
+                                   (call-interactively #'bookmark-bmenu-list))
                          '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))))
 
+(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))))
 
-;;; 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
 ;;
 
-(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)
-    (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)))
 
-(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
+   "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))
 
-(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\\|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.
+  (advice-add browse-url-browser-function :before-while
+              (lambda (url &rest _args)
+               "Handle gemini, gopher, and finger schemes using Elpher."
+                (let ((scheme (downcase (car (split-string url ":" t)))))
+                  (if (member scheme '("gemini" "gopher" "finger"))
+                      ;; `elpher-go' always returns nil, which will stop the
+                      ;; 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://"))
 
+;;; Mu4e:
+
+;; Make mu4e aware of the gemini world
+(setq mu4e~view-beginning-of-url-regexp
+      "\\(?:https?\\|gopher\\|finger\\|gemini\\)://\\|mailto:")
+
+;;; eww:
+
+;; Let elpher handle gemini, gopher links in eww buffer.
+(setq eww-use-browse-url
+      "\\`mailto:\\|\\(\\`gemini\\|\\`gopher\\|\\`finger\\)://")
+
+
 ;;; Interactive procedures
 ;;
 
@@ -1767,21 +2094,24 @@ If ADDRESS is already bookmarked, update the label only."
   "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))
+  (let ((trimmed-host-or-url (string-trim host-or-url)))
+    (unless (string-empty-p trimmed-host-or-url)
+      (let* ((address (elpher-address-from-url trimmed-host-or-url))
+             (page (elpher-make-page trimmed-host-or-url address)))
+        (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 ()
   "Go to a particular site read from the minibuffer, initialized with the current URL."
   (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 "Gopher or Gemini URL: "
+                           (unless (elpher-address-about-p address)
+                             (elpher-address-to-url address)))))
+    (unless (string-empty-p (string-trim url))
+      (elpher-visit-page (elpher-make-page url (elpher-address-from-url url))))))
 
 (defun elpher-redraw ()
   "Redraw current page."
@@ -1807,7 +2137,7 @@ When run interactively HOST-OR-URL is read from the minibuffer."
 (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)))
@@ -1822,9 +2152,7 @@ When run interactively HOST-OR-URL is read from the minibuffer."
   (interactive)
   (setq-local elpher-current-page nil)
   (setq-local elpher-history nil)
-  (let ((start-page (elpher-make-page "Elpher Start Page"
-                                      (elpher-make-special-address 'start))))
-    (elpher-visit-page start-page)))
+  (elpher-visit-page (elpher-make-start-page)))
 
 (defun elpher-download ()
   "Download the link at point."
@@ -1832,17 +2160,18 @@ 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)))
-          (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)
-  (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))
     (elpher-visit-page (elpher-make-page
@@ -1851,7 +2180,7 @@ When run interactively HOST-OR-URL is read from the minibuffer."
                        #'elpher-render-download
                        t)))
 
-(defun elpher-build-link-map ()
+(defun elpher--build-link-map ()
   "Build alist mapping link names to destination pages in current buffer."
   (let ((link-map nil)
         (b (next-button (point-min) t)))
@@ -1863,7 +2192,7 @@ When run interactively HOST-OR-URL is read from the minibuffer."
 (defun elpher-jump ()
   "Select a directory entry by name.  Similar to the info browser (m)enu command."
   (interactive)
-  (let* ((link-map (elpher-build-link-map)))
+  (let* ((link-map (elpher--build-link-map)))
     (if link-map
         (let ((key (let ((completion-ignore-case t))
                      (completing-read "Directory item/link: "
@@ -1877,7 +2206,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)))
-    (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)))
@@ -1888,89 +2217,21 @@ When run interactively HOST-OR-URL is read from the minibuffer."
             (elpher-go (elpher-address-to-url address-copy))))
       (error "Command invalid for %s" (elpher-page-display-string elpher-current-page)))))
 
-(defun elpher-bookmarks-current-p ()
-  "Return non-nil if current page is a bookmarks page."
-  (equal (elpher-address-type (elpher-page-address elpher-current-page))
-         '(special bookmarks)))
-
-(defun elpher-reload-bookmarks ()
-  "Reload bookmarks if current page is a bookmarks page."
-  (if (elpher-bookmarks-current-p)
-      (elpher-reload-current-page)))
-
-(defun elpher-bookmark-current ()
-  "Bookmark the current page."
-  (interactive)
-  (let ((address (elpher-page-address elpher-current-page))
-        (display-string (elpher-page-display-string elpher-current-page)))
-    (if (not (elpher-address-special-p address))
-        (let ((bookmark-display-string (read-string "Bookmark display string: "
-                                                    display-string)))
-          (elpher-add-address-bookmark address bookmark-display-string)
-          (message "Bookmark added."))
-      (error "Cannot bookmark %s" display-string))))
-
-(defun elpher-bookmark-link ()
-  "Bookmark the link at point."
-  (interactive)
-  (let ((button (button-at (point))))
-    (if button
-        (let* ((page (button-get button 'elpher-page))
-               (address (elpher-page-address page))
-               (display-string (elpher-page-display-string page)))
-          (if (not (elpher-address-special-p address))
-              (let ((bookmark-display-string (read-string "Bookmark display string: "
-                                                          display-string)))
-                (elpher-add-address-bookmark address bookmark-display-string)
-                (elpher-reload-bookmarks)
-                (message "Bookmark added."))
-            (error "Cannot bookmark %s" display-string)))
-      (error "No link selected"))))
-
-(defun elpher-unbookmark-current ()
-  "Remove bookmark for the current page."
-  (interactive)
-  (let ((address (elpher-page-address elpher-current-page)))
-    (when (and (not (elpher-address-special-p address))
-               (y-or-n-p "Really remove bookmark for the current page? "))
-      (elpher-remove-address-bookmark address)
-      (message "Bookmark removed."))))
-
-(defun elpher-unbookmark-link ()
-  "Remove bookmark for the link at point."
-  (interactive)
-  (let ((button (button-at (point))))
-    (if button
-        (when (y-or-n-p "Really remove bookmark for this link? ")
-          (let ((page (button-get button 'elpher-page)))
-            (elpher-remove-address-bookmark (elpher-page-address page))
-            (elpher-reload-bookmarks)
-            (message "Bookmark removed.")))
-      (error "No link selected"))))
-
-;;;###autoload
-(defun elpher-bookmarks ()
-  "Visit bookmarks page."
-  (interactive)
-  (switch-to-buffer elpher-buffer-name)
-  (elpher-visit-page
-   (elpher-make-page "Bookmarks Page" (elpher-make-special-address 'bookmarks))))
-
 (defun elpher-info-page (page)
-  "Display information on PAGE."
-  (let ((display-string (elpher-page-display-string page))
-        (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)))
+    (message "%s" (elpher-address-to-url address))))
 
 (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."
@@ -1979,20 +2240,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'."
-  (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))))
-    (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'."
@@ -2018,11 +2280,15 @@ When run interactively HOST-OR-URL is read from the minibuffer."
     (define-key map (kbd "<backtab>") 'elpher-prev-link)
     (define-key map (kbd "C-M-i") 'elpher-prev-link)
     (define-key map (kbd "u") 'elpher-back)
-    (define-key map (kbd "U") 'elpher-back-to-start)
+    (define-key map (kbd "-") 'elpher-back)
+    (define-key map (kbd "^") 'elpher-back)
     (define-key map [mouse-3] 'elpher-back)
-    (define-key map (kbd "O") 'elpher-root-dir)
+    (define-key map (kbd "U") 'elpher-back-to-start)
     (define-key map (kbd "g") 'elpher-go)
     (define-key map (kbd "o") 'elpher-go-current)
+    (define-key map (kbd "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)
@@ -2036,39 +2302,41 @@ 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 "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*)
-      (evil-define-key* 'motion map
-        (kbd "TAB") 'elpher-next-link
-        (kbd "C-") 'elpher-follow-current-link
-        (kbd "C-t") 'elpher-back
-        (kbd "u") 'elpher-back
-        (kbd "U") 'elpher-back-to-start
-        [mouse-3] 'elpher-back
-        (kbd "g") 'elpher-go
-        (kbd "o") 'elpher-go-current
-        (kbd "r") 'elpher-redraw
-        (kbd "R") 'elpher-reload
-        (kbd "T") 'elpher-toggle-tls
-        (kbd ".") 'elpher-view-raw
-        (kbd "d") 'elpher-download
-        (kbd "D") 'elpher-download-current
-        (kbd "m") 'elpher-jump
-        (kbd "i") 'elpher-info-link
-        (kbd "I") 'elpher-info-current
-        (kbd "c") 'elpher-copy-link-url
-        (kbd "C") 'elpher-copy-current-url
-        (kbd "a") 'elpher-bookmark-link
-        (kbd "A") 'elpher-bookmark-current
-        (kbd "x") 'elpher-unbookmark-link
-        (kbd "X") 'elpher-unbookmark-current
-        (kbd "B") 'elpher-bookmarks
-        (kbd "S") 'elpher-set-gopher-coding-system
-        (kbd "F") 'elpher-forget-current-certificate))
+      (evil-define-key*
+       'motion map
+       (kbd "TAB") 'elpher-next-link
+       (kbd "C-") 'elpher-follow-current-link
+       (kbd "C-t") 'elpher-back
+       (kbd "u") 'elpher-back
+       (kbd "-") 'elpher-back
+       (kbd "^") 'elpher-back
+       [mouse-3] 'elpher-back
+       (kbd "U") 'elpher-back-to-start
+       (kbd "g") 'elpher-go
+       (kbd "o") 'elpher-go-current
+       (kbd "O") 'elpher-root-dir
+       (kbd "s") 'elpher-show-history
+       (kbd "S") 'elpher-show-visited-pages
+       (kbd "r") 'elpher-redraw
+       (kbd "R") 'elpher-reload
+       (kbd "T") 'elpher-toggle-tls
+       (kbd ".") 'elpher-view-raw
+       (kbd "d") 'elpher-download
+       (kbd "D") 'elpher-download-current
+       (kbd "m") 'elpher-jump
+       (kbd "i") 'elpher-info-link
+       (kbd "I") 'elpher-info-current
+       (kbd "c") 'elpher-copy-link-url
+       (kbd "C") 'elpher-copy-current-url
+       (kbd "a") 'elpher-bookmark-link
+       (kbd "A") 'elpher-bookmark-current
+       (kbd "B") 'elpher-show-bookmarks
+       (kbd "!") 'elpher-set-gopher-coding-system
+       (kbd "F") 'elpher-forget-current-certificate))
     map)
   "Keymap for gopher client.")
 
@@ -2076,11 +2344,15 @@ When run interactively HOST-OR-URL is read from the minibuffer."
   "Major mode for elpher, an elisp gopher client.
 
 This mode is automatically enabled by the interactive
-functions which initialize the gopher client, namely
-`elpher', `elpher-go' and `elpher-bookmarks'."
+functions which initialize the client, namely
+`elpher', and `elpher-go'."
+  (setq-local elpher--gemini-page-headings nil)
   (setq-local elpher-current-page nil)
   (setq-local elpher-history nil)
-  (setq-local elpher-buffer-name (buffer-name)))
+  (setq-local elpher-buffer-name (buffer-name))
+  (setq-local bookmark-make-record-function #'elpher-bookmark-make-record)
+  (setq-local imenu-create-index-function (lambda () elpher--gemini-page-headings))
+  (setq-local xterm-color-preserve-properties t))
 
 (when (fboundp 'evil-set-initial-state)
   (evil-set-initial-state 'elpher-mode 'motion))
@@ -2095,24 +2367,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
-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))
-        (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)
-      (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