394 lines
15 KiB
EmacsLisp
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

;;; 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