guix-ui-generation.el 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531
  1. ;;; guix-ui-generation.el --- Interface for displaying generations -*- lexical-binding: t -*-
  2. ;; Copyright © 2014–2017 Alex Kost <alezost@gmail.com>
  3. ;; This file is part of Emacs-Guix.
  4. ;; Emacs-Guix is free software; you can redistribute it and/or modify
  5. ;; it under the terms of the GNU General Public License as published by
  6. ;; the Free Software Foundation, either version 3 of the License, or
  7. ;; (at your option) any later version.
  8. ;;
  9. ;; Emacs-Guix is distributed in the hope that it will be useful,
  10. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. ;; GNU General Public License for more details.
  13. ;;
  14. ;; You should have received a copy of the GNU General Public License
  15. ;; along with Emacs-Guix. If not, see <http://www.gnu.org/licenses/>.
  16. ;;; Commentary:
  17. ;; This file provides an interface for displaying profile generations in
  18. ;; 'list' and 'info' buffers, and commands for working with them.
  19. ;;; Code:
  20. (require 'cl-lib)
  21. (require 'dash)
  22. (require 'bui)
  23. (require 'guix nil t)
  24. (require 'guix-ui)
  25. (require 'guix-ui-package)
  26. (require 'guix-misc)
  27. (require 'guix-repl)
  28. (require 'guix-guile)
  29. (require 'guix-utils)
  30. (require 'guix-profiles)
  31. (guix-ui-define-entry-type generation)
  32. (defun guix-generation-get-entries (proc profile search-type
  33. search-values params)
  34. "Return 'generation' or 'system-generation' entries.
  35. PROC is the name of a Scheme procedure (either 'generation-sexps'
  36. or 'system-generation-sexps')."
  37. (apply #'guix-modify-objects
  38. (guix-eval-read (guix-make-guile-expression
  39. proc profile search-type search-values params))
  40. (when (or (null params)
  41. (memq 'number-of-packages params))
  42. (list
  43. (lambda (entry)
  44. (let ((generation (bui-entry-non-void-value entry 'number)))
  45. (if generation
  46. `((number-of-packages
  47. . ,(guix-profile-number-of-packages
  48. profile generation))
  49. ,@entry)
  50. entry)))))))
  51. (defun guix-generation-get-display (profile search-type &rest search-values)
  52. "Search for generations and show results.
  53. If PROFILE is nil, use `guix-current-profile'.
  54. See `guix-ui-get-entries' for the meaning of SEARCH-TYPE and
  55. SEARCH-VALUES."
  56. (apply #'bui-list-get-display-entries
  57. 'guix-generation
  58. (or profile guix-current-profile)
  59. search-type search-values))
  60. (defun guix-delete-generations (profile generations
  61. &optional operation-buffer)
  62. "Delete GENERATIONS from PROFILE.
  63. Each element from GENERATIONS is a generation number."
  64. (when (or (not guix-operation-confirm)
  65. (y-or-n-p
  66. (let ((count (length generations)))
  67. (if (> count 1)
  68. (format "Delete %d generations from profile '%s'? "
  69. count profile)
  70. (format "Delete generation %d from profile '%s'? "
  71. (car generations) profile)))))
  72. (guix-eval-in-repl
  73. (guix-make-guile-expression
  74. 'delete-generations* profile generations)
  75. operation-buffer)))
  76. (defun guix-switch-to-generation (profile generation
  77. &optional operation-buffer)
  78. "Switch PROFILE to GENERATION."
  79. (when (or (not guix-operation-confirm)
  80. (y-or-n-p (format "Switch profile '%s' to generation %d? "
  81. profile generation)))
  82. (guix-eval-in-repl
  83. (guix-make-guile-expression
  84. 'switch-to-generation* profile generation)
  85. operation-buffer)))
  86. (defun guix-generation-current-package-profile (&optional generation)
  87. "Return a directory where packages are installed for the
  88. current profile's GENERATION."
  89. (guix-package-profile (guix-ui-current-profile) generation))
  90. ;;; Generation 'info'
  91. (guix-ui-define-interface generation info
  92. :mode-name "Generation-Info"
  93. :buffer-name "*Guix Generation Info*"
  94. :get-entries-function 'guix-generation-info-get-entries
  95. :format '(guix-generation-info-insert-heading
  96. nil
  97. (prev-number format guix-generation-info-insert-previous)
  98. (current format guix-generation-info-insert-current)
  99. (number-of-packages format guix-generation-info-insert-packages)
  100. (file-name simple (indent bui-file))
  101. (time format (time)))
  102. :titles '((prev-number . "Prev. generation"))
  103. :required '(id number))
  104. (defface guix-generation-info-heading
  105. '((t :inherit bui-info-heading))
  106. "Face used for generation heading."
  107. :group 'guix-generation-info-faces)
  108. (defface guix-generation-info-current
  109. '((t :inherit guix-package-info-installed-outputs))
  110. "Face used if a generation is the current one."
  111. :group 'guix-generation-info-faces)
  112. (defface guix-generation-info-not-current
  113. '((t nil))
  114. "Face used if a generation is not the current one."
  115. :group 'guix-generation-info-faces)
  116. (defun guix-generation-info-get-entries (profile search-type
  117. &rest search-values)
  118. "Return 'generation' entries for displaying them in 'info' buffer."
  119. (guix-generation-get-entries
  120. 'generation-sexps
  121. profile search-type search-values
  122. (cl-union guix-generation-info-required-params
  123. (bui-info-displayed-params 'guix-generation))))
  124. (defun guix-generation-info-insert-heading (entry)
  125. "Insert generation ENTRY heading at point."
  126. (bui-format-insert
  127. (concat "Generation "
  128. (number-to-string (bui-entry-value entry 'number)))
  129. 'guix-generation-info-heading)
  130. (bui-newline))
  131. (defun guix-generation-info-insert-previous (prev-number entry)
  132. "Insert PREV-NUMBER and button to compare generations."
  133. (bui-format-insert prev-number)
  134. (bui-insert-indent)
  135. (when (> prev-number 0)
  136. (let ((number (bui-entry-non-void-value entry 'number)))
  137. (bui-insert-action-button
  138. "Compare"
  139. (lambda (btn)
  140. (guix-diff
  141. (guix-profile-generation-packages-buffer
  142. (button-get btn 'prev-number))
  143. (guix-profile-generation-packages-buffer
  144. (button-get btn 'number))))
  145. (format "Show Diff of packages installed in generations %d and %d"
  146. prev-number number)
  147. 'prev-number prev-number
  148. 'number number))))
  149. (defun guix-generation-info-insert-packages (number entry)
  150. "Insert the NUMBER of packages and button to display packages."
  151. (bui-format-insert number)
  152. (bui-insert-indent)
  153. (let ((number (bui-entry-non-void-value entry 'number)))
  154. (bui-insert-action-button
  155. "Packages"
  156. (lambda (btn)
  157. (guix-package-get-display
  158. (guix-generation-current-package-profile
  159. (button-get btn 'number))
  160. 'installed))
  161. (format "Show packages installed in generation %d" number)
  162. 'number number)))
  163. (defun guix-generation-info-insert-current (val entry)
  164. "Insert boolean value VAL showing whether this generation is current."
  165. (if val
  166. (bui-info-insert-value-format "Yes" 'guix-generation-info-current)
  167. (bui-info-insert-value-format "No" 'guix-generation-info-not-current)
  168. (bui-insert-indent)
  169. (let ((number (bui-entry-non-void-value entry 'number)))
  170. (bui-insert-action-button
  171. "Switch"
  172. (lambda (btn)
  173. (guix-switch-to-generation (guix-ui-current-profile)
  174. (button-get btn 'number)
  175. (current-buffer)))
  176. (format "Switch to generation %d (make it the current one)"
  177. number)
  178. 'number number)
  179. (bui-insert-indent)
  180. (bui-insert-action-button
  181. "Delete"
  182. (lambda (btn)
  183. (guix-delete-generations (guix-ui-current-profile)
  184. (list (button-get btn 'number))
  185. (current-buffer)))
  186. (format "Delete generation %d" number)
  187. 'number number))))
  188. ;;; Generation 'list'
  189. (guix-ui-define-interface generation list
  190. :mode-name "Generation-List"
  191. :buffer-name "*Guix Generations*"
  192. :get-entries-function 'guix-generation-list-get-entries
  193. :describe-function 'guix-ui-list-describe
  194. :format '((number nil 5 bui-list-sort-numerically-0 :right-align t)
  195. (current guix-generation-list-get-current 10 t)
  196. (number-of-packages nil 11 bui-list-sort-numerically-2
  197. :right-align t)
  198. (time bui-list-get-time 20 t)
  199. (file-name bui-list-get-file-name 30 t))
  200. :titles '((number . "N.")
  201. (number-of-packages . "Packages"))
  202. :hint 'guix-generation-list-hint
  203. :sort-key '(number . t)
  204. :marks '((delete . ?D)))
  205. (let ((map guix-generation-list-mode-map))
  206. (define-key map (kbd "P") 'guix-generation-list-show-packages)
  207. (define-key map (kbd "+") 'guix-generation-list-show-added-packages)
  208. (define-key map (kbd "-") 'guix-generation-list-show-removed-packages)
  209. (define-key map (kbd "=") 'guix-generation-list-diff)
  210. (define-key map (kbd "e") 'guix-generation-list-ediff)
  211. (define-key map (kbd "x") 'guix-generation-list-execute)
  212. (define-key map (kbd "c") 'guix-generation-list-set-current)
  213. (define-key map (kbd "d") 'guix-generation-list-mark-delete))
  214. (defvar guix-generation-list-default-hint
  215. '(("\\[guix-generation-list-show-packages]") " show packages;\n"
  216. ("\\[guix-generation-list-set-current]") " set current generation;\n"
  217. ("\\[guix-generation-list-diff]") " show Diff of the marked generations;\n"
  218. ("\\[guix-generation-list-mark-delete]") " mark for deletion; "
  219. ("\\[guix-generation-list-execute]") " execute operation (deletions);\n"))
  220. (defun guix-generation-list-hint ()
  221. (bui-format-hints
  222. guix-generation-list-default-hint
  223. (bui-default-hint)))
  224. (defun guix-generation-list-get-entries (profile search-type
  225. &rest search-values)
  226. "Return 'generation' entries for displaying them in 'list' buffer."
  227. (guix-generation-get-entries
  228. 'generation-sexps
  229. profile search-type search-values
  230. (cl-union guix-generation-list-required-params
  231. (bui-list-displayed-params 'guix-generation))))
  232. (defun guix-generation-list-get-current (val &optional _)
  233. "Return string from VAL showing whether this generation is current.
  234. VAL is a boolean value."
  235. (if val "(current)" ""))
  236. (defun guix-generation-list-set-current ()
  237. "Switch current profile to the generation at point."
  238. (interactive)
  239. (let* ((entry (bui-list-current-entry))
  240. (current (bui-entry-non-void-value entry 'current))
  241. (number (bui-entry-non-void-value entry 'number)))
  242. (if current
  243. (user-error "This generation is already the current one")
  244. (guix-switch-to-generation (guix-ui-current-profile)
  245. number (current-buffer)))))
  246. (defun guix-generation-list-show-packages ()
  247. "List installed packages for the generation at point."
  248. (interactive)
  249. (guix-package-get-display
  250. (guix-generation-current-package-profile (bui-list-current-id))
  251. 'installed))
  252. (defun guix-generation-list-generations-to-compare ()
  253. "Return a sorted list of 2 marked generations for comparing."
  254. (let ((numbers (bui-list-get-marked-id-list 'general)))
  255. (if (/= (length numbers) 2)
  256. (user-error "2 generations should be marked for comparing")
  257. (sort numbers #'<))))
  258. (defun guix-generation-list-profiles-to-compare ()
  259. "Return a sorted list of 2 marked generation profiles for comparing."
  260. (mapcar #'guix-generation-current-package-profile
  261. (guix-generation-list-generations-to-compare)))
  262. (defun guix-generation-list-show-added-packages ()
  263. "List package outputs added to the latest marked generation.
  264. If 2 generations are marked with \\[guix-list-mark], display
  265. outputs installed in the latest marked generation that were not
  266. installed in the other one."
  267. (interactive)
  268. (bui-get-display-entries
  269. 'guix-output 'list
  270. (cl-list* (guix-ui-current-profile)
  271. 'profile-diff
  272. (reverse (guix-generation-list-profiles-to-compare)))
  273. 'add))
  274. (defun guix-generation-list-show-removed-packages ()
  275. "List package outputs removed from the latest marked generation.
  276. If 2 generations are marked with \\[guix-list-mark], display
  277. outputs not installed in the latest marked generation that were
  278. installed in the other one."
  279. (interactive)
  280. (bui-get-display-entries
  281. 'guix-output 'list
  282. (cl-list* (guix-ui-current-profile)
  283. 'profile-diff
  284. (guix-generation-list-profiles-to-compare))
  285. 'add))
  286. (defun guix-generation-list-compare (diff-fun gen-fun)
  287. "Run GEN-FUN on the 2 marked generations and run DIFF-FUN on the results."
  288. (cl-multiple-value-bind (gen1 gen2)
  289. (guix-generation-list-generations-to-compare)
  290. (funcall diff-fun
  291. (funcall gen-fun gen1)
  292. (funcall gen-fun gen2))))
  293. (defun guix-generation-list-ediff-manifests ()
  294. "Run Ediff on manifests of the 2 marked generations."
  295. (interactive)
  296. (guix-generation-list-compare
  297. #'ediff-files
  298. #'guix-profile-generation-manifest-file))
  299. (defun guix-generation-list-diff-manifests ()
  300. "Run Diff on manifests of the 2 marked generations."
  301. (interactive)
  302. (guix-generation-list-compare
  303. #'guix-diff
  304. #'guix-profile-generation-manifest-file))
  305. (defun guix-generation-list-ediff-packages ()
  306. "Run Ediff on package outputs installed in the 2 marked generations."
  307. (interactive)
  308. (guix-generation-list-compare
  309. #'ediff-buffers
  310. #'guix-profile-generation-packages-buffer))
  311. (defun guix-generation-list-diff-packages ()
  312. "Run Diff on package outputs installed in the 2 marked generations."
  313. (interactive)
  314. (guix-generation-list-compare
  315. #'guix-diff
  316. #'guix-profile-generation-packages-buffer))
  317. (defun guix-generation-list-ediff (arg)
  318. "Run Ediff on package outputs installed in the 2 marked generations.
  319. With ARG, run Ediff on manifests of the marked generations."
  320. (interactive "P")
  321. (if arg
  322. (guix-generation-list-ediff-manifests)
  323. (guix-generation-list-ediff-packages)))
  324. (defun guix-generation-list-diff (arg)
  325. "Run Diff on package outputs installed in the 2 marked generations.
  326. With ARG, run Diff on manifests of the marked generations."
  327. (interactive "P")
  328. (if arg
  329. (guix-generation-list-diff-manifests)
  330. (guix-generation-list-diff-packages)))
  331. (defun guix-generation-list-mark-delete (&optional arg)
  332. "Mark the current generation for deletion and move to the next line.
  333. With ARG, mark all generations for deletion."
  334. (interactive "P")
  335. (if arg
  336. (bui-list-mark-all 'delete)
  337. (bui-list--mark 'delete t)))
  338. (defun guix-generation-list-execute ()
  339. "Delete marked generations."
  340. (interactive)
  341. (let ((marked (bui-list-get-marked-id-list 'delete)))
  342. (or marked
  343. (user-error "No generations marked for deletion"))
  344. (guix-delete-generations (guix-ui-current-profile)
  345. marked (current-buffer))))
  346. ;;; Inserting packages to compare generations
  347. (defcustom guix-generation-packages-buffer-name-function
  348. #'guix-generation-packages-buffer-name-default
  349. "Function used to define name of a buffer with generation packages.
  350. This function is called with 2 arguments: PROFILE (string) and
  351. GENERATION (number)."
  352. :type '(choice (function-item guix-generation-packages-buffer-name-default)
  353. (function-item guix-generation-packages-buffer-name-long)
  354. (function :tag "Other function"))
  355. :group 'guix-generation)
  356. (defcustom guix-generation-packages-update-buffer t
  357. "If non-nil, always update list of packages during comparing generations.
  358. If nil, generation packages are received only once. So when you
  359. compare generation 1 and generation 2, the packages for both
  360. generations will be received. Then if you compare generation 1
  361. and generation 3, only the packages for generation 3 will be
  362. received. Thus if you use comparing of different generations a
  363. lot, you may set this variable to nil to improve the
  364. performance."
  365. :type 'boolean
  366. :group 'guix-generation)
  367. (defvar guix-generation-output-name-width 30
  368. "Width of an output name \"column\".
  369. This variable is used in auxiliary buffers for comparing generations.")
  370. (defun guix-generation-packages (profile)
  371. "Return a list of sorted packages installed in PROFILE.
  372. Each element of the list is a list of the package specification
  373. and its store file name."
  374. (sort (guix-eval-read
  375. (guix-make-guile-expression
  376. 'profile->specifications+file-names profile))
  377. (lambda (a b)
  378. (string< (car a) (car b)))))
  379. (defun guix-generation-packages-buffer-name-default (profile generation)
  380. "Return name of a buffer for displaying GENERATION's package outputs.
  381. Use base name of PROFILE file name."
  382. (let ((profile-name (file-name-base (directory-file-name profile))))
  383. (format "*Guix %s: generation %s*"
  384. profile-name generation)))
  385. (defun guix-generation-packages-buffer-name-long (profile generation)
  386. "Return name of a buffer for displaying GENERATION's package outputs.
  387. Use the full PROFILE file name."
  388. (format "*Guix generation %s (%s)*"
  389. generation profile))
  390. (defun guix-generation-packages-buffer-name (profile generation)
  391. "Return name of a buffer for displaying GENERATION's package outputs."
  392. (funcall guix-generation-packages-buffer-name-function
  393. profile generation))
  394. (defun guix-generation-insert-package (name file-name)
  395. "Insert package output NAME and store FILE-NAME at point."
  396. (insert name)
  397. (indent-to guix-generation-output-name-width 2)
  398. (insert file-name "\n"))
  399. (defun guix-generation-insert-packages (buffer profile)
  400. "Insert package outputs installed in PROFILE in BUFFER."
  401. (with-current-buffer buffer
  402. (setq buffer-read-only nil
  403. indent-tabs-mode nil)
  404. (erase-buffer)
  405. (mapc (-lambda ((name file-name))
  406. (guix-generation-insert-package name file-name))
  407. (guix-generation-packages profile))))
  408. (defun guix-generation-packages-buffer (profile generation)
  409. "Return buffer with package outputs installed in PROFILE's GENERATION.
  410. Create the buffer if needed."
  411. (let ((buf-name (guix-generation-packages-buffer-name
  412. profile generation)))
  413. (or (and (null guix-generation-packages-update-buffer)
  414. (get-buffer buf-name))
  415. (let ((buf (get-buffer-create buf-name)))
  416. (guix-generation-insert-packages
  417. buf
  418. (guix-package-profile profile generation))
  419. buf))))
  420. (defun guix-profile-generation-manifest-file (generation)
  421. "Return the file name of a GENERATION's manifest.
  422. GENERATION is a generation number of the current profile."
  423. (guix-manifest-file (guix-ui-current-profile) generation))
  424. (defun guix-profile-generation-packages-buffer (generation)
  425. "Insert GENERATION's package outputs in a buffer and return it.
  426. GENERATION is a generation number of the current profile."
  427. (guix-generation-packages-buffer (guix-ui-current-profile)
  428. generation))
  429. ;;; Interactive commands
  430. ;;;###autoload
  431. (defun guix-generations (&optional profile)
  432. "Display information about all generations.
  433. If PROFILE is nil, use `guix-current-profile'.
  434. Interactively with prefix, prompt for PROFILE."
  435. (interactive (list (guix-ui-read-generation-profile)))
  436. (guix-generation-get-display profile 'all))
  437. ;;;###autoload
  438. (defun guix-last-generations (number &optional profile)
  439. "Display information about last NUMBER generations.
  440. If PROFILE is nil, use `guix-current-profile'.
  441. Interactively with prefix, prompt for PROFILE."
  442. (interactive
  443. (list (read-number "The number of last generations: ")
  444. (guix-ui-read-generation-profile)))
  445. (guix-generation-get-display profile 'last number))
  446. ;;;###autoload
  447. (defun guix-generations-by-time (from to &optional profile)
  448. "Display information about generations created between FROM and TO.
  449. FROM and TO should be time values.
  450. If PROFILE is nil, use `guix-current-profile'.
  451. Interactively with prefix, prompt for PROFILE."
  452. (interactive
  453. (list (guix-read-date "Find generations (from): ")
  454. (guix-read-date "Find generations (to): ")
  455. (guix-ui-read-generation-profile)))
  456. (guix-generation-get-display profile 'time
  457. (float-time from)
  458. (float-time to)))
  459. (provide 'guix-ui-generation)
  460. ;;; guix-ui-generation.el ends here