Version bump.
[elpher.git] / elpher.el
1 ;;; elpher.el --- A friendly gopher and gemini client  -*- lexical-binding:t -*-
2
3 ;; Copyright (C) 2019 Tim Vaughan
4
5 ;; Author: Tim Vaughan <plugd@thelambdalab.xyz>
6 ;; Created: 11 April 2019
7 ;; Version: 2.7.7
8 ;; Keywords: comm gopher
9 ;; Homepage: http://thelambdalab.xyz/elpher
10 ;; Package-Requires: ((emacs "26"))
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 practical and friendly gopher and gemini
30 ;; client for GNU Emacs.  It supports:
31
32 ;; - intuitive keyboard and mouse-driven browsing,
33 ;; - out-of-the-box compatibility with evil-mode,
34 ;; - clickable web and gopher links *in plain text*,
35 ;; - caching of visited sites,
36 ;; - pleasant and configurable colouring of Gopher directories,
37 ;; - direct visualisation of image files,
38 ;; - a simple bookmark management system,
39 ;; - gopher connections using TLS encryption,
40 ;; - the fledgling Gemini protocol,
41 ;; - the greybeard Finger protocol.
42
43 ;; To launch Elpher, simply use 'M-x elpher'.  This will open a start
44 ;; page containing information on key bindings and suggested starting
45 ;; points for your gopher exploration.
46
47 ;; Full instructions can be found in the Elpher info manual.
48
49 ;; Elpher is under active development.  Any suggestions for
50 ;; improvements are welcome, and can be made on the official
51 ;; project page, gopher://thelambdalab.xyz/1/projects/elpher/.
52
53 ;;; Code:
54
55 (provide 'elpher)
56
57 ;;; Dependencies
58 ;;
59
60 (require 'seq)
61 (require 'pp)
62 (require 'shr)
63 (require 'url-util)
64 (require 'subr-x)
65 (require 'dns)
66 (require 'ansi-color)
67 (require 'nsm)
68
69
70 ;;; Global constants
71 ;;
72
73 (defconst elpher-version "2.7.7"
74   "Current version of elpher.")
75
76 (defconst elpher-margin-width 6
77   "Width of left-hand margin used when rendering indicies.")
78
79 (defconst elpher-type-map
80   '(((gopher ?0) elpher-get-gopher-page elpher-render-text "txt" elpher-text)
81     ((gopher ?1) elpher-get-gopher-page elpher-render-index "/" elpher-index)
82     ((gopher ?4) elpher-get-gopher-page elpher-render-download "bin" elpher-binary)
83     ((gopher ?5) elpher-get-gopher-page elpher-render-download "bin" elpher-binary)
84     ((gopher ?7) elpher-get-gopher-query-page elpher-render-index "?" elpher-search)
85     ((gopher ?9) elpher-get-gopher-page elpher-render-download "bin" elpher-binary)
86     ((gopher ?g) elpher-get-gopher-page elpher-render-image "img" elpher-image)
87     ((gopher ?p) elpher-get-gopher-page elpher-render-image "img" elpher-image)
88     ((gopher ?I) elpher-get-gopher-page elpher-render-image "img" elpher-image)
89     ((gopher ?d) elpher-get-gopher-page elpher-render-download "doc" elpher-binary)
90     ((gopher ?P) elpher-get-gopher-page elpher-render-download "doc" elpher-binary)
91     ((gopher ?s) elpher-get-gopher-page elpher-render-download "snd" elpher-binary)
92     ((gopher ?h) elpher-get-gopher-page elpher-render-html "htm" elpher-html)
93     (gemini elpher-get-gemini-page elpher-render-gemini "gem" elpher-gemini)
94     (finger elpher-get-finger-page elpher-render-text "txt" elpher-text)
95     (telnet elpher-get-telnet-page nil "tel" elpher-telnet)
96     (other-url elpher-get-other-url-page nil "url" elpher-other-url)
97     ((special bookmarks) elpher-get-bookmarks-page nil "/" elpher-index)
98     ((special start) elpher-get-start-page nil))
99   "Association list from types to getters, renderers, margin codes and index faces.")
100
101
102 ;;; Customization group
103 ;;
104
105 (defgroup elpher nil
106   "A gopher client."
107   :group 'applications)
108
109 ;; General appearance and customizations
110
111 (defcustom elpher-open-urls-with-eww nil
112   "If non-nil, open URL selectors using eww.
113 Otherwise, use the system browser via the BROWSE-URL function."
114   :type '(boolean))
115
116 (defcustom elpher-use-header t
117   "If non-nil, display current page information in buffer header."
118   :type '(boolean))
119
120 (defcustom elpher-auto-disengage-TLS nil
121   "If non-nil, automatically disengage TLS following an unsuccessful connection.
122 While enabling this may seem convenient, it is also potentially dangerous as it
123 allows switching from an encrypted channel back to plain text without user input."
124   :type '(boolean))
125
126 (defcustom elpher-connection-timeout 5
127   "Specifies the number of seconds to wait for a network connection to time out."
128   :type '(integer))
129
130 (defcustom elpher-filter-ansi-from-text nil
131   "If non-nil, filter ANSI escape sequences from text.
132 The default behaviour is to use the ansi-color package to interpret these
133 sequences."
134   :type '(boolean))
135
136 (defcustom elpher-gemini-TLS-cert-checks nil
137   "If non-nil, verify gemini server TLS certs using the default security level.
138 Otherwise, certificate verification is disabled.
139
140 This defaults to off because it is standard practice for Gemini servers
141 to use self-signed certificates, meaning that most servers provide what
142 EMACS considers to be an invalid certificate."
143   :type '(boolean))
144
145 (defcustom elpher-gemini-max-fill-width 80
146   "Specify the maximum default width (in columns) of text/gemini documents.
147 The actual width used is the minimum of this value and the window width at
148 the time when the text is rendered."
149   :type '(integer))
150
151 (defcustom elpher-bookmarks-file (locate-user-emacs-file "elpher-bookmarks")
152   "Specify the name of the file where elpher bookmarks will be saved."
153   :type '(file))
154
155 ;; Face customizations
156
157 (defgroup elpher-faces nil
158   "Elpher face customizations."
159   :group 'elpher)
160
161 (defface elpher-index
162   '((t :inherit font-lock-keyword-face))
163   "Face used for directory type directory records.")
164
165 (defface elpher-text
166   '((t :inherit bold))
167   "Face used for text type directory records.")
168
169 (defface elpher-info
170   '((t :inherit default))
171   "Face used for info type directory records.")
172
173 (defface elpher-image
174   '((t :inherit font-lock-string-face))
175   "Face used for image type directory records.")
176
177 (defface elpher-search
178   '((t :inherit warning))
179   "Face used for search type directory records.")
180
181 (defface elpher-html
182   '((t :inherit font-lock-comment-face))
183   "Face used for html type directory records.")
184
185 (defface elpher-gemini
186   '((t :inherit font-lock-regexp-grouping-backslash))
187   "Face used for Gemini type directory records.")
188
189 (defface elpher-other-url
190   '((t :inherit font-lock-comment-face))
191   "Face used for other URL type links records.")
192
193 (defface elpher-telnet
194   '((t :inherit font-lock-function-name-face))
195   "Face used for telnet type directory records.")
196
197 (defface elpher-binary
198   '((t :inherit font-lock-doc-face))
199   "Face used for binary type directory records.")
200
201 (defface elpher-unknown
202   '((t :inherit error))
203   "Face used for directory records with unknown/unsupported types.")
204
205 (defface elpher-margin-key
206   '((t :inherit bold))
207   "Face used for directory margin key.")
208
209 (defface elpher-margin-brackets
210   '((t :inherit shadow))
211   "Face used for brackets around directory margin key.")
212
213 (defface elpher-gemini-heading1
214   '((t :inherit bold :height 1.8))
215   "Face used for gemini heading level 1.")
216
217 (defface elpher-gemini-heading2
218   '((t :inherit bold :height 1.5))
219   "Face used for gemini heading level 2.")
220
221 (defface elpher-gemini-heading3
222   '((t :inherit bold :height 1.2))
223   "Face used for gemini heading level 3.")
224
225 (defface elpher-gemini-preformatted
226   '((t :inherit fixed-pitch))
227   "Face used for pre-formatted gemini text blocks.")
228
229 ;;; Model
230 ;;
231
232 ;; Address
233
234 ;; An elpher "address" object is either a url object or a symbol.
235 ;; Symbol addresses are "special", corresponding to pages generated
236 ;; dynamically for and by elpher.  All others represent pages which
237 ;; rely on content retrieved over the network.
238
239 (defun elpher-address-from-url (url-string)
240   "Create a ADDRESS object corresponding to the given URL-STRING."
241   (let ((data (match-data))) ; Prevent parsing clobbering match data
242     (unwind-protect
243         (let ((url (url-generic-parse-url url-string)))
244           (unless (and (not (url-fullness url)) (url-type url))
245             (setf (url-fullness url) t)
246             (setf (url-filename url)
247                   (url-unhex-string (url-filename url)))
248             (unless (url-type url)
249               (setf (url-type url) "gopher"))
250             (when (or (equal "gopher" (url-type url))
251                       (equal "gophers" (url-type url)))
252               ;; Gopher defaults
253               (unless (url-host url)
254                 (setf (url-host url) (url-filename url))
255                 (setf (url-filename url) ""))
256               (when (or (equal (url-filename url) "")
257                         (equal (url-filename url) "/"))
258                 (setf (url-filename url) "/1")))
259             (when (equal "gemini" (url-type url))
260               ;; Gemini defaults
261               (if (equal (url-filename url) "")
262                   (setf (url-filename url) "/"))))
263           url)
264       (set-match-data data))))
265
266 (defun elpher-make-gopher-address (type selector host port &optional tls)
267   "Create an ADDRESS object using gopher directory record attributes.
268 The basic attributes include: TYPE, SELECTOR, HOST and PORT.
269 If the optional attribute TLS is non-nil, the address will be marked as
270 requiring gopher-over-TLS."
271   (cond
272    ((equal type ?i) nil)
273    ((and (equal type ?h)
274          (string-prefix-p "URL:" selector))
275     (elpher-address-from-url (elt (split-string selector "URL:") 1)))
276    ((equal type ?8)
277     (elpher-address-from-url
278      (concat "telnet"
279              "://" host
280              ":" (number-to-string port))))
281    (t
282     (elpher-address-from-url
283      (concat "gopher" (if tls "s" "")
284              "://" host
285              ":" (number-to-string port)
286              "/" (string type)
287              selector)))))
288
289 (defun elpher-make-special-address (type)
290   "Create an ADDRESS object corresponding to the given special address symbol TYPE."
291   type)
292
293 (defun elpher-address-to-url (address)
294   "Get string representation of ADDRESS, or nil if ADDRESS is special."
295   (if (not (elpher-address-special-p address))
296       (url-encode-url (url-recreate-url address))
297     nil))
298
299 (defun elpher-address-type (address)
300   "Retrieve type of ADDRESS object.
301 This is used to determine how to retrieve and render the document the
302 address refers to, via the table `elpher-type-map'."
303   (if (symbolp address)
304       (list 'special address)
305     (let ((protocol (url-type address)))
306       (cond ((or (equal protocol "gopher")
307                  (equal protocol "gophers"))
308              (list 'gopher
309                    (if (member (url-filename address) '("" "/"))
310                        ?1
311                      (string-to-char (substring (url-filename address) 1)))))
312             ((equal protocol "gemini")
313              'gemini)
314             ((equal protocol "telnet")
315              'telnet)
316             ((equal protocol "finger")
317              'finger)
318             (t 'other-url)))))
319
320 (defun elpher-address-protocol (address)
321   "Retrieve the transport protocol for ADDRESS.  This is nil for special addresses."
322   (if (symbolp address)
323       nil
324     (url-type address)))
325
326 (defun elpher-address-filename (address)
327   "Retrieve the filename component of ADDRESS.
328 For gopher addresses this is a combination of the selector type and selector."
329   (if (symbolp address)
330       nil
331     (url-filename address)))
332
333 (defun elpher-address-host (address)
334   "Retrieve host from ADDRESS object."
335   (url-host address))
336
337 (defun elpher-address-user (address)
338   "Retrieve user from ADDRESS object."
339   (url-user address))
340
341 (defun elpher-address-port (address)
342   "Retrieve port from ADDRESS object.
343 If no address is defined, returns 0.  (This is for compatibility with the URL library.)"
344   (if (symbolp address)
345       0
346     (url-port address)))
347
348 (defun elpher-address-special-p (address)
349   "Return non-nil if ADDRESS object is special (e.g. start page, bookmarks page)."
350   (symbolp address))
351
352 (defun elpher-address-gopher-p (address)
353   "Return non-nill if ADDRESS object is a gopher address."
354   (and (not (elpher-address-special-p address))
355        (member (elpher-address-protocol address) '("gopher gophers"))))
356
357 (defun elpher-gopher-address-selector (address)
358   "Retrieve gopher selector from ADDRESS object."
359   (if (member (url-filename address) '("" "/"))
360       ""
361     (substring (url-filename address) 2)))
362
363
364 ;; Cache
365
366 (defvar elpher-content-cache (make-hash-table :test 'equal))
367 (defvar elpher-pos-cache (make-hash-table :test 'equal))
368
369 (defun elpher-get-cached-content (address)
370   "Retrieve the cached content for ADDRESS, or nil if none exists."
371   (gethash address elpher-content-cache))
372
373 (defun elpher-cache-content (address content)
374   "Set the content cache for ADDRESS to CONTENT."
375   (puthash address content elpher-content-cache))
376
377 (defun elpher-get-cached-pos (address)
378   "Retrieve the cached cursor position for ADDRESS, or nil if none exists."
379   (gethash address elpher-pos-cache))
380
381 (defun elpher-cache-pos (address pos)
382   "Set the cursor position cache for ADDRESS to POS."
383   (puthash address pos elpher-pos-cache))
384
385
386 ;; Page
387
388 (defun elpher-make-page (display-string address)
389   "Create a page with DISPLAY-STRING and ADDRESS."
390   (list display-string address))
391
392 (defun elpher-page-display-string (page)
393   "Retrieve the display string corresponding to PAGE."
394   (elt page 0))
395
396 (defun elpher-page-address (page)
397   "Retrieve the address corresponding to PAGE."
398   (elt page 1))
399
400 (defun elpher-page-set-address (page new-address)
401   "Set the address corresponding to PAGE to NEW-ADDRESS."
402   (setcar (cdr page) new-address))
403
404 (defvar elpher-current-page nil)
405 (defvar elpher-history nil)
406
407 (defun elpher-visit-page (page &optional renderer no-history)
408   "Visit PAGE using its own renderer or RENDERER, if non-nil.
409 Additionally, push PAGE onto the stack of previously-visited pages,
410 unless NO-HISTORY is non-nil."
411   (elpher-save-pos)
412   (elpher-process-cleanup)
413   (unless (or no-history
414               (equal (elpher-page-address elpher-current-page)
415                      (elpher-page-address page)))
416     (push elpher-current-page elpher-history))
417   (setq elpher-current-page page)
418   (let* ((address (elpher-page-address page))
419          (type (elpher-address-type address))
420          (type-record (cdr (assoc type elpher-type-map))))
421     (if type-record
422         (funcall (car type-record)
423                  (if renderer
424                      renderer
425                    (cadr type-record)))
426       (elpher-visit-previous-page)
427       (pcase type
428         (`(gopher ,type-char)
429          (error "Unsupported gopher selector type '%c' for '%s'"
430                 type-char (elpher-address-to-url address)))
431         (other
432          (error "Unsupported address type '%S' for '%s'"
433                 other (elpher-address-to-url address)))))))
434
435 (defun elpher-visit-previous-page ()
436   "Visit the previous page in the history."
437   (let ((previous-page (pop elpher-history)))
438     (if previous-page
439         (elpher-visit-page previous-page nil t)
440       (error "No previous page"))))
441       
442 (defun elpher-reload-current-page ()
443   "Reload the current page, discarding any existing cached content."
444   (elpher-cache-content (elpher-page-address elpher-current-page) nil)
445   (elpher-visit-page elpher-current-page))
446
447 (defun elpher-save-pos ()
448   "Save the current position of point to the current page."
449   (when elpher-current-page
450     (elpher-cache-pos (elpher-page-address elpher-current-page) (point))))
451
452 (defun elpher-restore-pos ()
453   "Restore the position of point to that cached in the current page."
454   (let ((pos (elpher-get-cached-pos (elpher-page-address elpher-current-page))))
455     (if pos
456         (goto-char pos)
457       (goto-char (point-min)))))
458
459
460 ;;; Buffer preparation
461 ;;
462
463 (defun elpher-update-header ()
464   "If `elpher-use-header' is true, display current page info in window header."
465   (if elpher-use-header
466       (let* ((display-string (elpher-page-display-string elpher-current-page))
467              (address (elpher-page-address elpher-current-page))
468              (tls-string (if (and (not (elpher-address-special-p address))
469                                   (member (elpher-address-protocol address)
470                                           '("gophers" "gemini")))
471                              " [TLS encryption]"
472                            ""))
473              (header (concat display-string
474                              (propertize tls-string 'face 'bold))))
475         (setq header-line-format header))))
476
477 (defmacro elpher-with-clean-buffer (&rest args)
478   "Evaluate ARGS with a clean *elpher* buffer as current."
479   (list 'with-current-buffer "*elpher*"
480         '(elpher-mode)
481         (append (list 'let '((inhibit-read-only t))
482                       '(setq-local network-security-level
483                                    (default-value 'network-security-level))
484                       '(erase-buffer)
485                       '(elpher-update-header))
486                 args)))
487
488
489 ;;; Text Processing
490 ;;
491
492 (defvar elpher-user-coding-system nil
493   "User-specified coding system to use for decoding text responses.")
494
495 (defun elpher-decode (string)
496   "Decode STRING using autodetected or user-specified coding system."
497   (decode-coding-string string
498                         (if elpher-user-coding-system
499                             elpher-user-coding-system
500                           (detect-coding-string string t))))
501
502 (defun elpher-preprocess-text-response (string)
503   "Preprocess text selector response contained in STRING.
504 This involes decoding the character representation, and clearing
505 away CRs and any terminating period."
506   (elpher-decode (replace-regexp-in-string "\n\.\n$" "\n"
507                                            (replace-regexp-in-string "\r" "" string))))
508
509
510 ;;; Network error reporting
511 ;;
512
513 (defun elpher-network-error (address error)
514   "Display ERROR message following unsuccessful negotiation with ADDRESS.
515 ERROR can be either an error object or a string."
516   (elpher-with-clean-buffer
517    (insert (propertize "\n---- ERROR -----\n\n" 'face 'error)
518            "When attempting to retrieve " (elpher-address-to-url address) ":\n"
519            (if (stringp error) error (error-message-string error)) "\n"
520            (propertize "\n----------------\n\n" 'face 'error)
521            "Press 'u' to return to the previous page.")))
522
523
524 ;;; Gopher selector retrieval
525 ;;
526
527 (defvar elpher-network-timer nil
528   "Timer used for network connections.")
529
530 (defun elpher-process-cleanup ()
531   "Immediately shut down any extant elpher process and timers."
532   (let ((p (get-process "elpher-process")))
533     (if p (delete-process p)))
534   (if (timerp elpher-network-timer)
535       (cancel-timer elpher-network-timer)))
536
537 (defvar elpher-use-tls nil
538   "If non-nil, use TLS to communicate with gopher servers.")
539
540 (defun elpher-get-selector (address renderer &optional force-ipv4)
541   "Retrieve selector specified by ADDRESS, then render it using RENDERER.
542 If FORCE-IPV4 is non-nil, explicitly look up and use IPv4 address corresponding
543 to ADDRESS."
544   (when (equal (elpher-address-protocol address) "gophers")
545     (if (gnutls-available-p)
546         (when (not elpher-use-tls)
547           (setq elpher-use-tls t)
548           (message "Engaging TLS gopher mode."))
549       (error "Cannot retrieve TLS gopher selector: GnuTLS not available")))
550   (unless (< (elpher-address-port address) 65536)
551     (error "Cannot retrieve gopher selector: port number > 65536"))
552   (condition-case nil
553       (let* ((kill-buffer-query-functions nil)
554              (gnutls-verify-error nil) ; We use the NSM for verification
555              (port (elpher-address-port address))
556              (host (elpher-address-host address))
557              (selector-string-parts nil)
558              (bytes-received 0)
559              (hkbytes-received 0)
560              (proc (open-network-stream "elpher-process"
561                                         nil
562                                         (if force-ipv4 (dns-query host) host)
563                                         (if (> port 0) port 70)
564                                         :type (if elpher-use-tls 'tls 'plain)
565                                         :nowait t))
566              (timer (run-at-time elpher-connection-timeout
567                                  nil
568                                  (lambda ()
569                                    (pcase (process-status proc)
570                                      ('failed
571                                       (if (and (not (equal (elpher-address-protocol address)
572                                                            "gophers"))
573                                                elpher-use-tls
574                                                (or elpher-auto-disengage-TLS
575                                                    (yes-or-no-p "Could not establish encrypted connection.  Disable TLS mode?")))
576                                           (progn
577                                             (message "Disabling TLS mode.")
578                                             (setq elpher-use-tls nil)
579                                             (elpher-get-selector address renderer))
580                                         (elpher-network-error address "Could not establish encrypted connection")))
581                                      ('connect
582                                       (elpher-process-cleanup)
583                                       (unless force-ipv4
584                                         (message "Connection timed out. Retrying with IPv4 address.")
585                                         (elpher-get-selector address renderer t))))))))
586         (setq elpher-network-timer timer)
587         (set-process-coding-system proc 'binary)
588         (set-process-filter proc
589                             (lambda (_proc string)
590                               (when timer
591                                 (cancel-timer timer)
592                                 (setq timer nil))
593                               (setq bytes-received (+ bytes-received (length string)))
594                               (let ((new-hkbytes-received (/ bytes-received 102400)))
595                                 (when (> new-hkbytes-received hkbytes-received)
596                                   (setq hkbytes-received new-hkbytes-received)
597                                   (with-current-buffer "*elpher*"
598                                     (let ((inhibit-read-only t))
599                                       (goto-char (point-min))
600                                       (beginning-of-line 2)
601                                       (delete-region (point) (point-max))
602                                       (insert "("
603                                               (number-to-string (/ hkbytes-received 10.0))
604                                               " MB read)")))))
605                               (setq selector-string-parts
606                                     (cons string selector-string-parts))))
607         (set-process-sentinel proc
608                               (lambda (_proc event)
609                                 (condition-case the-error
610                                     (cond
611                                      ((string-prefix-p "deleted" event))
612                                      ((string-prefix-p "open" event)
613                                       (let ((inhibit-eol-conversion t))
614                                         (process-send-string
615                                          proc
616                                          (concat (elpher-gopher-address-selector address)
617                                                  "\r\n"))))
618                                      (t
619                                       (when timer
620                                         (cancel-timer timer)
621                                         (setq timer nil))
622                                       (funcall renderer (apply #'concat
623                                                                (reverse selector-string-parts)))
624                                       (elpher-restore-pos)))
625                                   (error
626                                    (elpher-network-error address the-error))))))
627     (error
628      (error "Error initiating connection to server"))))
629
630 (defun elpher-get-gopher-page (renderer)
631   "Getter function for gopher pages.
632 The RENDERER procedure is used to display the contents of the page
633 once they are retrieved from the gopher server."
634   (let* ((address (elpher-page-address elpher-current-page))
635          (content (elpher-get-cached-content address)))
636     (if (and content (funcall renderer nil))
637         (elpher-with-clean-buffer
638          (insert content)
639          (elpher-restore-pos))
640       (elpher-with-clean-buffer
641        (insert "LOADING... (use 'u' to cancel)\n"))
642       (condition-case the-error
643           (elpher-get-selector address renderer)
644         (error
645          (elpher-network-error address the-error))))))
646
647 ;; Index rendering
648
649 (defun elpher-insert-index (string)
650   "Insert the index corresponding to STRING into the current buffer."
651   ;; Should be able to split directly on CRLF, but some non-conformant
652   ;; LF-only servers sadly exist, hence the following.
653   (let ((str-processed (elpher-preprocess-text-response string)))
654     (dolist (line (split-string str-processed "\n"))
655       (ignore-errors
656         (unless (= (length line) 0)
657           (let* ((type (elt line 0))
658                  (fields (split-string (substring line 1) "\t"))
659                  (display-string (elt fields 0))
660                  (selector (elt fields 1))
661                  (host (elt fields 2))
662                  (port (if (elt fields 3)
663                            (string-to-number (elt fields 3))
664                          nil))
665                  (address (elpher-make-gopher-address type selector host port)))
666             (elpher-insert-index-record display-string address)))))))
667
668 (defun elpher-insert-margin (&optional type-name)
669   "Insert index margin, optionally containing the TYPE-NAME, into the current buffer."
670   (if type-name
671       (progn
672         (insert (format (concat "%" (number-to-string (- elpher-margin-width 1)) "s")
673                         (concat
674                          (propertize "[" 'face 'elpher-margin-brackets)
675                          (propertize type-name 'face 'elpher-margin-key)
676                          (propertize "]" 'face 'elpher-margin-brackets))))
677         (insert " "))
678     (insert (make-string elpher-margin-width ?\s))))
679
680 (defun elpher-page-button-help (page)
681   "Return a string containing the help text for a button corresponding to PAGE."
682   (let ((address (elpher-page-address page)))
683     (format "mouse-1, RET: open '%s'" (if (elpher-address-special-p address)
684                                           address
685                                         (elpher-address-to-url address)))))
686
687 (defun elpher-insert-index-record (display-string &optional address)
688   "Function to insert an index record into the current buffer.
689 The contents of the record are dictated by DISPLAY-STRING and ADDRESS.
690 If ADDRESS is not supplied or nil the record is rendered as an
691 'information' line."
692   (let* ((type (if address (elpher-address-type address) nil))
693          (type-map-entry (cdr (assoc type elpher-type-map))))
694     (if type-map-entry
695         (let* ((margin-code (elt type-map-entry 2))
696                (face (elt type-map-entry 3))
697                (filtered-display-string (ansi-color-filter-apply display-string))
698                (page (elpher-make-page filtered-display-string address)))
699           (elpher-insert-margin margin-code)
700           (insert-text-button filtered-display-string
701                               'face face
702                               'elpher-page page
703                               'action #'elpher-click-link
704                               'follow-link t
705                               'help-echo (elpher-page-button-help page)))
706       (pcase type
707         ('nil ;; Information
708          (elpher-insert-margin)
709          (let ((propertized-display-string
710                 (propertize display-string 'face 'elpher-info)))
711            (insert (elpher-process-text-for-display propertized-display-string))))
712         (`(gopher ,selector-type) ;; Unknown
713          (elpher-insert-margin (concat (char-to-string selector-type) "?"))
714          (insert (propertize display-string
715                              'face 'elpher-unknown)))))
716     (insert "\n")))
717
718 (defun elpher-click-link (button)
719   "Function called when the gopher link BUTTON is activated (via mouse or keypress)."
720   (let ((page (button-get button 'elpher-page)))
721     (elpher-visit-page page)))
722
723 (defun elpher-render-index (data &optional _mime-type-string)
724   "Render DATA as an index.  MIME-TYPE-STRING is unused."
725   (elpher-with-clean-buffer
726    (if (not data)
727        t
728      (elpher-insert-index data)
729      (elpher-cache-content (elpher-page-address elpher-current-page)
730                            (buffer-string)))))
731
732 ;; Text rendering
733
734 (defconst elpher-url-regex
735   "\\([a-zA-Z]+\\)://\\([a-zA-Z0-9.\-]*[a-zA-Z0-9\-]\\|\[[a-zA-Z0-9:]+\]\\)\\(:[0-9]+\\)?\\(/\\([0-9a-zA-Z\-_~?/@|:.%#=&]*[0-9a-zA-Z\-_~?/@|#]\\)?\\)?"
736   "Regexp used to locate and buttinofy URLs in text files loaded by elpher.")
737
738 (defun elpher-buttonify-urls (string)
739   "Turn substrings which look like urls in STRING into clickable buttons."
740   (with-temp-buffer
741     (insert string)
742     (goto-char (point-min))
743     (while (re-search-forward elpher-url-regex nil t)
744       (let ((page (elpher-make-page (substring-no-properties (match-string 0))
745                                     (elpher-address-from-url (match-string 0)))))
746           (make-text-button (match-beginning 0)
747                             (match-end 0)
748                             'elpher-page  page
749                             'action #'elpher-click-link
750                             'follow-link t
751                             'help-echo (elpher-page-button-help page)
752                             'face 'button)))
753     (buffer-string)))
754
755 (defconst elpher-ansi-regex "\x1b\\[[^m]*m"
756   "Wildly incomplete regexp used to strip out some troublesome ANSI escape sequences.")
757
758 (defun elpher-process-text-for-display (string)
759   "Perform any desired processing of STRING prior to display as text.
760 Currently includes buttonifying URLs and processing ANSI escape codes."
761   (elpher-buttonify-urls (if elpher-filter-ansi-from-text
762                              (ansi-color-filter-apply string)
763                            (ansi-color-apply string))))
764
765 (defun elpher-render-text (data &optional _mime-type-string)
766   "Render DATA as text.  MIME-TYPE-STRING is unused."
767   (elpher-with-clean-buffer
768    (if (not data)
769        t
770      (insert (elpher-process-text-for-display (elpher-preprocess-text-response data)))
771      (elpher-cache-content
772       (elpher-page-address elpher-current-page)
773       (buffer-string)))))
774
775 ;; Image retrieval
776
777 (defun elpher-render-image (data &optional _mime-type-string)
778   "Display DATA as image.  MIME-TYPE-STRING is unused."
779   (if (not data)
780       nil
781     (if (display-images-p)
782         (progn
783           (let ((image (create-image
784                         data
785                         nil t)))
786             (elpher-with-clean-buffer
787              (insert-image image)
788              (elpher-restore-pos))))
789       (elpher-render-download data))))
790
791 ;; Search retrieval and rendering
792
793 (defun elpher-get-gopher-query-page (renderer)
794   "Getter for gopher addresses requiring input.
795 The response is rendered using the rendering function RENDERER."
796    (let* ((address (elpher-page-address elpher-current-page))
797           (content (elpher-get-cached-content address))
798           (aborted t))
799     (if (and content (funcall renderer nil))
800         (elpher-with-clean-buffer
801          (insert content)
802          (elpher-restore-pos)
803          (message "Displaying cached search results.  Reload to perform a new search."))
804       (unwind-protect
805           (let* ((query-string (read-string "Query: "))
806                  (query-selector (concat (elpher-gopher-address-selector address) "\t" query-string))
807                  (search-address (elpher-make-gopher-address ?1
808                                                              query-selector
809                                                              (elpher-address-host address)
810                                                              (elpher-address-port address)
811                                                              (equal (elpher-address-type address) "gophers"))))
812             (setq aborted nil)
813
814             (elpher-with-clean-buffer
815              (insert "LOADING RESULTS... (use 'u' to cancel)"))
816             (elpher-get-selector search-address renderer))
817         (if aborted
818             (elpher-visit-previous-page))))))
819  
820 ;; Raw server response rendering
821
822 (defun elpher-render-raw (data &optional mime-type-string)
823   "Display raw DATA in buffer.  MIME-TYPE-STRING is also displayed if provided."
824   (if (not data)
825       nil
826     (elpher-with-clean-buffer
827      (when mime-type-string
828        (insert "MIME type specified by server: '" mime-type-string "'\n"))
829      (insert data)
830      (goto-char (point-min)))
831     (message "Displaying raw server response.  Reload or redraw to return to standard view.")))
832
833 ;; File save "rendering"
834
835 (defun elpher-render-download (data &optional _mime-type-string)
836   "Save DATA to file.  MIME-TYPE-STRING is unused."
837   (if (not data)
838       nil
839     (let* ((address (elpher-page-address elpher-current-page))
840            (selector (if (elpher-address-gopher-p address)
841                          (elpher-gopher-address-selector address)
842                        (elpher-address-filename address))))
843       (elpher-visit-previous-page) ; Do first in case of non-local exits.
844       (let* ((filename-proposal (file-name-nondirectory selector))
845              (filename (read-file-name "Download complete. Save file as: "
846                                        nil nil nil
847                                        (if (> (length filename-proposal) 0)
848                                            filename-proposal
849                                          "download.file"))))
850         (let ((coding-system-for-write 'binary))
851           (with-temp-file filename
852             (insert data)))
853         (message (format "Saved to file %s." filename))))))
854
855 ;; HTML rendering
856
857 (defun elpher-render-html (data &optional _mime-type-string)
858   "Render DATA as HTML using shr.  MIME-TYPE-STRING is unused."
859   (elpher-with-clean-buffer
860    (if (not data)
861        t
862      (let ((dom (with-temp-buffer
863                   (insert data)
864                   (libxml-parse-html-region (point-min) (point-max)))))
865        (shr-insert-document dom)))))
866
867 ;; Gemini page retrieval
868
869 (defvar elpher-gemini-redirect-chain)
870
871 (defun elpher-get-gemini-response (address renderer &optional force-ipv4)
872   "Retrieve gemini ADDRESS, then render using RENDERER.
873 If FORCE-IPV4 is non-nil, explicitly look up and use IPv4 address corresponding
874 to ADDRESS."
875   (unless elpher-gemini-TLS-cert-checks
876     (setq-local network-security-level 'low))
877   (if (not (gnutls-available-p))
878       (error "Cannot establish gemini connection: GnuTLS not available")
879     (unless (< (elpher-address-port address) 65536)
880       (error "Cannot establish gemini connection: port number > 65536"))
881     (condition-case nil
882         (let* ((kill-buffer-query-functions nil)
883                (gnutls-verify-error nil) ; We use the NSM for verification
884                (port (elpher-address-port address))
885                (host (elpher-address-host address))
886                (response-string-parts nil)
887                (bytes-received 0)
888                (hkbytes-received 0)
889                (proc (open-network-stream "elpher-process"
890                                           nil
891                                           (if force-ipv4 (dns-query host) host)
892                                           (if (> port 0) port 1965)
893                                           :type 'tls
894                                           :nowait t))
895                (timer (run-at-time elpher-connection-timeout nil
896                                    (lambda ()
897                                      (elpher-process-cleanup)
898                                      (unless force-ipv4
899                                         ; Try again with IPv4
900                                        (message "Connection timed out.  Retrying with IPv4.")
901                                        (elpher-get-gemini-response address renderer t))))))
902           (setq elpher-network-timer timer)
903           (set-process-coding-system proc 'binary)
904           (set-process-filter proc
905                               (lambda (_proc string)
906                                 (when timer
907                                   (cancel-timer timer)
908                                   (setq timer nil))
909                                 (setq bytes-received (+ bytes-received (length string)))
910                                 (let ((new-hkbytes-received (/ bytes-received 102400)))
911                                   (when (> new-hkbytes-received hkbytes-received)
912                                     (setq hkbytes-received new-hkbytes-received)
913                                     (with-current-buffer "*elpher*"
914                                       (let ((inhibit-read-only t))
915                                         (goto-char (point-min))
916                                         (beginning-of-line 2)
917                                         (delete-region (point) (point-max))
918                                         (insert "("
919                                                 (number-to-string (/ hkbytes-received 10.0))
920                                                 " MB read)")))))
921                                 (setq response-string-parts
922                                       (cons string response-string-parts))))
923           (set-process-sentinel proc
924                                 (lambda (proc event)
925                                   (condition-case the-error
926                                       (cond
927                                        ((string-prefix-p "open" event)    ; request URL
928                                         (let ((inhibit-eol-conversion t))
929                                           (process-send-string
930                                            proc
931                                            (concat (elpher-address-to-url address)
932                                                    "\r\n"))))
933                                        ((string-prefix-p "deleted" event)) ; do nothing
934                                        ((and (not response-string-parts)
935                                              (not force-ipv4))
936                                         ; Try again with IPv4
937                                         (message "Connection failed. Retrying with IPv4.")
938                                         (cancel-timer timer)
939                                         (elpher-get-gemini-response address renderer t))
940                                        (t
941                                         (funcall #'elpher-process-gemini-response
942                                                  (apply #'concat (reverse response-string-parts))
943                                                  renderer)
944                                         (elpher-restore-pos)))
945                                     (error
946                                      (elpher-network-error address the-error))))))
947       (error
948        (error "Error initiating connection to server")))))
949
950 (defun elpher-parse-gemini-response (response)
951   "Parse the RESPONSE string and return a list of components.
952 The list is of the form (code meta body).  A response of nil implies
953 that the response was malformed."
954   (let ((header-end-idx (string-match "\r\n" response)))
955     (if header-end-idx
956         (let ((header (string-trim (substring response 0 header-end-idx)))
957               (body (substring response (+ header-end-idx 2))))
958           (if (>= (length header) 2)
959               (let ((code (substring header 0 2))
960                     (meta (string-trim (substring header 2))))
961                 (list code meta body))
962             (error "Malformed response: No response status found in header %s" header)))
963       (error "Malformed response: No CRLF-delimited header found in response %s" response))))
964
965 (defun elpher-process-gemini-response (response-string renderer)
966   "Process the gemini response RESPONSE-STRING and pass the result to RENDERER."
967   (let ((response-components (elpher-parse-gemini-response response-string)))
968     (let ((response-code (elt response-components 0))
969           (response-meta (elt response-components 1))
970           (response-body (elt response-components 2)))
971       (pcase (elt response-code 0)
972         (?1 ; Input required
973          (elpher-with-clean-buffer
974           (insert "Gemini server is requesting input."))
975          (let* ((query-string (read-string (concat response-meta ": ")))
976                 (url (elpher-address-to-url (elpher-page-address elpher-current-page)))
977                 (query-address (elpher-address-from-url (concat url "?" query-string))))
978            (elpher-get-gemini-response query-address renderer)))
979         (?2 ; Normal response
980          (funcall renderer response-body response-meta))
981         (?3 ; Redirect
982          (message "Following redirect to %s" response-meta)
983          (if (>= (length elpher-gemini-redirect-chain) 5)
984              (error "More than 5 consecutive redirects followed"))
985          (let ((redirect-address (elpher-address-from-gemini-url response-meta)))
986            (if (member redirect-address elpher-gemini-redirect-chain)
987                (error "Redirect loop detected"))
988            (if (not (string= (elpher-address-protocol redirect-address)
989                              "gemini"))
990                (error "Server tried to automatically redirect to non-gemini URL: %s"
991                       response-meta))
992            (elpher-page-set-address elpher-current-page redirect-address)
993            (add-to-list 'elpher-gemini-redirect-chain redirect-address)
994            (elpher-get-gemini-response redirect-address renderer)))
995         (?4 ; Temporary failure
996          (error "Gemini server reports TEMPORARY FAILURE for this request: %s %s"
997                 response-code response-meta))
998         (?5 ; Permanent failure
999          (error "Gemini server reports PERMANENT FAILURE for this request: %s %s"
1000                 response-code response-meta))
1001         (?6 ; Client certificate required
1002          (error "Gemini server requires client certificate (unsupported at this time)"))
1003         (_other
1004          (error "Gemini server response unknown: %s %s"
1005                 response-code response-meta))))))
1006
1007 (defun elpher-get-gemini-page (renderer)
1008   "Getter which retrieves and renders a Gemini page and renders it using RENDERER."
1009   (let* ((address (elpher-page-address elpher-current-page))
1010          (content (elpher-get-cached-content address)))
1011     (condition-case the-error
1012         (if (and content (funcall renderer nil))
1013             (elpher-with-clean-buffer
1014               (insert content)
1015               (elpher-restore-pos))
1016           (elpher-with-clean-buffer
1017            (insert "LOADING GEMINI... (use 'u' to cancel)\n"))
1018           (setq elpher-gemini-redirect-chain nil)
1019           (elpher-get-gemini-response address renderer))
1020       (error
1021        (elpher-network-error address the-error)))))
1022
1023
1024 (defun elpher-render-gemini (body &optional mime-type-string)
1025   "Render gemini response BODY with rendering MIME-TYPE-STRING."
1026   (if (not body)
1027       t
1028     (let* ((mime-type-string* (if (or (not mime-type-string)
1029                                       (string-empty-p mime-type-string))
1030                                   "text/gemini; charset=utf-8"
1031                                 mime-type-string))
1032            (mime-type-split (split-string mime-type-string* ";" t))
1033            (mime-type (string-trim (car mime-type-split)))
1034            (parameters (mapcar (lambda (s)
1035                                  (let ((key-val (split-string s "=")))
1036                                    (list (downcase (string-trim (car key-val)))
1037                                          (downcase (string-trim (cadr key-val))))))
1038                                (cdr mime-type-split))))
1039       (when (string-prefix-p "text/" mime-type)
1040         (setq body (decode-coding-string
1041                     body
1042                     (if (assoc "charset" parameters)
1043                         (intern (cadr (assoc "charset" parameters)))
1044                       'utf-8)))
1045         (setq body (replace-regexp-in-string "\r" "" body)))
1046       (pcase mime-type
1047         ((or "text/gemini" "")
1048          (elpher-render-gemini-map body parameters))
1049         ("text/html"
1050          (elpher-render-html body))
1051         ((pred (string-prefix-p "text/"))
1052          (elpher-render-gemini-plain-text body parameters))
1053         ((pred (string-prefix-p "image/"))
1054          (elpher-render-image body))
1055         (_other
1056          (elpher-render-download body))))))
1057
1058 (defun elpher-gemini-get-link-url (link-line)
1059   "Extract the url portion of LINK-LINE, a gemini map file link line.
1060 Returns nil in the event that the contents of the line following the
1061 => prefix are empty."
1062   (let ((l (split-string (substring link-line 2))))
1063     (if l
1064         (string-trim (elt l 0))
1065       nil)))
1066
1067 (defun elpher-gemini-get-link-display-string (link-line)
1068   "Extract the display string portion of LINK-LINE, a gemini map file link line.
1069 Returns the url portion in the event that the display-string portion is empty."
1070   (let* ((rest (string-trim (elt (split-string link-line "=>") 1)))
1071          (idx (string-match "[ \t]" rest)))
1072     (string-trim (if idx
1073                      (substring rest (+ idx 1))
1074                    rest))))
1075
1076 (defun elpher-collapse-dot-sequences (filename)
1077   "Collapse dot sequences in FILENAME.
1078 For instance, the filename /a/b/../c/./d will reduce to /a/c/d"
1079   (let* ((path (split-string filename "/"))
1080          (path-reversed-normalized
1081           (seq-reduce (lambda (a b)
1082                         (cond ((and a (equal b "..") (cdr a)))
1083                               ((and (not a) (equal b "..")) a) ;leading .. are dropped
1084                               ((equal b ".") a)
1085                               (t (cons b a))))
1086                       path nil)))
1087     (string-join (reverse path-reversed-normalized) "/")))
1088
1089 (defun elpher-address-from-gemini-url (url)
1090   "Extract address from URL with defaults as per gemini map files."
1091   (let ((address (url-generic-parse-url url)))
1092     (unless (and (url-type address) (not (url-fullness address))) ;avoid mangling mailto: urls
1093       (setf (url-fullness address) t)
1094       (if (url-host address) ;if there is an explicit host, filenames are absolute
1095           (if (string-empty-p (url-filename address))
1096               (setf (url-filename address) "/")) ;ensure empty filename is marked as absolute
1097         (setf (url-host address) (url-host (elpher-page-address elpher-current-page)))
1098         (unless (string-prefix-p "/" (url-filename address)) ;deal with relative links
1099           (setf (url-filename address)
1100                 (concat (file-name-directory
1101                          (url-filename (elpher-page-address elpher-current-page)))
1102                         (url-filename address)))))
1103       (unless (url-type address)
1104         (setf (url-type address) "gemini"))
1105       (if (equal (url-type address) "gemini")
1106           (setf (url-filename address)
1107                 (elpher-collapse-dot-sequences (url-filename address)))))
1108     address))
1109
1110 (defun elpher-gemini-insert-link (link-line)
1111   "Insert link described by LINK-LINE into a text/gemini document."
1112   (let* ((url (elpher-gemini-get-link-url link-line))
1113          (display-string (elpher-gemini-get-link-display-string link-line))
1114          (address (elpher-address-from-gemini-url url))
1115          (type (if address (elpher-address-type address) nil))
1116          (type-map-entry (cdr (assoc type elpher-type-map))))
1117     (when display-string
1118       (insert "→ ")
1119       (if type-map-entry
1120           (let* ((face (elt type-map-entry 3))
1121                  (filtered-display-string (ansi-color-filter-apply display-string))
1122                  (page (elpher-make-page filtered-display-string address)))
1123             (insert-text-button filtered-display-string
1124                                 'face face
1125                                 'elpher-page page
1126                                 'action #'elpher-click-link
1127                                 'follow-link t
1128                                 'help-echo (elpher-page-button-help page)))
1129         (insert (propertize display-string 'face 'elpher-unknown)))
1130       (insert "\n"))))
1131   
1132 (defun elpher-gemini-insert-header (header-line)
1133   "Insert header described by HEADER-LINE into a text/gemini document.
1134 The gemini map file line describing the header is given
1135 by HEADER-LINE."
1136   (when (string-match "^\\(#+\\)[ \t]*" header-line)
1137     (let ((level (length (match-string 1 header-line)))
1138           (header (substring header-line (match-end 0))))
1139       (unless (display-graphic-p)
1140         (insert (make-string level ?#) " "))
1141       (insert (propertize header 'face
1142                           (pcase level
1143                             (1 'elpher-gemini-heading1)
1144                             (2 'elpher-gemini-heading2)
1145                             (3 'elpher-gemini-heading3)
1146                             (_ 'default)))
1147               "\n"))))
1148
1149 (defun elpher-gemini-insert-text (text-line)
1150   "Insert a plain non-preformatted TEXT-LINE into a text/gemini document.
1151 This function uses Emacs' auto-fill to wrap text sensibly to a maximum
1152 width defined by elpher-gemini-max-fill-width."
1153   (insert (elpher-process-text-for-display text-line))
1154   (let* ((prefix-end-idx (string-match "[^ \t*]" text-line))
1155          (fill-prefix (if prefix-end-idx
1156                           (let ((raw-prefix (substring text-line 0 prefix-end-idx)))
1157                             (replace-regexp-in-string "\*" " " raw-prefix))
1158                         nil)))
1159     (newline)))
1160
1161 (defun elpher-render-gemini-map (data _parameters)
1162   "Render DATA as a gemini map file, PARAMETERS is currently unused."
1163   (elpher-with-clean-buffer
1164    (let ((preformatted nil))
1165      (auto-fill-mode 1)
1166      (setq-local fill-column (min (window-width) elpher-gemini-max-fill-width))
1167      (dolist (line (split-string data "\n"))
1168        (cond
1169         ((string-prefix-p "```" line) (setq preformatted (not preformatted)))
1170         (preformatted (insert (elpher-process-text-for-display
1171                                (propertize line 'face 'elpher-gemini-preformatted))
1172                               "\n"))
1173         ((string-prefix-p "=>" line) (elpher-gemini-insert-link line))
1174         ((string-prefix-p "#" line) (elpher-gemini-insert-header line))
1175         (t (elpher-gemini-insert-text line)))))
1176    (elpher-cache-content
1177     (elpher-page-address elpher-current-page)
1178     (buffer-string))))
1179
1180 (defun elpher-render-gemini-plain-text (data _parameters)
1181   "Render DATA as plain text file.  PARAMETERS is currently unused."
1182   (elpher-with-clean-buffer
1183    (insert (elpher-process-text-for-display data))
1184    (elpher-cache-content
1185     (elpher-page-address elpher-current-page)
1186     (buffer-string))))
1187
1188 ;; Finger page connection
1189
1190 (defun elpher-get-finger-page (renderer &optional force-ipv4)
1191   "Opens a finger connection to the current page address and renders it using RENDERER."
1192   (let* ((address (elpher-page-address elpher-current-page))
1193          (content (elpher-get-cached-content address)))
1194     (if (and content (funcall renderer nil))
1195         (elpher-with-clean-buffer
1196          (insert content)
1197          (elpher-restore-pos))
1198       (elpher-with-clean-buffer
1199        (insert "LOADING... (use 'u' to cancel)\n"))
1200       (condition-case the-error
1201           (let* ((kill-buffer-query-functions nil)
1202                  (user (let ((filename (elpher-address-filename address)))
1203                          (if (> (length filename) 1)
1204                              (substring filename 1)
1205                            (elpher-address-user address))))
1206                  (port (let ((given-port (elpher-address-port address)))
1207                          (if (> given-port 0) given-port 79)))
1208                  (host (elpher-address-host address))
1209                  (selector-string-parts nil)
1210                  (proc (open-network-stream "elpher-process"
1211                                             nil
1212                                             (if force-ipv4 (dns-query host) host)
1213                                             port
1214                                             :type 'plain
1215                                             :nowait t))
1216                  (timer (run-at-time elpher-connection-timeout
1217                                      nil
1218                                      (lambda ()
1219                                        (pcase (process-status proc)
1220                                          ('connect
1221                                           (elpher-process-cleanup)
1222                                           (unless force-ipv4
1223                                             (message "Connection timed out. Retrying with IPv4 address.")
1224                                             (elpher-get-finger-page renderer t))))))))
1225             (setq elpher-network-timer timer)
1226             (set-process-coding-system proc 'binary)
1227             (set-process-filter proc
1228                                 (lambda (_proc string)
1229                                   (when timer
1230                                     (cancel-timer timer)
1231                                     (setq timer nil))
1232                                   (setq selector-string-parts
1233                                         (cons string selector-string-parts))))
1234             (set-process-sentinel proc
1235                                   (lambda (_proc event)
1236                                     (condition-case the-error
1237                                         (cond
1238                                          ((string-prefix-p "deleted" event))
1239                                          ((string-prefix-p "open" event)
1240                                           (let ((inhibit-eol-conversion t))
1241                                             (process-send-string
1242                                              proc
1243                                              (concat user "\r\n"))))
1244                                          (t
1245                                           (when timer
1246                                             (cancel-timer timer)
1247                                             (setq timer nil))
1248                                           (funcall renderer (apply #'concat
1249                                                                    (reverse selector-string-parts)))
1250                                           (elpher-restore-pos)))))))
1251         (error
1252          (elpher-network-error address the-error))))))
1253
1254
1255 ;; Other URL page opening
1256
1257 (defun elpher-get-other-url-page (renderer)
1258   "Getter which attempts to open the URL specified by the current page (RENDERER must be nil)."
1259   (when renderer
1260     (elpher-visit-previous-page)
1261     (error "Command not supported for general URLs"))
1262   (let* ((address (elpher-page-address elpher-current-page))
1263          (url (elpher-address-to-url address)))
1264     (progn
1265       (elpher-visit-previous-page) ; Do first in case of non-local exits.
1266       (message "Opening URL...")
1267       (if elpher-open-urls-with-eww
1268           (browse-web url)
1269         (browse-url url)))))
1270
1271 ;; Telnet page connection
1272
1273 (defun elpher-get-telnet-page (renderer)
1274   "Opens a telnet connection to the current page address (RENDERER must be nil)."
1275   (when renderer
1276     (elpher-visit-previous-page)
1277     (error "Command not supported for telnet URLs"))
1278   (let* ((address (elpher-page-address elpher-current-page))
1279          (host (elpher-address-host address))
1280          (port (elpher-address-port address)))
1281     (elpher-visit-previous-page)
1282     (if (> port 0)
1283         (telnet host port)
1284       (telnet host))))
1285
1286 ;; Start page page retrieval
1287
1288 (defun elpher-get-start-page (renderer)
1289   "Getter which displays the start page (RENDERER must be nil)."
1290   (when renderer
1291     (elpher-visit-previous-page)
1292     (error "Command not supported for start page"))
1293   (elpher-with-clean-buffer
1294    (insert "     --------------------------------------------\n"
1295            "           Elpher Gopher and Gemini Client       \n"
1296            "                   version " elpher-version "\n"
1297            "     --------------------------------------------\n"
1298            "\n"
1299            "Default bindings:\n"
1300            "\n"
1301            " - TAB/Shift-TAB: next/prev item on current page\n"
1302            " - RET/mouse-1: open item under cursor\n"
1303            " - m: select an item on current page by name (autocompletes)\n"
1304            " - u/mouse-3: return to previous page\n"
1305            " - o/O: visit different selector or the root menu of the current server\n"
1306            " - g: go to a particular address (gopher, gemini, finger)\n"
1307            " - d/D: download item under cursor or current page\n"
1308            " - i/I: info on item under cursor or current page\n"
1309            " - c/C: copy URL representation of item under cursor or current page\n"
1310            " - a/A: bookmark the item under cursor or current page\n"
1311            " - x/X: remove bookmark for item under cursor or current page\n"
1312            " - B: visit the bookmarks page\n"
1313            " - r: redraw current page (using cached contents if available)\n"
1314            " - R: reload current page (regenerates cache)\n"
1315            " - S: set character coding system for gopher (default is to autodetect)\n"
1316            " - T: toggle TLS gopher mode\n"
1317            " - .: display the raw server response for the current page\n"
1318            "\n"
1319            "Start your exploration of gopher space and gemini:\n")
1320    (elpher-insert-index-record "Floodgap Systems Gopher Server"
1321                                (elpher-make-gopher-address ?1 "" "gopher.floodgap.com" 70))
1322    (elpher-insert-index-record "Project Gemini home page"
1323                                (elpher-address-from-url "gemini://gemini.circumlunar.space/"))
1324    (insert "\n"
1325            "Alternatively, select a search engine and enter some search terms:\n")
1326    (elpher-insert-index-record "Gopher Search Engine (Veronica-2)"
1327                                (elpher-make-gopher-address ?7 "/v2/vs" "gopher.floodgap.com" 70))
1328    (elpher-insert-index-record "Gemini Search Engine (GUS)"
1329                                (elpher-address-from-url "gemini://gus.guru/search"))
1330    (insert "\n"
1331            "This page contains your bookmarked sites (also visit with B):\n")
1332    (elpher-insert-index-record "Your Bookmarks" 'bookmarks)
1333    (insert "\n"
1334            "For Elpher release news or to leave feedback, visit:\n")
1335    (elpher-insert-index-record "The Elpher Project Page"
1336                                (elpher-make-gopher-address ?1
1337                                                            "/projects/elpher/"
1338                                                            "thelambdalab.xyz"
1339                                                            70))
1340    (insert "\n"
1341            "** Refer to the ")
1342    (let ((help-string "RET,mouse-1: Open Elpher info manual (if available)"))
1343      (insert-text-button "Elpher info manual"
1344                          'face 'link
1345                          'action (lambda (_)
1346                                    (interactive)
1347                                    (info "(elpher)"))
1348                          'follow-link t
1349                          'help-echo help-string))
1350    (insert " for the full documentation. **\n")
1351    (insert (propertize
1352             (concat "  (This should be available if you have installed Elpher using\n"
1353                     "   MELPA. Otherwise you will have to install the manual yourself.)\n")
1354             'face 'shadow))
1355    (elpher-restore-pos)))
1356
1357 ;; Bookmarks page page retrieval
1358
1359 (defun elpher-get-bookmarks-page (renderer)
1360   "Getter to load and display the current bookmark list (RENDERER must be nil)."
1361   (when renderer
1362     (elpher-visit-previous-page)
1363     (error "Command not supported for bookmarks page"))
1364   (elpher-with-clean-buffer
1365    (insert "---- Bookmark list ----\n\n")
1366    (let ((bookmarks (elpher-load-bookmarks)))
1367      (if bookmarks
1368          (dolist (bookmark bookmarks)
1369            (let ((display-string (elpher-bookmark-display-string bookmark))
1370                  (address (elpher-address-from-url (elpher-bookmark-url bookmark))))
1371              (elpher-insert-index-record display-string address)))
1372        (insert "No bookmarks found.\n")))
1373    (insert "\n-----------------------\n"
1374            "\n"
1375            "- u: return to previous page\n"
1376            "- x: delete selected bookmark\n"
1377            "- a: rename selected bookmark\n"
1378            "\n"
1379            "Bookmarks are stored in the file ")
1380    (let ((filename elpher-bookmarks-file)
1381          (help-string "RET,mouse-1: Open bookmarks file in new buffer for editing."))
1382      (insert-text-button filename
1383                          'face 'link
1384                          'action (lambda (_)
1385                                    (interactive)
1386                                    (find-file filename))
1387                          'follow-link t
1388                          'help-echo help-string))
1389    (insert "\n")
1390    (elpher-restore-pos)))
1391   
1392
1393 ;;; Bookmarks
1394 ;;
1395
1396 (defun elpher-make-bookmark (display-string url)
1397   "Make an elpher bookmark.
1398 DISPLAY-STRING determines how the bookmark will appear in the
1399 bookmark list, while URL is the url of the entry."
1400   (list display-string url))
1401   
1402 (defun elpher-bookmark-display-string (bookmark)
1403   "Get the display string of BOOKMARK."
1404   (elt bookmark 0))
1405
1406 (defun elpher-set-bookmark-display-string (bookmark display-string)
1407   "Set the display string of BOOKMARK to DISPLAY-STRING."
1408   (setcar bookmark display-string))
1409
1410 (defun elpher-bookmark-url (bookmark)
1411   "Get the address for BOOKMARK."
1412   (elt bookmark 1))
1413
1414 (defun elpher-save-bookmarks (bookmarks)
1415   "Record the bookmark list BOOKMARKS to the user's bookmark file.
1416 Beware that this completely replaces the existing contents of the file."
1417   (with-temp-file elpher-bookmarks-file
1418     (erase-buffer)
1419     (insert "; Elpher bookmarks file\n\n"
1420             "; Bookmarks are stored as a list of (label URL) items.\n"
1421             "; Feel free to edit by hand, but take care to ensure\n"
1422             "; the list structure remains intact.\n\n")
1423     (pp bookmarks (current-buffer))))
1424
1425 (defun elpher-load-bookmarks ()
1426   "Get the list of bookmarks from the users's bookmark file."
1427   (let ((bookmarks
1428          (with-temp-buffer
1429            (ignore-errors
1430              (insert-file-contents elpher-bookmarks-file)
1431              (goto-char (point-min))
1432              (read (current-buffer))))))
1433     (if (and bookmarks (listp (cadar bookmarks)))
1434         (progn
1435           (message "Reading old bookmark file. (Will be updated on write.)")
1436           (mapcar (lambda (old-bm)
1437                     (list (car old-bm)
1438                           (elpher-address-to-url (apply #'elpher-make-gopher-address
1439                                                         (cadr old-bm)))))
1440                   bookmarks))
1441       bookmarks)))
1442
1443 (defun elpher-add-address-bookmark (address display-string)
1444   "Save a bookmark for ADDRESS with label DISPLAY-STRING.)))
1445 If ADDRESS is already bookmarked, update the label only."
1446   (let ((bookmarks (elpher-load-bookmarks))
1447         (url (elpher-address-to-url address)))
1448     (let ((existing-bookmark (rassoc (list url) bookmarks)))
1449       (if existing-bookmark
1450           (elpher-set-bookmark-display-string existing-bookmark display-string)
1451         (push (elpher-make-bookmark display-string url) bookmarks)))
1452     (elpher-save-bookmarks bookmarks)))
1453
1454 (defun elpher-remove-address-bookmark (address)
1455   "Remove any bookmark to ADDRESS."
1456   (let ((url (elpher-address-to-url address)))
1457     (elpher-save-bookmarks
1458      (seq-filter (lambda (bookmark)
1459                    (not (equal (elpher-bookmark-url bookmark) url)))
1460                  (elpher-load-bookmarks)))))
1461
1462 ;;; Interactive procedures
1463 ;;
1464
1465 (defun elpher-next-link ()
1466   "Move point to the next link on the current page."
1467   (interactive)
1468   (forward-button 1))
1469
1470 (defun elpher-prev-link ()
1471   "Move point to the previous link on the current page."
1472   (interactive)
1473   (backward-button 1))
1474
1475 (defun elpher-follow-current-link ()
1476   "Open the link or url at point."
1477   (interactive)
1478   (push-button))
1479
1480 (defun elpher-go (host-or-url)
1481   "Go to a particular gopher site HOST-OR-URL.
1482 When run interactively HOST-OR-URL is read from the minibuffer."
1483   (interactive "sGopher or Gemini URL: ")
1484   (let ((page (elpher-make-page host-or-url
1485                                 (elpher-address-from-url host-or-url))))
1486     (switch-to-buffer "*elpher*")
1487     (elpher-visit-page page)
1488     '()))
1489
1490 (defun elpher-go-current ()
1491   "Go to a particular site read from the minibuffer, initialized with the current URL."
1492   (interactive)
1493   (let ((address (elpher-page-address elpher-current-page)))
1494     (if (elpher-address-special-p address)
1495         (error "Command invalid for this page")
1496       (let ((url (read-string "Gopher or Gemini URL: " (elpher-address-to-url address))))
1497         (elpher-visit-page (elpher-make-page url (elpher-address-from-url url)))))))
1498
1499 (defun elpher-redraw ()
1500   "Redraw current page."
1501   (interactive)
1502   (elpher-visit-page elpher-current-page))
1503
1504 (defun elpher-reload ()
1505   "Reload current page."
1506   (interactive)
1507   (elpher-reload-current-page))
1508
1509 (defun elpher-toggle-tls ()
1510   "Toggle TLS encryption mode for gopher."
1511   (interactive)
1512   (setq elpher-use-tls (not elpher-use-tls))
1513   (if elpher-use-tls
1514       (if (gnutls-available-p)
1515           (message "TLS gopher mode enabled.  (Will not affect current page until reload.)")
1516         (setq elpher-use-tls nil)
1517         (error "Cannot enable TLS gopher mode: GnuTLS not available"))
1518     (message "TLS gopher mode disabled.  (Will not affect current page until reload.)")))
1519
1520 (defun elpher-view-raw ()
1521   "View raw server response for current page."
1522   (interactive)
1523   (if (elpher-address-special-p (elpher-page-address elpher-current-page))
1524       (error "This page was not generated by a server")
1525     (elpher-visit-page elpher-current-page
1526                        #'elpher-render-raw)))
1527
1528 (defun elpher-back ()
1529   "Go to previous site."
1530   (interactive)
1531   (elpher-visit-previous-page))
1532
1533 (defun elpher-download ()
1534   "Download the link at point."
1535   (interactive)
1536   (let ((button (button-at (point))))
1537     (if button
1538         (let ((page (button-get button 'elpher-page)))
1539           (if (elpher-address-special-p (elpher-page-address page))
1540               (error "Cannot download %s"
1541                      (elpher-page-display-string page))
1542             (elpher-visit-page (button-get button 'elpher-page)
1543                                #'elpher-render-download)))
1544       (error "No link selected"))))
1545
1546 (defun elpher-download-current ()
1547   "Download the current page."
1548   (interactive)
1549   (if (elpher-address-special-p (elpher-page-address elpher-current-page))
1550       (error "Cannot download %s"
1551              (elpher-page-display-string elpher-current-page))
1552     (elpher-visit-page (elpher-make-page
1553                         (elpher-page-display-string elpher-current-page)
1554                         (elpher-page-address elpher-current-page))
1555                        #'elpher-render-download
1556                        t)))
1557
1558 (defun elpher-build-link-map ()
1559   "Build alist mapping link names to destination pages in current buffer."
1560   (let ((link-map nil)
1561         (b (next-button (point-min) t)))
1562     (while b
1563       (push (cons (button-label b) b) link-map)
1564       (setq b (next-button (button-start b))))
1565     link-map))
1566
1567 (defun elpher-jump ()
1568   "Select a directory entry by name.  Similar to the info browser (m)enu command."
1569   (interactive)
1570   (let* ((link-map (elpher-build-link-map)))
1571     (if link-map
1572         (let ((key (let ((completion-ignore-case t))
1573                      (completing-read "Directory item/link: "
1574                                       link-map nil t))))
1575           (if (and key (> (length key) 0))
1576               (let ((b (cdr (assoc key link-map))))
1577                 (goto-char (button-start b))
1578                 (button-activate b)))))))
1579
1580 (defun elpher-root-dir ()
1581   "Visit root of current server."
1582   (interactive)
1583   (let ((address (elpher-page-address elpher-current-page)))
1584     (if (not (elpher-address-special-p address))
1585         (if (or (member (url-filename address) '("/" ""))
1586                 (and (elpher-address-gopher-p address)
1587                      (= (length (elpher-gopher-address-selector address)) 0)))
1588             (error "Already at root directory of current server")
1589           (let ((address-copy (elpher-address-from-url
1590                                (elpher-address-to-url address))))
1591             (setf (url-filename address-copy) "")
1592             (elpher-go (elpher-address-to-url address-copy))))
1593       (error "Command invalid for %s" (elpher-page-display-string elpher-current-page)))))
1594
1595 (defun elpher-bookmarks-current-p ()
1596   "Return non-nil if current page is a bookmarks page."
1597   (equal (elpher-address-type (elpher-page-address elpher-current-page))
1598          '(special bookmarks)))
1599
1600 (defun elpher-reload-bookmarks ()
1601   "Reload bookmarks if current page is a bookmarks page."
1602   (if (elpher-bookmarks-current-p)
1603       (elpher-reload-current-page)))
1604
1605 (defun elpher-bookmark-current ()
1606   "Bookmark the current page."
1607   (interactive)
1608   (let ((address (elpher-page-address elpher-current-page))
1609         (display-string (elpher-page-display-string elpher-current-page)))
1610     (if (not (elpher-address-special-p address))
1611         (let ((bookmark-display-string (read-string "Bookmark display string: "
1612                                                     display-string)))
1613           (elpher-add-address-bookmark address bookmark-display-string)
1614           (message "Bookmark added."))
1615       (error "Cannot bookmark %s" display-string))))
1616
1617 (defun elpher-bookmark-link ()
1618   "Bookmark the link at point."
1619   (interactive)
1620   (let ((button (button-at (point))))
1621     (if button
1622         (let* ((page (button-get button 'elpher-page))
1623                (address (elpher-page-address page))
1624                (display-string (elpher-page-display-string page)))
1625           (if (not (elpher-address-special-p address))
1626               (let ((bookmark-display-string (read-string "Bookmark display string: "
1627                                                           display-string)))
1628                 (elpher-add-address-bookmark address bookmark-display-string)
1629                 (elpher-reload-bookmarks)
1630                 (message "Bookmark added."))
1631             (error "Cannot bookmark %s" display-string)))
1632       (error "No link selected"))))
1633
1634 (defun elpher-unbookmark-current ()
1635   "Remove bookmark for the current page."
1636   (interactive)
1637   (let ((address (elpher-page-address elpher-current-page)))
1638     (unless (elpher-address-special-p address)
1639       (elpher-remove-address-bookmark address)
1640       (message "Bookmark removed."))))
1641
1642 (defun elpher-unbookmark-link ()
1643   "Remove bookmark for the link at point."
1644   (interactive)
1645   (let ((button (button-at (point))))
1646     (if button
1647         (let ((page (button-get button 'elpher-page)))
1648           (elpher-remove-address-bookmark (elpher-page-address page))
1649           (elpher-reload-bookmarks)
1650           (message "Bookmark removed."))
1651       (error "No link selected"))))
1652
1653 (defun elpher-bookmarks ()
1654   "Visit bookmarks page."
1655   (interactive)
1656   (switch-to-buffer "*elpher*")
1657   (elpher-visit-page
1658    (elpher-make-page "Bookmarks Page" (elpher-make-special-address 'bookmarks))))
1659
1660 (defun elpher-info-page (page)
1661   "Display information on PAGE."
1662   (let ((display-string (elpher-page-display-string page))
1663         (address (elpher-page-address page)))
1664     (if (elpher-address-special-p address)
1665         (message "Special page: %s" display-string)
1666       (message "%s" (elpher-address-to-url address)))))
1667
1668 (defun elpher-info-link ()
1669   "Display information on page corresponding to link at point."
1670   (interactive)
1671   (let ((button (button-at (point))))
1672     (if button
1673         (elpher-info-page (button-get button 'elpher-page))
1674       (error "No item selected"))))
1675   
1676 (defun elpher-info-current ()
1677   "Display information on current page."
1678   (interactive)
1679   (elpher-info-page elpher-current-page))
1680
1681 (defun elpher-copy-page-url (page)
1682   "Copy URL representation of address of PAGE to `kill-ring'."
1683   (let ((address (elpher-page-address page)))
1684     (if (elpher-address-special-p address)
1685         (error (format "Cannot represent %s as URL" (elpher-page-display-string page)))
1686       (let ((url (elpher-address-to-url address)))
1687         (message "Copied \"%s\" to kill-ring/clipboard." url)
1688         (kill-new url)))))
1689
1690 (defun elpher-copy-link-url ()
1691   "Copy URL of item at point to `kill-ring'."
1692   (interactive)
1693   (let ((button (button-at (point))))
1694     (if button
1695         (elpher-copy-page-url (button-get button 'elpher-page))
1696       (error "No item selected"))))
1697
1698 (defun elpher-copy-current-url ()
1699   "Copy URL of current page to `kill-ring'."
1700   (interactive)
1701   (elpher-copy-page-url elpher-current-page))
1702
1703 (defun elpher-set-gopher-coding-system ()
1704   "Specify an explicit character coding system for gopher selectors."
1705   (interactive)
1706   (let ((system (read-coding-system "Set coding system to use for gopher (default is to autodetect): " nil)))
1707     (setq elpher-user-coding-system system)
1708     (if system
1709         (message "Gopher coding system fixed to %s. (Reload to see effect)." system)
1710       (message "Gopher coding system set to autodetect. (Reload to see effect)."))))
1711
1712
1713 ;;; Mode and keymap
1714 ;;
1715
1716 (defvar elpher-mode-map
1717   (let ((map (make-sparse-keymap)))
1718     (define-key map (kbd "TAB") 'elpher-next-link)
1719     (define-key map (kbd "<backtab>") 'elpher-prev-link)
1720     (define-key map (kbd "C-M-i") 'elpher-prev-link)
1721     (define-key map (kbd "u") 'elpher-back)
1722     (define-key map [mouse-3] 'elpher-back)
1723     (define-key map (kbd "O") 'elpher-root-dir)
1724     (define-key map (kbd "g") 'elpher-go)
1725     (define-key map (kbd "o") 'elpher-go-current)
1726     (define-key map (kbd "r") 'elpher-redraw)
1727     (define-key map (kbd "R") 'elpher-reload)
1728     (define-key map (kbd "T") 'elpher-toggle-tls)
1729     (define-key map (kbd ".") 'elpher-view-raw)
1730     (define-key map (kbd "d") 'elpher-download)
1731     (define-key map (kbd "D") 'elpher-download-current)
1732     (define-key map (kbd "m") 'elpher-jump)
1733     (define-key map (kbd "i") 'elpher-info-link)
1734     (define-key map (kbd "I") 'elpher-info-current)
1735     (define-key map (kbd "c") 'elpher-copy-link-url)
1736     (define-key map (kbd "C") 'elpher-copy-current-url)
1737     (define-key map (kbd "a") 'elpher-bookmark-link)
1738     (define-key map (kbd "A") 'elpher-bookmark-current)
1739     (define-key map (kbd "x") 'elpher-unbookmark-link)
1740     (define-key map (kbd "X") 'elpher-unbookmark-current)
1741     (define-key map (kbd "B") 'elpher-bookmarks)
1742     (define-key map (kbd "S") 'elpher-set-gopher-coding-system)
1743     (when (fboundp 'evil-define-key*)
1744       (evil-define-key* 'motion map
1745         (kbd "TAB") 'elpher-next-link
1746         (kbd "C-") 'elpher-follow-current-link
1747         (kbd "C-t") 'elpher-back
1748         (kbd "u") 'elpher-back
1749         [mouse-3] 'elpher-back
1750         (kbd "g") 'elpher-go
1751         (kbd "o") 'elpher-go-current
1752         (kbd "r") 'elpher-redraw
1753         (kbd "R") 'elpher-reload
1754         (kbd "T") 'elpher-toggle-tls
1755         (kbd ".") 'elpher-view-raw
1756         (kbd "d") 'elpher-download
1757         (kbd "D") 'elpher-download-current
1758         (kbd "m") 'elpher-jump
1759         (kbd "i") 'elpher-info-link
1760         (kbd "I") 'elpher-info-current
1761         (kbd "c") 'elpher-copy-link-url
1762         (kbd "C") 'elpher-copy-current-url
1763         (kbd "a") 'elpher-bookmark-link
1764         (kbd "A") 'elpher-bookmark-current
1765         (kbd "x") 'elpher-unbookmark-link
1766         (kbd "X") 'elpher-unbookmark-current
1767         (kbd "B") 'elpher-bookmarks
1768         (kbd "S") 'elpher-set-gopher-coding-system))
1769     map)
1770   "Keymap for gopher client.")
1771
1772 (define-derived-mode elpher-mode special-mode "elpher"
1773   "Major mode for elpher, an elisp gopher client.
1774
1775 This mode is automatically enabled by the interactive
1776 functions which initialize the gopher client, namely
1777 `elpher', `elpher-go' and `elpher-bookmarks'.")
1778
1779 (when (fboundp 'evil-set-initial-state)
1780   (evil-set-initial-state 'elpher-mode 'motion))
1781
1782
1783 ;;; Main start procedure
1784 ;;
1785
1786 ;;;###autoload
1787 (defun elpher ()
1788   "Start elpher with default landing page."
1789   (interactive)
1790   (if (get-buffer "*elpher*")
1791       (switch-to-buffer "*elpher*")
1792     (switch-to-buffer "*elpher*")
1793     (setq elpher-current-page nil)
1794     (let ((start-page (elpher-make-page "Elpher Start Page"
1795                                         (elpher-make-special-address 'start))))
1796       (elpher-visit-page start-page)))
1797   "Started Elpher.") ; Otherwise (elpher) evaluates to start page string.
1798
1799 ;;; elpher.el ends here