-
Notifications
You must be signed in to change notification settings - Fork 38
/
atomic-chrome.el
391 lines (340 loc) · 15.2 KB
/
atomic-chrome.el
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
;;; atomic-chrome.el --- Edit Chrome text area with Emacs using Atomic Chrome
;; Copyright (C) 2016 alpha22jp <[email protected]>
;; Author: alpha22jp <[email protected]>
;; Package-Requires: ((emacs "24.4") (let-alist "1.0.4") (websocket "1.4"))
;; Keywords: chrome edit textarea
;; URL: https://github.com/alpha22jp/atomic-chrome
;; Version: 2.0.0
;; This program is free software; you can redistribute it and/or modify it under
;; the terms of the GNU General Public License as published by the Free Software
;; Foundation; either version 2 of the License, or (at your option) any later
;; version.
;; This program is distributed in the hope that it will be useful, but WITHOUT
;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
;; FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
;; details.
;; You should have received a copy of the GNU General Public License along with
;; this program; if not, write to the Free Software Foundation, Inc., 51
;; Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
;;; Commentary:
;; This is the Emacs version of Atomic Chrome which is an extension for Google
;; Chrome browser that allows you to edit text areas of the browser in Emacs.
;;
;; It's similar to Edit with Emacs, but has some advantages as below with the
;; help of websocket.
;;
;; * Live update
;; The input on Emacs is reflected to the browser instantly and continuously.
;; * Bidirectional communication
;; You can edit both on the browser and Emacs, they are synced to the same.
;;
;; Firefox is supported via the GhostText browser addon.
;;; Code:
(eval-when-compile (require 'cl-lib))
(require 'json)
(require 'let-alist)
(eval-when-compile (require 'subr-x))
(require 'websocket)
(defgroup atomic-chrome nil
"Edit browser text area with Emacs using Atomic Chrome or GhostText."
:prefix "atomic-chrome-"
:group 'applications)
(defcustom atomic-chrome-extension-type-list '(atomic-chrome ghost-text)
"List of browser extension type available."
:type '(repeat (choice (const :tag "Atomic Chrome" atomic-chrome)
(const :tag "Ghost Text" ghost-text)))
:group 'atomic-chrome)
(defcustom atomic-chrome-buffer-open-style 'split
"Specify the style to open new buffer for editing."
:type '(choice (const :tag "Open buffer with full window" full)
(const :tag "Open buffer with splitted window" split)
(const :tag "Open buffer with new frame" frame))
:group 'atomic-chrome)
(defcustom atomic-chrome-buffer-frame-width 80
"Width of editing buffer frame."
:type 'integer
:group 'atomic-chrome)
(defcustom atomic-chrome-buffer-frame-height 25
"Height of editing buffer frame."
:type 'integer
:group 'atomic-chrome)
(defcustom atomic-chrome-server-ghost-text-port 4001
"HTTP server port for Ghost Text."
:type 'integer
:group 'atomic-chrome)
(defcustom atomic-chrome-enable-auto-update t
"If non-nil, edit on Emacs is reflected to the browser instantly.
If nil, you need to type \"C-cC-s\" manually."
:type 'boolean
:group 'atomic-chrome)
(defcustom atomic-chrome-enable-bidirectional-edit t
"If non-nil, you can edit both on the browser text area and Emacs.
If nil, edit on browser is ignored while editing on Emacs."
:type 'boolean
:group 'atomic-chrome)
(defcustom atomic-chrome-default-major-mode 'text-mode
"Default major mode for editing buffer."
:type 'function
:group 'atomic-chrome)
(defcustom atomic-chrome-url-major-mode-alist nil
"Association list to select a major mode for a website.
Relates URL (or, for GhostText, hostname) regular expressions to
corresponding major modes."
:type '(alist :key-type (regexp :tag "regexp")
:value-type (function :tag "major mode"))
:group 'atomic-chrome)
(defcustom atomic-chrome-edit-mode-hook nil
"Customizable hook which run when the editing buffer is created."
:type 'hook
:group 'atomic-chrome)
(defcustom atomic-chrome-edit-done-hook nil
"Customizable hook which run when the editing buffer is closed."
:type 'hook
:group 'atomic-chrome)
(defvar atomic-chrome-server-atomic-chrome nil
"Websocket server connection handle for Atomic Chrome.")
(defvar atomic-chrome-server-ghost-text nil
"Websocket server connection handle for Ghost Text.")
(defvar atomic-chrome-buffer-table (make-hash-table :test 'equal)
"Hash table of editing buffer and its assciated data.
Each element has a list consisting of (websocket, frame).")
(defun atomic-chrome-get-websocket (buffer)
"Look up websocket associated with buffer BUFFER.
Looks in `atomic-chrome-buffer-table'."
(nth 0 (gethash buffer atomic-chrome-buffer-table)))
(defun atomic-chrome-get-frame (buffer)
"Look up frame associated with buffer BUFFER.
Looks in `atomic-chrome-buffer-table'."
(nth 1 (gethash buffer atomic-chrome-buffer-table)))
(defun atomic-chrome-get-buffer-by-socket (socket)
"Look up buffer which is associated to the websocket SOCKET.
Looks in `atomic-chrome-buffer-table'."
(let (buffer)
(cl-loop for key being the hash-keys of atomic-chrome-buffer-table
using (hash-values val)
do (when (equal (nth 0 val) socket) (setq buffer key)))
buffer))
(defun atomic-chrome-close-connection ()
"Close client connection associated with current buffer."
(let ((socket (atomic-chrome-get-websocket (current-buffer))))
(when socket
(remhash (current-buffer) atomic-chrome-buffer-table)
(websocket-close socket))))
(defun atomic-chrome-send-buffer-text ()
"Send request to update text with current buffer content."
(interactive)
(let ((socket (atomic-chrome-get-websocket (current-buffer)))
(text (buffer-substring-no-properties (point-min) (point-max))))
(when (and socket text)
(websocket-send-text
socket
(json-encode
(if (eq (websocket-server-conn socket) atomic-chrome-server-ghost-text)
(list (cons "text" text))
(list '("type" . "updateText")
(cons "payload" (list (cons "text" text))))))))
(set-buffer-modified-p nil)))
(defun atomic-chrome-set-major-mode (url)
"Set major mode for editing buffer depending on URL.
`atomic-chrome-url-major-mode-alist' can be used to select major mode.
The specified major mode is used if URL matches to one of the alist,
otherwise fallback to `atomic-chrome-default-major-mode'"
(funcall (or (and url (assoc-default url
atomic-chrome-url-major-mode-alist
'string-match))
atomic-chrome-default-major-mode)))
(defun atomic-chrome-show-edit-buffer (buffer title)
"Show editing buffer BUFFER.
Either creates a frame with title TITLE, or raises the selected
frame, depending on `atomic-chrome-buffer-open-style'."
(let ((edit-frame nil)
(frame-params (list (cons 'name (format "Atomic Chrome: %s" title))
(cons 'width atomic-chrome-buffer-frame-width)
(cons 'height atomic-chrome-buffer-frame-height))))
(when (eq atomic-chrome-buffer-open-style 'frame)
(setq edit-frame
(cond
((memq window-system '(pgtk x))
(if (or (not x-display-name) (string-match-p "wayland" x-display-name))
(make-frame frame-params)
(make-frame-on-display (getenv "DISPLAY") frame-params)))
;; Avoid using make-frame-on-display for Mac OS
((memq window-system '(ns mac))
(make-frame frame-params))
((memq window-system '(w32))
(make-frame-on-display "w32" frame-params))
(t
(make-frame frame-params))))
(select-frame edit-frame))
(if (eq atomic-chrome-buffer-open-style 'split)
(pop-to-buffer buffer)
(switch-to-buffer buffer))
(raise-frame edit-frame)
(select-frame-set-input-focus (window-frame (selected-window)))
edit-frame))
(defun atomic-chrome-create-buffer (socket url title text)
"Create buffer associated with websocket specified by SOCKET.
URL is used to determine the major mode of the buffer created,
TITLE is used for the buffer name and TEXT is inserted to the buffer."
(let ((buffer (generate-new-buffer (if (string-empty-p title) "No title" title))))
(with-current-buffer buffer
(puthash buffer
(list socket (atomic-chrome-show-edit-buffer buffer title))
atomic-chrome-buffer-table)
(atomic-chrome-set-major-mode url)
(insert text))))
(defun atomic-chrome-close-edit-buffer (buffer)
"Close buffer BUFFER if it's one of Atomic Chrome edit buffers."
(let ((frame (atomic-chrome-get-frame buffer))
(window (get-buffer-window buffer)))
(with-current-buffer buffer
(save-restriction
(run-hooks 'atomic-chrome-edit-done-hook)
(when frame (delete-frame frame))
(if (and (eq atomic-chrome-buffer-open-style 'split)
window)
(quit-window t window)
(kill-buffer buffer))))))
(defun atomic-chrome-close-current-buffer ()
"Close current buffer and connection from client."
(interactive)
(when (or (not (buffer-modified-p))
(yes-or-no-p "Buffer has not been saved, close anyway? "))
(atomic-chrome-close-edit-buffer (current-buffer))))
(defun atomic-chrome-update-buffer (socket text)
"Update text on buffer associated with SOCKET to TEXT."
(let ((buffer (atomic-chrome-get-buffer-by-socket socket)))
(when buffer
(with-current-buffer buffer
(erase-buffer)
(insert text)))))
(defun atomic-chrome-on-message (socket frame)
"Handle data received from the websocket client specified by SOCKET.
FRAME holds the raw data received."
(let ((msg (json-read-from-string
(decode-coding-string
(encode-coding-string (websocket-frame-payload frame) 'utf-8)
'utf-8))))
(let-alist msg
(if (eq (websocket-server-conn socket) atomic-chrome-server-ghost-text)
(if (atomic-chrome-get-buffer-by-socket socket)
(atomic-chrome-update-buffer socket .text)
(atomic-chrome-create-buffer socket .url .title .text))
(cond ((string= .type "register")
(atomic-chrome-create-buffer socket .payload.url .payload.title .payload.text))
((string= .type "updateText")
(when atomic-chrome-enable-bidirectional-edit
(atomic-chrome-update-buffer socket .payload.text))))))))
(defun atomic-chrome-on-close (socket)
"Function to handle request from client to close websocket SOCKET."
(let ((buffer (atomic-chrome-get-buffer-by-socket socket)))
(when buffer (atomic-chrome-close-edit-buffer buffer))))
(defvar atomic-chrome-edit-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "C-c C-s") 'atomic-chrome-send-buffer-text)
(define-key map (kbd "C-c C-c") 'atomic-chrome-close-current-buffer)
map)
"Keymap for minor mode `atomic-chrome-edit-mode'.")
(define-minor-mode atomic-chrome-edit-mode
"Minor mode enabled on buffers opened by Emacs Atomic Chrome server."
:group 'atomic-chrome
:lighter " AtomicChrome"
:init-value nil
:keymap atomic-chrome-edit-mode-map
(when atomic-chrome-edit-mode
(add-hook 'kill-buffer-hook 'atomic-chrome-close-connection nil t)
(when atomic-chrome-enable-auto-update
(add-hook 'post-command-hook 'atomic-chrome-send-buffer-text nil t))))
(defun atomic-chrome-turn-on-edit-mode ()
"Turn on `atomic-chrome-edit-mode' if the buffer is an editing buffer."
(when (gethash (current-buffer) atomic-chrome-buffer-table)
(atomic-chrome-edit-mode t)))
(define-global-minor-mode global-atomic-chrome-edit-mode
atomic-chrome-edit-mode atomic-chrome-turn-on-edit-mode)
(defun atomic-chrome-start-websocket-server (port)
"Create websocket server on port PORT."
(websocket-server
port
:host 'local
:on-message #'atomic-chrome-on-message
:on-open nil
:on-close #'atomic-chrome-on-close))
(defun atomic-chrome-start-httpd ()
"Start the HTTP server for Ghost Text query."
(interactive)
(make-network-process
:name "atomic-chrome-httpd"
:family 'ipv4
:host 'local
:service atomic-chrome-server-ghost-text-port
:filter 'atomic-chrome-httpd-process-filter
:filter-multibyte nil
:server t
:noquery t))
(defun atomic-chrome-normalize-header (header)
"Destructively capitalize the components of HEADER."
(mapconcat #'capitalize (split-string header "-") "-"))
(defun atomic-chrome-httpd-parse-string (string)
"Parse client http header STRING into alist."
(let* ((lines (split-string string "[\n\r]+"))
(req (list (split-string (car lines))))
(post (cadr (split-string string "\r\n\r\n"))))
(dolist (line (butlast (cdr lines)))
(push (list (atomic-chrome-normalize-header (car (split-string line ": ")))
(mapconcat #'identity
(cdr (split-string line ": ")) ": "))
req))
(push (list "Content" post) req)
(reverse req)))
(defun atomic-chrome-httpd-process-filter (proc string)
"Process filter of PROC which run each time client make a request.
STRING is the string process received."
(setf string (concat (process-get proc :previous-string) string))
(let* ((request (atomic-chrome-httpd-parse-string string))
(content-length (cadr (assoc "Content-Length" request)))
(uri (cl-cadar request))
(content (cadr (assoc "Content" request))))
(if (and content-length
(< (string-bytes content) (string-to-number content-length)))
(process-put proc :previous-string string)
(atomic-chrome-httpd-send-response proc))))
(defun atomic-chrome-httpd-send-response (proc)
"Send an HTTP 200 OK response back to process PROC."
(when (processp proc)
(unless atomic-chrome-server-ghost-text
(setq atomic-chrome-server-ghost-text
(atomic-chrome-start-websocket-server 64293)))
(let ((header "HTTP/1.0 200 OK\nContent-Type: application/json\n")
(body (json-encode '(:ProtocolVersion 1 :WebSocketPort 64293))))
(process-send-string proc (concat header "\n" body))
(process-send-eof proc))))
;;;###autoload
(defun atomic-chrome-start-server ()
"Start websocket server for atomic-chrome.
Fails silently if a server is already running."
(interactive)
(ignore-errors
(progn
(and (not atomic-chrome-server-atomic-chrome)
(memq 'atomic-chrome atomic-chrome-extension-type-list)
(setq atomic-chrome-server-atomic-chrome
(atomic-chrome-start-websocket-server 64292)))
(and (not (process-status "atomic-chrome-httpd"))
(memq 'ghost-text atomic-chrome-extension-type-list)
(atomic-chrome-start-httpd))
(global-atomic-chrome-edit-mode 1))))
;;;###autoload
(defun atomic-chrome-stop-server nil
"Stop websocket server for atomic-chrome."
(interactive)
(when atomic-chrome-server-atomic-chrome
(websocket-server-close atomic-chrome-server-atomic-chrome)
(setq atomic-chrome-server-atomic-chrome nil))
(when atomic-chrome-server-ghost-text
(websocket-server-close atomic-chrome-server-ghost-text)
(setq atomic-chrome-server-ghost-text nil))
(when (process-status "atomic-chrome-httpd")
(delete-process "atomic-chrome-httpd"))
(global-atomic-chrome-edit-mode 0))
(provide 'atomic-chrome)
;;; atomic-chrome.el ends here