diff --git a/CHANGELOG.md b/CHANGELOG.md index 44212444..d626ac77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Added +- `ghostel-query-before-killing` defcustom controls whether Emacs + asks for confirmation before killing a live ghostel buffer or + exiting Emacs while one is running. Defaults to `auto`: quiet + at the shell prompt, queries while a command is running (via + OSC 133 C/D markers). Set to `t` for always-on confirmation, + `nil` to restore the previous never-query behavior. + Closes [#288](https://github.com/dakra/ghostel/issues/288). + ## [0.26.0] — 2026-05-13 ### Added diff --git a/lisp/ghostel.el b/lisp/ghostel.el index 747b8585..2d3ac4a9 100644 --- a/lisp/ghostel.el +++ b/lisp/ghostel.el @@ -364,13 +364,30 @@ The default, `ghostel--set-title-default', renames the buffer to "Kill the buffer when the shell process exits." :type 'boolean) +(defcustom ghostel-query-before-killing 'auto + "Whether to confirm before killing a live ghostel buffer or exiting Emacs. + +The value controls the process query-on-exit flag (see +`set-process-query-on-exit-flag'), which Emacs honours both when +killing the buffer and when exiting Emacs while the buffer is live. + +t Always query while the shell process is alive. +nil Never query. +auto Query only while a shell command is running. Requires OSC 133 shell + integration: at a prompt the flag is nil, and it flips to t between + the OSC 133 C (command start) and D (command finish) markers." + :type '(choice (const :tag "Always" t) + (const :tag "Never" nil) + (const :tag "While a command is running" auto))) + (defcustom ghostel-exit-functions nil "Hook run when the terminal process exits. Each function is called with two arguments: the buffer and the exit event string." :type 'hook) -(defcustom ghostel-command-finish-functions nil +(defcustom ghostel-command-finish-functions + '(ghostel--query-before-killing-on-cmd-finish) "Hook run when a shell command finishes (OSC 133 D marker). Each function is called with two arguments: the buffer and the exit status (an integer, or nil if the shell did not report one). @@ -388,7 +405,8 @@ non-nil, in which case the error is re-signalled so the debugger can fire (standard `with-demoted-errors' semantics)." :type 'hook) -(defcustom ghostel-command-start-functions nil +(defcustom ghostel-command-start-functions + '(ghostel--query-before-killing-on-cmd-start) "Hook run when a shell command starts running (OSC 133 C marker). Each function is called with one argument: the buffer. @@ -4510,6 +4528,24 @@ is non-nil so the debugger fires for hook authors who want it." (apply fn args)) nil))) +(defun ghostel--query-before-killing-on-cmd-start (buf) + "Flip the process query-on-exit flag on for BUF while a command runs. +Active only when `ghostel-query-before-killing' is `auto'. +Hung off `ghostel-command-start-functions'." + (when (eq ghostel-query-before-killing 'auto) + (let ((proc (buffer-local-value 'ghostel--process buf))) + (when (process-live-p proc) + (set-process-query-on-exit-flag proc t))))) + +(defun ghostel--query-before-killing-on-cmd-finish (buf _exit) + "Clear the process query-on-exit flag on BUF when the command finishes. +Active only when `ghostel-query-before-killing' is `auto'. +Hung off `ghostel-command-finish-functions'." + (when (eq ghostel-query-before-killing 'auto) + (let ((proc (buffer-local-value 'ghostel--process buf))) + (when (process-live-p proc) + (set-process-query-on-exit-flag proc nil))))) + (defun ghostel--prompt-input-start () "From the start of a `ghostel-prompt' region, move past the prefix. If `ghostel-input' begins on the same line, point lands at its @@ -5969,7 +6005,12 @@ matches the PTY window size, and stores the process in ;; 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) + ;; For `auto', start nil — we spawn at a fresh prompt. The + ;; OSC 133 C/D handlers flip the flag while a command runs. + (set-process-query-on-exit-flag + proc (if (eq ghostel-query-before-killing 'auto) + nil + ghostel-query-before-killing)) (process-put proc 'adjust-window-size-function #'ghostel--window-adjust-process-window-size) proc))) diff --git a/test/ghostel-test.el b/test/ghostel-test.el index 908c8481..e400cb5e 100644 --- a/test/ghostel-test.el +++ b/test/ghostel-test.el @@ -4660,6 +4660,79 @@ bind `debug-on-error' to nil." (ghostel--osc133-marker "D" "0") (should later-ran))))) ; second hook still fired +;; ----------------------------------------------------------------------- +;; Test: ghostel-query-before-killing +;; ----------------------------------------------------------------------- + +(defmacro ghostel-test--with-cat-process (var &rest body) + "Spawn a long-lived `cat' process bound to VAR, run BODY, then clean up. +The process is killed and the temp buffer destroyed on exit so the +flag-flip tests don't leak processes between runs." + (declare (indent 1)) + `(let* ((buf (generate-new-buffer " *ghostel-test-query-cat*")) + (,var (make-process :name "ghostel-test-cat" + :buffer buf + :command '("cat") + :connection-type 'pipe + :noquery nil))) + (unwind-protect (progn ,@body) + (when (process-live-p ,var) + (delete-process ,var)) + (kill-buffer buf)))) + +(ert-deftest ghostel-test-query-before-killing-auto-toggles () + "`auto' flips the query-on-exit flag around OSC 133 C/D markers." + (ghostel-test--with-cat-process proc + (with-current-buffer (process-buffer proc) + (setq ghostel--process proc) + (let ((ghostel-query-before-killing 'auto)) + (set-process-query-on-exit-flag proc nil) ; baseline + (ghostel--query-before-killing-on-cmd-start (current-buffer)) + (should (process-query-on-exit-flag proc)) ; command running + (ghostel--query-before-killing-on-cmd-finish (current-buffer) 0) + (should-not (process-query-on-exit-flag proc)))))) ; back at prompt + +(ert-deftest ghostel-test-query-before-killing-nil-is-noop () + "When set to nil, the OSC 133 handlers must not touch the flag." + (ghostel-test--with-cat-process proc + (with-current-buffer (process-buffer proc) + (setq ghostel--process proc) + (let ((ghostel-query-before-killing nil)) + (set-process-query-on-exit-flag proc nil) + (ghostel--query-before-killing-on-cmd-start (current-buffer)) + (should-not (process-query-on-exit-flag proc)) ; unchanged + (ghostel--query-before-killing-on-cmd-finish (current-buffer) 0) + (should-not (process-query-on-exit-flag proc)))))) + +(ert-deftest ghostel-test-query-before-killing-t-is-noop () + "When set to t, the OSC 133 handlers must not touch the flag. +The flag is already t from spawn time, and `auto'-only toggling +would defeat the user's request to always be asked." + (ghostel-test--with-cat-process proc + (with-current-buffer (process-buffer proc) + (setq ghostel--process proc) + (let ((ghostel-query-before-killing t)) + (set-process-query-on-exit-flag proc t) + (ghostel--query-before-killing-on-cmd-start (current-buffer)) + (should (process-query-on-exit-flag proc)) ; still t + (ghostel--query-before-killing-on-cmd-finish (current-buffer) 0) + (should (process-query-on-exit-flag proc)))))) ; still t after D + +(ert-deftest ghostel-test-query-before-killing-handles-dead-process () + "Handlers must not raise if the process has already exited." + (ghostel-test--with-cat-process proc + (with-current-buffer (process-buffer proc) + (setq ghostel--process proc) + (delete-process proc) + (let ((ghostel-query-before-killing 'auto)) + (should-not (condition-case _ + (progn (ghostel--query-before-killing-on-cmd-start + (current-buffer)) + (ghostel--query-before-killing-on-cmd-finish + (current-buffer) 0) + nil) + (error t))))))) + ;; ----------------------------------------------------------------------- ;; Test: ghostel-compile--finalize ;; ----------------------------------------------------------------------- @@ -15553,6 +15626,10 @@ slip past the unit tests." ghostel-test-command-finish-hook-error-isolated ghostel-test-command-finish-hook-runs-synchronously ghostel-test-command-start-hook-runs-synchronously + ghostel-test-query-before-killing-auto-toggles + ghostel-test-query-before-killing-nil-is-noop + ghostel-test-query-before-killing-t-is-noop + ghostel-test-query-before-killing-handles-dead-process ghostel-test-compile-finalize-scans-errors ghostel-test-compile-finalize-appends-footer ghostel-test-compile-finalize-footer-on-failure