summary refs log tree commit diff
path: root/emacs/guix-list.el
diff options
context:
space:
mode:
Diffstat (limited to 'emacs/guix-list.el')
-rw-r--r--emacs/guix-list.el586
1 files changed, 586 insertions, 0 deletions
diff --git a/emacs/guix-list.el b/emacs/guix-list.el
new file mode 100644
index 0000000000..0460d20f1b
--- /dev/null
+++ b/emacs/guix-list.el
@@ -0,0 +1,586 @@
+;;; 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))
+    (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))
+    (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))
+
+
+;;; 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 ()
+  "Unmark the current line and move to the next line."
+  (interactive)
+  (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))
+
+
+(defvar guix-list-mode-map
+  (let ((map (make-sparse-keymap)))
+    (set-keymap-parent map tabulated-list-mode-map)
+    (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 "U")   'guix-list-unmark-all)
+    (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-describe' - display marked entries in info buffer.
+
+  - `guix-ENTRY-TYPE-mark-MARK-NAME' functions for each mark
+    specified in `:marks' argument."
+  (let* ((entry-type-str (symbol-name entry-type))
+         (entry-str      (concat entry-type-str " entries"))
+         (prefix         (concat "guix-" entry-type-str "-list"))
+         (mode-str       (concat prefix "-mode"))
+         (init-fun       (intern (concat prefix "-mode-initialize")))
+         (describe-fun   (intern (concat prefix "-describe")))
+         (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 ,describe-fun (&optional arg)
+         ,(concat "Describe " entry-str " marked with a general mark.\n"
+                  "If no entry is marked, describe the current " entry-type-str ".\n"
+                  "With prefix (if ARG is non-nil), describe the " entry-str "\n"
+                  "marked with any mark.")
+         (interactive "P")
+         (let* ((ids (or (apply #'guix-list-get-marked-id-list
+                                (unless arg '(general)))
+                         (list (guix-list-current-id))))
+                (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)))
+             (,(intern (concat "guix-" entry-type-str "-info-get-show"))
+              'id ids))))
+
+       (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)
+
+
+;;; 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-obsolete
+  '((t :inherit guix-package-info-obsolete))
+  "Face used if a package is obsolete."
+  :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 "RET") 'guix-package-list-describe)
+  (define-key map (kbd "x")   'guix-package-list-execute)
+  (define-key map (kbd "i")   'guix-package-list-mark-install)
+  (define-key map (kbd "^")   'guix-package-list-mark-upgrade)
+  (define-key map (kbd "d")   'guix-package-list-mark-delete))
+
+(defun guix-package-list-get-name (name entry)
+  "Return NAME of the package ENTRY.
+Colorize it with `guix-package-list-obsolete' if needed."
+  (guix-get-string name
+                   (when (guix-get-key-val entry 'obsolete)
+                     'guix-package-list-obsolete)))
+
+(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)
+             (derived-mode-p 'guix-package-list-mode)
+             (eq guix-search-type 'generation))
+    (error "Action marks are disabled for lists of 'generation packages'")))
+
+(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))
+         (available (guix-get-key-val entry 'outputs))
+         (installed (guix-get-installed-outputs entry))
+         (to-install (if arg
+                         (guix-completing-read-multiple
+                          "Output(s) to install: " available nil t)
+                       '("out")))
+         (to-install (cl-set-difference to-install installed
+                                        :test #'string=)))
+    (if to-install
+        (apply #'guix-list-mark 'install t to-install)
+      (user-error "This package is already installed"))))
+
+(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"))
+    (let ((to-delete (when arg
+                       (guix-completing-read-multiple
+                        "Output(s) to delete: " installed nil t))))
+      (if to-delete
+          (apply #'guix-list-mark 'delete t to-delete)
+        (guix-package-list-mark-delete-simple)))))
+
+(defun guix-package-list-mark-upgrade ()
+  "Mark the current package for upgrading and move to the next line."
+  (interactive)
+  (guix-package-list-marking-check)
+  (let ((entry (guix-list-current-entry)))
+    (or (guix-get-installed-outputs entry)
+        (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-upgrade-simple))))
+
+(defun guix-package-list-execute ()
+  "Perform actions on the marked packages."
+  (interactive)
+  (let ((actions (delq nil
+                       (mapcar #'guix-package-list-make-action
+                               '(install delete upgrade)))))
+    (if actions
+        (apply #'guix-process-package-actions actions)
+      (user-error "No operations specified"))))
+
+(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))))
+
+
+;;; 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-generation-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-package-list-get-show 'generation (guix-list-current-id)))
+
+(provide 'guix-list)
+
+;;; guix-list.el ends here