Defined special type and getter for start page.
[elpher.git] / elpher.el
index 4cc5ceb..94ba3be 100644 (file)
--- a/elpher.el
+++ b/elpher.el
@@ -29,7 +29,7 @@
 ;; Elpher aims to provide a full-featured gopher client for GNU Emacs.
 ;; It supports:
 
 ;; 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,
 ;; - caching of visited sites (both content and cursor position),
 ;; - pleasant and configurable colouring of Gopher directories,
 ;; - direct visualisation of image files,
          "i\tfake\tfake\t1"
          "iUsage:\tfake\tfake\t1"
          "i\tfake\tfake\t1"
          "i\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 - tab/shift-tab: next/prev item on current page\tfake\tfake\t1"
+         "i - RET/mouse-1: open item under cursor\tfake\tfake\t1"
+         "i - m: select an item on current page by name (autocompletes)\tfake\tfake\t1"
+         "i - u: return to parent\tfake\tfake\t1"
+         "i - O: visit the root menu of the current server\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 - 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 - 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"
 (defconst elpher-type-map
   '((?0 elpher-get-text-node "T" elpher-text)
     (?1 elpher-get-index-node "/" elpher-index)
 (defconst elpher-type-map
   '((?0 elpher-get-text-node "T" elpher-text)
     (?1 elpher-get-index-node "/" elpher-index)
-    (?g elpher-get-image-node "im" elpher-image)
-    (?p elpher-get-image-node "im" elpher-image)
-    (?I elpher-get-image-node "im" elpher-image)
     (?4 elpher-get-node-download "B" elpher-binary)
     (?5 elpher-get-node-download "B" elpher-binary)
     (?4 elpher-get-node-download "B" elpher-binary)
     (?5 elpher-get-node-download "B" elpher-binary)
-    (?9 elpher-get-node-download "B" elpher-binary)
     (?7 elpher-get-search-node "?" elpher-search)
     (?7 elpher-get-search-node "?" elpher-search)
-    (?h elpher-get-url "W" elpher-url))
+    (?8 elpher-get-telnet-node "?" elpher-telnet)
+    (?9 elpher-get-node-download "B" elpher-binary)
+    (?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)
+    (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.")
 
 
   "Association list from types to getters, margin codes and index faces.")
 
 
 ;; Face customizations
 
 (defface elpher-index
 ;; 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
   "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
   "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
   "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
   "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
   "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.")
 
   "Face used for url type directory records.")
 
+(defface elpher-telnet
+  '((t :inherit font-lock-function-name-face))
+  "Face used for telnet type directory records.")
+
 (defface elpher-binary
 (defface elpher-binary
-  '((t :inherit org-level-7))
+  '((t :inherit font-lock-doc-face))
   "Face used for binary type directory records.")
 
 (defface elpher-unknown
   "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
   "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
   "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
   "Face used for brackets around directory margin key.")
 
 ;; Other customizations
@@ -179,51 +188,61 @@ Otherwise, a list containing the selector, host and port of a directory to
 use as the start page."
   :type '(list string string integer))
 
 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))
 
 ;;; Model
 ;;
 
 ;; Address
 
 
 ;;; Model
 ;;
 
 ;; Address
 
-(defun elpher-make-address (selector host port)
-  "Create an address of a gopher object with SELECTOR, HOST and PORT."
-  (list selector host 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)
+  "Retrieve type from ADDRESS."
+  (elt address 0))
 
 (defun elpher-address-selector (address)
   "Retrieve selector from ADDRESS."
 
 (defun elpher-address-selector (address)
   "Retrieve selector from ADDRESS."
-  (car address))
+  (elt address 1))
 
 (defun elpher-address-host (address)
   "Retrieve host from ADDRESS."
 
 (defun elpher-address-host (address)
   "Retrieve host from ADDRESS."
-  (cadr address))
+  (elt address 2))
 
 (defun elpher-address-port (address)
   "Retrieve port from ADDRESS."
 
 (defun elpher-address-port (address)
   "Retrieve port from ADDRESS."
-  (caddr address))
+  (elt address 3))
 
 ;; Node
 
 
 ;; Node
 
-(defun elpher-make-node (parent address getter &optional content pos)
+(defun elpher-make-node (display-string parent address &optional content pos)
   "Create a node in the gopher page hierarchy.
 
   "Create a node in the gopher page hierarchy.
 
-PARENT specifies the parent of the node, ADDRESS specifies the address of
-the gopher page, GETTER provides the getter function used to obtain this
-page.
+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."
 
 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))
+  (list display-string parent address content pos))
+
+(defun elpher-node-display-string (node)
+  "Retrieve the display string of NODE."
+  (elt node 0))
 
 (defun elpher-node-parent (node)
   "Retrieve the parent node of NODE."
 
 (defun elpher-node-parent (node)
   "Retrieve the parent node of NODE."
-  (elt node 0))
+  (elt node 1))
 
 (defun elpher-node-address (node)
   "Retrieve the address of NODE."
 
 (defun elpher-node-address (node)
   "Retrieve the address of NODE."
-  (elt node 1))
-
-(defun elpher-node-getter (node)
-  "Retrieve the preferred getter function of NODE."
   (elt node 2))
 
 (defun elpher-node-content (node)
   (elt node 2))
 
 (defun elpher-node-content (node)
@@ -253,7 +272,9 @@ content and cursor position fields of the node."
   (setq elpher-current-node node)
   (if getter
       (funcall getter)
   (setq elpher-current-node node)
   (if getter
       (funcall getter)
-    (funcall (elpher-node-getter node))))
+    (let* ((address (elpher-node-address node))
+           (type (elpher-address-type address)))
+      (funcall (car (alist-get type elpher-type-map))))))
 
 (defun elpher-visit-parent-node ()
   "Visit the parent of the current node."
 
 (defun elpher-visit-parent-node ()
   "Visit the parent of the current node."
@@ -282,27 +303,46 @@ content and cursor position fields of the node."
 ;;; Buffer preparation
 ;;
 
 ;;; Buffer preparation
 ;;
 
+(defun elpher-update-header ()
+  "If `elpher-use-header' is true, display current node info in window header."
+  (if elpher-use-header
+      (setq header-line-format (elpher-node-display-string elpher-current-node))))
+
 (defmacro elpher-with-clean-buffer (&rest args)
   "Evaluate ARGS with a clean *elpher* buffer as current."
   (list 'with-current-buffer "*elpher*"
         '(elpher-mode)
         (append (list 'let '((inhibit-read-only t))
 (defmacro elpher-with-clean-buffer (&rest args)
   "Evaluate ARGS with a clean *elpher* buffer as current."
   (list 'with-current-buffer "*elpher*"
         '(elpher-mode)
         (append (list 'let '((inhibit-read-only t))
-                      '(erase-buffer))
+                      '(erase-buffer)
+                      '(elpher-update-header))
                 args)))
 
 
 ;;; Index rendering
 ;;
 
                 args)))
 
 
 ;;; 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.
 (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)
       (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."
 
 (defun elpher-insert-margin (&optional type-name)
   "Insert index margin, optionally containing the TYPE-NAME, into the current buffer."
@@ -319,7 +359,7 @@ content and cursor position fields of the node."
 (defun elpher-node-button-help (node)
   "Return a string containing the help text for a button corresponding to NODE."
   (let ((address (elpher-node-address node)))
 (defun elpher-node-button-help (node)
   "Return a string containing the help text for a button corresponding to NODE."
   (let ((address (elpher-node-address node)))
-    (if (eq (elpher-node-getter node) #'elpher-get-url)
+    (if (eq (elpher-address-type address) ?h)
         (let ((url (cadr (split-string (elpher-address-selector address) "URL:"))))
           (format "mouse-1, RET: open url '%s'" url))
       (format "mouse-1, RET: open '%s' on %s port %s"
         (let ((url (cadr (split-string (elpher-address-selector address) "URL:"))))
           (format "mouse-1, RET: open url '%s'" url))
       (format "mouse-1, RET: open '%s' on %s port %s"
@@ -327,49 +367,35 @@ content and cursor position fields of the node."
               (elpher-address-host address)
               (elpher-address-port address)))))
 
               (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 (elt fields 3)))
-    (elpher-insert-index-record-helper type display-string selector host port)))
-
-(defun elpher-insert-index-record-helper (type display-string 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
 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 by other functions to construct indices
-on the fly."
-  (let ((address (elpher-make-address selector host port))
+and PORT."
+  (let ((address (elpher-make-address type selector host port))
         (type-map-entry (alist-get type elpher-type-map)))
     (if type-map-entry
         (type-map-entry (alist-get type elpher-type-map)))
     (if type-map-entry
-        (let* ((getter (car type-map-entry))
-               (margin-code (cadr type-map-entry))
+        (let* ((margin-code (cadr type-map-entry))
                (face (caddr type-map-entry))
                (face (caddr type-map-entry))
-               (node (elpher-make-node elpher-current-node address getter)))
+               (node (elpher-make-node display-string elpher-current-node address)))
           (elpher-insert-margin margin-code)
           (insert-text-button display-string
                               'face face
                               'elpher-node node
           (elpher-insert-margin margin-code)
           (insert-text-button display-string
                               'face face
                               'elpher-node node
-                              'elpher-node-type type
                               'action #'elpher-click-link
                               'follow-link t
                               'help-echo (elpher-node-button-help node)))
       (pcase type
                               'action #'elpher-click-link
                               'follow-link t
                               'help-echo (elpher-node-button-help node)))
       (pcase type
-        (?i (elpher-insert-margin) ;; Information
-            (insert (propertize
-                     (if elpher-buttonify-urls-in-directories
-                         (elpher-buttonify-urls display-string)
-                       display-string)
-                     'face 'elpher-info)))
-        (tp (elpher-insert-margin (concat (char-to-string tp) "?"))
-            (insert (propertize display-string
-                                'face 'elpher-unknown-face)))))
+        (?i ;; Information
+         (elpher-insert-margin)
+         (insert (propertize
+                  (if elpher-buttonify-urls-in-directories
+                      (elpher-buttonify-urls display-string)
+                    display-string)
+                  'face 'elpher-info)))
+        (other ;; Unknown
+         (elpher-insert-margin (concat (char-to-string type) "?"))
+         (insert (propertize display-string
+                             'face 'elpher-unknown-face)))))
     (insert "\n")))
 
 (defun elpher-click-link (button)
     (insert "\n")))
 
 (defun elpher-click-link (button)
@@ -413,24 +439,16 @@ The result is stored as a string in the variable ‘elpher-selector-string’."
           (elpher-with-clean-buffer
            (insert content)
            (elpher-restore-pos)))
           (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-set-node-content elpher-current-node
+                                                         (buffer-string)))))))))
 
 ;; Text retrieval
 
 
 ;; Text retrieval
 
@@ -438,52 +456,53 @@ The result is stored as a string in the variable ‘elpher-selector-string’."
   "\\([a-zA-Z]+\\)://\\([a-zA-Z0-9.\-]+\\)\\(?3::[0-9]+\\)?\\(?4:/[^ \r\n\t(),]*\\)?"
   "Regexp used to locate and buttinofy URLs in text files loaded by elpher.")
 
   "\\([a-zA-Z]+\\)://\\([a-zA-Z0-9.\-]+\\)\\(?3::[0-9]+\\)?\\(?4:/[^ \r\n\t(),]*\\)?"
   "Regexp used to locate and buttinofy URLs in text files loaded by elpher.")
 
+(defun elpher-make-node-from-matched-url (parent &optional string)
+  "Convert most recent `elpher-url-regex' match to a node.
+
+PARENT defines the node to set as the parent to the new node.
+
+If STRING is non-nil, this is given as an argument to all `match-string'
+calls, as is necessary if the match is performed by `string-match'."
+  (let ((url (match-string 0 string))
+        (protocol (downcase (match-string 1 string))))
+    (if (string= protocol "gopher")
+        (let* ((host (match-string 2 string))
+               (port (if (> (length (match-string 3 string))  1)
+                         (string-to-number (substring (match-string 3 string) 1))
+                       70))
+               (type-and-selector (match-string 4 string))
+               (type (if (> (length type-and-selector) 1)
+                         (elt type-and-selector 1)
+                       ?1))
+               (selector (if (> (length type-and-selector) 1)
+                             (substring type-and-selector 2)
+                           ""))
+               (address (elpher-make-address type selector host port)))
+          (elpher-make-node url elpher-current-node address))
+      (let* ((host (match-string 2 string))
+             (port (if (> (length (match-string 3 string)) 1)
+                       (string-to-number (substring (match-string 3 string) 1))
+                     70))
+             (selector (concat "URL:" url))
+             (address (elpher-make-address ?h selector host port)))
+        (elpher-make-node url elpher-current-node address)))))
+
+
 (defun elpher-buttonify-urls (string)
   "Turn substrings which look like urls in STRING into clickable buttons."
   (with-temp-buffer
     (insert string)
     (goto-char (point-min))
     (while (re-search-forward elpher-url-regex nil t)
 (defun elpher-buttonify-urls (string)
   "Turn substrings which look like urls in STRING into clickable buttons."
   (with-temp-buffer
     (insert string)
     (goto-char (point-min))
     (while (re-search-forward elpher-url-regex nil t)
-      (let ((url (match-string 0))
-            (protocol (downcase (match-string 1))))
-        (let ((node
-               (if (string= protocol "gopher")
-                   (let* ((host (match-string 2))
-                          (port (if (> (length (match-string 3))  1)
-                                    (string-to-number (substring (match-string 3) 1))
-                                  70))
-                          (type-and-selector (match-string 4))
-                          (type (if (> (length type-and-selector) 1)
-                                    (elt type-and-selector 1)
-                                  ?1))
-                          (selector (if (> (length type-and-selector) 1)
-                                        (substring type-and-selector 2)
-                                      ""))
-                          (address (elpher-make-address selector host port))
-                          (getter (car (alist-get type elpher-type-map))))
-                     (elpher-make-node elpher-current-node address getter))
-                 (let* ((host (match-string 2))
-                        (port (if (> (length (match-string 3)) 1)
-                                  (string-to-number (substring (match-string 3) 1))
-                                70))
-                        (selector (concat "URL:" url))
-                        (address (elpher-make-address selector host port))
-                        (getter (car (alist-get ?h elpher-type-map))))
-                   (elpher-make-node elpher-current-node address getter)))))
+        (let ((node (elpher-make-node-from-matched-url elpher-current-node)))
           (make-text-button (match-beginning 0)
                             (match-end 0)
                             'elpher-node  node
                             'action #'elpher-click-link
                             'follow-link t
           (make-text-button (match-beginning 0)
                             (match-end 0)
                             'elpher-node  node
                             'action #'elpher-click-link
                             'follow-link t
-                            'help-echo (elpher-node-button-help node)))))
+                            'help-echo (elpher-node-button-help node))))
     (buffer-string)))
 
     (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))
 (defun elpher-get-text-node ()
   "Getter which retrieves the current node contents as a text document."
   (let ((content (elpher-node-content elpher-current-node))
@@ -500,7 +519,9 @@ 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
                               (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-restore-pos)
                                    (elpher-set-node-content elpher-current-node
                                                             (buffer-string))))))))))
@@ -524,8 +545,9 @@ The result is stored as a string in the variable ‘elpher-selector-string’."
                                  (lambda (proc event)
                                    (unless (string-prefix-p "deleted" event)
                                      (let ((image (create-image
                                  (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)
                                                    nil t)))
                                        (elpher-with-clean-buffer
                                         (insert-image image)
@@ -551,9 +573,10 @@ The result is stored as a string in the variable ‘elpher-selector-string’."
       (unwind-protect
           (let* ((query-string (read-string "Query: "))
                  (query-selector (concat (elpher-address-selector address) "\t" query-string))
       (unwind-protect
           (let* ((query-string (read-string "Query: "))
                  (query-selector (concat (elpher-address-selector address) "\t" query-string))
-                 (search-address (elpher-make-address query-selector
-                                                       (elpher-address-host address)
-                                                       (elpher-address-port address))))
+                 (search-address (elpher-make-address ?1
+                                                      query-selector
+                                                      (elpher-address-host address)
+                                                      (elpher-address-port address))))
             (setq aborted nil)
             (elpher-with-clean-buffer
              (insert "LOADING RESULTS..."))
             (setq aborted nil)
             (elpher-with-clean-buffer
              (insert "LOADING RESULTS..."))
@@ -616,7 +639,7 @@ The result is stored as a string in the variable ‘elpher-selector-string’."
 
 ;; URL retrieval
 
 
 ;; URL retrieval
 
-(defun elpher-get-url ()
+(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)))
   "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)))
@@ -626,97 +649,120 @@ The result is stored as a string in the variable ‘elpher-selector-string’."
           (browse-web url)
         (browse-url url)))))
 
           (browse-web url)
         (browse-url url)))))
 
+;; Telnet node connection
+
+(defun elpher-get-telnet-node ()
+  "Getter which opens a telnet connection to the server specified by the current node."
+  (let* ((address (elpher-node-address elpher-current-node))
+         (host (elpher-address-host address))
+         (port (elpher-address-port address)))
+    (elpher-visit-parent-node)
+    (telnet host port)))
+
+;; Start 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
 ;;
 
 ;;; Bookmarks
 ;;
 
-(defun elpher-make-bookmark (type display-string address)
-  (list type display-string address))
+(defun elpher-make-bookmark (display-string address)
+  "Make an elpher bookmark.
+DISPLAY-STRING determines how the bookmark will appear in the
+bookmark list, while ADDRESS is the address of the entry."
+  (list display-string address))
   
   
-(defun elpher-bookmark-type (bookmark)
-  (elt bookmark 0))
-
 (defun elpher-bookmark-display-string (bookmark)
 (defun elpher-bookmark-display-string (bookmark)
-  (elt bookmark 1))
+  "Get the display string of BOOKMARK."
+  (elt bookmark 0))
 
 (defun elpher-bookmark-address (bookmark)
 
 (defun elpher-bookmark-address (bookmark)
-  (elt bookmark 2))
+  "Get the address for BOOKMARK."
+  (elt bookmark 1))
 
 (defun elpher-save-bookmarks (bookmarks)
 
 (defun elpher-save-bookmarks (bookmarks)
+  "Record the bookmark list BOOKMARKS to the user's bookmark file.
+Beware that this completely replaces the existing contents of the file."
   (with-temp-file (locate-user-emacs-file "elpher-bookmarks")
     (erase-buffer)
     (pp bookmarks (current-buffer))))
 
 (defun elpher-load-bookmarks ()
   (with-temp-file (locate-user-emacs-file "elpher-bookmarks")
     (erase-buffer)
     (pp bookmarks (current-buffer))))
 
 (defun elpher-load-bookmarks ()
-  (with-temp-buffer 
+  "Get the list of bookmarks from the users's bookmark file."
+  (with-temp-buffer
     (ignore-errors
       (insert-file-contents (locate-user-emacs-file "elpher-bookmarks"))
       (goto-char (point-min))
       (read (current-buffer)))))
 
     (ignore-errors
       (insert-file-contents (locate-user-emacs-file "elpher-bookmarks"))
       (goto-char (point-min))
       (read (current-buffer)))))
 
-(defun elpher-add-bookmark (bookmark)
-  (let ((bookmarks (elpher-load-bookmarks)))
+(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)
     (elpher-save-bookmarks bookmarks)))
 
     (add-to-list 'bookmarks bookmark)
     (elpher-save-bookmarks bookmarks)))
 
-(defun elpher-remove-bookmark (bookmark)
-  (elpher-save-bookmarks
-   (seq-filter (lambda (this-bookmark)
-                 (not (equal bookmark this-bookmark)))
-               (elpher-load-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))))
+    (elpher-save-bookmarks
+     (seq-filter (lambda (this-bookmark)
+                   (not (equal bookmark this-bookmark)))
+                 (elpher-load-bookmarks)))))
      
 (defun elpher-display-bookmarks ()
      
 (defun elpher-display-bookmarks ()
+  "Display saved bookmark list."
   (interactive)
   (elpher-with-clean-buffer
   (interactive)
   (elpher-with-clean-buffer
-   (insert
-    "Use 'u' to return to the previous page.\n\n"
-    "---- Bookmark list ----\n\n")
+   (insert "Use 'r' to return to the previous page.\n\n"
+           "---- Bookmark list ----\n\n")
    (let ((bookmarks (elpher-load-bookmarks)))
      (if bookmarks
    (let ((bookmarks (elpher-load-bookmarks)))
      (if bookmarks
-         (dolist (bookmark (elpher-load-bookmarks))
-           (let ((type (elpher-bookmark-type bookmark))
-                 (display-string (elpher-bookmark-display-string bookmark))
+         (dolist (bookmark bookmarks)
+           (let ((display-string (elpher-bookmark-display-string bookmark))
                  (address (elpher-bookmark-address bookmark)))
                  (address (elpher-bookmark-address bookmark)))
-             (elpher-insert-index-record-helper type display-string
-                                                (elpher-address-selector address)
-                                                (elpher-address-host address)
-                                                (elpher-address-port address))))
+             (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)))
 
        (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
 (defun elpher-bookmark-link ()
   "Bookmark the link at point."
   (interactive)
   (let ((button (button-at (point))))
     (if button
-        (let ((node (button-get button 'elpher-node))
-              (type (button-get button 'elpher-node-type))
-              (label (button-label button)))
-          (if node
-              (progn
-                (elpher-add-bookmark
-                 (elpher-make-bookmark type
-                                       label
-                                       (elpher-node-address node)))
-                (message "Bookmarked \"%s\"" label))
-            (error "Can only bookmark gopher links, not general URLs.")))
-      (error "No link selected."))))
+        (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
 
 (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))
-              (type (button-get button 'elpher-node-type)))
-          (if node
-              (elpher-remove-bookmark 
-               (elpher-make-bookmark type
-                                     (button-label button)
-                                     (elpher-node-address node)))
-            (error "Can only bookmark gopher links, not general URLs.")))
-      (error "No link selected."))))
+        (elpher-remove-node-bookmark (button-get button 'elpher-node))
+      (error "No link selected"))))
 
 ;;; Interactive navigation procedures
 ;;
 
 ;;; Interactive navigation procedures
 ;;
@@ -739,16 +785,21 @@ The result is stored as a string in the variable ‘elpher-selector-string’."
 (defun elpher-go ()
   "Go to a particular gopher site."
   (interactive)
 (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 ""))
-         (port (read-string "Port (default 70): " nil nil 70))
-         (address (list selector hostname port)))
-    (elpher-visit-node
-     (elpher-make-node elpher-current-node
-                        address
-                        #'elpher-get-index-node))))
+  (let ((node
+         (let ((host-or-url (read-string "Gopher host or URL: ")))
+           (if (string-match elpher-url-regex host-or-url)
+               (elpher-make-node-from-matched-url elpher-current-node
+                                                  host-or-url)
+             (let ((selector (read-string "Selector (default none): " nil nil ""))
+                   (port-string (read-string "Port (default 70): " nil nil "70")))
+               (elpher-make-node (concat "gopher://" host-or-url
+                                         ":" port-string
+                                         "/1" selector)
+                                 elpher-current-node
+                                 (elpher-make-address ?1 selector host-or-url
+                                                      (string-to-number port-string))))))))
+    (switch-to-buffer "*elpher*")
+    (elpher-visit-node node)))
 
 (defun  elpher-redraw ()
   "Redraw current page."
 
 (defun  elpher-redraw ()
   "Redraw current page."
@@ -777,7 +828,7 @@ The result is stored as a string in the variable ‘elpher-selector-string’."
   (interactive)
   (if (elpher-node-parent elpher-current-node)
       (elpher-visit-parent-node)
   (interactive)
   (if (elpher-node-parent elpher-current-node)
       (elpher-visit-parent-node)
-    (error "No previous site.")))
+    (error "No previous site")))
 
 (defun elpher-download ()
   "Download the link at point."
 
 (defun elpher-download ()
   "Download the link at point."
@@ -788,8 +839,8 @@ The result is stored as a string in the variable ‘elpher-selector-string’."
           (if node
               (elpher-visit-node (button-get button 'elpher-node)
                                  #'elpher-get-node-download)
           (if node
               (elpher-visit-node (button-get button 'elpher-node)
                                  #'elpher-get-node-download)
-            (error "Can only download gopher links, not general URLs.")))
-      (error "No link selected."))))
+            (error "Can only download gopher links, not general URLs")))
+      (error "No link selected"))))
 
 (defun elpher-build-link-map ()
   "Build alist mapping link names to destination nodes in current buffer."
 
 (defun elpher-build-link-map ()
   "Build alist mapping link names to destination nodes in current buffer."
@@ -800,7 +851,7 @@ The result is stored as a string in the variable ‘elpher-selector-string’."
       (setq b (next-button (button-start b))))
     link-map))
 
       (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)))
   "Select a directory entry by name.  Similar to the info browser (m)enu command."
   (interactive)
   (let* ((link-map (elpher-build-link-map)))
@@ -816,19 +867,79 @@ The result is stored as a string in the variable ‘elpher-selector-string’."
 (defun elpher-root-dir ()
   "Visit root of current server."
   (interactive)
 (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)))
           (if (> (length selector) 0)
         (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)))
-            (error "Already at root directory of current server.")))
-      (error "Command invalid for Elpher start page."))))
+              (let ((root-address (elpher-make-address ?1 "" host port)))
+                (elpher-visit-node
+                 (elpher-make-node (concat "gopher://" host
+                                           ":" (number-to-string port)
+                                           "/1/")
+                                   elpher-current-node
+                                   root-address)))
+            (error "Already at root directory of current server")))
+      (error "Command invalid for this page"))))
+
+(defun elpher-info-node (node)
+  "Display information on NODE."
+  (let ((display-string (elpher-node-display-string node))
+        (address (elpher-node-address node)))
+    (if address
+        (message "`%s' on %s port %s"
+                (elpher-address-selector address)
+                (elpher-address-host address)
+                (elpher-address-port address))
+      (message "%s" display-string))))
+
+(defun elpher-info-link ()
+  "Display information on node corresponding to link at point."
+  (interactive)
+  (let ((button (button-at (point))))
+    (if button
+        (elpher-info-node (button-get button 'elpher-node))
+      (error "No item selected"))))
+  
+(defun elpher-info-current ()
+  "Display information on current node."
+  (interactive)
+  (elpher-info-node elpher-current-node))
+
+(defun elpher-get-address-url (address)
+  "Get URL representation of ADDRESS."
+  (concat "gopher://"
+          (elpher-address-host address)
+          (let ((port (elpher-address-port address)))
+            (if (equal port 70)
+                ""
+              (format ":%d" port)))
+          "/" (string (elpher-address-type address))
+          (elpher-address-selector address)))
+
+(defun elpher-copy-node-url (node)
+  "Copy URL representation of address of NODE to `kill-ring'."
+  (let ((address (elpher-node-address node)))
+    (if address
+        (let ((url (elpher-get-address-url address)))
+          (message url)
+          (kill-new url))
+      (error (format "Cannot represent %s as URL" (elpher-node-display-string node))))))
+
+(defun elpher-copy-link-url ()
+  "Copy URL of item at point to `kill-ring'."
+  (interactive)
+  (let ((button (button-at (point))))
+    (if button
+        (elpher-copy-node-url (button-get button 'elpher-node))
+      (error "No item selected"))))
 
 
+(defun elpher-copy-current-url ()
+  "Copy URL of current node to `kill-ring'."
+  (interactive)
+  (elpher-copy-node-url elpher-current-node))
 
 ;;; Mode and keymap
 ;;
 
 ;;; Mode and keymap
 ;;
@@ -844,9 +955,12 @@ The result is stored as a string in the variable ‘elpher-selector-string’."
     (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 "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)
     (when (fboundp 'evil-define-key)
     (when (fboundp 'evil-define-key)
-      (add-to-list 'evil-motion-state-modes 'elpher-mode)
       (evil-define-key 'motion map
         (kbd "TAB") 'elpher-next-link
         (kbd "C-]") 'elpher-follow-current-link
       (evil-define-key 'motion map
         (kbd "TAB") 'elpher-next-link
         (kbd "C-]") 'elpher-follow-current-link
@@ -858,9 +972,15 @@ The result is stored as a string in the variable ‘elpher-selector-string’."
         (kbd "R") 'elpher-reload
         (kbd "w") 'elpher-view-raw
         (kbd "d") 'elpher-download
         (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
+        (kbd "C") 'elpher-copy-current-url
         (kbd "a") 'elpher-bookmark-link
         (kbd "a") 'elpher-bookmark-link
+        (kbd "A") 'elpher-bookmark-current
         (kbd "x") 'elpher-unbookmark-link
         (kbd "x") 'elpher-unbookmark-link
+        (kbd "X") 'elpher-unbookmark-current
         (kbd "B") 'elpher-display-bookmarks))
     map)
   "Keymap for gopher client.")
         (kbd "B") 'elpher-display-bookmarks))
     map)
   "Keymap for gopher client.")
@@ -868,6 +988,8 @@ The result is stored as a string in the variable ‘elpher-selector-string’."
 (define-derived-mode elpher-mode special-mode "elpher"
   "Major mode for elpher, an elisp gopher client.")
 
 (define-derived-mode elpher-mode special-mode "elpher"
   "Major mode for elpher, an elisp gopher client.")
 
+(when (fboundp 'evil-set-initial-state)
+  (evil-set-initial-state 'elpher-mode 'motion))
 
 ;;; Main start procedure
 ;;
 
 ;;; Main start procedure
 ;;
@@ -880,9 +1002,7 @@ The result is stored as a string in the variable ‘elpher-selector-string’."
       (switch-to-buffer "*elpher*")
     (switch-to-buffer "*elpher*")
     (setq elpher-current-node nil)
       (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)))
+    (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.
 
       (elpher-visit-node start-node)))
   "Started Elpher.") ; Otherwise (elpher) evaluates to start page string.