From d0622e205793d5e1556d43a3a19f537bc357f1a4 Mon Sep 17 00:00:00 2001 From: Patryk Wychowaniec Date: Mon, 10 Feb 2025 13:29:49 +0100 Subject: [PATCH 1/4] nix-command-eshell: Hello, World! --- nix-command-eshell.el | 133 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 nix-command-eshell.el diff --git a/nix-command-eshell.el b/nix-command-eshell.el new file mode 100644 index 0000000..890e41d --- /dev/null +++ b/nix-command-eshell.el @@ -0,0 +1,133 @@ +;;; nix-command-eshell.el --- eshell support for nix-command -*- lexical-binding: t -*- + +;; Author: Patryk Wychowaniec +;; Homepage: https://github.com/NixOS/nix-mode +;; Keywords: nix, tools, eshell + +;; This file is NOT part of GNU Emacs. + +;;; Commentary: + +;; Provides an eshell-aware `nix' command[1]. +;; +;; Using this module causes `nix develop' and `nix shell' to extend the active +;; eshell session instead of spawning a separate `bash' process. +;; +;; This module also advices `eshell/exit' so that it exits the Nix environment +;; first and only the second call to `exit' actually closes the `eshell' buffer, +;; simulating a nested shell. Of course, if you didn't invoke `nix develop' +;; before, just one `exit' remains sufficient. +;; +;; Finally, it is recommended that you extend your eshell prompt to check +;; `nix-command-eshell-active-p' so that you know whether you're inside a Nix +;; environment or not, like: +;; +;; (defun +eshell/prompt () +;; (concat +;; (eshell/pwd) +;; (if (nix-command-eshell-active-p) " | nix" "") +;; " ")) +;; +;; NOTE: If any of the functionalities provided here malfunctions, remember that +;; you can always use `*' to avoid going through `eshell/nix' - that is, +;; `*nix develop' will revert back to the original behavior and start a +;; regular `bash' shell. +;; +;; [1] https://nixos.wiki/wiki/Nix_command + +;;; Code: + +(require 'eshell) +(require 'esh-mode) + +(push "nix" eshell-complex-commands) + +;;;###autoload +(defun eshell/nix (&rest args) + "eshell-aware wrapper for the `nix' command." + (let ((cmd (car args))) + (if (and (or (string= "develop" cmd) (string= "shell" cmd)) + (not (member "-c" args))) + (nix-command-eshell args) + (throw 'eshell-replace-command + (eshell-parse-command + (concat (char-to-string eshell-explicit-command-char) "nix") args))))) + +(defun nix-command-eshell-active-p () + "Return t if we're inside a `nix develop' or a `nix shell' subshell - useful +for customizing the prompt." + (boundp 'nix-command-eshell--prev-env)) + +(defun nix-command-eshell (args) + (make-local-variable 'process-environment) + (nix-command-eshell--leave) + (nix-command-eshell--spawn args)) + +(defun nix-command-eshell--spawn (args) + (throw 'eshell-external + (let* ((temp-file + (make-temp-file "nix")) + (command + (append '("nix") args (list "--command" "sh" "-c" (format "export > %s" temp-file)))) + (proc + (make-process + :name "nix" + :buffer (current-buffer) + :command command + :filter 'eshell-interactive-process-filter + :sentinel 'nix-command-eshell--sentinel))) + (process-put proc 'temp-file temp-file) + (eshell-record-process-object proc) + (eshell-record-process-properties proc) + proc))) + +(defun nix-command-eshell--sentinel (proc status) + (let ((cmd (car (eshell-commands-for-process proc))) + (buffer (process-buffer proc)) + (temp-file (process-get proc 'temp-file))) + (when (and cmd (buffer-live-p buffer) (eq 0 (process-exit-status proc))) + (with-current-buffer buffer + (nix-command-eshell--enter (nix-command-eshell--parse temp-file)))) + (when (not (process-live-p proc)) + (delete-file temp-file))) + (eshell-sentinel proc status)) + +(defun nix-command-eshell--parse (path) + (let ((env '()) + (env-regex + (rx "export " + (group (one-or-more (or alpha ?_))) + "=\"" + (group (zero-or-more (not "\"")))))) + (with-temp-buffer + (insert-file-contents path) + (while (search-forward-regexp env-regex nil t 1) + (let ((env-name (match-string 1)) + (env-value (match-string 2))) + (setq env (setenv-internal env env-name env-value nil))))) + env)) + +(defun nix-command-eshell--enter (env) + (setq-local nix-command-eshell--prev-env process-environment + process-environment env) + (nix-command-eshell--refresh)) + +(defun nix-command-eshell--leave () + (when (boundp 'nix-command-eshell--prev-env) + (setq-local process-environment nix-command-eshell--prev-env) + (makunbound 'nix-command-eshell--prev-env) + (nix-command-eshell--refresh))) + +(defun nix-command-eshell--leave-a (fn &rest args) + (if (boundp 'nix-command-eshell--prev-env) + (progn (nix-command-eshell--leave) ()) + (apply fn args))) + +(advice-add 'eshell/exit :around 'nix-command-eshell--leave-a) + +;; Most envvars get overwritten by doing `(setq-local process-environment ...)', +;; but some envvars are virtual and require special treatment to be refreshed. +(defun nix-command-eshell--refresh () + (eshell-set-path (getenv "PATH"))) + +(provide 'nix-command-eshell) From 02419421e0fb27129f07583eac526c9c5b33ecaf Mon Sep 17 00:00:00 2001 From: Patryk Wychowaniec Date: Sun, 16 Feb 2025 18:46:10 +0100 Subject: [PATCH 2/4] nix-command-eshell: v0.2 --- nix-command-eshell.el | 195 +++++++++++++++++++++++++++++------------- 1 file changed, 135 insertions(+), 60 deletions(-) diff --git a/nix-command-eshell.el b/nix-command-eshell.el index 890e41d..683032e 100644 --- a/nix-command-eshell.el +++ b/nix-command-eshell.el @@ -10,17 +10,26 @@ ;; Provides an eshell-aware `nix' command[1]. ;; -;; Using this module causes `nix develop' and `nix shell' to extend the active -;; eshell session instead of spawning a separate `bash' process. +;; Using this module causes `nix develop' and `nix shell' to extend the current +;; eshell session instead of spawning an external shell; other Nix commands get +;; passed-through. ;; -;; This module also advices `eshell/exit' so that it exits the Nix environment -;; first and only the second call to `exit' actually closes the `eshell' buffer, -;; simulating a nested shell. Of course, if you didn't invoke `nix develop' -;; before, just one `exit' remains sufficient. +;; This module also advices `eshell/exit' so that if a Nix shell is currently +;; active, it gets deactivated first and only the next call to `exit' actually +;; closes eshell. Note that exiting restores the previous working directory as +;; well - you can customize that with `nix-command-eshell-exit-behavior'. ;; -;; Finally, it is recommended that you extend your eshell prompt to check -;; `nix-command-eshell-active-p' so that you know whether you're inside a Nix -;; environment or not, like: +;; Nested shells are supported, so for instance: +;; +;; $ nix shell 'nixpkgs#netris' +;; $ nix shell 'nixpkgs#gcc' +;; +;; ... will bring both `netris' and `gcc' into scope - you'll have to invoke +;; `exit' twice to go back to your original shell then. +;; +;; When enabling this module, it is recommended that you extend your prompt to +;; check `nix-command-eshell-active-p' so that you know whether you're inside a +;; Nix shell or not, like: ;; ;; (defun +eshell/prompt () ;; (concat @@ -28,10 +37,11 @@ ;; (if (nix-command-eshell-active-p) " | nix" "") ;; " ")) ;; -;; NOTE: If any of the functionalities provided here malfunctions, remember that -;; you can always use `*' to avoid going through `eshell/nix' - that is, -;; `*nix develop' will revert back to the original behavior and start a -;; regular `bash' shell. +;; This module is compatible with `eat' and `envrc'. +;; +;; Finally, if any of the functionalities provided here malfunctions, remember +;; that you can always use `*' to avoid going through `eshell/nix' - that is, +;; `*nix develop' will start an external shell. ;; ;; [1] https://nixos.wiki/wiki/Nix_command @@ -40,6 +50,27 @@ (require 'eshell) (require 'esh-mode) +;; --- + +(defgroup nix-command-eshell nil + "Nix-Eshell integration." + :group 'nix) + +(defcustom nix-command-eshell-exit-behavior 'restore-pwd + "Behavior of the `exit' command when inside a Nix shell. + +This option determines whether `exit' should keep the current directory or +rather whether it should restore the directory where `nix' was called. The later +simulates an actual subshell better, but can be also annoying for some people." + :type '(choice + (const :tag "Keep current directory on exit" keep-pwd) + (const :tag "Restore previous directory on exit" restore-pwd)) + :group 'nix-command-eshell) + +;; --- + +(defvar nix-command-eshell--shells '()) + (push "nix" eshell-complex-commands) ;;;###autoload @@ -48,86 +79,130 @@ (let ((cmd (car args))) (if (and (or (string= "develop" cmd) (string= "shell" cmd)) (not (member "-c" args))) - (nix-command-eshell args) + (nix-command-eshell--spawn args) (throw 'eshell-replace-command (eshell-parse-command (concat (char-to-string eshell-explicit-command-char) "nix") args))))) -(defun nix-command-eshell-active-p () - "Return t if we're inside a `nix develop' or a `nix shell' subshell - useful -for customizing the prompt." - (boundp 'nix-command-eshell--prev-env)) +;;;###autoload +(defun eshell/nix-leave () + "Leave Nix subshell without returning to the previous directory." + (when (nix-command-eshell-active-p) + (let ((curr-pwd default-directory)) + (nix-command-eshell--leave) + (setq default-directory curr-pwd) + ()))) -(defun nix-command-eshell (args) - (make-local-variable 'process-environment) - (nix-command-eshell--leave) - (nix-command-eshell--spawn args)) +(defun nix-command-eshell-active-p () + "Return non-nil when we're inside a Nix shell." + (not (eq nix-command-eshell--shells '()))) (defun nix-command-eshell--spawn (args) - (throw 'eshell-external - (let* ((temp-file - (make-temp-file "nix")) - (command - (append '("nix") args (list "--command" "sh" "-c" (format "export > %s" temp-file)))) - (proc - (make-process - :name "nix" - :buffer (current-buffer) - :command command - :filter 'eshell-interactive-process-filter - :sentinel 'nix-command-eshell--sentinel))) - (process-put proc 'temp-file temp-file) - (eshell-record-process-object proc) - (eshell-record-process-properties proc) - proc))) + (let* ((temp-file + (make-temp-file "nix")) + (command + (append '("nix") args (list "--command" "sh" "-c" (format "export > %s" temp-file)))) + (proc + (make-process + :name "nix" + :buffer (current-buffer) + :command command + :filter 'eshell-interactive-process-filter + :sentinel 'nix-command-eshell--sentinel))) + (process-put proc 'temp-file temp-file) + (eshell-record-process-object proc) + (eshell-record-process-properties proc) + (throw 'eshell-external proc))) (defun nix-command-eshell--sentinel (proc status) (let ((cmd (car (eshell-commands-for-process proc))) (buffer (process-buffer proc)) (temp-file (process-get proc 'temp-file))) - (when (and cmd (buffer-live-p buffer) (eq 0 (process-exit-status proc))) + (when (and cmd + (buffer-live-p buffer) + (eq 0 (process-exit-status proc))) (with-current-buffer buffer - (nix-command-eshell--enter (nix-command-eshell--parse temp-file)))) + (nix-command-eshell--enter + (nix-command-eshell--parse temp-file)))) (when (not (process-live-p proc)) (delete-file temp-file))) (eshell-sentinel proc status)) (defun nix-command-eshell--parse (path) (let ((env '()) - (env-regex + (regex (rx "export " (group (one-or-more (or alpha ?_))) - "=\"" - (group (zero-or-more (not "\"")))))) + (opt + "=\"" + (group (zero-or-more (not "\""))))))) (with-temp-buffer (insert-file-contents path) - (while (search-forward-regexp env-regex nil t 1) - (let ((env-name (match-string 1)) - (env-value (match-string 2))) - (setq env (setenv-internal env env-name env-value nil))))) + (while (search-forward-regexp regex nil t 1) + (let ((key (match-string 1)) + (val (match-string 2))) + (if val + (push (format "%s=%s" key val) env) + (push key env))))) env)) (defun nix-command-eshell--enter (env) - (setq-local nix-command-eshell--prev-env process-environment - process-environment env) + (make-local-variable 'nix-command-eshell--shells) + (push (list :dir default-directory + :env env) + nix-command-eshell--shells) (nix-command-eshell--refresh)) (defun nix-command-eshell--leave () - (when (boundp 'nix-command-eshell--prev-env) - (setq-local process-environment nix-command-eshell--prev-env) - (makunbound 'nix-command-eshell--prev-env) - (nix-command-eshell--refresh))) + (when-let* ((shell (pop nix-command-eshell--shells))) + (when (eq 'restore-pwd nix-command-eshell-exit-behavior) + (let ((dir (plist-get shell :dir))) + (when (file-directory-p dir) + (setq-local default-directory dir)))) + (nix-command-eshell--refresh)) + (when (not (nix-command-eshell-active-p)) + (kill-local-variable 'exec-path) + (kill-local-variable 'process-environment) + (when (fboundp 'envrc--update) + (envrc--update)))) + +(defun nix-command-eshell--refresh () + (nix-command-eshell--refresh-env) + (nix-command-eshell--refresh-path)) + +(defun nix-command-eshell--refresh-env () + (setq-local process-environment '()) + (dolist (shell nix-command-eshell--shells) + (setq-local process-environment + (append process-environment (plist-get shell :env)))) + (dolist (val (or (default-value 'process-environment) '())) + (push process-environment val))) + +(defun nix-command-eshell--refresh-path () + (setq-local exec-path + (parse-colon-path + (getenv-internal "PATH" process-environment))) + (dolist (val (or (default-value 'exec-path) '())) + (push exec-path val)) + (eshell-set-path (getenv "PATH"))) + +;; --- -(defun nix-command-eshell--leave-a (fn &rest args) - (if (boundp 'nix-command-eshell--prev-env) +(defun nix-command-eshell--exit-a (fn &rest args) + (if (nix-command-eshell-active-p) (progn (nix-command-eshell--leave) ()) (apply fn args))) -(advice-add 'eshell/exit :around 'nix-command-eshell--leave-a) +(advice-add 'eshell/exit :around 'nix-command-eshell--exit-a) -;; Most envvars get overwritten by doing `(setq-local process-environment ...)', -;; but some envvars are virtual and require special treatment to be refreshed. -(defun nix-command-eshell--refresh () - (eshell-set-path (getenv "PATH"))) +;; --- + +(defun nix-command-eshell--envrc-clear-a (&rest _) + (when (nix-command-eshell-active-p) + (nix-command-eshell--refresh))) + +(advice-add 'envrc--clear :after 'nix-command-eshell--envrc-clear-a) + +;; --- (provide 'nix-command-eshell) From e370e7d58748aad57aea10d8d61ee39478211aed Mon Sep 17 00:00:00 2001 From: Patryk Wychowaniec Date: Sun, 16 Feb 2025 18:49:30 +0100 Subject: [PATCH 3/4] nix-command-eshell: s/module/package --- nix-command-eshell.el | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix-command-eshell.el b/nix-command-eshell.el index 683032e..542bf61 100644 --- a/nix-command-eshell.el +++ b/nix-command-eshell.el @@ -10,11 +10,11 @@ ;; Provides an eshell-aware `nix' command[1]. ;; -;; Using this module causes `nix develop' and `nix shell' to extend the current +;; Using this package causes `nix develop' and `nix shell' to extend the current ;; eshell session instead of spawning an external shell; other Nix commands get ;; passed-through. ;; -;; This module also advices `eshell/exit' so that if a Nix shell is currently +;; This package also advices `eshell/exit' so that if a Nix shell is currently ;; active, it gets deactivated first and only the next call to `exit' actually ;; closes eshell. Note that exiting restores the previous working directory as ;; well - you can customize that with `nix-command-eshell-exit-behavior'. @@ -27,7 +27,7 @@ ;; ... will bring both `netris' and `gcc' into scope - you'll have to invoke ;; `exit' twice to go back to your original shell then. ;; -;; When enabling this module, it is recommended that you extend your prompt to +;; When enabling this package, it is recommended that you extend your prompt to ;; check `nix-command-eshell-active-p' so that you know whether you're inside a ;; Nix shell or not, like: ;; @@ -37,7 +37,7 @@ ;; (if (nix-command-eshell-active-p) " | nix" "") ;; " ")) ;; -;; This module is compatible with `eat' and `envrc'. +;; This package is compatible with `eat' and `envrc'. ;; ;; Finally, if any of the functionalities provided here malfunctions, remember ;; that you can always use `*' to avoid going through `eshell/nix' - that is, From f1d588051d4fdd54c88eb7fb4a92b8352fb24f83 Mon Sep 17 00:00:00 2001 From: Patryk Wychowaniec Date: Wed, 19 Feb 2025 08:22:35 +0100 Subject: [PATCH 4/4] nix-command-eshell: Support Emacs 29 --- nix-command-eshell.el | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/nix-command-eshell.el b/nix-command-eshell.el index 542bf61..6cb7098 100644 --- a/nix-command-eshell.el +++ b/nix-command-eshell.el @@ -107,15 +107,27 @@ simulates an actual subshell better, but can be also annoying for some people." :name "nix" :buffer (current-buffer) :command command - :filter 'eshell-interactive-process-filter + :filter 'nix-command-eshell--filter :sentinel 'nix-command-eshell--sentinel))) (process-put proc 'temp-file temp-file) (eshell-record-process-object proc) (eshell-record-process-properties proc) (throw 'eshell-external proc))) +(defun nix-command-eshell--filter (proc output) + (if (fboundp 'eshell-interactive-process-filter) + ;; For Emacs >= 30 + (eshell-interactive-process-filter proc output)) + ;; For Emacs < 30 + (eshell-insertion-filter proc output)) + (defun nix-command-eshell--sentinel (proc status) - (let ((cmd (car (eshell-commands-for-process proc))) + (let ((cmd + (if (fboundp 'eshell-commands-for-process) + ;; For Emacs >= 30 + (car (eshell-commands-for-process proc)) + ;; For Emacs < 30 + t)) (buffer (process-buffer proc)) (temp-file (process-get proc 'temp-file))) (when (and cmd