Added some help text, bumped displayed version.
[elpher.git] / elpher.el
index 94ba3be..a24d056 100644 (file)
--- a/elpher.el
+++ b/elpher.el
@@ -4,7 +4,7 @@
 
 ;; Author: Tim Vaughan <tgvaughan@gmail.com>
 ;; Created: 11 April 2019
 
 ;; Author: Tim Vaughan <tgvaughan@gmail.com>
 ;; 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"))
 ;; Keywords: comm gopher
 ;; Homepage: https://github.com/tgvaughan/elpher
 ;; Package-Requires: ((emacs "25"))
@@ -55,7 +55,7 @@
 ;;; Global constants
 ;;
 
 ;;; Global constants
 ;;
 
-(defconst elpher-version "1.1.0"
+(defconst elpher-version "1.2.0"
   "Current version of elpher.")
 
 (defconst elpher-margin-width 6
   "Current version of elpher.")
 
 (defconst elpher-margin-width 6
          "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 - 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"
          "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"
          "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"
          "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))
 
   "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))
 (defcustom elpher-use-header t
   "If non-nil, display current node information in buffer header."
   :type '(boolean))
@@ -221,17 +218,14 @@ special address types, such as 'start for the start page."
 
 ;; Node
 
 
 ;; 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
   "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."
 
 (defun elpher-node-display-string (node)
   "Retrieve the display string of NODE."
@@ -245,21 +239,26 @@ content and cursor position fields of the node."
   "Retrieve the address of NODE."
   (elt node 2))
 
   "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-node-pos (node)
-  "Retrieve the cached cursor position for NODE, or nil if none exists."
-  (elt node 4))
+(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-content (node content)
-  "Set the content cache of NODE to CONTENT."
-  (setcar (nthcdr 3 node) content))
+(defun elpher-cache-content (address content)
+  "Set the content cache for ADDRESS to CONTENT."
+  (puthash address content 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-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
 
 
 ;; Node graph traversal
 
@@ -284,17 +283,17 @@ content and cursor position fields of the node."
       
 (defun elpher-reload-current-node ()
   "Reload the current node, discarding any existing cached content."
       
 (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-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."
 
 (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)))))
     (if pos
         (goto-char pos)
       (goto-char (point-min)))))
@@ -375,7 +374,7 @@ and PORT."
         (type-map-entry (alist-get type elpher-type-map)))
     (if type-map-entry
         (let* ((margin-code (cadr type-map-entry))
         (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
                (node (elpher-make-node display-string elpher-current-node address)))
           (elpher-insert-margin margin-code)
           (insert-text-button display-string
@@ -432,8 +431,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."
 
 (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
     (if content
         (progn
           (elpher-with-clean-buffer
@@ -447,8 +446,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-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
 
 
 ;; Text retrieval
 
@@ -505,8 +505,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."
 
 (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
     (if content
         (progn
           (elpher-with-clean-buffer
@@ -523,15 +523,16 @@ calls, as is necessary if the match is performed by `string-match'."
                                             (elpher-preprocess-text-response
                                              elpher-selector-string)))
                                    (elpher-restore-pos)
                                             (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."
 
 ;; 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
     (if content
         (progn
           (elpher-with-clean-buffer
@@ -553,17 +554,18 @@ calls, as is necessary if the match is performed by `string-match'."
                                         (insert-image image)
                                         (elpher-restore-pos))
                                        (if elpher-cache-images
                                         (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."
         (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
     (if content
         (progn
           (elpher-with-clean-buffer
@@ -586,8 +588,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-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))))))
 
         (if aborted
             (elpher-visit-parent-node))))))
 
@@ -595,8 +598,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."
 
 (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
     (elpher-with-clean-buffer
      (insert "LOADING RAW SERVER RESPONSE..."))
     (if address
@@ -659,13 +661,36 @@ calls, as is necessary if the match is performed by `string-match'."
     (elpher-visit-parent-node)
     (telnet host port)))
 
     (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)))
 
 (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
   
 
 ;;; Bookmarks
@@ -681,6 +706,10 @@ bookmark list, while ADDRESS is the address of the entry."
   "Get the display string of BOOKMARK."
   (elt bookmark 0))
 
   "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))
 (defun elpher-bookmark-address (bookmark)
   "Get the address for BOOKMARK."
   (elt bookmark 1))
@@ -700,71 +729,24 @@ Beware that this completely replaces the existing contents of the file."
       (goto-char (point-min))
       (read (current-buffer)))))
 
       (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)))
 
     (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
     (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)))
-
-(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"))))
+     (seq-filter (lambda (bookmark)
+                   (not (equal (elpher-bookmark-address bookmark) address)))
+                 (elpher-load-bookmarks))))
 
 
-;;; Interactive navigation procedures
+;;; Interactive procedures
 ;;
 
 (defun elpher-next-link ()
 ;;
 
 (defun elpher-next-link ()
@@ -884,6 +866,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"))))
 
             (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))
 (defun elpher-info-node (node)
   "Display information on NODE."
   (let ((display-string (elpher-node-display-string node))
@@ -960,6 +1001,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 "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
     (when (fboundp 'evil-define-key)
       (evil-define-key 'motion map
         (kbd "TAB") 'elpher-next-link
@@ -981,7 +1027,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 "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.")
 
     map)
   "Keymap for gopher client.")