Additional comments.
[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.0.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 browsing,
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 ;; - clickable web and gopher links in plain text.
37
38 ;; The caching mechanism works by maintaining a hierarchy of visited
39 ;; pages rather than a linear history, meaning that it is quick and
40 ;; easy to navigate this history.
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
53 ;;; Global constants
54 ;;
55
56 (defconst elpher-version "1.0.0"
57   "Current version of elpher.")
58
59 (defconst elpher-margin-width 6
60   "Width of left-hand margin used when rendering indicies.")
61
62 (defconst elpher-start-index
63   (mapconcat
64    'identity
65    (list "i\tfake\tfake\t1"
66          "i--------------------------------------------\tfake\tfake\t1"
67          "i           Elpher Gopher Client             \tfake\tfake\t1"
68          (format "i              version %s\tfake\tfake\t1" elpher-version)
69          "i--------------------------------------------\tfake\tfake\t1"
70          "i\tfake\tfake\t1"
71          "iBasic usage:\tfake\tfake\t1"
72          "i\tfake\tfake\t1"
73          "i - tab/shift-tab: next/prev directory entry on current page\tfake\tfake\t1"
74          "i - RET/mouse-1: open directory entry under cursor\tfake\tfake\t1"
75          "i - u: return to parent directory entry\tfake\tfake\t1"
76          "i - g: go to a particular page\tfake\tfake\t1"
77          "i - r: redraw current page (using cached contents if available)\tfake\tfake\t1"
78          "i - R: reload current page (regenerates cache)\tfake\tfake\t1"
79          "i - d: download directory entry under cursor\tfake\tfake\t1"
80          "i - w: display the raw server response for the current page\tfake\tfake\t1"
81          "i\tfake\tfake\t1"
82          "iPlaces to start exploring Gopherspace:\tfake\tfake\t1"
83          "i\tfake\tfake\t1"
84          "1Floodgap Systems Gopher Server\t\tgopher.floodgap.com\t70"
85          "i\tfake\tfake\t1"
86          "iAlternatively, select the following item and enter some\tfake\tfake\t1"
87          "isearch terms:\tfake\tfake\t1"
88          "i\tfake\tfake\t1"
89          "7Veronica-2 Gopher Search Engine\t/v2/vs\tgopher.floodgap.com\t70"
90          ".")
91    "\r\n")
92   "Source for elpher start page.")
93
94 (defconst elpher-type-map
95   '((?0 elpher-get-text-node "T" elpher-text)
96     (?1 elpher-get-index-node "/" elpher-index)
97     (?g elpher-get-image-node "im" elpher-image)
98     (?p elpher-get-image-node "im" elpher-image)
99     (?I elpher-get-image-node "im" elpher-image)
100     (?4 elpher-get-node-download "B" elpher-binary)
101     (?5 elpher-get-node-download "B" elpher-binary)
102     (?9 elpher-get-node-download "B" elpher-binary)
103     (?7 elpher-get-search-node "?" elpher-search))
104   "Association list from types to getters, margin codes and index faces.")
105
106
107 ;;; Customization group
108 ;;
109
110 (defgroup elpher nil
111   "A gopher client."
112   :group 'applications)
113
114 ;; Face customizations
115
116 (defface elpher-index
117   '((((background dark)) :foreground "deep sky blue")
118     (((background light)) :foreground "blue"))
119   "Face used for index records.")
120
121 (defface elpher-text
122   '((((background dark)) :foreground "white")
123     (((background light)) :weight bold))
124   "Face used for text records.")
125
126 (defface elpher-info '()
127   "Face used for info records.")
128
129 (defface elpher-image
130   '((((background dark)) :foreground "green")
131     (t :foreground "dark green"))
132   "Face used for image records.")
133
134 (defface elpher-search
135   '((((background light)) :foreground "orange")
136     (((background dark)) :foreground "dark orange"))
137   "Face used for search records.")
138
139 (defface elpher-url
140   '((((background dark)) :foreground "yellow")
141     (((background light)) :foreground "dark red"))
142   "Face used for url records.")
143
144 (defface elpher-binary
145   '((t :foreground "magenta"))
146   "Face used for binary records.")
147
148 (defface elpher-unknown
149   '((t :foreground "red"))
150   "Face used for unknown record types.")
151
152 (defface elpher-margin-key
153   '((((background dark)) :foreground "white"))
154   "Face used for margin key.")
155
156 (defface elpher-margin-brackets
157   '((t :foreground "blue"))
158   "Face used for brackets around margin key.")
159
160 ;; Other customizations
161
162 (defcustom elpher-open-urls-with-eww nil
163   "If non-nil, open URL selectors using eww.
164 Otherwise, use the system browser via the BROWSE-URL function."
165   :type '(boolean))
166
167 (defcustom elpher-cache-images nil
168   "If non-nil, cache images in memory in the same way as other content."
169   :type '(boolean))
170
171 (defcustom elpher-start-address nil
172   "If nil, the default start directory is shown when Elpher is started.
173 Otherwise, a list containing the selector, host and port of a directory to
174 use as the start page."
175   :type '(list string string integer))
176
177
178 ;;; Model
179 ;;
180
181 ;; Address
182
183 (defun elpher-make-address (selector host port)
184   "Create an address of a gopher object with SELECTOR, HOST and PORT."
185   (list selector host port))
186
187 (defun elpher-address-selector (address)
188   "Retrieve selector from ADDRESS."
189   (car address))
190
191 (defun elpher-address-host (address)
192   "Retrieve host from ADDRESS."
193   (cadr address))
194
195 (defun elpher-address-port (address)
196   "Retrieve port from ADDRESS."
197   (caddr address))
198
199 ;; Node
200
201 (defun elpher-make-node (parent address getter &optional content pos)
202   "Create a node in the gopher page hierarchy.
203
204 PARENT specifies the parent of the node, ADDRESS specifies the address of
205 the gopher page, GETTER provides the getter function used to obtain this
206 page.
207
208 The optional arguments CONTENT and POS can be used to fill the cached
209 content and cursor position fields of the node."
210   (list parent address getter content pos))
211
212 (defun elpher-node-parent (node)
213   "Retrieve the parent node of NODE."
214   (elt node 0))
215
216 (defun elpher-node-address (node)
217   "Retrieve the address of NODE."
218   (elt node 1))
219
220 (defun elpher-node-getter (node)
221   "Retrieve the preferred getter function of NODE."
222   (elt node 2))
223
224 (defun elpher-node-content (node)
225   "Retrieve the cached content of NODE, or nil if none exists."
226   (elt node 3))
227
228 (defun elpher-node-pos (node)
229   "Retrieve the cached cursor position for NODE, or nil if none exists."
230   (elt node 4))
231
232 (defun elpher-set-node-content (node content)
233   "Set the content cache of NODE to CONTENT."
234   (setcar (nthcdr 3 node) content))
235
236 (defun elpher-set-node-pos (node pos)
237   "Set the cursor position cache of NODE to POS."
238   (setcar (nthcdr 4 node) pos))
239
240 ;; Node graph traversal
241
242 (defvar elpher-current-node)
243
244 (defun elpher-visit-node (node &optional getter)
245   "Visit NODE using its own getter or GETTER, if non-nil."
246   (elpher-save-pos)
247   (elpher-process-cleanup)
248   (setq elpher-current-node node)
249   (if getter
250       (funcall getter)
251     (funcall (elpher-node-getter node))))
252
253 (defun elpher-visit-parent-node ()
254   "Visit the parent of the current node."
255   (let ((parent-node (elpher-node-parent elpher-current-node)))
256     (when parent-node
257       (elpher-visit-node parent-node))))
258       
259 (defun elpher-reload-current-node ()
260   "Reload the current node, discarding any existing cached content."
261   (elpher-set-node-content elpher-current-node nil)
262   (elpher-visit-node elpher-current-node))
263
264 (defun elpher-save-pos ()
265   "Save the current position of point to the current node."
266   (when elpher-current-node
267     (elpher-set-node-pos elpher-current-node (point))))
268
269 (defun elpher-restore-pos ()
270   "Restore the position of point to that cached in the current node."
271   (let ((pos (elpher-node-pos elpher-current-node)))
272     (if pos
273         (goto-char pos)
274       (goto-char (point-min)))))
275
276 ;;; Buffer preparation
277 ;;
278
279 (defmacro elpher-with-clean-buffer (&rest args)
280   "Evaluate ARGS with a clean *elpher* buffer as current."
281   (list 'progn
282         '(switch-to-buffer "*elpher*")
283         '(elpher-mode)
284         (append (list 'let '((inhibit-read-only t))
285                       '(erase-buffer))
286                 args)))
287
288 ;;; Index rendering
289 ;;
290
291 (defun elpher-insert-index (string)
292   "Insert the index corresponding to STRING into the current buffer."
293   (dolist (line (split-string string "\r\n"))
294     (unless (= (length line) 0)
295       (elpher-insert-index-record line))))
296
297 (defun elpher-insert-margin (&optional type-name)
298   "Insert index margin, optionally containing the TYPE-NAME, into the current buffer."
299   (if type-name
300       (progn
301         (insert (format (concat "%" (number-to-string (- elpher-margin-width 1)) "s")
302                         (concat
303                          (propertize "[" 'face 'elpher-margin-brackets)
304                          (propertize type-name 'face 'elpher-margin-key)
305                          (propertize "]" 'face 'elpher-margin-brackets))))
306         (insert " "))
307     (insert (make-string elpher-margin-width ?\s))))
308
309 (defun elpher-insert-index-record (line)
310   "Insert the index record corresponding to LINE into the current buffer."
311   (let* ((type (elt line 0))
312          (fields (split-string (substring line 1) "\t"))
313          (display-string (elt fields 0))
314          (selector (elt fields 1))
315          (host (elt fields 2))
316          (port (elt fields 3))
317          (address (elpher-make-address selector host port))
318          (type-map-entry (alist-get type elpher-type-map)))
319     (if type-map-entry
320         (let ((getter (car type-map-entry))
321               (margin-code (cadr type-map-entry))
322               (face (caddr type-map-entry)))
323           (elpher-insert-margin margin-code)
324           (insert-text-button display-string
325                               'face face
326                               'elpher-node (elpher-make-node elpher-current-node
327                                                                address
328                                                                getter)
329                               'action #'elpher-click-link
330                               'follow-link t
331                               'help-echo (format "mouse-1, RET: open %s on %s port %s"
332                                                  selector host port)))
333       (pcase type
334         (?i (elpher-insert-margin) ; Information
335             (insert (propertize display-string
336                                 'face 'elpher-info)))
337         (?h (elpher-insert-margin "W") ; Web link
338             (let ((url (elt (split-string selector "URL:") 1)))
339               (insert-text-button display-string
340                                   'face 'elpher-url
341                                   'elpher-url url
342                                   'action #'elpher-click-url
343                                   'follow-link t
344                                   'help-echo (format "mouse-1, RET: open url %s" url))))
345         (?.) ; Occurs at end of index, can safely ignore.
346         (tp (elpher-insert-margin (concat (char-to-string tp) "?"))
347             (insert (propertize display-string
348                                 'face 'elpher-unknown-face)))))
349     (insert "\n")))
350
351
352 ;;; Selector retrieval (all kinds)
353 ;;
354
355 (defun elpher-process-cleanup ()
356   "Immediately shut down any extant elpher process."
357   (let ((p (get-process "elpher-process")))
358     (if p (delete-process p))))
359
360 (defvar elpher-selector-string)
361
362 (defun elpher-get-selector (address after)
363   "Retrieve selector specified by ADDRESS, then execute AFTER.
364 The result is stored as a string in the variable â€˜elpher-selector-string’."
365   (setq elpher-selector-string "")
366   (make-network-process
367    :name "elpher-process"
368    :host (elpher-address-host address)
369    :service (elpher-address-port address)
370    :filter (lambda (proc string)
371              (setq elpher-selector-string (concat elpher-selector-string string)))
372    :sentinel after)
373   (process-send-string "elpher-process"
374                        (concat (elpher-address-selector address) "\n")))
375
376 ;; Index retrieval
377
378 (defun elpher-get-index-node ()
379   "Getter which retrieves the current node contents as an index."
380   (let ((content (elpher-node-content elpher-current-node))
381         (address (elpher-node-address elpher-current-node)))
382     (if content
383         (progn
384           (elpher-with-clean-buffer
385            (insert content))
386           (elpher-restore-pos))
387       (if address
388           (progn
389             (elpher-with-clean-buffer
390              (insert "LOADING DIRECTORY..."))
391             (elpher-get-selector address
392                                   (lambda (proc event)
393                                     (unless (string-prefix-p "deleted" event)
394                                       (elpher-with-clean-buffer
395                                        (elpher-insert-index elpher-selector-string))
396                                       (elpher-restore-pos)
397                                       (elpher-set-node-content elpher-current-node
398                                                                 (buffer-string))))))
399         (progn
400           (elpher-with-clean-buffer
401            (elpher-insert-index elpher-start-index))
402           (elpher-restore-pos)
403           (elpher-set-node-content elpher-current-node
404                                     (buffer-string)))))))
405
406 ;; Text retrieval
407
408 (defconst elpher-url-regex
409   "\\(https?\\|gopher\\)://\\([a-zA-Z0-9.\-]+\\)\\(?3::[0-9]+\\)?\\(?4:/[^ \r\n\t(),]*\\)?"
410   "Regexp used to locate and buttinofy URLs in text files loaded by elpher.")
411
412 (defun elpher-buttonify-urls (string)
413   "Turn substrings which look like urls in STRING into clickable buttons."
414   (with-temp-buffer
415     (insert string)
416     (goto-char (point-min))
417     (while (re-search-forward elpher-url-regex nil t)
418       (let ((url (match-string 0))
419             (protocol (downcase (match-string 1))))
420         (if (string= protocol "gopher")
421             (let* ((host (match-string 2))
422                    (port 70)
423                    (type-and-selector (match-string 4))
424                    (type (if (> (length type-and-selector) 1)
425                              (elt type-and-selector 1)
426                            ?1))
427                    (selector (if (> (length type-and-selector) 1)
428                                  (substring type-and-selector 2)
429                                ""))
430                    (address (elpher-make-address selector host port))
431                    (getter (car (alist-get type elpher-type-map))))
432               (make-text-button (match-beginning 0)
433                                 (match-end 0)
434                                 'elpher-node (elpher-make-node elpher-current-node
435                                                                  address
436                                                                  getter)
437                                 'action #'elpher-click-link
438                                 'follow-link t
439                                 'help-echo (format "mouse-1, RET: open %s on %s port %s"
440                                                    selector host port)))
441           (make-text-button (match-beginning 0)
442                             (match-end 0)
443                             'elpher-url url
444                             'action #'elpher-click-url
445                             'follow-link t
446                             'help-echo (format "mouse-1, RET: open url %s" url)))))
447     (buffer-string)))
448
449 (defun elpher-process-text (string)
450   "Remove CRs and trailing period from the gopher text document STRING."
451   (let* ((chopped-str (replace-regexp-in-string "\r\n\.\r\n$" "\r\n" string))
452          (cleaned-str (replace-regexp-in-string "\r" "" chopped-str)))
453     (elpher-buttonify-urls cleaned-str)))
454
455 (defun elpher-get-text-node ()
456   "Getter which retrieves the current node contents as a text document."
457   (let ((content (elpher-node-content elpher-current-node))
458         (address (elpher-node-address elpher-current-node)))
459     (if content
460         (progn
461           (elpher-with-clean-buffer
462            (insert content))
463           (elpher-restore-pos))
464       (progn
465         (elpher-with-clean-buffer
466          (insert "LOADING TEXT..."))
467         (elpher-get-selector address
468                               (lambda (proc event)
469                                 (unless (string-prefix-p "deleted" event)
470                                   (elpher-with-clean-buffer
471                                    (insert (elpher-process-text elpher-selector-string)))
472                                   (elpher-restore-pos)
473                                   (elpher-set-node-content elpher-current-node
474                                                             (buffer-string)))))))))
475
476 ;; Image retrieval
477
478 (defun elpher-get-image-node ()
479   "Getter which retrieves the current node contents as an image to view."
480   (let ((content (elpher-node-content elpher-current-node))
481         (address (elpher-node-address elpher-current-node)))
482     (if content
483         (progn
484           (elpher-with-clean-buffer
485            (insert-image content))
486           (setq cursor-type nil)
487           (elpher-restore-pos))
488       (if (display-images-p)
489           (progn
490             (elpher-with-clean-buffer
491              (insert "LOADING IMAGE..."))
492             (elpher-get-selector address
493                                  (lambda (proc event)
494                                    (unless (string-prefix-p "deleted" event)
495                                      (let ((image (create-image
496                                                    (encode-coding-string elpher-selector-string
497                                                                          'no-conversion)
498                                                    nil t)))
499                                        (elpher-with-clean-buffer
500                                         (insert-image image))
501                                        (setq cursor-type nil)
502                                        (elpher-restore-pos)
503                                        (if elpher-cache-images
504                                            (elpher-set-node-content elpher-current-node
505                                                                     image)))))))
506         (elpher-get-node-download)))))
507
508 ;; Search retrieval
509
510 (defun elpher-get-search-node ()
511   "Getter which submits a search query to the address of the current node."
512   (let ((content (elpher-node-content elpher-current-node))
513         (address (elpher-node-address elpher-current-node))
514         (aborted t))
515     (if content
516         (progn
517           (elpher-with-clean-buffer
518            (insert content))
519           (elpher-restore-pos)
520           (message "Displaying cached search results.  Reload to perform a new search."))
521       (unwind-protect
522           (let* ((query-string (read-string "Query: "))
523                  (query-selector (concat (elpher-address-selector address) "\t" query-string))
524                  (search-address (elpher-make-address query-selector
525                                                        (elpher-address-host address)
526                                                        (elpher-address-port address))))
527             (setq aborted nil)
528             (elpher-with-clean-buffer
529              (insert "LOADING RESULTS..."))
530             (elpher-get-selector search-address
531                                   (lambda (proc event)
532                                     (unless (string-prefix-p "deleted" event)
533                                       (elpher-with-clean-buffer
534                                        (elpher-insert-index elpher-selector-string))
535                                       (goto-char (point-min))
536                                       (elpher-set-node-content elpher-current-node
537                                                                 (buffer-string))))))
538         (if aborted
539             (elpher-visit-parent-node))))))
540
541 ;; Raw server response retrieval
542
543 (defun elpher-get-node-raw ()
544   "Getter which retrieves the raw server response for the current node."
545   (let* ((content (elpher-node-content elpher-current-node))
546          (address (elpher-node-address elpher-current-node)))
547     (elpher-with-clean-buffer
548      (insert "LOADING RAW SERVER RESPONSE..."))
549     (if address
550         (elpher-get-selector address
551                               (lambda (proc event)
552                                 (unless (string-prefix-p "deleted" event)
553                                   (elpher-with-clean-buffer
554                                    (insert elpher-selector-string))
555                                   (goto-char (point-min)))))
556       (progn
557         (elpher-with-clean-buffer
558          (insert elpher-start-index))
559         (goto-char (point-min)))))
560   (message "Displaying raw server response.  Reload or redraw to return to standard view."))
561  
562 ;; File export retrieval
563
564 (defvar elpher-download-filename)
565
566 (defun elpher-get-node-download ()
567   "Getter which retrieves the current node and writes the result to a file."
568   (let* ((address (elpher-node-address elpher-current-node))
569          (selector (elpher-address-selector address)))
570     (elpher-visit-parent-node) ; Do first in case of non-local exits.
571     (let* ((filename-proposal (file-name-nondirectory selector))
572            (filename (read-file-name "Save file as: "
573                                      nil nil nil
574                                      (if (> (length filename-proposal) 0)
575                                          filename-proposal
576                                        "gopher.file"))))
577       (message "Downloading...")
578       (setq elpher-download-filename filename)
579       (elpher-get-selector address
580                             (lambda (proc event)
581                               (let ((coding-system-for-write 'binary))
582                                 (with-temp-file elpher-download-filename
583                                   (insert elpher-selector-string)
584                                   (message (format "Download complate, saved to file %s."
585                                                    elpher-download-filename)))))))))
586
587
588 ;;; Navigation procedures
589 ;;
590
591 (defun elpher-next-link ()
592   "Move point to the next link on the current page."
593   (interactive)
594   (forward-button 1))
595
596 (defun elpher-prev-link ()
597   "Move point to the previous link on the current page."
598   (interactive)
599   (backward-button 1))
600
601 (defun elpher-click-link (button)
602   "Function called when the gopher link BUTTON is activated (via mouse or keypress)."
603   (let ((node (button-get button 'elpher-node)))
604     (elpher-visit-node node)))
605
606 (defun elpher-click-url (button)
607   "Function called when the url link BUTTON is activated (via mouse or keypress)."
608   (let ((url (button-get button 'elpher-url)))
609     (if elpher-open-urls-with-eww
610         (browse-web url)
611       (browse-url url))))
612
613 (defun elpher-follow-current-link ()
614   "Open the link or url at point."
615   (interactive)
616   (push-button))
617
618 (defun elpher-go ()
619   "Go to a particular gopher site."
620   (interactive)
621   (let* (
622          (hostname (read-string "Gopher host: "))
623          (selector (read-string "Selector (default none): " nil nil ""))
624          (port (read-string "Port (default 70): " nil nil 70))
625          (address (list selector hostname port)))
626     (elpher-visit-node
627      (elpher-make-node elpher-current-node
628                         address
629                         #'elpher-get-index-node))))
630
631 (defun  elpher-redraw ()
632   "Redraw current page."
633   (interactive)
634   (elpher-visit-node elpher-current-node))
635
636 (defun  elpher-reload ()
637   "Reload current page."
638   (interactive)
639   (elpher-reload-current-node))
640
641 (defun elpher-view-raw ()
642   "View current page as plain text."
643   (interactive)
644   (elpher-visit-node elpher-current-node
645                       #'elpher-get-node-raw))
646
647 (defun elpher-back ()
648   "Go to previous site."
649   (interactive)
650   (if (elpher-node-parent elpher-current-node)
651       (elpher-visit-parent-node)
652     (message "No previous site.")))
653
654 (defun elpher-download ()
655   "Download the link at point."
656   (interactive)
657   (let ((button (button-at (point))))
658     (if button
659         (let ((node (button-get button 'elpher-node)))
660           (if node
661               (elpher-visit-node (button-get button 'elpher-node)
662                                  #'elpher-get-node-download)
663             (message "Can only download gopher links, not general URLs.")))
664       (message "No link selected."))))
665
666 ;;; Mode and keymap
667 ;;
668
669 (defvar elpher-mode-map
670   (let ((map (make-sparse-keymap)))
671     (define-key map (kbd "TAB") 'elpher-next-link)
672     (define-key map (kbd "<backtab>") 'elpher-prev-link)
673     (define-key map (kbd "u") 'elpher-back)
674     (define-key map (kbd "g") 'elpher-go)
675     (define-key map (kbd "r") 'elpher-redraw)
676     (define-key map (kbd "R") 'elpher-reload)
677     (define-key map (kbd "w") 'elpher-view-raw)
678     (define-key map (kbd "d") 'elpher-download)
679     (when (fboundp 'evil-define-key)
680       (evil-define-key 'normal map
681         (kbd "TAB") 'elpher-next-link
682         (kbd "C-]") 'elpher-follow-current-link
683         (kbd "C-t") 'elpher-back
684         (kbd "u") 'elpher-back
685         (kbd "g") 'elpher-go
686         (kbd "r") 'elpher-redraw
687         (kbd "R") 'elpher-reload
688         (kbd "w") 'elpher-view-raw
689         (kbd "d") 'elpher-download))
690     map)
691   "Keymap for gopher client.")
692
693 (define-derived-mode elpher-mode special-mode "elpher"
694   "Major mode for elpher, an elisp gopher client.")
695
696
697 ;;; Main start procedure
698 ;;
699
700 ;;;###autoload
701 (defun elpher ()
702   "Start elpher with default landing page."
703   (interactive)
704   (setq elpher-current-node nil)
705   (let ((start-node (elpher-make-node nil
706                                       elpher-start-address
707                                       #'elpher-get-index-node)))
708     (elpher-visit-node start-node))
709   "Started Elpher.") ; Otherwise (elpher) evaluates to start page string.
710
711 ;;; elpher.el ends here