aboutsummaryrefslogtreecommitdiff
path: root/emacs
diff options
context:
space:
mode:
authorLudovic Courtès <ludo@gnu.org>2016-01-03 14:53:03 +0100
committerLudovic Courtès <ludo@gnu.org>2016-01-03 14:53:03 +0100
commit53334dd6e9e296e17110ebcd2b1f93f117ffe36a (patch)
tree2653db2eab9a204dab892ea8b6812cadf7209e84 /emacs
parent1575dcd134f4fae7255787293f4988bbd043de95 (diff)
parent51385362f76e2f823ac8d8cf720d06c386504069 (diff)
downloadpatches-53334dd6e9e296e17110ebcd2b1f93f117ffe36a.tar
patches-53334dd6e9e296e17110ebcd2b1f93f117ffe36a.tar.gz
Merge branch 'master' into core-updates
Diffstat (limited to 'emacs')
-rw-r--r--emacs/guix-backend.el68
-rw-r--r--emacs/guix-base.el863
-rw-r--r--emacs/guix-buffer.el622
-rw-r--r--emacs/guix-command.el9
-rw-r--r--emacs/guix-entry.el59
-rw-r--r--emacs/guix-external.el8
-rw-r--r--emacs/guix-hydra-build.el362
-rw-r--r--emacs/guix-hydra-jobset.el162
-rw-r--r--emacs/guix-hydra.el363
-rw-r--r--emacs/guix-info.el1007
-rw-r--r--emacs/guix-list.el960
-rw-r--r--emacs/guix-main.scm4
-rw-r--r--emacs/guix-messages.el26
-rw-r--r--emacs/guix-read.el123
-rw-r--r--emacs/guix-ui-generation.el433
-rw-r--r--emacs/guix-ui-package.el955
-rw-r--r--emacs/guix-ui.el333
-rw-r--r--emacs/guix-utils.el269
-rw-r--r--emacs/guix.el213
19 files changed, 4294 insertions, 2545 deletions
diff --git a/emacs/guix-backend.el b/emacs/guix-backend.el
index 82383e48ff..0736f85ec8 100644
--- a/emacs/guix-backend.el
+++ b/emacs/guix-backend.el
@@ -36,18 +36,13 @@
;; running code in the REPL (see
;; <https://github.com/jaor/geiser/issues/28>).
;;
-;; If you need to use "guix.el" in another Emacs (i.e. when there is
-;; a runnig "guile --listen..." REPL somewhere), you can either change
-;; `guix-default-port' in that Emacs instance or set
-;; `guix-use-guile-server' to t.
-;;
;; Guix REPLs (unlike the usual Geiser REPLs) are not added to
;; `geiser-repl--repls' variable, and thus cannot be used for evaluating
;; while editing scm-files. The only purpose of Guix REPLs is to be an
;; intermediate between "Guix/Guile level" and "Emacs interface level".
;; That being said you can still want to use a Guix REPL while hacking
-;; auxiliary scheme-files for "guix.el". You can just use "M-x
-;; connect-to-guile" (connect to "localhost" and `guix-default-port') to
+;; auxiliary scheme-files for "guix.el". You can just use
+;; `geiser-connect-local' command with `guix-repl-current-socket' to
;; have a usual Geiser REPL with all stuff defined by "guix.el" package.
;;; Code:
@@ -98,11 +93,17 @@ REPL while some packages are being installed/removed in the main REPL."
:type 'boolean
:group 'guix-repl)
-(defcustom guix-default-port 37246
- "Default port used if `guix-use-guile-server' is non-nil."
- :type 'integer
+(defcustom guix-repl-socket-file-name-function
+ #'guix-repl-socket-file-name
+ "Function used to define a socket file name used by Guix REPL.
+The function is called without arguments."
+ :type '(choice (function-item guix-repl-socket-file-name)
+ (function :tag "Other function"))
:group 'guix-repl)
+(defvar guix-repl-current-socket nil
+ "Name of a socket file used by the current Guix REPL.")
+
(defvar guix-repl-buffer nil
"Main Geiser REPL buffer used for communicating with Guix.
This REPL is used for processing package actions and for
@@ -139,17 +140,28 @@ See `guix-eval-in-repl' for details.")
"Message telling about successful Guix operation."
(message "Guix operation has been performed."))
-(defun guix-get-guile-program (&optional internal)
+(defun guix-get-guile-program (&optional socket)
"Return a value suitable for `geiser-guile-binary'."
- (if (or internal
- (not guix-use-guile-server))
+ (if (null socket)
guix-guile-program
(append (if (listp guix-guile-program)
guix-guile-program
(list guix-guile-program))
- ;; Guile understands "--listen=..." but not "--listen ..."
- (list (concat "--listen="
- (number-to-string guix-default-port))))))
+ (list (concat "--listen=" socket)))))
+
+(defun guix-repl-socket-file-name ()
+ "Return a name of a socket file used by Guix REPL."
+ (make-temp-name
+ (concat (file-name-as-directory temporary-file-directory)
+ "guix-repl-")))
+
+(defun guix-repl-delete-socket-maybe ()
+ "Delete `guix-repl-current-socket' file if it exists."
+ (and guix-repl-current-socket
+ (file-exists-p guix-repl-current-socket)
+ (delete-file guix-repl-current-socket)))
+
+(add-hook 'kill-emacs-hook 'guix-repl-delete-socket-maybe)
(defun guix-start-process-maybe (&optional start-msg end-msg)
"Start Geiser REPL configured for Guix if needed.
@@ -176,19 +188,21 @@ display messages."
(get-buffer-process repl))
(and start-msg (message start-msg))
(setq guix-repl-operation-p nil)
- (let ((geiser-guile-binary (guix-get-guile-program internal))
- (geiser-guile-init-file (or internal guix-helper-file))
+ (unless internal
+ ;; Guile leaves socket file after exit, so remove it if it
+ ;; exists (after the REPL restart).
+ (guix-repl-delete-socket-maybe)
+ (setq guix-repl-current-socket
+ (and guix-use-guile-server
+ (or guix-repl-current-socket
+ (funcall guix-repl-socket-file-name-function)))))
+ (let ((geiser-guile-binary (guix-get-guile-program
+ (unless internal
+ guix-repl-current-socket)))
+ (geiser-guile-init-file (unless internal guix-helper-file))
(repl (get-buffer-create
(guix-get-repl-buffer-name internal))))
- (condition-case err
- (guix-start-repl repl
- (and internal
- (geiser-repl--read-address
- "localhost" guix-default-port)))
- (text-read-only
- (error (concat "Couldn't start Guix REPL. Perhaps the port %s is busy.\n"
- "See buffer '%s' for details")
- guix-default-port (buffer-name repl))))
+ (guix-start-repl repl (and internal guix-repl-current-socket))
(set repl-var repl)
(and end-msg (message end-msg))
(unless internal
diff --git a/emacs/guix-base.el b/emacs/guix-base.el
index d9c70aae9e..dae658ebfa 100644
--- a/emacs/guix-base.el
+++ b/emacs/guix-base.el
@@ -22,124 +22,32 @@
;; This file provides some base and common definitions for guix.el
;; package.
-;; List and info buffers have many common patterns that are defined
-;; using `guix-define-buffer-type' macro from this file.
-
;;; Code:
(require 'cl-lib)
-(require 'guix-profiles)
(require 'guix-backend)
(require 'guix-guile)
+(require 'guix-read)
(require 'guix-utils)
-(require 'guix-history)
-(require 'guix-messages)
-
-
-;;; Parameters of the entries
-
-(defvar guix-param-titles
- '((package
- (id . "ID")
- (name . "Name")
- (version . "Version")
- (source . "Source")
- (license . "License")
- (synopsis . "Synopsis")
- (description . "Description")
- (home-url . "Home page")
- (outputs . "Outputs")
- (inputs . "Inputs")
- (native-inputs . "Native inputs")
- (propagated-inputs . "Propagated inputs")
- (location . "Location")
- (installed . "Installed"))
- (installed
- (path . "Installed path")
- (dependencies . "Dependencies")
- (output . "Output"))
- (output
- (id . "ID")
- (name . "Name")
- (version . "Version")
- (source . "Source")
- (license . "License")
- (synopsis . "Synopsis")
- (description . "Description")
- (home-url . "Home page")
- (output . "Output")
- (inputs . "Inputs")
- (native-inputs . "Native inputs")
- (propagated-inputs . "Propagated inputs")
- (location . "Location")
- (installed . "Installed")
- (path . "Installed path")
- (dependencies . "Dependencies"))
- (generation
- (id . "ID")
- (number . "Number")
- (prev-number . "Previous number")
- (current . "Current")
- (path . "Path")
- (time . "Time")))
- "List for defining titles of entry parameters.
-Titles are used for displaying information about entries.
-Each element of the list has a form:
+(require 'guix-ui)
- (ENTRY-TYPE . ((PARAM . TITLE) ...))")
+(defgroup guix nil
+ "Settings for Guix package manager and friends."
+ :prefix "guix-"
+ :group 'external)
-(defun guix-get-param-title (entry-type param)
- "Return title of an ENTRY-TYPE entry parameter PARAM."
- (or (guix-assq-value guix-param-titles
- entry-type param)
- (prog1 (symbol-name param)
- (message "Couldn't find title for '%S %S'."
- entry-type param))))
+(defgroup guix-faces nil
+ "Guix faces."
+ :group 'guix
+ :group 'faces)
-(defun guix-get-name-spec (name version &optional output)
+(defun guix-package-name-specification (name version &optional output)
"Return Guix package specification by its NAME, VERSION and OUTPUT."
(concat name "-" version
(when output (concat ":" output))))
-(defun guix-get-full-name (entry &optional output)
- "Return name specification of the package ENTRY and OUTPUT."
- (guix-get-name-spec (guix-assq-value entry 'name)
- (guix-assq-value entry 'version)
- output))
-
-(defun guix-entry-to-specification (entry)
- "Return name specification by the package or output ENTRY."
- (guix-get-name-spec (guix-assq-value entry 'name)
- (guix-assq-value entry 'version)
- (guix-assq-value entry 'output)))
-
-(defun guix-entries-to-specifications (entries)
- "Return name specifications by the package or output ENTRIES."
- (cl-remove-duplicates (mapcar #'guix-entry-to-specification entries)
- :test #'string=))
-
-(defun guix-get-installed-outputs (entry)
- "Return list of installed outputs for the package ENTRY."
- (mapcar (lambda (installed-entry)
- (guix-assq-value installed-entry 'output))
- (guix-assq-value entry 'installed)))
-
-(defun guix-get-entry-by-id (id entries)
- "Return entry from ENTRIES by entry ID."
- (cl-find-if (lambda (entry)
- (equal id (guix-assq-value entry 'id)))
- entries))
-
-(defun guix-get-package-id-and-output-by-output-id (oid)
- "Return list (PACKAGE-ID OUTPUT) by output id OID."
- (cl-multiple-value-bind (pid-str output)
- (split-string oid ":")
- (let ((pid (string-to-number pid-str)))
- (list (if (= 0 pid) pid-str pid)
- output))))
-
-;;; Location of the packages
+;;; Location of packages, profiles and manifests
(defvar guix-directory nil
"Default Guix directory.
@@ -179,538 +87,6 @@ For the meaning of location, see `guix-find-location'."
(guix-eval-read (guix-make-guile-expression
'package-location-string id-or-name)))
-
-;;; Receivable lists of packages, lint checkers, etc.
-
-(guix-memoized-defun guix-graph-type-names ()
- "Return a list of names of available graph node types."
- (guix-eval-read (guix-make-guile-expression 'graph-type-names)))
-
-(guix-memoized-defun guix-refresh-updater-names ()
- "Return a list of names of available refresh updater types."
- (guix-eval-read (guix-make-guile-expression 'refresh-updater-names)))
-
-(guix-memoized-defun guix-lint-checker-names ()
- "Return a list of names of available lint checkers."
- (guix-eval-read (guix-make-guile-expression 'lint-checker-names)))
-
-(guix-memoized-defun guix-package-names ()
- "Return a list of names of available packages."
- (sort
- ;; Work around <https://github.com/jaor/geiser/issues/64>:
- ;; list of strings is parsed much slower than list of lists,
- ;; so we use 'package-names-lists' instead of 'package-names'.
-
- ;; (guix-eval-read (guix-make-guile-expression 'package-names))
-
- (mapcar #'car
- (guix-eval-read (guix-make-guile-expression
- 'package-names-lists)))
- #'string<))
-
-
-;;; Buffers and auto updating.
-
-(defcustom guix-update-after-operation 'current
- "Define what information to update after executing an operation.
-
-After successful executing an operation in the Guix REPL (for
-example after installing a package), information in Guix buffers
-will or will not be automatically updated depending on a value of
-this variable.
-
-If nil, update nothing (do not revert any buffer).
-If `current', update the buffer from which an operation was performed.
-If `all', update all Guix buffers (not recommended)."
- :type '(choice (const :tag "Do nothing" nil)
- (const :tag "Update operation buffer" current)
- (const :tag "Update all Guix buffers" all))
- :group 'guix)
-
-(defcustom guix-buffer-name-function #'guix-buffer-name-default
- "Function used to define name of a buffer for displaying information.
-The function is called with 4 arguments: PROFILE, BUFFER-TYPE,
-ENTRY-TYPE, SEARCH-TYPE. See `guix-get-entries' for the meaning
-of the arguments."
- :type '(choice (function-item guix-buffer-name-default)
- (function-item guix-buffer-name-simple)
- (function :tag "Other function"))
- :group 'guix)
-
-(defun guix-buffer-name-simple (_profile buffer-type entry-type
- &optional _search-type)
- "Return name of a buffer used for displaying information.
-The name is defined by `guix-ENTRY-TYPE-BUFFER-TYPE-buffer-name'
-variable."
- (symbol-value
- (guix-get-symbol "buffer-name" buffer-type entry-type)))
-
-(defun guix-buffer-name-default (profile buffer-type entry-type
- &optional _search-type)
- "Return name of a buffer used for displaying information.
-The name is almost the same as the one defined by
-`guix-buffer-name-simple' except the PROFILE name is added to it."
- (let ((simple-name (guix-buffer-name-simple
- profile buffer-type entry-type))
- (profile-name (file-name-base (directory-file-name profile)))
- (re (rx string-start
- (group (? "*"))
- (group (*? any))
- (group (? "*"))
- string-end)))
- (or (string-match re simple-name)
- (error "Unexpected error in defining guix buffer name"))
- (let ((first* (match-string 1 simple-name))
- (name-body (match-string 2 simple-name))
- (last* (match-string 3 simple-name)))
- ;; Handle the case when buffer name is wrapped by '*'.
- (if (and (string= "*" first*)
- (string= "*" last*))
- (concat "*" name-body ": " profile-name "*")
- (concat simple-name ": " profile-name)))))
-
-(defun guix-buffer-name (profile buffer-type entry-type search-type)
- "Return name of a buffer used for displaying information.
-See `guix-buffer-name-function' for details."
- (let ((fun (if (functionp guix-buffer-name-function)
- guix-buffer-name-function
- #'guix-buffer-name-default)))
- (funcall fun profile buffer-type entry-type search-type)))
-
-(defun guix-switch-to-buffer (buffer)
- "Switch to a 'list' or 'info' BUFFER."
- (pop-to-buffer buffer
- '((display-buffer-reuse-window
- display-buffer-same-window))))
-
-(defun guix-buffer-p (&optional buffer modes)
- "Return non-nil if BUFFER mode is derived from any of the MODES.
-If BUFFER is nil, check current buffer.
-If MODES is nil, use `guix-list-mode' and `guix-info-mode'."
- (with-current-buffer (or buffer (current-buffer))
- (apply #'derived-mode-p
- (or modes
- '(guix-list-mode guix-info-mode)))))
-
-(defun guix-buffers (&optional modes)
- "Return list of all buffers with major modes derived from MODES.
-If MODES is nil, return list of all Guix 'list' and 'info' buffers."
- (cl-remove-if-not (lambda (buf)
- (guix-buffer-p buf modes))
- (buffer-list)))
-
-(defun guix-update-buffer (buffer)
- "Update information in a 'list' or 'info' BUFFER."
- (with-current-buffer buffer
- (guix-revert-buffer nil t)))
-
-(defun guix-update-buffers-maybe-after-operation ()
- "Update buffers after Guix operation if needed.
-See `guix-update-after-operation' for details."
- (let ((to-update
- (and guix-operation-buffer
- (cl-case guix-update-after-operation
- (current (and (buffer-live-p guix-operation-buffer)
- (guix-buffer-p guix-operation-buffer)
- (list guix-operation-buffer)))
- (all (guix-buffers))))))
- (setq guix-operation-buffer nil)
- (mapc #'guix-update-buffer to-update)))
-
-(add-hook 'guix-after-repl-operation-hook
- 'guix-update-buffers-maybe-after-operation)
-
-
-;;; Common definitions for buffer types
-
-(defvar guix-root-map
- (let ((map (make-sparse-keymap)))
- (define-key map (kbd "l") 'guix-history-back)
- (define-key map (kbd "r") 'guix-history-forward)
- (define-key map (kbd "g") 'revert-buffer)
- (define-key map (kbd "R") 'guix-redisplay-buffer)
- (define-key map (kbd "M") 'guix-apply-manifest)
- (define-key map (kbd "C-c C-z") 'guix-switch-to-repl)
- map)
- "Parent keymap for all guix modes.")
-
-(defvar-local guix-profile nil
- "Profile used for the current buffer.")
-(put 'guix-profile 'permanent-local t)
-
-(defvar-local guix-entries nil
- "List of the currently displayed entries.
-Each element of the list is alist with entry info of the
-following form:
-
- ((PARAM . VAL) ...)
-
-PARAM is a name of the entry parameter.
-VAL is a value of this parameter.")
-(put 'guix-entries 'permanent-local t)
-
-(defvar-local guix-buffer-type nil
- "Type of the current buffer.")
-(put 'guix-buffer-type 'permanent-local t)
-
-(defvar-local guix-entry-type nil
- "Type of the current entry.")
-(put 'guix-entry-type 'permanent-local t)
-
-(defvar-local guix-search-type nil
- "Type of the current search.")
-(put 'guix-search-type 'permanent-local t)
-
-(defvar-local guix-search-vals nil
- "Values of the current search.")
-(put 'guix-search-vals 'permanent-local t)
-
-(defsubst guix-set-vars (profile entries buffer-type entry-type
- search-type search-vals)
- "Set local variables for the current Guix buffer."
- (setq default-directory profile
- guix-profile profile
- guix-entries entries
- guix-buffer-type buffer-type
- guix-entry-type entry-type
- guix-search-type search-type
- guix-search-vals search-vals))
-
-(defun guix-get-symbol (postfix buffer-type &optional entry-type)
- (intern (concat "guix-"
- (when entry-type
- (concat (symbol-name entry-type) "-"))
- (symbol-name buffer-type) "-" postfix)))
-
-(defmacro guix-define-buffer-type (buf-type entry-type &rest args)
- "Define common for BUF-TYPE buffers for displaying ENTRY-TYPE entries.
-
-In the text below TYPE means ENTRY-TYPE-BUF-TYPE.
-
-This macro defines `guix-TYPE-mode', a custom group and several
-user variables.
-
-The following stuff should be defined outside this macro:
-
- - `guix-BUF-TYPE-mode' - parent mode for the defined mode.
-
- - `guix-TYPE-mode-initialize' (optional) - function for
- additional mode settings; it is called without arguments.
-
-Remaining argument (ARGS) should have a form [KEYWORD VALUE] ... The
-following keywords are available:
-
- - `:buffer-name' - default value for the defined
- `guix-TYPE-buffer-name' variable.
-
- - `:required' - default value for the defined
- `guix-TYPE-required-params' variable.
-
- - `:history-size' - default value for the defined
- `guix-TYPE-history-size' variable.
-
- - `:revert' - default value for the defined
- `guix-TYPE-revert-no-confirm' variable."
- (let* ((entry-type-str (symbol-name entry-type))
- (buf-type-str (symbol-name buf-type))
- (Entry-type-str (capitalize entry-type-str))
- (Buf-type-str (capitalize buf-type-str))
- (entry-str (concat entry-type-str " entries"))
- (buf-str (concat buf-type-str " buffer"))
- (prefix (concat "guix-" entry-type-str "-" buf-type-str))
- (group (intern prefix))
- (faces-group (intern (concat prefix "-faces")))
- (mode-map-str (concat prefix "-mode-map"))
- (parent-mode (intern (concat "guix-" buf-type-str "-mode")))
- (mode (intern (concat prefix "-mode")))
- (mode-init-fun (intern (concat prefix "-mode-initialize")))
- (buf-name-var (intern (concat prefix "-buffer-name")))
- (revert-var (intern (concat prefix "-revert-no-confirm")))
- (history-var (intern (concat prefix "-history-size")))
- (params-var (intern (concat prefix "-required-params")))
- (buf-name-val (format "*Guix %s %s*" Entry-type-str Buf-type-str))
- (revert-val nil)
- (history-val 20)
- (params-val '(id)))
-
- ;; Process the keyword args.
- (while (keywordp (car args))
- (pcase (pop args)
- (`:required (setq params-val (pop args)))
- (`:history-size (setq history-val (pop args)))
- (`:revert (setq revert-val (pop args)))
- (`:buffer-name (setq buf-name-val (pop args)))
- (_ (pop args))))
-
- `(progn
- (defgroup ,group nil
- ,(concat Buf-type-str " buffer with " entry-str ".")
- :prefix ,(concat prefix "-")
- :group ',(intern (concat "guix-" buf-type-str)))
-
- (defgroup ,faces-group nil
- ,(concat "Faces for " buf-type-str " buffer with " entry-str ".")
- :group ',(intern (concat "guix-" buf-type-str "-faces")))
-
- (defcustom ,buf-name-var ,buf-name-val
- ,(concat "Default name of the " buf-str " for displaying " entry-str ".")
- :type 'string
- :group ',group)
-
- (defcustom ,history-var ,history-val
- ,(concat "Maximum number of items saved in the history of the " buf-str ".\n"
- "If 0, the history is disabled.")
- :type 'integer
- :group ',group)
-
- (defcustom ,revert-var ,revert-val
- ,(concat "If non-nil, do not ask to confirm for reverting the " buf-str ".")
- :type 'boolean
- :group ',group)
-
- (defvar ,params-var ',params-val
- ,(concat "List of required " entry-type-str " parameters.\n\n"
- "Displayed parameters and parameters from this list are received\n"
- "for each " entry-type-str ".\n\n"
- "May be a special value `all', in which case all supported\n"
- "parameters are received (this may be very slow for a big number\n"
- "of entries).\n\n"
- "Do not remove `id' from this list as it is required for\n"
- "identifying an entry."))
-
- (define-derived-mode ,mode ,parent-mode ,(concat "Guix-" Buf-type-str)
- ,(concat "Major mode for displaying information about " entry-str ".\n\n"
- "\\{" mode-map-str "}")
- (setq-local revert-buffer-function 'guix-revert-buffer)
- (setq-local guix-history-size ,history-var)
- (and (fboundp ',mode-init-fun) (,mode-init-fun))))))
-
-(put 'guix-define-buffer-type 'lisp-indent-function 'defun)
-
-
-;;; Getting and displaying info about packages and generations
-
-(defcustom guix-package-list-type 'output
- "Define how to display packages in a list buffer.
-May be a symbol `package' or `output' (if `output', display each
-output on a separate line; if `package', display each package on
-a separate line)."
- :type '(choice (const :tag "List of packages" package)
- (const :tag "List of outputs" output))
- :group 'guix)
-
-(defcustom guix-package-info-type 'package
- "Define how to display packages in an info buffer.
-May be a symbol `package' or `output' (if `output', display each
-output separately; if `package', display outputs inside a package
-information)."
- :type '(choice (const :tag "Display packages" package)
- (const :tag "Display outputs" output))
- :group 'guix)
-
-(defun guix-get-entries (profile entry-type search-type search-vals
- &optional params)
- "Search for entries of ENTRY-TYPE.
-
-Call an appropriate scheme function and return a list of the
-form of `guix-entries'.
-
-ENTRY-TYPE should be one of the following symbols: `package',
-`output' or `generation'.
-
-SEARCH-TYPE may be one of the following symbols:
-
-- If ENTRY-TYPE is `package' or `output': `id', `name', `regexp',
- `all-available', `newest-available', `installed', `obsolete',
- `generation'.
-
-- If ENTRY-TYPE is `generation': `id', `last', `all', `time'.
-
-PARAMS is a list of parameters for receiving. If nil, get
-information with all available parameters."
- (guix-eval-read (guix-make-guile-expression
- 'entries
- profile params entry-type search-type search-vals)))
-
-(defun guix-get-show-entries (profile buffer-type entry-type search-type
- &rest search-vals)
- "Search for ENTRY-TYPE entries and show results in BUFFER-TYPE buffer.
-See `guix-get-entries' for the meaning of SEARCH-TYPE and SEARCH-VALS."
- (let ((entries (guix-get-entries profile entry-type search-type search-vals
- (guix-get-params-for-receiving
- buffer-type entry-type))))
- (guix-set-buffer profile entries buffer-type entry-type
- search-type search-vals)))
-
-(defun guix-set-buffer (profile entries buffer-type entry-type search-type
- search-vals &optional history-replace no-display)
- "Set up BUFFER-TYPE buffer for displaying ENTRY-TYPE ENTRIES.
-
-Insert ENTRIES in buffer, set variables and make history item.
-ENTRIES should have a form of `guix-entries'.
-
-See `guix-get-entries' for the meaning of SEARCH-TYPE and SEARCH-VALS.
-
-If HISTORY-REPLACE is non-nil, replace current history item,
-otherwise add the new one.
-
-If NO-DISPLAY is non-nil, do not switch to the buffer."
- (when entries
- (let ((buf (if (and (eq major-mode
- (guix-get-symbol "mode" buffer-type entry-type))
- (equal guix-profile profile))
- (current-buffer)
- (get-buffer-create
- (guix-buffer-name profile buffer-type
- entry-type search-type)))))
- (with-current-buffer buf
- (guix-show-entries entries buffer-type entry-type)
- (guix-set-vars profile entries buffer-type entry-type
- search-type search-vals)
- (funcall (if history-replace
- #'guix-history-replace
- #'guix-history-add)
- (guix-make-history-item)))
- (or no-display
- (guix-switch-to-buffer buf))))
- (guix-result-message profile entries entry-type
- search-type search-vals))
-
-(defun guix-show-entries (entries buffer-type entry-type)
- "Display ENTRY-TYPE ENTRIES in the current BUFFER-TYPE buffer."
- (let ((inhibit-read-only t))
- (erase-buffer)
- (funcall (symbol-function (guix-get-symbol
- "mode" buffer-type entry-type)))
- (funcall (guix-get-symbol "insert-entries" buffer-type)
- entries entry-type)
- (goto-char (point-min))))
-
-(defun guix-history-call (profile entries buffer-type entry-type
- search-type search-vals)
- "Function called for moving by history."
- (guix-show-entries entries buffer-type entry-type)
- (guix-set-vars profile entries buffer-type entry-type
- search-type search-vals)
- (guix-result-message profile entries entry-type
- search-type search-vals))
-
-(defun guix-make-history-item ()
- "Make and return a history item for the current buffer."
- (list #'guix-history-call
- guix-profile guix-entries guix-buffer-type guix-entry-type
- guix-search-type guix-search-vals))
-
-(defun guix-get-params-for-receiving (buffer-type entry-type)
- "Return parameters that should be received for BUFFER-TYPE, ENTRY-TYPE."
- (let* ((required-var (guix-get-symbol "required-params"
- buffer-type entry-type))
- (required (symbol-value required-var)))
- (unless (equal required 'all)
- (cl-union required
- (funcall (guix-get-symbol "get-displayed-params"
- buffer-type)
- entry-type)))))
-
-(defun guix-revert-buffer (_ignore-auto noconfirm)
- "Update information in the current buffer.
-The function is suitable for `revert-buffer-function'.
-See `revert-buffer' for the meaning of NOCONFIRM."
- (when (or noconfirm
- (symbol-value
- (guix-get-symbol "revert-no-confirm"
- guix-buffer-type guix-entry-type))
- (y-or-n-p "Update current information? "))
- (let* ((search-type guix-search-type)
- (search-vals guix-search-vals)
- (params (guix-get-params-for-receiving guix-buffer-type
- guix-entry-type))
- (entries (guix-get-entries
- guix-profile guix-entry-type
- guix-search-type guix-search-vals params))
- ;; If a REPL was restarted, package/output IDs are not actual
- ;; anymore, because 'object-address'-es died with the REPL, so if a
- ;; search by ID didn't give results, search again by name.
- (entries (if (and (null entries)
- (eq guix-search-type 'id)
- (or (eq guix-entry-type 'package)
- (eq guix-entry-type 'output)))
- (progn
- (setq search-type 'name
- search-vals (guix-entries-to-specifications
- guix-entries))
- (guix-get-entries
- guix-profile guix-entry-type
- search-type search-vals params))
- entries)))
- (guix-set-buffer guix-profile entries guix-buffer-type guix-entry-type
- search-type search-vals t t))))
-
-(cl-defun guix-redisplay-buffer (&key buffer profile entries buffer-type
- entry-type search-type search-vals)
- "Redisplay a Guix BUFFER.
-Restore the point and window positions after redisplaying if possible.
-
-This function will not update the information, use
-\"\\[revert-buffer]\" if you want the full update.
-
-If BUFFER is nil, use the current buffer. For the meaning of the
-rest arguments, see `guix-set-buffer'."
- (interactive)
- (or buffer (setq buffer (current-buffer)))
- (with-current-buffer buffer
- (or (derived-mode-p 'guix-info-mode 'guix-list-mode)
- (error "%S is not a Guix buffer" buffer))
- (let* ((point (point))
- (was-at-button (button-at point))
- ;; For simplicity, ignore an unlikely case when multiple
- ;; windows display the same BUFFER.
- (window (car (get-buffer-window-list buffer nil t)))
- (window-start (and window (window-start window))))
- (guix-set-buffer (or profile guix-profile)
- (or entries guix-entries)
- (or buffer-type guix-buffer-type)
- (or entry-type guix-entry-type)
- (or search-type guix-search-type)
- (or search-vals guix-search-vals)
- t t)
- (goto-char point)
- (and was-at-button
- (not (button-at (point)))
- (forward-button 1))
- (when window
- (set-window-point window (point))
- (set-window-start window window-start)))))
-
-
-;;; Generations
-
-(defcustom guix-generation-packages-buffer-name-function
- #'guix-generation-packages-buffer-name-default
- "Function used to define name of a buffer with generation packages.
-This function is called with 2 arguments: PROFILE (string) and
-GENERATION (number)."
- :type '(choice (function-item guix-generation-packages-buffer-name-default)
- (function-item guix-generation-packages-buffer-name-long)
- (function :tag "Other function"))
- :group 'guix)
-
-(defcustom guix-generation-packages-update-buffer t
- "If non-nil, always update list of packages during comparing generations.
-If nil, generation packages are received only once. So when you
-compare generation 1 and generation 2, the packages for both
-generations will be received. Then if you compare generation 1
-and generation 3, only the packages for generation 3 will be
-received. Thus if you use comparing of different generations a
-lot, you may set this variable to nil to improve the
-performance."
- :type 'boolean
- :group 'guix)
-
-(defvar guix-output-name-width 30
- "Width of an output name \"column\".
-This variable is used in auxiliary buffers for comparing generations.")
-
(defun guix-generation-file (profile generation)
"Return the file name of a PROFILE's GENERATION."
(format "%s-%s-link" profile generation))
@@ -724,74 +100,14 @@ this generation."
(guix-generation-file profile generation)
profile)))
-(defun guix-generation-packages (profile generation)
- "Return a list of sorted packages installed in PROFILE's GENERATION.
-Each element of the list is a list of the package specification and its path."
- (let ((names+paths (guix-eval-read
- (guix-make-guile-expression
- 'generation-package-specifications+paths
- profile generation))))
- (sort names+paths
- (lambda (a b)
- (string< (car a) (car b))))))
-
-(defun guix-generation-packages-buffer-name-default (profile generation)
- "Return name of a buffer for displaying GENERATION's package outputs.
-Use base name of PROFILE path."
- (let ((profile-name (file-name-base (directory-file-name profile))))
- (format "*Guix %s: generation %s*"
- profile-name generation)))
-
-(defun guix-generation-packages-buffer-name-long (profile generation)
- "Return name of a buffer for displaying GENERATION's package outputs.
-Use the full PROFILE path."
- (format "*Guix generation %s (%s)*"
- generation profile))
-
-(defun guix-generation-packages-buffer-name (profile generation)
- "Return name of a buffer for displaying GENERATION's package outputs."
- (let ((fun (if (functionp guix-generation-packages-buffer-name-function)
- guix-generation-packages-buffer-name-function
- #'guix-generation-packages-buffer-name-default)))
- (funcall fun profile generation)))
-
-(defun guix-generation-insert-package (name path)
- "Insert package output NAME and PATH at point."
- (insert name)
- (indent-to guix-output-name-width 2)
- (insert path "\n"))
-
-(defun guix-generation-insert-packages (buffer profile generation)
- "Insert package outputs installed in PROFILE's GENERATION in BUFFER."
- (with-current-buffer buffer
- (setq buffer-read-only nil
- indent-tabs-mode nil)
- (erase-buffer)
- (mapc (lambda (name+path)
- (guix-generation-insert-package
- (car name+path) (cadr name+path)))
- (guix-generation-packages profile generation))))
-
-(defun guix-generation-packages-buffer (profile generation)
- "Return buffer with package outputs installed in PROFILE's GENERATION.
-Create the buffer if needed."
- (let ((buf-name (guix-generation-packages-buffer-name
- profile generation)))
- (or (and (null guix-generation-packages-update-buffer)
- (get-buffer buf-name))
- (let ((buf (get-buffer-create buf-name)))
- (guix-generation-insert-packages buf profile generation)
- buf))))
-
-(defun guix-profile-generation-manifest-file (generation)
- "Return the file name of a GENERATION's manifest.
-GENERATION is a generation number of `guix-profile' profile."
- (guix-manifest-file guix-profile generation))
-
-(defun guix-profile-generation-packages-buffer (generation)
- "Insert GENERATION's package outputs in a buffer and return it.
-GENERATION is a generation number of `guix-profile' profile."
- (guix-generation-packages-buffer guix-profile generation))
+;;;###autoload
+(defun guix-edit (id-or-name)
+ "Edit (go to location of) package with ID-OR-NAME."
+ (interactive (list (guix-read-package-name)))
+ (let ((loc (guix-package-location id-or-name)))
+ (if loc
+ (guix-find-location loc)
+ (message "Couldn't find package location."))))
;;; Actions on packages and generations
@@ -865,101 +181,6 @@ VARIABLE is a name of an option variable.")
guix-operation-option-true-string
guix-operation-option-false-string))
-(defun guix-process-package-actions (profile actions
- &optional operation-buffer)
- "Process package ACTIONS on PROFILE.
-Each action is a list of the form:
-
- (ACTION-TYPE PACKAGE-SPEC ...)
-
-ACTION-TYPE is one of the following symbols: `install',
-`upgrade', `remove'/`delete'.
-PACKAGE-SPEC should have the following form: (ID [OUTPUT] ...)."
- (let (install upgrade remove)
- (mapc (lambda (action)
- (let ((action-type (car action))
- (specs (cdr action)))
- (cl-case action-type
- (install (setq install (append install specs)))
- (upgrade (setq upgrade (append upgrade specs)))
- ((remove delete) (setq remove (append remove specs))))))
- actions)
- (when (guix-continue-package-operation-p
- profile
- :install install :upgrade upgrade :remove remove)
- (guix-eval-in-repl
- (guix-make-guile-expression
- 'process-package-actions profile
- :install install :upgrade upgrade :remove remove
- :use-substitutes? (or guix-use-substitutes 'f)
- :dry-run? (or guix-dry-run 'f))
- (and (not guix-dry-run) operation-buffer)))))
-
-(cl-defun guix-continue-package-operation-p (profile
- &key install upgrade remove)
- "Return non-nil if a package operation should be continued.
-Ask a user if needed (see `guix-operation-confirm').
-INSTALL, UPGRADE, REMOVE are 'package action specifications'.
-See `guix-process-package-actions' for details."
- (or (null guix-operation-confirm)
- (let* ((entries (guix-get-entries
- profile 'package 'id
- (append (mapcar #'car install)
- (mapcar #'car upgrade)
- (mapcar #'car remove))
- '(id name version location)))
- (install-strings (guix-get-package-strings install entries))
- (upgrade-strings (guix-get-package-strings upgrade entries))
- (remove-strings (guix-get-package-strings remove entries)))
- (if (or install-strings upgrade-strings remove-strings)
- (let ((buf (get-buffer-create guix-temp-buffer-name)))
- (with-current-buffer buf
- (setq-local cursor-type nil)
- (setq buffer-read-only nil)
- (erase-buffer)
- (insert "Profile: " profile "\n\n")
- (guix-insert-package-strings install-strings "install")
- (guix-insert-package-strings upgrade-strings "upgrade")
- (guix-insert-package-strings remove-strings "remove")
- (let ((win (temp-buffer-window-show
- buf
- '((display-buffer-reuse-window
- display-buffer-at-bottom)
- (window-height . fit-window-to-buffer)))))
- (prog1 (guix-operation-prompt)
- (quit-window nil win)))))
- (message "Nothing to be done. If the REPL was restarted, information is not up-to-date.")
- nil))))
-
-(defun guix-get-package-strings (specs entries)
- "Return short package descriptions for performing package actions.
-See `guix-process-package-actions' for the meaning of SPECS.
-ENTRIES is a list of package entries to get info about packages."
- (delq nil
- (mapcar
- (lambda (spec)
- (let* ((id (car spec))
- (outputs (cdr spec))
- (entry (guix-get-entry-by-id id entries)))
- (when entry
- (let ((location (guix-assq-value entry 'location)))
- (concat (guix-get-full-name entry)
- (when outputs
- (concat ":"
- (guix-concat-strings outputs ",")))
- (when location
- (concat "\t(" location ")")))))))
- specs)))
-
-(defun guix-insert-package-strings (strings action)
- "Insert information STRINGS at point for performing package ACTION."
- (when strings
- (insert "Package(s) to " (propertize action 'face 'bold) ":\n")
- (mapc (lambda (str)
- (insert " " str "\n"))
- strings)
- (insert "\n")))
-
(defun guix-operation-prompt (&optional prompt)
"Prompt a user for continuing the current operation.
Return non-nil, if the operation should be continued; nil otherwise.
@@ -1014,34 +235,6 @@ Ask a user with PROMPT for continuing an operation."
guix-operation-option-separator)))
(force-mode-line-update))
-(defun guix-delete-generations (profile generations
- &optional operation-buffer)
- "Delete GENERATIONS from PROFILE.
-Each element from GENERATIONS is a generation number."
- (when (or (not guix-operation-confirm)
- (y-or-n-p
- (let ((count (length generations)))
- (if (> count 1)
- (format "Delete %d generations from profile '%s'? "
- count profile)
- (format "Delete generation %d from profile '%s'? "
- (car generations) profile)))))
- (guix-eval-in-repl
- (guix-make-guile-expression
- 'delete-generations* profile generations)
- operation-buffer)))
-
-(defun guix-switch-to-generation (profile generation
- &optional operation-buffer)
- "Switch PROFILE to GENERATION."
- (when (or (not guix-operation-confirm)
- (y-or-n-p (format "Switch profile '%s' to generation %d? "
- profile generation)))
- (guix-eval-in-repl
- (guix-make-guile-expression
- 'switch-to-generation* profile generation)
- operation-buffer)))
-
(defun guix-package-source-path (package-id)
"Return a store file path to a source of a package PACKAGE-ID."
(message "Calculating the source derivation ...")
@@ -1075,12 +268,12 @@ See Info node `(guix) Invoking guix package' for details.
Interactively, use the current profile and prompt for manifest
FILE. With a prefix argument, also prompt for PROFILE."
(interactive
- (let* ((default-profile (or guix-profile guix-current-profile))
+ (let* ((current-profile (guix-ui-current-profile))
(profile (if current-prefix-arg
(guix-profile-prompt)
- default-profile))
+ (or current-profile guix-current-profile)))
(file (read-file-name "File with manifest: "))
- (buffer (and guix-profile (current-buffer))))
+ (buffer (and current-profile (current-buffer))))
(list profile file buffer)))
(when (or (not guix-operation-confirm)
(y-or-n-p (format "Apply manifest from '%s' to profile '%s'? "
@@ -1174,12 +367,12 @@ The function is called with a single argument - a command line string."
(defun guix-update-buffers-maybe-after-pull ()
"Update buffers depending on `guix-update-after-pull'."
(when guix-update-after-pull
- (mapc #'guix-update-buffer
+ (mapc #'guix-ui-update-buffer
;; No need to update "generation" buffers.
- (guix-buffers '(guix-package-list-mode
- guix-package-info-mode
- guix-output-list-mode
- guix-output-info-mode)))
+ (guix-ui-buffers '(guix-package-list-mode
+ guix-package-info-mode
+ guix-output-list-mode
+ guix-output-info-mode)))
(message "Guix buffers have been updated.")))
;;;###autoload
diff --git a/emacs/guix-buffer.el b/emacs/guix-buffer.el
new file mode 100644
index 0000000000..af76e638b6
--- /dev/null
+++ b/emacs/guix-buffer.el
@@ -0,0 +1,622 @@
+;;; guix-buffer.el --- Buffer interface for displaying data -*- lexical-binding: t -*-
+
+;; Copyright © 2014, 2015 Alex Kost <alezost@gmail.com>
+
+;; This file is part of GNU Guix.
+
+;; GNU Guix 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.
+
+;; GNU Guix 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.
+
+;; 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 file provides a general 'buffer' interface for displaying an
+;; arbitrary data.
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'guix-history)
+(require 'guix-utils)
+
+(defvar guix-buffer-map
+ (let ((map (make-sparse-keymap)))
+ (define-key map (kbd "l") 'guix-history-back)
+ (define-key map (kbd "r") 'guix-history-forward)
+ (define-key map (kbd "g") 'revert-buffer)
+ (define-key map (kbd "R") 'guix-buffer-redisplay)
+ map)
+ "Parent keymap for Guix buffer modes.")
+
+
+;;; Buffer item
+
+(cl-defstruct (guix-buffer-item
+ (:constructor nil)
+ (:constructor guix-buffer-make-item
+ (entries buffer-type entry-type args))
+ (:copier nil))
+ entries buffer-type entry-type args)
+
+(defvar-local guix-buffer-item nil
+ "Data (structure) for the current Guix buffer.
+The structure consists of the following elements:
+
+- `entries': list of the currently displayed entries.
+
+ Each element of the list is an alist with an entry data of the
+ following form:
+
+ ((PARAM . VAL) ...)
+
+ PARAM is a name of the entry parameter.
+ VAL is a value of this parameter.
+
+- `entry-type': type of the currently displayed entries.
+
+- `buffer-type': type of the current buffer.
+
+- `args': search arguments used to get the current entries.")
+(put 'guix-buffer-item 'permanent-local t)
+
+(defmacro guix-buffer-with-item (item &rest body)
+ "Evaluate BODY using buffer ITEM.
+The following local variables are available inside BODY:
+`%entries', `%buffer-type', `%entry-type', `%args'.
+See `guix-buffer-item' for details."
+ (declare (indent 1) (debug t))
+ (let ((item-var (make-symbol "item")))
+ `(let ((,item-var ,item))
+ (let ((%entries (guix-buffer-item-entries ,item-var))
+ (%buffer-type (guix-buffer-item-buffer-type ,item-var))
+ (%entry-type (guix-buffer-item-entry-type ,item-var))
+ (%args (guix-buffer-item-args ,item-var)))
+ ,@body))))
+
+(defmacro guix-buffer-with-current-item (&rest body)
+ "Evaluate BODY using `guix-buffer-item'.
+See `guix-buffer-with-item' for details."
+ (declare (indent 0) (debug t))
+ `(guix-buffer-with-item guix-buffer-item
+ ,@body))
+
+(defmacro guix-buffer-define-current-item-accessor (name)
+ "Define `guix-buffer-current-NAME' function to access NAME
+element of `guix-buffer-item' structure.
+NAME should be a symbol."
+ (let* ((name-str (symbol-name name))
+ (accessor (intern (concat "guix-buffer-item-" name-str)))
+ (fun-name (intern (concat "guix-buffer-current-" name-str)))
+ (doc (format "\
+Return '%s' of the current Guix buffer.
+See `guix-buffer-item' for details."
+ name-str)))
+ `(defun ,fun-name ()
+ ,doc
+ (and guix-buffer-item
+ (,accessor guix-buffer-item)))))
+
+(defmacro guix-buffer-define-current-item-accessors (&rest names)
+ "Define `guix-buffer-current-NAME' functions for NAMES.
+See `guix-buffer-define-current-item-accessor' for details."
+ `(progn
+ ,@(mapcar (lambda (name)
+ `(guix-buffer-define-current-item-accessor ,name))
+ names)))
+
+(guix-buffer-define-current-item-accessors
+ entries entry-type buffer-type args)
+
+(defmacro guix-buffer-define-current-args-accessor (n prefix name)
+ "Define `PREFIX-NAME' function to access Nth element of 'args'
+field of `guix-buffer-item' structure.
+PREFIX and NAME should be strings."
+ (let ((fun-name (intern (concat prefix "-" name)))
+ (doc (format "\
+Return '%s' of the current Guix buffer.
+'%s' is the element number %d in 'args' of `guix-buffer-item'."
+ name name n)))
+ `(defun ,fun-name ()
+ ,doc
+ (nth ,n (guix-buffer-current-args)))))
+
+(defmacro guix-buffer-define-current-args-accessors (prefix &rest names)
+ "Define `PREFIX-NAME' functions for NAMES.
+See `guix-buffer-define-current-args-accessor' for details."
+ `(progn
+ ,@(cl-loop for name in names
+ for i from 0
+ collect `(guix-buffer-define-current-args-accessor
+ ,i ,prefix ,name))))
+
+
+;;; Wrappers for defined variables
+
+(defvar guix-buffer-data nil
+ "Alist with 'buffer' data.
+This alist is filled by `guix-buffer-define-interface' macro.")
+
+(defun guix-buffer-value (buffer-type entry-type symbol)
+ "Return SYMBOL's value for BUFFER-TYPE/ENTRY-TYPE from `guix-buffer-data'."
+ (symbol-value
+ (guix-assq-value guix-buffer-data buffer-type entry-type symbol)))
+
+(defun guix-buffer-get-entries (buffer-type entry-type args)
+ "Return ENTRY-TYPE entries.
+Call an appropriate 'get-entries' function from `guix-buffer'
+using ARGS as its arguments."
+ (apply (guix-buffer-value buffer-type entry-type 'get-entries)
+ args))
+
+(defun guix-buffer-mode-enable (buffer-type entry-type)
+ "Turn on major mode to display ENTRY-TYPE ENTRIES in BUFFER-TYPE buffer."
+ (funcall (guix-buffer-value buffer-type entry-type 'mode)))
+
+(defun guix-buffer-mode-initialize (buffer-type entry-type)
+ "Set up the current BUFFER-TYPE buffer to display ENTRY-TYPE entries."
+ (let ((fun (guix-buffer-value buffer-type entry-type 'mode-init)))
+ (when fun
+ (funcall fun))))
+
+(defun guix-buffer-insert-entries (entries buffer-type entry-type)
+ "Show ENTRY-TYPE ENTRIES in the current BUFFER-TYPE buffer."
+ (funcall (guix-buffer-value buffer-type entry-type 'insert-entries)
+ entries))
+
+(defun guix-buffer-show-entries-default (entries buffer-type entry-type)
+ "Show ENTRY-TYPE ENTRIES in the current BUFFER-TYPE buffer."
+ (let ((inhibit-read-only t))
+ (erase-buffer)
+ (guix-buffer-mode-enable buffer-type entry-type)
+ (guix-buffer-insert-entries entries buffer-type entry-type)
+ (goto-char (point-min))))
+
+(defun guix-buffer-show-entries (entries buffer-type entry-type)
+ "Show ENTRY-TYPE ENTRIES in the current BUFFER-TYPE buffer."
+ (funcall (guix-buffer-value buffer-type entry-type 'show-entries)
+ entries))
+
+(defun guix-buffer-message (entries buffer-type entry-type args)
+ "Display a message for BUFFER-ITEM after showing entries."
+ (let ((fun (guix-buffer-value buffer-type entry-type 'message)))
+ (when fun
+ (apply fun entries args))))
+
+(defun guix-buffer-name (buffer-type entry-type args)
+ "Return name of BUFFER-TYPE buffer for displaying ENTRY-TYPE entries."
+ (let ((str-or-fun (guix-buffer-value buffer-type entry-type
+ 'buffer-name)))
+ (if (stringp str-or-fun)
+ str-or-fun
+ (apply str-or-fun args))))
+
+(defun guix-buffer-param-title (buffer-type entry-type param)
+ "Return PARAM title for BUFFER-TYPE/ENTRY-TYPE."
+ (or (guix-assq-value (guix-buffer-value buffer-type entry-type 'titles)
+ param)
+ ;; Fallback to a title defined in 'info' interface.
+ (unless (eq buffer-type 'info)
+ (guix-assq-value (guix-buffer-value 'info entry-type 'titles)
+ param))
+ (guix-symbol-title param)))
+
+(defun guix-buffer-history-size (buffer-type entry-type)
+ "Return history size for BUFFER-TYPE/ENTRY-TYPE."
+ (guix-buffer-value buffer-type entry-type 'history-size))
+
+(defun guix-buffer-revert-confirm? (buffer-type entry-type)
+ "Return 'revert-confirm' value for BUFFER-TYPE/ENTRY-TYPE."
+ (guix-buffer-value buffer-type entry-type 'revert-confirm))
+
+
+;;; Displaying entries
+
+(defun guix-buffer-display (buffer)
+ "Switch to a Guix BUFFER."
+ (pop-to-buffer buffer
+ '((display-buffer-reuse-window
+ display-buffer-same-window))))
+
+(defun guix-buffer-history-item (buffer-item)
+ "Make and return a history item for displaying BUFFER-ITEM."
+ (list #'guix-buffer-set buffer-item))
+
+(defun guix-buffer-set (buffer-item &optional history)
+ "Set up the current buffer for displaying BUFFER-ITEM.
+HISTORY should be one of the following:
+
+ `nil' - do not save BUFFER-ITEM in history,
+
+ `add' - add it to history,
+
+ `replace' - replace the current history item."
+ (guix-buffer-with-item buffer-item
+ (when %entries
+ (guix-buffer-show-entries %entries %buffer-type %entry-type)
+ (setq guix-buffer-item buffer-item)
+ (when history
+ (funcall (cl-ecase history
+ (add #'guix-history-add)
+ (replace #'guix-history-replace))
+ (guix-buffer-history-item buffer-item))))
+ (guix-buffer-message %entries %buffer-type %entry-type %args)))
+
+(defun guix-buffer-display-entries-current
+ (entries buffer-type entry-type args &optional history)
+ "Show ENTRIES in the current Guix buffer.
+See `guix-buffer-item' for the meaning of BUFFER-TYPE, ENTRY-TYPE
+and ARGS, and `guix-buffer-set' for the meaning of HISTORY."
+ (let ((item (guix-buffer-make-item entries buffer-type
+ entry-type args)))
+ (guix-buffer-set item history)))
+
+(defun guix-buffer-get-display-entries-current
+ (buffer-type entry-type args &optional history)
+ "Search for entries and show them in the current Guix buffer.
+See `guix-buffer-display-entries-current' for details."
+ (guix-buffer-display-entries-current
+ (guix-buffer-get-entries buffer-type entry-type args)
+ buffer-type entry-type args history))
+
+(defun guix-buffer-display-entries
+ (entries buffer-type entry-type args &optional history)
+ "Show ENTRIES in a BUFFER-TYPE buffer.
+See `guix-buffer-display-entries-current' for details."
+ (let ((buffer (get-buffer-create
+ (guix-buffer-name buffer-type entry-type args))))
+ (with-current-buffer buffer
+ (guix-buffer-display-entries-current
+ entries buffer-type entry-type args history))
+ (when entries
+ (guix-buffer-display buffer))))
+
+(defun guix-buffer-get-display-entries
+ (buffer-type entry-type args &optional history)
+ "Search for entries and show them in a BUFFER-TYPE buffer.
+See `guix-buffer-display-entries-current' for details."
+ (guix-buffer-display-entries
+ (guix-buffer-get-entries buffer-type entry-type args)
+ buffer-type entry-type args history))
+
+(defun guix-buffer-revert (_ignore-auto noconfirm)
+ "Update the data in the current Guix buffer.
+This function is suitable for `revert-buffer-function'.
+See `revert-buffer' for the meaning of NOCONFIRM."
+ (guix-buffer-with-current-item
+ (when (or noconfirm
+ (not (guix-buffer-revert-confirm? %buffer-type %entry-type))
+ (y-or-n-p "Update the current buffer? "))
+ (guix-buffer-get-display-entries-current
+ %buffer-type %entry-type %args 'replace))))
+
+(defvar guix-buffer-after-redisplay-hook nil
+ "Hook run by `guix-buffer-redisplay'.
+This hook is called before seting up a window position.")
+
+(defun guix-buffer-redisplay ()
+ "Redisplay the current Guix buffer.
+Restore the point and window positions after redisplaying.
+
+This function does not update the buffer data, use
+'\\[revert-buffer]' if you want the full update."
+ (interactive)
+ (let* ((old-point (point))
+ ;; For simplicity, ignore an unlikely case when multiple
+ ;; windows display the same buffer.
+ (window (car (get-buffer-window-list (current-buffer) nil t)))
+ (window-start (and window (window-start window))))
+ (guix-buffer-set guix-buffer-item)
+ (goto-char old-point)
+ (run-hooks 'guix-buffer-after-redisplay-hook)
+ (when window
+ (set-window-point window (point))
+ (set-window-start window window-start))))
+
+(defun guix-buffer-redisplay-goto-button ()
+ "Redisplay the current buffer and go to the next button, if needed."
+ (let ((guix-buffer-after-redisplay-hook
+ (cons (lambda ()
+ (unless (button-at (point))
+ (forward-button 1)))
+ guix-buffer-after-redisplay-hook)))
+ (guix-buffer-redisplay)))
+
+
+;;; Interface definers
+
+(defmacro guix-define-groups (type &rest args)
+ "Define `guix-TYPE' and `guix-TYPE-faces' custom groups.
+Remaining arguments (ARGS) should have a form [KEYWORD VALUE] ...
+
+Optional keywords:
+
+ - `:parent-group' - name of a parent custom group.
+
+ - `:parent-faces-group' - name of a parent custom faces group.
+
+ - `:group-doc' - docstring of a `guix-TYPE' group.
+
+ - `:faces-group-doc' - docstring of a `guix-TYPE-faces' group."
+ (declare (indent 1))
+ (let* ((type-str (symbol-name type))
+ (prefix (concat "guix-" type-str))
+ (group (intern prefix))
+ (faces-group (intern (concat prefix "-faces"))))
+ (guix-keyword-args-let args
+ ((parent-group :parent-group 'guix)
+ (parent-faces-group :parent-faces-group 'guix-faces)
+ (group-doc :group-doc
+ (format "Settings for '%s' buffers."
+ type-str))
+ (faces-group-doc :faces-group-doc
+ (format "Faces for '%s' buffers."
+ type-str)))
+ `(progn
+ (defgroup ,group nil
+ ,group-doc
+ :group ',parent-group)
+
+ (defgroup ,faces-group nil
+ ,faces-group-doc
+ :group ',group
+ :group ',parent-faces-group)))))
+
+(defmacro guix-define-entry-type (entry-type &rest args)
+ "Define general code for ENTRY-TYPE.
+See `guix-define-groups'."
+ (declare (indent 1))
+ `(guix-define-groups ,entry-type
+ ,@args))
+
+(defmacro guix-define-buffer-type (buffer-type &rest args)
+ "Define general code for BUFFER-TYPE.
+See `guix-define-groups'."
+ (declare (indent 1))
+ `(guix-define-groups ,buffer-type
+ ,@args))
+
+(defmacro guix-buffer-define-interface (buffer-type entry-type &rest args)
+ "Define BUFFER-TYPE interface for displaying ENTRY-TYPE entries.
+Remaining arguments (ARGS) should have a form [KEYWORD VALUE] ...
+In the following description TYPE means ENTRY-TYPE-BUFFER-TYPE.
+
+Required keywords:
+
+ - `:buffer-name' - default value of the generated
+ `guix-TYPE-buffer-name' variable.
+
+ - `:get-entries-function' - default value of the generated
+ `guix-TYPE-get-function' variable.
+
+ - `:show-entries-function' - default value of the generated
+ `guix-TYPE-show-function' variable.
+
+ Alternatively, if `:show-entries-function' is not specified, a
+ default `guix-TYPE-show-entries' will be generated, and the
+ following keyword should be specified instead:
+
+ - `:insert-entries-function' - default value of the generated
+ `guix-TYPE-insert-function' variable.
+
+Optional keywords:
+
+ - `:message-function' - default value of the generated
+ `guix-TYPE-message-function' variable.
+
+ - `:titles' - default value of the generated
+ `guix-TYPE-titles' variable.
+
+ - `:history-size' - default value of the generated
+ `guix-TYPE-history-size' variable.
+
+ - `:revert-confirm?' - default value of the generated
+ `guix-TYPE-revert-confirm' variable.
+
+ - `:mode-name' - name (a string appeared in the mode-line) of
+ the generated `guix-TYPE-mode'.
+
+ - `:mode-init-function' - default value of the generated
+ `guix-TYPE-mode-initialize-function' variable.
+
+ - `:reduced?' - if non-nil, generate only group, faces group
+ and titles variable (if specified); all keywords become
+ optional."
+ (declare (indent 2))
+ (let* ((entry-type-str (symbol-name entry-type))
+ (buffer-type-str (symbol-name buffer-type))
+ (prefix (concat "guix-" entry-type-str "-"
+ buffer-type-str))
+ (group (intern prefix))
+ (faces-group (intern (concat prefix "-faces")))
+ (get-entries-var (intern (concat prefix "-get-function")))
+ (show-entries-var (intern (concat prefix "-show-function")))
+ (show-entries-fun (intern (concat prefix "-show-entries")))
+ (message-var (intern (concat prefix "-message-function")))
+ (buffer-name-var (intern (concat prefix "-buffer-name")))
+ (titles-var (intern (concat prefix "-titles")))
+ (history-size-var (intern (concat prefix "-history-size")))
+ (revert-confirm-var (intern (concat prefix "-revert-confirm"))))
+ (guix-keyword-args-let args
+ ((get-entries-val :get-entries-function)
+ (show-entries-val :show-entries-function)
+ (insert-entries-val :insert-entries-function)
+ (mode-name :mode-name (capitalize prefix))
+ (mode-init-val :mode-init-function)
+ (message-val :message-function)
+ (buffer-name-val :buffer-name)
+ (titles-val :titles)
+ (history-size-val :history-size 20)
+ (revert-confirm-val :revert-confirm? t)
+ (reduced? :reduced?))
+ `(progn
+ (defgroup ,group nil
+ ,(format "Displaying '%s' entries in '%s' buffer."
+ entry-type-str buffer-type-str)
+ :group ',(intern (concat "guix-" entry-type-str))
+ :group ',(intern (concat "guix-" buffer-type-str)))
+
+ (defgroup ,faces-group nil
+ ,(format "Faces for displaying '%s' entries in '%s' buffer."
+ entry-type-str buffer-type-str)
+ :group ',group
+ :group ',(intern (concat "guix-" entry-type-str "-faces"))
+ :group ',(intern (concat "guix-" buffer-type-str "-faces")))
+
+ (defcustom ,titles-var ,titles-val
+ ,(format "Alist of titles of '%s' parameters."
+ entry-type-str)
+ :type '(alist :key-type symbol :value-type string)
+ :group ',group)
+
+ ,(unless reduced?
+ `(progn
+ (defvar ,get-entries-var ,get-entries-val
+ ,(format "\
+Function used to receive '%s' entries for '%s' buffer."
+ entry-type-str buffer-type-str))
+
+ (defvar ,show-entries-var
+ ,(or show-entries-val `',show-entries-fun)
+ ,(format "\
+Function used to show '%s' entries in '%s' buffer."
+ entry-type-str buffer-type-str))
+
+ (defvar ,message-var ,message-val
+ ,(format "\
+Function used to display a message after showing '%s' entries.
+If nil, do not display messages."
+ entry-type-str))
+
+ (defcustom ,buffer-name-var ,buffer-name-val
+ ,(format "\
+Default name of '%s' buffer for displaying '%s' entries.
+May be a string or a function returning a string. The function
+is called with the same arguments as `%S'."
+ buffer-type-str entry-type-str get-entries-var)
+ :type '(choice string function)
+ :group ',group)
+
+ (defcustom ,history-size-var ,history-size-val
+ ,(format "\
+Maximum number of items saved in history of `%S' buffer.
+If 0, the history is disabled."
+ buffer-name-var)
+ :type 'integer
+ :group ',group)
+
+ (defcustom ,revert-confirm-var ,revert-confirm-val
+ ,(format "\
+If non-nil, ask to confirm for reverting `%S' buffer."
+ buffer-name-var)
+ :type 'boolean
+ :group ',group)
+
+ (guix-alist-put!
+ '((get-entries . ,get-entries-var)
+ (show-entries . ,show-entries-var)
+ (message . ,message-var)
+ (buffer-name . ,buffer-name-var)
+ (history-size . ,history-size-var)
+ (revert-confirm . ,revert-confirm-var))
+ 'guix-buffer-data ',buffer-type ',entry-type)
+
+ ,(unless show-entries-val
+ `(defun ,show-entries-fun (entries)
+ ,(format "\
+Show '%s' ENTRIES in the current '%s' buffer."
+ entry-type-str buffer-type-str)
+ (guix-buffer-show-entries-default
+ entries ',buffer-type ',entry-type)))
+
+ ,(when (or insert-entries-val
+ (null show-entries-val))
+ (let ((insert-entries-var
+ (intern (concat prefix "-insert-function"))))
+ `(progn
+ (defvar ,insert-entries-var ,insert-entries-val
+ ,(format "\
+Function used to print '%s' entries in '%s' buffer."
+ entry-type-str buffer-type-str))
+
+ (guix-alist-put!
+ ',insert-entries-var 'guix-buffer-data
+ ',buffer-type ',entry-type
+ 'insert-entries))))
+
+ ,(when (or mode-name
+ mode-init-val
+ (null show-entries-val))
+ (let* ((mode-str (concat prefix "-mode"))
+ (mode-map-str (concat mode-str "-map"))
+ (mode (intern mode-str))
+ (parent-mode (intern
+ (concat "guix-" buffer-type-str
+ "-mode")))
+ (mode-var (intern
+ (concat mode-str "-function")))
+ (mode-init-var (intern
+ (concat mode-str
+ "-initialize-function"))))
+ `(progn
+ (defvar ,mode-var ',mode
+ ,(format "\
+Major mode for displaying '%s' entries in '%s' buffer."
+ entry-type-str buffer-type-str))
+
+ (defvar ,mode-init-var ,mode-init-val
+ ,(format "\
+Function used to set up '%s' buffer for displaying '%s' entries."
+ buffer-type-str entry-type-str))
+
+ (define-derived-mode ,mode ,parent-mode ,mode-name
+ ,(format "\
+Major mode for displaying '%s' entries in '%s' buffer.
+
+\\{%s}"
+ entry-type-str buffer-type-str mode-map-str)
+ (setq-local revert-buffer-function
+ 'guix-buffer-revert)
+ (setq-local guix-history-size
+ (guix-buffer-history-size
+ ',buffer-type ',entry-type))
+ (guix-buffer-mode-initialize
+ ',buffer-type ',entry-type))
+
+ (guix-alist-put!
+ ',mode-var 'guix-buffer-data
+ ',buffer-type ',entry-type 'mode)
+ (guix-alist-put!
+ ',mode-init-var 'guix-buffer-data
+ ',buffer-type ',entry-type
+ 'mode-init))))))
+
+ (guix-alist-put!
+ ',titles-var 'guix-buffer-data
+ ',buffer-type ',entry-type 'titles)))))
+
+
+(defvar guix-buffer-font-lock-keywords
+ (eval-when-compile
+ `((,(rx "(" (group (or "guix-buffer-with-item"
+ "guix-buffer-with-current-item"
+ "guix-buffer-define-interface"
+ "guix-define-groups"
+ "guix-define-entry-type"
+ "guix-define-buffer-type"))
+ symbol-end)
+ . 1))))
+
+(font-lock-add-keywords 'emacs-lisp-mode guix-buffer-font-lock-keywords)
+
+(provide 'guix-buffer)
+
+;;; guix-buffer.el ends here
diff --git a/emacs/guix-command.el b/emacs/guix-command.el
index ccd85d25b9..9cb7032abc 100644
--- a/emacs/guix-command.el
+++ b/emacs/guix-command.el
@@ -690,7 +690,7 @@ Perform pull-specific actions after operation, see
open the log file(s)."
(let* ((args (if (member "--log-file" args)
args
- (apply #'list (car args) "--log-file" (cdr args))))
+ (cl-list* (car args) "--log-file" (cdr args))))
(output (guix-command-output args))
(files (split-string output "\n" t)))
(dolist (file files)
@@ -715,10 +715,9 @@ open the log file(s)."
(map-file (or wished-map-file (guix-png-file-name)))
(args (if wished-map-file
args
- (apply #'list
- (car args)
- (concat "--map-file=" map-file)
- (cdr args)))))
+ (cl-list* (car args)
+ (concat "--map-file=" map-file)
+ (cdr args)))))
(guix-command-output args)
(guix-find-file map-file)))
diff --git a/emacs/guix-entry.el b/emacs/guix-entry.el
new file mode 100644
index 0000000000..5eed2ed015
--- /dev/null
+++ b/emacs/guix-entry.el
@@ -0,0 +1,59 @@
+;;; guix-entry.el --- 'Entry' type -*- lexical-binding: t -*-
+
+;; Copyright © 2015 Alex Kost <alezost@gmail.com>
+
+;; This file is part of GNU Guix.
+
+;; GNU Guix 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.
+
+;; GNU Guix 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.
+
+;; 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 file provides an API for 'entry' type which is just an alist of
+;; KEY/VALUE pairs (KEY should be a symbol) with the required 'id' KEY.
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'guix-utils)
+
+(defalias 'guix-entry-value #'guix-assq-value)
+
+(defun guix-entry-id (entry)
+ "Return ENTRY ID."
+ (guix-entry-value entry 'id))
+
+(defun guix-entry-by-id (id entries)
+ "Return an entry from ENTRIES by its ID."
+ (cl-find-if (lambda (entry)
+ (equal (guix-entry-id entry) id))
+ entries))
+
+(defun guix-entries-by-ids (ids entries)
+ "Return entries with IDS (a list of identifiers) from ENTRIES."
+ (cl-remove-if-not (lambda (entry)
+ (member (guix-entry-id entry) ids))
+ entries))
+
+(defun guix-replace-entry (id new-entry entries)
+ "Replace an entry with ID from ENTRIES by NEW-ENTRY.
+Return a list of entries with the replaced entry."
+ (cl-substitute-if new-entry
+ (lambda (entry)
+ (equal id (guix-entry-id entry)))
+ entries
+ :count 1))
+
+(provide 'guix-entry)
+
+;;; guix-entry.el ends here
diff --git a/emacs/guix-external.el b/emacs/guix-external.el
index c80b36343d..f571ffd845 100644
--- a/emacs/guix-external.el
+++ b/emacs/guix-external.el
@@ -23,6 +23,7 @@
;;; Code:
+(require 'cl-lib)
(require 'guix-config)
(defgroup guix-external nil
@@ -67,10 +68,9 @@ If ARGS is nil, use `guix-dot-default-arguments'."
(or guix-dot-program
(error (concat "Couldn't find 'dot'.\n"
"Set guix-dot-program to a proper value")))
- (apply #'list
- guix-dot-program
- (concat "-o" output-file)
- (or args guix-dot-default-arguments)))
+ (cl-list* guix-dot-program
+ (concat "-o" output-file)
+ (or args guix-dot-default-arguments)))
(defun guix-dot-file-name ()
"Call `guix-dot-file-name-function'."
diff --git a/emacs/guix-hydra-build.el b/emacs/guix-hydra-build.el
new file mode 100644
index 0000000000..232221e773
--- /dev/null
+++ b/emacs/guix-hydra-build.el
@@ -0,0 +1,362 @@
+;;; guix-hydra-build.el --- Interface for Hydra builds -*- lexical-binding: t -*-
+
+;; Copyright © 2015 Alex Kost <alezost@gmail.com>
+
+;; This file is part of GNU Guix.
+
+;; GNU Guix 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.
+
+;; GNU Guix 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.
+
+;; 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 file provides an interface for displaying Hydra builds in
+;; 'list' and 'info' buffers.
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'guix-buffer)
+(require 'guix-list)
+(require 'guix-info)
+(require 'guix-hydra)
+(require 'guix-build-log)
+(require 'guix-utils)
+
+(guix-hydra-define-entry-type hydra-build
+ :search-types '((latest . guix-hydra-build-latest-api-url)
+ (queue . guix-hydra-build-queue-api-url))
+ :filters '(guix-hydra-build-filter-status)
+ :filter-names '((nixname . name)
+ (buildstatus . build-status)
+ (timestamp . time))
+ :filter-boolean-params '(finished busy))
+
+(defun guix-hydra-build-get-display (search-type &rest args)
+ "Search for Hydra builds and show results."
+ (apply #'guix-list-get-display-entries
+ 'hydra-build search-type args))
+
+(cl-defun guix-hydra-build-latest-prompt-args (&key project jobset
+ job system)
+ "Prompt for and return a list of 'latest builds' arguments."
+ (let* ((number (read-number "Number of latest builds: "))
+ (project (if current-prefix-arg
+ (guix-hydra-read-project nil project)
+ project))
+ (jobset (if current-prefix-arg
+ (guix-hydra-read-jobset nil jobset)
+ jobset))
+ (job-or-name (if current-prefix-arg
+ (guix-hydra-read-job nil job)
+ job))
+ (job (and job-or-name
+ (string-match-p guix-hydra-job-regexp
+ job-or-name)
+ job-or-name))
+ (system (if (and (not job)
+ (or current-prefix-arg
+ (and job-or-name (not system))))
+ (if job-or-name
+ (guix-while-null
+ (guix-hydra-read-system
+ (concat job-or-name ".") system))
+ (guix-hydra-read-system nil system))
+ system))
+ (job (or job
+ (and job-or-name
+ (concat job-or-name "." system)))))
+ (list number
+ :project project
+ :jobset jobset
+ :job job
+ :system system)))
+
+(defun guix-hydra-build-view-log (id)
+ "View build log of a hydra build ID."
+ (guix-build-log-find-file (guix-hydra-build-log-url id)))
+
+
+;;; Defining URLs
+
+(defun guix-hydra-build-url (id)
+ "Return Hydra URL of a build ID."
+ (guix-hydra-url "build/" (number-to-string id)))
+
+(defun guix-hydra-build-log-url (id)
+ "Return Hydra URL of the log file of a build ID."
+ (concat (guix-hydra-build-url id) "/log/raw"))
+
+(cl-defun guix-hydra-build-latest-api-url
+ (number &key project jobset job system)
+ "Return Hydra API URL to receive latest NUMBER of builds."
+ (guix-hydra-api-url "latestbuilds"
+ `(("nr" . ,number)
+ ("project" . ,project)
+ ("jobset" . ,jobset)
+ ("job" . ,job)
+ ("system" . ,system))))
+
+(defun guix-hydra-build-queue-api-url (number)
+ "Return Hydra API URL to receive the NUMBER of queued builds."
+ (guix-hydra-api-url "queue"
+ `(("nr" . ,number))))
+
+
+;;; Filters for processing raw entries
+
+(defun guix-hydra-build-filter-status (entry)
+ "Add 'status' parameter to 'hydra-build' ENTRY."
+ (let ((status (if (guix-entry-value entry 'finished)
+ (guix-hydra-build-status-number->name
+ (guix-entry-value entry 'build-status))
+ (if (guix-entry-value entry 'busy)
+ 'running
+ 'scheduled))))
+ (cons `(status . ,status)
+ entry)))
+
+
+;;; Build status
+
+(defface guix-hydra-build-status-running
+ '((t :inherit bold))
+ "Face used if hydra build is not finished."
+ :group 'guix-hydra-build-faces)
+
+(defface guix-hydra-build-status-scheduled
+ '((t))
+ "Face used if hydra build is scheduled."
+ :group 'guix-hydra-build-faces)
+
+(defface guix-hydra-build-status-succeeded
+ '((t :inherit success))
+ "Face used if hydra build succeeded."
+ :group 'guix-hydra-build-faces)
+
+(defface guix-hydra-build-status-cancelled
+ '((t :inherit warning))
+ "Face used if hydra build was cancelled."
+ :group 'guix-hydra-build-faces)
+
+(defface guix-hydra-build-status-failed
+ '((t :inherit error))
+ "Face used if hydra build failed."
+ :group 'guix-hydra-build-faces)
+
+(defvar guix-hydra-build-status-alist
+ '((0 . succeeded)
+ (1 . failed-build)
+ (2 . failed-dependency)
+ (3 . failed-other)
+ (4 . cancelled))
+ "Alist of hydra build status numbers and status names.
+Status numbers are returned by Hydra API, names (symbols) are
+used internally by the elisp code of this package.")
+
+(defun guix-hydra-build-status-number->name (number)
+ "Convert build status number to a name.
+See `guix-hydra-build-status-alist'."
+ (guix-assq-value guix-hydra-build-status-alist number))
+
+(defun guix-hydra-build-status-string (status)
+ "Return a human readable string for build STATUS."
+ (cl-case status
+ (scheduled
+ (guix-get-string "Scheduled" 'guix-hydra-build-status-scheduled))
+ (running
+ (guix-get-string "Running" 'guix-hydra-build-status-running))
+ (succeeded
+ (guix-get-string "Succeeded" 'guix-hydra-build-status-succeeded))
+ (cancelled
+ (guix-get-string "Cancelled" 'guix-hydra-build-status-cancelled))
+ (failed-build
+ (guix-hydra-build-status-fail-string))
+ (failed-dependency
+ (guix-hydra-build-status-fail-string "dependency"))
+ (failed-other
+ (guix-hydra-build-status-fail-string "other"))))
+
+(defun guix-hydra-build-status-fail-string (&optional reason)
+ "Return a string for a failed build."
+ (let ((base (guix-get-string "Failed" 'guix-hydra-build-status-failed)))
+ (if reason
+ (concat base " (" reason ")")
+ base)))
+
+(defun guix-hydra-build-finished? (entry)
+ "Return non-nil, if hydra build was finished."
+ (guix-entry-value entry 'finished))
+
+(defun guix-hydra-build-running? (entry)
+ "Return non-nil, if hydra build is running."
+ (eq (guix-entry-value entry 'status)
+ 'running))
+
+(defun guix-hydra-build-scheduled? (entry)
+ "Return non-nil, if hydra build is scheduled."
+ (eq (guix-entry-value entry 'status)
+ 'scheduled))
+
+(defun guix-hydra-build-succeeded? (entry)
+ "Return non-nil, if hydra build succeeded."
+ (eq (guix-entry-value entry 'status)
+ 'succeeded))
+
+(defun guix-hydra-build-cancelled? (entry)
+ "Return non-nil, if hydra build was cancelled."
+ (eq (guix-entry-value entry 'status)
+ 'cancelled))
+
+(defun guix-hydra-build-failed? (entry)
+ "Return non-nil, if hydra build failed."
+ (memq (guix-entry-value entry 'status)
+ '(failed-build failed-dependency failed-other)))
+
+
+;;; Hydra build 'info'
+
+(guix-hydra-info-define-interface hydra-build
+ :mode-name "Hydra-Build-Info"
+ :buffer-name "*Guix Hydra Build Info*"
+ :format '((name ignore (simple guix-info-heading))
+ ignore
+ guix-hydra-build-info-insert-url
+ (time format (time))
+ (status format guix-hydra-build-info-insert-status)
+ (project format (format guix-hydra-build-project))
+ (jobset format (format guix-hydra-build-jobset))
+ (job format (format guix-hydra-build-job))
+ (system format (format guix-hydra-build-system))
+ (priority format (format))))
+
+(defface guix-hydra-build-info-project
+ '((t :inherit link))
+ "Face for project names."
+ :group 'guix-hydra-build-info-faces)
+
+(defface guix-hydra-build-info-jobset
+ '((t :inherit link))
+ "Face for jobsets."
+ :group 'guix-hydra-build-info-faces)
+
+(defface guix-hydra-build-info-job
+ '((t :inherit link))
+ "Face for jobs."
+ :group 'guix-hydra-build-info-faces)
+
+(defface guix-hydra-build-info-system
+ '((t :inherit link))
+ "Face for system names."
+ :group 'guix-hydra-build-info-faces)
+
+(defmacro guix-hydra-build-define-button (name)
+ "Define `guix-hydra-build-NAME' button."
+ (let* ((name-str (symbol-name name))
+ (button-name (intern (concat "guix-hydra-build-" name-str)))
+ (face-name (intern (concat "guix-hydra-build-info-" name-str)))
+ (keyword (intern (concat ":" name-str))))
+ `(define-button-type ',button-name
+ :supertype 'guix
+ 'face ',face-name
+ 'help-echo ,(format "\
+Show latest builds for this %s (with prefix, prompt for all parameters)"
+ name-str)
+ 'action (lambda (btn)
+ (let ((args (guix-hydra-build-latest-prompt-args
+ ,keyword (button-label btn))))
+ (apply #'guix-hydra-build-get-display
+ 'latest args))))))
+
+(guix-hydra-build-define-button project)
+(guix-hydra-build-define-button jobset)
+(guix-hydra-build-define-button job)
+(guix-hydra-build-define-button system)
+
+(defun guix-hydra-build-info-insert-url (entry)
+ "Insert Hydra URL for the build ENTRY."
+ (guix-insert-button (guix-hydra-build-url (guix-entry-id entry))
+ 'guix-url)
+ (when (guix-hydra-build-finished? entry)
+ (guix-info-insert-indent)
+ (guix-info-insert-action-button
+ "Build log"
+ (lambda (btn)
+ (guix-hydra-build-view-log (button-get btn 'id)))
+ "View build log"
+ 'id (guix-entry-id entry))))
+
+(defun guix-hydra-build-info-insert-status (status &optional _)
+ "Insert a string with build STATUS."
+ (insert (guix-hydra-build-status-string status)))
+
+
+;;; Hydra build 'list'
+
+(guix-hydra-list-define-interface hydra-build
+ :mode-name "Hydra-Build-List"
+ :buffer-name "*Guix Hydra Build List*"
+ :format '((name nil 30 t)
+ (system nil 16 t)
+ (status guix-hydra-build-list-get-status 20 t)
+ (project nil 10 t)
+ (jobset nil 17 t)
+ (time guix-list-get-time 20 t)))
+
+(let ((map guix-hydra-build-list-mode-map))
+ (define-key map (kbd "B") 'guix-hydra-build-list-latest-builds)
+ (define-key map (kbd "L") 'guix-hydra-build-list-view-log))
+
+(defun guix-hydra-build-list-get-status (status &optional _)
+ "Return a string for build STATUS."
+ (guix-hydra-build-status-string status))
+
+(defun guix-hydra-build-list-latest-builds (number &rest args)
+ "Display latest NUMBER of Hydra builds of the current job.
+Interactively, prompt for NUMBER. With prefix argument, prompt
+for all ARGS."
+ (interactive
+ (let ((entry (guix-list-current-entry)))
+ (guix-hydra-build-latest-prompt-args
+ :project (guix-entry-value entry 'project)
+ :jobset (guix-entry-value entry 'name)
+ :job (guix-entry-value entry 'job)
+ :system (guix-entry-value entry 'system))))
+ (apply #'guix-hydra-latest-builds number args))
+
+(defun guix-hydra-build-list-view-log ()
+ "View build log of the current Hydra build."
+ (interactive)
+ (guix-hydra-build-view-log (guix-list-current-id)))
+
+
+;;; Interactive commands
+
+;;;###autoload
+(defun guix-hydra-latest-builds (number &rest args)
+ "Display latest NUMBER of Hydra builds.
+ARGS are the same arguments as for `guix-hydra-build-latest-api-url'.
+Interactively, prompt for NUMBER. With prefix argument, prompt
+for all ARGS."
+ (interactive (guix-hydra-build-latest-prompt-args))
+ (apply #'guix-hydra-build-get-display
+ 'latest number args))
+
+;;;###autoload
+(defun guix-hydra-queued-builds (number)
+ "Display the NUMBER of queued Hydra builds."
+ (interactive "NNumber of queued builds: ")
+ (guix-hydra-build-get-display 'queue number))
+
+(provide 'guix-hydra-build)
+
+;;; guix-hydra-build.el ends here
diff --git a/emacs/guix-hydra-jobset.el b/emacs/guix-hydra-jobset.el
new file mode 100644
index 0000000000..a4a55a36f2
--- /dev/null
+++ b/emacs/guix-hydra-jobset.el
@@ -0,0 +1,162 @@
+;;; guix-hydra-jobset.el --- Interface for Hydra jobsets -*- lexical-binding: t -*-
+
+;; Copyright © 2015 Alex Kost <alezost@gmail.com>
+
+;; This file is part of GNU Guix.
+
+;; GNU Guix 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.
+
+;; GNU Guix 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.
+
+;; 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 file provides an interface for displaying Hydra jobsets in
+;; 'list' and 'info' buffers.
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'guix-buffer)
+(require 'guix-list)
+(require 'guix-info)
+(require 'guix-hydra)
+(require 'guix-hydra-build)
+(require 'guix-utils)
+
+(guix-hydra-define-entry-type hydra-jobset
+ :search-types '((project . guix-hydra-jobset-api-url))
+ :filters '(guix-hydra-jobset-filter-id)
+ :filter-names '((nrscheduled . scheduled)
+ (nrsucceeded . succeeded)
+ (nrfailed . failed)
+ (nrtotal . total)))
+
+(defun guix-hydra-jobset-get-display (search-type &rest args)
+ "Search for Hydra builds and show results."
+ (apply #'guix-list-get-display-entries
+ 'hydra-jobset search-type args))
+
+
+;;; Defining URLs
+
+(defun guix-hydra-jobset-url (project jobset)
+ "Return Hydra URL of a PROJECT's JOBSET."
+ (guix-hydra-url "jobset/" project "/" jobset))
+
+(defun guix-hydra-jobset-api-url (project)
+ "Return Hydra API URL for jobsets by PROJECT."
+ (guix-hydra-api-url "jobsets"
+ `(("project" . ,project))))
+
+
+;;; Filters for processing raw entries
+
+(defun guix-hydra-jobset-filter-id (entry)
+ "Add 'ID' parameter to 'hydra-jobset' ENTRY."
+ (cons `(id . ,(guix-entry-value entry 'name))
+ entry))
+
+
+;;; Hydra jobset 'info'
+
+(guix-hydra-info-define-interface hydra-jobset
+ :mode-name "Hydra-Jobset-Info"
+ :buffer-name "*Guix Hydra Jobset Info*"
+ :format '((name ignore (simple guix-info-heading))
+ ignore
+ guix-hydra-jobset-info-insert-url
+ (project format guix-hydra-jobset-info-insert-project)
+ (scheduled format (format guix-hydra-jobset-info-scheduled))
+ (succeeded format (format guix-hydra-jobset-info-succeeded))
+ (failed format (format guix-hydra-jobset-info-failed))
+ (total format (format guix-hydra-jobset-info-total))))
+
+(defface guix-hydra-jobset-info-scheduled
+ '((t))
+ "Face used for the number of scheduled builds."
+ :group 'guix-hydra-jobset-info-faces)
+
+(defface guix-hydra-jobset-info-succeeded
+ '((t :inherit guix-hydra-build-status-succeeded))
+ "Face used for the number of succeeded builds."
+ :group 'guix-hydra-jobset-info-faces)
+
+(defface guix-hydra-jobset-info-failed
+ '((t :inherit guix-hydra-build-status-failed))
+ "Face used for the number of failed builds."
+ :group 'guix-hydra-jobset-info-faces)
+
+(defface guix-hydra-jobset-info-total
+ '((t))
+ "Face used for the total number of builds."
+ :group 'guix-hydra-jobset-info-faces)
+
+(defun guix-hydra-jobset-info-insert-project (project entry)
+ "Insert PROJECT button for the jobset ENTRY."
+ (let ((jobset (guix-entry-value entry 'name)))
+ (guix-insert-button
+ project 'guix-hydra-build-project
+ 'action (lambda (btn)
+ (let ((args (guix-hydra-build-latest-prompt-args
+ :project (button-get btn 'project)
+ :jobset (button-get btn 'jobset))))
+ (apply #'guix-hydra-build-get-display
+ 'latest args)))
+ 'project project
+ 'jobset jobset)))
+
+(defun guix-hydra-jobset-info-insert-url (entry)
+ "Insert Hydra URL for the jobset ENTRY."
+ (guix-insert-button (guix-hydra-jobset-url
+ (guix-entry-value entry 'project)
+ (guix-entry-value entry 'name))
+ 'guix-url))
+
+
+;;; Hydra jobset 'list'
+
+(guix-hydra-list-define-interface hydra-jobset
+ :mode-name "Hydra-Jobset-List"
+ :buffer-name "*Guix Hydra Jobset List*"
+ :format '((name nil 25 t)
+ (project nil 10 t)
+ (scheduled nil 12 t)
+ (succeeded nil 12 t)
+ (failed nil 9 t)
+ (total nil 10 t)))
+
+(let ((map guix-hydra-jobset-list-mode-map))
+ (define-key map (kbd "B") 'guix-hydra-jobset-list-latest-builds))
+
+(defun guix-hydra-jobset-list-latest-builds (number &rest args)
+ "Display latest NUMBER of Hydra builds of the current jobset.
+Interactively, prompt for NUMBER. With prefix argument, prompt
+for all ARGS."
+ (interactive
+ (let ((entry (guix-list-current-entry)))
+ (guix-hydra-build-latest-prompt-args
+ :project (guix-entry-value entry 'project)
+ :jobset (guix-entry-value entry 'name))))
+ (apply #'guix-hydra-latest-builds number args))
+
+
+;;; Interactive commands
+
+;;;###autoload
+(defun guix-hydra-jobsets (project)
+ "Display jobsets of PROJECT."
+ (interactive (list (guix-hydra-read-project)))
+ (guix-hydra-jobset-get-display 'project project))
+
+(provide 'guix-hydra-jobset)
+
+;;; guix-hydra-jobset.el ends here
diff --git a/emacs/guix-hydra.el b/emacs/guix-hydra.el
new file mode 100644
index 0000000000..429483946b
--- /dev/null
+++ b/emacs/guix-hydra.el
@@ -0,0 +1,363 @@
+;;; guix-hydra.el --- Common code for interacting with Hydra -*- lexical-binding: t -*-
+
+;; Copyright © 2015 Alex Kost <alezost@gmail.com>
+
+;; This file is part of GNU Guix.
+
+;; GNU Guix 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.
+
+;; GNU Guix 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.
+
+;; 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 file provides some general code for 'list'/'info' interfaces for
+;; Hydra (Guix build farm).
+
+;;; Code:
+
+(require 'json)
+(require 'guix-buffer)
+(require 'guix-entry)
+(require 'guix-utils)
+(require 'guix-help-vars)
+
+(guix-define-groups hydra)
+
+(defvar guix-hydra-job-regexp
+ (concat ".*\\." (regexp-opt guix-help-system-types) "\\'")
+ "Regexp matching a full name of Hydra job (including system).")
+
+(defun guix-hydra-message (entries search-type &rest _)
+ "Display a message after showing Hydra ENTRIES."
+ ;; XXX Add more messages maybe.
+ (when (null entries)
+ (if (eq search-type 'fake)
+ (message "The update is impossible due to lack of Hydra API.")
+ (message "Hydra has returned no results."))))
+
+(defun guix-hydra-list-describe (ids)
+ "Describe 'hydra' entries with IDS (list of identifiers)."
+ (guix-buffer-display-entries
+ (guix-entries-by-ids ids (guix-buffer-current-entries))
+ 'info (guix-buffer-current-entry-type)
+ ;; Hydra does not provide an API to receive builds/jobsets by
+ ;; IDs/names, so we use a 'fake' search type.
+ '(fake)
+ 'add))
+
+
+;;; Readers
+
+(defvar guix-hydra-projects
+ '("gnu" "guix")
+ "List of available Hydra projects.")
+
+(guix-define-readers
+ :completions-var guix-hydra-projects
+ :single-reader guix-hydra-read-project
+ :single-prompt "Project: ")
+
+(guix-define-readers
+ :single-reader guix-hydra-read-jobset
+ :single-prompt "Jobset: ")
+
+(guix-define-readers
+ :single-reader guix-hydra-read-job
+ :single-prompt "Job: ")
+
+(guix-define-readers
+ :completions-var guix-help-system-types
+ :single-reader guix-hydra-read-system
+ :single-prompt "System: ")
+
+
+;;; Defining URLs
+
+(defvar guix-hydra-url "http://hydra.gnu.org"
+ "URL of the Hydra build farm.")
+
+(defun guix-hydra-url (&rest url-parts)
+ "Return Hydra URL."
+ (apply #'concat guix-hydra-url "/" url-parts))
+
+(defun guix-hydra-api-url (type args)
+ "Return URL for receiving data using Hydra API.
+TYPE is the name of an allowed method.
+ARGS is alist of (KEY . VALUE) pairs.
+Skip ARG, if VALUE is nil or an empty string."
+ (declare (indent 1))
+ (let* ((fields (mapcar
+ (lambda (arg)
+ (pcase arg
+ (`(,key . ,value)
+ (unless (or (null value)
+ (equal "" value))
+ (concat (guix-hexify key) "="
+ (guix-hexify value))))
+ (_ (error "Wrong argument '%s'" arg))))
+ args))
+ (fields (mapconcat #'identity (delq nil fields) "&")))
+ (guix-hydra-url "api/" type "?" fields)))
+
+
+;;; Receiving data from Hydra
+
+(defun guix-hydra-receive-data (url)
+ "Return output received from URL and processed with `json-read'."
+ (with-temp-buffer
+ (url-insert-file-contents url)
+ (goto-char (point-min))
+ (let ((json-key-type 'symbol)
+ (json-array-type 'list)
+ (json-object-type 'alist))
+ (json-read))))
+
+(defun guix-hydra-get-entries (entry-type search-type &rest args)
+ "Receive ENTRY-TYPE entries from Hydra.
+SEARCH-TYPE is one of the types defined by `guix-hydra-define-interface'."
+ (unless (eq search-type 'fake)
+ (let* ((url (apply #'guix-hydra-search-url
+ entry-type search-type args))
+ (raw-entries (guix-hydra-receive-data url))
+ (entries (guix-hydra-filter-entries
+ raw-entries
+ (guix-hydra-filters entry-type))))
+ entries)))
+
+
+;;; Filters for processing raw entries
+
+(defun guix-hydra-filter-entries (entries filters)
+ "Filter ENTRIES using FILTERS.
+Call `guix-modify' on each entry from ENTRIES."
+ (mapcar (lambda (entry)
+ (guix-modify entry filters))
+ entries))
+
+(defun guix-hydra-filter-names (entry name-alist)
+ "Replace names of ENTRY parameters using NAME-ALIST.
+Each element of NAME-ALIST is (OLD-NAME . NEW-NAME) pair."
+ (mapcar (lambda (param)
+ (pcase param
+ (`(,name . ,val)
+ (let ((new-name (guix-assq-value name-alist name)))
+ (if new-name
+ (cons new-name val)
+ param)))))
+ entry))
+
+(defun guix-hydra-filter-boolean (entry params)
+ "Convert number PARAMS (0/1) of ENTRY to boolean values (nil/t)."
+ (mapcar (lambda (param)
+ (pcase param
+ (`(,name . ,val)
+ (if (memq name params)
+ (cons name (guix-number->bool val))
+ param))))
+ entry))
+
+
+;;; Wrappers for defined variables
+
+(defvar guix-hydra-entry-type-data nil
+ "Alist with hydra entry type data.
+This alist is filled by `guix-hydra-define-entry-type' macro.")
+
+(defun guix-hydra-entry-type-value (entry-type symbol)
+ "Return SYMBOL's value for ENTRY-TYPE from `guix-hydra'."
+ (symbol-value (guix-assq-value guix-hydra-entry-type-data
+ entry-type symbol)))
+
+(defun guix-hydra-search-url (entry-type search-type &rest args)
+ "Return URL to receive ENTRY-TYPE entries from Hydra."
+ (apply (guix-assq-value (guix-hydra-entry-type-value
+ entry-type 'search-types)
+ search-type)
+ args))
+
+(defun guix-hydra-filters (entry-type)
+ "Return a list of filters for ENTRY-TYPE."
+ (guix-hydra-entry-type-value entry-type 'filters))
+
+
+;;; Interface definers
+
+(defmacro guix-hydra-define-entry-type (entry-type &rest args)
+ "Define general code for ENTRY-TYPE.
+Remaining arguments (ARGS) should have a form [KEYWORD VALUE] ...
+
+Required keywords:
+
+ - `:search-types' - default value of the generated
+ `guix-ENTRY-TYPE-search-types' variable.
+
+Optional keywords:
+
+ - `:filters' - default value of the generated
+ `guix-ENTRY-TYPE-filters' variable.
+
+ - `:filter-names' - if specified, a generated
+ `guix-ENTRY-TYPE-filter-names' function for filtering these
+ names will be added to `guix-ENTRY-TYPE-filters' variable.
+
+ - `:filter-boolean-params' - if specified, a generated
+ `guix-ENTRY-TYPE-filter-boolean' function for filtering these
+ names will be added to `guix-ENTRY-TYPE-filters' variable.
+
+The rest keyword arguments are passed to
+`guix-define-entry-type' macro."
+ (declare (indent 1))
+ (let* ((entry-type-str (symbol-name entry-type))
+ (prefix (concat "guix-" entry-type-str))
+ (search-types-var (intern (concat prefix "-search-types")))
+ (filters-var (intern (concat prefix "-filters")))
+ (get-fun (intern (concat prefix "-get-entries"))))
+ (guix-keyword-args-let args
+ ((search-types-val :search-types)
+ (filters-val :filters)
+ (filter-names-val :filter-names)
+ (filter-bool-val :filter-boolean-params))
+ `(progn
+ (defvar ,search-types-var ,search-types-val
+ ,(format "\
+Alist of search types and according URL functions.
+Functions are used to define URL to receive '%s' entries."
+ entry-type-str))
+
+ (defvar ,filters-var ,filters-val
+ ,(format "\
+List of filters for '%s' parameters.
+Each filter is a function that should take an entry as a single
+argument, and should also return an entry."
+ entry-type-str))
+
+ ,(when filter-bool-val
+ (let ((filter-bool-var (intern (concat prefix
+ "-filter-boolean-params")))
+ (filter-bool-fun (intern (concat prefix
+ "-filter-boolean"))))
+ `(progn
+ (defvar ,filter-bool-var ,filter-bool-val
+ ,(format "\
+List of '%s' parameters that should be transformed to boolean values."
+ entry-type-str))
+
+ (defun ,filter-bool-fun (entry)
+ ,(format "\
+Run `guix-hydra-filter-boolean' with `%S' variable."
+ filter-bool-var)
+ (guix-hydra-filter-boolean entry ,filter-bool-var))
+
+ (setq ,filters-var
+ (cons ',filter-bool-fun ,filters-var)))))
+
+ ;; Do not move this clause up!: name filtering should be
+ ;; performed before any other filtering, so this filter should
+ ;; be consed after the boolean filter.
+ ,(when filter-names-val
+ (let* ((filter-names-var (intern (concat prefix
+ "-filter-names")))
+ (filter-names-fun filter-names-var))
+ `(progn
+ (defvar ,filter-names-var ,filter-names-val
+ ,(format "\
+Alist of '%s' parameter names returned by Hydra API and names
+used internally by the elisp code of this package."
+ entry-type-str))
+
+ (defun ,filter-names-fun (entry)
+ ,(format "\
+Run `guix-hydra-filter-names' with `%S' variable."
+ filter-names-var)
+ (guix-hydra-filter-names entry ,filter-names-var))
+
+ (setq ,filters-var
+ (cons ',filter-names-fun ,filters-var)))))
+
+ (defun ,get-fun (search-type &rest args)
+ ,(format "\
+Receive '%s' entries.
+See `guix-hydra-get-entries' for details."
+ entry-type-str)
+ (apply #'guix-hydra-get-entries
+ ',entry-type search-type args))
+
+ (guix-alist-put!
+ '((search-types . ,search-types-var)
+ (filters . ,filters-var))
+ 'guix-hydra-entry-type-data ',entry-type)
+
+ (guix-define-entry-type ,entry-type
+ :parent-group guix-hydra
+ :parent-faces-group guix-hydra-faces
+ ,@%foreign-args)))))
+
+(defmacro guix-hydra-define-interface (buffer-type entry-type &rest args)
+ "Define BUFFER-TYPE interface for displaying ENTRY-TYPE entries.
+
+This macro should be called after calling
+`guix-hydra-define-entry-type' with the same ENTRY-TYPE.
+
+ARGS are passed to `guix-BUFFER-TYPE-define-interface' macro."
+ (declare (indent 2))
+ (let* ((entry-type-str (symbol-name entry-type))
+ (buffer-type-str (symbol-name buffer-type))
+ (get-fun (intern (concat "guix-" entry-type-str
+ "-get-entries")))
+ (definer (intern (concat "guix-" buffer-type-str
+ "-define-interface"))))
+ `(,definer ,entry-type
+ :get-entries-function ',get-fun
+ :message-function 'guix-hydra-message
+ ,@args)))
+
+(defmacro guix-hydra-info-define-interface (entry-type &rest args)
+ "Define 'info' interface for displaying ENTRY-TYPE entries.
+See `guix-hydra-define-interface'."
+ (declare (indent 1))
+ `(guix-hydra-define-interface info ,entry-type
+ ,@args))
+
+(defmacro guix-hydra-list-define-interface (entry-type &rest args)
+ "Define 'list' interface for displaying ENTRY-TYPE entries.
+Remaining arguments (ARGS) should have a form [KEYWORD VALUE] ...
+
+Optional keywords:
+
+ - `:describe-function' - default value of the generated
+ `guix-ENTRY-TYPE-list-describe-function' variable (if not
+ specified, use `guix-hydra-list-describe').
+
+The rest keyword arguments are passed to
+`guix-hydra-define-interface' macro."
+ (declare (indent 1))
+ (guix-keyword-args-let args
+ ((describe-val :describe-function))
+ `(guix-hydra-define-interface list ,entry-type
+ :describe-function ,(or describe-val ''guix-hydra-list-describe)
+ ,@args)))
+
+
+(defvar guix-hydra-font-lock-keywords
+ (eval-when-compile
+ `((,(rx "(" (group (or "guix-hydra-define-entry-type"
+ "guix-hydra-define-interface"
+ "guix-hydra-info-define-interface"
+ "guix-hydra-list-define-interface"))
+ symbol-end)
+ . 1))))
+
+(font-lock-add-keywords 'emacs-lisp-mode guix-hydra-font-lock-keywords)
+
+(provide 'guix-hydra)
+
+;;; guix-hydra.el ends here
diff --git a/emacs/guix-info.el b/emacs/guix-info.el
index 1c7e79b954..644533eb29 100644
--- a/emacs/guix-info.el
+++ b/emacs/guix-info.el
@@ -1,4 +1,4 @@
-;;; guix-info.el --- Info buffers for displaying entries -*- lexical-binding: t -*-
+;;; guix-info.el --- 'Info' buffer interface for displaying data -*- lexical-binding: t -*-
;; Copyright © 2014, 2015 Alex Kost <alezost@gmail.com>
;; Copyright © 2015 Ludovic Courtès <ludo@gnu.org>
@@ -20,23 +20,16 @@
;;; Commentary:
-;; This file provides a help-like buffer for displaying information
-;; about Guix packages and generations.
+;; This file provides 'info' (help-like) buffer interface for displaying
+;; an arbitrary data.
;;; Code:
-(require 'guix-base)
+(require 'guix-buffer)
+(require 'guix-entry)
(require 'guix-utils)
-(defgroup guix-info nil
- "General settings for info buffers."
- :prefix "guix-info-"
- :group 'guix)
-
-(defgroup guix-info-faces nil
- "Faces for info buffers."
- :group 'guix-info
- :group 'guix-faces)
+(guix-define-buffer-type info)
(defface guix-info-heading
'((((type tty pc) (class color)) :weight bold)
@@ -80,122 +73,115 @@
"Mouse face used for action buttons."
:group 'guix-info-faces)
-(defcustom guix-info-ignore-empty-vals nil
+(defcustom guix-info-ignore-empty-values nil
"If non-nil, do not display parameters with nil values."
:type 'boolean
:group 'guix-info)
+(defcustom guix-info-fill t
+ "If non-nil, fill string parameters to fit the window.
+If nil, insert text parameters (like synopsis or description) in
+a raw form."
+ :type 'boolean
+ :group 'guix-info)
+
(defvar guix-info-param-title-format "%-18s: "
"String used to format a title of a parameter.
It should be a '%s'-sequence. After inserting a title formatted
with this string, a value of the parameter is inserted.
-This string is used by `guix-info-insert-title-default'.")
+This string is used by `guix-info-insert-title-format'.")
-(defvar guix-info-multiline-prefix (make-string 20 ?\s)
+(defvar guix-info-multiline-prefix
+ (make-string (length (format guix-info-param-title-format " "))
+ ?\s)
"String used to format multi-line parameter values.
If a value occupies more than one line, this string is inserted
in the beginning of each line after the first one.
-This string is used by `guix-info-insert-val-default'.")
+This string is used by `guix-info-insert-value-format'.")
(defvar guix-info-indent 2
"Number of spaces used to indent various parts of inserted text.")
-(defvar guix-info-fill-column 60
- "Column used for filling (word wrapping) parameters with long lines.
-If a value is not multi-line and it occupies more than this
-number of characters, it will be split into several lines.")
-
(defvar guix-info-delimiter "\n\f\n"
"String used to separate entries.")
-(defvar guix-info-insert-methods
- '((package
- (name guix-package-info-name)
- (version guix-package-info-version)
- (license guix-package-info-license)
- (synopsis guix-package-info-synopsis)
- (description guix-package-info-insert-description
- guix-info-insert-title-simple)
- (outputs guix-package-info-insert-outputs
- guix-info-insert-title-simple)
- (source guix-package-info-insert-source
- guix-info-insert-title-simple)
- (home-url guix-info-insert-url)
- (inputs guix-package-info-insert-inputs)
- (native-inputs guix-package-info-insert-native-inputs)
- (propagated-inputs guix-package-info-insert-propagated-inputs)
- (location guix-package-info-insert-location))
- (installed
- (path guix-package-info-insert-output-path
- guix-info-insert-title-simple)
- (dependencies guix-package-info-insert-output-dependencies
- guix-info-insert-title-simple))
- (output
- (name guix-package-info-name)
- (version guix-output-info-insert-version)
- (output guix-output-info-insert-output)
- (source guix-package-info-insert-source
- guix-info-insert-title-simple)
- (path guix-package-info-insert-output-path
- guix-info-insert-title-simple)
- (dependencies guix-package-info-insert-output-dependencies
- guix-info-insert-title-simple)
- (license guix-package-info-license)
- (synopsis guix-package-info-synopsis)
- (description guix-package-info-insert-description
- guix-info-insert-title-simple)
- (home-url guix-info-insert-url)
- (inputs guix-package-info-insert-inputs)
- (native-inputs guix-package-info-insert-native-inputs)
- (propagated-inputs guix-package-info-insert-propagated-inputs)
- (location guix-package-info-insert-location))
- (generation
- (number guix-generation-info-insert-number)
- (current guix-generation-info-insert-current)
- (path guix-info-insert-file-path)
- (time guix-info-insert-time)))
- "Methods for inserting parameter values.
-Each element of the list should have a form:
-
- (ENTRY-TYPE . ((PARAM INSERT-VALUE [INSERT-TITLE]) ...))
-
-INSERT-VALUE may be either nil, a face name or a function. If it
-is nil or a face, `guix-info-insert-val-default' function is
-called with parameter value and INSERT-VALUE as arguments. If it
-is a function, this function is called with parameter value and
-entry info (alist of parameters and their values) as arguments.
-
-INSERT-TITLE may be either nil, a face name or a function. If it
-is nil or a face, `guix-info-insert-title-default' function is
-called with parameter title and INSERT-TITLE as arguments. If it
-is a function, this function is called with parameter title as
-argument.")
-
-(defvar guix-info-displayed-params
- '((package name version synopsis outputs source location home-url
- license inputs native-inputs propagated-inputs description)
- (output name version output synopsis source path dependencies location
- home-url license inputs native-inputs propagated-inputs
- description)
- (installed path dependencies)
- (generation number prev-number current time path))
- "List of displayed entry parameters.
-Each element of the list should have a form:
-
- (ENTRY-TYPE . (PARAM ...))
-
-The order of displayed parameters is the same as in this list.")
-
-(defun guix-info-get-insert-methods (entry-type param)
- "Return list of insert methods for parameter PARAM of ENTRY-TYPE.
-See `guix-info-insert-methods' for details."
- (guix-assq-value guix-info-insert-methods
- entry-type param))
-
-(defun guix-info-get-displayed-params (entry-type)
- "Return parameters of ENTRY-TYPE that should be displayed."
- (guix-assq-value guix-info-displayed-params
- entry-type))
+
+;;; Wrappers for 'info' variables
+
+(defvar guix-info-data nil
+ "Alist with 'info' data.
+This alist is filled by `guix-info-define-interface' macro.")
+
+(defun guix-info-value (entry-type symbol)
+ "Return SYMBOL's value for ENTRY-TYPE from `guix-info-data'."
+ (symbol-value (guix-assq-value guix-info-data entry-type symbol)))
+
+(defun guix-info-param-title (entry-type param)
+ "Return a title of an ENTRY-TYPE parameter PARAM."
+ (guix-buffer-param-title 'info entry-type param))
+
+(defun guix-info-format (entry-type)
+ "Return 'info' format for ENTRY-TYPE."
+ (guix-info-value entry-type 'format))
+
+(defun guix-info-displayed-params (entry-type)
+ "Return a list of ENTRY-TYPE parameters that should be displayed."
+ (delq nil
+ (mapcar (lambda (spec)
+ (pcase spec
+ (`(,param . ,_) param)))
+ (guix-info-format entry-type))))
+
+
+;;; Inserting entries
+
+(defvar guix-info-title-aliases
+ '((format . guix-info-insert-title-format)
+ (simple . guix-info-insert-title-simple))
+ "Alist of aliases and functions to insert titles.")
+
+(defvar guix-info-value-aliases
+ '((format . guix-info-insert-value-format)
+ (indent . guix-info-insert-value-indent)
+ (simple . guix-info-insert-value-simple)
+ (time . guix-info-insert-time))
+ "Alist of aliases and functions to insert values.")
+
+(defun guix-info-title-function (fun-or-alias)
+ "Convert FUN-OR-ALIAS into a function to insert a title."
+ (or (guix-assq-value guix-info-title-aliases fun-or-alias)
+ fun-or-alias))
+
+(defun guix-info-value-function (fun-or-alias)
+ "Convert FUN-OR-ALIAS into a function to insert a value."
+ (or (guix-assq-value guix-info-value-aliases fun-or-alias)
+ fun-or-alias))
+
+(defun guix-info-title-method->function (method)
+ "Convert title METHOD into a function to insert a title."
+ (pcase method
+ ((pred null) #'ignore)
+ ((pred symbolp) (guix-info-title-function method))
+ (`(,fun-or-alias . ,rest-args)
+ (lambda (title)
+ (apply (guix-info-title-function fun-or-alias)
+ title rest-args)))
+ (_ (error "Unknown title method '%S'" method))))
+
+(defun guix-info-value-method->function (method)
+ "Convert value METHOD into a function to insert a value."
+ (pcase method
+ ((pred null) #'ignore)
+ ((pred functionp) method)
+ (`(,fun-or-alias . ,rest-args)
+ (lambda (value _)
+ (apply (guix-info-value-function fun-or-alias)
+ value rest-args)))
+ (_ (error "Unknown value method '%S'" method))))
+
+(defun guix-info-fill-column ()
+ "Return fill column for the current window."
+ (min (window-width) fill-column))
(defun guix-info-get-indent (&optional level)
"Return `guix-info-indent' \"multiplied\" by LEVEL spaces.
@@ -207,124 +193,128 @@ LEVEL is 1 by default."
(insert (guix-info-get-indent level)))
(defun guix-info-insert-entries (entries entry-type)
- "Display ENTRIES of ENTRY-TYPE in the current info buffer.
-ENTRIES should have a form of `guix-entries'."
+ "Display ENTRY-TYPE ENTRIES in the current info buffer."
(guix-mapinsert (lambda (entry)
(guix-info-insert-entry entry entry-type))
entries
guix-info-delimiter))
-(defun guix-info-insert-entry-default (entry entry-type
- &optional indent-level)
- "Insert ENTRY of ENTRY-TYPE into the current info buffer.
-If INDENT-LEVEL is non-nil, indent displayed information by this
-number of `guix-info-indent' spaces."
- (let ((region-beg (point)))
- (mapc (lambda (param)
- (guix-info-insert-param param entry entry-type))
- (guix-info-get-displayed-params entry-type))
- (when indent-level
- (indent-rigidly region-beg (point)
- (* indent-level guix-info-indent)))))
-
(defun guix-info-insert-entry (entry entry-type &optional indent-level)
"Insert ENTRY of ENTRY-TYPE into the current info buffer.
-Use `guix-info-insert-ENTRY-TYPE-function' or
-`guix-info-insert-entry-default' if it is nil."
- (let* ((var (intern (concat "guix-info-insert-"
- (symbol-name entry-type)
- "-function")))
- (fun (symbol-value var)))
- (if (functionp fun)
- (funcall fun entry)
- (guix-info-insert-entry-default entry entry-type indent-level))))
-
-(defun guix-info-insert-param (param entry entry-type)
+If INDENT-LEVEL is non-nil, indent displayed data by this number
+of `guix-info-indent' spaces."
+ (guix-with-indent (* (or indent-level 0)
+ guix-info-indent)
+ (dolist (spec (guix-info-format entry-type))
+ (guix-info-insert-entry-unit spec entry entry-type))))
+
+(defun guix-info-insert-entry-unit (format-spec entry entry-type)
"Insert title and value of a PARAM at point.
ENTRY is alist with parameters and their values.
ENTRY-TYPE is a type of ENTRY."
- (let ((val (guix-assq-value entry param)))
- (unless (and guix-info-ignore-empty-vals (null val))
- (let* ((title (guix-get-param-title entry-type param))
- (insert-methods (guix-info-get-insert-methods entry-type param))
- (val-method (car insert-methods))
- (title-method (cadr insert-methods)))
- (guix-info-method-funcall title title-method
- #'guix-info-insert-title-default)
- (guix-info-method-funcall val val-method
- #'guix-info-insert-val-default
- entry)
- (insert "\n")))))
-
-(defun guix-info-method-funcall (val method default-fun &rest args)
- "Call METHOD or DEFAULT-FUN.
-
-If METHOD is a function and VAL is non-nil, call this
-function by applying it to VAL and ARGS.
-
-If METHOD is a face, propertize inserted VAL with this face."
- (cond ((or (null method)
- (facep method))
- (funcall default-fun val method))
- ((functionp method)
- (apply method val args))
- (t (error "Unknown method '%S'" method))))
-
-(defun guix-info-insert-title-default (title &optional face format)
- "Insert TITLE formatted with `guix-info-param-title-format' at point."
+ (pcase format-spec
+ ((pred functionp)
+ (funcall format-spec entry)
+ (insert "\n"))
+ (`(,param ,title-method ,value-method)
+ (let ((value (guix-entry-value entry param)))
+ (unless (and guix-info-ignore-empty-values (null value))
+ (let ((title (guix-info-param-title entry-type param))
+ (insert-title (guix-info-title-method->function title-method))
+ (insert-value (guix-info-value-method->function value-method)))
+ (funcall insert-title title)
+ (funcall insert-value value entry)
+ (insert "\n")))))
+ (_ (error "Unknown format specification '%S'" format-spec))))
+
+(defun guix-info-insert-title-simple (title &optional face)
+ "Insert \"TITLE: \" string at point.
+If FACE is nil, use `guix-info-param-title'."
(guix-format-insert title
(or face 'guix-info-param-title)
- (or format guix-info-param-title-format)))
+ "%s: "))
-(defun guix-info-insert-title-simple (title &optional face)
- "Insert TITLE at point."
- (guix-info-insert-title-default title face "%s:"))
-
-(defun guix-info-insert-val-default (val &optional face)
- "Format and insert parameter value VAL at point.
-
-This function is intended to be called after
-`guix-info-insert-title-default'.
-
-If VAL is a one-line string longer than `guix-info-fill-column',
-split it into several short lines. See also
-`guix-info-multiline-prefix'.
-
-If FACE is non-nil, propertize inserted line(s) with this FACE."
- (guix-split-insert val face
- guix-info-fill-column
- (concat "\n" guix-info-multiline-prefix)))
-
-(defun guix-info-insert-val-simple (val &optional face-or-fun)
- "Format and insert parameter value VAL at point.
-
-This function is intended to be called after
-`guix-info-insert-title-simple'.
-
-If VAL is a one-line string longer than `guix-info-fill-column',
-split it into several short lines and indent each line with
-`guix-info-indent' spaces.
-
-If FACE-OR-FUN is a face, propertize inserted line(s) with this FACE.
-
-If FACE-OR-FUN is a function, call it with VAL as argument. If
-VAL is a list, call the function on each element of this list."
- (if (null val)
- (progn (guix-info-insert-indent)
- (guix-format-insert nil))
- (let ((prefix (concat "\n" (guix-info-get-indent))))
- (insert prefix)
- (if (functionp face-or-fun)
- (guix-mapinsert face-or-fun
- (if (listp val) val (list val))
- prefix)
- (guix-split-insert val face-or-fun
- guix-info-fill-column prefix)))))
-
-(defun guix-info-insert-time (seconds &optional _)
+(defun guix-info-insert-title-format (title &optional face)
+ "Insert TITLE using `guix-info-param-title-format' at point.
+If FACE is nil, use `guix-info-param-title'."
+ (guix-format-insert title
+ (or face 'guix-info-param-title)
+ guix-info-param-title-format))
+
+(defun guix-info-insert-value-simple (value &optional button-or-face indent)
+ "Format and insert parameter VALUE at point.
+
+VALUE may be split into several short lines to fit the current
+window, depending on `guix-info-fill', and each line is indented
+with INDENT number of spaces.
+
+If BUTTON-OR-FACE is a button type symbol, transform VALUE into
+this (these) button(s) and insert each one on a new line. If it
+is a face symbol, propertize inserted line(s) with this face."
+ (or indent (setq indent 0))
+ (guix-with-indent indent
+ (let* ((button? (guix-button-type? button-or-face))
+ (face (unless button? button-or-face))
+ (fill-col (unless (or button?
+ (and (stringp value)
+ (not guix-info-fill)))
+ (- (guix-info-fill-column) indent)))
+ (value (if (and value button?)
+ (guix-buttonize value button-or-face "\n")
+ value)))
+ (guix-split-insert value face fill-col "\n"))))
+
+(defun guix-info-insert-value-indent (value &optional button-or-face)
+ "Format and insert parameter VALUE at point.
+
+This function is intended to be called after inserting a title
+with `guix-info-insert-title-simple'.
+
+VALUE may be split into several short lines to fit the current
+window, depending on `guix-info-fill', and each line is indented
+with `guix-info-indent'.
+
+For the meaning of BUTTON-OR-FACE, see `guix-info-insert-value-simple'."
+ (when value (insert "\n"))
+ (guix-info-insert-value-simple value button-or-face guix-info-indent))
+
+(defun guix-info-insert-value-format (value &optional button-or-face
+ &rest button-properties)
+ "Format and insert parameter VALUE at point.
+
+This function is intended to be called after inserting a title
+with `guix-info-insert-title-format'.
+
+VALUE may be split into several short lines to fit the current
+window, depending on `guix-info-fill' and
+`guix-info-multiline-prefix'. If VALUE is a list, its elements
+will be separated with `guix-list-separator'.
+
+If BUTTON-OR-FACE is a button type symbol, transform VALUE into
+this (these) button(s). If it is a face symbol, propertize
+inserted line(s) with this face.
+
+BUTTON-PROPERTIES are passed to `guix-buttonize' (only if
+BUTTON-OR-FACE is a button type)."
+ (let* ((button? (guix-button-type? button-or-face))
+ (face (unless button? button-or-face))
+ (fill-col (when (or button?
+ guix-info-fill
+ (not (stringp value)))
+ (- (guix-info-fill-column)
+ (length guix-info-multiline-prefix))))
+ (value (if (and value button?)
+ (apply #'guix-buttonize
+ value button-or-face guix-list-separator
+ button-properties)
+ value)))
+ (guix-split-insert value face fill-col
+ (concat "\n" guix-info-multiline-prefix))))
+
+(defun guix-info-insert-time (seconds &optional face)
"Insert formatted time string using SECONDS at point."
- (guix-info-insert-val-default (guix-get-time-string seconds)
- 'guix-info-time))
+ (guix-format-insert (guix-get-time-string seconds)
+ (or face 'guix-info-time)))
;;; Buttons
@@ -359,21 +349,6 @@ VAL is a list, call the function on each element of this list."
'action (lambda (btn)
(browse-url (button-label btn))))
-(define-button-type 'guix-package-location
- :supertype 'guix
- 'face 'guix-package-info-location
- 'help-echo "Find location of this package"
- 'action (lambda (btn)
- (guix-find-location (button-label btn))))
-
-(define-button-type 'guix-package-name
- :supertype 'guix
- 'face 'guix-package-info-name-button
- 'help-echo "Describe this package"
- 'action (lambda (btn)
- (guix-get-show-entries guix-profile 'info guix-package-info-type
- 'name (button-label btn))))
-
(defun guix-info-button-copy-label (&optional pos)
"Copy a label of the button at POS into kill ring.
If POS is nil, use the current point position."
@@ -395,496 +370,112 @@ See `insert-text-button' for the meaning of PROPERTIES."
'help-echo message
properties))
-(defun guix-info-insert-file-path (path &optional _)
- "Make button from file PATH and insert it at point."
- (guix-insert-button path 'guix-file))
-
-(defun guix-info-insert-url (url &optional _)
- "Make button from URL and insert it at point."
- (guix-insert-button url 'guix-url))
-
+;;; Major mode and interface definer
+
(defvar guix-info-mode-map
(let ((map (make-sparse-keymap)))
(set-keymap-parent
- map (make-composed-keymap (list guix-root-map button-buffer-map)
+ map (make-composed-keymap (list guix-buffer-map button-buffer-map)
special-mode-map))
map)
- "Parent keymap for info buffers.")
+ "Keymap for `guix-info-mode' buffers.")
(define-derived-mode guix-info-mode special-mode "Guix-Info"
- "Parent mode for displaying information in info buffers.")
+ "Parent mode for displaying data in 'info' form."
+ (setq-local revert-buffer-function 'guix-buffer-revert))
+
+(defun guix-info-mode-initialize ()
+ "Set up the current 'info' buffer."
+ ;; Without this, syntactic fontification is performed, and it may
+ ;; break our highlighting. For example, description of "emacs-typo"
+ ;; package contains a single " (double-quote) character, so the
+ ;; default syntactic fontification highlights the rest text after it
+ ;; as a string. See (info "(elisp) Font Lock Basics") for details.
+ (setq font-lock-defaults '(nil t)))
+
+(defmacro guix-info-define-interface (entry-type &rest args)
+ "Define 'info' interface for displaying ENTRY-TYPE entries.
+Remaining arguments (ARGS) should have a form [KEYWORD VALUE] ...
+
+Required keywords:
+
+ - `:format' - default value of the generated
+ `guix-ENTRY-TYPE-info-format' variable.
+
+The rest keyword arguments are passed to
+`guix-buffer-define-interface' macro."
+ (declare (indent 1))
+ (let* ((entry-type-str (symbol-name entry-type))
+ (prefix (concat "guix-" entry-type-str "-info"))
+ (group (intern prefix))
+ (format-var (intern (concat prefix "-format"))))
+ (guix-keyword-args-let args
+ ((show-entries-val :show-entries-function)
+ (format-val :format))
+ `(progn
+ (defcustom ,format-var ,format-val
+ ,(format "\
+List of methods for inserting '%s' entry.
+Each METHOD should be either a function or should have the
+following form:
+
+ (PARAM INSERT-TITLE INSERT-VALUE)
+
+If METHOD is a function, it is called with an entry as argument.
+
+PARAM is a name of '%s' entry parameter.
+
+INSERT-TITLE may be either a symbol or a list. If it is a
+symbol, it should be a function or an alias from
+`guix-info-title-aliases', in which case it is called with title
+as argument. If it is a list, it should have a
+form (FUN-OR-ALIAS [ARGS ...]), in which case FUN-OR-ALIAS is
+called with title and ARGS as arguments.
+
+INSERT-VALUE may be either a symbol or a list. If it is a
+symbol, it should be a function or an alias from
+`guix-info-value-aliases', in which case it is called with value
+and entry as arguments. If it is a list, it should have a
+form (FUN-OR-ALIAS [ARGS ...]), in which case FUN-OR-ALIAS is
+called with value and ARGS as arguments.
+
+Parameters are inserted in the same order as defined by this list.
+After calling each METHOD, a new line is inserted."
+ entry-type-str entry-type-str)
+ :type 'sexp
+ :group ',group)
+
+ (guix-alist-put!
+ '((format . ,format-var))
+ 'guix-info-data ',entry-type)
+
+ ,(if show-entries-val
+ `(guix-buffer-define-interface info ,entry-type
+ :show-entries-function ,show-entries-val
+ ,@%foreign-args)
+
+ (let ((insert-fun (intern (concat prefix "-insert-entries"))))
+ `(progn
+ (defun ,insert-fun (entries)
+ ,(format "\
+Print '%s' ENTRIES in the current 'info' buffer."
+ entry-type-str)
+ (guix-info-insert-entries entries ',entry-type))
+
+ (guix-buffer-define-interface info ,entry-type
+ :insert-entries-function ',insert-fun
+ :mode-init-function 'guix-info-mode-initialize
+ ,@%foreign-args))))))))
-;;; Displaying packages
-
-(guix-define-buffer-type info package
- :required (id installed non-unique))
-
-(defface guix-package-info-heading
- '((t :inherit guix-info-heading))
- "Face for package name and version headings."
- :group 'guix-package-info-faces)
-
-(defface guix-package-info-name
- '((t :inherit font-lock-keyword-face))
- "Face used for a name of a package."
- :group 'guix-package-info-faces)
-
-(defface guix-package-info-name-button
- '((t :inherit button))
- "Face used for a full name that can be used to describe a package."
- :group 'guix-package-info-faces)
-
-(defface guix-package-info-version
- '((t :inherit font-lock-builtin-face))
- "Face used for a version of a package."
- :group 'guix-package-info-faces)
-
-(defface guix-package-info-synopsis
- '((((type tty pc) (class color)) :weight bold)
- (t :height 1.1 :weight bold :inherit variable-pitch))
- "Face used for a synopsis of a package."
- :group 'guix-package-info-faces)
-
-(defface guix-package-info-description
- '((t))
- "Face used for a description of a package."
- :group 'guix-package-info-faces)
+(defvar guix-info-font-lock-keywords
+ (eval-when-compile
+ `((,(rx "(" (group "guix-info-define-interface")
+ symbol-end)
+ . 1))))
-(defface guix-package-info-license
- '((t :inherit font-lock-string-face))
- "Face used for a license of a package."
- :group 'guix-package-info-faces)
-
-(defface guix-package-info-location
- '((t :inherit link))
- "Face used for a location of a package."
- :group 'guix-package-info-faces)
-
-(defface guix-package-info-installed-outputs
- '((default :weight bold)
- (((class color) (min-colors 88) (background light))
- :foreground "ForestGreen")
- (((class color) (min-colors 88) (background dark))
- :foreground "PaleGreen")
- (((class color) (min-colors 8))
- :foreground "green")
- (t :underline t))
- "Face used for installed outputs of a package."
- :group 'guix-package-info-faces)
-
-(defface guix-package-info-uninstalled-outputs
- '((t :weight bold))
- "Face used for uninstalled outputs of a package."
- :group 'guix-package-info-faces)
-
-(defface guix-package-info-obsolete
- '((t :inherit error))
- "Face used if a package is obsolete."
- :group 'guix-package-info-faces)
-
-(defvar guix-info-insert-package-function
- #'guix-package-info-insert-with-heading
- "Function used to insert a package information.
-It is called with a single argument - alist of package parameters.
-If nil, insert package in a default way.")
-
-(defvar guix-package-info-heading-params '(synopsis description)
- "List of parameters displayed in a heading along with name and version.")
-
-(defcustom guix-package-info-fill-heading t
- "If nil, insert heading parameters in a raw form, without
-filling them to fit the window."
- :type 'boolean
- :group 'guix-package-info)
-
-(defun guix-package-info-insert-heading (entry)
- "Insert the heading for package ENTRY.
-Show package name, version, and `guix-package-info-heading-params'."
- (guix-format-insert (concat (guix-assq-value entry 'name) " "
- (guix-assq-value entry 'version))
- 'guix-package-info-heading)
- (insert "\n\n")
- (mapc (lambda (param)
- (let ((val (guix-assq-value entry param))
- (face (guix-get-symbol (symbol-name param)
- 'info 'package)))
- (when val
- (let* ((col (min (window-width) fill-column))
- (val (if guix-package-info-fill-heading
- (guix-get-filled-string val col)
- val)))
- (guix-format-insert val (and (facep face) face))
- (insert "\n\n")))))
- guix-package-info-heading-params))
-
-(defun guix-package-info-insert-with-heading (entry)
- "Insert package ENTRY with its heading at point."
- (guix-package-info-insert-heading entry)
- (mapc (lambda (param)
- (unless (or (memq param '(name version))
- (memq param guix-package-info-heading-params))
- (guix-info-insert-param param entry 'package)))
- (guix-info-get-displayed-params 'package)))
-
-(defun guix-package-info-insert-description (desc &optional _)
- "Insert description DESC at point."
- (guix-info-insert-val-simple desc 'guix-package-info-description))
-
-(defun guix-package-info-insert-location (location &optional _)
- "Make button from file LOCATION and insert it at point."
- (guix-insert-button location 'guix-package-location))
-
-(defmacro guix-package-info-define-insert-inputs (&optional type)
- "Define a face and a function for inserting package inputs.
-TYPE is a type of inputs.
-Function name is `guix-package-info-insert-TYPE-inputs'.
-Face name is `guix-package-info-TYPE-inputs'."
- (let* ((type-str (symbol-name type))
- (type-name (and type (concat type-str "-")))
- (type-desc (and type (concat type-str " ")))
- (face (intern (concat "guix-package-info-" type-name "inputs")))
- (btn (intern (concat "guix-package-" type-name "input")))
- (fun (intern (concat "guix-package-info-insert-" type-name "inputs"))))
- `(progn
- (defface ,face
- '((t :inherit guix-package-info-name-button))
- ,(concat "Face used for " type-desc "inputs of a package.")
- :group 'guix-package-info-faces)
-
- (define-button-type ',btn
- :supertype 'guix-package-name
- 'face ',face)
-
- (defun ,fun (inputs &optional _)
- ,(concat "Make buttons from " type-desc "INPUTS and insert them at point.")
- (guix-package-info-insert-full-names inputs ',btn)))))
-
-(guix-package-info-define-insert-inputs)
-(guix-package-info-define-insert-inputs native)
-(guix-package-info-define-insert-inputs propagated)
-
-(defun guix-package-info-insert-full-names (names button-type)
- "Make BUTTON-TYPE buttons from package NAMES and insert them at point.
-NAMES is a list of strings."
- (if names
- (guix-info-insert-val-default
- (with-temp-buffer
- (guix-mapinsert (lambda (name)
- (guix-insert-button name button-type))
- names
- guix-list-separator)
- (buffer-substring (point-min) (point-max))))
- (guix-format-insert nil)))
-
-
-;;; Inserting outputs and installed parameters
-
-(defvar guix-package-info-output-format "%-10s"
- "String used to format output names of the packages.
-It should be a '%s'-sequence. After inserting an output name
-formatted with this string, an action button is inserted.")
-
-(defvar guix-package-info-obsolete-string "(This package is obsolete)"
- "String used if a package is obsolete.")
-
-(defvar guix-info-insert-installed-function nil
- "Function used to insert an installed information.
-It is called with a single argument - alist of installed
-parameters (`output', `path', `dependencies').
-If nil, insert installed info in a default way.")
-
-(defun guix-package-info-insert-outputs (outputs entry)
- "Insert OUTPUTS from package ENTRY at point."
- (and (guix-assq-value entry 'obsolete)
- (guix-package-info-insert-obsolete-text))
- (and (guix-assq-value entry 'non-unique)
- (guix-assq-value entry 'installed)
- (guix-package-info-insert-non-unique-text
- (guix-get-full-name entry)))
- (insert "\n")
- (mapc (lambda (output)
- (guix-package-info-insert-output output entry))
- outputs))
-
-(defun guix-package-info-insert-obsolete-text ()
- "Insert a message about obsolete package at point."
- (guix-info-insert-indent)
- (guix-format-insert guix-package-info-obsolete-string
- 'guix-package-info-obsolete))
-
-(defun guix-package-info-insert-non-unique-text (full-name)
- "Insert a message about non-unique package with FULL-NAME at point."
- (insert "\n")
- (guix-info-insert-indent)
- (insert "Installed outputs are displayed for a non-unique ")
- (guix-insert-button full-name 'guix-package-name)
- (insert " package."))
-
-(defun guix-package-info-insert-output (output entry)
- "Insert OUTPUT at point.
-Make some fancy text with buttons and additional stuff if the
-current OUTPUT is installed (if there is such output in
-`installed' parameter of a package ENTRY)."
- (let* ((installed (guix-assq-value entry 'installed))
- (obsolete (guix-assq-value entry 'obsolete))
- (installed-entry (cl-find-if
- (lambda (entry)
- (string= (guix-assq-value entry 'output)
- output))
- installed))
- (action-type (if installed-entry 'delete 'install)))
- (guix-info-insert-indent)
- (guix-format-insert output
- (if installed-entry
- 'guix-package-info-installed-outputs
- 'guix-package-info-uninstalled-outputs)
- guix-package-info-output-format)
- (guix-package-info-insert-action-button action-type entry output)
- (when obsolete
- (guix-info-insert-indent)
- (guix-package-info-insert-action-button 'upgrade entry output))
- (insert "\n")
- (when installed-entry
- (guix-info-insert-entry installed-entry 'installed 2))))
-
-(defun guix-package-info-insert-action-button (type entry output)
- "Insert button to process an action on a package OUTPUT at point.
-TYPE is one of the following symbols: `install', `delete', `upgrade'.
-ENTRY is an alist with package info."
- (let ((type-str (capitalize (symbol-name type)))
- (full-name (guix-get-full-name entry output)))
- (guix-info-insert-action-button
- type-str
- (lambda (btn)
- (guix-process-package-actions
- guix-profile
- `((,(button-get btn 'action-type) (,(button-get btn 'id)
- ,(button-get btn 'output))))
- (current-buffer)))
- (concat type-str " '" full-name "'")
- 'action-type type
- 'id (or (guix-assq-value entry 'package-id)
- (guix-assq-value entry 'id))
- 'output output)))
-
-(defun guix-package-info-insert-output-path (path &optional _)
- "Insert PATH of the installed output."
- (guix-info-insert-val-simple path #'guix-info-insert-file-path))
-
-(defalias 'guix-package-info-insert-output-dependencies
- 'guix-package-info-insert-output-path)
-
-
-;;; Inserting a source
-
-(defface guix-package-info-source
- '((t :inherit link :underline nil))
- "Face used for a source URL of a package."
- :group 'guix-package-info-faces)
-
-(defcustom guix-package-info-auto-find-source nil
- "If non-nil, find a source file after pressing a \"Show\" button.
-If nil, just display the source file path without finding."
- :type 'boolean
- :group 'guix-package-info)
-
-(defcustom guix-package-info-auto-download-source t
- "If nil, do not automatically download a source file if it doesn't exist.
-After pressing a \"Show\" button, a derivation of the package
-source is calculated and a store file path is displayed. If this
-variable is non-nil and the source file does not exist in the
-store, it will be automatically downloaded (with a possible
-prompt depending on `guix-operation-confirm' variable)."
- :type 'boolean
- :group 'guix-package-info)
-
-(defvar guix-package-info-download-buffer nil
- "Buffer from which a current download operation was performed.")
-
-(define-button-type 'guix-package-source
- :supertype 'guix
- 'face 'guix-package-info-source
- 'help-echo ""
- 'action (lambda (_)
- ;; As a source may not be a real URL (e.g., "mirror://..."),
- ;; no action is bound to a source button.
- (message "Yes, this is the source URL. What did you expect?")))
-
-(defun guix-package-info-insert-source-url (url &optional _)
- "Make button from source URL and insert it at point."
- (guix-insert-button url 'guix-package-source))
-
-(defun guix-package-info-show-source (entry-id package-id)
- "Show file name of a package source in the current info buffer.
-Find the file if needed (see `guix-package-info-auto-find-source').
-ENTRY-ID is an ID of the current entry (package or output).
-PACKAGE-ID is an ID of the package which source to show."
- (let* ((entry (guix-get-entry-by-id entry-id guix-entries))
- (file (guix-package-source-path package-id)))
- (or file
- (error "Couldn't define file path of the package source"))
- (let* ((new-entry (cons (cons 'source-file file)
- entry))
- (entries (cl-substitute-if
- new-entry
- (lambda (entry)
- (equal (guix-assq-value entry 'id)
- entry-id))
- guix-entries
- :count 1)))
- (guix-redisplay-buffer :entries entries)
- (if (file-exists-p file)
- (if guix-package-info-auto-find-source
- (guix-find-file file)
- (message "The source store path is displayed."))
- (if guix-package-info-auto-download-source
- (guix-package-info-download-source package-id)
- (message "The source does not exist in the store."))))))
-
-(defun guix-package-info-download-source (package-id)
- "Download a source of the package PACKAGE-ID."
- (setq guix-package-info-download-buffer (current-buffer))
- (guix-package-source-build-derivation
- package-id
- "The source does not exist in the store. Download it?"))
-
-(defun guix-package-info-insert-source (source entry)
- "Insert SOURCE from package ENTRY at point.
-SOURCE is a list of URLs."
- (guix-info-insert-indent)
- (if (null source)
- (guix-format-insert nil)
- (let* ((source-file (guix-assq-value entry 'source-file))
- (entry-id (guix-assq-value entry 'id))
- (package-id (or (guix-assq-value entry 'package-id)
- entry-id)))
- (if (null source-file)
- (guix-info-insert-action-button
- "Show"
- (lambda (btn)
- (guix-package-info-show-source (button-get btn 'entry-id)
- (button-get btn 'package-id)))
- "Show the source store path of the current package"
- 'entry-id entry-id
- 'package-id package-id)
- (unless (file-exists-p source-file)
- (guix-info-insert-action-button
- "Download"
- (lambda (btn)
- (guix-package-info-download-source
- (button-get btn 'package-id)))
- "Download the source into the store"
- 'package-id package-id))
- (guix-info-insert-val-simple source-file
- #'guix-info-insert-file-path))
- (guix-info-insert-val-simple source
- #'guix-package-info-insert-source-url))))
-
-(defun guix-package-info-redisplay-after-download ()
- "Redisplay an 'info' buffer after downloading the package source.
-This function is used to hide a \"Download\" button if needed."
- (when (buffer-live-p guix-package-info-download-buffer)
- (guix-redisplay-buffer :buffer guix-package-info-download-buffer)
- (setq guix-package-info-download-buffer nil)))
-
-(add-hook 'guix-after-source-download-hook
- 'guix-package-info-redisplay-after-download)
-
-
-;;; Displaying outputs
-
-(guix-define-buffer-type info output
- :buffer-name "*Guix Package Info*"
- :required (id package-id installed non-unique))
-
-(defvar guix-info-insert-output-function nil
- "Function used to insert an output information.
-It is called with a single argument - alist of output parameters.
-If nil, insert output in a default way.")
-
-(defun guix-output-info-insert-version (version entry)
- "Insert output VERSION and obsolete text if needed at point."
- (guix-info-insert-val-default version
- 'guix-package-info-version)
- (and (guix-assq-value entry 'obsolete)
- (guix-package-info-insert-obsolete-text)))
-
-(defun guix-output-info-insert-output (output entry)
- "Insert OUTPUT and action buttons at point."
- (let* ((installed (guix-assq-value entry 'installed))
- (obsolete (guix-assq-value entry 'obsolete))
- (action-type (if installed 'delete 'install)))
- (guix-info-insert-val-default
- output
- (if installed
- 'guix-package-info-installed-outputs
- 'guix-package-info-uninstalled-outputs))
- (guix-info-insert-indent)
- (guix-package-info-insert-action-button action-type entry output)
- (when obsolete
- (guix-info-insert-indent)
- (guix-package-info-insert-action-button 'upgrade entry output))))
-
-
-;;; Displaying generations
-
-(guix-define-buffer-type info generation)
-
-(defface guix-generation-info-number
- '((t :inherit font-lock-keyword-face))
- "Face used for a number of a generation."
- :group 'guix-generation-info-faces)
-
-(defface guix-generation-info-current
- '((t :inherit guix-package-info-installed-outputs))
- "Face used if a generation is the current one."
- :group 'guix-generation-info-faces)
-
-(defface guix-generation-info-not-current
- '((t nil))
- "Face used if a generation is not the current one."
- :group 'guix-generation-info-faces)
-
-(defvar guix-info-insert-generation-function nil
- "Function used to insert a generation information.
-It is called with a single argument - alist of generation parameters.
-If nil, insert generation in a default way.")
-
-(defun guix-generation-info-insert-number (number &optional _)
- "Insert generation NUMBER and action buttons."
- (guix-info-insert-val-default number 'guix-generation-info-number)
- (guix-info-insert-indent)
- (guix-info-insert-action-button
- "Packages"
- (lambda (btn)
- (guix-get-show-entries guix-profile 'list guix-package-list-type
- 'generation (button-get btn 'number)))
- "Show installed packages for this generation"
- 'number number)
- (guix-info-insert-indent)
- (guix-info-insert-action-button
- "Delete"
- (lambda (btn)
- (guix-delete-generations guix-profile (list (button-get btn 'number))
- (current-buffer)))
- "Delete this generation"
- 'number number))
-
-(defun guix-generation-info-insert-current (val entry)
- "Insert boolean value VAL showing whether this generation is current."
- (if val
- (guix-info-insert-val-default "Yes" 'guix-generation-info-current)
- (guix-info-insert-val-default "No" 'guix-generation-info-not-current)
- (guix-info-insert-indent)
- (guix-info-insert-action-button
- "Switch"
- (lambda (btn)
- (guix-switch-to-generation guix-profile (button-get btn 'number)
- (current-buffer)))
- "Switch to this generation (make it the current one)"
- 'number (guix-assq-value entry 'number))))
+(font-lock-add-keywords 'emacs-lisp-mode guix-info-font-lock-keywords)
(provide 'guix-info)
diff --git a/emacs/guix-list.el b/emacs/guix-list.el
index 560ae6a86f..7e57f42cb2 100644
--- a/emacs/guix-list.el
+++ b/emacs/guix-list.el
@@ -1,4 +1,4 @@
-;;; guix-list.el --- List buffers for displaying entries -*- lexical-binding: t -*-
+;;; guix-list.el --- 'List' buffer interface for displaying data -*- lexical-binding: t -*-
;; Copyright © 2014, 2015 Alex Kost <alezost@gmail.com>
@@ -19,26 +19,19 @@
;;; Commentary:
-;; This file provides a list-like buffer for displaying information
-;; about Guix packages and generations.
+;; This file provides 'list' buffer interface for displaying an arbitrary
+;; data.
;;; Code:
(require 'cl-lib)
(require 'tabulated-list)
+(require 'guix-buffer)
(require 'guix-info)
-(require 'guix-base)
+(require 'guix-entry)
(require 'guix-utils)
-(defgroup guix-list nil
- "General settings for list buffers."
- :prefix "guix-list-"
- :group 'guix)
-
-(defgroup guix-list-faces nil
- "Faces for list buffers."
- :group 'guix-list
- :group 'guix-faces)
+(guix-define-buffer-type list)
(defface guix-list-file-path
'((t :inherit guix-info-file-path))
@@ -50,153 +43,165 @@
"Face used for time stamps."
:group 'guix-list-faces)
-(defcustom guix-list-describe-warning-count 10
- "The maximum number of entries for describing without a warning.
-If a user wants to describe more than this number of marked
-entries, he will be prompted for confirmation."
- :type 'integer
- :group 'guix-list)
-
-(defvar guix-list-column-format
- `((package
- (name 20 t)
- (version 10 nil)
- (outputs 13 t)
- (installed 13 t)
- (synopsis 30 nil))
- (output
- (name 20 t)
- (version 10 nil)
- (output 9 t)
- (installed 12 t)
- (synopsis 30 nil))
- (generation
- (number 5
- ,(lambda (a b) (guix-list-sort-numerically 0 a b))
- :right-align t)
- (current 10 t)
- (time 20 t)
- (path 30 t)))
- "Columns displayed in list buffers.
-Each element of the list has a form:
-
- (ENTRY-TYPE . ((PARAM WIDTH SORT . PROPS) ...))
-
-PARAM is the name of an entry parameter of ENTRY-TYPE. For the
-meaning of WIDTH, SORT and PROPS, see `tabulated-list-format'.")
-
-(defvar guix-list-column-titles
- '((generation
- (number . "N.")))
- "Column titles for list buffers.
-Has the same structure as `guix-param-titles', but titles from
-this list have a priority.")
-
-(defvar guix-list-column-value-methods
- '((package
- (name . guix-package-list-get-name)
- (synopsis . guix-list-get-one-line)
- (description . guix-list-get-one-line)
- (installed . guix-package-list-get-installed-outputs))
- (output
- (name . guix-package-list-get-name)
- (synopsis . guix-list-get-one-line)
- (description . guix-list-get-one-line))
- (generation
- (current . guix-generation-list-get-current)
- (time . guix-list-get-time)
- (path . guix-list-get-file-path)))
- "Methods for inserting parameter values in columns.
-Each element of the list has a form:
+(defun guix-list-describe (&optional mark-names)
+ "Describe entries marked with a general mark.
+'Describe' means display entries in 'info' buffer.
+If no entries are marked, describe the current entry.
+With prefix argument, describe entries marked with any mark."
+ (interactive (list (unless current-prefix-arg '(general))))
+ (let* ((ids (or (apply #'guix-list-get-marked-id-list mark-names)
+ (list (guix-list-current-id))))
+ (count (length ids))
+ (entry-type (guix-buffer-current-entry-type)))
+ (when (or (<= count (guix-list-describe-warning-count entry-type))
+ (y-or-n-p (format "Do you really want to describe %d entries? "
+ count)))
+ (guix-list-describe-entries entry-type ids))))
- (ENTRY-TYPE . ((PARAM . FUN) ...))
+
+;;; Wrappers for 'list' variables
-PARAM is the name of an entry parameter of ENTRY-TYPE.
+(defvar guix-list-data nil
+ "Alist with 'list' data.
+This alist is filled by `guix-list-define-interface' macro.")
-FUN is a function returning a value that will be inserted. The
-function is called with 2 arguments: the first one is the value
-of the parameter; the second argument is an entry info (alist of
-parameters and their values).")
+(defun guix-list-value (entry-type symbol)
+ "Return SYMBOL's value for ENTRY-TYPE from `guix-list-data'."
+ (symbol-value (guix-assq-value guix-list-data entry-type symbol)))
-(defun guix-list-get-param-title (entry-type param)
- "Return title of an ENTRY-TYPE entry parameter PARAM."
- (or (guix-assq-value guix-list-column-titles
- entry-type param)
- (guix-get-param-title entry-type param)))
+(defun guix-list-param-title (entry-type param)
+ "Return column title of an ENTRY-TYPE parameter PARAM."
+ (guix-buffer-param-title 'list entry-type param))
-(defun guix-list-get-column-format (entry-type)
+(defun guix-list-format (entry-type)
"Return column format for ENTRY-TYPE."
- (guix-assq-value guix-list-column-format entry-type))
+ (guix-list-value entry-type 'format))
+
+(defun guix-list-displayed-params (entry-type)
+ "Return a list of ENTRY-TYPE parameters that should be displayed."
+ (mapcar #'car (guix-list-format entry-type)))
-(defun guix-list-get-displayed-params (entry-type)
- "Return list of parameters of ENTRY-TYPE that should be displayed."
- (mapcar #'car
- (guix-list-get-column-format entry-type)))
+(defun guix-list-sort-key (entry-type)
+ "Return sort key for ENTRY-TYPE."
+ (guix-list-value entry-type 'sort-key))
-(defun guix-list-get-sort-key (entry-type param &optional invert)
- "Return suitable sort key for `tabulated-list-sort-key'.
-Define column title by ENTRY-TYPE and PARAM. If INVERT is
-non-nil, invert the sort."
- (when (memq param (guix-list-get-displayed-params entry-type))
- (cons (guix-list-get-param-title entry-type param) invert)))
+(defun guix-list-additional-marks (entry-type)
+ "Return alist of additional marks for ENTRY-TYPE."
+ (guix-list-value entry-type 'marks))
+
+(defun guix-list-single-entry? (entry-type)
+ "Return non-nil, if a single entry of ENTRY-TYPE should be listed."
+ (guix-list-value entry-type 'list-single))
+
+(defun guix-list-describe-warning-count (entry-type)
+ "Return the maximum number of ENTRY-TYPE entries to describe."
+ (guix-list-value entry-type 'describe-count))
+
+(defun guix-list-describe-entries (entry-type ids)
+ "Describe ENTRY-TYPE entries with IDS in 'info' buffer"
+ (funcall (guix-list-value entry-type 'describe)
+ ids))
+
+
+;;; Tabulated list internals
(defun guix-list-sort-numerically (column a b)
"Compare COLUMN of tabulated entries A and B numerically.
-It is a sort predicate for `tabulated-list-format'.
+This function is used for sort predicates for `tabulated-list-format'.
Return non-nil, if B is bigger than A."
(cl-flet ((num (entry)
(string-to-number (aref (cadr entry) column))))
(> (num b) (num a))))
-(defun guix-list-make-tabulated-vector (entry-type fun)
+(defmacro guix-list-define-numerical-sorter (column)
+ "Define numerical sort predicate for COLUMN.
+See `guix-list-sort-numerically' for details."
+ (let ((name (intern (format "guix-list-sort-numerically-%d" column)))
+ (doc (format "\
+Predicate to sort tabulated list by column %d numerically.
+See `guix-list-sort-numerically' for details."
+ column)))
+ `(defun ,name (a b)
+ ,doc
+ (guix-list-sort-numerically ,column a b))))
+
+(defmacro guix-list-define-numerical-sorters (n)
+ "Define numerical sort predicates for columns from 0 to N.
+See `guix-list-define-numerical-sorter' for details."
+ `(progn
+ ,@(mapcar (lambda (i)
+ `(guix-list-define-numerical-sorter ,i))
+ (number-sequence 0 n))))
+
+(guix-list-define-numerical-sorters 9)
+
+(defun guix-list-tabulated-sort-key (entry-type)
+ "Return ENTRY-TYPE sort key for `tabulated-list-sort-key'."
+ (let ((sort-key (guix-list-sort-key entry-type)))
+ (and sort-key
+ (cons (guix-list-param-title entry-type (car sort-key))
+ (cdr sort-key)))))
+
+(defun guix-list-tabulated-vector (entry-type fun)
"Call FUN on each column specification for ENTRY-TYPE.
-FUN is called with 2 argument: parameter name and column
-specification (see `guix-list-column-format').
+FUN is applied to column specification as arguments (see
+`guix-list-format').
Return a vector made of values of FUN calls."
(apply #'vector
(mapcar (lambda (col-spec)
- (funcall fun (car col-spec) (cdr col-spec)))
- (guix-list-get-column-format entry-type))))
+ (apply fun col-spec))
+ (guix-list-format entry-type))))
-(defun guix-list-get-list-format (entry-type)
+(defun guix-list-tabulated-format (entry-type)
"Return ENTRY-TYPE list specification for `tabulated-list-format'."
- (guix-list-make-tabulated-vector
+ (guix-list-tabulated-vector
entry-type
- (lambda (param spec)
- (cons (guix-list-get-param-title entry-type param)
- spec))))
+ (lambda (param _ &rest rest-spec)
+ (cons (guix-list-param-title entry-type param)
+ rest-spec))))
-(defun guix-list-insert-entries (entries entry-type)
- "Display ENTRIES of ENTRY-TYPE in the current list buffer.
-ENTRIES should have a form of `guix-entries'."
- (setq tabulated-list-entries
- (guix-list-get-tabulated-entries entries entry-type))
- (tabulated-list-print))
-
-(defun guix-list-get-tabulated-entries (entries entry-type)
- "Return list of values of ENTRY-TYPE for `tabulated-list-entries'.
-Values are taken from ENTRIES which should have the form of
-`guix-entries'."
+(defun guix-list-tabulated-entries (entries entry-type)
+ "Return a list of ENTRY-TYPE values for `tabulated-list-entries'."
(mapcar (lambda (entry)
- (list (guix-assq-value entry 'id)
- (guix-list-get-tabulated-entry entry entry-type)))
+ (list (guix-entry-id entry)
+ (guix-list-tabulated-entry entry entry-type)))
entries))
-(defun guix-list-get-tabulated-entry (entry entry-type)
+(defun guix-list-tabulated-entry (entry entry-type)
"Return array of values for `tabulated-list-entries'.
-Parameters are taken from ENTRY of ENTRY-TYPE."
- (guix-list-make-tabulated-vector
+Parameters are taken from ENTRY-TYPE ENTRY."
+ (guix-list-tabulated-vector
entry-type
- (lambda (param _)
- (let ((val (guix-assq-value entry param))
- (fun (guix-assq-value guix-list-column-value-methods
- entry-type param)))
+ (lambda (param fun &rest _)
+ (let ((val (guix-entry-value entry param)))
(if fun
(funcall fun val entry)
(guix-get-string val))))))
+
+;;; Displaying entries
+
+(defun guix-list-get-display-entries (entry-type &rest args)
+ "Search for entries and show them in a 'list' buffer preferably."
+ (let ((entries (guix-buffer-get-entries 'list entry-type args)))
+ (if (or (null entries) ; = 0
+ (cdr entries) ; > 1
+ (guix-list-single-entry? entry-type)
+ (null (guix-buffer-value 'info entry-type 'show-entries)))
+ (guix-buffer-display-entries entries 'list entry-type args 'add)
+ (if (equal (guix-buffer-value 'info entry-type 'get-entries)
+ (guix-buffer-value 'list entry-type 'get-entries))
+ (guix-buffer-display-entries entries 'info entry-type args 'add)
+ (guix-buffer-get-display-entries 'info entry-type args 'add)))))
+
+(defun guix-list-insert-entries (entries entry-type)
+ "Print ENTRY-TYPE ENTRIES in the current buffer."
+ (setq tabulated-list-entries
+ (guix-list-tabulated-entries entries entry-type))
+ (tabulated-list-print))
+
(defun guix-list-get-one-line (val &optional _)
"Return one-line string from a multi-line string VAL.
VAL may be nil."
@@ -217,22 +222,18 @@ VAL may be nil."
'follow-link t
'help-echo "Find file"))
+
+;;; 'List' lines
+
(defun guix-list-current-id ()
- "Return ID of the current entry."
+ "Return ID of the entry at point."
(or (tabulated-list-get-id)
(user-error "No entry here")))
(defun guix-list-current-entry ()
- "Return alist of the current entry info."
- (guix-get-entry-by-id (guix-list-current-id) guix-entries))
-
-(defun guix-list-current-package-id ()
- "Return ID of the current package."
- (cl-ecase major-mode
- (guix-package-list-mode
- (guix-list-current-id))
- (guix-output-list-mode
- (guix-assq-value (guix-list-current-entry) 'package-id))))
+ "Return entry at point."
+ (guix-entry-by-id (guix-list-current-id)
+ (guix-buffer-current-entries)))
(defun guix-list-for-each-line (fun &rest args)
"Call FUN with ARGS for each entry line."
@@ -263,20 +264,28 @@ Each element of the list has a form:
(ID MARK-NAME . ARGS)
ID is an entry ID.
-MARK-NAME is a symbol from `guix-list-mark-alist'.
+MARK-NAME is a symbol from `guix-list-marks'.
ARGS is a list of additional values.")
-(defvar guix-list-mark-alist
+(defvar-local guix-list-marks nil
+ "Alist of available mark names and mark characters.")
+
+(defvar guix-list-default-marks
'((empty . ?\s)
(general . ?*))
- "Alist of available mark names and mark characters.")
+ "Alist of default mark names and mark characters.")
+
+(defun guix-list-marks (entry-type)
+ "Return alist of available marks for ENTRY-TYPE."
+ (append guix-list-default-marks
+ (guix-list-additional-marks entry-type)))
-(defsubst guix-list-get-mark (name)
+(defun guix-list-get-mark (name)
"Return mark character by its NAME."
- (or (guix-assq-value guix-list-mark-alist name)
+ (or (guix-assq-value guix-list-marks name)
(error "Mark '%S' not found" name)))
-(defsubst guix-list-get-mark-string (name)
+(defun guix-list-get-mark-string (name)
"Return mark string by its NAME."
(string (guix-list-get-mark name)))
@@ -288,11 +297,11 @@ ARGS is a list of additional values.")
"Return list of specs of entries marked with any mark from MARK-NAMES.
Entry specs are elements from `guix-list-marked' list.
If MARK-NAMES are not specified, use all marks from
-`guix-list-mark-alist' except the `empty' one."
+`guix-list-marks' except the `empty' one."
(or mark-names
(setq mark-names
(delq 'empty
- (mapcar #'car guix-list-mark-alist))))
+ (mapcar #'car guix-list-marks))))
(cl-remove-if-not (lambda (assoc)
(memq (cadr assoc) mark-names))
guix-list-marked))
@@ -314,7 +323,7 @@ See `guix-list-get-marked' for details."
(defun guix-list--mark (mark-name &optional advance &rest args)
"Put a mark on the current line.
Also add the current entry to `guix-list-marked' using its ID and ARGS.
-MARK-NAME is a symbol from `guix-list-mark-alist'.
+MARK-NAME is a symbol from `guix-list-marks'.
If ADVANCE is non-nil, move forward by one line after marking."
(let ((id (guix-list-current-id)))
(if (eq mark-name 'empty)
@@ -337,7 +346,7 @@ With ARG, mark all lines."
(defun guix-list-mark-all (&optional mark-name)
"Mark all lines with MARK-NAME mark.
-MARK-NAME is a symbol from `guix-list-mark-alist'.
+MARK-NAME is a symbol from `guix-list-marks'.
Interactively, put a general mark on all lines."
(interactive)
(or mark-name (setq mark-name 'general))
@@ -363,7 +372,7 @@ With ARG, unmark all lines."
(guix-list-mark-all 'empty))
(defun guix-list-restore-marks ()
- "Put marks according to `guix-list-mark-alist'."
+ "Put marks according to `guix-list-marked'."
(guix-list-for-each-line
(lambda ()
(let ((mark-name (car (guix-assq-value guix-list-marked
@@ -380,520 +389,183 @@ Same as `tabulated-list-sort', but also restore marks after sorting."
(guix-list-restore-marks))
+;;; Major mode and interface definer
+
(defvar guix-list-mode-map
(let ((map (make-sparse-keymap)))
(set-keymap-parent
- map (make-composed-keymap guix-root-map
+ map (make-composed-keymap guix-buffer-map
tabulated-list-mode-map))
(define-key map (kbd "RET") 'guix-list-describe)
+ (define-key map (kbd "i") 'guix-list-describe)
(define-key map (kbd "m") 'guix-list-mark)
(define-key map (kbd "*") 'guix-list-mark)
(define-key map (kbd "u") 'guix-list-unmark)
(define-key map (kbd "DEL") 'guix-list-unmark-backward)
(define-key map [remap tabulated-list-sort] 'guix-list-sort)
map)
- "Parent keymap for list buffers.")
+ "Keymap for `guix-list-mode' buffers.")
(define-derived-mode guix-list-mode tabulated-list-mode "Guix-List"
- "Parent mode for displaying information in list buffers."
- (setq tabulated-list-padding 2))
-
-(defmacro guix-list-define-entry-type (entry-type &rest args)
- "Define common stuff for displaying ENTRY-TYPE entries in list buffers.
-
-Remaining argument (ARGS) should have a form [KEYWORD VALUE] ... The
-following keywords are available:
-
- - `:sort-key' - default sort key for the tabulated list buffer.
-
- - `:invert-sort' - if non-nil, invert initial sort.
-
- - `:marks' - default value for the defined
- `guix-ENTRY-TYPE-mark-alist' variable.
-
-This macro defines the following functions:
-
- - `guix-ENTRY-TYPE-mark-MARK-NAME' functions for each mark
- specified in `:marks' argument."
- (let* ((entry-type-str (symbol-name entry-type))
- (prefix (concat "guix-" entry-type-str "-list"))
- (mode-str (concat prefix "-mode"))
- (init-fun (intern (concat prefix "-mode-initialize")))
- (marks-var (intern (concat prefix "-mark-alist")))
- (marks-val nil)
- (sort-key nil)
- (invert-sort nil))
-
- ;; Process the keyword args.
- (while (keywordp (car args))
- (pcase (pop args)
- (`:sort-key (setq sort-key (pop args)))
- (`:invert-sort (setq invert-sort (pop args)))
- (`:marks (setq marks-val (pop args)))
- (_ (pop args))))
-
- `(progn
- (defvar ,marks-var ',marks-val
- ,(concat "Alist of additional marks for `" mode-str "'.\n"
- "Marks from this list are added to `guix-list-mark-alist'."))
-
- ,@(mapcar (lambda (mark-spec)
- (let* ((mark-name (car mark-spec))
- (mark-name-str (symbol-name mark-name)))
- `(defun ,(intern (concat prefix "-mark-" mark-name-str "-simple")) ()
- ,(concat "Put '" mark-name-str "' mark and move to the next line.\n"
- "Also add the current entry to `guix-list-marked'.")
- (interactive)
- (guix-list--mark ',mark-name t))))
- marks-val)
-
- (defun ,init-fun ()
- ,(concat "Initial settings for `" mode-str "'.")
- ,(when sort-key
- `(setq tabulated-list-sort-key
- (guix-list-get-sort-key
- ',entry-type ',sort-key ,invert-sort)))
- (setq tabulated-list-format
- (guix-list-get-list-format ',entry-type))
- (setq-local guix-list-mark-alist
- (append guix-list-mark-alist ,marks-var))
- (tabulated-list-init-header)))))
-
-(put 'guix-list-define-entry-type 'lisp-indent-function 'defun)
-
-(defun guix-list-describe-maybe (entry-type ids)
- "Describe ENTRY-TYPE entries in info buffer using list of IDS."
- (let ((count (length ids)))
- (when (or (<= count guix-list-describe-warning-count)
- (y-or-n-p (format "Do you really want to describe %d entries? "
- count)))
- (apply #'guix-get-show-entries
- guix-profile 'info entry-type 'id ids))))
-
-(defun guix-list-describe (&optional arg)
- "Describe entries marked with a general mark.
-If no entries are marked, describe the current entry.
-With prefix (if ARG is non-nil), describe entries marked with any mark."
- (interactive "P")
- (let ((ids (or (apply #'guix-list-get-marked-id-list
- (unless arg '(general)))
- (list (guix-list-current-id)))))
- (guix-list-describe-maybe guix-entry-type ids)))
-
-(defun guix-list-edit-package ()
- "Go to the location of the current package."
- (interactive)
- (guix-edit (guix-list-current-package-id)))
-
-
-;;; Displaying packages
-
-(guix-define-buffer-type list package)
-
-(guix-list-define-entry-type package
- :sort-key name
- :marks ((install . ?I)
- (upgrade . ?U)
- (delete . ?D)))
-
-(defface guix-package-list-installed
- '((t :inherit guix-package-info-installed-outputs))
- "Face used if there are installed outputs for the current package."
- :group 'guix-package-list-faces)
-
-(defface guix-package-list-obsolete
- '((t :inherit guix-package-info-obsolete))
- "Face used if a package is obsolete."
- :group 'guix-package-list-faces)
-
-(defcustom guix-package-list-generation-marking-enabled nil
- "If non-nil, allow putting marks in a list with 'generation packages'.
-
-By default this is disabled, because it may be confusing. For
-example a package is installed in some generation, so a user can
-mark it for deletion in the list of packages from this
-generation, but the package may not be installed in the latest
-generation, so actually it cannot be deleted.
-
-If you managed to understand the explanation above or if you
-really know what you do or if you just don't care, you can set
-this variable to t. It should not do much harm anyway (most
-likely)."
- :type 'boolean
- :group 'guix-package-list)
-
-(let ((map guix-package-list-mode-map))
- (define-key map (kbd "e") 'guix-list-edit-package)
- (define-key map (kbd "x") 'guix-package-list-execute)
- (define-key map (kbd "i") 'guix-package-list-mark-install)
- (define-key map (kbd "d") 'guix-package-list-mark-delete)
- (define-key map (kbd "U") 'guix-package-list-mark-upgrade)
- (define-key map (kbd "^") 'guix-package-list-mark-upgrades))
-
-(defun guix-package-list-get-name (name entry)
- "Return NAME of the package ENTRY.
-Colorize it with `guix-package-list-installed' or
-`guix-package-list-obsolete' if needed."
- (guix-get-string name
- (cond ((guix-assq-value entry 'obsolete)
- 'guix-package-list-obsolete)
- ((guix-assq-value entry 'installed)
- 'guix-package-list-installed))))
-
-(defun guix-package-list-get-installed-outputs (installed &optional _)
- "Return string with outputs from INSTALLED entries."
- (guix-get-string
- (mapcar (lambda (entry)
- (guix-assq-value entry 'output))
- installed)))
-
-(defun guix-package-list-marking-check ()
- "Signal an error if marking is disabled for the current buffer."
- (when (and (not guix-package-list-generation-marking-enabled)
- (or (derived-mode-p 'guix-package-list-mode)
- (derived-mode-p 'guix-output-list-mode))
- (eq guix-search-type 'generation))
- (error "Action marks are disabled for lists of 'generation packages'")))
-
-(defun guix-package-list-mark-outputs (mark default
- &optional prompt available)
- "Mark the current package with MARK and move to the next line.
-If PROMPT is non-nil, use it to ask a user for outputs from
-AVAILABLE list, otherwise mark all DEFAULT outputs."
- (let ((outputs (if prompt
- (guix-completing-read-multiple
- prompt available nil t)
- default)))
- (apply #'guix-list--mark mark t outputs)))
-
-(defun guix-package-list-mark-install (&optional arg)
- "Mark the current package for installation and move to the next line.
-With ARG, prompt for the outputs to install (several outputs may
-be separated with \",\")."
- (interactive "P")
- (guix-package-list-marking-check)
- (let* ((entry (guix-list-current-entry))
- (all (guix-assq-value entry 'outputs))
- (installed (guix-get-installed-outputs entry))
- (available (cl-set-difference all installed :test #'string=)))
- (or available
- (user-error "This package is already installed"))
- (guix-package-list-mark-outputs
- 'install '("out")
- (and arg "Output(s) to install: ")
- available)))
-
-(defun guix-package-list-mark-delete (&optional arg)
- "Mark the current package for deletion and move to the next line.
-With ARG, prompt for the outputs to delete (several outputs may
-be separated with \",\")."
- (interactive "P")
- (guix-package-list-marking-check)
- (let* ((entry (guix-list-current-entry))
- (installed (guix-get-installed-outputs entry)))
- (or installed
- (user-error "This package is not installed"))
- (guix-package-list-mark-outputs
- 'delete installed
- (and arg "Output(s) to delete: ")
- installed)))
-
-(defun guix-package-list-mark-upgrade (&optional arg)
- "Mark the current package for upgrading and move to the next line.
-With ARG, prompt for the outputs to upgrade (several outputs may
-be separated with \",\")."
- (interactive "P")
- (guix-package-list-marking-check)
- (let* ((entry (guix-list-current-entry))
- (installed (guix-get-installed-outputs entry)))
- (or installed
- (user-error "This package is not installed"))
- (when (or (guix-assq-value entry 'obsolete)
- (y-or-n-p "This package is not obsolete. Try to upgrade it anyway? "))
- (guix-package-list-mark-outputs
- 'upgrade installed
- (and arg "Output(s) to upgrade: ")
- installed))))
-
-(defun guix-list-mark-package-upgrades (fun)
- "Mark all obsolete packages for upgrading.
-Use FUN to perform marking of the current line. FUN should
-accept an entry as argument."
- (guix-package-list-marking-check)
- (let ((obsolete (cl-remove-if-not
- (lambda (entry)
- (guix-assq-value entry 'obsolete))
- guix-entries)))
- (guix-list-for-each-line
- (lambda ()
- (let* ((id (guix-list-current-id))
- (entry (cl-find-if
- (lambda (entry)
- (equal id (guix-assq-value entry 'id)))
- obsolete)))
- (when entry
- (funcall fun entry)))))))
-
-(defun guix-package-list-mark-upgrades ()
- "Mark all obsolete packages for upgrading."
- (interactive)
- (guix-list-mark-package-upgrades
- (lambda (entry)
- (apply #'guix-list--mark
- 'upgrade nil
- (guix-get-installed-outputs entry)))))
-
-(defun guix-list-execute-package-actions (fun)
- "Perform actions on the marked packages.
-Use FUN to define actions suitable for `guix-process-package-actions'.
-FUN should accept action-type as argument."
- (let ((actions (delq nil
- (mapcar fun '(install delete upgrade)))))
- (if actions
- (guix-process-package-actions
- guix-profile actions (current-buffer))
- (user-error "No operations specified"))))
-
-(defun guix-package-list-execute ()
- "Perform actions on the marked packages."
- (interactive)
- (guix-list-execute-package-actions #'guix-package-list-make-action))
+ "Parent mode for displaying data in 'list' form.")
+
+(defun guix-list-mode-initialize (entry-type)
+ "Set up the current 'list' buffer for displaying ENTRY-TYPE entries."
+ (setq tabulated-list-padding 2
+ tabulated-list-format (guix-list-tabulated-format entry-type)
+ tabulated-list-sort-key (guix-list-tabulated-sort-key entry-type))
+ (setq-local guix-list-marks (guix-list-marks entry-type))
+ (tabulated-list-init-header))
+
+(defmacro guix-list-define-interface (entry-type &rest args)
+ "Define 'list' interface for displaying ENTRY-TYPE entries.
+Remaining arguments (ARGS) should have a form [KEYWORD VALUE] ...
+
+Required keywords:
+
+ - `:format' - default value of the generated
+ `guix-ENTRY-TYPE-list-format' variable.
+
+Optional keywords:
+
+ - `:sort-key' - default value of the generated
+ `guix-ENTRY-TYPE-list-sort-key' variable.
+
+ - `:describe-function' - default value of the generated
+ `guix-ENTRY-TYPE-describe-function' variable.
+
+ - `:list-single?' - default value of the generated
+ `guix-ENTRY-TYPE-list-single' variable.
+
+ - `:marks' - default value of the generated
+ `guix-ENTRY-TYPE-list-marks' variable.
+
+The rest keyword arguments are passed to
+`guix-buffer-define-interface' macro."
+ (declare (indent 1))
+ (let* ((entry-type-str (symbol-name entry-type))
+ (prefix (concat "guix-" entry-type-str "-list"))
+ (group (intern prefix))
+ (describe-var (intern (concat prefix "-describe-function")))
+ (describe-count-var (intern (concat prefix
+ "-describe-warning-count")))
+ (format-var (intern (concat prefix "-format")))
+ (sort-key-var (intern (concat prefix "-sort-key")))
+ (list-single-var (intern (concat prefix "-single")))
+ (marks-var (intern (concat prefix "-marks"))))
+ (guix-keyword-args-let args
+ ((show-entries-val :show-entries-function)
+ (describe-val :describe-function)
+ (describe-count-val :describe-count 10)
+ (format-val :format)
+ (sort-key-val :sort-key)
+ (list-single-val :list-single?)
+ (marks-val :marks))
+ `(progn
+ (defcustom ,format-var ,format-val
+ ,(format "\
+List of format values of the displayed columns.
+Each element of the list has a form:
-(defun guix-package-list-make-action (action-type)
- "Return action specification for the packages marked with ACTION-TYPE.
-Return nil, if there are no packages marked with ACTION-TYPE.
-The specification is suitable for `guix-process-package-actions'."
- (let ((specs (guix-list-get-marked-args action-type)))
- (and specs (cons action-type specs))))
+ (PARAM VALUE-FUN WIDTH SORT . PROPS)
+
+PARAM is a name of '%s' entry parameter.
+
+VALUE-FUN may be either nil or a function returning a value that
+will be inserted. The function is called with 2 arguments: the
+first one is the value of the parameter; the second one is an
+entry (alist of parameter names and values).
+
+For the meaning of WIDTH, SORT and PROPS, see
+`tabulated-list-format'."
+ entry-type-str)
+ :type 'sexp
+ :group ',group)
+
+ (defcustom ,sort-key-var ,sort-key-val
+ ,(format "\
+Default sort key for 'list' buffer with '%s' entries.
+Should be nil (no sort) or have a form:
+
+ (PARAM . FLIP)
+
+PARAM is the name of '%s' entry parameter. For the meaning of
+FLIP, see `tabulated-list-sort-key'."
+ entry-type-str entry-type-str)
+ :type '(choice (const :tag "No sort" nil)
+ (cons symbol boolean))
+ :group ',group)
+
+ (defvar ,marks-var ,marks-val
+ ,(format "\
+Alist of additional marks for 'list' buffer with '%s' entries.
+Marks from this list are used along with `guix-list-default-marks'."
+ entry-type-str))
+
+ (defcustom ,list-single-var ,list-single-val
+ ,(format "\
+If non-nil, list '%s' entry even if it is the only matching result.
+If nil, show a single '%s' entry in the 'info' buffer."
+ entry-type-str entry-type-str)
+ :type 'boolean
+ :group ',group)
+
+ (defcustom ,describe-count-var ,describe-count-val
+ ,(format "\
+The maximum number of '%s' entries to describe without a warning.
+If a user wants to describe more than this number of marked
+entries, he will be prompted for confirmation.
+See also `guix-list-describe'."
+ entry-type-str)
+ :type 'integer
+ :group ',group)
+
+ (defvar ,describe-var ,describe-val
+ ,(format "Function used to describe '%s' entries."
+ entry-type-str))
+
+ (guix-alist-put!
+ '((describe . ,describe-var)
+ (describe-count . ,describe-count-var)
+ (format . ,format-var)
+ (sort-key . ,sort-key-var)
+ (list-single . ,list-single-var)
+ (marks . ,marks-var))
+ 'guix-list-data ',entry-type)
+
+ ,(if show-entries-val
+ `(guix-buffer-define-interface list ,entry-type
+ :show-entries-function ,show-entries-val
+ ,@%foreign-args)
+
+ (let ((insert-fun (intern (concat prefix "-insert-entries")))
+ (mode-init-fun (intern (concat prefix "-mode-initialize"))))
+ `(progn
+ (defun ,insert-fun (entries)
+ ,(format "\
+Print '%s' ENTRIES in the current 'list' buffer."
+ entry-type-str)
+ (guix-list-insert-entries entries ',entry-type))
+
+ (defun ,mode-init-fun ()
+ ,(format "\
+Set up the current 'list' buffer for displaying '%s' entries."
+ entry-type-str)
+ (guix-list-mode-initialize ',entry-type))
+
+ (guix-buffer-define-interface list ,entry-type
+ :insert-entries-function ',insert-fun
+ :mode-init-function ',mode-init-fun
+ ,@%foreign-args))))))))
-;;; Displaying outputs
-
-(guix-define-buffer-type list output
- :buffer-name "*Guix Package List*"
- :required (package-id))
-
-(guix-list-define-entry-type output
- :sort-key name
- :marks ((install . ?I)
- (upgrade . ?U)
- (delete . ?D)))
-
-(let ((map guix-output-list-mode-map))
- (define-key map (kbd "RET") 'guix-output-list-describe)
- (define-key map (kbd "e") 'guix-list-edit-package)
- (define-key map (kbd "x") 'guix-output-list-execute)
- (define-key map (kbd "i") 'guix-output-list-mark-install)
- (define-key map (kbd "d") 'guix-output-list-mark-delete)
- (define-key map (kbd "U") 'guix-output-list-mark-upgrade)
- (define-key map (kbd "^") 'guix-output-list-mark-upgrades))
-
-(defun guix-output-list-mark-install ()
- "Mark the current output for installation and move to the next line."
- (interactive)
- (guix-package-list-marking-check)
- (let* ((entry (guix-list-current-entry))
- (installed (guix-assq-value entry 'installed)))
- (if installed
- (user-error "This output is already installed")
- (guix-list--mark 'install t))))
-
-(defun guix-output-list-mark-delete ()
- "Mark the current output for deletion and move to the next line."
- (interactive)
- (guix-package-list-marking-check)
- (let* ((entry (guix-list-current-entry))
- (installed (guix-assq-value entry 'installed)))
- (if installed
- (guix-list--mark 'delete t)
- (user-error "This output is not installed"))))
-
-(defun guix-output-list-mark-upgrade ()
- "Mark the current output for deletion and move to the next line."
- (interactive)
- (guix-package-list-marking-check)
- (let* ((entry (guix-list-current-entry))
- (installed (guix-assq-value entry 'installed)))
- (or installed
- (user-error "This output is not installed"))
- (when (or (guix-assq-value entry 'obsolete)
- (y-or-n-p "This output is not obsolete. Try to upgrade it anyway? "))
- (guix-list--mark 'upgrade t))))
-
-(defun guix-output-list-mark-upgrades ()
- "Mark all obsolete package outputs for upgrading."
- (interactive)
- (guix-list-mark-package-upgrades
- (lambda (_) (guix-list--mark 'upgrade))))
-
-(defun guix-output-list-execute ()
- "Perform actions on the marked outputs."
- (interactive)
- (guix-list-execute-package-actions #'guix-output-list-make-action))
-
-(defun guix-output-list-make-action (action-type)
- "Return action specification for the outputs marked with ACTION-TYPE.
-Return nil, if there are no outputs marked with ACTION-TYPE.
-The specification is suitable for `guix-process-output-actions'."
- (let ((ids (guix-list-get-marked-id-list action-type)))
- (and ids (cons action-type
- (mapcar #'guix-get-package-id-and-output-by-output-id
- ids)))))
-
-(defun guix-output-list-describe (&optional arg)
- "Describe outputs or packages marked with a general mark.
-If no entries are marked, describe the current output or package.
-With prefix (if ARG is non-nil), describe entries marked with any mark.
-Also see `guix-package-info-type'."
- (interactive "P")
- (if (eq guix-package-info-type 'output)
- (guix-list-describe arg)
- (let* ((oids (or (apply #'guix-list-get-marked-id-list
- (unless arg '(general)))
- (list (guix-list-current-id))))
- (pids (mapcar (lambda (oid)
- (car (guix-get-package-id-and-output-by-output-id
- oid)))
- oids)))
- (guix-list-describe-maybe 'package (cl-remove-duplicates pids)))))
+(defvar guix-list-font-lock-keywords
+ (eval-when-compile
+ `((,(rx "(" (group "guix-list-define-interface")
+ symbol-end)
+ . 1))))
-
-;;; Displaying generations
-
-(guix-define-buffer-type list generation)
-
-(guix-list-define-entry-type generation
- :sort-key number
- :invert-sort t
- :marks ((delete . ?D)))
-
-(let ((map guix-generation-list-mode-map))
- (define-key map (kbd "RET") 'guix-generation-list-show-packages)
- (define-key map (kbd "+") 'guix-generation-list-show-added-packages)
- (define-key map (kbd "-") 'guix-generation-list-show-removed-packages)
- (define-key map (kbd "=") 'guix-generation-list-diff)
- (define-key map (kbd "D") 'guix-generation-list-diff)
- (define-key map (kbd "e") 'guix-generation-list-ediff)
- (define-key map (kbd "x") 'guix-generation-list-execute)
- (define-key map (kbd "i") 'guix-list-describe)
- (define-key map (kbd "s") 'guix-generation-list-switch)
- (define-key map (kbd "d") 'guix-generation-list-mark-delete))
-
-(defun guix-generation-list-get-current (val &optional _)
- "Return string from VAL showing whether this generation is current.
-VAL is a boolean value."
- (if val "(current)" ""))
-
-(defun guix-generation-list-switch ()
- "Switch current profile to the generation at point."
- (interactive)
- (let* ((entry (guix-list-current-entry))
- (current (guix-assq-value entry 'current))
- (number (guix-assq-value entry 'number)))
- (if current
- (user-error "This generation is already the current one")
- (guix-switch-to-generation guix-profile number (current-buffer)))))
-
-(defun guix-generation-list-show-packages ()
- "List installed packages for the generation at point."
- (interactive)
- (guix-get-show-entries guix-profile 'list guix-package-list-type
- 'generation (guix-list-current-id)))
-
-(defun guix-generation-list-generations-to-compare ()
- "Return a sorted list of 2 marked generations for comparing."
- (let ((numbers (guix-list-get-marked-id-list 'general)))
- (if (/= (length numbers) 2)
- (user-error "2 generations should be marked for comparing")
- (sort numbers #'<))))
-
-(defun guix-generation-list-show-added-packages ()
- "List package outputs added to the latest marked generation.
-If 2 generations are marked with \\[guix-list-mark], display
-outputs installed in the latest marked generation that were not
-installed in the other one."
- (interactive)
- (apply #'guix-get-show-entries
- guix-profile 'list 'output 'generation-diff
- (reverse (guix-generation-list-generations-to-compare))))
-
-(defun guix-generation-list-show-removed-packages ()
- "List package outputs removed from the latest marked generation.
-If 2 generations are marked with \\[guix-list-mark], display
-outputs not installed in the latest marked generation that were
-installed in the other one."
- (interactive)
- (apply #'guix-get-show-entries
- guix-profile 'list 'output 'generation-diff
- (guix-generation-list-generations-to-compare)))
-
-(defun guix-generation-list-compare (diff-fun gen-fun)
- "Run GEN-FUN on the 2 marked generations and run DIFF-FUN on the results."
- (cl-multiple-value-bind (gen1 gen2)
- (guix-generation-list-generations-to-compare)
- (funcall diff-fun
- (funcall gen-fun gen1)
- (funcall gen-fun gen2))))
-
-(defun guix-generation-list-ediff-manifests ()
- "Run Ediff on manifests of the 2 marked generations."
- (interactive)
- (guix-generation-list-compare
- #'ediff-files
- #'guix-profile-generation-manifest-file))
-
-(defun guix-generation-list-diff-manifests ()
- "Run Diff on manifests of the 2 marked generations."
- (interactive)
- (guix-generation-list-compare
- #'guix-diff
- #'guix-profile-generation-manifest-file))
-
-(defun guix-generation-list-ediff-packages ()
- "Run Ediff on package outputs installed in the 2 marked generations."
- (interactive)
- (guix-generation-list-compare
- #'ediff-buffers
- #'guix-profile-generation-packages-buffer))
-
-(defun guix-generation-list-diff-packages ()
- "Run Diff on package outputs installed in the 2 marked generations."
- (interactive)
- (guix-generation-list-compare
- #'guix-diff
- #'guix-profile-generation-packages-buffer))
-
-(defun guix-generation-list-ediff (arg)
- "Run Ediff on package outputs installed in the 2 marked generations.
-With ARG, run Ediff on manifests of the marked generations."
- (interactive "P")
- (if arg
- (guix-generation-list-ediff-manifests)
- (guix-generation-list-ediff-packages)))
-
-(defun guix-generation-list-diff (arg)
- "Run Diff on package outputs installed in the 2 marked generations.
-With ARG, run Diff on manifests of the marked generations."
- (interactive "P")
- (if arg
- (guix-generation-list-diff-manifests)
- (guix-generation-list-diff-packages)))
-
-(defun guix-generation-list-mark-delete (&optional arg)
- "Mark the current generation for deletion and move to the next line.
-With ARG, mark all generations for deletion."
- (interactive "P")
- (if arg
- (guix-list-mark-all 'delete)
- (guix-list--mark 'delete t)))
-
-(defun guix-generation-list-execute ()
- "Delete marked generations."
- (interactive)
- (let ((marked (guix-list-get-marked-id-list 'delete)))
- (or marked
- (user-error "No generations marked for deletion"))
- (guix-delete-generations guix-profile marked (current-buffer))))
+(font-lock-add-keywords 'emacs-lisp-mode guix-list-font-lock-keywords)
(provide 'guix-list)
diff --git a/emacs/guix-main.scm b/emacs/guix-main.scm
index 7175b103da..6f9eb422e0 100644
--- a/emacs/guix-main.scm
+++ b/emacs/guix-main.scm
@@ -58,7 +58,6 @@
(guix licenses)
(guix utils)
(guix ui)
- (guix scripts graph)
(guix scripts lint)
(guix scripts package)
(guix scripts pull)
@@ -989,7 +988,8 @@ Return #t if the shell command was executed successfully."
(define (graph-type-names)
"Return a list of names of available graph node types."
- (map node-type-name %node-types))
+ (map (@ (guix graph) node-type-name)
+ (@ (guix scripts graph) %node-types)))
(define (refresh-updater-names)
"Return a list of names of available refresh updater types."
diff --git a/emacs/guix-messages.el b/emacs/guix-messages.el
index 2bf99de6fa..eb2a76e216 100644
--- a/emacs/guix-messages.el
+++ b/emacs/guix-messages.el
@@ -31,9 +31,8 @@
(defvar guix-messages
`((package
(id
- (0 "Packages not found.")
- (1 "")
- (many "%d packages." count))
+ ,(lambda (_ entries ids)
+ (guix-message-packages-by-id entries 'package ids)))
(name
,(lambda (_ entries names)
(guix-message-packages-by-name entries 'package names)))
@@ -67,9 +66,8 @@
(output
(id
- (0 "Package outputs not found.")
- (1 "")
- (many "%d package outputs." count))
+ ,(lambda (_ entries ids)
+ (guix-message-packages-by-id entries 'output ids)))
(name
,(lambda (_ entries names)
(guix-message-packages-by-name entries 'output names)))
@@ -147,6 +145,22 @@
(guix-message-string-entry-type
entry-type 'plural)))))
+(defun guix-message-packages-by-id (entries entry-type ids)
+ "Display a message for packages or outputs searched by IDS."
+ (let* ((count (length entries))
+ (str-beg (guix-message-string-entries count entry-type))
+ (str-end (if (> count 1)
+ (concat "with the following IDs: "
+ (mapconcat #'guix-get-string ids ", "))
+ (concat "with ID " (guix-get-string (car ids))))))
+ (if (zerop count)
+ (message "%s %s.
+Most likely, Guix REPL was restarted, so IDs are not actual
+anymore, because they live only during the REPL process.
+Try \"M-x guix-search-by-name\"."
+ str-beg str-end)
+ (message "%s %s." str-beg str-end))))
+
(defun guix-message-packages-by-name (entries entry-type names)
"Display a message for packages or outputs searched by NAMES."
(let* ((count (length entries))
diff --git a/emacs/guix-read.el b/emacs/guix-read.el
index e60af9c2f7..3bc7b16587 100644
--- a/emacs/guix-read.el
+++ b/emacs/guix-read.el
@@ -26,95 +26,40 @@
(require 'guix-help-vars)
(require 'guix-utils)
-(require 'guix-base)
-
-(defun guix-read-file-name (prompt &optional dir default-filename
- mustmatch initial predicate)
- "Read file name.
-This function is similar to `read-file-name' except it also
-expands the file name."
- (expand-file-name (read-file-name prompt dir default-filename
- mustmatch initial predicate)))
-
-(defmacro guix-define-reader (name read-fun completions prompt)
- "Define NAME function to read from minibuffer.
-READ-FUN may be `completing-read', `completing-read-multiple' or
-another function with the same arguments."
- `(defun ,name (&optional prompt initial-contents)
- (,read-fun ,(if prompt
- `(or prompt ,prompt)
- 'prompt)
- ,completions nil nil initial-contents)))
-
-(defmacro guix-define-readers (&rest args)
- "Define reader functions.
-
-ARGS should have a form [KEYWORD VALUE] ... The following
-keywords are available:
-
- - `completions-var' - variable used to get completions.
-
- - `completions-getter' - function used to get completions.
-
- - `single-reader', `single-prompt' - name of a function to read
- a single value, and a prompt for it.
-
- - `multiple-reader', `multiple-prompt' - name of a function to
- read multiple values, and a prompt for it.
-
- - `multiple-separator' - if specified, another
- `<multiple-reader-name>-string' function returning a string
- of multiple values separated the specified separator will be
- defined."
- (let (completions-var
- completions-getter
- single-reader
- single-prompt
- multiple-reader
- multiple-prompt
- multiple-separator)
-
- ;; Process the keyword args.
- (while (keywordp (car args))
- (pcase (pop args)
- (`:completions-var (setq completions-var (pop args)))
- (`:completions-getter (setq completions-getter (pop args)))
- (`:single-reader (setq single-reader (pop args)))
- (`:single-prompt (setq single-prompt (pop args)))
- (`:multiple-reader (setq multiple-reader (pop args)))
- (`:multiple-prompt (setq multiple-prompt (pop args)))
- (`:multiple-separator (setq multiple-separator (pop args)))
- (_ (pop args))))
-
- (let ((completions
- (cond ((and completions-var completions-getter)
- `(or ,completions-var
- (setq ,completions-var
- (funcall ',completions-getter))))
- (completions-var
- completions-var)
- (completions-getter
- `(funcall ',completions-getter)))))
- `(progn
- ,(when (and completions-var
- (not (boundp completions-var)))
- `(defvar ,completions-var nil))
-
- ,(when single-reader
- `(guix-define-reader ,single-reader completing-read
- ,completions ,single-prompt))
-
- ,(when multiple-reader
- `(guix-define-reader ,multiple-reader completing-read-multiple
- ,completions ,multiple-prompt))
-
- ,(when (and multiple-reader multiple-separator)
- (let ((name (intern (concat (symbol-name multiple-reader)
- "-string"))))
- `(defun ,name (&optional prompt initial-contents)
- (guix-concat-strings
- (,multiple-reader prompt initial-contents)
- ,multiple-separator))))))))
+(require 'guix-backend)
+(require 'guix-guile)
+
+
+;;; Receivable lists of packages, lint checkers, etc.
+
+(guix-memoized-defun guix-graph-type-names ()
+ "Return a list of names of available graph node types."
+ (guix-eval-read (guix-make-guile-expression 'graph-type-names)))
+
+(guix-memoized-defun guix-refresh-updater-names ()
+ "Return a list of names of available refresh updater types."
+ (guix-eval-read (guix-make-guile-expression 'refresh-updater-names)))
+
+(guix-memoized-defun guix-lint-checker-names ()
+ "Return a list of names of available lint checkers."
+ (guix-eval-read (guix-make-guile-expression 'lint-checker-names)))
+
+(guix-memoized-defun guix-package-names ()
+ "Return a list of names of available packages."
+ (sort
+ ;; Work around <https://github.com/jaor/geiser/issues/64>:
+ ;; list of strings is parsed much slower than list of lists,
+ ;; so we use 'package-names-lists' instead of 'package-names'.
+
+ ;; (guix-eval-read (guix-make-guile-expression 'package-names))
+
+ (mapcar #'car
+ (guix-eval-read (guix-make-guile-expression
+ 'package-names-lists)))
+ #'string<))
+
+
+;;; Readers
(guix-define-readers
:completions-var guix-help-system-types
diff --git a/emacs/guix-ui-generation.el b/emacs/guix-ui-generation.el
new file mode 100644
index 0000000000..aa71645b4e
--- /dev/null
+++ b/emacs/guix-ui-generation.el
@@ -0,0 +1,433 @@
+;;; guix-ui-generation.el --- Interface for displaying generations -*- lexical-binding: t -*-
+
+;; Copyright © 2014, 2015 Alex Kost <alezost@gmail.com>
+
+;; This file is part of GNU Guix.
+
+;; GNU Guix 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.
+
+;; GNU Guix 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.
+
+;; 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 file provides an interface for displaying profile generations in
+;; 'list' and 'info' buffers, and commands for working with them.
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'guix-buffer)
+(require 'guix-list)
+(require 'guix-info)
+(require 'guix-ui)
+(require 'guix-ui-package)
+(require 'guix-base)
+(require 'guix-backend)
+(require 'guix-guile)
+(require 'guix-entry)
+(require 'guix-utils)
+
+(guix-ui-define-entry-type generation)
+
+(defun guix-generation-get-display (profile search-type &rest search-values)
+ "Search for generations and show results.
+
+If PROFILE is nil, use `guix-current-profile'.
+
+See `guix-ui-get-entries' for the meaning of SEARCH-TYPE and
+SEARCH-VALUES."
+ (apply #'guix-list-get-display-entries
+ 'generation
+ (or profile guix-current-profile)
+ search-type search-values))
+
+(defun guix-delete-generations (profile generations
+ &optional operation-buffer)
+ "Delete GENERATIONS from PROFILE.
+Each element from GENERATIONS is a generation number."
+ (when (or (not guix-operation-confirm)
+ (y-or-n-p
+ (let ((count (length generations)))
+ (if (> count 1)
+ (format "Delete %d generations from profile '%s'? "
+ count profile)
+ (format "Delete generation %d from profile '%s'? "
+ (car generations) profile)))))
+ (guix-eval-in-repl
+ (guix-make-guile-expression
+ 'delete-generations* profile generations)
+ operation-buffer)))
+
+(defun guix-switch-to-generation (profile generation
+ &optional operation-buffer)
+ "Switch PROFILE to GENERATION."
+ (when (or (not guix-operation-confirm)
+ (y-or-n-p (format "Switch profile '%s' to generation %d? "
+ profile generation)))
+ (guix-eval-in-repl
+ (guix-make-guile-expression
+ 'switch-to-generation* profile generation)
+ operation-buffer)))
+
+
+;;; Generation 'info'
+
+(guix-ui-info-define-interface generation
+ :buffer-name "*Guix Generation Info*"
+ :format '((number format guix-generation-info-insert-number)
+ (prev-number format (format))
+ (current format guix-generation-info-insert-current)
+ (path simple (indent guix-file))
+ (time format (time)))
+ :titles '((path . "File name")
+ (prev-number . "Previous number")))
+
+(defface guix-generation-info-number
+ '((t :inherit font-lock-keyword-face))
+ "Face used for a number of a generation."
+ :group 'guix-generation-info-faces)
+
+(defface guix-generation-info-current
+ '((t :inherit guix-package-info-installed-outputs))
+ "Face used if a generation is the current one."
+ :group 'guix-generation-info-faces)
+
+(defface guix-generation-info-not-current
+ '((t nil))
+ "Face used if a generation is not the current one."
+ :group 'guix-generation-info-faces)
+
+(defun guix-generation-info-insert-number (number &optional _)
+ "Insert generation NUMBER and action buttons."
+ (guix-info-insert-value-format number 'guix-generation-info-number)
+ (guix-info-insert-indent)
+ (guix-info-insert-action-button
+ "Packages"
+ (lambda (btn)
+ (guix-buffer-get-display-entries
+ 'list guix-package-list-type
+ (list (guix-ui-current-profile)
+ 'generation (button-get btn 'number))
+ 'add))
+ "Show installed packages for this generation"
+ 'number number)
+ (guix-info-insert-indent)
+ (guix-info-insert-action-button
+ "Delete"
+ (lambda (btn)
+ (guix-delete-generations (guix-ui-current-profile)
+ (list (button-get btn 'number))
+ (current-buffer)))
+ "Delete this generation"
+ 'number number))
+
+(defun guix-generation-info-insert-current (val entry)
+ "Insert boolean value VAL showing whether this generation is current."
+ (if val
+ (guix-info-insert-value-format "Yes" 'guix-generation-info-current)
+ (guix-info-insert-value-format "No" 'guix-generation-info-not-current)
+ (guix-info-insert-indent)
+ (guix-info-insert-action-button
+ "Switch"
+ (lambda (btn)
+ (guix-switch-to-generation (guix-ui-current-profile)
+ (button-get btn 'number)
+ (current-buffer)))
+ "Switch to this generation (make it the current one)"
+ 'number (guix-entry-value entry 'number))))
+
+
+;;; Generation 'list'
+
+(guix-ui-list-define-interface generation
+ :buffer-name "*Guix Generation List*"
+ :format '((number nil 5 guix-list-sort-numerically-0 :right-align t)
+ (current guix-generation-list-get-current 10 t)
+ (time guix-list-get-time 20 t)
+ (path guix-list-get-file-path 30 t))
+ :titles '((number . "N."))
+ :sort-key '(number . t)
+ :marks '((delete . ?D)))
+
+(let ((map guix-generation-list-mode-map))
+ (define-key map (kbd "RET") 'guix-generation-list-show-packages)
+ (define-key map (kbd "+") 'guix-generation-list-show-added-packages)
+ (define-key map (kbd "-") 'guix-generation-list-show-removed-packages)
+ (define-key map (kbd "=") 'guix-generation-list-diff)
+ (define-key map (kbd "D") 'guix-generation-list-diff)
+ (define-key map (kbd "e") 'guix-generation-list-ediff)
+ (define-key map (kbd "x") 'guix-generation-list-execute)
+ (define-key map (kbd "s") 'guix-generation-list-switch)
+ (define-key map (kbd "c") 'guix-generation-list-switch)
+ (define-key map (kbd "d") 'guix-generation-list-mark-delete))
+
+(defun guix-generation-list-get-current (val &optional _)
+ "Return string from VAL showing whether this generation is current.
+VAL is a boolean value."
+ (if val "(current)" ""))
+
+(defun guix-generation-list-switch ()
+ "Switch current profile to the generation at point."
+ (interactive)
+ (let* ((entry (guix-list-current-entry))
+ (current (guix-entry-value entry 'current))
+ (number (guix-entry-value entry 'number)))
+ (if current
+ (user-error "This generation is already the current one")
+ (guix-switch-to-generation (guix-ui-current-profile)
+ number (current-buffer)))))
+
+(defun guix-generation-list-show-packages ()
+ "List installed packages for the generation at point."
+ (interactive)
+ (guix-package-get-display
+ (guix-ui-current-profile)
+ 'generation (guix-list-current-id)))
+
+(defun guix-generation-list-generations-to-compare ()
+ "Return a sorted list of 2 marked generations for comparing."
+ (let ((numbers (guix-list-get-marked-id-list 'general)))
+ (if (/= (length numbers) 2)
+ (user-error "2 generations should be marked for comparing")
+ (sort numbers #'<))))
+
+(defun guix-generation-list-show-added-packages ()
+ "List package outputs added to the latest marked generation.
+If 2 generations are marked with \\[guix-list-mark], display
+outputs installed in the latest marked generation that were not
+installed in the other one."
+ (interactive)
+ (guix-buffer-get-display-entries
+ 'list 'output
+ (cl-list* (guix-ui-current-profile)
+ 'generation-diff
+ (reverse (guix-generation-list-generations-to-compare)))
+ 'add))
+
+(defun guix-generation-list-show-removed-packages ()
+ "List package outputs removed from the latest marked generation.
+If 2 generations are marked with \\[guix-list-mark], display
+outputs not installed in the latest marked generation that were
+installed in the other one."
+ (interactive)
+ (guix-buffer-get-display-entries
+ 'list 'output
+ (cl-list* (guix-ui-current-profile)
+ 'generation-diff
+ (guix-generation-list-generations-to-compare))
+ 'add))
+
+(defun guix-generation-list-compare (diff-fun gen-fun)
+ "Run GEN-FUN on the 2 marked generations and run DIFF-FUN on the results."
+ (cl-multiple-value-bind (gen1 gen2)
+ (guix-generation-list-generations-to-compare)
+ (funcall diff-fun
+ (funcall gen-fun gen1)
+ (funcall gen-fun gen2))))
+
+(defun guix-generation-list-ediff-manifests ()
+ "Run Ediff on manifests of the 2 marked generations."
+ (interactive)
+ (guix-generation-list-compare
+ #'ediff-files
+ #'guix-profile-generation-manifest-file))
+
+(defun guix-generation-list-diff-manifests ()
+ "Run Diff on manifests of the 2 marked generations."
+ (interactive)
+ (guix-generation-list-compare
+ #'guix-diff
+ #'guix-profile-generation-manifest-file))
+
+(defun guix-generation-list-ediff-packages ()
+ "Run Ediff on package outputs installed in the 2 marked generations."
+ (interactive)
+ (guix-generation-list-compare
+ #'ediff-buffers
+ #'guix-profile-generation-packages-buffer))
+
+(defun guix-generation-list-diff-packages ()
+ "Run Diff on package outputs installed in the 2 marked generations."
+ (interactive)
+ (guix-generation-list-compare
+ #'guix-diff
+ #'guix-profile-generation-packages-buffer))
+
+(defun guix-generation-list-ediff (arg)
+ "Run Ediff on package outputs installed in the 2 marked generations.
+With ARG, run Ediff on manifests of the marked generations."
+ (interactive "P")
+ (if arg
+ (guix-generation-list-ediff-manifests)
+ (guix-generation-list-ediff-packages)))
+
+(defun guix-generation-list-diff (arg)
+ "Run Diff on package outputs installed in the 2 marked generations.
+With ARG, run Diff on manifests of the marked generations."
+ (interactive "P")
+ (if arg
+ (guix-generation-list-diff-manifests)
+ (guix-generation-list-diff-packages)))
+
+(defun guix-generation-list-mark-delete (&optional arg)
+ "Mark the current generation for deletion and move to the next line.
+With ARG, mark all generations for deletion."
+ (interactive "P")
+ (if arg
+ (guix-list-mark-all 'delete)
+ (guix-list--mark 'delete t)))
+
+(defun guix-generation-list-execute ()
+ "Delete marked generations."
+ (interactive)
+ (let ((marked (guix-list-get-marked-id-list 'delete)))
+ (or marked
+ (user-error "No generations marked for deletion"))
+ (guix-delete-generations (guix-ui-current-profile)
+ marked (current-buffer))))
+
+
+;;; Inserting packages to compare generations
+
+(defcustom guix-generation-packages-buffer-name-function
+ #'guix-generation-packages-buffer-name-default
+ "Function used to define name of a buffer with generation packages.
+This function is called with 2 arguments: PROFILE (string) and
+GENERATION (number)."
+ :type '(choice (function-item guix-generation-packages-buffer-name-default)
+ (function-item guix-generation-packages-buffer-name-long)
+ (function :tag "Other function"))
+ :group 'guix-generation)
+
+(defcustom guix-generation-packages-update-buffer t
+ "If non-nil, always update list of packages during comparing generations.
+If nil, generation packages are received only once. So when you
+compare generation 1 and generation 2, the packages for both
+generations will be received. Then if you compare generation 1
+and generation 3, only the packages for generation 3 will be
+received. Thus if you use comparing of different generations a
+lot, you may set this variable to nil to improve the
+performance."
+ :type 'boolean
+ :group 'guix-generation)
+
+(defvar guix-generation-output-name-width 30
+ "Width of an output name \"column\".
+This variable is used in auxiliary buffers for comparing generations.")
+
+(defun guix-generation-packages (profile generation)
+ "Return a list of sorted packages installed in PROFILE's GENERATION.
+Each element of the list is a list of the package specification
+and its store path."
+ (let ((names+paths (guix-eval-read
+ (guix-make-guile-expression
+ 'generation-package-specifications+paths
+ profile generation))))
+ (sort names+paths
+ (lambda (a b)
+ (string< (car a) (car b))))))
+
+(defun guix-generation-packages-buffer-name-default (profile generation)
+ "Return name of a buffer for displaying GENERATION's package outputs.
+Use base name of PROFILE file name."
+ (let ((profile-name (file-name-base (directory-file-name profile))))
+ (format "*Guix %s: generation %s*"
+ profile-name generation)))
+
+(defun guix-generation-packages-buffer-name-long (profile generation)
+ "Return name of a buffer for displaying GENERATION's package outputs.
+Use the full PROFILE file name."
+ (format "*Guix generation %s (%s)*"
+ generation profile))
+
+(defun guix-generation-packages-buffer-name (profile generation)
+ "Return name of a buffer for displaying GENERATION's package outputs."
+ (funcall guix-generation-packages-buffer-name-function
+ profile generation))
+
+(defun guix-generation-insert-package (name path)
+ "Insert package output NAME and store PATH at point."
+ (insert name)
+ (indent-to guix-generation-output-name-width 2)
+ (insert path "\n"))
+
+(defun guix-generation-insert-packages (buffer profile generation)
+ "Insert package outputs installed in PROFILE's GENERATION in BUFFER."
+ (with-current-buffer buffer
+ (setq buffer-read-only nil
+ indent-tabs-mode nil)
+ (erase-buffer)
+ (mapc (lambda (name+path)
+ (guix-generation-insert-package
+ (car name+path) (cadr name+path)))
+ (guix-generation-packages profile generation))))
+
+(defun guix-generation-packages-buffer (profile generation)
+ "Return buffer with package outputs installed in PROFILE's GENERATION.
+Create the buffer if needed."
+ (let ((buf-name (guix-generation-packages-buffer-name
+ profile generation)))
+ (or (and (null guix-generation-packages-update-buffer)
+ (get-buffer buf-name))
+ (let ((buf (get-buffer-create buf-name)))
+ (guix-generation-insert-packages buf profile generation)
+ buf))))
+
+(defun guix-profile-generation-manifest-file (generation)
+ "Return the file name of a GENERATION's manifest.
+GENERATION is a generation number of the current profile."
+ (guix-manifest-file (guix-ui-current-profile) generation))
+
+(defun guix-profile-generation-packages-buffer (generation)
+ "Insert GENERATION's package outputs in a buffer and return it.
+GENERATION is a generation number of the current profile."
+ (guix-generation-packages-buffer (guix-ui-current-profile)
+ generation))
+
+
+;;; Interactive commands
+
+;;;###autoload
+(defun guix-generations (&optional profile)
+ "Display information about all generations.
+If PROFILE is nil, use `guix-current-profile'.
+Interactively with prefix, prompt for PROFILE."
+ (interactive (list (guix-ui-read-profile)))
+ (guix-generation-get-display profile 'all))
+
+;;;###autoload
+(defun guix-last-generations (number &optional profile)
+ "Display information about last NUMBER generations.
+If PROFILE is nil, use `guix-current-profile'.
+Interactively with prefix, prompt for PROFILE."
+ (interactive
+ (list (read-number "The number of last generations: ")
+ (guix-ui-read-profile)))
+ (guix-generation-get-display profile 'last number))
+
+;;;###autoload
+(defun guix-generations-by-time (from to &optional profile)
+ "Display information about generations created between FROM and TO.
+FROM and TO should be time values.
+If PROFILE is nil, use `guix-current-profile'.
+Interactively with prefix, prompt for PROFILE."
+ (interactive
+ (list (guix-read-date "Find generations (from): ")
+ (guix-read-date "Find generations (to): ")
+ (guix-ui-read-profile)))
+ (guix-generation-get-display profile 'time
+ (float-time from)
+ (float-time to)))
+
+(provide 'guix-ui-generation)
+
+;;; guix-ui-generation.el ends here
diff --git a/emacs/guix-ui-package.el b/emacs/guix-ui-package.el
new file mode 100644
index 0000000000..e0c98eaed6
--- /dev/null
+++ b/emacs/guix-ui-package.el
@@ -0,0 +1,955 @@
+;;; guix-ui-package.el --- Interface for displaying packages -*- lexical-binding: t -*-
+
+;; Copyright © 2014, 2015 Alex Kost <alezost@gmail.com>
+
+;; This file is part of GNU Guix.
+
+;; GNU Guix 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.
+
+;; GNU Guix 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.
+
+;; 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 file provides an interface for displaying packages and outputs
+;; in 'list' and 'info' buffers, and commands for working with them.
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'guix-buffer)
+(require 'guix-list)
+(require 'guix-info)
+(require 'guix-ui)
+(require 'guix-base)
+(require 'guix-backend)
+(require 'guix-guile)
+(require 'guix-entry)
+(require 'guix-utils)
+(require 'guix-hydra-build)
+
+(guix-ui-define-entry-type package)
+(guix-ui-define-entry-type output)
+
+(defcustom guix-package-list-type 'output
+ "Define how to display packages in 'list' buffer.
+Should be a symbol `package' or `output' (if `output', display each
+output on a separate line; if `package', display each package on
+a separate line)."
+ :type '(choice (const :tag "List of packages" package)
+ (const :tag "List of outputs" output))
+ :group 'guix-package)
+
+(defcustom guix-package-info-type 'package
+ "Define how to display packages in 'info' buffer.
+Should be a symbol `package' or `output' (if `output', display
+each output separately; if `package', display outputs inside
+package data)."
+ :type '(choice (const :tag "Display packages" package)
+ (const :tag "Display outputs" output))
+ :group 'guix-package)
+
+(defun guix-package-get-display (profile search-type &rest search-values)
+ "Search for packages/outputs and show results.
+
+If PROFILE is nil, use `guix-current-profile'.
+
+See `guix-ui-get-entries' for the meaning of SEARCH-TYPE and
+SEARCH-VALUES.
+
+Results are displayed in the list buffer, unless a single package
+is found and `guix-package-list-single' is nil."
+ (let* ((args (cl-list* (or profile guix-current-profile)
+ search-type search-values))
+ (entries (guix-buffer-get-entries
+ 'list guix-package-list-type args)))
+ (if (or guix-package-list-single
+ (null entries)
+ (cdr entries))
+ (guix-buffer-display-entries
+ entries 'list guix-package-list-type args 'add)
+ (guix-buffer-get-display-entries
+ 'info guix-package-info-type args 'add))))
+
+(defun guix-package-entry->name-specification (entry &optional output)
+ "Return name specification of the package ENTRY and OUTPUT."
+ (guix-package-name-specification
+ (guix-entry-value entry 'name)
+ (guix-entry-value entry 'version)
+ (or output (guix-entry-value entry 'output))))
+
+(defun guix-package-entries->name-specifications (entries)
+ "Return name specifications by the package or output ENTRIES."
+ (cl-remove-duplicates (mapcar #'guix-package-entry->name-specification
+ entries)
+ :test #'string=))
+
+(defun guix-package-installed-outputs (entry)
+ "Return a list of installed outputs for the package ENTRY."
+ (mapcar (lambda (installed-entry)
+ (guix-entry-value installed-entry 'output))
+ (guix-entry-value entry 'installed)))
+
+(defun guix-package-id-and-output-by-output-id (output-id)
+ "Return a list (PACKAGE-ID OUTPUT) by OUTPUT-ID."
+ (cl-multiple-value-bind (package-id-str output)
+ (split-string output-id ":")
+ (let ((package-id (string-to-number package-id-str)))
+ (list (if (= 0 package-id) package-id-str package-id)
+ output))))
+
+
+;;; Processing package actions
+
+(defun guix-process-package-actions (profile actions
+ &optional operation-buffer)
+ "Process package ACTIONS on PROFILE.
+Each action is a list of the form:
+
+ (ACTION-TYPE PACKAGE-SPEC ...)
+
+ACTION-TYPE is one of the following symbols: `install',
+`upgrade', `remove'/`delete'.
+PACKAGE-SPEC should have the following form: (ID [OUTPUT] ...)."
+ (let (install upgrade remove)
+ (mapc (lambda (action)
+ (let ((action-type (car action))
+ (specs (cdr action)))
+ (cl-case action-type
+ (install (setq install (append install specs)))
+ (upgrade (setq upgrade (append upgrade specs)))
+ ((remove delete) (setq remove (append remove specs))))))
+ actions)
+ (when (guix-continue-package-operation-p
+ profile
+ :install install :upgrade upgrade :remove remove)
+ (guix-eval-in-repl
+ (guix-make-guile-expression
+ 'process-package-actions profile
+ :install install :upgrade upgrade :remove remove
+ :use-substitutes? (or guix-use-substitutes 'f)
+ :dry-run? (or guix-dry-run 'f))
+ (and (not guix-dry-run) operation-buffer)))))
+
+(cl-defun guix-continue-package-operation-p (profile
+ &key install upgrade remove)
+ "Return non-nil if a package operation should be continued.
+Ask a user if needed (see `guix-operation-confirm').
+INSTALL, UPGRADE, REMOVE are 'package action specifications'.
+See `guix-process-package-actions' for details."
+ (or (null guix-operation-confirm)
+ (let* ((entries (guix-ui-get-entries
+ profile 'package 'id
+ (append (mapcar #'car install)
+ (mapcar #'car upgrade)
+ (mapcar #'car remove))
+ '(id name version location)))
+ (install-strings (guix-get-package-strings install entries))
+ (upgrade-strings (guix-get-package-strings upgrade entries))
+ (remove-strings (guix-get-package-strings remove entries)))
+ (if (or install-strings upgrade-strings remove-strings)
+ (let ((buf (get-buffer-create guix-temp-buffer-name)))
+ (with-current-buffer buf
+ (setq-local cursor-type nil)
+ (setq buffer-read-only nil)
+ (erase-buffer)
+ (insert "Profile: " profile "\n\n")
+ (guix-insert-package-strings install-strings "install")
+ (guix-insert-package-strings upgrade-strings "upgrade")
+ (guix-insert-package-strings remove-strings "remove")
+ (let ((win (temp-buffer-window-show
+ buf
+ '((display-buffer-reuse-window
+ display-buffer-at-bottom)
+ (window-height . fit-window-to-buffer)))))
+ (prog1 (guix-operation-prompt)
+ (quit-window nil win)))))
+ (message "Nothing to be done.
+If Guix REPL was restarted, the data is not up-to-date.")
+ nil))))
+
+(defun guix-get-package-strings (specs entries)
+ "Return short package descriptions for performing package actions.
+See `guix-process-package-actions' for the meaning of SPECS.
+ENTRIES is a list of package entries to get info about packages."
+ (delq nil
+ (mapcar
+ (lambda (spec)
+ (let* ((id (car spec))
+ (outputs (cdr spec))
+ (entry (guix-entry-by-id id entries)))
+ (when entry
+ (let ((location (guix-entry-value entry 'location)))
+ (concat (guix-package-entry->name-specification entry)
+ (when outputs
+ (concat ":"
+ (guix-concat-strings outputs ",")))
+ (when location
+ (concat "\t(" location ")")))))))
+ specs)))
+
+(defun guix-insert-package-strings (strings action)
+ "Insert information STRINGS at point for performing package ACTION."
+ (when strings
+ (insert "Package(s) to " (propertize action 'face 'bold) ":\n")
+ (mapc (lambda (str)
+ (insert " " str "\n"))
+ strings)
+ (insert "\n")))
+
+
+;;; Package 'info'
+
+(guix-ui-info-define-interface package
+ :buffer-name "*Guix Package Info*"
+ :format '(guix-package-info-insert-heading
+ ignore
+ (synopsis ignore (simple guix-package-info-synopsis))
+ ignore
+ (description ignore (simple guix-package-info-description))
+ ignore
+ (outputs simple guix-package-info-insert-outputs)
+ (source simple guix-package-info-insert-source)
+ (location format (format guix-package-location))
+ (home-url format (format guix-url))
+ (license format (format guix-package-info-license))
+ (inputs format (format guix-package-input))
+ (native-inputs format (format guix-package-native-input))
+ (propagated-inputs format
+ (format guix-package-propagated-input)))
+ :titles '((home-url . "Home page"))
+ :required '(id name version installed non-unique))
+
+(guix-info-define-interface installed-output
+ :format '((path simple (indent guix-file))
+ (dependencies simple (indent guix-file)))
+ :titles '((path . "Store directory"))
+ :reduced? t)
+
+(defface guix-package-info-heading
+ '((t :inherit guix-info-heading))
+ "Face for package name and version headings."
+ :group 'guix-package-info-faces)
+
+(defface guix-package-info-name
+ '((t :inherit font-lock-keyword-face))
+ "Face used for a name of a package."
+ :group 'guix-package-info-faces)
+
+(defface guix-package-info-name-button
+ '((t :inherit button))
+ "Face used for a full name that can be used to describe a package."
+ :group 'guix-package-info-faces)
+
+(defface guix-package-info-version
+ '((t :inherit font-lock-builtin-face))
+ "Face used for a version of a package."
+ :group 'guix-package-info-faces)
+
+(defface guix-package-info-synopsis
+ '((((type tty pc) (class color)) :weight bold)
+ (t :height 1.1 :weight bold :inherit variable-pitch))
+ "Face used for a synopsis of a package."
+ :group 'guix-package-info-faces)
+
+(defface guix-package-info-description
+ '((t))
+ "Face used for a description of a package."
+ :group 'guix-package-info-faces)
+
+(defface guix-package-info-license
+ '((t :inherit font-lock-string-face))
+ "Face used for a license of a package."
+ :group 'guix-package-info-faces)
+
+(defface guix-package-info-location
+ '((t :inherit link))
+ "Face used for a location of a package."
+ :group 'guix-package-info-faces)
+
+(defface guix-package-info-source
+ '((t :inherit link :underline nil))
+ "Face used for a source URL of a package."
+ :group 'guix-package-info-faces)
+
+(defface guix-package-info-installed-outputs
+ '((default :weight bold)
+ (((class color) (min-colors 88) (background light))
+ :foreground "ForestGreen")
+ (((class color) (min-colors 88) (background dark))
+ :foreground "PaleGreen")
+ (((class color) (min-colors 8))
+ :foreground "green")
+ (t :underline t))
+ "Face used for installed outputs of a package."
+ :group 'guix-package-info-faces)
+
+(defface guix-package-info-uninstalled-outputs
+ '((t :weight bold))
+ "Face used for uninstalled outputs of a package."
+ :group 'guix-package-info-faces)
+
+(defface guix-package-info-obsolete
+ '((t :inherit error))
+ "Face used if a package is obsolete."
+ :group 'guix-package-info-faces)
+
+(defcustom guix-package-info-auto-find-source nil
+ "If non-nil, find a source file after pressing a \"Show\" button.
+If nil, just display the source file path without finding."
+ :type 'boolean
+ :group 'guix-package-info)
+
+(defcustom guix-package-info-auto-download-source t
+ "If nil, do not automatically download a source file if it doesn't exist.
+After pressing a \"Show\" button, a derivation of the package
+source is calculated and a store file path is displayed. If this
+variable is non-nil and the source file does not exist in the
+store, it will be automatically downloaded (with a possible
+prompt depending on `guix-operation-confirm' variable)."
+ :type 'boolean
+ :group 'guix-package-info)
+
+(defvar guix-package-info-download-buffer nil
+ "Buffer from which a current download operation was performed.")
+
+(defvar guix-package-info-output-format "%-10s"
+ "String used to format output names of the packages.
+It should be a '%s'-sequence. After inserting an output name
+formatted with this string, an action button is inserted.")
+
+(defvar guix-package-info-obsolete-string "(This package is obsolete)"
+ "String used if a package is obsolete.")
+
+(define-button-type 'guix-package-location
+ :supertype 'guix
+ 'face 'guix-package-info-location
+ 'help-echo "Find location of this package"
+ 'action (lambda (btn)
+ (guix-find-location (button-label btn))))
+
+(define-button-type 'guix-package-name
+ :supertype 'guix
+ 'face 'guix-package-info-name-button
+ 'help-echo "Describe this package"
+ 'action (lambda (btn)
+ (guix-buffer-get-display-entries-current
+ 'info guix-package-info-type
+ (list (guix-ui-current-profile)
+ 'name (button-label btn))
+ 'add)))
+
+(define-button-type 'guix-package-source
+ :supertype 'guix
+ 'face 'guix-package-info-source
+ 'help-echo ""
+ 'action (lambda (_)
+ ;; As a source may not be a real URL (e.g., "mirror://..."),
+ ;; no action is bound to a source button.
+ (message "Yes, this is the source URL. What did you expect?")))
+
+(defun guix-package-info-insert-heading (entry)
+ "Insert package ENTRY heading (name specification) at point."
+ (guix-insert-button
+ (guix-package-entry->name-specification entry)
+ 'guix-package-name
+ 'face 'guix-package-info-heading))
+
+(defmacro guix-package-info-define-insert-inputs (&optional type)
+ "Define a face and a function for inserting package inputs.
+TYPE is a type of inputs.
+Function name is `guix-package-info-insert-TYPE-inputs'.
+Face name is `guix-package-info-TYPE-inputs'."
+ (let* ((type-str (symbol-name type))
+ (type-name (and type (concat type-str "-")))
+ (type-desc (and type (concat type-str " ")))
+ (face (intern (concat "guix-package-info-" type-name "inputs")))
+ (btn (intern (concat "guix-package-" type-name "input"))))
+ `(progn
+ (defface ,face
+ '((t :inherit guix-package-info-name-button))
+ ,(concat "Face used for " type-desc "inputs of a package.")
+ :group 'guix-package-info-faces)
+
+ (define-button-type ',btn
+ :supertype 'guix-package-name
+ 'face ',face))))
+
+(guix-package-info-define-insert-inputs)
+(guix-package-info-define-insert-inputs native)
+(guix-package-info-define-insert-inputs propagated)
+
+(defun guix-package-info-insert-outputs (outputs entry)
+ "Insert OUTPUTS from package ENTRY at point."
+ (and (guix-entry-value entry 'obsolete)
+ (guix-package-info-insert-obsolete-text))
+ (and (guix-entry-value entry 'non-unique)
+ (guix-entry-value entry 'installed)
+ (guix-package-info-insert-non-unique-text
+ (guix-package-entry->name-specification entry)))
+ (insert "\n")
+ (dolist (output outputs)
+ (guix-package-info-insert-output output entry)))
+
+(defun guix-package-info-insert-obsolete-text ()
+ "Insert a message about obsolete package at point."
+ (guix-info-insert-indent)
+ (guix-format-insert guix-package-info-obsolete-string
+ 'guix-package-info-obsolete))
+
+(defun guix-package-info-insert-non-unique-text (full-name)
+ "Insert a message about non-unique package with FULL-NAME at point."
+ (insert "\n")
+ (guix-info-insert-indent)
+ (insert "Installed outputs are displayed for a non-unique ")
+ (guix-insert-button full-name 'guix-package-name)
+ (insert " package."))
+
+(defun guix-package-info-insert-output (output entry)
+ "Insert OUTPUT at point.
+Make some fancy text with buttons and additional stuff if the
+current OUTPUT is installed (if there is such output in
+`installed' parameter of a package ENTRY)."
+ (let* ((installed (guix-entry-value entry 'installed))
+ (obsolete (guix-entry-value entry 'obsolete))
+ (installed-entry (cl-find-if
+ (lambda (entry)
+ (string= (guix-entry-value entry 'output)
+ output))
+ installed))
+ (action-type (if installed-entry 'delete 'install)))
+ (guix-info-insert-indent)
+ (guix-format-insert output
+ (if installed-entry
+ 'guix-package-info-installed-outputs
+ 'guix-package-info-uninstalled-outputs)
+ guix-package-info-output-format)
+ (guix-package-info-insert-action-button action-type entry output)
+ (when obsolete
+ (guix-info-insert-indent)
+ (guix-package-info-insert-action-button 'upgrade entry output))
+ (insert "\n")
+ (when installed-entry
+ (guix-info-insert-entry installed-entry 'installed-output 2))))
+
+(defun guix-package-info-insert-action-button (type entry output)
+ "Insert button to process an action on a package OUTPUT at point.
+TYPE is one of the following symbols: `install', `delete', `upgrade'.
+ENTRY is an alist with package info."
+ (let ((type-str (capitalize (symbol-name type)))
+ (full-name (guix-package-entry->name-specification entry output)))
+ (guix-info-insert-action-button
+ type-str
+ (lambda (btn)
+ (guix-process-package-actions
+ (guix-ui-current-profile)
+ `((,(button-get btn 'action-type) (,(button-get btn 'id)
+ ,(button-get btn 'output))))
+ (current-buffer)))
+ (concat type-str " '" full-name "'")
+ 'action-type type
+ 'id (or (guix-entry-value entry 'package-id)
+ (guix-entry-id entry))
+ 'output output)))
+
+(defun guix-package-info-show-source (entry-id package-id)
+ "Show file name of a package source in the current info buffer.
+Find the file if needed (see `guix-package-info-auto-find-source').
+ENTRY-ID is an ID of the current entry (package or output).
+PACKAGE-ID is an ID of the package which source to show."
+ (let* ((entries (guix-buffer-current-entries))
+ (entry (guix-entry-by-id entry-id entries))
+ (file (guix-package-source-path package-id)))
+ (or file
+ (error "Couldn't define file name of the package source"))
+ (let* ((new-entry (cons (cons 'source-file file)
+ entry))
+ (new-entries (guix-replace-entry entry-id new-entry entries)))
+ (setf (guix-buffer-item-entries guix-buffer-item)
+ new-entries)
+ (guix-buffer-redisplay-goto-button)
+ (if (file-exists-p file)
+ (if guix-package-info-auto-find-source
+ (guix-find-file file)
+ (message "The source store path is displayed."))
+ (if guix-package-info-auto-download-source
+ (guix-package-info-download-source package-id)
+ (message "The source does not exist in the store."))))))
+
+(defun guix-package-info-download-source (package-id)
+ "Download a source of the package PACKAGE-ID."
+ (setq guix-package-info-download-buffer (current-buffer))
+ (guix-package-source-build-derivation
+ package-id
+ "The source does not exist in the store. Download it?"))
+
+(defun guix-package-info-insert-source (source entry)
+ "Insert SOURCE from package ENTRY at point.
+SOURCE is a list of URLs."
+ (if (null source)
+ (guix-format-insert nil)
+ (let* ((source-file (guix-entry-value entry 'source-file))
+ (entry-id (guix-entry-id entry))
+ (package-id (or (guix-entry-value entry 'package-id)
+ entry-id)))
+ (if (null source-file)
+ (guix-info-insert-action-button
+ "Show"
+ (lambda (btn)
+ (guix-package-info-show-source (button-get btn 'entry-id)
+ (button-get btn 'package-id)))
+ "Show the source store directory of the current package"
+ 'entry-id entry-id
+ 'package-id package-id)
+ (unless (file-exists-p source-file)
+ (guix-info-insert-action-button
+ "Download"
+ (lambda (btn)
+ (guix-package-info-download-source
+ (button-get btn 'package-id)))
+ "Download the source into the store"
+ 'package-id package-id))
+ (guix-info-insert-value-indent source-file 'guix-file))
+ (guix-info-insert-value-indent source 'guix-package-source))))
+
+(defun guix-package-info-redisplay-after-download ()
+ "Redisplay an 'info' buffer after downloading the package source.
+This function is used to hide a \"Download\" button if needed."
+ (when (buffer-live-p guix-package-info-download-buffer)
+ (with-current-buffer guix-package-info-download-buffer
+ (guix-buffer-redisplay-goto-button))
+ (setq guix-package-info-download-buffer nil)))
+
+(add-hook 'guix-after-source-download-hook
+ 'guix-package-info-redisplay-after-download)
+
+
+;;; Package 'list'
+
+(guix-ui-list-define-interface package
+ :buffer-name "*Guix Package List*"
+ :format '((name guix-package-list-get-name 20 t)
+ (version nil 10 nil)
+ (outputs nil 13 t)
+ (installed guix-package-list-get-installed-outputs 13 t)
+ (synopsis guix-list-get-one-line 30 nil))
+ :sort-key '(name)
+ :marks '((install . ?I)
+ (upgrade . ?U)
+ (delete . ?D)))
+
+(let ((map guix-package-list-mode-map))
+ (define-key map (kbd "B") 'guix-package-list-latest-builds)
+ (define-key map (kbd "e") 'guix-package-list-edit)
+ (define-key map (kbd "x") 'guix-package-list-execute)
+ (define-key map (kbd "i") 'guix-package-list-mark-install)
+ (define-key map (kbd "d") 'guix-package-list-mark-delete)
+ (define-key map (kbd "U") 'guix-package-list-mark-upgrade)
+ (define-key map (kbd "^") 'guix-package-list-mark-upgrades))
+
+(defface guix-package-list-installed
+ '((t :inherit guix-package-info-installed-outputs))
+ "Face used if there are installed outputs for the current package."
+ :group 'guix-package-list-faces)
+
+(defface guix-package-list-obsolete
+ '((t :inherit guix-package-info-obsolete))
+ "Face used if a package is obsolete."
+ :group 'guix-package-list-faces)
+
+(defcustom guix-package-list-generation-marking-enabled nil
+ "If non-nil, allow putting marks in a list with 'generation packages'.
+
+By default this is disabled, because it may be confusing. For
+example, a package is installed in some generation, so a user can
+mark it for deletion in the list of packages from this
+generation, but the package may not be installed in the latest
+generation, so actually it cannot be deleted.
+
+If you managed to understand the explanation above or if you
+really know what you do or if you just don't care, you can set
+this variable to t. It should not do much harm anyway (most
+likely)."
+ :type 'boolean
+ :group 'guix-package-list)
+
+(defun guix-package-list-get-name (name entry)
+ "Return NAME of the package ENTRY.
+Colorize it with `guix-package-list-installed' or
+`guix-package-list-obsolete' if needed."
+ (guix-get-string name
+ (cond ((guix-entry-value entry 'obsolete)
+ 'guix-package-list-obsolete)
+ ((guix-entry-value entry 'installed)
+ 'guix-package-list-installed))))
+
+(defun guix-package-list-get-installed-outputs (installed &optional _)
+ "Return string with outputs from INSTALLED entries."
+ (guix-get-string
+ (mapcar (lambda (entry)
+ (guix-entry-value entry 'output))
+ installed)))
+
+(defun guix-package-list-marking-check ()
+ "Signal an error if marking is disabled for the current buffer."
+ (when (and (not guix-package-list-generation-marking-enabled)
+ (or (derived-mode-p 'guix-package-list-mode)
+ (derived-mode-p 'guix-output-list-mode))
+ (eq (guix-ui-current-search-type) 'generation))
+ (error "Action marks are disabled for lists of 'generation packages'")))
+
+(defun guix-package-list-mark-outputs (mark default
+ &optional prompt available)
+ "Mark the current package with MARK and move to the next line.
+If PROMPT is non-nil, use it to ask a user for outputs from
+AVAILABLE list, otherwise mark all DEFAULT outputs."
+ (let ((outputs (if prompt
+ (guix-completing-read-multiple
+ prompt available nil t)
+ default)))
+ (apply #'guix-list--mark mark t outputs)))
+
+(defun guix-package-list-mark-install (&optional arg)
+ "Mark the current package for installation and move to the next line.
+With ARG, prompt for the outputs to install (several outputs may
+be separated with \",\")."
+ (interactive "P")
+ (guix-package-list-marking-check)
+ (let* ((entry (guix-list-current-entry))
+ (all (guix-entry-value entry 'outputs))
+ (installed (guix-package-installed-outputs entry))
+ (available (cl-set-difference all installed :test #'string=)))
+ (or available
+ (user-error "This package is already installed"))
+ (guix-package-list-mark-outputs
+ 'install '("out")
+ (and arg "Output(s) to install: ")
+ available)))
+
+(defun guix-package-list-mark-delete (&optional arg)
+ "Mark the current package for deletion and move to the next line.
+With ARG, prompt for the outputs to delete (several outputs may
+be separated with \",\")."
+ (interactive "P")
+ (guix-package-list-marking-check)
+ (let* ((entry (guix-list-current-entry))
+ (installed (guix-package-installed-outputs entry)))
+ (or installed
+ (user-error "This package is not installed"))
+ (guix-package-list-mark-outputs
+ 'delete installed
+ (and arg "Output(s) to delete: ")
+ installed)))
+
+(defun guix-package-list-mark-upgrade (&optional arg)
+ "Mark the current package for upgrading and move to the next line.
+With ARG, prompt for the outputs to upgrade (several outputs may
+be separated with \",\")."
+ (interactive "P")
+ (guix-package-list-marking-check)
+ (let* ((entry (guix-list-current-entry))
+ (installed (guix-package-installed-outputs entry)))
+ (or installed
+ (user-error "This package is not installed"))
+ (when (or (guix-entry-value entry 'obsolete)
+ (y-or-n-p "This package is not obsolete. Try to upgrade it anyway? "))
+ (guix-package-list-mark-outputs
+ 'upgrade installed
+ (and arg "Output(s) to upgrade: ")
+ installed))))
+
+(defun guix-package-mark-upgrades (fun)
+ "Mark all obsolete packages for upgrading.
+Use FUN to perform marking of the current line. FUN should
+take an entry as argument."
+ (guix-package-list-marking-check)
+ (let ((obsolete (cl-remove-if-not
+ (lambda (entry)
+ (guix-entry-value entry 'obsolete))
+ (guix-buffer-current-entries))))
+ (guix-list-for-each-line
+ (lambda ()
+ (let* ((id (guix-list-current-id))
+ (entry (cl-find-if
+ (lambda (entry)
+ (equal id (guix-entry-id entry)))
+ obsolete)))
+ (when entry
+ (funcall fun entry)))))))
+
+(defun guix-package-list-mark-upgrades ()
+ "Mark all obsolete packages for upgrading."
+ (interactive)
+ (guix-package-mark-upgrades
+ (lambda (entry)
+ (apply #'guix-list--mark
+ 'upgrade nil
+ (guix-package-installed-outputs entry)))))
+
+(defun guix-package-execute-actions (fun)
+ "Perform actions on the marked packages.
+Use FUN to define actions suitable for `guix-process-package-actions'.
+FUN should take action-type as argument."
+ (let ((actions (delq nil
+ (mapcar fun '(install delete upgrade)))))
+ (if actions
+ (guix-process-package-actions (guix-ui-current-profile)
+ actions (current-buffer))
+ (user-error "No operations specified"))))
+
+(defun guix-package-list-execute ()
+ "Perform actions on the marked packages."
+ (interactive)
+ (guix-package-execute-actions #'guix-package-list-make-action))
+
+(defun guix-package-list-make-action (action-type)
+ "Return action specification for the packages marked with ACTION-TYPE.
+Return nil, if there are no packages marked with ACTION-TYPE.
+The specification is suitable for `guix-process-package-actions'."
+ (let ((specs (guix-list-get-marked-args action-type)))
+ (and specs (cons action-type specs))))
+
+(defun guix-package-list-edit ()
+ "Go to the location of the current package."
+ (interactive)
+ (guix-edit (guix-list-current-id)))
+
+(defun guix-package-list-latest-builds (number &rest args)
+ "Display latest NUMBER of Hydra builds of the current package.
+Interactively, prompt for NUMBER. With prefix argument, prompt
+for all ARGS."
+ (interactive
+ (let ((entry (guix-list-current-entry)))
+ (guix-hydra-build-latest-prompt-args
+ :job (guix-package-name-specification
+ (guix-entry-value entry 'name)
+ (guix-entry-value entry 'version)))))
+ (apply #'guix-hydra-latest-builds number args))
+
+
+;;; Output 'info'
+
+(guix-ui-info-define-interface output
+ :buffer-name "*Guix Package Info*"
+ :format '((name format (format guix-package-info-name))
+ (version format guix-output-info-insert-version)
+ (output format guix-output-info-insert-output)
+ (synopsis simple (indent guix-package-info-synopsis))
+ (source simple guix-package-info-insert-source)
+ (path simple (indent guix-file))
+ (dependencies simple (indent guix-file))
+ (location format (format guix-package-location))
+ (home-url format (format guix-url))
+ (license format (format guix-package-info-license))
+ (inputs format (format guix-package-input))
+ (native-inputs format (format guix-package-native-input))
+ (propagated-inputs format
+ (format guix-package-propagated-input))
+ (description simple (indent guix-package-info-description)))
+ :titles guix-package-info-titles
+ :required '(id package-id installed non-unique))
+
+(defun guix-output-info-insert-version (version entry)
+ "Insert output VERSION and obsolete text if needed at point."
+ (guix-info-insert-value-format version
+ 'guix-package-info-version)
+ (and (guix-entry-value entry 'obsolete)
+ (guix-package-info-insert-obsolete-text)))
+
+(defun guix-output-info-insert-output (output entry)
+ "Insert OUTPUT and action buttons at point."
+ (let* ((installed (guix-entry-value entry 'installed))
+ (obsolete (guix-entry-value entry 'obsolete))
+ (action-type (if installed 'delete 'install)))
+ (guix-info-insert-value-format
+ output
+ (if installed
+ 'guix-package-info-installed-outputs
+ 'guix-package-info-uninstalled-outputs))
+ (guix-info-insert-indent)
+ (guix-package-info-insert-action-button action-type entry output)
+ (when obsolete
+ (guix-info-insert-indent)
+ (guix-package-info-insert-action-button 'upgrade entry output))))
+
+
+;;; Output 'list'
+
+(guix-ui-list-define-interface output
+ :buffer-name "*Guix Package List*"
+ :describe-function 'guix-output-list-describe
+ :format '((name guix-package-list-get-name 20 t)
+ (version nil 10 nil)
+ (output nil 9 t)
+ (installed nil 12 t)
+ (synopsis guix-list-get-one-line 30 nil))
+ :required '(id package-id)
+ :sort-key '(name)
+ :marks '((install . ?I)
+ (upgrade . ?U)
+ (delete . ?D)))
+
+(let ((map guix-output-list-mode-map))
+ (define-key map (kbd "B") 'guix-package-list-latest-builds)
+ (define-key map (kbd "e") 'guix-output-list-edit)
+ (define-key map (kbd "x") 'guix-output-list-execute)
+ (define-key map (kbd "i") 'guix-output-list-mark-install)
+ (define-key map (kbd "d") 'guix-output-list-mark-delete)
+ (define-key map (kbd "U") 'guix-output-list-mark-upgrade)
+ (define-key map (kbd "^") 'guix-output-list-mark-upgrades))
+
+(defun guix-output-list-mark-install ()
+ "Mark the current output for installation and move to the next line."
+ (interactive)
+ (guix-package-list-marking-check)
+ (let* ((entry (guix-list-current-entry))
+ (installed (guix-entry-value entry 'installed)))
+ (if installed
+ (user-error "This output is already installed")
+ (guix-list--mark 'install t))))
+
+(defun guix-output-list-mark-delete ()
+ "Mark the current output for deletion and move to the next line."
+ (interactive)
+ (guix-package-list-marking-check)
+ (let* ((entry (guix-list-current-entry))
+ (installed (guix-entry-value entry 'installed)))
+ (if installed
+ (guix-list--mark 'delete t)
+ (user-error "This output is not installed"))))
+
+(defun guix-output-list-mark-upgrade ()
+ "Mark the current output for upgrading and move to the next line."
+ (interactive)
+ (guix-package-list-marking-check)
+ (let* ((entry (guix-list-current-entry))
+ (installed (guix-entry-value entry 'installed)))
+ (or installed
+ (user-error "This output is not installed"))
+ (when (or (guix-entry-value entry 'obsolete)
+ (y-or-n-p "This output is not obsolete. Try to upgrade it anyway? "))
+ (guix-list--mark 'upgrade t))))
+
+(defun guix-output-list-mark-upgrades ()
+ "Mark all obsolete package outputs for upgrading."
+ (interactive)
+ (guix-package-mark-upgrades
+ (lambda (_) (guix-list--mark 'upgrade))))
+
+(defun guix-output-list-execute ()
+ "Perform actions on the marked outputs."
+ (interactive)
+ (guix-package-execute-actions #'guix-output-list-make-action))
+
+(defun guix-output-list-make-action (action-type)
+ "Return action specification for the outputs marked with ACTION-TYPE.
+Return nil, if there are no outputs marked with ACTION-TYPE.
+The specification is suitable for `guix-process-output-actions'."
+ (let ((ids (guix-list-get-marked-id-list action-type)))
+ (and ids (cons action-type
+ (mapcar #'guix-package-id-and-output-by-output-id
+ ids)))))
+
+(defun guix-output-list-describe (ids)
+ "Describe outputs with IDS (list of output identifiers).
+See `guix-package-info-type'."
+ (if (eq guix-package-info-type 'output)
+ (guix-buffer-get-display-entries
+ 'info 'output
+ (cl-list* (guix-ui-current-profile) 'id ids)
+ 'add)
+ (let ((pids (mapcar (lambda (oid)
+ (car (guix-package-id-and-output-by-output-id
+ oid)))
+ ids)))
+ (guix-buffer-get-display-entries
+ 'info 'package
+ (cl-list* (guix-ui-current-profile)
+ 'id (cl-remove-duplicates pids))
+ 'add))))
+
+(defun guix-output-list-edit ()
+ "Go to the location of the current package."
+ (interactive)
+ (guix-edit (guix-entry-value (guix-list-current-entry)
+ 'package-id)))
+
+
+;;; Interactive commands
+
+(defvar guix-package-search-params '(name synopsis description)
+ "Default list of package parameters for searching by regexp.")
+
+(defvar guix-package-search-history nil
+ "A history of minibuffer prompts.")
+
+;;;###autoload
+(defun guix-search-by-name (name &optional profile)
+ "Search for Guix packages by NAME.
+NAME is a string with name specification. It may optionally contain
+a version number. Examples: \"guile\", \"guile-2.0.11\".
+
+If PROFILE is nil, use `guix-current-profile'.
+Interactively with prefix, prompt for PROFILE."
+ (interactive
+ (list (read-string "Package name: " nil 'guix-package-search-history)
+ (guix-ui-read-profile)))
+ (guix-package-get-display profile 'name name))
+
+;;;###autoload
+(defun guix-search-by-regexp (regexp &optional params profile)
+ "Search for Guix packages by REGEXP.
+PARAMS are package parameters that should be searched.
+If PARAMS are not specified, use `guix-package-search-params'.
+
+If PROFILE is nil, use `guix-current-profile'.
+Interactively with prefix, prompt for PROFILE."
+ (interactive
+ (list (read-regexp "Regexp: " nil 'guix-package-search-history)
+ nil (guix-ui-read-profile)))
+ (guix-package-get-display profile 'regexp regexp
+ (or params guix-package-search-params)))
+
+;;;###autoload
+(defun guix-installed-packages (&optional profile)
+ "Display information about installed Guix packages.
+If PROFILE is nil, use `guix-current-profile'.
+Interactively with prefix, prompt for PROFILE."
+ (interactive (list (guix-ui-read-profile)))
+ (guix-package-get-display profile 'installed))
+
+;;;###autoload
+(defun guix-obsolete-packages (&optional profile)
+ "Display information about obsolete Guix packages.
+If PROFILE is nil, use `guix-current-profile'.
+Interactively with prefix, prompt for PROFILE."
+ (interactive (list (guix-ui-read-profile)))
+ (guix-package-get-display profile 'obsolete))
+
+;;;###autoload
+(defun guix-all-available-packages (&optional profile)
+ "Display information about all available Guix packages.
+If PROFILE is nil, use `guix-current-profile'.
+Interactively with prefix, prompt for PROFILE."
+ (interactive (list (guix-ui-read-profile)))
+ (guix-package-get-display profile 'all-available))
+
+;;;###autoload
+(defun guix-newest-available-packages (&optional profile)
+ "Display information about the newest available Guix packages.
+If PROFILE is nil, use `guix-current-profile'.
+Interactively with prefix, prompt for PROFILE."
+ (interactive (list (guix-ui-read-profile)))
+ (guix-package-get-display profile 'newest-available))
+
+(provide 'guix-ui-package)
+
+;;; guix-ui-package.el ends here
diff --git a/emacs/guix-ui.el b/emacs/guix-ui.el
new file mode 100644
index 0000000000..7fef7c355c
--- /dev/null
+++ b/emacs/guix-ui.el
@@ -0,0 +1,333 @@
+;;; guix-ui.el --- Common code for Guix package management interface -*- lexical-binding: t -*-
+
+;; Copyright © 2014, 2015 Alex Kost <alezost@gmail.com>
+
+;; This file is part of GNU Guix.
+
+;; GNU Guix 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.
+
+;; GNU Guix 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.
+
+;; 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 file provides some general code for 'list'/'info' interfaces for
+;; packages and generations.
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'guix-backend)
+(require 'guix-buffer)
+(require 'guix-guile)
+(require 'guix-utils)
+(require 'guix-messages)
+
+(guix-define-groups ui
+ :group-doc "\
+Settings for 'ui' (Guix package management) buffers.
+This group includes settings for displaying packages, outputs and
+generations in 'list' and 'info' buffers.")
+
+(defvar guix-ui-map
+ (let ((map (make-sparse-keymap)))
+ (define-key map (kbd "M") 'guix-apply-manifest)
+ (define-key map (kbd "C-c C-z") 'guix-switch-to-repl)
+ map)
+ "Parent keymap for Guix package/generation buffers.")
+
+(guix-buffer-define-current-args-accessors
+ "guix-ui-current" "profile" "search-type" "search-values")
+
+(defun guix-ui-read-profile ()
+ "Return `guix-current-profile' or prompt for it.
+This function is intended for using in `interactive' forms."
+ (if current-prefix-arg
+ (guix-profile-prompt)
+ guix-current-profile))
+
+(defun guix-ui-get-entries (profile entry-type search-type search-values
+ &optional params)
+ "Receive ENTRY-TYPE entries for PROFILE.
+Call an appropriate scheme procedure and return a list of entries.
+
+ENTRY-TYPE should be one of the following symbols: `package',
+`output' or `generation'.
+
+SEARCH-TYPE may be one of the following symbols:
+
+- If ENTRY-TYPE is `package' or `output': `id', `name', `regexp',
+ `all-available', `newest-available', `installed', `obsolete',
+ `generation'.
+
+- If ENTRY-TYPE is `generation': `id', `last', `all', `time'.
+
+PARAMS is a list of parameters for receiving. If nil, get data
+with all available parameters."
+ (guix-eval-read
+ (guix-make-guile-expression
+ 'entries
+ profile params entry-type search-type search-values)))
+
+(defun guix-ui-list-describe (ids)
+ "Describe 'ui' entries with IDS (list of identifiers)."
+ (guix-buffer-get-display-entries
+ 'info (guix-buffer-current-entry-type)
+ (cl-list* (guix-ui-current-profile) 'id ids)
+ 'add))
+
+
+;;; Buffers and auto updating
+
+(defcustom guix-ui-update-after-operation 'current
+ "Define what kind of data to update after executing an operation.
+
+After successful executing an operation in the Guix REPL (for
+example after installing a package), the data in Guix buffers
+will or will not be automatically updated depending on a value of
+this variable.
+
+If nil, update nothing (do not revert any buffer).
+If `current', update the buffer from which an operation was performed.
+If `all', update all Guix buffers (not recommended)."
+ :type '(choice (const :tag "Do nothing" nil)
+ (const :tag "Update operation buffer" current)
+ (const :tag "Update all Guix buffers" all))
+ :group 'guix-ui)
+
+(defcustom guix-ui-buffer-name-function
+ #'guix-ui-buffer-name-default
+ "Function used to define a name of a Guix buffer.
+The function is called with 2 arguments: BASE-NAME and PROFILE."
+ :type '(choice (function-item guix-ui-buffer-name-default)
+ (function-item guix-ui-buffer-name-simple)
+ (function :tag "Other function"))
+ :group 'guix-ui)
+
+(defun guix-ui-buffer-name-simple (base-name &rest _)
+ "Return BASE-NAME."
+ base-name)
+
+;; TODO separate '*...*' logic from the real profile appending. Also add
+;; another function to return '*Guix ...: /full/path/to/profile*' name.
+(defun guix-ui-buffer-name-default (base-name profile)
+ "Return buffer name by appending BASE-NAME and PROFILE's base file name."
+ (let ((profile-name (file-name-base (directory-file-name profile)))
+ (re (rx string-start
+ (group (? "*"))
+ (group (*? any))
+ (group (? "*"))
+ string-end)))
+ (or (string-match re base-name)
+ (error "Unexpected error in defining guix buffer name"))
+ (let ((first* (match-string 1 base-name))
+ (name-body (match-string 2 base-name))
+ (last* (match-string 3 base-name)))
+ ;; Handle the case when buffer name is wrapped by '*'.
+ (if (and (string= "*" first*)
+ (string= "*" last*))
+ (concat "*" name-body ": " profile-name "*")
+ (concat base-name ": " profile-name)))))
+
+(defun guix-ui-buffer-name (base-name profile)
+ "Return Guix buffer name based on BASE-NAME and profile.
+See `guix-ui-buffer-name-function' for details."
+ (funcall guix-ui-buffer-name-function
+ base-name profile))
+
+(defun guix-ui-buffer? (&optional buffer modes)
+ "Return non-nil if BUFFER mode is derived from any of the MODES.
+If BUFFER is nil, check current buffer.
+If MODES is nil, use `guix-list-mode' and `guix-info-mode'."
+ (with-current-buffer (or buffer (current-buffer))
+ (apply #'derived-mode-p
+ (or modes '(guix-list-mode guix-info-mode)))))
+
+(defun guix-ui-buffers (&optional modes)
+ "Return a list of all buffers with major modes derived from MODES.
+If MODES is nil, return list of all Guix 'list' and 'info' buffers."
+ (cl-remove-if-not (lambda (buf)
+ (guix-ui-buffer? buf modes))
+ (buffer-list)))
+
+(defun guix-ui-update-buffer (buffer)
+ "Update data in a 'list' or 'info' BUFFER."
+ (with-current-buffer buffer
+ (guix-buffer-revert nil t)))
+
+(defun guix-ui-update-buffers-after-operation ()
+ "Update buffers after Guix operation if needed.
+See `guix-ui-update-after-operation' for details."
+ (let ((to-update
+ (and guix-operation-buffer
+ (cl-case guix-ui-update-after-operation
+ (current (and (buffer-live-p guix-operation-buffer)
+ (guix-ui-buffer? guix-operation-buffer)
+ (list guix-operation-buffer)))
+ (all (guix-ui-buffers))))))
+ (setq guix-operation-buffer nil)
+ (mapc #'guix-ui-update-buffer to-update)))
+
+(add-hook 'guix-after-repl-operation-hook
+ 'guix-ui-update-buffers-after-operation)
+
+
+;;; Interface definers
+
+(defmacro guix-ui-define-entry-type (entry-type &rest args)
+ "Define general code for ENTRY-TYPE.
+Remaining arguments (ARGS) should have a form [KEYWORD VALUE] ...
+
+The rest keyword arguments are passed to
+`guix-define-entry-type' macro."
+ (declare (indent 1))
+ `(guix-define-entry-type ,entry-type
+ :parent-group guix-ui
+ :parent-faces-group guix-ui-faces
+ ,@args))
+
+(defmacro guix-ui-define-interface (buffer-type entry-type &rest args)
+ "Define BUFFER-TYPE interface for displaying ENTRY-TYPE entries.
+Remaining arguments (ARGS) should have a form [KEYWORD VALUE] ...
+In the following description TYPE means ENTRY-TYPE-BUFFER-TYPE.
+
+Required keywords:
+
+ - `:buffer-name' - base part of a buffer name. It is used in a
+ generated `guix-TYPE-buffer-name' function; see
+ `guix-ui-buffer-name' for details.
+
+Optional keywords:
+
+ - `:required' - default value of the generated
+ `guix-TYPE-required-params' variable.
+
+The rest keyword arguments are passed to
+`guix-BUFFER-TYPE-define-interface' macro.
+
+Along with the mentioned definitions, this macro also defines:
+
+ - `guix-TYPE-mode-map' - keymap based on `guix-ui-map' and
+ `guix-BUFFER-TYPE-mode-map'.
+
+ - `guix-TYPE-get-entries' - a wrapper around `guix-ui-get-entries'.
+
+ - `guix-TYPE-message' - a wrapper around `guix-result-message'."
+ (declare (indent 2))
+ (let* ((entry-type-str (symbol-name entry-type))
+ (buffer-type-str (symbol-name buffer-type))
+ (prefix (concat "guix-" entry-type-str "-"
+ buffer-type-str))
+ (mode-str (concat prefix "-mode"))
+ (mode-map (intern (concat mode-str "-map")))
+ (parent-map (intern (format "guix-%s-mode-map"
+ buffer-type-str)))
+ (required-var (intern (concat prefix "-required-params")))
+ (buffer-name-fun (intern (concat prefix "-buffer-name")))
+ (get-fun (intern (concat prefix "-get-entries")))
+ (message-fun (intern (concat prefix "-message")))
+ (displayed-fun (intern (format "guix-%s-displayed-params"
+ buffer-type-str)))
+ (definer (intern (format "guix-%s-define-interface"
+ buffer-type-str))))
+ (guix-keyword-args-let args
+ ((buffer-name-val :buffer-name)
+ (required-val :required ''(id)))
+ `(progn
+ (defvar ,mode-map
+ (let ((map (make-sparse-keymap)))
+ (set-keymap-parent
+ map (make-composed-keymap ,parent-map guix-ui-map))
+ map)
+ ,(format "Keymap for `%s' buffers." mode-str))
+
+ (defvar ,required-var ,required-val
+ ,(format "\
+List of the required '%s' parameters.
+These parameters are received by `%S'
+along with the displayed parameters.
+
+Do not remove `id' from this list as it is required for
+identifying an entry."
+ entry-type-str get-fun))
+
+ (defun ,buffer-name-fun (profile &rest _)
+ ,(format "\
+Return a name of '%s' buffer for displaying '%s' entries.
+See `guix-ui-buffer-name' for details."
+ buffer-type-str entry-type-str)
+ (guix-ui-buffer-name ,buffer-name-val profile))
+
+ (defun ,get-fun (profile search-type &rest search-values)
+ ,(format "\
+Receive '%s' entries for displaying them in '%s' buffer.
+See `guix-ui-get-entries' for details."
+ entry-type-str buffer-type-str)
+ (guix-ui-get-entries
+ profile ',entry-type search-type search-values
+ (cl-union ,required-var
+ (,displayed-fun ',entry-type))))
+
+ (defun ,message-fun (entries profile search-type
+ &rest search-values)
+ ,(format "\
+Display a message after showing '%s' entries."
+ entry-type-str)
+ (guix-result-message
+ profile entries ',entry-type search-type search-values))
+
+ (,definer ,entry-type
+ :get-entries-function ',get-fun
+ :message-function ',message-fun
+ :buffer-name ',buffer-name-fun
+ ,@%foreign-args)))))
+
+(defmacro guix-ui-info-define-interface (entry-type &rest args)
+ "Define 'info' interface for displaying ENTRY-TYPE entries.
+See `guix-ui-define-interface'."
+ (declare (indent 1))
+ `(guix-ui-define-interface info ,entry-type
+ ,@args))
+
+(defmacro guix-ui-list-define-interface (entry-type &rest args)
+ "Define 'list' interface for displaying ENTRY-TYPE entries.
+Remaining arguments (ARGS) should have a form [KEYWORD VALUE] ...
+
+Optional keywords:
+
+ - `:describe-function' - default value of the generated
+ `guix-ENTRY-TYPE-list-describe-function' variable (if not
+ specified, use `guix-ui-list-describe').
+
+The rest keyword arguments are passed to
+`guix-ui-define-interface' macro."
+ (declare (indent 1))
+ (guix-keyword-args-let args
+ ((describe-val :describe-function))
+ `(guix-ui-define-interface list ,entry-type
+ :describe-function ,(or describe-val ''guix-ui-list-describe)
+ ,@args)))
+
+
+(defvar guix-ui-font-lock-keywords
+ (eval-when-compile
+ `((,(rx "(" (group (or "guix-ui-define-entry-type"
+ "guix-ui-define-interface"
+ "guix-ui-info-define-interface"
+ "guix-ui-list-define-interface"))
+ symbol-end)
+ . 1))))
+
+(font-lock-add-keywords 'emacs-lisp-mode guix-ui-font-lock-keywords)
+
+(provide 'guix-ui)
+
+;;; guix-ui.el ends here
diff --git a/emacs/guix-utils.el b/emacs/guix-utils.el
index 5f3f3ecc10..8c1a5b42de 100644
--- a/emacs/guix-utils.el
+++ b/emacs/guix-utils.el
@@ -64,6 +64,17 @@ Use `guix-time-format'."
"Return one-line string from a multi-line STR."
(replace-regexp-in-string "\n" " " str))
+(defmacro guix-with-indent (indent &rest body)
+ "Evaluate BODY and indent inserted text by INDENT number of spaces."
+ (declare (indent 1) (debug t))
+ (let ((region-beg-var (make-symbol "region-beg"))
+ (indent-var (make-symbol "indent")))
+ `(let ((,region-beg-var (point))
+ (,indent-var ,indent))
+ ,@body
+ (unless (zerop ,indent-var)
+ (indent-rigidly ,region-beg-var (point) ,indent-var)))))
+
(defun guix-format-insert (val &optional face format)
"Convert VAL into a string and insert it at point.
If FACE is non-nil, propertize VAL with FACE.
@@ -93,6 +104,28 @@ See `insert-text-button' for the meaning of PROPERTIES."
:type (or type 'button)
properties)))
+(defun guix-buttonize (value button-type separator &rest properties)
+ "Make BUTTON-TYPE button(s) from VALUE.
+Return a string with button(s).
+
+VALUE should be a string or a list of strings. If it is a list
+of strings, buttons are separated with SEPARATOR string.
+
+PROPERTIES are passed to `guix-insert-button'."
+ (with-temp-buffer
+ (let ((labels (if (listp value) value (list value))))
+ (guix-mapinsert (lambda (label)
+ (apply #'guix-insert-button
+ label button-type properties))
+ labels
+ separator))
+ (buffer-substring (point-min) (point-max))))
+
+(defun guix-button-type? (symbol)
+ "Return non-nil, if SYMBOL is a button type."
+ (and symbol
+ (get symbol 'button-category-symbol)))
+
(defun guix-split-insert (val &optional face col separator)
"Convert VAL into a string, split it and insert at point.
@@ -111,14 +144,11 @@ Separate inserted lines with SEPARATOR."
(defun guix-split-string (str &optional col)
"Split string STR by lines and return list of result strings.
-If COL is non-nil and STR is a one-line string longer than COL,
-split it into several short lines."
- (let ((strings (split-string str "\n *")))
- (if (and col
- (null (cdr strings)) ; if not multi-line
- (> (length str) col))
- (split-string (guix-get-filled-string str col) "\n")
- strings)))
+If COL is non-nil, fill STR to this column."
+ (let ((str (if col
+ (guix-get-filled-string str col)
+ str)))
+ (split-string str "\n *" t)))
(defun guix-get-filled-string (str col)
"Return string by filling STR to column COL."
@@ -144,6 +174,15 @@ add both to the end and to the beginning."
(t
(concat separator str separator)))))
+(defun guix-hexify (value)
+ "Convert VALUE to string and hexify it."
+ (url-hexify-string (guix-get-string value)))
+
+(defun guix-number->bool (number)
+ "Convert NUMBER to boolean value.
+Return nil, if NUMBER is 0; return t otherwise."
+ (not (zerop number)))
+
(defun guix-shell-quote-argument (argument)
"Quote shell command ARGUMENT.
This function is similar to `shell-quote-argument', but less strict."
@@ -154,6 +193,15 @@ This function is similar to `shell-quote-argument', but less strict."
(replace-regexp-in-string
(rx (not (any alnum "-=,./\n"))) "\\\\\\&" argument))))
+(defun guix-symbol-title (symbol)
+ "Return SYMBOL's name, a string.
+This is like `symbol-name', but fancier."
+ (if (eq symbol 'id)
+ "ID"
+ (let ((str (replace-regexp-in-string "-" " " (symbol-name symbol))))
+ (concat (capitalize (substring str 0 1))
+ (substring str 1)))))
+
(defun guix-command-symbol (&optional args)
"Return symbol by concatenating 'guix' and ARGS (strings)."
(intern (guix-concat-strings (cons "guix" args) "-")))
@@ -175,6 +223,15 @@ If NO-MESSAGE? is non-nil, do not display a message about it."
See also `guix-copy-as-kill'."
(guix-copy-as-kill (guix-command-string args) no-message?))
+(defun guix-completing-read (prompt table &optional predicate
+ require-match initial-input
+ hist def inherit-input-method)
+ "Same as `completing-read' but return nil instead of an empty string."
+ (let ((res (completing-read prompt table predicate
+ require-match initial-input
+ hist def inherit-input-method)))
+ (unless (string= "" res) res)))
+
(defun guix-completing-read-multiple (prompt table &optional predicate
require-match initial-input
hist def inherit-input-method)
@@ -193,6 +250,14 @@ Return time value."
(require 'org)
(org-read-date nil t nil prompt))
+(defun guix-read-file-name (prompt &optional dir default-filename
+ mustmatch initial predicate)
+ "Read file name.
+This function is similar to `read-file-name' except it also
+expands the file name."
+ (expand-file-name (read-file-name prompt dir default-filename
+ mustmatch initial predicate)))
+
(defcustom guix-find-file-function #'find-file
"Function used to find a file.
The function is called by `guix-find-file' with a file name as a
@@ -226,6 +291,15 @@ single argument."
(while (re-search-forward ,regexp nil t)
,@body)))
+(defmacro guix-while-null (&rest body)
+ "Evaluate BODY until its result becomes non-nil."
+ (declare (indent 0) (debug t))
+ (let ((result-var (make-symbol "result")))
+ `(let (,result-var)
+ (while (null ,result-var)
+ (setq ,result-var ,@body))
+ ,result-var)))
+
(defun guix-modify (object modifiers)
"Apply MODIFIERS to OBJECT.
OBJECT is passed as an argument to the first function from
@@ -237,8 +311,57 @@ modifier call."
(guix-modify (funcall (car modifiers) object)
(cdr modifiers))))
+(defmacro guix-keyword-args-let (args varlist &rest body)
+ "Parse ARGS, bind variables from VARLIST and eval BODY.
+
+Find keyword values in ARGS, bind them to variables according to
+VARLIST, then evaluate BODY.
+
+ARGS is a keyword/value property list.
+
+Each element of VARLIST has a form:
+
+ (SYMBOL KEYWORD [DEFAULT-VALUE])
+
+SYMBOL is a varible name. KEYWORD is a symbol that will be
+searched in ARGS for an according value. If the value of KEYWORD
+does not exist, bind SYMBOL to DEFAULT-VALUE or nil.
+
+The rest arguments (that present in ARGS but not in VARLIST) will
+be bound to `%foreign-args' variable.
+
+Example:
+
+ (guix-keyword-args-let '(:two 8 :great ! :guix is)
+ ((one :one 1)
+ (two :two 2)
+ (foo :smth))
+ (list one two foo %foreign-args))
+
+ => (1 8 nil (:guix is :great !))"
+ (declare (indent 2))
+ (let ((args-var (make-symbol "args")))
+ `(let (,@(mapcar (lambda (spec)
+ (pcase-let ((`(,name ,_ ,val) spec))
+ (list name val)))
+ varlist)
+ (,args-var ,args)
+ %foreign-args)
+ (while ,args-var
+ (pcase ,args-var
+ (`(,key ,val . ,rest-args)
+ (cl-case key
+ ,@(mapcar (lambda (spec)
+ (pcase-let ((`(,name ,key ,_) spec))
+ `(,key (setq ,name val))))
+ varlist)
+ (t (setq %foreign-args
+ (cl-list* key val %foreign-args))))
+ (setq ,args-var rest-args))))
+ ,@body)))
+
-;;; Alist accessors
+;;; Alist procedures
(defmacro guix-define-alist-accessor (name assoc-fun)
"Define NAME function to access alist values using ASSOC-FUN."
@@ -256,6 +379,48 @@ accessed with KEYS."
(guix-define-alist-accessor guix-assq-value assq)
(guix-define-alist-accessor guix-assoc-value assoc)
+(defun guix-alist-put (value alist &rest keys)
+ "Put (add or replace if exists) VALUE to ALIST using KEYS.
+Return the new alist.
+
+ALIST is alist of alists of alists ... which can be consecutively
+accessed with KEYS.
+
+Example:
+
+ (guix-alist-put
+ 'foo
+ '((one (a . 1) (b . 2))
+ (two (m . 7) (n . 8)))
+ 'one 'b)
+
+ => ((one (a . 1) (b . foo))
+ (two (m . 7) (n . 8)))"
+ (or keys (error "Keys should be specified"))
+ (guix-alist-put-1 value alist keys))
+
+(defun guix-alist-put-1 (value alist keys)
+ "Subroutine of `guix-alist-put'."
+ (cond
+ ((null keys)
+ value)
+ ((null alist)
+ (list (cons (car keys)
+ (guix-alist-put-1 value nil (cdr keys)))))
+ ((eq (car keys) (caar alist))
+ (cons (cons (car keys)
+ (guix-alist-put-1 value (cdar alist) (cdr keys)))
+ (cdr alist)))
+ (t
+ (cons (car alist)
+ (guix-alist-put-1 value (cdr alist) keys)))))
+
+(defun guix-alist-put! (value variable &rest keys)
+ "Modify alist VARIABLE (symbol) by putting VALUE using KEYS.
+See `guix-alist-put' for details."
+ (set variable
+ (apply #'guix-alist-put value (symbol-value variable) keys)))
+
;;; Diff
@@ -267,6 +432,77 @@ accessed with KEYS."
(diff old new (or switches guix-diff-switches) no-async))
+;;; Completing readers definers
+
+(defmacro guix-define-reader (name read-fun completions prompt)
+ "Define NAME function to read from minibuffer.
+READ-FUN may be `completing-read', `completing-read-multiple' or
+another function with the same arguments."
+ `(defun ,name (&optional prompt initial-contents)
+ (,read-fun ,(if prompt
+ `(or prompt ,prompt)
+ 'prompt)
+ ,completions nil nil initial-contents)))
+
+(defmacro guix-define-readers (&rest args)
+ "Define reader functions.
+
+ARGS should have a form [KEYWORD VALUE] ... The following
+keywords are available:
+
+ - `completions-var' - variable used to get completions.
+
+ - `completions-getter' - function used to get completions.
+
+ - `single-reader', `single-prompt' - name of a function to read
+ a single value, and a prompt for it.
+
+ - `multiple-reader', `multiple-prompt' - name of a function to
+ read multiple values, and a prompt for it.
+
+ - `multiple-separator' - if specified, another
+ `<multiple-reader-name>-string' function returning a string
+ of multiple values separated the specified separator will be
+ defined."
+ (guix-keyword-args-let args
+ ((completions-var :completions-var)
+ (completions-getter :completions-getter)
+ (single-reader :single-reader)
+ (single-prompt :single-prompt)
+ (multiple-reader :multiple-reader)
+ (multiple-prompt :multiple-prompt)
+ (multiple-separator :multiple-separator))
+ (let ((completions
+ (cond ((and completions-var completions-getter)
+ `(or ,completions-var
+ (setq ,completions-var
+ (funcall ',completions-getter))))
+ (completions-var
+ completions-var)
+ (completions-getter
+ `(funcall ',completions-getter)))))
+ `(progn
+ ,(when (and completions-var
+ (not (boundp completions-var)))
+ `(defvar ,completions-var nil))
+
+ ,(when single-reader
+ `(guix-define-reader ,single-reader guix-completing-read
+ ,completions ,single-prompt))
+
+ ,(when multiple-reader
+ `(guix-define-reader ,multiple-reader completing-read-multiple
+ ,completions ,multiple-prompt))
+
+ ,(when (and multiple-reader multiple-separator)
+ (let ((name (intern (concat (symbol-name multiple-reader)
+ "-string"))))
+ `(defun ,name (&optional prompt initial-contents)
+ (guix-concat-strings
+ (,multiple-reader prompt initial-contents)
+ ,multiple-separator))))))))
+
+
;;; Memoizing
(defun guix-memoize (function)
@@ -303,9 +539,18 @@ See `defun' for the meaning of arguments."
,(or docstring
(format "Memoized version of `%S'." definition))))
-(defvar guix-memoized-font-lock-keywords
+
+(defvar guix-utils-font-lock-keywords
(eval-when-compile
- `((,(rx "("
+ `((,(rx "(" (group (or "guix-define-reader"
+ "guix-define-readers"
+ "guix-keyword-args-let"
+ "guix-while-null"
+ "guix-while-search"
+ "guix-with-indent"))
+ symbol-end)
+ . 1)
+ (,(rx "("
(group "guix-memoized-" (or "defun" "defalias"))
symbol-end
(zero-or-more blank)
@@ -314,7 +559,7 @@ See `defun' for the meaning of arguments."
(1 font-lock-keyword-face)
(2 font-lock-function-name-face nil t)))))
-(font-lock-add-keywords 'emacs-lisp-mode guix-memoized-font-lock-keywords)
+(font-lock-add-keywords 'emacs-lisp-mode guix-utils-font-lock-keywords)
(provide 'guix-utils)
diff --git a/emacs/guix.el b/emacs/guix.el
deleted file mode 100644
index ac6efbb475..0000000000
--- a/emacs/guix.el
+++ /dev/null
@@ -1,213 +0,0 @@
-;;; guix.el --- Interface for GNU Guix package manager
-
-;; Copyright © 2014, 2015 Alex Kost <alezost@gmail.com>
-
-;; Package-Requires: ((geiser "0.3"))
-;; Keywords: tools
-
-;; This file is part of GNU Guix.
-
-;; GNU Guix 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.
-
-;; GNU Guix 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.
-
-;; 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 package provides an interface for searching, listing and getting
-;; information about Guix packages and generations; and for
-;; installing/upgrading/removing packages.
-
-;;; Code:
-
-(require 'guix-base)
-(require 'guix-list)
-(require 'guix-info)
-(require 'guix-utils)
-(require 'guix-read)
-
-(defgroup guix nil
- "Interface for Guix package manager."
- :prefix "guix-"
- :group 'external)
-
-(defgroup guix-faces nil
- "Guix faces."
- :group 'guix
- :group 'faces)
-
-(defcustom guix-list-single-package nil
- "If non-nil, list a package even if it is the only matching result.
-If nil, show a single package in the info buffer."
- :type 'boolean
- :group 'guix)
-
-(defvar guix-search-params '(name synopsis description)
- "Default list of package parameters for searching by regexp.")
-
-(defvar guix-search-history nil
- "A history of minibuffer prompts.")
-
-(defun guix-get-show-packages (profile search-type &rest search-vals)
- "Search for packages and show results.
-
-If PROFILE is nil, use `guix-current-profile'.
-
-See `guix-get-entries' for the meaning of SEARCH-TYPE and
-SEARCH-VALS.
-
-Results are displayed in the list buffer, unless a single package
-is found and `guix-list-single-package' is nil."
- (or profile (setq profile guix-current-profile))
- (let ((packages (guix-get-entries profile guix-package-list-type
- search-type search-vals
- (guix-get-params-for-receiving
- 'list guix-package-list-type))))
- (if (or guix-list-single-package
- (cdr packages))
- (guix-set-buffer profile packages 'list guix-package-list-type
- search-type search-vals)
- (let ((packages (guix-get-entries profile guix-package-info-type
- search-type search-vals
- (guix-get-params-for-receiving
- 'info guix-package-info-type))))
- (guix-set-buffer profile packages 'info guix-package-info-type
- search-type search-vals)))))
-
-(defun guix-get-show-generations (profile search-type &rest search-vals)
- "Search for generations and show results.
-
-If PROFILE is nil, use `guix-current-profile'.
-
-See `guix-get-entries' for the meaning of SEARCH-TYPE and
-SEARCH-VALS."
- (apply #'guix-get-show-entries
- (or profile guix-current-profile)
- 'list 'generation search-type search-vals))
-
-;;;###autoload
-(defun guix-search-by-name (name &optional profile)
- "Search for Guix packages by NAME.
-NAME is a string with name specification. It may optionally contain
-a version number. Examples: \"guile\", \"guile-2.0.11\".
-
-If PROFILE is nil, use `guix-current-profile'.
-Interactively with prefix, prompt for PROFILE."
- (interactive
- (list (read-string "Package name: " nil 'guix-search-history)
- (and current-prefix-arg
- (guix-profile-prompt))))
- (guix-get-show-packages profile 'name name))
-
-;;;###autoload
-(defun guix-search-by-regexp (regexp &optional params profile)
- "Search for Guix packages by REGEXP.
-PARAMS are package parameters that should be searched.
-If PARAMS are not specified, use `guix-search-params'.
-
-If PROFILE is nil, use `guix-current-profile'.
-Interactively with prefix, prompt for PROFILE."
- (interactive
- (list (read-regexp "Regexp: " nil 'guix-search-history)
- nil
- (and current-prefix-arg
- (guix-profile-prompt))))
- (guix-get-show-packages profile 'regexp regexp
- (or params guix-search-params)))
-
-;;;###autoload
-(defun guix-installed-packages (&optional profile)
- "Display information about installed Guix packages.
-If PROFILE is nil, use `guix-current-profile'.
-Interactively with prefix, prompt for PROFILE."
- (interactive
- (list (and current-prefix-arg
- (guix-profile-prompt))))
- (guix-get-show-packages profile 'installed))
-
-;;;###autoload
-(defun guix-obsolete-packages (&optional profile)
- "Display information about obsolete Guix packages.
-If PROFILE is nil, use `guix-current-profile'.
-Interactively with prefix, prompt for PROFILE."
- (interactive
- (list (and current-prefix-arg
- (guix-profile-prompt))))
- (guix-get-show-packages profile 'obsolete))
-
-;;;###autoload
-(defun guix-all-available-packages (&optional profile)
- "Display information about all available Guix packages.
-If PROFILE is nil, use `guix-current-profile'.
-Interactively with prefix, prompt for PROFILE."
- (interactive
- (list (and current-prefix-arg
- (guix-profile-prompt))))
- (guix-get-show-packages profile 'all-available))
-
-;;;###autoload
-(defun guix-newest-available-packages (&optional profile)
- "Display information about the newest available Guix packages.
-If PROFILE is nil, use `guix-current-profile'.
-Interactively with prefix, prompt for PROFILE."
- (interactive
- (list (and current-prefix-arg
- (guix-profile-prompt))))
- (guix-get-show-packages profile 'newest-available))
-
-;;;###autoload
-(defun guix-generations (&optional profile)
- "Display information about all generations.
-If PROFILE is nil, use `guix-current-profile'.
-Interactively with prefix, prompt for PROFILE."
- (interactive
- (list (and current-prefix-arg
- (guix-profile-prompt))))
- (guix-get-show-generations profile 'all))
-
-;;;###autoload
-(defun guix-last-generations (number &optional profile)
- "Display information about last NUMBER generations.
-If PROFILE is nil, use `guix-current-profile'.
-Interactively with prefix, prompt for PROFILE."
- (interactive
- (list (read-number "The number of last generations: ")
- (and current-prefix-arg
- (guix-profile-prompt))))
- (guix-get-show-generations profile 'last number))
-
-;;;###autoload
-(defun guix-generations-by-time (from to &optional profile)
- "Display information about generations created between FROM and TO.
-FROM and TO should be time values.
-If PROFILE is nil, use `guix-current-profile'.
-Interactively with prefix, prompt for PROFILE."
- (interactive
- (list (guix-read-date "Find generations (from): ")
- (guix-read-date "Find generations (to): ")
- (and current-prefix-arg
- (guix-profile-prompt))))
- (guix-get-show-generations profile 'time
- (float-time from)
- (float-time to)))
-
-;;;###autoload
-(defun guix-edit (id-or-name)
- "Edit (go to location of) package with ID-OR-NAME."
- (interactive (list (guix-read-package-name)))
- (let ((loc (guix-package-location id-or-name)))
- (if loc
- (guix-find-location loc)
- (message "Couldn't find package location."))))
-
-(provide 'guix)
-
-;;; guix.el ends here