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