X-Git-Url: https://thelambdalab.xyz/gitweb/index.cgi?a=blobdiff_plain;f=elpher.el;h=531b2da071c23a68cadd8d26a6b80b4b06bd6db9;hb=refs%2Ftags%2Fv1.2.0;hp=94ba3be1e91a60073260d018b2c8bed2f8f9029b;hpb=e11e54a48e2b343228784eee21e8258fb3335f33;p=elpher.git diff --git a/elpher.el b/elpher.el index 94ba3be..531b2da 100644 --- a/elpher.el +++ b/elpher.el @@ -4,7 +4,7 @@ ;; 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")) @@ -55,7 +55,7 @@ ;;; Global constants ;; -(defconst elpher-version "1.1.0" +(defconst elpher-version "1.2.0" "Current version of elpher.") (defconst elpher-margin-width 6 @@ -80,14 +80,17 @@ "i - g: go to a particular menu or item\tfake\tfake\t1" "i - i/I: info on item under cursor or current page\tfake\tfake\t1" "i - c/C: copy URL representation of item under cursor or current page\tfake\tfake\t1" + "i - a/A: bookmark the item under cursor or current page\tfake\tfake\t1" + "i - x/X: remove bookmark for item under cursor or current page\tfake\tfake\t1" + "i - B: visit the bookmarks page\tfake\tfake\t1" "i - r: redraw current page (using cached contents if available)\tfake\tfake\t1" "i - R: reload current page (regenerates cache)\tfake\tfake\t1" "i - d: download directory entry under cursor\tfake\tfake\t1" "i - w: display the raw server response for the current page\tfake\tfake\t1" "i\tfake\tfake\t1" - "iPlaces to start exploring Gopherspace:\tfake\tfake\t1" + "iWhere to start exploring Gopherspace:\tfake\tfake\t1" "i\tfake\tfake\t1" - "1Floodgap Systems Gopher Server\t\tgopher.floodgap.com\t70" + "1Floodgap Systems Gopher Server\t/\tgopher.floodgap.com\t70" "i\tfake\tfake\t1" "iAlternatively, select the following item and enter some\tfake\tfake\t1" "isearch terms:\tfake\tfake\t1" @@ -182,12 +185,6 @@ Otherwise, use the system browser via the BROWSE-URL function." "If non-nil, cache images in memory in the same way as other content." :type '(boolean)) -(defcustom elpher-start-address nil - "If nil, the default start directory is shown when Elpher is started. -Otherwise, a list containing the selector, host and port of a directory to -use as the start page." - :type '(list string string integer)) - (defcustom elpher-use-header t "If non-nil, display current node information in buffer header." :type '(boolean)) @@ -219,19 +216,19 @@ special address types, such as 'start for the start page." "Retrieve port from ADDRESS." (elt address 3)) +(defun elpher-address-special-p (address) + (not (elpher-address-host address))) + ;; Node -(defun elpher-make-node (display-string parent address &optional content pos) +(defun elpher-make-node (display-string parent address) "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. - -The optional arguments CONTENT and POS can be used to fill the cached -content and cursor position fields of the node." - (list display-string parent address content pos)) +address of the gopher page." + (list display-string parent address)) (defun elpher-node-display-string (node) "Retrieve the display string of NODE." @@ -245,21 +242,26 @@ content and cursor position fields of the node." "Retrieve the address of NODE." (elt node 2)) -(defun elpher-node-content (node) - "Retrieve the cached content of NODE, or nil if none exists." - (elt node 3)) +;; Cache -(defun elpher-node-pos (node) - "Retrieve the cached cursor position for NODE, or nil if none exists." - (elt node 4)) +(defvar elpher-content-cache (make-hash-table :test 'equal)) +(defvar elpher-pos-cache (make-hash-table :test 'equal)) -(defun elpher-set-node-content (node content) - "Set the content cache of NODE to CONTENT." - (setcar (nthcdr 3 node) content)) +(defun elpher-get-cached-content (address) + "Retrieve the cached content for ADDRESS, or nil if none exists." + (gethash address elpher-content-cache)) -(defun elpher-set-node-pos (node pos) - "Set the cursor position cache of NODE to POS." - (setcar (nthcdr 4 node) pos)) +(defun elpher-cache-content (address content) + "Set the content cache for ADDRESS to CONTENT." + (puthash address content elpher-content-cache)) + +(defun elpher-get-cached-pos (address) + "Retrieve the cached cursor position for ADDRESS, or nil if none exists." + (gethash address elpher-pos-cache)) + +(defun elpher-cache-pos (address pos) + "Set the cursor position cache for ADDRESS to POS." + (puthash address pos elpher-pos-cache)) ;; Node graph traversal @@ -284,17 +286,17 @@ content and cursor position fields of the node." (defun elpher-reload-current-node () "Reload the current node, discarding any existing cached content." - (elpher-set-node-content elpher-current-node nil) + (elpher-cache-content (elpher-node-address elpher-current-node) nil) (elpher-visit-node elpher-current-node)) (defun elpher-save-pos () "Save the current position of point to the current node." (when elpher-current-node - (elpher-set-node-pos elpher-current-node (point)))) + (elpher-cache-pos (elpher-node-address elpher-current-node) (point)))) (defun elpher-restore-pos () "Restore the position of point to that cached in the current node." - (let ((pos (elpher-node-pos elpher-current-node))) + (let ((pos (elpher-get-cached-pos (elpher-node-address elpher-current-node)))) (if pos (goto-char pos) (goto-char (point-min))))) @@ -374,8 +376,8 @@ 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)) - (face (caddr 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) (insert-text-button display-string @@ -432,8 +434,8 @@ The result is stored as a string in the variable ‘elpher-selector-string’." (defun elpher-get-index-node () "Getter which retrieves the current node contents as an index." - (let ((content (elpher-node-content elpher-current-node)) - (address (elpher-node-address elpher-current-node))) + (let* ((address (elpher-node-address elpher-current-node)) + (content (elpher-get-cached-content address))) (if content (progn (elpher-with-clean-buffer @@ -447,8 +449,9 @@ The result is stored as a string in the variable ‘elpher-selector-string’." (elpher-with-clean-buffer (elpher-insert-index elpher-selector-string) (elpher-restore-pos) - (elpher-set-node-content elpher-current-node - (buffer-string))))))))) + (elpher-cache-content + (elpher-node-address elpher-current-node) + (buffer-string))))))))) ;; Text retrieval @@ -505,8 +508,8 @@ calls, as is necessary if the match is performed by `string-match'." (defun elpher-get-text-node () "Getter which retrieves the current node contents as a text document." - (let ((content (elpher-node-content elpher-current-node)) - (address (elpher-node-address elpher-current-node))) + (let* ((address (elpher-node-address elpher-current-node)) + (content (elpher-get-cached-content address))) (if content (progn (elpher-with-clean-buffer @@ -523,15 +526,16 @@ calls, as is necessary if the match is performed by `string-match'." (elpher-preprocess-text-response elpher-selector-string))) (elpher-restore-pos) - (elpher-set-node-content elpher-current-node - (buffer-string)))))))))) + (elpher-cache-content + (elpher-node-address elpher-current-node) + (buffer-string)))))))))) ;; Image retrieval (defun elpher-get-image-node () "Getter which retrieves the current node contents as an image to view." - (let ((content (elpher-node-content elpher-current-node)) - (address (elpher-node-address elpher-current-node))) + (let* ((address (elpher-node-address elpher-current-node)) + (content (elpher-get-cached-content address))) (if content (progn (elpher-with-clean-buffer @@ -553,17 +557,18 @@ calls, as is necessary if the match is performed by `string-match'." (insert-image image) (elpher-restore-pos)) (if elpher-cache-images - (elpher-set-node-content elpher-current-node - image))))))) + (elpher-cache-content + (elpher-node-address elpher-current-node) + image))))))) (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 ((content (elpher-node-content elpher-current-node)) - (address (elpher-node-address elpher-current-node)) - (aborted t)) + (let* ((address (elpher-node-address elpher-current-node)) + (content (elpher-get-cached-content address)) + (aborted t)) (if content (progn (elpher-with-clean-buffer @@ -586,8 +591,9 @@ calls, as is necessary if the match is performed by `string-match'." (elpher-with-clean-buffer (elpher-insert-index elpher-selector-string)) (goto-char (point-min)) - (elpher-set-node-content elpher-current-node - (buffer-string)))))) + (elpher-cache-content + (elpher-node-address elpher-current-node) + (buffer-string)))))) (if aborted (elpher-visit-parent-node)))))) @@ -595,8 +601,7 @@ calls, as is necessary if the match is performed by `string-match'." (defun elpher-get-node-raw () "Getter which retrieves the raw server response for the current node." - (let* ((content (elpher-node-content elpher-current-node)) - (address (elpher-node-address elpher-current-node))) + (let ((address (elpher-node-address elpher-current-node))) (elpher-with-clean-buffer (insert "LOADING RAW SERVER RESPONSE...")) (if address @@ -659,13 +664,36 @@ calls, as is necessary if the match is performed by `string-match'." (elpher-visit-parent-node) (telnet host port))) -;; Start node retrieval +;; Start page node retrieval (defun elpher-get-start-node () "Getter which displays the start page." (elpher-with-clean-buffer (elpher-insert-index elpher-start-index) (elpher-restore-pos))) + +;; Bookmarks page node retrieval + +(defun elpher-get-bookmarks-node () + "Getter which loads and displays 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-bookmark-address bookmark))) + (elpher-insert-index-record display-string + (elpher-address-type address) + (elpher-address-selector address) + (elpher-address-host address) + (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") + (elpher-restore-pos))) ;;; Bookmarks @@ -681,6 +709,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)) @@ -700,71 +732,24 @@ 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))))) - -(defun elpher-display-bookmarks () - "Display saved bookmark list." - (interactive) - (elpher-with-clean-buffer - (insert "Use 'r' to return to the previous page.\n\n" - "---- Bookmark list ----\n\n") - (let ((bookmarks (elpher-load-bookmarks))) - (if bookmarks - (dolist (bookmark bookmarks) - (let ((display-string (elpher-bookmark-display-string bookmark)) - (address (elpher-bookmark-address bookmark))) - (elpher-insert-index-record display-string - (elpher-address-type address) - (elpher-address-selector address) - (elpher-address-host address) - (elpher-address-port address)))) - (insert "No bookmarks found.\n"))) - (insert "\n-----------------------") - (goto-char (point-min)) - (elpher-next-link))) + (seq-filter (lambda (bookmark) + (not (equal (elpher-bookmark-address bookmark) address))) + (elpher-load-bookmarks)))) -(defun elpher-bookmark-current () - "Bookmark the current node." - (interactive) - (elpher-add-node-bookmark elpher-current-node)) - -(defun elpher-bookmark-link () - "Bookmark the link at point." - (interactive) - (let ((button (button-at (point)))) - (if button - (elpher-add-node-bookmark (button-get button 'elpher-node)) - (error "No link selected")))) - -(defun elpher-unbookmark-current () - "Remove bookmark for the current node." - (interactive) - (elpher-remove-node-bookmark elpher-current-node)) - -(defun elpher-unbookmark-link () - "Remove bookmark for the link at point." - (interactive) - (let ((button (button-at (point)))) - (if button - (elpher-remove-node-bookmark (button-get button 'elpher-node)) - (error "No link selected")))) - -;;; Interactive navigation procedures +;;; Interactive procedures ;; (defun elpher-next-link () @@ -783,7 +768,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: "))) @@ -816,11 +803,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 () @@ -836,10 +825,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 () @@ -857,7 +846,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)))) @@ -884,6 +873,65 @@ Beware that this completely replaces the existing contents of the file." (error "Already at root directory of current server"))) (error "Command invalid for this page")))) +(defun elpher-bookmarks-current-p () + "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 () + "Reload bookmarks if current node is a bookmarks page." + (if (elpher-bookmarks-current-p) + (elpher-reload-current-node))) + +(defun elpher-bookmark-current () + "Bookmark the current node." + (interactive) + (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 + (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) + (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 + (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) + (elpher-visit-node + (elpher-make-node "Bookmarks" + elpher-current-node + (elpher-make-address 'bookmarks)))) + (defun elpher-info-node (node) "Display information on NODE." (let ((display-string (elpher-node-display-string node)) @@ -960,6 +1008,11 @@ Beware that this completely replaces the existing contents of the file." (define-key map (kbd "I") 'elpher-info-current) (define-key map (kbd "c") 'elpher-copy-link-url) (define-key map (kbd "C") 'elpher-copy-current-url) + (define-key map (kbd "a") 'elpher-bookmark-link) + (define-key map (kbd "A") 'elpher-bookmark-current) + (define-key map (kbd "x") 'elpher-unbookmark-link) + (define-key map (kbd "X") 'elpher-unbookmark-current) + (define-key map (kbd "B") 'elpher-bookmarks) (when (fboundp 'evil-define-key) (evil-define-key 'motion map (kbd "TAB") 'elpher-next-link @@ -981,7 +1034,7 @@ Beware that this completely replaces the existing contents of the file." (kbd "A") 'elpher-bookmark-current (kbd "x") 'elpher-unbookmark-link (kbd "X") 'elpher-unbookmark-current - (kbd "B") 'elpher-display-bookmarks)) + (kbd "B") 'elpher-bookmarks)) map) "Keymap for gopher client.")