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