Change browse-url advice for older releases
[elpher.git] / elpher.el
index 64f4a36..f94b33f 100644 (file)
--- a/elpher.el
+++ b/elpher.el
@@ -1,13 +1,28 @@
-;;; 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) 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-2020 Tim Vaughan <plugd@thelambdalab.xyz>
 
 ;; Author: Tim Vaughan <plugd@thelambdalab.xyz>
 ;; Created: 11 April 2019
 
 ;; Author: Tim Vaughan <plugd@thelambdalab.xyz>
 ;; Created: 11 April 2019
-;; Version: 2.10.2
+;; Version: 2.11.0
 ;; Keywords: comm gopher
 ;; Keywords: comm gopher
-;; Homepage: http://thelambdalab.xyz/elpher
-;; Package-Requires: ((emacs "26.2"))
+;; Homepage: https://alexschroeder.ch/cgit/elpher
+;; Package-Requires: ((emacs "27.1"))
 
 ;; This file is not part of GNU Emacs.
 
 
 ;; This file is not part of GNU Emacs.
 
@@ -48,7 +63,7 @@
 
 ;; Elpher is under active development.  Any suggestions for
 ;; improvements are welcome, and can be made on the official
 
 ;; 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/.
+;; project page, https://alexschroeder.ch/cgit/elpher.
 
 ;;; Code:
 
 
 ;;; Code:
 
 (require 'url-util)
 (require 'subr-x)
 (require 'dns)
 (require 'url-util)
 (require 'subr-x)
 (require 'dns)
-(require 'ansi-color)
 (require 'nsm)
 (require 'gnutls)
 (require 'socks)
 
 (require 'nsm)
 (require 'gnutls)
 (require 'socks)
 
+;;; ANSI colors or XTerm colors
+
+(or (require 'xterm-color nil t)
+    (require 'ansi-color))
+
+(defalias 'elpher-color-filter-apply
+  (if (fboundp 'xterm-color-filter)
+      (lambda (s)
+        (let ((xterm-color-render nil))
+          (xterm-color-filter s)))
+    'ansi-color-filter-apply)
+  "A function to filter out ANSI escape sequences.")
+(defalias 'elpher-color-apply
+  (if (fboundp 'xterm-color-filter)
+      'xterm-color-filter
+    'ansi-color-apply)
+  "A function to apply ANSI escape sequences.")
 
 ;;; Global constants
 ;;
 
 
 ;;; Global constants
 ;;
 
-(defconst elpher-version "2.10.2"
+(defconst elpher-version "2.11.0"
   "Current version of elpher.")
 
 (defconst elpher-margin-width 6
   "Current version of elpher.")
 
 (defconst elpher-margin-width 6
   "Association list from types to getters, renderers, margin codes and index faces.")
 
 
   "Association list from types to getters, renderers, margin codes and index faces.")
 
 
+;;; Internal variables
+;;
+
+;; buffer-local
+(defvar elpher--gemini-page-headings nil
+  "List of headings on the page.")
+
+(defvar elpher--gemini-page-links nil
+  "List of links on the page.")
+
+(defvar elpher--gemini-page-links-cache (make-hash-table :test 'equal)
+  "Hash of addresses and page links.")
+
 ;;; 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 +165,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
@@ -146,7 +191,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")))
 
@@ -189,6 +234,11 @@ 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-gemini-number-links nil
+  "If non-nil, number links in gemini pages when rendering.
+Links can be accessed by pressing `v' ('visit') followed by the link number."
+  :type '(boolean))
+
 ;; Face customizations
 
 (defgroup elpher-faces nil
 ;; Face customizations
 
 (defgroup elpher-faces nil
@@ -460,8 +510,8 @@ 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
+(defvar elpher-current-page nil) ; buffer local
+(defvar elpher-history nil)      ; buffer local
 
 (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.
@@ -476,12 +526,17 @@ unless NO-HISTORY is non-nil."
   (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))
-         (type-record (cdr (assoc type elpher-type-map))))
+         (type-record (cdr (assoc type elpher-type-map)))
+         (page-links nil))
     (if type-record
     (if type-record
-        (funcall (car type-record)
-                 (if renderer
-                     renderer
-                   (cadr type-record)))
+        (progn
+          (funcall (car type-record)
+                   (if renderer
+                       renderer
+                     (cadr type-record)))
+          (setq page-links (gethash address elpher--gemini-page-links-cache))
+          (if page-links
+              (setq elpher--gemini-page-links page-links)))
       (elpher-visit-previous-page)
       (pcase type
         (`(gopher ,type-char)
       (elpher-visit-previous-page)
       (pcase type
         (`(gopher ,type-char)
@@ -582,7 +637,7 @@ 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))))
 
 
@@ -703,10 +758,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
@@ -961,7 +1016,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))
     (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
                (page (elpher-make-page filtered-display-string address)))
           (elpher-insert-margin margin-code)
           (insert-text-button filtered-display-string
@@ -999,7 +1054,7 @@ If ADDRESS is not supplied or nil the record is rendered as an
 ;; Text rendering
 
 (defconst elpher-url-regex
 ;; 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\-_~?/@|#]\\)?\\)?"
+  "\\([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)
   "Regexp used to locate and buttinofy URLs in text files loaded by elpher.")
 
 (defun elpher-buttonify-urls (string)
@@ -1010,24 +1065,24 @@ If ADDRESS is not supplied or nil the record is rendered as an
     (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)))))
     (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)))
+        (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"
     (buffer-string)))
 
 (defconst elpher-ansi-regex "\x1b\\[[^m]*m"
-  "Wildly incomplete regexp used to strip out some troublesome ANSI escape sequences.")
+  "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
 
 (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))))
+                             (elpher-color-filter-apply string)
+                           (elpher-color-apply string))))
 
 (defun elpher-render-text (data &optional _mime-type-string)
   "Render DATA as text.  MIME-TYPE-STRING is unused."
 
 (defun elpher-render-text (data &optional _mime-type-string)
   "Render DATA as text.  MIME-TYPE-STRING is unused."
@@ -1060,9 +1115,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."
 (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)
@@ -1216,6 +1271,11 @@ that the response was malformed."
          (error "Gemini server response unknown: %s %s"
                 response-code response-meta))))))
 
          (error "Gemini server response unknown: %s %s"
                 response-code response-meta))))))
 
+(unless (fboundp 'read-answer)
+  (defun read-answer (question answers)
+    "Backfill for the new read-answer code."
+    (completing-read question (mapcar 'identity answers))))
+
 (defun elpher-choose-client-certificate ()
   "Prompt for a client certificate to use to establish a TLS connection."
   (let* ((read-answer-short t))
 (defun elpher-choose-client-certificate ()
   "Prompt for a client certificate to use to establish a TLS connection."
   (let* ((read-answer-short t))
@@ -1269,8 +1329,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)
@@ -1375,11 +1435,12 @@ treatment that a separate function is warranted."
          (address (elpher-address-from-gemini-url url))
          (type (if address (elpher-address-type address) nil))
          (type-map-entry (cdr (assoc type elpher-type-map))))
          (address (elpher-address-from-gemini-url url))
          (type (if address (elpher-address-type address) nil))
          (type-map-entry (cdr (assoc type elpher-type-map))))
+    (setq elpher--gemini-page-links (append elpher--gemini-page-links `(,address)))
     (when display-string
       (insert elpher-gemini-link-string)
       (if type-map-entry
           (let* ((face (elt type-map-entry 3))
     (when display-string
       (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
                  (page (elpher-make-page filtered-display-string address)))
             (insert-text-button filtered-display-string
                                 'face face
@@ -1402,10 +1463,12 @@ 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)))
+      (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))
       (unless (display-graphic-p)
         (insert (make-string level ?#) " "))
       (insert (propertize header 'face face))
@@ -1414,14 +1477,14 @@ 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
 (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)
+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
   (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 "\*"
+                      (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 +1492,45 @@ 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))
+         (adaptive-fill-mode t))
     (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
     (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
-   (let ((preformatted nil))
+   (setq elpher--gemini-page-headings nil)
+   (let ((preformatted nil)
+         (link-counter 1))
      (auto-fill-mode 1)
      (setq-local fill-column (min (window-width) elpher-gemini-max-fill-width))
      (auto-fill-mode 1)
      (setq-local fill-column (min (window-width) elpher-gemini-max-fill-width))
+     (setq elpher--gemini-page-links '())
      (dolist (line (split-string data "\n"))
        (cond
         ((string-prefix-p "```" line) (setq preformatted (not preformatted)))
         (preformatted (insert (elpher-process-text-for-display
                                (propertize line 'face 'elpher-gemini-preformatted))
                               "\n"))
      (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)
+         (progn
+           (if elpher-gemini-number-links
+               (insert
+                (concat
+                 "["
+                 (number-to-string link-counter)
+                 "] ")))
+           (setq link-counter (1+ link-counter))
+           (elpher-gemini-insert-link line)))
         ((string-prefix-p "#" line) (elpher-gemini-insert-header line))
         (t (elpher-gemini-insert-text 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)
    (elpher-cache-content
     (elpher-page-address elpher-current-page)
-    (buffer-string))))
+    (buffer-string))
+   (puthash
+    (elpher-page-address elpher-current-page)
+    elpher--gemini-page-links
+    elpher--gemini-page-links-cache)))
 
 (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."
@@ -1541,6 +1621,7 @@ The result is rendered using RENDERER."
            " - 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"
            " - u/mouse-3/U: return to previous page or to the start page\n"
            " - o/O: visit different selector or the root menu of the current server\n"
            " - g: go to a particular address (gopher, gemini, finger)\n"
+           " - v: visit a numbered link on a gemini page\n"
            " - d/D: download item under cursor or current page\n"
            " - i/I: info on item under cursor or current page\n"
            " - c/C: copy URL representation of item under cursor or current page\n"
            " - 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"
@@ -1563,8 +1644,8 @@ 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))
            "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)
    (insert "\n"
            "This page contains your bookmarked sites (also visit with B):\n")
    (elpher-insert-index-record "Your Bookmarks" 'bookmarks)
@@ -1700,6 +1781,128 @@ If ADDRESS is already bookmarked, update the label only."
                    (not (equal (elpher-bookmark-url bookmark) url)))
                  (elpher-load-bookmarks)))))
 
                    (not (equal (elpher-bookmark-url bookmark) url)))
                  (elpher-load-bookmarks)))))
 
+;;; Integrations
+;;
+
+;;; Org
+
+;; Avoid byte compilation warnings.
+(eval-when-compile
+  (declare-function org-link-store-props "ol")
+  (declare-function org-link-set-parameters "ol"))
+
+(defun elpher-org-export-link (link description format protocol)
+  "Export a LINK with DESCRIPTION for the given PROTOCOL and FORMAT.
+
+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* ((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-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)))
+
+(with-eval-after-load 'org
+  (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"))))
+
+;;; Browse URL
+
+;; Avoid byte compilation warnings.
+(eval-when-compile
+  (defvar thing-at-point-uri-schemes))
+
+;;;###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))
+
+;; 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:
+
+(eval-when-compile
+  (defvar mu4e~view-beginning-of-url-regexp))
+
+(with-eval-after-load 'mu4e-view
+  ;; Make mu4e aware of the gemini world
+  (setq mu4e~view-beginning-of-url-regexp
+        "\\(?:https?\\|gopher\\|finger\\|gemini\\)://\\|mailto:"))
+
 ;;; Interactive procedures
 ;;
 
 ;;; Interactive procedures
 ;;
 
@@ -1734,10 +1937,19 @@ When run interactively HOST-OR-URL is read from the minibuffer."
   "Go to a particular site read from the minibuffer, initialized with the current URL."
   (interactive)
   (let ((address (elpher-page-address elpher-current-page)))
   "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 ((url (read-string "Gopher or Gemini URL: "
+                            (unless (elpher-address-special-p address)
+                              (elpher-address-to-url address)))))
+      (elpher-visit-page (elpher-make-page url (elpher-address-from-url url))))))
+
+(defun elpher-visit-gemini-numbered-link (n)
+  "Visit link designated by a number N."
+  (interactive "nLink number: ")
+  (if (or (> n (length elpher--gemini-page-links))
+          (< n 1))
+      (user-error "Invalid link number"))
+  (let ((address (nth (1- n) elpher--gemini-page-links)))
+    (elpher-go (url-recreate-url address))))
 
 (defun elpher-redraw ()
   "Redraw current page."
 
 (defun elpher-redraw ()
   "Redraw current page."
@@ -1997,34 +2209,39 @@ When run interactively HOST-OR-URL is read from the minibuffer."
     (define-key map (kbd "B") 'elpher-bookmarks)
     (define-key map (kbd "S") 'elpher-set-gopher-coding-system)
     (define-key map (kbd "F") 'elpher-forget-current-certificate)
     (define-key map (kbd "B") 'elpher-bookmarks)
     (define-key map (kbd "S") 'elpher-set-gopher-coding-system)
     (define-key map (kbd "F") 'elpher-forget-current-certificate)
+    (define-key map (kbd "v") 'elpher-visit-gemini-numbered-link)
     (when (fboundp 'evil-define-key*)
     (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
+       (kbd "U") 'elpher-back-to-start
+       [mouse-3] 'elpher-back
+       (kbd "o") '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 "J") '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
+       (kbd "v") 'elpher-visit-gemini-numbered-link))
     map)
   "Keymap for gopher client.")
 
     map)
   "Keymap for gopher client.")
 
@@ -2034,9 +2251,14 @@ When run interactively HOST-OR-URL is read from the minibuffer."
 This mode is automatically enabled by the interactive
 functions which initialize the gopher client, namely
 `elpher', `elpher-go' and `elpher-bookmarks'."
 This mode is automatically enabled by the interactive
 functions which initialize the gopher client, namely
 `elpher', `elpher-go' and `elpher-bookmarks'."
+  (setq-local elpher--gemini-page-headings nil)
   (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 imenu-create-index-function
+              (lambda ()
+                elpher--gemini-page-headings)))
 
 (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))
@@ -2051,24 +2273,25 @@ 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))
+      (let ((start-page (elpher-make-page
+                         "Elpher Start Page"
+                         (elpher-make-special-address 'start))))
+        (elpher-visit-page 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