X-Git-Url: https://thelambdalab.xyz/gitweb/index.cgi?a=blobdiff_plain;f=elpher.el;h=4b08a503845a23b1fa9acc5deb572edebd70a0e1;hb=eb3e211429970e06f91443e6193fb33c5b237998;hp=0aaa06c54b0dce1437ad9db9a079f78009f6ca88;hpb=996f1bc282d90bff5a1b363230c85d63d6880ca4;p=elpher.git diff --git a/elpher.el b/elpher.el index 0aaa06c..4b08a50 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.1.0 +;; Version: 1.2.0 ;; 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.1.0" +(defconst elpher-version "1.2.0" "Current version of elpher.") (defconst elpher-margin-width 6 @@ -111,6 +113,7 @@ (?g elpher-get-image-node "im" elpher-image) (?p elpher-get-image-node "im" elpher-image) (?I elpher-get-image-node "im" elpher-image) + (?d elpher-get-node-download "d" elpher-binary) (?h elpher-get-url-node "W" elpher-url) (bookmarks elpher-get-bookmarks-node "#" elpher-index) (start elpher-get-start-node "#" elpher-index)) @@ -216,6 +219,10 @@ special address types, such as 'start for the start page." "Retrieve port from ADDRESS." (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) @@ -373,7 +380,7 @@ and PORT." (let ((address (elpher-make-address type selector host port)) (type-map-entry (alist-get type elpher-type-map))) (if type-map-entry - (let* ((margin-code (cadr 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))) (elpher-insert-margin margin-code) @@ -394,7 +401,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) @@ -417,15 +424,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 @@ -439,7 +454,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) @@ -514,7 +529,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) @@ -541,7 +556,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) @@ -581,7 +596,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) @@ -600,7 +615,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) @@ -641,15 +656,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 @@ -672,10 +714,9 @@ 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 "Use 'u' to return to the previous page.\n\n" - "---- Bookmark list ----\n\n") + (insert "---- Bookmark list ----\n\n") (let ((bookmarks (elpher-load-bookmarks))) (if bookmarks (dolist (bookmark bookmarks) @@ -687,7 +728,12 @@ calls, as is necessary if the match is performed by `string-match'." (elpher-address-host address) (elpher-address-port address)))) (insert "No bookmarks found.\n"))) - (insert "\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))) @@ -704,6 +750,10 @@ bookmark list, while ADDRESS is the address of the entry." "Get the display string of BOOKMARK." (elt bookmark 0)) +(defun elpher-set-bookmark-display-string (bookmark display-string) + "Set the display string of BOOKMARK to DISPLAY-STRING." + (setcar bookmark display-string)) + (defun elpher-bookmark-address (bookmark) "Get the address for BOOKMARK." (elt bookmark 1)) @@ -713,6 +763,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 () @@ -723,23 +777,22 @@ Beware that this completely replaces the existing contents of the file." (goto-char (point-min)) (read (current-buffer))))) -(defun elpher-add-node-bookmark (node) - "Add bookmark to NODE to the saved list of bookmarks." - (let ((bookmark (elpher-make-bookmark (elpher-node-display-string node) - (elpher-node-address node))) - (bookmarks (elpher-load-bookmarks))) - (add-to-list 'bookmarks bookmark) +(defun elpher-add-address-bookmark (address display-string) + "Save a bookmark for ADDRESS with label DISPLAY-STRING. +If ADDRESS is already bookmarked, update the label only." + (let ((bookmarks (elpher-load-bookmarks))) + (let ((existing-bookmark (rassoc (list address) bookmarks))) + (if existing-bookmark + (elpher-set-bookmark-display-string existing-bookmark display-string) + (add-to-list 'bookmarks (elpher-make-bookmark display-string address)))) (elpher-save-bookmarks bookmarks))) -(defun elpher-remove-node-bookmark (node) - "Remove bookmark to NODE from the saved list of bookmarks." - (let ((bookmark (elpher-make-bookmark (elpher-node-display-string node) - (elpher-node-address node)))) +(defun elpher-remove-address-bookmark (address) + "Remove any bookmark to ADDRESS." (elpher-save-bookmarks - (seq-filter (lambda (this-bookmark) - (not (equal bookmark this-bookmark))) - (elpher-load-bookmarks))))) - + (seq-filter (lambda (bookmark) + (not (equal (elpher-bookmark-address bookmark) address))) + (elpher-load-bookmarks)))) ;;; Interactive procedures ;; @@ -760,7 +813,9 @@ Beware that this completely replaces the existing contents of the file." (push-button)) (defun elpher-go () - "Go to a particular gopher site." + "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." (interactive) (let ((node (let ((host-or-url (read-string "Gopher host or URL: "))) @@ -793,11 +848,13 @@ Beware that this completely replaces the existing contents of the file." (message "No current site."))) (defun elpher-view-raw () - "View current page as plain text." + "View raw server response for current page." (interactive) (if elpher-current-node - (elpher-visit-node elpher-current-node - #'elpher-get-node-raw) + (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)) (message "No current site."))) (defun elpher-back () @@ -813,10 +870,10 @@ Beware that this completely replaces the existing contents of the file." (let ((button (button-at (point)))) (if button (let ((node (button-get button 'elpher-node))) - (if node - (elpher-visit-node (button-get button 'elpher-node) - #'elpher-get-node-download) - (error "Can only download gopher links, not general URLs"))) + (if (elpher-address-special-p (elpher-node-address node)) + (error "Cannot download this link") + (elpher-visit-node (button-get button 'elpher-node) + #'elpher-get-node-download))) (error "No link selected")))) (defun elpher-build-link-map () @@ -834,7 +891,7 @@ Beware that this completely replaces the existing contents of the file." (let* ((link-map (elpher-build-link-map))) (if link-map (let ((key (let ((completion-ignore-case t)) - (completing-read "Directory entry/link (tab to autocomplete): " + (completing-read "Directory item/link: " link-map nil t)))) (if (and key (> (length key) 0)) (let ((b (cdr (assoc key link-map)))) @@ -862,7 +919,7 @@ Beware that this completely replaces the existing contents of the file." (error "Command invalid for this page")))) (defun elpher-bookmarks-current-p () - "Return true if current node is a bookmarks page." + "Return non-nil if current node is a bookmarks page." (eq (elpher-address-type (elpher-node-address elpher-current-node)) 'bookmarks)) (defun elpher-reload-bookmarks () @@ -873,38 +930,49 @@ Beware that this completely replaces the existing contents of the file." (defun elpher-bookmark-current () "Bookmark the current node." (interactive) - (if (not (elpher-bookmarks-current-p)) - (elpher-add-node-bookmark elpher-current-node))) + (unless (elpher-bookmarks-current-p) + (let ((address (elpher-node-address elpher-current-node)) + (display-string (read-string "Bookmark display string: " + (elpher-node-display-string elpher-current-node)))) + (elpher-add-address-bookmark address display-string) + (message "Bookmark added.")))) (defun elpher-bookmark-link () "Bookmark the link at point." (interactive) (let ((button (button-at (point)))) (if button - (progn - (elpher-add-node-bookmark (button-get button 'elpher-node)) - (elpher-reload-bookmarks)) + (let* ((node (button-get button 'elpher-node)) + (address (elpher-node-address node)) + (display-string (read-string "Bookmark display string: " + (elpher-node-display-string node)))) + (elpher-add-address-bookmark address display-string) + (elpher-reload-bookmarks) + (message "Bookmark added.")) (error "No link selected")))) (defun elpher-unbookmark-current () "Remove bookmark for the current node." (interactive) - (if (not (elpher-bookmarks-current-p)) - (elpher-remove-node-bookmark elpher-current-node))) + (unless (elpher-bookmarks-current-p) + (elpher-remove-address-bookmark (elpher-node-address elpher-current-node)) + (message "Bookmark removed."))) (defun elpher-unbookmark-link () "Remove bookmark for the link at point." (interactive) (let ((button (button-at (point)))) (if button - (progn - (elpher-remove-node-bookmark (button-get button 'elpher-node)) - (elpher-reload-bookmarks)) + (let ((node (button-get button 'elpher-node))) + (elpher-remove-address-bookmark (elpher-node-address node)) + (elpher-reload-bookmarks) + (message "Bookmark removed.")) (error "No link selected")))) (defun elpher-bookmarks () "Visit bookmarks." (interactive) + (switch-to-buffer "*elpher*") (elpher-visit-node (elpher-make-node "Bookmarks" elpher-current-node