Removed unused start page address customization.
[elpher.git] / elpher.el
1 ;;; elpher.el --- Full-featured gopher client.
2
3 ;; Copyright (C) 2019 Tim Vaughan
4
5 ;; Author: Tim Vaughan <tgvaughan@gmail.com>
6 ;; Created: 11 April 2019
7 ;; Version: 1.1.0
8 ;; Keywords: comm gopher
9 ;; Homepage: https://github.com/tgvaughan/elpher
10 ;; Package-Requires: ((emacs "25"))
11
12 ;; This file is not part of GNU Emacs.
13
14 ;; This program is free software: you can redistribute it and/or modify
15 ;; it under the terms of the GNU General Public License as published by
16 ;; the Free Software Foundation, either version 3 of the License, or
17 ;; (at your option) any later version.
18
19 ;; This program is distributed in the hope that it will be useful,
20 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
21 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
22 ;; GNU General Public License for more details.
23
24 ;; You should have received a copy of the GNU General Public License
25 ;; along with this file.  If not, see <http://www.gnu.org/licenses/>.
26
27 ;;; Commentary:
28
29 ;; Elpher aims to provide a full-featured gopher client for GNU Emacs.
30 ;; It supports:
31
32 ;; - intuitive keyboard and mouse-driven interface,
33 ;; - caching of visited sites (both content and cursor position),
34 ;; - pleasant and configurable colouring of Gopher directories,
35 ;; - direct visualisation of image files,
36 ;; - (m)enu key support, similar to Emacs' info browser,
37 ;; - clickable web and gopher links in plain text.
38
39 ;; Visited pages are stored as a hierarchy rather than a linear history,
40 ;; meaning that navigation between these pages is quick and easy.
41
42 ;; To launch Elpher, simply use 'M-x elpher'.  This will open a start
43 ;; page containing information on key bindings and suggested starting
44 ;; points for your gopher exploration.
45
46 ;; Faces, caching options and start page can be configured via
47 ;; the Elpher customization group in Applications.
48
49 ;;; Code:
50
51 (provide 'elpher)
52 (require 'seq)
53 (require 'pp)
54
55 ;;; Global constants
56 ;;
57
58 (defconst elpher-version "1.1.0"
59   "Current version of elpher.")
60
61 (defconst elpher-margin-width 6
62   "Width of left-hand margin used when rendering indicies.")
63
64 (defconst elpher-start-index
65   (mapconcat
66    'identity
67    (list "i\tfake\tfake\t1"
68          "i     --------------------------------------------\tfake\tfake\t1"
69          "i                Elpher Gopher Client             \tfake\tfake\t1"
70          (format "i                   version %s\tfake\tfake\t1" elpher-version)
71          "i     --------------------------------------------\tfake\tfake\t1"
72          "i\tfake\tfake\t1"
73          "iUsage:\tfake\tfake\t1"
74          "i\tfake\tfake\t1"
75          "i - tab/shift-tab: next/prev item on current page\tfake\tfake\t1"
76          "i - RET/mouse-1: open item under cursor\tfake\tfake\t1"
77          "i - m: select an item on current page by name (autocompletes)\tfake\tfake\t1"
78          "i - u: return to parent\tfake\tfake\t1"
79          "i - O: visit the root menu of the current server\tfake\tfake\t1"
80          "i - g: go to a particular menu or item\tfake\tfake\t1"
81          "i - i/I: info on item under cursor or current page\tfake\tfake\t1"
82          "i - c/C: copy URL representation of item under cursor or current page\tfake\tfake\t1"
83          "i - r: redraw current page (using cached contents if available)\tfake\tfake\t1"
84          "i - R: reload current page (regenerates cache)\tfake\tfake\t1"
85          "i - d: download directory entry under cursor\tfake\tfake\t1"
86          "i - w: display the raw server response for the current page\tfake\tfake\t1"
87          "i\tfake\tfake\t1"
88          "iPlaces to start exploring Gopherspace:\tfake\tfake\t1"
89          "i\tfake\tfake\t1"
90          "1Floodgap Systems Gopher Server\t/\tgopher.floodgap.com\t70"
91          "i\tfake\tfake\t1"
92          "iAlternatively, select the following item and enter some\tfake\tfake\t1"
93          "isearch terms:\tfake\tfake\t1"
94          "i\tfake\tfake\t1"
95          "7Veronica-2 Gopher Search Engine\t/v2/vs\tgopher.floodgap.com\t70"
96          ".\r\n")
97    "\r\n")
98   "Source for elpher start page.")
99
100 (defconst elpher-type-map
101   '((?0 elpher-get-text-node "T" elpher-text)
102     (?1 elpher-get-index-node "/" elpher-index)
103     (?4 elpher-get-node-download "B" elpher-binary)
104     (?5 elpher-get-node-download "B" elpher-binary)
105     (?7 elpher-get-search-node "?" elpher-search)
106     (?8 elpher-get-telnet-node "?" elpher-telnet)
107     (?9 elpher-get-node-download "B" elpher-binary)
108     (?g elpher-get-image-node "im" elpher-image)
109     (?p elpher-get-image-node "im" elpher-image)
110     (?I elpher-get-image-node "im" elpher-image)
111     (?h elpher-get-url-node "W" elpher-url)
112     (bookmarks elpher-get-bookmarks-node "#" elpher-index)
113     (start elpher-get-start-node "#" elpher-index))
114   "Association list from types to getters, margin codes and index faces.")
115
116
117 ;;; Customization group
118 ;;
119
120 (defgroup elpher nil
121   "A gopher client."
122   :group 'applications)
123
124 ;; Face customizations
125
126 (defface elpher-index
127   '((t :inherit font-lock-keyword-face))
128   "Face used for directory type directory records.")
129
130 (defface elpher-text
131   '((t :inherit bold))
132   "Face used for text type directory records.")
133
134 (defface elpher-info
135   '((t :inherit default))
136   "Face used for info type directory records.")
137
138 (defface elpher-image
139   '((t :inherit font-lock-string-face))
140   "Face used for image type directory records.")
141
142 (defface elpher-search
143   '((t :inherit warning))
144   "Face used for search type directory records.")
145
146 (defface elpher-url
147   '((t :inherit font-lock-comment-face))
148   "Face used for url type directory records.")
149
150 (defface elpher-telnet
151   '((t :inherit font-lock-function-name-face))
152   "Face used for telnet type directory records.")
153
154 (defface elpher-binary
155   '((t :inherit font-lock-doc-face))
156   "Face used for binary type directory records.")
157
158 (defface elpher-unknown
159   '((t :inherit error))
160   "Face used for directory records with unknown/unsupported types.")
161
162 (defface elpher-margin-key
163   '((t :inherit bold))
164   "Face used for directory margin key.")
165
166 (defface elpher-margin-brackets
167   '((t :inherit shadow))
168   "Face used for brackets around directory margin key.")
169
170 ;; Other customizations
171
172 (defcustom elpher-open-urls-with-eww nil
173   "If non-nil, open URL selectors using eww.
174 Otherwise, use the system browser via the BROWSE-URL function."
175   :type '(boolean))
176
177 (defcustom elpher-buttonify-urls-in-directories nil
178   "If non-nil, turns URLs matched in directories into clickable buttons."
179   :type '(boolean))
180
181 (defcustom elpher-cache-images nil
182   "If non-nil, cache images in memory in the same way as other content."
183   :type '(boolean))
184
185 (defcustom elpher-use-header t
186   "If non-nil, display current node information in buffer header."
187   :type '(boolean))
188
189 ;;; Model
190 ;;
191
192 ;; Address
193
194 (defun elpher-make-address (type &optional selector host port)
195   "Create an address of a gopher object with TYPE, SELECTOR, HOST and PORT.
196 Although selector host and port are optional, they are only omitted for
197 special address types, such as 'start for the start page."
198   (list type selector host port))
199
200 (defun elpher-address-type (address)
201   "Retrieve type from ADDRESS."
202   (elt address 0))
203
204 (defun elpher-address-selector (address)
205   "Retrieve selector from ADDRESS."
206   (elt address 1))
207
208 (defun elpher-address-host (address)
209   "Retrieve host from ADDRESS."
210   (elt address 2))
211
212 (defun elpher-address-port (address)
213   "Retrieve port from ADDRESS."
214   (elt address 3))
215
216 ;; Node
217
218 (defun elpher-make-node (display-string parent address)
219   "Create a node in the gopher page hierarchy.
220
221 DISPLAY-STRING records the display string used for the page.
222
223 PARENT specifies the parent of the node, and ADDRESS specifies the
224 address of the gopher page."
225   (list display-string parent address))
226
227 (defun elpher-node-display-string (node)
228   "Retrieve the display string of NODE."
229   (elt node 0))
230
231 (defun elpher-node-parent (node)
232   "Retrieve the parent node of NODE."
233   (elt node 1))
234
235 (defun elpher-node-address (node)
236   "Retrieve the address of NODE."
237   (elt node 2))
238
239 ;; Cache
240
241 (defvar elpher-content-cache (make-hash-table :test 'equal))
242 (defvar elpher-pos-cache (make-hash-table :test 'equal))
243
244 (defun elpher-get-cached-content (address)
245   "Retrieve the cached content for ADDRESS, or nil if none exists."
246   (gethash address elpher-content-cache))
247
248 (defun elpher-cache-content (address content)
249   "Set the content cache for ADDRESS to CONTENT."
250   (puthash address content elpher-content-cache))
251
252 (defun elpher-get-cached-pos (address)
253   "Retrieve the cached cursor position for ADDRESS, or nil if none exists."
254   (gethash address elpher-pos-cache))
255
256 (defun elpher-cache-pos (address pos)
257   "Set the cursor position cache for ADDRESS to POS."
258   (puthash address pos elpher-pos-cache))
259
260 ;; Node graph traversal
261
262 (defvar elpher-current-node nil)
263
264 (defun elpher-visit-node (node &optional getter)
265   "Visit NODE using its own getter or GETTER, if non-nil."
266   (elpher-save-pos)
267   (elpher-process-cleanup)
268   (setq elpher-current-node node)
269   (if getter
270       (funcall getter)
271     (let* ((address (elpher-node-address node))
272            (type (elpher-address-type address)))
273       (funcall (car (alist-get type elpher-type-map))))))
274
275 (defun elpher-visit-parent-node ()
276   "Visit the parent of the current node."
277   (let ((parent-node (elpher-node-parent elpher-current-node)))
278     (when parent-node
279       (elpher-visit-node parent-node))))
280       
281 (defun elpher-reload-current-node ()
282   "Reload the current node, discarding any existing cached content."
283   (elpher-cache-content (elpher-node-address elpher-current-node) nil)
284   (elpher-visit-node elpher-current-node))
285
286 (defun elpher-save-pos ()
287   "Save the current position of point to the current node."
288   (when elpher-current-node
289     (elpher-cache-pos (elpher-node-address elpher-current-node) (point))))
290
291 (defun elpher-restore-pos ()
292   "Restore the position of point to that cached in the current node."
293   (let ((pos (elpher-get-cached-pos (elpher-node-address elpher-current-node))))
294     (if pos
295         (goto-char pos)
296       (goto-char (point-min)))))
297
298
299 ;;; Buffer preparation
300 ;;
301
302 (defun elpher-update-header ()
303   "If `elpher-use-header' is true, display current node info in window header."
304   (if elpher-use-header
305       (setq header-line-format (elpher-node-display-string elpher-current-node))))
306
307 (defmacro elpher-with-clean-buffer (&rest args)
308   "Evaluate ARGS with a clean *elpher* buffer as current."
309   (list 'with-current-buffer "*elpher*"
310         '(elpher-mode)
311         (append (list 'let '((inhibit-read-only t))
312                       '(erase-buffer)
313                       '(elpher-update-header))
314                 args)))
315
316
317 ;;; Index rendering
318 ;;
319
320 (defun elpher-preprocess-text-response (string)
321   "Clear away CRs and terminating period from STRING."
322   (replace-regexp-in-string "\n\.\n$" "\n"
323                             (replace-regexp-in-string "\r" ""
324                                                       string)))
325
326 (defun elpher-insert-index (string)
327   "Insert the index corresponding to STRING into the current buffer."
328   ;; Should be able to split directly on CRLF, but some non-conformant
329   ;; LF-only servers sadly exist, hence the following.
330   (let ((str-processed (elpher-preprocess-text-response string)))
331     (dolist (line (split-string str-processed "\n"))
332       (unless (= (length line) 0)
333         (let* ((type (elt line 0))
334                (fields (split-string (substring line 1) "\t"))
335                (display-string (elt fields 0))
336                (selector (elt fields 1))
337                (host (elt fields 2))
338                (port (if (elt fields 3)
339                          (string-to-number (elt fields 3))
340                        nil)))
341           (elpher-insert-index-record display-string type selector host port))))))
342
343 (defun elpher-insert-margin (&optional type-name)
344   "Insert index margin, optionally containing the TYPE-NAME, into the current buffer."
345   (if type-name
346       (progn
347         (insert (format (concat "%" (number-to-string (- elpher-margin-width 1)) "s")
348                         (concat
349                          (propertize "[" 'face 'elpher-margin-brackets)
350                          (propertize type-name 'face 'elpher-margin-key)
351                          (propertize "]" 'face 'elpher-margin-brackets))))
352         (insert " "))
353     (insert (make-string elpher-margin-width ?\s))))
354
355 (defun elpher-node-button-help (node)
356   "Return a string containing the help text for a button corresponding to NODE."
357   (let ((address (elpher-node-address node)))
358     (if (eq (elpher-address-type address) ?h)
359         (let ((url (cadr (split-string (elpher-address-selector address) "URL:"))))
360           (format "mouse-1, RET: open url '%s'" url))
361       (format "mouse-1, RET: open '%s' on %s port %s"
362               (elpher-address-selector address)
363               (elpher-address-host address)
364               (elpher-address-port address)))))
365
366 (defun elpher-insert-index-record (display-string type selector host port)
367   "Function to insert an index record into the current buffer.
368 The contents of the record are dictated by TYPE, DISPLAY-STRING, SELECTOR, HOST
369 and PORT."
370   (let ((address (elpher-make-address type selector host port))
371         (type-map-entry (alist-get type elpher-type-map)))
372     (if type-map-entry
373         (let* ((margin-code (cadr type-map-entry))
374                (face (elt type-map-entry 2))
375                (node (elpher-make-node display-string elpher-current-node address)))
376           (elpher-insert-margin margin-code)
377           (insert-text-button display-string
378                               'face face
379                               'elpher-node node
380                               'action #'elpher-click-link
381                               'follow-link t
382                               'help-echo (elpher-node-button-help node)))
383       (pcase type
384         (?i ;; Information
385          (elpher-insert-margin)
386          (insert (propertize
387                   (if elpher-buttonify-urls-in-directories
388                       (elpher-buttonify-urls display-string)
389                     display-string)
390                   'face 'elpher-info)))
391         (other ;; Unknown
392          (elpher-insert-margin (concat (char-to-string type) "?"))
393          (insert (propertize display-string
394                              'face 'elpher-unknown-face)))))
395     (insert "\n")))
396
397 (defun elpher-click-link (button)
398   "Function called when the gopher link BUTTON is activated (via mouse or keypress)."
399   (let ((node (button-get button 'elpher-node)))
400     (elpher-visit-node node)))
401
402
403 ;;; Selector retrieval (all kinds)
404 ;;
405
406 (defun elpher-process-cleanup ()
407   "Immediately shut down any extant elpher process."
408   (let ((p (get-process "elpher-process")))
409     (if p (delete-process p))))
410
411 (defvar elpher-selector-string)
412
413 (defun elpher-get-selector (address after)
414   "Retrieve selector specified by ADDRESS, then execute AFTER.
415 The result is stored as a string in the variable â€˜elpher-selector-string’."
416   (setq elpher-selector-string "")
417   (make-network-process
418    :name "elpher-process"
419    :host (elpher-address-host address)
420    :service (elpher-address-port address)
421    :filter (lambda (proc string)
422              (setq elpher-selector-string (concat elpher-selector-string string)))
423    :sentinel after)
424   (process-send-string "elpher-process"
425                        (concat (elpher-address-selector address) "\n")))
426
427 ;; Index retrieval
428
429 (defun elpher-get-index-node ()
430   "Getter which retrieves the current node contents as an index."
431   (let* ((address (elpher-node-address elpher-current-node))
432          (content (elpher-get-cached-content address)))
433     (if content
434         (progn
435           (elpher-with-clean-buffer
436            (insert content)
437            (elpher-restore-pos)))
438       (elpher-with-clean-buffer
439        (insert "LOADING DIRECTORY..."))
440       (elpher-get-selector address
441                            (lambda (proc event)
442                              (unless (string-prefix-p "deleted" event)
443                                (elpher-with-clean-buffer
444                                 (elpher-insert-index elpher-selector-string)
445                                 (elpher-restore-pos)
446                                 (elpher-cache-content
447                                  (elpher-node-address elpher-current-node)
448                                  (buffer-string)))))))))
449
450 ;; Text retrieval
451
452 (defconst elpher-url-regex
453   "\\([a-zA-Z]+\\)://\\([a-zA-Z0-9.\-]+\\)\\(?3::[0-9]+\\)?\\(?4:/[^ \r\n\t(),]*\\)?"
454   "Regexp used to locate and buttinofy URLs in text files loaded by elpher.")
455
456 (defun elpher-make-node-from-matched-url (parent &optional string)
457   "Convert most recent `elpher-url-regex' match to a node.
458
459 PARENT defines the node to set as the parent to the new node.
460
461 If STRING is non-nil, this is given as an argument to all `match-string'
462 calls, as is necessary if the match is performed by `string-match'."
463   (let ((url (match-string 0 string))
464         (protocol (downcase (match-string 1 string))))
465     (if (string= protocol "gopher")
466         (let* ((host (match-string 2 string))
467                (port (if (> (length (match-string 3 string))  1)
468                          (string-to-number (substring (match-string 3 string) 1))
469                        70))
470                (type-and-selector (match-string 4 string))
471                (type (if (> (length type-and-selector) 1)
472                          (elt type-and-selector 1)
473                        ?1))
474                (selector (if (> (length type-and-selector) 1)
475                              (substring type-and-selector 2)
476                            ""))
477                (address (elpher-make-address type selector host port)))
478           (elpher-make-node url elpher-current-node address))
479       (let* ((host (match-string 2 string))
480              (port (if (> (length (match-string 3 string)) 1)
481                        (string-to-number (substring (match-string 3 string) 1))
482                      70))
483              (selector (concat "URL:" url))
484              (address (elpher-make-address ?h selector host port)))
485         (elpher-make-node url elpher-current-node address)))))
486
487
488 (defun elpher-buttonify-urls (string)
489   "Turn substrings which look like urls in STRING into clickable buttons."
490   (with-temp-buffer
491     (insert string)
492     (goto-char (point-min))
493     (while (re-search-forward elpher-url-regex nil t)
494         (let ((node (elpher-make-node-from-matched-url elpher-current-node)))
495           (make-text-button (match-beginning 0)
496                             (match-end 0)
497                             'elpher-node  node
498                             'action #'elpher-click-link
499                             'follow-link t
500                             'help-echo (elpher-node-button-help node))))
501     (buffer-string)))
502
503 (defun elpher-get-text-node ()
504   "Getter which retrieves the current node contents as a text document."
505   (let* ((address (elpher-node-address elpher-current-node))
506          (content (elpher-get-cached-content address)))
507     (if content
508         (progn
509           (elpher-with-clean-buffer
510            (insert content)
511            (elpher-restore-pos)))
512       (progn
513         (elpher-with-clean-buffer
514          (insert "LOADING TEXT..."))
515         (elpher-get-selector address
516                               (lambda (proc event)
517                                 (unless (string-prefix-p "deleted" event)
518                                   (elpher-with-clean-buffer
519                                    (insert (elpher-buttonify-urls
520                                             (elpher-preprocess-text-response
521                                              elpher-selector-string)))
522                                    (elpher-restore-pos)
523                                    (elpher-cache-content
524                                     (elpher-node-address elpher-current-node)
525                                     (buffer-string))))))))))
526
527 ;; Image retrieval
528
529 (defun elpher-get-image-node ()
530   "Getter which retrieves the current node contents as an image to view."
531   (let* ((address (elpher-node-address elpher-current-node))
532          (content (elpher-get-cached-content address)))
533     (if content
534         (progn
535           (elpher-with-clean-buffer
536            (insert-image content)
537            (elpher-restore-pos)))
538       (if (display-images-p)
539           (progn
540             (elpher-with-clean-buffer
541              (insert "LOADING IMAGE..."))
542             (elpher-get-selector address
543                                  (lambda (proc event)
544                                    (unless (string-prefix-p "deleted" event)
545                                      (let ((image (create-image
546                                                    (encode-coding-string
547                                                     elpher-selector-string
548                                                     'no-conversion)
549                                                    nil t)))
550                                        (elpher-with-clean-buffer
551                                         (insert-image image)
552                                         (elpher-restore-pos))
553                                        (if elpher-cache-images
554                                            (elpher-cache-content
555                                             (elpher-node-address elpher-current-node)
556                                             image)))))))
557         (elpher-get-node-download)))))
558
559 ;; Search retrieval
560
561 (defun elpher-get-search-node ()
562   "Getter which submits a search query to the address of the current node."
563   (let* ((address (elpher-node-address elpher-current-node))
564          (content (elpher-get-cached-content address))
565          (aborted t))
566     (if content
567         (progn
568           (elpher-with-clean-buffer
569            (insert content)
570            (elpher-restore-pos))
571           (message "Displaying cached search results.  Reload to perform a new search."))
572       (unwind-protect
573           (let* ((query-string (read-string "Query: "))
574                  (query-selector (concat (elpher-address-selector address) "\t" query-string))
575                  (search-address (elpher-make-address ?1
576                                                       query-selector
577                                                       (elpher-address-host address)
578                                                       (elpher-address-port address))))
579             (setq aborted nil)
580             (elpher-with-clean-buffer
581              (insert "LOADING RESULTS..."))
582             (elpher-get-selector search-address
583                                   (lambda (proc event)
584                                     (unless (string-prefix-p "deleted" event)
585                                       (elpher-with-clean-buffer
586                                        (elpher-insert-index elpher-selector-string))
587                                       (goto-char (point-min))
588                                       (elpher-cache-content
589                                        (elpher-node-address elpher-current-node)
590                                        (buffer-string))))))
591         (if aborted
592             (elpher-visit-parent-node))))))
593
594 ;; Raw server response retrieval
595
596 (defun elpher-get-node-raw ()
597   "Getter which retrieves the raw server response for the current node."
598   (let ((address (elpher-node-address elpher-current-node)))
599     (elpher-with-clean-buffer
600      (insert "LOADING RAW SERVER RESPONSE..."))
601     (if address
602         (elpher-get-selector address
603                               (lambda (proc event)
604                                 (unless (string-prefix-p "deleted" event)
605                                   (elpher-with-clean-buffer
606                                    (insert elpher-selector-string)
607                                    (goto-char (point-min))))))
608       (progn
609         (elpher-with-clean-buffer
610          (insert elpher-start-index))
611         (goto-char (point-min)))))
612   (message "Displaying raw server response.  Reload or redraw to return to standard view."))
613  
614 ;; File export retrieval
615
616 (defvar elpher-download-filename)
617
618 (defun elpher-get-node-download ()
619   "Getter which retrieves the current node and writes the result to a file."
620   (let* ((address (elpher-node-address elpher-current-node))
621          (selector (elpher-address-selector address)))
622     (elpher-visit-parent-node) ; Do first in case of non-local exits.
623     (let* ((filename-proposal (file-name-nondirectory selector))
624            (filename (read-file-name "Save file as: "
625                                      nil nil nil
626                                      (if (> (length filename-proposal) 0)
627                                          filename-proposal
628                                        "gopher.file"))))
629       (message "Downloading...")
630       (setq elpher-download-filename filename)
631       (elpher-get-selector address
632                             (lambda (proc event)
633                               (let ((coding-system-for-write 'binary))
634                                 (with-temp-file elpher-download-filename
635                                   (insert elpher-selector-string)
636                                   (message (format "Download complate, saved to file %s."
637                                                    elpher-download-filename)))))))))
638
639 ;; URL retrieval
640
641 (defun elpher-get-url-node ()
642   "Getter which attempts to open the URL specified by the current node."
643   (let* ((address (elpher-node-address elpher-current-node))
644          (selector (elpher-address-selector address)))
645     (elpher-visit-parent-node) ; Do first in case of non-local exits.
646     (let ((url (elt (split-string selector "URL:") 1)))
647       (if elpher-open-urls-with-eww
648           (browse-web url)
649         (browse-url url)))))
650
651 ;; Telnet node connection
652
653 (defun elpher-get-telnet-node ()
654   "Getter which opens a telnet connection to the server specified by the current node."
655   (let* ((address (elpher-node-address elpher-current-node))
656          (host (elpher-address-host address))
657          (port (elpher-address-port address)))
658     (elpher-visit-parent-node)
659     (telnet host port)))
660
661 ;; Start page node retrieval
662
663 (defun elpher-get-start-node ()
664   "Getter which displays the start page."
665   (elpher-with-clean-buffer
666    (elpher-insert-index elpher-start-index)
667    (elpher-restore-pos)))
668
669 ;; Bookmarks
670   
671
672 ;;; Bookmarks
673 ;;
674
675 (defun elpher-make-bookmark (display-string address)
676   "Make an elpher bookmark.
677 DISPLAY-STRING determines how the bookmark will appear in the
678 bookmark list, while ADDRESS is the address of the entry."
679   (list display-string address))
680   
681 (defun elpher-bookmark-display-string (bookmark)
682   "Get the display string of BOOKMARK."
683   (elt bookmark 0))
684
685 (defun elpher-bookmark-address (bookmark)
686   "Get the address for BOOKMARK."
687   (elt bookmark 1))
688
689 (defun elpher-save-bookmarks (bookmarks)
690   "Record the bookmark list BOOKMARKS to the user's bookmark file.
691 Beware that this completely replaces the existing contents of the file."
692   (with-temp-file (locate-user-emacs-file "elpher-bookmarks")
693     (erase-buffer)
694     (pp bookmarks (current-buffer))))
695
696 (defun elpher-load-bookmarks ()
697   "Get the list of bookmarks from the users's bookmark file."
698   (with-temp-buffer
699     (ignore-errors
700       (insert-file-contents (locate-user-emacs-file "elpher-bookmarks"))
701       (goto-char (point-min))
702       (read (current-buffer)))))
703
704 (defun elpher-add-node-bookmark (node)
705   "Add bookmark to NODE to the saved list of bookmarks."
706   (let ((bookmark (elpher-make-bookmark (elpher-node-display-string node)
707                                         (elpher-node-address node)))
708         (bookmarks (elpher-load-bookmarks)))
709     (add-to-list 'bookmarks bookmark)
710     (elpher-save-bookmarks bookmarks)))
711
712 (defun elpher-remove-node-bookmark (node)
713   "Remove bookmark to NODE from the saved list of bookmarks."
714   (let ((bookmark (elpher-make-bookmark (elpher-node-display-string node)
715                                         (elpher-node-address node))))
716     (elpher-save-bookmarks
717      (seq-filter (lambda (this-bookmark)
718                    (not (equal bookmark this-bookmark)))
719                  (elpher-load-bookmarks)))))
720      
721 (defun elpher-display-bookmarks ()
722   "Display saved bookmark list."
723   (interactive)
724   (elpher-with-clean-buffer
725    (insert "Use 'r' to return to the previous page.\n\n"
726            "---- Bookmark list ----\n\n")
727    (let ((bookmarks (elpher-load-bookmarks)))
728      (if bookmarks
729          (dolist (bookmark bookmarks)
730            (let ((display-string (elpher-bookmark-display-string bookmark))
731                  (address (elpher-bookmark-address bookmark)))
732              (elpher-insert-index-record display-string
733                                          (elpher-address-type address)
734                                          (elpher-address-selector address)
735                                          (elpher-address-host address)
736                                          (elpher-address-port address))))
737        (insert "No bookmarks found.\n")))
738    (insert "\n-----------------------")
739    (goto-char (point-min))
740    (elpher-next-link)))
741
742 (defun elpher-bookmark-current ()
743   "Bookmark the current node."
744   (interactive)
745   (elpher-add-node-bookmark elpher-current-node))
746
747 (defun elpher-bookmark-link ()
748   "Bookmark the link at point."
749   (interactive)
750   (let ((button (button-at (point))))
751     (if button
752         (elpher-add-node-bookmark (button-get button 'elpher-node))
753       (error "No link selected"))))
754
755 (defun elpher-unbookmark-current ()
756   "Remove bookmark for the current node."
757   (interactive)
758   (elpher-remove-node-bookmark elpher-current-node))
759
760 (defun elpher-unbookmark-link ()
761   "Remove bookmark for the link at point."
762   (interactive)
763   (let ((button (button-at (point))))
764     (if button
765         (elpher-remove-node-bookmark (button-get button 'elpher-node))
766       (error "No link selected"))))
767
768 ;;; Interactive navigation procedures
769 ;;
770
771 (defun elpher-next-link ()
772   "Move point to the next link on the current page."
773   (interactive)
774   (forward-button 1))
775
776 (defun elpher-prev-link ()
777   "Move point to the previous link on the current page."
778   (interactive)
779   (backward-button 1))
780
781 (defun elpher-follow-current-link ()
782   "Open the link or url at point."
783   (interactive)
784   (push-button))
785
786 (defun elpher-go ()
787   "Go to a particular gopher site."
788   (interactive)
789   (let ((node
790          (let ((host-or-url (read-string "Gopher host or URL: ")))
791            (if (string-match elpher-url-regex host-or-url)
792                (elpher-make-node-from-matched-url elpher-current-node
793                                                   host-or-url)
794              (let ((selector (read-string "Selector (default none): " nil nil ""))
795                    (port-string (read-string "Port (default 70): " nil nil "70")))
796                (elpher-make-node (concat "gopher://" host-or-url
797                                          ":" port-string
798                                          "/1" selector)
799                                  elpher-current-node
800                                  (elpher-make-address ?1 selector host-or-url
801                                                       (string-to-number port-string))))))))
802     (switch-to-buffer "*elpher*")
803     (elpher-visit-node node)))
804
805 (defun  elpher-redraw ()
806   "Redraw current page."
807   (interactive)
808   (if elpher-current-node
809       (elpher-visit-node elpher-current-node)
810     (message "No current site.")))
811
812 (defun  elpher-reload ()
813   "Reload current page."
814   (interactive)
815   (if elpher-current-node
816       (elpher-reload-current-node)
817     (message "No current site.")))
818
819 (defun elpher-view-raw ()
820   "View current page as plain text."
821   (interactive)
822   (if elpher-current-node
823       (elpher-visit-node elpher-current-node
824                          #'elpher-get-node-raw)
825     (message "No current site.")))
826
827 (defun elpher-back ()
828   "Go to previous site."
829   (interactive)
830   (if (elpher-node-parent elpher-current-node)
831       (elpher-visit-parent-node)
832     (error "No previous site")))
833
834 (defun elpher-download ()
835   "Download the link at point."
836   (interactive)
837   (let ((button (button-at (point))))
838     (if button
839         (let ((node (button-get button 'elpher-node)))
840           (if node
841               (elpher-visit-node (button-get button 'elpher-node)
842                                  #'elpher-get-node-download)
843             (error "Can only download gopher links, not general URLs")))
844       (error "No link selected"))))
845
846 (defun elpher-build-link-map ()
847   "Build alist mapping link names to destination nodes in current buffer."
848   (let ((link-map nil)
849         (b (next-button (point-min) t)))
850     (while b
851       (add-to-list 'link-map (cons (button-label b) b))
852       (setq b (next-button (button-start b))))
853     link-map))
854
855 (defun elpher-jump ()
856   "Select a directory entry by name.  Similar to the info browser (m)enu command."
857   (interactive)
858   (let* ((link-map (elpher-build-link-map)))
859     (if link-map
860         (let ((key (let ((completion-ignore-case t))
861                      (completing-read "Directory entry/link (tab to autocomplete): "
862                                       link-map nil t))))
863           (if (and key (> (length key) 0))
864               (let ((b (cdr (assoc key link-map))))
865                 (goto-char (button-start b))
866                 (button-activate b)))))))
867
868 (defun elpher-root-dir ()
869   "Visit root of current server."
870   (interactive)
871   (let* ((address (elpher-node-address elpher-current-node))
872          (host (elpher-address-host address)))
873     (if host
874         (let ((host (elpher-address-host address))
875               (selector (elpher-address-selector address))
876               (port (elpher-address-port address)))
877           (if (> (length selector) 0)
878               (let ((root-address (elpher-make-address ?1 "" host port)))
879                 (elpher-visit-node
880                  (elpher-make-node (concat "gopher://" host
881                                            ":" (number-to-string port)
882                                            "/1/")
883                                    elpher-current-node
884                                    root-address)))
885             (error "Already at root directory of current server")))
886       (error "Command invalid for this page"))))
887
888 (defun elpher-info-node (node)
889   "Display information on NODE."
890   (let ((display-string (elpher-node-display-string node))
891         (address (elpher-node-address node)))
892     (if address
893         (message "`%s' on %s port %s"
894                 (elpher-address-selector address)
895                 (elpher-address-host address)
896                 (elpher-address-port address))
897       (message "%s" display-string))))
898
899 (defun elpher-info-link ()
900   "Display information on node corresponding to link at point."
901   (interactive)
902   (let ((button (button-at (point))))
903     (if button
904         (elpher-info-node (button-get button 'elpher-node))
905       (error "No item selected"))))
906   
907 (defun elpher-info-current ()
908   "Display information on current node."
909   (interactive)
910   (elpher-info-node elpher-current-node))
911
912 (defun elpher-get-address-url (address)
913   "Get URL representation of ADDRESS."
914   (concat "gopher://"
915           (elpher-address-host address)
916           (let ((port (elpher-address-port address)))
917             (if (equal port 70)
918                 ""
919               (format ":%d" port)))
920           "/" (string (elpher-address-type address))
921           (elpher-address-selector address)))
922
923 (defun elpher-copy-node-url (node)
924   "Copy URL representation of address of NODE to `kill-ring'."
925   (let ((address (elpher-node-address node)))
926     (if address
927         (let ((url (elpher-get-address-url address)))
928           (message url)
929           (kill-new url))
930       (error (format "Cannot represent %s as URL" (elpher-node-display-string node))))))
931
932 (defun elpher-copy-link-url ()
933   "Copy URL of item at point to `kill-ring'."
934   (interactive)
935   (let ((button (button-at (point))))
936     (if button
937         (elpher-copy-node-url (button-get button 'elpher-node))
938       (error "No item selected"))))
939
940 (defun elpher-copy-current-url ()
941   "Copy URL of current node to `kill-ring'."
942   (interactive)
943   (elpher-copy-node-url elpher-current-node))
944
945 ;;; Mode and keymap
946 ;;
947
948 (defvar elpher-mode-map
949   (let ((map (make-sparse-keymap)))
950     (define-key map (kbd "TAB") 'elpher-next-link)
951     (define-key map (kbd "<backtab>") 'elpher-prev-link)
952     (define-key map (kbd "u") 'elpher-back)
953     (define-key map (kbd "O") 'elpher-root-dir)
954     (define-key map (kbd "g") 'elpher-go)
955     (define-key map (kbd "r") 'elpher-redraw)
956     (define-key map (kbd "R") 'elpher-reload)
957     (define-key map (kbd "w") 'elpher-view-raw)
958     (define-key map (kbd "d") 'elpher-download)
959     (define-key map (kbd "m") 'elpher-jump)
960     (define-key map (kbd "i") 'elpher-info-link)
961     (define-key map (kbd "I") 'elpher-info-current)
962     (define-key map (kbd "c") 'elpher-copy-link-url)
963     (define-key map (kbd "C") 'elpher-copy-current-url)
964     (when (fboundp 'evil-define-key)
965       (evil-define-key 'motion map
966         (kbd "TAB") 'elpher-next-link
967         (kbd "C-]") 'elpher-follow-current-link
968         (kbd "C-t") 'elpher-back
969         (kbd "u") 'elpher-back
970         (kbd "O") 'elpher-root-dir
971         (kbd "g") 'elpher-go
972         (kbd "r") 'elpher-redraw
973         (kbd "R") 'elpher-reload
974         (kbd "w") 'elpher-view-raw
975         (kbd "d") 'elpher-download
976         (kbd "m") 'elpher-jump
977         (kbd "i") 'elpher-info-link
978         (kbd "I") 'elpher-info-current
979         (kbd "c") 'elpher-copy-link-url
980         (kbd "C") 'elpher-copy-current-url
981         (kbd "a") 'elpher-bookmark-link
982         (kbd "A") 'elpher-bookmark-current
983         (kbd "x") 'elpher-unbookmark-link
984         (kbd "X") 'elpher-unbookmark-current
985         (kbd "B") 'elpher-display-bookmarks))
986     map)
987   "Keymap for gopher client.")
988
989 (define-derived-mode elpher-mode special-mode "elpher"
990   "Major mode for elpher, an elisp gopher client.")
991
992 (when (fboundp 'evil-set-initial-state)
993   (evil-set-initial-state 'elpher-mode 'motion))
994
995 ;;; Main start procedure
996 ;;
997
998 ;;;###autoload
999 (defun elpher ()
1000   "Start elpher with default landing page."
1001   (interactive)
1002   (if (get-buffer "*elpher*")
1003       (switch-to-buffer "*elpher*")
1004     (switch-to-buffer "*elpher*")
1005     (setq elpher-current-node nil)
1006     (let ((start-node (elpher-make-node "Elpher Start Page" nil (elpher-make-address 'start))))
1007       (elpher-visit-node start-node)))
1008   "Started Elpher.") ; Otherwise (elpher) evaluates to start page string.
1009
1010 ;;; elpher.el ends here