guix-utils.el 21 KB

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