summaryrefslogtreecommitdiff
path: root/guix-shell.el
blob: df0ea8d4d6f0569d2aca0d7daed58018c065433b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
;;; -*- lexical-binding: t -*-
;;;
;;; Copyright © 2022 David Thompson <davet@gnu.org>
;;;
;;; emacs-guix-shell is free software: you can redistribute it and/or modify
;;; it under the terms of the GNU General Public License as published by the
;;; Free Software Foundation, either version 3 of the License, or (at your
;;; option) any later version.
;;;
;;; emacs-guix-shell is distributed in the hope that it will be useful, but
;;; WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
;;; or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
;;; for more details.
;;;
;;; You should have received a copy of the GNU General Public License
;;; along with this program.  If not, see
;;; <http://www.gnu.org/licenses/>.

(defvar guix-shell--search-paths '()
  "Project environment variable cache.")

(defun guix-shell--current-directory ()
  "Return the project directory for the current buffer."
  (let ((project (project-current)))
    (if project
        (project-root project)
      default-directory)))

(defun guix-shell--search-paths-for-directory (directory)
  "Execute 'guix shell' in DIRECTORY, parse the output, and return
an alist of search path environment variables."
  (let ((buffer (get-buffer-create "*guix-shell-temp*"))
        (names '()))
    (with-current-buffer buffer
      (setq default-directory directory)
      (erase-buffer)
      (message "running guix shell...")
      (call-process "guix" nil t nil "shell" "--search-paths")
      ;; Gather up the names of all the environment variables and transform
      ;; the lines into echo commands that we can eval to get the actual
      ;; search path values.
      (goto-char (point-min))
      (while (not (eobp))
        (let ((start (point)))
          (forward-word)
          (let ((word (buffer-substring-no-properties start (point))))
            (if (string-equal word "export")
                (progn
                  (forward-whitespace 1)
                  (delete-backward-char (- (point) start))
                  (let ((var-start (point)))
                    (search-forward "=")
                    (backward-char 1)
                    (push (buffer-substring-no-properties var-start (point)) names)
                    (delete-backward-char (- (point) var-start)))
                  (delete-char 1)
                  (insert "echo ")
                  (beginning-of-line)
                  (forward-line 1))
              (progn
                (goto-char start)
                (kill-whole-line))))))
      (setq names (reverse names))
      ;; Eval the search paths.
      (call-process-region (point-min) (point-max) "sh" t t nil)
      (message "guix shell complete")
      ;; Iterate over the result lines and create an environment variable
      ;; mapping.
      (goto-char (point-min))
      (let ((env-vars '()))
        (dolist (var names)
          (let ((start (point)))
            (end-of-line 1)
            (let ((search-path (buffer-substring-no-properties start (point))))
              (push (cons var search-path) env-vars))
            (beginning-of-line)
            (forward-line 1)))
        (kill-buffer buffer)
        env-vars))))

(defun guix-shell--update-search-paths-for-directory (directory)
  "Refresh the cached 'guix shell' search paths for DIRECTORY."
  (when directory
    (let ((search-paths (guix-shell--search-paths-for-directory directory)))
      (setq guix-shell--search-paths
            (cons (cons directory (or search-paths 'none))
                  (assoc-delete-all directory guix-shell--search-paths)))
      search-paths)))

(defun guix-shell--cached-search-paths-for-directory (directory)
  "Return an alist of 'guix shell' search paths cached for
DIRECTORY.  If there are no cached search paths, nil is returned.
If the search paths are cached but DIRECTORY has no 'guix shell'
search paths present, 'none' is returned.."
  (cdr (assoc directory guix-shell--search-paths)))

;; From envrc.el
(defun guix-shell--merged-environment (process-env pairs)
  "Make a `process-environment' value that merges PROCESS-ENV with PAIRS.
PAIRS is an alist obtained from direnv's output.  Values from
PROCESS-ENV will be included, but their values will be masked by
Emacs' handling of `process-environment' if they also appear in
PAIRS."
  (append (mapcar (lambda (pair)
                    (format "%s=%s" (car pair) (cdr pair)))
                  pairs)
          process-env))

(defun guix-shell--apply-search-paths (search-paths)
  "Apply SEARCH-PATHS to the environment of the current buffer."
  (setq-local process-environment
              (guix-shell--merged-environment process-environment search-paths))
  (setq-local exec-path (append (parse-colon-path (getenv "PATH"))
                                (list exec-directory)))
  (when (derived-mode-p 'eshell-mode)
    (setq-local eshell-path-env path)))

(defun guix-shell--apply-search-paths-for-directory (directory)
  "Set search path environment variables from 'guix shell' for
DIRECTORY."
  (when directory
    (let ((search-paths
           (or (guix-shell--cached-search-paths-for-directory directory)
               (guix-shell--update-search-paths-for-directory directory))))
      (unless (eq search-paths 'none)
        (guix-shell--apply-search-paths search-paths)))))

(defun guix-shell--apply-search-paths-for-current-directory ()
  "Set search path environment variables from 'guix shell' in the
context of the current directory."
  (guix-shell--apply-search-paths-for-directory (guix-shell--current-directory)))

(defun guix-shell--clear (buffer)
  "Remove any effects of `guix-shell-mode' from BUFFER."
  (with-current-buffer buffer
    (kill-local-variable 'exec-path)
    (kill-local-variable 'process-environment)
    (kill-local-variable 'eshell-path-env)))

(defun guix-shell-update ()
  "Update the search paths set by 'guix shell'."
  (interactive)
  (guix-shell--update-search-paths-for-directory
   (guix-shell--current-directory))
  (guix-shell--clear (current-buffer))
  (guix-shell-apply-search-paths-for-current-directory))

;;;###autoload
(define-minor-mode guix-shell-mode
  "A local minor mode in which environment variables are set by
'guix shell'."
  :init-value nil
  (if guix-shell-mode
      (guix-shell--apply-search-paths-for-current-directory)
    (guix-shell--clear (current-buffer))))

;;;###autoload
(define-globalized-minor-mode guix-shell-global-mode guix-shell-mode
  (lambda ()
    (unless (or (minibufferp) (file-remote-p default-directory))
      (guix-shell-mode 1))))