emacs: Rewrite scheme side in a functional manner.
[jackhill/guix/guix.git] / emacs / guix-base.el
1 ;;; guix-base.el --- Common definitions -*- lexical-binding: t -*-
2
3 ;; Copyright © 2014 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 some base and common definitions for guix.el
23 ;; package.
24
25 ;; List and info buffers have many common patterns that are defined
26 ;; using `guix-define-buffer-type' macro from this file.
27
28 ;;; Code:
29
30 (require 'cl-lib)
31 (require 'guix-backend)
32 (require 'guix-utils)
33
34 \f
35 ;;; Profiles
36
37 (defvar guix-user-profile
38 (expand-file-name "~/.guix-profile")
39 "User profile.")
40
41 (defvar guix-default-profile
42 (concat (or (getenv "NIX_STATE_DIR") "/var/guix")
43 "/profiles/per-user/"
44 (getenv "USER")
45 "/guix-profile")
46 "Default Guix profile.")
47
48 (defvar guix-current-profile guix-default-profile
49 "Current profile.")
50
51 (defun guix-set-current-profile (path)
52 "Set `guix-current-profile' to PATH.
53 Interactively, prompt for PATH. With prefix, use
54 `guix-default-profile'."
55 (interactive
56 (list (if current-prefix-arg
57 guix-default-profile
58 (read-file-name "Set profile: "
59 (file-name-directory guix-current-profile)))))
60 (let ((path (directory-file-name (expand-file-name path))))
61 (setq guix-current-profile
62 (if (string= path guix-user-profile)
63 guix-default-profile
64 path))
65 (message "Current profile has been set to '%s'."
66 guix-current-profile)))
67
68 \f
69 ;;; Parameters of the entries
70
71 (defvar guix-param-titles
72 '((package
73 (id . "ID")
74 (name . "Name")
75 (version . "Version")
76 (license . "License")
77 (synopsis . "Synopsis")
78 (description . "Description")
79 (home-url . "Home page")
80 (outputs . "Outputs")
81 (inputs . "Inputs")
82 (native-inputs . "Native inputs")
83 (propagated-inputs . "Propagated inputs")
84 (location . "Location")
85 (installed . "Installed"))
86 (installed
87 (path . "Installed path")
88 (dependencies . "Dependencies")
89 (output . "Output"))
90 (generation
91 (id . "ID")
92 (number . "Number")
93 (prev-number . "Previous number")
94 (path . "Path")
95 (time . "Time")))
96 "List for defining titles of entry parameters.
97 Titles are used for displaying information about entries.
98 Each element of the list has a form:
99
100 (ENTRY-TYPE . ((PARAM . TITLE) ...))")
101
102 (defun guix-get-param-title (entry-type param)
103 "Return title of an ENTRY-TYPE entry parameter PARAM."
104 (or (guix-get-key-val guix-param-titles
105 entry-type param)
106 (prog1 (symbol-name param)
107 (message "Couldn't find title for '%S %S'."
108 entry-type param))))
109
110 (defun guix-get-name-spec (name version &optional output)
111 "Return Guix package specification by its NAME, VERSION and OUTPUT."
112 (concat name "-" version
113 (when output (concat ":" output))))
114
115 (defun guix-get-full-name (entry &optional output)
116 "Return name specification of the package ENTRY and OUTPUT."
117 (guix-get-name-spec (guix-get-key-val entry 'name)
118 (guix-get-key-val entry 'version)
119 output))
120
121 (defun guix-get-installed-outputs (entry)
122 "Return list of installed outputs for the package ENTRY."
123 (mapcar (lambda (installed-entry)
124 (guix-get-key-val installed-entry 'output))
125 (guix-get-key-val entry 'installed)))
126
127 (defun guix-get-entry-by-id (id entries)
128 "Return entry from ENTRIES by entry ID."
129 (cl-find-if (lambda (entry)
130 (equal id (guix-get-key-val entry 'id)))
131 entries))
132
133 \f
134 ;;; Location of the packages
135
136 (defvar guix-directory nil
137 "Default Guix directory.
138 If it is not set by a user, it is set after starting Guile REPL.
139 This directory is used to define location of the packages.")
140
141 (defun guix-set-directory ()
142 "Set `guix-directory' if needed."
143 (or guix-directory
144 (setq guix-directory
145 (guix-eval-read "%guix-dir"))))
146
147 (add-hook 'guix-after-start-repl-hook 'guix-set-directory)
148
149 (defun guix-find-location (location)
150 "Go to LOCATION of a package.
151 LOCATION is a string of the form:
152
153 \"PATH:LINE:COLUMN\"
154
155 If PATH is relative, it is considered to be relative to
156 `guix-directory'."
157 (cl-multiple-value-bind (path line col)
158 (split-string location ":")
159 (let ((file (expand-file-name path guix-directory))
160 (line (string-to-number line))
161 (col (string-to-number col)))
162 (find-file file)
163 (goto-char (point-min))
164 (forward-line (- line 1))
165 (move-to-column col)
166 (recenter 1))))
167
168 \f
169 ;;; Common definitions for buffer types
170
171 (defvar-local guix-entries nil
172 "List of the currently displayed entries.
173 Each element of the list is alist with entry info of the
174 following form:
175
176 ((PARAM . VAL) ...)
177
178 PARAM is a name of the entry parameter.
179 VAL is a value of this parameter.")
180 (put 'guix-entries 'permanent-local t)
181
182 (defvar-local guix-buffer-type nil
183 "Type of the current buffer.")
184 (put 'guix-buffer-type 'permanent-local t)
185
186 (defvar-local guix-entry-type nil
187 "Type of the current entry.")
188 (put 'guix-entry-type 'permanent-local t)
189
190 (defvar-local guix-search-type nil
191 "Type of the current search.")
192 (put 'guix-search-type 'permanent-local t)
193
194 (defvar-local guix-search-vals nil
195 "Values of the current search.")
196 (put 'guix-search-vals 'permanent-local t)
197
198 (defsubst guix-set-vars (entries buffer-type entry-type
199 search-type search-vals)
200 (setq guix-entries entries
201 guix-buffer-type buffer-type
202 guix-entry-type entry-type
203 guix-search-type search-type
204 guix-search-vals search-vals))
205
206 (defun guix-get-symbol (postfix buffer-type &optional entry-type)
207 (intern (concat "guix-"
208 (when entry-type
209 (concat (symbol-name entry-type) "-"))
210 (symbol-name buffer-type) "-" postfix)))
211
212 (defmacro guix-define-buffer-type (buf-type entry-type &rest args)
213 "Define common for BUF-TYPE buffers for displaying ENTRY-TYPE entries.
214
215 In the text below TYPE means ENTRY-TYPE-BUF-TYPE.
216
217 This macro defines `guix-TYPE-mode', a custom group and several
218 user variables.
219
220 The following stuff should be defined outside this macro:
221
222 - `guix-BUF-TYPE-mode' - parent mode for the defined mode.
223
224 - `guix-TYPE-mode-initialize' (optional) - function for
225 additional mode settings; it is called without arguments.
226
227 Remaining argument (ARGS) should have a form [KEYWORD VALUE] ... The
228 following keywords are available:
229
230 - `:required' - default value for the defined
231 `guix-TYPE-required-params' variable.
232
233 - `:history-size' - default value for the defined
234 `guix-TYPE-history-size' variable.
235
236 - `:revert' - default value for the defined
237 `guix-TYPE-revert-no-confirm' variable."
238 (let* ((entry-type-str (symbol-name entry-type))
239 (buf-type-str (symbol-name buf-type))
240 (Entry-type-str (capitalize entry-type-str))
241 (Buf-type-str (capitalize buf-type-str))
242 (entry-str (concat entry-type-str " entries"))
243 (buf-str (concat buf-type-str " buffer"))
244 (prefix (concat "guix-" entry-type-str "-" buf-type-str))
245 (group (intern prefix))
246 (mode-map-str (concat prefix "-mode-map"))
247 (mode-map (intern mode-map-str))
248 (parent-mode (intern (concat "guix-" buf-type-str "-mode")))
249 (mode (intern (concat prefix "-mode")))
250 (mode-init-fun (intern (concat prefix "-mode-initialize")))
251 (buf-name-var (intern (concat prefix "-buffer-name")))
252 (revert-var (intern (concat prefix "-revert-no-confirm")))
253 (history-var (intern (concat prefix "-history-size")))
254 (params-var (intern (concat prefix "-required-params")))
255 (revert-val nil)
256 (history-val 20)
257 (params-val '(id)))
258
259 ;; Process the keyword args.
260 (while (keywordp (car args))
261 (pcase (pop args)
262 (`:required (setq params-val (pop args)))
263 (`:history-size (setq history-val (pop args)))
264 (`:revert (setq revert-val (pop args)))
265 (_ (pop args))))
266
267 `(progn
268 (defgroup ,group nil
269 ,(concat Buf-type-str " buffer with " entry-str ".")
270 :prefix ,(concat prefix "-")
271 :group ',(intern (concat "guix-" buf-type-str)))
272
273 (defcustom ,buf-name-var ,(format "*Guix %s %s*"
274 Entry-type-str Buf-type-str)
275 ,(concat "Default name of the " buf-str " for displaying " entry-str ".")
276 :type 'string
277 :group ',group)
278
279 (defcustom ,history-var ,history-val
280 ,(concat "Maximum number of items saved in the history of the " buf-str ".\n"
281 "If 0, the history is disabled.")
282 :type 'integer
283 :group ',group)
284
285 (defcustom ,revert-var ,revert-val
286 ,(concat "If non-nil, do not ask to confirm for reverting the " buf-str ".")
287 :type 'boolean
288 :group ',group)
289
290 (defvar ,params-var ',params-val
291 ,(concat "List of required " entry-type-str " parameters.\n\n"
292 "Displayed parameters and parameters from this list are received\n"
293 "for each " entry-type-str ".\n\n"
294 "May be a special value `all', in which case all supported\n"
295 "parameters are received (this may be very slow for a big number\n"
296 "of entries).\n\n"
297 "Do not remove `id' from this list as it is required for\n"
298 "identifying an entry."))
299
300 (define-derived-mode ,mode ,parent-mode ,(concat "Guix-" Buf-type-str)
301 ,(concat "Major mode for displaying information about " entry-str ".\n\n"
302 "\\{" mode-map-str "}")
303 (setq-local revert-buffer-function 'guix-revert-buffer)
304 (setq-local guix-history-size ,history-var)
305 (and (fboundp ',mode-init-fun) (,mode-init-fun)))
306
307 (let ((map ,mode-map))
308 (define-key map (kbd "l") 'guix-history-back)
309 (define-key map (kbd "r") 'guix-history-forward)
310 (define-key map (kbd "g") 'revert-buffer)
311 (define-key map (kbd "R") 'guix-redisplay-buffer)
312 (define-key map (kbd "C-c C-z") 'guix-switch-to-repl)))))
313
314 (put 'guix-define-buffer-type 'lisp-indent-function 'defun)
315
316 \f
317 ;;; Getting info about packages and generations
318
319 (defun guix-get-entries (entry-type search-type search-vals
320 &optional params)
321 "Search for entries of ENTRY-TYPE.
322
323 Call an appropriate scheme function and return a list of the
324 form of `guix-entries'.
325
326 ENTRY-TYPE should be one of the following symbols: `package',
327 `output' or `generation'.
328
329 SEARCH-TYPE may be one of the following symbols:
330
331 - If ENTRY-TYPE is `package' or `output': `id', `name', `regexp',
332 `all-available', `newest-available', `installed', `obsolete',
333 `generation'.
334
335 - If ENTRY-TYPE is `generation': `id', `last', `all'.
336
337 PARAMS is a list of parameters for receiving. If nil, get
338 information with all available parameters."
339 (guix-eval-read (guix-make-guile-expression
340 'entries
341 guix-current-profile params
342 entry-type search-type search-vals)))
343
344 (defun guix-get-show-entries (buffer-type entry-type search-type
345 &rest search-vals)
346 "Search for ENTRY-TYPE entries and show results in BUFFER-TYPE buffer.
347 See `guix-get-entries' for the meaning of SEARCH-TYPE and SEARCH-VALS."
348 (let ((entries (guix-get-entries entry-type search-type search-vals
349 (guix-get-params-for-receiving
350 buffer-type entry-type))))
351 (guix-set-buffer entries buffer-type entry-type
352 search-type search-vals)))
353
354 (defun guix-set-buffer (entries buffer-type entry-type search-type
355 search-vals &optional history-replace)
356 "Set up BUFFER-TYPE buffer for displaying ENTRY-TYPE ENTRIES.
357
358 Display ENTRIES, set variables and make history item.
359 ENTRIES should have a form of `guix-entries'.
360
361 See `guix-get-entries' for the meaning of SEARCH-TYPE and SEARCH-VALS.
362
363 If HISTORY-REPLACE is non-nil, replace current history item,
364 otherwise add the new one."
365 (when entries
366 (let ((buf (if (eq major-mode (guix-get-symbol
367 "mode" buffer-type entry-type))
368 (current-buffer)
369 (get-buffer-create
370 (symbol-value
371 (guix-get-symbol "buffer-name"
372 buffer-type entry-type))))))
373 (with-current-buffer buf
374 (guix-show-entries entries buffer-type entry-type)
375 (guix-set-vars entries buffer-type entry-type
376 search-type search-vals)
377 (funcall (if history-replace
378 #'guix-history-replace
379 #'guix-history-add)
380 (guix-make-history-item)))
381 (pop-to-buffer buf
382 '((display-buffer-reuse-window
383 display-buffer-same-window)))))
384 (guix-result-message entries entry-type search-type search-vals))
385
386 (defun guix-show-entries (entries buffer-type entry-type)
387 "Display ENTRY-TYPE ENTRIES in the current BUFFER-TYPE buffer."
388 (let ((inhibit-read-only t))
389 (erase-buffer)
390 (funcall (symbol-function (guix-get-symbol
391 "mode" buffer-type entry-type)))
392 (funcall (guix-get-symbol "insert-entries" buffer-type)
393 entries entry-type)
394 (goto-char (point-min))))
395
396 (defun guix-history-call (entries buffer-type entry-type
397 search-type search-vals)
398 "Function called for moving by history."
399 (guix-show-entries entries buffer-type entry-type)
400 (guix-set-vars entries buffer-type entry-type
401 search-type search-vals)
402 (guix-result-message entries entry-type search-type search-vals))
403
404 (defun guix-make-history-item ()
405 "Make and return a history item for the current buffer."
406 (list #'guix-history-call
407 guix-entries guix-buffer-type guix-entry-type
408 guix-search-type guix-search-vals))
409
410 (defun guix-get-params-for-receiving (buffer-type entry-type)
411 "Return parameters that should be received for BUFFER-TYPE, ENTRY-TYPE."
412 (let* ((required-var (guix-get-symbol "required-params"
413 buffer-type entry-type))
414 (required (symbol-value required-var)))
415 (unless (equal required 'all)
416 (cl-union required
417 (funcall (guix-get-symbol "get-displayed-params"
418 buffer-type)
419 entry-type)))))
420
421 (defun guix-revert-buffer (_ignore-auto noconfirm)
422 "Update information in the current buffer.
423 The function is suitable for `revert-buffer-function'.
424 See `revert-buffer' for the meaning of NOCONFIRM."
425 (when (or noconfirm
426 (symbol-value
427 (guix-get-symbol "revert-no-confirm"
428 guix-buffer-type guix-entry-type))
429 (y-or-n-p "Update current information? "))
430 (let ((entries (guix-get-entries
431 guix-entry-type guix-search-type guix-search-vals
432 (guix-get-params-for-receiving guix-buffer-type
433 guix-entry-type))))
434 (guix-set-buffer entries guix-buffer-type guix-entry-type
435 guix-search-type guix-search-vals t))))
436
437 (defun guix-redisplay-buffer ()
438 "Redisplay current information.
439 This function will not update the information, use
440 \"\\[revert-buffer]\" if you want the full update."
441 (interactive)
442 (guix-show-entries guix-entries guix-buffer-type guix-entry-type)
443 (guix-result-message guix-entries guix-entry-type
444 guix-search-type guix-search-vals))
445
446 \f
447 ;;; Messages
448
449 (defvar guix-messages
450 '((package
451 (id
452 (0 "Packages not found.")
453 (1 "")
454 (many "%d packages." count))
455 (name
456 (0 "The package '%s' not found." val)
457 (1 "A single package with name '%s'." val)
458 (many "%d packages with '%s' name." count val))
459 (regexp
460 (0 "No packages matching '%s'." val)
461 (1 "A single package matching '%s'." val)
462 (many "%d packages matching '%s'." count val))
463 (all-available
464 (0 "No packages are available for some reason.")
465 (1 "A single available package (that's strange).")
466 (many "%d available packages." count))
467 (newest-available
468 (0 "No packages are available for some reason.")
469 (1 "A single newest available package (that's strange).")
470 (many "%d newest available packages." count))
471 (installed
472 (0 "No installed packages.")
473 (1 "A single installed package.")
474 (many "%d installed packages." count))
475 (obsolete
476 (0 "No obsolete packages.")
477 (1 "A single obsolete package.")
478 (many "%d obsolete packages." count))
479 (generation
480 (0 "No packages installed in generation %d." val)
481 (1 "A single package installed in generation %d." val)
482 (many "%d packages installed in generation %d." count val)))
483 (generation
484 (id
485 (0 "Generations not found.")
486 (1 "")
487 (many "%d generations." count))
488 (last
489 (0 "No available generations.")
490 (1 "The last generation.")
491 (many "%d last generations." count))
492 (all
493 (0 "No available generations.")
494 (1 "A single available generation.")
495 (many "%d available generations." count)))))
496
497 (defun guix-result-message (entries entry-type search-type search-vals)
498 "Display an appropriate message after displaying ENTRIES."
499 (let* ((val (car search-vals))
500 (count (length entries))
501 (count-key (if (> count 1) 'many count))
502 (msg-spec (guix-get-key-val guix-messages
503 entry-type search-type count-key))
504 (format (car msg-spec))
505 (args (cdr msg-spec)))
506 (mapc (lambda (subst)
507 (setq args (cl-substitute (car subst) (cdr subst) args)))
508 (list (cons count 'count)
509 (cons val 'val)))
510 (apply #'message format args)))
511
512 \f
513 ;;; Actions on packages and generations
514
515 (defcustom guix-operation-confirm t
516 "If nil, do not prompt to confirm an operation."
517 :type 'boolean
518 :group 'guix)
519
520 (defcustom guix-use-substitutes t
521 "If non-nil, use substitutes for the Guix packages."
522 :type 'boolean
523 :group 'guix)
524
525 (defvar guix-dry-run nil
526 "If non-nil, do not perform the real actions, just simulate.")
527
528 (defvar guix-temp-buffer-name " *Guix temp*"
529 "Name of a buffer used for displaying info before executing operation.")
530
531 (defun guix-process-package-actions (&rest actions)
532 "Process package ACTIONS.
533 Each action is a list of the form:
534
535 (ACTION-TYPE PACKAGE-SPEC ...)
536
537 ACTION-TYPE is one of the following symbols: `install',
538 `upgrade', `remove'/`delete'.
539 PACKAGE-SPEC should have the following form: (ID [OUTPUT] ...)."
540 (let (install upgrade remove)
541 (mapc (lambda (action)
542 (let ((action-type (car action))
543 (specs (cdr action)))
544 (cl-case action-type
545 (install (setq install (append install specs)))
546 (upgrade (setq upgrade (append upgrade specs)))
547 ((remove delete) (setq remove (append remove specs))))))
548 actions)
549 (when (guix-continue-package-operation-p
550 :install install :upgrade upgrade :remove remove)
551 (guix-eval-in-repl
552 (guix-make-guile-expression
553 'process-package-actions guix-current-profile
554 :install install :upgrade upgrade :remove remove
555 :use-substitutes? (or guix-use-substitutes 'f)
556 :dry-run? (or guix-dry-run 'f))))))
557
558 (cl-defun guix-continue-package-operation-p (&key install upgrade remove)
559 "Return non-nil if a package operation should be continued.
560 Ask a user if needed (see `guix-operation-confirm').
561 INSTALL, UPGRADE, REMOVE are 'package action specifications'.
562 See `guix-process-package-actions' for details."
563 (or (null guix-operation-confirm)
564 (let* ((entries (guix-get-entries
565 'package 'id
566 (append (mapcar #'car install)
567 (mapcar #'car upgrade)
568 (mapcar #'car remove))
569 '(id name version location)))
570 (install-strings (guix-get-package-strings install entries))
571 (upgrade-strings (guix-get-package-strings upgrade entries))
572 (remove-strings (guix-get-package-strings remove entries)))
573 (if (or install-strings upgrade-strings remove-strings)
574 (let ((buf (get-buffer-create guix-temp-buffer-name)))
575 (with-current-buffer buf
576 (setq-local cursor-type nil)
577 (setq buffer-read-only nil)
578 (erase-buffer)
579 (guix-insert-package-strings install-strings "install")
580 (guix-insert-package-strings upgrade-strings "upgrade")
581 (guix-insert-package-strings remove-strings "remove")
582 (let ((win (temp-buffer-window-show
583 buf
584 '((display-buffer-reuse-window
585 display-buffer-at-bottom)
586 (window-height . fit-window-to-buffer)))))
587 (prog1 (y-or-n-p "Continue operation? ")
588 (quit-window nil win)))))
589 (message "Nothing to be done. If the REPL was restarted, information is not up-to-date.")
590 nil))))
591
592 (defun guix-get-package-strings (specs entries)
593 "Return short package descriptions for performing package actions.
594 See `guix-process-package-actions' for the meaning of SPECS.
595 ENTRIES is a list of package entries to get info about packages."
596 (delq nil
597 (mapcar
598 (lambda (spec)
599 (let* ((id (car spec))
600 (outputs (cdr spec))
601 (entry (guix-get-entry-by-id id entries)))
602 (when entry
603 (let ((location (guix-get-key-val entry 'location)))
604 (concat (guix-get-full-name entry)
605 (when outputs
606 (concat ":"
607 (mapconcat #'identity outputs ",")))
608 (when location
609 (concat "\t(" location ")")))))))
610 specs)))
611
612 (defun guix-insert-package-strings (strings action)
613 "Insert information STRINGS at point for performing package ACTION."
614 (when strings
615 (insert "Package(s) to " (guix-get-string action 'bold) ":\n")
616 (mapc (lambda (str)
617 (insert " " str "\n"))
618 strings)
619 (insert "\n")))
620
621 (provide 'guix-base)
622
623 ;;; guix-base.el ends here