-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathpullover.el
More file actions
248 lines (210 loc) · 9.23 KB
/
Copy pathpullover.el
File metadata and controls
248 lines (210 loc) · 9.23 KB
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
;;; pullover.el --- Pulling other apps over to do things -*- lexical-binding: t; coding: utf-8 -*-
;; Copyright (C) 2019 Tuấn-Anh Nguyễn
;;
;; Author: Tuấn-Anh Nguyễn <ubolonton@gmail.com>
;; Keywords: convenience, mac
;; Homepage: https://github.com/ubolonton/pullover
;; Version: 0.1.1
;; Package-Requires: ((emacs "25.1"))
;; License: MIT
;;; Commentary:
;; This package allows extracting text from other apps, editing it in Emacs, then sending it back.
;; It is useful when you have to work with apps (or web pages) that have poor editing support, like
;; bad key bindings, small edit areas, or little syntax highlighting.
;;; Code:
(eval-and-compile
(unless (equal system-type 'darwin)
(error "Package pullover currently only works on macOS")))
(unless (member window-system '(mac ns))
(error "Package pullover requires a GUI"))
(require 'pullover-osa)
(require 'pullover-dyn)
;;; TODO: Consider allowing multiple pullover sessions at the same time.
(defvar pullover--buffer nil)
(eval-and-compile
(defvar pullover--debug nil))
(defvar pullover--clipboard-state)
(defvar pullover--tmp-dir "~/.emacs.d/.pullover")
(defvar-local pullover--app nil)
(put 'pullover--app 'permanent-local t)
(defun pullover--save-string (new file)
"Save NEW string in FILE. Return the old content of the file.
If they are the same, return the symbol `unchanged'."
(let ((old (condition-case _
(with-temp-buffer
(insert-file-contents file)
(buffer-string))
(file-missing nil))))
(if (equal old new)
'unchanged
(with-temp-file file
(insert new))
old)))
(defcustom pullover-emacsclient-command (pcase window-system
(`mac "/Applications/EmacsMac.app/Contents/MacOS/bin/emacsclient")
(`ns "/Applications/Emacs.app/Contents/MacOS/bin/emacsclient")
(_ "emacsclient"))
"Command used to invoke emacsclient.
Set this if your Emacs.app's emacsclient is not on the shell's search path."
:group 'pullover
:type 'string
:set (lambda (symbol value)
(mkdir pullover--tmp-dir t)
(pullover--save-string value
(concat (file-name-as-directory pullover--tmp-dir) "emacsclient_cmd"))
(set-default symbol value)))
(defcustom pullover-clipboard-timeout 100
"Number of milliseconds to wait for the external app to copy the text."
:group 'pullover
:type 'integer)
(defcustom pullover-major-mode 'text-mode
"Major mode to use for pullover sessions."
:group 'pullover
:type 'function)
(defvar pullover-copy-text-function #'pullover-dyn--copy-text
"Function used to copy text from the specified app into the clipboard.
This is ignored if the wrapper script is used, which is the recommended way.
Possible values are:
- `pullover-dyn--copy-text': Shows a notification message if it takes more than
1 second to copy text.
- `pullover-osa--copy-text': May be a little bit faster, but doesn't show any
notification message.")
(defvar pullover-paste-text-function #'pullover-dyn--paste-text
"Function used to paste text from the clipboard into the specified app.
This is ignored if the wrapper script is used, which is the recommended way.")
(defcustom pullover-activate-app-function #'pullover-dyn--activate-app
"Function used to activate the specified app."
:group 'pullover
:type 'function)
(defmacro pullover--bench (text &rest body)
"Eval BODY, printing the run time, prefixed with TEXT."
(declare (indent 1))
(if pullover--debug
`(let ((pullover--result))
(message "%s %s" ,text
(benchmark-run (setq pullover--result ,@body)))
pullover--result)
`(progn ,@body)))
;;;###autoload
(defun pullover-activate-emacs ()
"Switch to Emacs, making in the frontmost app."
(funcall pullover-activate-app-function "org.gnu.Emacs"))
(defmacro pullover-with-clipboard-wait (timeout &rest body)
"Wait for a new item to appear in the clipboard, while evaluating BODY.
This is typically used to check whether the evaluation of BODY copied a new item
to the clipboard, directly or asynchronously.
Return (RESULT . CHANGE-COUNT), where CHANGE-COUNT represents the latest state
of the clipboard. If there's no new item in the clipboard after TIMEOUT
milliseconds, return (RESULT . nil). The time taken to evaluate BODY is ignored.
This will block the current thread. Therefore it's recommended to use a small
value for TIMEOUT."
(declare (indent 1))
`(let* ((start (pullover-dyn--change-count))
(result (progn ,@body)))
(cons result (pullover-dyn--wait-for-clipboard ,timeout start))))
;;;###autoload
(defun pullover-checkpoint-clipboard ()
"Return Emacs's PID after recording current clipboard's state.
This should only be used by the external-wrapper flow. See
`pullover-start-or-finish' for more details."
(setq pullover--clipboard-state (pullover-dyn--change-count))
(emacs-pid))
;;;###autoload
(defun pullover-start-or-finish (&optional app)
"Start an editing session by opening a new buffer after waiting for new text.
If APP is specified, assume that copying was done externally by the wrapper
script, which should have called `pullover-checkpoint-clipboard' in advance.
This should only be used in the external-wrapper flow.
If Emacs is the current app, this function calls `pullover-finish' to finalize
the editing session."
(pcase-let
((`((,app . ,change-count) . ,is-external)
(pcase app
(`nil
;; TODO: Avoid blocking the main thread like this. One way to do it is making a background
;; thread that signals the main thread upon completion, with `thread-yield'. However, that
;; currently results in Emacs being aborted (gc_in_progress || waiting_for_input)'.
(cons (pullover-with-clipboard-wait pullover-clipboard-timeout
;; TODO: Terminate the clipboard wait if `app' is nil.
(pullover--bench "copy-text "
(funcall pullover-copy-text-function nil)))
nil))
;; The wrapper scirpt decided that Emacs is the current app.
(""
(cons (cons nil nil)
t))
;; The wrapper script returned the current app identifier.
(app
(cons (cons
app
(pullover-dyn--wait-for-clipboard pullover-clipboard-timeout pullover--clipboard-state))
t)))))
(if (null app)
(progn
(message "Invoked while inside Emacs. Trying to finish a pullover session ...")
(pullover-finish is-external))
;; TODO: If there's already another on-going, ask user what to do.
(when (buffer-live-p pullover--buffer)
(kill-buffer pullover--buffer))
(setq pullover--buffer (generate-new-buffer app))
(pullover-activate-emacs)
(switch-to-buffer pullover--buffer)
(setq pullover--app app)
;; Paste only if the item in the clipboard is new (from the other app).
(when change-count
(clipboard-yank))
;; TODO: Use an app-to-major-mode mapping.
(when (fboundp pullover-major-mode)
(funcall pullover-major-mode))
(pullover-mode +1)
;; This is to let the wrapper script know it's a new session.
'started)))
(defun pullover-finish (&optional no-paste)
"Finish an editing session, sending the edited text back to original app.
If NO-PASTE is t, just copy the buffer's text to the clipboard, leaving it to
the wrapper script to paste the text. This should only be used in the
external-wrapper flow.
Return the identifier of the original app."
(interactive)
(unless (buffer-live-p pullover--buffer)
(error "No pullover session"))
(with-current-buffer pullover--buffer
;; XXX: This is to work around `copy-region-as-kill' trying to append text if `last-command' is
;; `kill-region'. TODO: Find a more reliable way to put buffer's text into the clipboard.
(let ((last-command nil))
(clipboard-kill-ring-save (point-min) (point-max)))
(let ((app pullover--app))
(unwind-protect
(unless no-paste
(pullover--bench "paste-text"
(funcall pullover-paste-text-function pullover--app)))
(funcall pullover-activate-app-function pullover--app)
(setq pullover--buffer nil)
(kill-buffer))
app)))
(defun pullover-cancel ()
"Cancel an editing session, switching back to the original app.
The text being edited is discarded."
(interactive)
(unless (buffer-live-p pullover--buffer)
(error "No pullover session"))
(with-current-buffer pullover--buffer
(unwind-protect
(funcall pullover-activate-app-function pullover--app)
(kill-buffer)
(setq pullover--buffer nil))))
;;; TODO: I'm not sure these bindings make sense.
(defvar pullover-mode-map
(let ((map (make-sparse-keymap)))
(define-key map [remap server-edit] 'pullover-cancel)
(define-key map [remap kill-buffer] 'pullover-cancel)
map)
"Keymap of `pullover-mode'.")
(define-minor-mode pullover-mode
"Minor mode for editing text grabbed from another app, then sending it back."
:init-value nil
:lighter "pullover"
:keymap pullover-mode-map
())
(provide 'pullover)
;;; pullover.el ends here