-
Notifications
You must be signed in to change notification settings - Fork 4
/
voicemacs-base.el
368 lines (285 loc) · 13.4 KB
/
voicemacs-base.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
(require 'json)
(require 'voicemacs-server)
(defgroup voicemacs nil
"Utilities to make Emacs easier to control by voice."
:prefix "voicemacs-")
;; TODO: Use a set, not a list - more efficient
(defvar voicemacs--exposed-functions '()
"Functions that should be exposed by voicemacs.")
(defvar voicemacs--data (make-hash-table :test 'equal)
"Data that should be mirrored to clients.")
(defun voicemacs-expose-function (func)
"Expose `FUNC' over the RPC server (when in `voicemacs-mode')."
(unless (member func voicemacs--exposed-functions)
(push func voicemacs--exposed-functions)))
(defun voicemacs-remove-function (func)
"Remove a function exposed with `voicemacs-expose-function'."
(setq voicemacs--exposed-functions
(remove func voicemacs--exposed-functions)))
(cl-defun voicemacs--queue-once (func &key (args '()) (time 0))
"Queue a function to run once, when Emacs is next idle.
If the function is already queued, it will be replaced (and the
replacement will use the new timing). Note it will also remove
the function from ordinary timers."
(cancel-function-timers func)
(apply #'run-with-idle-timer (append (list time nil func) args)))
(defun voicemacs--equal (item-1 item-2)
"Check equality of two objects - tolerates equivalent hash maps.
Will raise an error if the items can't be serialized."
(or (equal item-1 item-2)
;; Check JSON forms so we can tolerate hash maps.
;;
;; HACK: JSON comparison as a standin for in-depth compare function.
;; Replace this at some point.
(string= (json-rpc-server--emulate-legacy-encode item-1)
(json-rpc-server--emulate-legacy-encode item-2))))
(defun voicemacs-update-data (key value)
"Update data `key' to be `value', and send the new value to the client."
(puthash key value voicemacs--data)
;; TODO: Fix circular imports
(voicemacs--broadcast-update key value))
(defun voicemacs--update-if-changed (key value)
"Update data under `key', iff `value' is different."
(unless (condition-case nil
(voicemacs--equal value (voicemacs--get-data key))
(wrong-type-argument
;; HACK: Some strings can't be serialized - if that happens, return
;; `t' to suppress updates.
t))
(voicemacs-update-data key value)))
(defun voicemacs--hash-subset (hash-table keys)
"Get a subset of a hash table with only the keys in `keys'."
(let ((subset (make-hash-table)))
(mapc (lambda (key)
(puthash key (gethash key hash-table) subset))
keys)
subset))
(defun voicemacs--get-data (key)
"Get the current value of `key' in `voicemacs--data'."
(gethash key voicemacs--data))
(defvar voicemacs--sync-setup-hook '()
"Hook run when voicemacs enables synchronization.
Don't hook functions to this hook directly - use
`voicemacs--add-sync'.")
(defvar voicemacs--sync-teardown-hook '()
"Hook run when voicemacs disables synchronization.
Don't hook functions to this hook directly - use
`voicemacs--add-sync'.")
;; TODO: Improve documentation
(defun voicemacs--sync-add (setup teardown)
"Add a new data synchronizer to `voicemacs-mode'.
`SETUP' is the function to run when synchronization is enabled.
`TEARDOWN' is the function to run when synchornization is
disabled.
Note that if `voicemacs-mode' is active, any previous versions of
`SETUP' will not be torn down. That needs to be done manually
before calling `voicemacs--sync-add'."
;; Need to fire it manually if `voicemacs-mode' is already active.
(when voicemacs-mode
(funcall setup))
(add-hook 'voicemacs--sync-setup-hook setup)
(add-hook 'voicemacs--sync-teardown-hook teardown))
(defun voicemacs--mode-disable ()
"Post-enable hook for `voicemacs-mode'."
(cancel-function-timers 'voicemacs--maybe-restart-server)
;; Disable syncing
(run-hooks 'voicemacs--sync-teardown-hook)
;; Defensive, also free the memory
(clrhash voicemacs--data)
;; Now stop the server.
(voicemacs--stop-server)
)
(defun voicemacs--maybe-restart-server ()
"If the server is dead (and it shouldn't be), restart it."
(when (and voicemacs-mode
(not voicemacs--server-process))
(message "Voicemacs server has been killed erroneously. Restarting it.")
(voicemacs--start-server)))
(defun voicemacs--running-under-wsl ()
"Is this Emacs instance running under Windows Subsystem for Linux?"
(let ((uname (shell-command-to-string "uname -a")))
(and (string-match-p "Linux" uname)
(string-match-p "Microsoft" uname))))
(defun voicemacs--mode-enable ()
"Post-disable hook for `voicemacs-mode'."
;; Start server
(voicemacs--start-server)
(message "Voicemacs server started")
;; HACK: Some packages kill the server erroneously, e.g. `persp-mode' when it
;; restores the previous perspective. Don't try and play whack-a-mole with
;; them, just poll & restart.
(run-with-idle-timer 0.01 0 'voicemacs--maybe-restart-server)
;; Setup sync
(clrhash voicemacs--data)
(run-hooks 'voicemacs--sync-setup-hook)
;; TODO: Maybe put a signifier that voicemacs is active in the title?
)
(defun voicemacs--format-symbol (string-spec &rest args)
"Intern a symbol from a format string."
(intern (apply 'format string-spec args)))
(defun voicemacs--define-sync-docstring (string-spec &rest args)
"Create a docstring for a function generated by `voicemacs--define-sync'."
(concat (apply 'format string-spec args)
"\n\n"
"This is an auto-generated function created by `voicemacs-define-sync'."))
(cl-defmacro voicemacs-define-sync (key &key
(update 'NOT-PROVIDED)
(enable 'NOT-PROVIDED)
(disable 'NOT-PROVIDED)
(defer t)
(delay nil))
"Register a new `KEY' to synchronize.
`KEY' should be an unquoted symbol, the key to synchronize.
This method automatically generates a number of functions that
handle synchronization (and hooks them):
`voicemacs--sync-<key>' - immediately evaluate a new value for
`KEY', and synchronize it. `:UPDATE' should be a form that will
be evaluated to get the new value.
`voicemacs--queue-sync-<key>' - queue `voicemacs--sync-<key>' to
fire a single time, once Emacs is idle. This can be used to
defer expensive synchronizations so they only happen once (when
activity has finished).
`:DEFER' determines which of these is used as the default
synchronization function within the enable & disable
functions (described below). When `:DEFER' is t,
`voicemacs--queue-sync-<key>' will be used. When `:DEFER' is nil,
it's `voicemacs--sync-<key>'.
`voicemacs--enable-sync-<key>' - enable synchronization of this
key. The `:ENABLE' form will be evaluated in this function to
set up synchronization (e.g. when `voicemacs-mode' is enabled).
For example, you may wish to attach the sync function to a
hook, or add it as advice. Use the variable `sync-func' within
this form to reference the default sync function.
`voicemacs--disable-sync-<key>' - disable synchronization. The
`:DISABLE' form will be evaluated in this function to disable
synchronization (e.g. when `voicemacs-mode' is disabled). It
should reverse the effects of `voicemacs--enable-sync-<key>'.
Use the variable `sync-func' within this form to reference the
default sync function.
Pass `:DELAY' (in seconds) to increase the time before a queued
sync fires. `:DELAY' can only be passed when `:DEFER' is t.
If `voicemacs-mode' is already active, synchronization of this
key will be enabled immediately.
---
Usage examples:
(voicemacs-define-sync in-comment
:update (if (voicemacs-in-comment-p) t :json-false)
:enable (run-with-idle-timer 0 0 sync-func)
:disable (cancel-function-timers sync-func)
:defer nil)
(voicemacs-define-sync defined-commands
:update (voicemacs--defined-commands)
:enable (advice-add 'defun :after sync-func)
:disable (advice-remove 'defun sync-func)
:defer t
:delay 1)
For more examples, see native uses within the `voicemacs'
source."
(declare (indent 1))
;; Cache expansions
`(let* ((-key (quote ,key))
(-update-form (quote ,update))
(-defer ,defer)
(-enable-form (quote ,enable))
(-disable-form (quote ,disable))
(-delay ,delay))
;; Validate inputs
(when (member 'NOT-PROVIDED
(list -key -update-form -enable-form -disable-form))
(error "Must provide each of `KEY', `UPDATE', `ENABLE' & `DISABLE'"))
(when (and -delay (not -defer))
(error "`DEFER' must be `t' if a `DELAY' is provided."))
(unless (and (or (symbolp -key) (stringp -key))
(string-match-p "[-a-zA-Z0-9]+" (format "%s" -key)))
;; Restrict the key format so the client can deal with it easily.
(error "`KEY' may only contain letters, numbers and dashes"))
;; Generate function names
(let* ((immediate-sync-func (voicemacs--format-symbol
"voicemacs--sync-%s" -key))
(deferred-sync-func (voicemacs--format-symbol
"voicemacs--queue-sync-%s" -key))
(enable-sync-func (voicemacs--format-symbol
"voicemacs--enable-sync-%s" -key))
(disable-sync-func (voicemacs--format-symbol
"voicemacs--disable-sync-%s" -key))
(sync-action (if -defer deferred-sync-func immediate-sync-func)))
;; Tear down the previous version of the sync, if necessary
(when (and voicemacs-mode (fboundp disable-sync-func))
(with-demoted-errors "Error reversing previous Voicemacs sync:\n%s"
(funcall disable-sync-func)))
;; Define function to sync immediately
(eval `(defun ,immediate-sync-func (&rest _)
,(voicemacs--define-sync-docstring
(concat "Evaluate & synchronize the state of sync key `%s'."
"\n\nState will only be updated if it has changed.")
-key)
(voicemacs--update-if-changed (quote ,-key) ,-update-form)))
;; Define function to sync only when idle
(eval `(defun ,deferred-sync-func (&rest _)
,(voicemacs--define-sync-docstring
"Queue sync of key `%s' once idle." -key)
(voicemacs--queue-once (quote ,immediate-sync-func)
:time (or ,-delay 0))))
;; Define functions that set up & tear down syncing
(eval `(defun ,enable-sync-func ()
,(voicemacs--define-sync-docstring
"Enable synchronization of `%s'" -key)
(let ((sync-func (quote ,sync-action)))
,-enable-form)
;; Sync current state up-front. Don't delay because if
;; `ENABLE-FORM' set up e.g. a repeating idle timer, a delay
;; could interfere.
(,immediate-sync-func)))
(eval `(defun ,disable-sync-func ()
,(voicemacs--define-sync-docstring
"Disable synchronization of `%s'" -key)
;; TODO: Reset data here? Is it reset somewhere else?
(let ((sync-func (quote ,sync-action)))
,-disable-form)))
;; Finally, register the generated setup & teardown functions
(voicemacs--sync-add enable-sync-func disable-sync-func))))
(cl-defmacro voicemacs-define-sync-change-buffer (key &key
(update 'NOT-PROVIDED)
(defer t)
(delay nil))
"Add a data key that synchronizes on buffer change.
Like `voicemacs-define-sync', but `:enable' and `:disable' are
handled for you - they will bind `sync-func' to a buffer change.
See `voicemacs-define-sync' for documentation on the remaining
parameters."
(declare (indent 1))
`(voicemacs-define-sync ,key
:update ,update
:enable (voicemacs--hook-change-buffer sync-func)
:disable (voicemacs--unhook-change-buffer sync-func)
:defer ,defer
:delay ,delay))
(define-minor-mode voicemacs-mode
"Minor mode to allow voice recognition software to communicate with Emacs."
:group 'voicemacs
:global t
:lighter nil
:after-hook (if voicemacs-mode
(voicemacs--mode-enable)
(voicemacs--mode-disable)))
(defun voicemacs--hook-change-buffer (func)
"Hook a function to fire whenever the active buffer changes.
This includes switching windows.
There's no built-in hook for this so the current implementation
is a heuristic. It has a huge false-positive rate; `func' will
fire often, even if the buffer hasn't changed. Don't hook slow
functions."
;; TODO: Observe buffer changes more directly, these are dodgy heuristics.
;; TODO: Maybe hook buffer change? Have a look at it. Idle might be enough.
(add-hook 'after-change-major-mode-hook func)
;; This is a reasonable proxy for when we're switching the buffer.
(add-hook 'post-command-hook func)
;; Run it idly to catch events that fall through the net.
(run-with-idle-timer 1 0 func))
(defun voicemacs--unhook-change-buffer (func)
"Unhook a function that was hooked with `voicemacs--hook-change-buffer'."
(remove-hook 'after-change-major-mode-hook func)
(remove-hook 'post-command-hook func)
(cancel-function-timers func))
(provide 'voicemacs-base)
;;; voicemacs-base.el ends here