394 lines
15 KiB
EmacsLisp
394 lines
15 KiB
EmacsLisp
;;; paradox-execute.el --- executing package transactions -*- lexical-binding:t -*-
|
||
|
||
;; Copyright (C) 2014-2015 Artur Malabarba <bruce.connor.am@gmail.com>
|
||
|
||
;; Author: Artur Malabarba <bruce.connor.am@gmail.com>
|
||
;; 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
|