forked from Malabarba/paradox
-
Notifications
You must be signed in to change notification settings - Fork 0
/
paradox-execute.el
393 lines (357 loc) · 15.1 KB
/
paradox-execute.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
392
393
;;; paradox-execute.el --- executing package transactions -*- lexical-binding:t -*-
;; Copyright (C) 2014-2015 Artur Malabarba <[email protected]>
;; Author: Artur Malabarba <[email protected]>
;; Prefix: paradox
;; Separator: -
;;; License:
;;
;; 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 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.
;;
;;; Commentary:
;;
;; Functions related to executing package-menu transactions.
;; Everything that happens when you hit `x' is in here.
;;; Code:
(require 'cl-lib)
(require 'seq)
(require 'package)
(require 'paradox-core)
(require 'paradox-github)
(defgroup paradox-execute nil
"Paradox Packages Menu configurations."
:prefix "paradox-"
:package-version '(paradox . "2.0")
:group 'paradox)
(defvar paradox--current-filter)
;;; Customization Variables
(defcustom paradox-execute-asynchronously 'ask
"Whether the install/delete/upgrade should be asynchronous.
Possible values are:
t, which means always;
nil, which means never;
ask, which means ask each time."
:type '(choice (const :tag "Always" t)
(const :tag "Never" nil)
(const :tag "Ask each time" ask))
:package-version '(paradox . "2.0")
:group 'paradox-execute)
(defcustom paradox-async-display-buffer-function #'display-buffer
"Function used to display *Paradox Report* buffer after asynchronous upgrade.
Set this to nil to avoid displaying the buffer. Or set this to a
function like `display-buffer' or `pop-to-buffer'.
This is only used if `paradox-menu-execute' was given a non-nil
NOQUERY argument. Otherwise, only a message is displayed."
:type '(choice (const :tag "Don't display the buffer" nil)
function)
:package-version '(paradox . "2.0")
:group 'paradox-execute)
;;; Execution Hook
(defvar paradox-after-execute-functions nil
"List of functions run after performing package transactions.
These are run after a set of installation, deletion, or upgrades
has been performed. Each function in this hook must take a single
argument. An associative list of the form
((SYMBOL . DATA) (SYMBOL . DATA) ...)
This list contains the following entries, describing what
occurred during the execution:
SYMBOL DATA
`installed' List of installed packages.
`deleted' List of deleted packages.
`activated' List of activated packages.
`error' List of errors.
`async' Non-nil if transaction was performed asynchronously.
`noquery' The NOQUERY argument given to `paradox-menu-execute'.")
(put 'risky-local-variable-p 'paradox-after-execute-functions t)
(mapc (lambda (x) (add-hook 'paradox-after-execute-functions x t))
'(paradox--activate-if-asynchronous
paradox--refresh-package-buffer
paradox--report-buffer-print
paradox--report-buffer-display-if-noquery
paradox--report-message
))
(defun paradox--refresh-package-buffer (_)
"Refresh the *Packages* buffer, if it exists."
(let ((buf (get-buffer "*Packages*")))
(when (buffer-live-p buf)
(with-current-buffer buf
(revert-buffer)))))
(defun paradox--activate-if-asynchronous (alist)
"Activate packages after an asynchronous operation.
Argument ALIST describes the operation."
(let-alist alist
(when .async
(dolist (pkg .activated)
(if (fboundp 'package--list-loaded-files)
(package-activate-1 pkg 'reload)
(package-activate-1 pkg))))))
(defun paradox--print-package-list (list)
"Print LIST at point."
(let* ((width (apply #'max
(mapcar (lambda (x) (string-width (symbol-name (package-desc-name x))))
list)))
(tabulated-list-format
`[("Package" ,(1+ width) nil)
("Version" 0 nil)])
(tabulated-list-padding 2))
(mapc
(lambda (p) (tabulated-list-print-entry
p
`[,(symbol-name (package-desc-name p))
,(package-version-join (package-desc-version p))]))
list)))
(defun paradox--report-buffer-print (alist)
"Print a transaction report in *Package Report* buffer.
Possibly display the buffer or message the user depending on the
situation.
Argument ALIST describes the operation."
(let-alist alist
(let ((buf (get-buffer-create "*Paradox Report*"))
(inhibit-read-only t))
(with-current-buffer buf
(goto-char (point-max))
;; TODO: Write our own mode for this.
(special-mode)
(insert "\n\n")
(save-excursion
(insert (format-time-string "Package transaction finished. %c\n"))
(when .error
(insert "Errors:\n ")
(dolist (it .error)
(princ it (current-buffer))
(insert "\n"))
(insert "\n\n"))
(when .installed
(insert "Installed:\n")
(paradox--print-package-list .installed)
(insert "\n"))
(when .deleted
(insert "Deleted:\n")
(paradox--print-package-list .deleted)
(insert "\n")))))))
(defun paradox--report-buffer-display-if-noquery (alist)
"Display report buffer if `paradox-execute' was called with a NOQUERY prefix.
ALIST describes the transaction.
`paradox-async-display-buffer-function' is used if transaction
was asynchronous. Otherwise, `pop-to-buffer' is used."
(let-alist alist
;; The user has never seen the packages in this transaction. So
;; we display them in a buffer.
(when (or .noquery .error)
(let ((buf (get-buffer "*Paradox Report*")))
(when (buffer-live-p buf)
(cond
;; If we're async, the user might be doing something else, so
;; we don't steal focus.
((and .async paradox-async-display-buffer-function)
(funcall paradox-async-display-buffer-function buf))
;; If we're not async, just go ahead and pop.
((or (not .async)
;; If there's an error, display the buffer even if
;; `paradox-async-display-buffer-function' is nil.
.error)
(pop-to-buffer buf))))))))
(defun paradox--report-message (alist)
"Message the user about the executed transaction.
ALIST describes the transaction."
(let-alist alist
(message "%s%s"
(paradox--format-message nil .installed .deleted)
(if (memq 'paradox--report-buffer-print paradox-after-execute-functions)
" See the buffer *Paradox Report* for more details." ""))
(when .errors
(message "Errors encountered during the operation: %S\n%s"
.errors
(if (memq 'paradox--report-buffer-print paradox-after-execute-functions)
" See the buffer *Paradox Report* for more details." "")))))
;;; Execution
(defun paradox-menu-execute (&optional noquery)
"Perform marked Package Menu actions.
Packages marked for installation are downloaded and installed;
packages marked for deletion are removed.
Afterwards, if `paradox-automatically-star' is t, automatically
star new packages, and unstar removed packages. Upgraded packages
aren't changed.
Synchronicity of the actions depends on
`paradox-execute-asynchronously'. Optional argument NOQUERY
non-nil means do not ask the user to confirm. If asynchronous,
never ask anyway."
(interactive "P")
(unless (derived-mode-p 'paradox-menu-mode)
(error "The current buffer is not in Paradox Menu mode"))
(when (and (stringp paradox-github-token)
(eq paradox-automatically-star 'unconfigured))
(customize-save-variable
'paradox-automatically-star
(y-or-n-p "When you install new packages would you like them to be automatically starred?
\(They will be unstarred when you delete them) ")))
(when (and (stringp paradox--current-filter)
(string-match "Upgradable" paradox--current-filter))
(setq tabulated-list-sort-key '("Status" . nil))
(setq paradox--current-filter nil))
(paradox--menu-execute-1 noquery))
(defmacro paradox--perform-package-transaction (install delete)
"Install all packages from INSTALL and delete those from DELETE.
Return an alist with properties listing installed,
deleted, and activated packages, and errors."
`(let (activated installed deleted errored)
(advice-add #'package-activate-1 :after
(lambda (pkg &rest _)
(ignore-errors (push pkg activated)))
'((name . paradox--track-activated)))
(condition-case err
(progn
(dolist (pkg ,install)
;; 2nd arg introduced in 25.
(if (version<= "25" emacs-version)
(package-install pkg 'dont-select)
(package-install pkg))
(push pkg installed))
(let ((delete-list ,delete))
(dolist (pkg (if (fboundp 'package--sort-by-dependence)
(package--sort-by-dependence delete-list)
delete-list))
(condition-case err
(progn (package-delete pkg)
(push pkg deleted))
(error (push err errored))))))
(error (push err errored)))
(advice-remove #'package-activate-1 'paradox--track-activated)
(list (cons 'installed (nreverse installed))
(cons 'deleted (nreverse deleted))
(cons 'activated (nreverse activated))
(cons 'error (nreverse errored)))))
(defvar paradox--current-filter)
(declare-function async-inject-variables "async")
(defun paradox--menu-execute-1 (&optional noquery)
"Implementation used by `paradox-menu-execute'.
NOQUERY, if non-nil, means to execute without prompting the
user."
(let ((before-alist (paradox--repo-alist))
install-list delete-list)
(save-excursion
(goto-char (point-min))
(let ((p (point))
(inhibit-read-only t))
(while (not (eobp))
(let ((c (char-after)))
(if (eq c ?\s)
(forward-line 1)
(push (tabulated-list-get-id)
(pcase c
(`?D delete-list)
(`?I install-list)))
(delete-region p (point))
(forward-line 1)
(setq p (point)))))
(when (or delete-list install-list)
(delete-region p (point))
(ignore-errors
(set-window-start (selected-window) (point-min))))))
(if (not (or delete-list install-list))
(message "No operations specified.")
;; Confirm with the user.
(when (or noquery
(y-or-n-p (paradox--format-message 'question install-list delete-list)))
;; On Emacs 25, update the selected packages list.
(when (fboundp 'package--update-selected-packages)
(let-alist (package-menu--partition-transaction install-list delete-list)
(package--update-selected-packages .install .delete)))
;; Background or foreground?
(if (or (not install-list)
(not (pcase paradox-execute-asynchronously
(`nil nil)
(`ask
(if noquery nil
(y-or-n-p "Execute in the background (see `paradox-execute-asynchronously')? ")))
(_ t))))
;; Synchronous execution
(progn
(let ((alist (paradox--perform-package-transaction install-list delete-list)))
(run-hook-with-args 'paradox-after-execute-functions
`((noquery . ,noquery) (async . nil) ,@alist)))
(when (and (stringp paradox-github-token) paradox-automatically-star)
(paradox--post-execute-star-unstar before-alist (paradox--repo-alist))))
;; Start spinning
(paradox--start-spinner)
;; Async execution
(unless (require 'async nil t)
(error "For asynchronous execution please install the `async' package"))
;; We have to do this with eval, because `async-start' is a
;; macro and it might not have been defined at compile-time.
(eval
`(async-start
(lambda ()
(require 'package)
,(async-inject-variables "\\`package-")
(setq package-menu-async nil)
(dolist (elt package-alist)
(package-activate (car elt) 'force))
(let ((alist ,(macroexpand
`(paradox--perform-package-transaction ',install-list ',delete-list))))
(list package-alist
(when (boundp 'package-selected-packages)
package-selected-packages)
package-archive-contents
;; This is the alist that will be passed to the hook.
(cons '(noquery . ,noquery) (cons '(async . t) alist)))))
(lambda (x)
(setq package-alist (pop x)
package-selected-packages (pop x)
package-archive-contents (pop x))
(when (spinner-p paradox--spinner)
(spinner-stop paradox--spinner)
(setq paradox--spinner nil))
(setq paradox--executing nil)
(run-hook-with-args 'paradox-after-execute-functions (pop x))
(paradox--post-execute-star-unstar ',before-alist (paradox--repo-alist))))))))))
;;; Aux functions
(defun paradox--repo-alist ()
"List of known repos."
(delete-dups
(remove nil
(mapcar
(lambda (it) (gethash it paradox--package-repo-list))
package-alist))))
(defun paradox--format-message (question-p install-list delete-list)
"Format a message regarding a transaction.
If QUESTION-P is non-nil, format a question suitable for
`y-or-n-p', otherwise format a report in the past sense.
INSTALL-LIST and DELETE-LIST are a list of packages about to be
installed and deleted, respectively."
(concat
(when install-list
(let ((len (length install-list)))
(format "Install%s %d package%s"
(if question-p "" "ed")
len
(if (> len 1) "s" ""))))
(when (and install-list (not delete-list))
(if question-p "? " "."))
(when (and install-list delete-list)
", and ")
(when delete-list
(let ((len (length delete-list)))
(format "Delete%s %d package%s%s"
(if question-p "" "d")
len
(if (> len 1) "s" "")
(if question-p "? " "."))))))
(defun paradox--post-execute-star-unstar (before after)
"Star repos in AFTER absent from BEFORE, unstar vice-versa."
(let ((repos (hash-table-keys paradox--user-starred-repos)))
(mapc #'paradox--star-repo
(seq-difference (seq-difference after before) repos))
(mapc #'paradox--unstar-repo
(seq-intersection (seq-difference before after) repos))))
(provide 'paradox-execute)
;;; paradox-execute.el ends here