guix-utils.el 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589
  1. ;;; guix-utils.el --- General utility functions -*- lexical-binding: t -*-
  2. ;; Copyright © 2014–2018 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 auxiliary general code for Emacs-Guix package.
  18. ;;; Code:
  19. (require 'cl-lib)
  20. (require 'dash)
  21. (require 'bui-utils)
  22. (require 'guix nil t)
  23. (defun guix-guixsd? ()
  24. "Return non-nil, if current system is GuixSD."
  25. (file-exists-p "/run/current-system"))
  26. (defun guix-assert-build-farm ()
  27. "Raise an error if `build-farm' package does not exist."
  28. (unless (require 'build-farm nil t)
  29. (error "This feature requires `build-farm' package, \
  30. which is not installed")))
  31. (defun guix-concat-strings (strings separator &optional location)
  32. "Return new string by concatenating STRINGS with SEPARATOR.
  33. If LOCATION is a symbol `head', add another SEPARATOR to the
  34. beginning of the returned string; if `tail' - add SEPARATOR to
  35. the end of the string; if nil, do not add SEPARATOR; otherwise
  36. add both to the end and to the beginning."
  37. (let ((str (mapconcat #'identity strings separator)))
  38. (cond ((null location)
  39. str)
  40. ((eq location 'head)
  41. (concat separator str))
  42. ((eq location 'tail)
  43. (concat str separator))
  44. (t
  45. (concat separator str separator)))))
  46. (defun guix-list-maybe (object)
  47. "If OBJECT is list, return it; otherwise return (list OBJECT)."
  48. (if (listp object)
  49. object
  50. (list object)))
  51. (defun guix-shell-quote-argument (argument)
  52. "Quote shell command ARGUMENT.
  53. This function is similar to `shell-quote-argument', but less strict."
  54. (if (equal argument "")
  55. "''"
  56. (replace-regexp-in-string
  57. "\n" "'\n'"
  58. (replace-regexp-in-string
  59. (rx (not (any alnum "-=,./\n"))) "\\\\\\&" argument))))
  60. (defun guix-command-symbol (&optional args)
  61. "Return symbol by concatenating 'guix-command' and ARGS (strings)."
  62. (intern (guix-concat-strings (cons "guix-command" args) "-")))
  63. (defun guix-command-string (&optional args)
  64. "Return 'guix ARGS ...' string with quoted shell arguments."
  65. (let ((args (mapcar #'guix-shell-quote-argument args)))
  66. (guix-concat-strings (cons "guix" args) " ")))
  67. (defun guix-copy-command-as-kill (args &optional no-message?)
  68. "Put 'guix ARGS ...' string into `kill-ring'.
  69. See also `guix-copy-as-kill'."
  70. (bui-copy-as-kill (guix-command-string args) no-message?))
  71. (defun guix-compose-buffer-name (base-name postfix)
  72. "Return buffer name by appending BASE-NAME and POSTFIX.
  73. In a simple case the result is:
  74. BASE-NAME: POSTFIX
  75. If BASE-NAME is wrapped by '*', then the result is:
  76. *BASE-NAME: POSTFIX*"
  77. (let ((re (rx string-start
  78. (group (? "*"))
  79. (group (*? any))
  80. (group (? "*"))
  81. string-end)))
  82. (or (string-match re base-name)
  83. (error "Unexpected error in defining buffer name"))
  84. (let ((first* (match-string 1 base-name))
  85. (name-body (match-string 2 base-name))
  86. (last* (match-string 3 base-name)))
  87. ;; Handle the case when buffer name is wrapped by '*'.
  88. (if (and (string= "*" first*)
  89. (string= "*" last*))
  90. (concat "*" name-body ": " postfix "*")
  91. (concat base-name ": " postfix)))))
  92. (defun guix-completing-read (prompt table &optional predicate
  93. require-match initial-input
  94. hist def inherit-input-method)
  95. "Same as `completing-read' but return nil instead of an empty string."
  96. (let ((res (completing-read prompt table predicate
  97. require-match initial-input
  98. hist def inherit-input-method)))
  99. (unless (string= "" res) res)))
  100. (defun guix-completing-read-multiple (prompt table &optional predicate
  101. require-match initial-input
  102. hist def inherit-input-method)
  103. "Same as `completing-read-multiple' but remove duplicates in result."
  104. (cl-remove-duplicates
  105. (completing-read-multiple prompt table predicate
  106. require-match initial-input
  107. hist def inherit-input-method)
  108. :test #'string=))
  109. (declare-function org-read-date "org" t)
  110. (defun guix-read-date (prompt)
  111. "Prompt for a date or time using `org-read-date'.
  112. Return time value."
  113. (require 'org)
  114. (org-read-date nil t nil prompt))
  115. (declare-function pcmpl-unix-user-names "pcmpl-unix")
  116. (defun guix-read-user-name (&optional prompt initial-input)
  117. "Prompt for a user name using completions."
  118. (require 'pcmpl-unix)
  119. (guix-completing-read (or prompt "User name: ")
  120. (pcmpl-unix-user-names)
  121. nil nil initial-input))
  122. (defun guix-switch-to-buffer-or-funcall (buffer-or-name function
  123. &optional message)
  124. "Switch to BUFFER-OR-NAME if it exists.
  125. If BUFFER-OR-NAME does not exist, call FUNCTION without
  126. arguments, also display a message if MESSAGE is specified (it can
  127. be either nil, a string, or another value for a default
  128. message)."
  129. (let ((buffer (get-buffer buffer-or-name)))
  130. (if buffer
  131. (progn
  132. (switch-to-buffer buffer)
  133. (when message
  134. (message (if (stringp message)
  135. message
  136. (substitute-command-keys "\
  137. Press '\\[revert-buffer]' to update this buffer.")))))
  138. (funcall function))))
  139. (defun guix-display-buffer (buffer)
  140. "Switch to BUFFER, preferably reusing a window displaying this buffer."
  141. (pop-to-buffer buffer
  142. '((display-buffer-reuse-window
  143. display-buffer-same-window))))
  144. (cl-defun guix-pretty-print-buffer
  145. (&optional buffer-or-name
  146. &key (modified-flag nil modified-flag-bound?))
  147. "Pretty-print the contents of BUFFER-OR-NAME.
  148. MODIFIED-FLAG defines if the buffer should marked as modified or
  149. unmodified. If this flag is not set, the modification status
  150. of the buffer stays unchanged (as it was before prettifying)."
  151. (let ((modified? (buffer-modified-p))
  152. (inhibit-read-only t))
  153. (with-current-buffer (or buffer-or-name (current-buffer))
  154. (goto-char (point-max))
  155. (let (sexp-beg)
  156. (while (setq sexp-beg (scan-sexps (point) -1))
  157. (goto-char sexp-beg)
  158. (delete-horizontal-space t)
  159. (unless (= (point) (line-beginning-position))
  160. (insert "\n"))
  161. (indent-pp-sexp 'pp)))
  162. (set-buffer-modified-p (if modified-flag-bound?
  163. modified-flag
  164. modified?)))))
  165. (defun guix-pretty-print-file (file-name &optional mode)
  166. "Show FILE-NAME contents in MODE and pretty-print it.
  167. If MODE is nil, use `scheme-mode'.
  168. Put the point in the beginning of buffer.
  169. Return buffer with the prettified contents."
  170. (let* ((base-name (file-name-nondirectory file-name))
  171. (buffer (generate-new-buffer base-name)))
  172. (with-current-buffer buffer
  173. (insert-file-contents file-name)
  174. (goto-char (point-min))
  175. (funcall (or mode 'scheme-mode)))
  176. (guix-pretty-print-buffer buffer)
  177. buffer))
  178. (defun guix-replace-match (regexp string &optional group)
  179. "Replace all occurrences of REGEXP with STRING in the current buffer.
  180. GROUP specifies a parenthesized expression used in REGEXP."
  181. (save-excursion
  182. (goto-char (point-min))
  183. (while (re-search-forward regexp nil t)
  184. (replace-match string nil nil nil group))))
  185. (defmacro guix-while-search (regexp &rest body)
  186. "Evaluate BODY after each search for REGEXP in the current buffer."
  187. (declare (indent 1) (debug t))
  188. `(save-excursion
  189. (goto-char (point-min))
  190. (while (re-search-forward ,regexp nil t)
  191. ,@body)))
  192. (defmacro guix-while-null (&rest body)
  193. "Evaluate BODY until its result becomes non-nil."
  194. (declare (indent 0) (debug t))
  195. (let ((result-var (make-symbol "result")))
  196. `(let (,result-var)
  197. (while (null ,result-var)
  198. (setq ,result-var ,@body))
  199. ,result-var)))
  200. (defun guix-modify (object &rest modifiers)
  201. "Apply MODIFIERS to OBJECT.
  202. OBJECT is passed as an argument to the first function from
  203. MODIFIERS list, the returned result is passed to the second
  204. function from the list and so on. Return result of the last
  205. modifier call."
  206. (if (null modifiers)
  207. object
  208. (apply #'guix-modify
  209. (funcall (car modifiers) object)
  210. (cdr modifiers))))
  211. (defun guix-modify-objects (objects &rest modifiers)
  212. "Apply MODIFIERS to each object from a list of OBJECTS.
  213. See `guix-modify' for details."
  214. (--map (apply #'guix-modify it modifiers)
  215. objects))
  216. (defun guix-make-symbol (&rest symbols)
  217. "Return `guix-SYMBOLS-...' symbol."
  218. (apply #'bui-make-symbol 'guix symbols))
  219. (defmacro guix-define-groups (name &rest args)
  220. "Define `guix-NAME' and `guix-NAME-faces' customization groups.
  221. See `bui-define-groups' for details."
  222. (declare (indent 1))
  223. `(bui-define-groups ,(bui-make-symbol 'guix name)
  224. :parent-group guix
  225. :parent-faces-group guix-faces
  226. ,@args))
  227. ;;; Files and Dired
  228. (defcustom guix-find-file-function #'find-file
  229. "Function used to find a file.
  230. This function is called by `guix-find-file' with a file name as a
  231. single argument."
  232. :type '(choice (function-item find-file)
  233. (function-item org-open-file)
  234. (function :tag "Other function"))
  235. :group 'guix)
  236. (defcustom guix-support-dired t
  237. "Whether guix commands support `dired-mode' or not.
  238. Some commands (like `guix-hash' or `guix-package-from-file') take
  239. a file name as argument. If you are in `dired-mode', you may or
  240. may not wish to use the file at point for these commands. This
  241. variable allows you to control this behavior.
  242. If non-nil, do not prompt for a file name in `dired-mode' and use
  243. the file on the current line instead.
  244. If nil, always prompt for a file name."
  245. :type 'boolean
  246. :group 'guix)
  247. (defcustom guix-file-size-string-function
  248. #'guix-file-size-string-default
  249. "Function used to return a string with file size.
  250. This function is called with a number (file size) as a single
  251. argument."
  252. :type '(choice (function-item guix-file-size-string-default)
  253. (function-item file-size-human-readable)
  254. (function :tag "Other function"))
  255. :group 'guix)
  256. (defun guix-file-size-string-default (size)
  257. "Return file SIZE string in both human readable format and bytes."
  258. (format "%s (%d bytes)"
  259. (file-size-human-readable size)
  260. size))
  261. (defun guix-file-size-string (size)
  262. "Return file SIZE string using `guix-file-size-string-function'."
  263. (funcall guix-file-size-string-function size))
  264. (defun guix-file-name (file-name)
  265. "Expand FILE-NAME and remove trailing slash if needed."
  266. (directory-file-name (expand-file-name file-name)))
  267. (defun guix-read-file-name (&optional prompt dir default-filename
  268. mustmatch initial predicate)
  269. "Read file name.
  270. This function is similar to `read-file-name' except it also
  271. expands the file name."
  272. (expand-file-name
  273. (read-file-name (or prompt "File: ")
  274. dir default-filename
  275. mustmatch initial predicate)))
  276. (declare-function dired-get-filename "dired" t)
  277. (defun guix-read-file-name-maybe (&optional prompt dir default-filename
  278. mustmatch initial predicate)
  279. "Read file name or get it from `dired-mode'.
  280. See `guix-support-dired' for details. See also `guix-read-file-name'."
  281. (if (and guix-support-dired
  282. (derived-mode-p 'dired-mode))
  283. (dired-get-filename)
  284. (guix-read-file-name prompt dir default-filename
  285. mustmatch initial predicate)))
  286. (defun guix-read-os-file-name ()
  287. "Read file name with GuixSD 'operating-system' declaration."
  288. (guix-read-file-name-maybe "System configuration file: "))
  289. (defun guix-find-file (file)
  290. "Find FILE (using `guix-find-file-function') if it exists."
  291. (if (file-exists-p file)
  292. (funcall guix-find-file-function file)
  293. (message "File '%s' does not exist." file)))
  294. (defvar url-handler-regexp)
  295. (defun guix-find-file-or-url (file-or-url)
  296. "Find FILE-OR-URL."
  297. ;; The code is taken from `browse-url-emacs'.
  298. (require 'url-handlers)
  299. (let ((file-name-handler-alist
  300. (cons (cons url-handler-regexp 'url-file-handler)
  301. file-name-handler-alist)))
  302. (find-file file-or-url)))
  303. (defun guix-assert-files-exist (&rest files)
  304. "Raise an error if any of FILES does not exist."
  305. (dolist (file files)
  306. (unless (file-exists-p file)
  307. (user-error "File does not exist: '%s'" file))))
  308. (defun guix-guile-site-directory (&optional root compiled)
  309. "Return default directory with Guile site files.
  310. Return nil, if this directory does not exist.
  311. ROOT is the parent directory where the default one is placed.
  312. Example of ROOT: \"/usr/local\".
  313. By default, the directory with Scheme files is returned, for
  314. example:
  315. ROOT/share/guile/site/2.2
  316. However, if COMPILED is non-nil, the directory with
  317. compiled (.go) files is returned, for example:
  318. ROOT/lib/guile/2.2/site-ccache
  319. "
  320. (let* ((dir (expand-file-name (if compiled
  321. "lib/guile"
  322. "share/guile/site")
  323. (or root "/")))
  324. (dir (and (file-exists-p dir)
  325. ;; digit "[0-9]" is the part of file name (which is
  326. ;; "2.3" or alike). Is there a better way to find
  327. ;; the directory?
  328. (car (directory-files dir t "[0-9]")))))
  329. (when dir
  330. (if compiled
  331. (expand-file-name "site-ccache" dir)
  332. dir))))
  333. ;;; Temporary file names
  334. (defvar guix-temporary-directory nil
  335. "Directory for writing temporary Guix files.
  336. If nil, it will be set when it will be used the first time.
  337. This directory will be deleted on Emacs exit.")
  338. (defun guix-temporary-directory ()
  339. "Return `guix-temporary-directory' (set it if needed)."
  340. (or (and guix-temporary-directory
  341. (file-exists-p guix-temporary-directory)
  342. guix-temporary-directory)
  343. (setq guix-temporary-directory
  344. (make-temp-file "emacs-guix-" 'dir))))
  345. (defun guix-temporary-file-name (name &optional suffix)
  346. "Return file NAME from `guix-temporary-directory'.
  347. If such file name already exists, or if SUFFIX string is
  348. specified, make the returned name unique."
  349. (let* ((file-name (expand-file-name name (guix-temporary-directory)))
  350. (file-name (if suffix
  351. (concat (make-temp-name file-name) suffix)
  352. file-name)))
  353. (if (file-exists-p file-name)
  354. (guix-temporary-file-name name (or suffix ""))
  355. file-name)))
  356. (defun guix-delete-temporary-directory ()
  357. "Delete `guix-temporary-directory' if it exists."
  358. (when (and guix-temporary-directory
  359. (file-exists-p guix-temporary-directory))
  360. (condition-case nil
  361. (delete-directory (guix-temporary-directory) 'recursive)
  362. (error
  363. (message "Failed to delete temporary Guix directory: %s"
  364. guix-temporary-directory)))))
  365. (add-hook 'kill-emacs-hook 'guix-delete-temporary-directory)
  366. ;;; Fontification
  367. (defvar guix-font-lock-flush-function
  368. (if (fboundp 'font-lock-flush)
  369. #'font-lock-flush ; appeared in Emacs 25.1
  370. #'jit-lock-refontify)
  371. "Function used to refontify a buffer.
  372. This function is called without arguments after
  373. enabling/disabling `guix-prettify-mode',
  374. `guix-build-log-minor-mode' and `guix-devel-mode'.
  375. If nil, do not perform refontifying.")
  376. (defun guix-font-lock-flush ()
  377. "Refontify the current buffer using `guix-font-lock-flush-function'."
  378. (when guix-font-lock-flush-function
  379. (if (fboundp guix-font-lock-flush-function)
  380. (funcall guix-font-lock-flush-function)
  381. (message "Unknown function: %S" guix-font-lock-flush-function))))
  382. ;;; Diff
  383. (defvar guix-diff-switches "-u"
  384. "A string or list of strings specifying switches to be passed to diff.")
  385. (defun guix-diff (old new &optional switches no-async)
  386. "Same as `diff', but use `guix-diff-switches' as default."
  387. (diff old new (or switches guix-diff-switches) no-async))
  388. ;;; Completing readers definers
  389. (defmacro guix-define-reader (name read-fun completions prompt
  390. &optional require-match default)
  391. "Define NAME function to read from minibuffer.
  392. READ-FUN may be `completing-read', `completing-read-multiple' or
  393. another function with the same arguments."
  394. (declare (indent 1))
  395. `(defun ,name (&optional prompt initial-contents)
  396. (,read-fun (or prompt ,prompt)
  397. ,completions nil ,require-match
  398. initial-contents nil ,default)))
  399. (defmacro guix-define-readers (&rest args)
  400. "Define reader functions.
  401. ARGS should have a form [KEYWORD VALUE] ... The following
  402. keywords are available:
  403. - `completions-var' - variable used to get completions.
  404. - `completions-getter' - function used to get completions.
  405. - `require-match' - if the match is required (see
  406. `completing-read' for details); default is t.
  407. - `default' - default value.
  408. - `single-reader', `single-prompt' - name of a function to read
  409. a single value, and a prompt for it.
  410. - `multiple-reader', `multiple-prompt' - name of a function to
  411. read multiple values, and a prompt for it.
  412. - `multiple-separator' - if specified, another
  413. `<multiple-reader-name>-string' function returning a string
  414. of multiple values separated the specified separator will be
  415. defined."
  416. (bui-plist-let args
  417. ((completions-var :completions-var)
  418. (completions-getter :completions-getter)
  419. (require-match :require-match t)
  420. (default :default)
  421. (single-reader :single-reader)
  422. (single-prompt :single-prompt)
  423. (multiple-reader :multiple-reader)
  424. (multiple-prompt :multiple-prompt)
  425. (multiple-separator :multiple-separator))
  426. (let ((completions
  427. (cond ((and completions-var completions-getter)
  428. `(or ,completions-var
  429. (setq ,completions-var
  430. (funcall ',completions-getter))))
  431. (completions-var
  432. completions-var)
  433. (completions-getter
  434. `(funcall ',completions-getter)))))
  435. `(progn
  436. ,(when (and completions-var
  437. (not (boundp completions-var)))
  438. `(defvar ,completions-var nil))
  439. ,(when single-reader
  440. `(guix-define-reader ,single-reader
  441. guix-completing-read ,completions ,single-prompt
  442. ,require-match ,default))
  443. ,(when multiple-reader
  444. `(guix-define-reader ,multiple-reader
  445. completing-read-multiple ,completions ,multiple-prompt
  446. ,require-match ,default))
  447. ,(when (and multiple-reader multiple-separator)
  448. (let ((name (intern (concat (symbol-name multiple-reader)
  449. "-string"))))
  450. `(defun ,name (&optional prompt initial-contents)
  451. (guix-concat-strings
  452. (,multiple-reader prompt initial-contents)
  453. ,multiple-separator))))))))
  454. ;;; Memoizing
  455. (defun guix-memoize (function)
  456. "Return a memoized version of FUNCTION."
  457. (let ((cache (make-hash-table :test 'equal)))
  458. (lambda (&rest args)
  459. (let ((result (gethash args cache 'not-found)))
  460. (if (eq result 'not-found)
  461. (let ((result (apply function args)))
  462. (puthash args result cache)
  463. result)
  464. result)))))
  465. (defmacro guix-memoized-defun (name arglist docstring &rest body)
  466. "Define a memoized function NAME.
  467. See `defun' for the meaning of arguments."
  468. (declare (doc-string 3) (indent 2))
  469. `(defalias ',name
  470. (guix-memoize (lambda ,arglist ,@body))
  471. ;; Add '(name args ...)' string with real arglist to the docstring,
  472. ;; because *Help* will display '(name &rest ARGS)' for a defined
  473. ;; function (since `guix-memoize' returns a lambda with '(&rest
  474. ;; args)').
  475. ,(format "(%S %s)\n\n%s"
  476. name
  477. (mapconcat #'symbol-name arglist " ")
  478. docstring)))
  479. (defmacro guix-memoized-defalias (symbol definition &optional docstring)
  480. "Set SYMBOL's function definition to memoized version of DEFINITION."
  481. (declare (doc-string 3) (indent 1))
  482. `(defalias ',symbol
  483. (guix-memoize #',definition)
  484. ,(or docstring
  485. (format "Memoized version of `%S'." definition))))
  486. (provide 'guix-utils)
  487. ;;; guix-utils.el ends here