Added view-text and support for url links.
[elpher.git] / elopher.el
1 ;;; elopher.el --- gopher client
2
3 ;;; Commentary:
4
5 ;; Simple gopher client in elisp.
6
7 ;;; Code:
8
9 ;;; Global constants
10 ;;
11
12 (defconst elopher-version "1.0.0"
13   "Current version of elopher.")
14
15 (defconst elopher-margin-width 6
16   "Width of left-hand margin used when rendering indicies.")
17
18 (defconst elopher-start-index
19   (mapconcat 'identity
20    (list "i\tfake\tfake\t1"
21          "i--------------------------------------------\tfake\tfake\t1"
22          "i          Elopher Gopher Client             \tfake\tfake\t1"
23          (format "i              version %s\tfake\tfake\t1" elopher-version)
24          "i--------------------------------------------\tfake\tfake\t1"
25          "i\tfake\tfake\t1"
26          "iBasic usage:\tfake\tfake\t1"
27          "i\tfake\tfake\t1"
28          "i - tab/shift-tab: next/prev directory entry on current page\tfake\tfake\t1"
29          "i - RET/mouse-1: open directory entry under cursor\tfake\tfake\t1"
30          "i - u: return to parent directory entry\tfake\tfake\t1"
31          "i - g: go to a particular page\tfake\tfake\t1"
32          "i - r: reload current page\tfake\tfake\t1"
33          "i - t: display the current page as text (i.e. \"source\")\tfake\tfake\t1"
34          "i\tfake\tfake\t1"
35          "iPlaces to start exploring Gopherspace:\tfake\tfake\t1"
36          "i\tfake\tfake\t1"
37          "1Floodgap Systems Gopher Server\t\tgopher.floodgap.com\t70"
38          "i\tfake\tfake\t1"
39          "iAlternatively, select the following item and enter some\tfake\tfake\t1"
40          "isearch terms:\tfake\tfake\t1"
41          "i\tfake\tfake\t1"
42          "7Veronica-2 Gopher Search Engine\t/v2/vs\tgopher.floodgap.com\t70"
43          ".")
44    "\r\n"))
45
46
47 ;;; Customization group
48 ;;
49
50 (defgroup elopher nil
51   "A simple gopher client."
52   :group 'applications)
53
54 (defcustom elopher-index-face '(foreground-color . "cyan")
55   "Face used for index records."
56   :type '(face))
57
58 (defcustom elopher-text-face '(foreground-color . "white")
59   "Face used for text records."
60   :type '(face))
61
62 (defcustom elopher-info-face '(foreground-color . "gray")
63   "Face used for info records."
64   :type '(face))
65
66 (defcustom elopher-image-face '(foreground-color . "green")
67   "Face used for image records."
68   :type '(face))
69
70 (defcustom elopher-search-face '(foreground-color . "orange")
71   "Face used for image records."
72   :type '(face))
73
74 (defcustom elopher-http-face '(foreground-color . "yellow")
75   "Face used for image records."
76   :type '(face))
77
78 (defcustom elopher-unknown-face '(foreground-color . "red")
79   "Face used for unknown record types."
80   :type '(face))
81
82 (defcustom elopher-open-urls-with-eww nil
83   "If non-nil, open URL selectors using eww.
84 Otherwise, use the system browser via the BROWSE-URL function."
85   :type '(boolean))
86
87 ;;; Model
88 ;;
89
90 ;; Address
91
92 (defun elopher-make-address (selector host port)
93   (list selector host port))
94
95 (defun elopher-address-selector (address)
96   (car address))
97
98 (defun elopher-address-host (address)
99   (cadr address))
100
101 (defun elopher-address-port (address)
102   (caddr address))
103
104 ;; Node
105
106 (defun elopher-make-node (parent address getter &optional content pos)
107   (list parent address getter content pos))
108
109 (defun elopher-node-parent (node)
110   (elt node 0))
111
112 (defun elopher-node-address (node)
113   (elt node 1))
114
115 (defun elopher-node-getter (node)
116   (elt node 2))
117
118 (defun elopher-node-content (node)
119   (elt node 3))
120
121 (defun elopher-node-pos (node)
122   (elt node 4))
123
124 (defun elopher-set-node-content (node content)
125   (setcar (nthcdr 3 node) content))
126
127 (defun elopher-set-node-pos (node pos)
128   (setcar (nthcdr 4 node) pos))
129
130 (defun elopher-save-pos ()
131   (when elopher-current-node
132     (elopher-set-node-pos elopher-current-node (point))))
133
134 (defun elopher-restore-pos ()
135   (let ((pos (elopher-node-pos elopher-current-node)))
136     (if pos
137         (goto-char pos)
138       (goto-char (point-min)))))
139
140 ;; Node graph traversal
141
142 (defvar elopher-current-node)
143
144 (defun elopher-visit-node (node &optional text)
145   (elopher-save-pos)
146   (setq elopher-current-node node)
147   (if text
148       (progn
149         (elopher-set-node-content elopher-current-node nil)
150         (elopher-get-text-node))
151     (funcall (elopher-node-getter node))))
152
153 (defun elopher-visit-parent-node ()
154   (let ((parent-node (elopher-node-parent elopher-current-node)))
155     (when parent-node
156       (elopher-visit-node parent-node))))
157       
158 (defun elopher-reload-current-node ()
159   (elopher-set-node-content elopher-current-node nil)
160   (elopher-visit-node elopher-current-node))
161
162 ;;; Buffer preparation
163 ;;
164
165 (defmacro elopher-with-clean-buffer (&rest args)
166   (list 'progn
167         '(switch-to-buffer "*elopher*")
168         '(elopher-mode)
169         (append (list 'let '((inhibit-read-only t))
170                       '(erase-buffer))
171                 args)))
172
173 ;;; Index rendering
174 ;;
175
176 (defun elopher-insert-index (string)
177   "Insert the index corresponding to STRING into the current buffer."
178   (dolist (line (split-string string "\r\n"))
179     (unless (= (length line) 0)
180       (elopher-insert-index-record line))))
181
182 (defun elopher-insert-margin (&optional type-name)
183   "Insert index margin, optionally containing the TYPE-NAME, into the current buffer."
184   (if type-name
185       (progn
186         (insert (format (concat "%" (number-to-string (- elopher-margin-width 1)) "s")
187                         (concat
188                          (propertize "[" 'face '(foreground-color . "blue"))
189                          (propertize type-name 'face '(foreground-color . "white"))
190                          (propertize "]" 'face '(foreground-color . "blue")))))
191         (insert " "))
192     (insert (make-string elopher-margin-width ?\s))))
193
194 (defun elopher-insert-index-record (line)
195   "Insert the index record corresponding to LINE into the current buffer."
196   (let* ((type (elt line 0))
197          (fields (split-string (substring line 1) "\t"))
198          (display-string (elt fields 0))
199          (selector (elt fields 1))
200          (host (elt fields 2))
201          (port (elt fields 3))
202          (address (elopher-make-address selector host port))
203          (help-string (format "mouse-1, RET: open %s on %s port %s"
204                               selector host port)))
205     (pcase type
206       (?i (elopher-insert-margin)
207           (insert (propertize display-string
208                               'face elopher-info-face)))
209       (?0 (elopher-insert-margin "T")
210           (insert-text-button display-string
211                               'face elopher-text-face
212                               'elopher-node (elopher-make-node elopher-current-node
213                                                                address
214                                                                #'elopher-get-text-node)
215                               'action #'elopher-click-link
216                               'follow-link t
217                               'help-echo help-string))
218       (?1 (elopher-insert-margin "/")
219           (insert-text-button display-string
220                               'face elopher-index-face
221                               'elopher-node (elopher-make-node elopher-current-node
222                                                                address
223                                                                #'elopher-get-index-node)
224                               'action #'elopher-click-link
225                               'follow-link t
226                               'help-echo help-string))
227       ((or ?g ?p ?I) (elopher-insert-margin "im")
228           (insert-text-button display-string
229                               'face elopher-image-face
230                               'elopher-node (elopher-make-node elopher-current-node
231                                                                address
232                                                                #'elopher-get-image-node)
233                               'action #'elopher-click-link
234                               'follow-link t
235                               'help-echo help-string))
236       (?7 (elopher-insert-margin "S")
237           (insert-text-button display-string
238                               'face elopher-search-face
239                               'elopher-node (elopher-make-node elopher-current-node
240                                                               address
241                                                               #'elopher-get-search-node)
242                               'action #'elopher-click-link
243                               'follow-link t
244                               'help-echo help-string))
245       (?h (elopher-insert-margin "W")
246           (let ((url (elt (split-string selector "URL:") 1)))
247             (insert-text-button display-string
248                                 'face elopher-http-face
249                                 'elopher-url url
250                                 'action #'elopher-click-url
251                                 'follow-link t
252                                 'help-echo (format "mouse-1, RET: open url %s" url))))
253       (?.) ; Occurs at end of index, can safely ignore.
254       (tp (elopher-insert-margin (concat (char-to-string tp) "?"))
255           (insert (propertize display-string
256                               'face elopher-unknown-face))))
257     (insert "\n")))
258
259
260 ;;; Selector retrieval (all kinds)
261 ;;
262
263 (defvar elopher-selector-string)
264
265 (defun elopher-get-selector (address after)
266   "Retrieve selector specified by ADDRESS, then execute AFTER.
267 The result is stored as a string in the variable elopher-selector-string."
268   (setq elopher-selector-string "")
269   (let ((p (get-process "elopher-process")))
270     (if p (delete-process p)))
271   (make-network-process
272    :name "elopher-process"
273    :host (elopher-address-host address)
274    :service (elopher-address-port address)
275    :filter (lambda (proc string)
276              (setq elopher-selector-string (concat elopher-selector-string string)))
277    :sentinel after)
278   (process-send-string "elopher-process"
279                        (concat (elopher-address-selector address) "\n")))
280
281 ;; Index retrieval
282
283 (defun elopher-get-index-node ()
284   (let ((content (elopher-node-content elopher-current-node))
285         (address (elopher-node-address elopher-current-node)))
286     (if content
287         (progn
288           (elopher-with-clean-buffer
289             (insert content))
290           (elopher-restore-pos))
291       (if address
292           (progn
293             (elopher-with-clean-buffer
294              (insert "LOADING DIRECTORY..."))
295             ;; (let ((inhibit-read-only t))
296               ;; (insert "LOADING DIRECTORY..."))
297             (elopher-get-selector address
298                                   (lambda (proc event)
299                                     (elopher-with-clean-buffer
300                                       (elopher-insert-index elopher-selector-string))
301                                     (elopher-restore-pos)
302                                     (elopher-set-node-content elopher-current-node
303                                                               (buffer-string)))))
304         (progn
305           (elopher-with-clean-buffer
306             (elopher-insert-index elopher-start-index))
307           (elopher-restore-pos)
308           (elopher-set-node-content elopher-current-node
309                                     (buffer-string)))))))
310
311 ;; Text retrieval
312
313 (defun elopher-strip-CRs (string)
314   (replace-regexp-in-string "\r" "" string))
315
316 (defun elopher-get-text-node ()
317   (let ((content (elopher-node-content elopher-current-node))
318         (address (elopher-node-address elopher-current-node)))
319     (if content
320         (progn
321           (elopher-with-clean-buffer
322             (insert content))
323           (elopher-restore-pos))
324       (progn
325         (elopher-with-clean-buffer
326           (insert "LOADING TEXT..."))
327         (elopher-get-selector address
328                               (lambda (proc event)
329                                 (elopher-with-clean-buffer
330                                   (insert (elopher-strip-CRs elopher-selector-string)))
331                                 (elopher-restore-pos)
332                                 (elopher-set-node-content elopher-current-node
333                                                           (buffer-string))))))))
334
335 ;; Image retrieval
336
337 (defun elopher-get-image-node ()
338   (let ((content (elopher-node-content elopher-current-node))
339         (address (elopher-node-address elopher-current-node)))
340     (if content
341         (progn
342           (elopher-with-clean-buffer
343             (insert-image content))
344           (setq cursor-type nil)
345           (elopher-restore-pos))
346       (progn
347         (elopher-with-clean-buffer
348           (insert "LOADING IMAGE..."))
349         (elopher-get-selector address
350                               (lambda (proc event)
351                                 (let ((image (create-image
352                                               (string-as-unibyte elopher-selector-string)
353                                               nil t)))
354                                   (elopher-with-clean-buffer
355                                    (insert-image image))
356                                   (setq cursor-type nil)
357                                   (elopher-restore-pos)
358                                   (elopher-set-node-content elopher-current-node image))))))))
359
360 ;; Search retrieval
361
362 (defun elopher-get-search-node ()
363   (let* ((content (elopher-node-content elopher-current-node))
364          (address (elopher-node-address elopher-current-node)))
365     (if content
366         (progn
367           (elopher-with-clean-buffer
368             (insert content))
369           (elopher-restore-pos)
370           (message "Displaying cached search results. Reload to perform a new search."))
371       (let* ((query-string (read-string "Query: "))
372              (query-selector (concat (elopher-address-selector address) "\t" query-string))
373              (search-address (elopher-make-address query-selector
374                                                    (elopher-address-host address)
375                                                    (elopher-address-port address))))
376         (elopher-with-clean-buffer
377          (insert "LOADING RESULTS..."))
378         (elopher-get-selector search-address
379                               (lambda (proc event)
380                                 (elopher-with-clean-buffer
381                                   (elopher-insert-index elopher-selector-string))
382                                 (goto-char (point-min))
383                                 (elopher-set-node-content elopher-current-node
384                                                           (buffer-string))))))))
385
386
387 ;;; Navigation procedures
388 ;;
389
390 (defun elopher-next-link ()
391   (interactive)
392   (forward-button 1))
393
394 (defun elopher-prev-link ()
395   (interactive)
396   (backward-button 1))
397
398 (defun elopher-click-link (button)
399   (let ((node (button-get button 'elopher-node)))
400     (elopher-visit-node node)))
401
402 (defun elopher-click-url (button)
403   (let ((url (button-get button 'elopher-url)))
404     (if elopher-open-urls-with-eww
405         (browse-web url)
406       (browse-url url))))
407
408 (defun elopher-follow-closest-link ()
409   (interactive)
410   (push-button))
411
412 (defun elopher-go ()
413   "Go to a particular gopher site."
414   (interactive)
415   (let* (
416          (hostname (read-string "Gopher host: "))
417          (selector (read-string "Selector (default none): " nil nil ""))
418          (port (read-string "Port (default 70): " nil nil 70))
419          (address (list selector hostname port)))
420     (elopher-visit-node
421      (elopher-make-node elopher-current-node
422                         address
423                         #'elopher-get-index-node))))
424
425 (defun  elopher-reload ()
426   "Reload current page."
427   (interactive)
428   (elopher-reload-current-node))
429
430 (defun elopher-view-text ()
431   "View current page as plain text."
432   (interactive)
433   (elopher-visit-node elopher-current-node t))
434
435 (defun elopher-back ()
436   "Go to previous site."
437   (interactive)
438   (if (elopher-node-parent elopher-current-node)
439       (elopher-visit-parent-node)
440     (message "No previous site.")))
441
442
443 ;;; Mode and keymap
444 ;;
445
446 (defvar elopher-mode-map
447   (let ((map (make-sparse-keymap)))
448     (define-key map (kbd "<tab>") 'elopher-next-link)
449     (define-key map (kbd "<S-tab>") 'elopher-prev-link)
450     (define-key map (kbd "u") 'elopher-back)
451     (define-key map (kbd "g") 'elopher-go)
452     (define-key map (kbd "r") 'elopher-reload)
453     (define-key map (kbd "t") 'elopher-view-text)
454     (when (fboundp 'evil-define-key)
455       (evil-define-key 'normal map
456         (kbd "C-]") 'elopher-follow-closest-link
457         (kbd "C-t") 'elopher-back
458         (kbd "u") 'elopher-back
459         (kbd "g") 'elopher-go
460         (kbd "r") 'elopher-reload
461         (kbd "t") 'elopher-view-text))
462     map)
463   "Keymap for gopher client.")
464
465 (define-derived-mode elopher-mode special-mode "elopher"
466   "Major mode for elopher, an elisp gopher client.")
467
468
469 ;;; Main start procedure
470 ;;
471 (defun elopher ()
472   "Start elopher with default landing page."
473   (interactive)
474   (setq elopher-current-node nil)
475   (let ((start-node (elopher-make-node nil nil #'elopher-get-index-node)))
476     (elopher-visit-node start-node)))
477
478 ;;; elopher.el ends here
479