guix-ui-generation.el 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  1. ;;; guix-ui-generation.el --- Interface for displaying generations -*- lexical-binding: t -*-
  2. ;; Copyright © 2014–2016 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. :sort-key '(number . t)
  203. :marks '((delete . ?D)))
  204. (let ((map guix-generation-list-mode-map))
  205. (define-key map (kbd "P") 'guix-generation-list-show-packages)
  206. (define-key map (kbd "+") 'guix-generation-list-show-added-packages)
  207. (define-key map (kbd "-") 'guix-generation-list-show-removed-packages)
  208. (define-key map (kbd "=") 'guix-generation-list-diff)
  209. (define-key map (kbd "D") '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 "s") 'guix-generation-list-switch)
  213. (define-key map (kbd "c") 'guix-generation-list-switch)
  214. (define-key map (kbd "d") 'guix-generation-list-mark-delete))
  215. (defun guix-generation-list-get-entries (profile search-type
  216. &rest search-values)
  217. "Return 'generation' entries for displaying them in 'list' buffer."
  218. (guix-generation-get-entries
  219. 'generation-sexps
  220. profile search-type search-values
  221. (cl-union guix-generation-list-required-params
  222. (bui-list-displayed-params 'guix-generation))))
  223. (defun guix-generation-list-get-current (val &optional _)
  224. "Return string from VAL showing whether this generation is current.
  225. VAL is a boolean value."
  226. (if val "(current)" ""))
  227. (defun guix-generation-list-switch ()
  228. "Switch current profile to the generation at point."
  229. (interactive)
  230. (let* ((entry (bui-list-current-entry))
  231. (current (bui-entry-non-void-value entry 'current))
  232. (number (bui-entry-non-void-value entry 'number)))
  233. (if current
  234. (user-error "This generation is already the current one")
  235. (guix-switch-to-generation (guix-ui-current-profile)
  236. number (current-buffer)))))
  237. (defun guix-generation-list-show-packages ()
  238. "List installed packages for the generation at point."
  239. (interactive)
  240. (guix-package-get-display
  241. (guix-generation-current-package-profile (bui-list-current-id))
  242. 'installed))
  243. (defun guix-generation-list-generations-to-compare ()
  244. "Return a sorted list of 2 marked generations for comparing."
  245. (let ((numbers (bui-list-get-marked-id-list 'general)))
  246. (if (/= (length numbers) 2)
  247. (user-error "2 generations should be marked for comparing")
  248. (sort numbers #'<))))
  249. (defun guix-generation-list-profiles-to-compare ()
  250. "Return a sorted list of 2 marked generation profiles for comparing."
  251. (mapcar #'guix-generation-current-package-profile
  252. (guix-generation-list-generations-to-compare)))
  253. (defun guix-generation-list-show-added-packages ()
  254. "List package outputs added to the latest marked generation.
  255. If 2 generations are marked with \\[guix-list-mark], display
  256. outputs installed in the latest marked generation that were not
  257. installed in the other one."
  258. (interactive)
  259. (bui-get-display-entries
  260. 'guix-output 'list
  261. (cl-list* (guix-ui-current-profile)
  262. 'profile-diff
  263. (reverse (guix-generation-list-profiles-to-compare)))
  264. 'add))
  265. (defun guix-generation-list-show-removed-packages ()
  266. "List package outputs removed from the latest marked generation.
  267. If 2 generations are marked with \\[guix-list-mark], display
  268. outputs not installed in the latest marked generation that were
  269. installed in the other one."
  270. (interactive)
  271. (bui-get-display-entries
  272. 'guix-output 'list
  273. (cl-list* (guix-ui-current-profile)
  274. 'profile-diff
  275. (guix-generation-list-profiles-to-compare))
  276. 'add))
  277. (defun guix-generation-list-compare (diff-fun gen-fun)
  278. "Run GEN-FUN on the 2 marked generations and run DIFF-FUN on the results."
  279. (cl-multiple-value-bind (gen1 gen2)
  280. (guix-generation-list-generations-to-compare)
  281. (funcall diff-fun
  282. (funcall gen-fun gen1)
  283. (funcall gen-fun gen2))))
  284. (defun guix-generation-list-ediff-manifests ()
  285. "Run Ediff on manifests of the 2 marked generations."
  286. (interactive)
  287. (guix-generation-list-compare
  288. #'ediff-files
  289. #'guix-profile-generation-manifest-file))
  290. (defun guix-generation-list-diff-manifests ()
  291. "Run Diff on manifests of the 2 marked generations."
  292. (interactive)
  293. (guix-generation-list-compare
  294. #'guix-diff
  295. #'guix-profile-generation-manifest-file))
  296. (defun guix-generation-list-ediff-packages ()
  297. "Run Ediff on package outputs installed in the 2 marked generations."
  298. (interactive)
  299. (guix-generation-list-compare
  300. #'ediff-buffers
  301. #'guix-profile-generation-packages-buffer))
  302. (defun guix-generation-list-diff-packages ()
  303. "Run Diff on package outputs installed in the 2 marked generations."
  304. (interactive)
  305. (guix-generation-list-compare
  306. #'guix-diff
  307. #'guix-profile-generation-packages-buffer))
  308. (defun guix-generation-list-ediff (arg)
  309. "Run Ediff on package outputs installed in the 2 marked generations.
  310. With ARG, run Ediff on manifests of the marked generations."
  311. (interactive "P")
  312. (if arg
  313. (guix-generation-list-ediff-manifests)
  314. (guix-generation-list-ediff-packages)))
  315. (defun guix-generation-list-diff (arg)
  316. "Run Diff on package outputs installed in the 2 marked generations.
  317. With ARG, run Diff on manifests of the marked generations."
  318. (interactive "P")
  319. (if arg
  320. (guix-generation-list-diff-manifests)
  321. (guix-generation-list-diff-packages)))
  322. (defun guix-generation-list-mark-delete (&optional arg)
  323. "Mark the current generation for deletion and move to the next line.
  324. With ARG, mark all generations for deletion."
  325. (interactive "P")
  326. (if arg
  327. (bui-list-mark-all 'delete)
  328. (bui-list--mark 'delete t)))
  329. (defun guix-generation-list-execute ()
  330. "Delete marked generations."
  331. (interactive)
  332. (let ((marked (bui-list-get-marked-id-list 'delete)))
  333. (or marked
  334. (user-error "No generations marked for deletion"))
  335. (guix-delete-generations (guix-ui-current-profile)
  336. marked (current-buffer))))
  337. ;;; Inserting packages to compare generations
  338. (defcustom guix-generation-packages-buffer-name-function
  339. #'guix-generation-packages-buffer-name-default
  340. "Function used to define name of a buffer with generation packages.
  341. This function is called with 2 arguments: PROFILE (string) and
  342. GENERATION (number)."
  343. :type '(choice (function-item guix-generation-packages-buffer-name-default)
  344. (function-item guix-generation-packages-buffer-name-long)
  345. (function :tag "Other function"))
  346. :group 'guix-generation)
  347. (defcustom guix-generation-packages-update-buffer t
  348. "If non-nil, always update list of packages during comparing generations.
  349. If nil, generation packages are received only once. So when you
  350. compare generation 1 and generation 2, the packages for both
  351. generations will be received. Then if you compare generation 1
  352. and generation 3, only the packages for generation 3 will be
  353. received. Thus if you use comparing of different generations a
  354. lot, you may set this variable to nil to improve the
  355. performance."
  356. :type 'boolean
  357. :group 'guix-generation)
  358. (defvar guix-generation-output-name-width 30
  359. "Width of an output name \"column\".
  360. This variable is used in auxiliary buffers for comparing generations.")
  361. (defun guix-generation-packages (profile)
  362. "Return a list of sorted packages installed in PROFILE.
  363. Each element of the list is a list of the package specification
  364. and its store file name."
  365. (sort (guix-eval-read
  366. (guix-make-guile-expression
  367. 'profile->specifications+file-names profile))
  368. (lambda (a b)
  369. (string< (car a) (car b)))))
  370. (defun guix-generation-packages-buffer-name-default (profile generation)
  371. "Return name of a buffer for displaying GENERATION's package outputs.
  372. Use base name of PROFILE file name."
  373. (let ((profile-name (file-name-base (directory-file-name profile))))
  374. (format "*Guix %s: generation %s*"
  375. profile-name generation)))
  376. (defun guix-generation-packages-buffer-name-long (profile generation)
  377. "Return name of a buffer for displaying GENERATION's package outputs.
  378. Use the full PROFILE file name."
  379. (format "*Guix generation %s (%s)*"
  380. generation profile))
  381. (defun guix-generation-packages-buffer-name (profile generation)
  382. "Return name of a buffer for displaying GENERATION's package outputs."
  383. (funcall guix-generation-packages-buffer-name-function
  384. profile generation))
  385. (defun guix-generation-insert-package (name file-name)
  386. "Insert package output NAME and store FILE-NAME at point."
  387. (insert name)
  388. (indent-to guix-generation-output-name-width 2)
  389. (insert file-name "\n"))
  390. (defun guix-generation-insert-packages (buffer profile)
  391. "Insert package outputs installed in PROFILE in BUFFER."
  392. (with-current-buffer buffer
  393. (setq buffer-read-only nil
  394. indent-tabs-mode nil)
  395. (erase-buffer)
  396. (mapc (-lambda ((name file-name))
  397. (guix-generation-insert-package name file-name))
  398. (guix-generation-packages profile))))
  399. (defun guix-generation-packages-buffer (profile generation)
  400. "Return buffer with package outputs installed in PROFILE's GENERATION.
  401. Create the buffer if needed."
  402. (let ((buf-name (guix-generation-packages-buffer-name
  403. profile generation)))
  404. (or (and (null guix-generation-packages-update-buffer)
  405. (get-buffer buf-name))
  406. (let ((buf (get-buffer-create buf-name)))
  407. (guix-generation-insert-packages
  408. buf
  409. (guix-package-profile profile generation))
  410. buf))))
  411. (defun guix-profile-generation-manifest-file (generation)
  412. "Return the file name of a GENERATION's manifest.
  413. GENERATION is a generation number of the current profile."
  414. (guix-manifest-file (guix-ui-current-profile) generation))
  415. (defun guix-profile-generation-packages-buffer (generation)
  416. "Insert GENERATION's package outputs in a buffer and return it.
  417. GENERATION is a generation number of the current profile."
  418. (guix-generation-packages-buffer (guix-ui-current-profile)
  419. generation))
  420. ;;; Interactive commands
  421. ;;;###autoload
  422. (defun guix-generations (&optional profile)
  423. "Display information about all generations.
  424. If PROFILE is nil, use `guix-current-profile'.
  425. Interactively with prefix, prompt for PROFILE."
  426. (interactive (list (guix-ui-read-generation-profile)))
  427. (guix-generation-get-display profile 'all))
  428. ;;;###autoload
  429. (defun guix-last-generations (number &optional profile)
  430. "Display information about last NUMBER generations.
  431. If PROFILE is nil, use `guix-current-profile'.
  432. Interactively with prefix, prompt for PROFILE."
  433. (interactive
  434. (list (read-number "The number of last generations: ")
  435. (guix-ui-read-generation-profile)))
  436. (guix-generation-get-display profile 'last number))
  437. ;;;###autoload
  438. (defun guix-generations-by-time (from to &optional profile)
  439. "Display information about generations created between FROM and TO.
  440. FROM and TO should be time values.
  441. If PROFILE is nil, use `guix-current-profile'.
  442. Interactively with prefix, prompt for PROFILE."
  443. (interactive
  444. (list (guix-read-date "Find generations (from): ")
  445. (guix-read-date "Find generations (to): ")
  446. (guix-ui-read-generation-profile)))
  447. (guix-generation-get-display profile 'time
  448. (float-time from)
  449. (float-time to)))
  450. (provide 'guix-ui-generation)
  451. ;;; guix-ui-generation.el ends here