X-Git-Url: https://thelambdalab.xyz/gitweb/index.cgi?a=blobdiff_plain;f=elpher.el;h=62710903fece150b4c99a4a00fcab667b9d21bcb;hb=a0abd72b3a4dd84b44dc088a42431ff9a452bd34;hp=476bb5507de9af2616e0a3c59495a9123ee7473b;hpb=72888f553399948ac698b598c9197e12c6f58eeb;p=elpher.git diff --git a/elpher.el b/elpher.el index 476bb55..6271090 100644 --- a/elpher.el +++ b/elpher.el @@ -29,7 +29,7 @@ ;; Elpher aims to provide a full-featured gopher client for GNU Emacs. ;; It supports: -;; - intuitive keyboard and mouse-driven browsing, +;; - intuitive keyboard and mouse-driven interface, ;; - caching of visited sites (both content and cursor position), ;; - pleasant and configurable colouring of Gopher directories, ;; - direct visualisation of image files, @@ -87,7 +87,7 @@ "i\tfake\tfake\t1" "iPlaces 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" @@ -108,7 +108,9 @@ (?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)) + (?h elpher-get-url-node "W" 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.") @@ -122,47 +124,47 @@ ;; Face customizations (defface elpher-index - '((t :inherit org-drawer)) + '((t :inherit font-lock-keyword-face)) "Face used for directory type directory records.") (defface elpher-text - '((t :inherit org-tag)) + '((t :inherit bold)) "Face used for text type directory records.") (defface elpher-info - '((t :inherit org-default)) + '((t :inherit default)) "Face used for info type directory records.") (defface elpher-image - '((t :inherit org-level-4)) + '((t :inherit font-lock-string-face)) "Face used for image type directory records.") (defface elpher-search - '((t :inherit org-level-5)) + '((t :inherit warning)) "Face used for search type directory records.") (defface elpher-url - '((t :inherit org-level-6)) + '((t :inherit font-lock-comment-face)) "Face used for url type directory records.") (defface elpher-telnet - '((t :inherit org-level-6)) + '((t :inherit font-lock-function-name-face)) "Face used for telnet type directory records.") (defface elpher-binary - '((t :inherit org-level-7)) + '((t :inherit font-lock-doc-face)) "Face used for binary type directory records.") (defface elpher-unknown - '((t :inherit org-warning)) + '((t :inherit error)) "Face used for directory records with unknown/unsupported types.") (defface elpher-margin-key - '((t :inherit org-tag)) + '((t :inherit bold)) "Face used for directory margin key.") (defface elpher-margin-brackets - '((t :inherit org-special-keyword)) + '((t :inherit shadow)) "Face used for brackets around directory margin key.") ;; Other customizations @@ -180,12 +182,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)) @@ -195,8 +191,10 @@ use as the start page." ;; Address -(defun elpher-make-address (type selector host port) - "Create an address of a gopher object with TYPE, SELECTOR, HOST and PORT." +(defun elpher-make-address (type &optional selector host port) + "Create an address of a gopher object with TYPE, SELECTOR, HOST and PORT. +Although selector host and port are optional, they are only omitted for +special address types, such as 'start for the start page." (list type selector host port)) (defun elpher-address-type (address) @@ -217,17 +215,14 @@ use as the start page." ;; 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." @@ -241,21 +236,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 + +(defvar elpher-content-cache (make-hash-table :test 'equal)) +(defvar elpher-pos-cache (make-hash-table :test 'equal)) + +(defun elpher-get-cached-content (address) + "Retrieve the cached content for ADDRESS, or nil if none exists." + (gethash address elpher-content-cache)) -(defun elpher-node-pos (node) - "Retrieve the cached cursor position for NODE, or nil if none exists." - (elt node 4)) +(defun elpher-cache-content (address content) + "Set the content cache for ADDRESS to CONTENT." + (puthash address content elpher-content-cache)) -(defun elpher-set-node-content (node content) - "Set the content cache of NODE to CONTENT." - (setcar (nthcdr 3 node) content)) +(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-set-node-pos (node pos) - "Set the cursor position cache of NODE to POS." - (setcar (nthcdr 4 node) pos)) +(defun elpher-cache-pos (address pos) + "Set the cursor position cache for ADDRESS to POS." + (puthash address pos elpher-pos-cache)) ;; Node graph traversal @@ -266,17 +266,10 @@ content and cursor position fields of the node." (elpher-save-pos) (elpher-process-cleanup) (setq elpher-current-node node) - (with-current-buffer "*elpher*" - (setq header-line-format "hello")) - ;; (let ((inhibit-read-only t)) - - ;; (force-mode-line-update)) (if getter (funcall getter) (let* ((address (elpher-node-address node)) - (type (if address - (elpher-address-type address) - ?1))) + (type (elpher-address-type address))) (funcall (car (alist-get type elpher-type-map)))))) (defun elpher-visit-parent-node () @@ -287,17 +280,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))))) @@ -324,15 +317,28 @@ content and cursor position fields of the node." ;;; Index rendering ;; +(defun elpher-preprocess-text-response (string) + "Clear away CRs and terminating period from STRING." + (replace-regexp-in-string "\n\.\n$" "\n" + (replace-regexp-in-string "\r" "" + string))) + (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 ;; LF-only servers sadly exist, hence the following. - (let* ((str-no-period (replace-regexp-in-string "\r\n\.\r\n$" "\r\n" string)) - (str-no-cr (replace-regexp-in-string "\r" "" str-no-period))) - (dolist (line (split-string str-no-cr "\n")) + (let ((str-processed (elpher-preprocess-text-response string))) + (dolist (line (split-string str-processed "\n")) (unless (= (length line) 0) - (elpher-insert-index-record line))))) + (let* ((type (elt line 0)) + (fields (split-string (substring line 1) "\t")) + (display-string (elt fields 0)) + (selector (elt fields 1)) + (host (elt fields 2)) + (port (if (elt fields 3) + (string-to-number (elt fields 3)) + nil))) + (elpher-insert-index-record display-string type selector host port)))))) (defun elpher-insert-margin (&optional type-name) "Insert index margin, optionally containing the TYPE-NAME, into the current buffer." @@ -357,28 +363,15 @@ content and cursor position fields of the node." (elpher-address-host address) (elpher-address-port address))))) -(defun elpher-insert-index-record (line) - "Insert the index record corresponding to LINE into the current buffer." - (let* ((type (elt line 0)) - (fields (split-string (substring line 1) "\t")) - (display-string (elt fields 0)) - (selector (elt fields 1)) - (host (elt fields 2)) - (port (string-to-number (elt fields 3)))) - (elpher-insert-index-record-helper display-string type selector host port))) - -(defun elpher-insert-index-record-helper (display-string type selector host port) - "Helper function to insert an index record into the current buffer. +(defun elpher-insert-index-record (display-string type selector host port) + "Function to insert an index record into the current buffer. The contents of the record are dictated by TYPE, DISPLAY-STRING, SELECTOR, HOST -and PORT. - -This function is essentially the second half of `elpher-insert-index-record', -but broken out so that it can be used elsewhere." +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)) + (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 @@ -435,31 +428,24 @@ 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 (insert content) (elpher-restore-pos))) - (if address - (progn - (elpher-with-clean-buffer - (insert "LOADING DIRECTORY...")) - (elpher-get-selector address - (lambda (proc event) - (unless (string-prefix-p "deleted" event) - (elpher-with-clean-buffer - (elpher-insert-index elpher-selector-string) - (elpher-restore-pos) - (elpher-set-node-content elpher-current-node - (buffer-string))))))) - (progn - (elpher-with-clean-buffer - (elpher-insert-index elpher-start-index) - (elpher-restore-pos) - (elpher-set-node-content elpher-current-node - (buffer-string)))))))) + (elpher-with-clean-buffer + (insert "LOADING DIRECTORY...")) + (elpher-get-selector address + (lambda (proc event) + (unless (string-prefix-p "deleted" event) + (elpher-with-clean-buffer + (elpher-insert-index elpher-selector-string) + (elpher-restore-pos) + (elpher-cache-content + (elpher-node-address elpher-current-node) + (buffer-string))))))))) ;; Text retrieval @@ -514,16 +500,10 @@ calls, as is necessary if the match is performed by `string-match'." 'help-echo (elpher-node-button-help node)))) (buffer-string))) -(defun elpher-process-text (string) - "Remove CRs and trailing period from the gopher text document STRING." - (let* ((chopped-str (replace-regexp-in-string "\r\n\.\r\n$" "\r\n" string)) - (cleaned-str (replace-regexp-in-string "\r" "" chopped-str))) - (elpher-buttonify-urls cleaned-str))) - (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 @@ -536,17 +516,20 @@ calls, as is necessary if the match is performed by `string-match'." (lambda (proc event) (unless (string-prefix-p "deleted" event) (elpher-with-clean-buffer - (insert (elpher-process-text elpher-selector-string)) + (insert (elpher-buttonify-urls + (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 @@ -560,24 +543,26 @@ calls, as is necessary if the match is performed by `string-match'." (lambda (proc event) (unless (string-prefix-p "deleted" event) (let ((image (create-image - (encode-coding-string elpher-selector-string - 'no-conversion) + (encode-coding-string + elpher-selector-string + 'no-conversion) nil t))) (elpher-with-clean-buffer (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 @@ -600,8 +585,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)))))) @@ -609,8 +595,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 @@ -673,6 +658,35 @@ calls, as is necessary if the match is performed by `string-match'." (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 + (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 "Use 'u' 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-----------------------") + (elpher-restore-pos))) + ;;; Bookmarks ;; @@ -723,54 +737,8 @@ Beware that this completely replaces the existing contents of the file." (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-helper 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))) - -(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 () @@ -797,13 +765,13 @@ Beware that this completely replaces the existing contents of the file." (elpher-make-node-from-matched-url elpher-current-node host-or-url) (let ((selector (read-string "Selector (default none): " nil nil "")) - (port (string-to-number (read-string "Port (default 70): " - nil nil 70)))) + (port-string (read-string "Port (default 70): " nil nil "70"))) (elpher-make-node (concat "gopher://" host-or-url - ":" port + ":" port-string "/1" selector) elpher-current-node - (elpher-make-address ?1 selector host-or-url port))))))) + (elpher-make-address ?1 selector host-or-url + (string-to-number port-string)))))))) (switch-to-buffer "*elpher*") (elpher-visit-node node))) @@ -857,7 +825,7 @@ Beware that this completely replaces the existing contents of the file." (setq b (next-button (button-start b)))) link-map)) -(defun elpher-menu () +(defun elpher-jump () "Select a directory entry by name. Similar to the info browser (m)enu command." (interactive) (let* ((link-map (elpher-build-link-map))) @@ -873,8 +841,9 @@ Beware that this completely replaces the existing contents of the file." (defun elpher-root-dir () "Visit root of current server." (interactive) - (let ((address (elpher-node-address elpher-current-node))) - (if address + (let* ((address (elpher-node-address elpher-current-node)) + (host (elpher-address-host address))) + (if host (let ((host (elpher-address-host address)) (selector (elpher-address-selector address)) (port (elpher-address-port address))) @@ -887,7 +856,56 @@ Beware that this completely replaces the existing contents of the file." elpher-current-node root-address))) (error "Already at root directory of current server"))) - (error "Command invalid for Elpher start page")))) + (error "Command invalid for this page")))) + +(defun elpher-bookmarks-current-p () + "Return true 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) + (if (not (elpher-bookmarks-current-p)) + (elpher-add-node-bookmark elpher-current-node))) + +(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)) + (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))) + +(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)) + (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." @@ -960,11 +978,16 @@ Beware that this completely replaces the existing contents of the file." (define-key map (kbd "R") 'elpher-reload) (define-key map (kbd "w") 'elpher-view-raw) (define-key map (kbd "d") 'elpher-download) - (define-key map (kbd "m") 'elpher-menu) + (define-key map (kbd "m") 'elpher-jump) (define-key map (kbd "i") 'elpher-info-link) (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 @@ -977,7 +1000,7 @@ Beware that this completely replaces the existing contents of the file." (kbd "R") 'elpher-reload (kbd "w") 'elpher-view-raw (kbd "d") 'elpher-download - (kbd "m") 'elpher-menu + (kbd "m") 'elpher-jump (kbd "i") 'elpher-info-link (kbd "I") 'elpher-info-current (kbd "c") 'elpher-copy-link-url @@ -986,7 +1009,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.") @@ -1007,8 +1030,7 @@ Beware that this completely replaces the existing contents of the file." (switch-to-buffer "*elpher*") (switch-to-buffer "*elpher*") (setq elpher-current-node nil) - (let ((start-node (elpher-make-node "Elpher Start Page" - nil elpher-start-address))) + (let ((start-node (elpher-make-node "Elpher Start Page" nil (elpher-make-address 'start)))) (elpher-visit-node start-node))) "Started Elpher.") ; Otherwise (elpher) evaluates to start page string.