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