Merge branch 'master' into manual
[elpher.git] / elpher.el
1 ;;; elpher.el --- A friendly gopher client.
2
3 ;; Copyright (C) 2019 Tim Vaughan
4
5 ;; Author: Tim Vaughan <tgvaughan@gmail.com>
6 ;; Created: 11 April 2019
7 ;; Version: 1.4.3
8 ;; Keywords: comm gopher
9 ;; Homepage: https://github.com/tgvaughan/elpher
10 ;; Package-Requires: ((emacs "25"))
11
12 ;; This file is not part of GNU Emacs.
13
14 ;; This program is free software: you can redistribute it and/or modify
15 ;; it under the terms of the GNU General Public License as published by
16 ;; the Free Software Foundation, either version 3 of the License, or
17 ;; (at your option) any later version.
18
19 ;; This program is distributed in the hope that it will be useful,
20 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
21 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
22 ;; GNU General Public License for more details.
23
24 ;; You should have received a copy of the GNU General Public License
25 ;; along with this file.  If not, see <http://www.gnu.org/licenses/>.
26
27 ;;; Commentary:
28
29 ;; Elpher aims to provide a practical and friendly gopher client
30 ;; for GNU Emacs.  It supports:
31
32 ;; - an intuitive keyboard and mouse-driven interface,
33 ;; - caching of visited sites (both content and cursor position),
34 ;; - pleasant and configurable colouring of Gopher directories,
35 ;; - direct visualisation of image files,
36 ;; - (m)enu key support, similar to Emacs' info browser,
37 ;; - clickable web and gopher links in plain text,
38 ;; - a simple bookmark management system,
39 ;; - support for TLS gopherholes.
40
41 ;; To launch Elpher, simply use 'M-x elpher'.  This will open a start
42 ;; page containing information on key bindings and suggested starting
43 ;; points for your gopher exploration.
44
45 ;; Faces, caching and other options can be configured via
46 ;; the Elpher customization group in Applications.
47
48 ;; Elpher is under active development, and any suggestions for
49 ;; improvements are welcome!
50
51 ;;; Code:
52
53 (provide 'elpher)
54 (require 'seq)
55 (require 'pp)
56 (require 'shr)
57
58 ;;; Global constants
59 ;;
60
61 (defconst elpher-version "1.4.3"
62   "Current version of elpher.")
63
64 (defconst elpher-margin-width 6
65   "Width of left-hand margin used when rendering indicies.")
66
67 (defconst elpher-start-index
68   (mapconcat
69    'identity
70    (list "i\tfake\tfake\t1"
71          "i     --------------------------------------------\tfake\tfake\t1"
72          "i                Elpher Gopher Client             \tfake\tfake\t1"
73          (format "i                   version %s\tfake\tfake\t1" elpher-version)
74          "i     --------------------------------------------\tfake\tfake\t1"
75          "i\tfake\tfake\t1"
76          "iUsage:\tfake\tfake\t1"
77          "i\tfake\tfake\t1"
78          "i - tab/shift-tab: next/prev item on current page\tfake\tfake\t1"
79          "i - RET/mouse-1: open item under cursor\tfake\tfake\t1"
80          "i - m: select an item on current page by name (autocompletes)\tfake\tfake\t1"
81          "i - u: return to parent\tfake\tfake\t1"
82          "i - O: visit the root menu of the current server\tfake\tfake\t1"
83          "i - g: go to a particular gopher address\tfake\tfake\t1"
84          "i - i/I: info on item under cursor or current page\tfake\tfake\t1"
85          "i - c/C: copy URL representation of item under cursor or current page\tfake\tfake\t1"
86          "i - a/A: bookmark the item under cursor or current page\tfake\tfake\t1"
87          "i - x/X: remove bookmark for item under cursor or current page\tfake\tfake\t1"
88          "i - B: visit the bookmarks page\tfake\tfake\t1"
89          "i - r: redraw current page (using cached contents if available)\tfake\tfake\t1"
90          "i - R: reload current page (regenerates cache)\tfake\tfake\t1"
91          "i - T: toggle TLS mode\tfake\tfake\t1"
92          "i - d/D: download item under cursor or current page\tfake\tfake\t1"
93          "i - w: display the raw server response for the current page\tfake\tfake\t1"
94          "i - S: set an explicit character coding system (default is to autodetect)\tfake\tfake\t1"
95          "i\tfake\tfake\t1"
96          "iWhere to start exploring Gopherspace:\tfake\tfake\t1"
97          "i\tfake\tfake\t1"
98          "1Floodgap Systems Gopher Server\t/\tgopher.floodgap.com\t70"
99          "i\tfake\tfake\t1"
100          "iAlternatively, select the following item and enter some\tfake\tfake\t1"
101          "isearch terms:\tfake\tfake\t1"
102          "i\tfake\tfake\t1"
103          "7Veronica-2 Gopher Search Engine\t/v2/vs\tgopher.floodgap.com\t70"
104          ".\r\n")
105    "\r\n")
106   "Source for elpher start page.")
107
108 (defconst elpher-type-map
109   '((?0 elpher-get-text-node "txt" elpher-text)
110     (?1 elpher-get-index-node "/" elpher-index)
111     (?4 elpher-get-node-download "bin" elpher-binary)
112     (?5 elpher-get-node-download "bin" elpher-binary)
113     (?7 elpher-get-search-node "?" elpher-search)
114     (?8 elpher-get-telnet-node "tel" elpher-telnet)
115     (?9 elpher-get-node-download "bin" elpher-binary)
116     (?g elpher-get-image-node "img" elpher-image)
117     (?p elpher-get-image-node "img" elpher-image)
118     (?I elpher-get-image-node "img" elpher-image)
119     (?d elpher-get-node-download "doc" elpher-binary)
120     (?h elpher-get-url-node "web" elpher-url)
121     (bookmarks elpher-get-bookmarks-node "#" elpher-index)
122     (start elpher-get-start-node "#" elpher-index))
123   "Association list from types to getters, margin codes and index faces.")
124
125
126 ;;; Customization group
127 ;;
128
129 (defgroup elpher nil
130   "A gopher client."
131   :group 'applications)
132
133 ;; Face customizations
134
135 (defface elpher-index
136   '((t :inherit font-lock-keyword-face))
137   "Face used for directory type directory records.")
138
139 (defface elpher-text
140   '((t :inherit bold))
141   "Face used for text type directory records.")
142
143 (defface elpher-info
144   '((t :inherit default))
145   "Face used for info type directory records.")
146
147 (defface elpher-image
148   '((t :inherit font-lock-string-face))
149   "Face used for image type directory records.")
150
151 (defface elpher-search
152   '((t :inherit warning))
153   "Face used for search type directory records.")
154
155 (defface elpher-url
156   '((t :inherit font-lock-comment-face))
157   "Face used for url type directory records.")
158
159 (defface elpher-telnet
160   '((t :inherit font-lock-function-name-face))
161   "Face used for telnet type directory records.")
162
163 (defface elpher-binary
164   '((t :inherit font-lock-doc-face))
165   "Face used for binary type directory records.")
166
167 (defface elpher-unknown
168   '((t :inherit error))
169   "Face used for directory records with unknown/unsupported types.")
170
171 (defface elpher-margin-key
172   '((t :inherit bold))
173   "Face used for directory margin key.")
174
175 (defface elpher-margin-brackets
176   '((t :inherit shadow))
177   "Face used for brackets around directory margin key.")
178
179 ;; Other customizations
180
181 (defcustom elpher-open-urls-with-eww nil
182   "If non-nil, open URL selectors using eww.
183 Otherwise, use the system browser via the BROWSE-URL function."
184   :type '(boolean))
185
186 (defcustom elpher-buttonify-urls-in-directories nil
187   "If non-nil, turns URLs matched in directories into clickable buttons."
188   :type '(boolean))
189
190 (defcustom elpher-cache-images nil
191   "If non-nil, cache images in memory in the same way as other content."
192   :type '(boolean))
193
194 (defcustom elpher-use-header t
195   "If non-nil, display current node information in buffer header."
196   :type '(boolean))
197
198 (defcustom elpher-auto-disengage-TLS nil
199   "If non-nil, automatically disengage TLS following an unsuccessful connection.
200 While enabling this may seem convenient, it is also potentially dangerous as it
201 allows switching from an encrypted channel back to plain text without user input."
202   :type '(boolean))
203
204
205 ;;; Model
206 ;;
207
208 ;; Address
209
210 (defun elpher-make-address (type &optional selector host port use-tls)
211   "Create an address of a gopher object with TYPE, SELECTOR, HOST and PORT.
212 Although selector host and port are optional, they are only omitted for
213 special address types, such as 'start for the start page.
214
215 Setting the USE-TLS parameter to non-nil causes Elpher to engage TLS mode
216 before attempting to connect to the server."
217   (if use-tls
218       (list type selector host port 'tls)
219     (list type selector host port)))
220
221 (defun elpher-address-type (address)
222   "Retrieve type from ADDRESS."
223   (elt address 0))
224
225 (defun elpher-address-selector (address)
226   "Retrieve selector from ADDRESS."
227   (elt address 1))
228
229 (defun elpher-address-host (address)
230   "Retrieve host from ADDRESS."
231   (elt address 2))
232
233 (defun elpher-address-port (address)
234   "Retrieve port from ADDRESS."
235   (elt address 3))
236
237 (defun elpher-address-use-tls-p (address)
238   "Return non-nil if ADDRESS is marked as needing TLS."
239   (elt address 4))
240
241 (defun elpher-address-special-p (address)
242   "Return non-nil if ADDRESS is special (e.g. start page, bookmarks page)."
243   (not (elpher-address-host address)))
244
245 ;; Node
246
247 (defun elpher-make-node (display-string address &optional parent)
248   "Create a node in the gopher page hierarchy.
249
250 DISPLAY-STRING records the display string used for the page.
251
252 ADDRESS specifies the address of the gopher page.
253
254 The optional PARENT specifies the parent node in the hierarchy.
255 This is set every time the node is visited, so while it forms
256 an important part of the node data there is no need to set it
257 initially."
258   (list display-string address parent))
259
260 (defun elpher-node-display-string (node)
261   "Retrieve the display string of NODE."
262   (elt node 0))
263
264 (defun elpher-node-address (node)
265   "Retrieve the address of NODE."
266   (elt node 1))
267
268 (defun elpher-node-parent (node)
269   "Retrieve the parent node of NODE."
270   (elt node 2))
271
272 (defun elpher-set-node-parent (node parent)
273   "Set the parent node of NODE to be PARENT."
274   (setcar (cdr (cdr node)) parent))
275
276 ;; Cache
277
278 (defvar elpher-content-cache (make-hash-table :test 'equal))
279 (defvar elpher-pos-cache (make-hash-table :test 'equal))
280
281 (defun elpher-get-cached-content (address)
282   "Retrieve the cached content for ADDRESS, or nil if none exists."
283   (gethash address elpher-content-cache))
284
285 (defun elpher-cache-content (address content)
286   "Set the content cache for ADDRESS to CONTENT."
287   (puthash address content elpher-content-cache))
288
289 (defun elpher-get-cached-pos (address)
290   "Retrieve the cached cursor position for ADDRESS, or nil if none exists."
291   (gethash address elpher-pos-cache))
292
293 (defun elpher-cache-pos (address pos)
294   "Set the cursor position cache for ADDRESS to POS."
295   (puthash address pos elpher-pos-cache))
296
297 ;; Node graph traversal
298
299 (defvar elpher-current-node nil)
300
301 (defun elpher-visit-node (node &optional getter preserve-parent)
302   "Visit NODE using its own getter or GETTER, if non-nil.
303 Additionally, set the parent of NODE to `elpher-current-node',
304 unless PRESERVE-PARENT is non-nil."
305   (elpher-save-pos)
306   (elpher-process-cleanup)
307   (unless preserve-parent
308     (if (and (elpher-node-parent elpher-current-node)
309              (equal (elpher-node-address elpher-current-node)
310                     (elpher-node-address node)))
311         (elpher-set-node-parent node (elpher-node-parent elpher-current-node))
312       (elpher-set-node-parent node elpher-current-node)))
313   (setq elpher-current-node node)
314   (if getter
315       (funcall getter)
316     (let* ((address (elpher-node-address node))
317            (type (elpher-address-type address)))
318       (funcall (car (alist-get type elpher-type-map))))))
319
320 (defun elpher-visit-parent-node ()
321   "Visit the parent of the current node."
322   (let ((parent-node (elpher-node-parent elpher-current-node)))
323     (when parent-node
324       (elpher-visit-node parent-node nil t))))
325       
326 (defun elpher-reload-current-node ()
327   "Reload the current node, discarding any existing cached content."
328   (elpher-cache-content (elpher-node-address elpher-current-node) nil)
329   (elpher-visit-node elpher-current-node))
330
331 (defun elpher-save-pos ()
332   "Save the current position of point to the current node."
333   (when elpher-current-node
334     (elpher-cache-pos (elpher-node-address elpher-current-node) (point))))
335
336 (defun elpher-restore-pos ()
337   "Restore the position of point to that cached in the current node."
338   (let ((pos (elpher-get-cached-pos (elpher-node-address elpher-current-node))))
339     (if pos
340         (goto-char pos)
341       (goto-char (point-min)))))
342
343
344 ;;; Buffer preparation
345 ;;
346
347 (defun elpher-update-header ()
348   "If `elpher-use-header' is true, display current node info in window header."
349   (if elpher-use-header
350       (setq header-line-format (elpher-node-display-string elpher-current-node))))
351
352 (defmacro elpher-with-clean-buffer (&rest args)
353   "Evaluate ARGS with a clean *elpher* buffer as current."
354   (list 'with-current-buffer "*elpher*"
355         '(elpher-mode)
356         (append (list 'let '((inhibit-read-only t))
357                       '(erase-buffer)
358                       '(elpher-update-header))
359                 args)))
360
361
362 ;;; Text Processing
363 ;;
364
365 (defvar elpher-user-coding-system nil
366   "User-specified coding system to use for decoding text responses.")
367
368 (defun elpher-decode (string)
369   "Decode STRING using autodetected or user-specified coding system."
370   (decode-coding-string string
371                         (if elpher-user-coding-system
372                             elpher-user-coding-system
373                           (detect-coding-string string t))))
374
375 (defun elpher-preprocess-text-response (string)
376   "Preprocess text selector response contained in STRING.
377 This involes decoding the character representation, and clearing
378 away CRs and any terminating period."
379   (elpher-decode (replace-regexp-in-string "\n\.\n$" "\n"
380                                            (replace-regexp-in-string "\r" "" string))))
381
382 ;;; Index rendering
383 ;;
384
385 (defun elpher-insert-index (string)
386   "Insert the index corresponding to STRING into the current buffer."
387   ;; Should be able to split directly on CRLF, but some non-conformant
388   ;; LF-only servers sadly exist, hence the following.
389   (let ((str-processed (elpher-preprocess-text-response string)))
390     (dolist (line (split-string str-processed "\n"))
391       (unless (= (length line) 0)
392         (let* ((type (elt line 0))
393                (fields (split-string (substring line 1) "\t"))
394                (display-string (elt fields 0))
395                (selector (elt fields 1))
396                (host (elt fields 2))
397                (port (if (elt fields 3)
398                          (string-to-number (elt fields 3))
399                        nil))
400                (address (elpher-make-address type selector host port)))
401           (elpher-insert-index-record display-string address))))))
402
403 (defun elpher-insert-margin (&optional type-name)
404   "Insert index margin, optionally containing the TYPE-NAME, into the current buffer."
405   (if type-name
406       (progn
407         (insert (format (concat "%" (number-to-string (- elpher-margin-width 1)) "s")
408                         (concat
409                          (propertize "[" 'face 'elpher-margin-brackets)
410                          (propertize type-name 'face 'elpher-margin-key)
411                          (propertize "]" 'face 'elpher-margin-brackets))))
412         (insert " "))
413     (insert (make-string elpher-margin-width ?\s))))
414
415 (defun elpher-node-button-help (node)
416   "Return a string containing the help text for a button corresponding to NODE."
417   (let ((address (elpher-node-address node)))
418     (if (eq (elpher-address-type address) ?h)
419         (let ((url (cadr (split-string (elpher-address-selector address) "URL:"))))
420           (format "mouse-1, RET: open url '%s'" url))
421       (format "mouse-1, RET: open '%s' on %s port %s"
422               (elpher-address-selector address)
423               (elpher-address-host address)
424               (elpher-address-port address)))))
425
426
427 (defun elpher-insert-index-record (display-string address)
428   "Function to insert an index record into the current buffer.
429 The contents of the record are dictated by DISPLAY-STRING and ADDRESS."
430   (let* ((type (elpher-address-type address))
431          (type-map-entry (alist-get type elpher-type-map)))
432     (if type-map-entry
433         (let* ((margin-code (elt type-map-entry 1))
434                (face (elt type-map-entry 2))
435                (node (elpher-make-node display-string address)))
436           (elpher-insert-margin margin-code)
437           (insert-text-button display-string
438                               'face face
439                               'elpher-node node
440                               'action #'elpher-click-link
441                               'follow-link t
442                               'help-echo (elpher-node-button-help node)))
443       (pcase type
444         (?i ;; Information
445          (elpher-insert-margin)
446          (insert (propertize
447                   (if elpher-buttonify-urls-in-directories
448                       (elpher-buttonify-urls display-string)
449                     display-string)
450                   'face 'elpher-info)))
451         (other ;; Unknown
452          (elpher-insert-margin (concat (char-to-string type) "?"))
453          (insert (propertize display-string
454                              'face 'elpher-unknown)))))
455     (insert "\n")))
456
457 (defun elpher-click-link (button)
458   "Function called when the gopher link BUTTON is activated (via mouse or keypress)."
459   (let ((node (button-get button 'elpher-node)))
460     (elpher-visit-node node)))
461
462
463 ;;; Selector retrieval (all kinds)
464 ;;
465
466 (defun elpher-process-cleanup ()
467   "Immediately shut down any extant elpher process."
468   (let ((p (get-process "elpher-process")))
469     (if p (delete-process p))))
470
471 (defvar elpher-use-tls nil
472   "If non-nil, use TLS to communicate with gopher servers.")
473
474 (defvar elpher-selector-string)
475
476 (defun elpher-get-selector (address after &optional propagate-error)
477   "Retrieve selector specified by ADDRESS, then execute AFTER.
478 The result is stored as a string in the variable â€˜elpher-selector-string’.
479
480 Usually errors result in an error page being displayed.  This is only
481 appropriate if the selector is to be directly viewed.  If PROPAGATE-ERROR
482 is non-nil, this message is not displayed.  Instead, the error propagates
483 up to the calling function."
484   (setq elpher-selector-string "")
485   (when (elpher-address-use-tls-p address)
486       (if (gnutls-available-p)
487           (when (not elpher-use-tls)
488             (setq elpher-use-tls t)
489             (message "Engaging TLS mode."))
490         (error "Cannot retrieve TLS selector: GnuTLS not available")))
491   (condition-case the-error
492       (let* ((kill-buffer-query-functions nil)
493              (proc (open-network-stream "elpher-process"
494                                        nil
495                                        (elpher-address-host address)
496                                        (elpher-address-port address)
497                                        :type (if elpher-use-tls 'tls 'plain))))
498         (set-process-coding-system proc 'binary)
499         (set-process-filter proc
500                             (lambda (proc string)
501                               (setq elpher-selector-string
502                                     (concat elpher-selector-string string))))
503         (set-process-sentinel proc after)
504         (process-send-string proc
505                              (concat (elpher-address-selector address) "\n")))
506     (error
507      (if (and (consp the-error)
508               (eq (car the-error) 'gnutls-error)
509               (not (elpher-address-use-tls-p address))
510               (or elpher-auto-disengage-TLS
511                   (yes-or-no-p "Could not establish encrypted connection.  Disable TLS mode? ")))
512          (progn
513            (message "Disengaging TLS mode.")
514            (setq elpher-use-tls nil)
515            (elpher-get-selector address after))
516        (elpher-process-cleanup)
517        (if propagate-error
518            (error the-error)
519          (elpher-with-clean-buffer
520           (insert (propertize "\n---- ERROR -----\n\n" 'face 'error)
521                   "Failed to connect to " (elpher-get-address-url address) ".\n"
522                   (propertize "\n----------------\n\n" 'face 'error)
523                   "Press 'u' to return to the previous page.")))))))
524
525 ;; Index retrieval
526
527 (defun elpher-get-index-node ()
528   "Getter which retrieves the current node contents as an index."
529   (let* ((address (elpher-node-address elpher-current-node))
530          (content (elpher-get-cached-content address)))
531     (if content
532         (progn
533           (elpher-with-clean-buffer
534            (insert content)
535            (elpher-restore-pos)))
536       (elpher-with-clean-buffer
537        (insert "LOADING DIRECTORY... (use 'u' to cancel)"))
538       (elpher-get-selector address
539                            (lambda (proc event)
540                              (unless (string-prefix-p "deleted" event)
541                                (elpher-with-clean-buffer
542                                 (elpher-insert-index elpher-selector-string)
543                                 (elpher-restore-pos)
544                                 (elpher-cache-content
545                                  (elpher-node-address elpher-current-node)
546                                  (buffer-string)))))))))
547
548 ;; Text retrieval
549
550 (defconst elpher-url-regex
551   "\\([a-zA-Z]+\\)://\\([a-zA-Z0-9.\-]+\\)\\(?3::[0-9]+\\)?\\(?4:/[^ \r\n\t(),]*\\)?"
552   "Regexp used to locate and buttinofy URLs in text files loaded by elpher.")
553
554 (defun elpher-make-node-from-matched-url (&optional string)
555   "Convert most recent `elpher-url-regex' match to a node.
556
557 If STRING is non-nil, this is given as an argument to all `match-string'
558 calls, as is necessary if the match is performed by `string-match'."
559   (let ((url (match-string 0 string))
560         (protocol (downcase (match-string 1 string))))
561     (if (or (string= protocol "gopher")
562             (string= protocol "gophers"))
563         (let* ((host (match-string 2 string))
564                (port (if (> (length (match-string 3 string))  1)
565                          (string-to-number (substring (match-string 3 string) 1))
566                        70))
567                (type-and-selector (match-string 4 string))
568                (type (if (> (length type-and-selector) 1)
569                          (elt type-and-selector 1)
570                        ?1))
571                (selector (if (> (length type-and-selector) 1)
572                              (substring type-and-selector 2)
573                            ""))
574                (use-tls (string= protocol "gophers"))
575                (address (elpher-make-address type selector host port use-tls)))
576           (elpher-make-node url address))
577       (let* ((host (match-string 2 string))
578              (port (if (> (length (match-string 3 string)) 1)
579                        (string-to-number (substring (match-string 3 string) 1))
580                      70))
581              (selector (concat "URL:" url))
582              (address (elpher-make-address ?h selector host port)))
583         (elpher-make-node url address)))))
584
585
586 (defun elpher-buttonify-urls (string)
587   "Turn substrings which look like urls in STRING into clickable buttons."
588   (with-temp-buffer
589     (insert string)
590     (goto-char (point-min))
591     (while (re-search-forward elpher-url-regex nil t)
592         (let ((node (elpher-make-node-from-matched-url)))
593           (make-text-button (match-beginning 0)
594                             (match-end 0)
595                             'elpher-node  node
596                             'action #'elpher-click-link
597                             'follow-link t
598                             'help-echo (elpher-node-button-help node))))
599     (buffer-string)))
600
601 (defun elpher-get-text-node ()
602   "Getter which retrieves the current node contents as a text document."
603   (let* ((address (elpher-node-address elpher-current-node))
604          (content (elpher-get-cached-content address)))
605     (if content
606         (progn
607           (elpher-with-clean-buffer
608            (insert content)
609            (elpher-restore-pos)))
610       (progn
611         (elpher-with-clean-buffer
612          (insert "LOADING TEXT... (use 'u' to cancel)"))
613         (elpher-get-selector address
614                               (lambda (proc event)
615                                 (unless (string-prefix-p "deleted" event)
616                                   (elpher-with-clean-buffer
617                                    (insert (elpher-buttonify-urls
618                                             (elpher-preprocess-text-response
619                                              elpher-selector-string)))
620                                    (elpher-restore-pos)
621                                    (elpher-cache-content
622                                     (elpher-node-address elpher-current-node)
623                                     (buffer-string))))))))))
624
625 ;; Image retrieval
626
627 (defun elpher-get-image-node ()
628   "Getter which retrieves the current node contents as an image to view."
629   (let* ((address (elpher-node-address elpher-current-node))
630          (content (elpher-get-cached-content address)))
631     (if content
632         (progn
633           (elpher-with-clean-buffer
634            (insert-image content)
635            (elpher-restore-pos)))
636       (if (display-images-p)
637           (progn
638             (elpher-with-clean-buffer
639              (insert "LOADING IMAGE... (use 'u' to cancel)"))
640             (elpher-get-selector address
641                                  (lambda (proc event)
642                                    (unless (string-prefix-p "deleted" event)
643                                      (let ((image (create-image
644                                                    elpher-selector-string
645                                                    nil t)))
646                                        (elpher-with-clean-buffer
647                                         (insert-image image)
648                                         (elpher-restore-pos))
649                                        (if elpher-cache-images
650                                            (elpher-cache-content
651                                             (elpher-node-address elpher-current-node)
652                                             image)))))))
653         (elpher-get-node-download)))))
654
655 ;; Search retrieval
656
657 (defun elpher-get-search-node ()
658   "Getter which submits a search query to the address of the current node."
659   (let* ((address (elpher-node-address elpher-current-node))
660          (content (elpher-get-cached-content address))
661          (aborted t))
662     (if content
663         (progn
664           (elpher-with-clean-buffer
665            (insert content)
666            (elpher-restore-pos))
667           (message "Displaying cached search results.  Reload to perform a new search."))
668       (unwind-protect
669           (let* ((query-string (read-string "Query: "))
670                  (query-selector (concat (elpher-address-selector address) "\t" query-string))
671                  (search-address (elpher-make-address ?1
672                                                       query-selector
673                                                       (elpher-address-host address)
674                                                       (elpher-address-port address))))
675             (setq aborted nil)
676             (elpher-with-clean-buffer
677              (insert "LOADING RESULTS... (use 'u' to cancel)"))
678             (elpher-get-selector search-address
679                                   (lambda (proc event)
680                                     (unless (string-prefix-p "deleted" event)
681                                       (elpher-with-clean-buffer
682                                        (elpher-insert-index elpher-selector-string))
683                                       (goto-char (point-min))
684                                       (elpher-cache-content
685                                        (elpher-node-address elpher-current-node)
686                                        (buffer-string))))))
687         (if aborted
688             (elpher-visit-parent-node))))))
689
690 ;; Raw server response retrieval
691
692 (defun elpher-get-node-raw ()
693   "Getter which retrieves the raw server response for the current node."
694   (let ((address (elpher-node-address elpher-current-node)))
695     (elpher-with-clean-buffer
696      (insert "LOADING RAW SERVER RESPONSE... (use 'u' to cancel)"))
697     (if address
698         (elpher-get-selector address
699                               (lambda (proc event)
700                                 (unless (string-prefix-p "deleted" event)
701                                   (elpher-with-clean-buffer
702                                    (insert elpher-selector-string)
703                                    (goto-char (point-min))))))
704       (progn
705         (elpher-with-clean-buffer
706          (insert elpher-start-index))
707         (goto-char (point-min)))))
708   (message "Displaying raw server response.  Reload or redraw to return to standard view."))
709  
710 ;; File export retrieval
711
712 (defvar elpher-download-filename)
713
714 (defun elpher-get-node-download ()
715   "Getter which retrieves the current node and writes the result to a file."
716   (let* ((address (elpher-node-address elpher-current-node))
717          (selector (elpher-address-selector address)))
718     (elpher-visit-parent-node) ; Do first in case of non-local exits.
719     (let* ((filename-proposal (file-name-nondirectory selector))
720            (filename (read-file-name "Save file as: "
721                                      nil nil nil
722                                      (if (> (length filename-proposal) 0)
723                                          filename-proposal
724                                        "gopher.file"))))
725       (message "Downloading...")
726       (setq elpher-download-filename filename)
727       (condition-case the-error
728           (elpher-get-selector address
729                                (lambda (proc event)
730                                  (let ((coding-system-for-write 'binary))
731                                    (with-temp-file elpher-download-filename
732                                      (insert elpher-selector-string)
733                                      (message (format "Download complate, saved to file %s."
734                                                       elpher-download-filename)))))
735                                t)
736         (error
737          (error "Error downloading %s" elpher-download-filename))))))
738
739 ;; URL retrieval
740
741 (defun elpher-insert-rendered-html (string)
742   "Use shr to insert rendered view of html STRING into current buffer."
743   (let ((dom (with-temp-buffer
744                (insert string)
745                (libxml-parse-html-region (point-min) (point-max)))))
746     (shr-insert-document dom)))
747
748 (defun elpher-get-url-node ()
749   "Getter which attempts to open the URL specified by the current node."
750   (let* ((address (elpher-node-address elpher-current-node))
751          (selector (elpher-address-selector address)))
752     (let ((url (elt (split-string selector "URL:") 1)))
753       (if url
754           (progn
755             (elpher-visit-parent-node) ; Do first in case of non-local exits.
756             (message "Opening URL...")
757             (if elpher-open-urls-with-eww
758                 (browse-web url)
759               (browse-url url)))
760         (let ((content (elpher-get-cached-content address)))
761           (if content
762               (progn
763                 (elpher-with-clean-buffer
764                  (insert content)
765                  (elpher-restore-pos)))
766             (elpher-with-clean-buffer
767              (insert "LOADING HTML... (use 'u' to cancel)"))
768             (elpher-get-selector address
769                                  (lambda (proc event)
770                                    (unless (string-prefix-p "deleted" event)
771                                      (elpher-with-clean-buffer
772                                       (elpher-insert-rendered-html elpher-selector-string)
773                                       (goto-char (point-min))
774                                       (elpher-cache-content
775                                        (elpher-node-address elpher-current-node)
776                                        (buffer-string))))))))))))
777
778 ;; Telnet node connection
779
780 (defun elpher-get-telnet-node ()
781   "Getter which opens a telnet connection to the server specified by the current node."
782   (let* ((address (elpher-node-address elpher-current-node))
783          (host (elpher-address-host address))
784          (port (elpher-address-port address)))
785     (elpher-visit-parent-node)
786     (telnet host port)))
787
788 ;; Start page node retrieval
789
790 (defun elpher-get-start-node ()
791   "Getter which displays the start page."
792   (elpher-with-clean-buffer
793    (elpher-insert-index elpher-start-index)
794    (elpher-restore-pos)))
795
796 ;; Bookmarks page node retrieval
797
798 (defun elpher-get-bookmarks-node ()
799   "Getter to load and display the current bookmark list."
800   (elpher-with-clean-buffer
801    (insert "---- Bookmark list ----\n\n")
802    (let ((bookmarks (elpher-load-bookmarks)))
803      (if bookmarks
804          (dolist (bookmark bookmarks)
805            (let ((display-string (elpher-bookmark-display-string bookmark))
806                  (address (elpher-bookmark-address bookmark)))
807              (elpher-insert-index-record display-string address)))
808        (insert "No bookmarks found.\n")))
809    (insert "\n-----------------------\n\n"
810            "- u: return to previous page\n"
811            "- x: delete selected bookmark\n"
812            "- a: rename selected bookmark\n\n"
813            "Bookmarks are stored in the file "
814            (locate-user-emacs-file "elpher-bookmarks"))
815    (elpher-restore-pos)))
816   
817
818 ;;; Bookmarks
819 ;;
820
821 (defun elpher-make-bookmark (display-string address)
822   "Make an elpher bookmark.
823 DISPLAY-STRING determines how the bookmark will appear in the
824 bookmark list, while ADDRESS is the address of the entry."
825   (list display-string address))
826   
827 (defun elpher-bookmark-display-string (bookmark)
828   "Get the display string of BOOKMARK."
829   (elt bookmark 0))
830
831 (defun elpher-set-bookmark-display-string (bookmark display-string)
832   "Set the display string of BOOKMARK to DISPLAY-STRING."
833   (setcar bookmark display-string))
834
835 (defun elpher-bookmark-address (bookmark)
836   "Get the address for BOOKMARK."
837   (elt bookmark 1))
838
839 (defun elpher-save-bookmarks (bookmarks)
840   "Record the bookmark list BOOKMARKS to the user's bookmark file.
841 Beware that this completely replaces the existing contents of the file."
842   (with-temp-file (locate-user-emacs-file "elpher-bookmarks")
843     (erase-buffer)
844     (insert "; Elpher gopher bookmarks file\n\n"
845             "; Bookmarks are stored as a list of (label (type selector host port))\n"
846             "; s-expressions, where type is stored as a character (i.e. 49 = ?1).\n"
847             "; Feel free to edit by hand, but ensure this structure remains intact.\n\n")
848     (pp bookmarks (current-buffer))))
849
850 (defun elpher-load-bookmarks ()
851   "Get the list of bookmarks from the users's bookmark file."
852   (with-temp-buffer
853     (ignore-errors
854       (insert-file-contents (locate-user-emacs-file "elpher-bookmarks"))
855       (goto-char (point-min))
856       (read (current-buffer)))))
857
858 (defun elpher-add-address-bookmark (address display-string)
859   "Save a bookmark for ADDRESS with label DISPLAY-STRING.
860 If ADDRESS is already bookmarked, update the label only."
861   (let ((bookmarks (elpher-load-bookmarks)))
862     (let ((existing-bookmark (rassoc (list address) bookmarks)))
863       (if existing-bookmark
864           (elpher-set-bookmark-display-string existing-bookmark display-string)
865         (add-to-list 'bookmarks (elpher-make-bookmark display-string address))))
866     (elpher-save-bookmarks bookmarks)))
867
868 (defun elpher-remove-address-bookmark (address)
869   "Remove any bookmark to ADDRESS."
870     (elpher-save-bookmarks
871      (seq-filter (lambda (bookmark)
872                    (not (equal (elpher-bookmark-address bookmark) address)))
873                  (elpher-load-bookmarks))))
874
875 ;;; Interactive procedures
876 ;;
877
878 (defun elpher-next-link ()
879   "Move point to the next link on the current page."
880   (interactive)
881   (forward-button 1))
882
883 (defun elpher-prev-link ()
884   "Move point to the previous link on the current page."
885   (interactive)
886   (backward-button 1))
887
888 (defun elpher-follow-current-link ()
889   "Open the link or url at point."
890   (interactive)
891   (push-button))
892
893 (defun elpher-go ()
894   "Go to a particular gopher site read from the minibuffer.
895 The site may be specified via a URL or explicitly in terms of
896 host, selector and port."
897   (interactive)
898   (let ((node
899          (let ((host-or-url (read-string "Gopher host or URL: ")))
900            (if (string-match elpher-url-regex host-or-url)
901                (elpher-make-node-from-matched-url host-or-url)
902              (let ((selector (read-string "Selector (default none): " nil nil ""))
903                    (port-string (read-string "Port (default 70): " nil nil "70")))
904                (elpher-make-node (concat "gopher://" host-or-url
905                                          ":" port-string
906                                          "/1" selector)
907                                  (elpher-make-address ?1 selector host-or-url
908                                                       (string-to-number port-string))))))))
909     (switch-to-buffer "*elpher*")
910     (elpher-visit-node node)))
911
912 (defun elpher-redraw ()
913   "Redraw current page."
914   (interactive)
915   (if elpher-current-node
916       (elpher-visit-node elpher-current-node)
917     (message "No current site.")))
918
919 (defun elpher-reload ()
920   "Reload current page."
921   (interactive)
922   (if elpher-current-node
923       (elpher-reload-current-node)
924     (message "No current site.")))
925
926 (defun elpher-toggle-tls ()
927   "Toggle TLS encryption mode."
928   (interactive)
929   (setq elpher-use-tls (not elpher-use-tls))
930   (if elpher-use-tls
931       (if (gnutls-available-p)
932           (message "TLS mode enabled.  (Will not affect current page until reload.)")
933         (setq elpher-use-tls nil)
934         (error "Cannot enable TLS mode: GnuTLS not available"))
935     (message "TLS mode disabled.  (Will not affect current page until reload.)")))
936
937 (defun elpher-view-raw ()
938   "View raw server response for current page."
939   (interactive)
940   (if elpher-current-node
941       (if (elpher-address-special-p (elpher-node-address elpher-current-node))
942           (error "This page was not generated by a server")
943         (elpher-visit-node elpher-current-node
944                            #'elpher-get-node-raw))
945     (message "No current site.")))
946
947 (defun elpher-back ()
948   "Go to previous site."
949   (interactive)
950   (if (elpher-node-parent elpher-current-node)
951       (elpher-visit-parent-node)
952     (error "No previous site")))
953
954 (defun elpher-download ()
955   "Download the link at point."
956   (interactive)
957   (let ((button (button-at (point))))
958     (if button
959         (let ((node (button-get button 'elpher-node)))
960           (if (elpher-address-special-p (elpher-node-address node))
961               (error "Cannot download this link")
962             (elpher-visit-node (button-get button 'elpher-node)
963                                #'elpher-get-node-download)))
964       (error "No link selected"))))
965
966 (defun elpher-download-current ()
967   "Download the current page."
968   (interactive)
969   (if (elpher-address-special-p (elpher-node-address elpher-current-node))
970       (error "Cannot download this page")
971     (elpher-visit-node (elpher-make-node
972                         (elpher-node-display-string elpher-current-node)
973                         (elpher-node-address elpher-current-node)
974                         elpher-current-node)
975                        #'elpher-get-node-download
976                        t)))
977
978 (defun elpher-build-link-map ()
979   "Build alist mapping link names to destination nodes in current buffer."
980   (let ((link-map nil)
981         (b (next-button (point-min) t)))
982     (while b
983       (add-to-list 'link-map (cons (button-label b) b))
984       (setq b (next-button (button-start b))))
985     link-map))
986
987 (defun elpher-jump ()
988   "Select a directory entry by name.  Similar to the info browser (m)enu command."
989   (interactive)
990   (let* ((link-map (elpher-build-link-map)))
991     (if link-map
992         (let ((key (let ((completion-ignore-case t))
993                      (completing-read "Directory item/link: "
994                                       link-map nil t))))
995           (if (and key (> (length key) 0))
996               (let ((b (cdr (assoc key link-map))))
997                 (goto-char (button-start b))
998                 (button-activate b)))))))
999
1000 (defun elpher-root-dir ()
1001   "Visit root of current server."
1002   (interactive)
1003   (let* ((address (elpher-node-address elpher-current-node))
1004          (host (elpher-address-host address)))
1005     (if host
1006         (let ((host (elpher-address-host address))
1007               (selector (elpher-address-selector address))
1008               (port (elpher-address-port address)))
1009           (if (> (length selector) 0)
1010               (let ((root-address (elpher-make-address ?1 "" host port)))
1011                 (elpher-visit-node
1012                  (elpher-make-node (concat "gopher://" host
1013                                            ":" (number-to-string port)
1014                                            "/1/")
1015                                    root-address)))
1016             (error "Already at root directory of current server")))
1017       (error "Command invalid for this page"))))
1018
1019 (defun elpher-bookmarks-current-p ()
1020   "Return non-nil if current node is a bookmarks page."
1021   (eq (elpher-address-type (elpher-node-address elpher-current-node)) 'bookmarks))
1022
1023 (defun elpher-reload-bookmarks ()
1024   "Reload bookmarks if current node is a bookmarks page."
1025   (if (elpher-bookmarks-current-p)
1026       (elpher-reload-current-node)))
1027
1028 (defun elpher-bookmark-current ()
1029   "Bookmark the current node."
1030   (interactive)
1031   (let ((address (elpher-node-address elpher-current-node))
1032         (display-string (elpher-node-display-string elpher-current-node)))
1033     (if (not (elpher-address-special-p address))
1034         (let ((bookmark-display-string (read-string "Bookmark display string: "
1035                                                     display-string)))
1036           (elpher-add-address-bookmark address bookmark-display-string)
1037           (message "Bookmark added."))
1038       (error "Cannot bookmark %s" display-string))))
1039
1040 (defun elpher-bookmark-link ()
1041   "Bookmark the link at point."
1042   (interactive)
1043   (let ((button (button-at (point))))
1044     (if button
1045         (let* ((node (button-get button 'elpher-node))
1046                (address (elpher-node-address node))
1047                (display-string (elpher-node-display-string node)))
1048           (if (not (elpher-address-special-p address))
1049               (let ((bookmark-display-string (read-string "Bookmark display string: "
1050                                                           display-string)))
1051                 (elpher-add-address-bookmark address bookmark-display-string)
1052                 (elpher-reload-bookmarks)
1053                 (message "Bookmark added."))
1054             (error "Cannot bookmark %s" display-string)))
1055       (error "No link selected"))))
1056
1057 (defun elpher-unbookmark-current ()
1058   "Remove bookmark for the current node."
1059   (interactive)
1060   (let ((address (elpher-node-address elpher-current-node)))
1061     (unless (elpher-address-special-p address)
1062       (elpher-remove-address-bookmark address)
1063       (message "Bookmark removed."))))
1064
1065 (defun elpher-unbookmark-link ()
1066   "Remove bookmark for the link at point."
1067   (interactive)
1068   (let ((button (button-at (point))))
1069     (if button
1070         (let ((node (button-get button 'elpher-node)))
1071           (elpher-remove-address-bookmark (elpher-node-address node))
1072           (elpher-reload-bookmarks)
1073           (message "Bookmark removed."))
1074       (error "No link selected"))))
1075
1076 (defun elpher-bookmarks ()
1077   "Visit bookmarks page."
1078   (interactive)
1079   (switch-to-buffer "*elpher*")
1080   (elpher-visit-node
1081    (elpher-make-node "Bookmarks Page" (elpher-make-address 'bookmarks))))
1082
1083 (defun elpher-info-node (node)
1084   "Display information on NODE."
1085   (let ((display-string (elpher-node-display-string node))
1086         (address (elpher-node-address node)))
1087     (if (not (elpher-address-special-p address))
1088         (message "`%s' on %s port %s"
1089                 (elpher-address-selector address)
1090                 (elpher-address-host address)
1091                 (elpher-address-port address))
1092       (message "%s" display-string))))
1093
1094 (defun elpher-info-link ()
1095   "Display information on node corresponding to link at point."
1096   (interactive)
1097   (let ((button (button-at (point))))
1098     (if button
1099         (elpher-info-node (button-get button 'elpher-node))
1100       (error "No item selected"))))
1101   
1102 (defun elpher-info-current ()
1103   "Display information on current node."
1104   (interactive)
1105   (elpher-info-node elpher-current-node))
1106
1107 (defun elpher-get-address-url (address)
1108   "Get URL representation of ADDRESS."
1109   (let ((type (elpher-address-type address))
1110         (selector (elpher-address-selector address))
1111         (host (elpher-address-host address))
1112         (port (elpher-address-port address)))
1113     (if (and (equal type ?h)
1114              (string-prefix-p "URL:" selector))
1115         (elt (split-string selector "URL:") 1)
1116       (concat "gopher"
1117               (if (elpher-address-use-tls-p address) "s" "")
1118               "://"
1119               host
1120               (if (equal port 70)
1121                   ""
1122                 (format ":%d" port))
1123               "/" (string type)
1124               selector))))
1125
1126 (defun elpher-copy-node-url (node)
1127   "Copy URL representation of address of NODE to `kill-ring'."
1128   (let ((address (elpher-node-address node)))
1129     (if (elpher-address-special-p address)
1130         (error (format "Cannot represent %s as URL" (elpher-node-display-string node)))
1131       (let ((url (elpher-get-address-url address)))
1132         (message "Copied \"%s\" to kill-ring/clipboard." url)
1133         (kill-new url)))))
1134
1135 (defun elpher-copy-link-url ()
1136   "Copy URL of item at point to `kill-ring'."
1137   (interactive)
1138   (let ((button (button-at (point))))
1139     (if button
1140         (elpher-copy-node-url (button-get button 'elpher-node))
1141       (error "No item selected"))))
1142
1143 (defun elpher-copy-current-url ()
1144   "Copy URL of current node to `kill-ring'."
1145   (interactive)
1146   (elpher-copy-node-url elpher-current-node))
1147
1148 (defun elpher-set-coding-system ()
1149   "Specify an explicit character coding system."
1150   (interactive)
1151   (let ((system (read-coding-system "Set coding system to use (default is to autodetect): " nil)))
1152     (setq elpher-user-coding-system system)
1153     (if system
1154         (message "Coding system fixed to %s. (Reload to see effect)." system)
1155       (message "Coding system set to autodetect. (Reload to see effect)."))))
1156
1157 ;;; Mode and keymap
1158 ;;
1159
1160 (defvar elpher-mode-map
1161   (let ((map (make-sparse-keymap)))
1162     (define-key map (kbd "TAB") 'elpher-next-link)
1163     (define-key map (kbd "<backtab>") 'elpher-prev-link)
1164     (define-key map (kbd "u") 'elpher-back)
1165     (define-key map (kbd "O") 'elpher-root-dir)
1166     (define-key map (kbd "g") 'elpher-go)
1167     (define-key map (kbd "r") 'elpher-redraw)
1168     (define-key map (kbd "R") 'elpher-reload)
1169     (define-key map (kbd "T") 'elpher-toggle-tls)
1170     (define-key map (kbd "w") 'elpher-view-raw)
1171     (define-key map (kbd "d") 'elpher-download)
1172     (define-key map (kbd "D") 'elpher-download-current)
1173     (define-key map (kbd "m") 'elpher-jump)
1174     (define-key map (kbd "i") 'elpher-info-link)
1175     (define-key map (kbd "I") 'elpher-info-current)
1176     (define-key map (kbd "c") 'elpher-copy-link-url)
1177     (define-key map (kbd "C") 'elpher-copy-current-url)
1178     (define-key map (kbd "a") 'elpher-bookmark-link)
1179     (define-key map (kbd "A") 'elpher-bookmark-current)
1180     (define-key map (kbd "x") 'elpher-unbookmark-link)
1181     (define-key map (kbd "X") 'elpher-unbookmark-current)
1182     (define-key map (kbd "B") 'elpher-bookmarks)
1183     (define-key map (kbd "S") 'elpher-set-coding-system)
1184     (when (fboundp 'evil-define-key)
1185       (evil-define-key 'motion map
1186         (kbd "TAB") 'elpher-next-link
1187         (kbd "C-]") 'elpher-follow-current-link
1188         (kbd "C-t") 'elpher-back
1189         (kbd "u") 'elpher-back
1190         (kbd "O") 'elpher-root-dir
1191         (kbd "g") 'elpher-go
1192         (kbd "r") 'elpher-redraw
1193         (kbd "R") 'elpher-reload
1194         (kbd "T") 'elpher-toggle-tls
1195         (kbd "w") 'elpher-view-raw
1196         (kbd "d") 'elpher-download
1197         (kbd "D") 'elpher-download-current
1198         (kbd "m") 'elpher-jump
1199         (kbd "i") 'elpher-info-link
1200         (kbd "I") 'elpher-info-current
1201         (kbd "c") 'elpher-copy-link-url
1202         (kbd "C") 'elpher-copy-current-url
1203         (kbd "a") 'elpher-bookmark-link
1204         (kbd "A") 'elpher-bookmark-current
1205         (kbd "x") 'elpher-unbookmark-link
1206         (kbd "X") 'elpher-unbookmark-current
1207         (kbd "B") 'elpher-bookmarks
1208         (kbd "S") 'elpher-set-coding-system))
1209     map)
1210   "Keymap for gopher client.")
1211
1212 (define-derived-mode elpher-mode special-mode "elpher"
1213   "Major mode for elpher, an elisp gopher client.
1214
1215 This mode is automatically enabled by the interactive
1216 functions which initialize the gopher client, namely
1217 `elpher', `elpher-go' and `elpher-bookmarks'.")
1218
1219 (when (fboundp 'evil-set-initial-state)
1220   (evil-set-initial-state 'elpher-mode 'motion))
1221
1222 ;;; Main start procedure
1223 ;;
1224
1225 ;;;###autoload
1226 (defun elpher ()
1227   "Start elpher with default landing page."
1228   (interactive)
1229   (if (get-buffer "*elpher*")
1230       (switch-to-buffer "*elpher*")
1231     (switch-to-buffer "*elpher*")
1232     (setq elpher-current-node nil)
1233     (let ((start-node (elpher-make-node "Elpher Start Page"
1234                                         (elpher-make-address 'start))))
1235       (elpher-visit-node start-node)))
1236   "Started Elpher.") ; Otherwise (elpher) evaluates to start page string.
1237
1238 ;;; elpher.el ends here