X-Git-Url: https://thelambdalab.xyz/gitweb/index.cgi?a=blobdiff_plain;f=elpher.el;h=d1d795c649e42237bb40ac26129a2b48cda4bcb5;hb=4796bc48d4083ccab4be79075b15ea70acdc2fa0;hp=531b2da071c23a68cadd8d26a6b80b4b06bd6db9;hpb=14f8d5cd99838da14b292fe4992ed0a4aa70af41;p=elpher.git diff --git a/elpher.el b/elpher.el index 531b2da..d1d795c 100644 --- a/elpher.el +++ b/elpher.el @@ -1,10 +1,10 @@ -;;; elpher.el --- Full-featured gopher client. +;;; elpher.el --- A friendly gopher client. ;; Copyright (C) 2019 Tim Vaughan ;; Author: Tim Vaughan ;; Created: 11 April 2019 -;; Version: 1.2.0 +;; Version: 1.2.4 ;; Keywords: comm gopher ;; Homepage: https://github.com/tgvaughan/elpher ;; Package-Requires: ((emacs "25")) @@ -26,7 +26,7 @@ ;;; Commentary: -;; Elpher aims to provide a full-featured gopher client for GNU Emacs. +;; Elpher aims to provide a practical gopher client for GNU Emacs. ;; It supports: ;; - intuitive keyboard and mouse-driven interface, @@ -34,7 +34,8 @@ ;; - pleasant and configurable colouring of Gopher directories, ;; - direct visualisation of image files, ;; - (m)enu key support, similar to Emacs' info browser, -;; - clickable web and gopher links in plain text. +;; - clickable web and gopher links in plain text, +;; - a simple bookmark management system. ;; Visited pages are stored as a hierarchy rather than a linear history, ;; meaning that navigation between these pages is quick and easy. @@ -43,7 +44,7 @@ ;; page containing information on key bindings and suggested starting ;; points for your gopher exploration. -;; Faces, caching options and start page can be configured via +;; Faces, caching and other options can be configured via ;; the Elpher customization group in Applications. ;;; Code: @@ -51,11 +52,12 @@ (provide 'elpher) (require 'seq) (require 'pp) +(require 'shr) ;;; Global constants ;; -(defconst elpher-version "1.2.0" +(defconst elpher-version "1.2.4" "Current version of elpher.") (defconst elpher-margin-width 6 @@ -101,17 +103,18 @@ "Source for elpher start page.") (defconst elpher-type-map - '((?0 elpher-get-text-node "T" elpher-text) + '((?0 elpher-get-text-node "txt" elpher-text) (?1 elpher-get-index-node "/" elpher-index) - (?4 elpher-get-node-download "B" elpher-binary) - (?5 elpher-get-node-download "B" elpher-binary) + (?4 elpher-get-node-download "bin" elpher-binary) + (?5 elpher-get-node-download "bin" elpher-binary) (?7 elpher-get-search-node "?" elpher-search) - (?8 elpher-get-telnet-node "?" elpher-telnet) - (?9 elpher-get-node-download "B" elpher-binary) - (?g elpher-get-image-node "im" elpher-image) - (?p elpher-get-image-node "im" elpher-image) - (?I elpher-get-image-node "im" elpher-image) - (?h elpher-get-url-node "W" elpher-url) + (?8 elpher-get-telnet-node "tel" elpher-telnet) + (?9 elpher-get-node-download "bin" elpher-binary) + (?g elpher-get-image-node "img" elpher-image) + (?p elpher-get-image-node "img" elpher-image) + (?I elpher-get-image-node "img" elpher-image) + (?d elpher-get-node-download "doc" elpher-binary) + (?h elpher-get-url-node "web" elpher-url) (bookmarks elpher-get-bookmarks-node "#" elpher-index) (start elpher-get-start-node "#" elpher-index)) "Association list from types to getters, margin codes and index faces.") @@ -217,31 +220,40 @@ special address types, such as 'start for the start page." (elt address 3)) (defun elpher-address-special-p (address) + "Return non-nil if ADDRESS is special (e.g. start page, bookmarks page)." (not (elpher-address-host address))) ;; Node -(defun elpher-make-node (display-string parent address) +(defun elpher-make-node (display-string address &optional parent) "Create a node in the gopher page hierarchy. DISPLAY-STRING records the display string used for the page. -PARENT specifies the parent of the node, and ADDRESS specifies the -address of the gopher page." - (list display-string parent address)) +ADDRESS specifies the address of the gopher page. + +The optional PARENT specifies the parent node in the hierarchy. +This is set every time the node is visited, so while it forms +an important part of the node data there is no need to set it +initially." + (list display-string address parent)) (defun elpher-node-display-string (node) "Retrieve the display string of NODE." (elt node 0)) -(defun elpher-node-parent (node) - "Retrieve the parent node of NODE." - (elt node 1)) - (defun elpher-node-address (node) "Retrieve the address of NODE." + (elt node 1)) + +(defun elpher-node-parent (node) + "Retrieve the parent node of NODE." (elt node 2)) +(defun elpher-set-node-parent (node parent) + "Set the parent node of NODE to be PARENT." + (setcar (cdr (cdr node)) parent)) + ;; Cache (defvar elpher-content-cache (make-hash-table :test 'equal)) @@ -267,10 +279,18 @@ address of the gopher page." (defvar elpher-current-node nil) -(defun elpher-visit-node (node &optional getter) - "Visit NODE using its own getter or GETTER, if non-nil." +(defun elpher-visit-node (node &optional getter preserve-parent) + "Visit NODE using its own getter or GETTER, if non-nil. +Additionally, set the parent of NODE to `elpher-current-node', +unless PRESERVE-PARENT is non-nil." (elpher-save-pos) (elpher-process-cleanup) + (unless preserve-parent + (if (and (elpher-node-parent elpher-current-node) + (equal (elpher-node-address elpher-current-node) + (elpher-node-address 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) @@ -282,7 +302,7 @@ address of the gopher page." "Visit the parent of the current node." (let ((parent-node (elpher-node-parent elpher-current-node))) (when parent-node - (elpher-visit-node parent-node)))) + (elpher-visit-node parent-node nil t)))) (defun elpher-reload-current-node () "Reload the current node, discarding any existing cached content." @@ -378,7 +398,7 @@ and PORT." (if type-map-entry (let* ((margin-code (elt type-map-entry 1)) (face (elt type-map-entry 2)) - (node (elpher-make-node display-string elpher-current-node address))) + (node (elpher-make-node display-string address))) (elpher-insert-margin margin-code) (insert-text-button display-string 'face face @@ -397,7 +417,7 @@ and PORT." (other ;; Unknown (elpher-insert-margin (concat (char-to-string type) "?")) (insert (propertize display-string - 'face 'elpher-unknown-face))))) + 'face 'elpher-unknown))))) (insert "\n"))) (defun elpher-click-link (button) @@ -420,15 +440,23 @@ and PORT." "Retrieve selector specified by ADDRESS, then execute AFTER. The result is stored as a string in the variable ‘elpher-selector-string’." (setq elpher-selector-string "") - (make-network-process - :name "elpher-process" - :host (elpher-address-host address) - :service (elpher-address-port address) - :filter (lambda (proc string) - (setq elpher-selector-string (concat elpher-selector-string string))) - :sentinel after) - (process-send-string "elpher-process" - (concat (elpher-address-selector address) "\n"))) + (condition-case nil + (progn + (make-network-process :name "elpher-process" + :host (elpher-address-host address) + :service (elpher-address-port address) + :filter (lambda (proc string) + (setq elpher-selector-string + (concat elpher-selector-string string))) + :sentinel after) + (process-send-string "elpher-process" + (concat (elpher-address-selector address) "\n"))) + (error + (elpher-with-clean-buffer + (insert (propertize "\n---- ERROR -----\n\n" 'face 'error) + "Failed to connect to " (elpher-get-address-url address) ".\n" + (propertize "\n----------------\n\n" 'face 'error) + "Press 'u' to return to the previous page."))))) ;; Index retrieval @@ -442,7 +470,7 @@ The result is stored as a string in the variable ‘elpher-selector-string’." (insert content) (elpher-restore-pos))) (elpher-with-clean-buffer - (insert "LOADING DIRECTORY...")) + (insert "LOADING DIRECTORY... (use 'u' to cancel)")) (elpher-get-selector address (lambda (proc event) (unless (string-prefix-p "deleted" event) @@ -459,11 +487,9 @@ The result is stored as a string in the variable ‘elpher-selector-string’." "\\([a-zA-Z]+\\)://\\([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-make-node-from-matched-url (parent &optional string) +(defun elpher-make-node-from-matched-url (&optional string) "Convert most recent `elpher-url-regex' match to a node. -PARENT defines the node to set as the parent to the new node. - If STRING is non-nil, this is given as an argument to all `match-string' calls, as is necessary if the match is performed by `string-match'." (let ((url (match-string 0 string)) @@ -481,14 +507,14 @@ calls, as is necessary if the match is performed by `string-match'." (substring type-and-selector 2) "")) (address (elpher-make-address type selector host port))) - (elpher-make-node url elpher-current-node address)) + (elpher-make-node url address)) (let* ((host (match-string 2 string)) (port (if (> (length (match-string 3 string)) 1) (string-to-number (substring (match-string 3 string) 1)) 70)) (selector (concat "URL:" url)) (address (elpher-make-address ?h selector host port))) - (elpher-make-node url elpher-current-node address))))) + (elpher-make-node url address))))) (defun elpher-buttonify-urls (string) @@ -497,7 +523,7 @@ calls, as is necessary if the match is performed by `string-match'." (insert string) (goto-char (point-min)) (while (re-search-forward elpher-url-regex nil t) - (let ((node (elpher-make-node-from-matched-url elpher-current-node))) + (let ((node (elpher-make-node-from-matched-url))) (make-text-button (match-beginning 0) (match-end 0) 'elpher-node node @@ -517,7 +543,7 @@ calls, as is necessary if the match is performed by `string-match'." (elpher-restore-pos))) (progn (elpher-with-clean-buffer - (insert "LOADING TEXT...")) + (insert "LOADING TEXT... (use 'u' to cancel)")) (elpher-get-selector address (lambda (proc event) (unless (string-prefix-p "deleted" event) @@ -544,7 +570,7 @@ calls, as is necessary if the match is performed by `string-match'." (if (display-images-p) (progn (elpher-with-clean-buffer - (insert "LOADING IMAGE...")) + (insert "LOADING IMAGE... (use 'u' to cancel)")) (elpher-get-selector address (lambda (proc event) (unless (string-prefix-p "deleted" event) @@ -584,7 +610,7 @@ calls, as is necessary if the match is performed by `string-match'." (elpher-address-port address)))) (setq aborted nil) (elpher-with-clean-buffer - (insert "LOADING RESULTS...")) + (insert "LOADING RESULTS... (use 'u' to cancel)")) (elpher-get-selector search-address (lambda (proc event) (unless (string-prefix-p "deleted" event) @@ -603,7 +629,7 @@ calls, as is necessary if the match is performed by `string-match'." "Getter which retrieves the raw server response for the current node." (let ((address (elpher-node-address elpher-current-node))) (elpher-with-clean-buffer - (insert "LOADING RAW SERVER RESPONSE...")) + (insert "LOADING RAW SERVER RESPONSE... (use 'u' to cancel)")) (if address (elpher-get-selector address (lambda (proc event) @@ -644,15 +670,42 @@ calls, as is necessary if the match is performed by `string-match'." ;; URL 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-url-node () "Getter which attempts to open the URL specified by the current node." (let* ((address (elpher-node-address elpher-current-node)) (selector (elpher-address-selector address))) - (elpher-visit-parent-node) ; Do first in case of non-local exits. (let ((url (elt (split-string selector "URL:") 1))) - (if elpher-open-urls-with-eww - (browse-web url) - (browse-url url))))) + (if url + (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))) + (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)))))))))))) ;; Telnet node connection @@ -675,7 +728,7 @@ calls, as is necessary if the match is performed by `string-match'." ;; Bookmarks page node retrieval (defun elpher-get-bookmarks-node () - "Getter which loads and displays the current bookmark list." + "Getter to load and display the current bookmark list." (elpher-with-clean-buffer (insert "---- Bookmark list ----\n\n") (let ((bookmarks (elpher-load-bookmarks))) @@ -690,9 +743,11 @@ calls, as is necessary if the match is performed by `string-match'." (elpher-address-port 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") + "- 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))) @@ -722,6 +777,10 @@ bookmark list, while ADDRESS is the address of the entry." Beware that this completely replaces the existing contents of the file." (with-temp-file (locate-user-emacs-file "elpher-bookmarks") (erase-buffer) + (insert "; Elpher gopher bookmarks file\n\n" + "; Bookmarks are stored as a list of (label (type selector host port))\n" + "; s-expressions, where type is stored as a character (i.e. 49 = ?1).\n" + "; Feel free to edit by hand, but ensure this structure remains intact.\n\n") (pp bookmarks (current-buffer)))) (defun elpher-load-bookmarks () @@ -775,14 +834,12 @@ host, selector and port." (let ((node (let ((host-or-url (read-string "Gopher host or URL: "))) (if (string-match elpher-url-regex host-or-url) - (elpher-make-node-from-matched-url elpher-current-node - host-or-url) + (elpher-make-node-from-matched-url host-or-url) (let ((selector (read-string "Selector (default none): " nil nil "")) (port-string (read-string "Port (default 70): " nil nil "70"))) (elpher-make-node (concat "gopher://" host-or-url ":" port-string "/1" selector) - elpher-current-node (elpher-make-address ?1 selector host-or-url (string-to-number port-string)))))))) (switch-to-buffer "*elpher*") @@ -807,7 +864,7 @@ host, selector and port." (interactive) (if elpher-current-node (if (elpher-address-special-p (elpher-node-address elpher-current-node)) - (error "This page was not generated by a server.") + (error "This page was not generated by a server") (elpher-visit-node elpher-current-node #'elpher-get-node-raw)) (message "No current site."))) @@ -868,7 +925,6 @@ host, selector and port." (elpher-make-node (concat "gopher://" host ":" (number-to-string port) "/1/") - elpher-current-node root-address))) (error "Already at root directory of current server"))) (error "Command invalid for this page")))) @@ -927,10 +983,9 @@ host, selector and port." (defun elpher-bookmarks () "Visit bookmarks." (interactive) + (switch-to-buffer "*elpher*") (elpher-visit-node - (elpher-make-node "Bookmarks" - elpher-current-node - (elpher-make-address 'bookmarks)))) + (elpher-make-node "Bookmarks" (elpher-make-address 'bookmarks)))) (defun elpher-info-node (node) "Display information on NODE." @@ -958,14 +1013,20 @@ host, selector and port." (defun elpher-get-address-url (address) "Get URL representation of ADDRESS." - (concat "gopher://" - (elpher-address-host address) - (let ((port (elpher-address-port address))) - (if (equal port 70) - "" - (format ":%d" port))) - "/" (string (elpher-address-type address)) - (elpher-address-selector address))) + (let ((type (elpher-address-type address)) + (selector (elpher-address-selector address)) + (host (elpher-address-host address)) + (port (elpher-address-port address))) + (if (and (equal type ?h) + (string-prefix-p "URL:" selector)) + (elt (split-string selector "URL:") 1) + (concat "gopher://" + host + (if (equal port 70) + "" + (format ":%d" port)) + "/" (string type) + selector)))) (defun elpher-copy-node-url (node) "Copy URL representation of address of NODE to `kill-ring'." @@ -1055,7 +1116,8 @@ host, selector and port." (switch-to-buffer "*elpher*") (switch-to-buffer "*elpher*") (setq elpher-current-node nil) - (let ((start-node (elpher-make-node "Elpher Start Page" nil (elpher-make-address 'start)))) + (let ((start-node (elpher-make-node "Elpher Start Page" + (elpher-make-address 'start)))) (elpher-visit-node start-node))) "Started Elpher.") ; Otherwise (elpher) evaluates to start page string.