Continuing with factoring.
[elpher.git] / elpher.el
index 1fa5ed5..f0d84df 100644 (file)
--- a/elpher.el
+++ b/elpher.el
@@ -4,7 +4,7 @@
 
 ;; Author: Tim Vaughan <tgvaughan@gmail.com>
 ;; Created: 11 April 2019
 
 ;; Author: Tim Vaughan <tgvaughan@gmail.com>
 ;; Created: 11 April 2019
-;; Version: 1.4.7
+;; Version: 2.0.0
 ;; Keywords: comm gopher
 ;; Homepage: https://github.com/tgvaughan/elpher
 ;; Package-Requires: ((emacs "26"))
 ;; Keywords: comm gopher
 ;; Homepage: https://github.com/tgvaughan/elpher
 ;; Package-Requires: ((emacs "26"))
   "Width of left-hand margin used when rendering indicies.")
 
 (defconst elpher-type-map
   "Width of left-hand margin used when rendering indicies.")
 
 (defconst elpher-type-map
-  '(((gopher ?0) elpher-get-text-node "txt" elpher-text)
-    ((gopher ?1) elpher-get-index-node "/" elpher-index)
-    ((gopher ?4) elpher-get-node-download "bin" elpher-binary)
-    ((gopher ?5) elpher-get-node-download "bin" elpher-binary)
-    ((gopher ?7) elpher-get-search-node "?" elpher-search)
-    ((gopher ?8) elpher-get-telnet-node "tel" elpher-telnet)
-    ((gopher ?9) elpher-get-node-download "bin" elpher-binary)
-    ((gopher ?g) elpher-get-image-node "img" elpher-image)
-    ((gopher ?p) elpher-get-image-node "img" elpher-image)
-    ((gopher ?I) elpher-get-image-node "img" elpher-image)
-    ((gopher ?d) elpher-get-node-download "doc" elpher-binary)
-    ((gopher ?P) elpher-get-node-download "doc" elpher-binary)
-    ((gopher ?s) elpher-get-node-download "snd" elpher-binary)
-    ((gopher ?h) elpher-get-html-node "htm" elpher-html)
-    (other-url elpher-get-other-url-node "url" elpher-other-url)
-    ((special bookmarks) elpher-get-bookmarks-node)
-    ((special start) elpher-get-start-node))
+  '(((gopher ?0) elpher-get-gopher-node elpher-render-text "txt" elpher-text)
+    ((gopher ?1) elpher-get-gopher-node elpher-render-index "/" elpher-index)
+    ((gopher ?4) elpher-get-gopher-node elpher-render-download "bin" elpher-binary)
+    ((gopher ?5) elpher-get-gopher-node elpher-render-download "bin" elpher-binary)
+    ((gopher ?7) elpher-get-gopher-query-node elpher-render-index "?" elpher-search)
+    ((gopher ?9) elpher-get-gopher-node elpher-render-node-download "bin" elpher-binary)
+    ((gopher ?g) elpher-get-gopher-node elpher-render-image "img" elpher-image)
+    ((gopher ?p) elpher-get-gopher-node elpher-render-image "img" elpher-image)
+    ((gopher ?I) elpher-get-gopher-node elpher-render-image "img" elpher-image)
+    ((gopher ?d) elpher-get-gopher-node elpher-render-download "doc" elpher-binary)
+    ((gopher ?P) elpher-get-gopher-node elpher-render-download "doc" elpher-binary)
+    ((gopher ?s) elpher-get-gopher-node elpher-render-download "snd" elpher-binary)
+    ((gopher ?h) elpher-get-gopher-node elpher-render-html "htm" elpher-html)
+    (gemini elpher-get-gemini-node elpher-render-gemini "gem" elpher-gemini)
+    (telnet elpher-get-telnet-node nil "tel" elpher-telnet)
+    (other-url elpher-get-other-url-node nil "url" elpher-other-url)
+    ((special bookmarks) elpher-get-bookmarks-node nil)
+    ((special start) elpher-get-start-node nil))
   "Association list from types to getters, margin codes and index faces.")
 
 
   "Association list from types to getters, margin codes and index faces.")
 
 
 
 (defface elpher-html
   '((t :inherit font-lock-comment-face))
 
 (defface elpher-html
   '((t :inherit font-lock-comment-face))
-  "Face used for url type directory records.")
+  "Face used for html type directory records.")
+
+(defface elpher-gemini
+  '((t :inherit font-lock-function-name-face))
+  "Face used for html type directory records.")
 
 (defface elpher-other-url
   '((t :inherit font-lock-comment-face))
 
 (defface elpher-other-url
   '((t :inherit font-lock-comment-face))
-  "Face used for url type directory records.")
+  "Face used for other URL type links records.")
 
 (defface elpher-telnet
   '((t :inherit font-lock-function-name-face))
 
 (defface elpher-telnet
   '((t :inherit font-lock-function-name-face))
@@ -185,26 +190,22 @@ allows switching from an encrypted channel back to plain text without user input
     (unwind-protect
         (let ((url (url-generic-parse-url url-string)))
           (setf (url-fullness url) t)
     (unwind-protect
         (let ((url (url-generic-parse-url url-string)))
           (setf (url-fullness url) t)
-          (unless (url-host url)
-            (setf (url-host url) (url-filename url))
-            (setf (url-filename url) ""))
+          (setf (url-filename url)
+                (url-unhex-string (url-filename url)))
           (unless (url-type url)
             (setf (url-type url) "gopher"))
           (unless (url-type url)
             (setf (url-type url) "gopher"))
-          (if (and (url-type url)
-                   (url-host url))
-              (let ((is-gopher (or (equal "gopher" (url-type url))
-                                   (equal "gophers" (url-type url)))))
-                (setf (url-filename url)
-                      (url-unhex-string (url-filename url)))
-                (when (or (equal (url-filename url) "")
-                          (equal (url-filename url) "/"))
-                  (if is-gopher
-                      (setf (url-filename url) "/1")))
-                (unless (> (url-port url) 0)
-                  (if is-gopher
-                      (setf (url-port url) 70)))
-                url)
-            (error "Malformed URL" url)))
+          (let ((is-gopher (or (equal "gopher" (url-type url))
+                               (equal "gophers" (url-type url))))
+                (is-gemini (equal "gemini" (url-type url))))
+            (when is-gopher
+              ;; Gopher defaults
+              (unless (url-host url)
+                (setf (url-host url) (url-filename url))
+                (setf (url-filename url) ""))
+              (when (or (equal (url-filename url) "")
+                        (equal (url-filename url) "/"))
+                (setf (url-filename url) "/1"))))
+          url)
       (set-match-data data))))
 
 (defun elpher-make-gopher-address (type selector host port &optional tls)
       (set-match-data data))))
 
 (defun elpher-make-gopher-address (type selector host port &optional tls)
@@ -237,7 +238,10 @@ attributes: TYPE, SELECTOR, HOST and PORT."
     (let ((protocol (url-type address)))
       (cond ((or (equal protocol "gopher")
                  (equal protocol "gophers"))
     (let ((protocol (url-type address)))
       (cond ((or (equal protocol "gopher")
                  (equal protocol "gophers"))
-             (list 'gopher (string-to-char (substring (url-filename address) 1))))
+             (list 'gopher
+                   (if (member (url-filename address) '("" "/"))
+                       ?1
+                     (string-to-char (substring (url-filename address) 1)))))
             ((equal protocol "gemini")
              'gemini)
             (t 'other-url)))))
             ((equal protocol "gemini")
              'gemini)
             (t 'other-url)))))
@@ -266,11 +270,14 @@ attributes: TYPE, SELECTOR, HOST and PORT."
 
 (defun elpher-address-gopher-p (address)
   "Return non-nill if ADDRESS object is a gopher address."
 
 (defun elpher-address-gopher-p (address)
   "Return non-nill if ADDRESS object is a gopher address."
-  (memq (elpher-address-protocol address) '("gopher gophers")))
+  (and (not (elpher-address-special-p address))
+       (member (elpher-address-protocol address) '("gopher gophers"))))
 
 (defun elpher-gopher-address-selector (address)
   "Retrieve gopher selector from ADDRESS object."
 
 (defun elpher-gopher-address-selector (address)
   "Retrieve gopher selector from ADDRESS object."
-  (substring (url-filename address) 2))
+  (if (member (url-filename address) '("" "/"))
+      ""
+    (substring (url-filename address) 2)))
 
 ;; Node
 
 
 ;; Node
 
@@ -328,8 +335,8 @@ initially."
 
 (defvar elpher-current-node nil)
 
 
 (defvar elpher-current-node nil)
 
-(defun elpher-visit-node (node &optional getter preserve-parent)
-  "Visit NODE using its own getter or GETTER, if non-nil.
+(defun elpher-visit-node (node &optional renderer preserve-parent)
+  "Visit NODE using its own renderer or RENDERER, if non-nil.
 Additionally, set the parent of NODE to `elpher-current-node',
 unless PRESERVE-PARENT is non-nil."
   (elpher-save-pos)
 Additionally, set the parent of NODE to `elpher-current-node',
 unless PRESERVE-PARENT is non-nil."
   (elpher-save-pos)
@@ -341,21 +348,22 @@ unless PRESERVE-PARENT is non-nil."
         (elpher-set-node-parent node (elpher-node-parent elpher-current-node))
       (elpher-set-node-parent node elpher-current-node)))
   (setq elpher-current-node node)
         (elpher-set-node-parent node (elpher-node-parent elpher-current-node))
       (elpher-set-node-parent node elpher-current-node)))
   (setq elpher-current-node node)
-  (if getter
-      (funcall getter)
-    (let* ((address (elpher-node-address node))
-           (type (elpher-address-type address))
-           (type-record (cdr (assoc type elpher-type-map))))
-      (if type-record
-          (funcall (car type-record))
-        (elpher-visit-parent-node)
-        (pcase type
-          (`(gopher ,type-char)
-           (error "Unsupported gopher selector type '%c' for '%s'"
-                  type-char (elpher-address-to-url address)))
-          (else
-           (error "Unsupported address type '%S' for '%s'"
-                  type (elpher-address-to-url address))))))))
+  (let* ((address (elpher-node-address node))
+         (type (elpher-address-type address))
+         (type-record (cdr (assoc type elpher-type-map))))
+    (if type-record
+        (funcall (car type-record)
+                 (if renderer
+                     renderer
+                   (cadr type-record)))
+      (elpher-visit-parent-node)
+      (pcase type
+        (`(gopher ,type-char)
+         (error "Unsupported gopher selector type '%c' for '%s'"
+                type-char (elpher-address-to-url address)))
+        (else
+         (error "Unsupported address type '%S' for '%s'"
+                type (elpher-address-to-url address)))))))
 
 (defun elpher-visit-parent-node ()
   "Visit the parent of the current node."
 
 (defun elpher-visit-parent-node ()
   "Visit the parent of the current node."
@@ -423,75 +431,16 @@ away CRs and any terminating period."
 ;;; Index rendering
 ;;
 
 ;;; 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"))
-      (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
-      (progn
-        (insert (format (concat "%" (number-to-string (- elpher-margin-width 1)) "s")
-                        (concat
-                         (propertize "[" 'face 'elpher-margin-brackets)
-                         (propertize type-name 'face 'elpher-margin-key)
-                         (propertize "]" 'face 'elpher-margin-brackets))))
-        (insert " "))
-    (insert (make-string elpher-margin-width ?\s))))
-
-(defun elpher-node-button-help (node)
-  "Return a string containing the help text for a button corresponding to NODE."
-  (let ((address (elpher-node-address node)))
-    (format "mouse-1, RET: open '%s'" (elpher-address-to-url address))))
-
-(defun elpher-insert-index-record (display-string address)
-  "Function to insert an index record into the current buffer.
-The contents of the record are dictated by DISPLAY-STRING and ADDRESS."
-  (let* ((type (elpher-address-type address))
-         (type-map-entry (cdr (assoc type elpher-type-map))))
-    (if type-map-entry
-        (let* ((margin-code (elt type-map-entry 1))
-               (face (elt type-map-entry 2))
-               (node (elpher-make-node display-string address)))
-          (elpher-insert-margin margin-code)
-          (insert-text-button display-string
-                              'face face
-                              'elpher-node node
-                              'action #'elpher-click-link
-                              'follow-link t
-                              'help-echo (elpher-node-button-help node)))
-      (pcase type
-        ('(gopher ?i) ;; Information
-         (elpher-insert-margin)
-         (insert (propertize
-                  (if elpher-buttonify-urls-in-directories
-                      (elpher-buttonify-urls display-string)
-                    display-string)
-                  'face 'elpher-info)))
-        (`(gopher ,selector-type) ;; Unknown
-         (elpher-insert-margin (concat (char-to-string selector-type) "?"))
-         (insert (propertize display-string
-                             'face 'elpher-unknown)))))
-    (insert "\n")))
+;;; Network error reporting
+;;
 
 
-(defun elpher-click-link (button)
-  "Function called when the gopher link BUTTON is activated (via mouse or keypress)."
-  (let ((node (button-get button 'elpher-node)))
-    (elpher-visit-node node)))
+(defun elpher-network-error (address error)
+  (elpher-with-clean-buffer
+   (insert (propertize "\n---- ERROR -----\n\n" 'face 'error)
+           "When attempting to retrieve " (elpher-address-to-url address) ":\n"
+           (error-message-string the-error) ".\n"
+           (propertize "\n----------------\n\n" 'face 'error)
+           "Press 'u' to return to the previous page.")))
 
 
 ;;; Gopher selector retrieval (all kinds)
 
 
 ;;; Gopher selector retrieval (all kinds)
@@ -527,7 +476,9 @@ up to the calling function."
              (proc (open-network-stream "elpher-process"
                                        nil
                                        (elpher-address-host address)
              (proc (open-network-stream "elpher-process"
                                        nil
                                        (elpher-address-host address)
-                                       (elpher-address-port address)
+                                       (if (> (elpher-address-port address) 0)
+                                           (elpher-address-port address)
+                                         70)
                                        :type (if elpher-use-tls 'tls 'plain))))
         (set-process-coding-system proc 'binary)
         (set-process-filter proc
                                        :type (if elpher-use-tls 'tls 'plain))))
         (set-process-coding-system proc 'binary)
         (set-process-filter proc
@@ -556,33 +507,106 @@ up to the calling function."
                   (propertize "\n----------------\n\n" 'face 'error)
                   "Press 'u' to return to the previous page.")))))))
 
                   (propertize "\n----------------\n\n" 'face 'error)
                   "Press 'u' to return to the previous page.")))))))
 
-;; Index retrieval
-
-(defun elpher-get-index-node ()
-  "Getter which retrieves the current node contents as an index."
+(defun elpher-get-gopher-node (renderer)
   (let* ((address (elpher-node-address elpher-current-node))
          (content (elpher-get-cached-content address)))
   (let* ((address (elpher-node-address elpher-current-node))
          (content (elpher-get-cached-content address)))
-    (if content
-        (progn
-          (elpher-with-clean-buffer
-           (insert content)
-           (elpher-restore-pos)))
+    (if (not (and content (funcall renderer nil content)))
       (elpher-with-clean-buffer
       (elpher-with-clean-buffer
-       (insert "LOADING DIRECTORY... (use 'u' to cancel)"))
+       (insert "LOADING... (use 'u' to cancel)"))
       (elpher-get-selector address
                            (lambda (proc event)
                              (unless (string-prefix-p "deleted" event)
       (elpher-get-selector address
                            (lambda (proc event)
                              (unless (string-prefix-p "deleted" event)
-                               (elpher-with-clean-buffer
-                                (elpher-insert-index elpher-selector-string)
-                                (elpher-restore-pos)
-                                (elpher-cache-content
-                                 (elpher-node-address elpher-current-node)
-                                 (buffer-string)))))))))
+                               (funcall renderer elpher-selector-string))))))
+  (elpher-restore-pos))
+
+;; 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"))
+      (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))))))
 
 
-;; Text retrieval
+(defun elpher-insert-margin (&optional type-name)
+  "Insert index margin, optionally containing the TYPE-NAME, into the current buffer."
+  (if type-name
+      (progn
+        (insert (format (concat "%" (number-to-string (- elpher-margin-width 1)) "s")
+                        (concat
+                         (propertize "[" 'face 'elpher-margin-brackets)
+                         (propertize type-name 'face 'elpher-margin-key)
+                         (propertize "]" 'face 'elpher-margin-brackets))))
+        (insert " "))
+    (insert (make-string elpher-margin-width ?\s))))
+
+(defun elpher-node-button-help (node)
+  "Return a string containing the help text for a button corresponding to NODE."
+  (let ((address (elpher-node-address node)))
+    (format "mouse-1, RET: open '%s'" (elpher-address-to-url address))))
+
+(defun elpher-insert-index-record (display-string &optional address)
+  "Function to insert an index record into the current buffer.
+The contents of the record are dictated by DISPLAY-STRING and ADDRESS.
+If ADDRESS is not supplied or nil the record is rendered as an
+'information' line."
+  (let* ((type (if address (elpher-address-type address) nil))
+         (type-map-entry (cdr (assoc type elpher-type-map))))
+    (if type-map-entry
+        (let* ((margin-code (elt type-map-entry 1))
+               (face (elt type-map-entry 2))
+               (node (elpher-make-node display-string address)))
+          (elpher-insert-margin margin-code)
+          (insert-text-button display-string
+                              'face face
+                              'elpher-node node
+                              'action #'elpher-click-link
+                              'follow-link t
+                              'help-echo (elpher-node-button-help node)))
+      (pcase type
+        ((or '(gopher ?i) 'nil) ;; Information
+         (elpher-insert-margin)
+         (insert (propertize
+                  (if elpher-buttonify-urls-in-directories
+                      (elpher-buttonify-urls display-string)
+                    display-string)
+                  'face 'elpher-info)))
+        (`(gopher ,selector-type) ;; Unknown
+         (elpher-insert-margin (concat (char-to-string selector-type) "?"))
+         (insert (propertize display-string
+                             'face 'elpher-unknown)))))
+    (insert "\n")))
+
+(defun elpher-click-link (button)
+  "Function called when the gopher link BUTTON is activated (via mouse or keypress)."
+  (let ((node (button-get button 'elpher-node)))
+    (elpher-visit-node node)))
+
+(defun elpher-render-index (data &optional cached-data)
+  "Render DATA as an index, using CACHED-DATA instead if supplied."
+  (elpher-with-clean-buffer
+   (if cached-data
+       (progn (insert cached-data) t)
+     (elpher-insert-index data)
+     (elpher-cache-content (elpher-node-address elpher-current-node)
+                           (buffer-string)))))
+
+;; Text rendering
 
 (defconst elpher-url-regex
 
 (defconst elpher-url-regex
-  "\\([a-zA-Z]+\\)://\\([a-zA-Z0-9.\-]+\\|\[[a-zA-Z0-9:]+\]\\)\\(?3::[0-9]+\\)?\\(?4:/[^ \r\n\t(),]*\\)?"
+  "\\([a-zA-Z]+\\)://\\([a-zA-Z0-9.\-]+\\|\[[a-zA-Z0-9:]+\]\\)\\(?3::[0-9]+\\)?\\(?4:/[^<> \r\n\t(),]*\\)?"
   "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)
@@ -601,165 +625,285 @@ up to the calling function."
                             'help-echo (elpher-node-button-help node))))
     (buffer-string)))
 
                             'help-echo (elpher-node-button-help node))))
     (buffer-string)))
 
-(defun elpher-get-text-node ()
-  "Getter which retrieves the current node contents as a text document."
-  (let* ((address (elpher-node-address elpher-current-node))
-         (content (elpher-get-cached-content address)))
-    (if content
-        (progn
-          (elpher-with-clean-buffer
-           (insert content)
-           (elpher-restore-pos)))
-      (progn
-        (elpher-with-clean-buffer
-         (insert "LOADING TEXT... (use 'u' to cancel)"))
-        (elpher-get-selector address
-                              (lambda (proc event)
-                                (unless (string-prefix-p "deleted" event)
-                                  (elpher-with-clean-buffer
-                                   (insert (elpher-buttonify-urls
-                                            (elpher-preprocess-text-response
-                                             elpher-selector-string)))
-                                   (elpher-restore-pos)
-                                   (elpher-cache-content
-                                    (elpher-node-address elpher-current-node)
-                                    (buffer-string))))))))))
+(defun elpher-render-text (data &optional cached-data)
+  "Render DATA as text, using CACHED-DATA instead if supplied."
+  (elpher-with-clean-buffer
+   (if cached-data
+       (progn (insert cached-data) t)
+     (insert (elpher-buttonify-urls
+              (elpher-preprocess-text-response)
+              elpher-selector-string))
+     (elpher-cache-content
+      (elpher-node-address elpher-current-node)
+      (buffer-string)))))
 
 ;; Image retrieval
 
 
 ;; Image retrieval
 
-(defun elpher-get-image-node ()
-  "Getter which retrieves the current node contents as an image to view."
-  (let* ((address (elpher-node-address elpher-current-node)))
+(defun elpher-render-image (data &optional cached-data)
+  "Display DATA as image, CACHED-DATA is ignored."
+  (if cached-data
+      f
     (if (display-images-p)
         (progn
     (if (display-images-p)
         (progn
-          (elpher-with-clean-buffer
-           (insert "LOADING IMAGE... (use 'u' to cancel)"))
-          (elpher-get-selector address
-                               (lambda (proc event)
-                                 (unless (string-prefix-p "deleted" event)
-                                   (let ((image (create-image
-                                                 elpher-selector-string
-                                                 nil t)))
-                                     (elpher-with-clean-buffer
-                                      (insert-image image)
-                                      (elpher-restore-pos)))))))
-      (elpher-get-node-download))))
-
-;; Search retrieval
-
-(defun elpher-get-search-node ()
-  "Getter which submits a search query to the address of the current node."
-  (let* ((address (elpher-node-address elpher-current-node))
-         (content (elpher-get-cached-content address))
-         (aborted t))
+          (let ((image (create-image
+                        data
+                        nil t)))
+            (elpher-with-clean-buffer
+             (insert-image image)
+             (elpher-restore-pos))))
+      (elpher-save-to-file data))))
+
+;; Search retrieval and rendering
+
+(defun elpher-get-gopher-query-node (renderer)
+   (let* ((address (elpher-node-address elpher-current-node))
+          (content (elpher-get-cached-content address))
+          (aborted t))
     (if content
         (progn
     (if content
         (progn
-          (elpher-with-clean-buffer
-           (insert content)
-           (elpher-restore-pos))
+          (funcall renderer nil content)
+          (elpher-restore-pos)
           (message "Displaying cached search results.  Reload to perform a new search."))
       (unwind-protect
           (let* ((query-string (read-string "Query: "))
                  (query-selector (concat (elpher-gopher-address-selector address) "\t" query-string))
                  (search-address (elpher-make-gopher-address ?1
           (message "Displaying cached search results.  Reload to perform a new search."))
       (unwind-protect
           (let* ((query-string (read-string "Query: "))
                  (query-selector (concat (elpher-gopher-address-selector address) "\t" query-string))
                  (search-address (elpher-make-gopher-address ?1
-                                                      query-selector
-                                                      (elpher-address-host address)
-                                                      (elpher-address-port address))))
+                                                             query-selector
+                                                             (elpher-address-host address)
+                                                             (elpher-address-port address)
+                                                             (equal (elpher-address-type address) "gophers"))))
             (setq aborted nil)
             (setq aborted nil)
+
             (elpher-with-clean-buffer
              (insert "LOADING RESULTS... (use 'u' to cancel)"))
             (elpher-get-selector search-address
             (elpher-with-clean-buffer
              (insert "LOADING RESULTS... (use 'u' to cancel)"))
             (elpher-get-selector search-address
-                                  (lambda (proc event)
-                                    (unless (string-prefix-p "deleted" event)
-                                      (elpher-with-clean-buffer
-                                       (elpher-insert-index elpher-selector-string))
-                                      (goto-char (point-min))
-                                      (elpher-cache-content
-                                       (elpher-node-address elpher-current-node)
-                                       (buffer-string))))))
+                                 (lambda (proc event)
+                                   (unless (string-prefix-p "deleted" event)
+                                     (funcall renderer elpher-selector-string)
+                                     (elpher-restore-pos)))))
         (if aborted
             (elpher-visit-parent-node))))))
         (if aborted
             (elpher-visit-parent-node))))))
+;; Raw server response rendering
 
 
-;; Raw server response retrieval
-
-(defun elpher-get-node-raw ()
-  "Getter which retrieves the raw server response for the current node."
-  (let ((address (elpher-node-address elpher-current-node)))
+(defun elpher-render-raw (data &optional cached-data)
+  "Display raw DATA in buffer. CACHED-DATA is ignored."
+  (if cached-data
+      f
     (elpher-with-clean-buffer
     (elpher-with-clean-buffer
-     (insert "LOADING RAW SERVER RESPONSE... (use 'u' to cancel)"))
-    (elpher-get-selector address
-                         (lambda (proc event)
-                           (unless (string-prefix-p "deleted" event)
-                             (elpher-with-clean-buffer
-                              (insert elpher-selector-string)
-                              (goto-char (point-min)))))))
-  (message "Displaying raw server response.  Reload or redraw to return to standard view."))
-;; File export retrieval
+     (insert data)
+     (goto-char (point-min)))
+    (message "Displaying raw server response.  Reload or redraw to return to standard view.")))
+
+;; File save renderer
+
+(defun elpher-render-download (data &optional cached-data)
+  "Save DATA to file. CACHED-DATA is ignored."
+  (if cached-data
+      f
+    (let* ((address (elpher-node-address elpher-current-node))
+           (selector (elpher-gopher-address-selector address)))
+      (elpher-visit-parent-node) ; Do first in case of non-local exits.
+      (let* ((filename-proposal (file-name-nondirectory selector))
+             (filename (read-file-name "Download complete. Save file as: "
+                                       nil nil nil
+                                       (if (> (length filename-proposal) 0)
+                                           filename-proposal
+                                         "gopher.file"))))
+        (with-temp-file filename
+          (insert elpher-selector-string)
+          (message (format "Saved to file %s."
+                           elpher-download-filename)))))))
 
 
-(defvar elpher-download-filename)
+;; HTML node retrieval
 
 
-(defun elpher-get-node-download ()
-  "Getter which retrieves the current node and writes the result to a file."
-  (let* ((address (elpher-node-address elpher-current-node))
-         (selector (elpher-gopher-address-selector address)))
-    (elpher-visit-parent-node) ; Do first in case of non-local exits.
-    (let* ((filename-proposal (file-name-nondirectory selector))
-           (filename (read-file-name "Save file as: "
-                                     nil nil nil
-                                     (if (> (length filename-proposal) 0)
-                                         filename-proposal
-                                       "gopher.file"))))
-      (message "Downloading...")
-      (setq elpher-download-filename filename)
-      (condition-case the-error
-          (elpher-get-selector address
-                               (lambda (proc event)
-                                 (let ((coding-system-for-write 'binary))
-                                   (with-temp-file elpher-download-filename
-                                     (insert elpher-selector-string)
-                                     (message (format "Download complate, saved to file %s."
-                                                      elpher-download-filename)))))
-                               t)
-        (error
-         (error "Error downloading %s" elpher-download-filename))))))
+(defun elpher-render-html (data &optional cached-data)
+  "Render DATA as HTML using shr, or insert CACHED-DATA if non-nil."
+  (elpher-with-clean-buffer
+   (if cached-data
+       (progn (insert cached-data) t)
+     (let ((dom (with-temp-buffer
+                  (insert string)
+                  (libxml-parse-html-region (point-min) (point-max)))))
+       (shr-insert-document dom)))))
+
+;;;;;;;;; ONLY COMPLETED TO HERE ;;;;;;;;;;;;;;;;;;;;
+
+;; Gemini node retrieval
+
+(defvar elpher-gemini-response)
+
+(defun elpher-get-gemini (address after)
+  "Retrieve gemini ADDRESS, then execute AFTER.
+The response is stored in the variable ‘elpher-gemini-response’."
+  (setq elpher-gemini-response "")
+  (if (not (gnutls-available-p))
+      (error "Cannot retrieve TLS selector: GnuTLS not available")
+    (let* ((kill-buffer-query-functions nil)
+           (proc (open-network-stream "elpher-process"
+                                      nil
+                                      (elpher-address-host address)
+                                      (if (> (elpher-address-port address) 0)
+                                          (elpher-address-port address)
+                                        1965)
+                                      :type 'tls)))
+      (set-process-coding-system proc 'binary)
+      (set-process-filter proc
+                          (lambda (proc string)
+                            (setq elpher-gemini-response
+                                  (concat elpher-gemini-response string))))
+      (set-process-sentinel proc after)
+      (process-send-string proc
+                           (concat (elpher-address-to-url address) "\r\n")))))
+
+
+(defun elpher-render-gemini-response (mime-type-string)
+  (let* ((mime-type-string* (if (string-empty-p mime-type-string)
+                                "text/gemini; charset=utf-8"
+                              mime-type-string))
+         (mime-type-split (split-string mime-type-string* ";"))
+         (mime-type (string-trim (car mime-type-split)))
+         (parameters (mapcar (lambda (s)
+                               (let ((key-val (split-string s "=")))
+                                 (list (downcase (string-trim (car key-val)))
+                                       (downcase (string-trim (cadr key-val))))))
+                             (cdr mime-type-split))))
+    (if (and (equal "text/gemini" mime-type)
+             (not (assoc "charset" parameters)))
+        (setq parameters (cons (list "charset" "utf-8") parameters)))
+    (when (string-prefix-p "text/" mime-type)
+      (if (assoc "charset" parameters)
+          (setq elpher-gemini-response
+                (decode-coding-string elpher-gemini-response
+                                      (intern (cadr (assoc "charset" parameters))))))
+      (setq elpher-gemini-response
+            (replace-regexp-in-string "\r" "" elpher-gemini-response)))
+    (pcase mime-type
+      ((or "text/gemini" "")
+       (elpher-render--mimetype-text/gemini elpher-gemini-response parameters))
+      ((pred (string-prefix-p "text/"))
+       (elpher-render--mimetype-text/plain elpher-gemini-response parameters))
+      ((pred (string-prefix-p "image/"))
+       (elpher-render--mimetype-image/* elpher-gemini-response parameters))
+      (other
+       (error "Unsupported MIME type %S" mime-type)))))
+
+(defun elpher-gemini-get-link-url (line)
+  (string-trim (elt (split-string (substring line 2)) 0)))
+
+(defun elpher-gemini-get-link-display-string (line)
+  (let* ((rest (string-trim (elt (split-string line "=>") 1)))
+         (idx (string-match "[ \t]" rest)))
+    (if idx
+        (string-trim (substring rest (+ idx 1)))
+      "")))
+
+(defun elpher-address-from-gemini-url (url)
+  "Extract address from URL with defaults as per gemini map files."
+  (let ((address (url-generic-parse-url url)))
+    (unless (and (url-type address) (not (url-fullness address))) ;avoid mangling mailto: urls
+      (setf (url-fullness address) t)
+      (unless (url-host address) ;if there is an explicit host, filenames are explicit
+        (setf (url-host address) (url-host (elpher-node-address elpher-current-node)))
+        (unless (string-prefix-p "/" (url-filename address)) ;deal with relative links
+          (setf (url-filename address)
+                (concat (file-name-directory 
+                         (url-filename (elpher-node-address elpher-current-node)))
+                        (url-filename address)))))
+      (unless (url-type address)
+        (setf (url-type address) "gemini")))
+    address))
+
+(defun elpher-render--mimetype-text/gemini (data parameters)
+  (elpher-with-clean-buffer
+   (dolist (line (split-string data "\n"))
+     (if (string-prefix-p "=>" line)
+         (let* ((url (elpher-gemini-get-link-url line))
+                (display-string (elpher-gemini-get-link-display-string line))
+                (address (elpher-address-from-gemini-url url)))
+           (if (> (length display-string) 0)
+               (elpher-insert-index-record display-string address)
+             (elpher-insert-index-record url address)))
+       (elpher-insert-index-record line)))
+   (elpher-restore-pos)
+   (elpher-cache-content
+    (elpher-node-address elpher-current-node)
+    (buffer-string))))
+
+(defun elpher-render--mimetype-text/plain (data parameters)
+  (elpher-with-clean-buffer
+   (insert (elpher-buttonify-urls (elpher-preprocess-text-response data)))
+   (elpher-restore-pos)
+   (elpher-cache-content
+    (elpher-node-address elpher-current-node)
+    (buffer-string))))
+
+(defun elpher-render--mimetype-image/* (data parameters)
+  (let ((image (create-image data nil t)))
+    (elpher-with-clean-buffer
+     (insert-image image)
+     (elpher-restore-pos))))
 
 
-;; HTML node retrieval
+(defun elpher-process-gemini-response (proc event)
+  (condition-case the-error
+      (unless (string-prefix-p "deleted" event)
+        (let ((response-code (car (split-string elpher-gemini-response-header)))
+              (meta (string-trim
+                     (substring elpher-gemini-response-header
+                                (string-match "[ \t]+"
+                                              elpher-gemini-response-header)))))
+          (pcase (elt response-code 0)
+            (?1 ; Input required
+             (elpher-with-clean-buffer
+              (insert "Gemini server is requesting input."))
+             (let* ((query-string (read-string (concat meta ": ")))
+                    (url (elpher-address-to-url (elpher-node-address elpher-current-node)))
+                    (query-address (elpher-address-from-url (concat url "?" query-string))))
+               (elpher-get-gemini query-address #'elpher-process-gemini-response)))
+            (?2 ; Normal response
+             (message elpher-gemini-response-header)
+             (elpher-render-gemini-response meta))
+            (?3 ; Redirect
+             (message "Following redirect to %s" meta)
+             (let ((redirect-address (elpher-address-from-gemini-url meta)))
+               (elpher-get-gemini redirect-address #'elpher-process-gemini-response)))
+            (?4 ; Temporary failure
+             (error "Gemini server reports TEMPORARY FAILURE for this request"))
+            (?5 ; Permanent failure
+             (error "Gemini server reports PERMANENT FAILURE for this request"))
+            (?6 ; Client certificate required
+             (error "Gemini server requires client certificate (unsupported at this time)"))
+            (other
+             (error "Gemini server responded with unknown response code %S"
+                    response-code)))))
+    (error
+     (elpher-network-error (elpher-node-address elpher-current-node) the-error))))
 
 
-(defun elpher-insert-rendered-html (string)
-  "Use shr to insert rendered view of html STRING into current buffer."
-  (let ((dom (with-temp-buffer
-               (insert string)
-               (libxml-parse-html-region (point-min) (point-max)))))
-    (shr-insert-document dom)))
 
 
-(defun elpher-get-html-node ()
-  "Getter which retrieves and renders an HTML node."
+(defun elpher-get-gemini-node (renderer)
+  "Getter which retrieves and renders a Gemini node."
   (let* ((address (elpher-node-address elpher-current-node))
   (let* ((address (elpher-node-address elpher-current-node))
-         (selector (elpher-gopher-address-selector address)))
-    (let ((content (elpher-get-cached-content address)))
-      (if content
-          (progn
-            (elpher-with-clean-buffer
-             (insert content)
-             (elpher-restore-pos)))
-        (elpher-with-clean-buffer
-         (insert "LOADING HTML... (use 'u' to cancel)"))
-        (elpher-get-selector address
-                             (lambda (proc event)
-                               (unless (string-prefix-p "deleted" event)
-                                 (elpher-with-clean-buffer
-                                  (elpher-insert-rendered-html elpher-selector-string)
-                                  (goto-char (point-min))
-                                  (elpher-cache-content
-                                   (elpher-node-address elpher-current-node)
-                                   (buffer-string))))))))))
+         (content (elpher-get-cached-content address)))
+    (condition-case the-error
+        (if content
+            (progn
+              (elpher-with-clean-buffer
+               (insert content)
+               (elpher-restore-pos)))
+          (elpher-with-clean-buffer
+           (insert "LOADING GEMINI... (use 'u' to cancel)"))
+          (elpher-get-gemini address #'elpher-process-gemini-response))
+      (error
+       (elpher-network-error address the-error)))))
 
 
 ;; Other URL node opening
 
 
 
 ;; Other URL node opening
 
-(defun elpher-get-other-url-node ()
-  "Getter which attempts to open the URL specified by the current node."
+(defun elpher-get-other-url-node (renderer)
+  "Getter which attempts to open the URL specified by the current node (RENDERER must be nil)."
+  (when renderer
+    (elpher-visit-parent-node)
+    (error "Command not supported for general URLs"))
   (let* ((address (elpher-node-address elpher-current-node))
          (url (elpher-address-to-url address)))
     (progn
   (let* ((address (elpher-node-address elpher-current-node))
          (url (elpher-address-to-url address)))
     (progn
@@ -771,8 +915,11 @@ up to the calling function."
 
 ;; Telnet node connection
 
 
 ;; Telnet node connection
 
-(defun elpher-get-telnet-node ()
-  "Getter which opens a telnet connection to the server specified by the current node."
+(defun elpher-get-telnet-node (renderer)
+  "Opens a telnet connection to the current node address (RENDERER must be nil)."
+  (when renderer
+    (elpher-visit-parent-node)
+    (error "Command not supported for telnet URLs"))
   (let* ((address (elpher-node-address elpher-current-node))
          (host (elpher-address-host address))
          (port (elpher-address-port address)))
   (let* ((address (elpher-node-address elpher-current-node))
          (host (elpher-address-host address))
          (port (elpher-address-port address)))
@@ -781,8 +928,11 @@ up to the calling function."
 
 ;; Start page node retrieval
 
 
 ;; Start page node retrieval
 
-(defun elpher-get-start-node ()
-  "Getter which displays the start page."
+(defun elpher-get-start-node (renderer)
+  "Getter which displays the start page (RENDERER must be nil)."
+  (when renderer
+    (elpher-visit-parent-node)
+    (error "Command not supported for start page."))
   (elpher-with-clean-buffer
    (insert "     --------------------------------------------\n"
            "                Elpher Gopher Client             \n"
   (elpher-with-clean-buffer
    (insert "     --------------------------------------------\n"
            "                Elpher Gopher Client             \n"
@@ -835,8 +985,11 @@ up to the calling function."
 
 ;; Bookmarks page node retrieval
 
 
 ;; Bookmarks page node retrieval
 
-(defun elpher-get-bookmarks-node ()
-  "Getter to load and display the current bookmark list."
+(defun elpher-get-bookmarks-node (renderer)
+  "Getter to load and display the current bookmark list (RENDERER must be nil)."
+  (when renderer
+    (elpher-visit-parent-node)
+    (error "Command not supported for bookmarks page."))
   (elpher-with-clean-buffer
    (insert "---- Bookmark list ----\n\n")
    (let ((bookmarks (elpher-load-bookmarks)))
   (elpher-with-clean-buffer
    (insert "---- Bookmark list ----\n\n")
    (let ((bookmarks (elpher-load-bookmarks)))
@@ -934,12 +1087,10 @@ If ADDRESS is already bookmarked, update the label only."
   (push-button))
 
 (defun elpher-go ()
   (push-button))
 
 (defun elpher-go ()
-  "Go to a particular gopher site read from the minibuffer.
-The site may be specified via a URL or explicitly in terms of
-host, selector and port."
+  "Go to a particular gopher site read from the minibuffer."
   (interactive)
   (let ((node
   (interactive)
   (let ((node
-         (let ((host-or-url (read-string "Gopher host or URL: ")))
+         (let ((host-or-url (read-string "Gopher or Gemini URL: ")))
            (elpher-make-node host-or-url
                              (elpher-address-from-url host-or-url)))))
     (switch-to-buffer "*elpher*")
            (elpher-make-node host-or-url
                              (elpher-address-from-url host-or-url)))))
     (switch-to-buffer "*elpher*")
@@ -951,7 +1102,7 @@ host, selector and port."
   (let ((address (elpher-node-address elpher-current-node)))
     (if (elpher-address-special-p address)
         (error "Command not valid for this page")
   (let ((address (elpher-node-address elpher-current-node)))
     (if (elpher-address-special-p address)
         (error "Command not valid for this page")
-      (let ((url (read-string "URL: " (elpher-address-to-url address))))
+      (let ((url (read-string "Gopher or Gemini URL: " (elpher-address-to-url address))))
         (elpher-visit-node (elpher-make-node url (elpher-address-from-url url)))))))
 
 (defun elpher-redraw ()
         (elpher-visit-node (elpher-make-node url (elpher-address-from-url url)))))))
 
 (defun elpher-redraw ()
@@ -1045,17 +1196,18 @@ host, selector and port."
 (defun elpher-root-dir ()
   "Visit root of current server."
   (interactive)
 (defun elpher-root-dir ()
   "Visit root of current server."
   (interactive)
-  (let* ((address (elpher-node-address elpher-current-node))
-         (host (elpher-address-host address)))
-    (if host
-        (let ((host (elpher-address-host address))
-              (selector (elpher-gopher-address-selector address))
-              (port (elpher-address-port address)))
-          (if (> (length selector) 0)
-              (let ((root-address (elpher-make-gopher-address ?1 "" host port)))
-                (elpher-visit-node
-                 (elpher-make-node (elpher-address-to-url root-address))))
-            (error "Already at root directory of current server")))
+  (let ((address (elpher-node-address elpher-current-node)))
+    (if (not (elpher-address-special-p address))
+        (if (or (member (url-filename address) '("/" ""))
+                (and (elpher-address-gopher-p address)
+                     (= (length (elpher-gopher-address-selector address)) 0)))
+            (error "Already at root directory of current server")
+          (let ((address-copy (elpher-address-from-url
+                               (elpher-address-to-url address))))
+            (setf (url-filename address-copy) "")
+            (elpher-visit-node
+             (elpher-make-node (elpher-address-to-url address-copy)
+                               address-copy))))
       (error "Command invalid for this page"))))
 
 (defun elpher-bookmarks-current-p ()
       (error "Command invalid for this page"))))
 
 (defun elpher-bookmarks-current-p ()
@@ -1210,7 +1362,6 @@ host, selector and port."
         (kbd "C-") 'elpher-follow-current-link
         (kbd "C-t") 'elpher-back
         (kbd "u") 'elpher-back
         (kbd "C-") 'elpher-follow-current-link
         (kbd "C-t") 'elpher-back
         (kbd "u") 'elpher-back
-        (kbd "O") 'elpher-root-dir
         (kbd "g") 'elpher-go
         (kbd "o") 'elpher-go-current
         (kbd "r") 'elpher-redraw
         (kbd "g") 'elpher-go
         (kbd "o") 'elpher-go-current
         (kbd "r") 'elpher-redraw
@@ -1234,7 +1385,7 @@ host, selector and port."
   "Keymap for gopher client.")
 
 (define-derived-mode elpher-mode special-mode "elpher"
   "Keymap for gopher client.")
 
 (define-derived-mode elpher-mode special-mode "elpher"
-  "Major mode for elpher, an elisp gopher client.
+  "Major mode for elpher, an elisp gopher client.)
 
 This mode is automatically enabled by the interactive
 functions which initialize the gopher client, namely
 
 This mode is automatically enabled by the interactive
 functions which initialize the gopher client, namely