Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

### Added
- `ghostel-pre-spawn-hook`, run inside `ghostel--spawn-pty` just
before `make-process` with `process-environment` dynamically
bound to the about-to-be-spawned env. Hook functions can
`setenv` to inject entries the child inherits. Intended for
integrations like with-editor — with a `with-editor-setup-environment`
exposed upstream, users can wire Magit's `EDITOR` plumbing into
ghostel buffers via
`(add-hook 'ghostel-pre-spawn-hook
#'with-editor-setup-environment)`. Fires for both
`ghostel`/`ghostel-project` and `ghostel-exec` spawns;
`ghostel-compile` has its own `make-process` and is not covered.

### Fixed
- Launching `M-x ghostel` from a TRAMP `default-directory` (e.g.
after `find-file /ssh:host:`) now produces a usable remote shell.
Expand Down
55 changes: 36 additions & 19 deletions lisp/ghostel.el
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,18 @@ Errors in hook functions are demoted to messages via
non-nil so the debugger can fire)."
:type 'hook)

(defcustom ghostel-pre-spawn-hook nil
"Hook run inside `ghostel--spawn-pty' just before `make-process'.
Each function is called with no arguments in the buffer that will
host the new process. `process-environment' is dynamically bound
to the env that will be passed to the child, so hook functions can
inject or override entries with `setenv' and the spawned process
inherits them.

Use this hook for one-time pre-spawn setup; see `ghostel-environment'
for static env entries that don't depend on runtime state."
:type 'hook)

(defcustom ghostel-eval-cmds '(("find-file" find-file)
("find-file-other-window" find-file-other-window)
("dired" dired)
Expand Down Expand Up @@ -3534,25 +3546,30 @@ matches the PTY window size, and stores the process in
(tramp-terminal-type (if remote-p
(ghostel--remote-tramp-terminal-type
extra-env)
tramp-terminal-type))
(proc (make-process
:name "ghostel"
:buffer (current-buffer)
:command shell-command
:connection-type 'pty
:file-handler remote-p
:filter #'ghostel--filter
:sentinel #'ghostel--sentinel)))
(setq ghostel--process proc)
;; Raw binary I/O — no encoding/decoding by Emacs
(set-process-coding-system proc 'binary 'binary)
;; Set the PTY's actual window size (ioctl TIOCSWINSZ) so that
;; the program's line editor (readline/ZLE) can render properly.
(set-process-window-size proc height width)
(set-process-query-on-exit-flag proc nil)
(process-put proc 'adjust-window-size-function
#'ghostel--window-adjust-process-window-size)
proc))
tramp-terminal-type)))
;; Pre-spawn hook: runs while `process-environment' is dynamically
;; bound to the about-to-be-spawned env, so hook functions can
;; `setenv' to inject/override entries that the child inherits.
;; See `ghostel-pre-spawn-hook'.
(run-hooks 'ghostel-pre-spawn-hook)
(let ((proc (make-process
:name "ghostel"
:buffer (current-buffer)
:command shell-command
:connection-type 'pty
:file-handler remote-p
:filter #'ghostel--filter
:sentinel #'ghostel--sentinel)))
(setq ghostel--process proc)
;; Raw binary I/O — no encoding/decoding by Emacs
(set-process-coding-system proc 'binary 'binary)
;; Set the PTY's actual window size (ioctl TIOCSWINSZ) so that
;; the program's line editor (readline/ZLE) can render properly.
(set-process-window-size proc height width)
(set-process-query-on-exit-flag proc nil)
(process-put proc 'adjust-window-size-function
#'ghostel--window-adjust-process-window-size)
proc)))

(defun ghostel--start-process ()
"Start the shell process with a PTY.
Expand Down
41 changes: 41 additions & 0 deletions test/ghostel-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -8315,6 +8315,47 @@ buffer eventually shows up."
(should (equal (nth 1 captured) 80))))
(kill-buffer buf))))

(ert-deftest ghostel-test-pre-spawn-hook-injects-into-process-environment ()
"Hook `setenv' calls reach the spawned process via `process-environment'.
`ghostel-pre-spawn-hook' fires with `process-environment' dynamically
bound to the about-to-be-spawned env, so hook functions that call
`setenv' inject entries the child process actually inherits.

Contract relied on by integrations like with-editor: drive a real
`/bin/sh' through `ghostel--start-process', have the hook `setenv' a
sentinel value, and verify the value reached `make-process'. Also
verifies the hook fires in the spawning buffer with `default-directory'
intact (with-editor's `with-editor--setup' reads `default-directory')."
(let ((captured-env nil)
captured-buffer
captured-default-directory
(orig-make-process (symbol-function #'make-process)))
(cl-letf (((symbol-function #'make-process)
(lambda (&rest plist)
(setq captured-env process-environment)
(apply orig-make-process plist))))
(with-temp-buffer
(setq-local ghostel--term-rows 24
ghostel--term-cols 80)
(let* ((process-environment '("PATH=/usr/bin:/bin" "HOME=/tmp"))
(ghostel-shell "/bin/sh")
(ghostel-shell-integration nil)
(default-directory "/tmp/")
(test-buffer (current-buffer))
(ghostel-pre-spawn-hook
(list (lambda ()
(setq captured-buffer (current-buffer))
(setq captured-default-directory default-directory)
(setenv "GHOSTEL_PRE_SPAWN_TEST" "ok"))))
(proc (ghostel--start-process)))
(unwind-protect
(progn
(should (eq captured-buffer test-buffer))
(should (equal captured-default-directory "/tmp/"))
(should (member "GHOSTEL_PRE_SPAWN_TEST=ok" captured-env)))
(when (process-live-p proc)
(delete-process proc))))))))

;; -----------------------------------------------------------------------
;; Test: ghostel-eshell integration
;; -----------------------------------------------------------------------
Expand Down
Loading