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