Personal emacs config
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

393 lines
15 KiB

  1. ;;; paradox-execute.el --- executing package transactions -*- lexical-binding:t -*-
  2. ;; Copyright (C) 2014-2015 Artur Malabarba <bruce.connor.am@gmail.com>
  3. ;; Author: Artur Malabarba <bruce.connor.am@gmail.com>
  4. ;; Prefix: paradox
  5. ;; Separator: -
  6. ;;; License:
  7. ;;
  8. ;; This file is NOT part of GNU Emacs.
  9. ;;
  10. ;; This program is free software; you can redistribute it and/or
  11. ;; modify it under the terms of the GNU General Public License
  12. ;; as published by the Free Software Foundation; either version 2
  13. ;; of the License, or (at your option) any later version.
  14. ;;
  15. ;; This program is distributed in the hope that it will be useful,
  16. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18. ;; GNU General Public License for more details.
  19. ;;
  20. ;;; Commentary:
  21. ;;
  22. ;; Functions related to executing package-menu transactions.
  23. ;; Everything that happens when you hit `x' is in here.
  24. ;;; Code:
  25. (require 'cl-lib)
  26. (require 'seq)
  27. (require 'package)
  28. (require 'paradox-core)
  29. (require 'paradox-github)
  30. (defgroup paradox-execute nil
  31. "Paradox Packages Menu configurations."
  32. :prefix "paradox-"
  33. :package-version '(paradox . "2.0")
  34. :group 'paradox)
  35. (defvar paradox--current-filter)
  36. ;;; Customization Variables
  37. (defcustom paradox-execute-asynchronously 'ask
  38. "Whether the install/delete/upgrade should be asynchronous.
  39. Possible values are:
  40. t, which means always;
  41. nil, which means never;
  42. ask, which means ask each time."
  43. :type '(choice (const :tag "Always" t)
  44. (const :tag "Never" nil)
  45. (const :tag "Ask each time" ask))
  46. :package-version '(paradox . "2.0")
  47. :group 'paradox-execute)
  48. (defcustom paradox-async-display-buffer-function #'display-buffer
  49. "Function used to display *Paradox Report* buffer after asynchronous upgrade.
  50. Set this to nil to avoid displaying the buffer. Or set this to a
  51. function like `display-buffer' or `pop-to-buffer'.
  52. This is only used if `paradox-menu-execute' was given a non-nil
  53. NOQUERY argument. Otherwise, only a message is displayed."
  54. :type '(choice (const :tag "Don't display the buffer" nil)
  55. function)
  56. :package-version '(paradox . "2.0")
  57. :group 'paradox-execute)
  58. ;;; Execution Hook
  59. (defvar paradox-after-execute-functions nil
  60. "List of functions run after performing package transactions.
  61. These are run after a set of installation, deletion, or upgrades
  62. has been performed. Each function in this hook must take a single
  63. argument. An associative list of the form
  64. ((SYMBOL . DATA) (SYMBOL . DATA) ...)
  65. This list contains the following entries, describing what
  66. occurred during the execution:
  67. SYMBOL DATA
  68. `installed' List of installed packages.
  69. `deleted' List of deleted packages.
  70. `activated' List of activated packages.
  71. `error' List of errors.
  72. `async' Non-nil if transaction was performed asynchronously.
  73. `noquery' The NOQUERY argument given to `paradox-menu-execute'.")
  74. (put 'risky-local-variable-p 'paradox-after-execute-functions t)
  75. (mapc (lambda (x) (add-hook 'paradox-after-execute-functions x t))
  76. '(paradox--activate-if-asynchronous
  77. paradox--refresh-package-buffer
  78. paradox--report-buffer-print
  79. paradox--report-buffer-display-if-noquery
  80. paradox--report-message
  81. ))
  82. (defun paradox--refresh-package-buffer (_)
  83. "Refresh the *Packages* buffer, if it exists."
  84. (let ((buf (get-buffer "*Packages*")))
  85. (when (buffer-live-p buf)
  86. (with-current-buffer buf
  87. (revert-buffer)))))
  88. (defun paradox--activate-if-asynchronous (alist)
  89. "Activate packages after an asynchronous operation.
  90. Argument ALIST describes the operation."
  91. (let-alist alist
  92. (when .async
  93. (dolist (pkg .activated)
  94. (if (fboundp 'package--list-loaded-files)
  95. (package-activate-1 pkg 'reload)
  96. (package-activate-1 pkg))))))
  97. (defun paradox--print-package-list (list)
  98. "Print LIST at point."
  99. (let* ((width (apply #'max
  100. (mapcar (lambda (x) (string-width (symbol-name (package-desc-name x))))
  101. list)))
  102. (tabulated-list-format
  103. `[("Package" ,(1+ width) nil)
  104. ("Version" 0 nil)])
  105. (tabulated-list-padding 2))
  106. (mapc
  107. (lambda (p) (tabulated-list-print-entry
  108. p
  109. `[,(symbol-name (package-desc-name p))
  110. ,(package-version-join (package-desc-version p))]))
  111. list)))
  112. (defun paradox--report-buffer-print (alist)
  113. "Print a transaction report in *Package Report* buffer.
  114. Possibly display the buffer or message the user depending on the
  115. situation.
  116. Argument ALIST describes the operation."
  117. (let-alist alist
  118. (let ((buf (get-buffer-create "*Paradox Report*"))
  119. (inhibit-read-only t))
  120. (with-current-buffer buf
  121. (goto-char (point-max))
  122. ;; TODO: Write our own mode for this.
  123. (special-mode)
  124. (insert "\n \n")
  125. (save-excursion
  126. (insert (format-time-string "Package transaction finished. %c\n"))
  127. (when .error
  128. (insert "Errors:\n ")
  129. (dolist (it .error)
  130. (princ it (current-buffer))
  131. (insert "\n"))
  132. (insert "\n\n"))
  133. (when .installed
  134. (insert "Installed:\n")
  135. (paradox--print-package-list .installed)
  136. (insert "\n"))
  137. (when .deleted
  138. (insert "Deleted:\n")
  139. (paradox--print-package-list .deleted)
  140. (insert "\n")))))))
  141. (defun paradox--report-buffer-display-if-noquery (alist)
  142. "Display report buffer if `paradox-execute' was called with a NOQUERY prefix.
  143. ALIST describes the transaction.
  144. `paradox-async-display-buffer-function' is used if transaction
  145. was asynchronous. Otherwise, `pop-to-buffer' is used."
  146. (let-alist alist
  147. ;; The user has never seen the packages in this transaction. So
  148. ;; we display them in a buffer.
  149. (when (or .noquery .error)
  150. (let ((buf (get-buffer "*Paradox Report*")))
  151. (when (buffer-live-p buf)
  152. (cond
  153. ;; If we're async, the user might be doing something else, so
  154. ;; we don't steal focus.
  155. ((and .async paradox-async-display-buffer-function)
  156. (funcall paradox-async-display-buffer-function buf))
  157. ;; If we're not async, just go ahead and pop.
  158. ((or (not .async)
  159. ;; If there's an error, display the buffer even if
  160. ;; `paradox-async-display-buffer-function' is nil.
  161. .error)
  162. (pop-to-buffer buf))))))))
  163. (defun paradox--report-message (alist)
  164. "Message the user about the executed transaction.
  165. ALIST describes the transaction."
  166. (let-alist alist
  167. (message "%s%s"
  168. (paradox--format-message nil .installed .deleted)
  169. (if (memq 'paradox--report-buffer-print paradox-after-execute-functions)
  170. " See the buffer *Paradox Report* for more details." ""))
  171. (when .errors
  172. (message "Errors encountered during the operation: %S\n%s"
  173. .errors
  174. (if (memq 'paradox--report-buffer-print paradox-after-execute-functions)
  175. " See the buffer *Paradox Report* for more details." "")))))
  176. ;;; Execution
  177. (defun paradox-menu-execute (&optional noquery)
  178. "Perform marked Package Menu actions.
  179. Packages marked for installation are downloaded and installed;
  180. packages marked for deletion are removed.
  181. Afterwards, if `paradox-automatically-star' is t, automatically
  182. star new packages, and unstar removed packages. Upgraded packages
  183. aren't changed.
  184. Synchronicity of the actions depends on
  185. `paradox-execute-asynchronously'. Optional argument NOQUERY
  186. non-nil means do not ask the user to confirm. If asynchronous,
  187. never ask anyway."
  188. (interactive "P")
  189. (unless (derived-mode-p 'paradox-menu-mode)
  190. (error "The current buffer is not in Paradox Menu mode"))
  191. (when (and (stringp paradox-github-token)
  192. (eq paradox-automatically-star 'unconfigured))
  193. (customize-save-variable
  194. 'paradox-automatically-star
  195. (y-or-n-p "When you install new packages would you like them to be automatically starred?
  196. \(They will be unstarred when you delete them) ")))
  197. (when (and (stringp paradox--current-filter)
  198. (string-match "Upgradable" paradox--current-filter))
  199. (setq tabulated-list-sort-key '("Status" . nil))
  200. (setq paradox--current-filter nil))
  201. (paradox--menu-execute-1 noquery))
  202. (defmacro paradox--perform-package-transaction (install delete)
  203. "Install all packages from INSTALL and delete those from DELETE.
  204. Return an alist with properties listing installed,
  205. deleted, and activated packages, and errors."
  206. `(let (activated installed deleted errored)
  207. (advice-add #'package-activate-1 :after
  208. (lambda (pkg &rest _)
  209. (ignore-errors (push pkg activated)))
  210. '((name . paradox--track-activated)))
  211. (condition-case err
  212. (progn
  213. (dolist (pkg ,install)
  214. ;; 2nd arg introduced in 25.
  215. (if (version<= "25" emacs-version)
  216. (package-install pkg 'dont-select)
  217. (package-install pkg))
  218. (push pkg installed))
  219. (let ((delete-list ,delete))
  220. (dolist (pkg (if (fboundp 'package--sort-by-dependence)
  221. (package--sort-by-dependence delete-list)
  222. delete-list))
  223. (condition-case err
  224. (progn (package-delete pkg)
  225. (push pkg deleted))
  226. (error (push err errored))))))
  227. (error (push err errored)))
  228. (advice-remove #'package-activate-1 'paradox--track-activated)
  229. (list (cons 'installed (nreverse installed))
  230. (cons 'deleted (nreverse deleted))
  231. (cons 'activated (nreverse activated))
  232. (cons 'error (nreverse errored)))))
  233. (defvar paradox--current-filter)
  234. (declare-function async-inject-variables "async")
  235. (defun paradox--menu-execute-1 (&optional noquery)
  236. "Implementation used by `paradox-menu-execute'.
  237. NOQUERY, if non-nil, means to execute without prompting the
  238. user."
  239. (let ((before-alist (paradox--repo-alist))
  240. install-list delete-list)
  241. (save-excursion
  242. (goto-char (point-min))
  243. (let ((p (point))
  244. (inhibit-read-only t))
  245. (while (not (eobp))
  246. (let ((c (char-after)))
  247. (if (eq c ?\s)
  248. (forward-line 1)
  249. (push (tabulated-list-get-id)
  250. (pcase c
  251. (`?D delete-list)
  252. (`?I install-list)))
  253. (delete-region p (point))
  254. (forward-line 1)
  255. (setq p (point)))))
  256. (when (or delete-list install-list)
  257. (delete-region p (point))
  258. (ignore-errors
  259. (set-window-start (selected-window) (point-min))))))
  260. (if (not (or delete-list install-list))
  261. (message "No operations specified.")
  262. ;; Confirm with the user.
  263. (when (or noquery
  264. (y-or-n-p (paradox--format-message 'question install-list delete-list)))
  265. ;; On Emacs 25, update the selected packages list.
  266. (when (fboundp 'package--update-selected-packages)
  267. (let-alist (package-menu--partition-transaction install-list delete-list)
  268. (package--update-selected-packages .install .delete)))
  269. ;; Background or foreground?
  270. (if (or (not install-list)
  271. (not (pcase paradox-execute-asynchronously
  272. (`nil nil)
  273. (`ask
  274. (if noquery nil
  275. (y-or-n-p "Execute in the background (see `paradox-execute-asynchronously')? ")))
  276. (_ t))))
  277. ;; Synchronous execution
  278. (progn
  279. (let ((alist (paradox--perform-package-transaction install-list delete-list)))
  280. (run-hook-with-args 'paradox-after-execute-functions
  281. `((noquery . ,noquery) (async . nil) ,@alist)))
  282. (when (and (stringp paradox-github-token) paradox-automatically-star)
  283. (paradox--post-execute-star-unstar before-alist (paradox--repo-alist))))
  284. ;; Start spinning
  285. (paradox--start-spinner)
  286. ;; Async execution
  287. (unless (require 'async nil t)
  288. (error "For asynchronous execution please install the `async' package"))
  289. ;; We have to do this with eval, because `async-start' is a
  290. ;; macro and it might not have been defined at compile-time.
  291. (eval
  292. `(async-start
  293. (lambda ()
  294. (require 'package)
  295. ,(async-inject-variables "\\`package-")
  296. (setq package-menu-async nil)
  297. (dolist (elt package-alist)
  298. (package-activate (car elt) 'force))
  299. (let ((alist ,(macroexpand
  300. `(paradox--perform-package-transaction ',install-list ',delete-list))))
  301. (list package-alist
  302. (when (boundp 'package-selected-packages)
  303. package-selected-packages)
  304. package-archive-contents
  305. ;; This is the alist that will be passed to the hook.
  306. (cons '(noquery . ,noquery) (cons '(async . t) alist)))))
  307. (lambda (x)
  308. (setq package-alist (pop x)
  309. package-selected-packages (pop x)
  310. package-archive-contents (pop x))
  311. (when (spinner-p paradox--spinner)
  312. (spinner-stop paradox--spinner)
  313. (setq paradox--spinner nil))
  314. (setq paradox--executing nil)
  315. (run-hook-with-args 'paradox-after-execute-functions (pop x))
  316. (paradox--post-execute-star-unstar ',before-alist (paradox--repo-alist))))))))))
  317. ;;; Aux functions
  318. (defun paradox--repo-alist ()
  319. "List of known repos."
  320. (delete-dups
  321. (remove nil
  322. (mapcar
  323. (lambda (it) (gethash it paradox--package-repo-list))
  324. package-alist))))
  325. (defun paradox--format-message (question-p install-list delete-list)
  326. "Format a message regarding a transaction.
  327. If QUESTION-P is non-nil, format a question suitable for
  328. `y-or-n-p', otherwise format a report in the past sense.
  329. INSTALL-LIST and DELETE-LIST are a list of packages about to be
  330. installed and deleted, respectively."
  331. (concat
  332. (when install-list
  333. (let ((len (length install-list)))
  334. (format "Install%s %d package%s"
  335. (if question-p "" "ed")
  336. len
  337. (if (> len 1) "s" ""))))
  338. (when (and install-list (not delete-list))
  339. (if question-p "? " "."))
  340. (when (and install-list delete-list)
  341. ", and ")
  342. (when delete-list
  343. (let ((len (length delete-list)))
  344. (format "Delete%s %d package%s%s"
  345. (if question-p "" "d")
  346. len
  347. (if (> len 1) "s" "")
  348. (if question-p "? " "."))))))
  349. (defun paradox--post-execute-star-unstar (before after)
  350. "Star repos in AFTER absent from BEFORE, unstar vice-versa."
  351. (let ((repos (hash-table-keys paradox--user-starred-repos)))
  352. (mapc #'paradox--star-repo
  353. (seq-difference (seq-difference after before) repos))
  354. (mapc #'paradox--unstar-repo
  355. (seq-intersection (seq-difference before after) repos))))
  356. (provide 'paradox-execute)
  357. ;;; paradox-execute.el ends here