+ (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))))))
+
+;; HTML node retrieval
+
+(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."
+ (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))))))))))
+
+;; Gemini node retrieval
+
+(defvar elpher-gemini-response)
+(defvar elpher-gemini-response-header)
+(defvar elpher-gemini-in-header)
+
+(defun elpher-gemini-response-code ()
+ (elt (split-string elpher-gemini-response-header) 0))
+
+(defun elpher-gemini-response-meta ()
+ (string-trim (substring elpher-gemini-response-header
+ (string-match "[ \t]+" elpher-gemini-response-header))))
+
+(defun elpher-get-gemini (address after)
+ "Retrieve gemini ADDRESS, then execute AFTER.
+The response header is stored in the variable ‘elpher-gemini-response-header’.
+If available, the response is stored in the variable ‘elpher-gemini-response’.
+
+Usually errors result in an error page being displayed. This is only
+appropriate if the selector is to be directly viewed. If PROPAGATE-ERROR
+is non-nil, this message is not displayed. Instead, the error propagates
+up to the calling function."
+ (setq elpher-gemini-response "")
+ (setq elpher-gemini-response-header "")
+ (setq elpher-gemini-in-header t)
+ (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)
+ (elpher-address-port address)
+ :type 'tls)))
+ (set-process-coding-system proc 'binary)
+ (set-process-filter proc
+ (lambda (proc string)
+ (if elpher-gemini-in-header
+ (progn
+ (setq elpher-gemini-response-header
+ (concat elpher-gemini-response-header
+ (elt (split-string string "\r\n") 0)))
+ (let ((idx (string-match "\r\n" string)))
+ (setq elpher-gemini-response
+ (substring string (+ idx 2)))
+ (setq elpher-gemini-in-header nil)))
+ (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-raw)
+ (let* ((mime-type-full (if (string-empty-p mime-type-raw)
+ "text/gemini; charset=utf-8"
+ mime-type-raw))
+ (mime-type-split (split-string mime-type-full ";"))
+ (mime-type (string-trim (elt mime-type-split 0)))
+ (parameters (if (> (length mime-type-split) 1)
+ (string-trim (elt mime-type-split 1))
+ "")))
+ ;; (message "MIME type %S with parameters %S" mime-type parameters)
+ (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)
+ (let ((address (url-generic-parse-url url)))
+ (unless (equal (url-type address) "mailto")
+ (setf (url-fullness address) t)
+ (unless (url-host address)
+ (setf (url-host address) (url-host (elpher-node-address elpher-current-node)))
+ (unless (string-prefix-p "/" (url-filename address))
+ (setf (url-filename address)
+ (concat (file-name-as-directory
+ (url-filename (elpher-node-address elpher-current-node)))
+ (url-filename address)))))
+ (unless (url-type address)
+ (setf (url-type address) "gemini"))
+ (unless (> (url-port address) 0)
+ (pcase (url-type address)
+ ("gemini" (setf (url-port address) 1965))
+ ("gopher" (setf (url-port address) 70)))))
+ address))
+
+(defun elpher-render--mimetype-text/gemini (data parameters)
+ (elpher-with-clean-buffer
+ (dolist (line (split-string (elpher-preprocess-text-response 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))))
+
+(defun elpher-process-gemini-response (proc event)
+ (condition-case the-error
+ (unless (string-prefix-p "deleted" event)
+ (let ((response-code (elpher-gemini-response-code))
+ (meta (elpher-gemini-response-meta)))
+ (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 ()
+ "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 ()
+ "Getter which attempts to open the URL specified by the current node."
+ (let* ((address (elpher-node-address elpher-current-node))
+ (url (elpher-address-to-url address)))
+ (progn
+ (elpher-visit-parent-node) ; Do first in case of non-local exits.
+ (message "Opening URL...")
+ (if elpher-open-urls-with-eww
+ (browse-web url)
+ (browse-url url)))))
+
+;; Telnet node connection
+
+(defun elpher-get-telnet-node ()
+ "Getter which opens a telnet connection to the server specified by the current node."
+ (let* ((address (elpher-node-address elpher-current-node))
+ (host (elpher-address-host address))
+ (port (elpher-address-port address)))
+ (elpher-visit-parent-node)
+ (telnet host port)))
+
+;; Start page node retrieval
+
+(defun elpher-get-start-node ()
+ "Getter which displays the start page."
+ (elpher-with-clean-buffer
+ (insert " --------------------------------------------\n"
+ " Elpher Gopher Client \n"
+ " version " elpher-version "\n"
+ " --------------------------------------------\n"
+ "\n"
+ "Default bindings:\n"
+ "\n"
+ " - TAB/Shift-TAB: next/prev item on current page\n"
+ " - RET/mouse-1: open item under cursor\n"
+ " - m: select an item on current page by name (autocompletes)\n"
+ " - u: return to previous page\n"
+ " - o/O: visit different selector or the root menu of the current server\n"
+ " - g: go to a particular gopher address\n"
+ " - i/I: info on item under cursor or current page\n"
+ " - c/C: copy URL representation of item under cursor or current page\n"
+ " - a/A: bookmark the item under cursor or current page\n"
+ " - x/X: remove bookmark for item under cursor or current page\n"
+ " - B: visit the bookmarks page\n"
+ " - r: redraw current page (using cached contents if available)\n"
+ " - R: reload current page (regenerates cache)\n"
+ " - T: toggle TLS mode\n"
+ " - d/D: download item under cursor or current page\n"
+ " - .: display the raw server response for the current page\n"
+ " - S: set an explicit character coding system (default is to autodetect)\n"
+ "\n"
+ "Start your exploration of gopher space:\n")
+ (elpher-insert-index-record "Floodgap Systems Gopher Server"
+ (elpher-make-gopher-address ?1 "" "gopher.floodgap.com" 70))
+ (insert "\n"
+ "Alternatively, select the following item and enter some search terms:\n")
+ (elpher-insert-index-record "Veronica-2 Gopher Search Engine"
+ (elpher-make-gopher-address ?7 "/v2/vs" "gopher.floodgap.com" 70))
+ (insert "\n"
+ "** Refer to the ")
+ (let ((help-string "RET,mouse-1: Open Elpher info manual (if available)"))
+ (insert-text-button "Elpher info manual"
+ 'face 'link
+ 'action (lambda (button)
+ (interactive)
+ (info "(elpher)"))
+ 'follow-link t
+ 'help-echo help-string))
+ (insert " for the full documentation. **\n")
+ (insert (propertize
+ (concat " (This should be available if you have installed Elpher using\n"
+ " MELPA. Otherwise you will have to install the manual yourself.)")
+ 'face 'shadow))
+ (elpher-restore-pos)))
+
+;; Bookmarks page node retrieval
+
+(defun elpher-get-bookmarks-node ()
+ "Getter to load and display the current bookmark list."
+ (elpher-with-clean-buffer
+ (insert "---- Bookmark list ----\n\n")
+ (let ((bookmarks (elpher-load-bookmarks)))
+ (if bookmarks
+ (dolist (bookmark bookmarks)
+ (let ((display-string (elpher-bookmark-display-string bookmark))
+ (address (elpher-address-from-url (elpher-bookmark-url bookmark))))
+ (elpher-insert-index-record display-string address)))
+ (insert "No bookmarks found.\n")))
+ (insert "\n-----------------------\n\n"
+ "- u: return to previous page\n"
+ "- x: delete selected bookmark\n"
+ "- a: rename selected bookmark\n\n"
+ "Bookmarks are stored in the file "
+ (locate-user-emacs-file "elpher-bookmarks"))
+ (elpher-restore-pos)))
+