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