forked from sunrise-commander/sunrise-commander
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsunrise-loop.el
357 lines (303 loc) · 14.4 KB
/
sunrise-loop.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
;;; sunrise-loop.el --- Background file operations for the Sunrise Commander -*- lexical-binding: t -*-
;; Copyright (C) 2008-2012 José Alfredo Romero Latouche.
;; SPDX-License-Identifier: GPL-3.0-or-later
;; Author: José Alfredo Romero Latouche <escherdragon@gmail.com>
;; Štěpán Němec <stepnem@gmail.com>
;; Maintainer: José Alfredo Romero Latouche <escherdragon@gmail.com>
;; Created: 27 Jun 2008
;; Version: 3
;; Package-Requires: ((emacs "24.4"))
;; Keywords: files, sunrise commander, background copy rename move
;; URL: https://github.com/sunrise-commander/sunrise-commander
;; This file is not part of GNU Emacs.
;; 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 3 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 de- tails.
;; You should have received a copy of the GNU General Public License along
;; with this program. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; This extension adds to the Sunrise Commander the capability of performing
;; copy and rename operations in the background. It provides prefixable
;; drop-in replacements for the `sunrise-do-copy' and `sunrise-do-rename'
;; commands and uses them to redefine their bindings in the `sunrise-mode-map'
;; keymap. When invoked the usual way (by pressing C or R), these new
;; functions work exactly as the old ones, i.e. they simply pass the control
;; flow to the logic already provided by Sunrise, but when prefixed (e.g. by
;; pressing C-u C or C-u R) they launch a separate Elisp intepreter in the
;; background, delegate to it the execution of all further operations and
;; return immediately, so the Emacs UI remains fully responsive while any
;; potentially long-running copy or move tasks can be let alone to eventually
;; reach their completion in the background.
;; After all requested actions have been performed, the background interpreter
;; remains active for a short period of time (30 seconds by default, but it
;; can be customized), after which it shuts down automatically.
;; At any moment you can abort all tasks scheduled and under execution and
;; force the background interpreter to shut down by invoking the
;; `sunrise-loop-stop' command (M-x sunrise-loop-stop).
;; If you need to debug something or are just curious about how this extension
;; works, you can set the variable `sunrise-loop-debug' to t to have the
;; interpreter launched in debug mode. In this mode all input and output of
;; background operations are sent to a buffer named *SUNRISE-LOOP*. To return
;; to normal mode set `sunrise-loop-debug' back to nil and use
;; `sunrise-loop-stop' to kill the currently running interpreter.
;; The extension disables itself and tries to do its best to keep out of the
;; way when working with remote directories through FTP (e.g. when using
;; ange-ftp), since in these cases the execution of file transfers in the
;; background should be managed directly by the FTP client.
;; It was written on GNU Emacs 23 on Linux, and tested on GNU Emacs 22 and 23
;; for Linux and on EmacsW32 (version 22) for Windows.
;;; Installation and Usage:
;; 1) Put this file somewhere in your Emacs `load-path'.
;; 2) Add a (require 'sunrise-loop) expression to your .emacs file somewhere
;; after the (require 'sunrise) one.
;; 3) Evaluate the new expression, or reload your .emacs file, or restart
;; Emacs.
;; 4) The next time you need to copy of move any big files, just prefix the
;; appropriate command with C-u.
;; 5) Enjoy ;-)
;; 6) You can use `unload-feature' to get rid of the provided functionality
;; completely.
;;; Code:
(require 'sunrise)
(defcustom sunrise-loop-debug nil
"Activate debug mode in the Sunrise Loop extension.
When set, the background elisp interpreter is launched in such a
way that all background input and output are sent to a buffer
named *SUNRISE LOOP* and automatic lifecycle management is
disabled (i.e. you have to kill the interpreter manually using
sunrise-loop-stop to get rid of it)."
:group 'sunrise
:type 'boolean)
(defcustom sunrise-loop-timeout 30
"Number of seconds to wait while idle before shutting down the interpreter.
After executing one or more operations in the background, the
Sunrise Loop Elisp interpreter will be killed automatically after
this amount of time."
:group 'sunrise
:type 'integer)
(defcustom sunrise-loop-use-popups t
"When non-nil, display pop-up notification when execution queue is emptied."
:group 'sunrise
:type 'boolean)
(defvar sunrise-loop-process nil)
(defvar sunrise-loop-timer nil)
(defvar sunrise-loop-scope nil)
(defvar sunrise-loop-queue nil)
(defun sunrise-loop-start ()
"Launch and initiate a new background Elisp interpreter.
The new interpreter runs in batch mode and inherits all functions
from the Sunrise Commander (sunrise.el) and from this
file."
(let ((process-connection-type nil)
(sunrise-main (symbol-file 'sunrise-mode))
(sunrise-loop (symbol-file 'sunrise-loop-cmd-loop))
(emacs (concat invocation-directory invocation-name)))
(setq sunrise-loop-process (start-process
"Sunrise-Loop"
(if sunrise-loop-debug "*SUNRISE-LOOP*" nil)
emacs
"-batch" "-q" "-no-site-file"
"-l" sunrise-main "-l" sunrise-loop
"-eval" "(sunrise-loop-cmd-loop)"))
(sunrise-loop-enqueue `(setq load-path (quote ,load-path)))
(sunrise-loop-enqueue '(require 'sunrise))
(if sunrise-loop-debug
(sunrise-loop-enqueue '(setq sunrise-loop-debug t))
(set-process-filter sunrise-loop-process 'sunrise-loop-filter))
(setq sunrise-loop-queue nil)))
(defun sunrise-loop-disable-timer ()
"Disable the automatic shutdown timer.
This is done every time we send a new task to the background
interpreter, lest it gets nuked before completing its queue."
(if sunrise-loop-timer
(progn
(cancel-timer sunrise-loop-timer)
(setq sunrise-loop-timer nil))))
(defun sunrise-loop-enable-timer ()
"Enable the automatic shutdown timer.
This is done every time we receive confirmation from the
background interpreter that all the tasks delegated to it have
been completed. Once this function is executed, if no new tasks
are enqueued before `sunrise-loop-timeout' seconds, the interpreter is
killed."
(sunrise-loop-disable-timer)
(setq sunrise-loop-timer
(run-with-timer sunrise-loop-timeout nil 'sunrise-loop-stop)))
(defun sunrise-loop-stop (&optional interrupt)
"Shut down the background Elisp interpreter and clean up after it.
If INTERRUPT is non-nil, force an immediate shutdown."
(interactive "p")
(sunrise-loop-disable-timer)
(when sunrise-loop-queue
(cond (interrupt
(sunrise-loop-notify
"Aborted. Some operations may remain unfinished.")
(setq sunrise-loop-queue nil))
(t (sunrise-loop-enable-timer))))
(unless sunrise-loop-queue
(delete-process sunrise-loop-process)
(setq sunrise-loop-process nil)))
(defun sunrise-loop-notify (string)
"Show message STRING to notify the user about an event."
(if (and window-system sunrise-loop-use-popups)
(x-popup-dialog t (list string '("OK")) t)
(message (concat "[[" string "]]"))))
(defun sunrise-loop-filter (_process output)
"Process filter for the background interpreter.
OUTPUT is partial output from the interpreter."
(mapc (lambda (line)
(cond ((string-match "^\\[\\[\\*\\([^\]\*]+\\)\\*\\]\\]$" line)
(sunrise-loop-notify (match-string 1 line)))
((and (or (string-match "^\\[\\[" line)
(string-match "^Sunrise Loop: " line))
(< 0 (length line)))
(message "%s" line))
((eq ?^ (string-to-char line))
(let ((command (substring line 1)))
(when (string= command (car sunrise-loop-queue))
(pop sunrise-loop-queue)
(sunrise-loop-enable-timer)
(unless sunrise-loop-queue
(sunrise-loop-notify "Background job finished!")))))
(t nil)))
(split-string output "\n")))
(defun sunrise-loop-enqueue (form)
"Delegate evaluation of FORM to the background interpreter.
If no such interpreter is currently running, launches a new one."
(sunrise-loop-disable-timer)
(unless sunrise-loop-process
(sunrise-loop-start))
(let ((command (prin1-to-string form)))
(setq sunrise-loop-queue (append sunrise-loop-queue (list (md5 command))))
(process-send-string sunrise-loop-process command)
(process-send-string sunrise-loop-process "\n")))
(defun sunrise-loop-cmd-loop ()
"Main execution loop for the background Elisp interpreter."
(sunrise-ad-disable "^sunrise-loop-")
(defun read-char nil ?y) ;; Always answer "yes" to any prompt
(let ((command) (signature))
(while t
(setq command (read))
(setq signature (md5 (prin1-to-string command)))
(condition-case description
(progn
(if sunrise-loop-debug
(message "%s" (concat "[[Executing in background: "
(prin1-to-string command) "]]")))
(eval command)
(message "[[Command successfully invoked in background]]"))
(error (message "%s" (concat "[[*ERROR IN BACKGROUND JOB: "
(prin1-to-string description) "*]]"))))
(message "^%s" signature))))
(defun sunrise-loop-applicable-p ()
"Return non-nil if an operation is suitable for the background interpreter."
(and (null (string-match "^/ftp:" dired-directory))
(null (string-match "^/ftp:" sunrise-other-directory))))
(defun sunrise-loop-do-copy (&optional arg)
"Drop-in prefixable replacement for the `sunrise-do-copy' command.
When invoked with a prefix argument ARG, sets a flag that is used
later by advice to decide whether to delegate further copy
operations to the background interpreter."
(interactive "P")
(if (and arg (sunrise-loop-applicable-p))
(let ((sunrise-loop-scope t))
(sunrise-do-copy))
(sunrise-do-copy)))
(defun sunrise-loop-do-clone (&optional arg)
"Drop-in prefixable replacement for the `sunrise-do-clone' command.
When invoked with a prefix argument ARG, sets a flag that is used
later by advice to decide whether to delegate further copy
operations to the background interpreter."
(interactive "P")
(if (and arg (sunrise-loop-applicable-p))
(let ((sunrise-loop-scope t))
(call-interactively 'sunrise-do-clone))
(call-interactively 'sunrise-do-clone)))
(defun sunrise-loop-do-rename (&optional arg)
"Drop-in prefixable replacement for the `sunrise-do-rename' command.
When invoked with a prefix argument ARG, sets a flag that is used
later by advice to decide whether to delegate further rename
operations to the background interpreter."
(interactive "P")
(if (and arg (sunrise-loop-applicable-p))
(let ((sunrise-loop-scope t))
(sunrise-do-rename))
(sunrise-do-rename)))
(defadvice sunrise-progress-prompt (around sunrise-loop-advice-progress-prompt
activate)
"Display \"Sunrise Loop\" instead of \"Sunrise\" in the prompt."
(setq ad-return-value
(concat (if sunrise-loop-scope "Sunrise Loop: " "Sunrise: ")
(ad-get-arg 0)
"...")))
(defadvice y-or-n-p (before sunrise-loop-advice-y-or-n-p activate)
"Modify all confirmation request messages inside a loop scope."
(when sunrise-loop-scope
(setq (ad-get-arg 0)
(replace-regexp-in-string
"\?" " in the background? (overwrites ALWAYS!)" (ad-get-arg 0)))))
(defadvice dired-mark-read-file-name
(before sunrise-loop-advice-dired-mark-read-file-name
(prompt dir op-symbol arg files &optional default)
activate)
"Modify all queries from Dired inside a loop scope."
(if sunrise-loop-scope
(setq prompt (replace-regexp-in-string
"^\\([^ ]+\\) ?\\(.*\\)"
"\\1 (in background - overwrites ALWAYS!) \\2" prompt))))
(defadvice dired-create-files
(around sunrise-loop-advice-dired-create-files
(file-creator operation fn-list name-constructor
&optional marker-char)
activate)
"Delegate `dired-do-copy' ops in a loop to background interpreter."
(if sunrise-loop-scope
(with-no-warnings
(sunrise-loop-enqueue
`(let ((target ,target)) ; cf. `dired-do-create-files'
(dired-create-files (function ,file-creator)
,operation
(quote ,fn-list)
,name-constructor nil))))
ad-do-it))
(defadvice sunrise-clone-files
(around sunrise-loop-advice-clone-files
(file-path-list
target-dir
clone-op
progress
&optional do-overwrite)
activate)
"Delegate `sunrise-do-copy' ops in a loop to background interpreter."
(if sunrise-loop-scope
(sunrise-loop-enqueue
`(sunrise-clone-files
(quote ,file-path-list) ,target-dir #',clone-op ',progress 'ALWAYS))
ad-do-it))
(defadvice sunrise-move-files
(around sunrise-loop-advice-move-files
(file-path-list target-dir progress &optional do-overwrite)
activate)
"Delegate `sunrise-do-rename' ops in a loop to background interpreter."
(if sunrise-loop-scope
(sunrise-loop-enqueue
`(sunrise-move-files
(quote ,file-path-list) ,target-dir ',progress 'ALWAYS))
ad-do-it))
(define-key sunrise-mode-map "C" 'sunrise-loop-do-copy)
(define-key sunrise-mode-map "K" 'sunrise-loop-do-clone)
(define-key sunrise-mode-map "R" 'sunrise-loop-do-rename)
(defun sunrise-loop-unload-function ()
"Unload the Sunrise Commander loop extension."
(sunrise-ad-disable "^sunrise-loop-")
(define-key sunrise-mode-map "C" 'sunrise-do-copy)
(define-key sunrise-mode-map "K" 'sunrise-do-clone)
(define-key sunrise-mode-map "R" 'sunrise-do-rename))
(provide 'sunrise-loop)
;;; sunrise-loop.el ends here