Debugging after refactor: now functional.
[elpher.git] / elpher.el
index f0d84df..3735345 100644 (file)
--- a/elpher.el
+++ b/elpher.el
@@ -1,4 +1,4 @@
-;;; elpher.el --- A friendly gopher client.
+;;; elpher.el --- A friendly gopher client.  -*- lexical-binding:t -*-
 
 ;; Copyright (C) 2019 Tim Vaughan
 
@@ -428,9 +428,6 @@ away CRs and any terminating period."
                                            (replace-regexp-in-string "\r" "" string))))
 
 
-;;; Index rendering
-;;
-
 ;;; Network error reporting
 ;;
 
@@ -438,12 +435,12 @@ away CRs and any terminating period."
   (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"
+           (error-message-string error) ".\n"
            (propertize "\n----------------\n\n" 'face 'error)
            "Press 'u' to return to the previous page.")))
 
 
-;;; Gopher selector retrieval (all kinds)
+;;; Gopher selector retrieval
 ;;
 
 (defun elpher-process-cleanup ()
@@ -510,18 +507,20 @@ up to the calling function."
 (defun elpher-get-gopher-node (renderer)
   (let* ((address (elpher-node-address elpher-current-node))
          (content (elpher-get-cached-content address)))
-    (if (not (and content (funcall renderer nil content)))
+    (if (and content (funcall renderer nil))
+        (elpher-with-clean-buffer
+         (insert content)
+         (elpher-restore-pos))
       (elpher-with-clean-buffer
        (insert "LOADING... (use 'u' to cancel)"))
       (elpher-get-selector address
                            (lambda (proc event)
                              (unless (string-prefix-p "deleted" event)
-                               (funcall renderer elpher-selector-string))))))
-  (elpher-restore-pos))
+                               (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
@@ -565,8 +564,8 @@ If ADDRESS is not supplied or nil the record is rendered as an
   (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))
+        (let* ((margin-code (elt type-map-entry 2))
+               (face (elt type-map-entry 3))
                (node (elpher-make-node display-string address)))
           (elpher-insert-margin margin-code)
           (insert-text-button display-string
@@ -594,11 +593,11 @@ If ADDRESS is not supplied or nil the record is rendered as an
   (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."
+(defun elpher-render-index (data &optional mime-type-string)
+  "Render DATA as an index, MIME-TYPE-STRING is unused"
   (elpher-with-clean-buffer
-   (if cached-data
-       (progn (insert cached-data) t)
+   (if (not data)
+       t
      (elpher-insert-index data)
      (elpher-cache-content (elpher-node-address elpher-current-node)
                            (buffer-string)))))
@@ -625,24 +624,22 @@ If ADDRESS is not supplied or nil the record is rendered as an
                             'help-echo (elpher-node-button-help node))))
     (buffer-string)))
 
-(defun elpher-render-text (data &optional cached-data)
-  "Render DATA as text, using CACHED-DATA instead if supplied."
+(defun elpher-render-text (data &optional mime-type-string)
+  "Render DATA as text, MIME-TYPE-STRING is unused."
   (elpher-with-clean-buffer
-   (if cached-data
-       (progn (insert cached-data) t)
-     (insert (elpher-buttonify-urls
-              (elpher-preprocess-text-response)
-              elpher-selector-string))
+   (if (not data)
+       t
+     (insert (elpher-buttonify-urls (elpher-preprocess-text-response data)))
      (elpher-cache-content
       (elpher-node-address elpher-current-node)
       (buffer-string)))))
 
 ;; Image retrieval
 
-(defun elpher-render-image (data &optional cached-data)
-  "Display DATA as image, CACHED-DATA is ignored."
-  (if cached-data
-      f
+(defun elpher-render-image (data &optional mime-type-string)
+  "Display DATA as image, MIME-TYPE-STRING is unused."
+  (if (not data)
+      nil
     (if (display-images-p)
         (progn
           (let ((image (create-image
@@ -659,11 +656,11 @@ If ADDRESS is not supplied or nil the record is rendered as an
    (let* ((address (elpher-node-address elpher-current-node))
           (content (elpher-get-cached-content address))
           (aborted t))
-    (if content
-        (progn
-          (funcall renderer nil content)
-          (elpher-restore-pos)
-          (message "Displaying cached search results.  Reload to perform a new search."))
+    (if (and content (funcall renderer nil))
+        (elpher-with-clean-buffer
+         (insert 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))
@@ -686,21 +683,21 @@ If ADDRESS is not supplied or nil the record is rendered as an
  
 ;; Raw server response rendering
 
-(defun elpher-render-raw (data &optional cached-data)
-  "Display raw DATA in buffer. CACHED-DATA is ignored."
-  (if cached-data
-      f
+(defun elpher-render-raw (data &optional mime-type-string)
+  "Display raw DATA in buffer, MIME-TYPE-STRING is unused."
+  (if (not data)
+      nil
     (elpher-with-clean-buffer
      (insert data)
      (goto-char (point-min)))
     (message "Displaying raw server response.  Reload or redraw to return to standard view.")))
 
-;; File save renderer
+;; File save "rendering"
 
-(defun elpher-render-download (data &optional cached-data)
-  "Save DATA to file. CACHED-DATA is ignored."
-  (if cached-data
-      f
+(defun elpher-render-download (data &optional mime-type-string)
+  "Save DATA to file, MIME-TYPE-STRING is unused."
+  (if (not data)
+      nil
     (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.
@@ -709,31 +706,29 @@ If ADDRESS is not supplied or nil the record is rendered as an
                                        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)))))))
+                                         "download.file"))))
+        (let ((coding-system-for-write 'binary))
+          (with-temp-file filename
+            (insert data)))
+        (message (format "Saved to file %s." filename))))))
 
-;; HTML node retrieval
+;; HTML rendering
 
-(defun elpher-render-html (data &optional cached-data)
-  "Render DATA as HTML using shr, or insert CACHED-DATA if non-nil."
+(defun elpher-render-html (data &optional mime-type-string)
+  "Render DATA as HTML using shr, MIME-TYPE-STRING is unused."
   (elpher-with-clean-buffer
-   (if cached-data
-       (progn (insert cached-data) t)
+   (if (not 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)
+(defun elpher-get-gemini-response (address after)
   "Retrieve gemini ADDRESS, then execute AFTER.
 The response is stored in the variable ‘elpher-gemini-response’."
   (setq elpher-gemini-response "")
@@ -757,36 +752,109 @@ The response is stored in the variable ‘elpher-gemini-response’."
                            (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-process-gemini-response (renderer)
+  "Process the gemini response found in the variable `elpher-gemini-response' and
+pass the result to RENDERER."
+  (condition-case the-error
+      (let* ((response-header (car (split-string elpher-gemini-response "\r\n")))
+             (response-body (substring elpher-gemini-response
+                                       (+ (string-match "\r\n" elpher-gemini-response) 2)))
+             (response-code (car (split-string response-header)))
+             (response-meta (string-trim
+                             (substring response-header
+                                        (string-match "[ \t]+" 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 response-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-response query-address
+                                         (lambda (proc event)
+                                           (unless (string-prefix-p "deleted" event)
+                                             (funcall #'elpher-process-gemini-response
+                                                      renderer)
+                                             (elpher-restore-pos))))))
+          (?2 ; Normal response
+           (message response-header)
+           (funcall renderer response-body response-meta))
+          (?3 ; Redirect
+           (message "Following redirect to %s" response-meta)
+           (let ((redirect-address (elpher-address-from-gemini-url response-meta)))
+             (elpher-get-gemini-response redirect-address
+                                         (lambda (proc event)
+                                           (unless (string-prefix-p "deleted" event)
+                                             (funcall #'elpher-process-gemini-response
+                                                      renderer)
+                                             (elpher-restore-pos))))))
+          (?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-get-gemini-node (renderer)
+  "Getter which retrieves and renders a Gemini node and renders it using RENDERER."
+  (let* ((address (elpher-node-address elpher-current-node))
+         (content (elpher-get-cached-content address)))
+    (condition-case the-error
+        (if (and content (funcall renderer nil))
+            (elpher-with-clean-buffer
+              (insert content)
+              (elpher-restore-pos))
+          (elpher-with-clean-buffer
+           (insert "LOADING GEMINI... (use 'u' to cancel)"))
+          (elpher-get-gemini-response address
+                                      (lambda (proc event)
+                                        (unless (string-prefix-p "deleted" event)
+                                          (funcall #'elpher-process-gemini-response
+                                                   renderer)
+                                          (elpher-restore-pos)))))
+      (error
+       (elpher-network-error address the-error)))))
+
+
+(defun elpher-render-gemini (body &optional mime-type-string)
+  "Render gemini response BODY with rendering hints in META."
+  (if (not body)
+      t
+    (let* ((mime-type-string* (if (or (not mime-type-string)
+                                      (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-gemini-text/gemini body parameters))
+        ((pred (string-prefix-p "text/"))
+         (elpher-render-gemini-text/plain body parameters))
+        ((pred (string-prefix-p "image/"))
+         (elpher-render-image body))
+        (other
+         (error "Unsupported MIME type %S" mime-type))))))
 
 (defun elpher-gemini-get-link-url (line)
   (string-trim (elt (split-string (substring line 2)) 0)))
@@ -814,7 +882,7 @@ The response is stored in the variable ‘elpher-gemini-response’."
         (setf (url-type address) "gemini")))
     address))
 
-(defun elpher-render--mimetype-text/gemini (data parameters)
+(defun elpher-render-gemini-text/gemini (data parameters)
   (elpher-with-clean-buffer
    (dolist (line (split-string data "\n"))
      (if (string-prefix-p "=>" line)
@@ -825,78 +893,17 @@ The response is stored in the variable ‘elpher-gemini-response’."
                (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)
+(defun elpher-render-gemini-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))))
-
-(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-get-gemini-node (renderer)
-  "Getter which retrieves and renders a Gemini node."
-  (let* ((address (elpher-node-address elpher-current-node))
-         (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
 
 (defun elpher-get-other-url-node (renderer)
@@ -1101,7 +1108,7 @@ If ADDRESS is already bookmarked, update the label only."
   (interactive)
   (let ((address (elpher-node-address elpher-current-node)))
     (if (elpher-address-special-p address)
-        (error "Command not valid for this page")
+        (error "Command invalid for this page")
       (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)))))))
 
@@ -1137,7 +1144,7 @@ If ADDRESS is already bookmarked, update the label only."
       (if (elpher-address-special-p (elpher-node-address elpher-current-node))
           (error "This page was not generated by a server")
         (elpher-visit-node elpher-current-node
-                           #'elpher-get-node-raw))
+                           #'elpher-render-raw))
     (message "No current site.")))
 
 (defun elpher-back ()
@@ -1154,21 +1161,23 @@ If ADDRESS is already bookmarked, update the label only."
     (if button
         (let ((node (button-get button 'elpher-node)))
           (if (elpher-address-special-p (elpher-node-address node))
-              (error "Cannot download this link")
+              (error "Cannot download %s"
+                     (elpher-node-display-string node))
             (elpher-visit-node (button-get button 'elpher-node)
-                               #'elpher-get-node-download)))
+                               #'elpher-render-download)))
       (error "No link selected"))))
 
 (defun elpher-download-current ()
   "Download the current page."
   (interactive)
   (if (elpher-address-special-p (elpher-node-address elpher-current-node))
-      (error "Cannot download this page")
+      (error "Cannot download %s"
+             (elpher-node-display-string elpher-current-node))
     (elpher-visit-node (elpher-make-node
                         (elpher-node-display-string elpher-current-node)
                         (elpher-node-address elpher-current-node)
                         elpher-current-node)
-                       #'elpher-get-node-download
+                       #'elpher-render-download
                        t)))
 
 (defun elpher-build-link-map ()
@@ -1208,7 +1217,7 @@ If ADDRESS is already bookmarked, update the label only."
             (elpher-visit-node
              (elpher-make-node (elpher-address-to-url address-copy)
                                address-copy))))
-      (error "Command invalid for this page"))))
+      (error "Command invalid for %s" (elpher-node-display-string elpher-current-node)))))
 
 (defun elpher-bookmarks-current-p ()
   "Return non-nil if current node is a bookmarks page."
@@ -1356,8 +1365,8 @@ If ADDRESS is already bookmarked, update the label only."
     (define-key map (kbd "X") 'elpher-unbookmark-current)
     (define-key map (kbd "B") 'elpher-bookmarks)
     (define-key map (kbd "S") 'elpher-set-coding-system)
-    (when (fboundp 'evil-define-key)
-      (evil-define-key 'motion map
+    (when (fboundp 'evil-mode)
+      (evil-define-key* 'motion map
         (kbd "TAB") 'elpher-next-link
         (kbd "C-") 'elpher-follow-current-link
         (kbd "C-t") 'elpher-back
@@ -1385,7 +1394,7 @@ If ADDRESS is already bookmarked, update the label only."
   "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