History almost working.
[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-page
19   (string-join
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 - tab/shift-tab: next/prev directory entry\tfake\tfake\t1"
28          "i - RET/mouse-1: open directory entry\tfake\tfake\t1"
29          "i - u: return to parent directory entry\tfake\tfake\t1"
30          "i - g: go to a particular site\tfake\tfake\t1"
31          "i\tfake\tfake\t1"
32          "iPlaces to start exploring Gopherspace:\tfake\tfake\t1"
33          "1Floodgap Systems Gopher Server\t\tgopher.floodgap.com\t70"
34          "1Super-Dimensional Fortress\t\tsdf.org\t70"
35          "i\tfake\tfake\t1"
36          "iTest entries:\tfake\tfake\t1"
37          "pXKCD comic image\t/fun/xkcd/comics/2130/2137/text_entry.png\tgopher.floodgap.com\t70"
38          "1Test server\t\tlocalhost\t70"
39          ".")
40    "\r\n"))
41
42
43 ;;; Customization group
44 ;;
45
46 (defgroup elopher nil
47   "A simple gopher client."
48   :group 'applications)
49
50 (defcustom elopher-index-face '(foreground-color . "cyan")
51   "Face used for index records.")
52 (defcustom elopher-text-face '(foreground-color . "white")
53   "Face used for text records.")
54 (defcustom elopher-info-face '(foreground-color . "gray")
55   "Face used for info records.")
56 (defcustom elopher-image-face '(foreground-color . "green")
57   "Face used for image records.")
58 (defcustom elopher-unknown-face '(foreground-color . "red")
59   "Face used for unknown record types.")
60
61 ;;; Model
62 ;;
63
64 ;; Address
65
66 (defun elopher-make-address (selector host port)
67   (list selector host port))
68
69 (defun elopher-address-selector (address)
70   (car address))
71
72 (defun elopher-address-host (address)
73   (cadr address))
74
75 (defun elopher-address-port (address)
76   (caddr address))
77
78 ;; Node
79
80 (defun elopher-make-node (parent address getter &optional content)
81   (list parent address getter content))
82
83 (defun elopher-node-parent (node)
84   (car node))
85
86 (defun elopher-node-address (node)
87   (cadr node))
88
89 (defun elopher-node-getter (node)
90   (caddr node))
91
92 (defun elopher-node-content (node)
93   (cadddr node))
94
95 (defun elopher-set-node-content (node content)
96   (setcar (cdddr node) content))
97
98 (defvar elopher-start-node (elopher-make-node nil nil #'elopher-get-index-node))
99 (defvar elopher-current-node)
100
101 (defun elopher-visit-node (node)
102   (elopher-prepare-buffer)
103   (setq elopher-current-node node)
104   (funcall (elopher-node-getter node)))
105
106 (defun elopher-visit-parent-node ()
107   (let ((parent-node (elopher-node-parent elopher-current-node)))
108     (when parent-node
109       (setq elopher-current-node parent-node)
110       (elopher-visit-node elopher-current-node))))
111       
112 (defun elopher-reload-current-node ()
113   (elopher-set-node-content elopher-current-node nil)
114   (elopher-visit-node elopher-current-node))
115
116
117 ;;; Buffer preparation
118 ;;
119
120 (defun elopher-prepare-buffer ()
121   (switch-to-buffer "*elopher*")
122   (elopher-mode)
123   (let ((inhibit-read-only t))
124     (erase-buffer)))
125
126
127 ;;; Index rendering
128 ;;
129
130 (defun elopher-insert-index (string)
131   "Inserts the index corresponding to STRING into the current buffer."
132   (dolist (line (split-string string "\r\n"))
133     (unless (string-empty-p line)
134       (elopher-insert-index-record line))))
135
136 (defun elopher-insert-margin (&optional type-name)
137   (if type-name
138       (progn
139         (insert (format (concat "%" (number-to-string (- elopher-margin-width 1)) "s")
140                         (concat
141                          (propertize "[" 'face '(foreground-color . "blue"))
142                          (propertize type-name 'face '(foreground-color . "white"))
143                          (propertize "]" 'face '(foreground-color . "blue")))))
144         (insert " "))
145     (insert (make-string elopher-margin-width ?\s))))
146
147 (defun elopher-insert-index-record (line)
148   "Inserts the index record corresponding to LINE into the current buffer."
149   (let* ((type (elt line 0))
150          (fields (split-string (substring line 1) "\t"))
151          (display-string (elt fields 0))
152          (address (elopher-make-address (elt fields 1) (elt fields 2) (elt fields 3))))
153     (pcase type
154       (?i (elopher-insert-margin)
155           (insert (propertize display-string
156                               'face elopher-info-face)))
157       (?0 (elopher-insert-margin "T")
158           (insert-text-button display-string
159                               'face elopher-text-face
160                               'elopher-node (elopher-make-node elopher-current-node
161                                                                address
162                                                                #'elopher-get-text-node)
163                               'action #'elopher-click-link
164                               'follow-link t))
165       (?1 (elopher-insert-margin "/")
166           (insert-text-button display-string
167                               'face elopher-index-face
168                               'elopher-node (elopher-make-node elopher-current-node
169                                                                address
170                                                                #'elopher-get-index-node)
171                               'action #'elopher-click-link
172                               'follow-link t))
173       (?.) ; Occurs at end of index, can safely ignore.
174       (tp (elopher-insert-margin (concat (char-to-string tp) "?"))
175           (insert (propertize display-string
176                               'face elopher-unknown-face))))
177     (insert "\n")))
178
179
180 ;;; Selector retrieval (all kinds)
181 ;;
182
183 (defvar elopher-selector-string)
184
185 (defun elopher-get-selector (address after)
186   (setq elopher-selector-string "")
187   (let ((p (get-process "elopher-process")))
188     (if p (delete-process p)))
189   (make-network-process
190    :name "elopher-process"
191    :host (elopher-address-host address)
192    :service (elopher-address-port address)
193    :filter (lambda (proc string)
194              (setq elopher-selector-string (concat elopher-selector-string string)))
195    :sentinel after)
196   (process-send-string "elopher-process"
197                        (concat (elopher-address-selector address) "\n")))
198
199 ;; Index retrieval
200
201 (defun elopher-get-index-node ()
202   (let ((content (elopher-node-content elopher-current-node))
203         (address (elopher-node-address elopher-current-node)))
204     (if content
205         (insert content)
206       (if address
207           (elopher-get-selector address
208                                 (lambda (proc event)
209                                   (let ((inhibit-read-only t))
210                                     (erase-buffer)
211                                     (elopher-insert-index elopher-selector-string))
212                                   (elopher-set-node-content elopher-current-node
213                                                             (buffer-string))))
214         (let ((inhibit-read-only t))
215           (erase-buffer)
216           (elopher-insert-index elopher-start-page))))))
217
218 ;; Text retrieval
219
220 (defun elopher-get-text-node ()
221   (let ((content (elopher-node-content elopher-current-node))
222         (address (elopher-node-address elopher-current-node)))
223     (if content
224         (let ((inhibit-read-only t))
225           (insert content))
226       (elopher-get-selector address
227                             (lambda (proc event)
228                               (let ((inhibit-read-only t))
229                                 (erase-buffer)
230                                 (insert elopher-selector-string))
231                               (elopher-set-node-content elopher-current-node
232                                                         elopher-selector-string))))))
233
234 ;;; Navigation methods
235 ;;
236
237 (defun elopher-next-link ()
238   (interactive)
239   (forward-button 1))
240
241 (defun elopher-prev-link ()
242   (interactive)
243   (backward-button 1))
244
245 (defun elopher-click-link (button)
246   (let ((node (button-get button 'elopher-node)))
247     (elopher-visit-node node)))
248
249 (defun elopher-follow-closest-link ()
250   (interactive)
251   (push-button))
252
253 (defun elopher-go ()
254   "Go to a particular gopher site."
255   (interactive)
256   (let* ((selector "")
257          (hostname (read-from-minibuffer "Gopher host: "))
258          (port 70)
259          (address (list selector hostname port)))
260     (elopher-visit-node
261      (elopher-make-node elopher-current-node
262                         address
263                         #'elopher-get-index-node))))
264
265 (defun  elopher-reload ()
266   "Reload current site."
267   (interactive)
268   (elopher-reload-current-node))
269
270 (defun elopher-back ()
271   "Go to previous site."
272   (interactive)
273   (elopher-visit-parent-node))
274
275 ;;; Main start procedure
276 ;;
277 (defun elopher ()
278   "Start elopher with default landing page."
279   (interactive)
280   (elopher-visit-node elopher-start-node))
281
282
283 ;;; Mode and keymap
284 ;;
285
286 (defvar elopher-mode-map
287   (let ((map (make-sparse-keymap)))
288     (define-key map (kbd "<tab>") 'elopher-next-link)
289     (define-key map (kbd "<S-tab>") 'elopher-prev-link)
290     (define-key map (kbd "u") 'elopher-back)
291     (define-key map (kbd "g") 'elopher-go)
292     (define-key map (kbd "r") 'elopher-reload)
293     (when (require 'evil nil t)
294       (evil-define-key 'normal map
295         (kbd "C-]") 'elopher-follow-closest-link
296         (kbd "C-t") 'elopher-back
297         (kbd "u") 'elopher-back
298         (kbd "g") 'elopher-go
299         (kbd "r") 'elopher-reload))
300     map)
301   "Keymap for gopher client.")
302
303 (define-derived-mode elopher-mode special-mode "elopher"
304   "Major mode for elopher, an elisp gopher client.")
305
306
307 ;;; elopher.el ends here