Merge branch 'gtk-im-modules'
[jackhill/guix/guix.git] / emacs / guix-list.el
dissimilarity index 76%
index 3342175..c91c67c 100644 (file)
-;;; guix-list.el --- List buffers for displaying entries   -*- lexical-binding: t -*-
-
-;; Copyright © 2014 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 list-like buffer for displaying information
-;; about Guix packages and generations.
-
-;;; Code:
-
-(require 'cl-lib)
-(require 'tabulated-list)
-(require 'guix-info)
-(require 'guix-history)
-(require 'guix-base)
-(require 'guix-utils)
-
-(defgroup guix-list nil
-  "General settings for list buffers."
-  :prefix "guix-list-"
-  :group 'guix)
-
-(defface guix-list-file-path
-  '((t :inherit guix-info-file-path))
-  "Face used for file paths."
-  :group 'guix-list)
-
-(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)
-     (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
-     (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:
-
-  (ENTRY-TYPE . ((PARAM . FUN) ...))
-
-PARAM is the name of an entry parameter of ENTRY-TYPE.
-
-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-get-param-title (entry-type param)
-  "Return title of an ENTRY-TYPE entry parameter PARAM."
-  (or (guix-get-key-val guix-list-column-titles
-                        entry-type param)
-      (guix-get-param-title entry-type param)))
-
-(defun guix-list-get-column-format (entry-type)
-  "Return column format for ENTRY-TYPE."
-  (guix-get-key-val guix-list-column-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-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-sort-numerically (column a b)
-  "Compare COLUMN of tabulated entries A and B numerically.
-It is a sort predicate 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)
-  "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').
-
-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))))
-
-(defun guix-list-get-list-format (entry-type)
-  "Return ENTRY-TYPE list specification for `tabulated-list-format'."
-  (guix-list-make-tabulated-vector
-   entry-type
-   (lambda (param spec)
-     (cons (guix-list-get-param-title entry-type param)
-           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'."
-  (mapcar (lambda (entry)
-            (list (guix-get-key-val entry 'id)
-                  (guix-list-get-tabulated-entry entry entry-type)))
-          entries))
-
-(defun guix-list-get-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
-   entry-type
-   (lambda (param _)
-     (let ((val (guix-get-key-val entry param))
-           (fun (guix-get-key-val guix-list-column-value-methods
-                                  entry-type param)))
-       (if (and val fun)
-           (funcall fun val entry)
-         (guix-get-string val))))))
-
-(defun guix-list-get-one-line (str &optional _)
-  "Return one-line string from a multi-line STR."
-  (guix-get-one-line str))
-
-(defun guix-list-get-time (seconds &optional _)
-  "Return formatted time string from SECONDS."
-  (guix-get-time-string seconds))
-
-(defun guix-list-get-file-path (path &optional _)
-  "Return PATH button specification for `tabulated-list-entries'."
-  (list path
-        'face 'guix-list-file-path
-        'action (lambda (btn) (find-file (button-label btn)))
-        'follow-link t
-        'help-echo "Find file"))
-
-(defun guix-list-current-id ()
-  "Return ID of the current entry."
-  (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-for-each-line (fun &rest args)
-  "Call FUN with ARGS for each entry line."
-  (or (derived-mode-p 'guix-list-mode)
-      (error "The current buffer is not in Guix List mode"))
-  (save-excursion
-    (goto-char (point-min))
-    (while (not (eobp))
-      (apply fun args)
-      (forward-line))))
-
-(defun guix-list-fold-lines (fun init)
-  "Fold over entry lines in the current list buffer.
-Call FUN with RESULT as argument for each line, using INIT as
-the initial value of RESULT.  Return the final result."
-  (let ((res init))
-    (guix-list-for-each-line
-     (lambda () (setq res (funcall fun res))))
-    res))
-
-\f
-;;; Marking and sorting
-
-(defvar-local guix-list-marked nil
-  "List of the marked entries.
-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'.
-ARGS is a list of additional values.")
-
-(defvar guix-list-mark-alist
-  '((empty   . ?\s)
-    (general . ?*))
-  "Alist of available mark names and mark characters.")
-
-(defsubst guix-list-get-mark (name)
-  "Return mark character by its NAME."
-  (or (guix-get-key-val guix-list-mark-alist name)
-      (error "Mark '%S' not found" name)))
-
-(defsubst guix-list-get-mark-string (name)
-  "Return mark string by its NAME."
-  (string (guix-list-get-mark name)))
-
-(defun guix-list-current-mark ()
-  "Return mark character of the current line."
-  (char-after (line-beginning-position)))
-
-(defun guix-list-get-marked (&rest mark-names)
-  "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."
-  (or mark-names
-      (setq mark-names
-            (delq 'empty
-                  (mapcar #'car guix-list-mark-alist))))
-  (cl-remove-if-not (lambda (assoc)
-                      (memq (cadr assoc) mark-names))
-                    guix-list-marked))
-
-(defun guix-list-get-marked-args (mark-name)
-  "Return list of (ID . ARGS) elements from lines marked with MARK-NAME.
-See `guix-list-marked' for the meaning of ARGS."
-  (mapcar (lambda (spec)
-            (let ((id (car spec))
-                  (args (cddr spec)))
-              (cons id args)))
-          (guix-list-get-marked mark-name)))
-
-(defun guix-list-get-marked-id-list (&rest mark-names)
-  "Return list of IDs of entries marked with any mark from MARK-NAMES.
-See `guix-list-get-marked' for details."
-  (mapcar #'car (apply #'guix-list-get-marked mark-names)))
-
-(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'.
-If ADVANCE is non-nil, move forward by one line after marking.
-Interactively, put a general mark and move to the next line."
-  (interactive '(general t))
-  (let ((id (guix-list-current-id)))
-    (if (eq mark-name 'empty)
-        (setq guix-list-marked (assq-delete-all id guix-list-marked))
-      (let ((assoc (assq id guix-list-marked))
-            (val (cons mark-name args)))
-        (if assoc
-            (setcdr assoc val)
-          (push (cons id val) guix-list-marked)))))
-  (tabulated-list-put-tag (guix-list-get-mark-string mark-name)
-                          advance))
-
-(defun guix-list-mark-all (mark-name)
-  "Mark all lines with MARK-NAME mark.
-MARK-NAME is a symbol from `guix-list-mark-alist'.
-Interactively, put a general mark on all lines."
-  (interactive '(general))
-  (guix-list-for-each-line #'guix-list-mark mark-name))
-
-(defun guix-list-unmark (&optional arg)
-  "Unmark the current line and move to the next line.
-With ARG, unmark all lines."
-  (interactive "P")
-  (if arg
-      (guix-list-unmark-all)
-    (guix-list-mark 'empty t)))
-
-(defun guix-list-unmark-backward ()
-  "Move up one line and unmark it."
-  (interactive)
-  (forward-line -1)
-  (guix-list-mark 'empty))
-
-(defun guix-list-unmark-all ()
-  "Unmark all lines."
-  (interactive)
-  (guix-list-mark-all 'empty))
-
-(defun guix-list-restore-marks ()
-  "Put marks according to `guix-list-mark-alist'."
-  (guix-list-for-each-line
-   (lambda ()
-     (let ((mark-name (car (guix-get-key-val guix-list-marked
-                                             (guix-list-current-id)))))
-       (tabulated-list-put-tag
-        (guix-list-get-mark-string (or mark-name 'empty)))))))
-
-(defun guix-list-sort (&optional n)
-  "Sort guix list entries by the column at point.
-With a numeric prefix argument N, sort the Nth column.
-Same as `tabulated-list-sort', but also restore marks after sorting."
-  (interactive "P")
-  (tabulated-list-sort n)
-  (guix-list-restore-marks))
-
-\f
-(defvar guix-list-mode-map
-  (let ((map (make-sparse-keymap)))
-    (set-keymap-parent map tabulated-list-mode-map)
-    (define-key map (kbd "RET") 'guix-list-describe)
-    (define-key map (kbd "m")   'guix-list-mark)
-    (define-key map (kbd "*")   'guix-list-mark)
-    (define-key map (kbd "M")   'guix-list-mark-all)
-    (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.")
-
-(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 '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)))
-
-\f
-;;; 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)
-
-(defface guix-package-list-obsolete
-  '((t :inherit guix-package-info-obsolete))
-  "Face used if a package is obsolete."
-  :group 'guix-package-list)
-
-(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-package-list)
-
-(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 "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-get-key-val entry 'obsolete)
-                          'guix-package-list-obsolete)
-                         ((guix-get-key-val 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-get-key-val 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-get-key-val 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-get-key-val 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-get-key-val 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-get-key-val 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
-        (apply #'guix-process-package-actions actions)
-      (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))
-
-(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))))
-
-\f
-;;; Displaying outputs
-
-(guix-define-buffer-type list output
-  :buffer-name "*Guix Package List*")
-
-(guix-list-define-entry-type output
-  :sort-key name
-  :marks ((install . ?I)
-          (upgrade . ?U)
-          (delete  . ?D)))
-
-(defcustom guix-output-list-describe-type 'package
-  "Define how to describe outputs in a list buffer.
-May be a symbol `package' or `output' (if `output', describe only
-marked outputs; if `package', describe all outputs of the marked
-packages)."
-  :type '(choice (const :tag "Describe packages" package)
-                 (const :tag "Describe outputs" output))
-  :group 'guix-output-list)
-
-(let ((map guix-output-list-mode-map))
-  (define-key map (kbd "RET") 'guix-output-list-describe)
-  (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-get-key-val 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-get-key-val 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-get-key-val entry 'installed)))
-    (or installed
-        (user-error "This output is not installed"))
-    (when (or (guix-get-key-val 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-output-list-describe-type'."
-  (interactive "P")
-  (if (eq guix-output-list-describe-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)))))
-
-\f
-;;; 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 "i")   'guix-list-describe)
-  (define-key map (kbd "d")   'guix-generation-list-mark-delete-simple))
-
-(defun guix-generation-list-show-packages ()
-  "List installed packages for the generation at point."
-  (interactive)
-  (guix-get-show-entries 'list guix-package-list-type 'generation
-                         (guix-list-current-id)))
-
-(provide 'guix-list)
-
-;;; guix-list.el ends here
+;;; guix-list.el --- 'List' 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 'list' buffer interface for displaying an arbitrary
+;; data.
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'tabulated-list)
+(require 'guix-buffer)
+(require 'guix-info)
+(require 'guix-entry)
+(require 'guix-utils)
+
+(guix-define-buffer-type list)
+
+(defface guix-list-file-name
+  '((t :inherit guix-info-file-name))
+  "Face used for file names."
+  :group 'guix-list-faces)
+
+(defface guix-list-url
+  '((t :inherit guix-info-url))
+  "Face used for URLs."
+  :group 'guix-list-faces)
+
+(defface guix-list-time
+  '((t :inherit guix-info-time))
+  "Face used for time stamps."
+  :group 'guix-list-faces)
+
+(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))))
+
+\f
+;;; Wrappers for 'list' variables
+
+(defvar guix-list-data nil
+  "Alist with 'list' data.
+This alist is filled by `guix-list-define-interface' macro.")
+
+(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-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-format (entry-type)
+  "Return column format for 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-sort-key (entry-type)
+  "Return sort key for ENTRY-TYPE."
+  (guix-list-value entry-type 'sort-key))
+
+(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))
+
+\f
+;;; Tabulated list internals
+
+(defun guix-list-sort-numerically (column a b)
+  "Compare COLUMN of tabulated entries A and B numerically.
+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))))
+
+(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 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)
+                   (apply fun col-spec))
+                 (guix-list-format entry-type))))
+
+(defun guix-list-tabulated-format (entry-type)
+  "Return ENTRY-TYPE list specification for `tabulated-list-format'."
+  (guix-list-tabulated-vector
+   entry-type
+   (lambda (param _ &rest rest-spec)
+     (cons (guix-list-param-title entry-type param)
+           rest-spec))))
+
+(defun guix-list-tabulated-entries (entries entry-type)
+  "Return a list of ENTRY-TYPE values for `tabulated-list-entries'."
+  (mapcar (lambda (entry)
+            (list (guix-entry-id entry)
+                  (guix-list-tabulated-entry entry entry-type)))
+          entries))
+
+(defun guix-list-tabulated-entry (entry entry-type)
+  "Return array of values for `tabulated-list-entries'.
+Parameters are taken from ENTRY-TYPE ENTRY."
+  (guix-list-tabulated-vector
+   entry-type
+   (lambda (param fun &rest _)
+     (let ((val (guix-entry-value entry param)))
+       (if fun
+           (funcall fun val entry)
+         (guix-get-string val))))))
+
+\f
+;;; 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."
+  (if val
+      (guix-get-one-line val)
+    (guix-get-string nil)))
+
+(defun guix-list-get-time (seconds &optional _)
+  "Return formatted time string from SECONDS."
+  (guix-get-string (guix-get-time-string seconds)
+                   'guix-list-time))
+
+(defun guix-list-get-file-name (file-name &optional _)
+  "Return FILE-NAME button specification for `tabulated-list-entries'."
+  (list file-name
+        'face 'guix-list-file-name
+        'action (lambda (btn) (find-file (button-label btn)))
+        'follow-link t
+        'help-echo "Find file"))
+
+(defun guix-list-get-url (url &optional _)
+  "Return URL button specification for `tabulated-list-entries'."
+  (list url
+        'face 'guix-list-url
+        'action (lambda (btn) (browse-url (button-label btn)))
+        'follow-link t
+        'help-echo "Browse URL"))
+
+\f
+;;; 'List' lines
+
+(defun guix-list-current-id ()
+  "Return ID of the entry at point."
+  (or (tabulated-list-get-id)
+      (user-error "No entry here")))
+
+(defun guix-list-current-entry ()
+  "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."
+  (or (derived-mode-p 'guix-list-mode)
+      (error "The current buffer is not in Guix List mode"))
+  (save-excursion
+    (goto-char (point-min))
+    (while (not (eobp))
+      (apply fun args)
+      (forward-line))))
+
+(defun guix-list-fold-lines (fun init)
+  "Fold over entry lines in the current list buffer.
+Call FUN with RESULT as argument for each line, using INIT as
+the initial value of RESULT.  Return the final result."
+  (let ((res init))
+    (guix-list-for-each-line
+     (lambda () (setq res (funcall fun res))))
+    res))
+
+\f
+;;; Marking and sorting
+
+(defvar-local guix-list-marked nil
+  "List of the marked entries.
+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-marks'.
+ARGS is a list of additional values.")
+
+(defvar-local guix-list-marks nil
+  "Alist of available mark names and mark characters.")
+
+(defvar guix-list-default-marks
+  '((empty   . ?\s)
+    (general . ?*))
+  "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)))
+
+(defun guix-list-get-mark (name)
+  "Return mark character by its NAME."
+  (or (guix-assq-value guix-list-marks name)
+      (error "Mark '%S' not found" name)))
+
+(defun guix-list-get-mark-string (name)
+  "Return mark string by its NAME."
+  (string (guix-list-get-mark name)))
+
+(defun guix-list-current-mark ()
+  "Return mark character of the current line."
+  (char-after (line-beginning-position)))
+
+(defun guix-list-get-marked (&rest mark-names)
+  "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-marks' except the `empty' one."
+  (or mark-names
+      (setq mark-names
+            (delq 'empty
+                  (mapcar #'car guix-list-marks))))
+  (cl-remove-if-not (lambda (assoc)
+                      (memq (cadr assoc) mark-names))
+                    guix-list-marked))
+
+(defun guix-list-get-marked-args (mark-name)
+  "Return list of (ID . ARGS) elements from lines marked with MARK-NAME.
+See `guix-list-marked' for the meaning of ARGS."
+  (mapcar (lambda (spec)
+            (let ((id (car spec))
+                  (args (cddr spec)))
+              (cons id args)))
+          (guix-list-get-marked mark-name)))
+
+(defun guix-list-get-marked-id-list (&rest mark-names)
+  "Return list of IDs of entries marked with any mark from MARK-NAMES.
+See `guix-list-get-marked' for details."
+  (mapcar #'car (apply #'guix-list-get-marked mark-names)))
+
+(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-marks'.
+If ADVANCE is non-nil, move forward by one line after marking."
+  (let ((id (guix-list-current-id)))
+    (if (eq mark-name 'empty)
+        (setq guix-list-marked (assq-delete-all id guix-list-marked))
+      (let ((assoc (assq id guix-list-marked))
+            (val (cons mark-name args)))
+        (if assoc
+            (setcdr assoc val)
+          (push (cons id val) guix-list-marked)))))
+  (tabulated-list-put-tag (guix-list-get-mark-string mark-name)
+                          advance))
+
+(defun guix-list-mark (&optional arg)
+  "Mark the current line and move to the next line.
+With ARG, mark all lines."
+  (interactive "P")
+  (if arg
+      (guix-list-mark-all)
+    (guix-list--mark 'general t)))
+
+(defun guix-list-mark-all (&optional mark-name)
+  "Mark all lines with MARK-NAME mark.
+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))
+  (guix-list-for-each-line #'guix-list--mark mark-name))
+
+(defun guix-list-unmark (&optional arg)
+  "Unmark the current line and move to the next line.
+With ARG, unmark all lines."
+  (interactive "P")
+  (if arg
+      (guix-list-unmark-all)
+    (guix-list--mark 'empty t)))
+
+(defun guix-list-unmark-backward ()
+  "Move up one line and unmark it."
+  (interactive)
+  (forward-line -1)
+  (guix-list--mark 'empty))
+
+(defun guix-list-unmark-all ()
+  "Unmark all lines."
+  (interactive)
+  (guix-list-mark-all 'empty))
+
+(defun guix-list-restore-marks ()
+  "Put marks according to `guix-list-marked'."
+  (guix-list-for-each-line
+   (lambda ()
+     (let ((mark-name (car (guix-assq-value guix-list-marked
+                                            (guix-list-current-id)))))
+       (tabulated-list-put-tag
+        (guix-list-get-mark-string (or mark-name 'empty)))))))
+
+(defun guix-list-sort (&optional n)
+  "Sort guix list entries by the column at point.
+With a numeric prefix argument N, sort the Nth column.
+Same as `tabulated-list-sort', but also restore marks after sorting."
+  (interactive "P")
+  (tabulated-list-sort n)
+  (guix-list-restore-marks))
+
+\f
+;;; Major mode and interface definer
+
+(defvar guix-list-mode-map
+  (let ((map (make-sparse-keymap)))
+    (set-keymap-parent
+     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)
+  "Keymap for `guix-list-mode' buffers.")
+
+(define-derived-mode guix-list-mode tabulated-list-mode "Guix-List"
+  "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:
+
+  (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))))))))
+
+\f
+(defvar guix-list-font-lock-keywords
+  (eval-when-compile
+    `((,(rx "(" (group "guix-list-define-interface")
+            symbol-end)
+       . 1))))
+
+(font-lock-add-keywords 'emacs-lisp-mode guix-list-font-lock-keywords)
+
+(provide 'guix-list)
+
+;;; guix-list.el ends here