emacs: Speed up starting the REPL.
[jackhill/guix/guix.git] / emacs / guix-ui-package.el
CommitLineData
c80ce104
AK
1;;; guix-ui-package.el --- Interface for displaying packages -*- lexical-binding: t -*-
2
3;; Copyright © 2014, 2015 Alex Kost <alezost@gmail.com>
4
5;; This file is part of GNU Guix.
6
7;; GNU Guix is free software; you can redistribute it and/or modify
8;; it under the terms of the GNU General Public License as published by
9;; the Free Software Foundation, either version 3 of the License, or
10;; (at your option) any later version.
11
12;; GNU Guix is distributed in the hope that it will be useful,
13;; but WITHOUT ANY WARRANTY; without even the implied warranty of
14;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15;; GNU General Public License for more details.
16
17;; You should have received a copy of the GNU General Public License
18;; along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20;;; Commentary:
21
22;; This file provides an interface for displaying packages and outputs
23;; in 'list' and 'info' buffers, and commands for working with them.
24
25;;; Code:
26
27(require 'cl-lib)
28(require 'guix-buffer)
29(require 'guix-list)
30(require 'guix-info)
31(require 'guix-ui)
32(require 'guix-base)
33(require 'guix-backend)
34(require 'guix-guile)
35(require 'guix-entry)
36(require 'guix-utils)
b8fa5a2a 37(require 'guix-hydra)
5c8994d9 38(require 'guix-hydra-build)
83aab70b 39(require 'guix-read)
cefb7aea 40(require 'guix-license)
e20f051e 41(require 'guix-profiles)
c80ce104 42
8ed2c92e
AK
43(guix-ui-define-entry-type package)
44(guix-ui-define-entry-type output)
c80ce104
AK
45
46(defcustom guix-package-list-type 'output
47 "Define how to display packages in 'list' buffer.
48Should be a symbol `package' or `output' (if `output', display each
49output on a separate line; if `package', display each package on
50a separate line)."
51 :type '(choice (const :tag "List of packages" package)
52 (const :tag "List of outputs" output))
53 :group 'guix-package)
54
55(defcustom guix-package-info-type 'package
56 "Define how to display packages in 'info' buffer.
57Should be a symbol `package' or `output' (if `output', display
58each output separately; if `package', display outputs inside
59package data)."
60 :type '(choice (const :tag "Display packages" package)
61 (const :tag "Display outputs" output))
62 :group 'guix-package)
63
c80ce104
AK
64(defun guix-package-get-display (profile search-type &rest search-values)
65 "Search for packages/outputs and show results.
66
67If PROFILE is nil, use `guix-current-profile'.
68
69See `guix-ui-get-entries' for the meaning of SEARCH-TYPE and
70SEARCH-VALUES.
71
72Results are displayed in the list buffer, unless a single package
73is found and `guix-package-list-single' is nil."
74 (let* ((args (cl-list* (or profile guix-current-profile)
75 search-type search-values))
76 (entries (guix-buffer-get-entries
77 'list guix-package-list-type args)))
78 (if (or guix-package-list-single
79 (null entries)
80 (cdr entries))
81 (guix-buffer-display-entries
82 entries 'list guix-package-list-type args 'add)
83 (guix-buffer-get-display-entries
84 'info guix-package-info-type args 'add))))
85
86(defun guix-package-entry->name-specification (entry &optional output)
87 "Return name specification of the package ENTRY and OUTPUT."
88 (guix-package-name-specification
89 (guix-entry-value entry 'name)
90 (guix-entry-value entry 'version)
91 (or output (guix-entry-value entry 'output))))
92
93(defun guix-package-entries->name-specifications (entries)
94 "Return name specifications by the package or output ENTRIES."
95 (cl-remove-duplicates (mapcar #'guix-package-entry->name-specification
96 entries)
97 :test #'string=))
98
99(defun guix-package-installed-outputs (entry)
100 "Return a list of installed outputs for the package ENTRY."
101 (mapcar (lambda (installed-entry)
102 (guix-entry-value installed-entry 'output))
103 (guix-entry-value entry 'installed)))
104
105(defun guix-package-id-and-output-by-output-id (output-id)
106 "Return a list (PACKAGE-ID OUTPUT) by OUTPUT-ID."
107 (cl-multiple-value-bind (package-id-str output)
108 (split-string output-id ":")
109 (let ((package-id (string-to-number package-id-str)))
110 (list (if (= 0 package-id) package-id-str package-id)
111 output))))
112
113\f
114;;; Processing package actions
115
116(defun guix-process-package-actions (profile actions
117 &optional operation-buffer)
118 "Process package ACTIONS on PROFILE.
119Each action is a list of the form:
120
121 (ACTION-TYPE PACKAGE-SPEC ...)
122
123ACTION-TYPE is one of the following symbols: `install',
124`upgrade', `remove'/`delete'.
125PACKAGE-SPEC should have the following form: (ID [OUTPUT] ...)."
126 (let (install upgrade remove)
127 (mapc (lambda (action)
128 (let ((action-type (car action))
129 (specs (cdr action)))
130 (cl-case action-type
131 (install (setq install (append install specs)))
132 (upgrade (setq upgrade (append upgrade specs)))
133 ((remove delete) (setq remove (append remove specs))))))
134 actions)
135 (when (guix-continue-package-operation-p
136 profile
137 :install install :upgrade upgrade :remove remove)
138 (guix-eval-in-repl
139 (guix-make-guile-expression
140 'process-package-actions profile
141 :install install :upgrade upgrade :remove remove
142 :use-substitutes? (or guix-use-substitutes 'f)
143 :dry-run? (or guix-dry-run 'f))
144 (and (not guix-dry-run) operation-buffer)))))
145
146(cl-defun guix-continue-package-operation-p (profile
147 &key install upgrade remove)
148 "Return non-nil if a package operation should be continued.
149Ask a user if needed (see `guix-operation-confirm').
150INSTALL, UPGRADE, REMOVE are 'package action specifications'.
151See `guix-process-package-actions' for details."
152 (or (null guix-operation-confirm)
153 (let* ((entries (guix-ui-get-entries
154 profile 'package 'id
155 (append (mapcar #'car install)
156 (mapcar #'car upgrade)
157 (mapcar #'car remove))
158 '(id name version location)))
159 (install-strings (guix-get-package-strings install entries))
160 (upgrade-strings (guix-get-package-strings upgrade entries))
161 (remove-strings (guix-get-package-strings remove entries)))
162 (if (or install-strings upgrade-strings remove-strings)
163 (let ((buf (get-buffer-create guix-temp-buffer-name)))
164 (with-current-buffer buf
165 (setq-local cursor-type nil)
166 (setq buffer-read-only nil)
167 (erase-buffer)
168 (insert "Profile: " profile "\n\n")
169 (guix-insert-package-strings install-strings "install")
170 (guix-insert-package-strings upgrade-strings "upgrade")
171 (guix-insert-package-strings remove-strings "remove")
172 (let ((win (temp-buffer-window-show
173 buf
174 '((display-buffer-reuse-window
175 display-buffer-at-bottom)
176 (window-height . fit-window-to-buffer)))))
177 (prog1 (guix-operation-prompt)
178 (quit-window nil win)))))
179 (message "Nothing to be done.
180If Guix REPL was restarted, the data is not up-to-date.")
181 nil))))
182
183(defun guix-get-package-strings (specs entries)
184 "Return short package descriptions for performing package actions.
185See `guix-process-package-actions' for the meaning of SPECS.
186ENTRIES is a list of package entries to get info about packages."
187 (delq nil
188 (mapcar
189 (lambda (spec)
190 (let* ((id (car spec))
191 (outputs (cdr spec))
192 (entry (guix-entry-by-id id entries)))
193 (when entry
194 (let ((location (guix-entry-value entry 'location)))
195 (concat (guix-package-entry->name-specification entry)
196 (when outputs
197 (concat ":"
198 (guix-concat-strings outputs ",")))
199 (when location
200 (concat "\t(" location ")")))))))
201 specs)))
202
203(defun guix-insert-package-strings (strings action)
204 "Insert information STRINGS at point for performing package ACTION."
205 (when strings
206 (insert "Package(s) to " (propertize action 'face 'bold) ":\n")
207 (mapc (lambda (str)
208 (insert " " str "\n"))
209 strings)
210 (insert "\n")))
211
212\f
213;;; Package 'info'
214
215(guix-ui-info-define-interface package
216 :buffer-name "*Guix Package Info*"
217 :format '(guix-package-info-insert-heading
218 ignore
219 (synopsis ignore (simple guix-package-info-synopsis))
220 ignore
221 (description ignore (simple guix-package-info-description))
222 ignore
223 (outputs simple guix-package-info-insert-outputs)
224 (source simple guix-package-info-insert-source)
225 (location format (format guix-package-location))
226 (home-url format (format guix-url))
cefb7aea 227 (license format (format guix-package-license))
0a5ec709 228 (systems format guix-package-info-insert-systems)
c80ce104
AK
229 (inputs format (format guix-package-input))
230 (native-inputs format (format guix-package-native-input))
231 (propagated-inputs format
232 (format guix-package-propagated-input)))
0a5ec709
AK
233 :titles '((home-url . "Home page")
234 (systems . "Supported systems"))
c80ce104
AK
235 :required '(id name version installed non-unique))
236
237(guix-info-define-interface installed-output
238 :format '((path simple (indent guix-file))
239 (dependencies simple (indent guix-file)))
240 :titles '((path . "Store directory"))
241 :reduced? t)
242
243(defface guix-package-info-heading
244 '((t :inherit guix-info-heading))
245 "Face for package name and version headings."
246 :group 'guix-package-info-faces)
247
248(defface guix-package-info-name
249 '((t :inherit font-lock-keyword-face))
250 "Face used for a name of a package."
251 :group 'guix-package-info-faces)
252
253(defface guix-package-info-name-button
254 '((t :inherit button))
255 "Face used for a full name that can be used to describe a package."
256 :group 'guix-package-info-faces)
257
258(defface guix-package-info-version
259 '((t :inherit font-lock-builtin-face))
260 "Face used for a version of a package."
261 :group 'guix-package-info-faces)
262
263(defface guix-package-info-synopsis
264 '((((type tty pc) (class color)) :weight bold)
265 (t :height 1.1 :weight bold :inherit variable-pitch))
266 "Face used for a synopsis of a package."
267 :group 'guix-package-info-faces)
268
269(defface guix-package-info-description
270 '((t))
271 "Face used for a description of a package."
272 :group 'guix-package-info-faces)
273
274(defface guix-package-info-license
275 '((t :inherit font-lock-string-face))
276 "Face used for a license of a package."
277 :group 'guix-package-info-faces)
278
279(defface guix-package-info-location
280 '((t :inherit link))
281 "Face used for a location of a package."
282 :group 'guix-package-info-faces)
283
284(defface guix-package-info-source
285 '((t :inherit link :underline nil))
286 "Face used for a source URL of a package."
287 :group 'guix-package-info-faces)
288
289(defface guix-package-info-installed-outputs
290 '((default :weight bold)
291 (((class color) (min-colors 88) (background light))
292 :foreground "ForestGreen")
293 (((class color) (min-colors 88) (background dark))
294 :foreground "PaleGreen")
295 (((class color) (min-colors 8))
296 :foreground "green")
297 (t :underline t))
298 "Face used for installed outputs of a package."
299 :group 'guix-package-info-faces)
300
301(defface guix-package-info-uninstalled-outputs
302 '((t :weight bold))
303 "Face used for uninstalled outputs of a package."
304 :group 'guix-package-info-faces)
305
306(defface guix-package-info-obsolete
307 '((t :inherit error))
308 "Face used if a package is obsolete."
309 :group 'guix-package-info-faces)
310
311(defcustom guix-package-info-auto-find-source nil
312 "If non-nil, find a source file after pressing a \"Show\" button.
313If nil, just display the source file path without finding."
314 :type 'boolean
315 :group 'guix-package-info)
316
317(defcustom guix-package-info-auto-download-source t
318 "If nil, do not automatically download a source file if it doesn't exist.
319After pressing a \"Show\" button, a derivation of the package
320source is calculated and a store file path is displayed. If this
321variable is non-nil and the source file does not exist in the
322store, it will be automatically downloaded (with a possible
323prompt depending on `guix-operation-confirm' variable)."
324 :type 'boolean
325 :group 'guix-package-info)
326
327(defvar guix-package-info-download-buffer nil
328 "Buffer from which a current download operation was performed.")
329
330(defvar guix-package-info-output-format "%-10s"
331 "String used to format output names of the packages.
332It should be a '%s'-sequence. After inserting an output name
333formatted with this string, an action button is inserted.")
334
335(defvar guix-package-info-obsolete-string "(This package is obsolete)"
336 "String used if a package is obsolete.")
337
338(define-button-type 'guix-package-location
339 :supertype 'guix
340 'face 'guix-package-info-location
341 'help-echo "Find location of this package"
342 'action (lambda (btn)
343 (guix-find-location (button-label btn))))
344
cefb7aea
AK
345(define-button-type 'guix-package-license
346 :supertype 'guix
347 'face 'guix-package-info-license
348 'help-echo "Browse license URL"
349 'action (lambda (btn)
350 (guix-browse-license-url (button-label btn))))
351
c80ce104
AK
352(define-button-type 'guix-package-name
353 :supertype 'guix
354 'face 'guix-package-info-name-button
355 'help-echo "Describe this package"
356 'action (lambda (btn)
357 (guix-buffer-get-display-entries-current
358 'info guix-package-info-type
359 (list (guix-ui-current-profile)
c292a6f3
AK
360 'name (or (button-get btn 'spec)
361 (button-label btn)))
c80ce104
AK
362 'add)))
363
6b3a1ce8
AK
364(define-button-type 'guix-package-heading
365 :supertype 'guix-package-name
366 'face 'guix-package-info-heading)
367
c80ce104
AK
368(define-button-type 'guix-package-source
369 :supertype 'guix
370 'face 'guix-package-info-source
371 'help-echo ""
372 'action (lambda (_)
373 ;; As a source may not be a real URL (e.g., "mirror://..."),
374 ;; no action is bound to a source button.
375 (message "Yes, this is the source URL. What did you expect?")))
376
377(defun guix-package-info-insert-heading (entry)
c292a6f3 378 "Insert package ENTRY heading (name and version) at point."
c80ce104 379 (guix-insert-button
c292a6f3
AK
380 (concat (guix-entry-value entry 'name) " "
381 (guix-entry-value entry 'version))
382 'guix-package-heading
383 'spec (guix-package-entry->name-specification entry)))
c80ce104 384
0a5ec709
AK
385(defun guix-package-info-insert-systems (systems entry)
386 "Insert supported package SYSTEMS at point."
387 (guix-info-insert-value-format
388 systems 'guix-hydra-build-system
389 'action (lambda (btn)
390 (let ((args (guix-hydra-build-latest-prompt-args
391 :job (button-get btn 'job-name)
392 :system (button-label btn))))
393 (apply #'guix-hydra-build-get-display
394 'latest args)))
b8fa5a2a 395 'job-name (guix-hydra-job-name-specification
0a5ec709
AK
396 (guix-entry-value entry 'name)
397 (guix-entry-value entry 'version))))
398
c80ce104
AK
399(defmacro guix-package-info-define-insert-inputs (&optional type)
400 "Define a face and a function for inserting package inputs.
401TYPE is a type of inputs.
402Function name is `guix-package-info-insert-TYPE-inputs'.
403Face name is `guix-package-info-TYPE-inputs'."
404 (let* ((type-str (symbol-name type))
405 (type-name (and type (concat type-str "-")))
406 (type-desc (and type (concat type-str " ")))
407 (face (intern (concat "guix-package-info-" type-name "inputs")))
408 (btn (intern (concat "guix-package-" type-name "input"))))
409 `(progn
410 (defface ,face
411 '((t :inherit guix-package-info-name-button))
412 ,(concat "Face used for " type-desc "inputs of a package.")
413 :group 'guix-package-info-faces)
414
415 (define-button-type ',btn
416 :supertype 'guix-package-name
417 'face ',face))))
418
419(guix-package-info-define-insert-inputs)
420(guix-package-info-define-insert-inputs native)
421(guix-package-info-define-insert-inputs propagated)
422
423(defun guix-package-info-insert-outputs (outputs entry)
424 "Insert OUTPUTS from package ENTRY at point."
425 (and (guix-entry-value entry 'obsolete)
426 (guix-package-info-insert-obsolete-text))
427 (and (guix-entry-value entry 'non-unique)
428 (guix-entry-value entry 'installed)
429 (guix-package-info-insert-non-unique-text
430 (guix-package-entry->name-specification entry)))
431 (insert "\n")
432 (dolist (output outputs)
433 (guix-package-info-insert-output output entry)))
434
435(defun guix-package-info-insert-obsolete-text ()
436 "Insert a message about obsolete package at point."
437 (guix-info-insert-indent)
438 (guix-format-insert guix-package-info-obsolete-string
439 'guix-package-info-obsolete))
440
441(defun guix-package-info-insert-non-unique-text (full-name)
442 "Insert a message about non-unique package with FULL-NAME at point."
443 (insert "\n")
444 (guix-info-insert-indent)
445 (insert "Installed outputs are displayed for a non-unique ")
446 (guix-insert-button full-name 'guix-package-name)
447 (insert " package."))
448
449(defun guix-package-info-insert-output (output entry)
450 "Insert OUTPUT at point.
451Make some fancy text with buttons and additional stuff if the
452current OUTPUT is installed (if there is such output in
453`installed' parameter of a package ENTRY)."
454 (let* ((installed (guix-entry-value entry 'installed))
455 (obsolete (guix-entry-value entry 'obsolete))
456 (installed-entry (cl-find-if
457 (lambda (entry)
458 (string= (guix-entry-value entry 'output)
459 output))
460 installed))
260795b7
AK
461 (action-type (if installed-entry 'delete 'install))
462 (profile (guix-ui-current-profile)))
c80ce104
AK
463 (guix-info-insert-indent)
464 (guix-format-insert output
465 (if installed-entry
466 'guix-package-info-installed-outputs
467 'guix-package-info-uninstalled-outputs)
468 guix-package-info-output-format)
260795b7
AK
469 ;; Do not allow a user to install/delete anything to/from a system
470 ;; profile, so add action buttons only for non-system profiles.
471 (when (and profile
472 (not (guix-system-profile? profile)))
473 (guix-package-info-insert-action-button action-type entry output)
474 (when obsolete
475 (guix-info-insert-indent)
476 (guix-package-info-insert-action-button 'upgrade entry output)))
c80ce104
AK
477 (insert "\n")
478 (when installed-entry
479 (guix-info-insert-entry installed-entry 'installed-output 2))))
480
481(defun guix-package-info-insert-action-button (type entry output)
482 "Insert button to process an action on a package OUTPUT at point.
483TYPE is one of the following symbols: `install', `delete', `upgrade'.
484ENTRY is an alist with package info."
485 (let ((type-str (capitalize (symbol-name type)))
486 (full-name (guix-package-entry->name-specification entry output)))
487 (guix-info-insert-action-button
488 type-str
489 (lambda (btn)
490 (guix-process-package-actions
491 (guix-ui-current-profile)
492 `((,(button-get btn 'action-type) (,(button-get btn 'id)
493 ,(button-get btn 'output))))
494 (current-buffer)))
495 (concat type-str " '" full-name "'")
496 'action-type type
497 'id (or (guix-entry-value entry 'package-id)
498 (guix-entry-id entry))
499 'output output)))
500
501(defun guix-package-info-show-source (entry-id package-id)
502 "Show file name of a package source in the current info buffer.
503Find the file if needed (see `guix-package-info-auto-find-source').
504ENTRY-ID is an ID of the current entry (package or output).
505PACKAGE-ID is an ID of the package which source to show."
506 (let* ((entries (guix-buffer-current-entries))
507 (entry (guix-entry-by-id entry-id entries))
508 (file (guix-package-source-path package-id)))
509 (or file
510 (error "Couldn't define file name of the package source"))
511 (let* ((new-entry (cons (cons 'source-file file)
512 entry))
513 (new-entries (guix-replace-entry entry-id new-entry entries)))
514 (setf (guix-buffer-item-entries guix-buffer-item)
515 new-entries)
516 (guix-buffer-redisplay-goto-button)
517 (if (file-exists-p file)
518 (if guix-package-info-auto-find-source
519 (guix-find-file file)
520 (message "The source store path is displayed."))
521 (if guix-package-info-auto-download-source
522 (guix-package-info-download-source package-id)
523 (message "The source does not exist in the store."))))))
524
525(defun guix-package-info-download-source (package-id)
526 "Download a source of the package PACKAGE-ID."
527 (setq guix-package-info-download-buffer (current-buffer))
528 (guix-package-source-build-derivation
529 package-id
530 "The source does not exist in the store. Download it?"))
531
532(defun guix-package-info-insert-source (source entry)
533 "Insert SOURCE from package ENTRY at point.
534SOURCE is a list of URLs."
535 (if (null source)
536 (guix-format-insert nil)
537 (let* ((source-file (guix-entry-value entry 'source-file))
538 (entry-id (guix-entry-id entry))
539 (package-id (or (guix-entry-value entry 'package-id)
540 entry-id)))
541 (if (null source-file)
542 (guix-info-insert-action-button
543 "Show"
544 (lambda (btn)
545 (guix-package-info-show-source (button-get btn 'entry-id)
546 (button-get btn 'package-id)))
547 "Show the source store directory of the current package"
548 'entry-id entry-id
549 'package-id package-id)
550 (unless (file-exists-p source-file)
551 (guix-info-insert-action-button
552 "Download"
553 (lambda (btn)
554 (guix-package-info-download-source
555 (button-get btn 'package-id)))
556 "Download the source into the store"
557 'package-id package-id))
558 (guix-info-insert-value-indent source-file 'guix-file))
559 (guix-info-insert-value-indent source 'guix-package-source))))
560
561(defun guix-package-info-redisplay-after-download ()
562 "Redisplay an 'info' buffer after downloading the package source.
563This function is used to hide a \"Download\" button if needed."
564 (when (buffer-live-p guix-package-info-download-buffer)
565 (with-current-buffer guix-package-info-download-buffer
566 (guix-buffer-redisplay-goto-button))
567 (setq guix-package-info-download-buffer nil)))
568
569(add-hook 'guix-after-source-download-hook
570 'guix-package-info-redisplay-after-download)
571
572\f
573;;; Package 'list'
574
575(guix-ui-list-define-interface package
576 :buffer-name "*Guix Package List*"
577 :format '((name guix-package-list-get-name 20 t)
578 (version nil 10 nil)
579 (outputs nil 13 t)
580 (installed guix-package-list-get-installed-outputs 13 t)
581 (synopsis guix-list-get-one-line 30 nil))
582 :sort-key '(name)
583 :marks '((install . ?I)
584 (upgrade . ?U)
585 (delete . ?D)))
586
587(let ((map guix-package-list-mode-map))
5c8994d9 588 (define-key map (kbd "B") 'guix-package-list-latest-builds)
c80ce104
AK
589 (define-key map (kbd "e") 'guix-package-list-edit)
590 (define-key map (kbd "x") 'guix-package-list-execute)
591 (define-key map (kbd "i") 'guix-package-list-mark-install)
592 (define-key map (kbd "d") 'guix-package-list-mark-delete)
593 (define-key map (kbd "U") 'guix-package-list-mark-upgrade)
594 (define-key map (kbd "^") 'guix-package-list-mark-upgrades))
595
596(defface guix-package-list-installed
597 '((t :inherit guix-package-info-installed-outputs))
598 "Face used if there are installed outputs for the current package."
599 :group 'guix-package-list-faces)
600
601(defface guix-package-list-obsolete
602 '((t :inherit guix-package-info-obsolete))
603 "Face used if a package is obsolete."
604 :group 'guix-package-list-faces)
605
606(defcustom guix-package-list-generation-marking-enabled nil
607 "If non-nil, allow putting marks in a list with 'generation packages'.
608
609By default this is disabled, because it may be confusing. For
610example, a package is installed in some generation, so a user can
611mark it for deletion in the list of packages from this
612generation, but the package may not be installed in the latest
613generation, so actually it cannot be deleted.
614
615If you managed to understand the explanation above or if you
616really know what you do or if you just don't care, you can set
617this variable to t. It should not do much harm anyway (most
618likely)."
619 :type 'boolean
620 :group 'guix-package-list)
621
622(defun guix-package-list-get-name (name entry)
623 "Return NAME of the package ENTRY.
624Colorize it with `guix-package-list-installed' or
625`guix-package-list-obsolete' if needed."
626 (guix-get-string name
627 (cond ((guix-entry-value entry 'obsolete)
628 'guix-package-list-obsolete)
629 ((guix-entry-value entry 'installed)
630 'guix-package-list-installed))))
631
632(defun guix-package-list-get-installed-outputs (installed &optional _)
633 "Return string with outputs from INSTALLED entries."
634 (guix-get-string
635 (mapcar (lambda (entry)
636 (guix-entry-value entry 'output))
637 installed)))
638
639(defun guix-package-list-marking-check ()
640 "Signal an error if marking is disabled for the current buffer."
641 (when (and (not guix-package-list-generation-marking-enabled)
642 (or (derived-mode-p 'guix-package-list-mode)
643 (derived-mode-p 'guix-output-list-mode))
644 (eq (guix-ui-current-search-type) 'generation))
645 (error "Action marks are disabled for lists of 'generation packages'")))
646
647(defun guix-package-list-mark-outputs (mark default
648 &optional prompt available)
649 "Mark the current package with MARK and move to the next line.
650If PROMPT is non-nil, use it to ask a user for outputs from
651AVAILABLE list, otherwise mark all DEFAULT outputs."
652 (let ((outputs (if prompt
653 (guix-completing-read-multiple
654 prompt available nil t)
655 default)))
656 (apply #'guix-list--mark mark t outputs)))
657
658(defun guix-package-list-mark-install (&optional arg)
659 "Mark the current package for installation and move to the next line.
660With ARG, prompt for the outputs to install (several outputs may
661be separated with \",\")."
662 (interactive "P")
663 (guix-package-list-marking-check)
664 (let* ((entry (guix-list-current-entry))
665 (all (guix-entry-value entry 'outputs))
666 (installed (guix-package-installed-outputs entry))
667 (available (cl-set-difference all installed :test #'string=)))
668 (or available
669 (user-error "This package is already installed"))
670 (guix-package-list-mark-outputs
671 'install '("out")
672 (and arg "Output(s) to install: ")
673 available)))
674
675(defun guix-package-list-mark-delete (&optional arg)
676 "Mark the current package for deletion and move to the next line.
677With ARG, prompt for the outputs to delete (several outputs may
678be separated with \",\")."
679 (interactive "P")
680 (guix-package-list-marking-check)
681 (let* ((entry (guix-list-current-entry))
682 (installed (guix-package-installed-outputs entry)))
683 (or installed
684 (user-error "This package is not installed"))
685 (guix-package-list-mark-outputs
686 'delete installed
687 (and arg "Output(s) to delete: ")
688 installed)))
689
690(defun guix-package-list-mark-upgrade (&optional arg)
691 "Mark the current package for upgrading and move to the next line.
692With ARG, prompt for the outputs to upgrade (several outputs may
693be separated with \",\")."
694 (interactive "P")
695 (guix-package-list-marking-check)
696 (let* ((entry (guix-list-current-entry))
697 (installed (guix-package-installed-outputs entry)))
698 (or installed
699 (user-error "This package is not installed"))
700 (when (or (guix-entry-value entry 'obsolete)
701 (y-or-n-p "This package is not obsolete. Try to upgrade it anyway? "))
702 (guix-package-list-mark-outputs
703 'upgrade installed
704 (and arg "Output(s) to upgrade: ")
705 installed))))
706
707(defun guix-package-mark-upgrades (fun)
708 "Mark all obsolete packages for upgrading.
709Use FUN to perform marking of the current line. FUN should
710take an entry as argument."
711 (guix-package-list-marking-check)
712 (let ((obsolete (cl-remove-if-not
713 (lambda (entry)
714 (guix-entry-value entry 'obsolete))
715 (guix-buffer-current-entries))))
716 (guix-list-for-each-line
717 (lambda ()
718 (let* ((id (guix-list-current-id))
719 (entry (cl-find-if
720 (lambda (entry)
721 (equal id (guix-entry-id entry)))
722 obsolete)))
723 (when entry
724 (funcall fun entry)))))))
725
726(defun guix-package-list-mark-upgrades ()
727 "Mark all obsolete packages for upgrading."
728 (interactive)
729 (guix-package-mark-upgrades
730 (lambda (entry)
731 (apply #'guix-list--mark
732 'upgrade nil
733 (guix-package-installed-outputs entry)))))
734
260795b7
AK
735(defun guix-package-assert-non-system-profile ()
736 "Verify that the current profile is not a system one.
737The current profile is the one used by the current buffer."
738 (let ((profile (guix-ui-current-profile)))
739 (and profile
740 (guix-system-profile? profile)
741 (user-error "Packages cannot be installed or removed to/from \
742profile '%s'.
743Use 'guix system reconfigure' shell command to modify a system profile."
744 profile))))
745
c80ce104
AK
746(defun guix-package-execute-actions (fun)
747 "Perform actions on the marked packages.
748Use FUN to define actions suitable for `guix-process-package-actions'.
749FUN should take action-type as argument."
260795b7 750 (guix-package-assert-non-system-profile)
c80ce104
AK
751 (let ((actions (delq nil
752 (mapcar fun '(install delete upgrade)))))
753 (if actions
754 (guix-process-package-actions (guix-ui-current-profile)
755 actions (current-buffer))
756 (user-error "No operations specified"))))
757
758(defun guix-package-list-execute ()
759 "Perform actions on the marked packages."
760 (interactive)
761 (guix-package-execute-actions #'guix-package-list-make-action))
762
763(defun guix-package-list-make-action (action-type)
764 "Return action specification for the packages marked with ACTION-TYPE.
765Return nil, if there are no packages marked with ACTION-TYPE.
766The specification is suitable for `guix-process-package-actions'."
767 (let ((specs (guix-list-get-marked-args action-type)))
768 (and specs (cons action-type specs))))
769
2c04e2ee
AK
770(defun guix-package-list-edit (&optional directory)
771 "Go to the location of the current package.
772See `guix-find-location' for the meaning of DIRECTORY."
773 (interactive (list (guix-read-directory)))
774 (guix-edit (guix-list-current-id) directory))
c80ce104 775
5c8994d9
AK
776(defun guix-package-list-latest-builds (number &rest args)
777 "Display latest NUMBER of Hydra builds of the current package.
778Interactively, prompt for NUMBER. With prefix argument, prompt
779for all ARGS."
780 (interactive
781 (let ((entry (guix-list-current-entry)))
782 (guix-hydra-build-latest-prompt-args
b8fa5a2a 783 :job (guix-hydra-job-name-specification
5c8994d9
AK
784 (guix-entry-value entry 'name)
785 (guix-entry-value entry 'version)))))
786 (apply #'guix-hydra-latest-builds number args))
787
c80ce104
AK
788\f
789;;; Output 'info'
790
791(guix-ui-info-define-interface output
792 :buffer-name "*Guix Package Info*"
793 :format '((name format (format guix-package-info-name))
794 (version format guix-output-info-insert-version)
795 (output format guix-output-info-insert-output)
796 (synopsis simple (indent guix-package-info-synopsis))
797 (source simple guix-package-info-insert-source)
798 (path simple (indent guix-file))
799 (dependencies simple (indent guix-file))
800 (location format (format guix-package-location))
801 (home-url format (format guix-url))
cefb7aea 802 (license format (format guix-package-license))
0a5ec709 803 (systems format guix-package-info-insert-systems)
c80ce104
AK
804 (inputs format (format guix-package-input))
805 (native-inputs format (format guix-package-native-input))
806 (propagated-inputs format
807 (format guix-package-propagated-input))
808 (description simple (indent guix-package-info-description)))
809 :titles guix-package-info-titles
810 :required '(id package-id installed non-unique))
811
812(defun guix-output-info-insert-version (version entry)
813 "Insert output VERSION and obsolete text if needed at point."
814 (guix-info-insert-value-format version
815 'guix-package-info-version)
816 (and (guix-entry-value entry 'obsolete)
817 (guix-package-info-insert-obsolete-text)))
818
819(defun guix-output-info-insert-output (output entry)
820 "Insert OUTPUT and action buttons at point."
821 (let* ((installed (guix-entry-value entry 'installed))
822 (obsolete (guix-entry-value entry 'obsolete))
823 (action-type (if installed 'delete 'install)))
824 (guix-info-insert-value-format
825 output
826 (if installed
827 'guix-package-info-installed-outputs
828 'guix-package-info-uninstalled-outputs))
829 (guix-info-insert-indent)
830 (guix-package-info-insert-action-button action-type entry output)
831 (when obsolete
832 (guix-info-insert-indent)
833 (guix-package-info-insert-action-button 'upgrade entry output))))
834
835\f
836;;; Output 'list'
837
838(guix-ui-list-define-interface output
839 :buffer-name "*Guix Package List*"
840 :describe-function 'guix-output-list-describe
841 :format '((name guix-package-list-get-name 20 t)
842 (version nil 10 nil)
843 (output nil 9 t)
844 (installed nil 12 t)
845 (synopsis guix-list-get-one-line 30 nil))
846 :required '(id package-id)
847 :sort-key '(name)
848 :marks '((install . ?I)
849 (upgrade . ?U)
850 (delete . ?D)))
851
852(let ((map guix-output-list-mode-map))
5c8994d9 853 (define-key map (kbd "B") 'guix-package-list-latest-builds)
c80ce104
AK
854 (define-key map (kbd "e") 'guix-output-list-edit)
855 (define-key map (kbd "x") 'guix-output-list-execute)
856 (define-key map (kbd "i") 'guix-output-list-mark-install)
857 (define-key map (kbd "d") 'guix-output-list-mark-delete)
858 (define-key map (kbd "U") 'guix-output-list-mark-upgrade)
859 (define-key map (kbd "^") 'guix-output-list-mark-upgrades))
860
861(defun guix-output-list-mark-install ()
862 "Mark the current output for installation and move to the next line."
863 (interactive)
864 (guix-package-list-marking-check)
865 (let* ((entry (guix-list-current-entry))
866 (installed (guix-entry-value entry 'installed)))
867 (if installed
868 (user-error "This output is already installed")
869 (guix-list--mark 'install t))))
870
871(defun guix-output-list-mark-delete ()
872 "Mark the current output for deletion and move to the next line."
873 (interactive)
874 (guix-package-list-marking-check)
875 (let* ((entry (guix-list-current-entry))
876 (installed (guix-entry-value entry 'installed)))
877 (if installed
878 (guix-list--mark 'delete t)
879 (user-error "This output is not installed"))))
880
881(defun guix-output-list-mark-upgrade ()
882 "Mark the current output for upgrading and move to the next line."
883 (interactive)
884 (guix-package-list-marking-check)
885 (let* ((entry (guix-list-current-entry))
886 (installed (guix-entry-value entry 'installed)))
887 (or installed
888 (user-error "This output is not installed"))
889 (when (or (guix-entry-value entry 'obsolete)
890 (y-or-n-p "This output is not obsolete. Try to upgrade it anyway? "))
891 (guix-list--mark 'upgrade t))))
892
893(defun guix-output-list-mark-upgrades ()
894 "Mark all obsolete package outputs for upgrading."
895 (interactive)
896 (guix-package-mark-upgrades
897 (lambda (_) (guix-list--mark 'upgrade))))
898
899(defun guix-output-list-execute ()
900 "Perform actions on the marked outputs."
901 (interactive)
902 (guix-package-execute-actions #'guix-output-list-make-action))
903
904(defun guix-output-list-make-action (action-type)
905 "Return action specification for the outputs marked with ACTION-TYPE.
906Return nil, if there are no outputs marked with ACTION-TYPE.
907The specification is suitable for `guix-process-output-actions'."
908 (let ((ids (guix-list-get-marked-id-list action-type)))
909 (and ids (cons action-type
910 (mapcar #'guix-package-id-and-output-by-output-id
911 ids)))))
912
913(defun guix-output-list-describe (ids)
914 "Describe outputs with IDS (list of output identifiers).
915See `guix-package-info-type'."
916 (if (eq guix-package-info-type 'output)
917 (guix-buffer-get-display-entries
918 'info 'output
919 (cl-list* (guix-ui-current-profile) 'id ids)
920 'add)
921 (let ((pids (mapcar (lambda (oid)
922 (car (guix-package-id-and-output-by-output-id
923 oid)))
924 ids)))
925 (guix-buffer-get-display-entries
926 'info 'package
927 (cl-list* (guix-ui-current-profile)
928 'id (cl-remove-duplicates pids))
929 'add))))
930
2c04e2ee
AK
931(defun guix-output-list-edit (&optional directory)
932 "Go to the location of the current package.
933See `guix-find-location' for the meaning of DIRECTORY."
934 (interactive (list (guix-read-directory)))
c80ce104 935 (guix-edit (guix-entry-value (guix-list-current-entry)
2c04e2ee
AK
936 'package-id)
937 directory))
c80ce104
AK
938
939\f
940;;; Interactive commands
941
942(defvar guix-package-search-params '(name synopsis description)
943 "Default list of package parameters for searching by regexp.")
944
945(defvar guix-package-search-history nil
946 "A history of minibuffer prompts.")
947
948;;;###autoload
e119ba90
AK
949(defun guix-packages-by-name (name &optional profile)
950 "Display Guix packages with NAME.
c80ce104 951NAME is a string with name specification. It may optionally contain
db0c709b 952a version number. Examples: \"guile\", \"guile@2.0.11\".
c80ce104
AK
953
954If PROFILE is nil, use `guix-current-profile'.
955Interactively with prefix, prompt for PROFILE."
956 (interactive
e119ba90 957 (list (guix-read-package-name)
494a62f2 958 (guix-ui-read-profile)))
c80ce104
AK
959 (guix-package-get-display profile 'name name))
960
83aab70b
AK
961;;;###autoload
962(defun guix-packages-by-license (license &optional profile)
963 "Display Guix packages with LICENSE.
964LICENSE is a license name string.
965If PROFILE is nil, use `guix-current-profile'.
966Interactively with prefix, prompt for PROFILE."
967 (interactive
968 (list (guix-read-license-name)
969 (guix-ui-read-profile)))
970 (guix-package-get-display profile 'license license))
971
c80ce104
AK
972;;;###autoload
973(defun guix-search-by-regexp (regexp &optional params profile)
974 "Search for Guix packages by REGEXP.
975PARAMS are package parameters that should be searched.
976If PARAMS are not specified, use `guix-package-search-params'.
977
978If PROFILE is nil, use `guix-current-profile'.
979Interactively with prefix, prompt for PROFILE."
980 (interactive
981 (list (read-regexp "Regexp: " nil 'guix-package-search-history)
494a62f2 982 nil (guix-ui-read-profile)))
c80ce104
AK
983 (guix-package-get-display profile 'regexp regexp
984 (or params guix-package-search-params)))
985
27a2e483
AK
986;;;###autoload
987(defun guix-search-by-name (regexp &optional profile)
988 "Search for Guix packages matching REGEXP in a package name.
989If PROFILE is nil, use `guix-current-profile'.
990Interactively with prefix, prompt for PROFILE."
991 (interactive
992 (list (read-string "Package name by regexp: "
993 nil 'guix-package-search-history)
994 (guix-ui-read-profile)))
995 (guix-search-by-regexp regexp '(name) profile))
996
c80ce104
AK
997;;;###autoload
998(defun guix-installed-packages (&optional profile)
999 "Display information about installed Guix packages.
1000If PROFILE is nil, use `guix-current-profile'.
1001Interactively with prefix, prompt for PROFILE."
494a62f2 1002 (interactive (list (guix-ui-read-profile)))
c80ce104
AK
1003 (guix-package-get-display profile 'installed))
1004
1005;;;###autoload
cfb1c62a
AK
1006(defun guix-installed-user-packages ()
1007 "Display information about Guix packages installed in a user profile."
1008 (interactive)
1009 (guix-installed-packages guix-user-profile))
1010
1011;;;###autoload
1012(defun guix-installed-system-packages ()
1013 "Display information about Guix packages installed in a system profile."
1014 (interactive)
1015 (guix-installed-packages
1016 (guix-packages-profile guix-system-profile nil t)))
1017
1018;;;###autoload
c80ce104
AK
1019(defun guix-obsolete-packages (&optional profile)
1020 "Display information about obsolete Guix packages.
1021If PROFILE is nil, use `guix-current-profile'.
1022Interactively with prefix, prompt for PROFILE."
494a62f2 1023 (interactive (list (guix-ui-read-profile)))
c80ce104
AK
1024 (guix-package-get-display profile 'obsolete))
1025
1026;;;###autoload
1027(defun guix-all-available-packages (&optional profile)
1028 "Display information about all available Guix packages.
1029If PROFILE is nil, use `guix-current-profile'.
1030Interactively with prefix, prompt for PROFILE."
494a62f2 1031 (interactive (list (guix-ui-read-profile)))
c80ce104
AK
1032 (guix-package-get-display profile 'all-available))
1033
1034;;;###autoload
1035(defun guix-newest-available-packages (&optional profile)
1036 "Display information about the newest available Guix packages.
1037If PROFILE is nil, use `guix-current-profile'.
1038Interactively with prefix, prompt for PROFILE."
494a62f2 1039 (interactive (list (guix-ui-read-profile)))
c80ce104
AK
1040 (guix-package-get-display profile 'newest-available))
1041
1042(provide 'guix-ui-package)
1043
1044;;; guix-ui-package.el ends here