Clarified commentary.
[elpher.git] / elpher.el
index 25acb24..d4c582a 100644 (file)
--- a/elpher.el
+++ b/elpher.el
@@ -36,9 +36,8 @@
 ;; - (m)enu key support, similar to Emacs' info browser,
 ;; - clickable web and gopher links in plain text.
 
-;; The caching mechanism works by maintaining a hierarchy of visited
-;; pages rather than a linear history, meaning that it is quick and
-;; easy to navigate this history.
+;; Visited pages are stored as a hierarchy rather than a linear history,
+;; meaning that navigation between these pages is quick and easy.
 
 ;; To launch Elpher, simply use 'M-x elpher'.  This will open a start
 ;; page containing information on key bindings and suggested starting
@@ -54,7 +53,7 @@
 ;;; Global constants
 ;;
 
-(defconst elpher-version "1.0.0"
+(defconst elpher-version "1.1.0"
   "Current version of elpher.")
 
 (defconst elpher-margin-width 6
   (mapconcat
    'identity
    (list "i\tfake\tfake\t1"
-         "i--------------------------------------------\tfake\tfake\t1"
-         "i           Elpher Gopher Client             \tfake\tfake\t1"
-         (format "i              version %s\tfake\tfake\t1" elpher-version)
-         "i--------------------------------------------\tfake\tfake\t1"
+         "i     --------------------------------------------\tfake\tfake\t1"
+         "i                Elpher Gopher Client             \tfake\tfake\t1"
+         (format "i                   version %s\tfake\tfake\t1" elpher-version)
+         "i     --------------------------------------------\tfake\tfake\t1"
          "i\tfake\tfake\t1"
-         "iBasic usage:\tfake\tfake\t1"
+         "iUsage:\tfake\tfake\t1"
          "i\tfake\tfake\t1"
          "i - tab/shift-tab: next/prev directory entry on current page\tfake\tfake\t1"
          "i - RET/mouse-1: open directory entry under cursor\tfake\tfake\t1"
          "i - m: select a directory entry by name (autocompletes)\tfake\tfake\t1"
          "i - u: return to parent directory entry\tfake\tfake\t1"
+         "i - O: visit the root directory of the current server\tfake\tfake\t1"
          "i - g: go to a particular 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"
@@ -89,7 +89,7 @@
          "isearch terms:\tfake\tfake\t1"
          "i\tfake\tfake\t1"
          "7Veronica-2 Gopher Search Engine\t/v2/vs\tgopher.floodgap.com\t70"
-         ".")
+         ".\r\n")
    "\r\n")
   "Source for elpher start page.")
 
 Otherwise, use the system browser via the BROWSE-URL function."
   :type '(boolean))
 
+(defcustom elpher-buttonify-urls-in-directories nil
+  "If non-nil, turns URLs matched in directories into clickable buttons."
+  :type '(boolean))
+
 (defcustom elpher-cache-images nil
   "If non-nil, cache images in memory in the same way as other content."
   :type '(boolean))
@@ -172,7 +176,6 @@ Otherwise, a list containing the selector, host and port of a directory to
 use as the start page."
   :type '(list string string integer))
 
-
 ;;; Model
 ;;
 
@@ -196,6 +199,9 @@ use as the start page."
 
 ;; Node
 
+(defvar elpher-seen-nodes (make-hash-table :test 'equal)
+  "Table mapping addresses to existing (seen) node objects.")
+
 (defun elpher-make-node (parent address getter &optional content pos)
   "Create a node in the gopher page hierarchy.
 
@@ -204,8 +210,16 @@ the gopher page, GETTER provides the getter function used to obtain this
 page.
 
 The optional arguments CONTENT and POS can be used to fill the cached
-content and cursor position fields of the node."
-  (list parent address getter content pos))
+content and cursor position fields of the node.
+
+If the hash table `elpher-seen-nodes' contains a key equal to ADDRESS,
+the node contained as its value will be returned instead."
+  (let ((existing-node (gethash address elpher-seen-nodes)))
+    (if existing-node
+        existing-node
+      (let ((new-node (list parent address getter content pos)))
+        (puthash address new-node elpher-seen-nodes)
+        new-node))))
 
 (defun elpher-node-parent (node)
   "Retrieve the parent node of NODE."
@@ -237,7 +251,7 @@ content and cursor position fields of the node."
 
 ;; Node graph traversal
 
-(defvar elpher-current-node)
+(defvar elpher-current-node nil)
 
 (defun elpher-visit-node (node &optional getter)
   "Visit NODE using its own getter or GETTER, if non-nil."
@@ -276,8 +290,7 @@ content and cursor position fields of the node."
 
 (defmacro elpher-with-clean-buffer (&rest args)
   "Evaluate ARGS with a clean *elpher* buffer as current."
-  (list 'progn
-        '(switch-to-buffer "*elpher*")
+  (list 'with-current-buffer "*elpher*"
         '(elpher-mode)
         (append (list 'let '((inhibit-read-only t))
                       '(erase-buffer))
@@ -290,9 +303,11 @@ content and cursor position fields of the node."
   "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.
-  (dolist (line (split-string (replace-regexp-in-string "\r" "" string) "\n"))
-    (unless (= (length line) 0)
-      (elpher-insert-index-record line))))
+  (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"))
+      (unless (= (length line) 0)
+        (elpher-insert-index-record line)))))
 
 (defun elpher-insert-margin (&optional type-name)
   "Insert index margin, optionally containing the TYPE-NAME, into the current buffer."
@@ -332,8 +347,11 @@ content and cursor position fields of the node."
                                                  selector host port)))
       (pcase type
         (?i (elpher-insert-margin) ;; Information
-            (insert (propertize display-string
-                                'face 'elpher-info)))
+            (insert (propertize
+                     (if elpher-buttonify-urls-in-directories
+                         (elpher-buttonify-urls display-string)
+                       display-string)
+                     'face 'elpher-info)))
         (?h (elpher-insert-margin "W") ;; Web link
             (let ((url (elt (split-string selector "URL:") 1)))
               (insert-text-button display-string
@@ -342,7 +360,6 @@ content and cursor position fields of the node."
                                   'action #'elpher-click-url
                                   'follow-link t
                                   'help-echo (format "mouse-1, RET: open url %s" url))))
-        (?.) ;; Occurs at end of index, can safely ignore.
         (tp (elpher-insert-margin (concat (char-to-string tp) "?"))
             (insert (propertize display-string
                                 'face 'elpher-unknown-face)))))
@@ -382,8 +399,8 @@ The result is stored as a string in the variable ‘elpher-selector-string’."
     (if content
         (progn
           (elpher-with-clean-buffer
-           (insert content))
-          (elpher-restore-pos))
+           (insert content)
+           (elpher-restore-pos)))
       (if address
           (progn
             (elpher-with-clean-buffer
@@ -392,16 +409,16 @@ The result is stored as a string in the variable ‘elpher-selector-string’."
                                   (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))))))
+                                       (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-insert-index elpher-start-index)
+           (elpher-restore-pos)
+           (elpher-set-node-content elpher-current-node
+                                    (buffer-string))))))))
 
 ;; Text retrieval
 
@@ -459,8 +476,8 @@ The result is stored as a string in the variable ‘elpher-selector-string’."
     (if content
         (progn
           (elpher-with-clean-buffer
-           (insert content))
-          (elpher-restore-pos))
+           (insert content)
+           (elpher-restore-pos)))
       (progn
         (elpher-with-clean-buffer
          (insert "LOADING TEXT..."))
@@ -468,10 +485,10 @@ The result is stored as a string in the variable ‘elpher-selector-string’."
                               (lambda (proc event)
                                 (unless (string-prefix-p "deleted" event)
                                   (elpher-with-clean-buffer
-                                   (insert (elpher-process-text elpher-selector-string)))
-                                  (elpher-restore-pos)
-                                  (elpher-set-node-content elpher-current-node
-                                                            (buffer-string)))))))))
+                                   (insert (elpher-process-text elpher-selector-string))
+                                   (elpher-restore-pos)
+                                   (elpher-set-node-content elpher-current-node
+                                                            (buffer-string))))))))))
 
 ;; Image retrieval
 
@@ -482,9 +499,8 @@ The result is stored as a string in the variable ‘elpher-selector-string’."
     (if content
         (progn
           (elpher-with-clean-buffer
-           (insert-image content))
-          (setq cursor-type nil)
-          (elpher-restore-pos))
+           (insert-image content)
+           (elpher-restore-pos)))
       (if (display-images-p)
           (progn
             (elpher-with-clean-buffer
@@ -497,9 +513,8 @@ The result is stored as a string in the variable ‘elpher-selector-string’."
                                                                          'no-conversion)
                                                    nil t)))
                                        (elpher-with-clean-buffer
-                                        (insert-image image))
-                                       (setq cursor-type nil)
-                                       (elpher-restore-pos)
+                                        (insert-image image)
+                                        (elpher-restore-pos))
                                        (if elpher-cache-images
                                            (elpher-set-node-content elpher-current-node
                                                                     image)))))))
@@ -515,8 +530,8 @@ The result is stored as a string in the variable ‘elpher-selector-string’."
     (if content
         (progn
           (elpher-with-clean-buffer
-           (insert content))
-          (elpher-restore-pos)
+           (insert content)
+           (elpher-restore-pos))
           (message "Displaying cached search results.  Reload to perform a new search."))
       (unwind-protect
           (let* ((query-string (read-string "Query: "))
@@ -551,8 +566,8 @@ The result is stored as a string in the variable ‘elpher-selector-string’."
                               (lambda (proc event)
                                 (unless (string-prefix-p "deleted" event)
                                   (elpher-with-clean-buffer
-                                   (insert elpher-selector-string))
-                                  (goto-char (point-min)))))
+                                   (insert elpher-selector-string)
+                                   (goto-char (point-min))))))
       (progn
         (elpher-with-clean-buffer
          (insert elpher-start-index))
@@ -618,6 +633,7 @@ The result is stored as a string in the variable ‘elpher-selector-string’."
 (defun elpher-go ()
   "Go to a particular gopher site."
   (interactive)
+  (switch-to-buffer "*elpher*")
   (let* (
          (hostname (read-string "Gopher host: "))
          (selector (read-string "Selector (default none): " nil nil ""))
@@ -631,18 +647,24 @@ The result is stored as a string in the variable ‘elpher-selector-string’."
 (defun  elpher-redraw ()
   "Redraw current page."
   (interactive)
-  (elpher-visit-node elpher-current-node))
+  (if elpher-current-node
+      (elpher-visit-node elpher-current-node)
+    (message "No current site.")))
 
 (defun  elpher-reload ()
   "Reload current page."
   (interactive)
-  (elpher-reload-current-node))
+  (if elpher-current-node
+      (elpher-reload-current-node)
+    (message "No current site.")))
 
 (defun elpher-view-raw ()
   "View current page as plain text."
   (interactive)
-  (elpher-visit-node elpher-current-node
-                      #'elpher-get-node-raw))
+  (if elpher-current-node
+      (elpher-visit-node elpher-current-node
+                         #'elpher-get-node-raw)
+    (message "No current site.")))
 
 (defun elpher-back ()
   "Go to previous site."
@@ -678,12 +700,29 @@ The result is stored as a string in the variable ‘elpher-selector-string’."
   (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): " link-map nil t))))
+                     (completing-read "Directory entry/link (tab to autocomplete): "
+                                      link-map nil t))))
           (if (and key (> (length key) 0))
               (let ((b (cdr (assoc key link-map))))
                 (goto-char (button-start b))
                 (button-activate b)))))))
 
+(defun elpher-root-dir ()
+  "Visit root of current server."
+  (interactive)
+  (let ((address (elpher-node-address elpher-current-node)))
+    (if address
+        (let ((host (elpher-address-host address))
+              (selector (elpher-address-selector address))
+              (port (elpher-address-port address)))
+          (if (> (length selector) 0)
+              (let ((root-address (elpher-make-address "" host port)))
+                (elpher-visit-node (elpher-make-node elpher-current-node
+                                                     root-address
+                                                     #'elpher-get-index-node)))
+            (message "Already at root directory of current server.")))
+      (message "Command invalid for Elpher start page."))))
+
 ;;; Mode and keymap
 ;;
 
@@ -692,6 +731,7 @@ The result is stored as a string in the variable ‘elpher-selector-string’."
     (define-key map (kbd "TAB") 'elpher-next-link)
     (define-key map (kbd "<backtab>") 'elpher-prev-link)
     (define-key map (kbd "u") 'elpher-back)
+    (define-key map (kbd "O") 'elpher-root-dir)
     (define-key map (kbd "g") 'elpher-go)
     (define-key map (kbd "r") 'elpher-redraw)
     (define-key map (kbd "R") 'elpher-reload)
@@ -704,6 +744,7 @@ The result is stored as a string in the variable ‘elpher-selector-string’."
         (kbd "C-]") 'elpher-follow-current-link
         (kbd "C-t") 'elpher-back
         (kbd "u") 'elpher-back
+        (kbd "O") 'elpher-root-dir
         (kbd "g") 'elpher-go
         (kbd "r") 'elpher-redraw
         (kbd "R") 'elpher-reload
@@ -724,11 +765,14 @@ The result is stored as a string in the variable ‘elpher-selector-string’."
 (defun elpher ()
   "Start elpher with default landing page."
   (interactive)
-  (setq elpher-current-node nil)
-  (let ((start-node (elpher-make-node nil
-                                      elpher-start-address
-                                      #'elpher-get-index-node)))
-    (elpher-visit-node start-node))
+  (if (get-buffer "*elpher*")
+      (switch-to-buffer "*elpher*")
+    (switch-to-buffer "*elpher*")
+    (setq elpher-current-node nil)
+    (let ((start-node (elpher-make-node nil
+                                        elpher-start-address
+                                        #'elpher-get-index-node)))
+      (elpher-visit-node start-node)))
   "Started Elpher.") ; Otherwise (elpher) evaluates to start page string.
 
 ;;; elpher.el ends here